diff --git a/e2e/gradle/src/utils/create-gradle-project.ts b/e2e/gradle/src/utils/create-gradle-project.ts index 1e4755082e1b7..3cdfe94b23fa0 100644 --- a/e2e/gradle/src/utils/create-gradle-project.ts +++ b/e2e/gradle/src/utils/create-gradle-project.ts @@ -3,6 +3,7 @@ import { isWindows, runCommand, tmpProjPath, + updateFile, } from '@nx/e2e/utils'; import { execSync } from 'child_process'; import { createFileSync, writeFileSync } from 'fs-extra'; @@ -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`, { @@ -60,4 +57,31 @@ export function createGradleProject( `{"name": "${addProjectJsonNamePrefix}utilities"}` ); } + + addLocalPluginManagement(`settings.gradle${type === 'kotlin' ? '.kts' : ''}`); + addLocalPluginManagement( + `buildSrc/settings.gradle${type === 'kotlin' ? '.kts' : ''}` + ); + + e2eConsoleLogger( + execSync(`${gradleCommand} publishToMavenLocal`, { + cwd: `${__dirname}/../../../../packages/gradle/native`, + }).toString() + ); +} + +function addLocalPluginManagement(file: string) { + updateFile( + file, + (content) => + `pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + // Add other repositories if needed + } +} +` + 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..f7583eca2985b --- /dev/null +++ b/packages/gradle/native/gradle.properties @@ -0,0 +1,6 @@ +# 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 +version=0.0.1 diff --git a/e2e/gradle/gradle/libs.versions.toml b/packages/gradle/native/gradle/libs.versions.toml similarity index 66% rename from e2e/gradle/gradle/libs.versions.toml rename to packages/gradle/native/gradle/libs.versions.toml index 4ac3234a6a7c3..387b04dd62a65 100644 --- a/e2e/gradle/gradle/libs.versions.toml +++ b/packages/gradle/native/gradle/libs.versions.toml @@ -1,2 +1,5 @@ # 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" } diff --git a/e2e/gradle/gradle/wrapper/gradle-wrapper.jar b/packages/gradle/native/gradle/wrapper/gradle-wrapper.jar similarity index 69% rename from e2e/gradle/gradle/wrapper/gradle-wrapper.jar rename to packages/gradle/native/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4b..a4b76b9530d66 100644 Binary files a/e2e/gradle/gradle/wrapper/gradle-wrapper.jar 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..be98ac8f43d21 --- /dev/null +++ b/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/CreateNodesTask.kt @@ -0,0 +1,192 @@ +package io.nx.gradle.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +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 + +data class GradleTargets(val targets: MutableMap, val targetGroups: MutableMap>) +data class Metadata(val targetGroups: MutableMap>, val technologies: Array, val description: String?) +data class ProjectConfiguration(val targets: MutableMap, val metadata: Metadata, val name: String) +data class Dependency(val source: String, val target: String, val sourceFile: String) +data class Node(val project: ProjectConfiguration, 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 = "" + + @TaskAction + fun action() { + val rootProjectDirectory = project.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() + project.getAllprojects().forEach { project -> + try { + // get dependencies of project + val dependencies = getDependenciesForProject(project) + + val gradleTargets = this.processTargetsForProject(project, workspaceRoot, rootProjectDirectory) + var projectRoot = project.getProjectDir().getPath() + val projectConfig = ProjectConfiguration( + gradleTargets.targets, + Metadata(gradleTargets.targetGroups, arrayOf("Gradle"), project.getDescription()), + project.getName() + ) + projectNodes.put(projectRoot, Node(projectConfig, dependencies)) + } catch (e: Error) { + } // ignore errors + } + val gson = Gson() + val json = gson.toJson(projectNodes) + val file = File(outputDirectory, "${project.name}.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 command: String; + val operSys = System.getProperty("os.name").lowercase(); + if (operSys.contains("win")) { + command = ".\\gradlew.bat " + } else { + command = "./gradlew " + } + command += project.getBuildTreePath() + if (!command.endsWith(":")) { + command += ":" + } + + project.getTasks().forEach { task -> + val target = mutableMapOf() + val metadata = mutableMapOf() + var taskCommand = command.toString() + metadata.put("description", task.getDescription()) + metadata.put("technologies", arrayOf("Gradle")) + 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)) + } + } + + var 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}" + }) + } + target.put("metadata", metadata) + + taskCommand += task.name + target.put("command", taskCommand) + target.put("options", mapOf("cwd" to rootProjectDirectory.getPath())) + + targets.put(task.name, target) + } + + return GradleTargets( + targets, + targetGroups + ); + } +} + +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): MutableSet { + var dependencies = mutableSetOf(); + project.getConfigurations().filter { config -> + val configName = config.name + configName == "compileClasspath" || configName == "implementationDependenciesMetadata" + }.forEach { + it.getAllDependencies().filter { + it is ProjectDependency + }.forEach { + dependencies.add( + Dependency( + project.getProjectDir().getPath(), + it.getName(), + 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() + ) + ) + } + return dependencies +} 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..58be693af8fa7 --- /dev/null +++ b/packages/gradle/native/plugin/src/main/kotlin/io/nx/gradle/plugin/Nodes.kt @@ -0,0 +1,21 @@ +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.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..9b0dcc785710d 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" diff --git a/packages/gradle/plugin.spec.ts b/packages/gradle/plugin-v1.spec.ts similarity index 58% rename from packages/gradle/plugin.spec.ts rename to packages/gradle/plugin-v1.spec.ts index 78dc8efe8bb1d..615baea5a50ef 100644 --- a/packages/gradle/plugin.spec.ts +++ b/packages/gradle/plugin-v1.spec.ts @@ -1,10 +1,10 @@ import { CreateNodesContext } 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 GradleReport } from './src/plugin-v1/utils/get-gradle-report'; let gradleReport: GradleReport; -jest.mock('./src/utils/get-gradle-report', () => { +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), @@ -12,7 +12,7 @@ jest.mock('./src/utils/get-gradle-report', () => { }; }); -describe('@nx/gradle/plugin', () => { +describe('@nx/gradle/plugin-v1', () => { let createNodesFunction = createNodesV2[1]; let context: CreateNodesContext; let tempFs: TempFs; @@ -75,55 +75,7 @@ describe('@nx/gradle/plugin', () => { [ [ "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.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.ts b/packages/gradle/src/generators/init/init.ts index 2a7f9028ec457..d395508504670 100644 --- a/packages/gradle/src/generators/init/init.ts +++ b/packages/gradle/src/generators/init/init.ts @@ -3,13 +3,12 @@ import { formatFiles, GeneratorCallback, globAsync, - logger, readNxJson, runTasksInSerial, 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'; @@ -90,50 +89,20 @@ 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); } 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/nodes.spec.ts b/packages/gradle/src/plugin-v1/nodes.spec.ts similarity index 99% rename from packages/gradle/src/plugin/nodes.spec.ts rename to packages/gradle/src/plugin-v1/nodes.spec.ts index 2860777af9a02..580d0e4ba0f3b 100644 --- a/packages/gradle/src/plugin/nodes.spec.ts +++ b/packages/gradle/src/plugin-v1/nodes.spec.ts @@ -1,10 +1,10 @@ import { CreateNodesContext } from '@nx/devkit'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; -import { type GradleReport } from '../utils/get-gradle-report'; +import { type GradleReport } from './utils/get-gradle-report'; let gradleReport: GradleReport; -jest.mock('../utils/get-gradle-report', () => { +jest.mock('./utils/get-gradle-report', () => { return { GRADLE_BUILD_FILES: new Set(['build.gradle', 'build.gradle.kts']), populateGradleReport: jest.fn().mockImplementation(() => void 0), 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/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.ts b/packages/gradle/src/plugin/nodes.ts index cbedf477d1b37..196c117d5a092 100644 --- a/packages/gradle/src/plugin/nodes.ts +++ b/packages/gradle/src/plugin/nodes.ts @@ -1,61 +1,37 @@ 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; [taskTargetName: string]: string | undefined | boolean; } function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions { options ??= {}; - options.testTargetName ??= 'test'; - options.classesTargetName ??= 'classes'; - options.buildTargetName ??= 'build'; + options.ciTargetName ??= 'test-ci'; return options; } @@ -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 { projects } = getCurrentNodesReport(); try { return createNodesFromFiles( - makeCreateNodesForGradleConfigFile( - gradleReport, - targetsCache, - gradleProjectRootToTestFilesMap - ), + makeCreateNodesForGradleConfigFile(projects, 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,30 @@ 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, - }); - } - }); - } - - 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] = []; + let targets = {}; + // rename target name if it is provided + Object.entries(project.targets).forEach(([taskName, target]) => { + const targetName = options?.[`${taskName}TargetName`] as string; + 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] = []; - } - - 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', }); - }); + project.targets = targets; + project.root = projectRoot; - 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'], - }, - }, + return { + projects: { + [projectRoot]: project, }, - }, + }; }; - 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/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..84539e116f076 --- /dev/null +++ b/packages/gradle/src/plugin/utils/get-nodes-from-gradle-plugin.ts @@ -0,0 +1,164 @@ +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 NodesReportJSON { + [appName: string]: { + project: Partial; + dependencies: Array; + }; +} + +export interface NodesReport { + projects: Record>; + 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 projects: Record> = {}; + 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: NodesReportJSON = + readJsonFile(file); + for (const [projectRoot, node] of Object.entries(nodesReportJson)) { + projects[projectRoot] = node.project; + dependencies = dependencies.concat(node.dependencies); + } + } + index++; + } + + return { + projects, + dependencies, + }; +} 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;