Skip to content

Instantly share code, notes, and snippets.

@jverkoey
Created March 9, 2026 02:28
Show Gist options
  • Select an option

  • Save jverkoey/6115f88ed45f1a0bac73252120d42526 to your computer and use it in GitHub Desktop.

Select an option

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
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
}
{
"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"
}
}
}
}
public enum LeafValue: Sendable {
public static let number = 42
}
import LeafLib
public struct MiddleHelper: Sendable {
public static let name = "middle-\(LeafValue.number)"
}
// 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"),
]
)
#!/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