Skip to content

Compile auto-generated Swift sources (Bundle.module, asset symbols) in Tier 2#131

Merged
obj-p merged 7 commits intomainfrom
fix/generated-sources
Apr 21, 2026
Merged

Compile auto-generated Swift sources (Bundle.module, asset symbols) in Tier 2#131
obj-p merged 7 commits intomainfrom
fix/generated-sources

Conversation

@obj-p
Copy link
Copy Markdown
Owner

@obj-p obj-p commented Apr 20, 2026

Summary

Three fixes around SPM/Xcode build-system integration:

  1. SPM: walk <binPath>/<Target>.build/DerivedSources/*.swift and append to the compile set. Surfaces resource_bundle_accessor.swift so previews using Bundle.module build.
  2. Xcode: walk <DERIVED_FILE_DIR>/DerivedSources/*.swift and append. Surfaces GeneratedAssetSymbols.swift, GeneratedStringSymbols.swift, etc. so previews using Color(.brandPrimary) / Image.foo build.
  3. SPM build-state recovery: auto-retry swift build after cleaning .build/<triple>/ + .build/build.db when llbuild reports "command X not registered" (a known incremental-state bug across cross-process / cross-triple invocations).

Bazel is documented-N/A — rules_swift.swift_library does not synthesize a Bundle.module equivalent. Comment records known adjacent gaps (swift_proto_library, swift_grpc_library, consumer-macro outputs).

The first two are pure static helpers plus a single-line call site in build(). No change to BuildContext or Compiler — the existing sourceFiles: [URL]?Compiler.compileCombined(additionalSourceFiles:) pipeline already carries arbitrary URLs.

Failing-before repros

// examples/spm/Sources/ToDo/ToDoView.swift
value: items.first { !$0.isComplete }?.title
    ?? Bundle.module.localizedString(forKey: "todo.empty.title", value: "All done!", table: nil),
$ previewsmcp snapshot examples/spm/Sources/ToDo/ToDoView.swift -o /tmp/out.png --project examples/spm
# Before: error: type 'Bundle' has no member 'module'
# After: /tmp/out.png
// examples/xcodeproj/Sources/ToDo/ToDoView.swift
color: Color(.brandPrimary)   // symbol from Assets.xcassets/BrandPrimary.colorset
$ (cd examples/xcodeproj && xcodegen generate) \
  && previewsmcp snapshot examples/xcodeproj/Sources/ToDo/ToDoView.swift -o /tmp/out.png --project examples/xcodeproj
# Before: error: type 'Color' has no member 'brandPrimary'
# After: /tmp/out.png
# Cross-invocation flake (reproduces on main; fixed by SPMBuildRecovery)
$ rm -rf examples/spm/.build examples/PreviewSetup/.build && previewsmcp kill-daemon
$ swift test --filter PreviewsCoreTests        # 0 failures
$ swift test --filter CLIIntegrationTests
# Before: 99 failures cascading from "command X not registered"
# After:  0 failures

Example updates

  • examples/spm/ declares resources: [.process("Resources")] and ships Localizable.xcstrings; ToDoView reads Bundle.module.localizedString(...).
  • examples/xcodeproj/ and examples/xcworkspace/ ship Assets.xcassets/BrandPrimary.colorset, set ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: YES explicitly (the library-target default isn't reliable across XcodeGen versions), and ToDoView uses Color(.brandPrimary).
  • XcodeGen version used locally: 2.44.1. pbxproj is gitignored per repo convention; CI regenerates via xcodegen generate (the existing snapshotXcodeproj test already does this).

Regression coverage

Existing integration tests now transparently exercise the generated-source paths because the examples they drive use Bundle.module / Color(.brandPrimary):

  • SnapshotCommandTests.basicMacOSSnapshot (SPM)
  • SnapshotCommandTests.snapshotXcodeproj (Xcode project)
  • SnapshotCommandTests.snapshotXcworkspace (Xcode workspace)

New unit tests for the helpers (no swift build / xcodebuild required):

  • BuildSystemTests.collectGeneratedSources_spm_findsResourceAccessor / _emptyWhenMissing
  • BuildSystemTests.collectGeneratedSources_xcode_findsDerivedSwift / _emptyWhenMissing
  • SPMBuildRecoveryTests.parseStaleTripleDirectory* (4 cases)

Test plan

  • swift test --filter 'BuildSystemTests' — 57 tests pass
  • swift test --filter 'SPMBuildRecoveryTests' — 4 tests pass
  • swift test --filter 'SnapshotCommandTests.basicMacOSSnapshot' passes after fix
  • swift test --filter 'SnapshotCommandTests.snapshotXcodeproj' passes
  • swift test --filter 'SnapshotCommandTests.snapshotXcworkspace' passes
  • swift test --filter PreviewsCoreTests && swift test --filter CLIIntegrationTests — 0 failures (was 99 before SPMBuildRecovery)
  • Manual: previewsmcp snapshot against both examples produces valid PNGs

Known adjacent issues (not fixed here)

  • The thunk generator transforms string literals into DesignTimeStore.shared.string(...) returning String, which breaks String(localized: "...", bundle: .module) (requires String.LocalizationValue). The example uses Bundle.module.localizedString(forKey:value:table:) instead. Fixing the thunk generator is out of scope for this PR.

🤖 Generated with Claude Code

obj-p and others added 7 commits April 20, 2026 11:59
SPM emits resource_bundle_accessor.swift (defining Bundle.module) under
<binPath>/<Target>.build/DerivedSources/ when a target declares
.process/.copy resources. SPMBuildSystem.collectSourceFiles only walked
Sources/<Target>/, so previews that reference Bundle.module failed to
compile with "type 'Bundle' has no member 'module'".

Add a pure static helper, collectGeneratedSources(binPath:targetName:),
and union its results into BuildContext.sourceFiles after swift build.
Shallow glob with no filename whitelist — SPM may rename/add generated
files across Swift versions.

Example coverage: examples/spm/Sources/ToDo now declares a resource
bundle (Localizable.xcstrings) and ToDoView uses Bundle.module, so
existing basicMacOSSnapshot test fails at compile time if this fix
regresses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xcodebuild emits GeneratedAssetSymbols.swift (and GeneratedStringSymbols,
GeneratedPlistSymbols, Core Data NSManagedObject subclasses, etc.) into
<DERIVED_FILE_DIR>/DerivedSources/. OutputFileMap.json — which
XcodeBuildSystem.collectSourceFiles reads — only lists the swift-driver's
input files, not these generated ones, so previews referencing
Color(.brandPrimary) / Image.foo asset-symbol extensions failed to
compile.

Add a pure static helper,
XcodeBuildSystem.collectGeneratedSources(derivedFileDir:), and union its
results into BuildContext.sourceFiles alongside the OutputFileMap
sources. Shallow glob with no filename whitelist — Xcode's generator set
changes across releases.

Examples: xcodeproj and xcworkspace now ship an Assets.xcassets with a
BrandPrimary color, explicitly set
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES (the
framework-target default isn't reliable across XcodeGen versions), and
ToDoView uses Color(.brandPrimary). Existing snapshotXcodeproj /
snapshotXcworkspace tests fail at compile time if this fix regresses.

xcodegen version: 2.44.1 (pbxproj is gitignored; CI regenerates on demand).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rules_swift's swift_library does not synthesize a Bundle.module accessor
the way SPM does, so there is no analogous file for BazelBuildSystem to
pick up. Record the decision and the known gaps (swift_proto_library,
swift_grpc_library, consumer-macro outputs) where future work would extend
this method.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
llbuild's task database (.build/build.db) is shared across triples and
can become inconsistent when the same package is built for multiple
triples in different processes — typically:

  $ swift test --filter PreviewsCoreTests       # builds example for macOS
  $ swift test --filter CLIIntegrationTests     # also builds for iOS

The second invocation hits "command X not registered" errors as llbuild
tries to use a task graph node whose registration was overwritten by
the previous build. Reproduces deterministically when the two test
suites run as separate invocations; ~99 cascading failures.

Add SPMBuildRecovery.runSwift(arguments:workingDirectory:) which wraps
swift build, parses the offending path out of stderr on failure, removes
the affected .build/<triple>/ AND the shared .build/build.db, and
retries once. SPMBuildSystem and SetupBuilder both route through it.

Verified: PreviewsCoreTests then CLIIntegrationTests as separate
invocations now passes all 271 tests (was 99 failures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the cleanup step into cleanStaleArtifacts(tripleDir:) and add two
tests: one that verifies both <triple>/ and the sibling build.db are
removed (and a peer triple dir is preserved), and one that confirms the
helper tolerates missing paths. Closes the only meaningful coverage gap
in the recovery flow — the parser was already tested, the runSwift()
composition is now trivial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recovery's cleanup deletes both .build/<triple>/ and the shared
.build/build.db. The parser previously walked up to whatever directory
sat under .build/, so a future SPM error mentioning a path under
something like .build/workspace-state/ would have caused us to nuke
that directory plus the database — well outside the bug's scope.

Add looksLikeTriple() and require the resolved directory to match the
3+ hyphen-separated lowercase-token shape (arm64-apple-macosx,
arm64-apple-ios-simulator, x86_64-unknown-linux-gnu). Negative test
locks in the workspace-state case.

Also adds a nit-test verifying SPMBuildSystem.collectGeneratedSources
filters by .swift extension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@obj-p obj-p enabled auto-merge (squash) April 21, 2026 00:25
@obj-p obj-p merged commit 8eab984 into main Apr 21, 2026
4 checks passed
@obj-p obj-p deleted the fix/generated-sources branch April 21, 2026 00:39
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