Skip to content

Commit

Permalink
Configurable java executable (#750)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wfhartford committed Mar 14, 2024
1 parent ee41e2f commit 12ad318
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 20 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
30 changes: 10 additions & 20 deletions src/main/groovy/com/google/protobuf/gradle/GenerateProtoTask.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -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) {
Expand All @@ -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<String> outputBaseDir) {
checkInitializing()
Preconditions.checkState(this.outputBaseDir == null, 'outputBaseDir is already set')
Expand Down Expand Up @@ -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")
Expand All @@ -754,15 +743,16 @@ 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 ?
"@ECHO OFF\r\n\"${escapePathWindows(javaExe)}\" -jar \"${escapePathWindows(jarAbsolutePath)}\" %*\r\n" :
"#!/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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ abstract class ProtobufExtension {
@PackageScope
final Provider<String> defaultGeneratedFilesBaseDir

@PackageScope
final Provider<String> defaultJavaExecutablePath

public ProtobufExtension(final Project project) {
this.project = project
this.tasks = new GenerateProtoTaskCollection(project)
Expand All @@ -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<ProtoSourceSet> getSourceSets() {
return this.sourceSets
Expand All @@ -97,6 +112,13 @@ abstract class ProtobufExtension {
@PackageScope
abstract Property<String> 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<String> getJavaExecutablePath()

@PackageScope
void configureTasks() {
this.taskConfigActions.each { action ->
Expand Down
8 changes: 8 additions & 0 deletions src/main/groovy/com/google/protobuf/gradle/Utils.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
25 changes: 25 additions & 0 deletions testProjectConfigureJavaExecutable/build.gradle
Original file line number Diff line number Diff line change
@@ -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 {}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 12ad318

Please sign in to comment.