diff --git a/src/main/kotlin/build/buf/gradle/AbstractBufExecTask.kt b/src/main/kotlin/build/buf/gradle/AbstractBufExecTask.kt new file mode 100644 index 00000000..afcbf7c6 --- /dev/null +++ b/src/main/kotlin/build/buf/gradle/AbstractBufExecTask.kt @@ -0,0 +1,50 @@ +// Copyright 2024 Buf Technologies, Inc. +// +// 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 build.buf.gradle + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import java.io.File + +/** + * A task executing buf executable as part of its operation. + */ +abstract class AbstractBufExecTask : AbstractBufTask() { + /** The buf executable. */ + @get:InputFiles + internal abstract val bufExecutable: ConfigurableFileCollection + + /** + * The directory in which buf is executed. + * The actual real input files in this directory have to be tracked per command separately, + * so it is just an @Input, not @InputDirectory. + */ + @get:Input + internal abstract val workingDir: Property + + /** Whether the project has protobuf plugin enabled. */ + @get:Input + internal abstract val hasProtobufGradlePlugin: Property + + /** Directories possibly containing input .proto files. */ + @get:InputFiles + internal abstract val candidateProtoDirs: ConfigurableFileCollection + + /** Whether the project has buf workspace or not. */ + @get:Input + internal abstract val hasWorkspace: Property +} diff --git a/src/main/kotlin/build/buf/gradle/AbstractBufTask.kt b/src/main/kotlin/build/buf/gradle/AbstractBufTask.kt new file mode 100644 index 00000000..5dccbd94 --- /dev/null +++ b/src/main/kotlin/build/buf/gradle/AbstractBufTask.kt @@ -0,0 +1,29 @@ +// Copyright 2024 Buf Technologies, Inc. +// +// 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 build.buf.gradle + +import org.gradle.api.DefaultTask +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import java.io.File + +abstract class AbstractBufTask : DefaultTask() { + /** + * This property has to be set to project directory. It is only used for relativization of paths, + * so it is just an @Input, not @InputDirectory. + */ + @get:Input + internal abstract val projectDir: Property +} diff --git a/src/main/kotlin/build/buf/gradle/BreakingConfiguration.kt b/src/main/kotlin/build/buf/gradle/BreakingConfiguration.kt index 385b4282..637a2164 100644 --- a/src/main/kotlin/build/buf/gradle/BreakingConfiguration.kt +++ b/src/main/kotlin/build/buf/gradle/BreakingConfiguration.kt @@ -50,10 +50,18 @@ private fun Project.addSchemaDependency(artifactDetails: ArtifactDetails) { } private fun Project.configureBreakingTask() { - registerBufTask(BUF_BREAKING_TASK_NAME) { + registerBufExecTask(BUF_BREAKING_TASK_NAME) { group = VERIFICATION_GROUP description = "Checks that Protobuf API definitions are backwards-compatible with previous versions." dependsOn(BUF_BUILD_TASK_NAME) + + if (project.bufV1SyntaxOnly()) { + v1SyntaxOnly.set(true) + publicationFile.set(project.bufBuildPublicationFile) + } else { + v1SyntaxOnly.set(false) + } + configFile.set(singleFileFromConfiguration(BUF_BREAKING_CONFIGURATION_NAME)) } } diff --git a/src/main/kotlin/build/buf/gradle/BreakingTask.kt b/src/main/kotlin/build/buf/gradle/BreakingTask.kt index e532888d..66a9450d 100644 --- a/src/main/kotlin/build/buf/gradle/BreakingTask.kt +++ b/src/main/kotlin/build/buf/gradle/BreakingTask.kt @@ -14,19 +14,35 @@ package build.buf.gradle -import org.gradle.api.DefaultTask +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class BreakingTask : AbstractBufExecTask() { + @get:Input + internal abstract val v1SyntaxOnly: Property + + /** The input publication file. */ + @get:InputFile + @get:Optional + internal abstract val publicationFile: Property + + /** The input breaking config file. */ + @get:InputFile + internal abstract val configFile: Property -abstract class BreakingTask : DefaultTask() { @TaskAction fun bufBreaking() { val args = mutableListOf() args.add("breaking") - if (project.bufV1SyntaxOnly()) { - args.add(bufBuildPublicationFile) + if (v1SyntaxOnly.get()) { + args.add(publicationFile.get()) } args.add("--against") - args.add(singleFileFromConfiguration(BUF_BREAKING_CONFIGURATION_NAME)) + args.add(configFile.get()) execBuf(*args.toTypedArray()) { """ |Some Protobuf files had breaking changes: diff --git a/src/main/kotlin/build/buf/gradle/BufPlugin.kt b/src/main/kotlin/build/buf/gradle/BufPlugin.kt index 10be7afb..ea278d2a 100644 --- a/src/main/kotlin/build/buf/gradle/BufPlugin.kt +++ b/src/main/kotlin/build/buf/gradle/BufPlugin.kt @@ -35,6 +35,7 @@ class BufPlugin : Plugin { } private fun Project.configureBuf() { + createBufBinaryDependencyConfiguration() configureLint() configureFormat() configureBuild() diff --git a/src/main/kotlin/build/buf/gradle/BufSupport.kt b/src/main/kotlin/build/buf/gradle/BufSupport.kt index 8598cc62..a4da9ea1 100644 --- a/src/main/kotlin/build/buf/gradle/BufSupport.kt +++ b/src/main/kotlin/build/buf/gradle/BufSupport.kt @@ -15,11 +15,15 @@ package build.buf.gradle import org.gradle.api.Project -import org.gradle.api.Task +import org.gradle.kotlin.dsl.dependencies import java.nio.charset.StandardCharsets const val BUF_BINARY_CONFIGURATION_NAME = "bufTool" +internal fun Project.createBufBinaryDependencyConfiguration() { + configurations.create(BUF_BINARY_CONFIGURATION_NAME) +} + internal fun Project.configureBufDependency() { val os = System.getProperty("os.name").lowercase() val osPart = @@ -39,60 +43,61 @@ internal fun Project.configureBufDependency() { val extension = getExtension() - createConfigurationWithDependency( - BUF_BINARY_CONFIGURATION_NAME, - mapOf( - "group" to "build.buf", - "name" to "buf", - "version" to extension.toolVersion, - "classifier" to "$osPart-$archPart", - "ext" to "exe", - ), - ) + dependencies { + add( + BUF_BINARY_CONFIGURATION_NAME, + mapOf( + "group" to "build.buf", + "name" to "buf", + "version" to extension.toolVersion, + "classifier" to "$osPart-$archPart", + "ext" to "exe", + ), + ) + } } -internal fun Task.execBuf( +internal fun AbstractBufExecTask.execBuf( vararg args: Any, customErrorMessage: ((String) -> String)? = null, ) { execBuf(args.asList(), customErrorMessage) } -internal fun Task.execBuf( +internal fun AbstractBufExecTask.execBuf( args: Iterable, customErrorMessage: ((String) -> String)? = null, ) { - with(project) { - val executable = singleFileFromConfiguration(BUF_BINARY_CONFIGURATION_NAME) - - if (!executable.canExecute()) { - executable.setExecutable(true) - } + val executable = bufExecutable.singleFile - val workingDir = - if (hasProtobufGradlePlugin()) { - bufbuildDir - } else { - projectDir - } + if (!executable.canExecute()) { + executable.setExecutable(true) + } - val processArgs = listOf(executable.absolutePath) + args + val processArgs = listOf(executable.absolutePath) + args + val workingDirValue = workingDir.get() - logger.info("Running buf from $workingDir: `buf ${args.joinToString(" ")}`") - val result = ProcessRunner().use { it.shell(workingDir, processArgs) } + logger.info("Running buf from $workingDirValue: `buf ${args.joinToString(" ")}`") + val result = ProcessRunner().use { it.shell(workingDirValue, processArgs) } - if (result.exitCode != 0) { - if (customErrorMessage != null) { - val stdOut = result.stdOut.toString(StandardCharsets.UTF_8) - val stdErr = result.stdErr.toString(StandardCharsets.UTF_8) - val ex = IllegalStateException(customErrorMessage(stdOut)) - if (stdErr.isNotEmpty()) { - ex.addSuppressed(IllegalStateException(result.toString())) - } - throw ex - } else { - error(result.toString()) + if (result.exitCode != 0) { + if (customErrorMessage != null) { + val stdOut = result.stdOut.toString(StandardCharsets.UTF_8) + val stdErr = result.stdErr.toString(StandardCharsets.UTF_8) + val ex = IllegalStateException(customErrorMessage(stdOut)) + if (stdErr.isNotEmpty()) { + ex.addSuppressed(IllegalStateException(result.toString())) } + throw ex + } else { + error(result.toString()) } } } + +internal fun AbstractBufExecTask.obtainDefaultProtoFileSet() = + project.fileTree(workingDir.get()) { + include("**/*.proto") + // not to interfere with random plugins producing output to build dir + exclude("build") + } diff --git a/src/main/kotlin/build/buf/gradle/BuildConfiguration.kt b/src/main/kotlin/build/buf/gradle/BuildConfiguration.kt index a49d1796..320c21e0 100644 --- a/src/main/kotlin/build/buf/gradle/BuildConfiguration.kt +++ b/src/main/kotlin/build/buf/gradle/BuildConfiguration.kt @@ -15,7 +15,6 @@ package build.buf.gradle import org.gradle.api.Project -import org.gradle.api.Task import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.kotlin.dsl.create @@ -28,16 +27,16 @@ private const val BUF_BUILD_PUBLICATION_FILE_BASE_NAME = "image" const val BUF_IMAGE_PUBLICATION_NAME = "bufImagePublication" internal fun Project.configureBuild() { - registerBufTask(BUF_BUILD_TASK_NAME) { + registerBufExecTask(BUF_BUILD_TASK_NAME) { group = BUILD_GROUP description = "Builds a Buf image from a Protobuf schema." if (hasProtobufGradlePlugin()) { dependsOn(COPY_BUF_CONFIG_TASK_NAME) - } else { - // Called already during workspace configuration if the protobuf-gradle-plugin has been applied - createsOutput() } + + inputFiles.setFrom(obtainDefaultProtoFileSet()) + publicationFile.set(project.bufBuildPublicationFile) } } @@ -64,8 +63,5 @@ private val Project.bufBuildPublicationFileExtension deets.imageFormat.formatName + deets.compressionFormat?.let { ".${it.ext}" }.orEmpty() } -private val Project.bufBuildPublicationFile +internal val Project.bufBuildPublicationFile get() = File(bufbuildDir, "$BUF_BUILD_PUBLICATION_FILE_BASE_NAME.$bufBuildPublicationFileExtension") - -internal val Task.bufBuildPublicationFile - get() = project.bufBuildPublicationFile diff --git a/src/main/kotlin/build/buf/gradle/BuildTask.kt b/src/main/kotlin/build/buf/gradle/BuildTask.kt index 05e3f610..bbdd8848 100644 --- a/src/main/kotlin/build/buf/gradle/BuildTask.kt +++ b/src/main/kotlin/build/buf/gradle/BuildTask.kt @@ -14,12 +14,24 @@ package build.buf.gradle -import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class BuildTask : AbstractBufExecTask() { + /** The input files. */ + @get:InputFiles + internal abstract val inputFiles: ConfigurableFileCollection + + /** The output publication file. */ + @get:OutputFile + internal abstract val publicationFile: Property -abstract class BuildTask : DefaultTask() { @TaskAction fun bufBuild() { - execBuf("build", "--output", bufBuildPublicationFile) + execBuf("build", "--output", publicationFile.get()) } } diff --git a/src/main/kotlin/build/buf/gradle/ConfigSupport.kt b/src/main/kotlin/build/buf/gradle/ConfigSupport.kt index 1506992a..86acc276 100644 --- a/src/main/kotlin/build/buf/gradle/ConfigSupport.kt +++ b/src/main/kotlin/build/buf/gradle/ConfigSupport.kt @@ -15,7 +15,6 @@ package build.buf.gradle import org.gradle.api.Project -import org.gradle.api.Task import org.gradle.api.tasks.Copy import org.gradle.kotlin.dsl.register import java.io.File @@ -25,7 +24,7 @@ const val COPY_BUF_CONFIG_TASK_NAME = "copyBufConfig" internal fun Project.configureCopyBufConfig() { tasks.register(COPY_BUF_CONFIG_TASK_NAME) { from(listOfNotNull(bufConfigFile())) - into(bufbuildDir) + into(project.bufbuildDir) rename { "buf.yaml" } } } @@ -47,8 +46,6 @@ internal fun Project.bufConfigFile() = } } -internal fun Task.bufConfigFile() = project.bufConfigFile() - private fun Project.resolveConfig(): File? { val ext = getExtension() return configurations.getByName(BUF_CONFIGURATION_NAME).let { diff --git a/src/main/kotlin/build/buf/gradle/DirectorySpecificBufExecutionSupport.kt b/src/main/kotlin/build/buf/gradle/DirectorySpecificBufExecutionSupport.kt index 83e2da35..d3709f46 100644 --- a/src/main/kotlin/build/buf/gradle/DirectorySpecificBufExecutionSupport.kt +++ b/src/main/kotlin/build/buf/gradle/DirectorySpecificBufExecutionSupport.kt @@ -14,17 +14,16 @@ package build.buf.gradle -import org.gradle.api.Task import java.io.File -internal fun Task.execBufInSpecificDirectory( +internal fun AbstractBufExecTask.execBufInSpecificDirectory( vararg bufCommand: String, customErrorMessage: ((String) -> String)? = null, ) { execBufInSpecificDirectory(bufCommand.asList(), emptyList(), customErrorMessage) } -internal fun Task.execBufInSpecificDirectory( +internal fun AbstractBufExecTask.execBufInSpecificDirectory( bufCommand: String, extraArgs: Iterable, customErrorMessage: (String) -> String, @@ -32,17 +31,19 @@ internal fun Task.execBufInSpecificDirectory( execBufInSpecificDirectory(listOf(bufCommand), extraArgs, customErrorMessage) } -private fun Task.execBufInSpecificDirectory( +private fun AbstractBufExecTask.execBufInSpecificDirectory( bufCommand: Iterable, extraArgs: Iterable, customErrorMessage: ((String) -> String)? = null, ) { - fun runWithArgs(file: File? = null) = bufCommand + listOfNotNull(file?.let { project.makeMangledRelativizedPathStr(it) }) + extraArgs + fun runWithArgs(file: File? = null) = bufCommand + listOfNotNull(file?.let { makeMangledRelativizedPathStr(it) }) + extraArgs when { - project.hasProtobufGradlePlugin() -> - project.projectDefinedProtoDirs().forEach { execBuf(runWithArgs(it), customErrorMessage) } - project.hasWorkspace() -> + hasProtobufGradlePlugin.get() -> + candidateProtoDirs + .filter { anyProtos(it) } + .forEach { execBuf(runWithArgs(it), customErrorMessage) } + hasWorkspace.get() -> execBuf(bufCommand, customErrorMessage) else -> execBuf(runWithArgs(), customErrorMessage) diff --git a/src/main/kotlin/build/buf/gradle/ExtensionSupport.kt b/src/main/kotlin/build/buf/gradle/ExtensionSupport.kt index 812384af..f1f7696d 100644 --- a/src/main/kotlin/build/buf/gradle/ExtensionSupport.kt +++ b/src/main/kotlin/build/buf/gradle/ExtensionSupport.kt @@ -15,7 +15,6 @@ package build.buf.gradle import org.gradle.api.Project -import org.gradle.api.Task import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.getByName @@ -27,8 +26,6 @@ internal fun Project.createExtension() { internal fun Project.getExtension() = extensions.getByName(BUF_EXTENSION_NAME) -internal fun Task.getExtension() = project.getExtension() - internal fun Project.runBreakageCheck() = with(getExtension()) { checkSchemaAgainstLatestRelease || previousVersion != null diff --git a/src/main/kotlin/build/buf/gradle/FormatApplyTask.kt b/src/main/kotlin/build/buf/gradle/FormatApplyTask.kt index 987fc65e..9a753e63 100644 --- a/src/main/kotlin/build/buf/gradle/FormatApplyTask.kt +++ b/src/main/kotlin/build/buf/gradle/FormatApplyTask.kt @@ -14,10 +14,20 @@ package build.buf.gradle -import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFiles import org.gradle.api.tasks.TaskAction -abstract class FormatApplyTask : DefaultTask() { +abstract class FormatApplyTask : AbstractBufExecTask() { + /** The input files to be formatted. */ + @get:InputFiles + internal abstract val inputFiles: ConfigurableFileCollection + + /** The output files that have been formatted. */ + @get:OutputFiles + internal abstract val outputFiles: ConfigurableFileCollection + @TaskAction fun bufFormatApply() { execBufInSpecificDirectory("format", "-w") diff --git a/src/main/kotlin/build/buf/gradle/FormatCheckTask.kt b/src/main/kotlin/build/buf/gradle/FormatCheckTask.kt index 34de8578..04c5b8ae 100644 --- a/src/main/kotlin/build/buf/gradle/FormatCheckTask.kt +++ b/src/main/kotlin/build/buf/gradle/FormatCheckTask.kt @@ -14,10 +14,15 @@ package build.buf.gradle -import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.TaskAction -abstract class FormatCheckTask : DefaultTask() { +abstract class FormatCheckTask : AbstractBufExecTask() { + /** The input files to be checked. */ + @get:InputFiles + internal abstract val inputFiles: ConfigurableFileCollection + @TaskAction fun bufFormatCheck() { execBufInSpecificDirectory("format", "-d", "--exit-code") { diff --git a/src/main/kotlin/build/buf/gradle/FormatConfiguration.kt b/src/main/kotlin/build/buf/gradle/FormatConfiguration.kt index 109149a0..7645cf76 100644 --- a/src/main/kotlin/build/buf/gradle/FormatConfiguration.kt +++ b/src/main/kotlin/build/buf/gradle/FormatConfiguration.kt @@ -27,18 +27,23 @@ internal fun Project.configureFormat() { } private fun Project.configureBufFormatCheck() { - registerBufTask(BUF_FORMAT_CHECK_TASK_NAME) { + registerBufExecTask(BUF_FORMAT_CHECK_TASK_NAME) { group = VERIFICATION_GROUP description = "Checks that a Protobuf schema is formatted according to Buf's formatting rules." enabled = getExtension().enforceFormat + + inputFiles.setFrom(obtainDefaultProtoFileSet()) } tasks.named(CHECK_TASK_NAME).dependsOn(BUF_FORMAT_CHECK_TASK_NAME) } private fun Project.configureBufFormatApply() { - registerBufTask(BUF_FORMAT_APPLY_TASK_NAME) { + registerBufExecTask(BUF_FORMAT_APPLY_TASK_NAME) { group = VERIFICATION_GROUP description = "Formats a Protobuf schema according to Buf's formatting rules." + + inputFiles.setFrom(obtainDefaultProtoFileSet()) + outputFiles.setFrom(obtainDefaultProtoFileSet()) } } diff --git a/src/main/kotlin/build/buf/gradle/GenerateConfiguration.kt b/src/main/kotlin/build/buf/gradle/GenerateConfiguration.kt index e9ba363d..946d794d 100644 --- a/src/main/kotlin/build/buf/gradle/GenerateConfiguration.kt +++ b/src/main/kotlin/build/buf/gradle/GenerateConfiguration.kt @@ -16,16 +16,42 @@ package build.buf.gradle import org.gradle.api.Project import org.gradle.language.base.plugins.LifecycleBasePlugin.BUILD_GROUP +import java.io.File const val BUF_GENERATE_TASK_NAME = "bufGenerate" const val GENERATED_DIR = "generated" internal fun Project.configureGenerate() { - registerBufTask(BUF_GENERATE_TASK_NAME) { + registerBufExecTask(BUF_GENERATE_TASK_NAME) { group = BUILD_GROUP description = "Generates code from a Protobuf schema." - createsOutput() + val generateOptions = project.getExtension().generateOptions + includeImports.set(generateOptions?.includeImports ?: false) + templateFile.set(generateOptions?.let { resolveTemplateFile(it) }) + inputFiles.setFrom(obtainDefaultProtoFileSet()) + outputDirectory.set(File(project.bufbuildDir, GENERATED_DIR)) } } + +private fun Project.resolveTemplateFile(generateOptions: GenerateOptions): File { + val defaultTemplateFile = project.file("buf.gen.yaml").validOrNull() + return if (generateOptions.templateFileLocation != null) { + val specifiedTemplateFile = generateOptions.templateFileLocation.validOrNull() + check(specifiedTemplateFile != null) { + "Specified templateFileLocation does not exist." + } + check(defaultTemplateFile == null || specifiedTemplateFile == defaultTemplateFile) { + "Buf gen template file specified in the project directory as well as with templateFileLocation; pick one." + } + specifiedTemplateFile + } else { + check(defaultTemplateFile != null) { + "No buf.gen.yaml file found in the project directory." + } + defaultTemplateFile + } +} + +private fun File?.validOrNull() = this?.takeIf { it.isFile && it.exists() } diff --git a/src/main/kotlin/build/buf/gradle/GenerateTask.kt b/src/main/kotlin/build/buf/gradle/GenerateTask.kt index 0c2b825e..9d1edfb5 100644 --- a/src/main/kotlin/build/buf/gradle/GenerateTask.kt +++ b/src/main/kotlin/build/buf/gradle/GenerateTask.kt @@ -14,54 +14,53 @@ package build.buf.gradle -import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction import java.io.File -abstract class GenerateTask : DefaultTask() { +abstract class GenerateTask : AbstractBufExecTask() { + /** Whether to include imports. */ + @get:Input + internal abstract val includeImports: Property + + /** Template file. */ + @get:InputFile + @get:Optional + internal abstract val templateFile: Property + + /** The input proto files. */ + @get:InputFiles + internal abstract val inputFiles: ConfigurableFileCollection + + /** The directory to output generated files. */ + @get:OutputDirectory + internal abstract val outputDirectory: Property + @TaskAction fun bufGenerate() { - val args = listOf("generate", "--output", File(bufbuildDir, GENERATED_DIR)) + val args = listOf("generate", "--output", outputDirectory.get()) execBuf(args + additionalArgs()) } private fun additionalArgs(): List { - val generateOptions = getExtension().generateOptions val importOptions = - if (generateOptions?.includeImports == true) { + if (includeImports.get()) { listOf("--include-imports") } else { emptyList() } val templateFileOption = - resolveTemplateFile()?.let { + templateFile.orNull?.let { listOf("--template", it.absolutePath) } ?: emptyList() return importOptions + templateFileOption } - - private fun resolveTemplateFile(): File? { - return getExtension().generateOptions?.let { generateOptions -> - val defaultTemplateFile = project.file("buf.gen.yaml").validOrNull() - if (generateOptions.templateFileLocation != null) { - val specifiedTemplateFile = generateOptions.templateFileLocation.validOrNull() - check(specifiedTemplateFile != null) { - "Specified templateFileLocation does not exist." - } - check(defaultTemplateFile == null || specifiedTemplateFile == defaultTemplateFile) { - "Buf gen template file specified in the project directory as well as with templateFileLocation; pick one." - } - specifiedTemplateFile - } else { - check(defaultTemplateFile != null) { - "No buf.gen.yaml file found in the project directory." - } - defaultTemplateFile - } - } - } - - private fun File?.validOrNull() = this?.takeIf { it.isFile && it.exists() } } diff --git a/src/main/kotlin/build/buf/gradle/GradleSupport.kt b/src/main/kotlin/build/buf/gradle/GradleSupport.kt index 2080407e..975d8cfa 100644 --- a/src/main/kotlin/build/buf/gradle/GradleSupport.kt +++ b/src/main/kotlin/build/buf/gradle/GradleSupport.kt @@ -15,7 +15,6 @@ package build.buf.gradle import org.gradle.api.Project -import org.gradle.api.Task import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.dependencies @@ -32,5 +31,3 @@ internal fun Project.createConfigurationWithDependency( } internal fun Project.singleFileFromConfiguration(configuration: String) = configurations.getByName(configuration).singleFile - -internal fun Task.singleFileFromConfiguration(configuration: String) = project.singleFileFromConfiguration(configuration) diff --git a/src/main/kotlin/build/buf/gradle/LintConfiguration.kt b/src/main/kotlin/build/buf/gradle/LintConfiguration.kt index b8ca14ae..1a56e6ec 100644 --- a/src/main/kotlin/build/buf/gradle/LintConfiguration.kt +++ b/src/main/kotlin/build/buf/gradle/LintConfiguration.kt @@ -21,9 +21,12 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP const val BUF_LINT_TASK_NAME = "bufLint" internal fun Project.configureLint() { - registerBufTask(BUF_LINT_TASK_NAME) { + registerBufExecTask(BUF_LINT_TASK_NAME) { group = VERIFICATION_GROUP description = "Checks that a Protobuf schema conforms to the Buf lint configuration." + + bufConfigFile.set(project.bufConfigFile()) + inputFiles.setFrom(obtainDefaultProtoFileSet()) } tasks.named(CHECK_TASK_NAME).dependsOn(BUF_LINT_TASK_NAME) diff --git a/src/main/kotlin/build/buf/gradle/LintTask.kt b/src/main/kotlin/build/buf/gradle/LintTask.kt index e2b192dc..8e3232ef 100644 --- a/src/main/kotlin/build/buf/gradle/LintTask.kt +++ b/src/main/kotlin/build/buf/gradle/LintTask.kt @@ -14,18 +14,31 @@ package build.buf.gradle -import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.TaskAction import java.io.File import java.nio.file.Files.lines import kotlin.streams.asSequence -abstract class LintTask : DefaultTask() { +abstract class LintTask : AbstractBufExecTask() { + /** The input proto files. */ + @get:InputFiles + internal abstract val inputFiles: ConfigurableFileCollection + + /** The input buf configuration file. */ + @get:InputFile + @get:Optional + internal abstract val bufConfigFile: Property + @TaskAction fun bufLint() { execBufInSpecificDirectory( "lint", - bufConfigFile()?.let { listOf("--config", it.readAndStripComments()) }.orEmpty(), + bufConfigFile.orNull?.let { listOf("--config", it.readAndStripComments()) }.orEmpty(), ) { """ |Some Protobuf files had lint violations: diff --git a/src/main/kotlin/build/buf/gradle/OutputSupport.kt b/src/main/kotlin/build/buf/gradle/OutputSupport.kt index ac2e2f88..b29f6302 100644 --- a/src/main/kotlin/build/buf/gradle/OutputSupport.kt +++ b/src/main/kotlin/build/buf/gradle/OutputSupport.kt @@ -15,18 +15,10 @@ package build.buf.gradle import org.gradle.api.Project -import org.gradle.api.Task const val BUF_BUILD_DIR = "bufbuild" internal val Project.bufbuildDir get() = layout.buildDirectory.dir(BUF_BUILD_DIR).get().asFile -internal val Task.bufbuildDir - get() = project.bufbuildDir - -internal fun Task.createsOutput() { - doFirst { project.bufbuildDir.mkdirs() } -} - internal fun ArtifactDetails.groupAndArtifact() = "$groupId:$artifactId" diff --git a/src/main/kotlin/build/buf/gradle/ProtobufGradlePluginSupport.kt b/src/main/kotlin/build/buf/gradle/ProtobufGradlePluginSupport.kt index 7051f46a..8abdc4c0 100644 --- a/src/main/kotlin/build/buf/gradle/ProtobufGradlePluginSupport.kt +++ b/src/main/kotlin/build/buf/gradle/ProtobufGradlePluginSupport.kt @@ -15,12 +15,19 @@ package build.buf.gradle import io.github.g00fy2.versioncompare.Version -import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.SourceDirectorySet import org.gradle.api.plugins.AppliedPlugin import org.gradle.api.plugins.ExtensionAware +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskProvider @@ -52,39 +59,77 @@ internal fun Project.bufV1SyntaxOnly() = Version(getExtension().toolVersion) < V internal fun Project.withProtobufGradlePlugin(action: (AppliedPlugin) -> Unit) = pluginManager.withPlugin("com.google.protobuf", action) internal fun Project.configureCreateSymLinksToModules() { - tasks.register(CREATE_SYM_LINKS_TO_MODULES_TASK_NAME) { + registerBufTask(CREATE_SYM_LINKS_TO_MODULES_TASK_NAME) { workspaceCommonConfig() + bufbuildDir.set(project.bufbuildDir) + candidateProtoDirs.setFrom(allProtoDirs()) } } -abstract class CreateSymLinksToModulesTask : DefaultTask() { +abstract class CreateSymLinksToModulesTask : AbstractBufTask() { + /** Buf output directory. Should be set to [BUF_BUILD_DIR]. */ + @get:OutputDirectory + internal abstract val bufbuildDir: Property + + /** Directories possibly containing input .proto files. */ + @get:InputFiles + internal abstract val candidateProtoDirs: ConfigurableFileCollection + @TaskAction fun createSymLinksToModules() { - allProtoDirs().forEach { - val symLinkFile = File(bufbuildDir, project.makeMangledRelativizedPathStr(it)) - if (!symLinkFile.exists()) { - logger.info("Creating symlink for $it at $symLinkFile") - Files.createSymbolicLink( - symLinkFile.toPath(), - bufbuildDir.toPath().relativize(project.file(it).toPath()), - ) + val bufbuildDirValue = bufbuildDir.get() + candidateProtoDirs + .filter { anyProtos(it) } + .forEach { + val symLinkFile = File(bufbuildDirValue, makeMangledRelativizedPathStr(it)) + if (!symLinkFile.exists()) { + logger.info("Creating symlink for $it at $symLinkFile") + Files.createSymbolicLink( + symLinkFile.toPath(), + bufbuildDirValue.toPath().relativize(it.toPath()), + ) + } } - } } } internal fun Project.configureWriteWorkspaceYaml() { - tasks.register(WRITE_WORKSPACE_YAML_TASK_NAME) { + registerBufTask(WRITE_WORKSPACE_YAML_TASK_NAME) { workspaceCommonConfig() + projectDir.set(project.projectDir) + candidateProtoDirs.setFrom(allProtoDirs()) + if (project.bufV1SyntaxOnly()) { + v1SyntaxOnly.set(true) + outputFile.set(File(project.bufbuildDir, "buf.work.yaml")) + } else { + v1SyntaxOnly.set(false) + outputFile.set(File(project.bufbuildDir, "buf.yaml")) + bufConfigFile.set(project.bufConfigFile()) + } } } -abstract class WriteWorkspaceYamlTask : DefaultTask() { - private val bufYamlGenerator = BufYamlGenerator() +abstract class WriteWorkspaceYamlTask : AbstractBufTask() { + @get:Input + internal abstract val v1SyntaxOnly: Property + + /** Directories possibly containing input .proto files. */ + @get:InputFiles + @get:Optional + internal abstract val candidateProtoDirs: ConfigurableFileCollection + + /** The input buf configuration file. */ + @get:InputFile + @get:Optional + internal abstract val bufConfigFile: Property + + /** Output yaml file. */ + @get:OutputFile + internal abstract val outputFile: Property @TaskAction fun writeWorkspaceYaml() { - if (project.bufV1SyntaxOnly()) { + if (v1SyntaxOnly.get()) { val bufWork = """ |version: v1 @@ -93,12 +138,15 @@ abstract class WriteWorkspaceYamlTask : DefaultTask() { """.trimMargin() logger.info("Writing generated buf.work.yaml:\n$bufWork") - File(bufbuildDir, "buf.work.yaml").writeText(bufWork) + outputFile.get().writeText(bufWork) } else { - val protoDirs = allProtoDirs().map { project.makeMangledRelativizedPathStr(it) } - val bufYaml = bufYamlGenerator.generate(project.bufConfigFile(), protoDirs) + val protoDirs = + candidateProtoDirs + .filter { anyProtos(it) } + .map { makeMangledRelativizedPathStr(it) } + val bufYaml = BufYamlGenerator().generate(bufConfigFile.orNull, protoDirs) logger.info("Writing generated buf.yaml:{}\n", bufYaml) - File(bufbuildDir, "buf.yaml").writeText(bufYaml) + outputFile.get().writeText(bufYaml) } } } @@ -109,12 +157,12 @@ private fun Task.workspaceCommonConfig() { .tasks .matching { it::class.java.name == "com.google.protobuf.gradle.ProtobufExtract_Decorated" }, ) - createsOutput() } -private fun Task.workspaceSymLinkEntries() = - allProtoDirs() - .map { project.makeMangledRelativizedPathStr(it) } +private fun WriteWorkspaceYamlTask.workspaceSymLinkEntries() = + candidateProtoDirs + .filter { anyProtos(it) } + .map { makeMangledRelativizedPathStr(it) } .joinToString("\n") { "| - $it" } // Returns all directories that have may have proto files relevant to processing the project's proto files. This @@ -123,7 +171,6 @@ private fun Task.workspaceSymLinkEntries() = private fun Task.allProtoDirs() = project.allProtoSourceSetDirs() .plus(project.file(Paths.get(BUILD_EXTRACTED_INCLUDE_PROTOS_MAIN))) - .filter { anyProtos(it) } .toSet() // Returns the list of directories containing proto files defined in *this* project. The returned directories do *not* @@ -165,26 +212,47 @@ private fun Project.projectProtoSourceSetDirs() = // directories explicitly added to the source set, as well as directories containing files from "protobuf" dependencies. private fun ExtensionAware.projectProtoSourceSetDirs() = extensions.getByName("proto").srcDirs - .filter { anyProtos(it) } .toSet() -internal fun Project.makeMangledRelativizedPathStr(file: File) = mangle(projectDir.toPath().relativize(file.toPath())) +internal fun AbstractBufTask.makeMangledRelativizedPathStr(file: File) = mangle(projectDir.get().toPath().relativize(file.toPath())) // Indicates if the specified directory contains any proto files. -private fun anyProtos(directory: File) = directory.walkTopDown().any { it.extension == "proto" } +internal fun anyProtos(directory: File) = directory.walkTopDown().any { it.extension == "proto" } private fun mangle(name: Path) = name.toString().replace("-", "--").replace(File.separator, "-") -internal inline fun Project.registerBufTask( +internal inline fun Project.registerBufTask( + name: String, + noinline configuration: T.() -> Unit, +): TaskProvider = + tasks.register(name) { + projectDir.set(project.projectDir) + configuration() + } + +internal inline fun Project.registerBufExecTask( name: String, noinline configuration: T.() -> Unit, -): TaskProvider { - val taskProvider = tasks.register(name, configuration) - withProtobufGradlePlugin { - afterEvaluate { - taskProvider.dependsOn(CREATE_SYM_LINKS_TO_MODULES_TASK_NAME) - taskProvider.dependsOn(WRITE_WORKSPACE_YAML_TASK_NAME) +): TaskProvider = + registerBufTask(name) { + bufExecutable.setFrom(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME)) + if (project.hasProtobufGradlePlugin()) { + hasProtobufGradlePlugin.set(true) + candidateProtoDirs.setFrom(projectDefinedProtoDirs()) + workingDir.set(project.bufbuildDir) + // whenever this task is part of the build, we must run it before any buf execution + mustRunAfter(COPY_BUF_CONFIG_TASK_NAME) + } else { + hasProtobufGradlePlugin.set(false) + workingDir.set(project.projectDir) + } + hasWorkspace.set(project.hasWorkspace()) + configuration() + }.also { taskProvider -> + withProtobufGradlePlugin { + afterEvaluate { + taskProvider.dependsOn(CREATE_SYM_LINKS_TO_MODULES_TASK_NAME) + taskProvider.dependsOn(WRITE_WORKSPACE_YAML_TASK_NAME) + } } } - return taskProvider -} diff --git a/src/test/kotlin/build/buf/gradle/AbstractBufIntegrationTest.kt b/src/test/kotlin/build/buf/gradle/AbstractBufIntegrationTest.kt index dd348813..41577bba 100644 --- a/src/test/kotlin/build/buf/gradle/AbstractBufIntegrationTest.kt +++ b/src/test/kotlin/build/buf/gradle/AbstractBufIntegrationTest.kt @@ -71,6 +71,7 @@ abstract class AbstractBufIntegrationTest : IntegrationTest { "-PprotobufVersion=3.23.4", "-PkotlinVersion=1.7.20", "-PandroidGradleVersion=7.3.0", + "--configuration-cache", ) .withDebug(false) // Enable for interactive debugging .let { WrappedRunner(it) } diff --git a/src/test/resources/GenerateTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle b/src/test/resources/GenerateTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle index dd5f9ede..cab29dde 100644 --- a/src/test/resources/GenerateTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle +++ b/src/test/resources/GenerateTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle @@ -2,6 +2,11 @@ plugins { id 'build.buf' } + +repositories { + mavenCentral() +} + buf { generate { templateFileLocation = project.file("subdir/buf.gen.yaml") diff --git a/src/test/resources/GenerateTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle b/src/test/resources/GenerateTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle index dd5f9ede..1e990700 100644 --- a/src/test/resources/GenerateTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle +++ b/src/test/resources/GenerateTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle @@ -2,6 +2,10 @@ plugins { id 'build.buf' } +repositories { + mavenCentral() +} + buf { generate { templateFileLocation = project.file("subdir/buf.gen.yaml") diff --git a/src/test/resources/GenerateTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle b/src/test/resources/GenerateTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle index e3e156b8..e677fffb 100644 --- a/src/test/resources/GenerateTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle +++ b/src/test/resources/GenerateTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle @@ -2,6 +2,10 @@ plugins { id 'build.buf' } +repositories { + mavenCentral() +} + buf { generate { } diff --git a/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle b/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle index dd5f9ede..1e990700 100644 --- a/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle +++ b/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_a_nonexistent_specified_template_file/build.gradle @@ -2,6 +2,10 @@ plugins { id 'build.buf' } +repositories { + mavenCentral() +} + buf { generate { templateFileLocation = project.file("subdir/buf.gen.yaml") diff --git a/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle b/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle index dd5f9ede..1e990700 100644 --- a/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle +++ b/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_both_default_and_specified_buf_gen_template_files/build.gradle @@ -2,6 +2,10 @@ plugins { id 'build.buf' } +repositories { + mavenCentral() +} + buf { generate { templateFileLocation = project.file("subdir/buf.gen.yaml") diff --git a/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle b/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle index e3e156b8..e677fffb 100644 --- a/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle +++ b/src/test/resources/GenerateWithWorkspaceTest/buf_generate_fails_with_no_default_template_file_and_no_override_specified/build.gradle @@ -2,6 +2,10 @@ plugins { id 'build.buf' } +repositories { + mavenCentral() +} + buf { generate { }