pixi-ros 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (50) hide show
  1. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/PKG-INFO +38 -2
  2. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/README.md +37 -1
  3. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/pixi.lock +1 -1
  4. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/pyproject.toml +1 -1
  5. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/cli.py +7 -7
  6. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/data/conda-forge.yaml +2 -0
  7. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/init.py +81 -18
  8. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/mappings.py +5 -1
  9. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/package_xml.py +69 -8
  10. pixi_ros-0.3.0/tests/examples/ws1/README_PIXI.md +125 -0
  11. pixi_ros-0.3.0/tests/examples/ws1/pixi.toml +38 -0
  12. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-a/package.xml +2 -2
  13. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-b/package.xml +3 -0
  14. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_init.py +192 -0
  15. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_package_xml.py +92 -0
  16. pixi_ros-0.2.0/tests/examples/ws1/pixi.toml +0 -39
  17. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/.gitattributes +0 -0
  18. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/.github/workflows/ci.yml +0 -0
  19. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/.github/workflows/publish-pypi.yml +0 -0
  20. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/.gitignore +0 -0
  21. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/LICENSE +0 -0
  22. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/pixi.toml +0 -0
  23. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/__init__.py +0 -0
  24. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/config.py +0 -0
  25. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/data/README.md +0 -0
  26. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/data/README_PIXI.md.template +0 -0
  27. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/utils.py +0 -0
  28. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/src/pixi_ros/workspace.py +0 -0
  29. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/pixi.lock +0 -0
  30. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-a/CMakeLists.txt +0 -0
  31. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-a/LICENSE +0 -0
  32. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-b/package-b/__init__.py +0 -0
  33. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-b/setup.cfg +0 -0
  34. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-b/setup.py +0 -0
  35. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-b/test/test_copyright.py +0 -0
  36. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-b/test/test_flake8.py +0 -0
  37. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/examples/ws1/src/package-b/test/test_pep257.py +0 -0
  38. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/fixtures/mock_workspace/README.md +0 -0
  39. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/fixtures/mock_workspace/src/legacy_pkg/package.xml +0 -0
  40. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/fixtures/mock_workspace/src/my_cpp_pkg/CMakeLists.txt +0 -0
  41. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/fixtures/mock_workspace/src/my_cpp_pkg/package.xml +0 -0
  42. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/fixtures/mock_workspace/src/my_mixed_pkg/package.xml +0 -0
  43. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/fixtures/mock_workspace/src/my_python_pkg/package.xml +0 -0
  44. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/fixtures/mock_workspace/src/my_python_pkg/setup.py +0 -0
  45. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_cli.py +0 -0
  46. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_config.py +0 -0
  47. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_gateway_availability.py +0 -0
  48. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_mappings.py +0 -0
  49. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_utils.py +0 -0
  50. {pixi_ros-0.2.0 → pixi_ros-0.3.0}/tests/test_workspace.py +0 -0
@@ -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
@@ -101,6 +101,41 @@ ros-humble-rclcpp = "*"
101
101
  ros-humble-std-msgs = "*"
