blob: b9e08aec6b12032965eb942eb3cd7e54d704dd27 [file] [view] [edit]
# Jiri Package Cache
## 1. Overview & Architecture
Jiri manages prebuilt packages (such as toolchains, compilers, and SDKs)
distributed via Chrome Infrastructure Package Deployer (CIPD).
In multi-worktree development environments or when packages update frequently,
installing standalone copies of packages directly into each workspace duplicates
tens of gigabytes of identical binaries across worktrees and incurs repeated
network downloads and disk space exhaustion.
To solve this, Jiri provides an **instance-based, content-addressable package
cache** shared across the main checkout and all active worktrees.
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ PackageCache (.jiri_root/packages/) │
│ │
│ instances/ │
│ ├── <A5DE1AnhsOBwACTgzeukZ-l2FbYuA9Q8tQNrHaFQblYC>/ ─── [Hardlink] ──┐ │
│ │ ├── bin/gn │ │
│ │ └── ... ▼ │
│ │ Workspace │
│ ├── <bdupDu8hA7pXmKNwzF8WTbt1rgDro7L5srqIBXBigisC>/ ─── [Hardlink] ─► Destination
│ │ └── bin/clang ▲ │
│ │ │ │
│ └── <SPHCOQ38Z7qb62orRA-gTUIZdEVnEdsb_qx5iKCUVZQC>/ ─── [Hardlink] ──┘ │
│ └── ... │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Core Concepts & Terminology
* **`PackageCache`**: The central cache manager for the repository-level package
cache located at `[root]/.jiri_root/packages`. Manages the global cache lock
(`.cache.lock`), CIPD ensure synchronization, and cache directory layout.
* **`PackageCacheInstance`**: An immutable, content-addressed CIPD package
instance stored in the cache at `[root]/.jiri_root/packages/instances/<instance_id>`,
keyed directly by its canonical **CIPD Instance ID**. Once downloaded, its
contents are pristine and never modified.
* **`WorkspaceDestination`**: A destination directory within a specific checkout
or worktree where binaries and tools reside (for example,
`<workspaceRoot>/prebuilt/third_party/gn/linux-amd64`). Encapsulates the
ordered list of package instances to project into that directory.
* **`CacheStamp` (`.jiri_cache_hash`)**: An atomic verification stamp file placed
at `<destDir>/.jiri_cache_hash` that records structured JSON metadata
describing the target path and package instances currently linked into the
destination directory. Enables sub-millisecond fast-path verification and
safe cache detachment.
* **`CIPDEnsureFile`**: In-memory and on-disk representation of the CIPD ensure
specification, mapping every required package instance to `@Subdir instances/<instance_id>`.
* **`WorktreePackageIndex`**: The aggregated package registry that discovers all
active worktrees, computes the unique union of `PackageCacheInstance` objects,
and maps `WorkspaceDestination` targets per workspace.
* **Workspace Hardlink Forest**: The projection of one or more cached package
instances (`PackageCacheInstance`) into a `WorkspaceDestination` via recursive
hard links.
---
## 2. The Three-Stage Lifecycle of `jiri fetch-packages`
Every package fetch operation in Jiri (`jiri fetch-packages` or `jiri update`)
executes through a single, unified three-stage pipeline:
```text
┌─────────────────────────────────────────────────────────────────────────────┐
│ Stage 1: Migrations & Local Adoption │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Non-blocking lock on .jiri_root/packages/.cache.lock acquired at entry │
│ • In-place local filesystem optimization; eliminates remote network calls │
│ • Adopts legacy package-group entries (.jiri_root/packages/<hex64>) │
│ • Adopts standalone CIPD installations (<root>/.cipd) into package cache │
│ • Immediately removes legacy directories upon adoption │
│ • Detaches workspace in O(1) time when package-cache=false is selected │
│ • Best-effort: failures are never fatal; Stage 2 guarantees convergence │
└──────────────────────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Stage 2: Native CIPD Cache Synchronization & Pruning │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Held under .cache.lock across ensure generation, CIPD sync, and pruning │
│ • Accumulates all instances across main tree and active worktrees │
│ • Master ensure file executed via `cipd ensure` with $ParanoidMode │
│ • Self-healing: validates, repairs, and downloads missing instances │
│ • Natively deletes unreferenced instances from instances/<instance_id> │
└──────────────────────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Stage 3: Workspace Hardlink Forest Construction │
├─────────────────────────────────────────────────────────────────────────────┤
│ • Evaluates each WorkspaceDestination declared in checkout manifest │
│ • FAST-PATH: If destination CacheStamp (.jiri_cache_hash) matches, <1ms │
│ • SLOW-PATH: Reconstructs clean hardlink forest from PackageCacheInstance │
│ • Resolves file collisions in manifest order, matching CIPD semantics │
│ • Writes atomic completion stamp (.jiri_cache_hash) as structured JSON │
│ • Standalone checkouts (package-cache=false) remain untouched │
│ • Held under .cache.lock; released upon completion of Stage 3 via defer │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Stage 1: Migrations & Local Adoption
Implemented in [`project/cache_migration.go`](file:///usr/local/google/home/awolter/src/jiri/project/cache_migration.go).
Stage 1 is a dedicated, zero-network migration phase executed under `.cache.lock` before any sync or linking:
* **Zero-Download Legacy Group Adoption (`MigrateLegacyGroupCache`)**: If legacy package-group
directories (`.jiri_root/packages/<hex64>`) exist, Jiri inspects their internal CIPD
state (`.cipd/pkgs/<index>/<instance_id>`). Because CIPD internally keeps
individual package instances isolated in these subdirectories, Jiri
hardlinks the pristine files directly into `instances/<instance_id>` and
replicates the metadata into `.jiri_root/packages/.cipd/pkgs/`. This converts
the existing cache locally in **~1–2 seconds** with **0 bytes of network
traffic**, immediately removing the legacy directory upon completion.
* **Zero-Download Standalone Adoption (`MigrateStandaloneToPackageCache`)**: When a developer
enables the package cache on a previously standalone checkout, Jiri inspects `<root>/.cipd/pkgs/`,
hardlinks the pristine instances into `instances/<instance_id>`, seeds CIPD metadata into the
shared cache, and retires `<root>/.cipd`. Stage 2 then recognizes the packages as already installed,
requiring **0 bytes of network download**.
* **Zero-Download Cache Opt-Out (`MigratePackageCacheToStandalone`)**: When switching to
`package-cache=false`, Jiri detaches each matching workspace directory in $O(1)$ time (<1ms)
by removing `.jiri_cache_hash`, and restores any missing directories from the local package cache
via hardlinks without downloading.
* **Best-Effort Tolerance**: Any unexpected read error during adoption is
logged as debug information and safely skipped; Stage 2 guarantees that all
declared instances are cleanly resolved and installed.
### Stage 2: Native CIPD Cache Synchronization & Pruning
Implemented in [`project/package_cache.go`](file:///usr/local/google/home/awolter/src/jiri/project/package_cache.go).
Stage 2 establishes the ground truth of the cache repository:
* **Lock Lifecycle & Concurrency**:
* **Acquisition**: Before Stage 1 begins, Jiri acquires an exclusive non-blocking
file lock on `.jiri_root/packages/.cache.lock`. If another worktree or concurrent
Jiri process is currently synchronizing or linking packages, Jiri fails fast
immediately with an actionable message prompting the user to wait and retry.
* **Scope**: The lock is held continuously across the entire package cache pipeline:
Stage 1 (migration and retirement), Stage 2 (master ensure generation, CIPD execution,
and pruning), and Stage 3 (workspace hardlink forest construction and stamping).
* **Release**: The lock is released via `defer unlock()` at the completion of Stage 3.
This provides complete end-to-end atomicity, eliminating any possible races between
concurrent package operations or background pruning.
* **Multi-Worktree Collection**: Jiri inspects the active manifests of the
main checkout and all registered worktrees, accumulating the complete set
of required package instances into a single master ensure file
(`instance_master.ensure`). Any unpinned tags or floating versions are
resolved via `cipd.ResolveEnsureFile`. Broken or inaccessible worktrees are
tolerated with a warning.
* **CIPD Native Pruning**: Jiri invokes `cipd ensure` targeting
`[root]/.jiri_root/packages`. CIPD verifies all declared instances under
`instances/<instance_id>`. Crucially, CIPD natively unlinks and deletes any
instance directories under `instances/` that are no longer referenced by the
master ensure file, completely eliminating the need for bespoke garbage
collection.
### Stage 3: Workspace Hardlink Forest Construction
Implemented in [`project/workspace_destination.go`](file:///usr/local/google/home/awolter/src/jiri/project/workspace_destination.go).
Stage 3 projects the synchronized instance store into each `WorkspaceDestination`
inside the active checkout:
* **Fast-Path Verification ($O(1)$)**: If `CacheStamp` (`<destDir>/.jiri_cache_hash`)
exists and its contents match the expected target hash via `MatchesTarget`,
Jiri immediately returns with 0 I/O. File modification times (`mtime`) are
preserved, preventing spurious rebuilds in Ninja and Bazel.
* **Clean Forest Reconstruction**: If the verification stamp is missing or
mismatched (e.g. following a version roll), Jiri removes the destination
directory and reconstructs a clean directory structure by recursively
hardlinking from the required `PackageCacheInstance` directories
(`instances/<instance_id>`).
* **Manifest-Order Collision Resolution**: When multiple packages share the
same destination path (for example, common SDK headers and platform-specific
binaries), instances are linked sequentially in manifest order. If two
packages provide the same file path, the later package in manifest order
replaces the file link, faithfully reproducing CIPD's native multi-package
semantics.
* **Atomic Completion Marker**: The stamp file `.jiri_cache_hash` is written
as the final step after all links and symlinks are successfully created.
---
## 3. Why Use CIPD Instance IDs as the Canonical Key?
Rather than synthesizing an ad-hoc hash (such as `sha256(name:version)`), the
package cache keys each instance directory directly by its **CIPD Instance
ID** (for example, `GvbSt6gklhdVYRp9r7bot-rllr0smxfv9rAElX3K798C`):
1. **Canonical Content Addressability**:
The Instance ID is CIPD's SHA-256 digest of the package archive payload. It
represents the canonical ground truth of the files contained within the
package.
2. **Deduplication Across Tags and References**:
Different git tags or floating refs (for example, `git_revision:abc1234` and
`latest`) that point to the exact same build artifact resolve to the
**identical Instance ID**. Worktrees referencing different tags pointing to
the same build share a single instance directory automatically.
3. **Hermetic Lockfile Alignment**:
In Fuchsia, all packages are pinned in `jiri.lock`, which records the
`instance_id` for each package. The master ensure file is fully pinned and
reproducible:
```text
@Subdir instances/GvbSt6gklhdVYRp9r7bot-rllr0smxfv9rAElX3K798C
gn/gn/linux-amd64 GvbSt6gklhdVYRp9r7bot-rllr0smxfv9rAElX3K798C
```
---
## 4. The Role and Necessity of `.jiri_cache_hash`
Because hardlinked files are standard, regular files residing in ordinary
directories, there is no OS primitive on Linux or macOS to check whether a
directory tree of tens of thousands of files matches a cache directory without
recursively walking and statting every inode.
`.jiri_cache_hash` is managed by the `CacheStamp` abstraction and serves three essential functions:
1. **$O(1)$ Version Roll Detection**:
The stamp file records the structured metadata of the instances linked into
a `WorkspaceDestination`:
```json
{
"format_version": 1,
"target_path": "prebuilt/third_party/gn/linux-amd64",
"instances": [
{
"package": "gn/gn/linux-amd64",
"version": "git_revision:b4f59045...",
"instance_id": "GvbSt6gklhdVYRp9r7bot-rllr0smxfv9rAElX3K798C"
}
]
}
```
`CacheStamp.MatchesTarget` checks if the installed instances match the expected manifest target.
If any package rolls to a new version, Jiri detects this in $O(1)$ time, wipes the directory, and
reconstructs the forest. For legacy checkouts, `CacheStamp` automatically falls back to parsing
plain-text hashes and instance IDs, upgrading them to structured JSON on the next write.
2. **Build Hermeticity (`mtime` Preservation)**:
Fuchsia checkouts contain ~150,000 prebuilt files (~30 GB). Touching or
re-linking these files on every `jiri update` or `fx build` updates file
modification times (`mtime`), invalidating build system action caches in
Ninja and Bazel and triggering multi-hour incremental rebuilds. Checking
`.jiri_cache_hash` validates the directory in **< 1 millisecond** with
**zero file touches**.
3. **Self-Describing & Reversible Audit Stamp**:
Because the stamp records the exact CIPD package name, version, and instance ID,
developers and tools can inspect `.jiri_cache_hash` directly with `cat` or `jq`
to discover what is installed on disk without parsing XML manifests or running CIPD.
---
## 5. Unified Pipeline for `package-cache=false` and `true`
Jiri avoids divergent code paths between cache-enabled and standalone checkouts.
Both configurations pass through the same 3-stage lifecycle:
<!-- mdformat off(reason: pipeline table) -->
| Stage | `package-cache=true` | `package-cache=false` |
|---|---|---|
| **Stage 1: Local Adoption** | Adopts legacy group cache directories into `instances/<id>`. | Detaches `WorkspaceDestination` in-place by removing `CacheStamp` (<1ms). |
| **Stage 2: Cache Sync** | Synchronizes `PackageCache` for the main tree and all worktrees via `WorktreePackageIndex`. | Synchronizes `PackageCache` for any active worktrees that require caching. |
| **Stage 3: Workspace Projection** | Projects hardlink forest and writes `CacheStamp`. | Reuses cached instances via `LinkPath` if destination directory is missing; otherwise leaves standalone checkout untouched. |
<!-- mdformat on -->
---
## 6. State Machine & Invariants (S1–S18)
During package synchronization, Jiri evaluates each workspace destination
against an exhaustive state matrix:
<!-- mdformat off(reason: state matrix table) -->
| State | Config | Cache (`instances/<id>`) | Workspace Directory | Scenario | Expected Behavior | Network Download? |
|---|---|---|---|---|---|---|
| **S1** | Enabled | Missing | Missing | Fresh checkout | Download instance to cache; hardlink to workspace; write stamp | **Yes** (initial fetch) |
| **S2** | Enabled | Valid | Valid Hardlink | Steady-state / no-op | Stamp matches: fast-path return nil (<1ms) | **No** (fast path) |
| **S3** | Enabled | Valid | Missing | New worktree or deleted workspace | Reconstruct hardlinks from cache; write stamp | **No** (instant link) |
| **S4** | Enabled | Valid | Stale Hardlink | Roll to pre-cached version | Replace workspace with hardlinks to target cache; update stamp | **No** (instant link) |
| **S5** | Enabled | Valid | Legacy Symlink | Upgrade legacy symlink checkout | Remove symlink; replace with hardlink forest; write stamp | **No** (local conversion) |
| **S6** | Enabled | Missing | Stale Hardlink | Version roll with cache enabled | Clean workspace; download new instance to cache; hardlink to workspace | **Yes** (mandatory) |
| **S7** | Enabled | Missing | Valid Standalone | Opt-in without roll | Download/verify clean instance in cache; link to workspace; write stamp | **Yes** (clean seed) |
| **S8** | Enabled | Missing | Stale Standalone | Opt-in across a roll | Discard stale standalone; download new version clean to cache; hardlink | **Yes** (mandatory) |
| **S9** | Enabled | Missing | Dirty Standalone | Dirty / untracked files in workspace | Discard dirty directory; clean download into cache; hardlink to workspace | **Yes** (clean download) |
| **S10** | Enabled | Invalid | Any | Corrupted cache (non-dir or file) | Purge corrupt entry; re-download to cache; reconstruct hardlinks | **Yes** |
| **S11** | Disabled | N/A | Missing | Fresh checkout, cache disabled | CIPD downloads standalone package directly to workspace; no stamp | **Yes** |
| **S12** | Disabled | N/A | Valid Standalone | Steady-state, cache disabled | CIPD verifies standalone package in-place, no-op | **No** (CIPD no-op) |
| **S13** | Disabled | N/A | Stale Standalone | Version roll, cache disabled | CIPD updates standalone workspace directory in-place | **Yes** (update download) |
| **S14** | Disabled | Valid | Valid Hardlink | Opt-out of cache (`true -> false`) | Retain existing hardlinks in-place; remove stamp file in O(1) time | **No** (instant detach) |
| **S15** | Disabled | Valid | Missing | Cache disabled, missing workspace | Hardlink from cache to workspace via `LinkPath`, avoiding network download | **No** (local hardlink) |
| **S16** | Disabled | Missing | Stale Hardlink | Opt-out across a version roll | Remove stale hardlinks and stamp; CIPD downloads new standalone version | **Yes** (mandatory) |
| **S17** | Disabled | Valid | Missing (Roll) | Roll while restoring from cache | Restore matching version from cache or clean download if uncached | **No** (if cached) |
| **S18** | Multi-WT | Valid | Multiple Worktrees | Multi-worktree roll & GC | Instances used by any worktree are preserved by CIPD; obsolete prebuilts cleaned | **Yes** (for new version) |
<!-- mdformat on -->
---
## 7. Garbage Collection & Obsolete Prebuilts
With the package cache, garbage collection is completely streamlined:
1. **Retirement of Bespoke Cache GC**:
Jiri's legacy custom garbage collector (`PackageGC`), which walked all
worktrees, matched directory timestamps, and evaluated mtime cutoff
heuristics, has been **completely removed**. Cache pruning is handled
natively and authoritatively by CIPD during Stage 2 synchronization based
on the master ensure file.
2. **Obsolete Prebuilt Directory Cleanup**:
When packages roll or are removed from the manifest, obsolete prebuilt
directories or dangling symlinks left behind in destination directories are
cleaned up by `cleanObsoletePrebuiltsForWorkspace` during `jiri update -gc`
and `jiri worktree prune`.
---
## 8. Verification & Test Suite
The package cache implementation is verified by unit and integration test
suites:
* **`//project/package_cache_test.go`**:
Consolidated test suite executing all 18 state machine scenarios (S1–S18),
verifying inode relationships (`stat.Ino`), fast-path stamp verification,
concurrency locking, multi-worktree rolls, and native CIPD pruning.
* **`//integrationtests/package_cache_toggle_test.go`**:
End-to-end integration test verifying seamless toggling between
`package-cache=true` and `package-cache=false`.
* **`//cmd/jiri/subcommands/update_test.go`**:
Verifies `jiri update -gc` cleans obsolete cached prebuilt directories.
---
## Appendix A: Historical Context & Legacy Group Cache Analysis
### The Legacy "Package Group" Architecture
Originally, Jiri grouped packages sharing a destination path:
* Jiri computed a composite group hash:
`sha256(pkgA.Name + ":" + pkgA.Version + "\n" + pkgB.Name + ":" + pkgB.Version)`.
* CIPD downloaded the combined packages into a temporary staging directory
(`tmp/merged-*`).
* Jiri renamed `tmp/merged-*` into `.jiri_root/packages/<group_hash>`.
* Jiri recursively hardlinked `.jiri_root/packages/<group_hash>` to
`workspaceDir`.
* Jiri implemented a custom `PackageGC` scanner (~450 lines of Go code) that
walked all worktrees using `findUsedCachePaths` to prevent active cache
directories from being deleted based on 7-day mtime thresholds.
### Flaws of the Legacy Package Group Design
1. **Redundant Downloads on Partial Rolls**:
When one package in a multi-package destination rolled (e.g. host tool
updated from v1 to v2), the composite group hash changed from
`hash(common:v1, tool:v1)` to `hash(common:v1, tool:v2)`. CIPD was forced
to re-download or re-extract `common` (e.g. 1.5 GB SDK common headers),
even though `common` had not changed at all.
2. **Duplication Across Architectures**:
If Linux and macOS builds shared a platform-independent package (e.g.
`sdk_common`), Jiri generated two separate composite directories in the
cache (`hash(common, linux)` and `hash(common, mac)`), storing duplicate
copies of `common`.
3. **Fragile Bespoke Garbage Collection**:
Walking multiple worktrees, matching directory stamps, and guessing
activity via mtime heuristics was error-prone and frequently led to active
worktree dependencies being deleted from the cache or orphaned directories
accumulating indefinitely.
4. **Temporary Staging Orchestration**:
Managing `tmp/merged-*` staging directories, atomic renames, and failure
rollbacks added substantial complexity and failure modes.
The instance-based cache replaces the legacy composite group model with an
instance-level store managed natively by CIPD, delivering zero network
redundancy, native pruning, and sub-millisecond steady-state performance.