diff --git a/e2e/gradle/gradle/libs.versions.toml b/e2e/gradle/gradle/libs.versions.toml deleted file mode 100644 index 4ac3234a6a7c3..0000000000000 --- a/e2e/gradle/gradle/libs.versions.toml +++ /dev/null @@ -1,2 +0,0 @@ -# This file was generated by the Gradle 'init' task. -# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format diff --git a/e2e/gradle/gradle/wrapper/gradle-wrapper.jar b/e2e/gradle/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e6441136f3d4b..0000000000000 Binary files a/e2e/gradle/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/e2e/gradle/src/gradle.test.ts b/e2e/gradle/src/gradle.test.ts index 856f77b43c592..44814f3779b96 100644 --- a/e2e/gradle/src/gradle.test.ts +++ b/e2e/gradle/src/gradle.test.ts @@ -6,6 +6,7 @@ import { runCLI, uniq, updateFile, + updateJson, } from '@nx/e2e/utils'; import { createGradleProject } from './utils/create-gradle-project'; @@ -30,9 +31,9 @@ describe('Gradle', () => { expect(projects).toContain(gradleProjectName); const buildOutput = runCLI('build app', { verbose: true }); - expect(buildOutput).toContain('nx run list:build'); + expect(buildOutput).toContain('nx run list:'); expect(buildOutput).toContain(':list:classes'); - expect(buildOutput).toContain('nx run utilities:build'); + expect(buildOutput).toContain('nx run utilities:'); expect(buildOutput).toContain(':utilities:classes'); checkFilesExist( @@ -82,8 +83,28 @@ dependencies { let buildOutput = runCLI('build app2', { verbose: true }); // app2 depends on app - expect(buildOutput).toContain('nx run app:build'); + expect(buildOutput).toContain('nx run app:'); expect(buildOutput).toContain(':app:classes'); + expect(buildOutput).toContain('nx run list:'); + expect(buildOutput).toContain(':list:classes'); + expect(buildOutput).toContain('nx run utilities:'); + expect(buildOutput).toContain(':utilities:classes'); + + checkFilesExist(`app2/build/libs/app2.jar`); + }); + + it('should run atomized test target', () => { + updateJson('nx.json', (json) => { + json.plugins.find((p) => p.plugin === '@nx/gradle').options[ + 'ciTargetName' + ] = 'test-ci'; + return json; + }); + + expect(() => { + runCLI('run app:test-ci--MessageUtilsTest', { verbose: true }); + runCLI('run list:test-ci--LinkedListTest', { verbose: true }); + }).not.toThrow(); }); } ); diff --git a/e2e/gradle/src/utils/create-gradle-project.ts b/e2e/gradle/src/utils/create-gradle-project.ts index 1e4755082e1b7..858aab94dbddb 100644 --- a/e2e/gradle/src/utils/create-gradle-project.ts +++ b/e2e/gradle/src/utils/create-gradle-project.ts @@ -5,6 +5,7 @@ import { tmpProjPath, } from '@nx/e2e/utils'; import { execSync } from 'child_process'; +import { readFileSync } from 'fs'; import { createFileSync, writeFileSync } from 'fs-extra'; import { join, resolve } from 'path'; @@ -15,14 +16,10 @@ export function createGradleProject( packageName: string = 'gradleProject', addProjectJsonNamePrefix: string = '' ) { - e2eConsoleLogger( - `Using java version: ${execSync('java -version')} ${execSync( - 'echo $JAVA_HOME' - )}` - ); + e2eConsoleLogger(`Using java version: ${execSync('java -version')}`); const gradleCommand = isWindows() - ? resolve(`${__dirname}/../../gradlew.bat`) - : resolve(`${__dirname}/../../gradlew`); + ? resolve(`${__dirname}/../../../../packages/gradle/native/gradlew.bat`) + : resolve(`${__dirname}/../../../../packages/gradle/native/gradlew`); e2eConsoleLogger( 'Using gradle version: ' + execSync(`${gradleCommand} --version`, { @@ -36,7 +33,7 @@ export function createGradleProject( ); e2eConsoleLogger( runCommand( - `${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package ${packageName} --no-incubating --split-project`, + `${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package ${packageName} --no-incubating --split-project --overwrite`, { cwd, } @@ -60,4 +57,37 @@ export function createGradleProject( `{"name": "${addProjectJsonNamePrefix}utilities"}` ); } + + addLocalPluginManagement( + join(cwd, `settings.gradle${type === 'kotlin' ? '.kts' : ''}`) + ); + addLocalPluginManagement( + join(cwd, `buildSrc/settings.gradle${type === 'kotlin' ? '.kts' : ''}`) + ); + // Disable configuration cache to avoid issues with the createNodes task + writeFileSync( + join(cwd, `gradle.properties`), + 'org.gradle.configuration-cache=false' + ); + + e2eConsoleLogger( + execSync(`${gradleCommand} publishToMavenLocal`, { + cwd: `${__dirname}/../../../../packages/gradle/native`, + }).toString() + ); +} + +function addLocalPluginManagement(filePath: string) { + let content = readFileSync(filePath).toString(); + content = + `pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + // Add other repositories if needed + } +} +` + content; + writeFileSync(filePath, content); } diff --git a/e2e/gradle/.gitattributes b/packages/gradle/native/.gitattributes similarity index 100% rename from e2e/gradle/.gitattributes rename to packages/gradle/native/.gitattributes diff --git a/packages/gradle/native/.gitignore b/packages/gradle/native/.gitignore new file mode 100644 index 0000000000000..1b6985c0094c8 --- /dev/null +++ b/packages/gradle/native/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/packages/gradle/native/gradle.properties b/packages/gradle/native/gradle.properties new file mode 100644 index 0000000000000..3c489f431e04d --- /dev/null +++ b/packages/gradle/native/gradle.properties @@ -0,0 +1,7 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=false +version=0.0.1 diff --git a/packages/gradle/native/gradle/libs.versions.toml b/packages/gradle/native/gradle/libs.versions.toml new file mode 100644 index 0000000000000..3b788e62d40b9 --- /dev/null +++ b/packages/gradle/native/gradle/libs.versions.toml @@ -0,0 +1,11 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[plugins] +jvm = { id = "org.jetbrains.kotlin.jvm", version = "1.9.20" } + +[versions] +kotlin-gradle-plugin = "2.0.21" + +[libraries] +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle-plugin" } diff --git a/packages/gradle/native/gradle/wrapper/gradle-wrapper.jar b/packages/gradle/native/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000..d64cd4917707c Binary files /dev/null and b/packages/gradle/native/gradle/wrapper/gradle-wrapper.jar differ diff --git a/e2e/gradle/gradle/wrapper/gradle-wrapper.properties b/packages/gradle/native/gradle/wrapper/gradle-wrapper.properties similarity index 74% rename from e2e/gradle/gradle/wrapper/gradle-wrapper.properties rename to packages/gradle/native/gradle/wrapper/gradle-wrapper.properties index a0777a32ceeb7..cea7a793a84b4 100644 --- a/e2e/gradle/gradle/wrapper/gradle-wrapper.properties +++ b/packages/gradle/native/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip -validateDistributionUrl=false +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/e2e/gradle/gradlew b/packages/gradle/native/gradlew similarity index 96% rename from e2e/gradle/gradlew rename to packages/gradle/native/gradlew index 1aa94a4269074..f3b75f3b0d4fa 100755 --- a/e2e/gradle/gradlew +++ b/packages/gradle/native/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/e2e/gradle/gradlew.bat b/packages/gradle/native/gradlew.bat similarity index 91% rename from e2e/gradle/gradlew.bat rename to packages/gradle/native/gradlew.bat index 93e3f59f135dd..9d21a21834d51 100644 --- a/e2e/gradle/gradlew.bat +++ b/packages/gradle/native/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/packages/gradle/native/plugin/build.gradle.kts b/packages/gradle/native/plugin/build.gradle.kts new file mode 100644 index 0000000000000..57475b238ae26 --- /dev/null +++ b/packages/gradle/native/plugin/build.gradle.kts @@ -0,0 +1,33 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Gradle plugin project to get you started. + * For more details on writing Custom Plugins, please refer to https://docs.gradle.org/8.5/userguide/custom_plugins.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the Java Gradle plugin development plugin to add support for developing Gradle plugins + `java-gradle-plugin` + `maven-publish` + + // Apply the Kotlin JVM plugin to add support for Kotlin. + alias(libs.plugins.jvm) +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +gradlePlugin { + // Define the plugin + val Nodes by plugins.creating { + id = "io.nx.gradle.plugin.Nodes" + implementationClass = "io.nx.gradle.plugin.Nodes" + } +} + +dependencies { + implementation("com.google.code.gson:gson:2.11.0") +} diff --git a/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/CreateNodesTask.kt b/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/CreateNodesTask.kt new file mode 100644 index 0000000000000..186c8299282d6 --- /dev/null +++ b/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/CreateNodesTask.kt @@ -0,0 +1,294 @@ +package io.nx.gradle.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import java.io.File +import org.gradle.api.tasks.options.Option +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction +import org.gradle.api.artifacts.ProjectDependency +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import java.lang.Error +import java.nio.file.Path +import org.gradle.api.logging.Logger + + +data class GradleTargets(val targets: MutableMap, val targetGroups: MutableMap>) +data class Metadata(val targetGroups: MutableMap>, val technologies: Array, val description: String?) +data class Node(val targets: MutableMap, val metadata: Metadata, val name: String) +data class Dependency(val source: String, val target: String, val sourceFile: String) +data class GradleNodesReport(val nodes: MutableMap, val dependencies: MutableSet) + +abstract class CreateNodesTask : DefaultTask() { + @Option(option = "outputDirectory", description = "Output directory, default to {workspaceRoot}/.nx/cache") + @Input + var outputDirectory: String = "" + + @Option(option = "workspaceRoot", description = "Workspace root, default to cwd") + @Input + var workspaceRoot: String = "" + + @Option(option = "hash", description = "hash adds to output file") + @Input + var hash: String = "" + + @get:Input + abstract var currentProject: Project + + private var logger = getLogger() + + @TaskAction + fun action() { + val rootProjectDirectory = currentProject.getProjectDir() + if (workspaceRoot == "") { + // assign the workspace root to root project's path + workspaceRoot = System.getProperty("user.dir") + } + if (outputDirectory == "") { + outputDirectory = File(workspaceRoot, ".nx/cache").getPath() + } + + val projectNodes = mutableMapOf() + var dependencies = mutableSetOf() + val allProjects = currentProject.getAllprojects() + allProjects.forEach { project -> + logger.info("CreateNodes: get nodes and dependencies for ${project}") + try { + // get dependencies of project + getDependenciesForProject(project, dependencies, allProjects) + + val gradleTargets = this.processTargetsForProject(project, workspaceRoot, rootProjectDirectory) + var projectRoot = project.getProjectDir().getPath() + val projectNode = Node( + gradleTargets.targets, + Metadata(gradleTargets.targetGroups, arrayOf("Gradle"), project.getDescription()), + project.getName() + ) + logger.info("CreateNodes: get nodes for ${projectRoot}") + projectNodes.put(projectRoot, projectNode) + } catch (e: Error) { + logger.info("CreateNodes: error ${e.toString()}") + } // ignore errors + } + + val gson = Gson() + val json = gson.toJson(GradleNodesReport(projectNodes, dependencies)) + val file = File(outputDirectory, "${currentProject.name}${hash}.json") + file.writeText(json) + println(file) + } + + fun processTargetsForProject( + project: Project, + workspaceRoot: String, + rootProjectDirectory: File + ): GradleTargets { + val targets = mutableMapOf() + val targetGroups = mutableMapOf>() + val projectRoot = project.getProjectDir().getPath() + + var gradlewCommand: String; + val operSys = System.getProperty("os.name").lowercase(); + if (operSys.contains("win")) { + gradlewCommand = ".\\gradlew.bat" + } else { + gradlewCommand = "./gradlew" + } + var gradleProject = project.getBuildTreePath() + if (!gradleProject.endsWith(":")) { + gradleProject += ":" + } + + project.getTasks().forEach { task -> + val target = mutableMapOf() + + val group: String? = task.getGroup(); + if (!group.isNullOrBlank()) { + if (targetGroups.contains(group)) { + targetGroups.get(group)?.add(task.name) + } else { + targetGroups.set(group, mutableListOf(task.name)) + } + } + + val inputs = task.getInputs().getSourceFiles() + if (!inputs.isEmpty()) { + target.put("inputs", inputs.mapNotNull { file -> + val path: String = file.getPath() + replaceRootInPath(path, projectRoot, workspaceRoot) + }) + } + val outputs = task.getOutputs().getFiles() + if (!outputs.isEmpty()) { + target.put("outputs", outputs.mapNotNull { file -> + val path: String = file.getPath() + replaceRootInPath(path, projectRoot, workspaceRoot) + }) + } + target.put("cache", true) + + val dependsOn = task.getTaskDependencies().getDependencies(task) + if (!dependsOn.isEmpty()) { + target.put("dependsOn", dependsOn.map { depTask -> + val depProject = depTask.getProject() + if (depProject == project) { + depTask.name + } + "${depProject.name}:${depTask.name}" + }) + } + + var cwd = rootProjectDirectory.getPath() + if (cwd.startsWith(workspaceRoot)) { + cwd = cwd.replace(workspaceRoot, ".") + } + if (!cwd.isNullOrBlank()) { + target.put("options", mapOf("cwd" to cwd)) + } + + if (task.name.startsWith("compileTest")) { + addTestCiTarget(inputs, gradlewCommand, gradleProject, target, targets, targetGroups, projectRoot, workspaceRoot) + } + + target.put("command", "${gradlewCommand} ${gradleProject}${task.name}") + + val metadata = mapOf( + "description" to task.getDescription(), + "technologies" to arrayOf("Gradle"), + "help" to mapOf( + "command" to "${gradlewCommand} help --task ${gradleProject}${task.name}" + ) + ) + target.put("metadata", metadata) + + targets.put(task.name, target) + } + + return GradleTargets( + targets, + targetGroups + ); + } + + fun addTestCiTarget( + testFiles: FileCollection, + gradlewCommand: String, + gradleProject: String, + target: MutableMap, + targets: MutableMap, + targetGroups: MutableMap>, + projectRoot: String, + workspaceRoot: String + ) { + if (testFiles.isEmpty()) { + return + } + if (!targetGroups.contains("verification")) { + targetGroups.set("verification", mutableListOf()) + } + val dependsOn = mutableListOf>() + testFiles.filter { testFile -> + val fileName = testFile.getName().split(".").first() + val regex = ".*(Test)(s)?\\d*".toRegex() + regex.matches(fileName) + }.forEach { testFile -> + val fileName = testFile.getName().split(".").first() // remove file extension + + val metadata = mapOf( + "description" to "Runs Gradle test ${fileName} in CI", + "technologies" to arrayOf("Gradle"), + "help" to mapOf( + "command" to "${gradlewCommand} help --task ${gradleProject}test" + ) + ) + + val testCiTarget = target.toMutableMap() + testCiTarget.put("command", "${gradlewCommand} ${gradleProject}test --tests ${fileName}") + testCiTarget.put("metadata", metadata) + testCiTarget.put("inputs", arrayOf(replaceRootInPath(testFile.getPath(), projectRoot, workspaceRoot))) + + val targetName = "ci--${fileName}" + logger.info("CreateNodes: Create test ci file ${targetName}") + targets.put(targetName, testCiTarget) + targetGroups.get("verification")?.add(targetName) + dependsOn.add( + mapOf( + "target" to targetName, + "projects" to "self", + "params" to "forward" + ) + ) + } + if (!dependsOn.isEmpty()) { + val testCiTarget = target.toMutableMap() + val metadata = mapOf( + "description" to "Runs Gradle Tests in CI", + "technologies" to arrayOf("Gradle"), + "help" to mapOf( + "command" to "${gradlewCommand} help --task ${gradleProject}test" + ) + ) + testCiTarget.put("executor", "nx:noop") + testCiTarget.remove("command") + testCiTarget.put("metadata", metadata) + testCiTarget.put("dependsOn", dependsOn) + targets.put("ci", testCiTarget) + targetGroups.get("verification")?.add("ci") + } + + } +} + +fun replaceRootInPath(p: String, projectRoot: String, workspaceRoot: String): String? { + var path = p + if (path.startsWith(projectRoot)) { + path = path.replace(projectRoot, "{projectRoot}") + return path + } else if (path.startsWith(workspaceRoot)) { + path = path.replace(workspaceRoot, "{workspaceRoot}") + return path + } + return null +} + +fun getDependenciesForProject(project: Project, dependencies: MutableSet, allProjects: Set) { + project.getConfigurations().filter { config -> + val configName = config.name + configName == "compileClasspath" || configName == "implementationDependenciesMetadata" + }.forEach { + it.getAllDependencies().filter { + it is ProjectDependency + }.forEach { + val foundProject = allProjects.find { p -> p.getName() == it.getName() } + if (foundProject != null) { + dependencies.add( + Dependency( + project.getProjectDir().getPath(), + foundProject.getProjectDir().getPath(), + project.getBuildFile().getPath() + ) + ) + } + } + } + project.getSubprojects().forEach { childProject -> + dependencies.add( + Dependency( + project.getProjectDir().getPath(), + childProject.getProjectDir().getPath(), + project.getBuildFile().getPath() + ) + ) + } + project.getGradle().includedBuilds.forEach { includedBuild -> + dependencies.add( + Dependency( + project.getProjectDir().getPath(), + includedBuild.getProjectDir().getPath(), + project.getBuildFile().getPath() + ) + ) + } +} diff --git a/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/Nodes.kt b/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/Nodes.kt new file mode 100644 index 0000000000000..8ed6aac3eb39f --- /dev/null +++ b/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/Nodes.kt @@ -0,0 +1,22 @@ +package io.nx.gradle.plugin + +import org.gradle.api.Project +import org.gradle.api.Plugin + +/** + * A plugin to create nx targets + */ +class Nodes: Plugin { + override fun apply(project: Project) { + // Register a task + project.tasks.register("createNodes", CreateNodesTask::class.java) { task -> + task.currentProject = project + task.setDescription("Create nodes and dependencies for Nx") + task.setGroup("Nx Custom") + // Run task for composite builds + project.getGradle().includedBuilds.forEach { includedBuild -> + task.dependsOn(includedBuild.task(":createNodes")) + } + } + } +} diff --git a/packages/gradle/native/settings.gradle.kts b/packages/gradle/native/settings.gradle.kts new file mode 100644 index 0000000000000..76010820c37a3 --- /dev/null +++ b/packages/gradle/native/settings.gradle.kts @@ -0,0 +1,10 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.5/userguide/building_swift_projects.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +rootProject.name = "plugin" +include("plugin") diff --git a/packages/gradle/package.json b/packages/gradle/package.json index 14f5ebf31c8b5..f91c2cdb536f6 100644 --- a/packages/gradle/package.json +++ b/packages/gradle/package.json @@ -26,6 +26,7 @@ "generators": "./generators.json", "exports": { ".": "./index.js", + "./plugin-v1": "./plugin-v1.js", "./package.json": "./package.json", "./migrations.json": "./migrations.json", "./generators.json": "./generators.json" @@ -34,7 +35,8 @@ "migrations": "./migrations.json" }, "dependencies": { - "@nx/devkit": "file:../devkit" + "@nx/devkit": "file:../devkit", + "dotenv": "~16.4.5" }, "publishConfig": { "access": "public" diff --git a/packages/gradle/plugin-v1.spec.ts b/packages/gradle/plugin-v1.spec.ts new file mode 100644 index 0000000000000..0caceb7f67ee2 --- /dev/null +++ b/packages/gradle/plugin-v1.spec.ts @@ -0,0 +1,131 @@ +import { CreateNodesContext } from '@nx/devkit'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { createNodesV2 } from './plugin-v1'; +import { type GradleReport } from './src/plugin-v1/utils/get-gradle-report'; + +let gradleReport: GradleReport; +jest.mock('./src/plugin-v1/utils/get-gradle-report', () => { + return { + GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']), + populateGradleReport: jest.fn().mockImplementation(() => void 0), + getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), + }; +}); + +describe('@nx/gradle/plugin-v1', () => { + let createNodesFunction = createNodesV2[1]; + let context: CreateNodesContext; + let tempFs: TempFs; + + beforeEach(async () => { + tempFs = new TempFs('gradle-plugin'); + gradleReport = { + gradleFileToGradleProjectMap: new Map([ + ['proj/build.gradle', 'proj'], + ]), + buildFileToDepsMap: new Map(), + gradleFileToOutputDirsMap: new Map>([ + ['proj/build.gradle', new Map([['build', 'build']])], + ]), + gradleProjectToTasksMap: new Map>([ + ['proj', new Set(['test'])], + ]), + gradleProjectToTasksTypeMap: new Map>([ + ['proj', new Map([['test', 'Verification']])], + ]), + gradleProjectToProjectName: new Map([['proj', 'proj']]), + gradleProjectNameToProjectRootMap: new Map([ + ['proj', 'proj'], + ]), + gradleProjectToChildProjects: new Map(), + }; + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + configFiles: [], + }; + tempFs.createFileSync('package.json', JSON.stringify({ name: 'repo' })); + tempFs.createFileSync( + 'my-app/project.json', + JSON.stringify({ name: 'my-app' }) + ); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + tempFs = null; + }); + + it('should create nodes', async () => { + tempFs.createFileSync('gradlew', ''); + + const nodes = await createNodesFunction( + ['gradlew', 'proj/build.gradle'], + undefined, + context + ); + + expect(nodes).toMatchInlineSnapshot(` + [ + [ + "proj/build.gradle", + { + "projects": { + "proj": { + "metadata": { + "targetGroups": { + "Verification": [ + "test", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "projectType": "application", + "targets": { + "test": { + "cache": true, + "command": "./gradlew proj:test", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + }, + }, + }, + }, + ], + ] + `); + }); +}); diff --git a/packages/gradle/plugin-v1.ts b/packages/gradle/plugin-v1.ts new file mode 100644 index 0000000000000..35195059db7a4 --- /dev/null +++ b/packages/gradle/plugin-v1.ts @@ -0,0 +1,2 @@ +export { createDependencies } from './src/plugin-v1/dependencies'; +export { createNodes, createNodesV2 } from './src/plugin-v1/nodes'; diff --git a/packages/gradle/plugin.spec.ts b/packages/gradle/plugin.spec.ts index 78dc8efe8bb1d..dcfffbd4b9a39 100644 --- a/packages/gradle/plugin.spec.ts +++ b/packages/gradle/plugin.spec.ts @@ -1,14 +1,15 @@ -import { CreateNodesContext } from '@nx/devkit'; +import { CreateNodesContext, readJsonFile } from '@nx/devkit'; import { TempFs } from '@nx/devkit/internal-testing-utils'; import { createNodesV2 } from './plugin'; -import { type GradleReport } from './src/utils/get-gradle-report'; +import { type NodesReport } from './src/plugin/utils/get-nodes-from-gradle-plugin'; +import { join } from 'path'; -let gradleReport: GradleReport; -jest.mock('./src/utils/get-gradle-report', () => { +let gradleReport: NodesReport; +jest.mock('./src/plugin/utils/get-nodes-from-gradle-plugin', () => { return { GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']), - populateGradleReport: jest.fn().mockImplementation(() => void 0), - getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), + populateNodes: jest.fn().mockImplementation(() => void 0), + getCurrentNodesReport: jest.fn().mockImplementation(() => gradleReport), }; }); @@ -19,26 +20,9 @@ describe('@nx/gradle/plugin', () => { beforeEach(async () => { tempFs = new TempFs('gradle-plugin'); - gradleReport = { - gradleFileToGradleProjectMap: new Map([ - ['proj/build.gradle', 'proj'], - ]), - buildFileToDepsMap: new Map(), - gradleFileToOutputDirsMap: new Map>([ - ['proj/build.gradle', new Map([['build', 'build']])], - ]), - gradleProjectToTasksMap: new Map>([ - ['proj', new Set(['test'])], - ]), - gradleProjectToTasksTypeMap: new Map>([ - ['proj', new Map([['test', 'Verification']])], - ]), - gradleProjectToProjectName: new Map([['proj', 'proj']]), - gradleProjectNameToProjectRootMap: new Map([ - ['proj', 'proj'], - ]), - gradleProjectToChildProjects: new Map(), - }; + gradleReport = readJsonFile( + join(__dirname, 'src/plugin/utils/__mocks__/gradle_tutorial.json') + ); context = { nxJsonConfiguration: { namedInputs: { @@ -80,44 +64,28 @@ describe('@nx/gradle/plugin', () => { "proj": { "metadata": { "targetGroups": { - "Verification": [ - "test", + "help": [ + "buildEnvironment", ], }, "technologies": [ - "gradle", + "Gradle", ], }, - "name": "proj", - "projectType": "application", + "name": "gradle-tutorial", + "root": "proj", "targets": { - "test": { + "buildEnvironment": { "cache": true, - "command": "./gradlew proj:test", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], + "command": "./gradlew :buildEnvironment", "metadata": { - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, + "description": "Displays all buildscript dependencies declared in root project 'gradle-tutorial'.", "technologies": [ - "gradle", + "Gradle", ], }, "options": { - "cwd": ".", + "cwd": "proj", }, }, }, diff --git a/packages/gradle/plugin.ts b/packages/gradle/plugin.ts index c6345f3ebd7c4..f1b3829f076d4 100644 --- a/packages/gradle/plugin.ts +++ b/packages/gradle/plugin.ts @@ -1,2 +1,2 @@ export { createDependencies } from './src/plugin/dependencies'; -export { createNodes, createNodesV2 } from './src/plugin/nodes'; +export { createNodesV2 } from './src/plugin/nodes'; diff --git a/packages/gradle/project.json b/packages/gradle/project.json index 720187647d20e..7b2ffcd5f492d 100644 --- a/packages/gradle/project.json +++ b/packages/gradle/project.json @@ -42,6 +42,11 @@ "glob": "**/*.d.ts", "output": "/" }, + { + "input": "packages/gradle", + "glob": "**/gradle.properties", + "output": "/" + }, { "input": "", "glob": "LICENSE", diff --git a/packages/gradle/src/generators/init/init.spec.ts b/packages/gradle/src/generators/init/init.spec.ts index 881688c79f383..42a1e09bf633f 100644 --- a/packages/gradle/src/generators/init/init.spec.ts +++ b/packages/gradle/src/generators/init/init.spec.ts @@ -24,7 +24,6 @@ describe('@nx/gradle:init', () => { "options": { "buildTargetName": "build", "classesTargetName": "classes", - "includeSubprojectsTasks": false, "testTargetName": "test", }, "plugin": "@nx/gradle", @@ -49,7 +48,6 @@ describe('@nx/gradle:init', () => { "options": { "buildTargetName": "build", "classesTargetName": "classes", - "includeSubprojectsTasks": false, "testTargetName": "test", }, "plugin": "@nx/gradle", diff --git a/packages/gradle/src/generators/init/init.ts b/packages/gradle/src/generators/init/init.ts index 2a7f9028ec457..7585410222e32 100644 --- a/packages/gradle/src/generators/init/init.ts +++ b/packages/gradle/src/generators/init/init.ts @@ -9,7 +9,7 @@ import { Tree, updateNxJson, } from '@nx/devkit'; -import { nxVersion } from '../../utils/versions'; +import { gradlePluginVersion, nxVersion } from '../../utils/versions'; import { InitGeneratorSchema } from './schema'; import { hasGradlePlugin } from '../../utils/has-gradle-plugin'; import { dirname, join, basename } from 'path'; @@ -31,6 +31,7 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { ); } await addBuildGradleFileNextToSettingsGradle(tree); + await disableConfigurationCacheProperty(tree); addPlugin(tree); updateNxJsonConfiguration(tree); @@ -52,7 +53,6 @@ function addPlugin(tree: Tree) { testTargetName: 'test', classesTargetName: 'classes', buildTargetName: 'build', - includeSubprojectsTasks: false, }, }); updateNxJson(tree, nxJson); @@ -67,16 +67,18 @@ export async function addBuildGradleFileNextToSettingsGradle(tree: Tree) { '**/settings.gradle?(.kts)', ]); settingsGradleFiles.forEach((settingsGradleFile) => { - addProjectReportToBuildGradle(settingsGradleFile, tree); + addCreateNodesPluginToBuildGradle(settingsGradleFile, tree); }); } /** * - creates a build.gradle file next to the settings.gradle file if it does not exist. - * - adds the project-report plugin to the build.gradle file if it does not exist. - * - adds a task to generate project reports for all subprojects and included builds. + * - adds the createNodes plugin to the build.gradle file if it does not exist. */ -function addProjectReportToBuildGradle(settingsGradleFile: string, tree: Tree) { +function addCreateNodesPluginToBuildGradle( + settingsGradleFile: string, + tree: Tree +) { const filename = basename(settingsGradleFile); let gradleFilePath = 'build.gradle'; if (filename.endsWith('.kts')) { @@ -90,55 +92,44 @@ function addProjectReportToBuildGradle(settingsGradleFile: string, tree: Tree) { buildGradleContent = tree.read(gradleFilePath).toString(); } - if (buildGradleContent.includes('allprojects')) { - if (!buildGradleContent.includes('"project-report"')) { - logger.warn(`Please add the project-report plugin to your ${gradleFilePath}: -allprojects { - apply { - plugin("project-report") - } -}`); + if (buildGradleContent.includes('plugins {')) { + if (!buildGradleContent.includes('"io.nx.gradle.plugin.Nodes"')) { + buildGradleContent = buildGradleContent.replace( + 'plugins {', + `plugins { + id("io.nx.gradle.plugin.Nodes") version("${gradlePluginVersion}")` + ); } } else { - buildGradleContent += `\n\rallprojects { - apply { - plugin("project-report") - } + buildGradleContent += `\n\rplugins { + id("io.nx.gradle.plugin.Nodes") version("${gradlePluginVersion}") }`; } - if (!buildGradleContent.includes(`tasks.register("projectReportAll")`)) { - if (gradleFilePath.endsWith('.kts')) { - buildGradleContent += `\n\rtasks.register("projectReportAll") { - // All project reports of subprojects - allprojects.forEach { - dependsOn(it.tasks.get("projectReport")) - } - - // All projectReportAll of included builds - gradle.includedBuilds.forEach { - dependsOn(it.task(":projectReportAll")) - } -}`; - } else { - buildGradleContent += `\n\rtasks.register("projectReportAll") { - // All project reports of subprojects - allprojects.forEach { - dependsOn(it.tasks.getAt("projectReport")) - } - - // All projectReportAll of included builds - gradle.includedBuilds.forEach { - dependsOn(it.task(":projectReportAll")) - } - }`; - } - } if (buildGradleContent) { tree.write(gradleFilePath, buildGradleContent); } } +/** + * Need to set org.gradle.configuration-cache=false for createNodes task to not throw an error. + * @param tree + */ +export async function disableConfigurationCacheProperty(tree: Tree) { + const gradlePropertiesFiles = await globAsync(tree, ['**/gradle.properties']); + gradlePropertiesFiles.forEach((gradlePropertiesFile) => { + const content = tree.read(gradlePropertiesFile).toString(); + if ( + content.includes('org.gradle.configuration-cache') && + !content.includes('org.gradle.configuration-cache=false') + ) { + logger.warn( + 'org.gradle.configuration-cache property is set to true. Setting it to false to avoid issues with createNodes task.' + ); + } + }); +} + export function updateNxJsonConfiguration(tree: Tree) { const nxJson = readNxJson(tree); diff --git a/packages/gradle/src/migrations/19-4-0/add-project-report-all.ts b/packages/gradle/src/migrations/19-4-0/add-project-report-all.ts index 0d9824e95e527..d0224195484c4 100644 --- a/packages/gradle/src/migrations/19-4-0/add-project-report-all.ts +++ b/packages/gradle/src/migrations/19-4-0/add-project-report-all.ts @@ -1,5 +1,5 @@ -import { Tree } from '@nx/devkit'; -import { addBuildGradleFileNextToSettingsGradle } from '../../generators/init/init'; +import { globAsync, logger, Tree } from '@nx/devkit'; +import { basename, dirname, join } from 'node:path'; /** * This migration adds task `projectReportAll` to build.gradle files @@ -7,3 +7,83 @@ import { addBuildGradleFileNextToSettingsGradle } from '../../generators/init/in export default async function update(tree: Tree) { await addBuildGradleFileNextToSettingsGradle(tree); } + +/** + * This function creates and populate build.gradle file next to the settings.gradle file. + */ +export async function addBuildGradleFileNextToSettingsGradle(tree: Tree) { + const settingsGradleFiles = await globAsync(tree, [ + '**/settings.gradle?(.kts)', + ]); + settingsGradleFiles.forEach((settingsGradleFile) => { + addProjectReportToBuildGradle(settingsGradleFile, tree); + }); +} + +/** + * - creates a build.gradle file next to the settings.gradle file if it does not exist. + * - adds the project-report plugin to the build.gradle file if it does not exist. + * - adds a task to generate project reports for all subprojects and included builds. + */ +function addProjectReportToBuildGradle(settingsGradleFile: string, tree: Tree) { + const filename = basename(settingsGradleFile); + let gradleFilePath = 'build.gradle'; + if (filename.endsWith('.kts')) { + gradleFilePath = 'build.gradle.kts'; + } + gradleFilePath = join(dirname(settingsGradleFile), gradleFilePath); + let buildGradleContent = ''; + if (!tree.exists(gradleFilePath)) { + tree.write(gradleFilePath, buildGradleContent); // create a build.gradle file near settings.gradle file if it does not exist + } else { + buildGradleContent = tree.read(gradleFilePath).toString(); + } + + if (buildGradleContent.includes('allprojects')) { + if (!buildGradleContent.includes('"project-report"')) { + logger.warn(`Please add the project-report plugin to your ${gradleFilePath}: +allprojects { + apply { + plugin("project-report") + } +}`); + } + } else { + buildGradleContent += `\n\rallprojects { + apply { + plugin("project-report") + } +}`; + } + + if (!buildGradleContent.includes(`tasks.register("projectReportAll")`)) { + if (gradleFilePath.endsWith('.kts')) { + buildGradleContent += `\n\rtasks.register("projectReportAll") { + // All project reports of subprojects + allprojects.forEach { + dependsOn(it.tasks.get("projectReport")) + } + + // All projectReportAll of included builds + gradle.includedBuilds.forEach { + dependsOn(it.task(":projectReportAll")) + } +}`; + } else { + buildGradleContent += `\n\rtasks.register("projectReportAll") { + // All project reports of subprojects + allprojects.forEach { + dependsOn(it.tasks.getAt("projectReport")) + } + + // All projectReportAll of included builds + gradle.includedBuilds.forEach { + dependsOn(it.task(":projectReportAll")) + } + }`; + } + } + if (buildGradleContent) { + tree.write(gradleFilePath, buildGradleContent); + } +} diff --git a/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.ts b/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.ts index b81506bbdf62c..216edef9bb170 100644 --- a/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.ts +++ b/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.ts @@ -1,6 +1,6 @@ import { Tree, readNxJson, updateNxJson } from '@nx/devkit'; import { hasGradlePlugin } from '../../utils/has-gradle-plugin'; -import { GradlePluginOptions } from '../../plugin/nodes'; +import { GradlePluginOptions } from '../../plugin-v1/nodes'; // This function add options includeSubprojectsTasks as true in nx.json for gradle plugin export default function update(tree: Tree) { diff --git a/packages/gradle/src/plugin/dependencies.spec.ts b/packages/gradle/src/plugin-v1/dependencies.spec.ts similarity index 99% rename from packages/gradle/src/plugin/dependencies.spec.ts rename to packages/gradle/src/plugin-v1/dependencies.spec.ts index 6b79e4616351c..8ca4074bc920d 100644 --- a/packages/gradle/src/plugin/dependencies.spec.ts +++ b/packages/gradle/src/plugin-v1/dependencies.spec.ts @@ -10,7 +10,6 @@ describe('processGradleDependencies', () => { it('should process gradle dependencies with composite build', () => { const depFilePath = join( __dirname, - '..', 'utils/__mocks__/gradle-composite-dependencies.txt' ); const dependencies = new Set([]); @@ -59,7 +58,6 @@ describe('processGradleDependencies', () => { it('should process gradle dependencies with regular build', () => { const depFilePath = join( __dirname, - '..', 'utils/__mocks__/gradle-dependencies.txt' ); const dependencies = new Set([]); diff --git a/packages/gradle/src/plugin-v1/dependencies.ts b/packages/gradle/src/plugin-v1/dependencies.ts new file mode 100644 index 0000000000000..a723f2a215948 --- /dev/null +++ b/packages/gradle/src/plugin-v1/dependencies.ts @@ -0,0 +1,144 @@ +import { + CreateDependencies, + CreateDependenciesContext, + DependencyType, + FileMap, + RawProjectGraphDependency, + validateDependency, +} from '@nx/devkit'; +import { readFileSync } from 'node:fs'; +import { basename, dirname } from 'node:path'; + +import { getCurrentGradleReport } from './utils/get-gradle-report'; +import { GRADLE_BUILD_FILES } from '../utils/split-config-files'; +import { newLineSeparator } from '../utils/exec-gradle'; + +export const createDependencies: CreateDependencies = async ( + _, + context: CreateDependenciesContext +) => { + const gradleFiles: string[] = findGradleFiles(context.filesToProcess); + if (gradleFiles.length === 0) { + return []; + } + + const gradleDependenciesStart = performance.mark('gradleDependencies:start'); + const { + gradleFileToGradleProjectMap, + gradleProjectNameToProjectRootMap, + buildFileToDepsMap, + gradleProjectToChildProjects, + } = getCurrentGradleReport(); + const dependencies: Set = new Set(); + + for (const gradleFile of gradleFiles) { + const gradleProject = gradleFileToGradleProjectMap.get(gradleFile); + const projectName = Object.values(context.projects).find( + (project) => project.root === dirname(gradleFile) + )?.name; + const depsFile = buildFileToDepsMap.get(gradleFile); + + if (projectName && depsFile) { + processGradleDependencies( + depsFile, + gradleProjectNameToProjectRootMap, + projectName, + gradleFile, + context, + dependencies + ); + } + gradleProjectToChildProjects.get(gradleProject)?.forEach((childProject) => { + if (childProject) { + const dependency: RawProjectGraphDependency = { + source: projectName as string, + target: childProject, + type: DependencyType.static, + sourceFile: gradleFile, + }; + validateDependency(dependency, context); + dependencies.add(dependency); + } + }); + } + + const gradleDependenciesEnd = performance.mark('gradleDependencies:end'); + performance.measure( + 'gradleDependencies', + gradleDependenciesStart.name, + gradleDependenciesEnd.name + ); + + return Array.from(dependencies); +}; + +function findGradleFiles(fileMap: FileMap): string[] { + const gradleFiles: string[] = []; + + for (const [_, files] of Object.entries(fileMap.projectFileMap)) { + for (const file of files) { + if (GRADLE_BUILD_FILES.has(basename(file.file))) { + gradleFiles.push(file.file); + } + } + } + + return gradleFiles; +} + +export function processGradleDependencies( + depsFile: string, + gradleProjectNameToProjectRoot: Map, + sourceProjectName: string, + gradleFile: string, + context: CreateDependenciesContext, + dependencies: Set +): void { + const lines = readFileSync(depsFile).toString().split(newLineSeparator); + let inDeps = false; + for (const line of lines) { + if ( + line.startsWith('implementationDependenciesMetadata') || + line.startsWith('compileClasspath') + ) { + inDeps = true; + continue; + } + + if (inDeps) { + if (line === '') { + inDeps = false; + continue; + } + const [indents, dep] = line.split('--- '); + if (indents === '\\' || indents === '+') { + let gradleProjectName: string | undefined; + if (dep.startsWith('project ')) { + gradleProjectName = dep + .substring('project '.length) + .replace(/ \(n\)$/, '') + .trim(); + } else if (dep.includes('-> project')) { + const [_, projectName] = dep.split('-> project'); + gradleProjectName = projectName.trim(); + } + const targetProjectRoot = gradleProjectNameToProjectRoot.get( + gradleProjectName + ) as string; + const targetProjectName = Object.values(context.projects).find( + (project) => project.root === targetProjectRoot + )?.name; + if (targetProjectName) { + const dependency: RawProjectGraphDependency = { + source: sourceProjectName, + target: targetProjectName, + type: DependencyType.static, + sourceFile: gradleFile, + }; + validateDependency(dependency, context); + dependencies.add(dependency); + } + } + } + } +} diff --git a/packages/gradle/src/plugin-v1/nodes.spec.ts b/packages/gradle/src/plugin-v1/nodes.spec.ts new file mode 100644 index 0000000000000..ed03509313add --- /dev/null +++ b/packages/gradle/src/plugin-v1/nodes.spec.ts @@ -0,0 +1,587 @@ +import { CreateNodesContext } from '@nx/devkit'; + +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { type GradleReport } from './utils/get-gradle-report'; + +let gradleReport: GradleReport; +jest.mock('./utils/get-gradle-report', () => { + return { + GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']), + populateGradleReport: jest.fn().mockImplementation(() => void 0), + getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), + }; +}); + +import { createNodesV2 } from './nodes'; + +describe('@nx/gradle/plugin-v1/nodes', () => { + let createNodesFunction = createNodesV2[1]; + let context: CreateNodesContext; + let tempFs: TempFs; + let cwd: string; + + beforeEach(async () => { + tempFs = new TempFs('test'); + gradleReport = { + gradleFileToGradleProjectMap: new Map([ + ['proj/build.gradle', 'proj'], + ]), + buildFileToDepsMap: new Map(), + gradleFileToOutputDirsMap: new Map>([ + ['proj/build.gradle', new Map([['build', 'build']])], + ]), + gradleProjectToTasksMap: new Map>([ + ['proj', new Set(['test'])], + ]), + gradleProjectToTasksTypeMap: new Map>([ + [ + 'proj', + new Map([ + ['test', 'Verification'], + ['build', 'Build'], + ]), + ], + ]), + gradleProjectToProjectName: new Map([['proj', 'proj']]), + gradleProjectNameToProjectRootMap: new Map([ + ['proj', 'proj'], + ]), + gradleProjectToChildProjects: new Map(), + }; + cwd = process.cwd(); + process.chdir(tempFs.tempDir); + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + configFiles: [], + }; + + await tempFs.createFiles({ + 'proj/build.gradle': ``, + gradlew: '', + }); + }); + + afterEach(() => { + jest.resetModules(); + process.chdir(cwd); + }); + + it('should create nodes based on gradle', async () => { + const results = await createNodesFunction( + ['proj/build.gradle'], + { + buildTargetName: 'build', + }, + context + ); + + expect(results).toMatchInlineSnapshot(` + [ + [ + "proj/build.gradle", + { + "projects": { + "proj": { + "metadata": { + "targetGroups": { + "Verification": [ + "test", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "projectType": "application", + "targets": { + "test": { + "cache": true, + "command": "./gradlew proj:test", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + }, + }, + }, + }, + ], + ] + `); + }); + + it('should create nodes include subprojects tasks', async () => { + const results = await createNodesFunction( + ['proj/build.gradle'], + { + buildTargetName: 'build', + includeSubprojectsTasks: true, + }, + context + ); + + expect(results).toMatchInlineSnapshot(` + [ + [ + "proj/build.gradle", + { + "projects": { + "proj": { + "metadata": { + "targetGroups": { + "Build": [ + "build", + ], + "Verification": [ + "test", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "projectType": "application", + "targets": { + "build": { + "cache": true, + "command": "./gradlew proj:build", + "dependsOn": [ + "^build", + "classes", + "test", + ], + "inputs": [ + "production", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:build", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + "outputs": [ + "build", + ], + }, + "test": { + "cache": true, + "command": "./gradlew proj:test", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + }, + }, + }, + }, + ], + ] + `); + }); + + it('should create nodes based on gradle for nested project root', async () => { + gradleReport = { + gradleFileToGradleProjectMap: new Map([ + ['nested/nested/proj/build.gradle', 'proj'], + ]), + buildFileToDepsMap: new Map(), + gradleFileToOutputDirsMap: new Map>([ + ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], + ]), + gradleProjectToTasksMap: new Map>([ + ['proj', new Set(['test'])], + ]), + gradleProjectToTasksTypeMap: new Map>([ + ['proj', new Map([['test', 'Verification']])], + ]), + gradleProjectToProjectName: new Map([['proj', 'proj']]), + gradleProjectNameToProjectRootMap: new Map([ + ['proj', 'proj'], + ]), + gradleProjectToChildProjects: new Map(), + }; + await tempFs.createFiles({ + 'nested/nested/proj/build.gradle': ``, + }); + + const results = await createNodesFunction( + ['nested/nested/proj/build.gradle'], + { + buildTargetName: 'build', + }, + context + ); + + expect(results).toMatchInlineSnapshot(` + [ + [ + "nested/nested/proj/build.gradle", + { + "projects": { + "nested/nested/proj": { + "metadata": { + "targetGroups": { + "Verification": [ + "test", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "projectType": "application", + "targets": { + "test": { + "cache": true, + "command": "./gradlew proj:test", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + }, + }, + }, + }, + ], + ] + `); + }); + + describe('with atomized tests targets', () => { + beforeEach(async () => { + gradleReport = { + gradleFileToGradleProjectMap: new Map([ + ['nested/nested/proj/build.gradle', 'proj'], + ]), + buildFileToDepsMap: new Map(), + gradleFileToOutputDirsMap: new Map>([ + ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], + ]), + gradleProjectToTasksMap: new Map>([ + ['proj', new Set(['test'])], + ]), + gradleProjectToTasksTypeMap: new Map>([ + ['proj', new Map([['test', 'Test']])], + ]), + gradleProjectToProjectName: new Map([['proj', 'proj']]), + gradleProjectNameToProjectRootMap: new Map([ + ['proj', 'proj'], + ]), + gradleProjectToChildProjects: new Map(), + }; + await tempFs.createFiles({ + 'nested/nested/proj/build.gradle': ``, + }); + await tempFs.createFiles({ + 'proj/src/test/java/test/rootTest.java': ``, + }); + await tempFs.createFiles({ + 'nested/nested/proj/src/test/java/test/aTest.java': ``, + }); + await tempFs.createFiles({ + 'nested/nested/proj/src/test/java/test/bTest.java': ``, + }); + await tempFs.createFiles({ + 'nested/nested/proj/src/test/java/test/cTests.java': ``, + }); + }); + + it('should create nodes with atomized tests targets based on gradle for nested project root', async () => { + const results = await createNodesFunction( + [ + 'nested/nested/proj/build.gradle', + 'proj/src/test/java/test/rootTest.java', + 'nested/nested/proj/src/test/java/test/aTest.java', + 'nested/nested/proj/src/test/java/test/bTest.java', + 'nested/nested/proj/src/test/java/test/cTests.java', + ], + { + buildTargetName: 'build', + ciTargetName: 'test-ci', + }, + context + ); + + expect(results).toMatchInlineSnapshot(` + [ + [ + "nested/nested/proj/build.gradle", + { + "projects": { + "nested/nested/proj": { + "metadata": { + "targetGroups": { + "Test": [ + "test-ci--aTest", + "test-ci--bTest", + "test-ci--cTests", + "test-ci", + "test", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "projectType": "application", + "targets": { + "test": { + "cache": false, + "command": "./gradlew proj:test", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + "test-ci": { + "cache": true, + "dependsOn": [ + { + "params": "forward", + "projects": "self", + "target": "test-ci--aTest", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--bTest", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--cTests", + }, + ], + "executor": "nx:noop", + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle Tests in CI", + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "nonAtomizedTarget": "test", + "technologies": [ + "gradle", + ], + }, + }, + "test-ci--aTest": { + "cache": true, + "command": "./gradlew proj:test --tests aTest", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle test nested/nested/proj/src/test/java/test/aTest.java in CI", + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + "test-ci--bTest": { + "cache": true, + "command": "./gradlew proj:test --tests bTest", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle test nested/nested/proj/src/test/java/test/bTest.java in CI", + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + "test-ci--cTests": { + "cache": true, + "command": "./gradlew proj:test --tests cTests", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "description": "Runs Gradle test nested/nested/proj/src/test/java/test/cTests.java in CI", + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + }, + }, + }, + }, + ], + ] + `); + }); + }); +}); diff --git a/packages/gradle/src/plugin-v1/nodes.ts b/packages/gradle/src/plugin-v1/nodes.ts new file mode 100644 index 0000000000000..7c087df75a42e --- /dev/null +++ b/packages/gradle/src/plugin-v1/nodes.ts @@ -0,0 +1,435 @@ +import { + CreateNodes, + CreateNodesV2, + CreateNodesContext, + ProjectConfiguration, + TargetConfiguration, + createNodesFromFiles, + readJsonFile, + writeJsonFile, + CreateNodesFunction, + logger, +} from '@nx/devkit'; +import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; +import { existsSync } from 'node:fs'; +import { basename, dirname, join } from 'node:path'; +import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; +import { findProjectForPath } from 'nx/src/devkit-internals'; + +import { + populateGradleReport, + getCurrentGradleReport, + GradleReport, +} from './utils/get-gradle-report'; +import { hashObject } from 'nx/src/hasher/file-hasher'; +import { + gradleConfigAndTestGlob, + gradleConfigGlob, + splitConfigFiles, +} from '../utils/split-config-files'; +import { getGradleExecFile, findGraldewFile } from '../utils/exec-gradle'; + +const cacheableTaskType = new Set(['Build', 'Verification']); +const dependsOnMap = { + build: ['^build', 'classes', 'test'], + testClasses: ['classes'], + test: ['testClasses'], + classes: ['^classes'], +}; + +interface GradleTask { + type: string; + name: string; +} + +export interface GradlePluginOptions { + includeSubprojectsTasks?: boolean; // default is false, show all gradle tasks in the project + ciTargetName?: string; + testTargetName?: string; + classesTargetName?: string; + buildTargetName?: string; + [taskTargetName: string]: string | undefined | boolean; +} + +function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions { + options ??= {}; + options.testTargetName ??= 'test'; + options.classesTargetName ??= 'classes'; + options.buildTargetName ??= 'build'; + return options; +} + +type GradleTargets = Record>; + +function readTargetsCache(cachePath: string): GradleTargets { + return existsSync(cachePath) ? readJsonFile(cachePath) : {}; +} + +export function writeTargetsToCache(cachePath: string, results: GradleTargets) { + writeJsonFile(cachePath, results); +} + +export const createNodesV2: CreateNodesV2 = [ + gradleConfigAndTestGlob, + async (files, options, context) => { + const { buildFiles, projectRoots, gradlewFiles, testFiles } = + splitConfigFiles(files); + const optionsHash = hashObject(options); + const cachePath = join( + workspaceDataDirectory, + `gradle-${optionsHash}.hash` + ); + const targetsCache = readTargetsCache(cachePath); + + await populateGradleReport( + context.workspaceRoot, + gradlewFiles.map((f) => join(context.workspaceRoot, f)) + ); + const gradleReport = getCurrentGradleReport(); + const gradleProjectRootToTestFilesMap = getGradleProjectRootToTestFilesMap( + testFiles, + projectRoots + ); + + try { + return createNodesFromFiles( + makeCreateNodesForGradleConfigFile( + gradleReport, + targetsCache, + gradleProjectRootToTestFilesMap + ), + buildFiles, + options, + context + ); + } finally { + writeTargetsToCache(cachePath, targetsCache); + } + }, +]; + +export const makeCreateNodesForGradleConfigFile = + ( + gradleReport: GradleReport, + targetsCache: GradleTargets = {}, + gradleProjectRootToTestFilesMap: Record = {} + ): CreateNodesFunction => + async ( + gradleFilePath, + options: GradlePluginOptions | undefined, + context: CreateNodesContext + ) => { + const projectRoot = dirname(gradleFilePath); + options = normalizeOptions(options); + + const hash = await calculateHashForCreateNodes( + projectRoot, + options ?? {}, + context + ); + targetsCache[hash] ??= await createGradleProject( + gradleReport, + gradleFilePath, + options, + context, + gradleProjectRootToTestFilesMap[projectRoot] + ); + const project = targetsCache[hash]; + if (!project) { + return {}; + } + return { + projects: { + [projectRoot]: project, + }, + }; + }; + +/** + @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead. + This function will change to the v2 function in Nx 20. + */ +export const createNodes: CreateNodes = [ + gradleConfigGlob, + async (buildFile, options, context) => { + logger.warn( + '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' + ); + const { gradlewFiles } = splitConfigFiles(context.configFiles); + await populateGradleReport(context.workspaceRoot, gradlewFiles); + const gradleReport = getCurrentGradleReport(); + const internalCreateNodes = + makeCreateNodesForGradleConfigFile(gradleReport); + return await internalCreateNodes(buildFile, options, context); + }, +]; + +async function createGradleProject( + gradleReport: GradleReport, + gradleFilePath: string, + options: GradlePluginOptions | undefined, + context: CreateNodesContext, + testFiles = [] +) { + try { + const { + gradleProjectToTasksTypeMap, + gradleProjectToTasksMap, + gradleFileToOutputDirsMap, + gradleFileToGradleProjectMap, + gradleProjectToProjectName, + } = gradleReport; + + const gradleProject = gradleFileToGradleProjectMap.get( + gradleFilePath + ) as string; + const projectName = gradleProjectToProjectName.get(gradleProject); + if (!projectName) { + return; + } + + const tasksTypeMap: Map = gradleProjectToTasksTypeMap.get( + gradleProject + ) as Map; + const tasksSet = gradleProjectToTasksMap.get(gradleProject) as Set; + let tasks: GradleTask[] = []; + tasksSet.forEach((taskName) => { + tasks.push({ + type: tasksTypeMap.get(taskName) as string, + name: taskName, + }); + }); + if (options.includeSubprojectsTasks) { + tasksTypeMap.forEach((taskType, taskName) => { + if (!tasksSet.has(taskName)) { + tasks.push({ + type: taskType, + name: taskName, + }); + } + }); + } + + const outputDirs = gradleFileToOutputDirsMap.get(gradleFilePath) as Map< + string, + string + >; + + const { targets, targetGroups } = await createGradleTargets( + tasks, + options, + context, + outputDirs, + gradleProject, + gradleFilePath, + testFiles + ); + const project: Partial = { + name: projectName, + projectType: 'application', + targets, + metadata: { + targetGroups, + technologies: ['gradle'], + }, + }; + + return project; + } catch (e) { + console.error(e); + return undefined; + } +} + +async function createGradleTargets( + tasks: GradleTask[], + options: GradlePluginOptions | undefined, + context: CreateNodesContext, + outputDirs: Map, + gradleProject: string, + gradleBuildFilePath: string, + testFiles: string[] = [] +): Promise<{ + targetGroups: Record; + targets: Record; +}> { + const inputsMap = createInputsMap(context); + const gradlewFileDirectory = dirname( + findGraldewFile(gradleBuildFilePath, context.workspaceRoot) + ); + + const targets: Record = {}; + const targetGroups: Record = {}; + for (const task of tasks) { + const targetName = options?.[`${task.name}TargetName`] ?? task.name; + + let outputs = [outputDirs.get(task.name)].filter(Boolean); + if (task.name === 'test') { + outputs = [ + outputDirs.get('testReport'), + outputDirs.get('testResults'), + ].filter(Boolean); + getTestCiTargets( + testFiles, + gradleProject, + targetName as string, + options.ciTargetName, + inputsMap['test'], + outputs, + task.type, + targets, + targetGroups, + gradlewFileDirectory + ); + } + + const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}${ + task.name + }`; + + targets[targetName as string] = { + command: `${getGradleExecFile()} ${taskCommandToRun}`, + options: { + cwd: gradlewFileDirectory, + }, + cache: cacheableTaskType.has(task.type), + inputs: inputsMap[task.name], + dependsOn: dependsOnMap[task.name], + metadata: { + technologies: ['gradle'], + help: { + command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, + example: { + options: { + args: ['--rerun'], + }, + }, + }, + }, + ...(outputs && outputs.length ? { outputs } : {}), + }; + + if (task.type) { + if (!targetGroups[task.type]) { + targetGroups[task.type] = []; + } + targetGroups[task.type].push(targetName as string); + } + } + return { targetGroups, targets }; +} + +function createInputsMap( + context: CreateNodesContext +): Record { + const namedInputs = context.nxJsonConfiguration.namedInputs; + return { + build: namedInputs?.production + ? ['production', '^production'] + : ['default', '^default'], + test: ['default', namedInputs?.production ? '^production' : '^default'], + classes: namedInputs?.production + ? ['production', '^production'] + : ['default', '^default'], + }; +} + +function getTestCiTargets( + testFiles: string[], + gradleProject: string, + testTargetName: string, + ciTargetName: string, + inputs: TargetConfiguration['inputs'], + outputs: string[], + targetGroupName: string, + targets: Record, + targetGroups: Record, + gradlewFileDirectory: string +): void { + if (!testFiles || testFiles.length === 0 || !ciTargetName) { + return; + } + const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}test`; + + if (!targetGroups[targetGroupName]) { + targetGroups[targetGroupName] = []; + } + + const dependsOn: TargetConfiguration['dependsOn'] = []; + testFiles.forEach((testFile) => { + const testName = basename(testFile).split('.')[0]; + const targetName = ciTargetName + '--' + testName; + + targets[targetName] = { + command: `${getGradleExecFile()} ${taskCommandToRun} --tests ${testName}`, + options: { + cwd: gradlewFileDirectory, + }, + cache: true, + inputs, + dependsOn: dependsOnMap['test'], + metadata: { + technologies: ['gradle'], + description: `Runs Gradle test ${testFile} in CI`, + help: { + command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, + example: { + options: { + args: ['--rerun'], + }, + }, + }, + }, + ...(outputs && outputs.length > 0 ? { outputs } : {}), + }; + targetGroups[targetGroupName].push(targetName); + dependsOn.push({ + target: targetName, + projects: 'self', + params: 'forward', + }); + }); + + targets[ciTargetName] = { + executor: 'nx:noop', + cache: true, + inputs, + dependsOn: dependsOn, + ...(outputs && outputs.length > 0 ? { outputs } : {}), + metadata: { + technologies: ['gradle'], + description: 'Runs Gradle Tests in CI', + nonAtomizedTarget: testTargetName, + help: { + command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, + example: { + options: { + args: ['--rerun'], + }, + }, + }, + }, + }; + targetGroups[targetGroupName].push(ciTargetName); +} + +function getGradleProjectRootToTestFilesMap( + testFiles: string[], + projectRoots: string[] +): Record | undefined { + if (testFiles.length === 0 || projectRoots.length === 0) { + return; + } + const roots = new Map(projectRoots.map((root) => [root, root])); + const testFilesToGradleProjectMap: Record = {}; + testFiles.forEach((testFile) => { + const projectRoot = findProjectForPath(testFile, roots); + if (projectRoot) { + if (!testFilesToGradleProjectMap[projectRoot]) { + testFilesToGradleProjectMap[projectRoot] = []; + } + testFilesToGradleProjectMap[projectRoot].push(testFile); + } + }); + return testFilesToGradleProjectMap; +} diff --git a/packages/gradle/src/utils/__mocks__/gradle-composite-dependencies.txt b/packages/gradle/src/plugin-v1/utils/__mocks__/gradle-composite-dependencies.txt similarity index 100% rename from packages/gradle/src/utils/__mocks__/gradle-composite-dependencies.txt rename to packages/gradle/src/plugin-v1/utils/__mocks__/gradle-composite-dependencies.txt diff --git a/packages/gradle/src/utils/__mocks__/gradle-dependencies.txt b/packages/gradle/src/plugin-v1/utils/__mocks__/gradle-dependencies.txt similarity index 100% rename from packages/gradle/src/utils/__mocks__/gradle-dependencies.txt rename to packages/gradle/src/plugin-v1/utils/__mocks__/gradle-dependencies.txt diff --git a/packages/gradle/src/utils/__mocks__/gradle-project-report-println.txt b/packages/gradle/src/plugin-v1/utils/__mocks__/gradle-project-report-println.txt similarity index 100% rename from packages/gradle/src/utils/__mocks__/gradle-project-report-println.txt rename to packages/gradle/src/plugin-v1/utils/__mocks__/gradle-project-report-println.txt diff --git a/packages/gradle/src/utils/__mocks__/gradle-project-report.txt b/packages/gradle/src/plugin-v1/utils/__mocks__/gradle-project-report.txt similarity index 100% rename from packages/gradle/src/utils/__mocks__/gradle-project-report.txt rename to packages/gradle/src/plugin-v1/utils/__mocks__/gradle-project-report.txt diff --git a/packages/gradle/src/utils/__mocks__/gradle-properties-report-child-projects.txt b/packages/gradle/src/plugin-v1/utils/__mocks__/gradle-properties-report-child-projects.txt similarity index 100% rename from packages/gradle/src/utils/__mocks__/gradle-properties-report-child-projects.txt rename to packages/gradle/src/plugin-v1/utils/__mocks__/gradle-properties-report-child-projects.txt diff --git a/packages/gradle/src/utils/__mocks__/gradle-properties-report-no-child-projects.txt b/packages/gradle/src/plugin-v1/utils/__mocks__/gradle-properties-report-no-child-projects.txt similarity index 100% rename from packages/gradle/src/utils/__mocks__/gradle-properties-report-no-child-projects.txt rename to packages/gradle/src/plugin-v1/utils/__mocks__/gradle-properties-report-no-child-projects.txt diff --git a/packages/gradle/src/utils/get-gradle-report.spec.ts b/packages/gradle/src/plugin-v1/utils/get-gradle-report.spec.ts similarity index 100% rename from packages/gradle/src/utils/get-gradle-report.spec.ts rename to packages/gradle/src/plugin-v1/utils/get-gradle-report.spec.ts diff --git a/packages/gradle/src/utils/get-gradle-report.ts b/packages/gradle/src/plugin-v1/utils/get-gradle-report.ts similarity index 98% rename from packages/gradle/src/utils/get-gradle-report.ts rename to packages/gradle/src/plugin-v1/utils/get-gradle-report.ts index f9de425ba8701..c12079050889e 100644 --- a/packages/gradle/src/utils/get-gradle-report.ts +++ b/packages/gradle/src/plugin-v1/utils/get-gradle-report.ts @@ -11,13 +11,10 @@ import { import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context'; import { dirname } from 'path'; -import { gradleConfigAndTestGlob } from './split-config-files'; -import { - getProjectReportLines, - fileSeparator, - newLineSeparator, -} from './get-project-report-lines'; +import { gradleConfigAndTestGlob } from '../../utils/split-config-files'; +import { getProjectReportLines } from './get-project-report-lines'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; +import { fileSeparator, newLineSeparator } from '../../utils/exec-gradle'; export interface GradleReport { gradleFileToGradleProjectMap: Map; diff --git a/packages/gradle/src/utils/get-project-report-lines.ts b/packages/gradle/src/plugin-v1/utils/get-project-report-lines.ts similarity index 88% rename from packages/gradle/src/utils/get-project-report-lines.ts rename to packages/gradle/src/plugin-v1/utils/get-project-report-lines.ts index ddeaf341c7861..988cd9d39d1e4 100644 --- a/packages/gradle/src/utils/get-project-report-lines.ts +++ b/packages/gradle/src/plugin-v1/utils/get-project-report-lines.ts @@ -1,16 +1,8 @@ import { AggregateCreateNodesError, logger } from '@nx/devkit'; -import { execGradleAsync } from './exec-gradle'; +import { execGradleAsync, newLineSeparator } from '../../utils/exec-gradle'; import { existsSync } from 'fs'; import { dirname, join } from 'path'; -export const fileSeparator = process.platform.startsWith('win') - ? 'file:///' - : 'file://'; - -export const newLineSeparator = process.platform.startsWith('win') - ? '\r\n' - : '\n'; - /** * This function executes the gradle projectReportAll task and returns the output as an array of lines. * @param gradlewFile the absolute path to the gradlew file diff --git a/packages/gradle/src/plugin/__snapshots__/nodes.spec.ts.snap b/packages/gradle/src/plugin/__snapshots__/nodes.spec.ts.snap new file mode 100644 index 0000000000000..1c0c1e553de91 --- /dev/null +++ b/packages/gradle/src/plugin/__snapshots__/nodes.spec.ts.snap @@ -0,0 +1,492 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@nx/gradle/plugin/nodes should create nodes based on gradle 1`] = ` +[ + [ + "proj/build.gradle", + { + "projects": { + "proj": { + "metadata": { + "targetGroups": { + "help": [ + "buildEnvironment", + ], + }, + "technologies": [ + "Gradle", + ], + }, + "name": "gradle-tutorial", + "root": "proj", + "targets": { + "buildEnvironment": { + "cache": true, + "command": "./gradlew :buildEnvironment", + "metadata": { + "description": "Displays all buildscript dependencies declared in root project 'gradle-tutorial'.", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + }, + }, + }, + }, + }, + ], +] +`; + +exports[`@nx/gradle/plugin/nodes should create nodes based on gradle for nested project root 1`] = ` +[ + [ + "nested/nested/proj/build.gradle", + { + "projects": { + "nested/nested/proj": { + "metadata": { + "targetGroups": { + "help": [ + "buildEnvironment", + ], + }, + "technologies": [ + "Gradle", + ], + }, + "name": "my-composite", + "root": "nested/nested/proj", + "targets": { + "buildEnvironment": { + "cache": true, + "command": "./gradlew :buildEnvironment", + "metadata": { + "description": "Displays all buildscript dependencies declared in root project 'my-composite'.", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "nested/nested/proj", + }, + }, + }, + }, + }, + }, + ], +] +`; + +exports[`@nx/gradle/plugin/nodes should create nodes with atomized tests targets based on gradle if ciTargetName is specified 1`] = ` +[ + [ + "proj/application/build.gradle", + { + "projects": { + "proj/application": { + "metadata": { + "targetGroups": { + "verification": [ + "test-ci", + ], + }, + "technologies": [ + "Gradle", + ], + }, + "name": "application", + "root": "proj/application", + "targets": { + "test-ci": { + "cache": true, + "dependsOn": [ + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest10", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest7", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest6", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest3", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest2", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest9", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest5", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest4", + }, + { + "params": "forward", + "projects": "self", + "target": "test-ci--DemoApplicationTest8", + }, + ], + "executor": "nx:noop", + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java", + ], + "metadata": { + "description": "Runs Gradle Tests in CI", + "nonAtomizedTarget": "test", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest10": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest10", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest2": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest2", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest3": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest3", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest4": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest4", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest5": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest5", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest6": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest6", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest7": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest7", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest8": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest8", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + "test-ci--DemoApplicationTest9": { + "cache": true, + "command": "./gradlew :application:test --tests DemoApplicationTest9", + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar", + ], + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java", + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java in CI", + "technologies": [ + "Gradle", + ], + }, + "options": { + "cwd": "proj", + }, + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin", + ], + }, + }, + }, + }, + }, + ], +] +`; + +exports[`@nx/gradle/plugin/nodes should not create nodes with atomized tests targets based on gradle if ciTargetName is not specified 1`] = ` +[ + [ + "proj/application/build.gradle", + { + "projects": { + "proj/application": { + "metadata": { + "targetGroups": { + "verification": [ + "ci", + ], + }, + "technologies": [ + "Gradle", + ], + }, + "name": "application", + "root": "proj/application", + "targets": {}, + }, + }, + }, + ], +] +`; diff --git a/packages/gradle/src/plugin/dependencies.ts b/packages/gradle/src/plugin/dependencies.ts index d306713064227..426844eac40c5 100644 --- a/packages/gradle/src/plugin/dependencies.ts +++ b/packages/gradle/src/plugin/dependencies.ts @@ -2,143 +2,48 @@ import { CreateDependencies, CreateDependenciesContext, DependencyType, - FileMap, - RawProjectGraphDependency, + StaticDependency, validateDependency, + workspaceRoot, } from '@nx/devkit'; -import { readFileSync } from 'node:fs'; -import { basename, dirname } from 'node:path'; +import { relative } from 'node:path'; -import { getCurrentGradleReport } from '../utils/get-gradle-report'; -import { GRADLE_BUILD_FILES } from '../utils/split-config-files'; -import { newLineSeparator } from '../utils/get-project-report-lines'; +import { getCurrentNodesReport } from './utils/get-nodes-from-gradle-plugin'; export const createDependencies: CreateDependencies = async ( _, context: CreateDependenciesContext ) => { - const gradleFiles: string[] = findGradleFiles(context.filesToProcess); - if (gradleFiles.length === 0) { - return []; - } - - const gradleDependenciesStart = performance.mark('gradleDependencies:start'); - const { - gradleFileToGradleProjectMap, - gradleProjectNameToProjectRootMap, - buildFileToDepsMap, - gradleProjectToChildProjects, - } = getCurrentGradleReport(); - const dependencies: Set = new Set(); - - for (const gradleFile of gradleFiles) { - const gradleProject = gradleFileToGradleProjectMap.get(gradleFile); - const projectName = Object.values(context.projects).find( - (project) => project.root === dirname(gradleFile) - )?.name; - const depsFile = buildFileToDepsMap.get(gradleFile); - - if (projectName && depsFile) { - processGradleDependencies( - depsFile, - gradleProjectNameToProjectRootMap, - projectName, - gradleFile, - context, - dependencies - ); - } - gradleProjectToChildProjects.get(gradleProject)?.forEach((childProject) => { - if (childProject) { - const dependency: RawProjectGraphDependency = { - source: projectName as string, - target: childProject, - type: DependencyType.static, - sourceFile: gradleFile, - }; - validateDependency(dependency, context); - dependencies.add(dependency); + const { dependencies: dependenciesFromReport } = getCurrentNodesReport(); + + const dependencies: Array = []; + dependenciesFromReport.forEach((dependencyFromPlugin: StaticDependency) => { + try { + const source = + relative(workspaceRoot, dependencyFromPlugin.source) || '.'; + const sourceProjectName = + Object.values(context.projects).find( + (project) => source === project.root + )?.name ?? dependencyFromPlugin.source; + const target = + relative(workspaceRoot, dependencyFromPlugin.target) || '.'; + const targetProjectName = + Object.values(context.projects).find( + (project) => target === project.root + )?.name ?? dependencyFromPlugin.target; + if (!sourceProjectName || !targetProjectName) { + return; } - }); - } - - const gradleDependenciesEnd = performance.mark('gradleDependencies:end'); - performance.measure( - 'gradleDependencies', - gradleDependenciesStart.name, - gradleDependenciesEnd.name - ); - - return Array.from(dependencies); + const dependency: StaticDependency = { + source: sourceProjectName, + target: targetProjectName, + type: DependencyType.static, + sourceFile: relative(workspaceRoot, dependencyFromPlugin.sourceFile), + }; + validateDependency(dependency, context); + dependencies.push(dependency); + } catch {} // ignore invalid dependencies + }); + + return dependencies; }; - -function findGradleFiles(fileMap: FileMap): string[] { - const gradleFiles: string[] = []; - - for (const [_, files] of Object.entries(fileMap.projectFileMap)) { - for (const file of files) { - if (GRADLE_BUILD_FILES.has(basename(file.file))) { - gradleFiles.push(file.file); - } - } - } - - return gradleFiles; -} - -export function processGradleDependencies( - depsFile: string, - gradleProjectNameToProjectRoot: Map, - sourceProjectName: string, - gradleFile: string, - context: CreateDependenciesContext, - dependencies: Set -): void { - const lines = readFileSync(depsFile).toString().split(newLineSeparator); - let inDeps = false; - for (const line of lines) { - if ( - line.startsWith('implementationDependenciesMetadata') || - line.startsWith('compileClasspath') - ) { - inDeps = true; - continue; - } - - if (inDeps) { - if (line === '') { - inDeps = false; - continue; - } - const [indents, dep] = line.split('--- '); - if (indents === '\\' || indents === '+') { - let gradleProjectName: string | undefined; - if (dep.startsWith('project ')) { - gradleProjectName = dep - .substring('project '.length) - .replace(/ \(n\)$/, '') - .trim(); - } else if (dep.includes('-> project')) { - const [_, projectName] = dep.split('-> project'); - gradleProjectName = projectName.trim(); - } - const targetProjectRoot = gradleProjectNameToProjectRoot.get( - gradleProjectName - ) as string; - const targetProjectName = Object.values(context.projects).find( - (project) => project.root === targetProjectRoot - )?.name; - if (targetProjectName) { - const dependency: RawProjectGraphDependency = { - source: sourceProjectName, - target: targetProjectName, - type: DependencyType.static, - sourceFile: gradleFile, - }; - validateDependency(dependency, context); - dependencies.add(dependency); - } - } - } - } -} diff --git a/packages/gradle/src/plugin/nodes.spec.ts b/packages/gradle/src/plugin/nodes.spec.ts index 2860777af9a02..d76a4929e459c 100644 --- a/packages/gradle/src/plugin/nodes.spec.ts +++ b/packages/gradle/src/plugin/nodes.spec.ts @@ -1,20 +1,20 @@ -import { CreateNodesContext } from '@nx/devkit'; - +import { CreateNodesContext, readJsonFile } from '@nx/devkit'; +import { join } from 'path'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; -import { type GradleReport } from '../utils/get-gradle-report'; +import { type NodesReport } from './utils/get-nodes-from-gradle-plugin'; -let gradleReport: GradleReport; -jest.mock('../utils/get-gradle-report', () => { +let gradleReport: NodesReport; +jest.mock('./utils/get-nodes-from-gradle-plugin', () => { return { GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']), - populateGradleReport: jest.fn().mockImplementation(() => void 0), - getCurrentGradleReport: jest.fn().mockImplementation(() => gradleReport), + populateNodes: jest.fn().mockImplementation(() => void 0), + getCurrentNodesReport: jest.fn().mockImplementation(() => gradleReport), }; }); import { createNodesV2 } from './nodes'; -describe('@nx/gradle/plugin', () => { +describe('@nx/gradle/plugin/nodes', () => { let createNodesFunction = createNodesV2[1]; let context: CreateNodesContext; let tempFs: TempFs; @@ -22,32 +22,9 @@ describe('@nx/gradle/plugin', () => { beforeEach(async () => { tempFs = new TempFs('test'); - gradleReport = { - gradleFileToGradleProjectMap: new Map([ - ['proj/build.gradle', 'proj'], - ]), - buildFileToDepsMap: new Map(), - gradleFileToOutputDirsMap: new Map>([ - ['proj/build.gradle', new Map([['build', 'build']])], - ]), - gradleProjectToTasksMap: new Map>([ - ['proj', new Set(['test'])], - ]), - gradleProjectToTasksTypeMap: new Map>([ - [ - 'proj', - new Map([ - ['test', 'Verification'], - ['build', 'Build'], - ]), - ], - ]), - gradleProjectToProjectName: new Map([['proj', 'proj']]), - gradleProjectNameToProjectRootMap: new Map([ - ['proj', 'proj'], - ]), - gradleProjectToChildProjects: new Map(), - }; + gradleReport = readJsonFile( + join(__dirname, 'utils/__mocks__/gradle_tutorial.json') + ); cwd = process.cwd(); process.chdir(tempFs.tempDir); context = { @@ -81,507 +58,49 @@ describe('@nx/gradle/plugin', () => { context ); - expect(results).toMatchInlineSnapshot(` - [ - [ - "proj/build.gradle", - { - "projects": { - "proj": { - "metadata": { - "targetGroups": { - "Verification": [ - "test", - ], - }, - "technologies": [ - "gradle", - ], - }, - "name": "proj", - "projectType": "application", - "targets": { - "test": { - "cache": true, - "command": "./gradlew proj:test", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], - "metadata": { - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - }, - }, - }, - }, - }, - ], - ] - `); + expect(results).toMatchSnapshot(); }); - it('should create nodes include subprojects tasks', async () => { + it('should create nodes based on gradle for nested project root', async () => { + gradleReport = readJsonFile( + join(__dirname, '/utils/__mocks__/gradle_composite.json') + ); + await tempFs.createFiles({ + 'nested/nested/proj/build.gradle': ``, + }); + const results = await createNodesFunction( - ['proj/build.gradle'], + ['nested/nested/proj/build.gradle'], { buildTargetName: 'build', - includeSubprojectsTasks: true, }, context ); - expect(results).toMatchInlineSnapshot(` - [ - [ - "proj/build.gradle", - { - "projects": { - "proj": { - "metadata": { - "targetGroups": { - "Build": [ - "build", - ], - "Verification": [ - "test", - ], - }, - "technologies": [ - "gradle", - ], - }, - "name": "proj", - "projectType": "application", - "targets": { - "build": { - "cache": true, - "command": "./gradlew proj:build", - "dependsOn": [ - "^build", - "classes", - "test", - ], - "inputs": [ - "production", - "^production", - ], - "metadata": { - "help": { - "command": "./gradlew help --task proj:build", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - "outputs": [ - "build", - ], - }, - "test": { - "cache": true, - "command": "./gradlew proj:test", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], - "metadata": { - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - }, - }, - }, - }, - }, - ], - ] - `); + expect(results).toMatchSnapshot(); }); - it('should create nodes based on gradle for nested project root', async () => { - gradleReport = { - gradleFileToGradleProjectMap: new Map([ - ['nested/nested/proj/build.gradle', 'proj'], - ]), - buildFileToDepsMap: new Map(), - gradleFileToOutputDirsMap: new Map>([ - ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], - ]), - gradleProjectToTasksMap: new Map>([ - ['proj', new Set(['test'])], - ]), - gradleProjectToTasksTypeMap: new Map>([ - ['proj', new Map([['test', 'Verification']])], - ]), - gradleProjectToProjectName: new Map([['proj', 'proj']]), - gradleProjectNameToProjectRootMap: new Map([ - ['proj', 'proj'], - ]), - gradleProjectToChildProjects: new Map(), - }; - await tempFs.createFiles({ - 'nested/nested/proj/build.gradle': ``, - }); - + it('should create nodes with atomized tests targets based on gradle if ciTargetName is specified', async () => { const results = await createNodesFunction( - ['nested/nested/proj/build.gradle'], + ['proj/application/build.gradle'], { buildTargetName: 'build', + ciTargetName: 'test-ci', }, context ); - expect(results).toMatchInlineSnapshot(` - [ - [ - "nested/nested/proj/build.gradle", - { - "projects": { - "nested/nested/proj": { - "metadata": { - "targetGroups": { - "Verification": [ - "test", - ], - }, - "technologies": [ - "gradle", - ], - }, - "name": "proj", - "projectType": "application", - "targets": { - "test": { - "cache": true, - "command": "./gradlew proj:test", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], - "metadata": { - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - }, - }, - }, - }, - }, - ], - ] - `); + expect(results).toMatchSnapshot(); }); - describe('with atomized tests targets', () => { - beforeEach(async () => { - gradleReport = { - gradleFileToGradleProjectMap: new Map([ - ['nested/nested/proj/build.gradle', 'proj'], - ]), - buildFileToDepsMap: new Map(), - gradleFileToOutputDirsMap: new Map>([ - ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], - ]), - gradleProjectToTasksMap: new Map>([ - ['proj', new Set(['test'])], - ]), - gradleProjectToTasksTypeMap: new Map>([ - ['proj', new Map([['test', 'Test']])], - ]), - gradleProjectToProjectName: new Map([['proj', 'proj']]), - gradleProjectNameToProjectRootMap: new Map([ - ['proj', 'proj'], - ]), - gradleProjectToChildProjects: new Map(), - }; - await tempFs.createFiles({ - 'nested/nested/proj/build.gradle': ``, - }); - await tempFs.createFiles({ - 'proj/src/test/java/test/rootTest.java': ``, - }); - await tempFs.createFiles({ - 'nested/nested/proj/src/test/java/test/aTest.java': ``, - }); - await tempFs.createFiles({ - 'nested/nested/proj/src/test/java/test/bTest.java': ``, - }); - await tempFs.createFiles({ - 'nested/nested/proj/src/test/java/test/cTests.java': ``, - }); - }); - - it('should create nodes with atomized tests targets based on gradle for nested project root', async () => { - const results = await createNodesFunction( - [ - 'nested/nested/proj/build.gradle', - 'proj/src/test/java/test/rootTest.java', - 'nested/nested/proj/src/test/java/test/aTest.java', - 'nested/nested/proj/src/test/java/test/bTest.java', - 'nested/nested/proj/src/test/java/test/cTests.java', - ], - { - buildTargetName: 'build', - ciTargetName: 'test-ci', - }, - context - ); - - expect(results).toMatchInlineSnapshot(` - [ - [ - "nested/nested/proj/build.gradle", - { - "projects": { - "nested/nested/proj": { - "metadata": { - "targetGroups": { - "Test": [ - "test-ci--aTest", - "test-ci--bTest", - "test-ci--cTests", - "test-ci", - "test", - ], - }, - "technologies": [ - "gradle", - ], - }, - "name": "proj", - "projectType": "application", - "targets": { - "test": { - "cache": false, - "command": "./gradlew proj:test", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], - "metadata": { - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - }, - "test-ci": { - "cache": true, - "dependsOn": [ - { - "params": "forward", - "projects": "self", - "target": "test-ci--aTest", - }, - { - "params": "forward", - "projects": "self", - "target": "test-ci--bTest", - }, - { - "params": "forward", - "projects": "self", - "target": "test-ci--cTests", - }, - ], - "executor": "nx:noop", - "inputs": [ - "default", - "^production", - ], - "metadata": { - "description": "Runs Gradle Tests in CI", - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "nonAtomizedTarget": "test", - "technologies": [ - "gradle", - ], - }, - }, - "test-ci--aTest": { - "cache": true, - "command": "./gradlew proj:test --tests aTest", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], - "metadata": { - "description": "Runs Gradle test nested/nested/proj/src/test/java/test/aTest.java in CI", - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - }, - "test-ci--bTest": { - "cache": true, - "command": "./gradlew proj:test --tests bTest", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], - "metadata": { - "description": "Runs Gradle test nested/nested/proj/src/test/java/test/bTest.java in CI", - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - }, - "test-ci--cTests": { - "cache": true, - "command": "./gradlew proj:test --tests cTests", - "dependsOn": [ - "testClasses", - ], - "inputs": [ - "default", - "^production", - ], - "metadata": { - "description": "Runs Gradle test nested/nested/proj/src/test/java/test/cTests.java in CI", - "help": { - "command": "./gradlew help --task proj:test", - "example": { - "options": { - "args": [ - "--rerun", - ], - }, - }, - }, - "technologies": [ - "gradle", - ], - }, - "options": { - "cwd": ".", - }, - }, - }, - }, - }, - }, - ], - ] - `); - }); + it('should not create nodes with atomized tests targets based on gradle if ciTargetName is not specified', async () => { + const results = await createNodesFunction( + ['proj/application/build.gradle'], + { + buildTargetName: 'build', + }, + context + ); + expect(results).toMatchSnapshot(); }); }); diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts index cbedf477d1b37..e2854632d840a 100644 --- a/packages/gradle/src/plugin/nodes.ts +++ b/packages/gradle/src/plugin/nodes.ts @@ -1,61 +1,38 @@ import { - CreateNodes, CreateNodesV2, CreateNodesContext, ProjectConfiguration, - TargetConfiguration, createNodesFromFiles, readJsonFile, writeJsonFile, CreateNodesFunction, - logger, + joinPathFragments, + workspaceRoot, } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { existsSync } from 'node:fs'; -import { basename, dirname, join } from 'node:path'; +import { dirname, join } from 'node:path'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; -import { findProjectForPath } from 'nx/src/devkit-internals'; -import { - populateGradleReport, - getCurrentGradleReport, - GradleReport, -} from '../utils/get-gradle-report'; import { hashObject } from 'nx/src/hasher/file-hasher'; import { gradleConfigAndTestGlob, - gradleConfigGlob, splitConfigFiles, } from '../utils/split-config-files'; -import { getGradleExecFile, findGraldewFile } from '../utils/exec-gradle'; - -const cacheableTaskType = new Set(['Build', 'Verification']); -const dependsOnMap = { - build: ['^build', 'classes', 'test'], - testClasses: ['classes'], - test: ['testClasses'], - classes: ['^classes'], -}; - -interface GradleTask { - type: string; - name: string; -} +import { + getCurrentNodesReport, + populateNodes, +} from './utils/get-nodes-from-gradle-plugin'; export interface GradlePluginOptions { - includeSubprojectsTasks?: boolean; // default is false, show all gradle tasks in the project - ciTargetName?: string; testTargetName?: string; - classesTargetName?: string; - buildTargetName?: string; + ciTargetName?: string; [taskTargetName: string]: string | undefined | boolean; } function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions { options ??= {}; options.testTargetName ??= 'test'; - options.classesTargetName ??= 'classes'; - options.buildTargetName ??= 'build'; return options; } @@ -72,8 +49,7 @@ export function writeTargetsToCache(cachePath: string, results: GradleTargets) { export const createNodesV2: CreateNodesV2 = [ gradleConfigAndTestGlob, async (files, options, context) => { - const { buildFiles, projectRoots, gradlewFiles, testFiles } = - splitConfigFiles(files); + const { buildFiles, gradlewFiles } = splitConfigFiles(files); const optionsHash = hashObject(options); const cachePath = join( workspaceDataDirectory, @@ -81,23 +57,15 @@ export const createNodesV2: CreateNodesV2 = [ ); const targetsCache = readTargetsCache(cachePath); - await populateGradleReport( + await populateNodes( context.workspaceRoot, gradlewFiles.map((f) => join(context.workspaceRoot, f)) ); - const gradleReport = getCurrentGradleReport(); - const gradleProjectRootToTestFilesMap = getGradleProjectRootToTestFilesMap( - testFiles, - projectRoots - ); + const { nodes } = getCurrentNodesReport(); try { return createNodesFromFiles( - makeCreateNodesForGradleConfigFile( - gradleReport, - targetsCache, - gradleProjectRootToTestFilesMap - ), + makeCreateNodesForGradleConfigFile(nodes, targetsCache), buildFiles, options, context @@ -110,9 +78,8 @@ export const createNodesV2: CreateNodesV2 = [ export const makeCreateNodesForGradleConfigFile = ( - gradleReport: GradleReport, - targetsCache: GradleTargets = {}, - gradleProjectRootToTestFilesMap: Record = {} + projects: Record>, + targetsCache: GradleTargets = {} ): CreateNodesFunction => async ( gradleFilePath, @@ -127,309 +94,64 @@ export const makeCreateNodesForGradleConfigFile = options ?? {}, context ); - targetsCache[hash] ??= await createGradleProject( - gradleReport, - gradleFilePath, - options, - context, - gradleProjectRootToTestFilesMap[projectRoot] - ); + targetsCache[hash] ??= + projects[projectRoot] ?? + projects[joinPathFragments(workspaceRoot, projectRoot)]; const project = targetsCache[hash]; if (!project) { return {}; } - return { - projects: { - [projectRoot]: project, - }, - }; - }; - -/** - @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead. - This function will change to the v2 function in Nx 20. - */ -export const createNodes: CreateNodes = [ - gradleConfigGlob, - async (buildFile, options, context) => { - logger.warn( - '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' - ); - const { gradlewFiles } = splitConfigFiles(context.configFiles); - await populateGradleReport(context.workspaceRoot, gradlewFiles); - const gradleReport = getCurrentGradleReport(); - const internalCreateNodes = - makeCreateNodesForGradleConfigFile(gradleReport); - return await internalCreateNodes(buildFile, options, context); - }, -]; - -async function createGradleProject( - gradleReport: GradleReport, - gradleFilePath: string, - options: GradlePluginOptions | undefined, - context: CreateNodesContext, - testFiles = [] -) { - try { - const { - gradleProjectToTasksTypeMap, - gradleProjectToTasksMap, - gradleFileToOutputDirsMap, - gradleFileToGradleProjectMap, - gradleProjectToProjectName, - } = gradleReport; - - const gradleProject = gradleFileToGradleProjectMap.get( - gradleFilePath - ) as string; - const projectName = gradleProjectToProjectName.get(gradleProject); - if (!projectName) { - return; - } - const tasksTypeMap: Map = gradleProjectToTasksTypeMap.get( - gradleProject - ) as Map; - const tasksSet = gradleProjectToTasksMap.get(gradleProject) as Set; - let tasks: GradleTask[] = []; - tasksSet.forEach((taskName) => { - tasks.push({ - type: tasksTypeMap.get(taskName) as string, - name: taskName, - }); - }); - if (options.includeSubprojectsTasks) { - tasksTypeMap.forEach((taskType, taskName) => { - if (!tasksSet.has(taskName)) { - tasks.push({ - type: taskType, - name: taskName, - }); + let targets = {}; + // rename target name if it is provided + Object.entries(project.targets).forEach(([taskName, target]) => { + let targetName = options?.[`${taskName}TargetName`] as string; + if (taskName.startsWith('ci')) { + if (options.ciTargetName) { + targetName = taskName.replace('ci', options.ciTargetName); + targets[targetName] = target; + if (targetName === options.ciTargetName) { + target.metadata.nonAtomizedTarget = options.testTargetName; + target.dependsOn.forEach((dep) => { + if (typeof dep !== 'string' && dep.target.startsWith('ci')) { + dep.target = dep.target.replace('ci', options.ciTargetName); + } + }); + } } - }); - } - - const outputDirs = gradleFileToOutputDirsMap.get(gradleFilePath) as Map< - string, - string - >; - - const { targets, targetGroups } = await createGradleTargets( - tasks, - options, - context, - outputDirs, - gradleProject, - gradleFilePath, - testFiles - ); - const project: Partial = { - name: projectName, - projectType: 'application', - targets, - metadata: { - targetGroups, - technologies: ['gradle'], - }, - }; - - return project; - } catch (e) { - console.error(e); - return undefined; - } -} - -async function createGradleTargets( - tasks: GradleTask[], - options: GradlePluginOptions | undefined, - context: CreateNodesContext, - outputDirs: Map, - gradleProject: string, - gradleBuildFilePath: string, - testFiles: string[] = [] -): Promise<{ - targetGroups: Record; - targets: Record; -}> { - const inputsMap = createInputsMap(context); - const gradlewFileDirectory = dirname( - findGraldewFile(gradleBuildFilePath, context.workspaceRoot) - ); - - const targets: Record = {}; - const targetGroups: Record = {}; - for (const task of tasks) { - const targetName = options?.[`${task.name}TargetName`] ?? task.name; - - let outputs = [outputDirs.get(task.name)].filter(Boolean); - if (task.name === 'test') { - outputs = [ - outputDirs.get('testReport'), - outputDirs.get('testResults'), - ].filter(Boolean); - getTestCiTargets( - testFiles, - gradleProject, - targetName as string, - options.ciTargetName, - inputsMap['test'], - outputs, - task.type, - targets, - targetGroups, - gradlewFileDirectory - ); - } - - const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}${ - task.name - }`; - - targets[targetName as string] = { - command: `${getGradleExecFile()} ${taskCommandToRun}`, - options: { - cwd: gradlewFileDirectory, - }, - cache: cacheableTaskType.has(task.type), - inputs: inputsMap[task.name], - dependsOn: dependsOnMap[task.name], - metadata: { - technologies: ['gradle'], - help: { - command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, - example: { - options: { - args: ['--rerun'], - }, - }, - }, - }, - ...(outputs && outputs.length ? { outputs } : {}), - }; - - if (task.type) { - if (!targetGroups[task.type]) { - targetGroups[task.type] = []; + } else if (targetName) { + targets[targetName] = target; + } else { + targets[taskName] = target; } - targetGroups[task.type].push(targetName as string); - } - } - return { targetGroups, targets }; -} - -function createInputsMap( - context: CreateNodesContext -): Record { - const namedInputs = context.nxJsonConfiguration.namedInputs; - return { - build: namedInputs?.production - ? ['production', '^production'] - : ['default', '^default'], - test: ['default', namedInputs?.production ? '^production' : '^default'], - classes: namedInputs?.production - ? ['production', '^production'] - : ['default', '^default'], - }; -} - -function getTestCiTargets( - testFiles: string[], - gradleProject: string, - testTargetName: string, - ciTargetName: string, - inputs: TargetConfiguration['inputs'], - outputs: string[], - targetGroupName: string, - targets: Record, - targetGroups: Record, - gradlewFileDirectory: string -): void { - if (!testFiles || testFiles.length === 0 || !ciTargetName) { - return; - } - const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}test`; - - if (!targetGroups[targetGroupName]) { - targetGroups[targetGroupName] = []; - } + }); + project.targets = targets; + + // rename target names in target groups if it is provided + Object.entries(project.metadata?.targetGroups).forEach( + ([groupName, group]) => { + let targetGroup = group + .map((taskName) => { + let targetName = options?.[`${taskName}TargetName`] as string; + if (targetName) { + return targetName; + } else if (options.ciTargetName && taskName.startsWith('ci')) { + targetName = taskName.replace('ci', options.ciTargetName); + return targetName; + } else { + return taskName; + } + }) + .filter(Boolean); + project.metadata.targetGroups[groupName] = targetGroup; + } + ); - const dependsOn: TargetConfiguration['dependsOn'] = []; - testFiles.forEach((testFile) => { - const testName = basename(testFile).split('.')[0]; - const targetName = ciTargetName + '--' + testName; + project.root = projectRoot; - targets[targetName] = { - command: `${getGradleExecFile()} ${taskCommandToRun} --tests ${testName}`, - options: { - cwd: gradlewFileDirectory, - }, - cache: true, - inputs, - dependsOn: dependsOnMap['test'], - metadata: { - technologies: ['gradle'], - description: `Runs Gradle test ${testFile} in CI`, - help: { - command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, - example: { - options: { - args: ['--rerun'], - }, - }, - }, + return { + projects: { + [projectRoot]: project, }, - ...(outputs && outputs.length > 0 ? { outputs } : {}), }; - targetGroups[targetGroupName].push(targetName); - dependsOn.push({ - target: targetName, - projects: 'self', - params: 'forward', - }); - }); - - targets[ciTargetName] = { - executor: 'nx:noop', - cache: true, - inputs, - dependsOn: dependsOn, - ...(outputs && outputs.length > 0 ? { outputs } : {}), - metadata: { - technologies: ['gradle'], - description: 'Runs Gradle Tests in CI', - nonAtomizedTarget: testTargetName, - help: { - command: `${getGradleExecFile()} help --task ${taskCommandToRun}`, - example: { - options: { - args: ['--rerun'], - }, - }, - }, - }, }; - targetGroups[targetGroupName].push(ciTargetName); -} - -function getGradleProjectRootToTestFilesMap( - testFiles: string[], - projectRoots: string[] -): Record | undefined { - if (testFiles.length === 0 || projectRoots.length === 0) { - return; - } - const roots = new Map(projectRoots.map((root) => [root, root])); - const testFilesToGradleProjectMap: Record = {}; - testFiles.forEach((testFile) => { - const projectRoot = findProjectForPath(testFile, roots); - if (projectRoot) { - if (!testFilesToGradleProjectMap[projectRoot]) { - testFilesToGradleProjectMap[projectRoot] = []; - } - testFilesToGradleProjectMap[projectRoot].push(testFile); - } - }); - return testFilesToGradleProjectMap; -} diff --git a/packages/gradle/src/plugin/utils/__mocks__/gradle_composite.json b/packages/gradle/src/plugin/utils/__mocks__/gradle_composite.json new file mode 100644 index 0000000000000..736e82469f445 --- /dev/null +++ b/packages/gradle/src/plugin/utils/__mocks__/gradle_composite.json @@ -0,0 +1,38 @@ +{ + "nodes": { + "nested/nested/proj": { + "targets": { + "buildEnvironment": { + "cache": true, + "metadata": { + "description": "Displays all buildscript dependencies declared in root project \u0027my-composite\u0027.", + "technologies": ["Gradle"] + }, + "command": "./gradlew :buildEnvironment", + "options": { + "cwd": "nested/nested/proj" + } + } + }, + "metadata": { + "targetGroups": { + "help": ["buildEnvironment"] + }, + "technologies": ["Gradle"] + }, + "name": "my-composite" + } + }, + "dependencies": [ + { + "source": "nested/nested/proj", + "target": "projectRoot/my-app", + "sourceFile": "projectRoot/build.gradle.kts" + }, + { + "source": "nested/nested/proj", + "target": "projectRoot/my-utils", + "sourceFile": "projectRoot/build.gradle.kts" + } + ] +} diff --git a/packages/gradle/src/plugin/utils/__mocks__/gradle_tutorial.json b/packages/gradle/src/plugin/utils/__mocks__/gradle_tutorial.json new file mode 100644 index 0000000000000..2ff1392f43934 --- /dev/null +++ b/packages/gradle/src/plugin/utils/__mocks__/gradle_tutorial.json @@ -0,0 +1,344 @@ +{ + "nodes": { + "proj": { + "targets": { + "buildEnvironment": { + "cache": true, + "metadata": { + "description": "Displays all buildscript dependencies declared in root project \u0027gradle-tutorial\u0027.", + "technologies": ["Gradle"] + }, + "command": "./gradlew :buildEnvironment", + "options": { "cwd": "proj" } + } + }, + "metadata": { + "targetGroups": { + "help": ["buildEnvironment"] + }, + "technologies": ["Gradle"] + }, + "name": "gradle-tutorial" + }, + "proj/application": { + "targets": { + "ci--DemoApplicationTest10": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest10", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest7": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest7", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest6": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest6", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest3": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest3", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest2": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest2", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest9": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest9", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest5": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest5", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest4": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest4", + "options": { "cwd": "proj" } + }, + "ci--DemoApplicationTest8": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + "application:classes", + "application:compileJava", + "library:jar" + ], + "metadata": { + "description": "Runs Gradle test proj/application/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java in CI", + "technologies": ["Gradle"] + }, + "command": "./gradlew :application:test --tests DemoApplicationTest8", + "options": { "cwd": "proj" } + }, + "ci": { + "inputs": [ + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest10.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest7.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest6.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest3.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest2.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest9.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest5.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest4.java", + "{projectRoot}/src/test/java/com/example/multimodule/application/DemoApplicationTest8.java" + ], + "outputs": [ + "{projectRoot}/build/classes/java/test", + "{projectRoot}/build/generated/sources/annotationProcessor/java/test", + "{projectRoot}/build/generated/sources/headers/java/test", + "{projectRoot}/build/tmp/compileTestJava/previous-compilation-data.bin" + ], + "cache": true, + "dependsOn": [ + { + "target": "ci--DemoApplicationTest10", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest7", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest6", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest3", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest2", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest9", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest5", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest4", + "projects": "self", + "params": "forward" + }, + { + "target": "ci--DemoApplicationTest8", + "projects": "self", + "params": "forward" + } + ], + "metadata": { + "description": "Runs Gradle Tests in CI", + "technologies": ["Gradle"] + }, + "options": { "cwd": "proj" }, + "executor": "nx:noop" + } + }, + "metadata": { + "targetGroups": { + "verification": ["ci"] + }, + "technologies": ["Gradle"] + }, + "name": "application" + } + } +} diff --git a/packages/gradle/src/plugin/utils/get-create-nodes-lines.ts b/packages/gradle/src/plugin/utils/get-create-nodes-lines.ts new file mode 100644 index 0000000000000..4529aea3ca9d9 --- /dev/null +++ b/packages/gradle/src/plugin/utils/get-create-nodes-lines.ts @@ -0,0 +1,47 @@ +import { AggregateCreateNodesError, logger, workspaceRoot } from '@nx/devkit'; +import { execGradleAsync, newLineSeparator } from '../../utils/exec-gradle'; +import { existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { cacheDirectoryForWorkspace } from 'nx/src/utils/cache-directory'; + +export async function getCreateNodesLines(gradlewFile: string) { + let createNodesBuffer: Buffer; + + // if there is no build.gradle or build.gradle.kts file, we cannot run the createNodes task + if ( + !existsSync(join(dirname(gradlewFile), 'build.gradle')) && + !existsSync(join(dirname(gradlewFile), 'build.gradle.kts')) + ) { + logger.warn( + `Could not find build file near ${gradlewFile}. Please run 'nx generate @nx/gradle:init' to generate the necessary tasks.` + ); + return []; + } + + try { + createNodesBuffer = await execGradleAsync(gradlewFile, [ + 'createNodes', + '--outputDirectory', + cacheDirectoryForWorkspace(workspaceRoot), + '--workspaceRoot', + workspaceRoot, + ]); + } catch (e) { + throw new AggregateCreateNodesError( + [ + [ + gradlewFile, + new Error( + `Could not run 'createNodes' task using ${gradlewFile}. Please run 'nx generate @nx/gradle:init' to generate the necessary tasks. ${e.message}`, + { cause: e } + ), + ], + ], + [] + ); + } + return createNodesBuffer + .toString() + .split(newLineSeparator) + .filter((line) => line.trim() !== ''); +} diff --git a/packages/gradle/src/plugin/utils/get-nodes-from-gradle-plugin.ts b/packages/gradle/src/plugin/utils/get-nodes-from-gradle-plugin.ts new file mode 100644 index 0000000000000..4a1b9aa80f6fa --- /dev/null +++ b/packages/gradle/src/plugin/utils/get-nodes-from-gradle-plugin.ts @@ -0,0 +1,162 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; + +import { + AggregateCreateNodesError, + ProjectConfiguration, + readJsonFile, + StaticDependency, + writeJsonFile, +} from '@nx/devkit'; + +import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context'; +import { gradleConfigAndTestGlob } from '../../utils/split-config-files'; +import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; +import { getCreateNodesLines } from './get-create-nodes-lines'; + +// the output json file from the gradle plugin +export interface NodesReport { + nodes: { + [appRoot: string]: Partial; + }; + dependencies: Array; +} + +export interface NodesReportCache extends NodesReport { + hash: string; +} + +function readNodesReportCache( + cachePath: string, + hash: string +): NodesReport | undefined { + const nodesReportCache: Partial = existsSync(cachePath) + ? readJsonFile(cachePath) + : undefined; + if (!nodesReportCache || nodesReportCache.hash !== hash) { + return; + } + return nodesReportCache as NodesReport; +} + +export function writeNodesReportToCache( + cachePath: string, + results: NodesReport +) { + let nodesReportJson: NodesReportCache = { + hash: gradleCurrentConfigHash, + ...results, + }; + + writeJsonFile(cachePath, nodesReportJson); +} + +let nodesReportCache: NodesReport; +let gradleCurrentConfigHash: string; +let nodesReportCachePath: string = join( + workspaceDataDirectory, + 'gradle-nodes.hash' +); + +export function getCurrentNodesReport() { + if (!nodesReportCache) { + throw new AggregateCreateNodesError( + [ + [ + null, + new Error( + `Expected cached gradle report. Please open an issue at https://github.com/nrwl/nx/issues/new/choose` + ), + ], + ], + [] + ); + } + return nodesReportCache; +} + +/** + * This function populates the gradle report cache. + * For each gradlew file, it runs the `projectReportAll` task and processes the output. + * If `projectReportAll` fails, it runs the `projectReport` task instead. + * It will throw an error if both tasks fail. + * It will accumulate the output of all gradlew files. + * @param workspaceRoot + * @param gradlewFiles absolute paths to all gradlew files in the workspace + * @returns Promise + */ +export async function populateNodes( + workspaceRoot: string, + gradlewFiles: string[] +): Promise { + const gradleConfigHash = await hashWithWorkspaceContext(workspaceRoot, [ + gradleConfigAndTestGlob, + ]); + nodesReportCache ??= readNodesReportCache( + nodesReportCachePath, + gradleConfigHash + ); + if ( + nodesReportCache && + (!gradleCurrentConfigHash || gradleConfigHash === gradleCurrentConfigHash) + ) { + return; + } + + const gradleCreateNodesStart = performance.mark('gradleCreateNodes:start'); + + const createNodesLines = await gradlewFiles.reduce( + async ( + createNodesLines: Promise, + gradlewFile: string + ): Promise => { + const allLines = await createNodesLines; + const currentLines = await getCreateNodesLines(gradlewFile); + return [...allLines, ...currentLines]; + }, + Promise.resolve([]) + ); + + const gradleCreateNodesEnd = performance.mark('gradleCreateNodes:end'); + performance.measure( + 'gradleCreateNodes', + gradleCreateNodesStart.name, + gradleCreateNodesEnd.name + ); + gradleCurrentConfigHash = gradleConfigHash; + nodesReportCache = processCreateNodes(createNodesLines); + writeNodesReportToCache(nodesReportCachePath, nodesReportCache); +} + +export function processCreateNodes(createNodesLines: string[]): NodesReport { + let index = 0; + let nodesReportForAllProjects: NodesReport = { + nodes: {}, + dependencies: [], + }; + let dependencies: Array = []; + while (index < createNodesLines.length) { + const line = createNodesLines[index].trim(); + if (line.startsWith('> Task ') && line.endsWith(':createNodes')) { + while ( + index < createNodesLines.length && + !createNodesLines[index].includes('.json') + ) { + index++; + } + const file = createNodesLines[index]; + const nodesReportJson: NodesReport = readJsonFile(file); + nodesReportForAllProjects.nodes = { + ...nodesReportForAllProjects.nodes, + ...nodesReportJson.nodes, + }; + nodesReportForAllProjects.dependencies = [ + ...nodesReportJson.dependencies, + ...dependencies, + ]; + } + index++; + } + + return nodesReportForAllProjects; +} diff --git a/packages/gradle/src/utils/exec-gradle.ts b/packages/gradle/src/utils/exec-gradle.ts index d695737b84e34..fff588b6c2b47 100644 --- a/packages/gradle/src/utils/exec-gradle.ts +++ b/packages/gradle/src/utils/exec-gradle.ts @@ -3,6 +3,14 @@ import { ExecFileOptions, execFile } from 'node:child_process'; import { existsSync } from 'node:fs'; import { dirname, join } from 'node:path'; +export const fileSeparator = process.platform.startsWith('win') + ? 'file:///' + : 'file://'; + +export const newLineSeparator = process.platform.startsWith('win') + ? '\r\n' + : '\n'; + /** * For gradle command, it needs to be run from the directory of the gradle binary * @returns gradle binary file name diff --git a/packages/gradle/src/utils/versions.ts b/packages/gradle/src/utils/versions.ts index e268dc8f82dd3..ef7bc54b1ed13 100644 --- a/packages/gradle/src/utils/versions.ts +++ b/packages/gradle/src/utils/versions.ts @@ -1 +1,10 @@ +import { config as loadDotEnvFile } from 'dotenv'; +import { join } from 'path'; + export const nxVersion = require('../../package.json').version; + +const gradleNativeProperties = loadDotEnvFile({ + path: join(__dirname, `../../native/gradle.properties`), +}); + +export const gradlePluginVersion = gradleNativeProperties.parsed?.version;