Skip to content

Commit

Permalink
gRPC gradle plugin rework
Browse files Browse the repository at this point in the history
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 5cceef5
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 180 deletions.
1 change: 0 additions & 1 deletion buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

This file was deleted.

6 changes: 3 additions & 3 deletions servicetalk-examples/grpc/helloworld/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 3 additions & 3 deletions servicetalk-examples/grpc/routeguide/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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())
}
}
}
6 changes: 3 additions & 3 deletions servicetalk-grpc-netty/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
89 changes: 13 additions & 76 deletions servicetalk-grpc-protoc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand Down

0 comments on commit 5cceef5

Please sign in to comment.