Skip to content

Commit 6629cd0

Browse files
committed
feat(spm): add multi-module Package.swift auto-generation
Add a new root-level plugin `co.touchlab.kmmbridge.spm` that automatically generates Package.swift for multi-module KMP projects. ## New Features - **spmDevBuildAll**: Builds all XCFrameworks locally and generates Package.swift with local paths for development - **kmmBridgePublishAll**: Publishes all modules and generates Package.swift with URLs for distribution - **generatePackageSwift**: Generates Package.swift from published metadata ## Changes - Add `KmmBridgeSpmPlugin` for root-level SPM management - Add `KmmBridgeSpmExtension` for configuration options - Add `SpmModuleMetadata` for JSON metadata exchange between modules - Add `writeSpmMetadata` task to each module for metadata generation - Disable module-level `spmDevBuild` when root SPM plugin is applied - Add comprehensive documentation in docs/SPM_MULTI_MODULE.md ## Benefits - No manual Package.swift editing required - No need for `useCustomPackageFile` or `perModuleVariablesBlock` flags - Unified workflow for both local development and CI publishing - Automatic platform version resolution (takes maximum) - Automatic Swift tools version resolution
1 parent a25f1bc commit 6629cd0

File tree

8 files changed

+1314
-1
lines changed

8 files changed

+1314
-1
lines changed

