Skip to content

Commit 3e33c49

Browse files
sloretzcottsay
andauthored
Warn when overriding packages at build time (#449)
* First prototype warning on build of overridden packages Signed-off-by: Shane Loretz <[email protected]> * Add FindInstalledPackagesExtensionPoint and improve override warning text Signed-off-by: Shane Loretz <[email protected]> * its -> their Signed-off-by: Shane Loretz <[email protected]> * Fix CI Signed-off-by: Shane Loretz <[email protected]> * Update docstring of find_installed_packages Signed-off-by: Shane Loretz <[email protected]> * Make unit tests aware of FindInstalledPackagesExtensionPoint Signed-off-by: Shane Loretz <[email protected]> * Fix import order Signed-off-by: Shane Loretz <[email protected]> * Test raises NotImplementedError Signed-off-by: Shane Loretz <[email protected]> * Add extend action for older python versions Signed-off-by: Shane Loretz <[email protected]> * Flake8 tuple whitespace Signed-off-by: Shane Loretz <[email protected]> * Spellcheck extra words Signed-off-by: Shane Loretz <[email protected]> * Make override warning clearer Signed-off-by: Shane Loretz <[email protected]> * Grammar in path arg documentation Signed-off-by: Shane Loretz <[email protected]> * Check packages is not None Signed-off-by: Shane Loretz <[email protected]> * Test warning when extension reports package that doesn't exist Signed-off-by: Shane Loretz <[email protected]> * Test two extensions finding same package in same install base Signed-off-by: Shane Loretz <[email protected]> * Fix expected string on windows Signed-off-by: Shane Loretz <[email protected]> Co-authored-by: Scott K Logan <[email protected]>
1 parent 029e2e7 commit 3e33c49

File tree

6 files changed

+326
-74
lines changed

6 files changed

+326
-74
lines changed

Diff for: colcon_core/shell/__init__.py

+78-32
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import warnings
1313

1414
from colcon_core.environment_variable import EnvironmentVariable
15-
from colcon_core.location import get_relative_package_index_path
1615
from colcon_core.logging import colcon_logger
1716
from colcon_core.plugin_system import instantiate_extensions
1817
from colcon_core.plugin_system import order_extensions_grouped_by_priority
@@ -563,46 +562,93 @@ def find_installed_packages_in_environment():
563562
return packages
564563

565564

566-
def find_installed_packages(install_base):
565+
class FindInstalledPackagesExtensionPoint:
566+
"""
567+
The interface for extensions to find installed packages.
568+
569+
This type of extension locates installed packages inside a prefix path.
570+
"""
571+
572+
"""The version of this extension interface."""
573+
EXTENSION_POINT_VERSION = '1.0'
574+
575+
"""The default priority of an extension."""
576+
PRIORITY = 100
577+
578+
def find_installed_packages(self, install_base: Path):
579+
"""
580+
Find installed packages in an install path.
581+
582+
This method must be overridden in a subclass.
583+
584+
:param Path prefix_path: The path of the install prefix
585+
:returns: The mapping from a package name to the prefix path, or None
586+
if the path is not an install layout supported by this extension.
587+
:rtype: Dict or None
588+
"""
589+
raise NotImplementedError()
590+
591+
592+
def get_find_installed_packages_extensions():
593+
"""
594+
Get the available package identification extensions.
595+
596+
The extensions are grouped by their priority and each group is ordered by
597+
the entry point name.
598+
599+
:rtype: OrderedDict
600+
"""
601+
extensions = instantiate_extensions(__name__ + '.find_installed_packages')
602+
for name, extension in extensions.items():
603+
extension.PACKAGE_IDENTIFICATION_NAME = name
604+
return order_extensions_grouped_by_priority(extensions)
605+
606+
607+
def find_installed_packages(install_base: Path):
567608
"""
568609
Find install packages under the install base path.
569610
570-
The path must contain a marker file with the install layout.
571-
Based on the install layout the packages are discovered i different
611+
Based on the install layout the packages may be discovered in different
572612
locations.
573613
574614
:param Path install_base: The base path to find installed packages in
575615
:returns: The mapping from a package name to the prefix path, None if the
576-
path doesn't exist or doesn't contain a marker file with a valid install
577-
layout
578-
:rtype: OrderedDict or None
616+
path is not a supported install layout or it doesn't exist
617+
:rtype: Dict or None
579618
"""
580-
marker_file = install_base / '.colcon_install_layout'
581-
if not marker_file.is_file():
582-
return None
583-
install_layout = marker_file.read_text().rstrip()
584-
if install_layout not in ('isolated', 'merged'):
585-
return None
619+
# priority means getting invoked first, but maybe that doesn't matter
620+
extensions = []
621+
prioritized_extensions = get_find_installed_packages_extensions()
622+
for ext_list in prioritized_extensions.values():
623+
extensions.extend(ext_list.values())
586624

625+
# Combine packages found by all extensions
587626
packages = {}
588-
if install_layout == 'isolated':
589-
# for each subdirectory look for the package specific file
590-
for p in install_base.iterdir():
591-
if not p.is_dir():
592-
continue
593-
if p.name.startswith('.'):
627+
valid_prefix = False
628+
629+
for ext in extensions:
630+
ext_packages = ext.find_installed_packages(install_base)
631+
if ext_packages is None:
632+
continue
633+
634+
valid_prefix = True
635+
for pkg, path in ext_packages.items():
636+
if not path.exists():
637+
logger.warning(
638+
"Ignoring '{pkg}' found at '{path}' because the path"
639+
' does not exist.'.format_map(locals()))
594640
continue
595-
marker = p / get_relative_package_index_path() / p.name
596-
if marker.is_file():
597-
packages[p.name] = p
598-
else:
599-
# find all files in the subdirectory
600-
if (install_base / get_relative_package_index_path()).is_dir():
601-
package_index = install_base / get_relative_package_index_path()
602-
for p in package_index.iterdir():
603-
if not p.is_file():
604-
continue
605-
if p.name.startswith('.'):
606-
continue
607-
packages[p.name] = install_base
641+
if pkg in packages and not path.samefile(packages[pkg]):
642+
# Same package found at different paths in the same prefix
643+
first_path = packages[pkg]
644+
logger.warning(
645+
"The package '{pkg}' previously found at "
646+
"'{first_path}' was found again at '{path}'."
647+
" Ignoring '{path}'".format_map(locals()))
648+
else:
649+
packages[pkg] = path
650+
651+
if not valid_prefix:
652+
# No extension said this was a valid prefix
653+
return None
608654
return packages

Diff for: colcon_core/shell/installed_packages.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2016-2021 Dirk Thomas
2+
# Licensed under the Apache License, Version 2.0
3+
4+
from pathlib import Path
5+
6+
from colcon_core.location import get_relative_package_index_path
7+
from colcon_core.shell import FindInstalledPackagesExtensionPoint
8+
9+
10+
class IsolatedInstalledPackageFinder(FindInstalledPackagesExtensionPoint):
11+
"""Find installed packages in colcon isolated install spaces."""
12+
13+
def find_installed_packages(self, install_base: Path):
14+
"""Find installed packages in colcon isolated install spaces."""
15+
marker_file = install_base / '.colcon_install_layout'
16+
if not marker_file.is_file():
17+
return None
18+
install_layout = marker_file.read_text().rstrip()
19+
if install_layout != 'isolated':
20+
return None
21+
22+
packages = {}
23+
# for each subdirectory look for the package specific file
24+
for p in install_base.iterdir():
25+
if not p.is_dir():
26+
continue
27+
if p.name.startswith('.'):
28+
continue
29+
marker = p / get_relative_package_index_path() / p.name
30+
if marker.is_file():
31+
packages[p.name] = p
32+
return packages
33+
34+
35+
class MergedInstalledPackageFinder(FindInstalledPackagesExtensionPoint):
36+
"""Find installed packages in colcon merged install spaces."""
37+
38+
def find_installed_packages(self, install_base: Path):
39+
"""Find installed packages in colcon isolated install spaces."""
40+
marker_file = install_base / '.colcon_install_layout'
41+
if not marker_file.is_file():
42+
return None
43+
install_layout = marker_file.read_text().rstrip()
44+
if install_layout != 'merged':
45+
return None
46+
47+
packages = {}
48+
# find all files in the subdirectory
49+
if (install_base / get_relative_package_index_path()).is_dir():
50+
package_index = install_base / get_relative_package_index_path()
51+
for p in package_index.iterdir():
52+
if not p.is_file():
53+
continue
54+
if p.name.startswith('.'):
55+
continue
56+
packages[p.name] = install_base
57+
return packages

Diff for: colcon_core/verb/build.py

+67
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# Copyright 2016-2018 Dirk Thomas
22
# Licensed under the Apache License, Version 2.0
33

4+
import argparse
45
from collections import OrderedDict
56
import os
67
import os.path
78
from pathlib import Path
9+
import sys
810
import traceback
911

1012
from colcon_core.argument_parser.destination_collector \
@@ -20,6 +22,8 @@
2022
as add_packages_arguments
2123
from colcon_core.package_selection import get_packages
2224
from colcon_core.plugin_system import satisfies_version
25+
from colcon_core.prefix_path import get_chained_prefix_path
26+
from colcon_core.shell import find_installed_packages
2327
from colcon_core.shell import get_shell_extensions
2428
from colcon_core.task import add_task_arguments
2529
from colcon_core.task import get_task_extension
@@ -31,6 +35,19 @@
3135
from colcon_core.verb import VerbExtensionPoint
3236

3337

38+
if sys.version_info < (3, 8):
39+
# TODO(sloretz) remove when minimum supported Python version is 3.8
40+
# https://stackoverflow.com/a/41153081
41+
class _ExtendAction(argparse.Action):
42+
"""Add argparse action to extend a list."""
43+
44+
def __call__(self, parser, namespace, values, option_string=None):
45+
"""Extend the list with new arguments."""
46+
items = getattr(namespace, self.dest) or []
47+
items.extend(values)
48+
setattr(namespace, self.dest, items)
49+
50+
3451
class BuildPackageArguments:
3552
"""Arguments to build a specific package."""
3653

@@ -81,6 +98,9 @@ def __init__(self): # noqa: D107
8198
satisfies_version(VerbExtensionPoint.EXTENSION_POINT_VERSION, '^1.0')
8299

83100
def add_arguments(self, *, parser): # noqa: D102
101+
if sys.version_info < (3, 8):
102+
# TODO(sloretz) remove when minimum supported Python version is 3.8
103+
parser.register('action', 'extend', _ExtendAction)
84104
parser.add_argument(
85105
'--build-base',
86106
default='build',
@@ -106,6 +126,13 @@ def add_arguments(self, *, parser): # noqa: D102
106126
help='Continue other packages when a package fails to build '
107127
'(packages recursively depending on the failed package are '
108128
'skipped)')
129+
parser.add_argument(
130+
'--allow-overriding',
131+
action='extend',
132+
default=[],
133+
metavar='PKG_NAME',
134+
nargs='+',
135+
help='Allow building packages that exist in underlay workspaces')
109136
add_executor_arguments(parser)
110137
add_event_handler_arguments(parser)
111138

@@ -133,6 +160,46 @@ def main(self, *, context): # noqa: D102
133160
jobs, unselected_packages = self._get_jobs(
134161
context.args, decorators, install_base)
135162

163+
underlay_packages = {}
164+
for prefix_path in get_chained_prefix_path():
165+
packages = find_installed_packages(Path(prefix_path))
166+
if packages:
167+
for pkg, path in packages.items():
168+
if pkg not in underlay_packages:
169+
underlay_packages[pkg] = []
170+
underlay_packages[pkg].append(str(path))
171+
172+
override_messages = {}
173+
for overlay_package in jobs.keys():
174+
if overlay_package in underlay_packages:
175+
if overlay_package not in context.args.allow_overriding:
176+
override_messages[overlay_package] = (
177+
"'{overlay_package}'".format_map(locals()) +
178+
' is in: ' +
179+
', '.join(underlay_packages[overlay_package]))
180+
181+
if override_messages:
182+
override_msg = (
183+
'Some selected packages are already built in one or more'
184+
' underlay workspaces:'
185+
'\n\t' +
186+
'\n\t'.join(override_messages.values()) +
187+
'\nIf a package in a merged underlay workspace is overridden'
188+
' and it installs headers, then all packages in the overlay'
189+
' must sort their include directories by workspace order.'
190+
' Failure to do so may result in build failures or undefined'
191+
' behavior at run time.'
192+
'\nIf the overridden package is used by another package'
193+
' in any underlay, then the overriding package in the'
194+
' overlay must be API and ABI compatible or undefined'
195+
' behavior at run time may occur.'
196+
'\n\nIf you understand the risks and want to override a'
197+
' package anyways, add the following to the command'
198+
' line:'
199+
'\n\t--allow-overriding ' +
200+
' '.join(sorted(override_messages.keys())))
201+
raise RuntimeError(override_msg)
202+
136203
on_error = OnError.interrupt \
137204
if not context.args.continue_on_error else OnError.skip_downstream
138205

Diff for: setup.cfg

+4
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ colcon_core.extension_point =
106106
colcon_core.prefix_path = colcon_core.prefix_path:PrefixPathExtensionPoint
107107
colcon_core.python_testing = colcon_core.task.python.test:PythonTestingStepExtensionPoint
108108
colcon_core.shell = colcon_core.shell:ShellExtensionPoint
109+
colcon_core.shell.find_installed_packages = colcon_core.shell:FindInstalledPackagesExtensionPoint
109110
colcon_core.task.build = colcon_core.task:TaskExtensionPoint
110111
colcon_core.task.test = colcon_core.task:TaskExtensionPoint
111112
colcon_core.verb = colcon_core.verb:VerbExtensionPoint
@@ -126,6 +127,9 @@ colcon_core.shell =
126127
bat = colcon_core.shell.bat:BatShell
127128
dsv = colcon_core.shell.dsv:DsvShell
128129
sh = colcon_core.shell.sh:ShShell
130+
colcon_core.shell.find_installed_packages =
131+
colcon_isolated = colcon_core.shell.installed_packages:IsolatedInstalledPackageFinder
132+
colcon_merged = colcon_core.shell.installed_packages:MergedInstalledPackageFinder
129133
colcon_core.task.build =
130134
python = colcon_core.task.python.build:PythonBuildTask
131135
colcon_core.task.test =

Diff for: test/spell_check.words

+3
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ setupscript
9595
setuptools
9696
shlex
9797
sigint
98+
sloretz
9899
stacklevel
100+
stackoverflow
99101
staticmethod
100102
stdeb
101103
stringify
@@ -122,3 +124,4 @@ unlinking
122124
unrenamed
123125
wildcards
124126
workaround
127+
workspaces

0 commit comments

Comments
 (0)