Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions docs/SPM_MULTI_MODULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# Multi-Module SPM Support

This guide explains how to use KMMBridge to publish multiple KMP modules as a single SPM package with automatic Package.swift generation.

## Overview

When you have multiple KMP darwin modules (e.g., `module-a-darwin`, `module-b-darwin`), KMMBridge can automatically:

1. Discover all modules with KMMBridge configured
2. Build/publish all XCFrameworks
3. Generate a unified `Package.swift` with all modules

No manual Package.swift editing required!

## Quick Start

### 1. Apply the SPM plugin to your root project

```kotlin
// Root build.gradle.kts
plugins {
id("co.touchlab.kmmbridge.spm")
}

kmmBridgeSpm {
packageName.set("my-sdk") // Optional, defaults to project name
}
```

### 2. Configure each darwin module (simplified)

```kotlin
// Each darwin module's build.gradle.kts
plugins {
id("co.touchlab.kmmbridge")
}

kmmbridge {
gitHubReleaseArtifacts()
spm(swiftToolsVersion = "5.9") {
iOS { v("15") }
macOS { v("15") }
}
}
```

Note: No need for `useCustomPackageFile` or `perModuleVariablesBlock` - the root plugin handles everything automatically!

### 3. Run the tasks

**For local development:**
```bash
./gradlew spmDevBuildAll
```

**For CI publishing:**
```bash
./gradlew kmmBridgePublishAll
```

## Available Tasks

| Task | Description |
|------|-------------|
| `spmDevBuildAll` | Builds all XCFrameworks locally and generates Package.swift with local paths |
| `kmmBridgePublishAll` | Publishes all modules to artifact storage and generates Package.swift with URLs |
| `generatePackageSwift` | Generates Package.swift from published module metadata |

> **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.
## Configuration Options

```kotlin
kmmBridgeSpm {
// SPM package name (default: rootProject.name)
packageName.set("my-awesome-sdk")

// Swift tools version (default: "5.9", or max from modules)
swiftToolsVersion.set("5.9")

// Output directory (default: rootProject.projectDir)
outputDirectory.set(file("."))

// Include only specific modules (default: all KMMBridge modules)
includeModules.set(setOf(
":module-a:module-a-darwin",
":module-b:module-b-darwin"
))

// Exclude specific modules
excludeModules.set(setOf(":legacy-module"))
}
```

## Generated Package.swift

### Local Development (`spmDevBuildAll`)

```swift
// swift-tools-version:5.9
// Generated by KMMBridge (LOCAL DEV) - DO NOT COMMIT
import PackageDescription

let package = Package(
name: "my-sdk",
platforms: [
.iOS(.v15),
.macOS(.v15)
],
products: [
.library(name: "ModuleA", targets: ["ModuleA"]),
.library(name: "ModuleB", targets: ["ModuleB"]),
],
targets: [
.binaryTarget(
name: "ModuleA",
path: "module-a/module-a-darwin/build/XCFrameworks/debug/ModuleA.xcframework"
),
.binaryTarget(
name: "ModuleB",
path: "module-b/module-b-darwin/build/XCFrameworks/debug/ModuleB.xcframework"
),
]
)
```

### CI Publishing (`kmmBridgePublishAll`)

```swift
// swift-tools-version:5.9
// Generated by KMMBridge - DO NOT EDIT MANUALLY
import PackageDescription

let package = Package(
name: "my-sdk",
platforms: [
.iOS(.v15),
.macOS(.v15)
],
products: [
.library(name: "ModuleA", targets: ["ModuleA"]),
.library(name: "ModuleB", targets: ["ModuleB"]),
],
targets: [
.binaryTarget(
name: "ModuleA",
url: "https://github.com/example/repo/releases/download/1.0.0/ModuleA.xcframework.zip",
checksum: "abc123..."
),
.binaryTarget(
name: "ModuleB",
url: "https://github.com/example/repo/releases/download/1.0.0/ModuleB.xcframework.zip",
checksum: "def456..."
),
]
)
```

## GitHub Actions Workflow

