SPM Multi-module support - #291
Open
faogustavo wants to merge 6 commits into
Open
faogustavo wants to merge 6 commits into
faogustavo wants to merge 6 commits into
Conversation
faogustavo
commented
Sep 14, 2026
Contributor
- This PR is a feature branch with changes from feat(spm): add multi-module Package.swift auto-generation #284 and the remaining review changes
- Unfortunately, the previous one didn't allow other devs to push the changes, so I've merged everything into a feature branch and then added the remaining items
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
- Address Copilot review feedback for SPM multi-module support - Move Package.swift content generation logic to a new class for testability - Simplify KmmBridgeSpmPlugin by delegating package generation - Enhance platform version parsing in SpmDependencyManager with max-version logic - Remove obsolete local Package.swift generation code, use centralized generator class
Fix broken swiftToolVersion example and document behavior added since the doc was first written: kmmBridgePublishAll's dependsOn-based atomicity, generatePackageSwift's fail-fast on partial metadata, per-module updatePackageSwift/spmDevBuild auto-skip under the root aggregator, the framework-name uniqueness requirement, and that Swift-tools-version max-resolution applies to both the remote and local-dev generation paths. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds root-level multi-module SPM support for KMMBridge, including automatic local and published Package.swift generation.
Changes:
- Adds SPM aggregation plugin, extension, registry, metadata, and generation tasks.
- Adds multi-module integration fixtures and tests.
- Adds documentation and Gradle plugin version-alias updates.
File summaries
| File | Description |
|---|---|
| test-projects/multi-module/settings.gradle.kts | Updated as part of this pull request. |
| test-projects/multi-module/module-excluded/src/iosMain/kotlin/co/touchlab/kmmbridgetest/moduleexcluded/StartSDK.kt | Updated as part of this pull request. |
| test-projects/multi-module/module-excluded/build.gradle.kts | Updated as part of this pull request. |
| test-projects/multi-module/module-b/src/iosMain/kotlin/co/touchlab/kmmbridgetest/moduleb/StartSDK.kt | Updated as part of this pull request. |
| test-projects/multi-module/module-b/build.gradle.kts | Updated as part of this pull request. |
| test-projects/multi-module/module-a/src/iosMain/kotlin/co/touchlab/kmmbridgetest/modulea/StartSDK.kt | Updated as part of this pull request. |
| test-projects/multi-module/module-a/build.gradle.kts | Updated as part of this pull request. |
| test-projects/multi-module/LICENSE.txt | Updated as part of this pull request. |
| test-projects/multi-module/gradlew.bat | Updated as part of this pull request. |
| test-projects/multi-module/gradlew | Updated as part of this pull request. |
| test-projects/multi-module/gradle/wrapper/gradle-wrapper.properties | Updated as part of this pull request. |
| test-projects/multi-module/gradle/libs.versions.toml | Updated as part of this pull request. |
| test-projects/multi-module/gradle.properties | Updated as part of this pull request. |
| test-projects/multi-module/build.gradle.kts | Updated as part of this pull request. |
| test-projects/basic/allshared/src/iosMain/kotlin/co.touchlab/kmmbridgetest/StartSDK.kt | Updated as part of this pull request. |
| test-projects/basic/allshared/build.gradle.kts | Updated as part of this pull request. |
| kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/spm/SpmModuleMetadataTest.kt | Updated as part of this pull request. |
| kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/spm/PackageSwiftGeneratorTest.kt | Updated as part of this pull request. |
| kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/spm/KmmBridgeSpmMultiModuleTest.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/SpmModuleMetadata.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/PackageSwiftGenerator.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/KmmBridgeSpmRegistry.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/KmmBridgeSpmPlugin.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/KmmBridgeSpmExtension.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/KMMBridge.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/ProjectExtensionsInternal.kt | Updated as part of this pull request. |
| kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/dependencymanager/SpmDependencyManager.kt | Updated as part of this pull request. |
| kmmbridge/build.gradle.kts | Updated as part of this pull request. |
| kmmbridge-test/build.gradle.kts | Updated as part of this pull request. |
| kmmbridge-gitlab/build.gradle.kts | Updated as part of this pull request. |
| kmmbridge-github/build.gradle.kts | Updated as part of this pull request. |
| gradle/libs.versions.toml | Updated as part of this pull request. |
| docs/SPM_MULTI_MODULE.md | Updated as part of this pull request. |
| build.gradle.kts | Updated as part of this pull request. |
Review details
Suppressed comments (5)
docs/SPM_MULTI_MODULE.md:82
spmDevBuildAlldepends directly on each module's debug XCFramework assemble task (KmmBridgeSpmPlugin.kt:190-191), not on the modulespmDevBuildtasks. Therefore those per-module tasks do not run and the claimedSkipping spmDevBuildmessages will not appear duringspmDevBuildAll; distinguish the root task from an explicit per-module invocation, or change the task graph if that behavior is intended.
> **Note**: When the root SPM plugin is applied, each module's own `spmDevBuild` and `updatePackageSwift`
> tasks still run (and can still be up-to-date-checked), but their write step short-circuits at
> execution time - each logs a `Skipping ...` message and defers to `spmDevBuildAll`/the root
> `generatePackageSwift` instead of writing its own single-module Package.swift and racing with the
> aggregated one. This is expected; you'll see these log lines for every module when running
kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/dependencymanager/SpmDependencyManager.kt:393
parsePlatformsMapdocuments that it keeps the maximum when multiple DSL entries map to one platform, but this comparison only parses whole-number strings and otherwise compares lexicographically. Valid dotted versions such as10.15and9.0therefore select9.0as the maximum, producing an incorrect platform requirement. Compare dot-separated numeric components, consistent with the comparator used byPackageSwiftGenerator.
return if (candidateInt != null && currentInt != null) {
candidateInt > currentInt
} else {
candidate > current
kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/KmmBridgeSpmPlugin.kt:165
modulesProvidercontains modules registered byconfigureLocalDeveven whenENABLE_PUBLISHINGis false, butBaseKMMBridgePluginonly creates each module'skmmBridgePublishtask inside the publishing branch. Therefore the documented./gradlew kmmBridgePublishAllinvocation resolves dependencies on missing:module:kmmBridgePublishtasks instead of warning/skipping; filter this task to publish-enabled registrations or fail early with the required configuration.
// Depend on all module kmmBridgePublish tasks, resolved lazily by task path. Modules
// only register themselves once publishing is configured (ENABLE_PUBLISHING), so a
// module without publishing enabled simply never appears here.
dependsOn(modulesProvider.map { modules -> modules.map { "${it.path}:${BaseKMMBridgePlugin.PUBLISH_TASK_NAME}" } })
kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/KmmBridgeSpmPlugin.kt:63
- Applying this plugin to a subproject only logs a warning, but still marks the shared registry as root-applied and registers the aggregate tasks on that subproject. That disables every module's per-module SPM writers without creating the aggregate tasks in the actual root build, so this misconfiguration should fail fast (or return before marking the registry).
if (project != project.rootProject) {
logger.warn("KmmBridgeSpmPlugin should be applied to the root project only")
}
kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/spm/KmmBridgeSpmPlugin.kt:195
- This repeats the eager
.get()problem forspmDevBuildAll: user-suppliedpackageName,outputDirectory, orswiftToolsVersionvalues configured after plugin application are ignored, so local generation writes the defaultPackage.swiftlocation/name and tools version instead of the configured ones. Use the extension properties as lazy providers through task registration and execution.
val outputDir = extension.outputDirectory.get()
val packageName = extension.packageName.get()
val defaultSwiftToolsVersion = extension.swiftToolsVersion.get()
- Files reviewed: 32/35 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+98
to
+99
| // Depend on all module metadata-write tasks, resolved lazily by task path | ||
| dependsOn(modulesProvider.map { modules -> modules.map { "${it.path}:$WRITE_SPM_METADATA_TASK_NAME" } }) |
Comment on lines
+101
to
+103
| val outputDir = extension.outputDirectory.get() | ||
| val packageName = extension.packageName.get() | ||
| val swiftToolsVersion = extension.swiftToolsVersion.get() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.