From 35e490868515811045fdf324c26600e52f8f4ecb Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Wed, 13 Mar 2024 15:04:45 -0700 Subject: [PATCH 1/2] Allow configuration of java executable The path of the java executable can now be configured in the ProtobufExtension and/or specific GenerateProtoTask instances. * GenerateProtoTask gains the javaExecutablePath Property, * ProtobufExtension gains the javaExecutablePath Property and the defaultJavaExecutablePath provider, which provides the default path using the same logic as previous versions * computeJavaExePath moved from GenerateProtoTask to ProtobufExtension since it is now only used in ProtobufExtension * isWindows moved from GenerateProtoTask to Util since it is now used in GenerateProtoTask and ProtobufExtension --- .../protobuf/gradle/GenerateProtoTask.groovy | 30 +++++++------------ .../protobuf/gradle/ProtobufExtension.groovy | 22 ++++++++++++++ .../com/google/protobuf/gradle/Utils.groovy | 8 +++++ 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy b/src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy index 4ce6d6d1..0ce54372 100644 --- a/src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy +++ b/src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy @@ -46,6 +46,7 @@ import org.gradle.api.file.ProjectLayout import org.gradle.api.file.SourceDirectorySet import org.gradle.api.logging.LogLevel import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.CacheableTask @@ -95,6 +96,10 @@ public abstract class GenerateProtoTask extends DefaultTask { private final ProjectLayout projectLayout = project.layout private final ToolsLocator toolsLocator = project.extensions.findByType(ProtobufExtension).tools + @Input + final Property javaExecutablePath = objectFactory.property(String) + .convention(project.extensions.findByType(ProtobufExtension).javaExecutablePath) + // These fields are set by the Protobuf plugin only when initializing the // task. Ideally they should be final fields, but Gradle task cannot have // constructor arguments. We use the initializing flag to prevent users from @@ -210,15 +215,7 @@ public abstract class GenerateProtoTask extends DefaultTask { } static int getCmdLengthLimit(String os) { - return isWindows(os) ? WINDOWS_CMD_LENGTH_LIMIT : DEFAULT_CMD_LENGTH_LIMIT - } - - static boolean isWindows(String os) { - return os != null && os.toLowerCase(Locale.ROOT).indexOf("win") > -1 - } - - static boolean isWindows() { - return isWindows(System.getProperty("os.name")) + return Utils.isWindows(os) ? WINDOWS_CMD_LENGTH_LIMIT : DEFAULT_CMD_LENGTH_LIMIT } static String escapePathUnix(String path) { @@ -243,14 +240,6 @@ public abstract class GenerateProtoTask extends DefaultTask { } } - static String computeJavaExePath(boolean isWindows) throws IOException { - File java = new File(System.getProperty("java.home"), isWindows ? "bin/java.exe" : "bin/java") - if (!java.exists()) { - throw new IOException("Could not find java executable at " + java.path) - } - return java.path - } - void setOutputBaseDir(Provider outputBaseDir) { checkInitializing() Preconditions.checkState(this.outputBaseDir == null, 'outputBaseDir is already set') @@ -744,7 +733,7 @@ public abstract class GenerateProtoTask extends DefaultTask { */ private String createJarTrampolineScript(String jarAbsolutePath) { assert jarAbsolutePath.endsWith(JAR_SUFFIX) - boolean isWindows = isWindows() + boolean isWindows = Utils.isWindows() String jarFileName = new File(jarAbsolutePath).getName() if (jarFileName.length() <= JAR_SUFFIX.length()) { throw new GradleException(".jar protoc plugin path '${jarAbsolutePath}' has no file name") @@ -754,7 +743,7 @@ public abstract class GenerateProtoTask extends DefaultTask { (isWindows ? "bat" : "sh")) try { mkdirsForFile(scriptExecutableFile) - String javaExe = computeJavaExePath(isWindows) + String javaExe = javaExecutablePath.get() // Rewrite the trampoline file unconditionally (even if it already exists) in case the dependency or versioning // changes we don't need to detect the delta (and the file content is cheap to re-generate). String trampoline = isWindows ? @@ -762,7 +751,8 @@ public abstract class GenerateProtoTask extends DefaultTask { "#!/bin/sh\nexec '${escapePathUnix(javaExe)}' -jar '${escapePathUnix(jarAbsolutePath)}' \"\$@\"\n" scriptExecutableFile.write(trampoline, US_ASCII.name()) setExecutableOrFail(scriptExecutableFile) - logger.info("Resolved artifact jar: ${jarAbsolutePath}. Created trampoline file: ${scriptExecutableFile}") + logger.info("Resolved artifact jar: ${jarAbsolutePath}. " + + "Created trampoline file: ${scriptExecutableFile} with java executable ${javaExe}") return scriptExecutableFile.path } catch (IOException e) { throw new GradleException("Unable to generate trampoline for .jar protoc plugin", e) diff --git a/src/main/groovy/com/google/protobuf/gradle/ProtobufExtension.groovy b/src/main/groovy/com/google/protobuf/gradle/ProtobufExtension.groovy index 2e380022..c08d203f 100644 --- a/src/main/groovy/com/google/protobuf/gradle/ProtobufExtension.groovy +++ b/src/main/groovy/com/google/protobuf/gradle/ProtobufExtension.groovy @@ -57,6 +57,9 @@ abstract class ProtobufExtension { @PackageScope final Provider defaultGeneratedFilesBaseDir + @PackageScope + final Provider defaultJavaExecutablePath + public ProtobufExtension(final Project project) { this.project = project this.tasks = new GenerateProtoTaskCollection(project) @@ -66,11 +69,23 @@ abstract class ProtobufExtension { it.asFile.path } this.generatedFilesBaseDirProperty.convention(defaultGeneratedFilesBaseDir) + this.defaultJavaExecutablePath = project.provider { + computeJavaExePath() + } + this.javaExecutablePath.convention(defaultJavaExecutablePath) this.sourceSets = project.objects.domainObjectContainer(ProtoSourceSet) { String name -> new DefaultProtoSourceSet(name, project.objects) } } + static String computeJavaExePath() throws IOException { + File java = new File(System.getProperty("java.home"), Utils.isWindows() ? "bin/java.exe" : "bin/java") + if (!java.exists()) { + throw new IOException("Could not find java executable at " + java.path) + } + return java.path + } + @PackageScope NamedDomainObjectContainer getSourceSets() { return this.sourceSets @@ -97,6 +112,13 @@ abstract class ProtobufExtension { @PackageScope abstract Property getGeneratedFilesBaseDirProperty() + /** + * The location of the java executable used to run java based + * code generation plugins. The default is the java executable + * running gradle. + */ + abstract Property getJavaExecutablePath() + @PackageScope void configureTasks() { this.taskConfigActions.each { action -> diff --git a/src/main/groovy/com/google/protobuf/gradle/Utils.groovy b/src/main/groovy/com/google/protobuf/gradle/Utils.groovy index 9ce0e679..215e65ed 100644 --- a/src/main/groovy/com/google/protobuf/gradle/Utils.groovy +++ b/src/main/groovy/com/google/protobuf/gradle/Utils.groovy @@ -134,4 +134,12 @@ class Utils { } } } + + static boolean isWindows(String os) { + return os != null && os.toLowerCase(Locale.ROOT).indexOf("win") > -1 + } + + static boolean isWindows() { + return isWindows(System.getProperty("os.name")) + } } From b729027157e4f90bfe9b87b066fcdfea5999ac57 Mon Sep 17 00:00:00 2001 From: Wesley Hartford Date: Thu, 14 Mar 2024 09:33:42 -0700 Subject: [PATCH 2/2] Add tests for configuration of java executable --- build.gradle | 1 + .../gradle/ProtobufJavaPluginTest.groovy | 90 +++++++++++++++++++ .../build.gradle | 25 ++++++ .../proto/com/example/tutorial/sample.proto | 15 ++++ 4 files changed, 131 insertions(+) create mode 100644 testProjectConfigureJavaExecutable/build.gradle create mode 100644 testProjectConfigureJavaExecutable/src/main/proto/com/example/tutorial/sample.proto diff --git a/build.gradle b/build.gradle index d5363863..acc8497d 100644 --- a/build.gradle +++ b/build.gradle @@ -174,6 +174,7 @@ tasks.named('test') { inputs.files fileTree("$projectDir/testProjectAndroidLibrary") inputs.files fileTree("$projectDir/testProjectBase") inputs.files fileTree("$projectDir/testProjectBuildTimeProto") + inputs.files fileTree("$projectDir/testProjectConfigureJavaExecutable") inputs.files fileTree("$projectDir/testProjectCustomProtoDir") inputs.files fileTree("$projectDir/testProjectDependent") inputs.files fileTree("$projectDir/testProjectDependentApp") diff --git a/src/test/groovy/com/google/protobuf/gradle/ProtobufJavaPluginTest.groovy b/src/test/groovy/com/google/protobuf/gradle/ProtobufJavaPluginTest.groovy index 14fc1ea2..51eac1d5 100644 --- a/src/test/groovy/com/google/protobuf/gradle/ProtobufJavaPluginTest.groovy +++ b/src/test/groovy/com/google/protobuf/gradle/ProtobufJavaPluginTest.groovy @@ -621,6 +621,96 @@ class ProtobufJavaPluginTest extends Specification { limit == GenerateProtoTask.DEFAULT_CMD_LENGTH_LIMIT } + void "test custom java executable in extension"() { + given: "a basic project" + Project project = setupBasicProject() + + when: "a java executable is specified on the protobuf extension" + project.extensions.getByType(ProtobufExtension).javaExecutablePath.set("/custom-java.exe") + + then: "all tasks get the custom executable" + assert project.extensions.getByType(ProtobufExtension).javaExecutablePath.get() == "/custom-java.exe" + assert ((GenerateProtoTask)project.tasks.generateProto).javaExecutablePath.get() == "/custom-java.exe" + assert ((GenerateProtoTask)project.tasks.generateTestProto).javaExecutablePath.get() == "/custom-java.exe" + } + + void "test custom java executable in task"() { + given: "a basic project" + Project project = setupBasicProject() + + when: "a java executable is specified on the generate proto task" + ((GenerateProtoTask)project.tasks.generateProto).javaExecutablePath.set("/custom-java.exe") + + then: "generate proto task uses configured executable" + assert ((GenerateProtoTask)project.tasks.generateProto).javaExecutablePath.get() == "/custom-java.exe" + + and: "extension and test task use default executable" + assert project.extensions.getByType(ProtobufExtension).javaExecutablePath + .get() == ProtobufExtension.computeJavaExePath() + assert ((GenerateProtoTask)project.tasks.generateTestProto).javaExecutablePath + .get() == ProtobufExtension.computeJavaExePath() + } + + void "test custom java executable in extension and task"() { + given: "a basic project" + Project project = setupBasicProject() + + when: "a java executable is specified on the protobuf extension and generate proto task" + project.extensions.getByType(ProtobufExtension).javaExecutablePath.set("/ext-java.exe") + ((GenerateProtoTask)project.tasks.generateProto).javaExecutablePath.set("/task-java.exe") + + then: "extension and test task use executable specified on the extension" + assert project.extensions.getByType(ProtobufExtension).javaExecutablePath.get() == "/ext-java.exe" + assert ((GenerateProtoTask)project.tasks.generateTestProto).javaExecutablePath.get() == "/ext-java.exe" + + and: "generate proto task uses executable specified on task" + assert ((GenerateProtoTask)project.tasks.generateProto).javaExecutablePath.get() == "/task-java.exe" + } + + @Unroll + void "test proto generation fails when java executable is invalid [gradle #gradleVersion]"() { + given: "project from testProject" + File projectDir = ProtobufPluginTestHelper.projectBuilder('testProjectConfigureJavaExecutable') + .copyDirs('testProjectConfigureJavaExecutable') + .build() + + when: "build is invoked using grpc plugin" + BuildResult result = ProtobufPluginTestHelper.getGradleRunner( + projectDir, + gradleVersion, + "build" + ).build() + + then: "it succeeds" + assert result.task(":build").outcome == TaskOutcome.SUCCESS + assert result.task(":generateProto").outcome == TaskOutcome.SUCCESS + + // Since we don't know if there are multiple JDKs installed, and it would + // be challenging to determine which one was actually executed, we're + // going to test that the executable change works by setting to something + // invalid and ensuring that the build fails for the right reason. + when: "protobuf java executor is invalid and build runs again" + new File(projectDir, "build.gradle") + .append(""" + protobuf { + javaExecutablePath.set("/nothing") + }""") + result = ProtobufPluginTestHelper.getGradleRunner( + projectDir, + gradleVersion, + "build" + ).buildAndFail() + + then: "generateProto FAILED" + result.task(":generateProto").outcome == TaskOutcome.FAILED + + and: "the failure was caused by a missing executable" + result.output.contains("exec: /nothing: not found") + + where: + gradleVersion << GRADLE_VERSIONS + } + private Project setupBasicProject() { Project project = ProjectBuilder.builder().build() project.apply plugin:'java' diff --git a/testProjectConfigureJavaExecutable/build.gradle b/testProjectConfigureJavaExecutable/build.gradle new file mode 100644 index 00000000..8a7e3074 --- /dev/null +++ b/testProjectConfigureJavaExecutable/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' + id 'com.google.protobuf' +} +repositories { mavenCentral() } +dependencies { + implementation 'com.google.protobuf:protobuf-java:3.0.0' +} +protobuf { + protoc { + artifact = 'com.google.protobuf:protoc:3.0.0' + } + plugins { + grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.0.3' } + grpcKotlin { artifact = 'io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar' } + } + generateProtoTasks { + all().configureEach { task -> + task.plugins { + grpc {} + grpcKotlin {} + } + } + } +} diff --git a/testProjectConfigureJavaExecutable/src/main/proto/com/example/tutorial/sample.proto b/testProjectConfigureJavaExecutable/src/main/proto/com/example/tutorial/sample.proto new file mode 100644 index 00000000..47d0b0c5 --- /dev/null +++ b/testProjectConfigureJavaExecutable/src/main/proto/com/example/tutorial/sample.proto @@ -0,0 +1,15 @@ + +syntax = "proto3"; + +option java_package = "com.example.tutorial"; +option java_outer_classname = "OuterSample"; +option java_multiple_files = true; + +message Msg { + string foo = 1; + SecondMsg blah = 2; +} + +message SecondMsg { + int32 blah = 1; +}