Skip to content

Commit fca546e

Browse files
authored
fix: if a spec is source, don't merge but force the source (#405)
1 parent a7654d9 commit fca546e

File tree

7 files changed

+587
-437
lines changed

7 files changed

+587
-437
lines changed

backends/pixi-build-ros/pixi.lock

Lines changed: 472 additions & 363 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backends/pixi-build-ros/pixi.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ scripts = ["../../scripts/activate.sh"]
2020
[dependencies]
2121
pydantic = ">=2.8.2,<3"
2222
py_rattler = ">=0.15.0,<0.16"
23-
pixi-build-ros = { path = "." }
23+
py-pixi-build-backend = "*"
24+
25+
[pypi-dependencies]
26+
pixi-build-ros = { path = ".", editable = true }
2427

2528
[environments]
2629
default = { features = ["test", "lint"], solve-group = "default" }
@@ -74,5 +77,5 @@ pixi-build-api-version = ">=2,<3"
7477
# should be added to `py-pixi-build-backend`
7578
typing-extensions = "*"
7679
# this depends has to match the test dependency, so switch comments if needed
77-
# py-pixi-build-backend = "*"
80+
#py-pixi-build-backend = "*"
7881
py-pixi-build-backend = { path = "../../py-pixi-build-backend" }

backends/pixi-build-ros/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ dependencies = [
77
"pyyaml",
88
"pydantic",
99
"py_rattler",
10-
"py-pixi-build-backend",
10+
# TODO: publish to pypi
11+
# "py-pixi-build-backend",
1112
]
1213
name = "pixi-build-ros"
1314
version = "0.3.0"

backends/pixi-build-ros/robostack.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,8 @@ liblz4-dev:
358358
robostack: [lz4]
359359
libmicrohttpd:
360360
robostack: [libmicrohttpd]
361+
libnanoflann-dev:
362+
robostack: [nanoflann]
361363
libncurses-dev:
362364
robostack:
363365
linux: [ncurses]
@@ -904,10 +906,14 @@ python3-tk:
904906
robostack: [tk]
905907
python3-tornado:
906908
robostack: [tornado]
909+
python3-transforms3d:
910+
robostack: [transforms3d]
907911
python3-twisted:
908912
robostack: [twisted]
909913
python3-typeguard:
910914
robostack: [typeguard]
915+
python3-types-pyyaml:
916+
robostack: [types-pyyaml]
911917
python3-unidiff:
912918
robostack: [unidiff]
913919
python3-usb:

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

