diff --git a/README.md b/README.md index 3b8f5cd..9f2922c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,23 @@ The tool generates `conda` rattler-build recipes to capture all the selected ROS The repo contains a `vinca` tool that reads a `vinca.yaml` file that contains all its metadata. -For an up-to-date example of how to write a `vinca.yaml`, check the repos of the mantained RoboStack distros: +For an up-to-date example of how to write a `vinca.yaml`, check the repos of the maintained RoboStack distros: * https://github.com/RoboStack/ros-noetic/ * https://github.com/RoboStack/ros-humble * https://github.com/RoboStack/ros-jazzy/ + +## Package naming + +The optional `package_name_mode` setting controls the transition from legacy distro-qualified names such as `ros-humble-rclcpp` to ROS-major-version names such as `ros2-rclcpp`: + +* `legacy` (default): generate only distro-qualified package names. +* `both`: generate the new names and compatibility packages under the legacy names. Each compatibility package depends on the corresponding new package at the same version. +* `new`: generate only the new names. + +Existing configurations remain on `legacy` when this setting is omitted. To start migrating a distribution to the new names, use: + +```yaml +package_name_mode: both +``` + +Once users and downstream projects have migrated, switch to `new` to stop generating the compatibility packages. New ROS 1 package names use the `ros-` prefix; new ROS 2 package names use `ros2-`. diff --git a/vinca/distro.py b/vinca/distro.py index 76020bc..85e46c7 100644 --- a/vinca/distro.py +++ b/vinca/distro.py @@ -181,6 +181,18 @@ def get_release_package_xml(self, pkg_name): def check_ros1(self): return self._distribution_type == "ros1" + def get_ros_version(self): + """Get ROS version number (1 or 2)""" + return "1" if self.check_ros1() else "2" + + def get_package_prefix(self): + """Get the package name prefix (ros for ROS1, ros2 for ROS2).""" + return "ros" if self.check_ros1() else "ros2" + + def get_legacy_package_prefix(self): + """Get the legacy distro-qualified package name prefix.""" + return f"ros-{self.name}" + def get_python_version(self): return self._python_version diff --git a/vinca/generate_azure.py b/vinca/generate_azure.py index 27853ae..31f32ab 100644 --- a/vinca/generate_azure.py +++ b/vinca/generate_azure.py @@ -10,7 +10,7 @@ from rich import print -from vinca.utils import get_repodata +from vinca.utils import extract_dependency_names, get_repodata from vinca.utils import literal_unicode as lu from vinca.distro import Distro from vinca.main import ( @@ -452,11 +452,9 @@ def main(): "host", [] ) + pkg["requirements"].get("run", []) - # sort out requirements that are not built in this run + # Normalize direct and conditional requirements to package names. for pkg_name, reqs in requirements.items(): - requirements[pkg_name] = [ - r.split()[0] for r in reqs if (isinstance(r, str) and r in reqs) - ] + requirements[pkg_name] = extract_dependency_names(reqs) G = nx.DiGraph() for pkg, reqs in requirements.items(): diff --git a/vinca/generate_gha.py b/vinca/generate_gha.py index b251425..a97a70a 100644 --- a/vinca/generate_gha.py +++ b/vinca/generate_gha.py @@ -10,7 +10,7 @@ from rich import print -from vinca.utils import get_repodata, NoAliasDumper +from vinca.utils import extract_dependency_names, get_repodata, NoAliasDumper from vinca.utils import literal_unicode as lu from vinca.distro import Distro from vinca.main import ( @@ -500,15 +500,9 @@ def main(): "host", [] ) + req_section.get("run", []) - # sort out requirements that are not built in this run + # Normalize direct and conditional requirements to package names. for pkg_name, reqs in requirements.items(): - requirements[pkg_name] = [ - r.split()[0] for r in reqs if (isinstance(r, str) and r in reqs) - ] - if platform == "emscripten-wasm32": - # Hot fix to add the only ros package inside a if else statement - if "ros-humble-rmw-wasm-cpp" in str(reqs): - requirements[pkg_name].append("ros-humble-rmw-wasm-cpp") + requirements[pkg_name] = extract_dependency_names(reqs) G = nx.DiGraph() for pkg, reqs in requirements.items(): diff --git a/vinca/generate_gitlab.py b/vinca/generate_gitlab.py index 80fadaa..11415e4 100644 --- a/vinca/generate_gitlab.py +++ b/vinca/generate_gitlab.py @@ -4,6 +4,8 @@ import sys import os +from vinca.utils import extract_dependency_names + try: from yaml import CLoader as Loader, CDumper as Dumper except ImportError: @@ -41,8 +43,9 @@ def main(): requirements = {} for pkg in metas: - requirements[pkg["package"]["name"]] = ( - pkg["requirements"]["host"] + pkg["requirements"]["run"] + requirement_section = pkg.get("requirements", {}) + requirements[pkg["package"]["name"]] = extract_dependency_names( + requirement_section.get("host", []) + requirement_section.get("run", []) ) print(requirements) @@ -50,9 +53,9 @@ def main(): G = nx.DiGraph() for pkg, reqs in requirements.items(): G.add_node(pkg) - for r in reqs: - if r.startswith("ros-"): - G.add_edge(pkg, r) + for requirement in reqs: + if requirement.startswith(("ros-", "ros2-")): + G.add_edge(pkg, requirement) # import matplotlib.pyplot as plt # nx.draw(G, with_labels=True, font_weight='bold') @@ -60,7 +63,6 @@ def main(): tg = list(reversed(list(nx.topological_sort(G)))) print(tg) - print(requirements["ros-melodic-ros-core"]) stages = [] current_stage = [] diff --git a/vinca/main.py b/vinca/main.py index eb5b148..8357850 100644 --- a/vinca/main.py +++ b/vinca/main.py @@ -14,10 +14,19 @@ from .resolve import resolve_pkgname from .template import write_recipe, write_recipe_package from .distro import Distro +from .naming import ( + generate_legacy_compatibility_output, + get_package_name, + get_package_name_mode, + get_package_prefix, + is_legacy_compatibility_output, + normalize_package_dependency, +) from .v1_selectors import evaluate_selectors from vinca import config from vinca.utils import ( + add_package_name_variants, get_repodata, get_pkg_build_number, get_pkg_additional_info, @@ -140,7 +149,7 @@ def parse_command_line(argv): return arguments -def get_depmods(vinca_conf, pkg_name): +def get_depmods(vinca_conf, pkg_name, distro): depmods = vinca_conf["depmods"].get(pkg_name, {}) rm_deps, add_deps = ( {"build": [], "host": [], "run": []}, @@ -148,19 +157,16 @@ def get_depmods(vinca_conf, pkg_name): ) for dep_type in ["build", "host", "run"]: - if depmods.get("remove_" + dep_type): - for el in depmods["remove_" + dep_type]: - if isinstance(el, dict): - rm_deps[dep_type].append(dict(el)) - else: - rm_deps[dep_type].append(el) + for dependency in depmods.get("remove_" + dep_type, []): + rm_deps[dep_type].append( + normalize_package_dependency(dependency, distro, vinca_conf) + ) + + for dependency in depmods.get("add_" + dep_type, []): + add_deps[dep_type].append( + normalize_package_dependency(dependency, distro, vinca_conf) + ) - if depmods.get("add_" + dep_type): - for el in depmods["add_" + dep_type]: - if isinstance(el, dict): - add_deps[dep_type].append(dict(el)) - else: - add_deps[dep_type].append(el) return rm_deps, add_deps @@ -169,6 +175,7 @@ def read_vinca_yaml(filepath): vinca_conf = evaluate_selectors( yaml.load(open(filepath, "r")), target_platform=get_conda_subdir() ) + vinca_conf["package_name_mode"] = get_package_name_mode(vinca_conf).value # normalize paths to absolute paths conda_index = [] @@ -204,16 +211,20 @@ def read_vinca_yaml(filepath): patches[splitted[0]]["any"].append(x) + add_package_name_variants(patches, vinca_conf["ros_distro"]) vinca_conf["_patches"] = patches tests = {} test_dir = Path(filepath).parent / "tests" - # Store the test directory directly for use in template.py - vinca_conf["_test_dir"] = test_dir for x in test_dir.glob("*.yaml"): tests[os.path.basename(x).split(".")[0]] = x + add_package_name_variants(tests, vinca_conf["ros_distro"]) vinca_conf["_tests"] = tests + test_folders = {path.name: path for path in test_dir.glob("*") if path.is_dir()} + add_package_name_variants(test_folders, vinca_conf["ros_distro"]) + vinca_conf["_test_folders"] = test_folders + if (patch_dir / "dependencies.yaml").exists(): vinca_conf["depmods"] = evaluate_selectors( yaml.load(open(patch_dir / "dependencies.yaml")), @@ -268,6 +279,34 @@ def read_snapshot(vinca_conf): return snapshot, additional +def should_skip_output(output, vinca_conf): + """Return whether an individual output is already available.""" + name = output["package"]["name"] + version = output["package"]["version"] + skip_built_packages = vinca_conf.get("skip_built_packages", []) + if vinca_conf.get("trigger_new_versions"): + return (name, version) in skip_built_packages + return name in skip_built_packages + + +def append_output_with_compatibility( + outputs, output, pkg_shortname, distro, vinca_conf +): + """Append canonical and compatibility outputs, filtering each independently.""" + candidate_outputs = [output] + compatibility_output = generate_legacy_compatibility_output( + output, pkg_shortname, distro, vinca_conf + ) + if compatibility_output is not None: + candidate_outputs.append(compatibility_output) + + for candidate in candidate_outputs: + if should_skip_output(candidate, vinca_conf): + print(f"Skipping {candidate['package']['name']} (already built)") + else: + outputs.append(candidate) + + def generate_output(pkg_shortname, vinca_conf, distro, version, all_pkgs=None): if not all_pkgs: all_pkgs = [] @@ -279,12 +318,6 @@ def generate_output(pkg_shortname, vinca_conf, distro, version, all_pkgs=None): if not pkg_names: return None - if vinca_conf.get("trigger_new_versions"): - if (pkg_names[0], version) in vinca_conf["skip_built_packages"]: - return None - else: - if pkg_names[0] in vinca_conf["skip_built_packages"]: - return None # handle dummy recipe generation for vendored packages output = {} if is_dummy_metapackage(pkg_shortname, vinca_conf): @@ -424,13 +457,12 @@ def generate_output(pkg_shortname, vinca_conf, distro, version, all_pkgs=None): "ros_workspace", "ros_environment", ]: - output["requirements"]["host"].append( - f"ros-{config.ros_distro}-ros-environment" - ) - output["requirements"]["host"].append(f"ros-{config.ros_distro}-ros-workspace") - output["requirements"]["run"].append(f"ros-{config.ros_distro}-ros-workspace") + package_prefix = get_package_prefix(distro, vinca_conf) + output["requirements"]["host"].append(f"{package_prefix}-ros-environment") + output["requirements"]["host"].append(f"{package_prefix}-ros-workspace") + output["requirements"]["run"].append(f"{package_prefix}-ros-workspace") - rm_deps, add_deps = get_depmods(vinca_conf, pkg.name) + rm_deps, add_deps = get_depmods(vinca_conf, pkg.name, distro) gdeps = [] if pkg.group_depends: for gdep in pkg.group_depends: @@ -469,7 +501,7 @@ def generate_output(pkg_shortname, vinca_conf, distro, version, all_pkgs=None): output["requirements"]["build"].append( { "if": "build_platform != target_platform", - "then": [f"ros-{config.ros_distro}-cyclonedds"], + "then": [f"{get_package_prefix(distro, vinca_conf)}-cyclonedds"], } ) @@ -519,39 +551,40 @@ def sortkey(k): if "cmake" not in output["requirements"]["build"]: output["requirements"]["build"].append("cmake") - if f"ros-{config.ros_distro}-mimick-vendor" in output["requirements"]["build"]: - output["requirements"]["build"].remove(f"ros-{config.ros_distro}-mimick-vendor") + package_prefix = get_package_prefix(distro, vinca_conf) + mimick_vendor_name = f"{package_prefix}-mimick-vendor" + if mimick_vendor_name in output["requirements"]["build"]: + output["requirements"]["build"].remove(mimick_vendor_name) output["requirements"]["build"].append( { "if": "target_platform != 'emscripten-wasm32'", - "then": [f"ros-{config.ros_distro}-mimick-vendor"], + "then": [mimick_vendor_name], } ) - if f"ros-{config.ros_distro}-mimick-vendor" in output["requirements"]["host"]: - output["requirements"]["host"].remove(f"ros-{config.ros_distro}-mimick-vendor") + if mimick_vendor_name in output["requirements"]["host"]: + output["requirements"]["host"].remove(mimick_vendor_name) output["requirements"]["build"].append( { "if": "target_platform != 'emscripten-wasm32'", - "then": [f"ros-{config.ros_distro}-mimick-vendor"], + "then": [mimick_vendor_name], } ) - if ( - f"ros-{config.ros_distro}-rosidl-default-generators" - in output["requirements"]["host"] - ): + rosidl_generators_name = f"{package_prefix}-rosidl-default-generators" + if rosidl_generators_name in output["requirements"]["host"]: output["requirements"]["build"].append( { "if": "target_platform == 'emscripten-wasm32'", - "then": [f"ros-{config.ros_distro}-rosidl-default-generators"], + "then": [rosidl_generators_name], } ) output["requirements"]["run"] = sorted(output["requirements"]["run"], key=sortkey) output["requirements"]["host"] = sorted(output["requirements"]["host"], key=sortkey) - if f"ros-{config.ros_distro}-pybind11-vendor" in output["requirements"]["host"]: + pybind11_vendor_name = f"{package_prefix}-pybind11-vendor" + if pybind11_vendor_name in output["requirements"]["host"]: output["requirements"]["host"] += ["pybind11"] if "pybind11" in output["requirements"]["host"]: output["requirements"]["build"] += [ @@ -637,7 +670,9 @@ def get_pkg(pkg_name): except AttributeError: print("Skip " + pkg_shortname + " due to invalid version / XML.") if output is not None: - outputs.append(output) + append_output_with_compatibility( + outputs, output, pkg_shortname, distro, vinca_conf + ) # Generate mutex package if configured as dictionary mutex_recipe = generate_mutex_package_recipe(vinca_conf, distro) @@ -665,7 +700,9 @@ def generate_outputs_version(distro, vinca_conf): version = distro.get_version(pkg_shortname) output = generate_output(pkg_shortname, vinca_conf, distro, version) if output is not None: - outputs.append(output) + append_output_with_compatibility( + outputs, output, pkg_shortname, distro, vinca_conf + ) return outputs @@ -1005,7 +1042,7 @@ def generate_mutex_package_recipe(vinca_conf, distro): def parse_package(pkg, distro, vinca_conf, path): name = pkg["name"].replace("_", "-") - final_name = f"ros-{distro.name}-{name}" + final_name = get_package_name(name, distro, vinca_conf) recipe = { "package": {"name": final_name, "version": pkg["version"]}, @@ -1130,7 +1167,8 @@ def print_generation_summary(distro, vinca_conf, outputs): required_by = provenance.get("required_by", {}) # names that actually produced a recipe in this run - generated_names = {o["package"]["name"] for o in outputs} + generated_outputs = {output["package"]["name"]: output for output in outputs} + generated_names = set(generated_outputs) matched_names = set() rows = [] @@ -1173,7 +1211,11 @@ def print_generation_summary(distro, vinca_conf, outputs): depended_on or "[dim]-[/dim]", ) for name in leftovers: - table.add_row(name, "[dim]no[/dim]", "[dim]auxiliary (e.g. mutex)[/dim]") + if is_legacy_compatibility_output(generated_outputs[name], distro, vinca_conf): + reason = "[dim]legacy compatibility package[/dim]" + else: + reason = "[dim]auxiliary (e.g. mutex)[/dim]" + table.add_row(name, "[dim]no[/dim]", reason) console = Console() console.print(table) @@ -1221,18 +1263,26 @@ def main(): for f in pkg_files: pkg = catkin_pkg.package.parse_package(f) recipe = parse_package(pkg, distro, vinca_conf, f) + recipes = [recipe] + compatibility_recipe = generate_legacy_compatibility_output( + recipe, pkg.name, distro, vinca_conf + ) + if compatibility_recipe is not None: + recipes.append(compatibility_recipe) if arguments.multiple_file: - write_recipe_package(recipe) + for generated_recipe in recipes: + write_recipe_package(generated_recipe) else: - outputs.append(recipe) + outputs.extend(recipes) if not arguments.multiple_file: sources = {} - for o in outputs: - sources[o["package"]["name"]] = o["source"] - del o["source"] - write_recipe(sources, outputs, vinca_conf) + for output in outputs: + source = output.pop("source", None) + if source is not None: + sources[output["package"]["name"]] = source + write_recipe(sources, outputs, vinca_conf, distro) else: if arguments.skip_already_built_repodata or vinca_conf.get("skip_existing"): @@ -1252,6 +1302,10 @@ def main(): additional_recipe_names.add(add_rec_y["package"]["name"]) else: if add_rec_y["package"]["name"] not in [ + "ros2-rmw-wasm-cpp", + "ros2-wasm-cpp", + "ros2-dynmsg", + "ros2-test-wasm", "ros-humble-rmw-wasm-cpp", "ros-humble-wasm-cpp", "ros-humble-dynmsg", @@ -1319,9 +1373,9 @@ def main(): outputs = generate_outputs(distro, vinca_conf) if arguments.multiple_file: - write_recipe(source, outputs, vinca_conf, False) + write_recipe(source, outputs, vinca_conf, distro, False) else: - write_recipe(source, outputs, vinca_conf) + write_recipe(source, outputs, vinca_conf, distro) print_generation_summary(distro, vinca_conf, outputs) diff --git a/vinca/migrate.py b/vinca/migrate.py index f3a2063..4f7635e 100644 --- a/vinca/migrate.py +++ b/vinca/migrate.py @@ -7,6 +7,7 @@ import subprocess import shutil import ruamel.yaml +from .naming import PackageNameMode, get_package_name_mode, get_package_prefix from .utils import get_repodata from vinca import config from vinca.distro import Distro @@ -44,7 +45,10 @@ def create_migration_instructions(arch, packages_to_migrate, trigger_branch): global distro_version, ros_prefix distro_version = vinca_conf["ros_distro"] - ros_prefix = f"ros-{distro_version}" + distro = Distro(distro_version) + ros_prefix = get_package_prefix(distro, vinca_conf) + legacy_prefix = distro.get_legacy_package_prefix() + package_name_mode = get_package_name_mode(vinca_conf) repodata = get_repodata(url, arch) @@ -52,7 +56,11 @@ def create_migration_instructions(arch, packages_to_migrate, trigger_branch): to_migrate = set() ros_pkgs = set() for pkey in packages: - if not pkey.startswith(ros_prefix): + if not pkey.startswith(f"{ros_prefix}-"): + continue + if package_name_mode is not PackageNameMode.LEGACY and pkey.startswith( + f"{legacy_prefix}-" + ): continue pname = pkey.rsplit("-", 2)[0] @@ -104,7 +112,6 @@ def create_migration_instructions(arch, packages_to_migrate, trigger_branch): print("Sorted to migrate: ", to_migrate) - distro = Distro(distro_version) # import IPython; IPython.embed() ros_names = [] diff --git a/vinca/naming.py b/vinca/naming.py new file mode 100644 index 0000000..39f6ea2 --- /dev/null +++ b/vinca/naming.py @@ -0,0 +1,111 @@ +"""Helpers for selecting and generating ROS package names.""" + +from enum import Enum + + +class PackageNameMode(str, Enum): + """Supported ROS package naming transition modes.""" + + LEGACY = "legacy" + BOTH = "both" + NEW = "new" + + +def get_package_name_mode(vinca_conf): + """Return the configured package name mode, defaulting to legacy names.""" + value = vinca_conf.get("package_name_mode", PackageNameMode.LEGACY.value) + try: + return PackageNameMode(value) + except ValueError as error: + choices = ", ".join(mode.value for mode in PackageNameMode) + raise ValueError( + f"Invalid package_name_mode {value!r}; expected one of: {choices}" + ) from error + + +def get_package_prefix(distro, vinca_conf): + """Return the prefix used for generated packages and their dependencies.""" + if get_package_name_mode(vinca_conf) is PackageNameMode.LEGACY: + return distro.get_legacy_package_prefix() + return distro.get_package_prefix() + + +def get_new_package_name(pkg_shortname, distro): + """Return the ROS-major-version package name.""" + return f"{distro.get_package_prefix()}-{pkg_shortname.replace('_', '-')}" + + +def get_legacy_package_name(pkg_shortname, distro): + """Return the distro-qualified package name.""" + return f"{distro.get_legacy_package_prefix()}-{pkg_shortname.replace('_', '-')}" + + +def get_package_name(pkg_shortname, distro, vinca_conf): + """Return the primary generated name for a ROS package.""" + if get_package_name_mode(vinca_conf) is PackageNameMode.LEGACY: + return get_legacy_package_name(pkg_shortname, distro) + return get_new_package_name(pkg_shortname, distro) + + +def normalize_package_dependency(dependency, distro, vinca_conf): + """Rewrite current-distro legacy dependency names for the selected mode.""" + if isinstance(dependency, dict): + return { + key: normalize_package_dependency(value, distro, vinca_conf) + for key, value in dependency.items() + } + if isinstance(dependency, list): + return [ + normalize_package_dependency(value, distro, vinca_conf) + for value in dependency + ] + if not isinstance(dependency, str): + return dependency + + legacy_prefix = f"{distro.get_legacy_package_prefix()}-" + if not dependency.startswith(legacy_prefix): + return dependency + + package_prefix = f"{get_package_prefix(distro, vinca_conf)}-" + return package_prefix + dependency[len(legacy_prefix) :] + + +def generate_legacy_compatibility_output(output, pkg_shortname, distro, vinca_conf): + """Create an old-name package that depends on the renamed package, if requested.""" + if get_package_name_mode(vinca_conf) is not PackageNameMode.BOTH: + return None + + canonical_name = get_new_package_name(pkg_shortname, distro) + if output["package"]["name"] != canonical_name: + return None + + legacy_name = get_legacy_package_name(pkg_shortname, distro) + version = output["package"]["version"] + return { + "package": {"name": legacy_name, "version": version}, + "build": {"script": ""}, + "requirements": {"run": [f"{canonical_name} =={version}"]}, + "about": {"summary": f"Compatibility package for {canonical_name}"}, + } + + +def is_legacy_compatibility_output(output, distro, vinca_conf): + """Return whether an output is one of the generated compatibility packages.""" + if get_package_name_mode(vinca_conf) is not PackageNameMode.BOTH: + return False + + legacy_prefix_getter = getattr(distro, "get_legacy_package_prefix", None) + if legacy_prefix_getter is None: + return False + + name = output["package"]["name"] + legacy_prefix = f"{legacy_prefix_getter()}-" + if not name.startswith(legacy_prefix): + return False + + shortname = name[len(legacy_prefix) :] + canonical_name = get_new_package_name(shortname, distro) + version = output["package"]["version"] + return output.get("requirements", {}).get("run") == [ + f"{canonical_name} =={version}" + ] diff --git a/vinca/resolve.py b/vinca/resolve.py index 883b2ba..b0438b9 100644 --- a/vinca/resolve.py +++ b/vinca/resolve.py @@ -1,6 +1,7 @@ import os from urllib.request import urlopen from vinca import config +from vinca.naming import get_package_name map_platform_python_to_conda = { "linux-64": "linux", @@ -68,10 +69,7 @@ def resolve_pkgname(pkg_shortname, vinca_conf, distro, is_rundep=False): ): return [] else: - return [ - "ros-%s-%s" - % (vinca_conf["ros_distro"], pkg_shortname.replace("_", "-")) - ] + return [get_package_name(pkg_shortname, distro, vinca_conf)] else: if is_rundep: # for run dependencies, remove the version pkg_names_pinned = [] diff --git a/vinca/template.py b/vinca/template.py index fa482ed..6d8c4c0 100644 --- a/vinca/template.py +++ b/vinca/template.py @@ -8,7 +8,12 @@ from ruamel import yaml from pathlib import Path -from vinca.utils import get_pkg_build_number +from vinca.naming import get_package_prefix, is_legacy_compatibility_output +from vinca.utils import ( + ensure_name_is_without_distro_prefix_and_with_underscores, + get_pkg_additional_info, + get_pkg_build_number, +) TEMPLATE = """\ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json @@ -78,7 +83,7 @@ def copyfile_with_exec_permissions(source_file, destination_file): ) -def write_recipe(source, outputs, vinca_conf, single_file=True): +def write_recipe(source, outputs, vinca_conf, distro, single_file=True): # single_file = False if single_file: file = yaml.YAML() @@ -115,37 +120,47 @@ def write_recipe(source, outputs, vinca_conf, single_file=True): meta["package"]["name"] = o["package"]["name"] meta["package"]["version"] = o["package"]["version"] + package_name = o["package"]["name"] + package_shortname = ( + ensure_name_is_without_distro_prefix_and_with_underscores( + package_name, vinca_conf + ) + ) + is_compatibility_output = is_legacy_compatibility_output( + o, distro, vinca_conf + ) + meta["build"]["number"] = get_pkg_build_number( - vinca_conf.get("build_number", 0), o["package"]["name"], vinca_conf + vinca_conf.get("build_number", 0), package_name, vinca_conf ) meta["build"]["post_process"] = post_process_items - if test := vinca_conf["_tests"].get(o["package"]["name"]): + if not is_compatibility_output and ( + test := vinca_conf["_tests"].get(package_name) + ): print("Using test: ", test) text = test.read_text() test_content = yaml.safe_load(text) meta["tests"] = test_content["tests"] - recipe_dir = (Path("recipes") / o["package"]["name"]).absolute() + recipe_dir = (Path("recipes") / package_name).absolute() os.makedirs(recipe_dir, exist_ok=True) - # Copy test folder contents if corresponding test folder exists - test_dir = vinca_conf.get("_test_dir") - if test_dir is not None: - test_folder_name = o["package"]["name"] - test_folder_path = test_dir / test_folder_name - - if test_folder_path.exists() and test_folder_path.is_dir(): - # Copy all contents of the test folder to the recipe directory - for item in test_folder_path.iterdir(): - if item.is_file(): - shutil.copy2(item, recipe_dir / item.name) - elif item.is_dir(): - # Use copytree for directories, but handle existing directories - dest_dir = recipe_dir / item.name - if dest_dir.exists(): - shutil.rmtree(dest_dir) - shutil.copytree(item, dest_dir) + if not is_compatibility_output and ( + test_folder_path := vinca_conf.get("_test_folders", {}).get( + package_name + ) + ): + # Copy all contents of the test folder to the recipe directory + for item in test_folder_path.iterdir(): + if item.is_file(): + shutil.copy2(item, recipe_dir / item.name) + elif item.is_dir(): + # Use copytree for directories, but handle existing directories + dest_dir = recipe_dir / item.name + if dest_dir.exists(): + shutil.rmtree(dest_dir) + shutil.copytree(item, dest_dir) with open(recipe_dir / "recipe.yaml", "w") as stream: file.dump(meta, stream) @@ -166,24 +181,11 @@ def write_recipe(source, outputs, vinca_conf, single_file=True): ) # Generate the build script directly in the recipe directory # Get additional CMake arguments from pkg_additional_info - from vinca.utils import ( - get_pkg_additional_info, - ensure_name_is_without_distro_prefix_and_with_underscores, - ) - - pkg_name = o["package"]["name"] - # Use the proper utility function to normalize the package name - pkg_shortname = ( - ensure_name_is_without_distro_prefix_and_with_underscores( - pkg_name, vinca_conf - ) - ) - additional_cmake_args = "" additional_folder = "" - if pkg_shortname: + if package_shortname: pkg_additional_info = get_pkg_additional_info( - pkg_shortname, vinca_conf + package_shortname, vinca_conf ) additional_cmake_args = pkg_additional_info.get( "additional_cmake_args", "" @@ -192,19 +194,23 @@ def write_recipe(source, outputs, vinca_conf, single_file=True): # Check if this package has folder info from additional_packages_snapshot if ( vinca_conf.get("_additional_packages_snapshot") - and pkg_shortname in vinca_conf["_additional_packages_snapshot"] + and package_shortname + in vinca_conf["_additional_packages_snapshot"] ): additional_folder = vinca_conf["_additional_packages_snapshot"][ - pkg_shortname + package_shortname ].get("additional_folder", "") generate_build_script_for_recipe( script_filename, recipe_dir / script_filename, + get_package_prefix(distro, vinca_conf), additional_cmake_args, additional_folder, ) - if "catkin" in o["package"]["name"] or "workspace" in o["package"]["name"]: + if not is_compatibility_output and ( + "catkin" in package_name or "workspace" in package_name + ): # Generate activation scripts directly in the recipe directory generate_activation_scripts_for_recipe(recipe_dir) @@ -239,7 +245,11 @@ def generate_template(template_in, template_out, extra_globals=None): def generate_build_script_for_recipe( - script_name, output_path, additional_cmake_args="", additional_folder="" + script_name, + output_path, + ros_package_prefix, + additional_cmake_args="", + additional_folder="", ): """Generate a specific build script directly in the recipe directory.""" @@ -259,6 +269,7 @@ def generate_build_script_for_recipe( template_in = resources.files("vinca") / script_templates[script_name] with open(output_path, "w") as output_file: extra_globals = {} + extra_globals["ros_package_prefix"] = ros_package_prefix if additional_cmake_args: extra_globals["additional_cmake_args"] = additional_cmake_args else: diff --git a/vinca/templates/bld_ament_python.bat.in b/vinca/templates/bld_ament_python.bat.in index 8d6ff92..5640724 100644 --- a/vinca/templates/bld_ament_python.bat.in +++ b/vinca/templates/bld_ament_python.bat.in @@ -5,7 +5,7 @@ setlocal set "PYTHONPATH=%LIBRARY_PREFIX%\lib\site-packages;%SP_DIR%" pushd %SRC_DIR%\%PKG_NAME%\src\work\@(additional_folder) -set "PKG_NAME_SHORT=%PKG_NAME:*ros-@(ros_distro)-=%" +set "PKG_NAME_SHORT=%PKG_NAME:*@(ros_package_prefix)-=%" set "PKG_NAME_SHORT=%PKG_NAME_SHORT:-=_%" :: If there is a setup.cfg that contains install-scripts then use pip to install diff --git a/vinca/templates/bld_catkin.bat.in b/vinca/templates/bld_catkin.bat.in index dc40f72..f534ce3 100644 --- a/vinca/templates/bld_catkin.bat.in +++ b/vinca/templates/bld_catkin.bat.in @@ -15,7 +15,7 @@ set CXX=cl.exe set CL=/DROS_BUILD_SHARED_LIBS=1 /DNOGDI=1 set "CATKIN_BUILD_BINARY_PACKAGE_ARGS=-DCATKIN_BUILD_BINARY_PACKAGE=1" -if "%PKG_NAME%" == "ros-@(ros_distro)-catkin" ( +if "%PKG_NAME%" == "@(ros_package_prefix)-catkin" ( :: create catkin cookie to make it is a catkin workspace type NUL > %LIBRARY_PREFIX%\.catkin :: keep the workspace activation scripts (e.g., local_setup.bat) @@ -46,7 +46,7 @@ cmake ^ %SRC_DIR%\%PKG_NAME%\src\work\@(additional_folder) if errorlevel 1 exit 1 -if "%PKG_NAME%" == "ros-@(ros_distro)-eigenpy" ( +if "%PKG_NAME%" == "@(ros_package_prefix)-eigenpy" ( cmake --build . --config Release --target all --parallel 1 if errorlevel 1 exit 1 ) else ( @@ -62,7 +62,7 @@ if "%SKIP_TESTING%" == "OFF" ( cmake --build . --config Release --target install if errorlevel 1 exit 1 -if "%PKG_NAME%" == "ros-@(ros_distro)-catkin" ( +if "%PKG_NAME%" == "@(ros_package_prefix)-catkin" ( :: Copy the [de]activate scripts to %PREFIX%\etc\conda\[de]activate.d. :: This will allow them to be run on environment activation. for %%F in (activate deactivate) DO ( @@ -71,7 +71,7 @@ if "%PKG_NAME%" == "ros-@(ros_distro)-catkin" ( ) ) -if "%PKG_NAME%" == "ros-@(ros_distro)-ros-workspace" ( +if "%PKG_NAME%" == "@(ros_package_prefix)-ros-workspace" ( :: Copy the [de]activate scripts to %PREFIX%\etc\conda\[de]activate.d. :: This will allow them to be run on environment activation. for %%F in (activate deactivate) DO ( diff --git a/vinca/templates/bld_catkin_merge.bat.in b/vinca/templates/bld_catkin_merge.bat.in index 7ff5a7a..24512c2 100644 --- a/vinca/templates/bld_catkin_merge.bat.in +++ b/vinca/templates/bld_catkin_merge.bat.in @@ -13,7 +13,7 @@ set CXX=cl.exe :: https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables?view=msvc-170 set CL=/DROS_BUILD_SHARED_LIBS=1 /DNOGDI=1 -set CATKIN_MAKE_ISOLATED=src\ros-@(ros_distro)-catkin\bin\catkin_make_isolated +set CATKIN_MAKE_ISOLATED=src\@(ros_package_prefix)-catkin\bin\catkin_make_isolated set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH:\=/% %PYTHON% %CATKIN_MAKE_ISOLATED% ^ diff --git a/vinca/templates/build_ament_cmake.sh.in b/vinca/templates/build_ament_cmake.sh.in index 84d1870..78a0a03 100644 --- a/vinca/templates/build_ament_cmake.sh.in +++ b/vinca/templates/build_ament_cmake.sh.in @@ -102,11 +102,11 @@ else export CMAKE_BLD="cmake" fi; -if [ "${PKG_NAME}" == "ros-humble-rmw-wasm-cpp" ]; then +if [ "${PKG_NAME}" == "@(ros_package_prefix)-rmw-wasm-cpp" ]; then WORK_DIR=$SRC_DIR/$PKG_NAME/src/work/rmw_wasm_cpp -elif [ "${PKG_NAME}" == "ros-humble-wasm-cpp" ]; then +elif [ "${PKG_NAME}" == "@(ros_package_prefix)-wasm-cpp" ]; then WORK_DIR=$SRC_DIR/$PKG_NAME/src/work/wasm_cpp -elif [ "${PKG_NAME}" == "dynmsg" ]; then +elif [ "${PKG_NAME}" == "@(ros_package_prefix)-dynmsg" ]; then WORK_DIR=$SRC_DIR/$PKG_NAME/src/work/dynmsg else WORK_DIR=$SRC_DIR/$PKG_NAME/src/work/@(additional_folder) diff --git a/vinca/templates/build_ament_python.sh.in b/vinca/templates/build_ament_python.sh.in index 5151721..5cbffb0 100644 --- a/vinca/templates/build_ament_python.sh.in +++ b/vinca/templates/build_ament_python.sh.in @@ -7,8 +7,8 @@ pushd $SRC_DIR/$PKG_NAME/src/work/@(additional_folder) # If there is a setup.cfg that contains install-scripts then we should not set it here if [ -f setup.cfg ] && grep -q "install[-_]scripts" setup.cfg; then - # Remove e.g. ros-humble- from PKG_NAME - PKG_NAME_SHORT=${PKG_NAME#*ros-@(ros_distro)-} + # Remove e.g. ros2- or ros- from PKG_NAME + PKG_NAME_SHORT=${PKG_NAME#*@(ros_package_prefix)-} # Substitute "-" with "_" PKG_NAME_SHORT=${PKG_NAME_SHORT//-/_} INSTALL_SCRIPTS_ARG="--install-scripts=$PREFIX/lib/$PKG_NAME_SHORT" diff --git a/vinca/templates/build_catkin.sh.in b/vinca/templates/build_catkin.sh.in index 94729fb..58c47a2 100644 --- a/vinca/templates/build_catkin.sh.in +++ b/vinca/templates/build_catkin.sh.in @@ -5,7 +5,7 @@ set -eo pipefail CATKIN_BUILD_BINARY_PACKAGE="ON" -if [ "${PKG_NAME}" == "ros-@(ros_distro)-catkin" ]; then +if [ "${PKG_NAME}" == "@(ros_package_prefix)-catkin" ]; then # create catkin cookie to make it is a catkin workspace touch $PREFIX/.catkin # keep the workspace activation scripts (e.g., local_setup.bat) @@ -85,7 +85,7 @@ fi export SKIP_TESTING=@(skip_testing) -if [ "${PKG_NAME}" == "ros-noetic-euslisp" ] || [ "${PKG_NAME}" = "ros-noetic-jskeus" ] || [ "${PKG_NAME}" = "ros-noetic-roseus" ]; then +if [ "${PKG_NAME}" == "@(ros_package_prefix)-euslisp" ] || [ "${PKG_NAME}" = "@(ros_package_prefix)-jskeus" ] || [ "${PKG_NAME}" = "@(ros_package_prefix)-roseus" ]; then GENERATOR="Unix Makefiles" else GENERATOR="Ninja" @@ -123,7 +123,7 @@ fi cmake --build . --config Release --target install -if [ "${PKG_NAME}" == "ros-@(ros_distro)-catkin" ]; then +if [ "${PKG_NAME}" == "@(ros_package_prefix)-catkin" ]; then # Copy the [de]activate scripts to $PREFIX/etc/conda/[de]activate.d. # This will allow them to be run on environment activation. for CHANGE in "activate" "deactivate" @@ -133,7 +133,7 @@ if [ "${PKG_NAME}" == "ros-@(ros_distro)-catkin" ]; then done fi -if [ "${PKG_NAME}" == "ros-@(ros_distro)-environment" ]; then +if [ "${PKG_NAME}" == "@(ros_package_prefix)-environment" ]; then for SCRIPT in "1.ros_distro.sh" "1.ros_etc_dir.sh" "1.ros_package_path.sh" "1.ros_python_version.sh" "1.ros_version.sh" do mkdir -p "${PREFIX}/etc/conda/activate.d" @@ -141,7 +141,7 @@ if [ "${PKG_NAME}" == "ros-@(ros_distro)-environment" ]; then done fi -if [ "${PKG_NAME}" == "ros-@(ros_distro)-ros-workspace" ]; then +if [ "${PKG_NAME}" == "@(ros_package_prefix)-ros-workspace" ]; then # Copy the [de]activate scripts to $PREFIX/etc/conda/[de]activate.d. # This will allow them to be run on environment activation. for CHANGE in "activate" "deactivate" diff --git a/vinca/test_naming_scheme.py b/vinca/test_naming_scheme.py new file mode 100644 index 0000000..3cce89b --- /dev/null +++ b/vinca/test_naming_scheme.py @@ -0,0 +1,261 @@ +"""Tests for the ROS package naming scheme.""" + +import pytest + +from vinca.distro import Distro +from vinca.main import append_output_with_compatibility, get_depmods +from vinca.naming import ( + PackageNameMode, + generate_legacy_compatibility_output, + get_package_name, + get_package_name_mode, +) +from vinca.resolve import resolve_pkgname +from vinca.utils import ( + add_package_name_variants, + ensure_name_is_without_distro_prefix_and_with_underscores, +) + + +def make_distro(distribution_type="ros2", name="humble"): + """Create a Distro without loading the remote rosdistro index.""" + distro = Distro.__new__(Distro) + distro._distribution_type = distribution_type + distro.distro_name = name + return distro + + +@pytest.mark.parametrize( + "input_name,expected_output", + [ + # New ROS 2 package names + ("ros2-my-package", "my_package"), + ("ros2-complex-package-name", "complex_package_name"), + ("ros2-ros-environment", "ros_environment"), + ("ros2-ros-workspace", "ros_workspace"), + ("ros2-catkin", "catkin"), + ("ros2-navigation2", "navigation2"), + ("ros2-my-very-long-package-name", "my_very_long_package_name"), + ("ros2-navigation-stack-extra", "navigation_stack_extra"), + ("ros2-my_package_name", "my_package_name"), + # New ROS 1 package names + ("ros-my-package", "my_package"), + ("ros-test-package", "test_package"), + ("ros-ros-environment", "ros_environment"), + ("ros-ros-workspace", "ros_workspace"), + ("ros-catkin", "catkin"), + ("ros-moveit", "moveit"), + ("ros-simple", "simple"), + # Legacy distro-qualified package names + ("ros-humble-my-package", "my_package"), + ("ros-humble-ros-workspace", "ros_workspace"), + # Packages without a prefix + ("my-package", "my_package"), + # Edge cases + ("ros2-a", "a"), + ("ros-a", "a"), + ("ros2-ros", "ros"), + ], +) +def test_package_normalization(input_name, expected_output): + result = ensure_name_is_without_distro_prefix_and_with_underscores( + input_name, {"ros_distro": "humble"} + ) + assert result == expected_output + + +@pytest.mark.parametrize( + "package_name", + [ + "ros2-my-package", + "ros2-complex-package-name", + "ros-simple-package", + "ros-another-test", + "ros-humble-legacy-package", + ], +) +def test_no_hyphens_in_normalized_output(package_name): + result = ensure_name_is_without_distro_prefix_and_with_underscores( + package_name, {"ros_distro": "humble"} + ) + assert "-" not in result + + +@pytest.mark.parametrize( + "distribution_type,distro_name,expected_prefix,expected_legacy_prefix,expected_version", + [ + ("ros1", "noetic", "ros", "ros-noetic", "1"), + ("ros2", "humble", "ros2", "ros-humble", "2"), + ], +) +def test_distro_prefixes_and_version( + distribution_type, + distro_name, + expected_prefix, + expected_legacy_prefix, + expected_version, +): + distro = make_distro(distribution_type, distro_name) + + assert distro.get_package_prefix() == expected_prefix + assert distro.get_legacy_package_prefix() == expected_legacy_prefix + assert distro.get_ros_version() == expected_version + + +@pytest.mark.parametrize( + "mode,expected_name", + [ + ("legacy", "ros-humble-my-package"), + ("both", "ros2-my-package"), + ("new", "ros2-my-package"), + ], +) +def test_package_name_mode_selects_primary_name(mode, expected_name): + distro = make_distro() + assert ( + get_package_name("my_package", distro, {"package_name_mode": mode}) + == expected_name + ) + + +def test_package_name_mode_defaults_to_legacy(): + distro = make_distro() + + assert get_package_name_mode({}) is PackageNameMode.LEGACY + assert get_package_name("my_package", distro, {}) == "ros-humble-my-package" + + +@pytest.mark.parametrize( + "mode,expected_name", + [ + ("legacy", "ros-humble-my-package"), + ("both", "ros2-my-package"), + ("new", "ros2-my-package"), + ], +) +def test_dependency_resolution_uses_selected_name_mode(mode, expected_name): + distro = make_distro() + distro.check_package = lambda _name: True + vinca_conf = {"_conda_indexes": [], "package_name_mode": mode} + + assert resolve_pkgname("my_package", vinca_conf, distro) == [expected_name] + + +@pytest.mark.parametrize( + "skip_built_packages,expected_names", + [ + (set(), ["ros2-my-package", "ros-humble-my-package"]), + ({"ros2-my-package"}, ["ros-humble-my-package"]), + ({"ros-humble-my-package"}, ["ros2-my-package"]), + ({"ros2-my-package", "ros-humble-my-package"}, []), + ], +) +def test_both_mode_filters_new_and_legacy_outputs_independently( + skip_built_packages, expected_names +): + distro = make_distro() + output = {"package": {"name": "ros2-my-package", "version": "1.2.3"}} + outputs = [] + vinca_conf = { + "package_name_mode": "both", + "skip_built_packages": skip_built_packages, + } + + append_output_with_compatibility(outputs, output, "my_package", distro, vinca_conf) + + assert [candidate["package"]["name"] for candidate in outputs] == expected_names + + +def test_version_aware_skip_filters_compatibility_output_independently(): + distro = make_distro() + output = {"package": {"name": "ros2-my-package", "version": "1.2.3"}} + outputs = [] + vinca_conf = { + "package_name_mode": "both", + "trigger_new_versions": True, + "skip_built_packages": {("ros-humble-my-package", "1.2.3")}, + } + + append_output_with_compatibility(outputs, output, "my_package", distro, vinca_conf) + + assert [candidate["package"]["name"] for candidate in outputs] == [ + "ros2-my-package" + ] + + +def test_depmods_use_selected_name_mode_recursively(): + distro = make_distro() + vinca_conf = { + "package_name_mode": "new", + "depmods": { + "my_package": { + "add_host": [ + "ros-humble-rclcpp >=1", + "ros-noetic-roscpp", + { + "if": "target_platform == 'emscripten-wasm32'", + "then": ["ros-humble-rmw-wasm-cpp"], + }, + ], + "remove_run": ["ros-humble-tl-expected"], + } + }, + } + + removed, added = get_depmods(vinca_conf, "my_package", distro) + + assert added["host"] == [ + "ros2-rclcpp >=1", + "ros-noetic-roscpp", + { + "if": "target_platform == 'emscripten-wasm32'", + "then": ["ros2-rmw-wasm-cpp"], + }, + ] + assert removed["run"] == ["ros2-tl-expected"] + + +def test_package_config_entries_are_available_under_old_and_new_names(): + value = object() + entries = {"ros-humble-my-package": value} + + add_package_name_variants(entries, "humble") + + assert entries["ros-humble-my-package"] is value + assert entries["ros-my-package"] is value + assert entries["ros2-my-package"] is value + + +def test_invalid_package_name_mode_is_rejected(): + with pytest.raises(ValueError, match="Invalid package_name_mode 'invalid'"): + get_package_name_mode({"package_name_mode": "invalid"}) + + +def test_both_mode_generates_legacy_compatibility_output(): + distro = make_distro() + output = {"package": {"name": "ros2-my-package", "version": "1.2.3"}} + + compatibility_output = generate_legacy_compatibility_output( + output, "my_package", distro, {"package_name_mode": "both"} + ) + + assert compatibility_output == { + "package": {"name": "ros-humble-my-package", "version": "1.2.3"}, + "build": {"script": ""}, + "requirements": {"run": ["ros2-my-package ==1.2.3"]}, + "about": {"summary": "Compatibility package for ros2-my-package"}, + } + + +@pytest.mark.parametrize("mode", ["legacy", "new"]) +def test_single_name_modes_do_not_generate_compatibility_output(mode): + distro = make_distro() + output_name = get_package_name("my_package", distro, {"package_name_mode": mode}) + output = {"package": {"name": output_name, "version": "1.2.3"}} + + assert ( + generate_legacy_compatibility_output( + output, "my_package", distro, {"package_name_mode": mode} + ) + is None + ) diff --git a/vinca/test_utils.py b/vinca/test_utils.py index d638b96..8ff9452 100644 --- a/vinca/test_utils.py +++ b/vinca/test_utils.py @@ -1,11 +1,28 @@ import json from unittest.mock import Mock, patch -from vinca.utils import get_repodata +from vinca.utils import extract_dependency_names, get_repodata EMPTY_REPODATA = {"packages": {}, "packages.conda": {}} +def test_extract_dependency_names_handles_conditional_requirements(): + requirements = [ + "ros2-rclcpp >=1", + { + "if": "target_platform == 'emscripten-wasm32'", + "then": ["ros2-rmw-wasm-cpp", "fmt"], + }, + "ros2-rclcpp >=1", + ] + + assert extract_dependency_names(requirements) == [ + "ros2-rclcpp", + "ros2-rmw-wasm-cpp", + "fmt", + ] + + def test_get_repodata_returns_empty_for_missing_local_repodata(tmp_path): assert get_repodata(str(tmp_path), "osx-arm64") == EMPTY_REPODATA diff --git a/vinca/utils.py b/vinca/utils.py index 85b7739..88cac32 100644 --- a/vinca/utils.py +++ b/vinca/utils.py @@ -60,12 +60,12 @@ def get_repodata(url_or_path, platform=None): print(f"Found cached repodata, age: {age}") max_age = 100_000 # seconds == 27 hours if age < max_age: - with open(fn) as fi: - try: + try: + with open(fn) as fi: return json.load(fi) - except json.JSONDecodeError: - print(f"Ignoring invalid cached repodata at {fn}") - os.remove(fn) + except json.JSONDecodeError: + print(f"Ignoring invalid cached repodata at {fn}") + os.remove(fn) repodata = requests.get(url_or_path) if repodata.status_code == 404: @@ -90,18 +90,61 @@ def get_repodata(url_or_path, platform=None): def ensure_name_is_without_distro_prefix_and_with_underscores(name, vinca_conf): - """ - Ensure that the name is without distro prefix and with underscores - e.g. "ros-humble-pkg-name" -> "pkg_name" - """ + """Normalize new and legacy ROS package names to their ROS package name.""" newname = name.replace("-", "_") - distro_prefix = "ros_" + vinca_conf.get("ros_distro") + "_" - if newname.startswith(distro_prefix): - newname = newname.replace(distro_prefix, "") + prefixes = ["ros2_"] + if ros_distro := vinca_conf.get("ros_distro"): + prefixes.append(f"ros_{ros_distro}_") + prefixes.append("ros_") + + for prefix in prefixes: + if newname.startswith(prefix): + return newname[len(prefix) :] return newname +def extract_dependency_names(requirements): + """Extract dependency names from strings and conditional requirement structures.""" + names = [] + + def visit(value): + if isinstance(value, str): + if value: + names.append(value.split()[0]) + elif isinstance(value, dict): + for key, nested_value in value.items(): + if key != "if": + visit(nested_value) + elif isinstance(value, (list, tuple)): + for nested_value in value: + visit(nested_value) + + visit(requirements) + return list(dict.fromkeys(names)) + + +def add_package_name_variants(entries, ros_distro): + """Make package-specific config entries available under old and new names.""" + legacy_prefix = f"ros-{ros_distro}-" + for name, value in list(entries.items()): + if name.startswith(legacy_prefix): + shortname = name[len(legacy_prefix) :] + elif name.startswith("ros2-"): + shortname = name[len("ros2-") :] + elif name.startswith("ros-"): + shortname = name[len("ros-") :] + else: + shortname = name + + for variant in ( + f"{legacy_prefix}{shortname}", + f"ros-{shortname}", + f"ros2-{shortname}", + ): + entries.setdefault(variant, value) + + def get_pkg_additional_info(pkg_name, vinca_conf): normalized_name = ensure_name_is_without_distro_prefix_and_with_underscores( pkg_name, vinca_conf