Collider Design¶
Scope and Goals¶
- Provide a package manager for Meson projects, centered on Meson's wrap system.
- Keep the workflow minimal: publish, search, add, lock, and setup.
- Support offline installs by caching wraps and archives locally.
- Allow hosting of internal repositories with a single filesystem layout that is WrapDB-compatible.
- Verify integrity of archives and wraps at every stage (publish, cache, install, lock).
Non-Goals¶
- Supporting build systems other than Meson.
- Providing a general-purpose package manager for arbitrary archive types.
- Managing binary artifacts. Collider manages only sources and patches as defined by wraps.
Core Concepts¶
- Wrap package. A Meson
.wrapfile plus a source archive and optional patch archive. - Repository. A local filesystem repository, a read-only WrapDB-compatible remote, or a Collider remote that extends WrapDB with write operations.
- Cache. A local store for wraps and archives that enables offline builds.
Repository Types¶
Filesystem Repository¶
Stores wraps and optional archives on disk. Generates releases.json for WrapDB compatibility when served over HTTP(S).
Layout
Patch archives may use any extension; collider patch produces .tar.xz by default.
Rationale. A single layout is straightforward to serve internally and maps directly to WrapDB clients.
URL Mapping
| Resource | URL |
|---|---|
| releases.json | <publish_url>/v2/releases.json |
| wrap file | <publish_url>/v2/<name>_<version>/<name>.wrap |
| archive file | <publish_url>/v2/archives/<name>_<version>/<filename> |
Wrap Repository (Remote)¶
- WrapDB-compatible endpoint backed by
releases.jsonand wraps. - Read-only: no publish or unpublish support.
- HTTPS preferred; HTTP is allowed with a warning.
Rationale. Aligns with Meson expectations and avoids maintaining a custom protocol.
Collider Repository (Remote with Write Extension)¶
Extends the WrapDB read API with two write endpoints under a _collider namespace. Supports all read operations (search, add, info) like a wrap repository, with additional publish and unpublish operations.
Write Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | <base>/v2/_collider/v1/push |
Publish a package. |
| DELETE | <base>/v2/_collider/v1/packages/<name>/<version> |
Unpublish a package. |
- Both endpoints require
Authorization: Bearer <token>. - The push payload is a single JSON object containing the wrap text, source archive (base64-encoded), and optional patch archive.
- Unpublish returns
204on success,404if the package is not found.
Authentication
- Built-in auth is intentionally minimal: a single static bearer token read from
$COLLIDER_PUSH_TOKEN(or a configurable env var via--push-token-env). - Production deployments should place a reverse proxy with proper authentication in front of the server.
- Collider keeps a pluggable auth seam for future in-process upgrades.
Rationale
- The WrapDB protocol is read-only. Without a write extension, publishing to a remote requires out-of-band file transfers.
- Namespacing under
_collider/v1avoids conflicts with WrapDBv2routes and allows independent versioning. - A single atomic push endpoint is simpler than multi-step upload flows and prevents partial-publish states.
- Static bearer token keeps the implementation minimal while preventing unauthorized writes.
Wrap File Requirements¶
source_url,source_filename,source_hashare required.- Patch fields are optional, but if any patch field is present, all must be present.
- HTTP URLs are allowed with a warning. HTTPS or local file paths are preferred.
Rationale
- A complete source tuple is required for reproducible builds.
- Mixed patch metadata creates ambiguous cache behavior.
- Warning on HTTP helps avoid accidental downgrade to insecure transports.
CLI Workflows¶
init¶
Creates collider.json if missing. Refuses to run outside a Meson project.
Rationale. Establishes a single source of truth before dependencies are added.
setup¶
Runs Meson setup and validates collider.json against Meson introspection.
lock¶
Resolves dependencies from collider.json (including transitive dependencies) and writes collider.lock. Does not modify subprojects/; only refreshes pinned resolution state.
publish¶
- Reads Meson introspection (
intro-projectinfo.jsonandmeson-info.json) to obtain project name, version, and source directory. - Requires a Meson build directory (default:
collider-build). - Generates a source archive with root directory
<name>-<version>. - Generates a wrap file pointing at the repository
publish_url. - Optional: stage a patch archive with
--patch-archive. - Filesystem repos use
publish_urlfromconfig.jsonto build archive URLs. - Collider repos are published via
POST /v2/_collider/v1/push; the bearer token is read from env (COLLIDER_PUSH_TOKENby default). - Auto-generates
[provide]using<name>and a sanitized<name>_depvariable.
Rationale
- Staging archives gives a single place to publish and consume dependencies.
- Rewriting URLs makes wraps self-contained against the repository base.
- Auto-generating the wrap removes manual wrap editing for internal projects.
patch¶
- Creates a patch archive (tar.xz) from Git changes in the project source directory.
- Uses Meson introspection (build directory, default
collider-build) for package name and version. - Output default:
dist/<name>_<version>_patch.tar.xz. Requires Git and an existing Meson build. - By default, includes the committed diff from
--baseplus staged, unstaged, and untracked files. Use--no-include-uncommittedto archive only the committed diff. - Options:
--base(revision to diff against),--include-uncommitted/--no-include-uncommitted,--output,--list(dry-run list of files). - Typical workflow for patched upstreams: (1) modify the source tree, (2) run
collider patchto produce the patch archive, (3) revert the tree to unmodified upstream, (4) runcollider publish <repo> --patch-archive <path>so the repository stores the base source archive plus the patch. Consumers receive base + patch on install.
repo add¶
- Adds a repository entry to
config.json. - Supports
filesystem,wrap, andcolliderrepository types. - Requires
--publish-urlfor filesystem repositories. - Warns (non-blocking) when another entry already points to the same repository URL.
repo remove¶
Removes a repository entry from config.json by name. Fails with EX_NOINPUT when the repository is not found.
serve¶
- Serves a filesystem repository over HTTP.
- Takes a filesystem path directly (not a config repository name).
- Creates repository layout (
releases.json,archives/) when the path does not exist. - Runs endpoint smoke checks when a repository path is created.
- Uses
--publish-urlwhen provided; otherwise defaults to a localfile://publish URL. - Intended for lightweight local testing and internal sharing.
- Exposes only Wrap API routes under
/v2/; other paths return 404. - Optional write extension at
POST /v2/_collider/v1/pushwhen push auth is configured. - Push auth is intentionally minimal (static bearer token). Strong auth should be handled externally.
search¶
Searches configured repositories by name and optional version constraint.
pkg info¶
- Shows available versions for a package across repositories.
- Marks versions that are fully cached for offline installs.
- Reports installed (subprojects wrap), declared (
collider.json), and candidate versions.
status¶
- Lists collider-managed dependencies from
collider.json. - Shows system dependencies declared in
collider.jsonseparately. - Reports whether the corresponding wrap file exists in
subprojects/. - Shows transitive dependencies from
collider.lock, or re-resolves them fromcollider.jsonwhen no lockfile is present and repository metadata is available. - Highlights wrap files present in
subprojects/but not tracked by Collider. - When
collider.lockexists, reports lock drift per dependency:ok,modified, ormissing.
pkg add¶
- Resolves the newest version across repositories satisfying the requested constraint, if any.
- Uses
--version <spec>when provided; otherwise reuses the constraint already declared incollider.json, if present. - Writes the
.wrapintosubprojects/. - Populates
subprojects/packagecache/to support offline builds. - Resolves and installs transitive dependencies (see Transitive Dependency Resolution).
- Adds the direct dependency to
collider.jsonif not already present. - Persists the requested version constraint in
collider.jsonwhen--versionis used. - Leaves
collider.lockunchanged; lockfile writes are explicit viacollider lock.
pkg remove¶
- Removes a collider-managed dependency from
collider.json. - Deletes the package's installed wrap from
subprojects/. - Leaves
collider.lockunchanged; warns if the package is still locked.
pkg upgrade¶
- Upgrades one or all collider-managed dependencies using the constraints declared in
collider.json. --version <spec>is only valid when a package name is provided, and updates that package's stored constraint before resolving.- Reinstalls only when the fetched wrap differs from the currently installed wrap.
- Leaves
collider.lockunchanged; warns if the installed version no longer matches the lock entry.
install¶
- If
collider.lockexists, restores all packages from it.- For each locked package, finds the configured repository whose URL matches the recorded
origin(URL normalization applied). Fails withEX_CONFIGif no configured repository matches. - Fetches the package from the origin repository. Fails with
EX_UNAVAILABLEif the origin repository does not provide the package. No fallback to other repositories. - Verifies the fetched wrap hash against the locked
wrap_hash. - Skips packages whose installed wrap already matches the locked hash.
- With
--offline, if the origin repository requires network access, falls back to the local cache and emits a provenance warning. Thewrap_hashcheck still protects content integrity.
- For each locked package, finds the configured repository whose URL matches the recorded
- If no lockfile exists, falls back to resolving from
collider.json(including transitive dependencies) without writingcollider.lock. - When both files exist, warns on incompatibilities:
- Locked package not declared in
collider.json. - Declared dependency with no lock entry.
- Locked version that does not satisfy a version specifier in
collider.json.
- Locked package not declared in
- Wrap hash verification is unconditional: a mismatch between the fetched package and the lockfile always fails with
EX_DATAERR, regardless of--frozen. --frozen: refuses to modifycollider.lock; fails if lock is missing or stale (lockfile drift). Intended for CI.--offline: disables network access and relies on local cache.
Cache and Offline Mode¶
- Cache root:
~/.config/collider/cache/. wraps/caches wrap files by name+version.archives/caches archives by hash.- Offline mode forbids network access but allows local
file://archives.
Rationale
- Meson resolves offline wraps via
subprojects/packagecache/. - Hash-keyed archives deduplicate identical content across wraps.
Data Models¶
collider.json¶
- Project description and dependencies (intent).
- Only explicitly listed dependencies are managed by Collider.
- Version field is optional; versionless dependencies and version ranges are both supported.
- Per-dependency
includeandexcludelists control transitive dependency overrides.
collider.lock¶
- Pinned resolution state for reproducible installs.
dependencies: Direct dependencies (fromcollider.json). Each entry hasversion(str),wrap_hash(str), andorigin(str, normalized repository URL).packages: Transitive dependencies only. Same entry shape. Install fetches from the configured repository matching the lockedoriginand verifieswrap_hash.wrap_hashis a SHA-256 of the.wrapfile text, which transitively pins archive hashes, filenames, and URLs.- System dependencies are not locked (no wrap artifact to pin).
collider.jsonrepresents intent (requested dependencies and optional constraints);collider.lockrepresents resolution (exact versions and integrity hashes).- Install precedence:
collider lockwritescollider.lockfromcollider.json.collider installusescollider.lockwhen present.- If no lockfile exists,
collider installresolves fromcollider.jsonwithout writing a new lockfile. - If both exist, Collider uses the lockfile and warns on mismatches with
collider.json. - Both
collider installandcollider lockenforce declared version constraints.
Rationale
- The
.wrapfile already containssource_hash,patch_hash, filenames, and URLs. A single hash of the wrap text avoids redundancy. - The hash is a verification gate, not a lookup key. Packages are fetched by name+version. The hash catches republished or tampered content.
- Existing validation in
WrapPackage.from_wrap_text()already rejects wraps with missing hashes, so no additional validation is needed in the lockfile path.
config.json¶
- List of repositories (
filesystem,wrap). - Filesystem repositories require
publish_urlfor hosted archive rewrites.
releases.json¶
- WrapDB-compatible list of packages and versions.
- Includes
dependency_nameswhen wraps define[provide]. - Generated from the repository layout on every add/remove.
Transitive Dependency Resolution¶
Problem¶
pkg add installs only the directly requested package. However, a package's meson.build may call dependency() on other packages that are not yet available in subprojects/. Without those transitive dependencies, meson setup fails at configure time.
Discovery¶
Collider uses meson introspect --scan-dependencies <meson.build> to discover dependencies declared in a build file. This command uses Meson's own AST parser and returns all dependency() calls as structured JSON, without requiring a build directory or a successful configure step.
[
{"name": "libfoo", "required": true, "version": [], "has_fallback": false, "conditional": false},
{"name": "libbar", "required": false, "version": [], "has_fallback": false, "conditional": false}
]
The same code path is used for all repository types, with no protocol changes or additional API endpoints.
Source and Patch Extraction¶
Many WrapDB packages ship upstream source tarballs that do not contain a meson.build. The Meson build files are provided separately in a patch archive (often a zip file). During scanning, Collider extracts both the source and patch archives and overlays the patch on top of the source tree before running introspection. The overlay correctly handles the common case where both archives share the same top-level directory prefix.
Dependency Classification¶
--scan-dependencies returns four flags per dependency:
| Flag | Meaning |
|---|---|
required: true |
Build fails if not found. |
required: false |
Build continues without it. |
has_fallback: true |
A named subproject fallback is declared. |
conditional: true |
Inside an if block; may not execute depending on build options. |
Default resolution behavior:
| Condition | Action | Rationale |
|---|---|---|
required: true, conditional: false |
Include | Hard dependency; always needed. |
required: false |
Include (info) | Optional but available. The user is informed. |
required: false, has_fallback: true |
Include | The project expects a subproject fallback. |
conditional: true |
Exclude (summarized) | May not be needed; depends on build options. All excluded conditional dependencies are listed in a single message. |
| Well-known system dependency | Skip silently | Compiler/OS intrinsics (threads, appleframeworks, openmp, blocks, cuda, mpi, coarray, dl, iconv, intl, atomic) cannot be provided by wraps. |
| Not in any configured repository | Skip (info) | Assumed system dependency. Each unknown name is reported once across the entire resolution run. |
| Empty name | Discard | dependency('', required: false) is a valid Meson idiom for null placeholder dependencies. |
required: get_option(...) is reported as required: true because the option value is unknown at scan time. This is the conservative (safe) interpretation.
User Controls¶
CLI flags
collider pkg add <name> --include-conditional # include conditional deps
collider pkg add <name> --exclude-optional # exclude optional deps
collider pkg add <name> --include <dep> # force-include a specific dep
collider pkg add <name> --exclude <dep> # force-exclude a specific dep
Precedence: --include/--exclude by name > --include-conditional/--exclude-optional > defaults.
Persistent overrides in collider.json
--include, --exclude, --include-conditional, and --exclude-optional are persisted in collider.json so that collider lock and collider install produce consistent results without repeating CLI flags:
{
"dependencies": [
{
"name": "<package>",
"source": "collider",
"version": ">=1.0",
"exclude": ["<dep-a>"],
"include": ["<dep-b>"],
"include_conditional": true,
"exclude_optional": false
}
]
}
System vs. Wrap Classification¶
No publisher-side classification is required. At resolution time, Collider infers the dependency type:
- If
dependency_namesacross configured repositories contain the name, it is a wrap dependency and is installed. - If no repository provides it, it is treated as a system dependency and skipped.
A set of Meson dependency names known to be intrinsic to the compiler or OS is silently discarded before lookup: threads, appleframeworks, openmp, blocks, cuda, mpi, coarray, dl, iconv, intl, atomic. Dependencies with an empty name are also discarded during scanning.
This consumer-side classification is more flexible than publisher-side classification because different consumers may have different repositories configured.
Resolution Engine¶
Collider uses resolvelib for dependency resolution. resolvelib handles graph walking, backtracking on conflicts, cycle detection, and producing a consistent resolution. Collider implements the Provider interface to connect resolvelib to the Meson/wrap ecosystem.
ColliderProvider implements:
| Method | Behavior |
|---|---|
identify() |
Returns the package name. |
get_preference() |
Prefers packages with fewer candidates (fail fast on conflicts). |
find_matches() |
Searches repositories for versions matching constraints; returns newest-first. |
is_satisfied_by() |
Checks whether a candidate satisfies a version constraint. |
get_dependencies() |
Downloads source and patch archives, extracts and overlays them, runs --scan-dependencies, applies filters, maps dependency names to packages via dependency_names, and returns requirements for providing packages. |
Dependency Name Mapping¶
Meson dependency names (e.g., absl_algorithm_container) do not necessarily match Collider package names (e.g., abseil-cpp). The mapping is derived from dependency_names in releases.json, which reflects the [provide] section of each wrap. A reverse index (dependency name to package name) is built once per resolution run from all configured repositories.
Resolution Flow¶
- Resolve the best version of the requested package across repositories.
- Download the source archive and patch archive (if present).
- Extract both to a temporary directory, overlaying the patch on top of the source.
- Run
meson introspect --scan-dependencieson the extractedmeson.build. - Discard empty names and well-known system dependencies.
- Apply filtering rules (default behavior + user overrides).
- For each included dependency, map to a providing package via the reverse index and recursively resolve.
- Install all resolved wraps into
subprojects/. - Add only the direct (user-requested) dependency to
collider.json. - Persist any
--include/--excludeoverrides intocollider.json.
Prerequisite: dependency_names¶
Transitive resolution requires at least one configured repository whose releases.json includes dependency_names entries. This field maps Meson dependency names to packages and is used to build the reverse index. When no repository provides dependency_names, transitive resolution is skipped and only the direct package is installed.
Multi-Root Resolution¶
collider lock and collider install resolve all declared dependencies in a single pass rather than resolving each dependency in isolation. This is implemented by resolve_all_dependencies, which feeds all root packages into one resolvelib session.
Single-pass resolution detects cross-root conflicts: if two declared dependencies require incompatible versions of the same transitive dependency, the resolver reports the conflict instead of silently installing one version and breaking the other.
Per-Root Include/Exclude¶
include and exclude overrides declared in collider.json are scoped to the root dependency they are declared on. When resolving multiple roots, each root's overrides apply only when scanning that root's meson.build - they do not affect scans of other roots or their transitive dependencies.
Internally, ColliderProvider stores per-root overrides in a root_overrides map. In get_dependencies(), overrides are looked up by candidate name. Candidates that are not roots use the provider-level defaults (no include/exclude).
Global Conditional and Optional Filters¶
include_conditional and exclude_optional are persisted per dependency in collider.json, but multi-root commands apply them as run-wide switches. If any declared dependency enables one of these flags, the combined collider lock / collider install / status resolution run uses that behavior for all roots.
Rollback¶
If transitive resolution fails after the direct package has been installed, pkg add rolls back the direct package: the .wrap file is removed from subprojects/ and any cached archives (source and patch) are deleted. This prevents a partially installed state where the direct package is present but unusable due to missing transitive dependencies.
Reinstall Guard¶
pkg add checks whether a .wrap file already exists for the requested package before performing any repository or network operations. If the wrap is present, the command exits immediately with an error and suggests --force. When --force is passed, existing artifacts (wrap file and subproject directory) are removed before proceeding with a fresh install.
Transitive Cleanup on Remove¶
pkg rm removes the direct package and then determines which remaining wraps are orphaned. Only wraps listed in collider.lock are considered for cleanup; manually placed wraps are never removed. When a lockfile exists, it re-resolves the remaining direct dependencies from collider.json to build the set of still-needed packages. Wraps in subprojects/ that are not in that set and are in the lockfile are removed automatically. If a transitive dep is shared with another direct dependency, it is kept. When no lockfile exists or resolution is not possible (no repository with dependency_names metadata), a warning is logged and manual cleanup is left to the user.
Offline Transitive Resolution¶
When --offline is set and a repository requires network access, the resolver checks the local wrap cache before giving up. If a cached wrap exists for the package and version, it is used for scanning without network access. If no cache entry is found, a warning is logged and the candidate is skipped.
Caching¶
- Scan results are cached per package+version in the local cache.
- The reverse index (dependency name to package name) is built in memory once per resolution run.
Impact on collider.json and collider.lock¶
collider.jsoncontains only direct (user-declared) dependencies, with optionalinclude/excludeandinclude_conditional/exclude_optionalfor transitive dependency control.collider.lockcontains all dependencies (direct independencies, transitive inpackages) with resolved versions, wrap hashes, andorigin(normalized repository URL) per entry.pkg addinstalls direct and transitive wraps intosubprojects/but only adds the direct dependency tocollider.json.installfrom lockfile restores everything (direct + transitive).installwithout a lockfile resolves fromcollider.jsonusing multi-root resolution.lockre-resolves transitively and writes the full dependency graph.
Limitations¶
--scan-dependenciesscans a singlemeson.buildfile. Projects that usesubdir()to splitdependency()calls across multiple files may produce incomplete scan results. For most WrapDB packages this is not an issue because patches typically consist of a singlemeson.build.- Rollback on transitive failure only removes the direct package. If some transitive dependencies were already installed before the failure, they are left in place. This is a known limitation that avoids accidentally removing wraps that may be shared with other packages.
Rationale
- Using Meson's own introspection avoids fragile regex parsing and leverages the exact parser Meson uses.
- A single code path for all repository types keeps the implementation simple and avoids protocol changes.
- resolvelib is battle-tested (used by pip) and handles the hard parts of resolution: backtracking, conflict detection, and cycle breaking.
- Consumer-side classification (system vs. wrap) adapts to each user's repository configuration.
- Single-pass multi-root resolution is more correct than per-root isolation because it detects shared transitive conflicts early.
Security and Integrity¶
- HTTP URLs are allowed but warned to avoid silent downgrade risks.
- Archives are validated against hashes before caching or publishing.
- Path traversal is prevented by using basename-only filenames.
- The lockfile's
wrap_hashdetects tampered or republished wrap files. Without the lockfile, an attacker controlling the repository could replace a wrap file (with matching archive hashes for malicious archives) undetected. The lockfile closes this gap by recording the expected wrap content hash at lock time.
Error Handling¶
- CLI commands return non-zero exit codes on failure.
- Failure conditions are logged at critical level for visibility.
Design Principles¶
- Three repository types: filesystem, WrapDB-compatible remote (read-only), and Collider remote (read-write).
- Single layout reused for both internal and hosted usage.
- No binary packaging or non-Meson integrations.
Testing Strategy¶
- Unit tests for wrap parsing, cache, repositories, and CLI workflows.
- Repository layout and URL rewrite behavior are verified.
- Offline cases are explicitly tested.
Open Questions¶
- Should
releases.jsonbe sorted strictly by version semantics rather than string?