All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.protobuf.gradle.ProtobufPlugin.groovy Maven / Gradle / Ivy

Go to download

Gradle build plugin to handle Protocol Buffers automated code generation and compilation

The newest version!
/*
 * Original work copyright (c) 2015, Alex Antonov. All rights reserved.
 * Modified work copyright (c) 2015, Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 * may be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.google.protobuf.gradle

import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.TestVariant
import com.android.build.gradle.api.UnitTestVariant
import com.android.builder.model.SourceProvider
import com.google.protobuf.gradle.internal.DefaultProtoSourceSet
import com.google.protobuf.gradle.internal.ProjectExt
import com.google.protobuf.gradle.tasks.ProtoSourceSet
import groovy.transform.CompileStatic
import groovy.transform.TypeChecked
import groovy.transform.TypeCheckingMode
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.file.CopySpec
import org.gradle.api.file.FileCollection
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.internal.artifacts.ArtifactAttributes
import org.gradle.api.plugins.AppliedPlugin
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.SourceSet
import org.gradle.language.jvm.tasks.ProcessResources
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

/**
 * The main class for the protobuf plugin.
 */
@CompileStatic
class ProtobufPlugin implements Plugin {
    // any one of these plugins should be sufficient to proceed with applying this plugin
    private static final List PREREQ_PLUGIN_OPTIONS = [
            'java',
            'java-library',
            'com.android.application',
            'com.android.feature',
            'com.android.library',
            'android',
            'android-library',
    ]

    private Project project
    private ProtobufExtension protobufExtension
    private boolean wasApplied = false

    void apply(final Project project) {
      if (GradleVersion.current() < GradleVersion.version("5.6")) {
        throw new GradleException(
          "Gradle version is ${project.gradle.gradleVersion}. Minimum supported version is 5.6")
      }

      this.protobufExtension = project.extensions.create("protobuf", ProtobufExtension, project)

      this.project = project

      // Provides the osdetector extension
      project.apply([plugin:com.google.gradle.osdetector.OsDetectorPlugin])

        // At least one of the prerequisite plugins must by applied before this plugin can be applied, so
        // we will use the PluginManager.withPlugin() callback mechanism to delay applying this plugin until
        // after that has been achieved. If project evaluation completes before one of the prerequisite plugins
        // has been applied then we will assume that none of prerequisite plugins were specified and we will
        // throw an Exception to alert the user of this configuration issue.
        Action applyWithPrerequisitePlugin = { AppliedPlugin prerequisitePlugin ->
          if (wasApplied) {
            project.logger.info('The com.google.protobuf plugin was already applied to the project: ' + project.path
                + ' and will not be applied again after plugin: ' + prerequisitePlugin.id)
          } else {
            wasApplied = true

            doApply()
          }
        }

        PREREQ_PLUGIN_OPTIONS.each { pluginName ->
          project.pluginManager.withPlugin(pluginName, applyWithPrerequisitePlugin)
        }

        project.afterEvaluate {
          if (!wasApplied) {
            throw new GradleException('The com.google.protobuf plugin could not be applied during project evaluation.'
                + ' The Java plugin or one of the Android plugins must be applied to the project first.')
          }
        }
    }

    @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP
    private void doApply() {
        boolean isAndroid = Utils.isAndroidProject(project)
        // Java projects will extract included protos from a 'compileProtoPath'
        // configuration of each source set, while Android projects will
        // extract included protos from {@code variant.compileConfiguration}
        // of each variant.
        Collection postConfigure = []
        if (isAndroid) {
          project.android.sourceSets.configureEach { sourceSet ->
            ProtoSourceSet protoSourceSet = protobufExtension.sourceSets.create(sourceSet.name)
            addSourceSetExtension(sourceSet, protoSourceSet)
            Configuration protobufConfig = createProtobufConfiguration(protoSourceSet)
            setupExtractProtosTask(protoSourceSet, protobufConfig)
          }

          NamedDomainObjectContainer variantSourceSets =
            project.objects.domainObjectContainer(ProtoSourceSet) { String name ->
              new DefaultProtoSourceSet(name, project.objects)
            }
          ProjectExt.forEachVariant(this.project) { BaseVariant variant ->
            addTasksForVariant(variant, variantSourceSets, postConfigure)
          }
        } else {
          project.sourceSets.configureEach { sourceSet ->
            ProtoSourceSet protoSourceSet = protobufExtension.sourceSets.create(sourceSet.name)
            addSourceSetExtension(sourceSet, protoSourceSet)
            Configuration protobufConfig = createProtobufConfiguration(protoSourceSet)
            Configuration compileProtoPath = createCompileProtoPathConfiguration(protoSourceSet)
            addTasksForSourceSet(sourceSet, protoSourceSet, protobufConfig, compileProtoPath, postConfigure)
          }
        }
        project.afterEvaluate {
          this.protobufExtension.configureTasks()
          // Disallow user configuration outside the config closures, because the operations just
          // after the doneConfig() loop over the generated outputs and will be out-of-date if
          // plugin output is added after this point.
          this.protobufExtension.generateProtoTasks.all().configureEach { it.doneConfig() }
          postConfigure.each { it.call() }
          // protoc and codegen plugin configuration may change through the protobuf{}
          // block. Only at this point the configuration has been finalized.
          project.protobuf.tools.resolve(project)
        }
    }

    /**
     * Creates a 'protobuf' configuration for the given source set. The build author can
     * configure dependencies for it. The extract-protos task of each source set will
     * extract protobuf files from dependencies in this configuration.
     */
    private Configuration createProtobufConfiguration(ProtoSourceSet protoSourceSet) {
      String protobufConfigName = Utils.getConfigName(protoSourceSet.name, 'protobuf')
      return project.configurations.create(protobufConfigName) { Configuration it ->
        it.visible = false
        it.transitive = true
      }
    }

