pixi-ros 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pixi_ros/cli.py +44 -31
- pixi_ros/data/conda-forge.yaml +6 -0
- pixi_ros/init.py +395 -206
- pixi_ros/mappings.py +39 -26
- pixi_ros/package_xml.py +69 -8
- pixi_ros/validator.py +245 -0
- {pixi_ros-0.2.0.dist-info → pixi_ros-0.4.0.dist-info}/METADATA +118 -25
- pixi_ros-0.4.0.dist-info/RECORD +17 -0
- pixi_ros-0.2.0.dist-info/RECORD +0 -16
- {pixi_ros-0.2.0.dist-info → pixi_ros-0.4.0.dist-info}/WHEEL +0 -0
- {pixi_ros-0.2.0.dist-info → pixi_ros-0.4.0.dist-info}/entry_points.txt +0 -0
- {pixi_ros-0.2.0.dist-info → pixi_ros-0.4.0.dist-info}/licenses/LICENSE +0 -0
pixi_ros/mappings.py
CHANGED
|
@@ -103,7 +103,11 @@ def reload_mappings():
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def map_ros_to_conda(
|
|
106
|
-
ros_package: str,
|
|
106
|
+
ros_package: str,
|
|
107
|
+
distro: str = "humble",
|
|
108
|
+
platform_override: str | None = None,
|
|
109
|
+
validator=None,
|
|
110
|
+
workspace_packages: set[str] | None = None,
|
|
107
111
|
) -> list[str]:
|
|
108
112
|
"""
|
|
109
113
|
Map a ROS package name to its conda package names.
|
|
@@ -116,6 +120,8 @@ def map_ros_to_conda(
|
|
|
116
120
|
ros_package: The ROS package name (e.g., "rclcpp", "udev", "opengl")
|
|
117
121
|
distro: The ROS distribution (e.g., "humble", "iron", "jazzy")
|
|
118
122
|
platform_override: Override platform detection (for testing)
|
|
123
|
+
validator: Optional RosDistroValidator instance for validation
|
|
124
|
+
workspace_packages: Optional set of workspace package names
|
|
119
125
|
|
|
120
126
|
Returns:
|
|
121
127
|
List of conda package names, which may include placeholder strings
|
|
@@ -130,6 +136,30 @@ def map_ros_to_conda(
|
|
|
130
136
|
>>> map_ros_to_conda("opengl", "humble") # doctest: +SKIP
|
|
131
137
|
['REQUIRE_OPENGL'] # placeholder from mapping file
|
|
132
138
|
"""
|
|
139
|
+
# If validator is provided, use full validation logic
|
|
140
|
+
if validator is not None:
|
|
141
|
+
mappings = get_mappings()
|
|
142
|
+
ws_packages = workspace_packages or set()
|
|
143
|
+
|
|
144
|
+
# Determine the platform to use for validation
|
|
145
|
+
if platform_override:
|
|
146
|
+
# Convert mapping platform to pixi platform for validator
|
|
147
|
+
mapping_to_pixi = {
|
|
148
|
+
"linux": "linux-64",
|
|
149
|
+
"osx": "osx-64",
|
|
150
|
+
"win64": "win-64",
|
|
151
|
+
}
|
|
152
|
+
platform = mapping_to_pixi.get(platform_override, "linux-64")
|
|
153
|
+
else:
|
|
154
|
+
# Use current platform
|
|
155
|
+
platform = str(Platform.current())
|
|
156
|
+
|
|
157
|
+
result = validator.validate_package(
|
|
158
|
+
ros_package, ws_packages, mappings, platform
|
|
159
|
+
)
|
|
160
|
+
return result.conda_packages
|
|
161
|
+
|
|
162
|
+
# Legacy behavior (backward compatibility)
|
|
133
163
|
mappings = get_mappings()
|
|
134
164
|
current_platform = platform_override or _detect_platform()
|
|
135
165
|
|
|
@@ -257,42 +287,25 @@ def is_system_package(package_name: str) -> bool:
|
|
|
257
287
|
|
|
258
288
|
def get_platforms() -> list[str]:
|
|
259
289
|
"""
|
|
260
|
-
Get list of supported pixi platforms
|
|
261
|
-
|
|
262
|
-
Extracts platform names from the mapping data and converts them to
|
|
263
|
-
standard pixi platform names.
|
|
264
|
-
|
|
265
|
-
Mapping files use: linux, osx, win64
|
|
266
|
-
Pixi uses: linux-64, osx-64, osx-arm64, win-64
|
|
290
|
+
Get list of supported pixi platforms, including current platform.
|
|
267
291
|
|
|
268
292
|
Returns:
|
|
269
293
|
List of pixi platform names
|
|
270
294
|
"""
|
|
271
|
-
mappings = get_mappings()
|
|
272
|
-
mapping_platforms = set()
|
|
273
295
|
|
|
274
|
-
#
|
|
275
|
-
|
|
276
|
-
for channel_mapping in package_mappings.values():
|
|
277
|
-
if isinstance(channel_mapping, dict):
|
|
278
|
-
# This is a platform-specific mapping
|
|
279
|
-
mapping_platforms.update(channel_mapping.keys())
|
|
296
|
+
# Hardcoded supported platforms, as a hint for the user.
|
|
297
|
+
pixi_platforms = ["linux-64", "linux-aarch64", "osx-64", "osx-arm64", "win-64"]
|
|
280
298
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
pixi_platforms.append("linux-64")
|
|
285
|
-
if "osx" in mapping_platforms:
|
|
286
|
-
pixi_platforms.extend(["osx-64", "osx-arm64"])
|
|
287
|
-
if "win64" in mapping_platforms or "win" in mapping_platforms:
|
|
288
|
-
pixi_platforms.append("win-64")
|
|
299
|
+
platform_str = str(Platform.current())
|
|
300
|
+
if not any(platform_str in p for p in pixi_platforms):
|
|
301
|
+
pixi_platforms.append(platform_str)
|
|
289
302
|
|
|
290
|
-
return pixi_platforms
|
|
303
|
+
return pixi_platforms
|
|
291
304
|
|
|
292
305
|
|
|
293
306
|
def get_ros_distros() -> list[str]:
|
|
294
307
|
"""
|
|
295
|
-
Get list of
|
|
308
|
+
Get list of known ROS distributions.
|
|
296
309
|
|
|
297
310
|
Returns:
|
|
298
311
|
List of ROS distro names
|
pixi_ros/package_xml.py
CHANGED
|
@@ -33,6 +33,11 @@ class PackageXML:
|
|
|
33
33
|
# Generic depends (shorthand for build, export, and exec)
|
|
34
34
|
depends: list[str] = field(default_factory=list)
|
|
35
35
|
|
|
36
|
+
# Version constraints for dependencies
|
|
37
|
+
# Maps package name to version constraint string
|
|
38
|
+
# (e.g., ">=3.12.4", ">=1.8.0,<2.0.0")
|
|
39
|
+
dependency_versions: dict[str, str] = field(default_factory=dict)
|
|
40
|
+
|
|
36
41
|
@classmethod
|
|
37
42
|
def from_file(cls, path: Path) -> "PackageXML":
|
|
38
43
|
"""
|
|
@@ -90,6 +95,36 @@ class PackageXML:
|
|
|
90
95
|
build_type = build_type_elem.text
|
|
91
96
|
|
|
92
97
|
# Extract dependencies
|
|
98
|
+
def parse_version_constraint(elem) -> str | None:
|
|
99
|
+
"""
|
|
100
|
+
Parse version constraint attributes from a dependency element.
|
|
101
|
+
|
|
102
|
+
Converts ROS package.xml version attributes to conda/pixi constraint syntax:
|
|
103
|
+
- version_lt="X" → <X
|
|
104
|
+
- version_lte="X" → <=X
|
|
105
|
+
- version_eq="X" → ==X
|
|
106
|
+
- version_gte="X" → >=X
|
|
107
|
+
- version_gt="X" → >X
|
|
108
|
+
|
|
109
|
+
Multiple constraints are combined with commas.
|
|
110
|
+
"""
|
|
111
|
+
constraints = []
|
|
112
|
+
|
|
113
|
+
version_attrs = [
|
|
114
|
+
("version_lt", "<"),
|
|
115
|
+
("version_lte", "<="),
|
|
116
|
+
("version_eq", "=="),
|
|
117
|
+
("version_gte", ">="),
|
|
118
|
+
("version_gt", ">"),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for attr, op in version_attrs:
|
|
122
|
+
value = elem.get(attr)
|
|
123
|
+
if value:
|
|
124
|
+
constraints.append(f"{op}{value}")
|
|
125
|
+
|
|
126
|
+
return ",".join(constraints) if constraints else None
|
|
127
|
+
|
|
93
128
|
def get_deps(tag: str) -> list[str]:
|
|
94
129
|
"""Extract all dependencies with the given tag."""
|
|
95
130
|
deps = []
|
|
@@ -98,16 +133,41 @@ class PackageXML:
|
|
|
98
133
|
deps.append(elem.text.strip())
|
|
99
134
|
return deps
|
|
100
135
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
def get_deps_with_versions(tag: str, version_map: dict[str, str]) -> list[str]:
|
|
137
|
+
"""Extract dependencies and populate version constraints."""
|
|
138
|
+
deps = []
|
|
139
|
+
for elem in root.findall(tag):
|
|
140
|
+
if elem.text:
|
|
141
|
+
pkg_name = elem.text.strip()
|
|
142
|
+
deps.append(pkg_name)
|
|
143
|
+
|
|
144
|
+
# Parse version constraint if present
|
|
145
|
+
constraint = parse_version_constraint(elem)
|
|
146
|
+
if constraint:
|
|
147
|
+
# If package already has a constraint, combine them
|
|
148
|
+
if pkg_name in version_map:
|
|
149
|
+
version_map[pkg_name] = (
|
|
150
|
+
f"{version_map[pkg_name]},{constraint}"
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
version_map[pkg_name] = constraint
|
|
154
|
+
return deps
|
|
155
|
+
|
|
156
|
+
# Parse all dependency types and collect version constraints
|
|
157
|
+
dependency_versions: dict[str, str] = {}
|
|
158
|
+
buildtool_depends = get_deps_with_versions(
|
|
159
|
+
"buildtool_depend", dependency_versions
|
|
160
|
+
)
|
|
161
|
+
build_depends = get_deps_with_versions("build_depend", dependency_versions)
|
|
162
|
+
build_export_depends = get_deps_with_versions(
|
|
163
|
+
"build_export_depend", dependency_versions
|
|
164
|
+
)
|
|
165
|
+
exec_depends = get_deps_with_versions("exec_depend", dependency_versions)
|
|
166
|
+
test_depends = get_deps_with_versions("test_depend", dependency_versions)
|
|
167
|
+
depends = get_deps_with_versions("depend", dependency_versions)
|
|
108
168
|
|
|
109
169
|
# Format 2 compatibility
|
|
110
|
-
run_depends =
|
|
170
|
+
run_depends = get_deps_with_versions("run_depend", dependency_versions)
|
|
111
171
|
|
|
112
172
|
return cls(
|
|
113
173
|
name=name_elem.text.strip(),
|
|
@@ -126,6 +186,7 @@ class PackageXML:
|
|
|
126
186
|
test_depends=test_depends,
|
|
127
187
|
run_depends=run_depends,
|
|
128
188
|
depends=depends,
|
|
189
|
+
dependency_versions=dependency_versions,
|
|
129
190
|
)
|
|
130
191
|
|
|
131
192
|
def get_all_build_dependencies(self) -> list[str]:
|
pixi_ros/validator.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""ROS package validation logic."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from rattler import Channel, Gateway, Platform
|
|
8
|
+
from rosdistro import get_cached_distribution, get_index, get_index_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PackageSource(Enum):
|
|
12
|
+
"""Source of a package."""
|
|
13
|
+
|
|
14
|
+
WORKSPACE = "workspace"
|
|
15
|
+
MAPPING = "mapping"
|
|
16
|
+
ROS_DISTRO = "ros_distro"
|
|
17
|
+
CONDA_FORGE = "conda_forge"
|
|
18
|
+
NOT_FOUND = "not_found"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class PackageValidationResult:
|
|
23
|
+
"""Result of package validation."""
|
|
24
|
+
|
|
25
|
+
package_name: str
|
|
26
|
+
source: PackageSource
|
|
27
|
+
conda_packages: list[str]
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RosDistroValidator:
|
|
32
|
+
"""Validator for ROS packages using rosdistro."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, distro_name: str):
|
|
35
|
+
"""
|
|
36
|
+
Initialize validator with ROS distribution.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
distro_name: ROS distribution name (e.g., "humble", "jazzy")
|
|
40
|
+
"""
|
|
41
|
+
self.distro_name = distro_name
|
|
42
|
+
self._distro = None
|
|
43
|
+
self._init_error = None
|
|
44
|
+
self._conda_forge_cache = {}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
index = get_index(get_index_url())
|
|
48
|
+
self._distro = get_cached_distribution(index, distro_name)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self._init_error = str(e)
|
|
51
|
+
|
|
52
|
+
def has_package(self, package_name: str) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Check if package exists in ROS distribution.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
package_name: ROS package name
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if package exists in distribution
|
|
61
|
+
"""
|
|
62
|
+
if self._distro is None:
|
|
63
|
+
return False
|
|
64
|
+
return package_name in self._distro.release_packages
|
|
65
|
+
|
|
66
|
+
def check_package_availability(
|
|
67
|
+
self, package_name: str, platform: str, channel_url: str
|
|
68
|
+
) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if package is available in the specified channel.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
package_name: Conda package name
|
|
74
|
+
platform: Platform string (e.g., "linux-64", "osx-arm64")
|
|
75
|
+
channel_url: Channel URL to check (e.g., "https://prefix.dev/conda-forge")
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if package is available in the channel
|
|
79
|
+
"""
|
|
80
|
+
# Check cache first
|
|
81
|
+
cache_key = (package_name, platform, channel_url)
|
|
82
|
+
if cache_key in self._conda_forge_cache:
|
|
83
|
+
return self._conda_forge_cache[cache_key]
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
gateway = Gateway()
|
|
87
|
+
channel = Channel(channel_url)
|
|
88
|
+
platform_obj = Platform(platform)
|
|
89
|
+
noarch_obj = Platform("noarch")
|
|
90
|
+
|
|
91
|
+
# Query with 10 second timeout, check both platform and noarch
|
|
92
|
+
repo_data = asyncio.wait_for(
|
|
93
|
+
gateway.query(
|
|
94
|
+
[channel],
|
|
95
|
+
[platform_obj, noarch_obj],
|
|
96
|
+
specs=[package_name],
|
|
97
|
+
recursive=False,
|
|
98
|
+
),
|
|
99
|
+
timeout=10.0,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Check if any records match
|
|
103
|
+
results = asyncio.run(repo_data)
|
|
104
|
+
for channel_records in results:
|
|
105
|
+
for record in channel_records:
|
|
106
|
+
if record.name.normalized == package_name.lower():
|
|
107
|
+
self._conda_forge_cache[cache_key] = True
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
self._conda_forge_cache[cache_key] = False
|
|
111
|
+
return False
|
|
112
|
+
except (asyncio.TimeoutError, Exception):
|
|
113
|
+
# On error or timeout, assume not available
|
|
114
|
+
self._conda_forge_cache[cache_key] = False
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def check_conda_forge_availability(self, package_name: str, platform: str) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Check if package is available on conda-forge.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
package_name: Conda package name
|
|
123
|
+
platform: Platform string (e.g., "linux-64", "osx-arm64")
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if package is available on conda-forge
|
|
127
|
+
"""
|
|
128
|
+
return self.check_package_availability(
|
|
129
|
+
package_name, platform, "https://prefix.dev/conda-forge"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def validate_package(
|
|
133
|
+
self,
|
|
134
|
+
package_name: str,
|
|
135
|
+
workspace_packages: set[str],
|
|
136
|
+
mappings: dict[str, dict[str, list[str] | dict[str, list[str]]]],
|
|
137
|
+
platform: str = "linux-64",
|
|
138
|
+
) -> PackageValidationResult:
|
|
139
|
+
"""
|
|
140
|
+
Validate a ROS package and determine its source.
|
|
141
|
+
|
|
142
|
+
Process:
|
|
143
|
+
1. Determine source (workspace/mapping/ros_distro/conda_forge/not_found)
|
|
144
|
+
2. Validate that packages actually exist in their expected channels
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
package_name: ROS package name
|
|
148
|
+
workspace_packages: Set of package names in the workspace
|
|
149
|
+
mappings: Package mappings from mapping files
|
|
150
|
+
platform: Target platform (default: "linux-64")
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
PackageValidationResult with source and conda package names
|
|
154
|
+
"""
|
|
155
|
+
# Step 1: Determine source without validation
|
|
156
|
+
source = None
|
|
157
|
+
conda_packages = []
|
|
158
|
+
|
|
159
|
+
# 1. Check if it's a workspace package
|
|
160
|
+
if package_name in workspace_packages:
|
|
161
|
+
return PackageValidationResult(
|
|
162
|
+
package_name=package_name,
|
|
163
|
+
source=PackageSource.WORKSPACE,
|
|
164
|
+
conda_packages=[],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# 2. Check if it's in the mapping file
|
|
168
|
+
if package_name in mappings:
|
|
169
|
+
channels = mappings[package_name]
|
|
170
|
+
if channels:
|
|
171
|
+
channel_mapping = next(iter(channels.values()))
|
|
172
|
+
if isinstance(channel_mapping, dict):
|
|
173
|
+
# Platform-specific mapping
|
|
174
|
+
pixi_to_mapping = {
|
|
175
|
+
"linux-64": "linux",
|
|
176
|
+
"linux-aarch64": "linux",
|
|
177
|
+
"osx-64": "osx",
|
|
178
|
+
"osx-arm64": "osx",
|
|
179
|
+
"win-64": "win64",
|
|
180
|
+
}
|
|
181
|
+
mapping_platform = pixi_to_mapping.get(platform, "linux")
|
|
182
|
+
packages = channel_mapping.get(mapping_platform, [])
|
|
183
|
+
source = PackageSource.MAPPING
|
|
184
|
+
conda_packages = packages if packages else []
|
|
185
|
+
elif isinstance(channel_mapping, list):
|
|
186
|
+
source = PackageSource.MAPPING
|
|
187
|
+
conda_packages = channel_mapping
|
|
188
|
+
|
|
189
|
+
# 3. Check if it's in ROS distro
|
|
190
|
+
if source is None and self.has_package(package_name):
|
|
191
|
+
conda_name = package_name.replace("_", "-")
|
|
192
|
+
source = PackageSource.ROS_DISTRO
|
|
193
|
+
conda_packages = [f"ros-{self.distro_name}-{conda_name}"]
|
|
194
|
+
|
|
195
|
+
# 4. Check if it's available on conda-forge (without ros-distro prefix)
|
|
196
|
+
if source is None:
|
|
197
|
+
conda_name = package_name.replace("_", "-")
|
|
198
|
+
if self.check_conda_forge_availability(conda_name, platform):
|
|
199
|
+
source = PackageSource.CONDA_FORGE
|
|
200
|
+
conda_packages = [conda_name]
|
|
201
|
+
|
|
202
|
+
# 5. Not found
|
|
203
|
+
if source is None:
|
|
204
|
+
print(
|
|
205
|
+
f"Package '{package_name}' not found in workspace, mappings, "
|
|
206
|
+
f"ROS distro, or conda-forge."
|
|
207
|
+
)
|
|
208
|
+
return PackageValidationResult(
|
|
209
|
+
package_name=package_name,
|
|
210
|
+
source=PackageSource.NOT_FOUND,
|
|
211
|
+
conda_packages=[],
|
|
212
|
+
error=f"Package '{package_name}' not found in any source",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Step 2: Validate packages exist in their expected channels
|
|
216
|
+
# Note: We don't validate mapped packages - we trust the mappings
|
|
217
|
+
if source == PackageSource.ROS_DISTRO:
|
|
218
|
+
# Validate ROS package exists in robostack channel
|
|
219
|
+
robostack_channel = f"https://prefix.dev/robostack-{self.distro_name}"
|
|
220
|
+
ros_conda_name = conda_packages[0]
|
|
221
|
+
|
|
222
|
+
if not self.check_package_availability(
|
|
223
|
+
ros_conda_name, platform, robostack_channel
|
|
224
|
+
):
|
|
225
|
+
print(
|
|
226
|
+
f"Package '{package_name}' found in ROS {self.distro_name} "
|
|
227
|
+
f"distro index but '{ros_conda_name}' not available in "
|
|
228
|
+
f"robostack-{self.distro_name}."
|
|
229
|
+
)
|
|
230
|
+
# Keep the conda package name so we can show it in NOT_FOUND
|
|
231
|
+
return PackageValidationResult(
|
|
232
|
+
package_name=package_name,
|
|
233
|
+
source=PackageSource.NOT_FOUND,
|
|
234
|
+
conda_packages=[ros_conda_name],
|
|
235
|
+
error=(
|
|
236
|
+
f"ROS package not available in robostack-{self.distro_name}"
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Source determined and validated
|
|
241
|
+
return PackageValidationResult(
|
|
242
|
+
package_name=package_name,
|
|
243
|
+
source=source,
|
|
244
|
+
conda_packages=conda_packages,
|
|
245
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixi-ros
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Pixi extension for ROS package management
|
|
5
5
|
Project-URL: Homepage, https://github.com/ruben-arts/pixi-ros
|
|
6
6
|
Project-URL: Repository, https://github.com/ruben-arts/pixi-ros
|
|
@@ -13,6 +13,7 @@ Requires-Dist: pathspec>=0.11.0
|
|
|
13
13
|
Requires-Dist: py-rattler>=0.6.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Requires-Dist: rich>=13.0.0
|
|
16
|
+
Requires-Dist: rosdistro>=0.9.0
|
|
16
17
|
Requires-Dist: tomlkit>=0.12.0
|
|
17
18
|
Requires-Dist: typer>=0.12.0
|
|
18
19
|
Description-Content-Type: text/markdown
|
|
@@ -63,10 +64,16 @@ pixi-ros init --distro humble
|
|
|
63
64
|
This will:
|
|
64
65
|
1. Discover all ROS packages in your workspace (by finding `package.xml` files)
|
|
65
66
|
2. Read dependencies from each `package.xml`
|
|
66
|
-
3.
|
|
67
|
+
3. **Validate and resolve** each dependency using the priority system:
|
|
68
|
+
- Skip workspace packages (built locally)
|
|
69
|
+
- Use custom mappings from YAML files
|
|
70
|
+
- Query ROS distro index for ROS packages
|
|
71
|
+
- Auto-detect packages on conda-forge
|
|
72
|
+
- Flag packages that can't be found
|
|
67
73
|
4. Generate/update `pixi.toml` with proper channels and dependencies
|
|
68
|
-
5. Check package availability
|
|
74
|
+
5. Check package availability in conda channels for each platform
|
|
69
75
|
6. Create helpful build/test/clean tasks
|
|
76
|
+
7. Display detailed validation results with source information
|
|
70
77
|
|
|
71
78
|
### Install and Build
|
|
72
79
|
|
|
@@ -88,26 +95,66 @@ pixi shell
|
|
|
88
95
|
|
|
89
96
|
## How It Works
|
|
90
97
|
|
|
91
|
-
### Dependency Mapping
|
|
98
|
+
### Dependency Mapping & Validation
|
|
92
99
|
|
|
93
|
-
`pixi-ros` reads all dependency types from `package.xml` files
|
|
94
|
-
It then does a best effort mapping of ROS package names to conda packages.
|
|
100
|
+
`pixi-ros` reads all dependency types from `package.xml` files and intelligently resolves them to conda packages using a **priority-based validation system**.
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
#### Validation Priority Order
|
|
103
|
+
|
|
104
|
+
When resolving a ROS package dependency, `pixi-ros` checks sources in this order:
|
|
105
|
+
|
|
106
|
+
1. **Workspace packages** (local source) → Skipped, won't be added to dependencies
|
|
107
|
+
2. **Mapping files** → Use custom conda package mappings from the embedded mapping.
|
|
108
|
+
3. **ROS distribution** → Query the official ROS distro index for `ros-{distro}-{package}` packages
|
|
109
|
+
4. **conda-forge** (auto-detection) → Search conda-forge for packages without ros-distro prefix
|
|
110
|
+
5. **NOT FOUND** → Mark as unavailable and comment out in `pixi.toml`
|
|
111
|
+
|
|
112
|
+
#### Package Sources
|
|
113
|
+
|
|
114
|
+
The dependency tables show where each package comes from:
|
|
115
|
+
|
|
116
|
+
- **ROS {distro}**: Official ROS distribution packages from robostack (e.g., `ros-humble-rclcpp`)
|
|
117
|
+
- **Mapping**: Custom mappings from YAML files (e.g., `cmake` → `cmake`, `udev` → `libusb + libudev`)
|
|
118
|
+
- **conda-forge**: Auto-detected packages available directly on conda-forge
|
|
119
|
+
- **Workspace**: Local packages in your workspace (skipped from dependencies)
|
|
120
|
+
- **NOT FOUND**: Packages that couldn't be resolved (commented out in `pixi.toml`)
|
|
99
121
|
|
|
100
122
|
The mapping rules are defined in YAML files (see `src/pixi_ros/data/conda-forge.yaml`) and can be customized by placing your own mapping files in `pixi-ros/*.yaml` or `~/.pixi-ros/*.yaml`.
|
|
101
123
|
|
|
102
|
-
After
|
|
124
|
+
After dependency resolution, `pixi-ros` validates package availability in the configured channels for each target platform by connecting to `https://prefix.dev`.
|
|
125
|
+
|
|
126
|
+
### Example Output
|
|
103
127
|
|
|
104
|
-
|
|
128
|
+
When you run `pixi-ros init --distro humble`, you'll see validation results:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Found 2 package(s): my_package, other_package
|
|
132
|
+
Initializing ROS humble distribution validator...
|
|
133
|
+
|
|
134
|
+
╭─────────────────── Package: my_package ───────────────────╮
|
|
135
|
+
│ ROS Dependency │ Type │ Conda Packages │ Source │
|
|
136
|
+
├────────────────┼─────────┼─────────────────────────┼──────────────┤
|
|
137
|
+
│ rclcpp │ Build │ ros-humble-rclcpp │ ROS humble │
|
|
138
|
+
│ std_msgs │ Runtime │ ros-humble-std-msgs │ ROS humble │
|
|
139
|
+
│ cmake │ Build │ cmake │ Mapping │
|
|
140
|
+
│ eigen │ Build │ eigen │ conda-forge │
|
|
141
|
+
╰────────────────┴─────────┴─────────────────────────┴──────────────╯
|
|
142
|
+
|
|
143
|
+
Validation Summary:
|
|
144
|
+
✓ 2 workspace packages (skipped)
|
|
145
|
+
✓ 1 packages from mappings
|
|
146
|
+
✓ 5 packages from ROS humble distro
|
|
147
|
+
✓ 1 packages from conda-forge (auto-detected)
|
|
148
|
+
|
|
149
|
+
Total external dependencies: 7
|
|
150
|
+
```
|
|
105
151
|
|
|
106
152
|
Given a `package.xml` with:
|
|
107
153
|
|
|
108
154
|
```xml
|
|
109
155
|
<depend>rclcpp</depend>
|
|
110
156
|
<build_depend>ament_cmake</build_depend>
|
|
157
|
+
<build_depend>cmake</build_depend>
|
|
111
158
|
<exec_depend>std_msgs</exec_depend>
|
|
112
159
|
```
|
|
113
160
|
|
|
@@ -115,17 +162,57 @@ Given a `package.xml` with:
|
|
|
115
162
|
|
|
116
163
|
```toml
|
|
117
164
|
[dependencies]
|
|
118
|
-
|
|
119
|
-
ros-humble-
|
|
120
|
-
|
|
165
|
+
# Base ROS dependencies
|
|
166
|
+
ros-humble-ros-base = "*"
|
|
167
|
+
pkg-config = "*"
|
|
168
|
+
compilers = "*"
|
|
169
|
+
make = "*"
|
|
170
|
+
ninja = "*"
|
|
171
|
+
|
|
172
|
+
# Build tools
|
|
173
|
+
colcon-common-extensions = "*"
|
|
174
|
+
|
|
175
|
+
# Workspace dependencies
|
|
176
|
+
cmake = "*" # From mapping
|
|
177
|
+
ros-humble-ament-cmake = "*" # From ROS humble
|
|
178
|
+
ros-humble-rclcpp = "*" # From ROS humble
|
|
179
|
+
ros-humble-std-msgs = "*" # From ROS humble
|
|
121
180
|
```
|
|
122
181
|
|
|
123
|
-
|
|
182
|
+
### Version Constraints
|
|
183
|
+
|
|
184
|
+
`pixi-ros` supports version constraints from `package.xml` files and automatically applies them to the generated `pixi.toml`.
|
|
185
|
+
|
|
186
|
+
#### Supported Version Attributes
|
|
124
187
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
188
|
+
You can specify version requirements in your `package.xml` using standard ROS version attributes:
|
|
189
|
+
|
|
190
|
+
| package.xml attribute | pixi.toml constraint | Description |
|
|
191
|
+
|----------------------|----------------------|-------------|
|
|
192
|
+
| `version_eq="X.Y.Z"` | `==X.Y.Z` | Exactly version X.Y.Z |
|
|
193
|
+
| `version_gte="X.Y.Z"` | `>=X.Y.Z` | Version X.Y.Z or newer |
|
|
194
|
+
| `version_gt="X.Y.Z"` | `>X.Y.Z` | Newer than version X.Y.Z |
|
|
195
|
+
| `version_lte="X.Y.Z"` | `<=X.Y.Z` | Version X.Y.Z or older |
|
|
196
|
+
| `version_lt="X.Y.Z"` | `<X.Y.Z` | Older than version X.Y.Z |
|
|
197
|
+
|
|
198
|
+
Multiple constraints can be combined on the same dependency and will be joined with commas in the output.
|
|
199
|
+
|
|
200
|
+
Given a `package.xml` with version constraints:
|
|
201
|
+
|
|
202
|
+
```xml
|
|
203
|
+
<depend version_gte="3.12.4">cmake</depend>
|
|
204
|
+
<build_depend version_gte="3.3.0" version_lt="4.0.0">eigen</build_depend>
|
|
205
|
+
<exec_depend version_eq="1.2.3">boost</exec_depend>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`pixi-ros init` generates:
|
|
209
|
+
|
|
210
|
+
```toml
|
|
211
|
+
[dependencies]
|
|
212
|
+
cmake = ">=3.12.4"
|
|
213
|
+
eigen = ">=3.3.0,<4.0.0"
|
|
214
|
+
boost = "==1.2.3"
|
|
215
|
+
```
|
|
129
216
|
|
|
130
217
|
## Command Reference
|
|
131
218
|
|
|
@@ -147,13 +234,16 @@ pixi-ros init
|
|
|
147
234
|
|
|
148
235
|
**What it does:**
|
|
149
236
|
- Scans workspace for `package.xml` files
|
|
150
|
-
- Reads all dependency types (build, exec, test)
|
|
237
|
+
- Reads all dependency types (build, exec, test) and version constraints
|
|
238
|
+
- **Validates dependencies** using the priority-based system (workspace → mapping → ROS distro → conda-forge)
|
|
151
239
|
- Maps ROS dependencies to conda packages for each platform
|
|
240
|
+
- Applies version constraints from package.xml to pixi.toml dependencies
|
|
152
241
|
- Configures robostack channels
|
|
153
242
|
- Checks package availability per platform
|
|
154
243
|
- Creates build tasks using colcon
|
|
155
244
|
- Generates helpful `README_PIXI.md`
|
|
156
245
|
- Sets up platform-specific dependencies in `pixi.toml`
|
|
246
|
+
- **Displays validation results** showing where each dependency was found
|
|
157
247
|
|
|
158
248
|
**Running multiple times:**
|
|
159
249
|
The command is idempotent - you can run it multiple times to update dependencies as your workspace changes.
|
|
@@ -261,12 +351,15 @@ workspace/
|
|
|
261
351
|
|
|
262
352
|
### Package Not Found
|
|
263
353
|
|
|
264
|
-
If pixi-ros marks packages as "NOT FOUND":
|
|
354
|
+
If pixi-ros marks packages as "NOT FOUND" (shown in red in the validation output):
|
|
355
|
+
|
|
356
|
+
1. **Check the ROS distro**: Verify the package exists in robostack: https://prefix.dev/channels/robostack-{distro}
|
|
357
|
+
2. **Check for typos**: Review your `package.xml` for spelling errors
|
|
358
|
+
3. **Check conda-forge**: Some packages may be available directly on conda-forge without the `ros-distro-` prefix
|
|
359
|
+
4. **Create a mapping**: Add a custom mapping in `pixi-ros/*.yaml` if the package has a different conda name
|
|
360
|
+
5. **Add to workspace**: Consider including the package source in your workspace instead of depending on it
|
|
265
361
|
|
|
266
|
-
|
|
267
|
-
2. Check for typos in `package.xml`
|
|
268
|
-
3. Some packages may have different names - check mapping files
|
|
269
|
-
4. Consider adding the package to your workspace instead of depending on it
|
|
362
|
+
The validation table shows exactly where each dependency was checked, making it easier to diagnose issues.
|
|
270
363
|
|
|
271
364
|
### Different Package Names
|
|
272
365
|
|