From cba0968f26924350607ff750c3c42ca3d16f76cd Mon Sep 17 00:00:00 2001 From: Ilya Gulya Date: Tue, 29 Oct 2024 19:34:23 +0500 Subject: [PATCH 01/25] external dependencies draft --- .gitignore | 3 +- examples/helloswift/build.gradle.kts | 21 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- examples/helloswift/gradlew | 0 .../native/HelloSwift/HelloSwift.swift | 5 + gradle/libs.versions.toml | 7 +- plugin/build.gradle.kts | 25 +- .../swiftklib/gradle/CinteropModulesTest.kt | 53 +++ .../gradle/SwiftPackageModulesTest.kt | 319 ++++++++++++++++++ .../ttypic/swiftklib/gradle/TestUtils.kt | 12 + .../gradle/fixture/BaseSwiftKlibFixture.kt | 97 ++++++ .../gradle/fixture/CInteropFixture.kt | 45 +++ .../swiftklib/gradle/fixture/KotlinSource.kt | 22 ++ .../swiftklib/gradle/fixture/PackageSource.kt | 27 ++ .../gradle/fixture/SwiftPackageFixture.kt | 72 ++++ .../swiftklib/gradle/fixture/SwiftSource.kt | 14 + .../swiftklib/gradle/RemotePackageBuilder.kt | 79 +++++ .../ttypic/swiftklib/gradle/SwiftKlibEntry.kt | 25 +- .../swiftklib/gradle/SwiftKlibPlugin.kt | 5 +- .../gradle/SwiftPackageDependency.kt | 77 +++++ .../gradle/SwiftPackageDependencyHandler.kt | 40 +++ .../gradle/api/ExperimentalSwiftklibApi.kt | 7 + .../swiftklib/gradle/task/CompileSwiftTask.kt | 45 ++- .../gradle/templates/CreatePackageSwift.kt | 43 ++- .../swiftklib/gradle/CinteropModulesTest.kt | 159 --------- 25 files changed, 1011 insertions(+), 193 deletions(-) mode change 100644 => 100755 examples/helloswift/gradlew create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageModulesTest.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/TestUtils.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/KotlinSource.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftSource.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/RemotePackageBuilder.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependencyHandler.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt delete mode 100644 plugin/src/test/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt diff --git a/.gitignore b/.gitignore index e510fa9..5429390 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ captures .externalNativeBuild .cxx local.properties -xcuserdata \ No newline at end of file +xcuserdata +.kotlin diff --git a/examples/helloswift/build.gradle.kts b/examples/helloswift/build.gradle.kts index 73c8298..5118223 100644 --- a/examples/helloswift/build.gradle.kts +++ b/examples/helloswift/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") version "1.9.20" + kotlin("multiplatform") version "2.0.21" id("io.github.ttypic.swiftklib") } @@ -26,14 +26,10 @@ kotlin { } } - @Suppress("UNUSED_VARIABLE") - sourceSets { - val commonMain by getting - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } - } + applyDefaultHierarchyTemplate() + + sourceSets.commonTest.dependencies { + implementation(kotlin("test")) } } @@ -41,5 +37,12 @@ swiftklib { create("HelloSwift") { path = file("native/HelloSwift") packageName("com.ttypic.objclibs.greeting") + + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + versionRange("4.0.0", "5.0.0") + } + } } } diff --git a/examples/helloswift/gradle/wrapper/gradle-wrapper.properties b/examples/helloswift/gradle/wrapper/gradle-wrapper.properties index 9ffa2da..02ceea8 100644 --- a/examples/helloswift/gradle/wrapper/gradle-wrapper.properties +++ b/examples/helloswift/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Nov 05 13:59:35 MSK 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/examples/helloswift/gradlew b/examples/helloswift/gradlew old mode 100644 new mode 100755 diff --git a/examples/helloswift/native/HelloSwift/HelloSwift.swift b/examples/helloswift/native/HelloSwift/HelloSwift.swift index e35831f..f6dd6c5 100644 --- a/examples/helloswift/native/HelloSwift/HelloSwift.swift +++ b/examples/helloswift/native/HelloSwift/HelloSwift.swift @@ -1,4 +1,9 @@ import Foundation +import KeychainAccess + +@objc public class KeychainManager: NSObject { + private let keychain = Keychain(service: "test-service") +} @objc public class HelloWorld : NSObject { @objc public class func helloWorld() -> String { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd5d702..acfade3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] -kotlin = "2.0.0" +kotlin = "2.0.21" gradlePublishPlugin = "1.2.1" junit-jupiter = "5.8.0" kotest = "5.9.1" - +autonomousapps-testkit = "0.10" [libraries] plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -15,7 +15,10 @@ test-junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = test-junit-jupiter-launcher = { module = "org.junit.jupiter:junit-jupiter-engine" } test-kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } +test-autonomousapps-support = { module = "com.autonomousapps:gradle-testkit-support", version.ref = "autonomousapps-testkit" } +test-autonomousapps-truth = { module = "com.autonomousapps:gradle-testkit-truth", version.ref = "autonomousapps-testkit" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } gradle-publish = { id = "com.gradle.plugin-publish", version.ref = "gradlePublishPlugin" } +autonomousapps-testkit = { id = "com.autonomousapps.testkit", version.ref = "autonomousapps-testkit" } diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index dd7da3d..b696a2f 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -2,20 +2,30 @@ plugins { id("java-gradle-plugin") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.publish) + alias(libs.plugins.autonomousapps.testkit) } dependencies { implementation(gradleApi()) implementation(libs.plugin.kotlin) - testImplementation(gradleTestKit()) - testImplementation(libs.test.junit.jupiter) - testImplementation(libs.test.kotest.assertions) - testRuntimeOnly(libs.test.junit.jupiter.launcher) + functionalTestImplementation(libs.test.junit.jupiter) + functionalTestImplementation(libs.test.kotest.assertions) + functionalTestRuntimeOnly(libs.test.junit.jupiter.launcher) } -tasks.named("test") { +gradleTestKitSupport { + withSupportLibrary() + withTruthLibrary() +} + +tasks.named("functionalTest") { useJUnitPlatform() + systemProperty("com.autonomousapps.test.versions.kotlin", libs.versions.kotlin.get()) + + beforeTest(closureOf { + logger.lifecycle("Running test: $this") + }) } version = "0.7.0-SNAPSHOT" @@ -23,6 +33,11 @@ group = "io.github.ttypic" kotlin { jvmToolchain(17) + compilerOptions { + optIn.addAll( + "io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi" + ) + } } @Suppress("UnstableApiUsage") diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt new file mode 100644 index 0000000..d8e5c26 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt @@ -0,0 +1,53 @@ +package io.github.ttypic.swiftklib.gradle + +import com.autonomousapps.kit.GradleBuilder.build +import com.autonomousapps.kit.truth.TestKitTruth.Companion.assertThat +import io.github.ttypic.swiftklib.gradle.fixture.CInteropFixture +import io.github.ttypic.swiftklib.gradle.fixture.KotlinSource +import io.github.ttypic.swiftklib.gradle.fixture.SwiftSource +import org.junit.jupiter.api.Test + +class CinteropModulesTest { + + @Test + fun `build with imported UIKit framework is successful`() { + assumeMacos() + + // Given + val fixture = CInteropFixture( + swiftSource = SwiftSource.of( + content = """ + import UIKit + @objc public class TestView: UIView {} + """.trimIndent() + ), + kotlinSource = KotlinSource.of( + content = """ + package test + import test.TestView + val view = TestView() + """.trimIndent() + ) + ) + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + } + + @Test + fun `build on linux results in warning about unsupported OS`() { + assumeLinux() + + // Given + val fixture = CInteropFixture() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("Current host OS is not macOS. Disabling SwiftKlib plugin") + } +} 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 new file mode 100644 index 0000000..7bc69ce --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageModulesTest.kt @@ -0,0 +1,319 @@ +package io.github.ttypic.swiftklib.gradle + +import com.autonomousapps.kit.GradleBuilder.build +import com.autonomousapps.kit.GradleBuilder.buildAndFail +import com.autonomousapps.kit.truth.TestKitTruth.Companion.assertThat +import io.github.ttypic.swiftklib.gradle.fixture.SwiftPackageFixture +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.io.File + +class SwiftPackageModulesTest { + + @Test + fun `build with remote SPM dependency using exact version is successful`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + import KeychainAccess + + @objc public class KeychainManager: NSObject { + private let keychain = Keychain(service: "test-service") + + @objc public func save(value: String, forKey key: String) throws { + try keychain.set(value, key: key) + } + } + """.trimIndent(), + swiftklibConfig = """ + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + exactVersion("4.2.2") + } + } + """.trimIndent() + ) + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + val packageResolved = fixture.getPackageResolvedFile() + assertTrue(packageResolved.exists()) + assertTrue(packageResolved.readText().contains("KeychainAccess")) + } + + @Test + fun `build with remote SPM dependency using version range is successful`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + import KeychainAccess + + @objc public class KeychainManager: NSObject { + private let keychain = Keychain(service: "test-service") + } + """.trimIndent(), + swiftklibConfig = """ + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + versionRange("4.0.0", "5.0.0") + } + } + """.trimIndent() + ) + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + val packageResolved = fixture.getPackageResolvedFile() + assertTrue(packageResolved.exists()) + assertTrue(packageResolved.readText().contains("KeychainAccess")) + } + + @Test + fun `build with remote SPM dependency using from version is successful`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + import KeychainAccess + + @objc public class KeychainManager: NSObject { + private let keychain = Keychain(service: "test-service") + } + """.trimIndent(), + swiftklibConfig = """ + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + fromVersion("4.0.0") + } + } + """.trimIndent() + ) + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + val packageResolved = fixture.getPackageResolvedFile() + assertTrue(packageResolved.exists()) + assertTrue(packageResolved.readText().contains("KeychainAccess")) + } + + @Test + fun `build with remote SPM dependency using branch is successful`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + import KeychainAccess + + @objc public class KeychainManager: NSObject { + private let keychain = Keychain(service: "test-service") + } + """.trimIndent(), + swiftklibConfig = """ + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + branch("master") + } + } + """.trimIndent() + ) + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + val packageResolved = fixture.getPackageResolvedFile() + assertTrue(packageResolved.exists()) + assertTrue(packageResolved.readText().contains("KeychainAccess")) + } + + @Test + fun `build with local SPM dependency is successful`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + import LocalPackage + + @objc public class VersionProvider: NSObject { + @objc public class func getLocalVersion() -> String { + return LocalHelper.getVersion() + } + } + """.trimIndent(), + swiftklibConfig = """ + """.trimIndent(), + withLocalPackage = true + ) + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + } + + @Test + fun `build with multiple dependencies is successful`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + import KeychainAccess + import SwiftyJSON + + @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(), + swiftklibConfig = """ + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + exactVersion("4.2.2") + } + remote("SwiftyJSON") { + github("SwiftyJSON", "SwiftyJSON") + versionRange("5.0.0", "6.0.0") + } + } + """.trimIndent() + ) + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + + val packageResolved = fixture.getPackageResolvedFile() + assertTrue(packageResolved.exists(), "Package.resolved file not found") + + val content = packageResolved.readText() + assertTrue( + content.contains("\"identity\" : \"KeychainAccess\"", ignoreCase = true), + "KeychainAccess dependency not found" + ) + assertTrue( + content.contains("\"identity\" : \"SwiftyJSON\"", ignoreCase = true), + "SwiftyJSON dependency not found" + ) + } + + @Test + fun `build fails with blank package name`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + @objc public class TestClass: NSObject {} + """.trimIndent(), + swiftklibConfig = """ + dependencies { + remote("") { + github("example", "test") + exactVersion("1.0.0") + } + } + """.trimIndent() + ) + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("Package name cannot be blank") + } + + @Test + fun `build fails with blank version`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + @objc public class TestClass: NSObject {} + """.trimIndent(), + swiftklibConfig = """ + dependencies { + remote("Test") { + github("example", "test") + exactVersion("") + } + } + """.trimIndent() + ) + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("Version cannot be blank") + } + + @Test + fun `build fails with missing version specification`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + @objc public class TestClass: NSObject {} + """.trimIndent(), + swiftklibConfig = """ + dependencies { + remote("Test") { + github("example", "test") + } + } + """.trimIndent() + ) + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("No version specification provided for remote package Test") + } + + @Test + fun `build fails with nonexistent local package`() { + // Given + val fixture = SwiftPackageFixture( + swiftCode = """ + import Foundation + @objc public class TestClass: NSObject {} + """.trimIndent(), + swiftklibConfig = """ + dependencies { + local("LocalPackage", file("nonexistent/path")) + } + """.trimIndent() + ) + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("Package path must exist") + } +} + diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/TestUtils.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/TestUtils.kt new file mode 100644 index 0000000..1023025 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/TestUtils.kt @@ -0,0 +1,12 @@ +package io.github.ttypic.swiftklib.gradle + +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.condition.OS + +fun assumeMacos() { + assumeTrue(OS.MAC.isCurrentOs) +} + +fun assumeLinux() { + assumeTrue(OS.LINUX.isCurrentOs) +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt new file mode 100644 index 0000000..8ca0073 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt @@ -0,0 +1,97 @@ +package io.github.ttypic.swiftklib.gradle.fixture + +import com.autonomousapps.kit.AbstractGradleProject +import com.autonomousapps.kit.GradleProject +import com.autonomousapps.kit.RootProject +import com.autonomousapps.kit.Source +import com.autonomousapps.kit.Subproject +import com.autonomousapps.kit.gradle.Plugin +import org.intellij.lang.annotations.Language + +abstract class BaseSwiftKlibFixture( + protected val swiftklibName: String = "test", + protected val swiftklibPackage: String = "test" +) : AbstractGradleProject() { + + protected val pluginVersion = System.getProperty("com.autonomousapps.plugin-under-test.version") + private var _gradleProject: GradleProject? = null + + protected val swiftklibSrcPath = "src/main/swift/$swiftklibName" + + val gradleProject: GradleProject + get() = _gradleProject ?: buildProject().also { _gradleProject = it } + + protected abstract fun buildProject(): GradleProject + + protected fun Subproject.Builder.withSwiftSource(source: SwiftSource) { + withFile("$swiftklibSrcPath/${source.filename}", source.content) + } + + protected fun Subproject.Builder.withSwiftSources(vararg sources: SwiftSource) { + sources.forEach { withSwiftSource(it) } + } + + protected fun RootProject.Builder.withPackageSwift(source: PackageSource) { + withFile("$swiftklibName/Package.swift", source.content) + } + + protected fun Subproject.Builder.withKotlinSource(source: KotlinSource): Source = + Source.kotlin(source.content) + .withPath(source.packageName, source.className) + .build().apply { + sources.add(this) + } + + protected fun Subproject.Builder.withDefaultKotlinSource(): Source = + withKotlinSource(KotlinSource.default()) + + protected fun Subproject.Builder.withBaseGradleSetup( + @Language("kotlin") additionalConfig: String = "", + @Language("kotlin") swiftklibConfig: String = "" + ) { + withBuildScript { + plugins( + Plugin.kotlinMultiplatform, + Plugin("io.github.ttypic.swiftklib", pluginVersion) + ) + withKotlin( + """ + @file:OptIn(io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi::class) + kotlin { + compilerOptions { + optIn.addAll("kotlinx.cinterop.ExperimentalForeignApi") + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.compilations { + val main by getting { + cinterops.create("$swiftklibName") + } + } + } + + $additionalConfig + } + + swiftklib { + create("$swiftklibName") { + path = file("$swiftklibSrcPath") + packageName("$swiftklibPackage") + + $swiftklibConfig + } + } + """.trimIndent() + ) + } + } + + companion object { + val Plugin.Companion.kotlinMultiplatform + get() = Plugin.of("org.jetbrains.kotlin.multiplatform", "2.0.21") + } +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt new file mode 100644 index 0000000..b97e145 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt @@ -0,0 +1,45 @@ +package io.github.ttypic.swiftklib.gradle.fixture + +import com.autonomousapps.kit.GradleProject +import com.autonomousapps.kit.GradleProject.DslKind +import com.autonomousapps.kit.Source + +class CInteropFixture( + private val swiftSource: SwiftSource? = null, + private val kotlinSource: KotlinSource? = null, + swiftklibName: String = "test", + swiftklibPackage: String = "test" +) : BaseSwiftKlibFixture(swiftklibName, swiftklibPackage) { + + override fun buildProject(): GradleProject { + return newGradleProjectBuilder(DslKind.KOTLIN) + .withRootProject { + withFile( + "gradle.properties", """ + kotlin.mpp.enableCInteropCommonization=true + """.trimIndent() + ) + } + .withSubproject("library") { + withSources() + withBaseGradleSetup() + } + .write() + } + + private fun com.autonomousapps.kit.Subproject.Builder.withSources() { + val sources = mutableListOf() + + swiftSource?.let { + withSwiftSource(it) + } + + if (kotlinSource != null) { + sources.add(withKotlinSource(kotlinSource)) + } else { + sources.add(withDefaultKotlinSource()) + } + + this.sources.addAll(sources) + } +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/KotlinSource.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/KotlinSource.kt new file mode 100644 index 0000000..9d76363 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/KotlinSource.kt @@ -0,0 +1,22 @@ +package io.github.ttypic.swiftklib.gradle.fixture + +import org.intellij.lang.annotations.Language + +class KotlinSource private constructor( + val packageName: String, + val className: String, + @Language("kotlin") val content: String +) { + companion object { + fun of( + packageName: String = "com.example", + className: String = "Test", + @Language("kotlin") content: String + ): KotlinSource = KotlinSource(packageName, className, content) + + fun default() = of(content = """ + package com.example + class Test + """.trimIndent()) + } +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt new file mode 100644 index 0000000..ce3a408 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt @@ -0,0 +1,27 @@ +package io.github.ttypic.swiftklib.gradle.fixture + +import org.intellij.lang.annotations.Language + +class PackageSource private constructor( + @Language("Swift") val content: String +) { + companion object { + fun packageSwift(@Language("Swift") content: String): PackageSource = + PackageSource(content) + + fun defaultPackage(name: String) = packageSwift(""" + // swift-tools-version:5.6 + import PackageDescription + + let package = Package( + name: "$name", + products: [ + .library(name: "$name", targets: ["$name"]), + ], + targets: [ + .target(name: "$name") + ] + ) + """.trimIndent()) + } +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt new file mode 100644 index 0000000..094752a --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt @@ -0,0 +1,72 @@ +package io.github.ttypic.swiftklib.gradle.fixture + +import com.autonomousapps.kit.GradleProject +import com.autonomousapps.kit.GradleProject.DslKind +import com.autonomousapps.kit.RootProject +import org.intellij.lang.annotations.Language +import java.io.File + +class SwiftPackageFixture( + @Language("swift") private val swiftCode: String, + @Language("kotlin") private val swiftklibConfig: String = "", + swiftklibName: String = "test", + private val withLocalPackage: Boolean = false, +) : BaseSwiftKlibFixture(swiftklibName) { + + override fun buildProject(): GradleProject { + return newGradleProjectBuilder(DslKind.KOTLIN) + .withRootProject { + if (withLocalPackage) { + withLocalPackage() + } + withFile( + "gradle.properties", """ + kotlin.mpp.enableCInteropCommonization=true + """.trimIndent() + ) + } + .withSubproject("library") { + sources.add(withDefaultKotlinSource()) + withSwiftSource(SwiftSource.of(content = swiftCode)) + withBaseGradleSetup( + swiftklibConfig = """ + $swiftklibConfig + ${getLocalPackageConfig()} + """.trimIndent() + ) + } + .write() + } + + private fun getLocalPackageConfig(): String { + if (!withLocalPackage) return "" + return """ + dependencies { + local("LocalPackage", rootProject.file("LocalPackage")) + } + """.trimIndent() + } + + private fun RootProject.Builder.withLocalPackage() { + withFile( + "LocalPackage/Package.swift", + PackageSource.defaultPackage("LocalPackage").content + ) + + withFile( + "LocalPackage/Sources/LocalPackage/LocalPackage.swift", + SwiftSource.of( + content = """ + import Foundation + + @objc public class LocalHelper: NSObject { + @objc public class func getVersion() -> String { return "1.0.0" } + } + """.trimIndent() + ).content + ) + } + + fun getPackageResolvedFile(): File = + File(gradleProject.rootDir, "library/build/swiftklib/$swiftklibName/iosArm64/swiftBuild/Package.resolved") +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftSource.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftSource.kt new file mode 100644 index 0000000..9bbca80 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftSource.kt @@ -0,0 +1,14 @@ +package io.github.ttypic.swiftklib.gradle.fixture + +import org.intellij.lang.annotations.Language + +class SwiftSource private constructor( + val filename: String, + @Language("Swift") val content: String +) { + companion object { + fun of(filename: String = "Test.swift", @Language("Swift") content: String): SwiftSource = + SwiftSource(filename, content) + } +} + 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 new file mode 100644 index 0000000..01bdc9c --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/RemotePackageBuilder.kt @@ -0,0 +1,79 @@ +package io.github.ttypic.swiftklib.gradle + +import io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import javax.inject.Inject + +@ExperimentalSwiftklibApi +class RemotePackageBuilder @Inject constructor( + private val objects: ObjectFactory, + private val name: String +) { + private val urlProperty: Property = objects.property(String::class.java) + private var dependency: SwiftPackageDependency.Remote? = null + private set + + @ExperimentalSwiftklibApi + fun github(owner: String, repo: String) { + require(owner.isNotBlank()) { "Owner cannot be blank" } + require(repo.isNotBlank()) { "Repo cannot be blank" } + + urlProperty.set("https://github.com/$owner/$repo.git") + } + + @ExperimentalSwiftklibApi + fun url(url: String) { + require(url.isNotBlank()) { "URL cannot be blank" } + urlProperty.set(url) + } + + @ExperimentalSwiftklibApi + fun exactVersion(version: String) { + dependency = SwiftPackageDependency.Remote.ExactVersion( + name = name, + url = requireUrl(), + version = version + ) + } + + @ExperimentalSwiftklibApi + fun versionRange( + from: String, + to: String, + inclusive: Boolean = true + ) { + dependency = SwiftPackageDependency.Remote.VersionRange( + name = name, + url = requireUrl(), + from = from, + to = to, + inclusive = inclusive + ) + } + + @ExperimentalSwiftklibApi + fun branch(branchName: String) { + dependency = SwiftPackageDependency.Remote.Branch( + name = name, + url = requireUrl(), + branchName = branchName + ) + } + + @ExperimentalSwiftklibApi + fun fromVersion(version: String) { + dependency = SwiftPackageDependency.Remote.FromVersion( + name = name, + url = requireUrl(), + version = version + ) + } + + @ExperimentalSwiftklibApi + internal fun build(): SwiftPackageDependency.Remote? = dependency + + private fun requireUrl(): String = + urlProperty.orNull + ?: throw IllegalStateException("URL must be set via github() or url() before specifying version") +} diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt index ebd0050..a4ccc51 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt @@ -1,21 +1,34 @@ package io.github.ttypic.swiftklib.gradle +import io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi +import org.gradle.api.Action import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import java.io.File import javax.inject.Inject abstract class SwiftKlibEntry @Inject constructor( val name: String, - objects: ObjectFactory, + val objects: ObjectFactory, ) { - val path: Property = objects.property(File::class.java) val packageName: Property = objects.property(String::class.java) - val minIos: Property = objects.property(Int::class.java) - val minMacos: Property = objects.property(Int::class.java) - val minTvos: Property = objects.property(Int::class.java) - val minWatchos: Property = objects.property(Int::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) + + internal val dependencies: ListProperty = + objects.listProperty(SwiftPackageDependency::class.java) fun packageName(name: String) = packageName.set(name) + + @ExperimentalSwiftklibApi + fun dependencies(action: Action) { + val handler = SwiftPackageDependencyHandler(objects) + action.execute(handler) + dependencies.set(handler.dependencies) + } } + 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 9c93385..12b8ffd 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 @@ -40,6 +40,7 @@ class SwiftKlibPlugin : Plugin { tasks.register( taskName, CompileSwiftTask::class.java, + project.hasProperty("swiftklibDebug"), name, target, buildDir, @@ -49,7 +50,9 @@ class SwiftKlibPlugin : Plugin { entry.minMacos, entry.minTvos, entry.minWatchos, - ) + ).configure { + it.dependenciesProperty = entry.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 new file mode 100644 index 0000000..96015a4 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt @@ -0,0 +1,77 @@ +package io.github.ttypic.swiftklib.gradle + +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import java.io.File +import java.io.Serializable + +internal sealed interface SwiftPackageDependency : Serializable { + @get:Input + val name: String + + data class Local( + @Input override val name: String, + @InputDirectory val path: File + ) : SwiftPackageDependency { + init { + require(name.isNotBlank()) { "Package name cannot be blank" } + require(path.exists()) { "Package path must exist: $path" } + } + } + + sealed interface Remote : SwiftPackageDependency { + @get:Input + val url: String + + data class ExactVersion( + @Input override val name: String, + @Input override val url: String, + @Input val version: String + ) : Remote { + init { + require(name.isNotBlank()) { "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 url: String, + @Input val from: String, + @Input val to: String, + @Input val inclusive: Boolean = true + ) : Remote { + init { + require(name.isNotBlank()) { "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" } + } + } + + data class Branch( + @Input override val name: String, + @Input override val url: String, + @Input val branchName: String + ) : Remote { + init { + require(name.isNotBlank()) { "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 url: String, + @Input val version: String + ) : Remote { + init { + require(name.isNotBlank()) { "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/SwiftPackageDependencyHandler.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependencyHandler.kt new file mode 100644 index 0000000..13b7235 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependencyHandler.kt @@ -0,0 +1,40 @@ +package io.github.ttypic.swiftklib.gradle + +import io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Nested +import java.io.File +import javax.inject.Inject + +@ExperimentalSwiftklibApi +class SwiftPackageDependencyHandler @Inject constructor( + private val objects: ObjectFactory +) { + private val _dependencies = objects.listProperty(SwiftPackageDependency::class.java).convention(emptyList()) + + @get:Nested + internal val dependencies: ListProperty = _dependencies + + @ExperimentalSwiftklibApi + fun local(name: String, path: File) { + val currentDeps = _dependencies.get().toMutableList() + currentDeps.add(SwiftPackageDependency.Local(name, path)) + _dependencies.set(currentDeps) + } + + @ExperimentalSwiftklibApi + fun remote(name: String, block: RemotePackageBuilder.() -> Unit) { + val builder = RemotePackageBuilder(objects, name) + builder.apply(block) + + val dependency = builder.build() + ?: throw IllegalStateException("No version specification provided for remote package $name") + + val currentDeps = _dependencies.get().toMutableList() + currentDeps.add(dependency) + _dependencies.set(currentDeps) + } +} + + 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 new file mode 100644 index 0000000..4d132ca --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt @@ -0,0 +1,7 @@ +package io.github.ttypic.swiftklib.gradle.api + +@RequiresOptIn(message = "This API is experimental. It may be changed in the future without notice.") +@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/task/CompileSwiftTask.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt index 042c621..aa75cc2 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 @@ -2,13 +2,16 @@ package io.github.ttypic.swiftklib.gradle.task import io.github.ttypic.swiftklib.gradle.CompileTarget import io.github.ttypic.swiftklib.gradle.EXTENSION_NAME +import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency import io.github.ttypic.swiftklib.gradle.templates.createPackageSwiftContents import io.github.ttypic.swiftklib.gradle.util.StringReplacingOutputStream import org.gradle.api.DefaultTask +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile @@ -21,6 +24,7 @@ import java.security.MessageDigest import javax.inject.Inject abstract class CompileSwiftTask @Inject constructor( + @Input val printDebug: Boolean, @Input val cinteropName: String, @Input val compileTarget: CompileTarget, @Input val buildDirectory: String, @@ -32,6 +36,10 @@ abstract class CompileSwiftTask @Inject constructor( @Optional @Input val minWatchosProperty: Property, ) : DefaultTask() { + @get:Optional + @get:Nested + internal abstract var dependenciesProperty: ListProperty + @get:Internal internal val targetDir: File get() { @@ -52,9 +60,16 @@ abstract class CompileSwiftTask @Inject constructor( @TaskAction fun produce() { val packageName: String = packageNameProperty.get() + val dependencies = dependenciesProperty.getOrElse(emptyList()) prepareBuildDirectory() - createPackageSwift() + createPackageSwift(dependencies) + + // Only resolve if we have dependencies + if (dependencies.isNotEmpty()) { + resolveSwiftPackages() + } + val xcodeMajorVersion = readXcodeMajorVersion() val (libPath, headerPath) = buildSwift(xcodeMajorVersion) @@ -87,12 +102,30 @@ abstract class CompileSwiftTask @Inject constructor( private fun buildDir() = File(swiftBuildDir, cinteropName) - /** - * Creates `Package.Swift` file for the library - */ - private fun createPackageSwift() { + private fun resolveSwiftPackages() { + logger.info("Resolving Swift Package dependencies...") + + val result = execOperations.exec { + it.executable = "xcrun" + it.workingDir = swiftBuildDir + it.args = listOf("swift", "package", "resolve") + it.isIgnoreExitValue = true + } + + if (result.exitValue != 0) { + throw RuntimeException("Failed to resolve Swift Package dependencies") + } + } + + private fun createPackageSwift(dependencies: List) { + val packageSwiftContents = createPackageSwiftContents(cinteropName, dependencies) + if (printDebug) { + logger.warn("======== Package.swift contents ========") + logger.warn(packageSwiftContents) + logger.warn("======== | Package.swift contents | ========") + } File(swiftBuildDir, "Package.swift") - .writeText(createPackageSwiftContents(cinteropName)) + .writeText(packageSwiftContents) } private fun buildSwift(xcodeVersion: Int): SwiftBuildResult { 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 4478bd6..a5d8dba 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,9 +1,12 @@ package io.github.ttypic.swiftklib.gradle.templates +import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency + internal fun createPackageSwiftContents( cinteropName: String, + dependencies: Collection ): String = """ - // swift-tools-version:5.5 + // swift-tools-version:5.6 import PackageDescription let package = Package( @@ -14,12 +17,46 @@ internal fun createPackageSwiftContents( type: .static, targets: ["$cinteropName"]) ], - dependencies: [], + dependencies: [ + ${dependencies.joinToString(",\n ") { it.toSwiftPackageDeclaration() }} + ], targets: [ .target( name: "$cinteropName", - dependencies: [], + dependencies: [ + ${dependencies.joinToString(",\n ") { "\"${it.name}\"" }} + ], path: "$cinteropName") ] ) """.trimIndent() + + +internal fun SwiftPackageDependency.toSwiftPackageDeclaration(): String = when (this) { + is SwiftPackageDependency.Local -> + """ + .package(path: "${path.absolutePath}") + """.trimIndent() + + is SwiftPackageDependency.Remote.ExactVersion -> + """ + .package(url: "$url", exact: "$version") + """.trimIndent() + + is SwiftPackageDependency.Remote.VersionRange -> { + val operator = if (inclusive) "..." else "..<" + """ + .package(url: "$url", "$from"$operator"$to") + """.trimIndent() + } + + is SwiftPackageDependency.Remote.Branch -> + """ + .package(url: "$url", branch: "$branchName") + """.trimIndent() + + is SwiftPackageDependency.Remote.FromVersion -> + """ + .package(url: "$url", from: "$version") + """.trimIndent() +} diff --git a/plugin/src/test/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt b/plugin/src/test/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt deleted file mode 100644 index dd1d6a5..0000000 --- a/plugin/src/test/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt +++ /dev/null @@ -1,159 +0,0 @@ -package io.github.ttypic.swiftklib.gradle - -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import io.kotest.matchers.string.shouldContain -import org.gradle.testkit.runner.BuildResult -import org.gradle.testkit.runner.GradleRunner -import org.gradle.testkit.runner.TaskOutcome -import org.intellij.lang.annotations.Language -import org.junit.jupiter.api.Assumptions.assumeTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.condition.OS -import org.junit.jupiter.api.io.TempDir -import java.io.File - -class CinteropModulesTest { - @TempDir - lateinit var testProjectDir: File - private lateinit var settingsFile: File - private lateinit var buildFile: File - private lateinit var swiftLocation: File - private lateinit var swiftCodeFile: File - private lateinit var kotlinLocation: File - private lateinit var kotlinCodeFile: File - private lateinit var gradlePropertiesFile: File - - @BeforeEach - fun setup() { - settingsFile = File(testProjectDir, "settings.gradle.kts") - buildFile = File(testProjectDir, "build.gradle.kts") - swiftLocation = File(testProjectDir, "swift") - swiftCodeFile = File(swiftLocation, "test.swift") - kotlinLocation = File(testProjectDir, "src/commonMain/kotlin/test") - kotlinCodeFile = File(kotlinLocation, "Test.kt") - gradlePropertiesFile = File(testProjectDir, "gradle.properties") - } - - @Test - fun `build with imported UIKit framework is successful`() { - assumeMacos() - - testBuild( - swiftCode = """ - import UIKit - - @objc public class TestView: UIView {} - """.trimIndent(), - kotlinCode = """ - import test.TestView - - val view = TestView() - """.trimIndent(), - ) { - task(":build") - .shouldNotBeNull() - .outcome.shouldBe(TaskOutcome.SUCCESS) - } - } - - - @Test - fun `build on linux results in warning about unsupported OS`() { - assumeLinux() - testBuild { - output.shouldContain("Current host OS is not macOS. Disabling SwiftKlib plugin") - } - } - - private fun assumeMacos() { - assumeTrue(OS.MAC.isCurrentOs) - } - - private fun assumeLinux() { - assumeTrue(OS.LINUX.isCurrentOs) - } - - private fun testBuild( - @Language("swift") - swiftCode: String? = null, - @Language("kotlin") - kotlinCode: String? = null, - swiftklibName: String = "test", - swiftklibPackage: String = "test", - asserter: BuildResult.() -> Unit, - ) { - gradlePropertiesFile.writeText( - """ - kotlin.mpp.enableCInteropCommonization=true - """.trimIndent() - ) - @Language("kotlin") - val settingsKts = """ - pluginManagement { - includeBuild("..") - } - - dependencyResolutionManagement { - repositories { - mavenCentral() - } - } - """.trimIndent() - settingsFile.writeText(settingsKts) - - @Language("kotlin") - val buildKts = """ - plugins { - embeddedKotlin("multiplatform") - id("io.github.ttypic.swiftklib") - } - - kotlin { - compilerOptions { - optIn.addAll( - "kotlinx.cinterop.ExperimentalForeignApi", - ) - } - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64(), - ).forEach { - it.compilations { - val main by getting { - cinterops.create("$swiftklibName") - } - } - } - } - - swiftklib { - create("$swiftklibName") { - path = file("${swiftLocation.absolutePath}") - packageName("$swiftklibPackage") - } - } - """.trimIndent() - buildFile.writeText(buildKts) - - swiftLocation.mkdirs() - kotlinLocation.mkdirs() - - if (swiftCode != null) { - swiftCodeFile.writeText(swiftCode) - } - if (kotlinCode != null) { - kotlinCodeFile.writeText(kotlinCode) - } - - GradleRunner.create() - .withProjectDir(testProjectDir) - .withArguments("build") - .withPluginClasspath() - .build() - .asserter() - } -} From 147dca982c3ffc6a663234449f12d0fd932fa85d Mon Sep 17 00:00:00 2001 From: Ilya Gulya Date: Tue, 29 Oct 2024 22:40:33 +0500 Subject: [PATCH 02/25] extract plugin public api and make test fixtures use it in tests --- plugin/build.gradle.kts | 1 + .../swiftklib/gradle/CinteropModulesTest.kt | 45 ++- .../gradle/SwiftPackageModulesTest.kt | 313 ++++++++--------- .../gradle/fixture/BaseSwiftKlibFixture.kt | 97 ----- .../gradle/fixture/CInteropFixture.kt | 45 --- .../swiftklib/gradle/fixture/PackageSource.kt | 27 -- .../gradle/fixture/SwiftKlibTestFixture.kt | 332 ++++++++++++++++++ .../gradle/fixture/SwiftPackageFixture.kt | 72 ---- .../ttypic/swiftklib/gradle/SwiftKlibEntry.kt | 34 -- .../swiftklib/gradle/SwiftKlibEntryImpl.kt | 49 +++ .../swiftklib/gradle/SwiftKlibPlugin.kt | 31 +- .../gradle/SwiftPackageDependencyHandler.kt | 40 --- .../gradle/api/RemotePackageConfiguration.kt | 38 ++ .../swiftklib/gradle/api/SwiftKlibEntry.kt | 18 + .../gradle/api/SwiftPackageConfiguration.kt | 19 + .../RemotePackageConfigurationImpl.kt | 65 ++++ .../internal/SwiftPackageConfigurationImpl.kt | 42 +++ 17 files changed, 768 insertions(+), 500 deletions(-) delete mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt delete mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt delete mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt create mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt delete mode 100644 plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt delete mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt delete mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependencyHandler.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt create mode 100644 plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/SwiftPackageConfigurationImpl.kt diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index b696a2f..4eaf068 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { functionalTestImplementation(libs.test.junit.jupiter) functionalTestImplementation(libs.test.kotest.assertions) + functionalTestImplementation(project(":plugin")) functionalTestRuntimeOnly(libs.test.junit.jupiter.launcher) } diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt index d8e5c26..e623eee 100644 --- a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt @@ -2,8 +2,8 @@ package io.github.ttypic.swiftklib.gradle import com.autonomousapps.kit.GradleBuilder.build import com.autonomousapps.kit.truth.TestKitTruth.Companion.assertThat -import io.github.ttypic.swiftklib.gradle.fixture.CInteropFixture import io.github.ttypic.swiftklib.gradle.fixture.KotlinSource +import io.github.ttypic.swiftklib.gradle.fixture.SwiftKlibTestFixture import io.github.ttypic.swiftklib.gradle.fixture.SwiftSource import org.junit.jupiter.api.Test @@ -14,21 +14,25 @@ class CinteropModulesTest { assumeMacos() // Given - val fixture = CInteropFixture( - swiftSource = SwiftSource.of( - content = """ - import UIKit - @objc public class TestView: UIView {} - """.trimIndent() - ), - kotlinSource = KotlinSource.of( - content = """ - package test - import test.TestView - val view = TestView() - """.trimIndent() + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of( + content = """ + import UIKit + @objc public class TestView: UIView {} + """.trimIndent() + ) ) - ) + .withKotlinSources( + KotlinSource.of( + content = """ + package test + import test.TestView + val view = TestView() + """.trimIndent() + ) + ) + .build() // When val result = build(fixture.gradleProject.rootDir, "build") @@ -42,7 +46,16 @@ class CinteropModulesTest { assumeLinux() // Given - val fixture = CInteropFixture() + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of( + content = """ + import Foundation + @objc public class TestClass: NSObject {} + """.trimIndent() + ) + ) + .build() // When val result = build(fixture.gradleProject.rootDir, "build") 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 7bc69ce..1f5cd5c 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 @@ -3,7 +3,8 @@ package io.github.ttypic.swiftklib.gradle import com.autonomousapps.kit.GradleBuilder.build import com.autonomousapps.kit.GradleBuilder.buildAndFail import com.autonomousapps.kit.truth.TestKitTruth.Companion.assertThat -import io.github.ttypic.swiftklib.gradle.fixture.SwiftPackageFixture +import io.github.ttypic.swiftklib.gradle.fixture.SwiftKlibTestFixture +import io.github.ttypic.swiftklib.gradle.fixture.SwiftSource import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.io.File @@ -13,140 +14,138 @@ class SwiftPackageModulesTest { @Test fun `build with remote SPM dependency using exact version is successful`() { // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - import KeychainAccess - - @objc public class KeychainManager: NSObject { - private let keychain = Keychain(service: "test-service") - - @objc public func save(value: String, forKey key: String) throws { - try keychain.set(value, key: key) + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import Foundation + import KeychainAccess + + @objc public class KeychainManager: NSObject { + private let keychain = Keychain(service: "test-service") + + @objc public func save(value: String, forKey key: String) throws { + try keychain.set(value, key: key) + } } - } - """.trimIndent(), - swiftklibConfig = """ + """.trimIndent()) + ) + .withConfiguration { dependencies { remote("KeychainAccess") { github("kishikawakatsumi", "KeychainAccess") exactVersion("4.2.2") } } - """.trimIndent() - ) + } + .build() // When val result = build(fixture.gradleProject.rootDir, "build") // Then assertThat(result).task(":library:build").succeeded() - val packageResolved = fixture.getPackageResolvedFile() - assertTrue(packageResolved.exists()) - assertTrue(packageResolved.readText().contains("KeychainAccess")) + assertPackageResolved(fixture, "KeychainAccess") } @Test fun `build with remote SPM dependency using version range is successful`() { - // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - import KeychainAccess - - @objc public class KeychainManager: NSObject { - private let keychain = Keychain(service: "test-service") - } - """.trimIndent(), - swiftklibConfig = """ - dependencies { - remote("KeychainAccess") { - github("kishikawakatsumi", "KeychainAccess") - versionRange("4.0.0", "5.0.0") + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import Foundation + import KeychainAccess + + @objc public class KeychainManager: NSObject { + private let keychain = Keychain(service: "test-service") } - } - """.trimIndent() - ) - - // When - val result = build(fixture.gradleProject.rootDir, "build") - - // Then - assertThat(result).task(":library:build").succeeded() - val packageResolved = fixture.getPackageResolvedFile() - assertTrue(packageResolved.exists()) - assertTrue(packageResolved.readText().contains("KeychainAccess")) - } - - @Test - fun `build with remote SPM dependency using from version is successful`() { - // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - import KeychainAccess - - @objc public class KeychainManager: NSObject { - private let keychain = Keychain(service: "test-service") - } - """.trimIndent(), - swiftklibConfig = """ + """.trimIndent()) + ) + .withConfiguration { dependencies { remote("KeychainAccess") { github("kishikawakatsumi", "KeychainAccess") - fromVersion("4.0.0") + versionRange("4.0.0", "5.0.0", true) } } - """.trimIndent() - ) + } + .build() // When val result = build(fixture.gradleProject.rootDir, "build") // Then assertThat(result).task(":library:build").succeeded() - val packageResolved = fixture.getPackageResolvedFile() - assertTrue(packageResolved.exists()) - assertTrue(packageResolved.readText().contains("KeychainAccess")) + assertPackageResolved(fixture, "KeychainAccess") } @Test fun `build with remote SPM dependency using branch is successful`() { - // Given - val fixture = SwiftPackageFixture( - swiftCode = """ + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ import Foundation import KeychainAccess @objc public class KeychainManager: NSObject { private let keychain = Keychain(service: "test-service") } - """.trimIndent(), - swiftklibConfig = """ + """.trimIndent()) + ) + .withConfiguration { dependencies { remote("KeychainAccess") { github("kishikawakatsumi", "KeychainAccess") branch("master") } } - """.trimIndent() - ) + } + .build() // When val result = build(fixture.gradleProject.rootDir, "build") // Then assertThat(result).task(":library:build").succeeded() - val packageResolved = fixture.getPackageResolvedFile() - assertTrue(packageResolved.exists()) - assertTrue(packageResolved.readText().contains("KeychainAccess")) + assertPackageResolved(fixture, "KeychainAccess") } @Test fun `build with local SPM dependency is successful`() { // Given - val fixture = SwiftPackageFixture( - swiftCode = """ + val localPackageDir = File(createTempDir(), "LocalPackage").apply { + mkdirs() + // Create Package.swift + File(this, "Package.swift").writeText(""" + // swift-tools-version:5.3 + import PackageDescription + + let package = Package( + name: "LocalPackage", + products: [ + .library(name: "LocalPackage", targets: ["LocalPackage"]), + ], + targets: [ + .target(name: "LocalPackage"), + ] + ) + """.trimIndent()) + + // Create source files + File(this, "Sources/LocalPackage").mkdirs() + File(this, "Sources/LocalPackage/LocalHelper.swift").writeText(""" + import Foundation + + @objc public class LocalHelper: NSObject { + @objc public class func getVersion() -> String { + return "1.0.0" + } + } + """.trimIndent()) + } + + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ import Foundation import LocalPackage @@ -155,38 +154,46 @@ class SwiftPackageModulesTest { return LocalHelper.getVersion() } } - """.trimIndent(), - swiftklibConfig = """ - """.trimIndent(), - withLocalPackage = true - ) - - // When - val result = build(fixture.gradleProject.rootDir, "build") - - // Then - assertThat(result).task(":library:build").succeeded() + """.trimIndent()) + ) + .withConfiguration { + dependencies { + local("LocalPackage", localPackageDir) + } + } + .build() + + try { + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + } finally { + localPackageDir.deleteRecursively() + } } @Test fun `build with multiple dependencies is successful`() { - // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - import KeychainAccess - import SwiftyJSON - - @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 + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import Foundation + import KeychainAccess + import SwiftyJSON + + @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(), - swiftklibConfig = """ + """.trimIndent()) + ) + .withConfiguration { dependencies { remote("KeychainAccess") { github("kishikawakatsumi", "KeychainAccess") @@ -194,49 +201,33 @@ class SwiftPackageModulesTest { } remote("SwiftyJSON") { github("SwiftyJSON", "SwiftyJSON") - versionRange("5.0.0", "6.0.0") + versionRange("5.0.0", "6.0.0", true) } } - """.trimIndent() - ) + } + .build() // When val result = build(fixture.gradleProject.rootDir, "build") // Then assertThat(result).task(":library:build").succeeded() - - val packageResolved = fixture.getPackageResolvedFile() - assertTrue(packageResolved.exists(), "Package.resolved file not found") - - val content = packageResolved.readText() - assertTrue( - content.contains("\"identity\" : \"KeychainAccess\"", ignoreCase = true), - "KeychainAccess dependency not found" - ) - assertTrue( - content.contains("\"identity\" : \"SwiftyJSON\"", ignoreCase = true), - "SwiftyJSON dependency not found" - ) + assertPackageResolved(fixture, "KeychainAccess", "SwiftyJSON") } @Test fun `build fails with blank package name`() { // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - @objc public class TestClass: NSObject {} - """.trimIndent(), - swiftklibConfig = """ + val fixture = SwiftKlibTestFixture.builder() + .withConfiguration { dependencies { - remote("") { + remote("") { // Empty package name github("example", "test") exactVersion("1.0.0") } } - """.trimIndent() - ) + } + .build() // When val result = buildAndFail(fixture.gradleProject.rootDir, "build") @@ -248,20 +239,16 @@ class SwiftPackageModulesTest { @Test fun `build fails with blank version`() { // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - @objc public class TestClass: NSObject {} - """.trimIndent(), - swiftklibConfig = """ + val fixture = SwiftKlibTestFixture.builder() + .withConfiguration { dependencies { remote("Test") { github("example", "test") - exactVersion("") + exactVersion("") // Empty version } } - """.trimIndent() - ) + } + .build() // When val result = buildAndFail(fixture.gradleProject.rootDir, "build") @@ -273,19 +260,16 @@ class SwiftPackageModulesTest { @Test fun `build fails with missing version specification`() { // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - @objc public class TestClass: NSObject {} - """.trimIndent(), - swiftklibConfig = """ + val fixture = SwiftKlibTestFixture.builder() + .withConfiguration { dependencies { remote("Test") { github("example", "test") + // No version specified } } - """.trimIndent() - ) + } + .build() // When val result = buildAndFail(fixture.gradleProject.rootDir, "build") @@ -297,17 +281,14 @@ class SwiftPackageModulesTest { @Test fun `build fails with nonexistent local package`() { // Given - val fixture = SwiftPackageFixture( - swiftCode = """ - import Foundation - @objc public class TestClass: NSObject {} - """.trimIndent(), - swiftklibConfig = """ + val nonexistentPath = File("nonexistent/path") + val fixture = SwiftKlibTestFixture.builder() + .withConfiguration { dependencies { - local("LocalPackage", file("nonexistent/path")) + local("LocalPackage", nonexistentPath) } - """.trimIndent() - ) + } + .build() // When val result = buildAndFail(fixture.gradleProject.rootDir, "build") @@ -315,5 +296,21 @@ class SwiftPackageModulesTest { // Then assertThat(result).output().contains("Package path must exist") } -} + + private fun assertPackageResolved(fixture: SwiftKlibTestFixture, vararg packageNames: String) { + val resolvedFile = File( + fixture.gradleProject.rootDir, + "library/build/swiftklib/test/iosArm64/swiftBuild/Package.resolved" + ) + 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" + ) + } + } +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt deleted file mode 100644 index 8ca0073..0000000 --- a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/BaseSwiftKlibFixture.kt +++ /dev/null @@ -1,97 +0,0 @@ -package io.github.ttypic.swiftklib.gradle.fixture - -import com.autonomousapps.kit.AbstractGradleProject -import com.autonomousapps.kit.GradleProject -import com.autonomousapps.kit.RootProject -import com.autonomousapps.kit.Source -import com.autonomousapps.kit.Subproject -import com.autonomousapps.kit.gradle.Plugin -import org.intellij.lang.annotations.Language - -abstract class BaseSwiftKlibFixture( - protected val swiftklibName: String = "test", - protected val swiftklibPackage: String = "test" -) : AbstractGradleProject() { - - protected val pluginVersion = System.getProperty("com.autonomousapps.plugin-under-test.version") - private var _gradleProject: GradleProject? = null - - protected val swiftklibSrcPath = "src/main/swift/$swiftklibName" - - val gradleProject: GradleProject - get() = _gradleProject ?: buildProject().also { _gradleProject = it } - - protected abstract fun buildProject(): GradleProject - - protected fun Subproject.Builder.withSwiftSource(source: SwiftSource) { - withFile("$swiftklibSrcPath/${source.filename}", source.content) - } - - protected fun Subproject.Builder.withSwiftSources(vararg sources: SwiftSource) { - sources.forEach { withSwiftSource(it) } - } - - protected fun RootProject.Builder.withPackageSwift(source: PackageSource) { - withFile("$swiftklibName/Package.swift", source.content) - } - - protected fun Subproject.Builder.withKotlinSource(source: KotlinSource): Source = - Source.kotlin(source.content) - .withPath(source.packageName, source.className) - .build().apply { - sources.add(this) - } - - protected fun Subproject.Builder.withDefaultKotlinSource(): Source = - withKotlinSource(KotlinSource.default()) - - protected fun Subproject.Builder.withBaseGradleSetup( - @Language("kotlin") additionalConfig: String = "", - @Language("kotlin") swiftklibConfig: String = "" - ) { - withBuildScript { - plugins( - Plugin.kotlinMultiplatform, - Plugin("io.github.ttypic.swiftklib", pluginVersion) - ) - withKotlin( - """ - @file:OptIn(io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi::class) - kotlin { - compilerOptions { - optIn.addAll("kotlinx.cinterop.ExperimentalForeignApi") - } - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.compilations { - val main by getting { - cinterops.create("$swiftklibName") - } - } - } - - $additionalConfig - } - - swiftklib { - create("$swiftklibName") { - path = file("$swiftklibSrcPath") - packageName("$swiftklibPackage") - - $swiftklibConfig - } - } - """.trimIndent() - ) - } - } - - companion object { - val Plugin.Companion.kotlinMultiplatform - get() = Plugin.of("org.jetbrains.kotlin.multiplatform", "2.0.21") - } -} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt deleted file mode 100644 index b97e145..0000000 --- a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/CInteropFixture.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.ttypic.swiftklib.gradle.fixture - -import com.autonomousapps.kit.GradleProject -import com.autonomousapps.kit.GradleProject.DslKind -import com.autonomousapps.kit.Source - -class CInteropFixture( - private val swiftSource: SwiftSource? = null, - private val kotlinSource: KotlinSource? = null, - swiftklibName: String = "test", - swiftklibPackage: String = "test" -) : BaseSwiftKlibFixture(swiftklibName, swiftklibPackage) { - - override fun buildProject(): GradleProject { - return newGradleProjectBuilder(DslKind.KOTLIN) - .withRootProject { - withFile( - "gradle.properties", """ - kotlin.mpp.enableCInteropCommonization=true - """.trimIndent() - ) - } - .withSubproject("library") { - withSources() - withBaseGradleSetup() - } - .write() - } - - private fun com.autonomousapps.kit.Subproject.Builder.withSources() { - val sources = mutableListOf() - - swiftSource?.let { - withSwiftSource(it) - } - - if (kotlinSource != null) { - sources.add(withKotlinSource(kotlinSource)) - } else { - sources.add(withDefaultKotlinSource()) - } - - this.sources.addAll(sources) - } -} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt deleted file mode 100644 index ce3a408..0000000 --- a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/PackageSource.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.ttypic.swiftklib.gradle.fixture - -import org.intellij.lang.annotations.Language - -class PackageSource private constructor( - @Language("Swift") val content: String -) { - companion object { - fun packageSwift(@Language("Swift") content: String): PackageSource = - PackageSource(content) - - fun defaultPackage(name: String) = packageSwift(""" - // swift-tools-version:5.6 - import PackageDescription - - let package = Package( - name: "$name", - products: [ - .library(name: "$name", targets: ["$name"]), - ], - targets: [ - .target(name: "$name") - ] - ) - """.trimIndent()) - } -} 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 new file mode 100644 index 0000000..0349427 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt @@ -0,0 +1,332 @@ +package io.github.ttypic.swiftklib.gradle.fixture + +import com.autonomousapps.kit.AbstractGradleProject +import com.autonomousapps.kit.GradleProject +import com.autonomousapps.kit.Source +import com.autonomousapps.kit.Subproject +import com.autonomousapps.kit.gradle.Plugin +import io.github.ttypic.swiftklib.gradle.api.RemotePackageConfiguration +import io.github.ttypic.swiftklib.gradle.api.SwiftKlibEntry +import io.github.ttypic.swiftklib.gradle.api.SwiftPackageConfiguration +import java.io.File +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +abstract class SwiftKlibTestFixture private constructor( + protected val configuration: TestConfiguration +) : AbstractGradleProject() { + private var _gradleProject: GradleProject? = null + + val gradleProject: GradleProject + get() = _gradleProject ?: createProject().also { _gradleProject = it } + + data class TestConfiguration( + val swiftklibName: String = "test", + val swiftklibPackage: String = "test", + val swiftSources: List = emptyList(), + val kotlinSources: List = emptyList(), + val additionalConfig: String = "", + val dslKind: GradleProject.DslKind = GradleProject.DslKind.KOTLIN, + val pluginVersion: String = System.getProperty("com.autonomousapps.plugin-under-test.version"), + internal val configurationBlock: SwiftKlibEntry.() -> Unit = {} + ) + + class Builder { + private var config = TestConfiguration() + + fun withName(name: String) = apply { + config = config.copy(swiftklibName = name) + } + + fun withSwiftSources(vararg sources: SwiftSource) = apply { + config = config.copy(swiftSources = sources.toList()) + } + + fun withKotlinSources(vararg sources: KotlinSource) = apply { + config = config.copy(kotlinSources = sources.toList()) + } + + fun withDslKind(dslKind: GradleProject.DslKind) = apply { + config = config.copy(dslKind = dslKind) + } + + fun withConfiguration(block: SwiftKlibEntry.() -> Unit) = apply { + config = config.copy(configurationBlock = block) + } + + fun withDefaultConfiguration(block: SwiftKlibEntry.() -> Unit) = apply { + config = config.copy(configurationBlock = block) + } + + fun build(): SwiftKlibTestFixture = object : SwiftKlibTestFixture(config) { + override fun createProject(): GradleProject = createDefaultProject() + } + } + + protected abstract fun createProject(): GradleProject + + protected fun createDefaultProject(): GradleProject { + val entry = TestSwiftKlibEntryImpl() + configuration.configurationBlock(entry) + + return newGradleProjectBuilder(configuration.dslKind) + .withRootProject { + withFile( + "gradle.properties", + "kotlin.mpp.enableCInteropCommonization=true" + ) + } + .withSubproject("library") { + setupSources() + setupGradleConfig(entry) + } + .write() + } + + private fun Subproject.Builder.setupSources() { + // Setup Swift sources + configuration.swiftSources.forEach { source -> + withFile( + "${configuration.swiftklibName}/src/main/swift/${source.filename}", + source.content + ) + } + + // Setup Kotlin sources + val kotlinSources = if (configuration.kotlinSources.isEmpty()) { + listOf(KotlinSource.default()) + } else { + configuration.kotlinSources + } + + kotlinSources.forEach { source -> + sources.add( + Source.kotlin(source.content) + .withPath(source.packageName, source.className) + .build() + ) + } + } + + private fun Subproject.Builder.setupGradleConfig(entry: TestSwiftKlibEntryImpl) { + withBuildScript { + plugins( + Plugin.kotlinMultiplatform, + Plugin("io.github.ttypic.swiftklib", configuration.pluginVersion) + ) + + withKotlin(createKotlinBlock(entry)) + } + } + + private fun createKotlinBlock(entry: TestSwiftKlibEntryImpl): String { + val configBlock = buildString { + appendLine("swiftklib {") + appendLine(" create(\"${configuration.swiftklibName}\") {") + appendLine(" path = file(\"${configuration.swiftklibName}/src/main/swift\")") + + appendLine(" packageName(\"${configuration.swiftklibPackage}\")") + + // Only add minimum version configurations if they differ from defaults + if (entry._minIos.hasValue()) { + appendLine(" minIos.set(${entry.minIos})") + } + if (entry._minMacos.hasValue()) { + appendLine(" minMacos.set(${entry.minMacos})") + } + if (entry._minTvos.hasValue()) { + appendLine(" minTvos.set(${entry.minTvos})") + } + if (entry._minWatchos.hasValue()) { + appendLine(" minWatchos.set(${entry.minWatchos})") + } + + if (entry.dependencies.isNotEmpty()) { + appendLine(" dependencies {") + entry.dependencies.forEach { dep -> + appendLine(" ${dep.toConfigString()}") + } + appendLine(" }") + } + + appendLine(" }") + appendLine("}") + } + + return """ + @file:OptIn(io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi::class) + kotlin { + compilerOptions { + optIn.addAll("kotlinx.cinterop.ExperimentalForeignApi") + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.compilations { + val main by getting { + cinterops.create("${configuration.swiftklibName}") + } + } + } + + ${configuration.additionalConfig} + } + + $configBlock + """.trimIndent() + } + + companion object { + fun builder() = Builder() + } +} + +val Plugin.Companion.kotlinMultiplatform + get() = Plugin.of("org.jetbrains.kotlin.multiplatform", "2.0.21") + + +private class TestSwiftKlibEntryImpl : SwiftKlibEntry { + val _path = notNull() + val _minIos = notNull() + val _minMacos = notNull() + val _minTvos = notNull() + val _minWatchos = 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 + + val dependencies = mutableListOf() + + override fun packageName(name: String) { + TODO("Package name changing in tests is not supported yet") + } + + override fun dependencies(configuration: SwiftPackageConfiguration.() -> Unit) { + val config = TestSwiftPackageConfigurationImpl() + config.configuration() + dependencies.addAll(config.dependencies) + } +} + + +private class TestSwiftPackageConfigurationImpl : SwiftPackageConfiguration { + internal val dependencies = mutableListOf() + + override fun local(name: String, path: File) { + dependencies.add(TestDependencyConfig.Local(name, path)) + } + + override fun remote(name: String, configuration: RemotePackageConfiguration.() -> Unit) { + val config = TestRemotePackageConfigurationImpl(name) + config.configuration() + dependencies.add(config.build()) + } +} + +private class TestRemotePackageConfigurationImpl(private val name: String) : RemotePackageConfiguration { + + private var url: String? = null + private var versionConfig: TestVersionConfig? = null + + override fun github(owner: String, repo: String) { + url = "https://github.com/$owner/$repo.git" + } + + override fun url(url: String) { + this.url = url + } + + override fun exactVersion(version: String) { + versionConfig = TestVersionConfig.Exact(version) + } + + override fun versionRange(from: String, to: String, inclusive: Boolean) { + versionConfig = TestVersionConfig.Range(from, to, inclusive) + } + + override fun branch(branchName: String) { + versionConfig = TestVersionConfig.Branch(branchName) + } + + override fun fromVersion(version: String) { + versionConfig = TestVersionConfig.From(version) + } + + internal fun build(): TestDependencyConfig.Remote { + return TestDependencyConfig.Remote( + name = name, + url = url, + version = versionConfig + ) + } +} + +private sealed interface TestDependencyConfig { + fun toConfigString(): String + + data class Local(val name: String, val path: File) : TestDependencyConfig { + override fun toConfigString() = """local("$name", file("${path.absolutePath}"))""" + } + + data class Remote( + val name: String, + val url: String?, + val version: TestVersionConfig? + ) : TestDependencyConfig { + override fun toConfigString() = buildString { + append("remote(\"$name\") {\n") + if (url != null) { + append(" url(\"$url\")\n") + } + if (version != null) { + append(" ${version.toConfigString()}\n") + } + append(" }") + } + } +} + +private sealed interface TestVersionConfig { + fun toConfigString(): String + + data class Exact(val version: String) : TestVersionConfig { + override fun toConfigString() = """exactVersion("$version")""" + } + + data class Range(val from: String, val to: String, val inclusive: Boolean) : TestVersionConfig { + override fun toConfigString() = """versionRange("$from", "$to", $inclusive)""" + } + + data class Branch(val name: String) : TestVersionConfig { + override fun toConfigString() = """branch("$name")""" + } + + data class From(val version: String) : TestVersionConfig { + override fun toConfigString() = """fromVersion("$version")""" + } +} + +private fun notNull() = NotNullVar() + +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.") + } + + public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value = value + } + + fun hasValue() = value != null + + public override fun toString(): String = + "NotNullProperty(${if (value != null) "value=$value" else "value not initialized yet"})" +} diff --git a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt deleted file mode 100644 index 094752a..0000000 --- a/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftPackageFixture.kt +++ /dev/null @@ -1,72 +0,0 @@ -package io.github.ttypic.swiftklib.gradle.fixture - -import com.autonomousapps.kit.GradleProject -import com.autonomousapps.kit.GradleProject.DslKind -import com.autonomousapps.kit.RootProject -import org.intellij.lang.annotations.Language -import java.io.File - -class SwiftPackageFixture( - @Language("swift") private val swiftCode: String, - @Language("kotlin") private val swiftklibConfig: String = "", - swiftklibName: String = "test", - private val withLocalPackage: Boolean = false, -) : BaseSwiftKlibFixture(swiftklibName) { - - override fun buildProject(): GradleProject { - return newGradleProjectBuilder(DslKind.KOTLIN) - .withRootProject { - if (withLocalPackage) { - withLocalPackage() - } - withFile( - "gradle.properties", """ - kotlin.mpp.enableCInteropCommonization=true - """.trimIndent() - ) - } - .withSubproject("library") { - sources.add(withDefaultKotlinSource()) - withSwiftSource(SwiftSource.of(content = swiftCode)) - withBaseGradleSetup( - swiftklibConfig = """ - $swiftklibConfig - ${getLocalPackageConfig()} - """.trimIndent() - ) - } - .write() - } - - private fun getLocalPackageConfig(): String { - if (!withLocalPackage) return "" - return """ - dependencies { - local("LocalPackage", rootProject.file("LocalPackage")) - } - """.trimIndent() - } - - private fun RootProject.Builder.withLocalPackage() { - withFile( - "LocalPackage/Package.swift", - PackageSource.defaultPackage("LocalPackage").content - ) - - withFile( - "LocalPackage/Sources/LocalPackage/LocalPackage.swift", - SwiftSource.of( - content = """ - import Foundation - - @objc public class LocalHelper: NSObject { - @objc public class func getVersion() -> String { return "1.0.0" } - } - """.trimIndent() - ).content - ) - } - - fun getPackageResolvedFile(): File = - File(gradleProject.rootDir, "library/build/swiftklib/$swiftklibName/iosArm64/swiftBuild/Package.resolved") -} diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt deleted file mode 100644 index a4ccc51..0000000 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.ttypic.swiftklib.gradle - -import io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi -import org.gradle.api.Action -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ListProperty -import org.gradle.api.provider.Property -import java.io.File -import javax.inject.Inject - -abstract class SwiftKlibEntry @Inject constructor( - val name: String, - val objects: ObjectFactory, -) { - 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) - - internal val dependencies: ListProperty = - objects.listProperty(SwiftPackageDependency::class.java) - - fun packageName(name: String) = packageName.set(name) - - @ExperimentalSwiftklibApi - fun dependencies(action: Action) { - val handler = SwiftPackageDependencyHandler(objects) - action.execute(handler) - dependencies.set(handler.dependencies) - } -} - 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 new file mode 100644 index 0000000..efe4286 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt @@ -0,0 +1,49 @@ +package io.github.ttypic.swiftklib.gradle + +import io.github.ttypic.swiftklib.gradle.api.SwiftKlibEntry +import io.github.ttypic.swiftklib.gradle.api.SwiftPackageConfiguration +import io.github.ttypic.swiftklib.gradle.internal.SwiftPackageConfigurationImpl +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property +import java.io.File +import javax.inject.Inject +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +internal abstract class SwiftKlibEntryImpl @Inject constructor( + val name: String, + private val objects: ObjectFactory, +) : 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) + + 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() + + internal val dependencyHandler = SwiftPackageConfigurationImpl(objects) + + override fun packageName(name: String) = _packageName.set(name) + + override fun dependencies(configuration: SwiftPackageConfiguration.() -> Unit) { + dependencyHandler.apply(configuration) + } +} + +fun Property.bind(): ReadWriteProperty { + return object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return get() + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + set(value) + } + } +} 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 12b8ffd..41deded 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 @@ -1,27 +1,36 @@ package io.github.ttypic.swiftklib.gradle +import io.github.ttypic.swiftklib.gradle.api.SwiftKlibEntry import io.github.ttypic.swiftklib.gradle.task.CompileSwiftTask import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.model.ObjectFactory +import org.gradle.api.reflect.TypeOf import org.gradle.configurationcache.extensions.capitalized import org.jetbrains.kotlin.gradle.tasks.CInteropProcess import org.jetbrains.kotlin.konan.target.HostManager +import kotlin.reflect.javaType +import kotlin.reflect.typeOf const val EXTENSION_NAME = "swiftklib" +@OptIn(ExperimentalStdlibApi::class) @Suppress("unused") class SwiftKlibPlugin : Plugin { override fun apply(target: Project) = with(target) { val objects: ObjectFactory = project.objects - val swiftKlibEntries: NamedDomainObjectContainer = - objects.domainObjectContainer(SwiftKlibEntry::class.java) { name -> - objects.newInstance(SwiftKlibEntry::class.java, name) + val swiftKlibEntries: NamedDomainObjectContainer = + objects.domainObjectContainer(SwiftKlibEntryImpl::class.java) { name -> + objects.newInstance(SwiftKlibEntryImpl::class.java, name) } - project.extensions.add(EXTENSION_NAME, swiftKlibEntries) + val type = TypeOf.typeOf>( + typeOf>().javaType + ) + + project.extensions.add(type, EXTENSION_NAME, swiftKlibEntries) if (!HostManager.hostIsMac) { logger.warn("Current host OS is not macOS. Disabling SwiftKlib plugin") @@ -44,14 +53,14 @@ class SwiftKlibPlugin : Plugin { name, target, buildDir, - entry.path, - entry.packageName, - entry.minIos, - entry.minMacos, - entry.minTvos, - entry.minWatchos, + entry._path, + entry._packageName, + entry._minIos, + entry._minMacos, + entry._minTvos, + entry._minWatchos, ).configure { - it.dependenciesProperty = entry.dependencies + it.dependenciesProperty = entry.dependencyHandler.dependencies } } } diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependencyHandler.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependencyHandler.kt deleted file mode 100644 index 13b7235..0000000 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependencyHandler.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.ttypic.swiftklib.gradle - -import io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi -import org.gradle.api.model.ObjectFactory -import org.gradle.api.provider.ListProperty -import org.gradle.api.tasks.Nested -import java.io.File -import javax.inject.Inject - -@ExperimentalSwiftklibApi -class SwiftPackageDependencyHandler @Inject constructor( - private val objects: ObjectFactory -) { - private val _dependencies = objects.listProperty(SwiftPackageDependency::class.java).convention(emptyList()) - - @get:Nested - internal val dependencies: ListProperty = _dependencies - - @ExperimentalSwiftklibApi - fun local(name: String, path: File) { - val currentDeps = _dependencies.get().toMutableList() - currentDeps.add(SwiftPackageDependency.Local(name, path)) - _dependencies.set(currentDeps) - } - - @ExperimentalSwiftklibApi - fun remote(name: String, block: RemotePackageBuilder.() -> Unit) { - val builder = RemotePackageBuilder(objects, name) - builder.apply(block) - - val dependency = builder.build() - ?: throw IllegalStateException("No version specification provided for remote package $name") - - val currentDeps = _dependencies.get().toMutableList() - currentDeps.add(dependency) - _dependencies.set(currentDeps) - } -} - - 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 new file mode 100644 index 0000000..c659cb6 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt @@ -0,0 +1,38 @@ +package io.github.ttypic.swiftklib.gradle.api + +@ExperimentalSwiftklibApi +interface RemotePackageConfiguration { + /** + * Sets GitHub repository as the package source. + */ + fun github(owner: String, repo: String) + + /** + * Sets custom URL as the package source. + */ + fun url(url: String) + + /** + * Specifies exact version of the package. + */ + fun exactVersion(version: String) + + /** + * Specifies version range for the package. + */ + fun versionRange( + from: String, + to: String, + inclusive: Boolean = true + ) + + /** + * Specifies branch to use for the package. + */ + fun branch(branchName: String) + + /** + * Specifies minimum version of the package. + */ + fun fromVersion(version: String) +} 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 new file mode 100644 index 0000000..2eec408 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt @@ -0,0 +1,18 @@ +package io.github.ttypic.swiftklib.gradle.api + +import java.io.File + +interface SwiftKlibEntry { + var path: File + + var minIos: Int + var minMacos: Int + var minTvos: Int + var minWatchos: Int + + fun packageName(name: String) + + @ExperimentalSwiftklibApi + fun dependencies(configuration: SwiftPackageConfiguration.() -> Unit) + +} 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 new file mode 100644 index 0000000..f2ef15f --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt @@ -0,0 +1,19 @@ +package io.github.ttypic.swiftklib.gradle.api + +@ExperimentalSwiftklibApi +interface SwiftPackageConfiguration { + /** + * Configures a local package dependency. + * @param name Package name + * @param path Local path to the package + */ + fun local(name: String, path: java.io.File) + + /** + * Configures a remote package dependency. + * @param name Package name + * @param configuration Configuration block for the remote package + */ + fun remote(name: String, 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 new file mode 100644 index 0000000..d87d587 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt @@ -0,0 +1,65 @@ +package io.github.ttypic.swiftklib.gradle.internal + +import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency +import io.github.ttypic.swiftklib.gradle.api.RemotePackageConfiguration +import org.gradle.api.model.ObjectFactory +import javax.inject.Inject + +internal class RemotePackageConfigurationImpl @Inject constructor( + private val objects: ObjectFactory, + private val name: String +) : RemotePackageConfiguration { + private val urlProperty = objects.property(String::class.java) + private var dependency: SwiftPackageDependency.Remote? = null + + override fun github(owner: String, repo: String) { + require(owner.isNotBlank()) { "Owner cannot be blank" } + require(repo.isNotBlank()) { "Repo cannot be blank" } + urlProperty.set("https://github.com/$owner/$repo.git") + } + + override fun url(url: String) { + require(url.isNotBlank()) { "URL cannot be blank" } + urlProperty.set(url) + } + + override fun exactVersion(version: String) { + dependency = SwiftPackageDependency.Remote.ExactVersion( + name = name, + url = requireUrl(), + version = version + ) + } + + override fun versionRange(from: String, to: String, inclusive: Boolean) { + dependency = SwiftPackageDependency.Remote.VersionRange( + name = name, + url = requireUrl(), + from = from, + to = to, + inclusive = inclusive + ) + } + + override fun branch(branchName: String) { + dependency = SwiftPackageDependency.Remote.Branch( + name = name, + url = requireUrl(), + branchName = branchName + ) + } + + override fun fromVersion(version: String) { + dependency = SwiftPackageDependency.Remote.FromVersion( + name = name, + url = requireUrl(), + version = version + ) + } + + internal fun build(): SwiftPackageDependency.Remote? = dependency + + private fun requireUrl(): String = + urlProperty.orNull + ?: throw IllegalStateException("URL must be set via github() or url() before specifying version") +} 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 new file mode 100644 index 0000000..a3a21f6 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/SwiftPackageConfigurationImpl.kt @@ -0,0 +1,42 @@ +package io.github.ttypic.swiftklib.gradle.internal + +import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency +import io.github.ttypic.swiftklib.gradle.api.ExperimentalSwiftklibApi +import io.github.ttypic.swiftklib.gradle.api.RemotePackageConfiguration +import io.github.ttypic.swiftklib.gradle.api.SwiftPackageConfiguration +import org.gradle.api.model.ObjectFactory +import javax.inject.Inject + +internal class SwiftPackageConfigurationImpl @Inject constructor( + private val objects: ObjectFactory +) : SwiftPackageConfiguration { + private val _dependencies = + objects + .listProperty(SwiftPackageDependency::class.java) + .convention(emptyList()) + + internal val dependencies get() = _dependencies + + @ExperimentalSwiftklibApi + override fun local(name: String, path: java.io.File) { + val currentDeps = _dependencies.get().toMutableList() + currentDeps.add(SwiftPackageDependency.Local(name, path)) + _dependencies.set(currentDeps) + } + + @ExperimentalSwiftklibApi + override fun remote( + name: String, + configuration: RemotePackageConfiguration.() -> Unit + ) { + val builder = RemotePackageConfigurationImpl(objects, name) + builder.apply(configuration) + + val dependency = builder.build() + ?: throw IllegalStateException("No version specification provided for remote package $name") + + val currentDeps = _dependencies.get().toMutableList() + currentDeps.add(dependency) + _dependencies.set(currentDeps) + } +} From e5456b5b4cdcf9c348440280c4981acbc4dab5dc Mon Sep 17 00:00:00 2001 From: frankois Date: Sun, 3 Nov 2024 20:20:29 +0100 Subject: [PATCH 03/25] using command line instead of string manipulation For VersionRange, it's not possible to have a specific one We need to try/cacth every swift step and print on the ouput the error, it's important for the user All test are passing except local package The local package need to be done --- .../gradle/fixture/SwiftKlibTestFixture.kt | 7 +- .../gradle/SwiftPackageDependency.kt | 18 ++- .../gradle/api/RemotePackageConfiguration.kt | 6 +- .../RemotePackageConfigurationImpl.kt | 19 ++- .../swiftklib/gradle/task/CompileSwiftTask.kt | 126 +++++++++++++++++- .../gradle/templates/CreatePackageSwift.kt | 63 ++------- 6 files changed, 166 insertions(+), 73 deletions(-) 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..71d190a 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 @@ -232,14 +232,17 @@ private class TestSwiftPackageConfigurationImpl : SwiftPackageConfiguration { private class TestRemotePackageConfigurationImpl(private val name: String) : 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 = name } override fun exactVersion(version: String) { 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..a71a62d 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,16 +2,20 @@ 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 + @get:Input @get:Optional + val packageName: String? data class Local( @Input override val name: String, - @InputDirectory val path: File + @InputDirectory val path: File, + @Input @get:Optional override val packageName: String? = null, ) : SwiftPackageDependency { init { require(name.isNotBlank()) { "Package name cannot be blank" } @@ -26,7 +30,8 @@ internal sealed interface SwiftPackageDependency : Serializable { data class ExactVersion( @Input override val name: String, @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" } @@ -40,7 +45,8 @@ internal sealed interface SwiftPackageDependency : Serializable { @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" } @@ -53,7 +59,8 @@ internal sealed interface SwiftPackageDependency : Serializable { data class Branch( @Input override val name: String, @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" } @@ -65,7 +72,8 @@ internal sealed interface SwiftPackageDependency : Serializable { data class FromVersion( @Input override val name: String, @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" } 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/internal/RemotePackageConfigurationImpl.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt index d87d587..f2405ca 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 @@ -10,24 +10,28 @@ internal class RemotePackageConfigurationImpl @Inject constructor( private val name: String ) : 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/task/CompileSwiftTask.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt index aa75cc2..248c7df 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 @@ -3,7 +3,7 @@ package io.github.ttypic.swiftklib.gradle.task import io.github.ttypic.swiftklib.gradle.CompileTarget import io.github.ttypic.swiftklib.gradle.EXTENSION_NAME import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency -import io.github.ttypic.swiftklib.gradle.templates.createPackageSwiftContents +import io.github.ttypic.swiftklib.gradle.templates.toSwiftArgs import io.github.ttypic.swiftklib.gradle.util.StringReplacingOutputStream import org.gradle.api.DefaultTask import org.gradle.api.provider.ListProperty @@ -20,6 +20,7 @@ import org.gradle.process.ExecOperations import java.io.ByteArrayOutputStream import java.io.File import java.math.BigInteger +import java.nio.file.Path import java.security.MessageDigest import javax.inject.Inject @@ -118,14 +119,119 @@ abstract class CompileSwiftTask @Inject constructor( } private fun createPackageSwift(dependencies: List) { - val packageSwiftContents = createPackageSwiftContents(cinteropName, dependencies) + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "init", + "--name", + cinteropName, + "--type", + "empty", + "--disable-xctest", + "--disable-swift-testing" + ) + }.run { + if (exitValue != 0) { + throw RuntimeException("Failed to init Swift Package") + } + } + + dependencies.forEach { dependency -> + if (dependency is SwiftPackageDependency.Local) { + addLocalPackage(dependency.path.absolutePath) + } else { + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.args = listOf("package", "add-dependency") + dependency.toSwiftArgs() + it.isIgnoreExitValue = true + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package dependency $dependency", + ) + } + } + } + } + + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.args = listOf( + "package", + "add-target", + "--path", + cinteropName, + cinteropName + ) + it.isIgnoreExitValue = true + }.run { + if (exitValue != 0) { + throw RuntimeException("Failed to add Swift Package target $cinteropName") + } + } + + dependencies.forEach { dependency -> + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "add-target-dependency", + dependency.name, + "--package", + dependency.packageName ?: dependency.name, + cinteropName, + ) + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package target dependency $cinteropName - package = ${dependency.name}", + ) + } + } + } + + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "add-product", + "--targets", + cinteropName, + "--type", + "static-library", + cinteropName + ) + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package library $cinteropName", + ) + } + } if (printDebug) { logger.warn("======== Package.swift contents ========") - logger.warn(packageSwiftContents) + logger.warn(File(swiftBuildDir, "Package.swift").readText()) logger.warn("======== | Package.swift contents | ========") } - File(swiftBuildDir, "Package.swift") - .writeText(packageSwiftContents) + } + + private fun addLocalPackage(path: String) { + File(swiftBuildDir, "Package.swift").readText().let { + val content = if (!it.contains("dependencies:")) { + it.replace("name: \"cinteropName\",", "name: \"cinteropName\", dependencies:[],") + } else { + + } + } } private fun buildSwift(xcodeVersion: Int): SwiftBuildResult { @@ -157,7 +263,8 @@ abstract class CompileSwiftTask @Inject constructor( ) } - val releaseBuildPath = File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-macosx/release") + val releaseBuildPath = + File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-macosx/release") return SwiftBuildResult( libPath = File(releaseBuildPath, "lib${cinteropName}.a"), @@ -245,7 +352,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 = 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..f47ae9d 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 @@ -2,61 +2,22 @@ package io.github.ttypic.swiftklib.gradle.templates import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency -internal fun createPackageSwiftContents( - cinteropName: String, - dependencies: Collection -): String = """ - // swift-tools-version:5.6 - import PackageDescription - - let package = Package( - name: "$cinteropName", - products: [ - .library( - name: "$cinteropName", - type: .static, - targets: ["$cinteropName"]) - ], - dependencies: [ - ${dependencies.joinToString(",\n ") { it.toSwiftPackageDeclaration() }} - ], - targets: [ - .target( - name: "$cinteropName", - dependencies: [ - ${dependencies.joinToString(",\n ") { "\"${it.name}\"" }} - ], - path: "$cinteropName") - ] - ) -""".trimIndent() - - -internal fun SwiftPackageDependency.toSwiftPackageDeclaration(): String = when (this) { +internal fun SwiftPackageDependency.toSwiftArgs(): List = when (this) { is SwiftPackageDependency.Local -> - """ - .package(path: "${path.absolutePath}") - """.trimIndent() - + emptyList() is SwiftPackageDependency.Remote.ExactVersion -> - """ - .package(url: "$url", exact: "$version") - """.trimIndent() - + listOf(url, "--exact", version) is SwiftPackageDependency.Remote.VersionRange -> { - val operator = if (inclusive) "..." else "..<" - """ - .package(url: "$url", "$from"$operator"$to") - """.trimIndent() + if (inclusive) { + // can't do inclusive range from command line + // but I think it's better to use up-to-next-minor-from + listOf(url, "--up-to-next-minor-from", from) + } else { + listOf(url, "--from", from, "--to", to) + } } - is SwiftPackageDependency.Remote.Branch -> - """ - .package(url: "$url", branch: "$branchName") - """.trimIndent() - + listOf(url, "--branch", branchName) is SwiftPackageDependency.Remote.FromVersion -> - """ - .package(url: "$url", from: "$version") - """.trimIndent() + listOf(url, "--from", version) } From a2cc7a27df2264b761deee440126153bfd9f05be Mon Sep 17 00:00:00 2001 From: frankois Date: Sun, 3 Nov 2024 21:57:10 +0100 Subject: [PATCH 04/25] fix local spm we can now use local path, some manual update has been done all tests are passing --- .../swiftklib/gradle/task/CompileSwiftTask.kt | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) 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 248c7df..7ad6295 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 @@ -20,7 +20,6 @@ import org.gradle.process.ExecOperations import java.io.ByteArrayOutputStream import java.io.File import java.math.BigInteger -import java.nio.file.Path import java.security.MessageDigest import javax.inject.Inject @@ -141,19 +140,31 @@ abstract class CompileSwiftTask @Inject constructor( dependencies.forEach { dependency -> if (dependency is SwiftPackageDependency.Local) { - addLocalPackage(dependency.path.absolutePath) + addDependencyBlockIfNeeded(cinteropName) + val escapePath = dependency.path.absolutePath.replace("/", "\\/") + execOperations.exec { + it.executable = "sed" + it.workingDir = swiftBuildDir + it.args = listOf( + "-i", + "''", + "/dependencies: \\[/,/]/ s/]/ \\n\\t.package(path: \"${escapePath}\"),\\n]/", + "Package.swift" + ) + it.isIgnoreExitValue = true + } } else { execOperations.exec { it.executable = "swift" it.workingDir = swiftBuildDir it.args = listOf("package", "add-dependency") + dependency.toSwiftArgs() it.isIgnoreExitValue = true - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package dependency $dependency", - ) - } + } + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package dependency $dependency", + ) } } } @@ -224,12 +235,11 @@ abstract class CompileSwiftTask @Inject constructor( } } - private fun addLocalPackage(path: String) { - File(swiftBuildDir, "Package.swift").readText().let { - val content = if (!it.contains("dependencies:")) { - it.replace("name: \"cinteropName\",", "name: \"cinteropName\", dependencies:[],") - } else { - + private fun addDependencyBlockIfNeeded(name: String) { + File(swiftBuildDir, "Package.swift").readText().run { + if (!contains("dependencies:")) { + val updated = replace("name: \"$name\"", "name: \"$name\",\n\tdependencies: []") + File(swiftBuildDir, "Package.swift").writeText(updated) } } } From cc493e3ec2edf57014d7c8f9668930944f42aa70 Mon Sep 17 00:00:00 2001 From: frankois Date: Mon, 4 Nov 2024 13:45:32 +0100 Subject: [PATCH 05/25] make the plugin working namespace add new test for building and liking Firebase using triple for specify the build target --- .../gradle/SwiftPackageModulesTest.kt | 36 +++++++++ .../gradle/fixture/SwiftKlibTestFixture.kt | 44 ++++++----- .../swiftklib/gradle/SwiftKlibEntryImpl.kt | 17 ++--- .../swiftklib/gradle/api/SwiftKlibEntry.kt | 8 +- .../swiftklib/gradle/task/CompileSwiftTask.kt | 75 ++++++++++++------- 5 files changed, 122 insertions(+), 58 deletions(-) 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..8f27149 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,42 @@ 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") + } private fun assertPackageResolved(fixture: SwiftKlibTestFixture, vararg packageNames: String) { val resolvedFile = File( 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 71d190a..01974e2 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,16 @@ 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.dependencies.isNotEmpty()) { @@ -190,16 +190,16 @@ 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() 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 val dependencies = mutableListOf() @@ -229,7 +229,8 @@ private class TestSwiftPackageConfigurationImpl : SwiftPackageConfiguration { } } -private class TestRemotePackageConfigurationImpl(private val name: String) : RemotePackageConfiguration { +private class TestRemotePackageConfigurationImpl(private val name: String) : + RemotePackageConfiguration { private var url: String? = null private var packageName: String? = null @@ -242,7 +243,7 @@ private class TestRemotePackageConfigurationImpl(private val name: String) : Rem override fun url(url: String, packageName: String?) { this.url = url - this.packageName = name + this.packageName = packageName } override fun exactVersion(version: String) { @@ -265,7 +266,8 @@ private class TestRemotePackageConfigurationImpl(private val name: String) : Rem return TestDependencyConfig.Remote( name = name, url = url, - version = versionConfig + version = versionConfig, + packageName = packageName ) } } @@ -280,12 +282,17 @@ private sealed interface TestDependencyConfig { data class Remote( val name: String, val url: String?, - val version: TestVersionConfig? + val version: TestVersionConfig?, + val packageName: String? ) : TestDependencyConfig { override fun toConfigString() = buildString { append("remote(\"$name\") {\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") @@ -321,7 +328,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/SwiftKlibEntryImpl.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt index efe4286..63d58de 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,15 @@ 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 _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() internal val dependencyHandler = SwiftPackageConfigurationImpl(objects) 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..436b2b3 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,10 @@ 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 fun packageName(name: String) 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 7ad6295..860b6a9 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,10 @@ 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, ) : DefaultTask() { @get:Optional @@ -81,10 +81,10 @@ 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") /** * Creates build directory or cleans up if it already exists @@ -168,7 +168,7 @@ abstract class CompileSwiftTask @Inject constructor( } } } - + addPlatformBlock(cinteropName) execOperations.exec { it.executable = "swift" it.workingDir = swiftBuildDir @@ -235,6 +235,27 @@ abstract class CompileSwiftTask @Inject constructor( } } + private fun addPlatformBlock(name: String) { + File(swiftBuildDir, "Package.swift").readText().run { + if (!contains("platforms:")) { + val entries = listOfNotNull( + ".iOS(\"$minIos\")".takeIf { !minIos.isNullOrEmpty() }, + ".macOS(\"$minMacos\")".takeIf { !minMacos.isNullOrEmpty() }, + ".tvOS(\"$minTvos\")".takeIf { !minTvos.isNullOrEmpty() }, + ".watchOS(\"$minWatchos\")".takeIf { !minWatchos.isNullOrEmpty() }, + ).joinToString(",") + if (entries.isNotEmpty()) { + val updated = + replace( + "name: \"$name\",\n", + "name: \"$name\",\n\tplatforms: [$entries],\n" + ) + File(swiftBuildDir, "Package.swift").writeText(updated) + } + } + } + } + private fun addDependencyBlockIfNeeded(name: String) { File(swiftBuildDir, "Package.swift").readText().run { if (!contains("dependencies:")) { @@ -274,7 +295,7 @@ abstract class CompileSwiftTask @Inject constructor( } val releaseBuildPath = - File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-macosx/release") + File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/release") return SwiftBuildResult( libPath = File(releaseBuildPath, "lib${cinteropName}.a"), @@ -284,16 +305,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. */ @@ -379,8 +400,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()}" ) @@ -409,13 +430,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 From 510232fc511f42b1215afe4282246f221147f0fe Mon Sep 17 00:00:00 2001 From: frankois Date: Mon, 4 Nov 2024 14:33:54 +0100 Subject: [PATCH 06/25] WIP: add multi product usage for a SPM repository you can set multiple product from a dependency like Firebase --- .../gradle/SwiftPackageModulesTest.kt | 39 +++++++++++++++++++ .../gradle/fixture/SwiftKlibTestFixture.kt | 14 +++++-- .../swiftklib/gradle/RemotePackageBuilder.kt | 2 +- .../gradle/SwiftPackageDependency.kt | 22 +++++------ .../gradle/api/SwiftPackageConfiguration.kt | 7 ++++ .../RemotePackageConfigurationImpl.kt | 2 +- .../internal/SwiftPackageConfigurationImpl.kt | 15 ++++--- .../swiftklib/gradle/task/CompileSwiftTask.kt | 35 +++++++++-------- 8 files changed, 99 insertions(+), 37 deletions(-) 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 8f27149..ebc03f3 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 @@ -334,6 +334,45 @@ class SwiftPackageModulesTest { 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") + } + private fun assertPackageResolved(fixture: SwiftKlibTestFixture, vararg packageNames: String) { val resolvedFile = File( fixture.gradleProject.rootDir, 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 01974e2..ee8934c 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 @@ -223,13 +223,17 @@ private class TestSwiftPackageConfigurationImpl : SwiftPackageConfiguration { } override fun remote(name: String, configuration: RemotePackageConfiguration.() -> Unit) { + remote(listOf(name), configuration) + } + + override fun remote(name: List, configuration: RemotePackageConfiguration.() -> Unit) { val config = TestRemotePackageConfigurationImpl(name) config.configuration() dependencies.add(config.build()) } } -private class TestRemotePackageConfigurationImpl(private val name: String) : +private class TestRemotePackageConfigurationImpl(private val name: List) : RemotePackageConfiguration { private var url: String? = null @@ -280,13 +284,17 @@ private sealed interface TestDependencyConfig { } data class Remote( - val name: String, + val name: List, val url: String?, 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) { if (packageName != null) { append(" url(\"$url\", \"$packageName\")\n") 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/SwiftPackageDependency.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt index a71a62d..1fdc126 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 @@ -8,17 +8,17 @@ 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, + @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()) { "Package name cannot be blank" } require(path.exists()) { "Package path must exist: $path" } } } @@ -28,20 +28,20 @@ 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 @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty()) { "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, @@ -49,7 +49,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty()) { "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" } @@ -57,26 +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 @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty()) { "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 @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotBlank()) { "Package name cannot be blank" } + require(name.isNotEmpty()) { "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/SwiftPackageConfiguration.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt index f2ef15f..3ace8d1 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 @@ -15,5 +15,12 @@ interface SwiftPackageConfiguration { * @param configuration Configuration block for the remote package */ fun remote(name: String, configuration: RemotePackageConfiguration.() -> Unit) + + /** + * Configures a remote package dependency. + * @param name a list of product to add + * @param configuration Configuration block for the remote package + */ + fun remote(name: 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 f2405ca..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,7 +7,7 @@ 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) 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..2b5ccf0 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,15 +20,12 @@ 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( - name: String, - configuration: RemotePackageConfiguration.() -> Unit - ) { + override fun remote(name: List, configuration: RemotePackageConfiguration.() -> Unit) { val builder = RemotePackageConfigurationImpl(objects, name) builder.apply(configuration) @@ -39,4 +36,12 @@ internal class SwiftPackageConfigurationImpl @Inject constructor( currentDeps.add(dependency) _dependencies.set(currentDeps) } + + @ExperimentalSwiftklibApi + override fun remote( + name: String, + configuration: RemotePackageConfiguration.() -> Unit + ) { + remote(listOf(name), configuration) + } } 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 860b6a9..9e0ea58 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 @@ -187,25 +187,28 @@ abstract class CompileSwiftTask @Inject constructor( } dependencies.forEach { dependency -> - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "add-target-dependency", - dependency.name, - "--package", - dependency.packageName ?: dependency.name, - cinteropName, - ) - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package target dependency $cinteropName - package = ${dependency.name}", + dependency.name.forEach { library -> + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "add-target-dependency", + library, + "--package", + dependency.packageName ?: library, + cinteropName, ) + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package target dependency $cinteropName - package = ${dependency.packageName ?: library}:$library", + ) + } } } + } execOperations.exec { From 2ddda8f9a1b578a5905ce89121a877a12bfc0bd7 Mon Sep 17 00:00:00 2001 From: frankois Date: Mon, 4 Nov 2024 14:56:59 +0100 Subject: [PATCH 07/25] fix test need to duplicate the code of remote function The exception is not forwarded correctly --- .../internal/SwiftPackageConfigurationImpl.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 2b5ccf0..c46ff29 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 @@ -25,7 +25,10 @@ internal class SwiftPackageConfigurationImpl @Inject constructor( } @ExperimentalSwiftklibApi - override fun remote(name: List, configuration: RemotePackageConfiguration.() -> Unit) { + override fun remote( + name: List, + configuration: RemotePackageConfiguration.() -> Unit + ) { val builder = RemotePackageConfigurationImpl(objects, name) builder.apply(configuration) @@ -42,6 +45,14 @@ internal class SwiftPackageConfigurationImpl @Inject constructor( name: String, configuration: RemotePackageConfiguration.() -> Unit ) { - remote(listOf(name), configuration) + val builder = RemotePackageConfigurationImpl(objects, listOf(name)) + builder.apply(configuration) + + val dependency = builder.build() + ?: throw IllegalStateException("No version specification provided for remote package $name") + + val currentDeps = _dependencies.get().toMutableList() + currentDeps.add(dependency) + _dependencies.set(currentDeps) } } From 8c4fb64fa22ee203f977c78297d13d163d1e1bea Mon Sep 17 00:00:00 2001 From: frankois Date: Mon, 4 Nov 2024 15:22:05 +0100 Subject: [PATCH 08/25] fix test check case when the list of packages have a empty string add some comment for the versioning of package --- .../ttypic/swiftklib/gradle/SwiftPackageDependency.kt | 10 +++++----- .../swiftklib/gradle/templates/CreatePackageSwift.kt | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) 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 1fdc126..1969d9f 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 @@ -18,7 +18,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null, ) : SwiftPackageDependency { init { - require(name.isNotEmpty()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } require(path.exists()) { "Package path must exist: $path" } } } @@ -34,7 +34,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(version.isNotBlank()) { "Version cannot be blank" } } @@ -49,7 +49,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "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" } @@ -63,7 +63,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(branchName.isNotBlank()) { "Branch name cannot be blank" } } @@ -76,7 +76,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty()) { "Package name cannot be blank" } + require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "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/templates/CreatePackageSwift.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt index f47ae9d..d1873c2 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 @@ -11,6 +11,7 @@ internal fun SwiftPackageDependency.toSwiftArgs(): List = when (this) { if (inclusive) { // can't do inclusive range from command line // but I think it's better to use up-to-next-minor-from + // it needs to be rethink listOf(url, "--up-to-next-minor-from", from) } else { listOf(url, "--from", from, "--to", to) From 6aeac88fc9187f745ec65895132c1a69ac886da6 Mon Sep 17 00:00:00 2001 From: frankois Date: Tue, 5 Nov 2024 09:04:28 +0100 Subject: [PATCH 09/25] update remote parameter name update comment in interface --- .../swiftklib/gradle/api/SwiftPackageConfiguration.kt | 6 +++--- .../gradle/internal/SwiftPackageConfigurationImpl.kt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) 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 3ace8d1..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,16 +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 name a list of product to add + * @param names a list of product's name to add * @param configuration Configuration block for the remote package */ - fun remote(name: List, configuration: RemotePackageConfiguration.() -> Unit) + fun remote(names: List, configuration: RemotePackageConfiguration.() -> Unit) } 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 c46ff29..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 @@ -26,14 +26,14 @@ internal class SwiftPackageConfigurationImpl @Inject constructor( @ExperimentalSwiftklibApi override fun remote( - name: List, + names: List, configuration: RemotePackageConfiguration.() -> Unit ) { - val builder = RemotePackageConfigurationImpl(objects, name) + val builder = RemotePackageConfigurationImpl(objects, names) builder.apply(configuration) val dependency = builder.build() - ?: throw IllegalStateException("No version specification provided for remote package $name") + ?: throw IllegalStateException("No version specification provided for remote package $names") val currentDeps = _dependencies.get().toMutableList() currentDeps.add(dependency) From f9bae40b93c16cc6e2b073d78d39c1289f813e97 Mon Sep 17 00:00:00 2001 From: frankois Date: Tue, 5 Nov 2024 09:20:41 +0100 Subject: [PATCH 10/25] add more complex case --- .../gradle/SwiftPackageModulesTest.kt | 57 +++++++++++++++++++ .../gradle/fixture/SwiftKlibTestFixture.kt | 4 +- 2 files changed, 59 insertions(+), 2 deletions(-) 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 ebc03f3..ea9dda7 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 @@ -373,6 +373,63 @@ class SwiftPackageModulesTest { 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") + } + private fun assertPackageResolved(fixture: SwiftKlibTestFixture, vararg packageNames: String) { val resolvedFile = File( fixture.gradleProject.rootDir, 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 ee8934c..7c1c1ce 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 @@ -226,8 +226,8 @@ private class TestSwiftPackageConfigurationImpl : SwiftPackageConfiguration { remote(listOf(name), configuration) } - override fun remote(name: List, configuration: RemotePackageConfiguration.() -> Unit) { - val config = TestRemotePackageConfigurationImpl(name) + override fun remote(names: List, configuration: RemotePackageConfiguration.() -> Unit) { + val config = TestRemotePackageConfigurationImpl(names) config.configuration() dependencies.add(config.build()) } From 85d4204eb89344c70bd6a136ff3292a3f1af03e9 Mon Sep 17 00:00:00 2001 From: frankois Date: Wed, 6 Nov 2024 09:04:30 +0100 Subject: [PATCH 11/25] Add tools version parameter We can now set the toolsVersion from the plugin By default, the command line uses the latest swift version available. By at some cases, it can't work and a specific version need to be set. --- .../gradle/SwiftPackageModulesTest.kt | 81 +++++++++++++++++-- .../gradle/fixture/SwiftKlibTestFixture.kt | 6 ++ .../swiftklib/gradle/SwiftKlibEntryImpl.kt | 2 + .../swiftklib/gradle/SwiftKlibPlugin.kt | 1 + .../swiftklib/gradle/api/SwiftKlibEntry.kt | 1 + .../swiftklib/gradle/task/CompileSwiftTask.kt | 21 ++++- 6 files changed, 105 insertions(+), 7 deletions(-) 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 ea9dda7..f506bea 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 @@ -430,6 +430,56 @@ class SwiftPackageModulesTest { 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 = "5.3" + dependencies { + } + } + .build() + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("package manifest version 5.3.0 is too old") + getManifestContent(fixture) { manifest -> + assertTrue(manifest.contains("swift-tools-version:5.3")) + } + } + private fun assertPackageResolved(fixture: SwiftKlibTestFixture, vararg packageNames: String) { val resolvedFile = File( fixture.gradleProject.rootDir, @@ -437,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 7c1c1ce..12b073c 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 @@ -140,6 +140,9 @@ abstract class SwiftKlibTestFixture private constructor( if (entry._minWatchos.hasValue()) { appendLine(" minWatchos = \"${entry.minWatchos}\"") } + if (!entry._toolsVersions.isNullOrEmpty()) { + appendLine(" toolsVersion = \"${entry.toolsVersion}\"") + } if (entry.dependencies.isNotEmpty()) { appendLine(" dependencies {") @@ -194,12 +197,15 @@ private class TestSwiftKlibEntryImpl : SwiftKlibEntry { val _minMacos = notNull() val _minTvos = notNull() val _minWatchos = notNull() + val _toolsVersions: String? + get() = toolsVersion override var path: File by _path 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? = null val dependencies = mutableListOf() 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 63d58de..9333a8e 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,6 +16,7 @@ internal abstract class SwiftKlibEntryImpl @Inject constructor( ) : SwiftKlibEntry { val _path: Property = objects.property(File::class.java) val _packageName: Property = objects.property(String::class.java) + val _toolsVersion: Property = objects.property(String::class.java) 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") @@ -25,6 +26,7 @@ internal abstract class SwiftKlibEntryImpl @Inject constructor( 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/api/SwiftKlibEntry.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt index 436b2b3..1840f47 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 @@ -9,6 +9,7 @@ interface SwiftKlibEntry { 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/task/CompileSwiftTask.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt index 9e0ea58..f9c75bb 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 @@ -34,6 +34,7 @@ abstract class CompileSwiftTask @Inject constructor( @Optional @Input val minMacosProperty: Property, @Optional @Input val minTvosProperty: Property, @Optional @Input val minWatchosProperty: Property, + @Optional @Input val toolsVersionProperty: Property, ) : DefaultTask() { @get:Optional @@ -85,7 +86,7 @@ abstract class CompileSwiftTask @Inject constructor( 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.get() /** * Creates build directory or cleans up if it already exists * and copies Swift source files to it @@ -138,6 +139,24 @@ abstract class CompileSwiftTask @Inject constructor( } } + if (!toolsVersion.isNullOrEmpty()) { + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "tools-version", + "--set", + toolsVersion + ) + }.run { + if (exitValue != 0) { + throw RuntimeException("Failed to set the tool version $toolsVersion") + } + } + } + dependencies.forEach { dependency -> if (dependency is SwiftPackageDependency.Local) { addDependencyBlockIfNeeded(cinteropName) From 81bdb72a821fe2bc50fc85116ab3476e95ba687f Mon Sep 17 00:00:00 2001 From: frankois Date: Wed, 6 Nov 2024 09:13:32 +0100 Subject: [PATCH 12/25] Change ExperimentalSwiftklibApi level It needs to be set at Warning Level or it will be considered as a Error. --- .../ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 { From f384dbcc477bfeb54159d1c4cfe05000ed887205 Mon Sep 17 00:00:00 2001 From: frankois Date: Wed, 6 Nov 2024 09:17:30 +0100 Subject: [PATCH 13/25] fix build (my bad) set a non null default value for toolsVersion in CompileTask --- .../github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f9c75bb..2778710 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 @@ -86,7 +86,7 @@ abstract class CompileSwiftTask @Inject constructor( 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.get() + private val toolsVersion get() = toolsVersionProperty.getOrElse("") /** * Creates build directory or cleans up if it already exists * and copies Swift source files to it @@ -317,7 +317,7 @@ abstract class CompileSwiftTask @Inject constructor( } val releaseBuildPath = - File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/release") + File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/debug") return SwiftBuildResult( libPath = File(releaseBuildPath, "lib${cinteropName}.a"), From ff6929d79962352a5332bc27289d7e86cef97749 Mon Sep 17 00:00:00 2001 From: frankois Date: Wed, 6 Nov 2024 09:19:50 +0100 Subject: [PATCH 14/25] rollback bad commit rollback unwanted commit change --- .../io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2778710..ad2df61 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 @@ -317,7 +317,7 @@ abstract class CompileSwiftTask @Inject constructor( } val releaseBuildPath = - File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/debug") + File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/release") return SwiftBuildResult( libPath = File(releaseBuildPath, "lib${cinteropName}.a"), From 0dca45c5553fd184320d1e0ac77b0ef31e40babb Mon Sep 17 00:00:00 2001 From: frankois Date: Wed, 6 Nov 2024 17:27:43 +0100 Subject: [PATCH 15/25] cleaning CompileSwiftTask moving all command for updating the manifest to CreatePackageSwift --- .../swiftklib/gradle/task/CompileSwiftTask.kt | 183 ++--------------- .../gradle/templates/CreatePackageSwift.kt | 191 +++++++++++++++++- 2 files changed, 208 insertions(+), 166 deletions(-) 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 ad2df61..c652f14 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 @@ -3,7 +3,7 @@ package io.github.ttypic.swiftklib.gradle.task import io.github.ttypic.swiftklib.gradle.CompileTarget import io.github.ttypic.swiftklib.gradle.EXTENSION_NAME import io.github.ttypic.swiftklib.gradle.SwiftPackageDependency -import io.github.ttypic.swiftklib.gradle.templates.toSwiftArgs +import io.github.ttypic.swiftklib.gradle.templates.createPackageSwiftContents import io.github.ttypic.swiftklib.gradle.util.StringReplacingOutputStream import org.gradle.api.DefaultTask import org.gradle.api.provider.ListProperty @@ -87,6 +87,7 @@ abstract class CompileSwiftTask @Inject constructor( private val minTvos get() = minTvosProperty.getOrElse("12.0") private val minWatchos get() = minWatchosProperty.getOrElse("4.0") private val toolsVersion get() = toolsVersionProperty.getOrElse("") + /** * Creates build directory or cleans up if it already exists * and copies Swift source files to it @@ -119,171 +120,21 @@ abstract class CompileSwiftTask @Inject constructor( } private fun createPackageSwift(dependencies: List) { - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "init", - "--name", - cinteropName, - "--type", - "empty", - "--disable-xctest", - "--disable-swift-testing" - ) - }.run { - if (exitValue != 0) { - throw RuntimeException("Failed to init Swift Package") - } - } - - if (!toolsVersion.isNullOrEmpty()) { - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "tools-version", - "--set", - toolsVersion - ) - }.run { - if (exitValue != 0) { - throw RuntimeException("Failed to set the tool version $toolsVersion") - } - } - } - - dependencies.forEach { dependency -> - if (dependency is SwiftPackageDependency.Local) { - addDependencyBlockIfNeeded(cinteropName) - val escapePath = dependency.path.absolutePath.replace("/", "\\/") - execOperations.exec { - it.executable = "sed" - it.workingDir = swiftBuildDir - it.args = listOf( - "-i", - "''", - "/dependencies: \\[/,/]/ s/]/ \\n\\t.package(path: \"${escapePath}\"),\\n]/", - "Package.swift" - ) - it.isIgnoreExitValue = true - } - } else { - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.args = listOf("package", "add-dependency") + dependency.toSwiftArgs() - it.isIgnoreExitValue = true - } - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package dependency $dependency", - ) - } - } - } - addPlatformBlock(cinteropName) - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.args = listOf( - "package", - "add-target", - "--path", - cinteropName, - cinteropName - ) - it.isIgnoreExitValue = true - }.run { - if (exitValue != 0) { - throw RuntimeException("Failed to add Swift Package target $cinteropName") - } - } - - dependencies.forEach { dependency -> - dependency.name.forEach { library -> - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "add-target-dependency", - library, - "--package", - dependency.packageName ?: library, - cinteropName, - ) - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package target dependency $cinteropName - package = ${dependency.packageName ?: library}:$library", - ) - } - } - } - - } - - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "add-product", - "--targets", - cinteropName, - "--type", - "static-library", - cinteropName - ) - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package library $cinteropName", - ) - } - } + createPackageSwiftContents( + cinteropName, + dependencies, + execOperations, + swiftBuildDir, + minIos, + minMacos, + minTvos, + minWatchos, + toolsVersion + ) if (printDebug) { logger.warn("======== Package.swift contents ========") logger.warn(File(swiftBuildDir, "Package.swift").readText()) - logger.warn("======== | Package.swift contents | ========") - } - } - - private fun addPlatformBlock(name: String) { - File(swiftBuildDir, "Package.swift").readText().run { - if (!contains("platforms:")) { - val entries = listOfNotNull( - ".iOS(\"$minIos\")".takeIf { !minIos.isNullOrEmpty() }, - ".macOS(\"$minMacos\")".takeIf { !minMacos.isNullOrEmpty() }, - ".tvOS(\"$minTvos\")".takeIf { !minTvos.isNullOrEmpty() }, - ".watchOS(\"$minWatchos\")".takeIf { !minWatchos.isNullOrEmpty() }, - ).joinToString(",") - if (entries.isNotEmpty()) { - val updated = - replace( - "name: \"$name\",\n", - "name: \"$name\",\n\tplatforms: [$entries],\n" - ) - File(swiftBuildDir, "Package.swift").writeText(updated) - } - } - } - } - - private fun addDependencyBlockIfNeeded(name: String) { - File(swiftBuildDir, "Package.swift").readText().run { - if (!contains("dependencies:")) { - val updated = replace("name: \"$name\"", "name: \"$name\",\n\tdependencies: []") - File(swiftBuildDir, "Package.swift").writeText(updated) - } + logger.warn("=addPlatformBlock======= | Package.swift contents | ========") } } @@ -317,7 +168,10 @@ abstract class CompileSwiftTask @Inject constructor( } val releaseBuildPath = - File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/release") + File( + swiftBuildDir, + ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/release" + ) return SwiftBuildResult( libPath = File(releaseBuildPath, "lib${cinteropName}.a"), @@ -346,7 +200,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 { 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 d1873c2..37dff98 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,12 +1,162 @@ 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, + execOperations: ExecOperations, + swiftBuildDir: File, + minIos: String, + minMacos: String, + minTvos: String, + minWatchos: String, + toolsVersion: String?, +) { + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "init", + "--name", + cinteropName, + "--type", + "empty", + "--disable-xctest", + "--disable-swift-testing" + ) + }.run { + if (exitValue != 0) { + throw RuntimeException("Failed to init Swift Package") + } + } + + if (!toolsVersion.isNullOrEmpty()) { + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "tools-version", + "--set", + toolsVersion + ) + }.run { + if (exitValue != 0) { + throw RuntimeException("Failed to set the tool version $toolsVersion") + } + } + } + + dependencies.forEach { dependency -> + if (dependency is SwiftPackageDependency.Local) { + addDependencyBlockIfNeeded(cinteropName, swiftBuildDir) + val escapePath = dependency.path.absolutePath.replace("/", "\\/") + execOperations.exec { + it.executable = "sed" + it.workingDir = swiftBuildDir + it.args = listOf( + "-i", + "''", + "/dependencies: \\[/,/]/ s/]/ \\n\\t.package(path: \"${escapePath}\"),\\n]/", + "Package.swift" + ) + it.isIgnoreExitValue = true + } + } else { + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.args = listOf("package", "add-dependency") + dependency.toSwiftArgs() + it.isIgnoreExitValue = true + } + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package dependency $dependency", + ) + } + } + } + addPlatformBlock(cinteropName, swiftBuildDir, minIos, minMacos, minTvos, minWatchos) + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.args = listOf( + "package", + "add-target", + "--path", + cinteropName, + cinteropName + ) + it.isIgnoreExitValue = true + }.run { + if (exitValue != 0) { + throw RuntimeException("Failed to add Swift Package target $cinteropName") + } + } + + dependencies.forEach { dependency -> + dependency.name.forEach { library -> + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "add-target-dependency", + library, + "--package", + dependency.packageName ?: library, + cinteropName, + ) + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package target dependency $cinteropName - package = ${dependency.packageName ?: library}:$library", + ) + } + } + } + + } + + execOperations.exec { + it.executable = "swift" + it.workingDir = swiftBuildDir + it.isIgnoreExitValue = true + it.args = listOf( + "package", + "add-product", + "--targets", + cinteropName, + "--type", + "static-library", + cinteropName + ) + }.run { + if (exitValue != 0) { + throw RuntimeException( + "Failed to add Swift Package library $cinteropName", + ) + } + } + +} + internal fun SwiftPackageDependency.toSwiftArgs(): List = when (this) { is SwiftPackageDependency.Local -> - emptyList() + emptyList() + is SwiftPackageDependency.Remote.ExactVersion -> listOf(url, "--exact", version) + is SwiftPackageDependency.Remote.VersionRange -> { if (inclusive) { // can't do inclusive range from command line @@ -17,8 +167,47 @@ internal fun SwiftPackageDependency.toSwiftArgs(): List = when (this) { listOf(url, "--from", from, "--to", to) } } + is SwiftPackageDependency.Remote.Branch -> listOf(url, "--branch", branchName) + is SwiftPackageDependency.Remote.FromVersion -> listOf(url, "--from", version) } + +private fun addDependencyBlockIfNeeded(name: String, swiftBuildDir: File) { + File(swiftBuildDir, "Package.swift").readText().run { + if (!contains("dependencies:")) { + val updated = replace("name: \"$name\"", "name: \"$name\",\n\tdependencies: []") + File(swiftBuildDir, "Package.swift").writeText(updated) + } + } +} + +private fun addPlatformBlock( + name: String, + swiftBuildDir: File, + minIos: String, + minMacos: String, + minTvos: String, + minWatchos: String +) { + File(swiftBuildDir, "Package.swift").readText().run { + if (!contains("platforms:")) { + val entries = listOfNotNull( + ".iOS(\"$minIos\")".takeIf { minIos.isNotEmpty() }, + ".macOS(\"$minMacos\")".takeIf { minMacos.isNotEmpty() }, + ".tvOS(\"$minTvos\")".takeIf { minTvos.isNotEmpty() }, + ".watchOS(\"$minWatchos\")".takeIf { minWatchos.isNotEmpty() }, + ).joinToString(",") + if (entries.isNotEmpty()) { + val updated = + replace( + "name: \"$name\",\n", + "name: \"$name\",\n\tplatforms: [$entries],\n" + ) + File(swiftBuildDir, "Package.swift").writeText(updated) + } + } + } +} From bbe6b4471b7458508c32c47caf272f021792bd62 Mon Sep 17 00:00:00 2001 From: frankois Date: Sun, 24 Nov 2024 09:15:56 +0100 Subject: [PATCH 16/25] simplify content check replace indexOfFirst with contains --- .../ttypic/swiftklib/gradle/SwiftPackageDependency.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 1969d9f..8cfb80e 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 @@ -18,7 +18,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null, ) : SwiftPackageDependency { init { - require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } + require(name.isNotEmpty() && !name.contains("")) { "Package name cannot be blank" } require(path.exists()) { "Package path must exist: $path" } } } @@ -34,7 +34,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } + require(name.isNotEmpty() && !name.contains("")) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(version.isNotBlank()) { "Version cannot be blank" } } @@ -49,7 +49,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } + require(name.isNotEmpty() && !name.contains("")) { "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" } @@ -63,7 +63,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } + require(name.isNotEmpty() && !name.contains("")) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(branchName.isNotBlank()) { "Branch name cannot be blank" } } @@ -76,7 +76,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && name.indexOfFirst { it.isBlank() } == -1) { "Package name cannot be blank" } + require(name.isNotEmpty() && !name.contains("")) { "Package name cannot be blank" } require(url.isNotBlank()) { "URL cannot be blank" } require(version.isNotBlank()) { "Version cannot be blank" } } From 18142b3c79b9d5d6df46c64bb484b336afb7c52b Mon Sep 17 00:00:00 2001 From: frankois Date: Sun, 24 Nov 2024 09:43:12 +0100 Subject: [PATCH 17/25] remove bad copy/paste --- .../io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c652f14..fc8190f 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 @@ -134,7 +134,7 @@ abstract class CompileSwiftTask @Inject constructor( if (printDebug) { logger.warn("======== Package.swift contents ========") logger.warn(File(swiftBuildDir, "Package.swift").readText()) - logger.warn("=addPlatformBlock======= | Package.swift contents | ========") + logger.warn("======== | Package.swift contents | ========") } } From 75273f045b466db14150d4e0f3f79fdae7fc9a24 Mon Sep 17 00:00:00 2001 From: frankois Date: Sun, 24 Nov 2024 15:24:02 +0100 Subject: [PATCH 18/25] use none as check if the List is not blank --- .../ttypic/swiftklib/gradle/SwiftPackageDependency.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 8cfb80e..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 @@ -18,7 +18,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null, ) : SwiftPackageDependency { init { - require(name.isNotEmpty() && !name.contains("")) { "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" } } } @@ -34,7 +34,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && !name.contains("")) { "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" } } @@ -49,7 +49,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && !name.contains("")) { "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" } @@ -63,7 +63,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && !name.contains("")) { "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" } } @@ -76,7 +76,7 @@ internal sealed interface SwiftPackageDependency : Serializable { @Input @get:Optional override val packageName: String? = null ) : Remote { init { - require(name.isNotEmpty() && !name.contains("")) { "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" } } From 32c2848f02fc734786c691cf0beb5ef4e03295c0 Mon Sep 17 00:00:00 2001 From: frankois Date: Tue, 26 Nov 2024 10:53:35 +0100 Subject: [PATCH 19/25] build the manifest from a template move from the CLI to String template toolsVersion is by default in version 5.6 and not the current compiler version The current template has been tested ion 5.6 and could not work in the later/earlier version if specified by the user. --- .../gradle/SwiftPackageModulesTest.kt | 6 +- .../gradle/fixture/SwiftKlibTestFixture.kt | 7 +- .../swiftklib/gradle/SwiftKlibEntryImpl.kt | 4 +- .../swiftklib/gradle/api/SwiftKlibEntry.kt | 2 +- .../swiftklib/gradle/task/CompileSwiftTask.kt | 11 +- .../gradle/templates/CreatePackageSwift.kt | 256 ++++++------------ 6 files changed, 90 insertions(+), 196 deletions(-) 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 f506bea..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 @@ -464,7 +464,7 @@ class SwiftPackageModulesTest { """.trimIndent()) ) .withConfiguration { - toolsVersion = "5.3" + toolsVersion = "100.0" dependencies { } } @@ -474,9 +474,9 @@ class SwiftPackageModulesTest { val result = buildAndFail(fixture.gradleProject.rootDir, "build") // Then - assertThat(result).output().contains("package manifest version 5.3.0 is too old") + assertThat(result).output().contains("is using Swift tools version 100.0.0") getManifestContent(fixture) { manifest -> - assertTrue(manifest.contains("swift-tools-version:5.3")) + assertTrue(manifest.contains("swift-tools-version: 100.0"), "must contains version 100.0") } } 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 12b073c..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 @@ -140,7 +140,7 @@ abstract class SwiftKlibTestFixture private constructor( if (entry._minWatchos.hasValue()) { appendLine(" minWatchos = \"${entry.minWatchos}\"") } - if (!entry._toolsVersions.isNullOrEmpty()) { + if (entry._toolsVersions.hasValue()) { appendLine(" toolsVersion = \"${entry.toolsVersion}\"") } @@ -197,15 +197,14 @@ private class TestSwiftKlibEntryImpl : SwiftKlibEntry { val _minMacos = notNull() val _minTvos = notNull() val _minWatchos = notNull() - val _toolsVersions: String? - get() = toolsVersion + val _toolsVersions = notNull() override var path: File by _path 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? = null + override var toolsVersion: String by _toolsVersions val dependencies = mutableListOf() 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 9333a8e..493ce66 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,7 +16,7 @@ internal abstract class SwiftKlibEntryImpl @Inject constructor( ) : SwiftKlibEntry { val _path: Property = objects.property(File::class.java) val _packageName: Property = objects.property(String::class.java) - val _toolsVersion: Property = objects.property(String::class.java) + val _toolsVersion: Property = objects.property(String::class.java).convention("5.6") 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") @@ -26,7 +26,7 @@ internal abstract class SwiftKlibEntryImpl @Inject constructor( 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() + override var toolsVersion: String by _toolsVersion.bind() internal val dependencyHandler = SwiftPackageConfigurationImpl(objects) 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 1840f47..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 @@ -9,7 +9,7 @@ interface SwiftKlibEntry { var minMacos: String var minTvos: String var minWatchos: String - var toolsVersion: String? + var toolsVersion: String fun packageName(name: String) 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 fc8190f..2b27ead 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 @@ -34,7 +34,7 @@ abstract class CompileSwiftTask @Inject constructor( @Optional @Input val minMacosProperty: Property, @Optional @Input val minTvosProperty: Property, @Optional @Input val minWatchosProperty: Property, - @Optional @Input val toolsVersionProperty: Property, + @Optional @Input val toolsVersionProperty: Property, ) : DefaultTask() { @get:Optional @@ -86,7 +86,7 @@ abstract class CompileSwiftTask @Inject constructor( 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("") + private val toolsVersion get() = toolsVersionProperty.getOrElse("5.6") /** * Creates build directory or cleans up if it already exists @@ -120,20 +120,19 @@ abstract class CompileSwiftTask @Inject constructor( } private fun createPackageSwift(dependencies: List) { - createPackageSwiftContents( + val manifest = createPackageSwiftContents( cinteropName, dependencies, - execOperations, - swiftBuildDir, minIos, minMacos, minTvos, minWatchos, toolsVersion ) + File(swiftBuildDir, "Package.swift").writeText(manifest) if (printDebug) { logger.warn("======== Package.swift contents ========") - logger.warn(File(swiftBuildDir, "Package.swift").readText()) + logger.warn(manifest) logger.warn("======== | Package.swift contents | ========") } } 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 37dff98..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 @@ -7,207 +7,103 @@ import java.io.File internal fun createPackageSwiftContents( cinteropName: String, dependencies: Collection, - execOperations: ExecOperations, - swiftBuildDir: File, minIos: String, minMacos: String, minTvos: String, minWatchos: String, - toolsVersion: String?, -) { - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "init", - "--name", - cinteropName, - "--type", - "empty", - "--disable-xctest", - "--disable-swift-testing" - ) - }.run { - if (exitValue != 0) { - throw RuntimeException("Failed to init Swift Package") - } - } - - if (!toolsVersion.isNullOrEmpty()) { - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "tools-version", - "--set", - toolsVersion - ) - }.run { - if (exitValue != 0) { - throw RuntimeException("Failed to set the tool version $toolsVersion") - } - } - } - - dependencies.forEach { dependency -> - if (dependency is SwiftPackageDependency.Local) { - addDependencyBlockIfNeeded(cinteropName, swiftBuildDir) - val escapePath = dependency.path.absolutePath.replace("/", "\\/") - execOperations.exec { - it.executable = "sed" - it.workingDir = swiftBuildDir - it.args = listOf( - "-i", - "''", - "/dependencies: \\[/,/]/ s/]/ \\n\\t.package(path: \"${escapePath}\"),\\n]/", - "Package.swift" - ) - it.isIgnoreExitValue = true - } - } else { - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.args = listOf("package", "add-dependency") + dependency.toSwiftArgs() - it.isIgnoreExitValue = true - } - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package dependency $dependency", - ) - } - } - } - addPlatformBlock(cinteropName, swiftBuildDir, minIos, minMacos, minTvos, minWatchos) - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.args = listOf( - "package", - "add-target", - "--path", - cinteropName, - cinteropName - ) - it.isIgnoreExitValue = true - }.run { - if (exitValue != 0) { - throw RuntimeException("Failed to add Swift Package target $cinteropName") - } - } - - dependencies.forEach { dependency -> - dependency.name.forEach { library -> - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "add-target-dependency", - library, - "--package", - dependency.packageName ?: library, - cinteropName, - ) - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package target dependency $cinteropName - package = ${dependency.packageName ?: library}:$library", - ) - } - } - } - - } + toolsVersion: String, +): String = """ + // swift-tools-version: $toolsVersion + import PackageDescription + + let package = Package( + name: "$cinteropName", + ${getPlatformBlock(minIos, minMacos, minTvos, minWatchos)}, + products: [ + .library( + name: "$cinteropName", + type: .static, + targets: ${getProductsTargets(cinteropName)}) + ], + dependencies: [ + ${getDependencies(dependencies)} + ], + targets: [ + .target( + name: "$cinteropName", + dependencies: [ + ${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]" +} - execOperations.exec { - it.executable = "swift" - it.workingDir = swiftBuildDir - it.isIgnoreExitValue = true - it.args = listOf( - "package", - "add-product", - "--targets", - cinteropName, - "--type", - "static-library", - cinteropName - ) - }.run { - if (exitValue != 0) { - throw RuntimeException( - "Failed to add Swift Package library $cinteropName", - ) +private fun getDependencies(dependencies: Collection): String { + return buildList { + dependencies.forEach { dependency -> + add(dependency.toSwiftPackageDependencyDeclaration()) } - } - + }.joinToString(",") } - -internal fun SwiftPackageDependency.toSwiftArgs(): List = when (this) { +private fun SwiftPackageDependency.toSwiftPackageDependencyDeclaration(): String = when (this) { is SwiftPackageDependency.Local -> - emptyList() + """ + .package(path: "${path.absolutePath}") + """.trimIndent() is SwiftPackageDependency.Remote.ExactVersion -> - listOf(url, "--exact", version) + """ + .package(url: "$url", exact: "$version") + """.trimIndent() is SwiftPackageDependency.Remote.VersionRange -> { - if (inclusive) { - // can't do inclusive range from command line - // but I think it's better to use up-to-next-minor-from - // it needs to be rethink - listOf(url, "--up-to-next-minor-from", from) - } else { - listOf(url, "--from", from, "--to", to) - } + val operator = if (inclusive) "..." else "..<" + """ + .package(url: "$url", "$from"$operator"$to") + """.trimIndent() } is SwiftPackageDependency.Remote.Branch -> - listOf(url, "--branch", branchName) + """ + .package(url: "$url", branch: "$branchName") + """.trimIndent() is SwiftPackageDependency.Remote.FromVersion -> - listOf(url, "--from", version) + """ + .package(url: "$url", from: "$version") + """.trimIndent() } -private fun addDependencyBlockIfNeeded(name: String, swiftBuildDir: File) { - File(swiftBuildDir, "Package.swift").readText().run { - if (!contains("dependencies:")) { - val updated = replace("name: \"$name\"", "name: \"$name\",\n\tdependencies: []") - File(swiftBuildDir, "Package.swift").writeText(updated) - } - } -} -private fun addPlatformBlock( - name: String, - swiftBuildDir: File, - minIos: String, - minMacos: String, - minTvos: String, - minWatchos: String -) { - File(swiftBuildDir, "Package.swift").readText().run { - if (!contains("platforms:")) { - val entries = listOfNotNull( - ".iOS(\"$minIos\")".takeIf { minIos.isNotEmpty() }, - ".macOS(\"$minMacos\")".takeIf { minMacos.isNotEmpty() }, - ".tvOS(\"$minTvos\")".takeIf { minTvos.isNotEmpty() }, - ".watchOS(\"$minWatchos\")".takeIf { minWatchos.isNotEmpty() }, - ).joinToString(",") - if (entries.isNotEmpty()) { - val updated = - replace( - "name: \"$name\",\n", - "name: \"$name\",\n\tplatforms: [$entries],\n" - ) - File(swiftBuildDir, "Package.swift").writeText(updated) +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\"]" } From e418129782c31e793f67840eb6b70c16daaf46e5 Mon Sep 17 00:00:00 2001 From: frankois Date: Tue, 26 Nov 2024 11:00:46 +0100 Subject: [PATCH 20/25] update tools version default version: 5.9 --- .../io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 493ce66..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,7 +16,7 @@ internal abstract class SwiftKlibEntryImpl @Inject constructor( ) : SwiftKlibEntry { val _path: Property = objects.property(File::class.java) val _packageName: Property = objects.property(String::class.java) - val _toolsVersion: Property = objects.property(String::class.java).convention("5.6") + 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") From e21cd68f9d40777b2714bd5a7a424f309b2d31df Mon Sep 17 00:00:00 2001 From: frankois Date: Tue, 26 Nov 2024 11:09:40 +0100 Subject: [PATCH 21/25] update tools version default version: 5.9 --- .../io/github/ttypic/swiftklib/gradle/task/CompileSwiftTask.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2b27ead..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 @@ -86,7 +86,7 @@ abstract class CompileSwiftTask @Inject constructor( 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.6") + private val toolsVersion get() = toolsVersionProperty.getOrElse("5.9") /** * Creates build directory or cleans up if it already exists From 6b81f9b42614c5e515a3672791b281e8dc53f1b2 Mon Sep 17 00:00:00 2001 From: Ilya Gulya Date: Wed, 27 Nov 2024 23:27:03 +0500 Subject: [PATCH 22/25] cleanup --- .../internal/SwiftPackageConfigurationImpl.kt | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) 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 8ad8632..2d6c77b 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 @@ -10,49 +10,37 @@ import javax.inject.Inject internal class SwiftPackageConfigurationImpl @Inject constructor( private val objects: ObjectFactory ) : SwiftPackageConfiguration { - private val _dependencies = + internal val dependencies = objects .listProperty(SwiftPackageDependency::class.java) .convention(emptyList()) - internal val dependencies get() = _dependencies @ExperimentalSwiftklibApi override fun local(name: String, path: java.io.File) { - val currentDeps = _dependencies.get().toMutableList() - currentDeps.add(SwiftPackageDependency.Local(listOf(name), path)) - _dependencies.set(currentDeps) + dependencies.add(SwiftPackageDependency.Local(listOf(name), path)) } @ExperimentalSwiftklibApi override fun remote( - names: List, + name: String, 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) + remote(listOf(name), configuration) } @ExperimentalSwiftklibApi override fun remote( - name: String, + names: List, configuration: RemotePackageConfiguration.() -> Unit ) { - val builder = RemotePackageConfigurationImpl(objects, listOf(name)) + val builder = RemotePackageConfigurationImpl(objects, names) builder.apply(configuration) val dependency = builder.build() - ?: throw IllegalStateException("No version specification provided for remote package $name") + ?: throw IllegalStateException("No version specification provided for remote package ${names.joinToString(", ")}") - val currentDeps = _dependencies.get().toMutableList() - currentDeps.add(dependency) - _dependencies.set(currentDeps) + dependencies.add(dependency) } + } From 71c9fa524f7fde0d9c85c8cb94881049795498dd Mon Sep 17 00:00:00 2001 From: Ilya Gulya Date: Wed, 27 Nov 2024 23:31:43 +0500 Subject: [PATCH 23/25] run functional tests on PR --- .github/workflows/Test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 7a5fd97..63322cc 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -1,4 +1,4 @@ -name: Release +name: Test on: push: @@ -30,4 +30,4 @@ jobs: check-latest: true - name: Run tests - run: ./gradlew test + run: ./gradlew test functionalTest From 4d6c27f7534620586681687541b5307b9e8c6b3f Mon Sep 17 00:00:00 2001 From: Ilya Gulya Date: Wed, 27 Nov 2024 23:36:51 +0500 Subject: [PATCH 24/25] don't fail test matrix fast. cancel currently running checks on new commits --- .github/workflows/Test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 63322cc..c665239 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -11,9 +11,14 @@ on: permissions: contents: write +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: strategy: + fail-fast: false matrix: os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} From 563219cda7e0c447a93b22c72d16c0d49e7e84fd Mon Sep 17 00:00:00 2001 From: Ilya Gulya Date: Wed, 27 Nov 2024 23:36:59 +0500 Subject: [PATCH 25/25] upgrade gradle to 8.11.1 --- gradle/wrapper/gradle-wrapper.jar | Bin 61574 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 31 +++++++++++++---------- gradlew.bat | 20 +++++++-------- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa754578e88a3dae77fce6e3dea56edbf..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ zQccdaaVK$5bz;*rnyK{_SQYM>;aES6Qs^lj9lEs6_J+%nIiuQC*fN;z8md>r_~Mfl zU%p5Dt_YT>gQqfr@`cR!$NWr~+`CZb%dn;WtzrAOI>P_JtsB76PYe*<%H(y>qx-`Kq!X_; z<{RpAqYhE=L1r*M)gNF3B8r(<%8mo*SR2hu zccLRZwGARt)Hlo1euqTyM>^!HK*!Q2P;4UYrysje@;(<|$&%vQekbn|0Ruu_Io(w4#%p6ld2Yp7tlA`Y$cciThP zKzNGIMPXX%&Ud0uQh!uQZz|FB`4KGD?3!ND?wQt6!n*f4EmCoJUh&b?;B{|lxs#F- z31~HQ`SF4x$&v00@(P+j1pAaj5!s`)b2RDBp*PB=2IB>oBF!*6vwr7Dp%zpAx*dPr zb@Zjq^XjN?O4QcZ*O+8>)|HlrR>oD*?WQl5ri3R#2?*W6iJ>>kH%KnnME&TT@ZzrHS$Q%LC?n|e>V+D+8D zYc4)QddFz7I8#}y#Wj6>4P%34dZH~OUDb?uP%-E zwjXM(?Sg~1!|wI(RVuxbu)-rH+O=igSho_pDCw(c6b=P zKk4ATlB?bj9+HHlh<_!&z0rx13K3ZrAR8W)!@Y}o`?a*JJsD+twZIv`W)@Y?Amu_u zz``@-e2X}27$i(2=9rvIu5uTUOVhzwu%mNazS|lZb&PT;XE2|B&W1>=B58#*!~D&) zfVmJGg8UdP*fx(>Cj^?yS^zH#o-$Q-*$SnK(ZVFkw+er=>N^7!)FtP3y~Xxnu^nzY zikgB>Nj0%;WOltWIob|}%lo?_C7<``a5hEkx&1ku$|)i>Rh6@3h*`slY=9U}(Ql_< zaNG*J8vb&@zpdhAvv`?{=zDedJ23TD&Zg__snRAH4eh~^oawdYi6A3w8<Ozh@Kw)#bdktM^GVb zrG08?0bG?|NG+w^&JvD*7LAbjED{_Zkc`3H!My>0u5Q}m!+6VokMLXxl`Mkd=g&Xx z-a>m*#G3SLlhbKB!)tnzfWOBV;u;ftU}S!NdD5+YtOjLg?X}dl>7m^gOpihrf1;PY zvll&>dIuUGs{Qnd- zwIR3oIrct8Va^Tm0t#(bJD7c$Z7DO9*7NnRZorrSm`b`cxz>OIC;jSE3DO8`hX955ui`s%||YQtt2 z5DNA&pG-V+4oI2s*x^>-$6J?p=I>C|9wZF8z;VjR??Icg?1w2v5Me+FgAeGGa8(3S z4vg*$>zC-WIVZtJ7}o9{D-7d>zCe|z#<9>CFve-OPAYsneTb^JH!Enaza#j}^mXy1 z+ULn^10+rWLF6j2>Ya@@Kq?26>AqK{A_| zQKb*~F1>sE*=d?A?W7N2j?L09_7n+HGi{VY;MoTGr_)G9)ot$p!-UY5zZ2Xtbm=t z@dpPSGwgH=QtIcEulQNI>S-#ifbnO5EWkI;$A|pxJd885oM+ zGZ0_0gDvG8q2xebj+fbCHYfAXuZStH2j~|d^sBAzo46(K8n59+T6rzBwK)^rfPT+B zyIFw)9YC-V^rhtK`!3jrhmW-sTmM+tPH+;nwjL#-SjQPUZ53L@A>y*rt(#M(qsiB2 zx6B)dI}6Wlsw%bJ8h|(lhkJVogQZA&n{?Vgs6gNSXzuZpEyu*xySy8ro07QZ7Vk1!3tJphN_5V7qOiyK8p z#@jcDD8nmtYi1^l8ml;AF<#IPK?!pqf9D4moYk>d99Im}Jtwj6c#+A;f)CQ*f-hZ< z=p_T86jog%!p)D&5g9taSwYi&eP z#JuEK%+NULWus;0w32-SYFku#i}d~+{Pkho&^{;RxzP&0!RCm3-9K6`>KZpnzS6?L z^H^V*s!8<>x8bomvD%rh>Zp3>Db%kyin;qtl+jAv8Oo~1g~mqGAC&Qi_wy|xEt2iz zWAJEfTV%cl2Cs<1L&DLRVVH05EDq`pH7Oh7sR`NNkL%wi}8n>IXcO40hp+J+sC!W?!krJf!GJNE8uj zg-y~Ns-<~D?yqbzVRB}G>0A^f0!^N7l=$m0OdZuqAOQqLc zX?AEGr1Ht+inZ-Qiwnl@Z0qukd__a!C*CKuGdy5#nD7VUBM^6OCpxCa2A(X;e0&V4 zM&WR8+wErQ7UIc6LY~Q9x%Sn*Tn>>P`^t&idaOEnOd(Ufw#>NoR^1QdhJ8s`h^|R_ zXX`c5*O~Xdvh%q;7L!_!ohf$NfEBmCde|#uVZvEo>OfEq%+Ns7&_f$OR9xsihRpBb z+cjk8LyDm@U{YN>+r46?nn{7Gh(;WhFw6GAxtcKD+YWV?uge>;+q#Xx4!GpRkVZYu zzsF}1)7$?%s9g9CH=Zs+B%M_)+~*j3L0&Q9u7!|+T`^O{xE6qvAP?XWv9_MrZKdo& z%IyU)$Q95AB4!#hT!_dA>4e@zjOBD*Y=XjtMm)V|+IXzjuM;(l+8aA5#Kaz_$rR6! zj>#&^DidYD$nUY(D$mH`9eb|dtV0b{S>H6FBfq>t5`;OxA4Nn{J(+XihF(stSche7$es&~N$epi&PDM_N`As;*9D^L==2Q7Z2zD+CiU(|+-kL*VG+&9!Yb3LgPy?A zm7Z&^qRG_JIxK7-FBzZI3Q<;{`DIxtc48k> zc|0dmX;Z=W$+)qE)~`yn6MdoJ4co;%!`ddy+FV538Y)j(vg}5*k(WK)KWZ3WaOG!8 z!syGn=s{H$odtpqFrT#JGM*utN7B((abXnpDM6w56nhw}OY}0TiTG1#f*VFZr+^-g zbP10`$LPq_;PvrA1XXlyx2uM^mrjTzX}w{yuLo-cOClE8MMk47T25G8M!9Z5ypOSV zAJUBGEg5L2fY)ZGJb^E34R2zJ?}Vf>{~gB!8=5Z) z9y$>5c)=;o0HeHHSuE4U)#vG&KF|I%-cF6f$~pdYJWk_dD}iOA>iA$O$+4%@>JU08 zS`ep)$XLPJ+n0_i@PkF#ri6T8?ZeAot$6JIYHm&P6EB=BiaNY|aA$W0I+nz*zkz_z zkEru!tj!QUffq%)8y0y`T&`fuus-1p>=^hnBiBqD^hXrPs`PY9tU3m0np~rISY09> z`P3s=-kt_cYcxWd{de@}TwSqg*xVhp;E9zCsnXo6z z?f&Sv^U7n4`xr=mXle94HzOdN!2kB~4=%)u&N!+2;z6UYKUDqi-s6AZ!haB;@&B`? z_TRX0%@suz^TRdCb?!vNJYPY8L_}&07uySH9%W^Tc&1pia6y1q#?*Drf}GjGbPjBS zbOPcUY#*$3sL2x4v_i*Y=N7E$mR}J%|GUI(>WEr+28+V z%v5{#e!UF*6~G&%;l*q*$V?&r$Pp^sE^i-0$+RH3ERUUdQ0>rAq2(2QAbG}$y{de( z>{qD~GGuOk559Y@%$?N^1ApVL_a704>8OD%8Y%8B;FCt%AoPu8*D1 zLB5X>b}Syz81pn;xnB}%0FnwazlWfUV)Z-~rZg6~b z6!9J$EcE&sEbzcy?CI~=boWA&eeIa%z(7SE^qgVLz??1Vbc1*aRvc%Mri)AJaAG!p z$X!_9Ds;Zz)f+;%s&dRcJt2==P{^j3bf0M=nJd&xwUGlUFn?H=2W(*2I2Gdu zv!gYCwM10aeus)`RIZSrCK=&oKaO_Ry~D1B5!y0R=%!i2*KfXGYX&gNv_u+n9wiR5 z*e$Zjju&ODRW3phN925%S(jL+bCHv6rZtc?!*`1TyYXT6%Ju=|X;6D@lq$8T zW{Y|e39ioPez(pBH%k)HzFITXHvnD6hw^lIoUMA;qAJ^CU?top1fo@s7xT13Fvn1H z6JWa-6+FJF#x>~+A;D~;VDs26>^oH0EI`IYT2iagy23?nyJ==i{g4%HrAf1-*v zK1)~@&(KkwR7TL}L(A@C_S0G;-GMDy=MJn2$FP5s<%wC)4jC5PXoxrQBFZ_k0P{{s@sz+gX`-!=T8rcB(=7vW}^K6oLWMmp(rwDh}b zwaGGd>yEy6fHv%jM$yJXo5oMAQ>c9j`**}F?MCry;T@47@r?&sKHgVe$MCqk#Z_3S z1GZI~nOEN*P~+UaFGnj{{Jo@16`(qVNtbU>O0Hf57-P>x8Jikp=`s8xWs^dAJ9lCQ z)GFm+=OV%AMVqVATtN@|vp61VVAHRn87}%PC^RAzJ%JngmZTasWBAWsoAqBU+8L8u z4A&Pe?fmTm0?mK-BL9t+{y7o(7jm+RpOhL9KnY#E&qu^}B6=K_dB}*VlSEiC9fn)+V=J;OnN)Ta5v66ic1rG+dGAJ1 z1%Zb_+!$=tQ~lxQrzv3x#CPb?CekEkA}0MYSgx$Jdd}q8+R=ma$|&1a#)TQ=l$1tQ z=tL9&_^vJ)Pk}EDO-va`UCT1m#Uty1{v^A3P~83_#v^ozH}6*9mIjIr;t3Uv%@VeW zGL6(CwCUp)Jq%G0bIG%?{_*Y#5IHf*5M@wPo6A{$Um++Co$wLC=J1aoG93&T7Ho}P z=mGEPP7GbvoG!uD$k(H3A$Z))+i{Hy?QHdk>3xSBXR0j!11O^mEe9RHmw!pvzv?Ua~2_l2Yh~_!s1qS`|0~0)YsbHSz8!mG)WiJE| z2f($6TQtt6L_f~ApQYQKSb=`053LgrQq7G@98#igV>y#i==-nEjQ!XNu9 z~;mE+gtj4IDDNQJ~JVk5Ux6&LCSFL!y=>79kE9=V}J7tD==Ga+IW zX)r7>VZ9dY=V&}DR))xUoV!u(Z|%3ciQi_2jl}3=$Agc(`RPb z8kEBpvY>1FGQ9W$n>Cq=DIpski};nE)`p3IUw1Oz0|wxll^)4dq3;CCY@RyJgFgc# zKouFh!`?Xuo{IMz^xi-h=StCis_M7yq$u) z?XHvw*HP0VgR+KR6wI)jEMX|ssqYvSf*_3W8zVTQzD?3>H!#>InzpSO)@SC8q*ii- z%%h}_#0{4JG;Jm`4zg};BPTGkYamx$Xo#O~lBirRY)q=5M45n{GCfV7h9qwyu1NxOMoP4)jjZMxmT|IQQh0U7C$EbnMN<3)Kk?fFHYq$d|ICu>KbY_hO zTZM+uKHe(cIZfEqyzyYSUBZa8;Fcut-GN!HSA9ius`ltNebF46ZX_BbZNU}}ZOm{M2&nANL9@0qvih15(|`S~z}m&h!u4x~(%MAO$jHRWNfuxWF#B)E&g3ghSQ9|> z(MFaLQj)NE0lowyjvg8z0#m6FIuKE9lDO~Glg}nSb7`~^&#(Lw{}GVOS>U)m8bF}x zVjbXljBm34Cs-yM6TVusr+3kYFjr28STT3g056y3cH5Tmge~ASxBj z%|yb>$eF;WgrcOZf569sDZOVwoo%8>XO>XQOX1OyN9I-SQgrm;U;+#3OI(zrWyow3 zk==|{lt2xrQ%FIXOTejR>;wv(Pb8u8}BUpx?yd(Abh6? zsoO3VYWkeLnF43&@*#MQ9-i-d0t*xN-UEyNKeyNMHw|A(k(_6QKO=nKMCxD(W(Yop zsRQ)QeL4X3Lxp^L%wzi2-WVSsf61dqliPUM7srDB?Wm6Lzn0&{*}|IsKQW;02(Y&| zaTKv|`U(pSzuvR6Rduu$wzK_W-Y-7>7s?G$)U}&uK;<>vU}^^ns@Z!p+9?St1s)dG zK%y6xkPyyS1$~&6v{kl?Md6gwM|>mt6Upm>oa8RLD^8T{0?HC!Z>;(Bob7el(DV6x zi`I)$&E&ngwFS@bi4^xFLAn`=fzTC;aimE^!cMI2n@Vo%Ae-ne`RF((&5y6xsjjAZ zVguVoQ?Z9uk$2ON;ersE%PU*xGO@T*;j1BO5#TuZKEf(mB7|g7pcEA=nYJ{s3vlbg zd4-DUlD{*6o%Gc^N!Nptgay>j6E5;3psI+C3Q!1ZIbeCubW%w4pq9)MSDyB{HLm|k zxv-{$$A*pS@csolri$Ge<4VZ}e~78JOL-EVyrbxKra^d{?|NnPp86!q>t<&IP07?Z z^>~IK^k#OEKgRH+LjllZXk7iA>2cfH6+(e&9ku5poo~6y{GC5>(bRK7hwjiurqAiZ zg*DmtgY}v83IjE&AbiWgMyFbaRUPZ{lYiz$U^&Zt2YjG<%m((&_JUbZcfJ22(>bi5 z!J?<7AySj0JZ&<-qXX;mcV!f~>G=sB0KnjWca4}vrtunD^1TrpfeS^4dvFr!65knK zZh`d;*VOkPs4*-9kL>$GP0`(M!j~B;#x?Ba~&s6CopvO86oM?-? zOw#dIRc;6A6T?B`Qp%^<U5 z19x(ywSH$_N+Io!6;e?`tWaM$`=Db!gzx|lQ${DG!zb1Zl&|{kX0y6xvO1o z220r<-oaS^^R2pEyY;=Qllqpmue|5yI~D|iI!IGt@iod{Opz@*ml^w2bNs)p`M(Io z|E;;m*Xpjd9l)4G#KaWfV(t8YUn@A;nK^#xgv=LtnArX|vWQVuw3}B${h+frU2>9^ z!l6)!Uo4`5k`<<;E(ido7M6lKTgWezNLq>U*=uz&s=cc$1%>VrAeOoUtA|T6gO4>UNqsdK=NF*8|~*sl&wI=x9-EGiq*aqV!(VVXA57 zw9*o6Ir8Lj1npUXvlevtn(_+^X5rzdR>#(}4YcB9O50q97%rW2me5_L=%ffYPUSRc z!vv?Kv>dH994Qi>U(a<0KF6NH5b16enCp+mw^Hb3Xs1^tThFpz!3QuN#}KBbww`(h z7GO)1olDqy6?T$()R7y%NYx*B0k_2IBiZ14&8|JPFxeMF{vSTxF-Vi3+ZOI=Thq2} zyQgjYY1_7^ZQHh{?P))4+qUiQJLi1&{yE>h?~jU%tjdV0h|FENbM3X(KnJdPKc?~k zh=^Ixv*+smUll!DTWH!jrV*wSh*(mx0o6}1@JExzF(#9FXgmTXVoU+>kDe68N)dkQ zH#_98Zv$}lQwjKL@yBd;U(UD0UCl322=pav<=6g>03{O_3oKTq;9bLFX1ia*lw;#K zOiYDcBJf)82->83N_Y(J7Kr_3lE)hAu;)Q(nUVydv+l+nQ$?|%MWTy`t>{havFSQloHwiIkGK9YZ79^9?AZo0ZyQlVR#}lF%dn5n%xYksXf8gnBm=wO7g_^! zauQ-bH1Dc@3ItZ-9D_*pH}p!IG7j8A_o94#~>$LR|TFq zZ-b00*nuw|-5C2lJDCw&8p5N~Z1J&TrcyErds&!l3$eSz%`(*izc;-?HAFD9AHb-| z>)id`QCrzRws^9(#&=pIx9OEf2rmlob8sK&xPCWS+nD~qzU|qG6KwA{zbikcfQrdH z+ zQg>O<`K4L8rN7`GJB0*3<3`z({lWe#K!4AZLsI{%z#ja^OpfjU{!{)x0ZH~RB0W5X zTwN^w=|nA!4PEU2=LR05x~}|B&ZP?#pNgDMwD*ajI6oJqv!L81gu=KpqH22avXf0w zX3HjbCI!n9>l046)5rr5&v5ja!xkKK42zmqHzPx$9Nn_MZk`gLeSLgC=LFf;H1O#B zn=8|^1iRrujHfbgA+8i<9jaXc;CQBAmQvMGQPhFec2H1knCK2x!T`e6soyrqCamX% zTQ4dX_E*8so)E*TB$*io{$c6X)~{aWfaqdTh=xEeGvOAN9H&-t5tEE-qso<+C!2>+ zskX51H-H}#X{A75wqFe-J{?o8Bx|>fTBtl&tcbdR|132Ztqu5X0i-pisB-z8n71%q%>EF}yy5?z=Ve`}hVh{Drv1YWL zW=%ug_&chF11gDv3D6B)Tz5g54H0mDHNjuKZ+)CKFk4Z|$RD zfRuKLW`1B>B?*RUfVd0+u8h3r-{@fZ{k)c!93t1b0+Q9vOaRnEn1*IL>5Z4E4dZ!7 ztp4GP-^1d>8~LMeb}bW!(aAnB1tM_*la=Xx)q(I0Y@__Zd$!KYb8T2VBRw%e$iSdZ zkwdMwd}eV9q*;YvrBFTv1>1+}{H!JK2M*C|TNe$ZSA>UHKk);wz$(F$rXVc|sI^lD zV^?_J!3cLM;GJuBMbftbaRUs$;F}HDEDtIeHQ)^EJJ1F9FKJTGH<(Jj`phE6OuvE) zqK^K`;3S{Y#1M@8yRQwH`?kHMq4tHX#rJ>5lY3DM#o@or4&^_xtBC(|JpGTfrbGkA z2Tu+AyT^pHannww!4^!$5?@5v`LYy~T`qs7SYt$JgrY(w%C+IWA;ZkwEF)u5sDvOK zGk;G>Mh&elvXDcV69J_h02l&O;!{$({fng9Rlc3ID#tmB^FIG^w{HLUpF+iB`|
NnX)EH+Nua)3Y(c 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 ScNgau(U?ep-K_E5zy1%ZQTdPn diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2617362..e2847c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 65dcd68..b740cf1 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..25da30d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail