Skip to content

Commit 366cfb0

Browse files
Refactor and enhance collider pkg subcommands for package management
- Introduced a new `pkg` subcommand to encapsulate package-related operations, including `install`, `search`, `push`, and `policy`. - Updated the README and documentation to reflect changes in command usage, emphasizing the new `collider pkg` prefix for commands. - Refactored existing `Install`, `Push`, `Search`, and `Policy` subcommands to reside under the `pkg` namespace, improving organization and clarity. - Enhanced error handling and validation across subcommands to ensure robust package management. - Added tests for the new `pkg` subcommand structure, ensuring comprehensive coverage for package operations.
1 parent ba3b3b6 commit 366cfb0

14 files changed

Lines changed: 185 additions & 30 deletions

File tree

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ collider setup -- --buildtype=debug
5959
3) Push the project to a repository:
6060

6161
```bash
62-
collider push local
62+
collider pkg push local
6363
```
6464

6565
The package name and version come from Meson introspection in the build directory
6666
(default: `collider-build`). Collider generates the source archive and wrap file
6767
and stores them in the repository. Filesystem repositories must have `publish_url`
6868
configured so Collider can generate archive URLs. To attach a patch archive for
69-
an external source, pass `--patch-archive`. Create the patch with `collider patch` from a modified source tree, then run `collider push` from the **unmodified** tree and pass the patch archive (tar.xz) with `--patch-archive` so the repository stores base source plus patch.
69+
an external source, pass `--patch-archive`. Create the patch with `collider patch` from a modified source tree, then run `collider pkg push` from the **unmodified** tree and pass the patch archive (tar.xz) with `--patch-archive` so the repository stores base source plus patch.
7070

7171
Collider also generates the wrap `[provide]` entry as `<name> = <name>_dep`
7272
(with `-` and `.` replaced by `_`). Ensure your Meson project exposes that
@@ -75,8 +75,8 @@ dependency variable for `dependency()` fallbacks.
7575
4) Install a dependency (online or offline):
7676

7777
```bash
78-
collider install my-lib
79-
collider install --offline my-lib
78+
collider pkg install my-lib
79+
collider pkg install --offline my-lib
8080
```
8181

8282
## Configuration
@@ -105,7 +105,7 @@ Filesystem repositories are directories containing a wrap layout and `releases.j
105105
Wraps are stored under `<name>_<version>/<name>.wrap` and `releases.json` is generated at the root.
106106
Collider stores the generated source archive (and optional patch archive) under
107107
`archives/<name>_<version>/`.
108-
Filesystem repositories require `publish_url`. Set it to the HTTPS, HTTP, or `file://` base where the repository is served; `collider push`
108+
Filesystem repositories require `publish_url`. Set it to the HTTPS, HTTP, or `file://` base where the repository is served; `collider pkg push`
109109
uses it to rewrite archive URLs without requiring per-command flags.
110110

111111
When served at `publish_url`, the expected URLs are:
@@ -124,9 +124,9 @@ in `config.json` that points to the server base URL.
124124
## Repository workflow
125125

126126
```bash
127-
collider push local
128-
collider search '^my-lib$' --repository local --version '>=1.0.0'
129-
collider install my-lib
127+
collider pkg push local
128+
collider pkg search '^my-lib$' --repository local --version '>=1.0.0'
129+
collider pkg install my-lib
130130
```
131131

132132
## Offline caching
@@ -159,7 +159,7 @@ Use `-v` or `--verbose` for debug logging.
159159
- `collider patch [--builddir PATH] [--base REV] [--output PATH] [--list] [--include-uncommitted / --no-include-uncommitted]`
160160
Create a patch archive (tar.xz) from Git changes for use with Meson wrap `patch_url`. Reads project name and version from Meson introspection. Default output: `dist/<name>_<version>_patch.tar.xz`. Requires Git and an existing Meson build directory.
161161

162-
- `collider push <repo> [--builddir PATH] [--patch-archive PATH] [--push-token-env VAR]`
162+
- `collider pkg push <repo> [--builddir PATH] [--patch-archive PATH] [--push-token-env VAR]`
163163
Generate a wrap and source archive, then publish it.
164164
For filesystem repositories, Collider writes directly to disk.
165165
For wrap repositories, Collider calls `POST /v2/_collider/v1/push` and reads bearer token from
@@ -171,16 +171,16 @@ Use `-v` or `--verbose` for debug logging.
171171
For non-filesystem repositories, `--publish-url` is ignored.
172172
If another repository already uses the same URL, Collider logs a warning but still adds the entry.
173173

174-
- `collider search <pattern> [--repository NAME ...] [--version SPEC]`
174+
- `collider pkg search <pattern> [--repository NAME ...] [--version SPEC]`
175175
Search repositories.
176176

177-
- `collider policy <name> [--repository NAME ...]`
177+
- `collider pkg policy <name> [--repository NAME ...]`
178178
Show versions, origins, and cache status.
179179

180180
- `collider status`
181181
Show collider-managed dependencies and local wrap status.
182182

183-
- `collider install <name> [--offline]`
183+
- `collider pkg install <name> [--offline]`
184184
Install a wrap package into `subprojects/`.
185185

186186
- `collider serve <path> [--host HOST] [--port PORT] [--push-token TOKEN] [--push-token-env VAR] [--publish-url URL]`
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 MOG Robotics OÜ
3+
4+
"""Package operations; subcommands are discovered from this package."""
5+
6+
from __future__ import annotations
7+
8+
import argparse
9+
import os
10+
import sys
11+
12+
from collider.Context import Context
13+
from collider.log import logger
14+
from collider.subcommand.SubcommandInterface import SubcommandInterface
15+
from collider.utils import core
16+
from collider.utils.compat import override
17+
18+
19+
Interface = SubcommandInterface
20+
21+
# Mutable holder so we can cache without a global statement (Pylint W0603).
22+
_PKG_SUBCOMMANDS_HOLDER: list[dict[str, type[SubcommandInterface]] | None] = [None]
23+
24+
25+
def _get_pkg_subcommands() -> dict[str, type[SubcommandInterface]]:
26+
"""
27+
Discover nested pkg subcommands once; reused in register() and execute().
28+
:return: Map of subcommand name to implementation class.
29+
"""
30+
if _PKG_SUBCOMMANDS_HOLDER[0] is None:
31+
pkg_module = sys.modules[__name__]
32+
_PKG_SUBCOMMANDS_HOLDER[0] = core.discover_plugins(pkg_module)
33+
return _PKG_SUBCOMMANDS_HOLDER[0]
34+
35+
36+
def _pkg_help_summary() -> str:
37+
"""Build the help line from discovered subcommand names."""
38+
names = sorted(_get_pkg_subcommands().keys())
39+
return 'Package operations: ' + ', '.join(names) + '.'
40+
41+
42+
class Pkg(SubcommandInterface):
43+
"""Package operations; subcommands are discovered from this package."""
44+
45+
@staticmethod
46+
def help() -> str:
47+
"""Short help string surfaced by the CLI."""
48+
return _pkg_help_summary()
49+
50+
@staticmethod
51+
def epilog() -> str | None:
52+
"""Optional examples appended to the help output."""
53+
lines = ['Examples:']
54+
for name in sorted(_get_pkg_subcommands().keys()):
55+
lines.append(f' ‣ collider pkg {name} <args>')
56+
return '\n'.join(lines) + '\n'
57+
58+
@staticmethod
59+
def register(parser: argparse.ArgumentParser) -> None:
60+
"""Keep argparse wiring co-located with the command."""
61+
subparsers = parser.add_subparsers(dest='pkg_subcommand', required=True)
62+
for name, subcommand_class in _get_pkg_subcommands().items():
63+
subparser = subparsers.add_parser(
64+
name,
65+
help=subcommand_class.help(),
66+
description=subcommand_class.help(),
67+
epilog=subcommand_class.epilog(),
68+
)
69+
subcommand_class.register(subparser)
70+
subparser.set_defaults(pkg_action=name)
71+
72+
@override
73+
def execute(self) -> int:
74+
"""Run the pkg command.
75+
:return: Exit code.
76+
"""
77+
action = getattr(self.args, 'pkg_action', None)
78+
subcommands = _get_pkg_subcommands()
79+
if action is None or action not in subcommands:
80+
logger.critical(f'Unknown pkg subcommand: {action}')
81+
return os.EX_USAGE
82+
subcommand_class = subcommands[action]
83+
return subcommand_class(self.args, self.context).execute()
84+
85+
86+
__all__ = ['Interface', 'Pkg']

doc/DESIGN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Patch archives may use any extension; `collider patch` produces a `.tar.xz` by d
4646

4747
### Wrap Repository (Remote)
4848
- WrapDB-compatible endpoint backed by `releases.json` and wraps.
49-
- Used by `collider install` and `collider search`.
49+
- Used by `collider pkg install` and `collider pkg search`.
5050
- HTTPS preferred; HTTP is allowed with a warning.
5151

5252
**Why**
@@ -81,7 +81,7 @@ Patch archives may use any extension; `collider patch` produces a `.tar.xz` by d
8181
- Generates a wrap file that points at the repository `publish_url`.
8282
- Optional: stage a patch archive with `--patch-archive`.
8383
- Filesystem repos use `publish_url` from `config.json` to build archive URLs.
84-
- Wrap repos can be pushed through `POST /v2/_collider/v1/push`; `collider push` reads bearer token from env (`COLLIDER_PUSH_TOKEN` by default).
84+
- Wrap repos can be pushed through `POST /v2/_collider/v1/push`; `collider pkg push` reads bearer token from env (`COLLIDER_PUSH_TOKEN` by default).
8585
- Auto-generates `[provide]` using `<name>` and a sanitized `<name>_dep` variable.
8686

8787
**Why**
@@ -94,7 +94,7 @@ Patch archives may use any extension; `collider patch` produces a `.tar.xz` by d
9494
- Uses Meson introspection (build directory, default `collider-build`) for package name and version.
9595
- Output default: `dist/<name>_<version>_patch.tar.xz`. Requires Git and an existing Meson build.
9696
- Options: `--base` (revision to diff against), `--include-uncommitted` / `--no-include-uncommitted`, `--output`, `--list` (dry-run list of files).
97-
- **Typical workflow for patched upstreams**: (1) Modify the source tree as needed, (2) Run `collider patch` to produce the patch archive (tar.xz), (3) Revert the tree to the unmodified upstream (or use a clean copy), (4) Run `collider push <repo> --patch-archive <path-to-patch.tar.xz>` so the repository stores the base source archive plus the patch. Consumers then get base + patch on install.
97+
- **Typical workflow for patched upstreams**: (1) Modify the source tree as needed, (2) Run `collider patch` to produce the patch archive (tar.xz), (3) Revert the tree to the unmodified upstream (or use a clean copy), (4) Run `collider pkg push <repo> --patch-archive <path-to-patch.tar.xz>` so the repository stores the base source archive plus the patch. Consumers then get base + patch on install.
9898

9999
### `repo add`
100100
- Adds a repository entry to `config.json`.

doc/WRAPAPI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ When push auth is configured, Collider also exposes:
4242
- Requires `Authorization: Bearer <token>`.
4343
- This is a minimal in-process auth mode.
4444

45-
`collider push <wrap-repo-name>` can use this endpoint automatically and reads the token from
45+
`collider pkg push <wrap-repo-name>` can use this endpoint automatically and reads the token from
4646
`$COLLIDER_PUSH_TOKEN` by default (`--push-token-env` can override variable name).
4747

4848
`<base>` is the server root used by `collider serve`.

test/common/common.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010
class Subcommand(str, Enum):
1111
INIT = 'init'
1212
SETUP = 'setup'
13-
PUSH = 'push'
14-
SEARCH = 'search'
15-
POLICY = 'policy'
16-
INSTALL = 'install'
13+
PKG = 'pkg'
1714
STATUS = 'status'
1815
REPO = 'repo'
1916

test/subcommand/test_install.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from collider.Package import WrapPackage
2020
from collider.repository.entries import RepoPackageEntry
2121
from collider.repository.implementation.RepositoryInterface import RepositoryInterface
22-
from collider.subcommand.Install import Install
22+
from collider.subcommand.pkg.Install import Install
2323
from collider.utils.packaging.Dependency import Dependency, DependencySource
2424
from collider.utils.packaging.PackageType import PackageType
2525
from collider.utils.packaging.repo_key import make_repo_key
@@ -86,7 +86,7 @@ def test_install_wrap_success(tmp_path: Path, monkeypatch) -> None:
8686
cwd = os.getcwd()
8787
try:
8888
os.chdir(tmp_path)
89-
with patch('collider.subcommand.Install.search_packages', return_value=all_matches):
89+
with patch('collider.subcommand.pkg.Install.search_packages', return_value=all_matches):
9090
assert cmd.execute() == os.EX_OK
9191
finally:
9292
os.chdir(cwd)
@@ -125,7 +125,7 @@ def test_install_offline_uses_cache(tmp_path: Path, caplog: pytest.LogCaptureFix
125125
cwd = os.getcwd()
126126
try:
127127
os.chdir(tmp_path)
128-
with patch('collider.subcommand.Install.search_packages', return_value=all_matches):
128+
with patch('collider.subcommand.pkg.Install.search_packages', return_value=all_matches):
129129
assert cmd.execute() == os.EX_OK
130130
finally:
131131
os.chdir(cwd)
@@ -155,7 +155,7 @@ def test_install_offline_remote_missing_cache(tmp_path: Path) -> None:
155155
cwd = os.getcwd()
156156
try:
157157
os.chdir(tmp_path)
158-
with patch('collider.subcommand.Install.search_packages', return_value=all_matches):
158+
with patch('collider.subcommand.pkg.Install.search_packages', return_value=all_matches):
159159
assert cmd.execute() == os.EX_IOERR
160160
finally:
161161
os.chdir(cwd)
@@ -182,7 +182,7 @@ def test_install_offline_local_missing_archive(tmp_path: Path) -> None:
182182
cwd = os.getcwd()
183183
try:
184184
os.chdir(tmp_path)
185-
with patch('collider.subcommand.Install.search_packages', return_value=all_matches):
185+
with patch('collider.subcommand.pkg.Install.search_packages', return_value=all_matches):
186186
assert cmd.execute() == os.EX_IOERR
187187
finally:
188188
os.chdir(cwd)
@@ -214,7 +214,7 @@ def test_install_updates_dependency_version(tmp_path: Path, monkeypatch) -> None
214214
cwd = os.getcwd()
215215
try:
216216
os.chdir(tmp_path)
217-
with patch('collider.subcommand.Install.search_packages', return_value=all_matches):
217+
with patch('collider.subcommand.pkg.Install.search_packages', return_value=all_matches):
218218
assert cmd.execute() == os.EX_OK
219219
finally:
220220
os.chdir(cwd)
@@ -253,7 +253,7 @@ def test_install_fails_on_existing_different_wrap(tmp_path: Path, monkeypatch) -
253253
cwd = os.getcwd()
254254
try:
255255
os.chdir(tmp_path)
256-
with patch('collider.subcommand.Install.search_packages', return_value=all_matches):
256+
with patch('collider.subcommand.pkg.Install.search_packages', return_value=all_matches):
257257
assert cmd.execute() == os.EX_IOERR
258258
finally:
259259
os.chdir(cwd)

0 commit comments

Comments
 (0)