// Copyright 2019 The Nomulus Authors. All Rights Reserved. // // 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. import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent buildscript { if (rootProject.enableDependencyLocking.toBoolean()) { // Lock buildscript dependencies. configurations.classpath { resolutionStrategy.activateDependencyLocking() // log4j has high-profile security vulnerabilities. It's a transitive // dependency used by Gradle itself during build, and not strictly needed. exclude group: 'org.apache.logging.log4j' } } dependencies { classpath 'com.google.cloud.tools:appengine-gradle-plugin:2.4.1' classpath 'net.ltgt.gradle:gradle-errorprone-plugin:2.0.2' classpath 'org.sonatype.aether:aether-api:1.13.1' classpath 'org.sonatype.aether:aether-impl:1.13.1' } } plugins { // Java static analysis plugins. Keep versions consistent with // ./buildSrc/build.gradle id 'nebula.lint' version '16.0.2' id 'net.ltgt.errorprone' version '2.0.2' id 'checkstyle' id 'com.github.johnrengelman.shadow' version '5.1.0' // NodeJs plugin id "com.github.node-gradle.node" version "3.0.1" id 'idea' id 'com.diffplug.gradle.spotless' version '3.25.0' id 'jacoco' id 'com.dorongold.task-tree' version '2.1.0' } dependencyLocking { lockAllConfigurations() } node { download = true version = "16.14.0" npmVersion = "6.14.11" } wrapper { distributionType = Wrapper.DistributionType.ALL } apply plugin: google.registry.gradle.plugin.ReportUploaderPlugin reportUploader { // Set the location where we want to upload the build results. // e.g. -P uploaderDestination=gcs://domain-registry-alpha-build-result-test // // If not set - the upload will be skipped destination = uploaderDestination // The location of the file containing the OAuth2 Google Cloud credentials. // // The file can contain a Service Account key file in JSON format from the // Google Developers Console or a stored user credential using the format // supported by the Cloud SDK. // // If no file is given - the default credentials are used. credentialsFile = uploaderCredentialsFile // If set to 'yes', each file will be uploaded to GCS in a separate thread. // This is MUCH faster. multithreadedUpload = uploaderMultithreadedUpload } apply from: 'dependencies.gradle' apply from: 'dependency_lic.gradle' apply from: 'utils.gradle' // Custom task to run checkLicense in buildSrc, which is not triggered // by root project tasks. A shell task is used because buildSrc tasks // cannot be referenced in the same way as tasks from a regular included // build. task checkBuildSrcLicense(type:Exec) { workingDir "${rootDir}/buildSrc" commandLine '../gradlew', 'checkLicense' } tasks.checkLicense.dependsOn(tasks.checkBuildSrcLicense) tasks.build.dependsOn(tasks.checkLicense) // Provide defaults for all of the project properties. // Only do linting if the build is successful. gradleLint.autoLintAfterFailure = false // Paths to main and test sources. ext.projectRootDir = "${rootDir}" // Tasks to deploy/stage all App Engine services task deploy { group = 'deployment' description = 'Deploys all services to App Engine.' } task stage { group = 'deployment' description = 'Generates application directories for all services.' } // App-engine environment configuration. We set up all of the variables in // the root project. def environments = ['production', 'sandbox', 'alpha', 'crash', 'qa'] def gcpProject = null apply from: "${rootDir.path}/projects.gradle" if (environment == '') { // Keep the project null, this will prevent deployment. Set the // environment to "alpha" because other code needs this property to // explode the war file. environment = 'alpha' } else if (environment != 'production' && environment != 'sandbox') { gcpProject = projects[environment] if (gcpProject == null) { throw new GradleException("-Penvironment must be one of " + "${projects.keySet()}.") } } rootProject.ext.environment = environment rootProject.ext.gcpProject = gcpProject rootProject.ext.prodOrSandboxEnv = environment in ['production', 'sandbox'] // Function to verify that the deployment parameters have been set. def verifyDeploymentParams() { if (prodOrSandboxEnv) { // Do not deploy to prod or sandbox. Print a prominent error in bright red. System.err.println('\033[31;1m-----------------------------------------------------------------') System.err.println('*** DANGER WILL ROBINSON!') System.err.println('*** You may not deploy to production or sandbox from gradle. Do a') System.err.println('*** release from Spinnaker, see deployment playbook.') System.err.println('-----------------------------------------------------------------') throw new GradleException('Aborting. See prominent error above.') } else if (gcpProject == null) { def error = 'You must specify -Penvironment={alpha,crash,qa}' System.err.println("\033[33;1m${error}\033[0m") throw GradleException("Aborting: ${error}") } } // Closure that we can just drop into all of our deployment tasks. rootProject.ext.verifyDeploymentConfig = { doFirst { verifyDeploymentParams() } } // Subproject configuration. // Alias this since it collides with the closure variable name def allowInsecure = allowInsecureProtocol allprojects { // Skip no-op project if (project.name == 'services') return repositories { if (!mavenUrl.isEmpty()) { maven { println "Java dependencies: Using repo ${mavenUrl}..." url mavenUrl allowInsecureProtocol = allowInsecure == "true" } } else { println "Java dependencies: Using Maven Central..." mavenCentral() google() maven { url "https://packages.confluent.io/maven/" content { includeGroup "io.confluent" } } } } if (rootProject.enableCrossReferencing.toBoolean()) { gradle.projectsEvaluated { tasks.withType(JavaCompile) { options.fork = true options.forkOptions.javaHome = file("${System.env.REAL_JAVA_HOME}") } } } } rootProject.ext { pyver = { exe -> try { ext.execInBash( exe + " -c 'import sys; print(sys.hexversion)' 2>/dev/null", "/") as Integer } catch (org.gradle.process.internal.ExecException e) { return -1; } } // Return the path to a usable python3 executable. getPythonExecutable = { // Find a python version greater than 3.7.3 (this is somewhat arbitrary, we // know we'd like at least 3.6, but 3.7.3 is the latest that ships with // Debian so it seems like that should be available anywhere). def MIN_PY_VER = 0x3070300 if (pyver('python') >= MIN_PY_VER) { return 'python' } else if (pyver('/usr/bin/python3') >= MIN_PY_VER) { return '/usr/bin/python3' } else { throw new GradleException("No usable Python version found (build " + "requires at least python 3.7.3)"); } } } task runPresubmits(type: Exec) { args('config/presubmits.py') doFirst { executable getPythonExecutable() } } def javadocSource = [] def javadocClasspath = [] def javadocDependentTasks = [] subprojects { // Skip no-op project if (project.name == 'services') return ext.createUberJar = { taskName, binaryName, mainClass, List configs = [project.configurations.runtimeClasspath], List srcOutput = [project.sourceSets.main.output], List excludes = [] -> project.tasks.create( taskName, com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { mergeServiceFiles() baseName = binaryName if (mainClass != '') { manifest { attributes 'Main-Class': mainClass } } zip64 = true classifier = '' archiveVersion = '' configurations = configs from srcOutput // Excludes signature files that accompany some dependency jars, like // bonuncycastle. If they are present, only classes from those signed jars are // made available to the class loader. // see https://discuss.gradle.org/t/signing-a-custom-gradle-plugin-thats-downloaded-by-the-build-system-from-github/1365 exclude "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA" exclude excludes // We do seem to get duplicates when constructing uber-jars, either // this is a product of something in gradle 7 or a product of gradle 7 // now giving an error about them when it didn't previously. duplicatesStrategy DuplicatesStrategy.WARN } } if (rootProject.enableDependencyLocking.toBoolean()) { buildscript { // Lock buildscript dependencies. configurations.classpath { resolutionStrategy.activateDependencyLocking() } } } afterEvaluate { if (rootProject.enableDependencyLocking.toBoolean() && project.name != 'integration' && project.name != 'java8compatibility') { // The ':integration' project runs server/schema integration tests using // dynamically specified jars with no transitive dependency. Therefore // dependency-locking does not make sense. Furthermore, during // evaluation it resolves the 'testRuntimeOnly' configuration, making it // immutable. Locking activation would trigger an invalid operation // exception. // // The ':java8compatibility' project is test-only. Its source does not go // into production. // // For all other projects, due to problem with the gradle-license-report // plugin, the dependencyLicenseReport configuration must opt out of // dependency-locking. See dependency_lic.gradle for the reason why. // // To selectively activate dependency locking without hardcoding them // in the 'configurations' block, the following code must run after // project evaluation, when all configurations have been created. configurations.each { if (it.name != 'dependencyLicenseReport' && it.name != 'integration') { it.resolutionStrategy.activateDependencyLocking() } } } } def services = [':services:default', ':services:backend', ':services:tools', ':services:pubapi'] // Set up all of the deployment projects. if (services.contains(project.path)) { apply from: "${rootDir.path}/appengine_war.gradle" // Return early, do not apply the settings below. return } apply from: "${rootDir.path}/java_common.gradle" if (project.name != 'docs') { compileJava { // TODO: Remove this once we migrate off AppEngine. options.release = 8 } } project.tasks.test.dependsOn runPresubmits def commonlyExcludedResources = ['**/*.java', '**/BUILD'] project.ext.javaDir = "${project.projectDir}/src/main/java" project.ext.javaTestDir = "${project.projectDir}/src/test/java" project.ext.resourcesSourceDir = "${project.projectDir}/src/main/resources" sourceSets { main { resources { srcDirs += project.ext.javaDir exclude commonlyExcludedResources } } test { resources { srcDirs += project.ext.javaTestDir exclude commonlyExcludedResources } } } // No need to produce javadoc for the docs subproject, which has no APIs to // expose to users. if (project.name != 'docs') { javadocSource << project.sourceSets.main.allJava javadocClasspath << project.sourceSets.main.runtimeClasspath javadocClasspath << "${buildDir}/generated/sources/annotationProcessor/java/main" javadocDependentTasks << project.tasks.compileJava } } // If "-P verboseTestOutput=true" is passed in, configure all subprojects to dump all of their // output and final test status (pass/fail, errors) for each test class. // // Note that we can't do this in the main subprojects section above because that's evaluated before // the subproject build files and the test tasks haven't been defined yet. We have to do it from // the projectsEvaluted hook, which gets called after the subprojects are configured. if (verboseTestOutput.toBoolean()) { gradle.projectsEvaluated({ subprojects { tasks.withType(Test) { testLogging { events TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.STANDARD_OUT, TestLogEvent.STANDARD_ERROR exceptionFormat TestExceptionFormat.FULL showExceptions true showCauses true showStackTraces true afterSuite { desc, result -> println "Results: ${result.resultType}, " + "${result.successfulTestCount}/${result.testCount} tests " + "passed, ${result.failedTestCount} failures."; } } } } }) } task checkDependenciesDotGradle { def buildSrcDepsFile = File.createTempFile('buildSrc', 'deps') buildSrcDepsFile.deleteOnExit() dependsOn createGetBuildSrcDirectDepsTask(buildSrcDepsFile) doLast { Set depsInUse = [] allprojects { configurations.all { it.dependencies.findAll { it.group != null }.each { // Note: .toString() is required since GString should // not be mixed with Java Strings. depsInUse.add("${it.group}:${it.name}".toString()) } } } if (buildSrcDepsFile.exists()) { depsInUse.addAll(buildSrcDepsFile.readLines()) } def unusedDeps = rootProject.dependencyMap.keySet() .findAll { !depsInUse.contains(it) } .toSorted() if (unusedDeps.isEmpty()) { return } logger.error( "Unused dependencies in dependencies.gradle:\n${unusedDeps.toListString()}") throw new IllegalStateException( "The dependencies.gradle file should only contain direct dependencies.") } } tasks.build.dependsOn(tasks.checkDependenciesDotGradle) def createGetBuildSrcDirectDepsTask(outputFileName) { return tasks .create( "getBuildSrcDeps_${java.util.UUID.randomUUID()}".toString(), Exec) { workingDir "${rootDir}/buildSrc" commandLine '../gradlew', 'exportDependencies', "-PdependencyExportFile=${outputFileName}" } } rootProject.ext { invokeJavaDiffFormatScript = { action -> println("JAVA_HOME=${System.env.JAVA_HOME}") println("PATH=${System.env.PATH}") println(ext.execInBash("type java", "${rootDir}")) println(ext.execInBash("java -version", "${rootDir}")) def scriptDir = rootDir.path.endsWith('buildSrc') ? "${rootDir}/../java-format" : "${rootDir}/java-format" def workingDir = rootDir.path.endsWith('buildSrc') ? "${rootDir}/.." : rootDir def formatDiffScript = "${scriptDir}/google-java-format-git-diff.sh" def pythonExe = getPythonExecutable() return ext.execInBash( "PYTHON=${pythonExe} ${formatDiffScript} ${action}", "${workingDir}") } } // Checks if modified lines in Java source files need reformatting. // Note that this task checks modified Java files in the entire repository. task javaIncrementalFormatCheck { doLast { // We can only do this in a git tree. if (new File("${rootDir}/.git").exists()) { def checkResult = invokeJavaDiffFormatScript("check") if (checkResult == 'true') { throw new IllegalStateException( "Some Java files need to be reformatted. You may use the " + "'javaIncrementalFormatDryRun' task to review\n " + "the changes, or the 'javaIncrementalFormatApply' task " + "to reformat.") } else if (checkResult != 'false') { throw new RuntimeException( "Failed to invoke format check script:\n" + checkResult) } println("Incremental Java format check ok.") } else { println("Omitting format check: not in a git directory.") } } } // Shows how modified lines in Java source files will change after formatting. // Note that this task checks modified Java files in the entire repository. task javaIncrementalFormatDryRun { doLast { println("${invokeJavaDiffFormatScript("show")}") } } tasks.build.dependsOn(tasks.javaIncrementalFormatCheck) // Checks if modified lines in Java source files need reformatting. // Note that this task processes modified Java files in the entire repository. task javaIncrementalFormatApply { doLast { invokeJavaDiffFormatScript("format") } } task javadoc(type: Javadoc) { source javadocSource // Java 11.0.17 has the following bug that affects annotation handling on // package-info.java: // https://bugs.openjdk.org/browse/JDK-8222091 exclude "**/package-info.java" classpath = files(javadocClasspath) destinationDir = file("${buildDir}/docs/javadoc") options.encoding = "UTF-8" // In a lot of places we don't write @return so suppress warnings about that. options.addBooleanOption('Xdoclint:all,-missing', true) options.addBooleanOption("-allow-script-in-comments",true) options.tags = ["type:a:Generic Type", "error:a:Expected Error", "invariant:a:Guaranteed Property"] } tasks.build.dependsOn(tasks.javadoc) // Task for doing development on core Nomulus. // This fixes code formatting automatically as necessary, builds and tests the // core Nomulus codebase, and runs all presubmits. task coreDev { dependsOn 'javaIncrementalFormatApply' dependsOn 'javadoc' dependsOn 'checkDependenciesDotGradle' dependsOn 'checkLicense' dependsOn ':console-webapp:runConsoleWebappUnitTests' dependsOn ':core:check' dependsOn 'assemble' } javadocDependentTasks.each { tasks.javadoc.dependsOn(it) } // Runs the script, which deploys cloud scheduler and tasks based on the config task deployCloudSchedulerAndQueue { doLast { def env = environment if (!prodOrSandboxEnv) { exec { commandLine 'go', 'run', "${rootDir}/release/builder/deployCloudSchedulerAndQueue.go", "${rootDir}/core/src/main/java/google/registry/env/${env}/default/WEB-INF/cloud-scheduler-tasks.xml", "domain-registry-${env}" } exec { commandLine 'go', 'run', "${rootDir}/release/builder/deployCloudSchedulerAndQueue.go", "${rootDir}/core/src/main/java/google/registry/env/common/default/WEB-INF/cloud-tasks-queue.xml", "domain-registry-${env}" } } } } // disable javadoc in subprojects, these will break because they don't have // the correct classpath (see above). gradle.taskGraph.whenReady { graph -> graph.getAllTasks().each { task -> def subprojectJavadoc = (task.path =~ /:.+:javadoc/) if (subprojectJavadoc) { println "Skipping ${task.path} for javadoc (only root javadoc works)" task.enabled = false } } }