| # Jiri Package Cache |
| |
| ## 1. Overview & Architecture |
| |
| Jiri manages prebuilt packages (such as toolchains, compilers, and SDKs) |
| distributed via Chrome Infrastructure Package Deployer (CIPD). |
| |
| By default (`package_cache=false`), Jiri installs packages as standalone |
| directories directly into the checkout root (for example, |
| `prebuilt/third_party/clang/linux-x64`). When multiple worktrees exist or when |
| packages update frequently, this model duplicates gigabytes of identical |
| binaries across worktrees and incurs repeated network downloads and disk usage. |
| |
| To solve this, Jiri provides a **unified, content-addressable package cache** |
| located under: |
| |
| ``` |
| [root]/.jiri_root/packages/<group_hash>/ |
| ``` |
| |
| ### Content-Addressable Group Hash |
| |
| A single destination path in the checkout may consist of one or more grouped |
| CIPD packages. The group hash is a SHA-256 digest computed from the sorted |
| package declarations: |
| |
| ``` |
| hash = sha256( |
| pkg1.Name + ":" + pkg1.Version + "\n" + |
| pkg2.Name + ":" + pkg2.Version + ...) |
| ``` |
| |
| Because the hash uniquely identifies the exact packages and versions, the |
| directory `.jiri_root/packages/<group_hash>` is **content-addressable and |
| immutable**. Once populated cleanly, it can be safely shared across all |
| worktrees in the Jiri root. |
| |
| --- |
| |
| ## 2. Evolution: From Symlinks to Hard Links |
| |
| ### The Legacy Symlink Model |
| |
| Originally, Jiri populated `.jiri_root/packages/<hash>` and symlinked the |
| workspace destination to the cache: |
| |
| ``` |
| destDir -> .jiri_root/packages/<hash> |
| ``` |
| |
| While simple, symlinked prebuilts caused significant problems in developer |
| workflows: |
| |
| - Compilers and build tools (Clang, Rust, Python) resolve `../` relative to |
| the symlink target rather than the symlink's location in the checkout, |
| breaking relative header searches, runtime library resolution, and build |
| hermeticity. |
| |
| - Tools scanning directories or modifying files could unintentionally alter |
| shared cache files or break symlinks. |
| |
| ### The Modern Hard Link Model |
| |
| To solve this, Jiri transitioned to **hard links**: |
| |
| - The cache directory `.jiri_root/packages/<hash>` remains the authoritative |
| master copy. |
| |
| - The workspace destination (`destDir`) is a real directory containing hard |
| links to the corresponding files in the cache. |
| |
| - Internal relative symlinks inside the package are preserved and adjusted. |
| |
| ### The Role of `.jiri_cache_hash` |
| |
| Because a hard-linked directory appears on disk as a normal, regular directory, |
| Jiri needs a mechanism to distinguish between: |
| |
| 1. A **standalone directory** downloaded with `package_cache=false` |
| (independent files). |
| |
| 2. A **cache-managed directory** hard-linked to `.jiri_root/packages/<hash>`. |
| |
| Jiri writes a sentinel file inside the destination directory: |
| |
| ``` |
| <destDir>/.jiri_cache_hash |
| ``` |
| |
| This file contains the `<hash>` string of the cache entry to which `destDir` is |
| currently hard-linked. |
| |
| --- |
| |
| ## 3. The State Machine |
| |
| During `jiri update` or `jiri fetch-packages`, Jiri evaluates each package group |
| against a state machine determined by three dimensions: |
| |
| 1. **Config**: `package-cache` enabled (`true`) or disabled (`false`). |
| |
| 2. **Cache Store**: `.jiri_root/packages/<target_hash>` (Missing, Valid, or |
| Invalid). |
| |
| 3. **Destination Directory**: `<destDir>` (Missing, Legacy Symlink, Valid |
| Hardlink, Stale Hardlink, Valid Standalone, Stale Standalone, or Dirty). |
| |
| ### State Matrix & Expected Behavior |
| |
| <!-- mdformat off(reason: state matrix table) --> |
| | State | Config | Cache | Destination | Scenario | Expected Behavior | Network Download? | |
| |---|---|---|---|---|---|---| |
| | **S1** | Enabled | Missing | Missing | Fresh checkout | Download from CIPD to tempDir, rename to cache, hardlink to dest, write hash | **Yes** (initial fetch) | |
| | **S2** | Enabled | Valid | Valid Hardlink | Steady-state / no-op | Hash matches: fast-path return nil | **No** (fast path) | |
| | **S3** | Enabled | Valid | Missing | New worktree or deleted destDir | Create hardlinks from cache to destDir, write hash | **No** (instant link) | |
| | **S4** | Enabled | Valid | Stale Hardlink | Roll to already-cached version | Replace destDir with hardlinks to target cache, update hash | **No** (instant link) | |
| | **S5** | Enabled | Valid | Legacy Symlink | Upgrade legacy symlink checkout | Remove symlink, replace with hardlinks to cache, write hash | **No** (local conversion) | |
| | **S6** | Enabled | Missing | Stale Hardlink | **Roll with cache enabled** (Clang 23 -> 24) | Remove stale hardlinks; download new version to cache; hardlink to dest | **Yes** (mandatory) | |
| | **S7** | Enabled | Missing | Valid Standalone | **Opt-in to cache without roll** | Verify with CIPD ensure (no-op), migrate to cache, hardlink back | **No** (local migration) | |
| | **S8** | Enabled | Missing | Stale Standalone | **Opt-in across a roll** | Update dest via CIPD ensure or download clean to cache; do not migrate stale files | **Yes** (mandatory) | |
| | **S9** | Enabled | Missing | Dirty Standalone | Pre-existing dirty / untracked files | Clean download into cache; replace destDir; do not poison cache | **Yes** (clean download) | |
| | **S10** | Enabled | Invalid | Any | Corrupted cache (non-dir or file) | Delete corrupt cache; re-download to cache; hardlink to dest | **Yes** | |
| | **S11** | Disabled | N/A | Missing | Fresh checkout with cache disabled | CIPD downloads standalone package to destDir, no hash file | **Yes** | |
| | **S12** | Disabled | N/A | Valid Standalone | Steady-state with cache disabled | CIPD confirms no changes, no-op | **No** (CIPD no-op) | |
| | **S13** | Disabled | N/A | Stale Standalone | Version roll with cache disabled | CIPD updates destDir in-place | **Yes** (update download) | |
| | **S14** | Disabled | Valid | Valid Hardlink | **Opt-out of cache (`true -> false`)** | Keep hardlinks in destDir, remove hash file | **No** (instant no-op) | |
| | **S15** | Disabled | Valid | Missing | Fresh destDir but package cached | Link cache to destDir (`LinkPath`), avoiding network download | **No** (local hardlink) | |
| | **S16** | Disabled | Missing | Stale Hardlink | Opt-out across a roll | Remove stale hardlinks and hash file; CIPD downloads new version | **Yes** (mandatory) | |
| <!-- mdformat on --> |
| |
| --- |
| |
| ## 4. Network Downloads vs. Local Migration |
| |
| ### When Network Downloads MUST Occur |
| |
| A network download from CIPD is mandatory whenever: |
| |
| 1. The requested package version is **not present anywhere locally** (neither |
| in `.jiri_root/packages/<target_hash>` nor as a valid standalone package in |
| `destDir`). |
| |
| 2. A **package roll** occurs (for example, Clang 23 $\to$ Clang 24) and the |
| machine does not yet have Clang 24. |
| |
| 3. The existing cache entry is corrupted (non-directory, truncated, or |
| invalid). |
| |
| ### When Network Downloads CAN Be Skipped |
| |
| Network downloads must be bypassed in favor of local filesystem operations in |
| the following situations: |
| |
| 1. **Cache Hit (`S2`, `S3`, `S4`)**: The target version already exists in |
| `.jiri_root/packages/<target_hash>`. Jiri creates hard links from the cache |
| to `destDir` in milliseconds. |
| |
| 2. **Opt-in with Valid Standalone (`S7`)**: The user has already downloaded the |
| correct version standalone (`package_cache=false`) and enables |
| `package_cache=true`. Jiri verifies the directory with CIPD ensure (which |
| does 0 network transfer) and moves it into the cache. |
| |
| 3. **Opt-out or Cache-Disabled with Cache Hit (`S14`, `S15`)**: The user has |
| `package_cache=false`, but the target package is already in |
| `.jiri_root/packages/<target_hash>`. Jiri links or retains the hardlinks in |
| `destDir` (`osutil.LinkPath`), avoiding a full disk copy or re-download. |
| |
| --- |
| |
| ## 5. Critical Failure Modes & Invariants |
| |
| ### 1. The Roll Migration Bug (Cache Poisoning) |
| |
| - **Root Cause**: If `cacheDir` was missing during a version roll, Jiri |
| previously checked `if destExists && !destIsSymlink` and blindly executed |
| `osutil.Rename(destDir, cacheDir)`. When rolling from Clang 23 to 24, it |
| renamed the old Clang 23 directory into the Clang 24 cache directory and |
| wrote the Clang 24 hash to `.jiri_cache_hash`. Jiri then believed Clang 24 |
| was installed and never contacted CIPD, permanently locking the user onto |
| the old compiler. |
| |
| - **Invariant**: **Never migrate `destDir` into `cacheDir` based solely on |
| directory existence.** Jiri must verify that `destDir` genuinely matches the |
| target package before migrating, or perform a clean download into |
| `cacheDir`. |
| |
| ### 2. Hardlink In-Place Mutation |
| |
| - **Root Cause**: If `destDir` contains hard links to an older cache entry |
| (indicated by `.jiri_cache_hash`), running CIPD ensure in-place inside |
| `destDir` can mutate files shared with `.jiri_root/packages/<old_hash>`, |
| corrupting other worktrees checking out older commits. |
| |
| - **Invariant**: Only standalone directories (`!destHasCacheHash && |
| !destIsSymlink`) may be updated in-place. If `destHasCacheHash` is present |
| and stale, `destDir` must be cleanly unlinked/removed before downloading or |
| populating the new version. |
| |
| ### 3. Untracked File Contamination |
| |
| - **Root Cause**: CIPD ensure does not delete untracked files by default. |
| Migrating an arbitrary directory into `.jiri_root/packages/` can |
| permanently contaminate the content-addressable cache with build artifacts |
| or dirty files. |
| |
| - **Invariant**: The content-addressable cache must only be populated from |
| clean CIPD installations or verified package roots (possessing `.versions/` |
| and `.cipd/`). |
| |
| ### 4. Opt-Out Hardlink Preservation & Metadata Cleanup |
| |
| - **Root Cause**: `restorePackageFromCache` previously checked `if |
| !destIsSymlink { return nil }`. When opting out of package cache, |
| hardlinked directories were ignored, leaving hardlinks attached to the |
| cache and leaving `.jiri_cache_hash` in `destDir`. |
| |
| - **Invariant**: When `package-cache=false`, `destDir` retains hardlinks (or is |
| hardlinked from cache via `osutil.LinkPath` if missing) without copying, and |
| `.jiri_cache_hash` must be removed. |
| |
| ### 5. Worktree Cache Preservation During GC |
| |
| - **Root Cause**: `findUsedCachePaths` previously checked only symlinks and |
| ignored hardlinked packages (directories containing `.jiri_cache_hash`). |
| When packages rolled in the main tree, `PackageGC` pruned cache entries that |
| were still required by worktrees. |
| |
| - **Invariant**: `findUsedCachePaths` must inspect `.jiri_cache_hash` files in |
| all registered worktrees to preserve cache directories in active use. |
| |
| ### 6. Obsolete Prebuilt Garbage Collection |
| |
| - **Root Cause**: Jiri previously did not remove obsolete hardlinks or |
| symlinks from `prebuilt/` when packages rolled or were removed from the |
| manifest, leaving orphaned files in the workspace. |
| |
| - **Invariant**: `FetchPackages` and `PackageGC` remove obsolete hardlinked |
| directories and dangling symlinks within `prebuilt/` that are no longer |
| declared in the manifest. |
| |
| --- |
| |
| ## 6. Verification & Test Suite |
| |
| The state machine and failure modes are tested in: |
| |
| ``` |
| //project/package_cache_test.go |
| ``` |
| |
| The suite contains unit tests for all states, using: |
| |
| - **Inode validation (`stat.Ino`)**: Verifies that hard links are established |
| and that local migrations preserve inodes without re-downloading. |
| |
| - **Canary file propagation**: Verifies that local cache-to-destination |
| restorations copy local cache content rather than falling back to network |
| downloads. |