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

Reading and Writing Package.swift

~15 min · packages-projects, swiftpm, manifest, modules

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"The manifest is Swift, but read it like a contract: what exists, for which platforms, built from what."

Why the Family Builds Mac Apps From a Manifest

Every Mac app in this family is a Swift package — no .xcodeproj anywhere on the desktop side. Package.swift is short, diffs cleanly, reviews like code, and builds the same way from a terminal, a script or a coding agent: swift build -c release --product Spark. The price is that the bundle, signing and install steps become your script's job, which is Track 6. For iOS and watch apps the family uses XcodeGen instead, because app targets need things a package cannot express — you will meet that in lesson four.

The Parts of a Manifest

  • The first line is load-bearing. // swift-tools-version: 6.2 selects which manifest API is available. It is a comment the tool parses; get it wrong and newer settings do not exist.
  • platforms sets deployment targets: the oldest macOS, iOS and watchOS the package promises to run on.
  • products are what the outside world can depend on: .library for code other packages import, .executable for a runnable tool or app binary.
  • targets are the modules: .target for Swift or C code, .executableTarget for a program, .testTarget for tests. Each lives in Sources/<Name> or Tests/<Name> unless you give a path.
  • dependencies name other packages by URL and version, or by local path.
  • swiftSettings / linkerSettings per target: language mode exceptions, upcoming features, linked system libraries and frameworks.
  • swiftLanguageModes: [.v6] at the end sets the package default. swift package init with the 6.3 toolchain already writes it.

The Module Split That Pays Off

Family Mac apps split into a pure core module with no AppKit import, the app module that composes AppKit and the core, and sometimes a module of shared kit code. The core builds and tests fast on any Mac, including one with only the Command Line Tools; the app module is thin. When the same app later grows an iPhone client, the core is already the thing the phone links.

Two Facts That Bite

A path dependency's identity is its directory name. When the family moved its shared package from a folder named swift/ to CwkKit/, every consumer that asked for package: "CwkKit" had failed until then with "unknown package 'CwkKit'; valid packages are: 'swift'". And after that rename, the package's own .build cache still remembered the old absolute path and failed with a bare error: fatalError — cleared by rm -rf .build. Read past fatalError; it is SwiftPM's wrapper and says nothing on its own.

Code

SparkKit: a core every platform can link, platform pieces behind it·swift
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "SparkKit",
    platforms: [.macOS(.v14), .iOS(.v17), .watchOS(.v10)],
    products: [
        .library(name: "SparkCore", targets: ["SparkCore"]),     // every platform, no UI imports
        .library(name: "SparkMac", targets: ["SparkMac"]),       // macOS pieces behind #if os(macOS)
        .library(name: "SparkPhone", targets: ["SparkPhone"]),   // UIKit pieces behind canImport
        .executable(name: "spark-selftest", targets: ["spark-selftest"]),
    ],
    targets: [
        .target(name: "SparkCore"),
        .target(name: "CSparkProc"),                                 // C shim, headers in include/
        .target(name: "SparkMac", dependencies: ["SparkCore", "CSparkProc"]),
        .target(name: "SparkPhone", dependencies: ["SparkCore"]),
        .executableTarget(name: "spark-selftest", dependencies: ["SparkCore"]),
        .testTarget(name: "SparkCoreTests", dependencies: ["SparkCore"]),
    ],
    swiftLanguageModes: [.v6]
)
The commands you will actually run·bash
swift package init --type executable --name Spark   # tools-version 6.3, swiftLanguageModes [.v6]
swift build                                          # debug, host platform
swift build -c release --product Spark               # the binary your bundle script copies
swift build -c release --show-bin-path               # where it landed
swift test                                           # host platform only (see lesson six)
swift package describe                               # what SwiftPM thinks the package is

External links

Exercise

Turn your Spark package into SparkKit using the manifest above: SparkCore with the Capture type, an empty SparkMac and SparkPhone, a spark-selftest executable and a SparkCoreTests target. Run swift build, swift run spark-selftest and swift test. Then rename the package directory, build again, and record the exact error you get and the command that clears it.
Hint
Create the folders Sources/SparkCore, Sources/SparkMac, Sources/SparkPhone, Sources/spark-selftest and Tests/SparkCoreTests, each with one Swift file, plus Sources/CSparkProc with an empty CSparkProc.c and an include/ folder, which the C lesson fills in. Without that folder the build stops at the manifest. After the rename, look past the first error: line for the module-cache path message.

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.