Foundation ports

Foundation ports are curated recipes that adapt important existing C/C++ libraries — libraries that do not yet ship a native cabin.toml — to Cabin's build model. They live under the repository's ports/ directory and are explicitly not a public registry; this directory is closed to arbitrary submissions and is intended to be retired incrementally as upstreams adopt native cabin.toml.

The only foundation port that ships today is ports/zlib/1.3.1 — the zlib compression library pinned to upstream release 1.3.1. This document covers only what is implemented; future ports require a curated review and their own follow-up work.

Bundled ports

The curated set of foundation ports is embedded in the cabin binary at compile time, so a user with only cabin installed can depend on a bundled port without copying any recipe files:

[dependencies]
zlib = { port = true, version = "^1.3" }

port = true declarations require a version = "<requirement>" field. The bundled set is resolved by (name, version_req); the highest-versioned entry whose version satisfies the requirement wins. With the current single-entry bundled set, the only effective check is that the request is satisfiable. Run cabin port list to see the names and versions shipped in your binary. The dependency name must match a bundled entry exactly; unknown names surface PortError::UnknownBuiltin.

Cabin's source repository under ports/ is the authoritative location for each recipe. cabin-port's builtin module embeds the same files via include_str!, so edits to ports/zlib/1.3.1/port.toml flow into the binary on the next cargo build. A round-trip test in cabin-port::builtin asserts the embedded text and the on-disk recipe stay in sync.

Local recipes (for recipe development)

{ port-path = "../ports/zlib/1.3.1" } keeps working — the path is interpreted relative to the consumer's cabin.toml. This form is intended for developing or vetting a recipe before it lands in the bundled set, and for users who vendor a recipe into their own tree.

Anatomy of a foundation port

A port is a directory containing two files:

ports/<name>/<version>/
  port.toml      — recipe (pinned source archive + identity)
  cabin.toml     — overlay manifest (describes the upstream
                   sources as a Cabin C/C++ target)

For example, zlib 1.3.1 lives at ports/zlib/1.3.1/.

Optional patches/ may be added later if a port genuinely needs one; this milestone ships no patch-application code.

port.toml schema

[port]
name = "zlib"
version = "1.3.1"
description = "Compression library"   # optional
license = "Zlib"                      # optional
homepage = "https://zlib.net/"        # optional URL
upstream = "https://github.com/madler/zlib"  # optional URL

[source]
type = "archive"
url = "https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz"
sha256 = "9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23"
strip_prefix = "zlib-1.3.1"           # optional

[overlay]
manifest = "cabin.toml"
Field Required Notes
[port].name yes Must equal the overlay manifest's [package].name.
[port].version yes SemVer string; must equal the overlay manifest's [package].version.
[port].description / license / homepage / upstream no Plain documentation fields. Surfaced via cabin metadata.
[source].type yes Only "archive" is supported. Every other value (git, tag, branch, latest, …) is rejected with unsupported source type.
[source].url yes file://, http://, or https:// URL pointing at the upstream .tar.gz.
[source].sha256 yes Lower-case 64-character hex digest. Upper-case and wrong-length values are rejected.
[source].strip_prefix no Single relative path component that must equal the first path segment of every archive entry. The component is stripped before extraction so the overlay manifest sits at the prepared directory's root.
[overlay].manifest yes Relative path inside the port directory pointing at the overlay cabin.toml. Absolute paths and .. are rejected.

Unknown fields and unknown top-level tables are rejected by the parser (deny_unknown_fields).

Overlay manifest

The overlay is an ordinary Cabin manifest with one constraint: its [package].name and [package].version must match the authoritative identity declared in port.toml. Mismatches surface as overlay manifest for port \ ` declares package ` `; expected to match the port identity`.

zlib's overlay declares a single library target with the 15 canonical zlib C sources and the archive root on the include path:

[package]
name = "zlib"
version = "1.3.1"

[target.zlib]
type = "library"
sources = [
    "adler32.c", "compress.c", "crc32.c", "deflate.c",
    "gzclose.c", "gzlib.c", "gzread.c", "gzwrite.c",
    "infback.c", "inffast.c", "inflate.c", "inftrees.c",
    "trees.c", "uncompr.c", "zutil.c",
]
include_dirs = ["."]

Depending on a foundation port

A downstream package opts in via either the bundled form (port = true, resolved by name against the embedded set) or the filesystem form (port-path, pointing at a recipe directory):

[dependencies]
zlib = { port = true, version = "^1.3" }  # bundled recipe
# -- or for local development --
zlib = { port-path = "../ports/zlib/1.3.1" }

port = true requires a sibling version = "<requirement>" field (see "Bundled ports" above). port-path is mutually exclusive with version — the recipe at the path supplies the version. Both forms are mutually exclusive with path, workspace, and system, and neither currently supports features, default-features, or optional (Cabin rejects each combination with a typed error so the rule is visible at parse time).

Preparation pipeline

When Cabin runs a workspace-loading command, the CLI orchestrates preparation before the workspace loader sees the manifest:

  1. Walk the manifest tree to discover every reachable port dependency.
  2. Load each port.toml and validate it.
  3. Decide a fetch source per port:
  4. file:// URLs become a LocalArchive pointing at the filesystem path;
  5. http:// / https:// URLs are downloaded via the same HTTP client cabin-index-http uses, with a five-hop redirect budget. Following redirects is safe because the SHA-256 pin in port.toml is verified against the final response bytes, and upstream release archives commonly hop from a forge (e.g. GitHub) to a CDN. Compressed archives larger than 64 MiB are refused by the HTTP client; no foundation port currently approaches that limit.
  6. A previously prepared archive (matching SHA-256 already in the port cache) short-circuits the download so repeat invocations stay network-free.
  7. Verify the archive's SHA-256 against port.toml. Mismatch surfaces checksum mismatch for port \ `: expected sha256:..., got sha256:...`.
  8. Safely extract the archive into the port cache with the declared strip_prefix. This step reuses cabin-artifact's extraction primitive, so the decompression-bomb caps, symlink rejection, and path-traversal protection apply identically.
  9. Copy the overlay manifest into the extracted source dir as cabin.toml.
  10. Cross-check the overlay's [package] identity against port.toml. Mismatch surfaces an explicit error.
  11. Drop a sibling .ok completion marker so the next invocation can reuse the prepared directory without re-extracting.

Once prepared, each port directory looks exactly like a regular Cabin path dependency: the existing workspace loader, build planner, and Ninja backend take over unchanged. Foundation ports are tagged PackageKind::Local because their on-disk contents are local working state; they never enter the lockfile and never round-trip through the registry layer.

Cache layout

Prepared ports live under the same root the rest of Cabin's artifact cache uses:

<cache>/ports/
  archives/sha256/<hex>.tar.gz
  sources/sha256/<hex>/
    cabin.toml         (overlay)
    <upstream files>
  sources/sha256/<hex>.ok    (completion marker)

The cache root resolution follows the documented chain: --cache-dirCABIN_CACHE_DIRCABIN_CACHE_HOME$XDG_CACHE_HOME/cabin$HOME/.cache/cabin. The default on a Unix-like system with no overrides is $HOME/.cache/cabin/, so the example layout above lives at ~/.cache/cabin/ports/archives/sha256/<hex>.tar.gz etc. The cache is shared across projects on the same machine — content is checksum-addressed, so two projects depending on the same port reuse the same on-disk recipe and source tree.

Offline / frozen interaction

  • --offline blocks remote downloads; preparation still succeeds when the archive is already in the cache or when the port declares a file:// URL.
  • --frozen forbids populating the cache. If the prepared source tree is not already on disk, preparation fails with cannot prepare port \ ` because --frozen was specified and the port is not cached`.

cabin metadata provenance

The metadata view exposes one entry per prepared port under a top-level ports array, sorted by canonical port directory. Each entry records the upstream URL, the verified SHA-256, the declared strip_prefix, the overlay manifest path, and the cache directory the upstream sources were extracted into:

"ports": [
  {
    "name": "zlib",
    "version": "1.3.1",
    "origin": { "kind": "builtin", "name": "zlib" },
    "source_dir": "/home/<user>/.cache/cabin/ports/sources/sha256/<hex>",
    "source": {
      "kind": "archive",
      "url": "https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz",
      "sha256": "sha256:9a93b2b7...",
      "strip_prefix": "zlib-1.3.1"
    }
  }
]

For a port-path dependency the entry looks the same except origin carries { "kind": "path", "port_dir": "/.../ports/zlib/1.3.1" } and overlay_manifest is present (pointing at the on-disk cabin.toml). overlay_manifest is omitted for bundled ports.

The dependency itself appears under the consumer package's dependencies array. For a port-path dependency the source shape carries an origin block matching the top-level ports array:

"source": { "kind": "port", "origin": { "kind": "path", "port_dir": "../ports/zlib/1.3.1" } }

For a bundled (port = true) dependency the shape is:

"source": { "kind": "port", "origin": { "kind": "builtin", "name": "zlib" } }

What foundation ports are not

  • Not Cabin's public registry. Cabin's registry layer is documented in registry-design.md and evolves independently.
  • Not a submission queue. New foundation ports require a curated review; this directory is intentionally small.
  • Not a vehicle for binary distribution. Only source archives are supported.
  • Not a workaround for missing build-script support. Ports describe libraries whose source layout already fits Cabin's target model (a fixed list of sources plus include directories). Libraries that need configure-time generation, CMake / Meson / Autotools driving, or custom build commands are out of scope.
  • Not a feature surface. The port dependency form does not yet support feature flags, optional gating, or shared/static variant selection.

Error catalog

Diagnostic Trigger
no bundled foundation port named ... port = true references a name not present in cabin_port::builtin::BUILTIN.
foundation-port dependency ... must specify a version ... port = true without a sibling version field. (ManifestError::PortDependencyMissingVersion)
no bundled foundation port ... satisfies ... port = true, version = "<req>" where no bundled entry's version matches <req>. The message lists the available versions. (PortError::BuiltinVersionNotFound)
unsupported source type port.toml's [source].type is anything other than "archive".
is missing [source].sha256 The sha256 field is absent.
invalid SHA-256 The sha256 field is the wrong length or contains non-lower-case-hex characters.
invalid `<field>` URL [source].url, homepage, or upstream is not a valid URL.
unsafe overlay manifest path [overlay].manifest is absolute or contains ...
unsupported archive URL scheme The archive URL is not file://, http://, or https://.
checksum mismatch The downloaded archive's SHA-256 does not match the recipe.
source archive does not contain the declared strip_prefix directory The archive's first path component does not equal the declared prefix.
overlay manifest was not found at <path> [overlay].manifest points at a non-existent file inside the port directory.
overlay manifest declares package \`| The overlay's[package]identity disagrees withport.toml`.
cannot download port \` because --offline was specified` A remote URL was reached while running in offline mode.
cannot prepare port \` because --frozen was specified and the port is not cached| The cache does not already hold a prepared copy under--frozen`.
foundation-port dependency <name> declared by package <parent> has not been prepared Internal invariant violation: the CLI orchestration layer did not run before the workspace loader.
foundation-port directory <port_dir> does not exist The consumer's port-path = "..." value does not resolve to an existing directory.

Retiring a foundation port

When an upstream project ships and maintains a native cabin.toml, the corresponding foundation port should be retired. The retirement steps are:

  1. Switch downstream [dependencies] entries from { port = true, version = "..." } or { port-path = "../ports/<name>/<version>" } to the appropriate path / version / workspace form pointing at the new upstream-maintained package.
  2. Remove the corresponding entry from BUILTIN in crates/cabin-port/src/builtin.rs.
  3. Delete the ports/<name>/<version>/ directory in the same commit.
  4. Update ports/README.md to remove the entry from the "Available ports" list.
  5. Note the retirement in the relevant release notes.