```yaml
name: KMMBridge-Release
on:
workflow_dispatch:

permissions:
contents: write

jobs:
publish:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-java@v4
with:
distribution: "adopt"
java-version: 17

- name: Create Release
id: release
uses: softprops/action-gh-release@v2
with:
draft: true
tag_name: ${{ github.ref_name }}

- name: Build and Publish
run: |
./gradlew kmmBridgePublishAll \
-PENABLE_PUBLISHING=true \
-PGITHUB_ARTIFACT_RELEASE_ID=${{ steps.release.outputs.id }} \
-PGITHUB_PUBLISH_TOKEN=${{ secrets.GITHUB_TOKEN }} \
-PGITHUB_REPO=${{ github.repository }}
- uses: touchlab/ga-update-release-tag@v1
with:
tagVersion: ${{ github.ref_name }}
```
## How It Works
### Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Root Project │
│ ┌─────────────────────────────────────────────────┐ │
│ │ co.touchlab.kmmbridge.spm plugin │ │
│ │ - Discovers all KMMBridge modules │ │
│ │ - Collects metadata from each module │ │
│ │ - Generates unified Package.swift │ │
│ └─────────────────────────────────────────────────┘ │
│ ▲ ▲ ▲ │
│ │ │ │ │
│ ┌────────┴──┐ ┌──────┴────┐ ┌────┴──────┐ │
│ │ Module A │ │ Module B │ │ Module C │ │
│ │ kmmbridge │ │ kmmbridge │ │ kmmbridge │ │
│ │ plugin │ │ plugin │ │ plugin │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────┘
```

### Flow

1. **Module Configuration**: Each darwin module configures `kmmbridge { spm { ... } }`
2. **Discovery**: Root plugin discovers all modules with KMMBridge
3. **Build/Publish**: Each module builds its XCFramework and (optionally) publishes
4. **Metadata**: Each module writes metadata JSON with framework info
5. **Generation**: Root plugin collects all metadata and generates Package.swift

## Migration from Manual Package.swift

If you're currently using `useCustomPackageFile = true` with manual markers:

### Before

```kotlin
// Each module
kmmbridge {
spm(useCustomPackageFile = true, perModuleVariablesBlock = true) { ... }
}

// Manual Package.swift with markers
// BEGIN KMMBRIDGE VARIABLES BLOCK FOR 'MyModule' (do not edit)
// ...
// END KMMBRIDGE BLOCK FOR 'MyModule'
```

### After

```kotlin
// Root build.gradle.kts
plugins {
id("co.touchlab.kmmbridge.spm")
}

// Each module (simplified)
kmmbridge {
spm { ... } // No special flags needed!
}

// Package.swift is auto-generated - delete your manual one!
```

## Troubleshooting

### "No KMMBridge modules found"

Make sure:
- Each module applies the `co.touchlab.kmmbridge` plugin
- Each module configures `spm { ... }` in the `kmmbridge` block

### "No module metadata found"

For `generatePackageSwift`:
- Metadata is created during publishing
- Run `kmmBridgePublishAll` instead, or use `spmDevBuildAll` for local dev

### "XCFramework not found"

For `spmDevBuildAll`:
- Make sure XCFrameworks are built first
- The task should auto-depend on assemble tasks, but you can run `./gradlew assembleXCFramework` manually

## Platform Resolution

When modules specify different platform versions, the plugin takes the **maximum** version for each platform:

```kotlin
// Module A: iOS 14, macOS 11
// Module B: iOS 15, macOS 12
// Module C: iOS 13, macOS 13

// Result: iOS 15, macOS 13
```

## Swift Tools Version Resolution

Similarly, Swift tools version is resolved to the maximum across all modules, or the configured default if none specified.
18 changes: 18 additions & 0 deletions kmmbridge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ gradlePlugin {
"consume",
)
}
register("kmmbridge-spm-plugin") {
id = "co.touchlab.kmmbridge.spm"
implementationClass = "co.touchlab.kmmbridge.spm.KmmBridgeSpmPlugin"
displayName = "KMMBridge SPM Package.swift Generator"
description = "Root-level plugin that auto-generates Package.swift from all KMMBridge modules"
tags =
listOf(
"kmm",
"kotlin",
"multiplatform",
"mobile",
"ios",
"xcode",
"framework",
"spm",
"swift-package-manager",
)
}
}
}

Expand Down
Loading