Created
March 9, 2026 02:28
-
-
Save jverkoey/6115f88ed45f1a0bac73252120d42526 to your computer and use it in GitHub Desktop.
Reproduction case for stale .swiftmodule files in swift-build (swbuild) - https://github.com/swiftlang/swift-build/issues/1171
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
| import Foundation | |
| #if canImport(MiddleLib) | |
| import MiddleLib | |
| #endif | |
| public struct AppGreeting: Sendable { | |
| #if canImport(MiddleLib) | |
| public static let message = MiddleHelper.name | |
| #else | |
| public static let message = "standalone" | |
| #endif | |
| } |
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
| { | |
| "action": "build", | |
| "configurationName": "Debug", | |
| "activeRunDestination": { | |
| "platform": "watchsimulator", | |
| "sdk": "watchsimulator", | |
| "targetArchitecture": "arm64", | |
| "supportedArchitectures": ["arm64"], | |
| "disableOnlyActiveArch": false | |
| }, | |
| "overrides": { | |
| "synthesized": { | |
| "table": { | |
| "CODE_SIGN_IDENTITY": "", | |
| "CODE_SIGNING_ALLOWED": "NO", | |
| "DEBUG_INFORMATION_FORMAT": "dwarf", | |
| "ONLY_ACTIVE_ARCH": "YES" | |
| } | |
| } | |
| } | |
| } |
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
| public enum LeafValue: Sendable { | |
| public static let number = 42 | |
| } |
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
| import LeafLib | |
| public struct MiddleHelper: Sendable { | |
| public static let name = "middle-\(LeafValue.number)" | |
| } |
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
| // swift-tools-version: 6.0 | |
| import PackageDescription | |
| let package = Package( | |
| name: "StaleModuleRepro", | |
| platforms: [.iOS(.v18), .watchOS(.v11)], | |
| products: [.library(name: "AppLib", targets: ["AppLib"])], | |
| targets: [ | |
| .target(name: "AppLib", dependencies: ["MiddleLib"]), | |
| .target(name: "MiddleLib", dependencies: ["LeafLib"]), | |
| .target(name: "LeafLib"), | |
| ] | |
| ) |
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
| #!/bin/bash | |
| # reproduce.sh — Reproduce stale .swiftmodule bug in swift-build (swbuild) | |
| # | |
| # This script demonstrates that swbuild leaves stale .swiftmodule files in | |
| # Build/Products/ across build descriptions, causing compilation failures | |
| # when the Swift compiler loads them from the module search path. | |
| # | |
| # Requirements: | |
| # - macOS with Xcode installed (needs watchOS simulator SDK) | |
| # - Internet access (clones swift-build on first run) | |
| # - ~5 minutes for initial swift-build compilation | |
| # | |
| # Usage: | |
| # bash reproduce.sh | |
| set -euo pipefail | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| SWBUILD_DIR="$SCRIPT_DIR/swift-build" | |
| SWBUILD="$SWBUILD_DIR/.build/release/swbuild" | |
| DD="$SCRIPT_DIR/DerivedData" | |
| # Upstream swbuild puts Products at DerivedData/Products/ (no Build/ prefix) | |
| PRODUCTS="$DD/Products/Debug-watchsimulator" | |
| PKGDIR="$SCRIPT_DIR/StaleModuleRepro" | |
| PARAMS="$SCRIPT_DIR/build-params.json" | |
| # ─── build swbuild if needed ──────────────────────────────────────────── | |
| if [[ ! -x "$SWBUILD" ]]; then | |
| echo "Building swbuild (one-time setup, takes a few minutes)..." | |
| echo "" | |
| if [[ ! -d "$SWBUILD_DIR" ]]; then | |
| git clone --depth 1 https://github.com/swiftlang/swift-build.git "$SWBUILD_DIR" | |
| fi | |
| # Build swbuild and its companion SWBBuildServiceBundle | |
| cd "$SWBUILD_DIR" | |
| swift build -c release --product SWBBuildServiceBundle 2>&1 | tail -3 | |
| swift build -c release --product swbuild 2>&1 | tail -3 | |
| cd "$SCRIPT_DIR" | |
| if [[ ! -x "$SWBUILD" ]]; then | |
| echo "ERROR: Failed to build swbuild" >&2 | |
| exit 1 | |
| fi | |
| echo "" | |
| fi | |
| echo "Using swbuild: $SWBUILD" | |
| # ─── clean prior runs ────────────────────────────────────────────────── | |
| rm -rf "$DD" | |
| # ─── Package.swift v1: all targets for all platforms ──────────────────── | |
| cat > "$PKGDIR/Package.swift" <<'MANIFEST' | |
| // swift-tools-version: 6.0 | |
| import PackageDescription | |
| let package = Package( | |
| name: "StaleModuleRepro", | |
| platforms: [.iOS(.v18), .watchOS(.v11)], | |
| products: [.library(name: "AppLib", targets: ["AppLib"])], | |
| targets: [ | |
| .target(name: "AppLib", dependencies: ["MiddleLib"]), | |
| .target(name: "MiddleLib", dependencies: ["LeafLib"]), | |
| .target(name: "LeafLib"), | |
| ] | |
| ) | |
| MANIFEST | |
| # ─── Build 1: everything builds for watchOS ───────────────────────────── | |
| echo "" | |
| echo "═══════════════════════════════════════════════════════════════════" | |
| echo " BUILD 1: All targets build for watchOS" | |
| echo "═══════════════════════════════════════════════════════════════════" | |
| echo "" | |
| echo "Package graph: AppLib -> MiddleLib -> LeafLib (all platforms)" | |
| echo "" | |
| "$SWBUILD" build "$PKGDIR" \ | |
| --allTargets \ | |
| --configuration Debug \ | |
| --derivedDataPath "$DD" \ | |
| --buildParametersFile "$PARAMS" \ | |
| 2>/dev/null >/dev/null | |
| echo "Build 1 succeeded." | |
| echo "" | |
| echo "Modules in Products/Debug-watchsimulator/:" | |
| for m in "$PRODUCTS"/*.swiftmodule; do | |
| [[ -d "$m" ]] && echo " $(basename "$m")" | |
| done | |
| # Verify the dependency chain is embedded in module metadata | |
| if strings "$PRODUCTS/MiddleLib.swiftmodule/arm64-apple-watchos-simulator.swiftmodule" 2>/dev/null | grep -q "LeafLib"; then | |
| echo "" | |
| echo "MiddleLib.swiftmodule metadata records LeafLib as a dependency." | |
| fi | |
| # ─── simulate stale state ────────────────────────────────────────────── | |
| echo "" | |
| echo "═══════════════════════════════════════════════════════════════════" | |
| echo " SIMULATE STALE STATE" | |
| echo "═══════════════════════════════════════════════════════════════════" | |
| echo "" | |
| echo "Deleting LeafLib.swiftmodule to create the inconsistent state that" | |
| echo "occurs when multiple builds with different PIFs share derived data." | |
| echo "" | |
| echo "In production, the missing module occurs because:" | |
| echo " - A prior build's SFR cleaned it (within its build description)" | |
| echo " - It was never built for this platform" | |
| echo " - A partial build left incomplete state" | |
| rm -rf "$PRODUCTS/LeafLib.swiftmodule" | |
| echo "" | |
| echo "State:" | |
| echo " MiddleLib.swiftmodule -- STALE (references LeafLib)" | |
| echo " LeafLib.swiftmodule -- DELETED" | |
| # ─── Package.swift v2: targets removed from package ───────────────────── | |
| echo "" | |
| echo "═══════════════════════════════════════════════════════════════════" | |
| echo " BUILD 2: MiddleLib + LeafLib removed from package graph" | |
| echo "═══════════════════════════════════════════════════════════════════" | |
| cat > "$PKGDIR/Package.swift" <<'MANIFEST' | |
| // swift-tools-version: 6.0 | |
| import PackageDescription | |
| let package = Package( | |
| name: "StaleModuleRepro", | |
| platforms: [.iOS(.v18), .watchOS(.v11)], | |
| products: [.library(name: "AppLib", targets: ["AppLib"])], | |
| targets: [ | |
| .target(name: "AppLib"), | |
| ] | |
| ) | |
| MANIFEST | |
| echo "" | |
| echo "Package graph now: AppLib only (no MiddleLib, no LeafLib)." | |
| echo "On a clean build, #if canImport(MiddleLib) -> false -> compiles fine." | |
| echo "But stale MiddleLib.swiftmodule is in the compiler's -I search path..." | |
| echo "" | |
| set +e | |
| "$SWBUILD" build "$PKGDIR" \ | |
| --allTargets \ | |
| --configuration Debug \ | |
| --derivedDataPath "$DD" \ | |
| --buildParametersFile "$PARAMS" \ | |
| 2>"$SCRIPT_DIR/build2.err" >/dev/null | |
| RC=$? | |
| set -e | |
| # ─── results ──────────────────────────────────────────────────────────── | |
| echo "" | |
| echo "===================================================================" | |
| if [[ $RC -ne 0 ]]; then | |
| echo " BUG REPRODUCED: Build failed due to stale .swiftmodule" | |
| echo "===================================================================" | |
| echo "" | |
| cat "$SCRIPT_DIR/build2.err" | sed 's/^/ /' | |
| echo "" | |
| echo "The compiler found stale MiddleLib.swiftmodule in the search path" | |
| echo "(Build/Products/Debug-watchsimulator/), loaded it because" | |
| echo "#if canImport(MiddleLib) evaluated to true, then chased its" | |
| echo "dependency chain to LeafLib -- which doesn't exist." | |
| echo "" | |
| echo "On a clean build (without stale modules), this succeeds because" | |
| echo "#if canImport(MiddleLib) correctly evaluates to false." | |
| # Prove clean build works | |
| echo "" | |
| echo "===================================================================" | |
| echo " CONTROL: Removing stale MiddleLib.swiftmodule and rebuilding..." | |
| echo "===================================================================" | |
| rm -rf "$PRODUCTS/MiddleLib.swiftmodule" | |
| set +e | |
| "$SWBUILD" build "$PKGDIR" \ | |
| --allTargets \ | |
| --configuration Debug \ | |
| --derivedDataPath "$DD" \ | |
| --buildParametersFile "$PARAMS" \ | |
| 2>/dev/null >/dev/null | |
| CLEAN_RC=$? | |
| set -e | |
| echo "" | |
| if [[ $CLEAN_RC -eq 0 ]]; then | |
| echo "Control build SUCCEEDED -- stale modules are the sole cause." | |
| else | |
| echo "Control build also failed (unexpected)." | |
| fi | |
| else | |
| echo " Build succeeded -- bug NOT reproduced." | |
| echo "===================================================================" | |
| fi | |
| # Restore Package.swift to v1 for re-runs | |
| cat > "$PKGDIR/Package.swift" <<'MANIFEST' | |
| // swift-tools-version: 6.0 | |
| import PackageDescription | |
| let package = Package( | |
| name: "StaleModuleRepro", | |
| platforms: [.iOS(.v18), .watchOS(.v11)], | |
| products: [.library(name: "AppLib", targets: ["AppLib"])], | |
| targets: [ | |
| .target(name: "AppLib", dependencies: ["MiddleLib"]), | |
| .target(name: "MiddleLib", dependencies: ["LeafLib"]), | |
| .target(name: "LeafLib"), | |
| ] | |
| ) | |
| MANIFEST | |
| rm -f "$SCRIPT_DIR/build2.err" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment