diff --git a/README.md b/README.md index 7f7a42a..33d8ec8 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,8 @@ swiftklib { ### Examples -More samples can be found in the [examples/](https://github.com/ttypic/swift-klib-plugin/tree/main/examples) folder. +- More samples can be found in the [examples/](https://github.com/ttypic/swift-klib-plugin/tree/main/examples) folder. +- Component demonstrating a multipurpose Kotlin Multiplatform and Swift Package audio player: [radioplayer-kt](https://github.com/markst/radioplayer-kt) ## License diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageModulesTest.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageModulesTest.kt index 1f5cd5c..135ec6f 100644 --- a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageModulesTest.kt +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageModulesTest.kt @@ -297,6 +297,188 @@ class SwiftPackageModulesTest { assertThat(result).output().contains("Package path must exist") } + @Test + fun `build with remote SPM dependency using Firebase is successful`() { + // Given + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import FirebaseAuth + import Firebase + + @objc public class FirebaseData: NSObject { + @objc public func printVersion() { + print(FirebaseVersion()) + print(ActionCodeOperation.emailLink) + } + } + """.trimIndent()) + ) + .withConfiguration { + minIos = "14.0" + minMacos = "10.15" + dependencies { + remote("FirebaseAuth") { + url("https://github.com/firebase/firebase-ios-sdk.git", "firebase-ios-sdk") + exactVersion("11.0.0") + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "firebase-ios-sdk") + } + + @Test + fun `build with remote SPM dependency using multi product Firebase is successful`() { + // Given + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import FirebaseAuth + import Firebase + import FirebaseRemoteConfig + + @objc public class FirebaseData: NSObject { + @objc public func testLinking() { + print(FirebaseVersion()) + print(ActionCodeOperation.emailLink) + print(RemoteConfigSettings()) + } + } + """.trimIndent()) + ) + .withConfiguration { + minIos = "14.0" + minMacos = "10.15" + dependencies { + remote(listOf("FirebaseAuth", "FirebaseRemoteConfig")) { + url("https://github.com/firebase/firebase-ios-sdk.git", "firebase-ios-sdk") + exactVersion("11.0.0") + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "firebase-ios-sdk") + } + + @Test + fun `build with complex and mix spm repo`() { + // Given + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import FirebaseAuth + import Firebase + import FirebaseRemoteConfig + import KeychainAccess + import SwiftyJSON + + @objc public class FirebaseData: NSObject { + @objc public func testLinking() { + print(FirebaseVersion()) + print(ActionCodeOperation.emailLink) + print(RemoteConfigSettings()) + } + } + @objc public class DataManager: NSObject { + private let keychain = Keychain(service: "test-service") + + @objc public func processJson(jsonString: String) throws -> String { + let json = try JSON(parseJSON: jsonString) + return json.description + } + } + """.trimIndent()) + ) + .withConfiguration { + minIos = "14.0" + minMacos = "10.15" + dependencies { + remote(listOf("FirebaseAuth", "FirebaseRemoteConfig")) { + url("https://github.com/firebase/firebase-ios-sdk.git", "firebase-ios-sdk") + exactVersion("11.0.0") + } + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + exactVersion("4.2.2") + } + remote("SwiftyJSON") { + github("SwiftyJSON", "SwiftyJSON") + versionRange("5.0.0", "6.0.0", true) + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "firebase-ios-sdk") + } + + @Test + fun `build with valid toolsVersion`() { + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import Foundation + """.trimIndent()) + ) + .withConfiguration { + toolsVersion = "5.5" + dependencies { + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + getManifestContent(fixture) { manifest -> + assertTrue(manifest.contains("swift-tools-version: 5.5")) + } + } + + @Test + fun `build with invalid toolsVersion`() { + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import Foundation + """.trimIndent()) + ) + .withConfiguration { + toolsVersion = "100.0" + dependencies { + } + } + .build() + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("is using Swift tools version 100.0.0") + getManifestContent(fixture) { manifest -> + assertTrue(manifest.contains("swift-tools-version: 100.0"), "must contains version 100.0") + } + } private fun assertPackageResolved(fixture: SwiftKlibTestFixture, vararg packageNames: String) { val resolvedFile = File( @@ -305,12 +487,31 @@ class SwiftPackageModulesTest { ) assertTrue(resolvedFile.exists(), "Package.resolved file not found") - val content = resolvedFile.readText() - packageNames.forEach { packageName -> - assertTrue( - content.contains("\"identity\" : \"$packageName\"", ignoreCase = true), - "$packageName dependency not found" - ) + getPackageResolvedContent(fixture) { content -> + packageNames.forEach { packageName -> + assertTrue( + content.contains("\"identity\" : \"$packageName\"", ignoreCase = true), + "$packageName dependency not found" + ) + } } } + + private fun getManifestContent(fixture: SwiftKlibTestFixture, content: (String) -> Unit) { + val resolvedFile = File( + fixture.gradleProject.rootDir, + "library/build/swiftklib/test/iosArm64/swiftBuild/Package.swift" + ) + assertTrue(resolvedFile.exists(), "Package.swift file not found") + content(resolvedFile.readText()) + } + + private fun getPackageResolvedContent(fixture: SwiftKlibTestFixture, content: (String) -> Unit) { + val resolvedFile = File( + fixture.gradleProject.rootDir, + "library/build/swiftklib/test/iosArm64/swiftBuild/Package.resolved" + ) + assertTrue(resolvedFile.exists(), "Package.resolved file not found") + content(resolvedFile.readText()) + } } diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt index 0349427..b1d2264 100644 --- a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt @@ -129,16 +129,19 @@ abstract class SwiftKlibTestFixture private constructor( // Only add minimum version configurations if they differ from defaults if (entry._minIos.hasValue()) { - appendLine(" minIos.set(${entry.minIos})") + appendLine(" minIos = \"${entry.minIos}\"") } if (entry._minMacos.hasValue()) { - appendLine(" minMacos.set(${entry.minMacos})") + appendLine(" minMacos = \"${entry.minMacos}\"") } if (entry._minTvos.hasValue()) { - appendLine(" minTvos.set(${entry.minTvos})") + appendLine(" minTvos = \"${entry.minTvos}\"") } if (entry._minWatchos.hasValue()) { - appendLine(" minWatchos.set(${entry.minWatchos})") + appendLine(" minWatchos = \"${entry.minWatchos}\"") + } + if (entry._toolsVersions.hasValue()) { + appendLine(" toolsVersion = \"${entry.toolsVersion}\"") } if (entry.dependencies.isNotEmpty()) { @@ -190,16 +193,18 @@ val Plugin.Companion.kotlinMultiplatform private class TestSwiftKlibEntryImpl : SwiftKlibEntry { val _path = notNull() - val _minIos = notNull() - val _minMacos = notNull() - val _minTvos = notNull() - val _minWatchos = notNull() + val _minIos = notNull() + val _minMacos = notNull() + val _minTvos = notNull() + val _minWatchos = notNull() + val _toolsVersions = notNull() override var path: File by _path - override var minIos: Int by _minIos - override var minMacos: Int by _minMacos - override var minTvos: Int by _minTvos - override var minWatchos: Int by _minWatchos + override var minIos: String by _minIos + override var minMacos: String by _minMacos + override var minTvos: String by _minTvos + override var minWatchos: String by _minWatchos + override var toolsVersion: String by _toolsVersions val dependencies = mutableListOf() @@ -223,23 +228,31 @@ private class TestSwiftPackageConfigurationImpl : SwiftPackageConfiguration { } override fun remote(name: String, configuration: RemotePackageConfiguration.() -> Unit) { - val config = TestRemotePackageConfigurationImpl(name) + remote(listOf(name), configuration) + } + + override fun remote(names: List, configuration: RemotePackageConfiguration.() -> Unit) { + val config = TestRemotePackageConfigurationImpl(names) config.configuration() dependencies.add(config.build()) } } -private class TestRemotePackageConfigurationImpl(private val name: String) : RemotePackageConfiguration { +private class TestRemotePackageConfigurationImpl(private val name: List) : + RemotePackageConfiguration { private var url: String? = null + private var packageName: String? = null private var versionConfig: TestVersionConfig? = null - override fun github(owner: String, repo: String) { + override fun github(owner: String, repo: String, packageName: String?) { url = "https://github.com/$owner/$repo.git" + this.packageName = packageName } - override fun url(url: String) { + override fun url(url: String, packageName: String?) { this.url = url + this.packageName = packageName } override fun exactVersion(version: String) { @@ -262,7 +275,8 @@ private class TestRemotePackageConfigurationImpl(private val name: String) : Rem return TestDependencyConfig.Remote( name = name, url = url, - version = versionConfig + version = versionConfig, + packageName = packageName ) } } @@ -275,14 +289,23 @@ private sealed interface TestDependencyConfig { } data class Remote( - val name: String, + val name: List, val url: String?, - val version: TestVersionConfig? + val version: TestVersionConfig?, + val packageName: String? ) : TestDependencyConfig { override fun toConfigString() = buildString { - append("remote(\"$name\") {\n") + if (name.size == 1) { + append("remote(\"${name.first()}\") {\n") + } else { + append("remote(listOf(\"${name.joinToString("\",\"")}\")) {\n") + } if (url != null) { - append(" url(\"$url\")\n") + if (packageName != null) { + append(" url(\"$url\", \"$packageName\")\n") + } else { + append(" url(\"$url\")\n") + } } if (version != null) { append(" ${version.toConfigString()}\n") @@ -318,7 +341,8 @@ private class NotNullVar() : ReadWriteProperty { private var value: T? = null public override fun getValue(thisRef: Any?, property: KProperty<*>): T { - return value ?: throw IllegalStateException("Property ${property.name} should be initialized before get.") + return value + ?: throw IllegalStateException("Property ${property.name} should be initialized before get.") } public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/RemotePackageBuilder.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/RemotePackageBuilder.kt index 01bdc9c..a07ad11 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/RemotePackageBuilder.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/RemotePackageBuilder.kt @@ -8,7 +8,7 @@ import javax.inject.Inject @ExperimentalSwiftklibApi class RemotePackageBuilder @Inject constructor( private val objects: ObjectFactory, - private val name: String + private val name: List ) { private val urlProperty: Property = objects.property(String::class.java) private var dependency: SwiftPackageDependency.Remote? = null diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt index efe4286..636f356 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt @@ -16,16 +16,17 @@ internal abstract class SwiftKlibEntryImpl @Inject constructor( ) : SwiftKlibEntry { val _path: Property = objects.property(File::class.java) val _packageName: Property = objects.property(String::class.java) - val _minIos: Property = objects.property(Int::class.java).convention(13) - val _minMacos: Property = objects.property(Int::class.java).convention(11) - val _minTvos: Property = objects.property(Int::class.java).convention(13) - val _minWatchos: Property = objects.property(Int::class.java).convention(8) - + val _toolsVersion: Property = objects.property(String::class.java).convention("5.9") + val _minIos: Property = objects.property(String::class.java).convention("12.0") + val _minMacos: Property = objects.property(String::class.java).convention("10.13") + val _minTvos: Property = objects.property(String::class.java).convention("12.0") + val _minWatchos: Property = objects.property(String::class.java).convention("4.0") override var path: File by _path.bind() - override var minIos: Int by _minIos.bind() - override var minMacos: Int by _minMacos.bind() - override var minTvos: Int by _minTvos.bind() - override var minWatchos: Int by _minWatchos.bind() + override var minIos: String by _minIos.bind() + override var minMacos: String by _minMacos.bind() + override var minTvos: String by _minTvos.bind() + override var minWatchos: String by _minWatchos.bind() + override var toolsVersion: String by _toolsVersion.bind() internal val dependencyHandler = SwiftPackageConfigurationImpl(objects) diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibPlugin.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibPlugin.kt index 41deded..ea47ef4 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibPlugin.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibPlugin.kt @@ -59,6 +59,7 @@ class SwiftKlibPlugin : Plugin { entry._minMacos, entry._minTvos, entry._minWatchos, + entry._toolsVersion ).configure { it.dependenciesProperty = entry.dependencyHandler.dependencies } diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt index 96015a4..491cea3 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt @@ -2,19 +2,23 @@ package io.github.ttypic.swiftklib.gradle import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional import java.io.File import java.io.Serializable internal sealed interface SwiftPackageDependency : Serializable { @get:Input - val name: String + val name: List + @get:Input @get:Optional + val packageName: String? data class Local( - @Input override val name: String, - @InputDirectory val path: File + @Input override val name: List, + @InputDirectory val path: File, + @Input @get:Optional override val packageName: String? = null, ) : SwiftPackageDependency { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } require(path.exists()) { "Package path must exist: $path" } } } @@ -24,26 +28,28 @@ internal sealed interface SwiftPackageDependency : Serializable { val url: String data class ExactVersion( - @Input override val name: String, + @Input override val name: List, @Input override val url: String, - @Input val version: String + @Input val version: String, + @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(version.isNotBlank()) { "Version cannot be blank" } } } data class VersionRange( - @Input override val name: String, + @Input override val name: List, @Input override val url: String, @Input val from: String, @Input val to: String, - @Input val inclusive: Boolean = true + @Input val inclusive: Boolean = true, + @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(from.isNotBlank()) { "From version cannot be blank" } require(to.isNotBlank()) { "To version cannot be blank" } @@ -51,24 +57,26 @@ internal sealed interface SwiftPackageDependency : Serializable { } data class Branch( - @Input override val name: String, + @Input override val name: List, @Input override val url: String, - @Input val branchName: String + @Input val branchName: String, + @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(branchName.isNotBlank()) { "Branch name cannot be blank" } } } data class FromVersion( - @Input override val name: String, + @Input override val name: List, @Input override val url: String, - @Input val version: String + @Input val version: String, + @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(version.isNotBlank()) { "Version cannot be blank" } } diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt index 4d132ca..13eda5d 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt @@ -1,6 +1,9 @@ package io.github.ttypic.swiftklib.gradle.api -@RequiresOptIn(message = "This API is experimental. It may be changed in the future without notice.") +@RequiresOptIn( + message = "This API is experimental. It may be changed in the future without notice.", + level = RequiresOptIn.Level.WARNING +) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) annotation class ExperimentalSwiftklibApi { diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt index c659cb6..3d07cb4 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt @@ -4,13 +4,15 @@ package io.github.ttypic.swiftklib.gradle.api interface RemotePackageConfiguration { /** * Sets GitHub repository as the package source. + * Specifies the main package name in case of multi target package (ex: Firebase) */ - fun github(owner: String, repo: String) + fun github(owner: String, repo: String, packageName: String? = null) /** * Sets custom URL as the package source. + * Specifies the main package name in case of multi target package (ex: Firebase) */ - fun url(url: String) + fun url(url: String, packageName: String? = null) /** * Specifies exact version of the package. diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt index 2eec408..d037a66 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt @@ -5,10 +5,11 @@ import java.io.File interface SwiftKlibEntry { var path: File - var minIos: Int - var minMacos: Int - var minTvos: Int - var minWatchos: Int + var minIos: String + var minMacos: String + var minTvos: String + var minWatchos: String + var toolsVersion: String fun packageName(name: String) diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt index f2ef15f..d45c323 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt @@ -11,9 +11,16 @@ interface SwiftPackageConfiguration { /** * Configures a remote package dependency. - * @param name Package name + * @param name the product's name to add * @param configuration Configuration block for the remote package */ fun remote(name: String, configuration: RemotePackageConfiguration.() -> Unit) + + /** + * Configures a remote package dependency. + * @param names a list of product's name to add + * @param configuration Configuration block for the remote package + */ + fun remote(names: List, configuration: RemotePackageConfiguration.() -> Unit) } diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt index d87d587..44b81fa 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt @@ -7,27 +7,31 @@ import javax.inject.Inject internal class RemotePackageConfigurationImpl @Inject constructor( private val objects: ObjectFactory, - private val name: String + private val name: List ) : RemotePackageConfiguration { private val urlProperty = objects.property(String::class.java) + private val packageName = objects.property(String::class.java) private var dependency: SwiftPackageDependency.Remote? = null - override fun github(owner: String, repo: String) { + override fun github(owner: String, repo: String, packageName: String?) { require(owner.isNotBlank()) { "Owner cannot be blank" } require(repo.isNotBlank()) { "Repo cannot be blank" } urlProperty.set("https://github.com/$owner/$repo.git") + this.packageName.set(packageName) } - override fun url(url: String) { + override fun url(url: String, packageName: String?) { require(url.isNotBlank()) { "URL cannot be blank" } urlProperty.set(url) + this.packageName.set(packageName) } override fun exactVersion(version: String) { dependency = SwiftPackageDependency.Remote.ExactVersion( name = name, url = requireUrl(), - version = version + version = version, + packageName = packageName.orNull ) } @@ -37,7 +41,8 @@ internal class RemotePackageConfigurationImpl @Inject constructor( url = requireUrl(), from = from, to = to, - inclusive = inclusive + inclusive = inclusive, + packageName = packageName.orNull ) } @@ -45,7 +50,8 @@ internal class RemotePackageConfigurationImpl @Inject constructor( dependency = SwiftPackageDependency.Remote.Branch( name = name, url = requireUrl(), - branchName = branchName + branchName = branchName, + packageName = packageName.orNull ) } @@ -53,7 +59,8 @@ internal class RemotePackageConfigurationImpl @Inject constructor( dependency = SwiftPackageDependency.Remote.FromVersion( name = name, url = requireUrl(), - version = version + version = version, + packageName = packageName.orNull ) } diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/SwiftPackageConfigurationImpl.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/SwiftPackageConfigurationImpl.kt index a3a21f6..8ad8632 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/SwiftPackageConfigurationImpl.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/SwiftPackageConfigurationImpl.kt @@ -20,7 +20,23 @@ internal class SwiftPackageConfigurationImpl @Inject constructor( @ExperimentalSwiftklibApi override fun local(name: String, path: java.io.File) { val currentDeps = _dependencies.get().toMutableList() - currentDeps.add(SwiftPackageDependency.Local(name, path)) + currentDeps.add(SwiftPackageDependency.Local(listOf(name), path)) + _dependencies.set(currentDeps) + } + + @ExperimentalSwiftklibApi + override fun remote( + names: List, + configuration: RemotePackageConfiguration.() -> Unit + ) { + val builder = RemotePackageConfigurationImpl(objects, names) + builder.apply(configuration) + + val dependency = builder.build() + ?: throw IllegalStateException("No version specification provided for remote package $names") + + val currentDeps = _dependencies.get().toMutableList() + currentDeps.add(dependency) _dependencies.set(currentDeps) } @@ -29,7 +45,7 @@ internal class SwiftPackageConfigurationImpl @Inject constructor( name: String, configuration: RemotePackageConfiguration.() -> Unit ) { - val builder = RemotePackageConfigurationImpl(objects, name) + val builder = RemotePackageConfigurationImpl(objects, listOf(name)) builder.apply(configuration) val dependency = builder.build() diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt index aa75cc2..998478c 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt @@ -30,10 +30,11 @@ abstract class CompileSwiftTask @Inject constructor( @Input val buildDirectory: String, @InputDirectory val pathProperty: Property, @Input val packageNameProperty: Property, - @Optional @Input val minIosProperty: Property, - @Optional @Input val minMacosProperty: Property, - @Optional @Input val minTvosProperty: Property, - @Optional @Input val minWatchosProperty: Property, + @Optional @Input val minIosProperty: Property, + @Optional @Input val minMacosProperty: Property, + @Optional @Input val minTvosProperty: Property, + @Optional @Input val minWatchosProperty: Property, + @Optional @Input val toolsVersionProperty: Property, ) : DefaultTask() { @get:Optional @@ -81,10 +82,11 @@ abstract class CompileSwiftTask @Inject constructor( ) } - private val minIos get() = minIosProperty.getOrElse(13) - private val minMacos get() = minMacosProperty.getOrElse(11) - private val minTvos get() = minTvosProperty.getOrElse(13) - private val minWatchos get() = minWatchosProperty.getOrElse(8) + private val minIos get() = minIosProperty.getOrElse("12.0") + private val minMacos get() = minMacosProperty.getOrElse("10.13") + private val minTvos get() = minTvosProperty.getOrElse("12.0") + private val minWatchos get() = minWatchosProperty.getOrElse("4.0") + private val toolsVersion get() = toolsVersionProperty.getOrElse("5.9") /** * Creates build directory or cleans up if it already exists @@ -118,14 +120,21 @@ abstract class CompileSwiftTask @Inject constructor( } private fun createPackageSwift(dependencies: List) { - val packageSwiftContents = createPackageSwiftContents(cinteropName, dependencies) + val manifest = createPackageSwiftContents( + cinteropName, + dependencies, + minIos, + minMacos, + minTvos, + minWatchos, + toolsVersion + ) + File(swiftBuildDir, "Package.swift").writeText(manifest) if (printDebug) { logger.warn("======== Package.swift contents ========") - logger.warn(packageSwiftContents) + logger.warn(manifest) logger.warn("======== | Package.swift contents | ========") } - File(swiftBuildDir, "Package.swift") - .writeText(packageSwiftContents) } private fun buildSwift(xcodeVersion: Int): SwiftBuildResult { @@ -157,7 +166,11 @@ abstract class CompileSwiftTask @Inject constructor( ) } - val releaseBuildPath = File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-macosx/release") + val releaseBuildPath = + File( + swiftBuildDir, + ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/release" + ) return SwiftBuildResult( libPath = File(releaseBuildPath, "lib${cinteropName}.a"), @@ -167,16 +180,16 @@ abstract class CompileSwiftTask @Inject constructor( private fun generateBuildArgs(): List { val sdkPath = readSdkPath() - val baseArgs = "swift build --arch ${compileTarget.arch()} -c release".split(" ") - - val xcrunArgs = listOf( - "-sdk", - sdkPath, - "-target", - compileTarget.asSwiftcTarget(compileTarget.operatingSystem()), - ).asSwiftcArgs() - - return baseArgs + xcrunArgs + return listOf( + "swift", + "build", + "-c", + "release", + "--triple", + "${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${minOs(compileTarget)}${compileTarget.simulatorSuffix()}", + "--sdk", + sdkPath + ) } /** Workaround for bug in toolchain where the sdk path (via `swiftc -sdk` flag) is not propagated to clang. */ @@ -186,7 +199,6 @@ abstract class CompileSwiftTask @Inject constructor( readSdkPath(), ).asCcArgs() - private fun List.asSwiftcArgs() = asBuildToolArgs("swiftc") private fun List.asCcArgs() = asBuildToolArgs("cc") private fun List.asBuildToolArgs(tool: String): List { @@ -245,7 +257,12 @@ abstract class CompileSwiftTask @Inject constructor( * Note: adds lib-file md5 hash to library in order to automatically * invalidate connected cinterop task */ - private fun createDefFile(libPath: File, headerPath: File, packageName: String, xcodeVersion: Int) { + private fun createDefFile( + libPath: File, + headerPath: File, + packageName: String, + xcodeVersion: Int + ) { val xcodePath = readXcodePath() val linkerPlatformVersion = @@ -257,8 +274,8 @@ abstract class CompileSwiftTask @Inject constructor( val basicLinkerOpts = listOf( "-L/usr/lib/swift", "-$linkerPlatformVersion", - "${minOs(compileTarget)}.0", - "${minOs(compileTarget)}.0", + minOs(compileTarget), + minOs(compileTarget), "-L${xcodePath}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/${compileTarget.os()}" ) @@ -287,13 +304,13 @@ abstract class CompileSwiftTask @Inject constructor( private fun CompileTarget.operatingSystem(): String = when (this) { - CompileTarget.iosX64, CompileTarget.iosArm64, CompileTarget.iosSimulatorArm64 -> "ios$minIos" - CompileTarget.watchosX64, CompileTarget.watchosArm64, CompileTarget.watchosSimulatorArm64 -> "watchos$minWatchos" - CompileTarget.tvosX64, CompileTarget.tvosArm64, CompileTarget.tvosSimulatorArm64 -> "tvos$minTvos" - CompileTarget.macosX64, CompileTarget.macosArm64 -> "macosx$minMacos" + CompileTarget.iosX64, CompileTarget.iosArm64, CompileTarget.iosSimulatorArm64 -> "ios" + CompileTarget.watchosX64, CompileTarget.watchosArm64, CompileTarget.watchosSimulatorArm64 -> "watchos" + CompileTarget.tvosX64, CompileTarget.tvosArm64, CompileTarget.tvosSimulatorArm64 -> "tvos" + CompileTarget.macosX64, CompileTarget.macosArm64 -> "macosx" } - private fun minOs(compileTarget: CompileTarget): Int = + private fun minOs(compileTarget: CompileTarget): String? = when (compileTarget) { CompileTarget.iosX64, CompileTarget.iosArm64, CompileTarget.iosSimulatorArm64 -> minIos CompileTarget.watchosX64, CompileTarget.watchosArm64, CompileTarget.watchosSimulatorArm64 -> minWatchos diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt index a5d8dba..55da262 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt @@ -1,38 +1,68 @@ package io.github.ttypic.swiftklib.gradle.templates import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency +import org.gradle.process.ExecOperations +import java.io.File internal fun createPackageSwiftContents( cinteropName: String, - dependencies: Collection + dependencies: Collection, + minIos: String, + minMacos: String, + minTvos: String, + minWatchos: String, + toolsVersion: String, ): String = """ - // swift-tools-version:5.6 + // swift-tools-version: $toolsVersion import PackageDescription let package = Package( name: "$cinteropName", + ${getPlatformBlock(minIos, minMacos, minTvos, minWatchos)}, products: [ .library( name: "$cinteropName", type: .static, - targets: ["$cinteropName"]) + targets: ${getProductsTargets(cinteropName)}) ], dependencies: [ - ${dependencies.joinToString(",\n ") { it.toSwiftPackageDeclaration() }} + ${getDependencies(dependencies)} ], targets: [ .target( name: "$cinteropName", dependencies: [ - ${dependencies.joinToString(",\n ") { "\"${it.name}\"" }} + ${getDependenciesTargets(dependencies)} ], path: "$cinteropName") ] ) """.trimIndent() +private fun getPlatformBlock( + minIos: String, + minMacos: String, + minTvos: String, + minWatchos: String +): String { + val entries = listOfNotNull( + ".iOS(\"$minIos\")".takeIf { minIos.isNotEmpty() }, + ".macOS(\"$minMacos\")".takeIf { minMacos.isNotEmpty() }, + ".tvOS(\"$minTvos\")".takeIf { minTvos.isNotEmpty() }, + ".watchOS(\"$minWatchos\")".takeIf { minWatchos.isNotEmpty() }, + ).joinToString(",") + return "platforms: [$entries]" +} -internal fun SwiftPackageDependency.toSwiftPackageDeclaration(): String = when (this) { +private fun getDependencies(dependencies: Collection): String { + return buildList { + dependencies.forEach { dependency -> + add(dependency.toSwiftPackageDependencyDeclaration()) + } + }.joinToString(",") +} + +private fun SwiftPackageDependency.toSwiftPackageDependencyDeclaration(): String = when (this) { is SwiftPackageDependency.Local -> """ .package(path: "${path.absolutePath}") @@ -60,3 +90,20 @@ internal fun SwiftPackageDependency.toSwiftPackageDeclaration(): String = when ( .package(url: "$url", from: "$version") """.trimIndent() } + + +private fun getDependenciesTargets( + dependencies: Collection +): String { + return buildList { + dependencies.forEach { dependency -> + dependency.name.forEach { library -> + add(".product(name: \"${library}\", package: \"${dependency.packageName ?: library}\")") + } + } + }.joinToString(",") +} + +private fun getProductsTargets(cinteropName: String): String { + return "[\"$cinteropName\"]" +}