    /**
     * Creates an internal 'compileProtoPath' configuration for the given source set that extends
     * compilation configurations as a bucket of dependencies with resources attribute.
     * The extract-include-protos task of each source set will extract protobuf files from
     * resolved dependencies in this configuration.
     *
     * 

For Java projects only. *

This works around 'java-library' plugin not exposing resources to consumers for compilation. */ private Configuration createCompileProtoPathConfiguration(ProtoSourceSet protoSourceSet) { String compileProtoConfigName = Utils.getConfigName(protoSourceSet.name, 'compileProtoPath') Configuration compileConfig = project.configurations.getByName(Utils.getConfigName(protoSourceSet.name, 'compileOnly')) Configuration implementationConfig = project.configurations.getByName(Utils.getConfigName(protoSourceSet.name, 'implementation')) return project.configurations.create(compileProtoConfigName) { Configuration it -> it.visible = false it.transitive = true it.extendsFrom = [compileConfig, implementationConfig] it.canBeConsumed = false it.getAttributes() // Variant attributes are not inherited. Setting it too loosely can // result in ambiguous variant selection errors. // CompileProtoPath only need proto files from dependency's resources. // LibraryElement "resources" is compatible with "jar" (if a "resources" variant is // not found, the "jar" variant will be used). .attribute( LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.getObjects().named(LibraryElements, LibraryElements.RESOURCES)) // Although variants with any usage has proto files, not setting usage attribute // can result in ambiguous variant selection if the producer provides multiple // variants with different usage attribute. // Preserve the usage attribute from CompileOnly and Implementation. .attribute( Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage, Usage.JAVA_RUNTIME)) } } /** * Adds the proto extension to the SourceSet, e.g., it creates * sourceSets.main.proto and sourceSets.test.proto. */ @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP private SourceDirectorySet addSourceSetExtension(Object sourceSet, ProtoSourceSet protoSourceSet) { String name = sourceSet.name SourceDirectorySet sds = protoSourceSet.proto sourceSet.extensions.add('proto', sds) sds.srcDir("src/${name}/proto") sds.include("**/*.proto") return sds } /** * Creates Protobuf tasks for a sourceSet in a Java project. */ private void addTasksForSourceSet( SourceSet sourceSet, ProtoSourceSet protoSourceSet, Configuration protobufConfig, Configuration compileProtoPath, Collection postConfigure) { Provider extractProtosTask = setupExtractProtosTask(protoSourceSet, protobufConfig) Provider extractIncludeProtosTask = setupExtractIncludeProtosTask( protoSourceSet, compileProtoPath) // Make protos in 'test' sourceSet able to import protos from the 'main' sourceSet. // Pass include proto files from main to test. if (Utils.isTest(sourceSet.name)) { protoSourceSet.includesFrom(protobufExtension.sourceSets.getByName("main")) } Provider generateProtoTask = addGenerateProtoTask(protoSourceSet) { it.sourceSet = sourceSet it.doneInitializing() it.builtins.maybeCreate("java") } sourceSet.java.srcDirs(protoSourceSet.output) // Include source proto files in the compiled archive, so that proto files from // dependent projects can import them. project.tasks.named(sourceSet.getTaskName('process', 'resources'), ProcessResources).configure { it.from(protoSourceSet.proto) { CopySpec cs -> cs.include '**/*.proto' } } postConfigure.add { project.plugins.withId("eclipse") { // This is required because the intellij/eclipse plugin does not allow adding source directories // that do not exist. The intellij/eclipse config files should be valid from the start. generateProtoTask.get().getOutputSourceDirectories().each { File outputDir -> outputDir.mkdirs() } } project.plugins.withId("idea") { boolean isTest = Utils.isTest(sourceSet.name) protoSourceSet.proto.srcDirs.each { File protoDir -> Utils.addToIdeSources(project, isTest, protoDir, false) } Utils.addToIdeSources(project, isTest, project.files(extractProtosTask).singleFile, true) Utils.addToIdeSources(project, isTest, project.files(extractIncludeProtosTask).singleFile, true) generateProtoTask.get().getOutputSourceDirectories().each { File outputDir -> Utils.addToIdeSources(project, isTest, outputDir, true) } } } } /** * Creates Protobuf tasks for a variant in an Android project. */ @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP private void addTasksForVariant( Object variant, NamedDomainObjectContainer variantSourceSets, Collection postConfigure ) { Boolean isTestVariant = variant instanceof TestVariant || variant instanceof UnitTestVariant ProtoSourceSet variantSourceSet = variantSourceSets.create(variant.name) // ExtractIncludeProto task, one per variant (compilation unit). // Proto definitions from an AAR dependencies are in its JAR resources. FileCollection classPathConfig = variant.compileConfiguration.incoming.artifactView { attributes.attribute( ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.JAR_TYPE ) }.files // Make protos in 'test' variant able to import protos from the 'main' variant. // Pass include proto files from main to test. if (variant instanceof TestVariant || variant instanceof UnitTestVariant) { postConfigure.add { variantSourceSet.includesFrom(protobufExtension.sourceSets.getByName("main")) variantSourceSet.includesFrom(variantSourceSets.getByName(variant.testedVariant.name)) } } setupExtractIncludeProtosTask(variantSourceSet, classPathConfig) // GenerateProto task, one per variant (compilation unit). variant.sourceSets.each { SourceProvider sourceProvider -> variantSourceSet.extendsFrom(protobufExtension.sourceSets.getByName(sourceProvider.name)) } Provider generateProtoTask = addGenerateProtoTask(variantSourceSet) { it.setVariant(variant, isTestVariant) it.flavors = variant.productFlavors.collect { it.name } if (variant.hasProperty('buildType')) { it.buildType = variant.buildType.name } it.doneInitializing() } if (project.android.hasProperty('libraryVariants')) { // Include source proto files in the compiled archive, so that proto files from // dependent projects can import them. variant.getProcessJavaResourcesProvider().configure { it.from(variantSourceSet.proto) { include '**/*.proto' } } } postConfigure.add { // This cannot be called once task execution has started. variant.registerJavaGeneratingTask(generateProtoTask.get(), generateProtoTask.get().outputSourceDirectories) project.plugins.withId("org.jetbrains.kotlin.android") { // Checking if Kotlin plugin is a recent one - 1.7.20+ if (it.respondsTo("getPluginVersion")) { KotlinAndroidProjectExtension kotlinExtension = project.extensions.getByType(KotlinAndroidProjectExtension) kotlinExtension.target.compilations.named(variant.name) { it.defaultSourceSet.kotlin.srcDirs(variantSourceSet.output) } } else { project.afterEvaluate { String compileKotlinTaskName = Utils.getKotlinAndroidCompileTaskName(project, variant.name) project.tasks.named(compileKotlinTaskName, KotlinCompile) { KotlinCompile task -> task.dependsOn(generateProtoTask) task.source(generateProtoTask.get().outputSourceDirectories) } } } } } } /** * Adds a task to run protoc and compile all proto source files for a sourceSet or variant. * * @param sourceSetOrVariantName the name of the sourceSet (Java) or * variant (Android) that this task will run for. * * @param sourceSets the sourceSets that contains the proto files to be * compiled. For Java it's the sourceSet that sourceSetOrVariantName stands * for; for Android it's the collection of sourceSets that the variant includes. */ private Provider addGenerateProtoTask( ProtoSourceSet protoSourceSet, Action configureAction ) { String sourceSetName = protoSourceSet.name String taskName = 'generate' + Utils.getSourceSetSubstringForTaskNames(sourceSetName) + 'Proto' Provider defaultGeneratedFilesBaseDir = protobufExtension.defaultGeneratedFilesBaseDir Provider generatedFilesBaseDirProvider = protobufExtension.generatedFilesBaseDirProperty Provider task = project.tasks.register(taskName, GenerateProtoTask) { CopyActionFacade copyActionFacade = CopyActionFacade.Loader.create(it.project, it.objectFactory) it.description = "Compiles Proto source for '${sourceSetName}'".toString() it.outputBaseDir = defaultGeneratedFilesBaseDir.map { "${it}/${sourceSetName}".toString() } it.addSourceDirs(protoSourceSet.proto) it.addIncludeDir(protoSourceSet.proto.sourceDirectories) it.addIncludeDir(protoSourceSet.includeProtoDirs) it.doLast { task -> String generatedFilesBaseDir = generatedFilesBaseDirProvider.get() if (generatedFilesBaseDir == defaultGeneratedFilesBaseDir.get()) { return } // Purposefully don't wire this up to outputs, as it can be mixed with other files. copyActionFacade.copy { CopySpec spec -> spec.includeEmptyDirs = false spec.from(it.outputBaseDir) spec.into("${generatedFilesBaseDir}/${sourceSetName}") } } configureAction.execute(it) } protoSourceSet.output.from(task.map { GenerateProtoTask it -> it.outputSourceDirectories }) return task } /** * Sets up a task to extract protos from protobuf dependencies. They are * treated as sources and will be compiled. * *

This task is per-sourceSet, for both Java and Android. In Android a * variant may have multiple sourceSets, each of these sourceSets will have * its own extraction task. */ private Provider setupExtractProtosTask( ProtoSourceSet protoSourceSet, Configuration protobufConfig ) { String sourceSetName = protoSourceSet.name String taskName = getExtractProtosTaskName(sourceSetName) Provider task = project.tasks.register(taskName, ProtobufExtract) { it.description = "Extracts proto files/dependencies specified by 'protobuf' configuration" it.destDir.set(getExtractedProtosDir(sourceSetName) as File) it.inputFiles.from(protobufConfig) } protoSourceSet.proto.srcDir(task) return task } private String getExtractProtosTaskName(String sourceSetName) { return 'extract' + Utils.getSourceSetSubstringForTaskNames(sourceSetName) + 'Proto' } /** * Sets up a task to extract protos from compile dependencies of a sourceSet, Those are needed * for imports in proto files, but they won't be compiled since they have already been compiled * in their own projects or artifacts. * *

This task is per-sourceSet for both Java and per variant for Android. */ private Provider setupExtractIncludeProtosTask( ProtoSourceSet protoSourceSet, FileCollection archives ) { String taskName = 'extractInclude' + Utils.getSourceSetSubstringForTaskNames(protoSourceSet.name) + 'Proto' Provider task = project.tasks.register(taskName, ProtobufExtract) { it.description = "Extracts proto files from compile dependencies for includes" it.destDir.set(getExtractedIncludeProtosDir(protoSourceSet.name) as File) it.inputFiles.from(archives) } protoSourceSet.includeProtoDirs.from(task) return task } private String getExtractedIncludeProtosDir(String sourceSetName) { return "${project.buildDir}/extracted-include-protos/${sourceSetName}" } private String getExtractedProtosDir(String sourceSetName) { return "${project.buildDir}/extracted-protos/${sourceSetName}" } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy