---
title: "Add the package, DroppyKit"
description: "Add DroppyKit to your droplet with Swift Package Manager."
canonical: "https://getdroppy.app/docs/droppykit/add-spm"
last-updated: "2026-09-09"
---

# Add the package

Add DroppyKit to your droplet with Swift Package Manager.

A droplet is a Swift package that builds a **dynamic library** and links DroppyKit. Droppy loads the built bundle into its own process, so the library type matters: a static library has nothing for the loader to open.

### Requirements

- **macOS**: 14.0 or later
- **Swift**: 6.2 or later (Xcode 26)
- **Droppy**: 15.3.0 or later

### The droppykit tool

`droppykit new`, `run`, `build`, `validate`, `submit`, `docs`, `agent`, `mcp`, `version` and `update` live in the SDK repository's `Scripts` folder, not in the package your droplet depends on: SwiftPM checks that out for the compiler, not for you. Clone the repository once and put the folder on your PATH.

```
git clone https://gitlab.com/droppyformac1/droppykit.git
export PATH="$PWD/droppykit/Scripts:$PATH"
droppykit
```

Add the `export` line to your shell profile to keep it. Running `droppykit` with no command prints the list above.

```
droppykit version    # which SDK this is, which one the PATH finds, what a droplet pins
droppykit update     # pull the newest SDK; inside a droplet, move its pin too
droppykit update --raise   # and require the new SDK: Package.swift from: and kit.minAPI
```

Two SDK versions are in play for every droplet: the checkout whose scripts build the bundle, and the tag the droplet's `Package.swift` pins, which is what its compile and its harness use. They drift apart when the checkout is pulled and the pin is not; `droppykit update` run inside the droplet moves both, and `droppykit version` shows both.

`droppykit update` does not raise what the droplet declares it needs, `from:` in `Package.swift` and `kit.minAPI` in `droplet.json`: a droplet that uses nothing new keeps loading in older Droppys. When it starts using something the new release added, `droppykit update --raise` requires that release in both. `droppykit build` checks the other direction: it reads which DroppyKit API the built droplet calls and fails when `kit.minAPI` is older than the newest of them, naming those calls and the version to declare. Declared too low, a droplet passes the handshake on an older Droppy and then fails to load there with "Symbol not found".

### In Package.swift

```
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: "WorldClock",
    platforms: [.macOS(.v14)],
    products: [
        .library(name: "WorldClock", type: .dynamic, targets: ["WorldClock"])
    ],
    dependencies: [
        .package(url: "https://gitlab.com/droppyformac1/droppykit.git", from: "1.6.0")
    ],
    targets: [
        .target(
            name: "WorldClock",
            dependencies: [.product(name: "DroppyKit", package: "droppykit")]
        ),
        .executableTarget(
            name: "WorldClockHarness",
            dependencies: [
                "WorldClock",
                .product(name: "DroppyKitHarness", package: "droppykit")
            ]
        )
    ]
)
```

The second target is the development harness. It is not shipped in your droplet — see [The harness](https://getdroppy.app/docs/droppykit/harness).

### In Xcode

File ▸ Add Package Dependencies, then paste `https://gitlab.com/droppyformac1/droppykit.git`. Add **DroppyKit** to your droplet target and **DroppyKitHarness** to the harness target.

### Do not build with a bare `swift build`

**Important.** Always build the shipping bundle with `Scripts/build-droplet.sh`.

A shipped droplet must contain its own code and **not** a copy of DroppyKit. Droppy already has DroppyKit as a resilient framework; a droplet carrying a second copy gives every shared type two metadata records, and the cast the loader makes between Droppy's [`Droplet`](https://getdroppy.app/docs/droppykit/api/droplet) and yours fails with no useful diagnostic. A plain `swift build` produces exactly that second copy, because SwiftPM folds the dependency in statically.

`droppykit build` does the link itself: it builds DroppyKit resiliently, wraps it in a framework whose install name is the one Droppy ships, and links your target's object file against that. The bundle then carries an undefined reference Droppy resolves at load.

It also compiles with `-enable-library-evolution`. Without it a droplet emits direct-access imports the resilient framework does not export, and the bundle passes every structural check, loads in the harness, and dies inside Droppy at dyld with `Symbol not found`.

`Scripts/validate-droplet.sh` refuses a bundle carrying either wrong shape, so a mistake here is caught before submission rather than after install.

### Checking the link

```
# 0 in a correct bundle, non-zero in a broken one.
nm -u WorldClock.droplet/Contents/MacOS/WorldClock | grep -c '9DroppyKit.*vau'

# Must name the framework, and must not name libDroppyKit.
otool -L WorldClock.droplet/Contents/MacOS/WorldClock | grep DroppyKit
```

## See also

[Your first dropletFrom an empty folder to a droplet running in the harness.](https://getdroppy.app/docs/droppykit/first-droplet) [Build with a coding agentClaude Code, Codex and Cursor: the brief, the MCP server, and pictures an agent can look at.](https://getdroppy.app/docs/droppykit/coding-agents) [The harnessDroppy's real Settings panel, running your droplet, with nothing installed.](https://getdroppy.app/docs/droppykit/harness)
