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/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, distro: str = "humble", platform_override: str | None = None
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 based on mapping files.
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
- # Iterate through mappings to find all platform keys
275
- for package_mappings in mappings.values():
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
- # Convert mapping platforms to pixi platforms
282
- pixi_platforms = []
283
- if "linux" in mapping_platforms:
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 if pixi_platforms else ["linux-64", "osx-64", "osx-arm64", "win-64"]
303
+ return pixi_platforms
291
304
 
292
305
 
293
306
  def get_ros_distros() -> list[str]:
294
307
  """
295
- Get list of supported ROS distributions.
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
- # Parse all dependency types
102
- buildtool_depends = get_deps("buildtool_depend")
103
- build_depends = get_deps("build_depend")
104
- build_export_depends = get_deps("build_export_depend")
105
- exec_depends = get_deps("exec_depend")
106
- test_depends = get_deps("test_depend")
107
- depends = get_deps("depend")
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 = get_deps("run_depend")
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.2.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. Map ROS package names to conda packages
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 and warn about missing packages
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
- - **ROS packages**: `ros-{distro}-{package}` from robostack channels (e.g., `ros-humble-rclcpp`)
97
- - **System packages**: Mapped to conda-forge equivalents (e.g., `cmake`, `eigen`)
98
- - **Platform-specific packages**: Different mappings per platform (e.g., OpenGL → `libgl-devel` on Linux, X11 packages on macOS)
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 the mapping, it validates package availability in the configured channels for each target platform. This starts a connection with `https://prefix.dev` to check if packages exist.
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
- ### Example
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
- ros-humble-ament-cmake = "*"
119
- ros-humble-rclcpp = "*"
120
- ros-humble-std-msgs = "*"
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
- ## Supported ROS Distributions
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
- - ROS 2 Humble: https://prefix.dev/robostack-humble
126
- - ROS 2 Iron: https://prefix.dev/robostack-iron
127
- - ROS 2 Jazzy: https://prefix.dev/robostack-jazzy
128
- - ROS 2 Rolling: https://prefix.dev/robostack-rolling
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
- 1. Check if the package exists in robostack: https://prefix.dev/channels/robostack-{distro}
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