colcon-uv 0.0.1__py2.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.
- colcon_uv/__init__.py +3 -0
- colcon_uv/cli.py +102 -0
- colcon_uv/dependencies/__init__.py +0 -0
- colcon_uv/dependencies/install.py +187 -0
- colcon_uv/package_augmentation/__init__.py +5 -0
- colcon_uv/package_augmentation/uv.py +122 -0
- colcon_uv/package_identification/__init__.py +5 -0
- colcon_uv/package_identification/uv.py +53 -0
- colcon_uv/task/__init__.py +6 -0
- colcon_uv/task/uv/__init__.py +6 -0
- colcon_uv/task/uv/build.py +170 -0
- colcon_uv/task/uv/test.py +47 -0
- colcon_uv-0.0.1.dist-info/METADATA +72 -0
- colcon_uv-0.0.1.dist-info/RECORD +16 -0
- colcon_uv-0.0.1.dist-info/WHEEL +5 -0
- colcon_uv-0.0.1.dist-info/entry_points.txt +14 -0
colcon_uv/__init__.py
ADDED
colcon_uv/cli.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Command-line interface for colcon-uv."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from colcon_core.plugin_system import satisfies_version
|
|
9
|
+
from colcon_core.verb import VerbExtensionPoint
|
|
10
|
+
|
|
11
|
+
from .dependencies.install import discover_packages, install_dependencies
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UvVerb(VerbExtensionPoint):
|
|
15
|
+
"""UV command for colcon."""
|
|
16
|
+
|
|
17
|
+
def __init__(self): # noqa: D107
|
|
18
|
+
super().__init__()
|
|
19
|
+
satisfies_version(VerbExtensionPoint.EXTENSION_POINT_VERSION, "^1.0")
|
|
20
|
+
|
|
21
|
+
def add_arguments(self, *, parser): # noqa: D102
|
|
22
|
+
subparsers = parser.add_subparsers(dest="uv_command", help="UV commands")
|
|
23
|
+
|
|
24
|
+
# Install subcommand
|
|
25
|
+
install_parser = subparsers.add_parser(
|
|
26
|
+
"install", help="Install dependencies for UV-based packages"
|
|
27
|
+
)
|
|
28
|
+
install_parser.add_argument(
|
|
29
|
+
"--base-paths",
|
|
30
|
+
nargs="*",
|
|
31
|
+
help="The base paths to recursively crawl for packages",
|
|
32
|
+
)
|
|
33
|
+
install_parser.add_argument(
|
|
34
|
+
"--uv-args", nargs="*", help="Additional arguments to pass to UV"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def main(self, *, context): # noqa: D102
|
|
38
|
+
args = context.args
|
|
39
|
+
logger = logging.getLogger("colcon.uv.cli")
|
|
40
|
+
|
|
41
|
+
if not hasattr(args, "uv_command") or args.uv_command != "install":
|
|
42
|
+
logger.error("Usage: colcon uv install [options]")
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
# Get base paths
|
|
46
|
+
base_paths = args.base_paths
|
|
47
|
+
if not base_paths:
|
|
48
|
+
base_paths = [Path(".")]
|
|
49
|
+
else:
|
|
50
|
+
base_paths = [Path(p) for p in base_paths]
|
|
51
|
+
|
|
52
|
+
# Discover UV packages using the install.py logic
|
|
53
|
+
try:
|
|
54
|
+
packages = discover_packages(base_paths)
|
|
55
|
+
except SystemExit:
|
|
56
|
+
logger.warning("No UV packages found")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
# Install dependencies for each package using install.py logic
|
|
60
|
+
install_base = Path("install") # Default install base
|
|
61
|
+
merge_install = False # Default to separate installs
|
|
62
|
+
|
|
63
|
+
for package in packages:
|
|
64
|
+
try:
|
|
65
|
+
install_dependencies(package, install_base, merge_install)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(
|
|
68
|
+
f"Failed to install dependencies for '{package.name}': {e}"
|
|
69
|
+
)
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main():
|
|
76
|
+
"""Run the CLI."""
|
|
77
|
+
parser = argparse.ArgumentParser(
|
|
78
|
+
description="Install dependencies for UV-based packages"
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--base-paths",
|
|
82
|
+
nargs="*",
|
|
83
|
+
help="The base paths to recursively crawl for packages",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--uv-args", nargs="*", help="Additional arguments to pass to UV"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
args = parser.parse_args()
|
|
90
|
+
|
|
91
|
+
# Set up logging
|
|
92
|
+
logging.basicConfig(
|
|
93
|
+
level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Run the UV verb
|
|
97
|
+
verb = UvVerb()
|
|
98
|
+
return verb.main(context=argparse.Namespace(args=args))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Install dependencies for UV packages."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
import tomli
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("colcon.uv.dependencies")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NotAUvPackageError(Exception):
|
|
16
|
+
"""Raised when a directory is not a UV package."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UvPackage:
|
|
22
|
+
"""Represents a UV package."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, path: Path, logger=None):
|
|
25
|
+
"""Initialize UV package."""
|
|
26
|
+
self.path = path
|
|
27
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
self.pyproject_file = path / "pyproject.toml"
|
|
30
|
+
if not self.pyproject_file.exists():
|
|
31
|
+
raise NotAUvPackageError(f"No pyproject.toml found in {path}")
|
|
32
|
+
|
|
33
|
+
# Load pyproject.toml
|
|
34
|
+
with open(self.pyproject_file, "rb") as f:
|
|
35
|
+
self.pyproject_data = tomli.load(f)
|
|
36
|
+
|
|
37
|
+
# Check if it's a UV package
|
|
38
|
+
if (
|
|
39
|
+
"tool" not in self.pyproject_data
|
|
40
|
+
or "colcon-uv-ros" not in self.pyproject_data["tool"]
|
|
41
|
+
):
|
|
42
|
+
raise NotAUvPackageError(
|
|
43
|
+
f"No [tool.colcon-uv-ros] section found in {self.pyproject_file}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Get package name
|
|
47
|
+
if (
|
|
48
|
+
"project" in self.pyproject_data
|
|
49
|
+
and "name" in self.pyproject_data["project"]
|
|
50
|
+
):
|
|
51
|
+
self.name = self.pyproject_data["project"]["name"]
|
|
52
|
+
else:
|
|
53
|
+
self.name = path.name
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
"""Main entry point for UV dependency installation."""
|
|
58
|
+
args = _parse_args()
|
|
59
|
+
logging.basicConfig(
|
|
60
|
+
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
61
|
+
format="%(levelname)s:%(name)s: %(message)s",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
for project in discover_packages(args.base_paths):
|
|
65
|
+
logger.info(f"Installing dependencies for {project.path.name}...")
|
|
66
|
+
install_dependencies(project, args.install_base, args.merge_install)
|
|
67
|
+
|
|
68
|
+
logger.info("Dependencies installed!")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def discover_packages(base_paths: List[Path]) -> List[UvPackage]:
|
|
72
|
+
"""Discover UV packages in the given base paths."""
|
|
73
|
+
projects: List[UvPackage] = []
|
|
74
|
+
|
|
75
|
+
potential_packages = []
|
|
76
|
+
for path in base_paths:
|
|
77
|
+
potential_packages += list(path.glob("*"))
|
|
78
|
+
|
|
79
|
+
for path in potential_packages:
|
|
80
|
+
if path.is_dir():
|
|
81
|
+
try:
|
|
82
|
+
project = UvPackage(path)
|
|
83
|
+
except NotAUvPackageError:
|
|
84
|
+
continue
|
|
85
|
+
else:
|
|
86
|
+
projects.append(project)
|
|
87
|
+
|
|
88
|
+
if len(projects) == 0:
|
|
89
|
+
base_paths_str = ", ".join([str(p) for p in base_paths])
|
|
90
|
+
logger.error(
|
|
91
|
+
f"No UV packages were found in the following paths: {base_paths_str}"
|
|
92
|
+
)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
return projects
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def install_dependencies(
|
|
99
|
+
project: UvPackage, install_base: Path, merge_install: bool
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Install dependencies for a UV package using UV."""
|
|
102
|
+
# Handle both contexts:
|
|
103
|
+
# 1. Direct install: install_base = /install, need to add package name
|
|
104
|
+
# 2. Build task: install_base = /install/package_name, already included
|
|
105
|
+
|
|
106
|
+
if not merge_install:
|
|
107
|
+
# Check if install_base already ends with the package name
|
|
108
|
+
if install_base.name != project.name:
|
|
109
|
+
install_base /= project.name
|
|
110
|
+
|
|
111
|
+
# Create the install directory first
|
|
112
|
+
install_base.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
|
|
114
|
+
# Venv path - this should be /install/PACKAGE_NAME/venv/
|
|
115
|
+
venv_path = install_base / "venv"
|
|
116
|
+
|
|
117
|
+
# Create virtual environment at the target location with system packages access
|
|
118
|
+
# --system-site-packages is needed because ROS 2 packages like rclpy are installed
|
|
119
|
+
# system-wide (not available on PyPI) and our nodes need access to them
|
|
120
|
+
subprocess.run(["uv", "venv", "--system-site-packages", str(venv_path)], check=True)
|
|
121
|
+
|
|
122
|
+
# Install dependencies and the package itself to the target venv
|
|
123
|
+
# Use --python to specify the target venv's python
|
|
124
|
+
python_exe = venv_path / "bin" / "python"
|
|
125
|
+
subprocess.run(
|
|
126
|
+
["uv", "pip", "install", "--python", str(python_exe), "-e", str(project.path)],
|
|
127
|
+
check=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def install_dependencies_from_descriptor(
|
|
132
|
+
pkg_descriptor, install_base: Path, merge_install: bool
|
|
133
|
+
):
|
|
134
|
+
"""Install dependencies from a PackageDescriptor object.
|
|
135
|
+
|
|
136
|
+
This is a convenience function for use by colcon build tasks.
|
|
137
|
+
"""
|
|
138
|
+
try:
|
|
139
|
+
uv_package = UvPackage(pkg_descriptor.path)
|
|
140
|
+
install_dependencies(uv_package, install_base, merge_install)
|
|
141
|
+
except NotAUvPackageError as e:
|
|
142
|
+
# Skip packages that aren't UV packages
|
|
143
|
+
logger.debug(f"Skipping non-UV package {pkg_descriptor.name}: {e}")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _parse_args() -> argparse.Namespace:
|
|
148
|
+
"""Parse command line arguments."""
|
|
149
|
+
parser = argparse.ArgumentParser(
|
|
150
|
+
description="Searches for UV packages and installs their dependencies "
|
|
151
|
+
"to a configurable install base"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--base-paths",
|
|
156
|
+
nargs="+",
|
|
157
|
+
type=Path,
|
|
158
|
+
default=[Path.cwd()],
|
|
159
|
+
help="The paths to start looking for UV projects in. Defaults to the "
|
|
160
|
+
"current directory.",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--install-base",
|
|
165
|
+
type=Path,
|
|
166
|
+
default=Path("install"),
|
|
167
|
+
help="The base path for all install prefixes (default: install)",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--merge-install",
|
|
172
|
+
action="store_true",
|
|
173
|
+
help="Merge all install prefixes into a single location",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"-v",
|
|
178
|
+
"--verbose",
|
|
179
|
+
action="store_true",
|
|
180
|
+
help="If provided, debug logs will be printed",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return parser.parse_args()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
main()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Augment packages that use UV for Python dependency management."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from colcon_core.package_augmentation import PackageAugmentationExtensionPoint
|
|
8
|
+
from colcon_core.package_augmentation.python import create_dependency_descriptor
|
|
9
|
+
from colcon_core.package_descriptor import PackageDescriptor
|
|
10
|
+
from colcon_core.plugin_system import satisfies_version
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("colcon.uv.package_augmentation")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UvPackageAugmentation(PackageAugmentationExtensionPoint):
|
|
16
|
+
"""Augment packages that use UV for Python dependency management."""
|
|
17
|
+
|
|
18
|
+
_TOOL_SECTION = "tool"
|
|
19
|
+
_COLCON_UV_ROS_SECTION = "colcon-uv-ros"
|
|
20
|
+
_DEPENDENCIES_SECTION = "dependencies"
|
|
21
|
+
_DATA_FILES_SECTION = "data-files"
|
|
22
|
+
_DEPEND_LIST = "depend"
|
|
23
|
+
_BUILD_DEPEND_LIST = "build_depend"
|
|
24
|
+
_EXEC_DEPEND_LIST = "exec_depend"
|
|
25
|
+
_TEST_DEPEND_LIST = "test_depend"
|
|
26
|
+
_PACKAGE_BUILD_CATEGORY = "build"
|
|
27
|
+
_PACKAGE_EXEC_CATEGORY = "run"
|
|
28
|
+
_PACKAGE_TEST_CATEGORY = "test"
|
|
29
|
+
|
|
30
|
+
def __init__(self): # noqa: D107
|
|
31
|
+
super().__init__()
|
|
32
|
+
satisfies_version(
|
|
33
|
+
PackageAugmentationExtensionPoint.EXTENSION_POINT_VERSION, "^1.0"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def augment_package(
|
|
37
|
+
self, desc: PackageDescriptor, *, additional_argument_names=None
|
|
38
|
+
): # noqa: D102
|
|
39
|
+
if desc.type != "uv":
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# Read pyproject.toml to get package metadata
|
|
43
|
+
pyproject_toml = desc.path / "pyproject.toml"
|
|
44
|
+
if not pyproject_toml.exists():
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import tomli
|
|
49
|
+
|
|
50
|
+
with open(pyproject_toml, "rb") as f:
|
|
51
|
+
data = tomli.load(f)
|
|
52
|
+
if (
|
|
53
|
+
self._TOOL_SECTION in data
|
|
54
|
+
and self._COLCON_UV_ROS_SECTION in data[self._TOOL_SECTION]
|
|
55
|
+
):
|
|
56
|
+
uv_config = data[self._TOOL_SECTION][self._COLCON_UV_ROS_SECTION]
|
|
57
|
+
|
|
58
|
+
# Set package name if not already set
|
|
59
|
+
if not desc.name and "name" in uv_config:
|
|
60
|
+
desc.name = uv_config["name"]
|
|
61
|
+
|
|
62
|
+
# Handle dependencies
|
|
63
|
+
if self._DEPENDENCIES_SECTION in uv_config:
|
|
64
|
+
deps = uv_config[self._DEPENDENCIES_SECTION]
|
|
65
|
+
|
|
66
|
+
# Build dependencies
|
|
67
|
+
if self._BUILD_DEPEND_LIST in deps:
|
|
68
|
+
build_deps = set(deps[self._BUILD_DEPEND_LIST])
|
|
69
|
+
else:
|
|
70
|
+
build_deps = set()
|
|
71
|
+
|
|
72
|
+
# Exec dependencies
|
|
73
|
+
if self._EXEC_DEPEND_LIST in deps:
|
|
74
|
+
exec_deps = set(deps[self._EXEC_DEPEND_LIST])
|
|
75
|
+
else:
|
|
76
|
+
exec_deps = set()
|
|
77
|
+
|
|
78
|
+
# Test dependencies
|
|
79
|
+
if self._TEST_DEPEND_LIST in deps:
|
|
80
|
+
test_deps = set(deps[self._TEST_DEPEND_LIST])
|
|
81
|
+
else:
|
|
82
|
+
test_deps = set()
|
|
83
|
+
|
|
84
|
+
# Handle general dependencies (add to both build and exec)
|
|
85
|
+
if self._DEPEND_LIST in deps:
|
|
86
|
+
depends = deps[self._DEPEND_LIST]
|
|
87
|
+
build_deps.update(depends)
|
|
88
|
+
exec_deps.update(depends)
|
|
89
|
+
|
|
90
|
+
# Set dependencies in descriptor
|
|
91
|
+
desc.dependencies[self._PACKAGE_BUILD_CATEGORY] = {
|
|
92
|
+
create_dependency_descriptor(dep) for dep in build_deps
|
|
93
|
+
}
|
|
94
|
+
desc.dependencies[self._PACKAGE_EXEC_CATEGORY] = {
|
|
95
|
+
create_dependency_descriptor(dep) for dep in exec_deps
|
|
96
|
+
}
|
|
97
|
+
desc.dependencies[self._PACKAGE_TEST_CATEGORY] = {
|
|
98
|
+
create_dependency_descriptor(dep) for dep in test_deps
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Handle data files
|
|
102
|
+
if self._DATA_FILES_SECTION in uv_config:
|
|
103
|
+
data_files = uv_config[self._DATA_FILES_SECTION]
|
|
104
|
+
for dest_dir, files in data_files.items():
|
|
105
|
+
# Create destination directory if it doesn't exist
|
|
106
|
+
dest_path = Path(dest_dir)
|
|
107
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
|
|
109
|
+
# Copy each file
|
|
110
|
+
for file in files:
|
|
111
|
+
src_path = desc.path / file
|
|
112
|
+
if src_path.is_file():
|
|
113
|
+
shutil.copy2(src_path, dest_path / src_path.name)
|
|
114
|
+
elif src_path.is_dir():
|
|
115
|
+
shutil.copytree(
|
|
116
|
+
src_path,
|
|
117
|
+
dest_path / src_path.name,
|
|
118
|
+
dirs_exist_ok=True,
|
|
119
|
+
)
|
|
120
|
+
except ImportError:
|
|
121
|
+
# If tomli is not available, we'll just use basic metadata
|
|
122
|
+
pass
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Identify packages that use UV for Python dependency management."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from colcon_core.package_identification import PackageIdentificationExtensionPoint
|
|
7
|
+
from colcon_core.plugin_system import satisfies_version
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("colcon.uv.package_identification")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UvPackageIdentification(PackageIdentificationExtensionPoint):
|
|
13
|
+
"""Identify packages that use UV for Python dependency management."""
|
|
14
|
+
|
|
15
|
+
# High priority to supersede other identifiers like ament_python
|
|
16
|
+
PRIORITY = 200
|
|
17
|
+
|
|
18
|
+
def __init__(self): # noqa: D107
|
|
19
|
+
super().__init__()
|
|
20
|
+
satisfies_version(
|
|
21
|
+
PackageIdentificationExtensionPoint.EXTENSION_POINT_VERSION, "^1.0"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def identify(self, desc): # noqa: D102
|
|
25
|
+
if desc.type is not None and desc.type != "uv.python":
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
pyproject_toml = desc.path / "pyproject.toml"
|
|
29
|
+
if not pyproject_toml.exists():
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
# Check if it's a UV package by looking for our custom section
|
|
33
|
+
try:
|
|
34
|
+
import tomli
|
|
35
|
+
|
|
36
|
+
with open(pyproject_toml, "rb") as f:
|
|
37
|
+
data = tomli.load(f)
|
|
38
|
+
if "tool" in data and "colcon-uv-ros" in data["tool"]:
|
|
39
|
+
desc.type = "uv.python"
|
|
40
|
+
# Get name from pyproject.toml if available, otherwise use directory name
|
|
41
|
+
if "project" in data and "name" in data["project"]:
|
|
42
|
+
desc.name = data["project"]["name"]
|
|
43
|
+
else:
|
|
44
|
+
desc.name = os.path.basename(desc.path)
|
|
45
|
+
return
|
|
46
|
+
except ImportError:
|
|
47
|
+
# If tomli is not available, we'll just check for the string
|
|
48
|
+
with open(pyproject_toml) as f:
|
|
49
|
+
content = f.read()
|
|
50
|
+
if "[tool.colcon-uv-ros]" in content:
|
|
51
|
+
desc.type = "uv.python"
|
|
52
|
+
desc.name = os.path.basename(desc.path)
|
|
53
|
+
return
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Build task for UV-based Python packages."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from colcon_core.logging import colcon_logger
|
|
7
|
+
from colcon_core.plugin_system import satisfies_version
|
|
8
|
+
from colcon_core.task import TaskExtensionPoint
|
|
9
|
+
|
|
10
|
+
from colcon_uv.dependencies.install import install_dependencies_from_descriptor
|
|
11
|
+
|
|
12
|
+
logger = colcon_logger.getChild("colcon.uv.task.build")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UvBuildTask(TaskExtensionPoint):
|
|
16
|
+
"""Build task for UV-based Python packages."""
|
|
17
|
+
|
|
18
|
+
def __init__(self): # noqa: D107
|
|
19
|
+
super().__init__()
|
|
20
|
+
satisfies_version(TaskExtensionPoint.EXTENSION_POINT_VERSION, "^1.0")
|
|
21
|
+
|
|
22
|
+
def add_arguments(self, *, parser): # noqa: D102
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--uv-args",
|
|
25
|
+
nargs="*",
|
|
26
|
+
metavar="*",
|
|
27
|
+
type=str.lstrip,
|
|
28
|
+
help="Pass arguments to UV. "
|
|
29
|
+
"Arguments matching other options must be prefixed by a space,\n"
|
|
30
|
+
'e.g. --uv-args " --help"',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def build(self, *, additional_hooks=None):
|
|
34
|
+
pkg = self.context.pkg
|
|
35
|
+
args = self.context.args
|
|
36
|
+
|
|
37
|
+
# UV sync handles EVERYTHING - deps + package installation
|
|
38
|
+
logger.info("Syncing dependencies and installing package...")
|
|
39
|
+
install_dependencies_from_descriptor(pkg, Path(args.install_base), False)
|
|
40
|
+
|
|
41
|
+
# Handle ROS-specific stuff (data files, environment hooks, etc.)
|
|
42
|
+
return_code = await self._add_data_files()
|
|
43
|
+
if return_code != 0:
|
|
44
|
+
return return_code
|
|
45
|
+
|
|
46
|
+
# Create executable symlinks for ROS 2
|
|
47
|
+
self._create_executable_symlinks()
|
|
48
|
+
|
|
49
|
+
# Add environment hooks for ROS
|
|
50
|
+
self._create_environment_hooks()
|
|
51
|
+
|
|
52
|
+
async def _add_data_files(self) -> int:
|
|
53
|
+
"""Install data files based on the [tool.colcon-uv-ros.data-files] table."""
|
|
54
|
+
pkg = self.context.pkg
|
|
55
|
+
args = self.context.args
|
|
56
|
+
|
|
57
|
+
pyproject_toml = pkg.path / "pyproject.toml"
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
import tomli
|
|
61
|
+
|
|
62
|
+
with open(pyproject_toml, "rb") as f:
|
|
63
|
+
data = tomli.load(f)
|
|
64
|
+
|
|
65
|
+
if "tool" not in data or "colcon-uv-ros" not in data["tool"]:
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
uv_config = data["tool"]["colcon-uv-ros"]
|
|
69
|
+
if "data-files" not in uv_config:
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
data_files = uv_config["data-files"]
|
|
73
|
+
except (ImportError, FileNotFoundError, KeyError):
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
if not isinstance(data_files, dict):
|
|
77
|
+
logger.error("data-files must be a table")
|
|
78
|
+
return 1
|
|
79
|
+
|
|
80
|
+
for destination, sources in data_files.items():
|
|
81
|
+
if not isinstance(sources, list):
|
|
82
|
+
logger.error(f"Field '{destination}' in data-files must be an array")
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
dest_path = Path(args.install_base) / destination
|
|
86
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
for source in sources:
|
|
89
|
+
source_path = pkg.path / Path(source)
|
|
90
|
+
if source_path.exists():
|
|
91
|
+
if source_path.is_dir():
|
|
92
|
+
shutil.copytree(
|
|
93
|
+
source_path,
|
|
94
|
+
dest_path / source_path.name,
|
|
95
|
+
dirs_exist_ok=True,
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
shutil.copy2(source_path, dest_path)
|
|
99
|
+
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
def _create_executable_symlinks(self):
|
|
103
|
+
"""Create symlinks for executables in the standard ROS 2 location."""
|
|
104
|
+
pkg = self.context.pkg
|
|
105
|
+
args = self.context.args
|
|
106
|
+
|
|
107
|
+
# Read pyproject.toml to get executable entry points
|
|
108
|
+
pyproject_toml = pkg.path / "pyproject.toml"
|
|
109
|
+
if not pyproject_toml.exists():
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
import tomli
|
|
114
|
+
|
|
115
|
+
with open(pyproject_toml, "rb") as f:
|
|
116
|
+
data = tomli.load(f)
|
|
117
|
+
|
|
118
|
+
if "project" not in data or "scripts" not in data["project"]:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
scripts = data["project"]["scripts"]
|
|
122
|
+
except (ImportError, FileNotFoundError, KeyError):
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Create lib/package_name directory for executables
|
|
126
|
+
lib_dir = Path(args.install_base) / "lib" / pkg.name
|
|
127
|
+
lib_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
# Create symlinks for each executable
|
|
130
|
+
venv_bin = Path(args.install_base) / "venv" / "bin"
|
|
131
|
+
|
|
132
|
+
for script_name, _entry_point in scripts.items():
|
|
133
|
+
venv_executable = venv_bin / script_name
|
|
134
|
+
ros_executable = lib_dir / script_name
|
|
135
|
+
|
|
136
|
+
if venv_executable.exists():
|
|
137
|
+
# Remove existing symlink if it exists
|
|
138
|
+
if ros_executable.exists() or ros_executable.is_symlink():
|
|
139
|
+
ros_executable.unlink()
|
|
140
|
+
|
|
141
|
+
# Create symlink
|
|
142
|
+
ros_executable.symlink_to(venv_executable)
|
|
143
|
+
logger.info(
|
|
144
|
+
f"Created executable symlink: {ros_executable} -> {venv_executable}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _create_environment_hooks(self):
|
|
148
|
+
"""Create ROS environment hooks."""
|
|
149
|
+
from colcon_core.environment import (
|
|
150
|
+
create_environment_hooks,
|
|
151
|
+
create_environment_scripts,
|
|
152
|
+
)
|
|
153
|
+
from colcon_core.shell import create_environment_hook
|
|
154
|
+
|
|
155
|
+
pkg = self.context.pkg
|
|
156
|
+
args = self.context.args
|
|
157
|
+
|
|
158
|
+
additional_hooks = create_environment_hook(
|
|
159
|
+
"ament_prefix_path",
|
|
160
|
+
Path(args.install_base) / pkg.name,
|
|
161
|
+
pkg.name,
|
|
162
|
+
"AMENT_PREFIX_PATH",
|
|
163
|
+
"",
|
|
164
|
+
mode="prepend",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
hooks = create_environment_hooks(Path(args.install_base) / pkg.name, pkg.name)
|
|
168
|
+
create_environment_scripts(
|
|
169
|
+
pkg, args, default_hooks=list(hooks), additional_hooks=additional_hooks
|
|
170
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Test task for UV-based Python packages."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from colcon_core.logging import colcon_logger
|
|
7
|
+
from colcon_core.plugin_system import satisfies_version
|
|
8
|
+
from colcon_core.task import TaskExtensionPoint
|
|
9
|
+
|
|
10
|
+
logger = colcon_logger.getChild("colcon.uv.task.test")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UvTestTask(TaskExtensionPoint):
|
|
14
|
+
"""Test task for UV-based Python packages."""
|
|
15
|
+
|
|
16
|
+
def __init__(self): # noqa: D107
|
|
17
|
+
super().__init__()
|
|
18
|
+
satisfies_version(TaskExtensionPoint.EXTENSION_POINT_VERSION, "^1.0")
|
|
19
|
+
|
|
20
|
+
def add_arguments(self, *, parser): # noqa: D102
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--uv-args",
|
|
23
|
+
nargs="*",
|
|
24
|
+
metavar="*",
|
|
25
|
+
type=str.lstrip,
|
|
26
|
+
help="Pass arguments to UV. "
|
|
27
|
+
"Arguments matching other options must be prefixed by a space,\n"
|
|
28
|
+
'e.g. --uv-args " --help"',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
async def test(self, *, additional_hooks=None): # noqa: D102
|
|
32
|
+
pkg = self.context.pkg
|
|
33
|
+
args = self.context.args
|
|
34
|
+
|
|
35
|
+
logger.info("Testing UV package in '{args.path}'".format_map(locals()))
|
|
36
|
+
|
|
37
|
+
# Get virtual environment path
|
|
38
|
+
venv_path = Path(args.build_base) / "venv"
|
|
39
|
+
if not venv_path.exists():
|
|
40
|
+
logger.error(
|
|
41
|
+
"Virtual environment not found. Please build the package first."
|
|
42
|
+
)
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# Run tests using pytest
|
|
46
|
+
logger.info("Running tests...")
|
|
47
|
+
subprocess.run([str(venv_path / "bin" / "pytest"), str(pkg.path)], check=True)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: colcon-uv
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Colcon extension providing support for Python projects that use UV
|
|
5
|
+
Project-URL: Homepage, https://github.com/nzlz/colcon-uv
|
|
6
|
+
Author-email: Nestor Gonzalez <nestor94gonzalez@gmail.com>
|
|
7
|
+
Maintainer-email: Nestor Gonzalez <nestor94gonzalez@gmail.com>
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
Keywords: colcon
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Plugins
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
16
|
+
Requires-Dist: colcon-core~=0.6
|
|
17
|
+
Requires-Dist: tomli>=1.2.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# colcon-uv
|
|
21
|
+
|
|
22
|
+
[](https://github.com/nzlz/colcon-uv/actions/workflows/ci.yml)
|
|
23
|
+
|
|
24
|
+
A **colcon extension** for building and testing Python packages that use **[uv](https://github.com/astral-sh/uv)** for dependency management.
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Fast Dependency Management**: Leverages UV's lightning-fast dependency resolution and installation
|
|
29
|
+
- **Modern Python Packaging**: Support for `pyproject.toml`-based packages following PEP 517/518 standards
|
|
30
|
+
- **ROS Integration**: Seamless integration with colcon build system and ROS package management
|
|
31
|
+
- **Dependency Isolation**: Prevents dependency conflicts between packages
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install colcon-uv
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
### Data Files
|
|
42
|
+
|
|
43
|
+
Similar to [colcon-poetry-ros](https://github.com/UrbanMachine/colcon-poetry-ros), you can specify data files using the `[tool.colcon-uv-ros.data-files]` section:
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[tool.colcon-uv-ros.data-files]
|
|
47
|
+
"share/ament_index/resource_index/packages" = ["resource/{package_name}"]
|
|
48
|
+
"share/{package_name}" = ["package.xml", "launch/", "config/"]
|
|
49
|
+
"lib/{package_name}" = ["scripts/"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Required entries** for all ROS packages:
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
[tool.colcon-uv-ros.data-files]
|
|
56
|
+
"share/ament_index/resource_index/packages" = ["resource/{package_name}"]
|
|
57
|
+
"share/{package_name}" = ["package.xml"]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Package Dependencies
|
|
61
|
+
|
|
62
|
+
Specify package dependencies for build ordering and to use system libraries (fetched from system paths, not installed in virtual environment):
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
[tool.colcon-uv-ros.dependencies]
|
|
66
|
+
depend = ["rclpy", "geometry_msgs"] # System packages (adds to both build_depend and exec_depend)
|
|
67
|
+
build_depend = ["bar_package"] # Build-time only dependency
|
|
68
|
+
exec_depend = ["std_msgs"] # Runtime system library
|
|
69
|
+
test_depend = ["qux_package"] # Test-time only dependency
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Important**: ROS system libraries like `rclpy`, `geometry_msgs`, `std_msgs`, etc. should be listed here so they are resolved from the system installation rather than being installed into the virtual environment.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
colcon_uv/__init__.py,sha256=8KK-O_XfVuu9Ubj2TcQft9hNogiJQ40U-Iip8vbu_pE,76
|
|
2
|
+
colcon_uv/cli.py,sha256=TX4_FP1FKDpR0pGkiiKc0-OQThQoYtqEs0EK-gpaJiE,3033
|
|
3
|
+
colcon_uv/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
colcon_uv/dependencies/install.py,sha256=v9oNIX8JhQtgus3coxu1oshWps7gupqTVRXSMwD_NMM,5637
|
|
5
|
+
colcon_uv/package_augmentation/__init__.py,sha256=_Z2QV8zXzG_8SfobclzxMrlLr0AvYuoMksEXwJEVl8Y,163
|
|
6
|
+
colcon_uv/package_augmentation/uv.py,sha256=WuE76pJ1OXqrKNh2__KE9NMQMlK6eQg6Cb-6YK6jQT0,5132
|
|
7
|
+
colcon_uv/package_identification/__init__.py,sha256=uLfNAqg-obGtQoLrwyUGtzTxT_dqLe9KsBfl-eTMjIc,171
|
|
8
|
+
colcon_uv/package_identification/uv.py,sha256=H1snv1YG349Jmxz0E8_Kh50UDSVOwngUirkd_9Y-iws,1978
|
|
9
|
+
colcon_uv/task/__init__.py,sha256=A09krWgApqaeh_YPpzURMSloItEz3SCMkd-pDmnozb4,178
|
|
10
|
+
colcon_uv/task/uv/__init__.py,sha256=1htbhWTxp42siw-xHbRrcRQCSuK6zIp6N_nOgYcf1_g,172
|
|
11
|
+
colcon_uv/task/uv/build.py,sha256=GalspkAe4FhuSkMf-cvj8-8_ySVVkbykeuSr3eeZ3_0,5756
|
|
12
|
+
colcon_uv/task/uv/test.py,sha256=1CAiHzJJIgFEOHJKesOV-xJGlsFA7V1iI247-zSENhM,1526
|
|
13
|
+
colcon_uv-0.0.1.dist-info/METADATA,sha256=PWuc7QI1Jlsdo0w5qDpurXKnsiQxGcn3nJRqsRTqL2g,2809
|
|
14
|
+
colcon_uv-0.0.1.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
|
|
15
|
+
colcon_uv-0.0.1.dist-info/entry_points.txt,sha256=c3r5DG7L2VfygiA4QDIr4qQXkb3nEIwC05hycOY6L84,390
|
|
16
|
+
colcon_uv-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[colcon_core.package_augmentation]
|
|
2
|
+
uv = colcon_uv.package_augmentation.uv:UvPackageAugmentation
|
|
3
|
+
|
|
4
|
+
[colcon_core.package_identification]
|
|
5
|
+
uv = colcon_uv.package_identification.uv:UvPackageIdentification
|
|
6
|
+
|
|
7
|
+
[colcon_core.task.build]
|
|
8
|
+
uv.python = colcon_uv.task.uv.build:UvBuildTask
|
|
9
|
+
|
|
10
|
+
[colcon_core.task.test]
|
|
11
|
+
uv.python = colcon_uv.task.uv.test:UvTestTask
|
|
12
|
+
|
|
13
|
+
[colcon_core.verb]
|
|
14
|
+
uv = colcon_uv.cli:UvVerb
|