diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 7a5fd97..c665239 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -1,4 +1,4 @@ -name: Release +name: Test on: push: @@ -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 }} @@ -30,4 +35,4 @@ jobs: check-latest: true - name: Run tests - run: ./gradlew test + run: ./gradlew test functionalTest 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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cb..e644113 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ 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 diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index dd7da3d..4eaf068 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -2,20 +2,31 @@ 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) + functionalTestImplementation(project(":plugin")) + 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 +34,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..e623eee --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/CinteropModulesTest.kt @@ -0,0 +1,66 @@ +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.KotlinSource +import io.github.ttypic.swiftklib.gradle.fixture.SwiftKlibTestFixture +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 = 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") + + // Then + assertThat(result).task(":library:build").succeeded() + } + + @Test + fun `build on linux results in warning about unsupported OS`() { + assumeLinux() + + // Given + 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") + + // 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..135ec6f --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageModulesTest.kt @@ -0,0 +1,517 @@ +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.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 + +class SwiftPackageModulesTest { + + @Test + fun `build with remote SPM dependency using exact version is successful`() { + // Given + 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()) + ) + .withConfiguration { + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + exactVersion("4.2.2") + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "KeychainAccess") + } + + @Test + fun `build with remote SPM dependency using version range is successful`() { + 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()) + ) + .withConfiguration { + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + versionRange("4.0.0", "5.0.0", true) + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "KeychainAccess") + } + + @Test + fun `build with remote SPM dependency using branch is successful`() { + 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()) + ) + .withConfiguration { + dependencies { + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + branch("master") + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "KeychainAccess") + } + + @Test + fun `build with local SPM dependency is successful`() { + // Given + 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 + + @objc public class VersionProvider: NSObject { + @objc public class func getLocalVersion() -> String { + return LocalHelper.getVersion() + } + } + """.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`() { + 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()) + ) + .withConfiguration { + dependencies { + 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, "KeychainAccess", "SwiftyJSON") + } + + @Test + fun `build fails with blank package name`() { + // Given + val fixture = SwiftKlibTestFixture.builder() + .withConfiguration { + dependencies { + remote("") { // Empty package name + github("example", "test") + exactVersion("1.0.0") + } + } + } + .build() + + // 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 = SwiftKlibTestFixture.builder() + .withConfiguration { + dependencies { + remote("Test") { + github("example", "test") + exactVersion("") // Empty version + } + } + } + .build() + + // 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 = SwiftKlibTestFixture.builder() + .withConfiguration { + dependencies { + remote("Test") { + github("example", "test") + // No version specified + } + } + } + .build() + + // 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 nonexistentPath = File("nonexistent/path") + val fixture = SwiftKlibTestFixture.builder() + .withConfiguration { + dependencies { + local("LocalPackage", nonexistentPath) + } + } + .build() + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("Package path must exist") + } + + @Test + fun `build with remote SPM dependency using Firebase is successful`() { + // Given + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import FirebaseAuth + import Firebase + + @objc public class FirebaseData: NSObject { + @objc public func printVersion() { + print(FirebaseVersion()) + print(ActionCodeOperation.emailLink) + } + } + """.trimIndent()) + ) + .withConfiguration { + minIos = "14.0" + minMacos = "10.15" + dependencies { + remote("FirebaseAuth") { + url("https://github.com/firebase/firebase-ios-sdk.git", "firebase-ios-sdk") + exactVersion("11.0.0") + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "firebase-ios-sdk") + } + + @Test + fun `build with remote SPM dependency using multi product Firebase is successful`() { + // Given + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import FirebaseAuth + import Firebase + import FirebaseRemoteConfig + + @objc public class FirebaseData: NSObject { + @objc public func testLinking() { + print(FirebaseVersion()) + print(ActionCodeOperation.emailLink) + print(RemoteConfigSettings()) + } + } + """.trimIndent()) + ) + .withConfiguration { + minIos = "14.0" + minMacos = "10.15" + dependencies { + remote(listOf("FirebaseAuth", "FirebaseRemoteConfig")) { + url("https://github.com/firebase/firebase-ios-sdk.git", "firebase-ios-sdk") + exactVersion("11.0.0") + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "firebase-ios-sdk") + } + + @Test + fun `build with complex and mix spm repo`() { + // Given + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import FirebaseAuth + import Firebase + import FirebaseRemoteConfig + import KeychainAccess + import SwiftyJSON + + @objc public class FirebaseData: NSObject { + @objc public func testLinking() { + print(FirebaseVersion()) + print(ActionCodeOperation.emailLink) + print(RemoteConfigSettings()) + } + } + @objc public class DataManager: NSObject { + private let keychain = Keychain(service: "test-service") + + @objc public func processJson(jsonString: String) throws -> String { + let json = try JSON(parseJSON: jsonString) + return json.description + } + } + """.trimIndent()) + ) + .withConfiguration { + minIos = "14.0" + minMacos = "10.15" + dependencies { + remote(listOf("FirebaseAuth", "FirebaseRemoteConfig")) { + url("https://github.com/firebase/firebase-ios-sdk.git", "firebase-ios-sdk") + exactVersion("11.0.0") + } + remote("KeychainAccess") { + github("kishikawakatsumi", "KeychainAccess") + exactVersion("4.2.2") + } + remote("SwiftyJSON") { + github("SwiftyJSON", "SwiftyJSON") + versionRange("5.0.0", "6.0.0", true) + } + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + assertPackageResolved(fixture, "firebase-ios-sdk") + } + + @Test + fun `build with valid toolsVersion`() { + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import Foundation + """.trimIndent()) + ) + .withConfiguration { + toolsVersion = "5.5" + dependencies { + } + } + .build() + + // When + val result = build(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).task(":library:build").succeeded() + getManifestContent(fixture) { manifest -> + assertTrue(manifest.contains("swift-tools-version: 5.5")) + } + } + + @Test + fun `build with invalid toolsVersion`() { + val fixture = SwiftKlibTestFixture.builder() + .withSwiftSources( + SwiftSource.of(content = """ + import Foundation + """.trimIndent()) + ) + .withConfiguration { + toolsVersion = "100.0" + dependencies { + } + } + .build() + + // When + val result = buildAndFail(fixture.gradleProject.rootDir, "build") + + // Then + assertThat(result).output().contains("is using Swift tools version 100.0.0") + getManifestContent(fixture) { manifest -> + assertTrue(manifest.contains("swift-tools-version: 100.0"), "must contains version 100.0") + } + } + + private fun assertPackageResolved(fixture: SwiftKlibTestFixture, vararg packageNames: String) { + val resolvedFile = File( + fixture.gradleProject.rootDir, + "library/build/swiftklib/test/iosArm64/swiftBuild/Package.resolved" + ) + assertTrue(resolvedFile.exists(), "Package.resolved file 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/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/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/SwiftKlibTestFixture.kt b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt new file mode 100644 index 0000000..b1d2264 --- /dev/null +++ b/plugin/src/functionalTest/kotlin/io/github/ttypic/swiftklib/gradle/fixture/SwiftKlibTestFixture.kt @@ -0,0 +1,356 @@ +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 = \"${entry.minIos}\"") + } + if (entry._minMacos.hasValue()) { + appendLine(" minMacos = \"${entry.minMacos}\"") + } + if (entry._minTvos.hasValue()) { + appendLine(" minTvos = \"${entry.minTvos}\"") + } + if (entry._minWatchos.hasValue()) { + appendLine(" minWatchos = \"${entry.minWatchos}\"") + } + if (entry._toolsVersions.hasValue()) { + appendLine(" toolsVersion = \"${entry.toolsVersion}\"") + } + + 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() + 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 by _toolsVersions + + 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) { + remote(listOf(name), configuration) + } + + override fun remote(names: List, configuration: RemotePackageConfiguration.() -> Unit) { + val config = TestRemotePackageConfigurationImpl(names) + config.configuration() + dependencies.add(config.build()) + } +} + +private class TestRemotePackageConfigurationImpl(private val name: List) : + RemotePackageConfiguration { + + private var url: String? = null + private var packageName: String? = null + private var versionConfig: TestVersionConfig? = null + + override fun github(owner: String, repo: String, packageName: String?) { + url = "https://github.com/$owner/$repo.git" + this.packageName = packageName + } + + override fun url(url: String, packageName: String?) { + this.url = url + this.packageName = packageName + } + + 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, + packageName = packageName + ) + } +} + +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: List, + val url: String?, + val version: TestVersionConfig?, + val packageName: String? + ) : TestDependencyConfig { + override fun toConfigString() = buildString { + 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") + } else { + 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/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..a07ad11 --- /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: List +) { + 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 deleted file mode 100644 index ebd0050..0000000 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntry.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.ttypic.swiftklib.gradle - -import org.gradle.api.model.ObjectFactory -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 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) - - fun packageName(name: String) = packageName.set(name) -} 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..636f356 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftKlibEntryImpl.kt @@ -0,0 +1,50 @@ +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 _toolsVersion: Property = objects.property(String::class.java).convention("5.9") + val _minIos: Property = objects.property(String::class.java).convention("12.0") + val _minMacos: Property = objects.property(String::class.java).convention("10.13") + val _minTvos: Property = objects.property(String::class.java).convention("12.0") + val _minWatchos: Property = objects.property(String::class.java).convention("4.0") + override var path: File by _path.bind() + override var minIos: String by _minIos.bind() + override var minMacos: String by _minMacos.bind() + override var minTvos: String by _minTvos.bind() + override var minWatchos: String by _minWatchos.bind() + override var toolsVersion: String by _toolsVersion.bind() + + internal val dependencyHandler = SwiftPackageConfigurationImpl(objects) + + 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 9c93385..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 @@ -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") @@ -40,16 +49,20 @@ class SwiftKlibPlugin : Plugin { tasks.register( taskName, CompileSwiftTask::class.java, + project.hasProperty("swiftklibDebug"), 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, + entry._toolsVersion + ).configure { + it.dependenciesProperty = entry.dependencyHandler.dependencies + } } } diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt new file mode 100644 index 0000000..491cea3 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/SwiftPackageDependency.kt @@ -0,0 +1,85 @@ +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: List + @get:Input @get:Optional + val packageName: String? + + data class Local( + @Input override val name: List, + @InputDirectory val path: File, + @Input @get:Optional override val packageName: String? = null, + ) : SwiftPackageDependency { + init { + require(name.isNotEmpty() && name.none { it.isBlank() }) { "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: List, + @Input override val url: String, + @Input val version: String, + @Input @get:Optional override val packageName: String? = null + ) : Remote { + init { + require(name.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } + require(url.isNotBlank()) { "URL cannot be blank" } + require(version.isNotBlank()) { "Version cannot be blank" } + } + } + + data class VersionRange( + @Input override val name: List, + @Input override val url: String, + @Input val from: String, + @Input val to: String, + @Input val inclusive: Boolean = true, + @Input @get:Optional override val packageName: String? = null + ) : Remote { + init { + 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" } + } + } + + data class Branch( + @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.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } + require(url.isNotBlank()) { "URL cannot be blank" } + require(branchName.isNotBlank()) { "Branch name cannot be blank" } + } + } + + data class FromVersion( + @Input override val name: List, + @Input override val url: String, + @Input val version: String, + @Input @get:Optional override val packageName: String? = null + ) : Remote { + init { + require(name.isNotEmpty() && name.none { it.isBlank() }) { "Package name cannot be blank" } + require(url.isNotBlank()) { "URL cannot be blank" } + require(version.isNotBlank()) { "Version cannot be blank" } + } + } + } +} diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt new file mode 100644 index 0000000..13eda5d --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/ExperimentalSwiftklibApi.kt @@ -0,0 +1,10 @@ +package io.github.ttypic.swiftklib.gradle.api + +@RequiresOptIn( + message = "This API is experimental. It may be changed in the future without notice.", + level = RequiresOptIn.Level.WARNING +) +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) +annotation class ExperimentalSwiftklibApi { +} diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt new file mode 100644 index 0000000..3d07cb4 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/RemotePackageConfiguration.kt @@ -0,0 +1,40 @@ +package io.github.ttypic.swiftklib.gradle.api + +@ExperimentalSwiftklibApi +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, 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, packageName: String? = null) + + /** + * 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..d037a66 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftKlibEntry.kt @@ -0,0 +1,19 @@ +package io.github.ttypic.swiftklib.gradle.api + +import java.io.File + +interface SwiftKlibEntry { + var path: File + + var minIos: String + var minMacos: String + var minTvos: String + var minWatchos: String + var toolsVersion: String + + 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..d45c323 --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/api/SwiftPackageConfiguration.kt @@ -0,0 +1,26 @@ +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 the product's name to add + * @param configuration Configuration block for the remote package + */ + fun remote(name: String, configuration: RemotePackageConfiguration.() -> Unit) + + /** + * Configures a remote package dependency. + * @param names a list of product's name to add + * @param configuration Configuration block for the remote package + */ + fun remote(names: List, configuration: RemotePackageConfiguration.() -> Unit) +} + diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt new file mode 100644 index 0000000..44b81fa --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/RemotePackageConfigurationImpl.kt @@ -0,0 +1,72 @@ +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: List +) : RemotePackageConfiguration { + private val urlProperty = objects.property(String::class.java) + private val packageName = objects.property(String::class.java) + private var dependency: SwiftPackageDependency.Remote? = null + + override fun github(owner: String, repo: String, 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, 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, + packageName = packageName.orNull + ) + } + + override fun versionRange(from: String, to: String, inclusive: Boolean) { + dependency = SwiftPackageDependency.Remote.VersionRange( + name = name, + url = requireUrl(), + from = from, + to = to, + inclusive = inclusive, + packageName = packageName.orNull + ) + } + + override fun branch(branchName: String) { + dependency = SwiftPackageDependency.Remote.Branch( + name = name, + url = requireUrl(), + branchName = branchName, + packageName = packageName.orNull + ) + } + + override fun fromVersion(version: String) { + dependency = SwiftPackageDependency.Remote.FromVersion( + name = name, + url = requireUrl(), + version = version, + packageName = packageName.orNull + ) + } + + 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..2d6c77b --- /dev/null +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/internal/SwiftPackageConfigurationImpl.kt @@ -0,0 +1,46 @@ +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 { + internal val dependencies = + objects + .listProperty(SwiftPackageDependency::class.java) + .convention(emptyList()) + + + @ExperimentalSwiftklibApi + override fun local(name: String, path: java.io.File) { + dependencies.add(SwiftPackageDependency.Local(listOf(name), path)) + } + + @ExperimentalSwiftklibApi + override fun remote( + name: String, + configuration: RemotePackageConfiguration.() -> Unit + ) { + remote(listOf(name), configuration) + } + + @ExperimentalSwiftklibApi + override fun remote( + names: List, + configuration: RemotePackageConfiguration.() -> Unit + ) { + val builder = RemotePackageConfigurationImpl(objects, names) + builder.apply(configuration) + + val dependency = builder.build() + ?: throw IllegalStateException("No version specification provided for remote package ${names.joinToString(", ")}") + + dependencies.add(dependency) + } + +} 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..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 @@ -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,17 +24,23 @@ 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, @InputDirectory val pathProperty: Property, @Input val packageNameProperty: Property, - @Optional @Input val minIosProperty: Property, - @Optional @Input val minMacosProperty: Property, - @Optional @Input val minTvosProperty: Property, - @Optional @Input val minWatchosProperty: Property, + @Optional @Input val minIosProperty: Property, + @Optional @Input val minMacosProperty: Property, + @Optional @Input val minTvosProperty: Property, + @Optional @Input val minWatchosProperty: Property, + @Optional @Input val toolsVersionProperty: Property, ) : DefaultTask() { + @get:Optional + @get:Nested + internal abstract var dependenciesProperty: ListProperty + @get:Internal internal val targetDir: File get() { @@ -52,9 +61,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) @@ -66,10 +82,11 @@ abstract class CompileSwiftTask @Inject constructor( ) } - private val minIos get() = minIosProperty.getOrElse(13) - private val minMacos get() = minMacosProperty.getOrElse(11) - private val minTvos get() = minTvosProperty.getOrElse(13) - private val minWatchos get() = minWatchosProperty.getOrElse(8) + private val minIos get() = minIosProperty.getOrElse("12.0") + private val minMacos get() = minMacosProperty.getOrElse("10.13") + private val minTvos get() = minTvosProperty.getOrElse("12.0") + private val minWatchos get() = minWatchosProperty.getOrElse("4.0") + private val toolsVersion get() = toolsVersionProperty.getOrElse("5.9") /** * Creates build directory or cleans up if it already exists @@ -87,12 +104,37 @@ abstract class CompileSwiftTask @Inject constructor( private fun buildDir() = File(swiftBuildDir, cinteropName) - /** - * Creates `Package.Swift` file for the library - */ - private fun createPackageSwift() { - File(swiftBuildDir, "Package.swift") - .writeText(createPackageSwiftContents(cinteropName)) + 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 manifest = createPackageSwiftContents( + cinteropName, + dependencies, + minIos, + minMacos, + minTvos, + minWatchos, + toolsVersion + ) + File(swiftBuildDir, "Package.swift").writeText(manifest) + if (printDebug) { + logger.warn("======== Package.swift contents ========") + logger.warn(manifest) + logger.warn("======== | Package.swift contents | ========") + } } private fun buildSwift(xcodeVersion: Int): SwiftBuildResult { @@ -124,7 +166,11 @@ abstract class CompileSwiftTask @Inject constructor( ) } - val releaseBuildPath = File(swiftBuildDir, ".build/${compileTarget.arch()}-apple-macosx/release") + val releaseBuildPath = + File( + swiftBuildDir, + ".build/${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${compileTarget.simulatorSuffix()}/release" + ) return SwiftBuildResult( libPath = File(releaseBuildPath, "lib${cinteropName}.a"), @@ -134,16 +180,16 @@ abstract class CompileSwiftTask @Inject constructor( private fun generateBuildArgs(): List { val sdkPath = readSdkPath() - val baseArgs = "swift build --arch ${compileTarget.arch()} -c release".split(" ") - - val xcrunArgs = listOf( - "-sdk", - sdkPath, - "-target", - compileTarget.asSwiftcTarget(compileTarget.operatingSystem()), - ).asSwiftcArgs() - - return baseArgs + xcrunArgs + return listOf( + "swift", + "build", + "-c", + "release", + "--triple", + "${compileTarget.arch()}-apple-${compileTarget.operatingSystem()}${minOs(compileTarget)}${compileTarget.simulatorSuffix()}", + "--sdk", + sdkPath + ) } /** Workaround for bug in toolchain where the sdk path (via `swiftc -sdk` flag) is not propagated to clang. */ @@ -153,7 +199,6 @@ abstract class CompileSwiftTask @Inject constructor( readSdkPath(), ).asCcArgs() - private fun List.asSwiftcArgs() = asBuildToolArgs("swiftc") private fun List.asCcArgs() = asBuildToolArgs("cc") private fun List.asBuildToolArgs(tool: String): List { @@ -212,7 +257,12 @@ abstract class CompileSwiftTask @Inject constructor( * Note: adds lib-file md5 hash to library in order to automatically * invalidate connected cinterop task */ - private fun createDefFile(libPath: File, headerPath: File, packageName: String, xcodeVersion: Int) { + private fun createDefFile( + libPath: File, + headerPath: File, + packageName: String, + xcodeVersion: Int + ) { val xcodePath = readXcodePath() val linkerPlatformVersion = @@ -224,8 +274,8 @@ abstract class CompileSwiftTask @Inject constructor( val basicLinkerOpts = listOf( "-L/usr/lib/swift", "-$linkerPlatformVersion", - "${minOs(compileTarget)}.0", - "${minOs(compileTarget)}.0", + minOs(compileTarget), + minOs(compileTarget), "-L${xcodePath}/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/${compileTarget.os()}" ) @@ -254,13 +304,13 @@ abstract class CompileSwiftTask @Inject constructor( private fun CompileTarget.operatingSystem(): String = when (this) { - CompileTarget.iosX64, CompileTarget.iosArm64, CompileTarget.iosSimulatorArm64 -> "ios$minIos" - CompileTarget.watchosX64, CompileTarget.watchosArm64, CompileTarget.watchosSimulatorArm64 -> "watchos$minWatchos" - CompileTarget.tvosX64, CompileTarget.tvosArm64, CompileTarget.tvosSimulatorArm64 -> "tvos$minTvos" - CompileTarget.macosX64, CompileTarget.macosArm64 -> "macosx$minMacos" + CompileTarget.iosX64, CompileTarget.iosArm64, CompileTarget.iosSimulatorArm64 -> "ios" + CompileTarget.watchosX64, CompileTarget.watchosArm64, CompileTarget.watchosSimulatorArm64 -> "watchos" + CompileTarget.tvosX64, CompileTarget.tvosArm64, CompileTarget.tvosSimulatorArm64 -> "tvos" + CompileTarget.macosX64, CompileTarget.macosArm64 -> "macosx" } - private fun minOs(compileTarget: CompileTarget): Int = + private fun minOs(compileTarget: CompileTarget): String? = when (compileTarget) { CompileTarget.iosX64, CompileTarget.iosArm64, CompileTarget.iosSimulatorArm64 -> minIos CompileTarget.watchosX64, CompileTarget.watchosArm64, CompileTarget.watchosSimulatorArm64 -> minWatchos diff --git a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt index 4478bd6..55da262 100644 --- a/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt +++ b/plugin/src/main/kotlin/io/github/ttypic/swiftklib/gradle/templates/CreatePackageSwift.kt @@ -1,25 +1,109 @@ 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, + minIos: String, + minMacos: String, + minTvos: String, + minWatchos: String, + toolsVersion: String, ): String = """ - // swift-tools-version:5.5 + // swift-tools-version: $toolsVersion import PackageDescription let package = Package( name: "$cinteropName", + ${getPlatformBlock(minIos, minMacos, minTvos, minWatchos)}, products: [ .library( name: "$cinteropName", type: .static, - targets: ["$cinteropName"]) + targets: ${getProductsTargets(cinteropName)}) + ], + dependencies: [ + ${getDependencies(dependencies)} ], - dependencies: [], targets: [ .target( name: "$cinteropName", - dependencies: [], + 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]" +} + +private fun getDependencies(dependencies: Collection): String { + return buildList { + dependencies.forEach { dependency -> + add(dependency.toSwiftPackageDependencyDeclaration()) + } + }.joinToString(",") +} + +private fun SwiftPackageDependency.toSwiftPackageDependencyDeclaration(): String = when (this) { + is SwiftPackageDependency.Local -> + """ + .package(path: "${path.absolutePath}") + """.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() +} + + +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\"]" +} 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() - } -}