Hive
fix(cache): deduplicate registry manifest variants
GitHub issue · Closed
Summary
This fixes a SwiftPM registry-resolution failure that could block fresh worktrees before the CLI workspace could even be generated.
The change makes the cache registry treat Swift package manifest variants by their canonical Swift tools version instead of only by their filename. When a package has both a default Package.swift manifest and an alternate Package@swift-X.Y.swift manifest that resolve to the same Swift tools version, the registry now keeps the default manifest and stops advertising or persisting the redundant alternate.
Motivation
New Codex worktrees were repeatedly getting stuck in this sequence:
- A focused CLI build or Xcode test needs the generated workspace.
tuist generate tuist ProjectDescription --no-openfails because external dependencies have not been installed yet.tuist installdelegates to SwiftPM registry resolution with--replace-scm-with-registry.- SwiftPM aborts while resolving registry metadata with errors like:
duplicate key found: 'Package@swift-5.5.0.swift'
duplicate key found: 'Package@swift-6.0.0.swift'
I reproduced the failure locally while trying to build the CLI. One concrete case was pointfreeco.swift-snapshot-testing, where SwiftPM failed with duplicate key found: 'Package@swift-6.0.0.swift'. A similar failure showed up earlier for swiftlang.swift-docc-symbolkit with Package@swift-5.5.0.swift.
This was especially frustrating because the error happens before the CLI code is compiled or tested. It makes agents report that they cannot run focused tests because the worktree cannot get past dependency resolution.
Root Cause
The registry was returning alternate manifest links based on the manifest filenames in metadata or S3:
Package.swift
Package@swift-6.0.swift
That is valid only if the alternate manifest represents a distinct Swift tools version. Some packages, however, publish an alternate manifest whose declared tools version is equivalent to the default manifest after SwiftPM canonicalization.
For pointfreeco/swift-snapshot-testing, the default manifest and Package@swift-6.0.swift both declare Swift tools version 6.0. Newer SwiftPM/Xcode canonicalizes that to a three-component key such as 6.0.0, so the two manifests collide internally as:
Package@swift-6.0.0.swift
The archive itself does not need to contain two literal Package@swift-6.0.0.swift files. The duplicate is created by SwiftPM while normalizing equivalent manifest versions from the registry response.
Approach
I added Cache.Registry.ManifestVariants as the single place that understands package manifest variants:
- parses alternate filenames such as
Package@swift-5.9.swift - parses
// swift-tools-version:declarations - canonicalizes tools versions to
major.minor.patch - filters alternates whose canonical tools version matches the default
Package.swift - filters duplicate alternates that canonicalize to the same tools version
That logic is applied in three places:
RegistryControlleruses it before generatingLinkheaders for alternate manifests. This fixes the active registry response path that SwiftPM consumes.ReleaseWorkeruses it before persisting release manifest metadata. This prevents newly indexed packages from storing redundant manifest variants.AlternateManifestsuses it in the S3 fallback path. When metadata is missing or stale, fallback discovery now reads the root manifest alongside alternates so it can make the same canonical-version decision.
Unknown or unparsable tools versions are still preserved. The filter only drops an alternate when it can prove that the canonical tools version was already represented by the default manifest or by an earlier alternate.
Alternatives Considered
I considered treating this as a CLI/worktree issue and relying on tuist install --force-resolved-versions, which did unblock this one local checkout. That is only a workaround: fresh worktrees without a usable resolved file still hit the registry response and fail before generation or testing.
I also considered suppressing all alternate manifests. That would be too broad because alternate manifests are still useful for packages that genuinely need different manifests across Swift tools versions.
Exact filename de-duplication was not enough either. The observed failures are caused by SwiftPM canonicalization, so 6, 6.0, and 6.0.0 need to be compared as the same tools version.
Impact
After this cache service change is deployed, fresh worktrees should be able to run tuist install, generate the CLI workspace, and run focused CLI builds/tests without hitting this duplicate-manifest SwiftPM failure.
The change is intentionally cache-side rather than CLI-side because the registry is the source of the invalid manifest variant set. Fixing the response also helps every SwiftPM client that resolves through the Tuist registry, not just Codex worktrees.
Validation
I validated the cache change with focused tests:
mix test test/cache/registry/alternate_manifests_test.exs test/cache/registry/release_worker_test.exs test/cache_web/controllers/registry_controller_test.exs
Result:
89 tests, 0 failures
I also formatted the touched cache files:
mix format lib/cache/registry/manifest_variants.ex lib/cache/registry/alternate_manifests.ex lib/cache/registry/release_worker.ex lib/cache_web/controllers/registry_controller.ex test/cache/registry/alternate_manifests_test.exs test/cache/registry/release_worker_test.exs test/cache_web/controllers/registry_controller_test.exs
Finally, I verified the original developer workflow after unblocking this local checkout with the resolved-file workaround that is needed until the live cache service is deployed:
tuist generate tuist ProjectDescription --no-open
xcodebuild build -workspace Tuist.xcworkspace -scheme tuist CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=""
Result:
** BUILD SUCCEEDED ** [422.510 sec]
No GitHub comments yet.