diff --git a/vinca/distro.py b/vinca/distro.py
index 9ce23fb..5df5339 100644
--- a/vinca/distro.py
+++ b/vinca/distro.py
@@ -5,6 +5,7 @@
import urllib.parse
import zipfile
+import catkin_pkg.package
import requests
from rosdistro import get_cached_distribution, get_index, get_index_url
from rosdistro.dependency_walker import DependencyWalker
@@ -151,6 +152,11 @@ def get_depends(self, pkg, ignore_pkgs=None):
print(f"{pkg} not in available packages anymore")
return dependencies
+ if self.snapshot:
+ dependencies = self._get_snapshot_recursive_depends(pkg, ignore_pkgs)
+ self._depends_cache[cache_key] = set(dependencies)
+ return dependencies
+
# if pkg comes from additional_packages_snapshot, extract from its package.xml
if (
self.additional_packages_snapshot
@@ -207,25 +213,74 @@ def get_depends(self, pkg, ignore_pkgs=None):
self._depends_cache[cache_key] = set(dependencies)
return dependencies
+ def _get_snapshot_recursive_depends(self, pkg, ignore_pkgs=None):
+ """Return ROS dependencies using only package manifests pinned by the snapshot."""
+ dependencies = set()
+ ignored = set(ignore_pkgs or [])
+ packages_to_check = {pkg}
+ checked_packages = set()
+ dependency_attributes = (
+ "buildtool_depends",
+ "buildtool_export_depends",
+ "build_depends",
+ "build_export_depends",
+ "run_depends",
+ "test_depends",
+ "exec_depends",
+ )
+
+ while packages_to_check:
+ package_name = sorted(packages_to_check)[0]
+ packages_to_check.remove(package_name)
+ if package_name in ignored or package_name in checked_packages:
+ continue
+ checked_packages.add(package_name)
+
+ package_xml = self.get_release_package_xml(package_name)
+ package = catkin_pkg.package.parse_package_string(package_xml)
+ package.evaluate_conditions(os.environ)
+ direct_dependencies = {
+ dependency.name
+ for attribute in dependency_attributes
+ for dependency in getattr(package, attribute)
+ if dependency.evaluated_condition is not False
+ and dependency.name not in ignored
+ and self.check_package(dependency.name)
+ }
+ new_dependencies = direct_dependencies - dependencies
+ dependencies |= new_dependencies
+ packages_to_check |= new_dependencies - checked_packages
+
+ return dependencies
+
+ def _get_snapshot_package_info(self, pkg_name):
+ if not self.snapshot:
+ return None
+ for name in (pkg_name, pkg_name.replace("_", "-")):
+ if name in self.snapshot:
+ return self.snapshot[name]
+ return None
+
def get_released_repo(self, pkg_name):
- if self.snapshot and pkg_name in self.snapshot:
+ pkg_info = self._get_snapshot_package_info(pkg_name)
+ if pkg_info is not None:
# In the case of snapshot, for rosdistro_additional_recipes
# we also support a 'rev' field, so depending on what is available
# we return either the tag or the rev, and the third argument is either 'rev' or 'tag'
- url = self.snapshot[pkg_name].get("url", None)
+ url = pkg_info.get("url", None)
# An archive URL is not a git repository: the reference is its sha256 checksum.
if is_archive_url(url):
- sha256 = self.snapshot[pkg_name].get("sha256", None)
+ sha256 = pkg_info.get("sha256", None)
if not sha256:
raise RuntimeError(
f"The archive source of '{pkg_name}' has no sha256 checksum: {url}"
)
return url, sha256, "sha256"
- if "tag" in self.snapshot[pkg_name].keys():
- tag_or_rev = self.snapshot[pkg_name].get("tag", None)
+ if "tag" in pkg_info:
+ tag_or_rev = pkg_info.get("tag", None)
ref_type = "tag"
else:
- tag_or_rev = self.snapshot[pkg_name].get("rev", None)
+ tag_or_rev = pkg_info.get("rev", None)
ref_type = "rev"
return url, tag_or_rev, ref_type
@@ -244,23 +299,27 @@ def check_package(self, pkg_name):
and pkg_name in self.additional_packages_snapshot
):
return True
- # the .replace('_', '-') is needed for packages like 'hpp-fcl' that have hypen and not underscore
+ if self.snapshot:
+ return (
+ self._get_snapshot_package_info(pkg_name) is not None
+ or pkg_name in self.build_packages
+ )
+ # the .replace('_', '-') is needed for packages like 'hpp-fcl' that have a hyphen and not underscore
# in the rosdistro metadata
if (
pkg_name in self._distro.release_packages
or pkg_name.replace("_", "-") in self._distro.release_packages
):
- return self.snapshot is None or (
- pkg_name in self.snapshot or pkg_name.replace("_", "-") in self.snapshot
- )
+ return True
elif pkg_name in self.build_packages:
return True
else:
return False
def get_version(self, pkg_name):
- if self.snapshot and pkg_name in self.snapshot:
- return self.snapshot[pkg_name].get("version", None)
+ pkg_info = self._get_snapshot_package_info(pkg_name)
+ if pkg_info is not None:
+ return pkg_info.get("version", None)
pkg = self._distro.release_packages[pkg_name]
repo = self._distro.repositories[pkg.repository_name].release_repository
@@ -273,6 +332,9 @@ def get_release_package_xml(self, pkg_name):
):
pkg_info = self.additional_packages_snapshot[pkg_name]
return self.get_package_xml_for_additional_package(pkg_info)
+ pkg_info = self._get_snapshot_package_info(pkg_name)
+ if pkg_info is not None:
+ return self.get_package_xml_for_additional_package(pkg_info)
return self._distro.get_release_package_xml(pkg_name)
def check_ros1(self):
@@ -294,6 +356,8 @@ def get_python_version(self):
return self._python_version
def get_package_names(self):
+ if self.snapshot:
+ return self.snapshot.keys()
return self._distro.release_packages.keys()
def get_package_xml_for_additional_package(self, pkg_info):
diff --git a/vinca/main.py b/vinca/main.py
index 4325d28..9770bee 100644
--- a/vinca/main.py
+++ b/vinca/main.py
@@ -405,15 +405,9 @@ def generate_output(pkg_shortname, vinca_conf, distro, version, all_pkgs=None):
xml = distro.get_release_package_xml(pkg_shortname)
- # If the snapshot is not aligned with the latest rosdistro (for example if a package is removed,
- # see https://github.com/RoboStack/ros-jazzy/pull/107#issuecomment-3338962041), get_release_package_xml can return none,
- # in that case we can just skip the package, remove if https://github.com/RoboStack/vinca/issues/93 is fixed
+ # Without a snapshot, rosdistro can return no manifest for an unreleased package.
if not xml:
- print(
- "Skip "
- + pkg_shortname
- + " as it is present in our snapshot, but not in the latest rosdistro cache."
- )
+ print(f"Skip {pkg_shortname} because no release package.xml is available.")
return None
pkg = catkin_pkg.package.parse_package_string(xml)
diff --git a/vinca/test_snapshot_metadata.py b/vinca/test_snapshot_metadata.py
new file mode 100644
index 0000000..3f115b8
--- /dev/null
+++ b/vinca/test_snapshot_metadata.py
@@ -0,0 +1,157 @@
+from unittest.mock import Mock
+
+import vinca.main as main
+from vinca.distro import Distro
+
+
+def package_xml(name, version, build_dependency=None):
+ dependency = (
+ f" {build_dependency}\n"
+ if build_dependency
+ else ""
+ )
+ return f"""\
+
+ {name}
+ {version}
+ Test package
+ Test
+ BSD-3-Clause
+ ament_cmake
+{dependency} ament_cmake
+
+"""
+
+
+SNAPSHOT_PACKAGE_XML = package_xml("snapshot_package", "1.0.0", "snapshot_dependency")
+LIVE_PACKAGE_XML = package_xml("snapshot_package", "2.0.0", "live_dependency")
+SNAPSHOT_DEPENDENCY_XML = package_xml("snapshot_dependency", "1.0.0")
+
+
+def make_snapshot_distro(monkeypatch):
+ snapshot = {
+ "snapshot_package": {
+ "url": "https://github.com/example/snapshot-package-release.git",
+ "version": "1.0.0",
+ "tag": "release/rolling/snapshot_package/1.0.0-1",
+ },
+ "snapshot_dependency": {
+ "url": "https://github.com/example/snapshot-dependency-release.git",
+ "version": "1.0.0",
+ "tag": "release/rolling/snapshot_dependency/1.0.0-1",
+ },
+ }
+ distro = Distro.__new__(Distro)
+ distro.distro_name = "rolling"
+ distro.snapshot = snapshot
+ distro.additional_packages_snapshot = None
+ distro.build_packages = set()
+ distro._distribution_type = "ros2"
+ distro._additional_xml_cache = {}
+ distro._depends_cache = {}
+ distro._distro = Mock()
+ distro._distro.get_release_package_xml.return_value = LIVE_PACKAGE_XML
+ distro._walker = Mock()
+
+ snapshot_xml_by_url = {
+ "https://raw.githubusercontent.com/example/snapshot-package-release/"
+ "release/rolling/snapshot_package/1.0.0-1/package.xml": (SNAPSHOT_PACKAGE_XML),
+ "https://raw.githubusercontent.com/example/snapshot-dependency-release/"
+ "release/rolling/snapshot_dependency/1.0.0-1/package.xml": (
+ SNAPSHOT_DEPENDENCY_XML
+ ),
+ }
+ monkeypatch.setattr(
+ distro,
+ "_download_raw_pkg_xml_or_cached",
+ lambda url: snapshot_xml_by_url[url],
+ )
+ return distro
+
+
+def test_snapshot_package_xml_and_dependencies_do_not_follow_live_rosdistro(
+ monkeypatch,
+):
+ distro = make_snapshot_distro(monkeypatch)
+
+ package_xml_content = distro.get_release_package_xml("snapshot_package")
+
+ assert distro.get_released_repo("snapshot_package") == (
+ "https://github.com/example/snapshot-package-release.git",
+ "release/rolling/snapshot_package/1.0.0-1",
+ "tag",
+ )
+ assert distro.get_version("snapshot_package") == "1.0.0"
+ assert "1.0.0" in package_xml_content
+ assert "snapshot_dependency" in package_xml_content
+ assert "live_dependency" not in package_xml_content
+ assert distro.get_depends("snapshot_package") == {"snapshot_dependency"}
+ distro._distro.get_release_package_xml.assert_not_called()
+ distro._walker.get_recursive_depends.assert_not_called()
+
+
+def test_snapshot_metadata_generates_dependency_required_by_pinned_source(
+ monkeypatch,
+):
+ distro = make_snapshot_distro(monkeypatch)
+ dependency_names = {
+ "snapshot_package": "ros2-snapshot-package",
+ "python": "python",
+ "ament_cmake": "ros2-ament-cmake",
+ "snapshot_dependency": "ros2-snapshot-dependency",
+ }
+ monkeypatch.setattr(
+ main,
+ "resolve_pkgname",
+ lambda name, *_args, **_kwargs: [dependency_names[name]],
+ )
+ config = {
+ "_selected_pkgs": {"snapshot_package"},
+ "_pkg_additional_info": {},
+ "depmods": {},
+ "package_name_mode": "new",
+ }
+
+ output = main.generate_output(
+ "snapshot_package",
+ config,
+ distro,
+ distro.get_version("snapshot_package"),
+ )
+
+ assert output["package"] == {
+ "name": "ros2-snapshot-package",
+ "version": "1.0.0",
+ }
+ assert "ros2-snapshot-dependency" in output["requirements"]["host"]
+ assert "ros2-live-dependency" not in output["requirements"]["host"]
+
+
+def test_snapshot_is_authoritative_for_package_membership(monkeypatch):
+ distro = make_snapshot_distro(monkeypatch)
+ distro._distro.release_packages = {"live_only": Mock()}
+
+ assert distro.check_package("snapshot_package")
+ assert not distro.check_package("live_only")
+ assert set(distro.get_package_names()) == {
+ "snapshot_package",
+ "snapshot_dependency",
+ }
+
+
+def test_empty_snapshot_keeps_live_rosdistro_behavior():
+ distro = Distro.__new__(Distro)
+ distro.snapshot = {}
+ distro.additional_packages_snapshot = None
+ distro.build_packages = set()
+ distro._depends_cache = {}
+ distro._distro = Mock()
+ distro._distro.release_packages = {"live_package": Mock()}
+ distro._distro.get_release_package_xml.return_value = LIVE_PACKAGE_XML
+ distro._walker = Mock()
+ distro._walker.get_recursive_depends.return_value = {"live_dependency"}
+
+ assert distro.check_package("live_package")
+ assert distro.get_release_package_xml("live_package") == LIVE_PACKAGE_XML
+ assert distro.get_depends("live_package") == {"live_dependency"}
+ assert set(distro.get_package_names()) == {"live_package"}