Skip to content

Pin the snapshot environment on the targets, and file the package-integration finding#147

Merged
kyleve merged 2 commits into
mainfrom
snapshot-target-env-pins
Jul 27, 2026
Merged

Pin the snapshot environment on the targets, and file the package-integration finding#147
kyleve merged 2 commits into
mainfrom
snapshot-target-env-pins

Conversation

@kyleve

@kyleve kyleve commented Jul 27, 2026

Copy link
Copy Markdown
Owner

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_* and TZ were set on the StuffSnapshotTests scheme alone. But Tuist also autogenerates schemes for these targets — WhereUISnapshotTests gets its own, and the Periscope/SwiftDataInspector snapshot bundles get grouped into PeriscopeTools and SwiftDataInspector by 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.

Generated scheme SNAPSHOT_EXPECTED entries before after
WhereUISnapshotTests 0 3
PeriscopeTools (grouped) 0 3
SwiftDataInspector (grouped) 0 3
StuffSnapshotTests (aggregate) 3 3
Stuff-iOS-Tests 0 0

Values are hoisted into one snapshotEnvironment constant 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 TZ too, because Xcode scheme environment variables apply to the whole test action rather than per target. I ran the PeriscopeTools scheme (unit + snapshot together) to confirm that's harmless.

2. File the package-integration finding

Project.swift uses Package.local(path:), which emits an XCLocalSwiftPackageReference and hands the package to Xcode's SPM integration — everything static per consumer, Tuist never sees the targets, PackageSettings inert (which is why an earlier attempt to set productTypes did nothing). The arrangement Tuist intends is to declare the local package as a dependency of a Tuist/Package.swift and 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:

Fatal error: Duplicate values for key: '/Users/kve/Development/Stuff4'

Confirmed that's the specific cause by pointing Tuist/Package.swift at 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 — PackageSettings has a baseProductType, so products opt into .framework individually. 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 StuffSnapshotTests scheme green with zero reference images modified; PeriscopeTools grouped scheme green; Stuff-iOS-Tests unaffected; ./swiftformat --lint clean. The prototype was fully reverted — no Tuist/ directory or Package.swift change is included here.

kyleve added 2 commits July 26, 2026 21:45
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
kyleve force-pushed the snapshot-target-env-pins branch from f382558 to 7ecc260 Compare July 27, 2026 04:46
@kyleve
kyleve changed the base branch from split-snapshot-targets to main July 27, 2026 04:46
@kyleve kyleve closed this Jul 27, 2026
@kyleve kyleve reopened this Jul 27, 2026
@kyleve
kyleve merged commit a5a8512 into main Jul 27, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant