From 5cceef5fda613836d3c2e43d8948276ea90c2009 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Wed, 25 Mar 2020 16:23:59 -0700 Subject: [PATCH] gRPC gradle plugin rework Motivation: 0ebe48ed3948f22bfc003c8a0c551572bb70bbbf 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. --- buildSrc/build.gradle | 1 - .../internal/build/ExecutableBuilder.java | 74 --------------- .../grpc/helloworld/build.gradle | 6 +- .../grpc/routeguide/build.gradle | 6 +- .../internal/ServiceTalkCorePlugin.groovy | 5 -- .../plugin/ServiceTalkGrpcPlugin.groovy | 82 +++++++++++++---- servicetalk-grpc-netty/build.gradle | 6 +- servicetalk-grpc-protoc/build.gradle | 89 +++---------------- 8 files changed, 89 insertions(+), 180 deletions(-) delete mode 100644 buildSrc/src/main/java/io/servicetalk/internal/build/ExecutableBuilder.java diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 56fd70656d..99b66fcec0 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -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" diff --git a/buildSrc/src/main/java/io/servicetalk/internal/build/ExecutableBuilder.java b/buildSrc/src/main/java/io/servicetalk/internal/build/ExecutableBuilder.java deleted file mode 100644 index a743cef5d4..0000000000 --- a/buildSrc/src/main/java/io/servicetalk/internal/build/ExecutableBuilder.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright © 2020 Apple Inc. and the ServiceTalk project authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.servicetalk.internal.build; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; - -public final class ExecutableBuilder { - - private ExecutableBuilder() { - // no instances - } - - public static void buildUnixExecutable(String uberJarName, File outputFile) throws IOException { - prepareOutputFile(outputFile); - try(FileOutputStream execOutputStream = new FileOutputStream(outputFile)) { - execOutputStream.write(("#!/bin/sh\n" + - "PWD_BEFORE=$PWD\n"+ - "cd $(dirname \"$0\")\n" + - "exec java -jar " + uberJarName + " \"$@\"\n" + - "cd $PWD_BEFORE\n").getBytes(StandardCharsets.US_ASCII)); - } - finalizeOutputFile(outputFile); - } - - public static void buildWindowsExecutable(String uberJarName, File outputFile) throws IOException { - prepareOutputFile(outputFile); - try(FileOutputStream execOutputStream = new FileOutputStream(outputFile)) { - execOutputStream.write(("@ECHO OFF\r\n" + - "pushd %~dp0\r\n" + - "java -jar " + uberJarName + " %*\r\n" + - "popd\r\n").getBytes(StandardCharsets.US_ASCII)); - } - finalizeOutputFile(outputFile); - } - - public static String addExecutablePostFix(String rawName, boolean isWindows) { - return rawName + (isWindows ? ".bat" : ".sh"); - } - - private static void prepareOutputFile(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()); - } - } -} diff --git a/servicetalk-examples/grpc/helloworld/build.gradle b/servicetalk-examples/grpc/helloworld/build.gradle index 8c0b970d07..031f0184ec 100644 --- a/servicetalk-examples/grpc/helloworld/build.gradle +++ b/servicetalk-examples/grpc/helloworld/build.gradle @@ -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" } diff --git a/servicetalk-examples/grpc/routeguide/build.gradle b/servicetalk-examples/grpc/routeguide/build.gradle index 45dc334990..e4ba74eae6 100644 --- a/servicetalk-examples/grpc/routeguide/build.gradle +++ b/servicetalk-examples/grpc/routeguide/build.gradle @@ -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" } diff --git a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkCorePlugin.groovy b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkCorePlugin.groovy index 58a5ccc2a0..a0cfcd2aa3 100644 --- a/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkCorePlugin.groovy +++ b/servicetalk-gradle-plugin-internal/src/main/groovy/io/servicetalk/gradle/plugin/internal/ServiceTalkCorePlugin.groovy @@ -126,11 +126,6 @@ class ServiceTalkCorePlugin implements Plugin { 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"]) - } } } } diff --git a/servicetalk-grpc-gradle-plugin/src/main/groovy/io/servicetalk/grpc/gradle/plugin/ServiceTalkGrpcPlugin.groovy b/servicetalk-grpc-gradle-plugin/src/main/groovy/io/servicetalk/grpc/gradle/plugin/ServiceTalkGrpcPlugin.groovy index cbf26929ca..49b57ccc85 100644 --- a/servicetalk-grpc-gradle-plugin/src/main/groovy/io/servicetalk/grpc/gradle/plugin/ServiceTalkGrpcPlugin.groovy +++ b/servicetalk-grpc-gradle-plugin/src/main/groovy/io/servicetalk/grpc/gradle/plugin/ServiceTalkGrpcPlugin.groovy @@ -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 { void apply(Project project) { if (GradleVersion.current().baseVersion < GradleVersion.version("4.10")) { @@ -40,21 +42,15 @@ class ServiceTalkGrpcPlugin implements Plugin { 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 { 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 { 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 { } } } + + 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()) + } + } } diff --git a/servicetalk-grpc-netty/build.gradle b/servicetalk-grpc-netty/build.gradle index 38a4c16480..21d583155e 100644 --- a/servicetalk-grpc-netty/build.gradle +++ b/servicetalk-grpc-netty/build.gradle @@ -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 { diff --git a/servicetalk-grpc-protoc/build.gradle b/servicetalk-grpc-protoc/build.gradle index 29cdc2d479..e60d8a80ea 100644 --- a/servicetalk-grpc-protoc/build.gradle +++ b/servicetalk-grpc-protoc/build.gradle @@ -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 {