Skip to content

Commit 7465500

Browse files
authored
fix(ros): pass extra-input-globs (#378)
1 parent d0fb54e commit 7465500

File tree

3 files changed

+133
-128
lines changed

3 files changed

+133
-128
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import os
2+
import pydantic
3+
import yaml
4+
from pathlib import Path
5+
from typing import Any
6+
7+
8+
def _parse_str_as_abs_path(value: str | Path, manifest_root: Path) -> Path:
9+
"""Parse a string as a Path."""
10+
# Ensure the debug directory is a Path object
11+
if isinstance(value, str):
12+
value = Path(value)
13+
# Ensure it's an absolute path
14+
if not value.is_absolute():
15+
# Convert to absolute path relative to manifest root
16+
return (manifest_root / value).resolve()
17+
return value
18+
19+
20+
PackageMapEntry = dict[str, list[str] | dict[str, list[str]]]
21+
22+
23+
class PackageMappingSource:
24+
"""Describes where additional package mapping data comes from."""
25+
26+
def __init__(self, mapping: dict[str, PackageMapEntry]):
27+
if mapping is None:
28+
raise ValueError("PackageMappingSource mapping cannot be null.")
29+
if not isinstance(mapping, dict):
30+
raise TypeError("PackageMappingSource mapping must be a dictionary.")
31+
# Copy to keep the source immutable for callers.
32+
self.mapping: dict[str, PackageMapEntry] = dict(mapping)
33+
34+
@classmethod
35+
def from_mapping(cls, mapping: dict[str, PackageMapEntry]) -> "PackageMappingSource":
36+
"""Create a source directly from a mapping dictionary."""
37+
return cls(mapping)
38+
39+
@classmethod
40+
def from_file(cls, file_path: str | Path) -> "PackageMappingSource":
41+
"""Create a source from a mapping file."""
42+
path = Path(file_path)
43+
if not path.exists():
44+
raise ValueError(f"Additional package map file '{path}' not found.")
45+
with open(path) as f:
46+
data = yaml.safe_load(f) or {}
47+
if not isinstance(data, dict):
48+
raise TypeError("Expected package map file to contain a dictionary.")
49+
return cls(data)
50+
51+
def get_package_mapping(self) -> dict[str, PackageMapEntry]:
52+
return dict(self.mapping)
53+
54+
55+
class ROSBackendConfig(pydantic.BaseModel, extra="forbid", arbitrary_types_allowed=True):
56+
"""ROS backend configuration."""
57+
58+
# ROS distribution to use, e.g., "foxy", "galactic", "humble"
59+
# TODO: This should be figured out in some other way, not from the config.
60+
distro: str
61+
62+
noarch: bool | None = None
63+
# Environment variables to set during the build
64+
env: dict[str, str] | None = None
65+
# Directory for debug files of this script
66+
debug_dir: Path | None = pydantic.Field(default=None, alias="debug-dir")
67+
# Extra input globs to include in the build hash
68+
extra_input_globs: list[str] | None = pydantic.Field(default=None, alias="extra-input-globs")
69+
70+
# Extra package mappings to use in the build
71+
extra_package_mappings: list[PackageMappingSource] = pydantic.Field(
72+
default_factory=list, alias="extra-package-mappings"
73+
)
74+
75+
def is_noarch(self) -> bool:
76+
"""Whether to build a noarch package or a platform-specific package."""
77+
return self.noarch is None or self.noarch
78+
79+
@pydantic.field_validator("debug_dir", mode="before")
80+
@classmethod
81+
def _parse_debug_dir(cls, value: Any, info: pydantic.ValidationInfo) -> Path | None:
82+
"""Parse debug directory if set."""
83+
if value is None:
84+
return None
85+
base_path = Path(os.getcwd())
86+
if info.context and "manifest_root" in info.context:
87+
base_path = Path(info.context["manifest_root"])
88+
return _parse_str_as_abs_path(value, base_path)
89+
90+
@pydantic.field_validator("extra_package_mappings", mode="before")
91+
@classmethod
92+
def _parse_package_mappings(
93+
cls, input_value: Any, info: pydantic.ValidationInfo
94+
) -> list[PackageMappingSource] | None:
95+
"""Parse additional package mappings if set."""
96+
if input_value is None:
97+
return []
98+
99+
base_path = Path(os.getcwd())
100+
if info.context and "manifest_root" in info.context:
101+
base_path = Path(info.context["manifest_root"])
102+
103+
result: list[PackageMappingSource] = []
104+
for raw_entry in input_value:
105+
# match for cases
106+
# it's already a package mapping source (usually for testing)
107+
if isinstance(raw_entry, PackageMappingSource):
108+
entry = raw_entry
109+
elif isinstance(raw_entry, dict):
110+
if "file" in raw_entry:
111+
file_value = raw_entry["file"]
112+
entry = PackageMappingSource.from_file(_parse_str_as_abs_path(file_value, base_path))
113+
elif "mapping" in raw_entry:
114+
mapping_value = raw_entry["mapping"]
115+
entry = PackageMappingSource.from_mapping(mapping_value)
116+
else:
117+
entry = PackageMappingSource.from_mapping(raw_entry)
118+
elif isinstance(raw_entry, str | Path):
119+
entry = PackageMappingSource.from_file(_parse_str_as_abs_path(raw_entry, base_path))
120+
else:
121+
raise ValueError(
122+
f"Unrecognized entry for extra-package-mappings: {raw_entry} of type {type(raw_entry)}."
123+
)
124+
result.append(entry)
125+
return result

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

Lines changed: 4 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
"""
44

55
from pathlib import Path
6-
import os
7-
import pydantic
86
from importlib.resources import files
97

108
from typing import Any
@@ -28,93 +26,8 @@
2826
convert_package_xml_to_catkin_package,
2927
get_package_xml_content,
3028
load_package_map_data,
31-
PackageMappingSource,
3229
)
33-
34-
35-
def _parse_str_as_abs_path(value: str | Path, manifest_root: Path) -> Path:
36-
"""Parse a string as a Path."""
37-
# Ensure the debug directory is a Path object
38-
if isinstance(value, str):
39-
value = Path(value)
40-
# Ensure it's an absolute path
41-
if not value.is_absolute():
42-
# Convert to absolute path relative to manifest root
43-
return (manifest_root / value).resolve()
44-
return value
45-
46-
47-
class ROSBackendConfig(pydantic.BaseModel, extra="forbid", arbitrary_types_allowed=True):
48-
"""ROS backend configuration."""
49-
50-
# TODO: This should be figured out in some other way, not from the config.
51-
distro: str
52-
53-
noarch: bool | None = None
54-
# Environment variables to set during the build
55-
env: dict[str, str] | None = None
56-
# Directory for debug files of this script
57-
debug_dir: Path | None = pydantic.Field(default=None, alias="debug-dir")
58-
# Extra input globs to include in the build hash
59-
extra_input_globs: list[str] | None = pydantic.Field(default=None, alias="extra-input-globs")
60-
# ROS distribution to use, e.g., "foxy", "galactic", "humble"
61-
62-
# Extra package mappings to use in the build
63-
extra_package_mappings: list[PackageMappingSource] = pydantic.Field(
64-
default_factory=list, alias="extra-package-mappings"
65-
)
66-
67-
def is_noarch(self) -> bool:
68-
"""Whether to build a noarch package or a platform-specific package."""
69-
return self.noarch is None or self.noarch
70-
71-
@pydantic.field_validator("debug_dir", mode="before")
72-
@classmethod
73-
def _parse_debug_dir(cls, value: Any, info: pydantic.ValidationInfo) -> Path | None:
74-
"""Parse debug directory if set."""
75-
if value is None:
76-
return None
77-
base_path = Path(os.getcwd())
78-
if info.context and "manifest_root" in info.context:
79-
base_path = Path(info.context["manifest_root"])
80-
return _parse_str_as_abs_path(value, base_path)
81-
82-
@pydantic.field_validator("extra_package_mappings", mode="before")
83-
@classmethod
84-
def _parse_package_mappings(
85-
cls, input_value: Any, info: pydantic.ValidationInfo
86-
) -> list[PackageMappingSource] | None:
87-
"""Parse additional package mappings if set."""
88-
if input_value is None:
89-
return []
90-
91-
base_path = Path(os.getcwd())
92-
if info.context and "manifest_root" in info.context:
93-
base_path = Path(info.context["manifest_root"])
94-
95-
result: list[PackageMappingSource] = []
96-
for raw_entry in input_value:
97-
# match for cases
98-
# it's already a package mapping source (usually for testing)
99-
if isinstance(raw_entry, PackageMappingSource):
100-
entry = raw_entry
101-
elif isinstance(raw_entry, dict):
102-
if "file" in raw_entry:
103-
file_value = raw_entry["file"]
104-
entry = PackageMappingSource.from_file(_parse_str_as_abs_path(file_value, base_path))
105-
elif "mapping" in raw_entry:
106-
mapping_value = raw_entry["mapping"]
107-
entry = PackageMappingSource.from_mapping(mapping_value)
108-
else:
109-
entry = PackageMappingSource.from_mapping(raw_entry)
110-
elif isinstance(raw_entry, str | Path):
111-
entry = PackageMappingSource.from_file(_parse_str_as_abs_path(raw_entry, base_path))
112-
else:
113-
raise ValueError(
114-
f"Unrecognized entry for extra-package-mappings: {raw_entry} of type {type(raw_entry)}."
115-
)
116-
result.append(entry)
117-
return result
30+
from .config import ROSBackendConfig, PackageMappingSource
11831

11932

12033
class ROSGenerator(GenerateRecipeProtocol): # type: ignore[misc] # MetadatProvider is not typed
@@ -223,9 +136,10 @@ def generate_recipe(
223136
# assert generated_recipe.recipe.build.script.content == build_script_lines, f"Script content {generated_recipe.recipe.build.script.content}, build script lines {build_script_lines}"
224137
return generated_recipe
225138

226-
def extract_input_globs_from_build(self, config: ROSBackendConfig, workdir: Path, editable: bool) -> list[str]:
139+
def extract_input_globs_from_build(self, config: dict[str, Any], workdir: Path, editable: bool) -> list[str]:
227140
"""Extract input globs for the build."""
228-
return get_build_input_globs(config, editable)
141+
ros_config = ROSBackendConfig.model_validate(config)
142+
return get_build_input_globs(ros_config, editable)
229143

230144
def default_variants(self, host_platform: Platform) -> dict[str, Any]:
231145
"""Get the default variants for the generator."""

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

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,19 @@
11
import os
22
from itertools import chain
33
from pathlib import Path
4-
from typing import Any
54

6-
import yaml
75
from catkin_pkg.package import Package as CatkinPackage, parse_package_string
86

97
from pixi_build_backend.types.intermediate_recipe import ConditionalRequirements
108
from pixi_build_backend.types.item import ItemPackageDependency
119
from pixi_build_backend.types.platform import Platform
1210
from pixi_build_ros.distro import Distro
1311

12+
from .config import PackageMapEntry, PackageMappingSource, ROSBackendConfig
1413

15-
PackageMapEntry = dict[str, list[str] | dict[str, list[str]]]
1614

17-
18-
class PackageMappingSource:
19-
"""Describes where additional package mapping data comes from."""
20-
21-
def __init__(self, mapping: dict[str, PackageMapEntry]):
22-
if mapping is None:
23-
raise ValueError("PackageMappingSource mapping cannot be null.")
24-
if not isinstance(mapping, dict):
25-
raise TypeError("PackageMappingSource mapping must be a dictionary.")
26-
# Copy to keep the source immutable for callers.
27-
self.mapping: dict[str, PackageMapEntry] = dict(mapping)
28-
29-
@classmethod
30-
def from_mapping(cls, mapping: dict[str, PackageMapEntry]) -> "PackageMappingSource":
31-
"""Create a source directly from a mapping dictionary."""
32-
return cls(mapping)
33-
34-
@classmethod
35-
def from_file(cls, file_path: str | Path) -> "PackageMappingSource":
36-
"""Create a source from a mapping file."""
37-
path = Path(file_path)
38-
if not path.exists():
39-
raise ValueError(f"Additional package map file '{path}' not found.")
40-
with open(path) as f:
41-
data = yaml.safe_load(f) or {}
42-
if not isinstance(data, dict):
43-
raise TypeError("Expected package map file to contain a dictionary.")
44-
return cls(data)
45-
46-
def get_package_mapping(self) -> dict[str, PackageMapEntry]:
47-
return dict(self.mapping)
48-
49-
50-
def get_build_input_globs(config: Any, editable: bool) -> list[str]:
15+
# Any in here means ROSBackendConfig
16+
def get_build_input_globs(config: ROSBackendConfig, editable: bool) -> list[str]:
5117
"""Get build input globs for ROS package."""
5218
base_globs = [
5319
# Source files
@@ -74,7 +40,7 @@ def get_build_input_globs(config: Any, editable: bool) -> list[str]:
7440
python_globs = [] if editable else ["**/*.py", "**/*.pyx"]
7541

7642
all_globs = base_globs + python_globs
77-
if hasattr(config, "extra_input_globs"):
43+
if hasattr(config, "extra_input_globs") and config.extra_input_globs is not None:
7844
all_globs.extend(config.extra_input_globs)
7945

8046
return all_globs

0 commit comments

Comments
 (0)