"Fossies" - the Fresh Open Source Software Archive

Member "snapcraft-3.8/snapcraft/plugins/colcon.py" (9 Sep 2019, 27301 Bytes) of package /linux/misc/snapcraft-3.8.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "colcon.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3.7.2_vs_3.8.

    1 # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
    2 #
    3 # Copyright (C) 2019 Canonical Ltd
    4 #
    5 # This program is free software: you can redistribute it and/or modify
    6 # it under the terms of the GNU General Public License version 3 as
    7 # published by the Free Software Foundation.
    8 #
    9 # This program is distributed in the hope that it will be useful,
   10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   12 # GNU General Public License for more details.
   13 #
   14 # You should have received a copy of the GNU General Public License
   15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
   16 
   17 """The colcon plugin is useful for building ROS2 parts.
   18 
   19 This plugin uses the common plugin keywords as well as those for "sources".
   20 For more information check the 'plugins' topic for the former and the
   21 'sources' topic for the latter.
   22 
   23 Additionally, this plugin uses the following plugin-specific keywords:
   24 
   25     - colcon-packages:
   26       (list of strings)
   27       List of colcon packages to build. If not specified, all packages in the
   28       workspace will be built. If set to an empty list ([]), no packages will
   29       be built, which could be useful if you only want ROS debs in the snap.
   30     - colcon-packages-ignore:
   31       (list of strings)
   32       List of colcon packages to ignore. If not specified or set to an empty
   33       list ([]), no packages will be ignored.
   34     - colcon-source-space:
   35       (string)
   36       The source space containing colcon packages (defaults to 'src').
   37     - colcon-rosdistro:
   38       (string)
   39       The ROS distro to use. Available options are bouncy and crystal (defaults to
   40       crystal), both of which are only compatible with core18 as the base.
   41     - colcon-cmake-args:
   42       (list of strings)
   43       Arguments to pass to cmake projects. Note that any arguments here which match
   44       colcon arguments need to be prefixed with a space. This can be done by quoting
   45       each argument with a leading space.
   46     - colcon-catkin-cmake-args:
   47       (list of strings)
   48       Arguments to pass to catkin packages. Note that any arguments here which match
   49       colcon arguments need to be prefixed with a space. This can be done by quoting
   50       each argument with a leading space.
   51     - colcon-ament-cmake-args:
   52       (list of strings)
   53       Arguments to pass to ament_cmake packages. Note that any arguments here which
   54       match colcon arguments need to be prefixed with a space. This can be done by
   55       quoting each argument with a leading space.
   56 """
   57 
   58 import contextlib
   59 import collections
   60 import os
   61 import logging
   62 import re
   63 import shutil
   64 import textwrap
   65 
   66 import snapcraft
   67 from snapcraft.plugins import _ros
   68 from snapcraft.plugins import _python
   69 from snapcraft import file_utils, repo
   70 from snapcraft.internal import errors, mangling
   71 
   72 logger = logging.getLogger(__name__)
   73 
   74 # Map bases to ROS releases
   75 _ROSDISTRO_TO_BASE_MAP = {"bouncy": "core18", "crystal": "core18", "dashing": "core18"}
   76 
   77 # Map bases to Ubuntu releases. Every base in _ROSDISTRO_TO_BASE_MAP needs to be
   78 # specified here.
   79 _BASE_TO_UBUNTU_RELEASE_MAP = {"core18": "bionic"}
   80 
   81 _SUPPORTED_DEPENDENCY_TYPES = {"apt", "pip"}
   82 
   83 _ROS_KEYRING_PATH = os.path.join(snapcraft.internal.common.get_keyringsdir(), "ros.gpg")
   84 
   85 
   86 class ColconInvalidSystemDependencyError(errors.SnapcraftError):
   87     fmt = (
   88         "Package {dependency!r} isn't a valid system dependency. Did you "
   89         "forget to add it to colcon-packages? If not, add the Ubuntu package "
   90         "containing it to stage-packages until you can get it into the rosdep "
   91         "database."
   92     )
   93 
   94     def __init__(self, dependency):
   95         super().__init__(dependency=dependency)
   96 
   97 
   98 class ColconUnsupportedDependencyTypeError(errors.SnapcraftError):
   99     fmt = (
  100         "Package {dependency!r} resolved to an unsupported type of "
  101         "dependency: {dependency_type!r}."
  102     )
  103 
  104     def __init__(self, dependency_type, dependency):
  105         super().__init__(dependency_type=dependency_type, dependency=dependency)
  106 
  107 
  108 class ColconWorkspaceIsRootError(errors.SnapcraftError):
  109     fmt = (
  110         "colcon-source-space cannot be the root of the colcon workspace; use a "
  111         "subdirectory."
  112     )
  113 
  114 
  115 class ColconAptDependencyFetchError(errors.SnapcraftError):
  116     fmt = "Failed to fetch apt dependencies: {message}"
  117 
  118     def __init__(self, message):
  119         super().__init__(message=message)
  120 
  121 
  122 class ColconPackagePathNotFoundError(errors.SnapcraftError):
  123     fmt = "Failed to find package path: {path!r}"
  124 
  125     def __init__(self, path):
  126         super().__init__(path=path)
  127 
  128 
  129 class ColconPluginBaseError(errors.PluginBaseError):
  130     fmt = (
  131         "The colcon plugin (used by part {part_name!r}) does not support using base "
  132         "{base!r} with rosdistro {rosdistro!r}."
  133     )
  134 
  135     def __init__(self, part_name, base, rosdistro):
  136         super(errors.PluginBaseError, self).__init__(
  137             part_name=part_name, base=base, rosdistro=rosdistro
  138         )
  139 
  140 
  141 class ColconPlugin(snapcraft.BasePlugin):
  142     @classmethod
  143     def schema(cls):
  144         schema = super().schema()
  145 
  146         schema["properties"]["colcon-rosdistro"] = {
  147             "type": "string",
  148             "default": "crystal",
  149             "enum": list(_ROSDISTRO_TO_BASE_MAP.keys()),
  150         }
  151 
  152         schema["properties"]["colcon-packages"] = {
  153             "type": "array",
  154             "minitems": 1,
  155             "uniqueItems": True,
  156             "items": {"type": "string"},
  157         }
  158 
  159         schema["properties"]["colcon-source-space"] = {
  160             "type": "string",
  161             "default": "src",
  162         }
  163 
  164         schema["properties"]["colcon-cmake-args"] = {
  165             "type": "array",
  166             "minitems": 1,
  167             "items": {"type": "string"},
  168             "default": [],
  169         }
  170 
  171         schema["properties"]["colcon-catkin-cmake-args"] = {
  172             "type": "array",
  173             "minitems": 1,
  174             "items": {"type": "string"},
  175             "default": [],
  176         }
  177 
  178         schema["properties"]["colcon-ament-cmake-args"] = {
  179             "type": "array",
  180             "minitems": 1,
  181             "items": {"type": "string"},
  182             "default": [],
  183         }
  184 
  185         schema["properties"]["colcon-packages-ignore"] = {
  186             "type": "array",
  187             "minitems": 1,
  188             "uniqueItems": True,
  189             "items": {"type": "string"},
  190             "default": [],
  191         }
  192 
  193         schema["required"] = ["source"]
  194 
  195         return schema
  196 
  197     @classmethod
  198     def get_pull_properties(cls):
  199         # Inform Snapcraft of the properties associated with pulling. If these
  200         # change in the YAML Snapcraft will consider the pull step dirty.
  201         return ["colcon-packages", "colcon-source-space", "colcon-rosdistro"]
  202 
  203     @classmethod
  204     def get_build_properties(cls):
  205         # Inform Snapcraft of the properties associated with building. If these
  206         # change in the YAML Snapcraft will consider the build step dirty.
  207         return [
  208             "colcon-cmake-args",
  209             "colcon-catkin-cmake-args",
  210             "colcon-ament-cmake-args",
  211             "colcon-packages-ignore",
  212         ]
  213 
  214     @property
  215     def _pip(self):
  216         if not self.__pip:
  217             self.__pip = _python.Pip(
  218                 python_major_version="3",  # ROS2 uses python3
  219                 part_dir=self.partdir,
  220                 install_dir=self.installdir,
  221                 stage_dir=self.project.stage_dir,
  222             )
  223 
  224         return self.__pip
  225 
  226     @property
  227     def PLUGIN_STAGE_SOURCES(self):
  228         ros_repo = "http://repo.ros2.org/ubuntu/main"
  229         ubuntu_repo = "http://${prefix}.ubuntu.com/${suffix}/"
  230         security_repo = "http://${security}.ubuntu.com/${suffix}/"
  231 
  232         return textwrap.dedent(
  233             """
  234             deb {ros_repo} {codename} main
  235             deb {ubuntu_repo} {codename} main universe
  236             deb {ubuntu_repo} {codename}-updates main universe
  237             deb {ubuntu_repo} {codename}-security main universe
  238             deb {security_repo} {codename}-security main universe
  239             """.format(
  240                 ros_repo=ros_repo,
  241                 ubuntu_repo=ubuntu_repo,
  242                 security_repo=security_repo,
  243                 codename=_BASE_TO_UBUNTU_RELEASE_MAP[
  244                     self.project.info.get_build_base()
  245                 ],
  246             )
  247         )
  248 
  249     @property
  250     def PLUGIN_STAGE_KEYRINGS(self):
  251         return [_ROS_KEYRING_PATH]
  252 
  253     def __init__(self, name, options, project):
  254         super().__init__(name, options, project)
  255         self.out_of_source_build = True
  256 
  257         self._rosdistro = options.colcon_rosdistro
  258         if project.info.get_build_base() != _ROSDISTRO_TO_BASE_MAP[self._rosdistro]:
  259             raise ColconPluginBaseError(
  260                 self.name, project.info.get_build_base(), self._rosdistro
  261             )
  262 
  263         # Beta warning. Remove this comment and warning once plugin is stable.
  264         logger.warn(
  265             "The colcon plugin is currently in beta, its API may break. Use at your "
  266             "own risk."
  267         )
  268 
  269         self.__pip = None
  270         self._rosdep_path = os.path.join(self.partdir, "rosdep")
  271         self._ros_underlay = os.path.join(
  272             self.installdir, "opt", "ros", self._rosdistro
  273         )
  274 
  275         # Colcon packages cannot be installed into the same workspace as upstream ROS
  276         # components, which are Ament. We need to install them alongside and chain the
  277         # workspaces (upstream is the underlay, stuff we're building here is the
  278         # overlay).
  279         self._ros_overlay = os.path.join(self.installdir, "opt", "ros", "snap")
  280 
  281         # Always fetch colcon in order to build the workspace
  282         self.stage_packages.append("python3-colcon-common-extensions")
  283 
  284         # Get a unique set of packages
  285         self._packages = None
  286         if options.colcon_packages is not None:
  287             self._packages = set(options.colcon_packages)
  288 
  289         # The path created via the `source` key (or a combination of `source`
  290         # and `source-subdir` keys) needs to point to a valid Colcon workspace
  291         # containing another subdirectory called the "source space." By
  292         # default, this is a directory named "src," but it can be remapped via
  293         # the `source-space` key. It's important that the source space is not
  294         # the root of the Colcon workspace, since Colcon won't work that way
  295         # and it'll create a circular link that causes rosdep to hang.
  296         if self.options.source_subdir:
  297             self._ros_package_path = os.path.join(
  298                 self.sourcedir,
  299                 self.options.source_subdir,
  300                 self.options.colcon_source_space,
  301             )
  302         else:
  303             self._ros_package_path = os.path.join(
  304                 self.sourcedir, self.options.colcon_source_space
  305             )
  306 
  307         if os.path.abspath(self.sourcedir) == os.path.abspath(self._ros_package_path):
  308             raise ColconWorkspaceIsRootError()
  309 
  310     def env(self, root):
  311         """Runtime environment for ROS binaries and services."""
  312 
  313         env = [
  314             'AMENT_PYTHON_EXECUTABLE="{}"'.format(
  315                 os.path.join(root, "usr", "bin", "python3")
  316             ),
  317             'COLCON_PYTHON_EXECUTABLE="{}"'.format(
  318                 os.path.join(root, "usr", "bin", "python3")
  319             ),
  320             'SNAP_COLCON_ROOT="{}"'.format(root),
  321         ]
  322 
  323         # Each of these lines is prepended with an `export` when the environment is
  324         # actually generated. In order to inject real shell code we have to hack it in
  325         # by appending it on the end of an item already in the environment.
  326         # FIXME: There should be a better way to do this. LP: #1792034
  327         env[-1] = env[-1] + "\n\n" + self._source_setup_sh(root)
  328 
  329         return env
  330 
  331     def pull(self):
  332         """Copy source into build directory and fetch dependencies.
  333 
  334         Colcon packages can specify their system dependencies in their
  335         package.xml. In order to support that, the Colcon packages are
  336         interrogated for their dependencies here. Since `stage-packages` are
  337         already installed by the time this function is run, the dependencies
  338         from the package.xml are pulled down explicitly.
  339         """
  340 
  341         super().pull()
  342 
  343         # Make sure the package path exists before continuing. We only care about doing
  344         # this if there are actually packages to build, which is indicated both by
  345         # self._packages being None as well as a non-empty list.
  346         packages_to_build = self._packages is None or len(self._packages) > 0
  347         if packages_to_build and not os.path.exists(self._ros_package_path):
  348             raise ColconPackagePathNotFoundError(self._ros_package_path)
  349 
  350         # Use rosdep for dependency detection and resolution
  351         rosdep = _ros.rosdep.Rosdep(
  352             ros_distro=self._rosdistro,
  353             ros_package_path=self._ros_package_path,
  354             rosdep_path=self._rosdep_path,
  355             ubuntu_distro=_BASE_TO_UBUNTU_RELEASE_MAP[
  356                 self.project.info.get_build_base()
  357             ],
  358             ubuntu_sources=self.PLUGIN_STAGE_SOURCES,
  359             ubuntu_keyrings=self.PLUGIN_STAGE_KEYRINGS,
  360             project=self.project,
  361         )
  362         rosdep.setup()
  363 
  364         self._setup_dependencies(rosdep)
  365 
  366     def _setup_dependencies(self, rosdep):
  367         # Parse the Colcon packages to pull out their system dependencies
  368         system_dependencies = _find_system_dependencies(self._packages, rosdep)
  369 
  370         # Pull down and install any apt dependencies that were discovered
  371         self._setup_apt_dependencies(system_dependencies.get("apt"))
  372 
  373         # Pull down and install any pip dependencies that were discovered
  374         self._setup_pip_dependencies(system_dependencies.get("pip"))
  375 
  376     def _setup_apt_dependencies(self, apt_dependencies):
  377         if apt_dependencies:
  378             ubuntudir = os.path.join(self.partdir, "ubuntu")
  379             os.makedirs(ubuntudir, exist_ok=True)
  380 
  381             logger.info("Preparing to fetch apt dependencies...")
  382             ubuntu = repo.Ubuntu(
  383                 ubuntudir,
  384                 sources=self.PLUGIN_STAGE_SOURCES,
  385                 keyrings=self.PLUGIN_STAGE_KEYRINGS,
  386                 project_options=self.project,
  387             )
  388 
  389             logger.info("Fetching apt dependencies...")
  390             try:
  391                 ubuntu.get(apt_dependencies)
  392             except repo.errors.PackageNotFoundError as e:
  393                 raise ColconAptDependencyFetchError(e.message)
  394 
  395             logger.info("Installing apt dependencies...")
  396             ubuntu.unpack(self.installdir)
  397 
  398     def _setup_pip_dependencies(self, pip_dependencies):
  399         if pip_dependencies:
  400             self._pip.setup()
  401 
  402             logger.info("Fetching pip dependencies...")
  403             self._pip.download(pip_dependencies)
  404 
  405             logger.info("Installing pip dependencies...")
  406             self._pip.install(pip_dependencies)
  407 
  408     def clean_pull(self):
  409         super().clean_pull()
  410 
  411         # Remove the rosdep path, if any
  412         with contextlib.suppress(FileNotFoundError):
  413             shutil.rmtree(self._rosdep_path)
  414 
  415         # Clean pip packages, if any
  416         self._pip.clean_packages()
  417 
  418     def _source_setup_sh(self, root):
  419         underlaydir = os.path.join(root, "opt", "ros", self._rosdistro)
  420         overlaydir = os.path.join(root, "opt", "ros", "snap")
  421 
  422         source_script = textwrap.dedent(
  423             """
  424             # First, source the upstream ROS underlay
  425             if [ -f "{underlay_setup}" ]; then
  426                 . "{underlay_setup}"
  427             fi
  428 
  429             # Then source the overlay
  430             if [ -f "{overlay_setup}" ]; then
  431                 . "{overlay_setup}"
  432             fi
  433         """
  434         ).format(
  435             underlay_setup=os.path.join(underlaydir, "setup.sh"),
  436             overlay_setup=os.path.join(overlaydir, "setup.sh"),
  437         )
  438 
  439         # We need to source ROS's setup.sh at this point. However, it accepts
  440         # arguments (thus will parse $@), and we really don't want it to, since
  441         # $@ in this context will be meant for the app being launched
  442         # (LP: #1660852). So we'll backup all args, source the setup.sh, then
  443         # restore all args for the wrapper's `exec` line.
  444         return textwrap.dedent(
  445             """
  446             # Shell quote arbitrary string by replacing every occurrence of '
  447             # with '\\'', then put ' at the beginning and end of the string.
  448             # Prepare yourself, fun regex ahead.
  449             quote()
  450             {{
  451                 for i; do
  452                     printf %s\\\\n "$i" | sed "s/\'/\'\\\\\\\\\'\'/g;1s/^/\'/;\$s/\$/\' \\\\\\\\/"
  453                 done
  454                 echo " "
  455             }}
  456 
  457             BACKUP_ARGS=$(quote "$@")
  458             set --
  459             {}
  460             eval "set -- $BACKUP_ARGS"
  461         """  # noqa: W605
  462         ).format(
  463             source_script
  464         )  # noqa
  465 
  466     def build(self):
  467         """Build Colcon packages.
  468 
  469         This function runs some pre-build steps to prepare the sources for building in
  470         the Snapcraft environment, builds the packages with colcon, and finally runs
  471         some post-build clean steps to prepare the newly-minted install to be packaged
  472         as a .snap.
  473         """
  474 
  475         super().build()
  476 
  477         logger.info("Preparing to build colcon packages...")
  478         self._prepare_build()
  479 
  480         logger.info("Building colcon packages...")
  481         self._build_colcon_packages()
  482 
  483         logger.info("Cleaning up newly installed colcon packages...")
  484         self._finish_build()
  485 
  486     def _prepare_build(self):
  487         # Fix all shebangs to use the in-snap python.
  488         mangling.rewrite_python_shebangs(self.installdir)
  489 
  490         # Rewrite the prefixes to point to the in-part rosdir instead of the system
  491         self._fix_prefixes()
  492 
  493         # Each Colcon package distributes .cmake files so they can be found via
  494         # find_package(). However, the Ubuntu packages pulled down as
  495         # dependencies contain .cmake files pointing to system paths (e.g.
  496         # /usr/lib, /usr/include, etc.). They need to be rewritten to point to
  497         # the install directory.
  498         def _new_path(path):
  499             if not path.startswith(self.installdir):
  500                 # Not using os.path.join here as `path` is absolute.
  501                 return self.installdir + path
  502             return path
  503 
  504         self._rewrite_cmake_paths(_new_path)
  505 
  506     def _rewrite_cmake_paths(self, new_path_callable):
  507         def _rewrite_paths(match):
  508             paths = match.group(1).strip().split(";")
  509             for i, path in enumerate(paths):
  510                 # Offer the opportunity to rewrite this path if it's absolute.
  511                 if os.path.isabs(path):
  512                     paths[i] = new_path_callable(path)
  513 
  514             return '"' + ";".join(paths) + '"'
  515 
  516         # Looking for any path-like string
  517         file_utils.replace_in_file(
  518             self._ros_underlay,
  519             re.compile(r".*Config.cmake$"),
  520             re.compile(r'"(.*?/.*?)"'),
  521             _rewrite_paths,
  522         )
  523 
  524     def _finish_build(self):
  525         # Fix all shebangs to use the in-snap python.
  526         mangling.rewrite_python_shebangs(self.installdir)
  527 
  528         # We've finished the build, but we need to make sure we turn the cmake
  529         # files back into something that doesn't include our installdir. This
  530         # way it's usable from the staging area, and won't clash with the same
  531         # file coming from other parts.
  532         pattern = re.compile(r"^{}".format(self.installdir))
  533 
  534         def _new_path(path):
  535             return pattern.sub("$ENV{SNAPCRAFT_STAGE}", path)
  536 
  537         self._rewrite_cmake_paths(_new_path)
  538 
  539         # Rewrite prefixes for both the underlay and overlay.
  540         self._fix_prefixes()
  541 
  542         # If pip dependencies were installed, generate a sitecustomize that
  543         # allows access to them.
  544         if self._pip.is_setup() and self._pip.list(user=True):
  545             _python.generate_sitecustomize(
  546                 "3", stage_dir=self.project.stage_dir, install_dir=self.installdir
  547             )
  548 
  549     def _fix_prefixes(self):
  550         installdir_pattern = re.compile(r"^{}".format(self.installdir))
  551         new_prefix = "$SNAP_COLCON_ROOT"
  552 
  553         def _rewrite_prefix(match):
  554             # Group 1 is the variable definition, group 2 is the path, which we may need
  555             # to modify.
  556             path = match.group(3).strip(" \n\t'\"")
  557 
  558             # Bail early if this isn't even a path, or if it's already been rewritten
  559             if os.path.sep not in path or new_prefix in path:
  560                 return match.group()
  561 
  562             # If the path doesn't start with the installdir, then it needs to point to
  563             # the underlay given that the upstream ROS packages are expecting to be in
  564             # /opt/ros/.
  565             if not path.startswith(self.installdir):
  566                 path = os.path.join(new_prefix, path.lstrip("/"))
  567 
  568             return match.expand(
  569                 '\\1\\2"{}"\\4'.format(installdir_pattern.sub(new_prefix, path))
  570             )
  571 
  572         # Set the AMENT_CURRENT_PREFIX throughout to the in-snap prefix
  573         snapcraft.file_utils.replace_in_file(
  574             self.installdir,
  575             re.compile(r""),
  576             re.compile(r"(\${)(AMENT_CURRENT_PREFIX:=)(.*)(})"),
  577             _rewrite_prefix,
  578         )
  579 
  580         # Set the COLCON_CURRENT_PREFIX (if it's in the installdir) to the in-snap
  581         # prefix
  582         snapcraft.file_utils.replace_in_file(
  583             self.installdir,
  584             re.compile(r""),
  585             re.compile(
  586                 r"()(COLCON_CURRENT_PREFIX=)(['\"].*{}.*)()".format(self.installdir)
  587             ),
  588             _rewrite_prefix,
  589         )
  590 
  591         # Set the _colcon_prefix_sh_COLCON_CURRENT_PREFIX throughout to the in-snap
  592         # prefix
  593         snapcraft.file_utils.replace_in_file(
  594             self.installdir,
  595             re.compile(r""),
  596             re.compile(r"()(_colcon_prefix_sh_COLCON_CURRENT_PREFIX=)(.*)()"),
  597             _rewrite_prefix,
  598         )
  599 
  600         # Set the _colcon_package_sh_COLCON_CURRENT_PREFIX throughout to the in-snap
  601         # prefix
  602         snapcraft.file_utils.replace_in_file(
  603             self.installdir,
  604             re.compile(r""),
  605             re.compile(r"()(_colcon_package_sh_COLCON_CURRENT_PREFIX=)(.*)()"),
  606             _rewrite_prefix,
  607         )
  608 
  609         # Set the _colcon_prefix_chain_sh_COLCON_CURRENT_PREFIX throughout to the in-snap
  610         # prefix
  611         snapcraft.file_utils.replace_in_file(
  612             self.installdir,
  613             re.compile(r""),
  614             re.compile(r"()(_colcon_prefix_chain_sh_COLCON_CURRENT_PREFIX=)(.*)()"),
  615             _rewrite_prefix,
  616         )
  617 
  618         # Set the _colcon_python_executable throughout to use the in-snap python
  619         snapcraft.file_utils.replace_in_file(
  620             self.installdir,
  621             re.compile(r""),
  622             re.compile(r"()(_colcon_python_executable=)(.*)()"),
  623             _rewrite_prefix,
  624         )
  625 
  626     def _build_colcon_packages(self):
  627         # Nothing to do if no packages were specified
  628         if self._packages is not None and len(self._packages) == 0:
  629             return
  630 
  631         colconcmd = ["colcon", "build", "--merge-install"]
  632 
  633         if self._packages:
  634             # Specify the packages to be built
  635             colconcmd.append("--packages-select")
  636             colconcmd.extend(self._packages)
  637 
  638         if self.options.colcon_packages_ignore:
  639             colconcmd.extend(
  640                 ["--packages-ignore"] + self.options.colcon_packages_ignore
  641             )
  642 
  643         # Don't clutter the real ROS workspace-- use the Snapcraft build
  644         # directory
  645         colconcmd.extend(["--build-base", self.builddir])
  646 
  647         # Account for a non-default source space by always specifying it
  648         colconcmd.extend(["--base-paths", self._ros_package_path])
  649 
  650         # Specify that the packages should be installed into the overlay
  651         colconcmd.extend(["--install-base", self._ros_overlay])
  652 
  653         # Specify the number of workers
  654         colconcmd.append("--parallel-workers={}".format(self.parallel_build_count))
  655 
  656         # All the arguments that follow are meant for CMake
  657         colconcmd.append("--cmake-args")
  658 
  659         build_type = "Release"
  660         if "debug" in self.options.build_attributes:
  661             build_type = "Debug"
  662         colconcmd.extend(["-DCMAKE_BUILD_TYPE={}".format(build_type)])
  663 
  664         # Finally, add any cmake-args requested from the plugin options
  665         colconcmd.extend(self.options.colcon_cmake_args)
  666 
  667         if self.options.colcon_catkin_cmake_args:
  668             colconcmd.extend(
  669                 ["--catkin-cmake-args"] + self.options.colcon_catkin_cmake_args
  670             )
  671 
  672         if self.options.colcon_ament_cmake_args:
  673             colconcmd.extend(
  674                 ["--ament-cmake-args"] + self.options.colcon_ament_cmake_args
  675             )
  676 
  677         self.run(colconcmd)
  678 
  679 
  680 def _find_system_dependencies(colcon_packages, rosdep):
  681     """Find system dependencies for a given set of Colcon packages."""
  682 
  683     resolved_dependencies = {}
  684     dependencies = set()
  685 
  686     logger.info("Determining system dependencies for Colcon packages...")
  687     if colcon_packages is not None:
  688         for package in colcon_packages:
  689             # Query rosdep for the list of dependencies for this package
  690             dependencies |= rosdep.get_dependencies(package)
  691     else:
  692         # Rather than getting dependencies for an explicit list of packages,
  693         # let's get the dependencies for the entire workspace.
  694         dependencies |= rosdep.get_dependencies()
  695 
  696     for dependency in dependencies:
  697         _resolve_package_dependencies(
  698             colcon_packages, dependency, rosdep, resolved_dependencies
  699         )
  700 
  701     # We currently have nested dict structure of:
  702     #    dependency name -> package type -> package names
  703     #
  704     # We want to return a flattened dict of package type -> package names.
  705     flattened_dependencies = collections.defaultdict(set)
  706     for dependency_types in resolved_dependencies.values():
  707         for key, value in dependency_types.items():
  708             flattened_dependencies[key] |= value
  709 
  710     # Finally, return that dict of dependencies
  711     return flattened_dependencies
  712 
  713 
  714 def _resolve_package_dependencies(
  715     colcon_packages, dependency, rosdep, resolved_dependencies
  716 ):
  717     # No need to resolve this dependency if we know it's local, or if
  718     # we've already resolved it into a system dependency
  719     if dependency in resolved_dependencies or (
  720         colcon_packages and dependency in colcon_packages
  721     ):
  722         return
  723 
  724     # In this situation, the package depends on something that we
  725     # weren't instructed to build. It's probably a system dependency,
  726     # but the developer could have also forgotten to tell us to build
  727     # it.
  728     try:
  729         these_dependencies = rosdep.resolve_dependency(dependency)
  730     except _ros.rosdep.RosdepDependencyNotResolvedError:
  731         raise ColconInvalidSystemDependencyError(dependency)
  732 
  733     for key, value in these_dependencies.items():
  734         if key not in _SUPPORTED_DEPENDENCY_TYPES:
  735             raise ColconUnsupportedDependencyTypeError(key, dependency)
  736 
  737         resolved_dependencies[dependency] = {key: value}