Lines changed: 4 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Python generator implementation using Python bindings.
2+
ROS generator implementation using Python bindings.
33
"""
44

55
from pathlib import Path
@@ -11,9 +11,9 @@
1111
GeneratedRecipe,
1212
)
1313
from .metadata_provider import ROSPackageXmlMetadataProvider
14-
from pixi_build_backend.types.intermediate_recipe import Script, ConditionalRequirements
14+
from pixi_build_backend.types.intermediate_recipe import Script
1515

16-
from pixi_build_backend.types.item import ItemPackageDependency, VecItemPackageDependency
16+
from pixi_build_backend.types.item import ItemPackageDependency
1717
from pixi_build_backend.types.platform import Platform
1818
from pixi_build_backend.types.project_model import ProjectModelV1
1919
from pixi_build_backend.types.python_params import PythonParams
@@ -25,6 +25,7 @@
2525
convert_package_xml_to_catkin_package,
2626
get_package_xml_content,
2727
load_package_map_data,
28+
merge_requirements,
2829
)
2930
from .config import ROSBackendConfig, PackageMappingSource
3031

@@ -153,69 +154,3 @@ def default_variants(self, host_platform: Platform) -> dict[str, Any]:
153154
if host_platform.is_windows:
154155
variants["cxx_compiler"] = ["vs2019"]
155156
return variants
156-
157-
158-
def merge_requirements(
159-
model_requirements: ConditionalRequirements,
160-
package_requirements: ConditionalRequirements,
161-
) -> ConditionalRequirements:
162-
"""Merge two sets of requirements."""
163-
merged = ConditionalRequirements()
164-
165-
merged.host = merge_unique_items(model_requirements.host, package_requirements.host)
166-
merged.build = merge_unique_items(model_requirements.build, package_requirements.build)
167-
merged.run = merge_unique_items(model_requirements.run, package_requirements.run)
168-
169-
# If the dependency is of type Source in one of the requirements, we need to set them to Source for all variants
170-
return merged
171-
172-
173-
def merge_unique_items(
174-
model: list[ItemPackageDependency] | VecItemPackageDependency,
175-
package: list[ItemPackageDependency] | VecItemPackageDependency,
176-
) -> list[ItemPackageDependency]:
177-
"""Merge unique items from source into target."""
178-
179-
def _find_matching(list_to_find: list[ItemPackageDependency], name: str) -> ItemPackageDependency | None:
180-
for dep in list_to_find:
181-
if dep.concrete.package_name == name:
182-
return dep
183-
else:
184-
return None
185-
186-
def _merge_specs(spec1: str, spec2: str, package_name: str) -> str:
187-
# remove the package name
188-
version_spec1 = spec1.removeprefix(package_name).strip()
189-
version_spec2 = spec2.removeprefix(package_name).strip()
190-
191-
if " " in version_spec1 or " " in version_spec2:
192-
raise ValueError(f"{version_spec1}, or {version_spec2} contains spaces, cannot merge specifiers.")
193-
194-
# early out with *, empty or ==
195-
if version_spec1 in ["*", ""] or "==" in version_spec2 or version_spec1 == version_spec2:
196-
return spec2
197-
if version_spec2 in ["*", ""] or "==" in version_spec1:
198-
return spec1
199-
return package_name + " " + ",".join([version_spec1, version_spec2])
200-
201-
result: list[ItemPackageDependency] = []
202-
templates_in_model = [str(i.template) for i in model]
203-
for item in list(model) + list(package):
204-
# It's concrete (i.e. no template)
205-
if item.concrete is not None:
206-
# It does not exist yet in model
207-
item_in_result = _find_matching(result, item.concrete.package_name)
208-
if not item_in_result:
209-
result.append(item)
210-
else:
211-
new_dep = ItemPackageDependency(
212-
name=_merge_specs(
213-
item_in_result.concrete.binary_spec, item.concrete.binary_spec, item.concrete.package_name
214-
)
215-
)
216-
result.remove(item_in_result)
217-
result.append(new_dep)
218-
219-
elif str(item.template) not in templates_in_model:
220-
result.append(item)
221-
return result

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

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from catkin_pkg.package import Package as CatkinPackage, parse_package_string, Dependency
88

99
from pixi_build_backend.types.intermediate_recipe import ConditionalRequirements
10-
from pixi_build_backend.types.item import ItemPackageDependency
10+
from pixi_build_backend.types.item import ItemPackageDependency, VecItemPackageDependency
1111
from pixi_build_backend.types.platform import Platform
1212
from pixi_build_ros.distro import Distro
1313
from rattler import Version
@@ -262,3 +262,85 @@ def package_xml_to_conda_requirements(
262262
cond.run = run_requirements
263263

264264
return cond
265+
266+
267+
def find_matching(list_to_find: list[ItemPackageDependency], name: str) -> ItemPackageDependency | None:
268+
for dep in list_to_find:
269+
if dep.concrete.package_name == name:
270+
return dep
271+
else:
272+
return None
273+
274+
275+
def normalize_spec(spec: str | None, package_name: str) -> str:
276+
"""Normalize a spec by removing package name and handling None."""
277+
if not spec:
278+
return ""
279+
return spec.removeprefix(package_name).strip()
280+
281+
282+
def merge_specs(spec1: str | None, spec2: str | None, package_name: str) -> str:
283+
# remove the package name
284+
version_spec1 = normalize_spec(spec1, package_name)
285+
version_spec2 = normalize_spec(spec2, package_name)
286+
287+
if " " in version_spec1 or " " in version_spec2:
288+
raise ValueError(f"{version_spec1}, or {version_spec2} contains spaces, cannot merge specifiers.")
289+
290+
# early out with *, empty or ==
291+
if version_spec1 in ["*", ""] or "==" in version_spec2 or version_spec1 == version_spec2:
292+
return spec2 or ""
293+
if version_spec2 in ["*", ""] or "==" in version_spec1:
294+
return spec1 or ""
295+
return package_name + " " + ",".join([version_spec1, version_spec2])
296+
297+
298+
def merge_unique_items(
299+
model: list[ItemPackageDependency] | VecItemPackageDependency,
300+
package: list[ItemPackageDependency] | VecItemPackageDependency,
301+
) -> list[ItemPackageDependency]:
302+
"""Merge unique items from source into target."""
303+
304+
result: list[ItemPackageDependency] = []
305+
templates_in_model = [str(i.template) for i in model]
306+
for item in list(model) + list(package):
307+
# It's concrete (i.e. no template)
308+
if item.concrete is not None:
309+
# It does not exist yet in model
310+
item_in_result = find_matching(result, item.concrete.package_name)
311+
if not item_in_result:
312+
result.append(item)
313+
elif item_in_result.concrete.is_source:
314+
# If the existing dependency is source, don't merge - keep the source one
315+
continue
316+
elif item.concrete.is_source:
317+
# If a new item is source, replace the existing one
318+
result = [dep for dep in result if dep.concrete.package_name != item.concrete.package_name]
319+
result.append(item)
320+
else:
321+
new_dep = ItemPackageDependency(
322+
name=merge_specs(
323+
item_in_result.concrete.binary_spec, item.concrete.binary_spec, item.concrete.package_name
324+
)
325+
)
326+
result.remove(item_in_result)
327+
result.append(new_dep)
328+
329+
elif str(item.template) not in templates_in_model:
330+
result.append(item)
331+
return result
332+
333+
334+
def merge_requirements(
335+
model_requirements: ConditionalRequirements,
336+
package_requirements: ConditionalRequirements,
337+
) -> ConditionalRequirements:
338+
"""Merge two sets of requirements."""
339+
merged = ConditionalRequirements()
340+
341+
merged.host = merge_unique_items(model_requirements.host, package_requirements.host)
342+
merged.build = merge_unique_items(model_requirements.build, package_requirements.build)
343+
merged.run = merge_unique_items(model_requirements.run, package_requirements.run)
344+
345+
# If the dependency is of type Source in one of the requirements, we need to set them to Source for all variants
346+
return merged

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
from pixi_build_backend.types.item import ItemPackageDependency
3-
from pixi_build_ros.ros_generator import merge_unique_items
3+
from pixi_build_ros.utils import merge_unique_items
44

55

66
def test_with_star_items():
@@ -40,3 +40,17 @@ def test_specs_with_spaces():
4040
with pytest.raises(ValueError) as exc:
4141
merge_unique_items(list1, list2)
4242
assert "contains spaces" in str(exc)
43+
44+
45+
def test_specs_with_none():
46+
list1 = [ItemPackageDependency("ros-noetic")]
47+
list2 = [ItemPackageDependency("ros-noetic <=2.0,<3.0")]
48+
result = merge_unique_items(list1, list2)
49+
assert result[0].concrete.binary_spec == list2[0].concrete.binary_spec
50+
51+
52+
def test_specs_with_source():
53+
list1 = [ItemPackageDependency('ros-noetic[url="https://blabla"]')]
54+
list2 = [ItemPackageDependency("ros-noetic <=2.0,<3.0")]
55+
result = merge_unique_items(list1, list2)
56+
assert str(result[0].concrete.source_spec.spec) == str(list1[0].concrete.source_spec.spec)

0 commit comments

Comments
 (0)