Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation #1

Merged
merged 2 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion colcon_override_check/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022 Scott K Logan
# Licensed under the Apache License, Version 2.0

__version__ = '0.0.0'
__version__ = '0.0.1'
Empty file.
171 changes: 171 additions & 0 deletions colcon_override_check/package_selection/override_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright 2022 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0

import argparse
from pathlib import Path
import sys

from colcon_core.location import get_relative_package_index_path
from colcon_core.package_selection import logger
from colcon_core.package_selection import PackageSelectionExtensionPoint
from colcon_core.plugin_system import satisfies_version
from colcon_installed_package_information.package_augmentation \
import augment_packages
from colcon_installed_package_information.package_discovery \
import discover_packages
from colcon_installed_package_information.package_identification \
import get_package_identification_extensions


if sys.version_info < (3, 8):
# TODO(sloretz) remove when minimum supported Python version is 3.8
# https://stackoverflow.com/a/41153081
class _ExtendAction(argparse.Action):
"""Add argparse action to extend a list."""

def __call__(self, parser, namespace, values, option_string=None):
"""Extend the list with new arguments."""
items = getattr(namespace, self.dest) or []
items.extend(values)
setattr(namespace, self.dest, items)


class OverrideCheckPackageSelection(PackageSelectionExtensionPoint):
"""Check for potential problems when overriding installed packages."""

# Because this is effectively observing the selected packages, this
# extension should run after other package selection extensions
PRIORITY = 10

TARGET_VERBS = ('build',)

def __init__(self): # noqa: D107
super().__init__()
satisfies_version(
PackageSelectionExtensionPoint.EXTENSION_POINT_VERSION, '^1.0')

def add_arguments(self, *, parser): # noqa: D102
if sys.version_info < (3, 8):
# TODO(sloretz) remove when minimum supported Python version is 3.8
parser.register('action', 'extend', _ExtendAction)

parser.add_argument(
'--allow-overriding',
action='extend',
default=[],
metavar='PKG_NAME',
nargs='+',
help='Allow building packages that exist in underlay workspaces')

def check_parameters(self, args, pkg_names): # noqa: D102
for pkg_name in args.allow_overriding or []:
if pkg_name not in pkg_names:
logger.warning(
"ignoring unknown package '{pkg_name}' in "
'--allow-overriding'.format_map(locals()))

def select_packages(self, args, decorators): # noqa: D102
verb_name = getattr(args, 'verb_name', None)
if verb_name not in OverrideCheckPackageSelection.TARGET_VERBS:
return

# Enumerate the names of packages selected to be built.
selected_names = {d.descriptor.name for d in decorators if d.selected}

# Enumerate the underlay packages.
identification_extensions = get_package_identification_extensions()
underlay_descriptors = discover_packages(
args, identification_extensions)
augment_packages(underlay_descriptors)
if not underlay_descriptors:
return

# Enumerate workspace packages already built (if applicable).
building_or_built = set(selected_names)
if hasattr(args, 'install_base'):
install_base = Path(args.install_base).resolve()

# Drop any enumerated underlay packages in our install base.
# This could happen when a workspace install base is activated and
# then built again.
rebuilding = [
d for d in underlay_descriptors if d.path == install_base]
underlay_descriptors.difference_update(rebuilding)
if not underlay_descriptors:
return
building_or_built.update(d.name for d in rebuilding)

# Explicitly enumerate which workspace packages have already been
# built/installed in case the install base hasn't already been
# activated.
index_path = install_base / get_relative_package_index_path()
if index_path.is_dir():
building_or_built.update(p.name for p in index_path.iterdir())

# Enumerate dependencies from underlay packages to selected packages.
reverse_dependencies = {}
for d in underlay_descriptors:
# If an underlay package is being built or has already been built,
# its dependencies are irrelevant because it is already, or will
# be, overridden itself.
if d.name in building_or_built:
continue

# Consider only runtime dependencies from underlay packages to
# selected packages.
deps = {
d.name for d in d.get_dependencies(categories=('run',))
if d.name in selected_names}

for dep in deps:
reverse_dependencies.setdefault(dep, set())
reverse_dependencies[dep].add(d.name)

# Compute the list of paths where potentially overridden packages are
# installed to.
underlay_paths = {}
for d in underlay_descriptors:
underlay_paths.setdefault(d.name, [])
underlay_paths[d.name].append(str(d.path))

