Pin the snapshot environment on the targets, and file the package-integration finding#147
Merged
Conversation
SNAPSHOT_EXPECTED_* and TZ were set on the StuffSnapshotTests scheme alone. Tuist also autogenerates schemes for these targets -- WhereUISnapshotTests gets its own, and the Periscope and SwiftDataInspector snapshot bundles get grouped into PeriscopeTools and SwiftDataInspector by name suffix -- and none of those carried the pins. Running one image bundle from Xcode therefore ran it unpinned, which does not fail loudly: the runner's fail-fast simulator guard never fires, and TZ-dependent references (widget day labels, log-viewer timestamps) silently compare against images recorded in another timezone. Hoists the values into one `snapshotEnvironment` constant and sets it in both places, which is deliberate rather than redundant: the target-level copy pins the autogenerated schemes, the scheme-level copy pins the aggregate CI invocation, and setting only one leaves the other quietly unpinned. Verified by counting SNAPSHOT_EXPECTED entries in each generated scheme before and after -- WhereUISnapshotTests went 0 -> 3, PeriscopeTools and SwiftDataInspector 0 -> 3 -- against a from-scratch generate, since a stale scheme file could otherwise have faked the result. Side effect worth knowing: the grouped schemes now run their module's unit tests under the pinned TZ too, because Xcode scheme environment variables apply to the whole test action rather than per target. Ran the PeriscopeTools scheme (unit + snapshot together) to confirm that is harmless. Stuff-iOS-Tests is unaffected -- it is an explicit scheme and gained no pins. Verified: full StuffSnapshotTests scheme green, no reference images modified; swiftformat --lint clean.
Project.swift uses Package.local(path:), which emits an XCLocalSwiftPackageReference and hands the package to Xcode's SPM integration: every product links statically into each consumer, Tuist never sees the targets, and PackageSettings is inert -- which is why an earlier attempt to set productTypes did nothing. The arrangement Tuist actually intends is to declare the local package as a dependency of a Tuist/Package.swift and consume products with .external(name:), so Tuist generates the targets and owns their product types and settings. Prototyped it. The mechanism works, but it is blocked on repo layout rather than on anything subtle: Tuist cannot vendor a local package whose directory is the project directory, and dies with "Duplicate values for key: /Users/kve/Development/Stuff4". Confirmed that is the specific cause by pointing Tuist/Package.swift at a throwaway probe package elsewhere, which vendored fine and advanced to graph construction. Projects that use this arrangement successfully avoid the collision by layout -- package at the repo root, manifests in a subdirectory -- where Stuff has both at the root. Only one escape route exists, and the item records why: the package cannot move into a subdirectory because SwiftPM rejects target paths outside the package root (verified with a minimal repro), so the Tuist manifests would have to move instead, rewriting every source glob plus ./ide, profile, CI, and the docs. Also records that the Where app can stay statically linked through all of this -- PackageSettings has a baseProductType, so products opt into .framework individually. Worth stating explicitly, because the change reads as "ship ten dylibs" and it does not have to be. Filed rather than acted on: the problems that motivated the investigation are already resolved, so this is a real but unurgent cleanup whose cost is a repo-layout move and should be its own decision.
kyleve
force-pushed
the
snapshot-target-env-pins
branch
from
July 27, 2026 04:46
f382558 to
7ecc260
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two small follow-ups from investigating how this repo's Tuist package integration differs from the arrangement Tuist actually intends.
1. Pin the snapshot environment on the targets, not only the scheme
SNAPSHOT_EXPECTED_*andTZwere set on theStuffSnapshotTestsscheme alone. But Tuist also autogenerates schemes for these targets —WhereUISnapshotTestsgets its own, and the Periscope/SwiftDataInspector snapshot bundles get grouped intoPeriscopeToolsandSwiftDataInspectorby name suffix — and none of those carried the pins.So running one image bundle from the Xcode UI ran it unpinned. That doesn't fail loudly: the runner's fail-fast simulator guard never fires, and TZ-dependent references (widget day labels, log-viewer timestamps) silently compare against images recorded in a different timezone.
SNAPSHOT_EXPECTEDentries beforeWhereUISnapshotTestsPeriscopeTools(grouped)SwiftDataInspector(grouped)StuffSnapshotTests(aggregate)Stuff-iOS-TestsValues are hoisted into one
snapshotEnvironmentconstant and set in both places. That's deliberate, not redundant: the target-level copy pins the autogenerated schemes, the scheme-level copy pins the CI invocation, and setting only one leaves the other quietly unpinned. Counts were measured against a from-scratch generate, since a stale scheme file could otherwise have faked the result.One side effect worth knowing: the grouped schemes now run their module's unit tests under the pinned
TZtoo, because Xcode scheme environment variables apply to the whole test action rather than per target. I ran thePeriscopeToolsscheme (unit + snapshot together) to confirm that's harmless.2. File the package-integration finding
Project.swiftusesPackage.local(path:), which emits anXCLocalSwiftPackageReferenceand hands the package to Xcode's SPM integration — everything static per consumer, Tuist never sees the targets,PackageSettingsinert (which is why an earlier attempt to setproductTypesdid nothing). The arrangement Tuist intends is to declare the local package as a dependency of aTuist/Package.swiftand consume products via.external(name:), so Tuist generates the targets and owns their product types.I prototyped it. The mechanism works; it's blocked on repo layout. Tuist can't vendor a local package whose directory is the project directory:
Confirmed that's the specific cause by pointing
Tuist/Package.swiftat a throwaway probe package elsewhere — it vendored fine and advanced to graph construction. Projects that use this arrangement successfully avoid the collision purely by layout: package at the repo root, Tuist manifests in a subdirectory. Stuff has both at the root.Only one escape route exists, and the filed item records why: the package can't move into a subdirectory, because SwiftPM rejects target paths outside the package root (
target 'Outside' in package 'pkg' is outside the package root, verified with a minimal repro). So the Tuist manifests would have to move, rewriting every source glob plus./ide,profile, CI, and the docs.It also records that the Where app can stay statically linked through all of this —
PackageSettingshas abaseProductType, so products opt into.frameworkindividually. Worth stating explicitly, since the change reads as "ship ten dylibs" and it doesn't have to be.Filed rather than acted on: the problems that motivated the investigation are already resolved, so this is real but unurgent cleanup whose cost is a repo-layout move and should be its own decision.
Verification
Full
StuffSnapshotTestsscheme green with zero reference images modified;PeriscopeToolsgrouped scheme green;Stuff-iOS-Testsunaffected;./swiftformat --lintclean. The prototype was fully reverted — noTuist/directory orPackage.swiftchange is included here.