diff --git a/zipline-gradle-plugin/build.gradle.kts b/zipline-gradle-plugin/build.gradle.kts index 1f7b4e4f04..164f64475a 100644 --- a/zipline-gradle-plugin/build.gradle.kts +++ b/zipline-gradle-plugin/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(kotlin("gradle-plugin-api")) implementation(projects.zipline) implementation(projects.ziplineBytecode) + implementation(projects.ziplineKotlinPlugin) implementation(projects.ziplineLoader) implementation(libs.http4k.core) implementation(libs.http4k.server.jetty) diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineApiDumpTask.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineApiDumpTask.kt new file mode 100644 index 0000000000..8920eb48cf --- /dev/null +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineApiDumpTask.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 Cash App + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.cash.zipline.gradle + +import app.cash.zipline.api.compatibility.ActualApiHasProblems +import app.cash.zipline.api.compatibility.ExpectedApiIsUpToDate +import app.cash.zipline.api.compatibility.ExpectedApiRequiresUpdates +import app.cash.zipline.api.compatibility.makeApiCompatibilityDecision +import app.cash.zipline.api.fir.readFirZiplineApi +import app.cash.zipline.api.toml.TomlZiplineApi +import app.cash.zipline.api.toml.readZiplineApi +import app.cash.zipline.api.toml.writeZiplineApi +import javax.inject.Inject +import okio.buffer +import okio.sink +import okio.source +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.internal.file.FileCollectionFactory +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +@Suppress("unused") // Public API for Gradle plugin users. +abstract class ZiplineApiDumpTask @Inject constructor( + fileCollectionFactory: FileCollectionFactory, +) : DefaultTask() { + + @get:OutputFile + abstract val ziplineApiFile: RegularFileProperty + + @get:InputFiles + internal val sourcepath = fileCollectionFactory.configurableFiles("sourcepath") + + @get:Classpath + internal val classpath = fileCollectionFactory.configurableFiles("classpath") + + @TaskAction + fun task() { + val tomlFile = ziplineApiFile.get().asFile + + val expectedZiplineApi = when { + tomlFile.exists() -> tomlFile.source().buffer().use { it.readZiplineApi() } + else -> TomlZiplineApi(listOf()) + } + + val actualZiplineApi = readFirZiplineApi(sourcepath.files, classpath.files) + + when (val decision = makeApiCompatibilityDecision(expectedZiplineApi, actualZiplineApi)) { + is ActualApiHasProblems -> { + throw Exception( + """ + |Zipline API has compatibility problems: + | ${decision.messages.joinToString(separator = "\n").replace("\n", "\n ") } + """.trimMargin(), + ) + } + + is ExpectedApiRequiresUpdates -> { + tomlFile.sink().buffer().use { it.writeZiplineApi(decision.updatedApi) } + } + + ExpectedApiIsUpToDate -> { + // Do nothing. + } + } + } +} diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt index 9e403ea794..2e597cc80e 100644 --- a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplinePlugin.kt @@ -27,6 +27,8 @@ import org.jetbrains.kotlin.gradle.plugin.SubpluginOption import org.jetbrains.kotlin.gradle.targets.js.ir.JsIrBinary import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool import org.slf4j.LoggerFactory @Suppress("unused") // Created reflectively by Gradle. @@ -74,6 +76,12 @@ class ZiplinePlugin : KotlinCompilerPluginSupportPlugin { it.dependsOn(kotlinWebpack) } } + + target.tasks.withType(KotlinCompile::class.java) { kotlinCompile -> + if (kotlinCompile.name == "compileKotlinJvm") { + registerZiplineApiDumpTask(target, kotlinCompile) + } + } } private fun registerCompileZiplineTask( @@ -105,6 +113,24 @@ class ZiplinePlugin : KotlinCompilerPluginSupportPlugin { return ziplineCompileTask } + private fun registerZiplineApiDumpTask( + project: Project, + kotlinCompileTool: KotlinCompileTool, + ): TaskProvider { + val result = project.tasks.register( + "ziplineApiDump", + ZiplineApiDumpTask::class.java, + ) + + result.configure { + it.ziplineApiFile.set(project.file("api/zipline-api.toml")) + it.sourcepath.setFrom(kotlinCompileTool.sources) + it.classpath.setFrom(kotlinCompileTool.libraries) + } + + return result + } + override fun applyToCompilation( kotlinCompilation: KotlinCompilation<*>, ): Provider> { diff --git a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt index 7fd84281c4..5dec2261c4 100644 --- a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt +++ b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt @@ -195,6 +195,36 @@ class ZiplinePluginTest { ) } + @Test + fun ziplineApiDumpCreatesTomlFile() { + val projectDir = File("src/test/projects/basic") + val ziplineApiToml = projectDir.resolve("lib/api/zipline-api.toml") + + try { + val taskName = ":lib:ziplineApiDump" + val result = createRunner(projectDir, "clean", taskName).build() + assertThat(SUCCESS_OUTCOMES) + .contains(result.task(taskName)!!.outcome) + + assertThat(ziplineApiToml.readText()).isEqualTo( + """ + |[app.cash.zipline.tests.GreetService] + | + |functions = [ + | # fun close(): kotlin.Unit + | "moYx+T3e", + | + | # fun greet(kotlin.String): kotlin.String + | "ipvircui", + |] + | + """.trimMargin(), + ) + } finally { + ziplineApiToml.delete() + } + } + private fun createRunner(projectDir: File, vararg taskNames: String): GradleRunner { val gradleRoot = projectDir.resolve("gradle").also { it.mkdir() } File("../gradle/wrapper").copyRecursively(gradleRoot.resolve("wrapper"), true)