# Finally, determine what packages are overriding underlay packages
overriding_names = selected_names.intersection(underlay_paths)

# Of those, ignore specifically allowed overrides
overriding_names.difference_update(args.allow_overriding)

# Of those, Ignore overrides which are (effectively) leaf packages
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way leaf packages are calculated looks fine to me. IIUC leaf packages eliminates the concern about ABI/API incompatibilities in the overridden package, but it doesn't eliminate the include directory search order problem. ros2/ros2#1150 is working to mitigate that for a lot of packages, but colcon doesn't have a way to know if a package has mitigated it. Since it can also lead to undefined behavior at runtime, I would recommend keeping the warning even when overridden packages are leaf packages in the underlay.

Copy link
Member Author

@cottsay cottsay Mar 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned that we'd be introducing a warning for what could be a valid workflow without giving the developer any way to resolve it properly, forcing them to suppress it. So even if the underlying packages have mitigated the problem, they'd still have to suppress the warning. That doesn't seem like a very good user experience, and will probably cause developers to simply ignore or disable these warnings because they "can't be fixed."

I think we need to come up with a plan for how to detect or declare "safe" packages before we do that, but I'm open to other thoughts.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Its unfortunate that it means it won't warn in the example that motivated ros2/ros2#1150 https://github.com/jacobperron/overlay_bug_reproduction , but I suppose all the include directory stuff mitigates that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that it's actually bit us in the wild is a strong case for maintaining some kind of warning for leafy sub-trees like that. I'm having trouble weighing the utility of warning in that case against a boy-who-cried-wolf sort of mentality being established regarding this warning.

Maybe we should disable this part and over-warn until we can come up with a way to detect or declare safe packages? Do we actually have any plans to do that?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should disable this part and over-warn until we can come up with a way to detect or declare safe packages? Do we actually have any plans to do that?

I don't think we have any plans. Given the volume of feedback against the first implementation of the warning, I think this PR strikes a good balance.

overriding_names.intersection_update(
name for name, deps in reverse_dependencies.items()
if deps)

override_messages = {}
for overriding_package in overriding_names:
override_messages[overriding_package] = (
"'{overriding_package}' is in: ".format_map(locals()) +
', '.join(underlay_paths[overriding_package]))

if override_messages:
override_msg = (
'Some selected packages are already built in one or more'
' underlay workspaces:'
'\n\t' +
'\n\t'.join(override_messages.values()) +
'\nIf a package in a merged underlay workspace is overridden'
' and it installs headers, then all packages in the overlay'
' must sort their include directories by workspace order.'
' Failure to do so may result in build failures or undefined'
' behavior at run time.'
'\nIf the overridden package is used by another package'
' in any underlay, then the overriding package in the'
' overlay must be API and ABI compatible or undefined'
' behavior at run time may occur.'
'\n\nIf you understand the risks and want to override a'
' package anyways, add the following to the command'
' line:'
'\n\t--allow-overriding ' +
' '.join(sorted(override_messages.keys())))

logger.warn(
override_msg + '\n\nThis may be promoted to an error in a'
' future release of colcon-core.')
5 changes: 4 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ keywords = colcon

[options]
install_requires =
colcon-core
colcon-core>=0.7.1
colcon-installed-package-information
packages = find:
zip_safe = true

Expand Down Expand Up @@ -56,6 +57,8 @@ filterwarnings =
junit_suite_name = colcon-override-check

[options.entry_points]
colcon_core.package_selection =
override_check = colcon_override_check.package_selection.override_check:OverrideCheckPackageSelection

[flake8]
import-order-style = google
Expand Down
2 changes: 1 addition & 1 deletion stdeb.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[colcon-override-check]
No-Python2:
Depends3: python3-colcon-core
Depends3: python3-colcon-core (>= 0.7.1), python3-colcon-installed-package-information
Suite: bionic focal jammy stretch buster bullseye
X-Python3-Version: >= 3.5
10 changes: 10 additions & 0 deletions test/spell_check.words
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
apache
argparse
colcon
deps
https
iterdir
nargs
noqa
pathlib
plugin
pytest
scott
scspell
setuptools
sloretz
stackoverflow
thomas
todo
workspaces