diff --git a/src/main/groovy/com/google/protobuf/gradle/ProtobufPlugin.groovy b/src/main/groovy/com/google/protobuf/gradle/ProtobufPlugin.groovy index b04d12e6..74fab305 100644 --- a/src/main/groovy/com/google/protobuf/gradle/ProtobufPlugin.groovy +++ b/src/main/groovy/com/google/protobuf/gradle/ProtobufPlugin.groovy @@ -38,6 +38,7 @@ import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task +import org.gradle.api.UnknownDomainObjectException import org.gradle.api.artifacts.Configuration import org.gradle.api.attributes.Attribute import org.gradle.api.attributes.LibraryElements @@ -46,7 +47,6 @@ import org.gradle.api.file.CopySpec import org.gradle.api.file.FileCollection import org.gradle.api.file.SourceDirectorySet import org.gradle.api.plugins.AppliedPlugin -import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.provider.Provider import org.gradle.api.tasks.SourceSet import org.gradle.language.jvm.tasks.ProcessResources @@ -119,8 +119,8 @@ class ProtobufPlugin implements Plugin { } @TypeChecked(TypeCheckingMode.SKIP) - private static void linkGenerateProtoTasksToTask(Task task, GenerateProtoTask genProtoTask) { - task.source genProtoTask.getOutputSourceDirectorySet().include("**/*.java", "**/*.kt") + private static void linkGenerateProtoTasksToTask(Task task, Provider genProtoTask) { + task.source genProtoTask.get().getOutputSourceDirectorySet().include("**/*.java", "**/*.kt") } @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP @@ -134,7 +134,8 @@ class ProtobufPlugin implements Plugin { if (isAndroid) { project.android.sourceSets.configureEach { sourceSet -> addSourceSetExtension(sourceSet) - createProtobufConfiguration(sourceSet.name) + Configuration protobufConfig = createProtobufConfiguration(sourceSet.name) + setupExtractProtosTask(sourceSet.name, protobufConfig) } getNonTestVariants().configureEach { variant -> addTasksForVariant(variant, false, postConfigure) @@ -147,10 +148,10 @@ class ProtobufPlugin implements Plugin { } } else { project.sourceSets.configureEach { sourceSet -> - addSourceSetExtension(sourceSet) - createProtobufConfiguration(sourceSet.name) - createCompileProtoPathConfiguration(sourceSet.name) - addTasksForSourceSet(sourceSet) + SourceDirectorySet protoSrcDirSet = addSourceSetExtension(sourceSet) + Configuration protobufConfig = createProtobufConfiguration(sourceSet.name) + Configuration compileProtoPath = createCompileProtoPathConfiguration(sourceSet.name) + addTasksForSourceSet(sourceSet, protoSrcDirSet, protobufConfig, compileProtoPath, postConfigure) } } project.afterEvaluate { @@ -163,9 +164,6 @@ class ProtobufPlugin implements Plugin { // 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) - - // Register proto and generated sources with IDE - addSourcesToIde(isAndroid) } } @@ -174,13 +172,11 @@ class ProtobufPlugin implements Plugin { * configure dependencies for it. The extract-protos task of each source set will * extract protobuf files from dependencies in this configuration. */ - private void createProtobufConfiguration(String sourceSetName) { + private Configuration createProtobufConfiguration(String sourceSetName) { String protobufConfigName = Utils.getConfigName(sourceSetName, 'protobuf') - if (project.configurations.findByName(protobufConfigName) == null) { - project.configurations.create(protobufConfigName) { Configuration it -> - it.visible = false - it.transitive = true - } + return project.configurations.create(protobufConfigName) { Configuration it -> + it.visible = false + it.transitive = true } } @@ -193,19 +189,18 @@ class ProtobufPlugin implements Plugin { *

For Java projects only. *

This works around 'java-library' plugin not exposing resources to consumers for compilation. */ - private void createCompileProtoPathConfiguration(String sourceSetName) { + private Configuration createCompileProtoPathConfiguration(String sourceSetName) { String compileProtoConfigName = Utils.getConfigName(sourceSetName, 'compileProtoPath') Configuration compileConfig = project.configurations.findByName(Utils.getConfigName(sourceSetName, 'compileOnly')) Configuration implementationConfig = project.configurations.findByName(Utils.getConfigName(sourceSetName, 'implementation')) - if (project.configurations.findByName(compileProtoConfigName) == null) { - project.configurations.create(compileProtoConfigName) { Configuration it -> - it.visible = false - it.transitive = true - it.extendsFrom = [compileConfig, implementationConfig] - it.canBeConsumed = false - }.getAttributes() + 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. @@ -229,12 +224,13 @@ class ProtobufPlugin implements Plugin { * sourceSets.main.proto and sourceSets.test.proto. */ @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP - private void addSourceSetExtension(Object sourceSet) { + private SourceDirectorySet addSourceSetExtension(Object sourceSet) { String name = sourceSet.name SourceDirectorySet sds = project.objects.sourceDirectorySet(name, "${name} Proto source") sourceSet.extensions.add('proto', sds) sds.srcDir("src/${name}/proto") sds.include("**/*.proto") + return sds } @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP @@ -246,26 +242,46 @@ class ProtobufPlugin implements Plugin { /** * Creates Protobuf tasks for a sourceSet in a Java project. */ - private void addTasksForSourceSet(final SourceSet sourceSet) { - GenerateProtoTask generateProtoTask = addGenerateProtoTask(sourceSet.name, [sourceSet]) - generateProtoTask.sourceSet = sourceSet - generateProtoTask.doneInitializing() - generateProtoTask.builtins.maybeCreate("java") - - setupExtractProtosTask(generateProtoTask, sourceSet.name) - setupExtractIncludeProtosTask(generateProtoTask, sourceSet.name) + private void addTasksForSourceSet( + final SourceSet sourceSet, SourceDirectorySet protoSrcDirSet, Configuration protobufConfig, + Configuration compileProtoPath, Collection postConfigure) { + Provider extractProtosTask = + setupExtractProtosTask(sourceSet.name, protobufConfig) + Provider extractIncludeProtosTask = setupExtractIncludeProtosTask( + sourceSet.name, false, compileProtoPath) + Provider generateProtoTask = addGenerateProtoTask( + sourceSet.name, protoSrcDirSet.sourceDirectories, project.files(extractProtosTask), + extractIncludeProtosTask) { + it.sourceSet = sourceSet + it.doneInitializing() + it.builtins.maybeCreate("java") + } // Include source proto files in the compiled archive, so that proto files from // dependent projects can import them. - ProcessResources processResourcesTask = - project.tasks.getByName(sourceSet.getTaskName('process', 'resources')) as ProcessResources - processResourcesTask.from(generateProtoTask.sourceDirs) { CopySpec it -> - it.include '**/*.proto' + project.tasks.named(sourceSet.getTaskName('process', 'resources'), ProcessResources).configure { + it.from(generateProtoTask.get().sourceDirs) { CopySpec cs -> + cs.include '**/*.proto' + } } SUPPORTED_LANGUAGES.each { String lang -> linkGenerateProtoTasksToTaskName(sourceSet.getCompileTaskName(lang), generateProtoTask) } + + postConfigure.add { + project.plugins.withId("idea") { + boolean isTest = Utils.isTest(sourceSet.name) + protoSrcDirSet.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) + } + } + } } /** @@ -273,15 +289,6 @@ class ProtobufPlugin implements Plugin { */ @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP private void addTasksForVariant(final Object variant, boolean isTestVariant, Collection postConfigure) { - // GenerateProto task, one per variant (compilation unit). - GenerateProtoTask generateProtoTask = addGenerateProtoTask(variant.name, variant.sourceSets) - generateProtoTask.setVariant(variant, isTestVariant) - generateProtoTask.flavors = ImmutableList.copyOf(variant.productFlavors.collect { it.name } ) - if (variant.hasProperty('buildType')) { - generateProtoTask.buildType = variant.buildType.name - } - generateProtoTask.doneInitializing() - // ExtractIncludeProto task, one per variant (compilation unit). // Proto definitions from an AAR dependencies are in its JAR resources. Attribute artifactType = Attribute.of("artifactType", String) @@ -297,23 +304,41 @@ class ProtobufPlugin implements Plugin { it.attribute(artifactType, "jar") } }.files : null - setupExtractIncludeProtosTask(generateProtoTask, variant.name, true, classPathConfig, testClassPathConfig) + Provider extractIncludeProtosTask = + setupExtractIncludeProtosTask(variant.name, true, classPathConfig, testClassPathConfig) + + // GenerateProto task, one per variant (compilation unit). + FileCollection sourceDirs = project.files(project.providers.provider { + variant.sourceSets.collect { it.proto.sourceDirectories } + }) + FileCollection extractProtosDirs = project.files(project.providers.provider { + variant.sourceSets.collect { + project.files(project.tasks.named(getExtractProtosTaskName(it.name))) + } + }) + Provider generateProtoTask = addGenerateProtoTask( + variant.name, sourceDirs, extractProtosDirs, extractIncludeProtosTask) { + it.setVariant(variant, isTestVariant) + it.flavors = ImmutableList.copyOf(variant.productFlavors.collect { it.name } ) + if (variant.hasProperty('buildType')) { + it.buildType = variant.buildType.name + } + it.doneInitializing() + } - // ExtractProto task, one per source set. if (project.android.hasProperty('libraryVariants')) { // Include source proto files in the compiled archive, so that proto files from // dependent projects can import them. - Task processResourcesTask = variant.getProcessJavaResourcesProvider().get() - processResourcesTask.from(generateProtoTask.sourceDirs) { + variant.getProcessJavaResourcesProvider().configure { + it.from(generateProtoTask.get().sourceDirs) { include '**/*.proto' + } } } - variant.sourceSets.each { - setupExtractProtosTask(generateProtoTask, it.name) - } postConfigure.add { // This cannot be called once task execution has started. - variant.registerJavaGeneratingTask(generateProtoTask, generateProtoTask.getOutputSourceDirectories()) + variant.registerJavaGeneratingTask( + generateProtoTask.get(), generateProtoTask.get().getOutputSourceDirectories()) linkGenerateProtoTasksToTaskName( Utils.getKotlinAndroidCompileTaskName(project, variant.name), generateProtoTask) } @@ -329,19 +354,24 @@ class ProtobufPlugin implements Plugin { * compiled. For Java it's the sourceSet that sourceSetOrVariantName stands * for; for Android it's the collection of sourceSets that the variant includes. */ - @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP - private GenerateProtoTask addGenerateProtoTask(String sourceSetOrVariantName, Collection sourceSets) { + private Provider addGenerateProtoTask( + String sourceSetOrVariantName, + FileCollection sourceDirs, + FileCollection extractProtosDirs, + Provider extractIncludeProtosTask, + Action configureAction) { String generateProtoTaskName = 'generate' + Utils.getSourceSetSubstringForTaskNames(sourceSetOrVariantName) + 'Proto' Provider outDir = project.providers.provider { "${this.protobufExtension.generatedFilesBaseDir}/${sourceSetOrVariantName}".toString() } - return project.tasks.create(generateProtoTaskName, GenerateProtoTask) { + return project.tasks.register(generateProtoTaskName, GenerateProtoTask) { it.description = "Compiles Proto source for '${sourceSetOrVariantName}'".toString() it.outputBaseDir = outDir - sourceSets.each { sourceSet -> - it.addSourceDirs(sourceSet.proto.sourceDirectories) - } + it.addSourceDirs(sourceDirs) + it.addSourceDirs(extractProtosDirs) + it.addIncludeDir(project.files(extractIncludeProtosTask)) + configureAction.execute(it) } } @@ -353,21 +383,18 @@ class ProtobufPlugin implements Plugin { * variant may have multiple sourceSets, each of these sourceSets will have * its own extraction task. */ - private Task setupExtractProtosTask(final GenerateProtoTask generateProtoTask, final String sourceSetName) { - String extractProtosTaskName = 'extract' + - Utils.getSourceSetSubstringForTaskNames(sourceSetName) + 'Proto' - ProtobufExtract task = project.tasks.findByName(extractProtosTaskName) as ProtobufExtract - if (task == null) { - task = project.tasks.create(extractProtosTaskName, ProtobufExtract) { - it.description = "Extracts proto files/dependencies specified by 'protobuf' configuration" - it.destDir.set(getExtractedProtosDir(sourceSetName) as File) - it.inputFiles.from(project.configurations[Utils.getConfigName(sourceSetName, 'protobuf')]) - it.isTest.set(Utils.isTest(sourceSetName)) - } + private Provider setupExtractProtosTask( + final String sourceSetName, Configuration protobufConfig) { + return project.tasks.register(getExtractProtosTaskName(sourceSetName), ProtobufExtract) { + it.description = "Extracts proto files/dependencies specified by 'protobuf' configuration" + it.destDir.set(getExtractedProtosDir(sourceSetName) as File) + it.inputFiles.from(protobufConfig) + it.isTest.set(Utils.isTest(sourceSetName)) } + } - generateProtoTask.addSourceDirs(project.files(task)) - return task + private String getExtractProtosTaskName(String sourceSetName) { + return 'extract' + Utils.getSourceSetSubstringForTaskNames(sourceSetName) + 'Proto' } /** @@ -378,55 +405,49 @@ class ProtobufPlugin implements Plugin { *

This task is per-sourceSet for both Java and per variant for Android. */ @TypeChecked(TypeCheckingMode.SKIP) // Don't depend on AGP - private void setupExtractIncludeProtosTask( - GenerateProtoTask generateProtoTask, + private Provider setupExtractIncludeProtosTask( String sourceSetOrVariantName, - boolean isAndroid = false, - FileCollection compileClasspathConfiguration = null, + boolean isAndroid, + FileCollection compileClasspathConfiguration, FileCollection testedCompileClasspathConfiguration = null) { String extractIncludeProtosTaskName = 'extractInclude' + Utils.getSourceSetSubstringForTaskNames(sourceSetOrVariantName) + 'Proto' - Task task = project.tasks.findByName(extractIncludeProtosTaskName) - if (task == null) { - task = project.tasks.create(extractIncludeProtosTaskName, ProtobufExtract) { - it.description = "Extracts proto files from compile dependencies for includes" - it.destDir = getExtractedIncludeProtosDir(sourceSetOrVariantName) as File - it.inputFiles.from(compileClasspathConfiguration - ?: project.configurations[Utils.getConfigName(sourceSetOrVariantName, 'compileProtoPath')]) - - // TL; DR: Make protos in 'test' sourceSet able to import protos from the 'main' - // sourceSet. Sub-configurations, e.g., 'testCompile' that extends 'compile', don't - // depend on the their super configurations. As a result, 'testCompile' doesn't depend on - // 'compile' and it cannot get the proto files from 'main' sourceSet through the - // configuration. However, - if (isAndroid) { - // TODO(zhangkun83): Android sourceSet doesn't have compileClasspath. If it did, we - // haven't figured out a way to put source protos in 'resources'. For now we use an - // ad-hoc solution that manually includes the source protos of 'main' and its - // dependencies. - if (Utils.isTest(sourceSetOrVariantName)) { - it.inputFiles.from project.android.sourceSets['main'].proto.sourceDirectories - it.inputFiles.from testedCompileClasspathConfiguration - } - } else { - // In Java projects, the compileClasspath of the 'test' sourceSet includes all the - // 'resources' of the output of 'main', in which the source protos are placed. This is - // nicer than the ad-hoc solution that Android has, because it works for any extended - // configuration, not just 'testCompile'. - it.inputFiles.from project.sourceSets[sourceSetOrVariantName].compileClasspath + return project.tasks.register(extractIncludeProtosTaskName, ProtobufExtract) { + it.description = "Extracts proto files from compile dependencies for includes" + it.destDir = getExtractedIncludeProtosDir(sourceSetOrVariantName) as File + it.inputFiles.from(compileClasspathConfiguration) + + // TL; DR: Make protos in 'test' sourceSet able to import protos from the 'main' + // sourceSet. Sub-configurations, e.g., 'testCompile' that extends 'compile', don't + // depend on the their super configurations. As a result, 'testCompile' doesn't depend on + // 'compile' and it cannot get the proto files from 'main' sourceSet through the + // configuration. However, + if (isAndroid) { + // TODO(zhangkun83): Android sourceSet doesn't have compileClasspath. If it did, we + // haven't figured out a way to put source protos in 'resources'. For now we use an + // ad-hoc solution that manually includes the source protos of 'main' and its + // dependencies. + if (Utils.isTest(sourceSetOrVariantName)) { + it.inputFiles.from project.android.sourceSets['main'].proto.sourceDirectories + it.inputFiles.from testedCompileClasspathConfiguration } - it.isTest = Utils.isTest(sourceSetOrVariantName) + } else { + // In Java projects, the compileClasspath of the 'test' sourceSet includes all the + // 'resources' of the output of 'main', in which the source protos are placed. This is + // nicer than the ad-hoc solution that Android has, because it works for any extended + // configuration, not just 'testCompile'. + it.inputFiles.from project.sourceSets[sourceSetOrVariantName].compileClasspath } + it.isTest = Utils.isTest(sourceSetOrVariantName) } - - generateProtoTask.addIncludeDir(project.files(task)) } - private void linkGenerateProtoTasksToTaskName(String compileTaskName, GenerateProtoTask genProtoTask) { - Task compileTask = project.tasks.findByName(compileTaskName) - if (compileTask != null) { - linkGenerateProtoTasksToTask(compileTask, genProtoTask) - } else { + private void linkGenerateProtoTasksToTaskName(String compileTaskName, Provider genProtoTask) { + try { + project.tasks.named(compileTaskName).configure { compileTask -> + linkGenerateProtoTasksToTask(compileTask, genProtoTask) + } + } catch (UnknownDomainObjectException ignore) { // It is possible for a compile task to not exist yet. For example, if someone applied // the proto plugin and then later applies the kotlin plugin. project.tasks.configureEach { Task task -> @@ -444,46 +465,4 @@ class ProtobufPlugin implements Plugin { private String getExtractedProtosDir(String sourceSetName) { return "${project.buildDir}/extracted-protos/${sourceSetName}" } - - /** - * Adds proto sources and generated sources to supported IDE plugins. - */ - private void addSourcesToIde(boolean isAndroid) { - // The generated javalite sources have lint issues. This is fixed upstream but - // there is still no release with the fix yet. - // https://github.com/google/protobuf/pull/2823 - - // TODO(zpencer): add gen sources from cross project GenerateProtoTasks - // This is an uncommon project set up but it is possible. - // We must avoid using private android APIs to find subprojects that the variant depends - // on, such as by walking through - // variant.variantData.variantDependency.compileConfiguration.allDependencies - // Gradle.getTaskGraph().getDependencies() should allow us to walk the task graph, - // but unfortunately that API is @Incubating. We can revisit it when it becomes stable. - // https://docs.gradle.org/4.8/javadoc/org/gradle/api/execution/ - // TaskExecutionGraph.html#getDependencies-org.gradle.api.Task- - - // TODO(zpencer): find a way to make android studio aware of the .proto files - // Simply adding the .proto dirs via addJavaSourceFoldersToModel does not seem to work. - - if (!isAndroid) { - // Make the proto source dirs known to IDEs - project.convention.getPlugin(JavaPluginConvention).sourceSets.each { sourceSet -> - SourceDirectorySet protoSrcDirSet = sourceSet.extensions.getByName('proto') as SourceDirectorySet - protoSrcDirSet.srcDirs.each { File protoDir -> - Utils.addToIdeSources(project, Utils.isTest(sourceSet.name), protoDir, false) - } - } - // Make the extracted proto dirs known to IDEs - project.tasks.withType(ProtobufExtract).each { ProtobufExtract extractProtoTask -> - Utils.addToIdeSources(project, extractProtoTask.isTest.get(), extractProtoTask.destDir.get().asFile, true) - } - // Make the generated code dirs known to IDEs - project.tasks.withType(GenerateProtoTask).each { GenerateProtoTask generateProtoTask -> - generateProtoTask.getOutputSourceDirectories().each { File outputDir -> - Utils.addToIdeSources(project, generateProtoTask.isTest, outputDir, true) - } - } - } - } } diff --git a/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginKotlinTest.groovy b/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginKotlinTest.groovy index 8abe2c5e..a1bbd9c0 100644 --- a/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginKotlinTest.groovy +++ b/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginKotlinTest.groovy @@ -10,8 +10,8 @@ import spock.lang.Unroll @CompileDynamic class ProtobufAndroidPluginKotlinTest extends Specification { - private static final List GRADLE_VERSION = ["5.6", "6.5-milestone-1", "7.4.2"] - private static final List ANDROID_PLUGIN_VERSION = ["3.5.0", "4.1.0-alpha10", "7.2.1"] + private static final List GRADLE_VERSION = ["5.6", "6.5.1", "7.4.2"] + private static final List ANDROID_PLUGIN_VERSION = ["3.5.0", "4.1.0", "7.2.1"] private static final List KOTLIN_VERSION = ["1.3.20", "1.3.20", "1.3.40"] /** diff --git a/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginTest.groovy b/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginTest.groovy index 67355be2..51cf1e3a 100644 --- a/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginTest.groovy +++ b/src/test/groovy/com/google/protobuf/gradle/ProtobufAndroidPluginTest.groovy @@ -15,7 +15,7 @@ import spock.lang.Unroll */ @CompileDynamic class ProtobufAndroidPluginTest extends Specification { - private static final List GRADLE_VERSION = ["5.6", "6.5", "6.8", "7.4.2"] + private static final List GRADLE_VERSION = ["5.6", "6.5.1", "6.8", "7.4.2"] private static final List ANDROID_PLUGIN_VERSION = ["3.5.0", "4.1.0", "4.2.0-alpha10", "7.2.1"] @Unroll diff --git a/src/test/groovy/com/google/protobuf/gradle/ProtobufKotlinDslPluginTest.groovy b/src/test/groovy/com/google/protobuf/gradle/ProtobufKotlinDslPluginTest.groovy index 034ccdd7..357ce8d0 100644 --- a/src/test/groovy/com/google/protobuf/gradle/ProtobufKotlinDslPluginTest.groovy +++ b/src/test/groovy/com/google/protobuf/gradle/ProtobufKotlinDslPluginTest.groovy @@ -14,7 +14,7 @@ import spock.lang.Unroll */ @CompileDynamic class ProtobufKotlinDslPluginTest extends Specification { - private static final List GRADLE_VERSIONS = ["5.6", "6.1.1", "6.5", "7.4.2"] + private static final List GRADLE_VERSIONS = ["5.6", "6.1.1", "6.5.1", "7.4.2"] private static final List ANDROID_PLUGIN_VERSION = ["3.5.0", "4.0.0", "4.1.0", "7.2.1"] @Unroll