pixi-ros 0.2.0__py3-none-any.whl → 0.3.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 CHANGED
@@ -32,7 +32,8 @@ def init(
32
32
  typer.Option(
33
33
  "--platform",
34
34
  "-p",
35
- help="Target platforms (e.g., linux-64, osx-arm64, win-64). Can be specified multiple times.",
35
+ help="Target platforms (e.g., linux-64, osx-arm64, win-64)."
36
+ " Can be specified multiple times.",
36
37
  ),
37
38
  ] = None,
38
39
  ):
@@ -103,20 +104,19 @@ def init(
103
104
  platforms.append(available_platforms[sel_num - 1])
104
105
  else:
105
106
  typer.echo(
106
- f"Error: Invalid selection {sel_num}. Please choose 1-{len(available_platforms)}",
107
+ f"Error: Invalid selection {sel_num}."
108
+ + f"Please choose 1-{len(available_platforms)}",
107
109
  err=True,
108
110
  )
109
111
  raise typer.Exit(code=1)
110
- except ValueError:
112
+ except ValueError as err:
111
113
  # User entered a name instead of number
112
114
  if sel in available_platforms:
113
115
  platforms.append(sel)
114
116
  else:
115
- typer.echo(
116
- f"Error: '{sel}' is not a valid platform", err=True
117
- )
117
+ typer.echo(f"Error: '{sel}' is not a valid platform", err=True)
118
118
  typer.echo(f"Available: {', '.join(available_platforms)}", err=True)
119
- raise typer.Exit(code=1)
119
+ raise typer.Exit(code=1) from err
120
120
 
121
121
  if not platforms:
122
122
  typer.echo("Error: No platforms selected", err=True)
@@ -792,6 +792,8 @@ python3-importlib-resources:
792
792
  pixi: [importlib_resources]
793
793
  python3-jinja2:
794
794
  pixi: [jinja2]
795
+ python3-jsonschema:
796
+ pixi: [jsonschema]
795
797
  python3-kitchen:
796
798
  pixi: [kitchen]
797
799
  python3-lark-parser:
pixi_ros/init.py CHANGED
@@ -254,8 +254,8 @@ def _ensure_workspace_section(
254
254
  if "channels" not in workspace:
255
255
  workspace["channels"] = []
256
256
 
257
- # Set platforms if not present or if platforms were provided
258
- if "platforms" not in workspace or platforms:
257
+ # Set or extend platforms
258
+ if "platforms" not in workspace:
259
259
  if platforms:
260
260
  # Platforms are already in pixi format (linux-64, osx-64, etc.)
261
261
  workspace["platforms"] = platforms
@@ -263,6 +263,18 @@ def _ensure_workspace_section(
263
263
  # Only add the current platform by default
264
264
  current_platform = str(Platform.current())
265
265
  workspace["platforms"] = [current_platform]
266
+ elif platforms:
267
+ # Extend existing platforms list with new ones (avoiding duplicates)
268
+ existing_platforms = workspace["platforms"]
269
+ if not isinstance(existing_platforms, list):
270
+ existing_platforms = [existing_platforms]
271
+
272
+ # Add new platforms that aren't already in the list
273
+ for platform in platforms:
274
+ if platform not in existing_platforms:
275
+ existing_platforms.append(platform)
276
+
277
+ workspace["platforms"] = existing_platforms
266
278
 
267
279
 
268
280
  def _ensure_channels(config: dict, distro: str):
@@ -365,6 +377,28 @@ def _ensure_dependencies(
365
377
  if cmake_version:
366
378
  dep_versions["cmake"] = cmake_version
367
379
 
380
+ # Collect version constraints from package.xml
381
+ for ros_dep, version_constraint in pkg.dependency_versions.items():
382
+ # Skip workspace packages
383
+ if ros_dep in workspace_pkg_names:
384
+ continue
385
+
386
+ # Map ROS package to conda packages
387
+ # Note: We use the first platform for mapping since version constraints
388
+ # should be the same across platforms for a given ROS package
389
+ conda_packages = map_ros_to_conda(ros_dep, distro)
390
+
391
+ # Apply version constraint to all mapped conda packages
392
+ for conda_dep in conda_packages:
393
+ if conda_dep and not conda_dep.startswith("REQUIRE_"):
394
+ # If package already has a constraint, combine them
395
+ if conda_dep in dep_versions:
396
+ dep_versions[conda_dep] = (
397
+ f"{dep_versions[conda_dep]},{version_constraint}"
398
+ )
399
+ else:
400
+ dep_versions[conda_dep] = version_constraint
401
+
368
402
  # Platforms come from CLI as pixi platform names (linux-64, osx-64, etc.)
369
403
  # Map them to mapping platform names for querying the mapping files
370
404
  pixi_to_mapping = {
@@ -445,11 +479,15 @@ def _ensure_dependencies(
445
479
  common_deps = set(platform_deps[mapping_platform_list[0]].keys())
446
480
 
447
481
  # For backwards compatibility when single platform, use old behavior
448
- dep_sources = platform_deps[mapping_platform_list[0]] if len(mapping_platform_list) == 1 else {
449
- dep: platform_deps[mapping_platform_list[0]][dep]
450
- for dep in common_deps
451
- if dep in platform_deps[mapping_platform_list[0]]
452
- }
482
+ dep_sources = (
483
+ platform_deps[mapping_platform_list[0]]
484
+ if len(mapping_platform_list) == 1
485
+ else {
486
+ dep: platform_deps[mapping_platform_list[0]][dep]
487
+ for dep in common_deps
488
+ if dep in platform_deps[mapping_platform_list[0]]
489
+ }
490
+ )
453
491
 
454
492
  # Create or get dependencies table
455
493
  if "dependencies" not in config:
@@ -497,7 +535,9 @@ def _ensure_dependencies(
497
535
  if dep_sources:
498
536
  dependencies.add(tomlkit.nl())
499
537
  if len(mapping_platform_list) > 1:
500
- dependencies.add(tomlkit.comment("Workspace dependencies (common across platforms)"))
538
+ dependencies.add(
539
+ tomlkit.comment("Workspace dependencies (common across platforms)")
540
+ )
501
541
  else:
502
542
  dependencies.add(tomlkit.comment("Workspace dependencies"))
503
543
 
@@ -513,7 +553,9 @@ def _ensure_dependencies(
513
553
 
514
554
  availability = {}
515
555
  if channels and packages_to_check:
516
- typer.echo(f"Checking common package availability for {first_pixi_platform}...")
556
+ typer.echo(
557
+ f"Checking common package availability for {first_pixi_platform}..."
558
+ )
517
559
  availability = _check_package_availability(
518
560
  packages_to_check, channels, first_platform
519
561
  )
@@ -564,7 +606,9 @@ def _ensure_dependencies(
564
606
 
565
607
  # If we also have windows, only move to unix if NOT on windows
566
608
  if has_win:
567
- win_deps = set(platform_deps.get("win64", {}).keys()) | set(platform_deps.get("win", {}).keys())
609
+ win_deps = set(platform_deps.get("win64", {}).keys()) | set(
610
+ platform_deps.get("win", {}).keys()
611
+ )
568
612
  unix_deps_keys = unix_candidates - win_deps
569
613
  else:
570
614
  unix_deps_keys = unix_candidates
@@ -591,7 +635,9 @@ def _ensure_dependencies(
591
635
  )
592
636
 
593
637
  # Check availability on linux platform as representative
594
- representative_pixi_platform = platform_groups.get("linux", platform_groups.get("osx", platforms))[0]
638
+ representative_pixi_platform = platform_groups.get(
639
+ "linux", platform_groups.get("osx", platforms)
640
+ )[0]
595
641
  platform_obj = Platform(representative_pixi_platform)
596
642
  packages_to_check = list(unix_deps.keys())
597
643
 
@@ -651,7 +697,9 @@ def _ensure_dependencies(
651
697
  # Add comment
652
698
  if len(target_deps) == 0:
653
699
  target_deps.add(
654
- tomlkit.comment(f"Platform-specific dependencies for {mapping_platform}")
700
+ tomlkit.comment(
701
+ f"Platform-specific dependencies for {mapping_platform}"
702
+ )
655
703
  )
656
704
 
657
705
  # Check availability for this mapping platform
@@ -662,7 +710,9 @@ def _ensure_dependencies(
662
710
 
663
711
  availability = {}
664
712
  if channels and packages_to_check:
665
- typer.echo(f"Checking package availability for {mapping_platform}...")
713
+ typer.echo(
714
+ f"Checking package availability for {mapping_platform}..."
715
+ )
666
716
  availability = _check_package_availability(
667
717
  packages_to_check, channels, platform_obj
668
718
  )
@@ -700,14 +750,27 @@ def _ensure_tasks(config: dict):
700
750
 
701
751
  # Define common ROS tasks if not present
702
752
  default_tasks = {
703
- "build": "colcon build",
704
- "test": "colcon test",
705
- "clean": "rm -rf build install log",
753
+ "build": {
754
+ "cmd": "colcon build",
755
+ "description": "Build the ROS workspace",
756
+ },
757
+ "test": {
758
+ "cmd": "colcon test",
759
+ "description": "Run tests for the workspace",
760
+ },
761
+ "clean": {
762
+ "cmd": "rm -rf build install log",
763
+ "description": "Clean build artifacts (build, install, log directories)",
764
+ },
706
765
  }
707
766
 
708
- for task_name, task_cmd in default_tasks.items():
767
+ for task_name, task_config in default_tasks.items():
709
768
  if task_name not in tasks:
710
- tasks[task_name] = task_cmd
769
+ # Create inline table for task configuration
770
+ task_table = tomlkit.inline_table()
771
+ task_table["cmd"] = task_config["cmd"]
772
+ task_table["description"] = task_config["description"]
773
+ tasks[task_name] = task_table
711
774
 
712
775
  config["tasks"] = tasks
713
776
 
pixi_ros/mappings.py CHANGED
@@ -287,7 +287,11 @@ def get_platforms() -> list[str]:
287
287
  if "win64" in mapping_platforms or "win" in mapping_platforms:
288
288
  pixi_platforms.append("win-64")
289
289
 
290
- return pixi_platforms if pixi_platforms else ["linux-64", "osx-64", "osx-arm64", "win-64"]
290
+ return (
291
+ pixi_platforms
292
+ if pixi_platforms
293
+ else ["linux-64", "osx-64", "osx-arm64", "win-64"]
294
+ )
291
295
 
292
296
 
293
297
  def get_ros_distros() -> list[str]:
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]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixi-ros
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -120,6 +120,41 @@ ros-humble-rclcpp = "*"
120
120
  ros-humble-std-msgs = "*"
121
121
  ```
122
122
 
123
+ ### Version Constraints
124
+
125
+ `pixi-ros` supports version constraints from `package.xml` files and automatically applies them to the generated `pixi.toml`.
126
+
127
+ #### Supported Version Attributes
128
+
129
+ You can specify version requirements in your `package.xml` using standard ROS version attributes:
130
+
131
+ | package.xml attribute | pixi.toml constraint | Description |
132
+ |----------------------|----------------------|-------------|
133
+ | `version_eq="X.Y.Z"` | `==X.Y.Z` | Exactly version X.Y.Z |
134
+ | `version_gte="X.Y.Z"` | `>=X.Y.Z` | Version X.Y.Z or newer |
135
+ | `version_gt="X.Y.Z"` | `>X.Y.Z` | Newer than version X.Y.Z |
136
+ | `version_lte="X.Y.Z"` | `<=X.Y.Z` | Version X.Y.Z or older |
137
+ | `version_lt="X.Y.Z"` | `<X.Y.Z` | Older than version X.Y.Z |
138
+
139
+ Multiple constraints can be combined on the same dependency and will be joined with commas in the output.
140
+
141
+ Given a `package.xml` with version constraints:
142
+
143
+ ```xml
144
+ <depend version_gte="3.12.4">cmake</depend>
145
+ <build_depend version_gte="3.3.0" version_lt="4.0.0">eigen</build_depend>
146
+ <exec_depend version_eq="1.2.3">boost</exec_depend>
147
+ ```
148
+
149
+ `pixi-ros init` generates:
150
+
151
+ ```toml
152
+ [dependencies]
153
+ cmake = ">=3.12.4"
154
+ eigen = ">=3.3.0,<4.0.0"
155
+ boost = "==1.2.3"
156
+ ```
157
+
123
158
  ## Supported ROS Distributions
124
159
 
125
160
  - ROS 2 Humble: https://prefix.dev/robostack-humble
@@ -147,8 +182,9 @@ pixi-ros init
147
182
 
148
183
  **What it does:**
149
184
  - Scans workspace for `package.xml` files
150
- - Reads all dependency types (build, exec, test)
185
+ - Reads all dependency types (build, exec, test) and version constraints
151
186
  - Maps ROS dependencies to conda packages for each platform
187
+ - Applies version constraints from package.xml to pixi.toml dependencies
152
188
  - Configures robostack channels
153
189
  - Checks package availability per platform
154
190
  - Creates build tasks using colcon
@@ -0,0 +1,16 @@
1
+ pixi_ros/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ pixi_ros/cli.py,sha256=85lRSSsnSWAxqhkwjT0OiObYdTu84YBJXO576k0fOlY,4351
3
+ pixi_ros/config.py,sha256=JevtFXh96UJj7aMUWp18J1xcPeQPoLzh3hdiu5uPR0s,834
4
+ pixi_ros/init.py,sha256=wX7VqJFuh0q3OGNZ7_BLVM3BYyTdbI7_BhzHJfJJLIw,30835
5
+ pixi_ros/mappings.py,sha256=AlXT_VsPV7KeEQxHkbBrnc79LXwizrCk7oG9-fYX5MI,10376
6
+ pixi_ros/package_xml.py,sha256=XnDkKuccSar1ndmvACZMZ7c2yUNXz7CcfQRwluZolqQ,8815
7
+ pixi_ros/utils.py,sha256=uGgB8CYiM_3KfBtqvUKqkEXXTffv8FkkaIC230peHUY,2026
8
+ pixi_ros/workspace.py,sha256=N5Aqcl77J8aLrEDr4T-XR9V5fBjZ1KQHXd4dkbgX8HU,6838
9
+ pixi_ros/data/README.md,sha256=Tdc2sTUuvoyEaHYlmM_C1pf3qr0o0P5Lu2ZUQ88tUjI,1602
10
+ pixi_ros/data/README_PIXI.md.template,sha256=q7g65oHmrEqKTtqOT7lgX6l9RI69w64B0DCLwhf8ocM,3076
11
+ pixi_ros/data/conda-forge.yaml,sha256=DeMfdzEuFCFXLmceV6ENmGpSBo6tMZh-Gx-ZkEAakT8,18941
12
+ pixi_ros-0.3.0.dist-info/METADATA,sha256=fDBOMPyY5DSt4N87s-pugMV0JVCmOOdDU76x8HyND0M,10714
13
+ pixi_ros-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ pixi_ros-0.3.0.dist-info/entry_points.txt,sha256=DpBwU4Djcej8gT42q8Ccuv-R9pdmGHyFV5p57_ogqfQ,47
15
+ pixi_ros-0.3.0.dist-info/licenses/LICENSE,sha256=pAZXnNE2dxxwXFIduGyn1gpvPefJtUYOYZOi3yeGG94,1068
16
+ pixi_ros-0.3.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- pixi_ros/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- pixi_ros/cli.py,sha256=ifctksMHxa6NlCimnnovYe7HEz-0YUcZ-UHvVPiZCc0,4337
3
- pixi_ros/config.py,sha256=JevtFXh96UJj7aMUWp18J1xcPeQPoLzh3hdiu5uPR0s,834
4
- pixi_ros/init.py,sha256=oqpgt-yEywDxQ410kpka5VTsmHalTHxoUyBrCMEj5YA,28507
5
- pixi_ros/mappings.py,sha256=3wr4uPlGBtc8BO-06RZEpZZzxkiGxd9zxNfV3OzAoxI,10344
6
- pixi_ros/package_xml.py,sha256=a1_zJVc73eMOt67BIk2ITnxLPN1RIUOs0oL42q6CLvI,6279
7
- pixi_ros/utils.py,sha256=uGgB8CYiM_3KfBtqvUKqkEXXTffv8FkkaIC230peHUY,2026
8
- pixi_ros/workspace.py,sha256=N5Aqcl77J8aLrEDr4T-XR9V5fBjZ1KQHXd4dkbgX8HU,6838
9
- pixi_ros/data/README.md,sha256=Tdc2sTUuvoyEaHYlmM_C1pf3qr0o0P5Lu2ZUQ88tUjI,1602
10
- pixi_ros/data/README_PIXI.md.template,sha256=q7g65oHmrEqKTtqOT7lgX6l9RI69w64B0DCLwhf8ocM,3076
11
- pixi_ros/data/conda-forge.yaml,sha256=O3r31jNNU-lPCgvFlmhytTp_R1UMgIBRwDaeSebI7wI,18900
12
- pixi_ros-0.2.0.dist-info/METADATA,sha256=H3IAkgh95uynWynNrOZrXGn6GzOZaYj41yMMlAdakT0,9436
13
- pixi_ros-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- pixi_ros-0.2.0.dist-info/entry_points.txt,sha256=DpBwU4Djcej8gT42q8Ccuv-R9pdmGHyFV5p57_ogqfQ,47
15
- pixi_ros-0.2.0.dist-info/licenses/LICENSE,sha256=pAZXnNE2dxxwXFIduGyn1gpvPefJtUYOYZOi3yeGG94,1068
16
- pixi_ros-0.2.0.dist-info/RECORD,,