plugins { id 'java' // changes the behavior of TARGET_JVM_VERSION_ATTRIBUTE id "com.google.osdetector" apply false id "me.champeau.gradle.japicmp" apply false id "net.ltgt.errorprone" apply false id 'com.google.cloud.tools.jib' apply false } import net.ltgt.gradle.errorprone.CheckSeverity import org.gradle.util.GUtil subprojects { apply plugin: "checkstyle" apply plugin: "idea" apply plugin: "signing" apply plugin: "jacoco" apply plugin: "com.google.osdetector" apply plugin: "net.ltgt.errorprone" group = "io.grpc" version = "1.56.1-SNAPSHOT" // CURRENT_GRPC_VERSION repositories { maven { // The google mirror is less flaky than mavenCentral() url "https://maven-central.storage-download.googleapis.com/maven2/" } mavenCentral() mavenLocal() } tasks.withType(JavaCompile).configureEach { it.options.compilerArgs += [ "-Xlint:all", "-Xlint:-options", "-Xlint:-path", "-Xlint:-try" ] it.options.encoding = "UTF-8" if (rootProject.hasProperty('failOnWarnings') && rootProject.failOnWarnings.toBoolean()) { it.options.compilerArgs += ["-Werror"] } } tasks.withType(GenerateModuleMetadata).configureEach { // Module metadata, introduced in Gradle 6.0, conflicts with our publishing task for // grpc-alts and grpc-compiler. enabled = false } def isAndroid = project.name in [ 'grpc-android', 'grpc-android-interop-testing', 'grpc-cronet'] ext { def exeSuffix = osdetector.os == 'windows' ? ".exe" : "" protocPluginBaseName = 'protoc-gen-grpc-java' javaPluginPath = "$rootDir/compiler/build/exe/java_plugin/$protocPluginBaseName$exeSuffix" configureProtoCompilation = { String generatedSourcePath = "${projectDir}/src/generated" project.protobuf { protoc { if (project.hasProperty('protoc')) { path = project.protoc } else { artifact = libs.protobuf.protoc.get() } } generateProtoTasks { all().each { task -> // Recompile protos when build.gradle has been changed, because // it's possible the version of protoc has been changed. task.inputs.file("${rootProject.projectDir}/build.gradle") .withPathSensitivity(PathSensitivity.RELATIVE) .withPropertyName('root build.gradle') if (isAndroid) { task.builtins { java { option 'lite' } } } } } } if (rootProject.childProjects.containsKey('grpc-compiler')) { // Only when the codegen is built along with the project, will we be able to run // the grpc code generator. def syncGeneratedSources = tasks.register("syncGeneratedSources") { } project.protobuf { plugins { grpc { path = javaPluginPath } } generateProtoTasks { all().each { task -> String variantOrSourceSet = isAndroid ? task.variant.name : task.sourceSet.name def syncTask = project.tasks.register("syncGeneratedSources${variantOrSourceSet}", Sync) { from task into "$generatedSourcePath/$variantOrSourceSet" include "grpc/" } syncGeneratedSources.configure { dependsOn syncTask } task.configure { dependsOn ':grpc-compiler:java_pluginExecutable' // Recompile protos when the codegen has been changed inputs.file javaPluginPath plugins { grpc { option 'noversion' } } if (isAndroid) { plugins { grpc { option 'lite' } } } } } } } // Re-sync as part of a normal build, to avoid forgetting to run the sync tasks.named("assemble").configure { dependsOn syncGeneratedSources } } else { // Otherwise, we just use the checked-in generated code. if (isAndroid) { project.android.sourceSets { debug { java { srcDir "${generatedSourcePath}/debug/grpc" } } release { java { srcDir "${generatedSourcePath}/release/grpc" } } } } else { project.sourceSets.each() { sourceSet -> sourceSet.java { srcDir "${generatedSourcePath}/${sourceSet.name}/grpc" } } } } tasks.withType(JavaCompile).configureEach { appendToProperty( it.options.errorprone.excludedPaths, ".*/src/generated/[^/]+/java/.*" + "|.*/build/generated/source/proto/[^/]+/java/.*", "|") } } libraries = libs appendToProperty = { Property property, String value, String separator -> if (property.present) { property.set(property.get() + separator + value) } else { property.set(value) } } } // Disable JavaDoc doclint on Java 8. It's annoying. if (JavaVersion.current().isJava8Compatible()) { allprojects { tasks.withType(Javadoc).configureEach { options.addStringOption('Xdoclint:none', '-quiet') } } } checkstyle { configDirectory = file("$rootDir/buildscripts") toolVersion = libs.checkstyle.get().version ignoreFailures = false if (rootProject.hasProperty("checkstyle.ignoreFailures")) { ignoreFailures = rootProject.properties["checkstyle.ignoreFailures"].toBoolean() } } if (!project.hasProperty('errorProne') || errorProne.toBoolean()) { dependencies { errorprone JavaVersion.current().isJava11Compatible() ? libs.errorprone.core : libs.errorprone.corejava8 } } else { // Disable Error Prone tasks.withType(JavaCompile).configureEach { options.errorprone.enabled = false } } plugins.withId("java") { sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { testImplementation libraries.junit, libraries.mockito.core, libraries.truth } tasks.named("compileTestJava").configure { // serialVersionUID is basically guaranteed to be useless in our tests options.compilerArgs += [ "-Xlint:-serial" ] } tasks.named("jar").configure { manifest { attributes('Implementation-Title': name, 'Implementation-Version': project.version) } } tasks.named("javadoc").configure { options { encoding = 'UTF-8' use = true links 'https://docs.oracle.com/javase/8/docs/api/' source = "8" } } tasks.named("checkstyleMain").configure { source = fileTree(dir: "$projectDir/src/main", include: "**/*.java") } tasks.named("checkstyleTest").configure { source = fileTree(dir: "$projectDir/src/test", include: "**/*.java") } // At a test failure, log the stack trace to the console so that we don't // have to open the HTML in a browser. tasks.named("test").configure { testLogging { exceptionFormat = 'full' showExceptions true showCauses true showStackTraces true } maxHeapSize = '1500m' } if (!project.hasProperty('errorProne') || errorProne.toBoolean()) { dependencies { annotationProcessor libs.guava.betaChecker } } tasks.named("compileJava").configure { // This project targets Java 7 (no time.Duration class) options.errorprone.check("PreferJavaTimeOverload", CheckSeverity.OFF) options.errorprone.check("JavaUtilDate", CheckSeverity.OFF) // The warning fails to provide a source location options.errorprone.check("MissingSummary", CheckSeverity.OFF) } tasks.named("compileTestJava").configure { // LinkedList doesn't hurt much in tests and has lots of usages options.errorprone.check("JdkObsolete", CheckSeverity.OFF) options.errorprone.check("PreferJavaTimeOverload", CheckSeverity.OFF) options.errorprone.check("JavaUtilDate", CheckSeverity.OFF) } plugins.withId("ru.vyarus.animalsniffer") { // Only available after java plugin has loaded animalsniffer { toolVersion = libs.animalsniffer.asProvider().get().version } } } plugins.withId("java-library") { // Detect Maven Enforcer's dependencyConvergence failures. We only care // for artifacts used as libraries by others with Maven. tasks.register('checkUpperBoundDeps') { inputs.files(configurations.runtimeClasspath).withNormalizer(ClasspathNormalizer) outputs.file("${buildDir}/tmp/${name}") // Fake output for UP-TO-DATE checking doLast { requireUpperBoundDepsMatch(configurations.runtimeClasspath, project) } } tasks.named('compileJava').configure { dependsOn checkUpperBoundDeps } } plugins.withId("me.champeau.jmh") { // invoke jmh on a single benchmark class like so: // ./gradlew -PjmhIncludeSingleClass=StatsTraceContextBenchmark clean :grpc-core:jmh tasks.named("compileJmhJava").configure { sourceCompatibility = 1.8 targetCompatibility = 1.8 } tasks.named("jmh").configure { warmupIterations = 10 iterations = 10 fork = 1 // None of our benchmarks need the tests, and we have pseudo-circular // dependencies that break when including them. (context's testCompile // depends on core; core's testCompile depends on testing) includeTests = false if (project.hasProperty('jmhIncludeSingleClass')) { includes = [ project.property('jmhIncludeSingleClass') ] } } } plugins.withId("com.github.johnrengelman.shadow") { tasks.named("shadowJar").configure { // Do a dance to remove Class-Path. This needs to run after the doFirst() from the // shadow plugin that adds Class-Path and before the core jar action. Using doFirst will // have this run before the shadow plugin, and doLast will run after the core jar // action. See #8606. // The shadow plugin adds another doFirst when application is used for setting // Main-Class. Ordering with it doesn't matter. actions.add(plugins.hasPlugin("application") ? 2 : 1, new Action() { @Override public void execute(Task task) { if (!task.manifest.attributes.remove("Class-Path")) { throw new AssertionError("Did not find Class-Path to remove from manifest") } } }) } } plugins.withId("maven-publish") { publishing { publications { // do not use mavenJava, as java plugin will modify it via "magic" maven(MavenPublication) { pom { name = project.group + ":" + project.name url = 'https://github.com/grpc/grpc-java' afterEvaluate { // description is not available until evaluated. description = project.description } scm { connection = 'scm:git:https://github.com/grpc/grpc-java.git' developerConnection = 'scm:git:git@github.com:grpc/grpc-java.git' url = 'https://github.com/grpc/grpc-java' } licenses { license { name = 'Apache 2.0' url = 'https://opensource.org/licenses/Apache-2.0' } } developers { developer { id = "grpc.io" name = "gRPC Contributors" email = "grpc-io@googlegroups.com" url = "https://grpc.io/" organization = "gRPC Authors" organizationUrl = "https://www.google.com" } } } } } repositories { maven { if (rootProject.hasProperty('repositoryDir')) { url = new File(rootProject.repositoryDir).toURI() } else { String stagingUrl if (rootProject.hasProperty('repositoryId')) { stagingUrl = 'https://oss.sonatype.org/service/local/staging/deployByRepositoryId/' + rootProject.repositoryId } else { stagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' } credentials { if (rootProject.hasProperty('ossrhUsername') && rootProject.hasProperty('ossrhPassword')) { username = rootProject.ossrhUsername password = rootProject.ossrhPassword } } def releaseUrl = stagingUrl def snapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' url = version.endsWith('SNAPSHOT') ? snapshotUrl : releaseUrl } } } } signing { required false sign publishing.publications.maven } plugins.withId("java") { java { withJavadocJar() withSourcesJar() } publishing { publications { maven { if (project.name != 'grpc-netty-shaded') { from components.java } } } } } } // Run with: ./gradlew japicmp --continue plugins.withId("me.champeau.gradle.japicmp") { def baselineGrpcVersion = '1.6.1' // Get the baseline version's jar for this subproject File baselineArtifact = null // Use a detached configuration, otherwise the current version's jar will take precedence // over the baseline jar. // A necessary hack, the intuitive thing does NOT work: // https://discuss.gradle.org/t/is-the-default-configuration-leaking-into-independent-configurations/2088/6 def oldGroup = project.group try { project.group = 'virtual_group_for_japicmp' String depModule = "io.grpc:${project.name}:${baselineGrpcVersion}@jar" String depJar = "${project.name}-${baselineGrpcVersion}.jar" Configuration configuration = configurations.detachedConfiguration( dependencies.create(depModule) ) baselineArtifact = files(configuration.files).filter { it.name.equals(depJar) }.singleFile } finally { project.group = oldGroup } // Add a japicmp task that compares the current .jar with baseline .jar tasks.register("japicmp", me.champeau.gradle.japicmp.JapicmpTask) { dependsOn jar oldClasspath = files(baselineArtifact) newClasspath = files(jar.archiveFile) onlyBinaryIncompatibleModified = false // Be quiet about things that did not change onlyModified = true // This task should fail if there are incompatible changes failOnModification = true ignoreMissingClasses = true htmlOutputFile = file("$buildDir/reports/japi.html") packageExcludes = ['io.grpc.internal'] // Also break on source incompatible changes, not just binary. // Eg adding abstract method to public class. // TODO(zpencer): enable after japicmp-gradle-plugin/pull/14 // breakOnSourceIncompatibility = true // Ignore any classes or methods marked @ExperimentalApi // TODO(zpencer): enable after japicmp-gradle-plugin/pull/15 // annotationExcludes = ['@io.grpc.ExperimentalApi'] } } } class DepAndParents { DependencyResult dep List parents } /** * Make sure that Maven would select the same versions as Gradle selected. * This is essentially the same as if we used Maven Enforcer's * requireUpperBoundDeps for our artifacts. */ def requireUpperBoundDepsMatch(Configuration conf, Project project) { // artifact name => version Map golden = conf.resolvedConfiguration.resolvedArtifacts.collectEntries { ResolvedArtifact it -> ModuleVersionIdentifier id = it.moduleVersion.id [id.group + ":" + id.name, id.version] } // Breadth-first search like Maven for dependency resolution Queue queue = new ArrayDeque<>() conf.incoming.resolutionResult.root.dependencies.each { queue.add(new DepAndParents(dep: it, parents: [project.displayName])) } Set found = new HashSet<>() while (!queue.isEmpty()) { DepAndParents depAndParents = queue.remove() ResolvedDependencyResult result = (ResolvedDependencyResult) depAndParents.dep ModuleVersionIdentifier id = result.selected.moduleVersion String artifact = id.group + ":" + id.name if (found.contains(artifact)) continue found.add(artifact) String version if (result.requested instanceof ProjectComponentSelector) { ProjectComponentSelector selector = (ProjectComponentSelector) result.requested version = project.findProject(selector.projectPath).version } else { version = ((ModuleComponentSelector) result.requested).version } String goldenVersion = golden[artifact] if (goldenVersion != version && "[$goldenVersion]" != version) { throw new RuntimeException( "Maven version skew: $artifact ($version != $goldenVersion) " + "Bad version dependency path: " + depAndParents.parents + " Run './gradlew $project.path:dependencies --configuration $conf.name' " + "to diagnose") } result.selected.dependencies.each { queue.add(new DepAndParents( dep: it, parents: depAndParents.parents + [artifact + ":" + version])) } } } repositories { mavenCentral() google() } def isAcceptableVersion(ModuleComponentIdentifier candidate) { String group = candidate.group String module = candidate.module String version = candidate.version if (group == 'com.google.guava') return true if (group == 'io.netty' && version.contains('Final')) return true if (module == 'android-api-level-19') return true return version ==~ /^[0-9]+(\.[0-9]+)+$/ } configurations { checkForUpdates { attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 8) resolutionStrategy { componentSelection { all { if (!isAcceptableVersion(it.candidate)) it.reject("Not stable version") } } } } } // Checks every dependency in the version catalog to see if there is a newer // version available. The 'checkForUpdates' configuration restricts the // versions considered. tasks.register('checkForUpdates') { doLast { def updateConf = project.configurations.checkForUpdates updateConf.setVisible(false) updateConf.setTransitive(false) def versionCatalog = project.extensions.getByType(VersionCatalogsExtension).named("libs") versionCatalog.libraryAliases.each { name -> def dep = versionCatalog.findLibrary(name).get().get() def oldConf = updateConf.copy() def oldDep = project.dependencies.create( group: dep.group, name: dep.name, version: dep.versionConstraint, classifier: 'pom') oldConf.dependencies.add(oldDep) def oldResolved = oldConf.resolvedConfiguration.resolvedArtifacts.iterator().next() def newConf = updateConf.copy() def newDep = project.dependencies.create( group: dep.group, name: dep.name, version: '+', classifier: 'pom') newConf.dependencies.add(newDep) def newResolved = newConf.resolvedConfiguration.resolvedArtifacts.iterator().next() if (oldResolved != newResolved) { def oldId = oldResolved.id.componentIdentifier def newId = newResolved.id.componentIdentifier println("${newId.group}:${newId.module} ${oldId.version} -> ${newId.version}") } } } }