Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable java executable #750

Merged
merged 2 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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')
ejona86 marked this conversation as resolved.
Show resolved Hide resolved
.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;
}
Loading