docs/SPM_MULTI_MODULE.md

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
# Multi-Module SPM Support
2+
3+
This guide explains how to use KMMBridge to publish multiple KMP modules as a single SPM package with automatic Package.swift generation.
4+
5+
## Overview
6+
7+
When you have multiple KMP darwin modules (e.g., `module-a-darwin`, `module-b-darwin`), KMMBridge can automatically:
8+
9+
1. Discover all modules with KMMBridge configured
10+
2. Build/publish all XCFrameworks
11+
3. Generate a unified `Package.swift` with all modules
12+
13+
No manual Package.swift editing required!
14+
15+
## Quick Start
16+
17+
### 1. Apply the SPM plugin to your root project
18+
19+
```kotlin
20+
// Root build.gradle.kts
21+
plugins {
22+
id("co.touchlab.kmmbridge.spm")
23+
}
24+
25+
kmmBridgeSpm {
26+
packageName.set("my-sdk") // Optional, defaults to project name
27+
}
28+
```
29+
30+
### 2. Configure each darwin module (simplified)
31+
32+
```kotlin
33+
// Each darwin module's build.gradle.kts
34+
plugins {
35+
id("co.touchlab.kmmbridge")
36+
}
37+
38+
kmmbridge {
39+
gitHubReleaseArtifacts()
40+
spm(swiftToolsVersion = "5.9") {
41+
iOS { v("15") }
42+
macOS { v("15") }
43+
}
44+
}
45+
```
46+
47+
Note: No need for `useCustomPackageFile` or `perModuleVariablesBlock` - the root plugin handles everything automatically!
48+
49+
### 3. Run the tasks
50+
51+
**For local development:**
52+
```bash
53+
./gradlew spmDevBuildAll
54+
```
55+
56+
**For CI publishing:**
57+
```bash
58+
./gradlew kmmBridgePublishAll
59+
```
60+
61+
## Available Tasks
62+
63+
| Task | Description |
64+
|------|-------------|
65+
| `spmDevBuildAll` | Builds all XCFrameworks locally and generates Package.swift with local paths |
66+
| `kmmBridgePublishAll` | Publishes all modules to artifact storage and generates Package.swift with URLs |
67+
| `generatePackageSwift` | Generates Package.swift from published module metadata |
68+
69+
> **Note**: When using the root SPM plugin, the module-level `spmDevBuild` task is automatically disabled to avoid conflicts. Use `spmDevBuildAll` instead for multi-module projects.
70+
71+
## Configuration Options
72+
73+
```kotlin
74+
kmmBridgeSpm {
75+
// SPM package name (default: rootProject.name)
76+
packageName.set("my-awesome-sdk")
77+
78+
// Swift tools version (default: "5.9", or max from modules)
79+
swiftToolsVersion.set("5.9")
80+
81+
// Output directory (default: rootProject.projectDir)
82+
outputDirectory.set(file("."))
83+
84+
// Include only specific modules (default: all KMMBridge modules)
85+
includeModules.set(setOf(
86+
":module-a:module-a-darwin",
87+
":module-b:module-b-darwin"
88+
))
89+
90+
// Exclude specific modules
91+
excludeModules.set(setOf(":legacy-module"))
92+
}
93+
```
94+
95+
## Generated Package.swift
96+
97+
### Local Development (`spmDevBuildAll`)
98+
99+
```swift
100+
// swift-tools-version:5.9
101+
// Generated by KMMBridge (LOCAL DEV) - DO NOT COMMIT
102+
import PackageDescription
103+
104+
let package = Package(
105+
name: "my-sdk",
106+
platforms: [
107+
.iOS(.v15),
108+
.macOS(.v15)
109+
],
110+
products: [
111+
.library(name: "ModuleA", targets: ["ModuleA"]),
112+
.library(name: "ModuleB", targets: ["ModuleB"]),
113+
],
114+
targets: [
115+
.binaryTarget(
116+
name: "ModuleA",
117+
path: "module-a/module-a-darwin/build/XCFrameworks/debug/ModuleA.xcframework"
118+
),
119+
.binaryTarget(
120+
name: "ModuleB",
121+
path: "module-b/module-b-darwin/build/XCFrameworks/debug/ModuleB.xcframework"
122+
),
123+
]
124+
)
125+
```
126+
127+
### CI Publishing (`kmmBridgePublishAll`)
128+
129+
```swift
130+
// swift-tools-version:5.9
131+
// Generated by KMMBridge - DO NOT EDIT MANUALLY
132+
import PackageDescription
133+
134+
let package = Package(
135+
name: "my-sdk",
136+
platforms: [
137+
.iOS(.v15),
138+
.macOS(.v15)
139+
],
140+
products: [
141+
.library(name: "ModuleA", targets: ["ModuleA"]),
142+
.library(name: "ModuleB", targets: ["ModuleB"]),
143+
],
144+
targets: [
145+
.binaryTarget(
146+
name: "ModuleA",
147+
url: "https://github.com/example/repo/releases/download/1.0.0/ModuleA.xcframework.zip",
148+
checksum: "abc123..."
149+
),
150+
.binaryTarget(
151+
name: "ModuleB",
152+
url: "https://github.com/example/repo/releases/download/1.0.0/ModuleB.xcframework.zip",
153+
checksum: "def456..."
154+
),
155+
]
156+
)
157+
```
158+
159+
## GitHub Actions Workflow
160+
161+
```yaml
162+
name: KMMBridge-Release
163+
on:
164+
workflow_dispatch:
165+
166+
permissions:
167+
contents: write
168+
169+
jobs:
170+
publish:
171+
runs-on: macos-latest
172+
steps:
173+
- uses: actions/checkout@v4
174+
with:
175+
fetch-depth: 0
176+
177+
- uses: actions/setup-java@v4
178+
with:
179+
distribution: "adopt"
180+
java-version: 17
181+
182+
- name: Create Release
183+
id: release
184+
uses: softprops/action-gh-release@v2
185+
with:
186+
draft: true
187+
tag_name: ${{ github.ref_name }}
188+
189+
- name: Build and Publish
190+
run: |
191+
./gradlew kmmBridgePublishAll \
192+
-PENABLE_PUBLISHING=true \
193+
-PGITHUB_ARTIFACT_RELEASE_ID=${{ steps.release.outputs.id }} \
194+
-PGITHUB_PUBLISH_TOKEN=${{ secrets.GITHUB_TOKEN }} \
195+
-PGITHUB_REPO=${{ github.repository }}
196+
197+
- uses: touchlab/ga-update-release-tag@v1
198+
with:
199+
tagVersion: ${{ github.ref_name }}
200+
```
201+
202+
## How It Works
203+
204+
### Architecture
205+
206+
```
207+
┌─────────────────────────────────────────────────────────┐
208+
│ Root Project │
209+
│ ┌─────────────────────────────────────────────────┐ │
210+
│ │ co.touchlab.kmmbridge.spm plugin │ │
211+
│ │ - Discovers all KMMBridge modules │ │
212+
│ │ - Collects metadata from each module │ │
213+
│ │ - Generates unified Package.swift │ │
214+
│ └─────────────────────────────────────────────────┘ │
215+
│ ▲ ▲ ▲ │
216+
│ │ │ │ │
217+
│ ┌────────┴──┐ ┌──────┴────┐ ┌────┴──────┐ │
218+
│ │ Module A │ │ Module B │ │ Module C │ │
219+
│ │ kmmbridge │ │ kmmbridge │ │ kmmbridge │ │
220+
│ │ plugin │ │ plugin │ │ plugin │ │
221+
│ └───────────┘ └───────────┘ └───────────┘ │
222+
└─────────────────────────────────────────────────────────┘
223+
```
224+
225+
### Flow
226+
227+
1. **Module Configuration**: Each darwin module configures `kmmbridge { spm { ... } }`
228+
2. **Discovery**: Root plugin discovers all modules with KMMBridge
229+
3. **Build/Publish**: Each module builds its XCFramework and (optionally) publishes
230+
4. **Metadata**: Each module writes metadata JSON with framework info
231+
5. **Generation**: Root plugin collects all metadata and generates Package.swift
232+
233+
## Migration from Manual Package.swift
234+
235+
If you're currently using `useCustomPackageFile = true` with manual markers:
236+
237+
### Before
238+
239+
```kotlin
240+
// Each module
241+
kmmbridge {
242+
spm(useCustomPackageFile = true, perModuleVariablesBlock = true) { ... }
243+
}
244+
245+
// Manual Package.swift with markers
246+
// BEGIN KMMBRIDGE VARIABLES BLOCK FOR 'MyModule' (do not edit)
247+
// ...
248+
// END KMMBRIDGE BLOCK FOR 'MyModule'
249+
```
250+
251+
### After
252+
253+
```kotlin
254+
// Root build.gradle.kts
255+
plugins {
256+
id("co.touchlab.kmmbridge.spm")
257+
}
258+
259+
// Each module (simplified)
260+
kmmbridge {
261+
spm { ... } // No special flags needed!
262+
}
263+
264+
// Package.swift is auto-generated - delete your manual one!
265+
```
266+
267+
## Troubleshooting
268+
269+
### "No KMMBridge modules found"
270+
271+
Make sure:
272+
- Each module applies the `co.touchlab.kmmbridge` plugin
273+
- Each module configures `spm { ... }` in the `kmmbridge` block
274+
275+
### "No module metadata found"
276+
277+
For `generatePackageSwift`:
278+
- Metadata is created during publishing
279+
- Run `kmmBridgePublishAll` instead, or use `spmDevBuildAll` for local dev
280+
281+
### "XCFramework not found"
282+
283+
For `spmDevBuildAll`:
284+
- Make sure XCFrameworks are built first
285+
- The task should auto-depend on assemble tasks, but you can run `./gradlew assembleXCFramework` manually
286+
287+
## Platform Resolution
288+
289+
When modules specify different platform versions, the plugin takes the **maximum** version for each platform:
290+
291+
```kotlin
292+
// Module A: iOS 14, macOS 11
293+
// Module B: iOS 15, macOS 12
294+
// Module C: iOS 13, macOS 13
295+
296+
// Result: iOS 15, macOS 13
297+
```
298+
299+
## Swift Tools Version Resolution
300+
301+
Similarly, Swift tools version is resolved to the maximum across all modules, or the configured default if none specified.

kmmbridge/build.gradle.kts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ gradlePlugin {
4747
"consume",
4848
)
4949
}
50+
register("kmmbridge-spm-plugin") {
51+
id = "co.touchlab.kmmbridge.spm"
52+
implementationClass = "co.touchlab.kmmbridge.spm.KmmBridgeSpmPlugin"
53+
displayName = "KMMBridge SPM Package.swift Generator"
54+
description = "Root-level plugin that auto-generates Package.swift from all KMMBridge modules"
55+
tags =
56+
listOf(
57+
"kmm",
58+
"kotlin",
59+
"multiplatform",
60+
"mobile",
61+
"ios",
62+
"xcode",
63+
"framework",
64+
"spm",
65+
"swift-package-manager",
66+
)
67+
}
5068
}
5169
}
5270

0 commit comments

Comments
 (0)