pax_global_header00006660000000000000000000000064147207002650014515gustar00rootroot0000000000000052 comment=fae6c7a7a9de08ce67add515544a7d6f77a1b8cc colcon-cmake-0.2.29/000077500000000000000000000000001472070026500141425ustar00rootroot00000000000000colcon-cmake-0.2.29/.github/000077500000000000000000000000001472070026500155025ustar00rootroot00000000000000colcon-cmake-0.2.29/.github/workflows/000077500000000000000000000000001472070026500175375ustar00rootroot00000000000000colcon-cmake-0.2.29/.github/workflows/ci.yaml000066400000000000000000000003211472070026500210120ustar00rootroot00000000000000--- name: Run tests on: push: branches: ['master'] pull_request: jobs: pytest: uses: colcon/ci/.github/workflows/pytest.yaml@main secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} colcon-cmake-0.2.29/LICENSE000066400000000000000000000261361472070026500151570ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. colcon-cmake-0.2.29/README.rst000066400000000000000000000002251472070026500156300ustar00rootroot00000000000000colcon-cmake ============ An extension for `colcon-core `_ to support `CMake `_ projects. colcon-cmake-0.2.29/colcon_cmake/000077500000000000000000000000001472070026500165575ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/__init__.py000066400000000000000000000001531472070026500206670ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 __version__ = '0.2.29' colcon-cmake-0.2.29/colcon_cmake/argcomplete_completer/000077500000000000000000000000001472070026500231335ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/argcomplete_completer/__init__.py000066400000000000000000000000001472070026500252320ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/argcomplete_completer/cmake_args.py000066400000000000000000000027101472070026500256010ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 # try import since this package doesn't depend on colcon-argcomplete try: from colcon_argcomplete.argcomplete_completer \ import ArgcompleteCompleterExtensionPoint except ImportError: class ArgcompleteCompleterExtensionPoint: # noqa: D101 pass from colcon_core.plugin_system import satisfies_version class CmakeArgcompleteCompleter(ArgcompleteCompleterExtensionPoint): """Completion of CMake arguments.""" def __init__(self): # noqa: D107 super().__init__() satisfies_version( ArgcompleteCompleterExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') def get_completer(self, parser, *args, **kwargs): # noqa: D102 if '--cmake-args' not in args: return None try: from argcomplete.completers import ChoicesCompleter except ImportError: return None return ChoicesCompleter(get_cmake_args_completer_choices()) def get_cmake_args_completer_choices(): """ Get the CMake completer choices. Currently this includes only the `CMAKE_BUILD_TYPE`. :rtype: list """ # HACK the quote and equal characters are currently a problem # see https://github.com/kislyuk/argcomplete/issues/94 return [ ' -DCMAKE_BUILD_TYPE=%s' % build_type for build_type in ( 'Debug', 'MinSizeRel', 'None', 'Release', 'RelWithDebInfo')] colcon-cmake-0.2.29/colcon_cmake/environment/000077500000000000000000000000001472070026500211235ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/environment/__init__.py000066400000000000000000000000001472070026500232220ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/environment/cmake_module_path.py000066400000000000000000000107771472070026500251520ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 from collections import OrderedDict from colcon_core.environment import EnvironmentExtensionPoint from colcon_core.environment import logger from colcon_core.plugin_system import satisfies_version from colcon_core.shell import create_environment_hook class CmakeModulePathEnvironment(EnvironmentExtensionPoint): """Extend the `CMAKE_MODULE_PATH` variable to find CMake modules.""" def __init__(self): # noqa: D107 super().__init__() satisfies_version( EnvironmentExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') def create_environment_hooks(self, prefix_path, pkg_name): # noqa: D102 hooks = OrderedDict() logger.log(1, "checking '%s' for CMake module files" % prefix_path) for path in self._get_potential_cmake_module_paths( prefix_path, pkg_name ): if not path.is_dir(): continue # skip paths which are the same but only differ in case if any(path.samefile(p) for p in hooks.keys()): continue for filename in path.iterdir(): if not filename.is_file(): continue if ( filename.name.startswith('Find') and filename.name.endswith('.cmake') ): hooks[path] = create_environment_hook( 'cmake_module_path' + (str(len(hooks)) if hooks else ''), prefix_path, pkg_name, 'CMAKE_MODULE_PATH', str(path.relative_to(prefix_path)), mode='prepend') return [hook for hook_list in hooks.values() for hook in hook_list] def _get_potential_cmake_module_paths(self, prefix_path, pkg_name): paths = [] # see https://cmake.org/cmake/help/latest/command/find_package.html # for the list of locations considered by default / convention # / (W) # /(cmake|CMake)/ (W) # skipped due to not being package specific in the --merge-install case # /*/ (W) # /*/(cmake|CMake)/ (W) # skipped non-FHS compliant directories # /(lib/|lib*|share)/cmake/*/ (U) # skipped arch specific directory since arch is unknown here paths.append(prefix_path / 'lib64' / 'cmake' / pkg_name) paths.append(prefix_path / 'lib32' / 'cmake' / pkg_name) paths.append(prefix_path / 'libx32' / 'cmake' / pkg_name) paths.append(prefix_path / 'lib' / 'cmake' / pkg_name) paths.append(prefix_path / 'share' / 'cmake' / pkg_name) # /(lib/|lib*|share)/*/ (U) # skipped arch specific directory since arch is unknown here paths.append(prefix_path / 'lib64' / pkg_name) paths.append(prefix_path / 'lib32' / pkg_name) paths.append(prefix_path / 'libx32' / pkg_name) paths.append(prefix_path / 'lib' / pkg_name) paths.append(prefix_path / 'share' / pkg_name) # /(lib/|lib*|share)/*/(cmake|CMake)/ (U) # skipped arch specific directory since arch is unknown here paths.append(prefix_path / 'lib64' / pkg_name / 'cmake') paths.append(prefix_path / 'lib32' / pkg_name / 'cmake') paths.append(prefix_path / 'libx32' / pkg_name / 'cmake') paths.append(prefix_path / 'lib' / pkg_name / 'cmake') paths.append(prefix_path / 'lib64' / pkg_name / 'CMake') paths.append(prefix_path / 'lib32' / pkg_name / 'CMake') paths.append(prefix_path / 'libx32' / pkg_name / 'CMake') paths.append(prefix_path / 'lib' / pkg_name / 'CMake') paths.append(prefix_path / 'share' / pkg_name / 'cmake') paths.append(prefix_path / 'share' / pkg_name / 'CMake') # /*/(lib/|lib*|share)/cmake/*/ (W/U) # /*/(lib/|lib*|share)/*/ (W/U) # /*/(lib/|lib*|share)/*/(cmake|CMake)/ (W/U) # /.framework/Resources/ (A) # /.framework/Resources/CMake/ (A) # /.framework/Versions/*/Resources/ (A) # /.framework/Versions/*/Resources/CMake/ (A) # /.app/Contents/Resources/ (A) # /.app/Contents/Resources/CMake/ (A) # skipped non-FHS compliant directories return paths colcon-cmake-0.2.29/colcon_cmake/environment/cmake_prefix_path.py000066400000000000000000000024241472070026500251500ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 import os from colcon_core.environment import EnvironmentExtensionPoint from colcon_core.environment import logger from colcon_core.plugin_system import satisfies_version from colcon_core.shell import create_environment_hook class CmakePrefixPathEnvironment(EnvironmentExtensionPoint): """Extend the `CMAKE_PREFIX_PATH` variable to find CMake config files.""" def __init__(self): # noqa: D107 super().__init__() satisfies_version( EnvironmentExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') def create_environment_hooks(self, prefix_path, pkg_name): # noqa: D102 hooks = [] logger.log(1, "checking '%s' for CMake config files" % prefix_path) for _, _, filenames in os.walk(str(prefix_path)): for filename in filenames: if filename.endswith('-config.cmake') or \ filename.endswith('Config.cmake'): hooks += create_environment_hook( 'cmake_prefix_path', prefix_path, pkg_name, 'CMAKE_PREFIX_PATH', '', mode='prepend') break else: continue break return hooks colcon-cmake-0.2.29/colcon_cmake/event_handler/000077500000000000000000000000001472070026500213755ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/event_handler/__init__.py000066400000000000000000000000001472070026500234740ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/event_handler/compile_commands.py000066400000000000000000000115111472070026500252570ustar00rootroot00000000000000# Copyright 2020 Dirk Thomas # Licensed under the Apache License, Version 2.0 import os from pathlib import Path from colcon_cmake.task.cmake import CMAKE_EXECUTABLE from colcon_core.event.command import Command from colcon_core.event.job import JobQueued from colcon_core.event.job import JobUnselected from colcon_core.event_handler import EventHandlerExtensionPoint from colcon_core.event_reactor import EventReactorShutdown from colcon_core.logging import colcon_logger from colcon_core.plugin_system import satisfies_version logger = colcon_logger.getChild(__name__) class CompileCommandsEventHandler(EventHandlerExtensionPoint): """ Generate a `compile_commands.json` file for the whole workspace. The file is created in the build directory and aggregates the data from all packages. """ FILENAME = 'compile_commands.json' # the priority should be lower than e.g. the status and summary extensions # in order to show those as soon as possible PRIORITY = 40 def __init__(self): # noqa: D107 super().__init__() satisfies_version( EventHandlerExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') self._package_names = set() self._any_cmake_commands = False def __call__(self, event): # noqa: D102 data = event[0] if isinstance(data, (JobQueued, JobUnselected)): # delay loading json for all packages self._package_names.add(data.identifier) elif isinstance(data, Command): if data.cmd[0] == CMAKE_EXECUTABLE: self._any_cmake_commands = True elif isinstance(data, EventReactorShutdown): # if CMake was never invoked there is no need (re-)generate a # workspace-level json file if not self._any_cmake_commands: return # if no package has a json file there is no need for a # workspace-level json file package_level_json_paths = self._get_package_level_json_paths() workspace_level_json_path = self._get_path() if not package_level_json_paths: if workspace_level_json_path.exists(): workspace_level_json_path.unlink() return # if the workspace-level json file is newer than all package-level # json files it doesn't need to be regenerated try: workspace_level_mtime = os.path.getmtime( str(workspace_level_json_path)) except OSError: pass else: for json_path in sorted(package_level_json_paths): try: mtime = os.path.getmtime(str(json_path)) except OSError: continue if mtime > workspace_level_mtime: break else: return # read all package-level json data # and combine them in the workspace-level file # for performance reasons avoid to load/dump json data with workspace_level_json_path.open('wb') as h: h.write(b'[') # keep deterministic order independent of aborted/selected pkgs written_compile_commands = False for json_path in sorted(package_level_json_paths): compile_commands = self._get_compile_commands(json_path) if not compile_commands: continue if written_compile_commands: # add an empty line between packages h.write(b',\n') else: written_compile_commands = True h.write(b'\n') h.write(compile_commands) if written_compile_commands: h.write(b'\n') h.write(b']\n') def _get_package_level_json_paths(self): json_paths = set() for package_name in self._package_names: json_path = self._get_path(package_name) if json_path.exists(): json_paths.add(json_path) return json_paths def _get_compile_commands(self, json_path): content = json_path.read_bytes() try: open_index = content.index(b'[') close_index = content.rindex(b']') except ValueError: logger.warning( "Data in '%s' is expected to be a list" % json_path.absolute()) return None return content[open_index + 1:close_index - 1].strip() def _get_path(self, package_name=None): path = Path(self.context.args.build_base) if package_name is not None: path /= package_name path /= CompileCommandsEventHandler.FILENAME return path colcon-cmake-0.2.29/colcon_cmake/package_identification/000077500000000000000000000000001472070026500232235ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/package_identification/__init__.py000066400000000000000000000000001472070026500253220ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/package_identification/cmake.py000066400000000000000000000201271472070026500246570ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 import os from pathlib import Path import re from colcon_core.dependency_descriptor import DependencyDescriptor from colcon_core.logging import colcon_logger from colcon_core.package_identification \ import PackageIdentificationExtensionPoint from colcon_core.plugin_system import satisfies_version logger = colcon_logger.getChild(__name__) class CmakePackageIdentification(PackageIdentificationExtensionPoint): """Identify CMake packages with `CMakeLists.txt` files.""" def __init__(self): # noqa: D107 super().__init__() satisfies_version( PackageIdentificationExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') def identify(self, metadata): # noqa: D102 if metadata.type is not None and metadata.type != 'cmake': return cmakelists_txt = metadata.path / 'CMakeLists.txt' if not cmakelists_txt.is_file(): return data = extract_data(cmakelists_txt) if not data['name'] and not metadata.name: raise RuntimeError( "Failed to extract project name from '%s'" % cmakelists_txt) if data['name'] == 'Project': lines = cmakelists_txt.read_text(errors='replace').splitlines() if 'catkin_workspace()' in lines: logger.warning( f"Ignoring '{cmakelists_txt}' since it seems to be a " "toplevel CMake file generated by 'catkin_make'") return metadata.type = 'cmake' if metadata.name is None: metadata.name = data['name'] metadata.dependencies['build'] |= data['depends'] metadata.dependencies['run'] |= data['depends'] def extract_data(cmakelists_txt): """ Extract the project name and dependencies from a CMakeLists.txt file. :param Path cmakelists_txt: The path of the CMakeLists.txt file :rtype: dict """ content = extract_content(cmakelists_txt) data = {} data['name'] = extract_project_name(content) # fall back to use the directory name if data['name'] is None: data['name'] = cmakelists_txt.parent.name # extract dependencies from all CMake files in the project directory depends_content = content + extract_content( cmakelists_txt.parent, exclude=[cmakelists_txt]) depends = extract_dependencies(depends_content) # exclude self references data['depends'] = depends - {data['name']} return data def extract_content(basepath, exclude=None): """ Get all non-comment lines from CMake files under the given basepath. All `CMakeLists.txt` files as well as files ending with `.cmake` are used. Directories starting with a dot (`.`) are being skipped. :param Path basepath: The path to recursively crawl :param list exclude: The paths to exclude :rtype: str """ if basepath.is_file(): content = basepath.read_text(errors='replace') elif basepath.is_dir(): content = '' for dirpath, dirnames, filenames in os.walk(str(basepath)): # skip subdirectories starting with a dot dirnames[:] = filter(lambda d: not d.startswith('.'), dirnames) dirnames.sort() for name in sorted(filenames): if name != 'CMakeLists.txt' and not name.endswith('.cmake'): continue path = Path(dirpath) / name if path in (exclude or []): continue content += path.read_text(errors='replace') + '\n' else: return '' return _remove_cmake_comments(content) def _remove_cmake_comments(content): lines = content.splitlines() for index, line in enumerate(lines): lines[index] = _remove_cmake_comments_from_line(line) return '\n'.join(lines) def _remove_cmake_comments_from_line(line): # match comments starting with # # which are not within a string enclosed in double quotes pattern = ( # strings '("[^"]*")' '|' # comments '(#.*)' '|' # other '([^#"]*)' ) modline = '' for matches in re.findall(pattern, line): modline += matches[0] + matches[2] return modline def extract_project_name(content): """ Extract the CMake project name from the CMake code. The `project()` call must be on a single line and the first argument must be a literal string for this function to be able to extract the name. :param str content: The CMake source code :returns: The project name, otherwise None :rtype: str """ # extract project name match = re.search( # case insensitive function name '(?i:project)' # optional white space r'\s*' # open parenthesis r'\(' # optional white space r'\s*' # optional "opening" quote '("?)' # project name '([a-zA-Z0-9_-]+)' # optional "closing" quote (only if an "opening" quote was used) r'\1' # optional language r'(\s+[^\)]*)?' # close parenthesis r'\)', content) if not match: return None return match.group(2) def extract_dependencies(content): """ Extract the dependencies from the CMake code. The `find_package()` and `pkg_check_modules` calls must be on a single line and the first argument must be a literal string for this function to be able to extract the dependency name. :param str content: The CMake source code :returns: The dependencies name :rtype: list """ metadata = { 'origin': 'cmake', } return { DependencyDescriptor(d, metadata=metadata) for d in extract_find_package_calls(content) | _extract_pkg_config_calls(content) } def extract_find_package_calls(content, *, function_name='find_package'): """ Extract `find_package()`-like calls from the CMake code. The function call must be on a single line and the first argument must be a literal string for this function to be able to extract it. :param str content: The CMake source code :returns: The first arguments of the functions (in the case of `find_package()` these are the package names) :rtype: set """ matches = re.findall( # case insensitive function name f'(?i:{function_name})' # optional white space r'\s*' # open parenthesis r'\(' # optional white space r'\s*' # optional "opening" quote '("?)' # package name '([a-zA-Z0-9_-]+)' # optional "closing" quote (only if an "opening" quote was used) r'\1' # white space r'(\s+' # optional arguments r'[^\)]*)?' # close parenthesis r'\)', content) return {m[1] for m in matches} def _extract_pkg_config_calls(content): pattern1 = '(?i:pkg_check_modules)' pattern2 = '(?i:pkg_search_module)' function_names_pattern = f'(?:{pattern1}|{pattern2})' matches = re.findall( # case insensitive function names function_names_pattern + # optional white space r'\s*' # open parenthesis r'\(' # optional white space r'\s*' # optional "opening" quote '("?)' # prefix '[a-zA-Z0-9_]+' # optional "closing" quote (only if an "opening" quote was used) r'\1' # optional options prefixed by white space r'(?:\s+(?:REQUIRED|QUIET|NO_CMAKE_PATH|NO_CMAKE_ENVIRONMENT_PATH))*' # package names prefixed by white space with opt. trailing white space '([^)]+)' # close parenthesis r'\)', content) names = set() for modules in [m[1].strip() for m in matches]: # split multiple modules for module in modules.split(): # remove optional version suffix for char in ('>', '=', '<'): if char in module: module = module[:module.index(char)] names.add(module) return names colcon-cmake-0.2.29/colcon_cmake/task/000077500000000000000000000000001472070026500175215ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/task/__init__.py000066400000000000000000000000001472070026500216200ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/task/cmake/000077500000000000000000000000001472070026500206015ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/task/cmake/__init__.py000066400000000000000000000246551472070026500227260ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 import json import os from pathlib import Path import re import shutil import subprocess import sys from colcon_core.environment_variable import EnvironmentVariable from colcon_core.subprocess import check_output from packaging.version import Version """Environment variable to override the CMake executable""" CMAKE_COMMAND_ENVIRONMENT_VARIABLE = EnvironmentVariable( 'CMAKE_COMMAND', 'The full path to the CMake executable') """Environment variable to override the CTest executable""" CTEST_COMMAND_ENVIRONMENT_VARIABLE = EnvironmentVariable( 'CTEST_COMMAND', 'The full path to the CTest executable') def which_executable(environment_variable, executable_name): """ Determine the path of an executable. An environment variable can be used to override the location instead of relying on searching the PATH. :param str environment_variable: The name of the environment variable :param str executable_name: The name of the executable :rtype: str """ value = os.getenv(environment_variable) if value: return value return shutil.which(executable_name) CMAKE_EXECUTABLE = which_executable( CMAKE_COMMAND_ENVIRONMENT_VARIABLE.name, 'cmake') CTEST_EXECUTABLE = which_executable( CTEST_COMMAND_ENVIRONMENT_VARIABLE.name, 'ctest') MSBUILD_EXECUTABLE = shutil.which('msbuild') FILE_API_CLIENT_NAME = 'client-colcon-cmake' def add_api_queries(path): """ Create or update CMake file API queries. :param str path: The path of the directory contain the generated build system """ api_base = Path(path) / '.cmake' / 'api' / 'v1' query_base = api_base / 'query' / FILE_API_CLIENT_NAME query_base.mkdir(parents=True, exist_ok=True) (query_base / 'codemodel-v2').touch() def _read_codemodel(path): api_base = Path(path) / '.cmake' / 'api' / 'v1' reply_base = api_base / 'reply' for index_path in sorted(reply_base.glob('index-*.json'), reverse=True): break else: return None with index_path.open('r') as f: index_data = json.load(f) try: codemodel_file = ( index_data['reply'] [FILE_API_CLIENT_NAME] ['codemodel-v2'] ['jsonFile'] ) except KeyError: return None with (reply_base / codemodel_file).open('r') as f: return json.load(f) def _get_codemodel_targets(path): codemodel_data = _read_codemodel(path) if codemodel_data is None: return None config_data = codemodel_data.get('configurations', ()) if len(config_data) != 1: return None targets = [] for dir_data in config_data[0].get('directories') or (): if dir_data.get('hasInstallRule') is True: targets.append('install') break for target_data in config_data[0].get('targets') or (): target_name = target_data.get('name') if target_name is not None: targets.append(target_name) return targets async def has_target(path, target): """ Check if the CMake generated build system has a specific target. :param str path: The path of the directory contain the generated build system :param str target: The name of the target :rtype: bool """ codemodel_targets = _get_codemodel_targets(path) if codemodel_targets is not None: return target in codemodel_targets generator = get_generator(path) if 'Unix Makefiles' in generator: return target in await get_makefile_targets(path) if 'Ninja' in generator: return target in get_ninja_targets(path) if 'Visual Studio' in generator: assert target == 'install' install_project_file = get_project_file(path, 'INSTALL') return install_project_file is not None assert False, \ f"'has_target' not implemented for CMake generator '{generator}'" async def get_makefile_targets(path): """ Get all targets from a `Makefile`. :param str path: The path of the directory contain the Makefile :returns: The target names :rtype: list """ output = await check_output([ CMAKE_EXECUTABLE, '--build', path, '--target', 'help'], cwd=path) lines = output.decode().splitlines() prefix = '... ' return [line[len(prefix):] for line in lines if line.startswith(prefix)] def get_ninja_targets(path): """ Get all targets from a `build.ninja` file. :param str path: The path of the directory contain the Makefile :returns: The target names :rtype: list """ output = subprocess.check_output([ CMAKE_EXECUTABLE, '--build', path, '--target', 'help'], cwd=path) lines = output.decode().splitlines() suffix = ':' return [ line.split(' ')[0][:-len(suffix)] for line in lines if len(line.split(' ')) == 2 and line.split(' ')[0].endswith(suffix)] def get_buildfile(cmake_cache): """ Get the buildfile of the used CMake generator. :param Path cmake_cache: The path of the directory contain the build system :returns: The buildfile :rtype: Path """ generator = get_variable_from_cmake_cache( str(cmake_cache.parent), 'CMAKE_GENERATOR') if generator is not None: if 'Ninja' in generator: return cmake_cache.parent / 'build.ninja' if 'Visual Studio' in generator: return cmake_cache.parent / 'ALL_BUILD.vcxproj' return cmake_cache.parent / 'Makefile' def get_generator(path, cmake_args=None): """ Get CMake generator name. Either the CMake generator is specified in the command line arguments or it is being read from the `CMakeCache.txt` file. :param str path: The path of the directory contain the CMake cache file :param list cmake_args: The CMake command line arguments :rtype: str """ # check for generator in the command line arguments first generator = None for i, cmake_arg in enumerate(cmake_args or []): if cmake_arg == '-G' and i < len(cmake_args) - 1: generator = cmake_args[i + 1] if cmake_arg.startswith('-G') and len(cmake_arg) > 2: generator = cmake_arg[2:] if generator is None: # get the generator from the CMake cache generator = get_variable_from_cmake_cache( path, 'CMAKE_GENERATOR') return generator def is_multi_configuration_generator(path, cmake_args=None): """ Check if the used CMake generator is a multi configuration generator. :param str path: The path of the directory contain the CMake cache file :param list cmake_args: The CMake command line arguments :rtype: bool """ known_multi_configuration_generators = ( 'Ninja Multi-Config', 'Visual Studio', 'Xcode', ) generator = get_generator(path, cmake_args) for multi in known_multi_configuration_generators: if multi in generator: return True return False def get_variable_from_cmake_cache(path, var, *, default=None): """ Get a variable value from the CMake cache. :param str path: The path of the directory contain the CMake cache file :param str var: The name of the variable :param default: The default value returned if the variable is not defined in the cache :rtype: str """ lines = _get_cmake_cache_lines(path) if lines is None: return default line_prefix = f'{var}:' for line in lines: if line.startswith(line_prefix): try: index = line.index('=') except ValueError: continue return line[index + 1:] return default def _get_cmake_cache_lines(path): cmake_cache = os.path.join(path, 'CMakeCache.txt') if not os.path.exists(cmake_cache): return None with open(cmake_cache, 'r') as h: content = h.read() return content.splitlines() def get_project_file(path, target): """ Get a Visual Studio project file for a specific target. :param str path: The path of the directory project files :param str target: The name of the target :returns: The path of the project file if it exists, otherwise None :rtype: str """ project_file = os.path.join(path, target + '.vcxproj') if not os.path.isfile(project_file): return None return project_file def get_visual_studio_version(): """ Get the Visual Studio version. :rtype: str """ return os.environ.get('VisualStudioVersion', None) """ Global variable for the cached CMake version number. When valid, this will be a packaging.version.Version. It may also be None when the CMake version could not be determined to avoid trying to determine it again. """ _cached_cmake_version = False def get_cmake_version(): """ Get the CMake version. The function caches the result on the first invocation and reuses that on subsequent invocations. :returns: The version as reported by `CMAKE_EXECUTABLE --version`, or None when the version number could not be determined :rtype packaging.version.Version """ global _cached_cmake_version if _cached_cmake_version is False: _cached_cmake_version = _parse_cmake_version() return _cached_cmake_version def _parse_cmake_version(): """ Parse the CMake version printed by `CMAKE_EXECUTABLE --version`. :returns: The version parsed by packaging.version.Version, or None :rtype packaging.version.Version """ try: output = subprocess.check_output( [CMAKE_EXECUTABLE, '--version'], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: print('Failed to determine CMake version: ' + e.output.decode(), file=sys.stderr) else: lines = output.decode().splitlines() if lines: # Parse just the version part of the string. return _parse_cmake_version_string(lines[0]) return None def _parse_cmake_version_string(version_string): """ Parse the given CMake version string. Expects strings of the form 'cmake version 3.15.1'. :param str version_string: The version string to parse. :returns: The parsed version string or None on failure to parse. :rtype packaging.version.Version """ # Extract just the version part of the string. ver_re_str = r'^(?:.*\s)?(\d+\.\d+\.\d+).*' ver_match = re.match(ver_re_str, version_string) if ver_match: return Version(ver_match.group(1)) return None colcon-cmake-0.2.29/colcon_cmake/task/cmake/build.py000066400000000000000000000331001472070026500222470ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 import ast from contextlib import suppress import os from pathlib import Path import re from colcon_cmake.task.cmake import add_api_queries from colcon_cmake.task.cmake import CMAKE_EXECUTABLE from colcon_cmake.task.cmake import get_buildfile from colcon_cmake.task.cmake import get_cmake_version from colcon_cmake.task.cmake import get_generator from colcon_cmake.task.cmake import get_variable_from_cmake_cache from colcon_cmake.task.cmake import get_visual_studio_version from colcon_cmake.task.cmake import has_target from colcon_cmake.task.cmake import is_multi_configuration_generator from colcon_core.environment import create_environment_scripts from colcon_core.logging import colcon_logger from colcon_core.plugin_system import satisfies_version from colcon_core.shell import get_command_environment from colcon_core.task import run from colcon_core.task import TaskExtensionPoint from packaging.version import Version logger = colcon_logger.getChild(__name__) class CmakeBuildTask(TaskExtensionPoint): """Build CMake packages.""" def __init__(self): # noqa: D107 super().__init__() satisfies_version(TaskExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') def add_arguments(self, *, parser): # noqa: D102 parser.add_argument( '--cmake-args', nargs='*', metavar='*', type=str.lstrip, help='Pass arguments to CMake projects. ' 'Arguments matching other options must be prefixed by a space,\n' 'e.g. --cmake-args " --help" (stdout might not be shown by ' 'default, e.g. add `--event-handlers console_cohesion+`)') parser.add_argument( '--cmake-target', help='Build a specific target instead of the default target') parser.add_argument( '--cmake-target-skip-unavailable', action='store_true', help="Skip building packages which don't have the target passed " 'to --cmake-target') parser.add_argument( '--cmake-clean-cache', action='store_true', help='Remove CMake cache before the build (implicitly forcing ' 'CMake configure step)') parser.add_argument( '--cmake-clean-first', action='store_true', help="Build target 'clean' first, then build (to only clean use " "'--cmake-target clean')") parser.add_argument( '--cmake-force-configure', action='store_true', help='Force CMake configure step') async def build( # noqa: D102 self, *, additional_hooks=None, skip_hook_creation=False, environment_callback=None, additional_targets=None ): pkg = self.context.pkg args = self.context.args logger.info(f"Building CMake package in '{args.path}'") try: env = await get_command_environment( 'build', args.build_base, self.context.dependencies) except RuntimeError as e: logger.error(str(e)) return 1 if environment_callback is not None: environment_callback(env) rc = await self._reconfigure(args, env) if rc: return rc # ensure that CMake cache contains the project name project_name = get_variable_from_cmake_cache( args.build_base, 'CMAKE_PROJECT_NAME') if project_name is None: # if not the CMake code hasn't called project() and can't be built logger.warning( f"Could not build CMake package '{pkg.name}' because the " "CMake cache has no 'CMAKE_PROJECT_NAME' variable") return rc = await self._build( args, env, additional_targets=additional_targets) if rc: return rc # skip install step if a specific target was requested if not args.cmake_target: if await has_target(args.build_base, 'install'): completed = await self._install(args, env) if completed.returncode: return completed.returncode else: logger.warning( 'Could not run installation step for package ' f"'{pkg.name}' because it has no 'install' target") if not skip_hook_creation: create_environment_scripts( pkg, args, additional_hooks=additional_hooks) async def _reconfigure(self, args, env): self.progress('cmake') cmake_cache = Path(args.build_base) / 'CMakeCache.txt' run_configure = args.cmake_force_configure if args.cmake_clean_cache and cmake_cache.exists(): cmake_cache.unlink() if not run_configure: run_configure = not cmake_cache.exists() if not run_configure: buildfile = get_buildfile(cmake_cache) run_configure = not buildfile.exists() # check CMake args from last run to decide on need to reconfigure if not run_configure: last_cmake_args = self._get_last_cmake_args(args.build_base) run_configure = (args.cmake_args or []) != (last_cmake_args or []) self._store_cmake_args(args.build_base, args.cmake_args) if not run_configure: return # invoke CMake / reconfigure target cmake_args = [args.path] cmake_args += (args.cmake_args or []) cmake_args += ['-DCMAKE_INSTALL_PREFIX=' + args.install_base] generator = get_generator(args.build_base, args.cmake_args) if os.name == 'nt' and generator is None: vsv = get_visual_studio_version() if vsv is None: raise RuntimeError( 'VisualStudioVersion is not set, ' 'please run within a Visual Studio Command Prompt.') supported_vsv = { '17.0': 'Visual Studio 17 2022', '16.0': 'Visual Studio 16 2019', '15.0': 'Visual Studio 15 2017', '14.0': 'Visual Studio 14 2015', } if vsv not in supported_vsv: raise RuntimeError(f"Unknown / unsupported VS version '{vsv}'") cmake_args += ['-G', supported_vsv[vsv]] # choose 'x64' on VS 14 and 15 if not specified explicitly # since otherwise 'Win32' is the default for those # newer versions default to the host architecture if '-A' not in cmake_args and vsv in ('14.0', '15.0'): cmake_args += ['-A', 'x64'] if CMAKE_EXECUTABLE is None: raise RuntimeError("Could not find 'cmake' executable") os.makedirs(args.build_base, exist_ok=True) add_api_queries(args.build_base) completed = await run( self.context, [CMAKE_EXECUTABLE] + cmake_args, cwd=args.build_base, env=env) # in the case CMake fails with an error code make sure to delete the # buildfile if it exists to avoid not running reconfigure next time if completed.returncode: buildfile = get_buildfile(cmake_cache) if buildfile.exists(): buildfile.unlink() return completed.returncode def _get_last_cmake_args(self, build_base): path = self._get_last_cmake_args_path(build_base) if not path.exists(): return None with path.open('r') as h: content = h.read() try: return ast.literal_eval(content) except SyntaxError as e: # noqa: F841 logger.error( f"Failed to parse previous --cmake-args from '{path}': {e}") return None def _store_cmake_args(self, build_base, cmake_args): path = self._get_last_cmake_args_path(build_base) with path.open('w') as h: h.write(str(cmake_args)) def _get_last_cmake_args_path(self, build_base): return Path(build_base) / 'cmake_args.last' async def _build(self, args, env, *, additional_targets=None): self.progress('build') # invoke build step if CMAKE_EXECUTABLE is None: raise RuntimeError("Could not find 'cmake' executable") targets = [] if args.cmake_target: targets.append(args.cmake_target) else: targets.append('') if additional_targets: targets += additional_targets multi_configuration_generator = is_multi_configuration_generator( args.build_base, args.cmake_args) if multi_configuration_generator: generator = get_generator(args.build_base) if 'Visual Studio' in generator: env = self._get_msbuild_environment(args, env) for i, target in enumerate(targets): cmd = [CMAKE_EXECUTABLE, '--build', args.build_base] if target: if args.cmake_target_skip_unavailable: if not await has_target(args.build_base, target): continue self.progress(f"build target '{target}'") cmd += ['--target', target] if i == 0 and args.cmake_clean_first: cmd += ['--clean-first'] if multi_configuration_generator: cmd += ['--config', self._get_configuration(args)] else: job_args = self._get_make_arguments(env) if job_args: cmd += ['--'] + job_args completed = await run( self.context, cmd, cwd=args.build_base, env=env) if completed.returncode: return completed.returncode def _get_configuration(self, args): # check for CMake build type in the command line arguments arg_prefix = '-DCMAKE_BUILD_TYPE=' build_type = None for cmake_arg in (args.cmake_args or []): if cmake_arg.startswith(arg_prefix): build_type = cmake_arg[len(arg_prefix):] if build_type is None: # get the CMake build type from the CMake cache build_type = get_variable_from_cmake_cache( args.build_base, 'CMAKE_BUILD_TYPE') if build_type in ('Debug', 'MinSizeRel', 'RelWithDebInfo'): return build_type return 'Release' def _get_msbuild_environment(self, args, env): generator = get_generator(args.build_base) if 'Visual Studio' in generator: if 'CL' in env: cl_split = env['CL'].split(' ') # check that /MP* isn't set already if any(x.startswith('/MP') for x in cl_split): # otherwise avoid overriding existing parameters return env else: cl_split = [] # build with multiple processes using the number of processors cl_split.append('/MP') env = dict(env) env['CL'] = ' '.join(cl_split) return env def _get_make_arguments(self, env): """ Get the make arguments to limit the number of simultaneously run jobs. The arguments are chosen based on the `cpu_count`, e.g. -j4 -l4. :param dict env: a dictionary with environment variables :returns: list of make arguments :rtype: list of strings """ # check MAKEFLAGS for -j/--jobs/-l/--load-average arguments makeflags = env.get('MAKEFLAGS', '') regex = ( r'(?:^|\s)' r'(-?(?:j|l)(?:\s*[0-9]+|\s|$))' r'|' r'(?:^|\s)' r'((?:--)?(?:jobs|load-average)(?:(?:=|\s+)[0-9]+|(?:\s|$)))' ) matches = re.findall(regex, makeflags) or [] matches = [m[0] or m[1] for m in matches] if matches: # do not extend make arguments, let MAKEFLAGS set things return [] # Use the number of CPU cores jobs = os.cpu_count() with suppress(AttributeError): # consider restricted set of CPUs if applicable jobs = min(jobs, len(os.sched_getaffinity(0))) if jobs is None: # the number of cores can't be determined return [] return [ f'-j{jobs}', f'-l{jobs}', ] async def _install(self, args, env): self.progress('install') if CMAKE_EXECUTABLE is None: raise RuntimeError("Could not find 'cmake' executable") cmd = [CMAKE_EXECUTABLE] cmake_ver = get_cmake_version() allow_job_args = True if cmake_ver and cmake_ver >= Version('3.15.0'): # CMake 3.15+ supports invoking `cmake --install` cmd += ['--install', args.build_base] # Job args not compatible with --install directive allow_job_args = False else: # fallback to the install target which implicitly runs a build if not cmake_ver: logger.warning( 'Unable to determine CMake version, building the install' "target instead of invoking 'cmake --install'") cmd += ['--build', args.build_base, '--target', 'install'] multi_configuration_generator = is_multi_configuration_generator( args.build_base, args.cmake_args) if multi_configuration_generator: cmd += ['--config', self._get_configuration(args)] elif allow_job_args: job_args = self._get_make_arguments(env) if job_args: cmd += ['--'] + job_args return await run( self.context, cmd, cwd=args.build_base, env=env) colcon-cmake-0.2.29/colcon_cmake/task/cmake/test.py000066400000000000000000000130251472070026500221330ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 from contextlib import suppress import os from pathlib import Path import shutil from colcon_cmake.task.cmake import CTEST_EXECUTABLE from colcon_cmake.task.cmake import get_variable_from_cmake_cache from colcon_core.event.test import TestFailure from colcon_core.logging import colcon_logger from colcon_core.plugin_system import satisfies_version from colcon_core.shell import get_command_environment from colcon_core.subprocess import check_output from colcon_core.task import run from colcon_core.task import TaskExtensionPoint logger = colcon_logger.getChild(__name__) class CmakeTestTask(TaskExtensionPoint): """Test CMake packages.""" def __init__(self): # noqa: D107 super().__init__() satisfies_version(TaskExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') def add_arguments(self, *, parser): # noqa: D102 parser.add_argument( '--ctest-args', nargs='*', metavar='*', type=str.lstrip, help='Pass arguments to CTest projects. ' 'Arguments matching other options must be prefixed by a space,\n' 'e.g. --ctest-args " --help" (stdout might not be shown by ' 'default, e.g. add `--event-handlers console_cohesion+`)') async def test(self, *, additional_hooks=None): # noqa: D102 pkg = self.context.pkg args = self.context.args logger.info(f"Testing CMake package in '{args.path}'") assert os.path.exists(args.build_base), \ 'Has this package been built before?' try: env = await get_command_environment( 'test', args.build_base, self.context.dependencies) except RuntimeError as e: logger.error(str(e)) return 1 if CTEST_EXECUTABLE is None: raise RuntimeError("Could not find 'ctest' executable") # check if CTest has any tests output = await check_output( [CTEST_EXECUTABLE, '--show-only'], cwd=args.build_base, env=env) for line in output.decode().splitlines(): if line.startswith(' '): break else: logger.log(5, f"No ctests found in '{args.path}'") return # CTest arguments ctest_args = [ # generate xml of test summary '-D', 'ExperimentalTest', '--no-compress-output', # show all test output '-V', '--force-new-ctest-process', ] + (args.ctest_args or []) # Check for explicit '-C' or '--build-type' in args.ctest_args. # If not, we'll add it based on the CMakeCache CMAKE_BUILD_TYPE value. if '-C' not in ctest_args and '--build-config' not in ctest_args: # choose configuration, required for multi-configuration generators ctest_args[0:0] = [ '-C', self._get_configuration_from_cmake(args.build_base), ] if args.retest_until_fail: count = args.retest_until_fail + 1 ctest_args += [ '--repeat-until-fail', str(count), ] # delete an existing Testing/TAG file to ensure a new tag name is used tag_file = Path(args.build_base) / 'Testing' / 'TAG' with suppress(FileNotFoundError): tag_file.unlink() rerun = 0 while True: # invoke CTest completed = await run( self.context, [CTEST_EXECUTABLE] + ctest_args, cwd=args.build_base, env=env) if not completed.returncode: break # try again if requested if args.retest_until_pass > rerun: if not rerun: ctest_args += [ '--rerun-failed', ] rerun += 1 continue # CTest reports failing tests if completed.returncode == 8: self.context.put_event_into_queue(TestFailure(pkg.name)) # the return code should still be 0 break return completed.returncode # copy Testing/TAG and Testing//Test.xml to custom location if args.test_result_base and args.test_result_base != args.build_base: if not tag_file.is_file(): return # find the latest Test.xml file latest_xml_dir = tag_file.read_text().splitlines()[0] latest_xml_path = tag_file.parent / latest_xml_dir / 'Test.xml' if not latest_xml_path.exists(): logger.warning( f"Skipping '{tag_file}': could not find latest XML file " f"'{latest_xml_path}'") return dst = Path(args.test_result_base) / 'Testing' / latest_xml_dir dst.mkdir(parents=True, exist_ok=True) _copy_file(str(tag_file), str(dst.parent / tag_file.name)) _copy_file(str(latest_xml_path), str(dst / latest_xml_path.name)) def _get_configuration_from_cmake(self, build_base): # get for CMake build type from the CMake cache build_type = get_variable_from_cmake_cache( build_base, 'CMAKE_BUILD_TYPE') if build_type in ('Debug', 'MinSizeRel', 'RelWithDebInfo'): return build_type return 'Release' def _copy_file(src, dst): if os.path.islink(dst): os.unlink(dst) elif os.path.isdir(dst): shutil.rmtree(dst) shutil.copy2(src, dst) colcon-cmake-0.2.29/colcon_cmake/test_result/000077500000000000000000000000001472070026500211345ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/test_result/__init__.py000066400000000000000000000000001472070026500232330ustar00rootroot00000000000000colcon-cmake-0.2.29/colcon_cmake/test_result/ctest.py000066400000000000000000000101441472070026500226300ustar00rootroot00000000000000# Copyright 2019 Dirk Thomas # Licensed under the Apache License, Version 2.0 from xml.etree.ElementTree import ElementTree from colcon_core.logging import colcon_logger from colcon_core.plugin_system import satisfies_version from colcon_test_result.test_result import Result from colcon_test_result.test_result import TestResultExtensionPoint logger = colcon_logger.getChild(__name__) class CtestTestResult(TestResultExtensionPoint): """ Collect the CTest results generated when testing a set of CMake packages. It checks each direct subdirectory of the passed build base for a 'Testing/TAG' file. The first line in that file contains the directory name of the latest 'Test.xml' result file relative to the 'Testing' directory. """ def __init__(self): # noqa: D107 super().__init__() satisfies_version( TestResultExtensionPoint.EXTENSION_POINT_VERSION, '^1.0') def get_test_results( # noqa: D102 self, basepath, *, collect_details, files=None ): results = set() # check all 'TAG' files in a directory named 'Testing' for tag_file in basepath.glob('**/Testing/TAG'): if not tag_file.is_file(): continue if files is not None: files.add(str(tag_file)) # find the latest Test.xml file latest_xml_dir = tag_file.read_text().splitlines()[0] latest_xml_path = tag_file.parent / latest_xml_dir / 'Test.xml' if not latest_xml_path.exists(): logger.warning( f"Skipping '{tag_file}': could not find latest XML file " f"'{latest_xml_path}'") continue # parse the XML file tree = ElementTree() root = tree.parse(str(latest_xml_path)) # check if the root tag looks like a CTest file if root.tag != 'Site': logger.warning( f"Skipping '{latest_xml_path}': the root tag is not " "'Site'") continue # look for a single 'Testing' child tag children = list(root) if len(children) != 1: logger.warning( f"Skipping '{latest_xml_path}': 'Site' tag is expected to " '"have exactly one child') continue if children[0].tag != 'Testing': logger.warning( f"Skipping '{latest_xml_path}': the child tag is not " "'Testing'") continue if files is not None: files.add(str(latest_xml_path)) # collect information from all 'Test' tags result = Result(str(latest_xml_path)) for child in children[0]: if child.tag != 'Test': continue result.test_count += 1 try: status = child.attrib['Status'] except KeyError: logger.warning( f"Skipping '{latest_xml_path}': a 'test' tag lacks a " "'Status' attribute") break if status == 'failed': result.failure_count += 1 elif status == 'notrun': result.skipped_count += 1 elif status != 'passed': result.error_count += 1 if collect_details and status == 'failed': lines = [child.findtext('Name')] lines.extend( _get_messages( 'failure message', child.findtext('Results/Measurement/Value'))) result.details.append('\n'.join(lines)) else: results.add(result) return results def _get_messages(label, message): lines = [] if message: lines.append('<<< ' + label) for line in message.strip('\n\r').splitlines(): lines.append(' ' + line) lines.append('>>>') return lines colcon-cmake-0.2.29/publish-python.yaml000066400000000000000000000005201472070026500200100ustar00rootroot00000000000000artifacts: - type: wheel uploads: - type: pypi - type: stdeb uploads: - type: packagecloud config: repository: dirk-thomas/colcon distributions: - ubuntu:focal - ubuntu:jammy - ubuntu:noble - debian:bookworm - debian:trixie colcon-cmake-0.2.29/setup.cfg000066400000000000000000000057711472070026500157750ustar00rootroot00000000000000[metadata] name = colcon-cmake version = attr: colcon_cmake.__version__ url = https://colcon.readthedocs.io project_urls = Changelog = https://github.com/colcon/colcon-cmake/milestones?direction=desc&sort=due_date&state=closed GitHub = https://github.com/colcon/colcon-cmake/ author = Dirk Thomas author_email = web@dirk-thomas.net maintainer = Dirk Thomas, Michel Hidalgo maintainer_email = web@dirk-thomas.net, michel@ekumenlabs.com classifiers = Development Status :: 3 - Alpha Environment :: Plugins Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: MacOS Operating System :: Microsoft :: Windows Operating System :: POSIX Programming Language :: Python Topic :: Software Development :: Build Tools license = Apache License, Version 2.0 description = Extension for colcon to support CMake packages. long_description = file: README.rst keywords = colcon [options] python_requires = >=3.6 install_requires = colcon-core>=0.5.6 # to set an environment variable when a package installs a library colcon-library-path colcon-test-result>=0.3.3 packaging packages = find: zip_safe = true [options.extras_require] test = flake8>=3.6.0 flake8-blind-except flake8-builtins flake8-class-newline flake8-comprehensions flake8-deprecated flake8-docstrings flake8-import-order flake8-quotes pep8-naming pylint pytest pytest-cov scspell3k>=2.2 [tool:pytest] filterwarnings = error # Suppress deprecation warnings in other packages ignore:lib2to3 package is deprecated::scspell ignore:pkg_resources is deprecated as an API::flake8_import_order ignore:SelectableGroups dict interface is deprecated::flake8 ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated::pyreadline junit_suite_name = colcon-cmake markers = flake8 linter [options.entry_points] colcon_argcomplete.argcomplete_completer = cmake_args = colcon_cmake.argcomplete_completer.cmake_args:CmakeArgcompleteCompleter colcon_core.environment = cmake_module_path = colcon_cmake.environment.cmake_module_path:CmakeModulePathEnvironment cmake_prefix_path = colcon_cmake.environment.cmake_prefix_path:CmakePrefixPathEnvironment colcon_core.environment_variable = cmake_command = colcon_cmake.task.cmake:CMAKE_COMMAND_ENVIRONMENT_VARIABLE ctest_command = colcon_cmake.task.cmake:CTEST_COMMAND_ENVIRONMENT_VARIABLE colcon_core.event_handler = compile_commands = colcon_cmake.event_handler.compile_commands:CompileCommandsEventHandler colcon_core.package_identification = cmake = colcon_cmake.package_identification.cmake:CmakePackageIdentification colcon_core.task.build = cmake = colcon_cmake.task.cmake.build:CmakeBuildTask colcon_core.task.test = cmake = colcon_cmake.task.cmake.test:CmakeTestTask colcon_test_result.test_result = ctest = colcon_cmake.test_result.ctest:CtestTestResult [flake8] import-order-style = google [coverage:run] source = colcon_cmake colcon-cmake-0.2.29/setup.py000066400000000000000000000001721472070026500156540ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 from setuptools import setup setup() colcon-cmake-0.2.29/stdeb.cfg000066400000000000000000000003711472070026500157250ustar00rootroot00000000000000[colcon-cmake] No-Python2: Depends3: cmake, python3-colcon-core (>= 0.5.6), python3-colcon-library-path, python3-colcon-test-result (>= 0.3.3), python3-packaging Suite: focal jammy noble bookworm trixie X-Python3-Version: >= 3.6 Debian-Version: 100 colcon-cmake-0.2.29/test/000077500000000000000000000000001472070026500151215ustar00rootroot00000000000000colcon-cmake-0.2.29/test/spell_check.words000066400000000000000000000007411472070026500204570ustar00rootroot00000000000000apache argcomplete asyncio autouse basepath buildfile cmake cmakelists codemodel colcon completers configs contextlib ctest ctests dcmake etree findtext getaffinity github https iterdir kazys kislyuk libx linter lstrip makefile makefiles makeflags modline monkeypatch msbuild mtime nargs noqa notrun pathlib pkgs plugin prepend pydocstyle pytest returncode rglob rindex rmtree rtype samefile scspell setuptools skipif stepanas tagname tempfile thomas tmpdir unittest vcxproj xcode colcon-cmake-0.2.29/test/test_copyright_license.py000066400000000000000000000024731472070026500222520ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 from pathlib import Path import sys import pytest @pytest.mark.linter def test_copyright_license(): missing = check_files([Path(__file__).parents[1]]) assert not len(missing), \ 'In some files no copyright / license line was found' def check_files(paths): missing = [] for path in paths: if path.is_dir(): for p in sorted(path.iterdir()): if p.name.startswith('.'): continue if p.name.endswith('.py') or p.is_dir(): missing += check_files([p]) if path.is_file(): content = path.read_text() if not content: continue lines = content.splitlines() has_copyright = any(filter( lambda line: line.startswith('# Copyright'), lines)) has_license = \ '# Licensed under the Apache License, Version 2.0' in lines if not has_copyright or not has_license: print( 'Could not find copyright / license in:', path, file=sys.stderr) missing .append(path) else: print('Found copyright / license in:', path) return missing colcon-cmake-0.2.29/test/test_environment_cmake_module_path.py000066400000000000000000000030241472070026500246160ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Copyright 2024 Open Source Robotics Foundation, Inc. # Licensed under the Apache License, Version 2.0 from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import patch from colcon_cmake.environment.cmake_module_path \ import CmakeModulePathEnvironment def test_cmake_module_path(): extension = CmakeModulePathEnvironment() with TemporaryDirectory(prefix='test_colcon_') as prefix_path: prefix_path = Path(prefix_path) with patch( 'colcon_cmake.environment.cmake_module_path.' 'create_environment_hook', return_value=['/some/hook', '/other/hook'] ): # No CMake configs exist hooks = extension.create_environment_hooks(prefix_path, 'pkg_name') assert len(hooks) == 0 pkg_share_path = prefix_path / 'share' / 'pkg_name' # Unrelated file unrelated_file = pkg_share_path / 'cmake' / 'README.md' unrelated_file.parent.mkdir(parents=True, exist_ok=True) unrelated_file.touch() hooks = extension.create_environment_hooks(prefix_path, 'pkg_name') assert len(hooks) == 0 # FindPkgName.cmake exists cmake_module = pkg_share_path / 'cmake' / 'FindPkgName.cmake' cmake_module.parent.mkdir(parents=True, exist_ok=True) cmake_module.touch() hooks = extension.create_environment_hooks(prefix_path, 'pkg_name') assert len(hooks) == 2 colcon-cmake-0.2.29/test/test_environment_cmake_prefix_path.py000066400000000000000000000036551472070026500246400ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Copyright 2024 Open Source Robotics Foundation, Inc. # Licensed under the Apache License, Version 2.0 from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import patch from colcon_cmake.environment.cmake_prefix_path \ import CmakePrefixPathEnvironment def test_cmake_prefix_path(): extension = CmakePrefixPathEnvironment() with TemporaryDirectory(prefix='test_colcon_') as prefix_path: prefix_path = Path(prefix_path) with patch( 'colcon_cmake.environment.cmake_prefix_path' '.create_environment_hook', return_value=['/some/hook', '/other/hook'] ): # No CMake configs exist hooks = extension.create_environment_hooks(prefix_path, 'pkg_name') assert len(hooks) == 0 pkg_share_path = prefix_path / 'share' / 'pkg_name' # Unrelated file unrelated_file = pkg_share_path / 'cmake' / 'README.md' unrelated_file.parent.mkdir(parents=True, exist_ok=True) unrelated_file.touch() hooks = extension.create_environment_hooks(prefix_path, 'pkg_name') assert len(hooks) == 0 # PkgNameConfig.cmake exists cmake_config = pkg_share_path / 'cmake' / 'PkgNameConfig.cmake' cmake_config.parent.mkdir(parents=True, exist_ok=True) cmake_config.touch() hooks = extension.create_environment_hooks(prefix_path, 'pkg_name') assert len(hooks) == 2 cmake_config.unlink() # pkg_name-config.cmake exists cmake_config = pkg_share_path / 'cmake' / 'pkg_name-config.cmake' cmake_config.parent.mkdir(parents=True, exist_ok=True) cmake_config.touch() hooks = extension.create_environment_hooks(prefix_path, 'pkg_name') assert len(hooks) == 2 cmake_config.unlink() colcon-cmake-0.2.29/test/test_flake8.py000066400000000000000000000033341472070026500177070ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Licensed under the Apache License, Version 2.0 import logging from pathlib import Path import sys import pytest @pytest.mark.flake8 @pytest.mark.linter def test_flake8(): from flake8.api.legacy import get_style_guide # avoid debug / info / warning messages from flake8 internals logging.getLogger('flake8').setLevel(logging.ERROR) # for some reason the pydocstyle logger changes to an effective level of 1 # set higher level to prevent the output to be flooded with debug messages logging.getLogger('pydocstyle').setLevel(logging.WARNING) style_guide = get_style_guide( extend_ignore=['D100', 'D104'], show_source=True, ) style_guide_tests = get_style_guide( extend_ignore=['D100', 'D101', 'D102', 'D103', 'D104', 'D105', 'D107'], show_source=True, ) stdout = sys.stdout sys.stdout = sys.stderr # implicitly calls report_errors() report = style_guide.check_files([ str(Path(__file__).parents[1] / 'colcon_cmake'), ]) report_tests = style_guide_tests.check_files([ str(Path(__file__).parents[1] / 'test'), ]) sys.stdout = stdout total_errors = report.total_errors + report_tests.total_errors if total_errors: # pragma: no cover # output summary with per-category counts print() if report.total_errors: report._application.formatter.show_statistics(report._stats) if report_tests.total_errors: report_tests._application.formatter.show_statistics( report_tests._stats) print(f'flake8 reported {total_errors} errors', file=sys.stderr) assert not total_errors, f'flake8 reported {total_errors} errors' colcon-cmake-0.2.29/test/test_package_identification_cmake.py000066400000000000000000000062571472070026500243500ustar00rootroot00000000000000# Copyright 2016-2018 Dirk Thomas # Copyright 2024 Open Source Robotics Foundation, Inc. # Licensed under the Apache License, Version 2.0 from pathlib import Path from tempfile import TemporaryDirectory from colcon_cmake.package_identification.cmake \ import CmakePackageIdentification from colcon_core.package_descriptor import PackageDescriptor def test_identify(): extension = CmakePackageIdentification() with TemporaryDirectory(prefix='test_colcon_') as basepath: desc = PackageDescriptor(basepath) desc.type = 'other' assert extension.identify(desc) is None assert desc.name is None desc.type = None assert extension.identify(desc) is None assert desc.name is None assert desc.type is None basepath = Path(basepath) (basepath / 'CMakeLists.txt').write_text('') assert extension.identify(desc) is None assert desc.name == basepath.name assert desc.type == 'cmake' desc = PackageDescriptor(basepath) (basepath / 'CMakeLists.txt').write_text( 'cmake_minimum_required(VERSION 3.10)\n' 'project(Project NONE)\n') assert extension.identify(desc) is None assert desc.name == 'Project' assert desc.type == 'cmake' desc = PackageDescriptor(basepath) (basepath / 'CMakeLists.txt').write_text( 'cmake_minimum_required(VERSION 3.10)\n' 'project(Project NONE)\n' 'catkin_workspace()\n') assert extension.identify(desc) is None assert desc.name is None assert desc.type is None desc = PackageDescriptor(basepath) (basepath / 'CMakeLists.txt').write_text( 'cmake_minimum_required(VERSION 3.10)\n' 'project(pkg-name NONE)\n') assert extension.identify(desc) is None assert desc.name == 'pkg-name' assert desc.type == 'cmake' assert set(desc.dependencies.keys()) == {'build', 'run'} assert not desc.dependencies['build'] assert not desc.dependencies['run'] assert extension.identify(desc) is None assert desc.name == 'pkg-name' assert desc.type == 'cmake' desc = PackageDescriptor(basepath) (basepath / 'CMakeLists.txt').write_text( 'cmake_minimum_required(VERSION 3.10)\n' 'project(other-name NONE)\n' 'find_package(PkgConfig REQUIRED)\n' 'pkg_check_modules(DEP_NAME REQUIRED dep-name>=1.1)\n' 'add_subdirectory(src)\n') (basepath / 'src').mkdir(parents=True, exist_ok=True) (basepath / 'src' / 'CMakeLists.txt').write_text( 'find_package(dep-name2 REQUIRED)\n') (basepath / 'src' / 'README.txt').write_text( 'find_package(other-dep-name REQUIRED)\n') assert extension.identify(desc) is None assert desc.name == 'other-name' assert desc.type == 'cmake' assert set(desc.dependencies.keys()) == {'build', 'run'} assert desc.dependencies['build'] == { 'dep-name', 'dep-name2', 'PkgConfig', } assert desc.dependencies['run'] == { 'dep-name', 'dep-name2', 'PkgConfig', } colcon-cmake-0.2.29/test/test_parse_cmake_version.py000066400000000000000000000030461472070026500225540ustar00rootroot00000000000000# Copyright 2020 Kazys Stepanas # Licensed under the Apache License, Version 2.0 from colcon_cmake.task.cmake import _parse_cmake_version_string def test_parse_cmake_version(): # Build version prefix string closely matching what cmake version outputs. base_prefix = 'cmake version ' # Expected results list. Each element is a tuple containing the following: # - Version string to parse. # - Numeric version tuple to compare against (major, minor, patch). # The second item is None where the parse string should not parse. test_items = [ (base_prefix + '3.0.0', (3, 0, 0)), (base_prefix + '3.0.0-dirty', (3, 0, 0)), (base_prefix + '3.0.0-rc1', (3, 0, 0)), (base_prefix + 'cmake version 3.0.0-rc1-dirty', (3, 0, 0)), (base_prefix + 'this.is.garbage', None), (base_prefix + '3.15.1', (3, 15, 1)), ('3.15.1', (3, 15, 1)), (base_prefix + '101.202.303-xxx', (101, 202, 303)), ('101.202.303-xxx', (101, 202, 303)), ('prefix 1 number 101.202.303-xxx', (101, 202, 303)), ('not the right format', None) ] # Iterate the strings and parse. for version_string, expected_version in test_items: parsed_version = _parse_cmake_version_string(version_string) if expected_version is None: # Input string was garbage. Assert parsing failed. assert parsed_version is None else: assert parsed_version._version.release[0:3] == expected_version if __name__ == '__main__': test_parse_cmake_version() colcon-cmake-0.2.29/test/test_spell_check.py000066400000000000000000000036111472070026500210070ustar00rootroot00000000000000# Copyright 2016-2019 Dirk Thomas # Licensed under the Apache License, Version 2.0 from pathlib import Path import pytest spell_check_words_path = Path(__file__).parent / 'spell_check.words' @pytest.fixture(scope='module') def known_words(): global spell_check_words_path return spell_check_words_path.read_text().splitlines() @pytest.mark.linter def test_spell_check(known_words): from scspell import Report from scspell import SCSPELL_BUILTIN_DICT from scspell import spell_check source_filenames = [Path(__file__).parents[1] / 'setup.py'] + \ list( (Path(__file__).parents[1] / 'colcon_cmake') .glob('**/*.py')) + \ list((Path(__file__).parents[1] / 'test').glob('**/*.py')) for source_filename in sorted(source_filenames): print('Spell checking:', source_filename) # check all files report = Report(known_words) spell_check( [str(p) for p in source_filenames], base_dicts=[SCSPELL_BUILTIN_DICT], report_only=report, additional_extensions=[('', 'Python')]) unknown_word_count = len(report.unknown_words) assert unknown_word_count == 0, \ f'Found {unknown_word_count} unknown words: ' + \ ', '.join(sorted(report.unknown_words)) unused_known_words = set(known_words) - report.found_known_words unused_known_word_count = len(unused_known_words) assert unused_known_word_count == 0, \ f'{unused_known_word_count} words in the word list are not used: ' + \ ', '.join(sorted(unused_known_words)) @pytest.mark.linter def test_spell_check_word_list_order(known_words): assert known_words == sorted(known_words), \ 'The word list should be ordered alphabetically' @pytest.mark.linter def test_spell_check_word_list_duplicates(known_words): assert len(known_words) == len(set(known_words)), \ 'The word list should not contain duplicates' colcon-cmake-0.2.29/test/test_task_cmake_build.py000066400000000000000000000065671472070026500220310ustar00rootroot00000000000000# Copyright 2019 Rover Robotics # Copyright 2024 Open Source Robotics Foundation, Inc. # Licensed under the Apache License, Version 2.0 import asyncio import os from pathlib import Path import shutil from types import SimpleNamespace from colcon_cmake.task.cmake.build import CmakeBuildTask from colcon_core.event_handler.console_direct import ConsoleDirectEventHandler from colcon_core.package_descriptor import PackageDescriptor from colcon_core.subprocess import new_event_loop from colcon_core.task import TaskContext import pytest @pytest.fixture(autouse=True) def monkey_patch_put_event_into_queue(monkeypatch): event_handler = ConsoleDirectEventHandler() monkeypatch.setattr( TaskContext, 'put_event_into_queue', lambda self, event: event_handler((event, 'cmake')), ) def _test_build_package( tmp_path, *, cmake_args=None, cmake_clean_cache=False, cmake_clean_first=False, cmake_force_configure=None, cmake_target=None, cmake_target_skip_unavailable=None, ): event_loop = new_event_loop() asyncio.set_event_loop(event_loop) try: package = PackageDescriptor(tmp_path / 'src') package.name = 'test-package' package.type = 'cmake' context = TaskContext( pkg=package, args=SimpleNamespace( path=str(tmp_path / 'src'), build_base=str(tmp_path / 'build'), install_base=str(tmp_path / 'install'), cmake_args=cmake_args, cmake_clean_cache=cmake_clean_cache, cmake_clean_first=cmake_clean_first, cmake_force_configure=cmake_force_configure, cmake_target=cmake_target, cmake_target_skip_unavailable=cmake_target_skip_unavailable, ), dependencies={} ) task = CmakeBuildTask() task.set_context(context=context) package.path.mkdir(exist_ok=True) (package.path / 'CMakeLists.txt').write_text( 'cmake_minimum_required(VERSION 3.5)\n' 'project(test-package NONE)\n' 'file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out-file"\n' ' CONTENT "Hello, World!")\n' 'install(FILES "${CMAKE_CURRENT_BINARY_DIR}/out-file"\n' ' DESTINATION "share")\n' 'add_custom_target(custom-target\n' ' ${CMAKE_COMMAND} -E echo "Hello, World!")\n' ) src_base = Path(task.context.args.path) source_files_before = set(src_base.rglob('*')) rc = event_loop.run_until_complete(task.build()) assert not rc source_files_after = set(src_base.rglob('*')) assert source_files_before == source_files_after install_base = Path(task.context.args.install_base) assert (cmake_target is None) == \ (install_base / 'share' / 'out-file').is_file() finally: event_loop.close() @pytest.mark.parametrize( 'cmake_target', [None, 'custom-target']) @pytest.mark.skipif( not shutil.which('cmake'), reason='CMake must be installed to run this test') @pytest.mark.skipif( os.name == 'nt' and 'VisualStudioVersion' not in os.environ, reason='Must be run from a developer command prompt') def test_build_package(tmpdir, cmake_target): tmp_path = Path(tmpdir) _test_build_package(tmp_path, cmake_target=cmake_target)