Skip to content
C.W.K.
Stream
Lesson 03 of 06 · published

A Path Dependency Compiles Someone Else's Working Tree

~16 min · packages-projects, swiftpm, path-dependency, stale-builds, diagnosis

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"The error named a file this repo does not own, has never edited, and whose last commit was hours old."

Why the Family Uses Them Anyway

The family's shared Swift lives in one package that more than a dozen apps consume with .package(path: "../SharedKit"). A path dependency means a change to the kit shows up in an app on the very next build — no tag, no version bump, no publishing step. For an audience of two people and a fleet of apps moving together, that immediacy is the point. It also comes with four failure shapes that look like something else entirely.

1. Another Session's Half-Finished Edit Breaks Your Build

A path dependency resolves to the working tree, not to a commit. A TestFlight upload died at its test step with cannot find 'decode' in scope — inside a kit file the app had never touched. A parallel session was mid-edit in the kit, and its unsaved-to-git change compiled straight into the app's build. The diagnosis takes one second: git -C ../SharedKit status --porcelain shows the file dirty. The correct response is to wait for the kit to compile and retry — never to fix or stash someone else's working tree.

2. An Archive Ships Uncommitted Kit Code

Worse than a broken build is a successful one. Between an app's test run and its archive, a kit session edited two files and had not finished a third. The archive tried to compile a kit that no commit contained, and failed only because that third file was unfinished. Had the edit been complete, the archive would have succeeded, and its build number would have named a binary nobody could reproduce. The family's upload pipeline now refuses to archive for delivery while the kit's sources differ from its HEAD, and stamps the kit commit into the build record.

3. A Consumer's Build Goes Stale and Stays Stale

A kit file gained a new type. The kit's own tests passed. A consumer app failed with "cannot find 'ProductManifest' in scope" on two builds in a row, while importing the same module successfully for six other types. The consumer's built module was older than the new source file. Do not read Package.swift first — compare two timestamps. If the consumer's module predates the kit source, it is staleness, and rm -rf .build in the consumer is the whole answer. Deleting only part of .build fails differently, because the build plan still names what you removed.

4. A Source-Compatible Change Breaks the Link

Adding a defaulted parameter to a public initializer is source-compatible — every call site still compiles. But default arguments are filled in at the call site, so the function's mangled symbol changes. Two consumers that construct the type then failed at link time with "Undefined symbols … PippaEndpoint.init(mode:host:port:)" — a symbol whose signature matched their own source exactly. They kept stale object files. swift package clean in each consumer fixed it with no source change. A clean two-package reproduction on Swift 6.3.3 recompiled the consumer and linked fine, so this is stale build state, not something every such change triggers. The tell: a compile error names a file and a line; this one names a mangled symbol and an object file. Do not revert the parameter to make it go away.

Code

Diagnose before you debug: dirty kit, or stale consumer?·bash
KIT=../SharedKit
APP=.

# 1. Is the kit's working tree dirty right now? (another session may be mid-edit)
git -C "$KIT" status --porcelain -- Sources Package.swift

# 2. Is the consumer's built module older than the kit source you just changed?
ls -lT "$APP"/.build/*/debug/Modules/SharedKitCore.swiftmodule
ls -lT "$KIT"/Sources/SharedKitCore/ProductManifest.swift

# Module older than source  -> stale consumer: clean the CONSUMER, whole .build or nothing
rm -rf "$APP/.build" && swift build

# Link error naming a symbol that matches your own source -> stale objects, not a signature break
swift package clean && swift build

External links

Exercise

Make a second package, SparkApp, that depends on your SparkKit by path. Reproduce two failures on purpose: (1) add a syntax error to a SparkKit file without committing and build SparkApp — note whose file the error names; (2) add a defaulted parameter to a public initializer in SparkKit that SparkApp calls, build SparkApp without cleaning, and note whether SwiftPM recompiled the consumer or left a link error. Record the command that fixes the first failure, the one you would run if the second ever fails at link time, and the check that would have told you the cause first.
Hint
Initialize SparkKit as a git repository so git status --porcelain can show the dirty file. A current SwiftPM usually recompiles the consumer after an interface change, and a clean two-package run on Swift 6.3.3 built and ran without a link error. The failure comes from build state that outlived the change, so a green run here is a result worth writing down, not a failed exercise.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.