Skip to content
Permalink
Browse files

gRPC gradle plugin rework (#983)

Motivation:
0ebe48e divided the
servicetalk-grpc-gradle plugin into two files:
1. an executable script
2. an uber jar with the plugin logic
The executable script assumed the uber jar would be co-located in the
same directory as the uber jar, but that isn't the case in gradle
caches. This means the plugin may fail to execute outside of the maven
m2 repository structure.

Modifications:
- Instead of publishing a static script for each platform which assumes
a co-located uber jar, dynamically generate the executable script
depending upon where the uber jar is resolved from for the local build.

Result:
servicetalk-grpc-gradle works with gradle cache directory structure and
local development.
  • Loading branch information
Scottmitch committed Mar 26, 2020
1 parent 8084127 commit 4a85c39a7af5cea8952806020a62eb61a14020bd
@@ -27,7 +27,6 @@ if (!repositories) {
groovyClass.getDeclaredMethod("inheritRepositoriesFromBuildscript", Project).invoke(null, project)
}

apply plugin: "java"
apply plugin: "java-gradle-plugin"
apply from: "../servicetalk-grpc-gradle-plugin/plugin-config.gradle"
apply from: "../servicetalk-gradle-plugin-internal/plugin-config.gradle"

This file was deleted.

@@ -33,7 +33,7 @@ serviceTalkGrpc {

// The following setting must be omitted in users projects and is necessary here
// only because we want to use the locally built version of the plugin
serviceTalkProtocPluginPath = "${project.rootProject.rootDir}/servicetalk-grpc-protoc/build/buildExecutable/" +
io.servicetalk.internal.build.ExecutableBuilder.addExecutablePostFix("protoc-gen-servicetalk_grpc",
org.gradle.internal.os.OperatingSystem.current().isWindows())
serviceTalkProtocPluginPath =
"${project.rootProject.rootDir}/servicetalk-grpc-protoc/build/buildExecutable/servicetalk-grpc-protoc-" +
project.version + "-all.jar"
}
@@ -35,7 +35,7 @@ serviceTalkGrpc {

// The following setting must be omitted in users projects and is necessary here
// only because we want to use the locally built version of the plugin
serviceTalkProtocPluginPath = "${project.rootProject.rootDir}/servicetalk-grpc-protoc/build/buildExecutable/" +
io.servicetalk.internal.build.ExecutableBuilder.addExecutablePostFix("protoc-gen-servicetalk_grpc",
org.gradle.internal.os.OperatingSystem.current().isWindows())
serviceTalkProtocPluginPath =
"${project.rootProject.rootDir}/servicetalk-grpc-protoc/build/buildExecutable/servicetalk-grpc-protoc-" +
project.version + "-all.jar"
}
@@ -126,11 +126,6 @@ class ServiceTalkCorePlugin implements Plugin<Project> {
idea.workspace.iws.withXml { XmlProvider provider ->
appendNodes(provider, getClass().getResourceAsStream("idea/iws-components.xml"))
}
// idea plugin doesn't account for buildSrc directory, so manually add it.
idea.module.iml.withXml { XmlProvider provider ->
Node contentNode = provider.asNode().component.find { it.@name == "NewModuleRootManager" }.content[0]
contentNode.appendNode("sourceFolder", [url: "file://\$MODULE_DIR\$/buildSrc/src/main/java"])
}
}
}
}
@@ -26,6 +26,8 @@ import org.gradle.plugins.ide.idea.IdeaPlugin
import org.gradle.plugins.ide.idea.model.IdeaModel
import org.gradle.util.GradleVersion

import java.nio.charset.StandardCharsets

class ServiceTalkGrpcPlugin implements Plugin<Project> {
void apply(Project project) {
if (GradleVersion.current().baseVersion < GradleVersion.version("4.10")) {
@@ -40,21 +42,15 @@ class ServiceTalkGrpcPlugin implements Plugin<Project> {
ServiceTalkGrpcExtension extension = project.extensions.create("serviceTalkGrpc", ServiceTalkGrpcExtension)
extension.conventionMapping.generatedCodeDir = { project.file("$project.buildDir/generated/source/proto") }

def compileOnlyDeps = project.getConfigurations().getByName("compileOnly")
project.beforeEvaluate {
def serviceTalkProtocPluginPath = extension.serviceTalkProtocPluginPath
if (!serviceTalkProtocPluginPath) {
compileOnlyDeps.add(
project.getDependencies().create("io.servicetalk:servicetalk-grpc-protoc:$serviceTalkVersion:all"))
}
}

def compileOnlyDeps = project.getConfigurations().getByName("compileOnly").getDependencies()
def testCompileOnlyDeps = project.getConfigurations().getByName("testCompileOnly").getDependencies()
project.afterEvaluate {
Properties pluginProperties = new Properties()
pluginProperties.load(getClass().getResourceAsStream("/META-INF/servicetalk-grpc-gradle-plugin.properties"))

// In order to locate servicetalk-grpc-protoc we need either the ServiceTalk version for artifact resolution
// or be provided with a direct path to the protoc plugin executable
def serviceTalkGrpcProtoc = "servicetalk-grpc-protoc"
def serviceTalkVersion = pluginProperties."implementation-version"
def serviceTalkProtocPluginPath = extension.serviceTalkProtocPluginPath
if (!serviceTalkVersion && !serviceTalkProtocPluginPath) {
@@ -67,6 +63,49 @@ class ServiceTalkGrpcPlugin implements Plugin<Project> {
throw new InvalidUserDataException("Please set `serviceTalkGrpc.protobufVersion`.")
}

// If this project is outside of ServiceTalk's gradle build we need to add an explicit dependency on the
// uber jar which contains the protoc logic, as otherwise the grpc-gradle-plugin will only add a dependency
// on the executable script
File uberJarFile
String scriptNamePrefix
if (serviceTalkProtocPluginPath) {
scriptNamePrefix = serviceTalkGrpcProtoc + "-" + project.version
uberJarFile = new File(serviceTalkProtocPluginPath)
} else {
scriptNamePrefix = serviceTalkGrpcProtoc + "-" + serviceTalkVersion
def stGrpcProtocDep =
project.getDependencies().create("io.servicetalk:$servicetalk-grpc-protoc:$serviceTalkVersion:all")
compileOnlyDeps.add(stGrpcProtocDep)
testCompileOnlyDeps.add(stGrpcProtocDep)

uberJarFile = project.configurations.compileOnly.find { it.name.startsWith(serviceTalkGrpcProtoc) }
if (uberJarFile == null) {
throw new IllegalStateException("failed to find the $serviceTalkGrpcProtoc:$serviceTalkVersion:all")
}
}

File scriptExecutableFile
try {
if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
scriptExecutableFile = File.createTempFile(scriptNamePrefix, ".bat")
prepareScriptFile(scriptExecutableFile)
new FileOutputStream(scriptExecutableFile).withCloseable { execOutputStream ->
execOutputStream.write(("@ECHO OFF\r\n" +
"java -jar " + uberJarFile.getAbsolutePath() + " %*\r\n").getBytes(StandardCharsets.US_ASCII))
}
} else {
scriptExecutableFile = File.createTempFile(scriptNamePrefix, ".sh")
prepareScriptFile(scriptExecutableFile)
new FileOutputStream(scriptExecutableFile).withCloseable { execOutputStream ->
execOutputStream.write(("#!/bin/sh\n" +
"exec java -jar " + uberJarFile.getAbsolutePath() + " \"\$@\"\n").getBytes(StandardCharsets.US_ASCII))
}
}
finalizeOutputFile(scriptExecutableFile)
} catch (Exception e) {
throw new IllegalStateException("servicetalk-grpc-gradle plugin failed to create executable script file which executes the protoc jar plugin.", e)
}

project.configure(project) {
Task ideaTask = extension.generateIdeConfiguration ? project.tasks.findByName("ideaModule") : null
Task eclipseTask = extension.generateIdeConfiguration ? project.tasks.findByName("eclipse") : null
@@ -78,12 +117,7 @@ class ServiceTalkGrpcPlugin implements Plugin<Project> {

plugins {
servicetalk_grpc {
if (serviceTalkProtocPluginPath) {
path = file(serviceTalkProtocPluginPath)
} else {
artifact = "io.servicetalk:servicetalk-grpc-protoc:$serviceTalkVersion@" +
(org.gradle.internal.os.OperatingSystem.current().isWindows() ? "bat" : "sh")
}
path = scriptExecutableFile
}
}

@@ -166,4 +200,22 @@ class ServiceTalkGrpcPlugin implements Plugin<Project> {
}
}
}

private static void prepareScriptFile(File outputFile) throws IOException {
if (!outputFile.exists()) {
if (!outputFile.getParentFile().isDirectory() && !outputFile.getParentFile().mkdirs()) {
throw new IOException("unable to make directories for file: " + outputFile.getCanonicalPath())
}
} else {
// Clear the file's contents
new PrintWriter(outputFile).close()
}
}

private static void finalizeOutputFile(File outputFile) throws IOException {
if (!outputFile.setExecutable(true)) {
outputFile.delete()
throw new IOException("unable to set file as executable: " + outputFile.getCanonicalPath())
}
}
}
@@ -54,9 +54,9 @@ serviceTalkGrpc {

// The following setting must be omitted in users projects and is necessary here
// only because we want to use the locally built version of the plugin
serviceTalkProtocPluginPath = "${project.rootProject.rootDir}/servicetalk-grpc-protoc/build/buildExecutable/" +
io.servicetalk.internal.build.ExecutableBuilder.addExecutablePostFix("protoc-gen-servicetalk_grpc",
org.gradle.internal.os.OperatingSystem.current().isWindows())
serviceTalkProtocPluginPath =
"${project.rootProject.rootDir}/servicetalk-grpc-protoc/build/buildExecutable/servicetalk-grpc-protoc-" +
project.version + "-all.jar"
}

afterEvaluate {
@@ -14,8 +14,6 @@
* limitations under the License.
*/

import static io.servicetalk.internal.build.ExecutableBuilder.*

buildscript {
dependencies {
classpath "com.github.jengelman.gradle.plugins:shadow:$shadowPluginVersion"
@@ -24,6 +22,7 @@ buildscript {
}

apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library"
apply plugin: "io.servicetalk.servicetalk-grpc-gradle-plugin"
apply plugin: "com.github.johnrengelman.shadow"
apply plugin: "com.google.protobuf"

@@ -52,7 +51,7 @@ shadowJar {

def grpcPluginUberJarName = project.name + "-" + project.version + "-all.jar"

task copyUberJarForDevelopment(type: Copy) {
task buildExecutable(type: Copy) {
dependsOn tasks.shadowJar
from shadowJar.outputs.files.singleFile
into file("$buildDir/buildExecutable")
@@ -61,94 +60,32 @@ task copyUberJarForDevelopment(type: Copy) {
return grpcPluginUberJarName
}
}

task buildExecutable {
def isWindows = org.gradle.internal.os.OperatingSystem.current().isWindows()
def outputFile = new File("$buildDir/buildExecutable/" +
addExecutablePostFix("protoc-gen-servicetalk_grpc", isWindows))
dependsOn tasks.copyUberJarForDevelopment
inputs.files shadowJar.outputs.files
outputs.file outputFile

doLast {
if (isWindows) {
buildWindowsExecutable(grpcPluginUberJarName, outputFile)
} else {
buildUnixExecutable(grpcPluginUberJarName, outputFile)
}
}
}
tasks.compileJava.finalizedBy(buildExecutable)

task buildExecutableWindowsPublishing {
def outputFile = new File("$buildDir/buildExecutable/" +
addExecutablePostFix("protoc-gen-servicetalk-windows_grpc", true))
dependsOn tasks.copyUberJarForDevelopment
inputs.files shadowJar.outputs.files
outputs.file outputFile

doLast {
buildWindowsExecutable(grpcPluginUberJarName, outputFile)
}
}

// we attempt to generate both grpc executables when on windows, and don't publish from windows anyways.
tasks.withType(PublishToMavenRepository) {
onlyIf {
!org.gradle.internal.os.OperatingSystem.current().isWindows()
}
}

publishing {
publications {
mavenJava {
artifact(buildExecutable.outputs.files.singleFile) {
classifier = "linux-x86_64"
extension = "sh"
builtBy buildExecutable
}
artifact(buildExecutable.outputs.files.singleFile) {
classifier = "osx-x86_64"
extension = "sh"
builtBy buildExecutable
}
artifact(buildExecutableWindowsPublishing.outputs.files.singleFile) {
classifier = "windows-x86_64"
extension = "bat"
builtBy buildExecutableWindowsPublishing
}
artifact(shadowJar.outputs.files.singleFile) {
classifier = "all"
extension = "jar"
builtBy buildExecutableWindowsPublishing
}
}
}
}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:$protobufVersion"
}
serviceTalkGrpc {
protobufVersion = project.property("protobufVersion")

plugins {
servicetalk_grpc {
path = "$buildDir/buildExecutable/" +
addExecutablePostFix("protoc-gen-servicetalk_grpc",
org.gradle.internal.os.OperatingSystem.current().isWindows())
}
}
// The following setting must be omitted in users projects and is necessary here
// only because we want to use the locally built version of the plugin
serviceTalkProtocPluginPath =
"${project.rootProject.rootDir}/servicetalk-grpc-protoc/build/buildExecutable/servicetalk-grpc-protoc-" +
project.version + "-all.jar"
}

// We validate that our protoc plugin outputs valid code by generating test classes which are compiled by Gradle
generateProtoTasks {
ofSourceSet("test").each { task ->
task.plugins {
servicetalk_grpc {
outputSubDir = "java"
}
}
}
}
afterEvaluate {
// break the circular dependency (compileJava->generateProto->buildExecutable->compileJava).
generateProto.enabled = false
}

clean {

0 comments on commit 4a85c39

Please sign in to comment.
You can’t perform that action at this time.