Skip to content

Commit 4606256

Browse files
chore: improve version parsing a little (#389)
Co-authored-by: Ruben Arts <ruben.arts@hotmail.com>
1 parent 5860fbb commit 4606256

File tree

3 files changed

+130
-70
lines changed

3 files changed

+130
-70
lines changed

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

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
from itertools import chain
44
from pathlib import Path
5+
from typing import Any
56

67
from catkin_pkg.package import Package as CatkinPackage, parse_package_string, Dependency
78

@@ -85,14 +86,13 @@ def load_package_map_data(package_map_sources: list[PackageMappingSource]) -> di
8586
return result
8687

8788

88-
def rosdep_to_conda_package_name(
89-
dep_name: str,
89+
def rosdep_to_conda_package_spec(
90+
dep: Dependency,
9091
distro: Distro,
9192
host_platform: Platform,
9293
package_map_data: dict[str, PackageMapEntry],
93-
spec_str: str = "",
9494
) -> list[str]:
95-
"""Convert a ROS dependency name to a conda package name."""
95+
"""Convert a ROS dependency name to a conda package spec."""
9696
if host_platform.is_linux:
9797
target_platform = "linux"
9898
elif host_platform.is_windows:
@@ -102,23 +102,25 @@ def rosdep_to_conda_package_name(
102102
else:
103103
raise RuntimeError(f"Unsupported platform: {host_platform}")
104104

105-
if dep_name not in package_map_data:
105+
spec_str = rosdep_nameless_matchspec(dep)
106+
107+
if dep.name not in package_map_data:
106108
# Package name isn't found in the package map, so we are going to assume it is a ROS package.
107-
return [f"ros-{distro.name}-{dep_name.replace('_', '-')}{spec_str or ''}"]
109+
return [f"ros-{distro.name}-{dep.name.replace('_', '-')}{spec_str or ''}"]
108110

109111
# Dependency found in package map
110112

111113
# Case 1: It's a custom ROS dependency
112-
if "ros" in package_map_data[dep_name]:
113-
return [f"ros-{distro.name}-{dep.replace('_', '-')}{spec_str}" for dep in package_map_data[dep_name]["ros"]]
114+
if "ros" in package_map_data[dep.name]:
115+
return [f"ros-{distro.name}-{dep.replace('_', '-')}{spec_str}" for dep in package_map_data[dep.name]["ros"]]
114116

115117
# Case 2: It's a custom package name
116-
elif "conda" in package_map_data[dep_name] or "robostack" in package_map_data[dep_name]:
118+
elif "conda" in package_map_data[dep.name] or "robostack" in package_map_data[dep.name]:
117119
# determine key
118-
key = "robostack" if "robostack" in package_map_data[dep_name] else "conda"
120+
key = "robostack" if "robostack" in package_map_data[dep.name] else "conda"
119121

120122
# Get the conda packages for the dependency
121-
conda_packages = package_map_data[dep_name].get(key, [])
123+
conda_packages = package_map_data[dep.name].get(key, [])
122124

123125
if isinstance(conda_packages, dict):
124126
# TODO: Handle different platforms
@@ -148,61 +150,72 @@ def rosdep_to_conda_package_name(
148150
else:
149151
raise ValueError(
150152
f"Version specifier can only be used for a package without constraint already present, "
151-
f"but found {conda_packages[0]} for {dep_name} "
153+
f"but found {conda_packages[0]} for {dep.name} "
152154
f"in the package map."
153155
)
154156
else:
155157
raise ValueError(
156158
f"Version specifier can only be used for one package, "
157-
f"but found {len(conda_packages)} packages for {dep_name} "
159+
f"but found {len(conda_packages)} packages for {dep.name} "
158160
f"in the package map."
159161
)
160162

161163
return conda_packages + additional_packages
162164
else:
163-
raise ValueError(f"Unknown package map entry: {dep_name}.")
164-
165-
166-
def _format_version_constraints_to_string(dependency: Dependency) -> str:
167-
"""Format the version constraints from a ros package.xml to a string"""
168-
for version in [
169-
dependency.version_eq,
170-
dependency.version_gte,
171-
dependency.version_gt,
172-
dependency.version_lte,
173-
dependency.version_lt,
174-
]:
165+
raise ValueError(f"Unknown package map entry: {dep.name}.")
166+
167+
168+
def rosdep_nameless_matchspec(dep: Dependency) -> str:
169+
"""Format the version constraints from a ros package.xml to a nameless matchspec"""
170+
right_ineq = [dep.version_lt, dep.version_lte]
171+
left_ineq = [dep.version_gt, dep.version_gte]
172+
eq = dep.version_eq
173+
174+
for version in left_ineq + right_ineq + [eq]:
175175
if version is None:
176176
continue
177177
elif version == "":
178178
raise ValueError(
179-
f"Incorrect version specification in package.xml: '{dependency.name}': version is empty string (\"\")"
179+
f"Incorrect version specification in package.xml: '{dep.name}': version is empty string (\"\")"
180180
)
181181
try:
182182
# check if we can parse the version
183183
Version(version)
184184
except TypeError as e:
185185
raise ValueError(
186-
f"Incorrect version specification in package.xml: '{dependency.name}' at version '{version}' "
186+
f"Incorrect version specification in package.xml: '{dep.name}' at version '{version}' "
187187
) from e
188188

189-
if dependency.version_eq:
190-
return f" =={dependency.version_eq}"
189+
def not_none(p: Any) -> bool:
190+
return p is not None
191+
192+
if all(map(not_none, right_ineq)):
193+
raise ValueError(f"Dependency {dep.name} cannot be specified by both `<` and `<=`")
194+
if all(map(not_none, left_ineq)):
195+
raise ValueError(f"Dependency {dep.name} cannot be specified by both `>` and `>=`")
196+
197+
some_inequality = any(map(lambda p: p is not None, right_ineq + left_ineq))
198+
if eq and some_inequality:
199+
raise ValueError(f"Dependency {dep.name} cannot be specified by both `=` and some inequality")
191200

192-
version_string_list = []
193-
if dependency.version_gte:
194-
version_string_list.append(f">={dependency.version_gte}")
195-
if dependency.version_gt:
196-
version_string_list.append(f">{dependency.version_gt}")
197-
if dependency.version_lte:
198-
version_string_list.append(f"<={dependency.version_lte}")
199-
if dependency.version_lt:
200-
version_string_list.append(f"<{dependency.version_lt}")
201+
if eq:
202+
return f"=={eq}"
201203

202-
if not version_string_list:
203-
return ""
204+
pair = []
204205

205-
return " " + ",".join(version_string_list)
206+
if dep.version_gt:
207+
pair.append(f">{dep.version_gt}")
208+
if dep.version_gte:
209+
pair.append(f">={dep.version_gte}")
210+
211+
if dep.version_lt:
212+
pair.append(f"<{dep.version_lt}")
213+
if dep.version_lte:
214+
pair.append(f"<={dep.version_lte}")
215+
216+
res = ",".join(pair)
217+
218+
return " " + res if len(pair) > 0 else res
206219

207220

208221
def package_xml_to_conda_requirements(
@@ -220,30 +233,22 @@ def package_xml_to_conda_requirements(
220233
build_deps += pkg.build_export_depends
221234
# Also add test dependencies, because they might be needed during build (i.e. for pytest/catch2 etc in CMake macros)
222235
build_deps += pkg.test_depends
223-
build_deps = [
224-
PackageNameWithSpec(name=d.name, spec=_format_version_constraints_to_string(d))
225-
for d in build_deps
226-
if d.evaluated_condition
227-
]
236+
build_deps = [d for d in build_deps if d.evaluated_condition]
228237
# Add the ros_workspace dependency as a default build dependency for ros2 packages
229238
if not distro.check_ros1():
230-
build_deps += [PackageNameWithSpec(name="ros_workspace")]
239+
build_deps += [Dependency(name="ros_workspace")]
231240
conda_build_deps_chain = [
232-
rosdep_to_conda_package_name(dep.name, distro, host_platform, package_map_data, dep.spec) for dep in build_deps
241+
rosdep_to_conda_package_spec(dep, distro, host_platform, package_map_data) for dep in build_deps
233242
]
234243
conda_build_deps = list(chain.from_iterable(conda_build_deps_chain))
235244

236245
run_deps = pkg.run_depends
237246
run_deps += pkg.exec_depends
238247
run_deps += pkg.build_export_depends
239248
run_deps += pkg.buildtool_export_depends
240-
run_deps = [
241-
PackageNameWithSpec(d.name, spec=_format_version_constraints_to_string(d))
242-
for d in run_deps
243-
if d.evaluated_condition
244-
]
249+
run_deps = [d for d in run_deps if d.evaluated_condition]
245250
conda_run_deps_chain = [
246-
rosdep_to_conda_package_name(dep.name, distro, host_platform, package_map_data, dep.spec) for dep in run_deps
251+
rosdep_to_conda_package_spec(dep, distro, host_platform, package_map_data) for dep in run_deps
247252
]
248253
conda_run_deps = list(chain.from_iterable(conda_run_deps_chain))
249254

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

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import tempfile
33
from pathlib import Path
44

5+
from catkin_pkg.package import Dependency
6+
57
import pytest
68
from pixi_build_backend.types.intermediate_recipe import Script
79

@@ -10,7 +12,7 @@
1012
from pixi_build_ros.utils import (
1113
convert_package_xml_to_catkin_package,
1214
package_xml_to_conda_requirements,
13-
rosdep_to_conda_package_name,
15+
rosdep_to_conda_package_spec,
1416
PackageMapEntry,
1517
)
1618
from pixi_build_backend.types.platform import Platform
@@ -197,7 +199,7 @@ def test_robostack_target_platform_linux(package_map: dict[str, PackageMapEntry]
197199
linux_platform = Platform("linux-64")
198200

199201
# Test packages with platform-specific mappings
200-
acl_packages = rosdep_to_conda_package_name("acl", distro, linux_platform, package_map)
202+
acl_packages = rosdep_to_conda_package_spec(Dependency(name="acl"), distro, linux_platform, package_map)
201203
assert acl_packages == ["libacl"], f"Expected ['libacl'] for acl on Linux, got {acl_packages}"
202204

203205

@@ -207,7 +209,7 @@ def test_robostack_target_platform_osx(package_map: dict[str, PackageMapEntry],
207209
osx_platform = Platform("osx-64")
208210

209211
# Test packages with platform-specific mappings
210-
acl_packages = rosdep_to_conda_package_name("acl", distro, osx_platform, package_map)
212+
acl_packages = rosdep_to_conda_package_spec(Dependency(name="acl"), distro, osx_platform, package_map)
211213
assert acl_packages == [], f"Expected [] for acl on macOS, got {acl_packages}"
212214

213215

@@ -217,7 +219,7 @@ def test_robostack_target_platform_windows(package_map: dict[str, PackageMapEntr
217219
win_platform = Platform("win-64")
218220

219221
# Test packages with platform-specific mappings
220-
binutils_packages = rosdep_to_conda_package_name("binutils", distro, win_platform, package_map)
222+
binutils_packages = rosdep_to_conda_package_spec(Dependency(name="binutils"), distro, win_platform, package_map)
221223
assert binutils_packages == [], f"Expected [] for binutils on Windows, got {binutils_packages}"
222224

223225

@@ -228,9 +230,9 @@ def test_robostack_target_platform_cross_platform(package_map: dict[str, Package
228230
win_platform = Platform("win-64")
229231

230232
# libudev-dev has different packages for each platform
231-
linux_udev = rosdep_to_conda_package_name("libudev-dev", distro, linux_platform, package_map)
232-
osx_udev = rosdep_to_conda_package_name("libudev-dev", distro, osx_platform, package_map)
233-
win_udev = rosdep_to_conda_package_name("libudev-dev", distro, win_platform, package_map)
233+
linux_udev = rosdep_to_conda_package_spec(Dependency(name="libudev-dev"), distro, linux_platform, package_map)
234+
osx_udev = rosdep_to_conda_package_spec(Dependency(name="libudev-dev"), distro, osx_platform, package_map)
235+
win_udev = rosdep_to_conda_package_spec(Dependency(name="libudev-dev"), distro, win_platform, package_map)
234236

235237
assert linux_udev == [
236238
"libusb",
@@ -240,9 +242,9 @@ def test_robostack_target_platform_cross_platform(package_map: dict[str, Package
240242
assert win_udev == ["libusb"], f"Expected ['libusb'] for libudev-dev on Windows, got {win_udev}"
241243

242244
# libomp-dev has different OpenMP implementations per platform
243-
linux_omp = rosdep_to_conda_package_name("libomp-dev", distro, linux_platform, package_map)
244-
osx_omp = rosdep_to_conda_package_name("libomp-dev", distro, osx_platform, package_map)
245-
win_omp = rosdep_to_conda_package_name("libomp-dev", distro, win_platform, package_map)
245+
linux_omp = rosdep_to_conda_package_spec(Dependency(name="libomp-dev"), distro, linux_platform, package_map)
246+
osx_omp = rosdep_to_conda_package_spec(Dependency(name="libomp-dev"), distro, osx_platform, package_map)
247+
win_omp = rosdep_to_conda_package_spec(Dependency(name="libomp-dev"), distro, win_platform, package_map)
246248

247249
assert linux_omp == ["libgomp"], f"Expected ['libgomp'] for libomp-dev on Linux, got {linux_omp}"
248250
assert osx_omp == ["llvm-openmp"], f"Expected ['llvm-openmp'] for libomp-dev on macOS, got {osx_omp}"
@@ -256,9 +258,9 @@ def test_robostack_require_opengl_handling(package_map: dict[str, PackageMapEntr
256258
win_platform = Platform("win-64")
257259

258260
# opengl package has REQUIRE_OPENGL handling
259-
linux_opengl = rosdep_to_conda_package_name("opengl", distro, linux_platform, package_map)
260-
osx_opengl = rosdep_to_conda_package_name("opengl", distro, osx_platform, package_map)
261-
win_opengl = rosdep_to_conda_package_name("opengl", distro, win_platform, package_map)
261+
linux_opengl = rosdep_to_conda_package_spec(Dependency(name="opengl"), distro, linux_platform, package_map)
262+
osx_opengl = rosdep_to_conda_package_spec(Dependency(name="opengl"), distro, osx_platform, package_map)
263+
win_opengl = rosdep_to_conda_package_spec(Dependency(name="opengl"), distro, win_platform, package_map)
262264

263265
# According to the code, REQUIRE_OPENGL should be replaced with actual packages on Linux
264266
# and should add xorg packages for linux/osx/unix platforms
@@ -279,12 +281,66 @@ def test_robostack_require_opengl_handling(package_map: dict[str, PackageMapEntr
279281
def test_spec_with_entry_in_map(package_map: dict[str, PackageMapEntry], distro: Distro):
280282
"""Test using a specifier string with a package which has already defined one in the package map"""
281283
with pytest.raises(ValueError) as excinfo:
282-
rosdep_to_conda_package_name("xtensor", distro, Platform.current(), package_map, spec_str="==2.0")
284+
rosdep_to_conda_package_spec(
285+
Dependency(name="xtensor", version_eq="2.0"), distro, Platform.current(), package_map
286+
)
283287
assert "Version specifier can only be used for a package without constraint already present" in str(excinfo)
284288

285289

286290
def test_spec_with_multiple_entries_in_map(package_map: dict[str, PackageMapEntry], distro: Distro):
287291
"""Test using a specifier string with a package which has multiple packages defined one in the package map"""
288292
with pytest.raises(ValueError) as excinfo:
289-
rosdep_to_conda_package_name("boost", distro, Platform.current(), package_map, spec_str="==2.0")
293+
rosdep_to_conda_package_spec(
294+
Dependency(name="boost", version_eq="2.0"), distro, Platform.current(), package_map
295+
)
290296
assert "Version specifier can only be used for one package" in str(excinfo)
297+
298+
299+
def test_rosdep_to_conda_package_spec_doesnt_add_matchspec_for_special_rosdeps():
300+
distro = Distro("jazzy")
301+
host_platform = Platform("linux-64")
302+
dep = Dependency(name="ament_cmake", version_eq="1.2.3")
303+
304+
packages = rosdep_to_conda_package_spec(dep, distro, host_platform, {})
305+
306+
assert packages == ["ros-jazzy-ament-cmake==1.2.3"]
307+
308+
309+
def test_rosdep_to_conda_package_spec_adds_matchspec_for_distro_packages():
310+
distro = Distro("jazzy")
311+
host_platform = Platform("linux-64")
312+
dep = Dependency(name="rclcpp", version_gte="18.0.0", version_lt="20.0.0")
313+
314+
packages = rosdep_to_conda_package_spec(dep, distro, host_platform, {})
315+
316+
assert packages == ["ros-jazzy-rclcpp >=18.0.0,<20.0.0"]
317+
318+
319+
def test_rosdep_to_conda_package_spec_adds_matchspec_for_ros_package_map_entries():
320+
distro = Distro("jazzy")
321+
host_platform = Platform("linux-64")
322+
package_map: dict[str, PackageMapEntry] = {"custom_ros_dep": {"ros": ["foo_util"]}}
323+
dep = Dependency(name="custom_ros_dep", version_gte="3.1")
324+
325+
packages = rosdep_to_conda_package_spec(dep, distro, host_platform, package_map)
326+
327+
assert packages == ["ros-jazzy-foo-util >=3.1"]
328+
329+
330+
def test_rosdep_to_conda_package_spec_adds_matchspec_for_unknown_dependencies():
331+
distro = Distro("jazzy")
332+
host_platform = Platform("linux-64")
333+
dep = Dependency(name="customlib", version_gt="1.0.0")
334+
335+
packages = rosdep_to_conda_package_spec(dep, distro, host_platform, {})
336+
337+
assert packages == ["ros-jazzy-customlib >1.0.0"]
338+
339+
340+
def test_rosdep_to_conda_package_spec_exception():
341+
distro = Distro("jazzy")
342+
host_platform = Platform("linux-64")
343+
dep = Dependency(name="rclcpp", version_gt="18.0.0", version_gte="20.0.0")
344+
345+
with pytest.raises(ValueError, match=".* `>` and `>=`"):
346+
rosdep_to_conda_package_spec(dep, distro, host_platform, {})

py-pixi-build-backend/src/types/generated_recipe.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@ impl GenerateRecipe for PyGenerateRecipe {
254254
) -> miette::Result<BTreeSet<String>> {
255255
Python::attach(|py| {
256256
let workdir = workdir.as_ref();
257-
let editable = editable;
258257

259258
// we don't pass the wrapper but the python inner model directly
260259
let py_object = config.model.clone();

0 commit comments

Comments
 (0)