pixi-ros 0.1.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/workspace.py ADDED
@@ -0,0 +1,213 @@
1
+ """Workspace discovery and management for ROS packages."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pathspec
6
+
7
+ from pixi_ros.package_xml import PackageXML
8
+
9
+
10
+ def load_gitignore_spec(workspace_root: Path) -> pathspec.PathSpec | None:
11
+ """
12
+ Load gitignore patterns from workspace root.
13
+
14
+ Args:
15
+ workspace_root: Root directory of the workspace
16
+
17
+ Returns:
18
+ PathSpec object with gitignore patterns, or None if no .gitignore exists
19
+ """
20
+ gitignore_path = workspace_root / ".gitignore"
21
+ if not gitignore_path.exists():
22
+ return None
23
+
24
+ try:
25
+ with open(gitignore_path) as f:
26
+ patterns = f.read().splitlines()
27
+ return pathspec.PathSpec.from_lines("gitignore", patterns)
28
+ except (OSError, ValueError):
29
+ return None
30
+
31
+
32
+ def find_package_xml(start_path: Path | None = None) -> Path | None:
33
+ """
34
+ Find the nearest package.xml file by searching upward from start_path.
35
+
36
+ Args:
37
+ start_path: Starting directory for search (defaults to cwd)
38
+
39
+ Returns:
40
+ Path to package.xml if found, None otherwise
41
+ """
42
+ if start_path is None:
43
+ start_path = Path.cwd()
44
+
45
+ current = start_path.resolve()
46
+
47
+ # Search upward until we hit the root
48
+ while True:
49
+ package_xml = current / "package.xml"
50
+ if package_xml.exists():
51
+ return package_xml
52
+
53
+ parent = current.parent
54
+ if parent == current: # Reached root
55
+ break
56
+ current = parent
57
+
58
+ return None
59
+
60
+
61
+ def find_workspace_root(start_path: Path | None = None) -> Path | None:
62
+ """
63
+ Find the workspace root by looking for a directory with ROS package.xml files.
64
+
65
+ Searches recursively for package.xml files, excluding hidden directories and
66
+ build artifacts. Returns the directory containing packages, or the parent of
67
+ a 'src' directory if packages are organized in that structure.
68
+
69
+ Args:
70
+ start_path: Starting directory for search (defaults to cwd)
71
+
72
+ Returns:
73
+ Path to workspace root if found, None otherwise
74
+ """
75
+ if start_path is None:
76
+ start_path = Path.cwd()
77
+
78
+ current = start_path.resolve()
79
+ skip_dirs = {"build", "install", "log", ".pixi"}
80
+
81
+ # Helper to check if a directory has package.xml files
82
+ def has_packages(path: Path) -> bool:
83
+ """Check if path contains any package.xml files (recursively)."""
84
+ # Load gitignore patterns if available
85
+ gitignore_spec = load_gitignore_spec(path)
86
+
87
+ for package_xml in path.rglob("package.xml"):
88
+ # Get relative path from workspace root
89
+ relative_path = package_xml.relative_to(path)
90
+ relative_parts = relative_path.parts
91
+
92
+ # Skip if any parent is hidden or in skip list
93
+ if any(
94
+ part.startswith(".") or part in skip_dirs for part in relative_parts
95
+ ):
96
+ continue
97
+
98
+ # Skip if matched by gitignore patterns
99
+ if gitignore_spec and gitignore_spec.match_file(str(relative_path)):
100
+ continue
101
+
102
+ return True
103
+ return False
104
+
105
+ # First, check if we're inside a package - search upward for package.xml
106
+ package_xml = find_package_xml(current)
107
+ if package_xml:
108
+ # We found a package.xml, so determine the workspace root
109
+ package_dir = package_xml.parent
110
+ potential_src = package_dir.parent
111
+
112
+ # Check if parent directory is named 'src'
113
+ if potential_src.name == "src":
114
+ # The workspace root is the parent of 'src'
115
+ return potential_src.parent
116
+
117
+ # Otherwise, return the parent of the package directory
118
+ return package_dir.parent
119
+
120
+ # Check if current directory has any packages
121
+ if has_packages(current):
122
+ # If the directory is named 'src', return its parent as the workspace root
123
+ if current.name == "src":
124
+ return current.parent
125
+ return current
126
+
127
+ # Search upward for a directory containing packages
128
+ while True:
129
+ if has_packages(current):
130
+ # If the directory is named 'src', return its parent as the workspace root
131
+ if current.name == "src":
132
+ return current.parent
133
+ return current
134
+
135
+ parent = current.parent
136
+ if parent == current: # Reached root
137
+ break
138
+ current = parent
139
+
140
+ # If no packages found anywhere, return the starting directory
141
+ # This allows initializing a new workspace in an empty directory
142
+ return start_path.resolve()
143
+
144
+
145
+ def discover_packages(workspace_root: Path) -> list[PackageXML]:
146
+ """
147
+ Discover all ROS packages in a workspace.
148
+
149
+ Recursively searches for all package.xml files in the workspace,
150
+ excluding hidden directories (those starting with a dot).
151
+
152
+ Args:
153
+ workspace_root: Root directory of the workspace
154
+
155
+ Returns:
156
+ List of parsed PackageXML objects
157
+
158
+ Raises:
159
+ ValueError: If workspace_root doesn't exist or isn't a directory
160
+ """
161
+ if not workspace_root.exists():
162
+ raise ValueError(f"Workspace root does not exist: {workspace_root}")
163
+
164
+ if not workspace_root.is_dir():
165
+ raise ValueError(f"Workspace root is not a directory: {workspace_root}")
166
+
167
+ packages = []
168
+
169
+ # Directories to skip during recursive search
170
+ skip_dirs = {"build", "install", "log", ".pixi", "tests", "test"}
171
+
172
+ # Load gitignore patterns if available
173
+ gitignore_spec = load_gitignore_spec(workspace_root)
174
+
175
+ # Recursively find all package.xml files
176
+ for package_xml_path in workspace_root.rglob("package.xml"):
177
+ # Get relative path from workspace root
178
+ relative_path = package_xml_path.relative_to(workspace_root)
179
+ relative_parts = relative_path.parts
180
+
181
+ # Skip if any parent directory is hidden (starts with .) or in skip list
182
+ if any(part.startswith(".") or part in skip_dirs for part in relative_parts):
183
+ continue
184
+
185
+ # Skip if matched by gitignore patterns
186
+ if gitignore_spec and gitignore_spec.match_file(str(relative_path)):
187
+ continue
188
+
189
+ try:
190
+ package = PackageXML.from_file(package_xml_path)
191
+ packages.append(package)
192
+ except (FileNotFoundError, ValueError) as e:
193
+ # Log warning but continue
194
+ print(f"Warning: Could not parse {package_xml_path}: {e}")
195
+
196
+ return packages
197
+
198
+
199
+ def is_workspace_package(
200
+ package_name: str, workspace_packages: list[PackageXML]
201
+ ) -> bool:
202
+ """
203
+ Check if a package name refers to a package in the workspace.
204
+
205
+ Args:
206
+ package_name: Name of the package to check
207
+ workspace_packages: List of packages in the workspace
208
+
209
+ Returns:
210
+ True if package is in the workspace, False otherwise
211
+ """
212
+ workspace_names = {pkg.name for pkg in workspace_packages}
213
+ return package_name in workspace_names
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.4
2
+ Name: pixi-ros
3
+ Version: 0.1.0
4
+ Summary: Pixi extension for ROS package management
5
+ Project-URL: Homepage, https://github.com/ruben-arts/pixi-ros
6
+ Project-URL: Repository, https://github.com/ruben-arts/pixi-ros
7
+ Author-email: Ruben Arts <ruben@prefix.dev>
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: lxml>=5.0.0
10
+ Requires-Dist: pathspec>=0.11.0
11
+ Requires-Dist: py-rattler>=0.6.0
12
+ Requires-Dist: pyyaml>=6.0
13
+ Requires-Dist: rich>=13.0.0
14
+ Requires-Dist: tomlkit>=0.12.0
15
+ Requires-Dist: typer>=0.12.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # pixi-ros
19
+
20
+ **Bridge your ROS workspace to the modern conda/Pixi ecosystem**
21
+
22
+ pixi-ros helps ROS developers transition from `rosdep` to [Pixi](https://pixi.sh) for package management.
23
+ It automatically reads your ROS workspace's `package.xml` files and generates a `pixi.toml` manifest with all dependencies resolved from conda channels (primarily [robostack](https://robostack.org/)).
24
+
25
+ ## Why pixi-ros?
26
+
27
+ If you're a ROS developer, you're probably familiar with `rosdep` managing dependencies.
28
+ `pixi-ros` gives you access to a more modern package management ecosystem:
29
+
30
+ - **Reproducible environments**: Lock files ensure everyone on your team has identical dependencies
31
+ - **Cross-platform**: Works seamlessly on Linux, macOS, and Windows
32
+ - **Fast and reliable**: Uses rattler (Rust implementation of conda) for speed
33
+ - **No system dependencies**: Everything isolated in project environments
34
+
35
+ ## Quick Start
36
+
37
+ ### Installation
38
+
39
+ Install pixi first if you haven't already:
40
+
41
+ ```bash
42
+ curl -fsSL https://pixi.sh/install.sh | bash
43
+ ```
44
+
45
+ Or follow instructions at https://pixi.sh/latest/installation/
46
+
47
+ Install pixi-ros globally using pixi:
48
+
49
+ ```bash
50
+ pixi global install pixi-ros
51
+ ```
52
+
53
+ ### Initialize Your ROS Workspace
54
+
55
+ Navigate to your ROS workspace and run:
56
+
57
+ ```bash
58
+ pixi-ros init --distro humble
59
+ ```
60
+
61
+ This will:
62
+ 1. Discover all ROS packages in your workspace (by finding `package.xml` files)
63
+ 2. Read dependencies from each `package.xml`
64
+ 3. Map ROS package names to conda packages
65
+ 4. Generate/update `pixi.toml` with proper channels and dependencies
66
+ 5. Check package availability and warn about missing packages
67
+ 6. Create helpful build/test/clean tasks
68
+
69
+ ### Install and Build
70
+
71
+ After initialization, use standard pixi commands:
72
+
73
+ ```bash
74
+ # Install all dependencies
75
+ pixi install
76
+
77
+ # Build your workspace
78
+ pixi run build
79
+
80
+ # Run tests
81
+ pixi run test
82
+
83
+ # Activate environment for direct ROS commands
84
+ pixi shell
85
+ ```
86
+
87
+ ## How It Works
88
+
89
+ ### Dependency Mapping
90
+
91
+ `pixi-ros` reads all dependency types from `package.xml` files.
92
+ It then does a best effort mapping of ROS package names to conda packages.
93
+
94
+ - **ROS packages**: `ros-{distro}-{package}` from robostack channels (e.g., `ros-humble-rclcpp`)
95
+ - **System packages**: Mapped to conda-forge equivalents (e.g., `cmake`, `eigen`)
96
+
97
+ After the mapping, it validates package availability in the configured channels. This starts a connection with `https://prefix.dev` to check if packages exist.
98
+
99
+ ### Example
100
+
101
+ Given a `package.xml` with:
102
+
103
+ ```xml
104
+ <depend>rclcpp</depend>
105
+ <build_depend>ament_cmake</build_depend>
106
+ <exec_depend>std_msgs</exec_depend>
107
+ ```
108
+
109
+ `pixi-ros init --distro humble` generates a `pixi.toml` with:
110
+
111
+ ```toml
112
+ [dependencies]
113
+ ros-humble-ament-cmake = "*"
114
+ ros-humble-rclcpp = "*"
115
+ ros-humble-std-msgs = "*"
116
+ ```
117
+
118
+ ## Supported ROS Distributions
119
+
120
+ - ROS 2 Humble: https://prefix.dev/robostack-humble
121
+ - ROS 2 Iron: https://prefix.dev/robostack-iron
122
+ - ROS 2 Jazzy: https://prefix.dev/robostack-jazzy
123
+ - ROS 2 Rolling: https://prefix.dev/robostack-rolling
124
+
125
+ ## Command Reference
126
+
127
+ ### `pixi-ros init`
128
+
129
+ Initialize or update a ROS workspace's `pixi.toml`.
130
+
131
+ ```bash
132
+ pixi-ros init --distro <ros_distro>
133
+ pixi-ros init
134
+ ```
135
+
136
+ **Options:**
137
+ - `--distro`, `-d`: ROS distribution (optional)
138
+
139
+ **What it does:**
140
+ - Scans workspace for `package.xml` files
141
+ - Reads all dependency types (build, exec, test)
142
+ - Maps ROS dependencies to conda packages
143
+ - Configures robostack channels
144
+ - Checks package availability
145
+ - Creates build tasks using colcon
146
+ - Generates helpful `README_PIXI.md`
147
+
148
+ **Running multiple times:**
149
+ The command is idempotent - you can run it multiple times to update dependencies as your workspace changes.
150
+
151
+ ## Philosophy
152
+
153
+ `pixi-ros` aims to be a quick **gateway drug**. It:
154
+
155
+ - Respects existing ROS conventions (package.xml as source of truth)
156
+ - Uses standard ROS build tools (colcon)
157
+ - Focuses only on dependency management and environment setup
158
+ - Doesn't replace `ros2` CLI or other ROS tooling
159
+ - Should eventually become unnecessary as the ecosystem matures
160
+
161
+ Think of it as a "gateway" to help ROS developers benefit from modern package management while keeping familiar workflows.
162
+
163
+ ## Project Structure
164
+
165
+ After initialization, your workspace will have:
166
+
167
+ ```
168
+ workspace/
169
+ ├── src/ # Your ROS packages
170
+ │ └── my_package/
171
+ │ ├── package.xml # ROS package manifest (source of truth)
172
+ │ └── ...
173
+ ├── pixi.toml # Generated pixi manifest
174
+ ├── pixi.lock # Locked dependencies (commit this!)
175
+ └── README_PIXI.md # Generated usage guide
176
+ ```
177
+
178
+ ## Troubleshooting
179
+
180
+ ### Package Not Found
181
+
182
+ If pixi-ros marks packages as "NOT FOUND":
183
+
184
+ 1. Check if the package exists in robostack: https://prefix.dev/channels/robostack-{distro}
185
+ 2. Check for typos in `package.xml`
186
+ 3. Some packages may have different names - check mapping files
187
+ 4. Consider adding the package to your workspace instead of depending on it
188
+
189
+ ### Different Package Names
190
+
191
+ pixi-ros includes mapping files for system packages (e.g., `cmake` → `cmake`, `eigen` → `eigen`). You can override mappings by creating `pixi-ros/*.yaml` files in your workspace or `~/.pixi-ros/`.
192
+
193
+ ### Platform-Specific Issues
194
+
195
+ Some packages have platform-specific mappings. pixi-ros handles this automatically, but you can test different platforms using the internal API with `platform_override`.
196
+
197
+ ## Contributing
198
+
199
+ Contributions welcome! Feel free to open issues or PRs on GitHub.
200
+
201
+ ## Learn More
202
+
203
+ - **Pixi**: https://pixi.sh
204
+ - **RoboStack**: https://robostack.org/
205
+ - **Conda**: https://docs.conda.io/
206
+ - **ROS 2**: https://docs.ros.org/
207
+
208
+ ## Disclaimer
209
+
210
+ This tool is build with heavy use of AI assistance and is under active development. Please report issues or contribute on GitHub!
211
+
212
+ I (Ruben) hope `pixi-ros` can die ASAP, as all of the workflows this tool provides should ideally be native to Pixi itself. But until then, I hope this initialization tool helps you get started!
@@ -0,0 +1,15 @@
1
+ pixi_ros/__init__.py,sha256=ovhhu7PJry3WtvhA2PgelrFWnWvrQkQKD798sW9GWjs,84
2
+ pixi_ros/cli.py,sha256=eLT1GNfq3JY8yzJrxDkNjeUtKI5T8T_57XaDMw_XX-s,2145
3
+ pixi_ros/config.py,sha256=JevtFXh96UJj7aMUWp18J1xcPeQPoLzh3hdiu5uPR0s,834
4
+ pixi_ros/init.py,sha256=Y4k2y_NZOwt0UQ4VMOIWr3Eyg3i2IPU83xtQPd1uW2U,19107
5
+ pixi_ros/mappings.py,sha256=WCo1ftPe8knJROy2NyLuPtk0R-zDGWTgCcQnGp5LkFA,9139
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.1.0.dist-info/METADATA,sha256=L1MyvMRZKUZxx90hlCNgur5k9VDQy3_PBhfJtN2PWVw,6475
13
+ pixi_ros-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ pixi_ros-0.1.0.dist-info/entry_points.txt,sha256=DpBwU4Djcej8gT42q8Ccuv-R9pdmGHyFV5p57_ogqfQ,47
15
+ pixi_ros-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pixi-ros = pixi_ros.cli:main