Skip to content

Commit 0f3ce69

Browse files
authored
feat: set ROS_VERSION and DISTRO automatically (#421)
1 parent 7d1068a commit 0f3ce69

File tree

5 files changed

+230
-21
lines changed

5 files changed

+230
-21
lines changed

backends/pixi-build-ros/src/pixi_build_ros/ros_generator.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
ROS generator implementation using Python bindings.
33
"""
44

5+
import os
56
from pathlib import Path
67
from importlib.resources import files
8+
from unittest.mock import patch
79

810
from typing import Any
911
from pixi_build_backend.types.generated_recipe import (
@@ -62,25 +64,35 @@ def generate_recipe(
6264

6365
# Read package.xml for dependency extraction
6466
package_xml_str = get_package_xml_content(manifest_root)
65-
package_xml = convert_package_xml_to_catkin_package(package_xml_str)
66-
67-
# load package map
68-
69-
# TODO: Currently hardcoded and not able to override, this should be configurable
70-
package_files = files("pixi_build_ros")
71-
robostack_file = Path(str(package_files)) / "robostack.yaml"
72-
# workaround for from source install
73-
if not robostack_file.is_file():
74-
robostack_file = Path(__file__).parent.parent.parent / "robostack.yaml"
75-
76-
package_map_data = load_package_map_data(
77-
backend_config.extra_package_mappings + [PackageMappingSource.from_file(robostack_file)]
78-
)
79-
80-
# Get requirements from package.xml
81-
package_requirements = package_xml_to_conda_requirements(
82-
package_xml, backend_config.distro, host_platform, package_map_data
83-
)
67+
ros_env_defaults = {
68+
"ROS_DISTRO": backend_config.distro.name,
69+
"ROS_VERSION": "1" if backend_config.distro.check_ros1() else "2",
70+
}
71+
user_env = dict(backend_config.env or {})
72+
patched_env = {**ros_env_defaults, **user_env}
73+
74+
# Ensure ROS-related environment variables are available while evaluating conditions.
75+
# uses the unitest patch for this
76+
with patch.dict(os.environ, patched_env, clear=False):
77+
package_xml = convert_package_xml_to_catkin_package(package_xml_str)
78+
79+
# load package map
80+
81+
# TODO: Currently hardcoded and not able to override, this should be configurable
82+
package_files = files("pixi_build_ros")
83+
robostack_file = Path(str(package_files)) / "robostack.yaml"
84+
# workaround for from source install
85+
if not robostack_file.is_file():
86+
robostack_file = Path(__file__).parent.parent.parent / "robostack.yaml"
87+
88+
package_map_data = load_package_map_data(
89+
backend_config.extra_package_mappings + [PackageMappingSource.from_file(robostack_file)]
90+
)
91+
92+
# Get requirements from package.xml
93+
package_requirements = package_xml_to_conda_requirements(
94+
package_xml, backend_config.distro, host_platform, package_map_data
95+
)
8496

8597
# Add standard dependencies
8698
build_deps = [
@@ -128,9 +140,12 @@ def generate_recipe(
128140
)
129141
build_script_lines = build_script_context.render()
130142

143+
script_env = dict(ros_env_defaults)
144+
script_env.update(user_env)
145+
131146
generated_recipe.recipe.build.script = Script(
132147
content=build_script_lines,
133-
env=backend_config.env,
148+
env=script_env,
134149
)
135150

136151
if backend_config.debug_dir:

backends/pixi-build-ros/tests/__snapshots__/test_package_xml.ambr

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,80 @@
11
# serializer version: 1
2+
# name: test_package_xml_condition_evaluation[ros1-default]
3+
dict({
4+
'distro': 'noetic',
5+
'env': dict({
6+
'ROS_DISTRO': 'noetic',
7+
'ROS_VERSION': '1',
8+
}),
9+
'filtered_build': list([
10+
'ros-noetic-catkin',
11+
'ros-noetic-roscpp',
12+
]),
13+
'filtered_run': list([
14+
'ros-noetic-roscpp',
15+
]),
16+
'override_env': dict({
17+
}),
18+
})
19+
# ---
20+
# name: test_package_xml_condition_evaluation[ros1-override-to-ros2]
21+
dict({
22+
'distro': 'noetic',
23+
'env': dict({
24+
'ROS_DISTRO': 'custom-noetic',
25+
'ROS_VERSION': '2',
26+
}),
27+
'filtered_build': list([
28+
'ros-noetic-ament-cmake',
29+
'ros-noetic-rclcpp',
30+
]),
31+
'filtered_run': list([
32+
'ros-noetic-rclcpp',
33+
]),
34+
'override_env': dict({
35+
'ROS_DISTRO': 'custom-noetic',
36+
'ROS_VERSION': '2',
37+
}),
38+
})
39+
# ---
40+
# name: test_package_xml_condition_evaluation[ros2-default]
41+
dict({
42+
'distro': 'jazzy',
43+
'env': dict({
44+
'ROS_DISTRO': 'jazzy',
45+
'ROS_VERSION': '2',
46+
}),
47+
'filtered_build': list([
48+
'ros-jazzy-ament-cmake',
49+
'ros-jazzy-rclcpp',
50+
]),
51+
'filtered_run': list([
52+
'ros-jazzy-rclcpp',
53+
]),
54+
'override_env': dict({
55+
}),
56+
})
57+
# ---
58+
# name: test_package_xml_condition_evaluation[ros2-override-to-ros1]
59+
dict({
60+
'distro': 'jazzy',
61+
'env': dict({
62+
'ROS_DISTRO': 'custom-jazzy',
63+
'ROS_VERSION': '1',
64+
}),
65+
'filtered_build': list([
66+
'ros-jazzy-catkin',
67+
'ros-jazzy-roscpp',
68+
]),
69+
'filtered_run': list([
70+
'ros-jazzy-roscpp',
71+
]),
72+
'override_env': dict({
73+
'ROS_DISTRO': 'custom-jazzy',
74+
'ROS_VERSION': '1',
75+
}),
76+
})
77+
# ---
278
# name: test_recipe_includes_project_run_dependency
379
'''
480
context: {}

backends/pixi-build-ros/tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,15 @@ def distro():
3939
def distro_noetic():
4040
"""Reusable distro noetic fixture."""
4141
return Distro("noetic")
42+
43+
44+
@pytest.fixture
45+
def distro_variant(request, distro: Distro, distro_noetic: Distro) -> Distro:
46+
"""Parameterizable fixture that yields either a ROS1 or ROS2 distro."""
47+
match request.param:
48+
case "ros2":
49+
return distro
50+
case "ros1":
51+
return distro_noetic
52+
case other:
53+
raise ValueError(f"Unknown distro marker: {other}")

backends/pixi-build-ros/tests/test_package_xml.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,102 @@ def test_package_xml_to_recipe_config(package_xmls: Path, package_map: dict[str,
6565
assert f"ros-{distro.name}-{pkg}" in run_names
6666

6767

68+
@pytest.mark.parametrize(
69+
("distro_variant", "override_env"),
70+
[
71+
pytest.param("ros2", None, id="ros2-default"),
72+
pytest.param("ros1", None, id="ros1-default"),
73+
pytest.param("ros2", {"ROS_VERSION": "1", "ROS_DISTRO": "custom-jazzy"}, id="ros2-override-to-ros1"),
74+
pytest.param("ros1", {"ROS_VERSION": "2", "ROS_DISTRO": "custom-noetic"}, id="ros1-override-to-ros2"),
75+
],
76+
indirect=["distro_variant"],
77+
)
78+
def test_package_xml_condition_evaluation(
79+
tmp_path: Path,
80+
distro_variant: Distro,
81+
override_env: dict[str, str] | None,
82+
snapshot,
83+
):
84+
"""Ensure package.xml respect conditional dependencies. Set through ROS environment variables"""
85+
inline_package_xml = """<?xml version="1.0"?>
86+
<package format="3">
87+
<name>conditional_pkg</name>
88+
<version>0.1.0</version>
89+
<description>Conditional dependency test</description>
90+
<maintainer email="test@example.com">Tester</maintainer>
91+
<license>MIT</license>
92+
<buildtool_depend condition="$ROS_VERSION == 2">ament_cmake</buildtool_depend>
93+
<buildtool_depend condition="$ROS_VERSION == 1">catkin</buildtool_depend>
94+
<build_depend condition="$ROS_VERSION == 2">rclcpp</build_depend>
95+
<build_depend condition="$ROS_VERSION == 1">roscpp</build_depend>
96+
<exec_depend condition="$ROS_VERSION == 2">rclcpp</exec_depend>
97+
<exec_depend condition="$ROS_VERSION == 1">roscpp</exec_depend>
98+
</package>
99+
"""
100+
package_xml_path = tmp_path / "package.xml"
101+
package_xml_path.write_text(inline_package_xml, encoding="utf-8")
102+
103+
model_payload = {
104+
"name": "conditional_pkg",
105+
"version": "0.1.0",
106+
"targets": {
107+
"defaultTarget": {
108+
"hostDependencies": {},
109+
"buildDependencies": {},
110+
"runDependencies": {},
111+
},
112+
"targets": {},
113+
},
114+
}
115+
model = ProjectModelV1.from_json(json.dumps(model_payload))
116+
generator = ROSGenerator()
117+
118+
config = {"distro": distro_variant, "noarch": False}
119+
if override_env is not None:
120+
config["env"] = override_env
121+
122+
generated_recipe = generator.generate_recipe(
123+
model=model,
124+
config=config,
125+
manifest_path=str(tmp_path),
126+
host_platform=Platform("linux-64"),
127+
)
128+
129+
build_names = [dep.concrete.package_name for dep in generated_recipe.recipe.requirements.build if dep.concrete]
130+
run_names = [dep.concrete.package_name for dep in generated_recipe.recipe.requirements.run if dep.concrete]
131+
132+
# Only track the dependencies declared in the inline package.xml snippet above.
133+
declared_deps = {"ament_cmake", "catkin", "rclcpp", "roscpp"}
134+
normalized_declared_deps = {dep.replace("_", "-") for dep in declared_deps}
135+
prefix = f"ros-{distro_variant.name}-"
136+
filtered_build = sorted(
137+
dep for dep in build_names if dep.startswith(prefix) and dep.removeprefix(prefix) in normalized_declared_deps
138+
)
139+
filtered_run = sorted(
140+
dep for dep in run_names if dep.startswith(prefix) and dep.removeprefix(prefix) in normalized_declared_deps
141+
)
142+
143+
# Only use the env variables we are interested in
144+
env_repr = str(generated_recipe.recipe.build.script.env)
145+
env_dict: dict[str, str] = {}
146+
for item in env_repr.strip("{} ,").split(","):
147+
item = item.strip()
148+
if not item:
149+
continue
150+
key, value = item.split(":", 1)
151+
env_dict[key.strip()] = value.strip()
152+
153+
snapshot.assert_match(
154+
{
155+
"distro": distro_variant.name,
156+
"override_env": override_env or {},
157+
"filtered_build": filtered_build,
158+
"filtered_run": filtered_run,
159+
"env": dict(sorted(env_dict.items())),
160+
}
161+
)
162+
163+
68164
def test_ament_cmake_package_xml_to_recipe_config(
69165
package_xmls: Path, package_map: dict[str, PackageMapEntry], distro: Distro
70166
):

docs/backends/pixi-build-ros.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,19 @@ Environment variables to set during the build process. These variables are avail
150150

151151
```toml
152152
[package.build.config]
153-
env = { ROS_VERSION = "2", AMENT_CMAKE_ENVIRONMENT_HOOKS_ENABLED = "1" }
153+
env = { AMENT_CMAKE_ENVIRONMENT_HOOKS_ENABLED = "1" }
154154
```
155155

156+
#### Automatically injected environment variables
157+
158+
The ROS backend keeps the following variables in sync with the selected distro, so you do not need to set them manually in `env`:
159+
160+
- `ROS_DISTRO` &mdash; set to the distro name you configure in `distro`.
161+
- `ROS_VERSION` &mdash; set to `"1"` for ROS 1 distros and `"2"` for ROS 2 distros.
162+
163+
These values are available both while evaluating `package.xml` conditionals and during the generated build script. Any custom entries you provide in `env` are merged on top of these defaults.
164+
If you explicitly set `ROS_DISTRO` or `ROS_VERSION` in `env`, your values take precedence over the defaults.
165+
156166
### `debug-dir`
157167

158168
- **Type**: `String` (path)

0 commit comments

Comments
 (0)