// swift-tools-version: 6.2 import Foundation import PackageDescription // Build channel, selected at manifest-evaluation time via the NUCLEIC_CHANNEL // environment variable (dev | canary | beta | rc | stable); defaults to `dev`. It drives two // things in lockstep: // • the app executable's PRODUCT name — which becomes the on-disk binary and // therefore the OS process name (Activity Monitor / `ps`): // dev -> nucleic-local canary -> nucleic-canary beta -> nucleic-beta // rc -> nucleic-rc stable -> nucleic // • a compile-time define (`NUCLEIC_STABLE/RC/BETA/CANARY/DEV`) read by NucleicApp for its build // identity + warning banner (red local / yellow canary / blue beta / gold release-candidate / // none stable), and by NucleicCore's `ContainerManager` to suffix non-release container names. // Branch mapping (see BUILD.md): dev builds from `dev`, canary from `canary`, beta from `staging`, // rc from `rc`, stable from `main`. // TailscaleKit (the embedded tsnet node behind the Tailnet sync transport) has no SwiftPM // distribution — it's an Xcode framework built from a pinned libtailscale commit by // scripts/build-tailscalekit.sh into third_party/TailscaleKit/ (untracked, ~100 MB). The // binary target is declared only when the artifact exists so a fresh clone builds without // Go/Xcode legwork; NucleicTailnet compiles either way (`#if canImport(TailscaleKit)`) and // reports "not built in" at runtime when the framework is absent. let tailscaleKitAvailable = FileManager.default.fileExists( atPath: Context.packageDirectory + "/third_party/TailscaleKit/TailscaleKit.xcframework") let nucleicChannel = (Context.environment["NUCLEIC_CHANNEL"] ?? "dev").lowercased() let (appProductName, channelDefine): (String, String) = { switch nucleicChannel { case "stable", "prod", "release": return ("nucleic", "NUCLEIC_STABLE") case "rc", "candidate": return ("nucleic-rc", "NUCLEIC_RC") case "beta": return ("nucleic-beta", "NUCLEIC_BETA") case "canary": return ("nucleic-canary", "NUCLEIC_CANARY") default: return ("nucleic-local", "NUCLEIC_DEV") } }() let package = Package( name: "Nucleic", // NucleicProtocol (the shared sync wire layer) must build for iOS too — that's what the // NucleicRemote iPhone client links. The host-only targets (NucleicCore/App/spike) use // macOS-only APIs and are simply never built for iOS. // // macOS floor is 26 (Tahoe): the container sandbox is built directly on Apple's // `containerization` framework, whose in-process vmnet networking requires macOS 26 + Apple // silicon. `containerization` is a dependency of NucleicCore ONLY (never of the iOS-linked // NucleicProtocol), so the iOS build resolves the package graph but never compiles it. // // iOS floor is 18.1: TailscaleKit ships with an 18.1 minimum (its `state()` methods use // the Failure-typed `any AsyncSequence` existential, an iOS 18 Swift-runtime feature), and // SwiftPM has no per-target floors, so the iOS-linked targets rise with it. Every device // that runs iOS 17 also runs 18.1, so this drops no hardware — only stale installs. platforms: [.macOS(.v26), .iOS("18.1")], products: [ .library(name: "NucleicProtocol", targets: ["NucleicProtocol"]), // The Tailnet transport (embedded tsnet node + fd-backed FrameChannel). A separate // product because the iPhone client links it directly (NucleicCore is host-only). .library(name: "NucleicTailnet", targets: ["NucleicTailnet"]), .library(name: "NucleicCore", targets: ["NucleicCore"]), // Product (binary / process) name varies by channel — see `appProductName`. .executable(name: appProductName, targets: ["NucleicApp"]), .executable(name: "nucleic-spike", targets: ["nucleic-spike"]), .executable(name: "fake-claude", targets: ["fake-claude"]), .executable(name: "fake-grok", targets: ["fake-grok"]), .executable(name: "container-spike", targets: ["container-spike"]), ], dependencies: [ .package(url: "https://github.com/groue/GRDB.swift.git", from: "7.0.0"), .package(url: "https://github.com/migueldeicaza/SwiftTerm.git", from: "1.2.0"), // Apple's containerization framework — the in-process runtime the `container` CLI is built // on. **Vendored** (third_party/containerization) at upstream commit 6b7b42ca rather than // pulled from github, because we carry a small patch: `LinuxContainer.Configuration` forwards // `vmExtensions` into `VMConfiguration.extensions` so we can attach a memory-balloon device // (see third_party/containerization/PATCHES.md). The package is pre-1.0 (source stability // holds only within a minor version); to bump, re-vendor the new commit and re-apply the // patch. All framework calls are centralized in `ContainerEngine`/`MemoryBalloon`. .package(path: "third_party/containerization"), // Sparkle — in-app auto-update for the directly-distributed (non-App-Store) macOS // builds. macOS-only and used ONLY by the NucleicApp executable target, so the iOS // resolution of the package graph never compiles it. Updates are delivered as signed, // notarized DMGs described by a per-channel appcast (see BUILD.md "Auto-update"). .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.6.0"), ], targets: [ // Pure, platform-agnostic: identifiers, the AgentEvent model, the sync wire // protocol (ClientMsg/HostMsg), CBOR framing, and the Noise SecureChannel. // No GRDB / Process / Network — compiles for macOS and iOS alike. .target(name: "NucleicProtocol"), // The Tailnet sync transport (SYNC_PROTOCOL §3): an embedded tsnet node // (TailscaleKit) plus the fd-backed FrameChannel both sides speak over it. Compiles // for macOS (host listener) and iOS (phone dialer) alike; when the TailscaleKit // artifact is absent the module still builds and `TailnetSupport.isBuiltIn` is false. .target( name: "NucleicTailnet", dependencies: ["NucleicProtocol"] + (tailscaleKitAvailable ? ["TailscaleKit"] : [])), .target( name: "NucleicCore", dependencies: [ "NucleicProtocol", "NucleicTailnet", .product(name: "GRDB", package: "GRDB.swift"), // In-process container runtime (host-only). EXT4 unpacking + vmnet networking are // reached transitively through `Containerization`; `ContainerizationOCI` provides // the `User`/`Platform` types used directly by `ContainerEngine`. .product(name: "Containerization", package: "containerization"), .product(name: "ContainerizationOCI", package: "containerization"), // For `AddressAllocator` — named in the `VZInstanceExtension.configureVZ` signature // our `MemoryBalloon` conforms to (it lives here, not in `Containerization`). .product(name: "ContainerizationExtras", package: "containerization"), ], // The build channel reaches the container-naming code here: `ContainerManager` appends a // per-channel suffix (e.g. `-beta`, `-local`) to non-release container names so a beta / // local-dev build's containers don't collide with a release's on the shared on-disk // container store (ContainerEngine.defaultStorageRoot). Same define NucleicApp's banner uses. swiftSettings: [.define(channelDefine)]), .executableTarget( name: "NucleicApp", dependencies: [ "NucleicCore", .product(name: "SwiftTerm", package: "SwiftTerm"), // Auto-update for direct (non-App-Store) distribution. macOS-only. .product(name: "Sparkle", package: "Sparkle"), ], swiftSettings: [.define(channelDefine)], // FoundationModels ships in macOS 26, but its Swift ABI is still shifting between the // 26/27 beta seeds: a symbol present in the SDK we build against (e.g. // `Generable.promptRepresentation`, emitted by the `@Generable` guided-generation code // in AppleIntelligence.swift) can be ABSENT from an older seed's framework. Strong-linked // (the Swift autolink default), that makes dyld abort BEFORE main() on any Mac whose seed // lacks the symbol — "Symbol not found … FoundationModels", EXC_CRASH / Namespace DYLD — // even though every use is already gated behind `#available` + `model.isAvailable`, since // those runtime guards never get to run. Weak-linking binds a missing symbol to NULL at // load instead of aborting, so the app always launches and the guards do their job (the // soft-AI features fall back to their heuristics). Verify the flag took with // `otool -l | grep -A3 FoundationModels` → expect LC_LOAD_WEAK_DYLIB. linkerSettings: [ .unsafeFlags(["-Xlinker", "-weak_framework", "-Xlinker", "FoundationModels"]), ], plugins: ["EmbedGitCommit"]), // Prebuild plugin: stamps the built-from commit into a generated `GitCommit.swift` // at build time, so the hash is baked into the binary (correct even on a // packaged beta / rc / stable archive, where git isn't present at runtime). .plugin(name: "EmbedGitCommit", capability: .buildTool()), .executableTarget(name: "nucleic-spike", dependencies: ["NucleicCore"]), .executableTarget(name: "fake-claude", dependencies: ["NucleicCore"]), .executableTarget(name: "fake-grok", dependencies: ["NucleicCore"]), // Standalone de-risking spike for the container rework: drives the real `ContainerEngine` // end-to-end — pulls an image from a registry, boots the VM, execs a process, and streams // its stdout through `ContainerizedProcessHandle` (the agent's path). Run signed with // signing/spike.entitlements; not shipped. .executableTarget( name: "container-spike", dependencies: [ "NucleicCore", .product(name: "Containerization", package: "containerization"), .product(name: "ContainerizationOCI", package: "containerization"), ]), .testTarget( name: "NucleicProtocolTests", dependencies: ["NucleicProtocol"] ), .testTarget( name: "NucleicCoreTests", dependencies: ["NucleicCore", "NucleicTailnet"], resources: [.copy("Fixtures")], // The TailscaleKit binary framework is staged at the build-products root, which // executables reach via their default rpath but .xctest bundles do not (their // rpaths point at PackageFrameworks + the bundle's own Frameworks dir). Walk the // three levels from Contents/MacOS/ back up to the products dir. linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@loader_path/../../.."])] ), ] + (tailscaleKitAvailable ? [.binaryTarget(name: "TailscaleKit", path: "third_party/TailscaleKit/TailscaleKit.xcframework")] : []) )