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

No Version, No Warning

~14 min · swift, packaging, compatibility, cost

Level 0Loose Parts
0 XP0/48 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Nothing to Publish, Everything to Build

The path package removed two things on purpose: a publish step and a version number. This track has argued that both were costs with no counterparty in this family, and that argument still holds. But a version number does one more job that is easy to forget until it is gone. It lets a consumer decide when to take a change. A pinned consumer can sit on last month's types until it is ready.

A path consumer cannot. It compiles the kit directory as it sits, so every change to a public type reaches every consumer at that consumer's next compile, whether that compile is a deliberate upgrade or an unrelated bug fix someone happened to start that afternoon. That is exactly the coherence the package was adopted for. It is also a bill, and the bill arrives at the consumer, not at the kit.

Within a month of the split, three different shapes of that bill had arrived, and none of them was visible to the kit's own suite.

Three Breaks Nobody's Suite Saw

  • A case added to a public enum. The kit's JSON value type gained a case for unsigned integers. The kit's tests passed, the build matrix passed, and every consumer that declares the package in its own package manifest built cleanly. A phone app built from a separate project file had a model with an exhaustive switch over that enum, and it stopped compiling at its next build. The repair was not only the switch. It was a rule: search every consumer for both spellings of a pattern match on that type, and build the project-file consumers too, not just the package-manifest ones.
  • A public type named like an app's own type. Inside an app, the app's own declaration shadows the kit's, so nothing complains. In that app's test target, which imports both the app and the kit, the two names collide and the tests stop compiling. The kit's Markdown model was therefore placed under one top-level namespace before it shipped, after a search of consumers' test folders found an app that already had a type with the flat name.
  • A product that brings a capability with it. The media product contains camera code. A phone app that links it — even for a different type in the same product — must declare why it uses the camera, or the store rejects the build by email after the release pipeline has already printed its success line. Three apps hit this on one day.

Each of these is a compatibility question that a published package would have forced someone to ask at publish time, with a changelog and a major-version bump. Without a version, nobody is forced to ask it. Somebody has to choose to.

A path dependency moves the compatibility check from the producer's publish step to every consumer's next build. That move is the point of the mechanism — no consumer can pin an older revision and sit on it, so every build compiles whatever the kit tree holds at that moment — and it is also its running cost. (Two consumers built an hour apart can still compile two different trees; what is gone is the option to stay behind on purpose.) Pay it deliberately: before a kit change is called done, build every kind of consumer, because each kind breaks differently.

The Growth Numbers, and What They Imply

In mid-September the package had nine products and fifty-three Swift files. A month later it has thirteen products and one hundred and nine files, roughly twenty-four and a half thousand lines, and its own suite has grown from about three hundred and seventy tests to roughly nine hundred. Products stay split by platform so that an app links only what its surfaces need — a phone composer does not link a web view to get a path fence.

The number that matters for the bill is not the product count. It is the number of consumer kinds: package-manifest consumers, project-file consumers, watch targets, test targets that import both an app and the kit. Each kind exposes a different class of break, and the kit's own suite is, by construction, none of them.

Code

One line in the kit, one red build somewhere else·swift
// In the kit (a path package). One case added:
public enum JSONValue: Codable, Sendable, Equatable {
    case null
    case bool(Bool)
    case integer(Int)
    case unsignedInteger(UInt64)   // NEW - the kit's suite is green
    case number(Double)
    case string(String)
    case array([JSONValue])
    case object([String: JSONValue])
}

// In a phone app's model, compiled from its own project file.
// Nothing in this file changed, and it no longer compiles:
func render(_ value: JSONValue) -> String {
    switch value {                 // error: switch must be exhaustive
    case .null:            return "null"
    case .bool(let b):     return b ? "true" : "false"
    case .integer(let i):  return String(i)
    case .number(let n):   return String(n)
    case .string(let s):   return s
    case .array, .object:  return "..."
    }
}

// The check that would have found it before the app did - run it
// across EVERY consumer, in both spellings of a case pattern:
//
//   grep -rnE 'case \.integer\(|case let \.integer' <consumers>
//
// and then build the project-file consumers, not only the ones that
// declare the package in a package manifest. A green package suite
// is evidence about the package.

External links

Exercise

If you consume a shared library by path or as a workspace member, list every KIND of consumer it has: package manifests, project files, test targets that import both an app and the library, extensions, watch or widget targets. For the library's last public change, write down which kinds were actually built before the change was called done. Every kind not on that list is where the next break will be found — by the consumer.
Hint
Exhaustive switches, test targets that import two modules, and capability declarations are the three places a path dependency's breaks tend to surface. Search for them by kind, not by repository.

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 in — Please sign in to comment.

No comments yet — be the first.