SwiftPM Manifest Caching#
Tuist's swifterpm module caches the JSON output of swift package dump-package to avoid re-invoking the Swift compiler on every tuist generate run. There are two stacked caching layers β a per-package cache managed by ManifestLoader and a cross-package indexed cache managed by PackageInfoCacheWriter β consumed by SwiftPackageManagerGraphLoader in the CLI.
Cache Layers and File Layout#
Layer 1 β ManifestLoader (per-package): Caches a single package's dump at .build/swifterpm/manifests/package.json inside each package directory. On the next call, dumpPackageJSON reads this file before shelling out to swift package dump-package.
Layer 2 β PackageInfoCacheWriter (cross-package index): Writes a shared index at .build/swifterpm/package-info/index.json containing an entry for every resolved package. Individual package dumps are stored under packages/ with filenames derived from the package identity and a hash:
- Remote (source-control / registry) packages:
{safeIdentity}-{entryHash}.json, whereentryHash=SHA256(location:version:revision)[:16] - Local (fileSystem) packages:
{safeIdentity}-{pathHash}.json, wherepathHash=SHA256(absolutePackagePath)[:16]
The index records a schema_version field: v1 stores absolute paths, v2 stores paths relative to the scratch directory for portability across hosts .
Freshness Check (Timestamp-Based)#
Both layers use the same strategy: compare the lastModificationDate of the cached JSON file against the lastModificationDate of Package.swift. If cacheDate >= manifestDate, the entry is considered fresh .
Special case: registry packages may not have a Package.swift on disk (the manifest is served remotely). When the file is absent, the entry is treated as always-fresh for entries of kind == "registry" .
The CLI's SwifterPMPackageInfoCache.load silently drops any entry that fails the freshness check, falling back to a full swift package dump-package invocation for that package .
Known Cache Invalidation Issues#
Environment-Sensitive Manifests#
swift package dump-package evaluates Package.swift at runtime, so environment variables like CI=true can alter the manifest's output without changing the file's mtime. A real incident illustrates the risk: stephencelis/CSQLite@3.53.3 had a manifest that conditionally appended a SwiftToolchainCSQLiteDynamic product referencing a non-existent target β but only when CI=true. Because the registry served a stale snapshot of the manifest, cold swift package resolve calls failed in Linux CI while local dev succeeded silently . The timestamp-based freshness check cannot distinguish outputs produced under different environments.
Workaround: Pin to a known-clean version (exact: "3.50.4") while a registry re-sync is arranged.
Path-Based Keys for Local Packages#
Local (fileSystem) dependency cache filenames are keyed on the absolute path of the package directory , not its content. Moving the project to a new directory β common in CI workspaces or git worktrees β produces a different hash, causing an unnecessary cache miss on every run. Issue #7659 tracks this and proposes switching to content-based hashing using ContentHasher.hash(path:), already used in Tuist's binary cache .
Silent Manifest Changes#
When a manifest's rendered output changes without any detectable file mutation β for example when a constant in a code generator is not bumped β neither the timestamp check nor the path hash will notice. Issue #11608 documented a production incident where this type of silent drift caused a reconciler to remain wedged for ~6 hours . The proposed fix across both contexts is to derive cache/revision keys from a content hash of the manifest inputs rather than metadata.
Improvement Directions#
Two themes emerge from the open issues:
-
Content-based hashing: Replace mtime and path comparisons with SHA256 hashes of
Package.swiftcontent for freshness checks, and hash the package directory tree (or at leastPackage.swift) instead of the absolute path for local-package filenames. A higher-level cache (SwiftPackageManagerGraphCache, added in PR #11760) already does this β it hashesworkspace-state.json, root manifest content, local package fingerprints, Swift/Tuist versions, and environment variables as its composite cache key . -
Dynamic input tracking: Capture the environment variables consumed during
dump-packageexecution and include them in the cache key. This would prevent environment-sensitive manifests (like those guarded byCI=true) from being served stale cached output to a different environment.