102
102
  ```
103
103
 
104
+ ### Version Constraints
105
+
106
+ `pixi-ros` supports version constraints from `package.xml` files and automatically applies them to the generated `pixi.toml`.
107
+
108
+ #### Supported Version Attributes
109
+
110
+ You can specify version requirements in your `package.xml` using standard ROS version attributes:
111
+
112
+ | package.xml attribute | pixi.toml constraint | Description |
113
+ |----------------------|----------------------|-------------|
114
+ | `version_eq="X.Y.Z"` | `==X.Y.Z` | Exactly version X.Y.Z |
115
+ | `version_gte="X.Y.Z"` | `>=X.Y.Z` | Version X.Y.Z or newer |
116
+ | `version_gt="X.Y.Z"` | `>X.Y.Z` | Newer than version X.Y.Z |
117
+ | `version_lte="X.Y.Z"` | `<=X.Y.Z` | Version X.Y.Z or older |
118
+ | `version_lt="X.Y.Z"` | `<X.Y.Z` | Older than version X.Y.Z |
119
+
120
+ Multiple constraints can be combined on the same dependency and will be joined with commas in the output.
121
+
122
+ Given a `package.xml` with version constraints:
123
+
124
+ ```xml
125
+ <depend version_gte="3.12.4">cmake</depend>
126
+ <build_depend version_gte="3.3.0" version_lt="4.0.0">eigen</build_depend>
127
+ <exec_depend version_eq="1.2.3">boost</exec_depend>
128
+ ```
129
+
130
+ `pixi-ros init` generates:
131
+
132
+ ```toml
133
+ [dependencies]
134
+ cmake = ">=3.12.4"
135
+ eigen = ">=3.3.0,<4.0.0"
136
+ boost = "==1.2.3"
137
+ ```
138
+
104
139
  ## Supported ROS Distributions
105
140
 
106
141
  - ROS 2 Humble: https://prefix.dev/robostack-humble
@@ -128,8 +163,9 @@ pixi-ros init
128
163
 
129
164
  **What it does:**
130
165
  - Scans workspace for `package.xml` files
131
- - Reads all dependency types (build, exec, test)
166
+ - Reads all dependency types (build, exec, test) and version constraints
132
167
  - Maps ROS dependencies to conda packages for each platform
168
+ - Applies version constraints from package.xml to pixi.toml dependencies
133
169
  - Configures robostack channels
134
170
  - Checks package availability per platform
135
171
  - Creates build tasks using colcon
@@ -1073,7 +1073,7 @@ packages:
1073
1073
  timestamp: 1769677743677
1074
1074
  - conda: .
1075
1075
  name: pixi-ros
1076
- version: 0.1.2
1076
+ version: 0.3.0
1077
1077
  build: pyh4616a5c_0
1078
1078
  subdir: noarch
1079
1079
  variants:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pixi-ros"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Pixi extension for ROS package management"
9
9
  authors = [
10
10
  { name = "Ruben Arts", email = "ruben@prefix.dev" }
@@ -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:
@@ -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
 
@@ -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]:
@@ -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]:
@@ -0,0 +1,125 @@
1
+ # Pixi-ROS Workspace
2
+
3
+ This ROS jazzy workspace is configured to use [Pixi](https://pixi.sh) for dependency management.
4
+
5
+ ## What is Pixi?
6
+
7
+ Pixi is a modern package manager that uses conda packages. It provides:
8
+ - Fast, reproducible dependency resolution
9
+ - Automatic environment management
10
+ - Reproducible with a lockfile
11
+ - Cross-platform support (Linux, macOS, Windows)
12
+
13
+ ## Getting Started
14
+
15
+ ### 1. Install Dependencies
16
+
17
+ ```bash
18
+ pixi install
19
+ ```
20
+
21
+ This installs all dependencies specified in `pixi.toml` from the configured channels.
22
+
23
+ ### 2. Build the Workspace
24
+
25
+ ```bash
26
+ pixi run build
27
+ ```
28
+
29
+ This runs `colcon build` to compile your ROS packages.
30
+
31
+ ### 3. Run Tests
32
+
33
+ ```bash
34
+ pixi run test
35
+ ```
36
+
37
+ This runs `colcon test` to execute your test suite.
38
+
39
+ ### 4. Clean Build Artifacts
40
+
41
+ ```bash
42
+ pixi run clean
43
+ ```
44
+
45
+ Removes `build/`, `install/`, and `log/` directories.
46
+
47
+ ### 5. Add additional dependencies
48
+
49
+ ```bash
50
+ pixi add <package-name>
51
+ # or for packages you would install with pip/uv/poetry:
52
+ pixi add --pypi <package-name>
53
+ ```
54
+
55
+ This adds new dependencies to `pixi.toml` and installs them.
56
+
57
+ ## Environment Activation
58
+
59
+ After the first build, pixi will automatically source the ROS setup script (`install/setup.bash`)
60
+ when you enter the pixi environment. This means you don't need to manually source it!
61
+
62
+ To activate the environment, run:
63
+
64
+ ```bash
65
+ pixi shell
66
+ ```
67
+
68
+ This starts a new shell with the ROS environment activated.
69
+
70
+ The environment will also be automatically activated when you run commands with `pixi run <command>`.
71
+
72
+ ## Adding Dependencies
73
+
74
+ ### Add a Conda Package
75
+
76
+ ```bash
77
+ pixi add <package-name>
78
+ ```
79
+
80
+ ### Add ROS Dependencies
81
+
82
+ When you add dependencies to your `package.xml` files, run:
83
+
84
+ ```bash
85
+ pixi ros init --distro jazzy
86
+ ```
87
+
88
+ This will update `pixi.toml` with the new dependencies.
89
+
90
+ ## Unavailable Packages
91
+
92
+ If you see commented-out packages in `pixi.toml` with `# NOT FOUND`, these packages
93
+ were not found in the configured channels. You may need to:
94
+ - Check if the package name is correct
95
+ - Add additional channels with `pixi project channel add <channel-url>`
96
+ - Install the package through pip: `pixi add --pypi <package-name>`
97
+ - Add it to [conda-forge](https://github.com/conda-forge/staged-recipes) or [RoboStack](https://robostack.github.io/Contributing.html)
98
+
99
+ ## Common Issues
100
+
101
+ ### Build Fails
102
+
103
+ If `pixi run build` fails:
104
+ 1. Make sure all dependencies are installed: `pixi install`
105
+ 2. Clean and rebuild: `pixi run clean && pixi run build`
106
+ 3. Validate the build task is correct for your workspace.
107
+
108
+ ### Environment Issues
109
+
110
+ If `ros2` commands aren't found:
111
+ 1. Run commands through pixi: `pixi run <command>`
112
+ 2. Or use a pixi shell: `pixi shell`
113
+
114
+ ## Learn More
115
+
116
+ - **Pixi Documentation**: https://pixi.sh
117
+ - **RoboStack Documentation**: https://robostack.github.io/
118
+ - **ROS jazzy Documentation**: https://docs.ros.org/en/jazzy/
119
+ - **pixi-ros GitHub**: https://github.com/prefix-dev/pixi-ros
120
+
121
+ ## Channels
122
+
123
+ This workspace uses the following channels:
124
+ - `https://prefix.dev/robostack-jazzy` - ROS packages
125
+ - `https://prefix.dev/conda-forge` - System dependencies
@@ -0,0 +1,38 @@
1
+ [workspace]
2
+ name = "ws1"
3
+ channels = ["https://prefix.dev/robostack-jazzy", "https://prefix.dev/conda-forge"]
4
+ platforms = ["linux-64"]
5
+
6
+ [dependencies]
7
+ # Base ROS dependencies
8
+ ros-jazzy-ros-base = "*"
9
+ pkg-config = "*"
10
+ compilers = "*"
11
+ make = "*"
12
+ ninja = "*"
13
+ ros-jazzy-ros2cli = "*"
14
+
15
+ # Build tools
16
+ colcon-common-extensions = "*"
17
+ cmake = "<4"
18
+
19
+ # Workspace dependencies
20
+ numpy = "*"
21
+ pytest = "*"
22
+ ros-jazzy-ament-cmake = "*"
23
+ ros-jazzy-ament-copyright = "*"
24
+ ros-jazzy-ament-flake8 = "*"
25
+ ros-jazzy-ament-lint-auto = ">=1.2.3,>=1.2.3"
26
+ ros-jazzy-ament-lint-common = "==1.0.0,==1.0.0"
27
+ ros-jazzy-ament-pep257 = "*"
28
+
29
+ [tasks]
30
+ build = {cmd = "colcon build", description = "Build the ROS workspace"}
31
+ build-no-error = {cmd = "colcon build --continue-on-error --cmake-args -DCMAKE_CXX_FLAGS=\"-Wno-error\"", description = "Build the workspace ignoring errors and warnings"}
32
+ test = {cmd = "colcon test", description = "Run tests for the workspace"}
33
+ clean = {cmd = "rm -rf build install log", description = "Clean build artifacts (build, install, log directories)"}
34
+
35
+ # Scripts to source on environment activation, found after first colcon build.
36
+
37
+ [activation]
38
+ scripts = ["install/setup.bash"]
@@ -9,8 +9,8 @@
9
9
 
10
10
  <buildtool_depend>ament_cmake</buildtool_depend>
11
11
 
12
- <test_depend>ament_lint_auto</test_depend>
13
- <test_depend>ament_lint_common</test_depend>
12
+ <test_depend version_gte="1.2.3">ament_lint_auto</test_depend>
13
+ <test_depend version_eq="1.0.0">ament_lint_common</test_depend>
14
14
 
15
15
  <export>
16
16
  <build_type>ament_cmake</build_type>
@@ -14,6 +14,9 @@
14
14
  <test_depend>ament_pep257</test_depend>
15
15
  <test_depend>python3-pytest</test_depend>
16
16
 
17
+ <test_depend version_gte="1.2.3">ament_lint_auto</test_depend>
18
+ <test_depend version_eq="1.0.0">ament_lint_common</test_depend>
19
+
17
20
  <export>
18
21
  <build_type>ament_python</build_type>
19
22
  </export>
@@ -364,3 +364,195 @@ def test_unix_target_for_linux_and_osx_deps():
364
364
  linux_deps = config["target"]["linux"]["dependencies"]
365
365
  # GL packages specific to Linux
366
366
  assert any("libgl-devel" in str(dep).lower() or "libopengl-devel" in str(dep).lower() for dep in linux_deps.keys())
367
+
368
+
369
+ def test_tasks_have_descriptions():
370
+ """Test that generated tasks include descriptions."""
371
+ from pixi_ros.init import init_workspace
372
+
373
+ with TemporaryDirectory() as tmpdir:
374
+ workspace_path = Path(tmpdir)
375
+ src_dir = workspace_path / "src"
376
+ src_dir.mkdir()
377
+
378
+ # Create a simple package.xml
379
+ pkg_xml = src_dir / "package.xml"
380
+ pkg_xml.write_text("""<?xml version="1.0"?>
381
+ <package format="2">
382
+ <name>test_pkg</name>
383
+ <version>0.0.1</version>
384
+ <description>Test</description>
385
+ <maintainer email="test@test.com">Test</maintainer>
386
+ <license>MIT</license>
387
+ </package>
388
+ """)
389
+
390
+ # Initialize workspace
391
+ init_workspace("humble", workspace_path, platforms=["linux-64"])
392
+
393
+ # Check pixi.toml was created
394
+ toml_path = workspace_path / "pixi.toml"
395
+ assert toml_path.exists()
396
+
397
+ # Parse and check tasks
398
+ import tomlkit
399
+ with open(toml_path) as f:
400
+ config = tomlkit.load(f)
401
+
402
+ assert "tasks" in config
403
+ tasks = config["tasks"]
404
+
405
+ # Check that expected tasks exist and have descriptions
406
+ expected_tasks = {
407
+ "build": {
408
+ "cmd": "colcon build",
409
+ "description": "Build the ROS workspace",
410
+ },
411
+ "test": {
412
+ "cmd": "colcon test",
413
+ "description": "Run tests for the workspace",
414
+ },
415
+ "clean": {
416
+ "cmd": "rm -rf build install log",
417
+ "description": "Clean build artifacts (build, install, log directories)",
418
+ },
419
+ }
420
+
421
+ for task_name, expected_config in expected_tasks.items():
422
+ assert task_name in tasks, f"Task '{task_name}' not found in pixi.toml"
423
+ task = tasks[task_name]
424
+
425
+ # Task should be a dict/table with cmd and description
426
+ assert isinstance(task, dict), f"Task '{task_name}' should be a dictionary"
427
+ assert "cmd" in task, f"Task '{task_name}' missing 'cmd' field"
428
+ assert "description" in task, f"Task '{task_name}' missing 'description' field"
429
+
430
+ # Verify the content matches
431
+ assert task["cmd"] == expected_config["cmd"], f"Task '{task_name}' has wrong command"
432
+ assert task["description"] == expected_config["description"], f"Task '{task_name}' has wrong description"
433
+
434
+
435
+ def test_platforms_extended_not_overridden():
436
+ """Test that running init multiple times extends the platforms list instead of overriding it."""
437
+ from pixi_ros.init import init_workspace
438
+
439
+ with TemporaryDirectory() as tmpdir:
440
+ workspace_path = Path(tmpdir)
441
+ src_dir = workspace_path / "src"
442
+ src_dir.mkdir()
443
+
444
+ # Create a simple package.xml
445
+ pkg_xml = src_dir / "package.xml"
446
+ pkg_xml.write_text("""<?xml version="1.0"?>
447
+ <package format="2">
448
+ <name>test_pkg</name>
449
+ <version>0.0.1</version>
450
+ <description>Test</description>
451
+ <maintainer email="test@test.com">Test</maintainer>
452
+ <license>MIT</license>
453
+ </package>
454
+ """)
455
+
456
+ # Initialize with single platform
457
+ init_workspace("humble", workspace_path, platforms=["linux-64"])
458
+
459
+ # Check pixi.toml was created with linux-64
460
+ toml_path = workspace_path / "pixi.toml"
461
+ assert toml_path.exists()
462
+
463
+ import tomlkit
464
+ with open(toml_path) as f:
465
+ config = tomlkit.load(f)
466
+
467
+ assert "workspace" in config
468
+ assert "platforms" in config["workspace"]
469
+ assert config["workspace"]["platforms"] == ["linux-64"]
470
+
471
+ # Initialize again with additional platforms
472
+ init_workspace("humble", workspace_path, platforms=["osx-arm64", "win-64"])
473
+
474
+ # Read updated config
475
+ with open(toml_path) as f:
476
+ config = tomlkit.load(f)
477
+
478
+ # Verify all platforms are present
479
+ platforms = config["workspace"]["platforms"]
480
+ assert "linux-64" in platforms, "Original platform should still be present"
481
+ assert "osx-arm64" in platforms, "New platform osx-arm64 should be added"
482
+ assert "win-64" in platforms, "New platform win-64 should be added"
483
+ assert len(platforms) == 3, "Should have exactly 3 platforms"
484
+
485
+ # Verify no duplicates if we run init again with overlapping platforms
486
+ init_workspace("humble", workspace_path, platforms=["linux-64", "osx-64"])
487
+
488
+ with open(toml_path) as f:
489
+ config = tomlkit.load(f)
490
+
491
+ platforms = config["workspace"]["platforms"]
492
+ # linux-64 should not be duplicated
493
+ assert platforms.count("linux-64") == 1, "linux-64 should not be duplicated"
494
+ # osx-64 should be added (new)
495
+ assert "osx-64" in platforms, "New platform osx-64 should be added"
496
+ # All previous platforms should still be there
497
+ assert "osx-arm64" in platforms
498
+ assert "win-64" in platforms
499
+
500
+
501
+ def test_version_constraints_from_package_xml():
502
+ """Test that version constraints from package.xml are applied to pixi.toml dependencies."""
503
+ from pixi_ros.init import init_workspace
504
+
505
+ with TemporaryDirectory() as tmpdir:
506
+ workspace_path = Path(tmpdir)
507
+ src_dir = workspace_path / "src"
508
+ src_dir.mkdir()
509
+
510
+ # Create a package.xml with version-constrained dependencies
511
+ pkg_xml = src_dir / "package.xml"
512
+ pkg_xml.write_text("""<?xml version="1.0"?>
513
+ <package format="2">
514
+ <name>test_pkg</name>
515
+ <version>0.0.1</version>
516
+ <description>Test</description>
517
+ <maintainer email="test@test.com">Test</maintainer>
518
+ <license>MIT</license>
519
+ <depend version_gte="3.12.4">cmake</depend>
520
+ <build_depend version_gte="3.3.0" version_lt="4.0.0">eigen</build_depend>
521
+ <exec_depend version_eq="1.2.3">boost</exec_depend>
522
+ </package>
523
+ """)
524
+
525
+ # Initialize workspace
526
+ init_workspace("humble", workspace_path, platforms=["linux-64"])
527
+
528
+ # Check pixi.toml was created
529
+ toml_path = workspace_path / "pixi.toml"
530
+ assert toml_path.exists()
531
+
532
+ # Parse and check dependencies
533
+ import tomlkit
534
+ with open(toml_path) as f:
535
+ config = tomlkit.load(f)
536
+
537
+ assert "dependencies" in config
538
+ dependencies = config["dependencies"]
539
+
540
+ # Check that cmake has the version constraint
541
+ # Note: cmake might already have a constraint from CMakeLists.txt detection
542
+ # so we just verify it has some constraint
543
+ if "cmake" in dependencies:
544
+ cmake_version = dependencies["cmake"]
545
+ assert cmake_version != "*", "cmake should have a version constraint"
546
+ assert ">=" in cmake_version or "<" in cmake_version, "cmake should have a version constraint operator"
547
+
548
+ # Check that eigen has the version constraint (>=3.3.0,<4.0.0)
549
+ if "eigen" in dependencies:
550
+ eigen_version = dependencies["eigen"]
551
+ assert eigen_version != "*", "eigen should have a version constraint"
552
+ assert ">=3.3.0" in eigen_version or ">=" in eigen_version, "eigen should have >= constraint"
553
+
554
+ # Check that boost has the exact version constraint (==1.2.3)
555
+ if "boost" in dependencies:
556
+ boost_version = dependencies["boost"]
557
+ assert boost_version != "*", "boost should have a version constraint"
558
+ assert "==" in boost_version or "1.2.3" in str(boost_version), "boost should have == constraint"
@@ -236,3 +236,95 @@ def test_parse_missing_required_fields():
236
236
  PackageXML.from_file(temp_path)
237
237
  finally:
238
238
  temp_path.unlink()
239
+
240
+
241
+ def test_parse_version_constraints():
242
+ """Test that version constraints are correctly parsed from package.xml."""
243
+ from tempfile import NamedTemporaryFile
244
+
245
+ with NamedTemporaryFile(mode="w", suffix=".xml", delete=False) as f:
246
+ f.write(
247
+ """<?xml version="1.0"?>
248
+ <package format="3">
249
+ <name>test_pkg</name>
250
+ <version>1.0.0</version>
251
+ <description>Test package</description>
252
+ <maintainer email="test@test.com">Test</maintainer>
253
+ <license>MIT</license>
254
+ <depend version_gte="3.12.4">cmake</depend>
255
+ <build_depend version_gte="3.3.0" version_lt="4.0.0">eigen</build_depend>
256
+ <exec_depend version_eq="1.2.3">boost</exec_depend>
257
+ <depend version_lte="2.0.0">pkg_without_constraint</depend>
258
+ <depend>pkg_without_any_version</depend>
259
+ </package>
260
+ """
261
+ )
262
+ f.flush()
263
+ temp_path = Path(f.name)
264
+
265
+ try:
266
+ pkg = PackageXML.from_file(temp_path)
267
+
268
+ # Check that version constraints are stored
269
+ assert "cmake" in pkg.dependency_versions
270
+ assert pkg.dependency_versions["cmake"] == ">=3.12.4"
271
+
272
+ assert "eigen" in pkg.dependency_versions
273
+ # Multiple constraints should be combined with comma
274
+ assert ">=3.3.0" in pkg.dependency_versions["eigen"]
275
+ assert "<4.0.0" in pkg.dependency_versions["eigen"]
276
+ assert "," in pkg.dependency_versions["eigen"]
277
+
278
+ assert "boost" in pkg.dependency_versions
279
+ assert pkg.dependency_versions["boost"] == "==1.2.3"
280
+
281
+ assert "pkg_without_constraint" in pkg.dependency_versions
282
+ assert pkg.dependency_versions["pkg_without_constraint"] == "<=2.0.0"
283
+
284
+ # Package without version constraint should not be in the map
285
+ assert "pkg_without_any_version" not in pkg.dependency_versions
286
+
287
+ # Check that packages are still in their respective dependency lists
288
+ assert "cmake" in pkg.depends
289
+ assert "eigen" in pkg.build_depends
290
+ assert "boost" in pkg.exec_depends
291
+ assert "pkg_without_constraint" in pkg.depends
292
+ assert "pkg_without_any_version" in pkg.depends
293
+ finally:
294
+ temp_path.unlink()
295
+
296
+
297
+ def test_parse_all_version_constraint_types():
298
+ """Test parsing all types of version constraints."""
299
+ from tempfile import NamedTemporaryFile
300
+
301
+ with NamedTemporaryFile(mode="w", suffix=".xml", delete=False) as f:
302
+ f.write(
303
+ """<?xml version="1.0"?>
304
+ <package format="3">
305
+ <name>test_pkg</name>
306
+ <version>1.0.0</version>
307
+ <description>Test package</description>
308
+ <maintainer email="test@test.com">Test</maintainer>
309
+ <license>MIT</license>
310
+ <depend version_lt="2.0.0">pkg_lt</depend>
311
+ <depend version_lte="2.5.0">pkg_lte</depend>
312
+ <depend version_eq="1.2.3">pkg_eq</depend>
313
+ <depend version_gte="1.0.0">pkg_gte</depend>
314
+ <depend version_gt="0.5.0">pkg_gt</depend>
315
+ </package>
316
+ """
317
+ )
318
+ f.flush()
319
+ temp_path = Path(f.name)
320
+
321
+ try:
322
+ pkg = PackageXML.from_file(temp_path)
323
+
324
+ assert pkg.dependency_versions["pkg_lt"] == "<2.0.0"
325
+ assert pkg.dependency_versions["pkg_lte"] == "<=2.5.0"
326
+ assert pkg.dependency_versions["pkg_eq"] == "==1.2.3"
327
+ assert pkg.dependency_versions["pkg_gte"] == ">=1.0.0"
328
+ assert pkg.dependency_versions["pkg_gt"] == ">0.5.0"
329
+ finally:
330
+ temp_path.unlink()
@@ -1,39 +0,0 @@
1
- [workspace]
2
- name = "ws1"
3
- channels = ["https://prefix.dev/robostack-jazzy", "https://prefix.dev/conda-forge"]
4
- platforms = ["osx-arm64"]
5
-
6
- [dependencies]
7
- # Base ROS dependencies
8
- ros-jazzy-ros-base = "*"
9
- ros-jazzy-ros2cli = "*"
10
-
11
- # Build tools
12
- colcon-common-extensions = "*"
13
- cmake = "<4"
14
-
15
- # From package: package-a
16
- ros-jazzy-ament-cmake = "*"
17
- ros-jazzy-ament-lint-auto = "*"
18
- ros-jazzy-ament-lint-common = "*"
19
-
20
- # From package: package-b
21
- numpy = "*"
22
- pytest = "*"
23
- ros-jazzy-ament-copyright = "*"
24
- ros-jazzy-ament-flake8 = "*"
25
- ros-jazzy-ament-pep257 = "*"
26
-
27
- [tasks]
28
- build = "colcon build"
29
- test = "colcon test"
30
- clean = "rm -rf build install log"
31
-
32
- [tool.pytest.ini_options]
33
- python_files = ["test_*.py"]
34
- python_functions = ["test_*"]
35
-
36
- # Scripts to source on environment activation, found after first colcon build.
37
-
38
- [activation]
39
- scripts = ["install/setup.bash"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes