From 61b350361903c1f74b3d6b7fa430d11f47ff4962 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Fri, 13 Sep 2024 14:28:59 -0400 Subject: [PATCH] feat(core): able to import gradle project (#27645) ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes # --- e2e/gradle/src/gradle-import.test.ts | 175 ++++++++++++++++++ e2e/gradle/src/gradle.test.ts | 39 +--- e2e/gradle/src/utils/create-gradle-project.ts | 59 ++++++ e2e/nx/project.json | 2 +- packages/gradle/src/generators/init/init.ts | 8 - packages/gradle/src/plugin/dependencies.ts | 8 +- packages/gradle/src/plugin/nodes.spec.ts | 18 ++ packages/gradle/src/plugin/nodes.ts | 65 ++++--- ...le-properties-report-no-child-projects.txt | 160 ++++++++++++++++ packages/gradle/src/utils/exec-gradle.spec.ts | 87 +++++++++ packages/gradle/src/utils/exec-gradle.ts | 76 ++++++-- .../src/utils/get-gradle-report.spec.ts | 9 + .../gradle/src/utils/get-gradle-report.ts | 105 +++++------ .../src/utils/get-project-report-lines.ts | 65 +++++++ .../src/utils/split-config-files.spec.ts | 88 +++++++++ .../gradle/src/utils/split-config-files.ts | 67 +++++++ packages/nx/src/command-line/init/init-v2.ts | 9 +- 17 files changed, 876 insertions(+), 164 deletions(-) create mode 100644 e2e/gradle/src/gradle-import.test.ts create mode 100644 e2e/gradle/src/utils/create-gradle-project.ts create mode 100644 packages/gradle/src/utils/__mocks__/gradle-properties-report-no-child-projects.txt create mode 100644 packages/gradle/src/utils/exec-gradle.spec.ts create mode 100644 packages/gradle/src/utils/get-project-report-lines.ts create mode 100644 packages/gradle/src/utils/split-config-files.spec.ts create mode 100644 packages/gradle/src/utils/split-config-files.ts diff --git a/e2e/gradle/src/gradle-import.test.ts b/e2e/gradle/src/gradle-import.test.ts new file mode 100644 index 0000000000000..1fb0d61c0846c --- /dev/null +++ b/e2e/gradle/src/gradle-import.test.ts @@ -0,0 +1,175 @@ +import { + checkFilesExist, + cleanupProject, + getSelectedPackageManager, + newProject, + runCLI, + updateJson, + updateFile, + e2eCwd, + readJson, +} from '@nx/e2e/utils'; +import { mkdirSync, rmdirSync, writeFileSync } from 'fs'; +import { execSync } from 'node:child_process'; +import { join } from 'path'; +import { createGradleProject } from './utils/create-gradle-project'; +import { createFileSync } from 'fs-extra'; + +describe('Nx Import Gradle', () => { + let proj: string; + const tempImportE2ERoot = join(e2eCwd, 'nx-import'); + beforeAll(() => { + proj = newProject({ + packages: ['@nx/js'], + unsetProjectNameAndRootFormat: false, + }); + + if (getSelectedPackageManager() === 'pnpm') { + updateFile( + 'pnpm-workspace.yaml', + `packages: + - 'projects/*' + ` + ); + } else { + updateJson('package.json', (json) => { + json.workspaces = ['projects/*']; + return json; + }); + } + + try { + rmdirSync(tempImportE2ERoot); + } catch {} + mkdirSync(tempImportE2ERoot, { recursive: true }); + }); + afterAll(() => cleanupProject()); + + it('should be able to import a kotlin gradle app', () => { + const tempGradleProjectName = 'created-gradle-app-kotlin'; + const tempGraldeProjectPath = join( + tempImportE2ERoot, + tempGradleProjectName + ); + try { + rmdirSync(tempGraldeProjectPath); + } catch {} + mkdirSync(tempGraldeProjectPath, { recursive: true }); + createGradleProject( + tempGradleProjectName, + 'kotlin', + tempGraldeProjectPath, + 'gradleProjectKotlin', + 'kotlin-' + ); + // Add project.json files to the gradle project to avoid duplicate project names + createFileSync(join(tempGraldeProjectPath, 'project.json')); + writeFileSync( + join(tempGraldeProjectPath, 'project.json'), + `{"name": "${tempGradleProjectName}"}` + ); + + execSync(`git init`, { + cwd: tempGraldeProjectPath, + }); + execSync(`git add .`, { + cwd: tempGraldeProjectPath, + }); + execSync(`git commit -am "initial commit"`, { + cwd: tempGraldeProjectPath, + }); + execSync(`git checkout -b main`, { + cwd: tempGraldeProjectPath, + }); + + const remote = tempGraldeProjectPath; + const ref = 'main'; + const source = '.'; + const directory = 'projects/gradle-app-kotlin'; + + runCLI( + `import ${remote} ${directory} --ref ${ref} --source ${source} --no-interactive`, + { + verbose: true, + } + ); + + checkFilesExist( + `${directory}/settings.gradle.kts`, + `${directory}/build.gradle.kts`, + `${directory}/gradlew`, + `${directory}/gradlew.bat` + ); + const nxJson = readJson('nx.json'); + const gradlePlugin = nxJson.plugins.find( + (plugin) => plugin.plugin === '@nx/gradle' + ); + expect(gradlePlugin).toBeDefined(); + expect(() => { + runCLI(`show projects`); + runCLI('build kotlin-app'); + }).not.toThrow(); + }); + + it('should be able to import a groovy gradle app', () => { + const tempGradleProjectName = 'created-gradle-app-groovy'; + const tempGraldeProjectPath = join( + tempImportE2ERoot, + tempGradleProjectName + ); + try { + rmdirSync(tempGraldeProjectPath); + } catch {} + mkdirSync(tempGraldeProjectPath, { recursive: true }); + createGradleProject( + tempGradleProjectName, + 'groovy', + tempGraldeProjectPath, + 'gradleProjectGroovy', + 'groovy-' + ); + // Add project.json files to the gradle project to avoid duplicate project names + createFileSync(join(tempGraldeProjectPath, 'project.json')); + writeFileSync( + join(tempGraldeProjectPath, 'project.json'), + `{"name": "${tempGradleProjectName}"}` + ); + + execSync(`git init`, { + cwd: tempGraldeProjectPath, + }); + execSync(`git add .`, { + cwd: tempGraldeProjectPath, + }); + execSync(`git commit -am "initial commit"`, { + cwd: tempGraldeProjectPath, + }); + execSync(`git checkout -b main`, { + cwd: tempGraldeProjectPath, + }); + + const remote = tempGraldeProjectPath; + const ref = 'main'; + const source = '.'; + const directory = 'projects/gradle-app-groovy'; + + runCLI( + `import ${remote} ${directory} --ref ${ref} --source ${source} --no-interactive`, + { + verbose: true, + } + ); + runCLI(`g @nx/gradle:init --no-interactive`); + + checkFilesExist( + `${directory}/build.gradle`, + `${directory}/settings.gradle`, + `${directory}/gradlew`, + `${directory}/gradlew.bat` + ); + expect(() => { + runCLI(`show projects`); + runCLI('build groovy-app'); + }).not.toThrow(); + }); +}); diff --git a/e2e/gradle/src/gradle.test.ts b/e2e/gradle/src/gradle.test.ts index 92126999a91de..b2e48f44e4a9d 100644 --- a/e2e/gradle/src/gradle.test.ts +++ b/e2e/gradle/src/gradle.test.ts @@ -2,17 +2,13 @@ import { checkFilesExist, cleanupProject, createFile, - e2eConsoleLogger, - isWindows, newProject, runCLI, - runCommand, - tmpProjPath, uniq, updateFile, } from '@nx/e2e/utils'; -import { execSync } from 'child_process'; -import { resolve } from 'path'; + +import { createGradleProject } from './utils/create-gradle-project'; describe('Gradle', () => { describe.each([{ type: 'kotlin' }, { type: 'groovy' }])( @@ -22,6 +18,7 @@ describe('Gradle', () => { beforeAll(() => { newProject(); createGradleProject(gradleProjectName, type); + runCLI(`add @nx/gradle`); }); afterAll(() => cleanupProject()); @@ -101,33 +98,3 @@ dependencies { } ); }); - -function createGradleProject( - projectName: string, - type: 'kotlin' | 'groovy' = 'kotlin' -) { - e2eConsoleLogger(`Using java version: ${execSync('java -version')}`); - const gradleCommand = isWindows() - ? resolve(`${__dirname}/../gradlew.bat`) - : resolve(`${__dirname}/../gradlew`); - e2eConsoleLogger( - 'Using gradle version: ' + - execSync(`${gradleCommand} --version`, { - cwd: tmpProjPath(), - }) - ); - e2eConsoleLogger( - execSync(`${gradleCommand} help --task :init`, { - cwd: tmpProjPath(), - }).toString() - ); - e2eConsoleLogger( - runCommand( - `${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package gradleProject --no-incubating --split-project`, - { - cwd: tmpProjPath(), - } - ) - ); - runCLI(`add @nx/gradle`); -} diff --git a/e2e/gradle/src/utils/create-gradle-project.ts b/e2e/gradle/src/utils/create-gradle-project.ts new file mode 100644 index 0000000000000..b1e11051b2a06 --- /dev/null +++ b/e2e/gradle/src/utils/create-gradle-project.ts @@ -0,0 +1,59 @@ +import { + e2eConsoleLogger, + isWindows, + runCommand, + tmpProjPath, +} from '@nx/e2e/utils'; +import { execSync } from 'child_process'; +import { createFileSync, writeFileSync } from 'fs-extra'; +import { join, resolve } from 'path'; + +export function createGradleProject( + projectName: string, + type: 'kotlin' | 'groovy' = 'kotlin', + cwd: string = tmpProjPath(), + packageName: string = 'gradleProject', + addProjectJsonNamePrefix: string = '' +) { + e2eConsoleLogger(`Using java version: ${execSync('java -version')}`); + const gradleCommand = isWindows() + ? resolve(`${__dirname}/../../gradlew.bat`) + : resolve(`${__dirname}/../../gradlew`); + e2eConsoleLogger( + 'Using gradle version: ' + + execSync(`${gradleCommand} --version`, { + cwd, + }) + ); + e2eConsoleLogger( + execSync(`${gradleCommand} help --task :init`, { + cwd, + }).toString() + ); + e2eConsoleLogger( + runCommand( + `${gradleCommand} init --type ${type}-application --dsl ${type} --project-name ${projectName} --package ${packageName} --no-incubating --split-project`, + { + cwd, + } + ) + ); + + if (addProjectJsonNamePrefix) { + createFileSync(join(cwd, 'app/project.json')); + writeFileSync( + join(cwd, 'app/project.json'), + `{"name": "${addProjectJsonNamePrefix}app"}` + ); + createFileSync(join(cwd, 'list/project.json')); + writeFileSync( + join(cwd, 'list/project.json'), + `{"name": "${addProjectJsonNamePrefix}list"}` + ); + createFileSync(join(cwd, 'utilities/project.json')); + writeFileSync( + join(cwd, 'utilities/project.json'), + `{"name": "${addProjectJsonNamePrefix}utilities"}` + ); + } +} diff --git a/e2e/nx/project.json b/e2e/nx/project.json index 06f8e0b0117f8..fdfd7f1a1c42f 100644 --- a/e2e/nx/project.json +++ b/e2e/nx/project.json @@ -1,7 +1,7 @@ { "name": "e2e-nx", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "e2e/nx-misc", + "sourceRoot": "e2e/nx", "projectType": "application", "implicitDependencies": ["nx", "js"], "// targets": "to see all targets run: nx show project e2e-nx --web", diff --git a/packages/gradle/src/generators/init/init.ts b/packages/gradle/src/generators/init/init.ts index bf35ce4dcc48a..77ba771b76656 100644 --- a/packages/gradle/src/generators/init/init.ts +++ b/packages/gradle/src/generators/init/init.ts @@ -9,7 +9,6 @@ import { Tree, updateNxJson, } from '@nx/devkit'; -import { execSync } from 'child_process'; import { nxVersion } from '../../utils/versions'; import { InitGeneratorSchema } from './schema'; import { hasGradlePlugin } from '../../utils/has-gradle-plugin'; @@ -18,13 +17,6 @@ import { dirname, join, basename } from 'path'; export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { const tasks: GeneratorCallback[] = []; - if (!tree.exists('settings.gradle') && !tree.exists('settings.gradle.kts')) { - logger.warn(`Could not find 'settings.gradle' or 'settings.gradle.kts' file in your gradle workspace. -A Gradle build should contain a 'settings.gradle' or 'settings.gradle.kts' file in its root directory. It may also contain a 'build.gradle' or 'build.gradle.kts' file. -Running 'gradle init':`); - execSync('gradle init', { stdio: 'inherit' }); - } - if (!options.skipPackageJson && tree.exists('package.json')) { tasks.push( addDependenciesToPackageJson( diff --git a/packages/gradle/src/plugin/dependencies.ts b/packages/gradle/src/plugin/dependencies.ts index ffbad26c728f9..d306713064227 100644 --- a/packages/gradle/src/plugin/dependencies.ts +++ b/packages/gradle/src/plugin/dependencies.ts @@ -9,11 +9,9 @@ import { import { readFileSync } from 'node:fs'; import { basename, dirname } from 'node:path'; -import { - GRADLE_BUILD_FILES, - getCurrentGradleReport, - newLineSeparator, -} from '../utils/get-gradle-report'; +import { getCurrentGradleReport } from '../utils/get-gradle-report'; +import { GRADLE_BUILD_FILES } from '../utils/split-config-files'; +import { newLineSeparator } from '../utils/get-project-report-lines'; export const createDependencies: CreateDependencies = async ( _, diff --git a/packages/gradle/src/plugin/nodes.spec.ts b/packages/gradle/src/plugin/nodes.spec.ts index dc81a797dae7a..47cbf615bcf03 100644 --- a/packages/gradle/src/plugin/nodes.spec.ts +++ b/packages/gradle/src/plugin/nodes.spec.ts @@ -116,6 +116,9 @@ describe('@nx/gradle/plugin', () => { "gradle", ], }, + "options": { + "cwd": ".", + }, }, }, }, @@ -200,6 +203,9 @@ describe('@nx/gradle/plugin', () => { "gradle", ], }, + "options": { + "cwd": ".", + }, }, }, }, @@ -310,6 +316,9 @@ describe('@nx/gradle/plugin', () => { "gradle", ], }, + "options": { + "cwd": ".", + }, }, "test-ci": { "cache": true, @@ -379,6 +388,9 @@ describe('@nx/gradle/plugin', () => { "gradle", ], }, + "options": { + "cwd": ".", + }, }, "test-ci--bTest": { "cache": true, @@ -406,6 +418,9 @@ describe('@nx/gradle/plugin', () => { "gradle", ], }, + "options": { + "cwd": ".", + }, }, "test-ci--cTests": { "cache": true, @@ -433,6 +448,9 @@ describe('@nx/gradle/plugin', () => { "gradle", ], }, + "options": { + "cwd": ".", + }, }, }, }, diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts index 0ed5255d1565e..f17aae096cbff 100644 --- a/packages/gradle/src/plugin/nodes.ts +++ b/packages/gradle/src/plugin/nodes.ts @@ -16,16 +16,18 @@ import { basename, dirname, join } from 'node:path'; import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { findProjectForPath } from 'nx/src/devkit-internals'; -import { getGradleExecFile } from '../utils/exec-gradle'; import { populateGradleReport, getCurrentGradleReport, GradleReport, - gradleConfigGlob, - GRADLE_BUILD_FILES, - gradleConfigAndTestGlob, } 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 = { @@ -76,7 +78,8 @@ export function writeTargetsToCache(cachePath: string, results: GradleTargets) { export const createNodesV2: CreateNodesV2 = [ gradleConfigAndTestGlob, async (files, options, context) => { - const { configFiles, projectRoots, testFiles } = splitConfigFiles(files); + const { buildFiles, projectRoots, gradlewFiles, testFiles } = + splitConfigFiles(files); const optionsHash = hashObject(options); const cachePath = join( workspaceDataDirectory, @@ -84,7 +87,10 @@ export const createNodesV2: CreateNodesV2 = [ ); const targetsCache = readTargetsCache(cachePath); - await populateGradleReport(context.workspaceRoot); + await populateGradleReport( + context.workspaceRoot, + gradlewFiles.map((f) => join(context.workspaceRoot, f)) + ); const gradleReport = getCurrentGradleReport(); const gradleProjectRootToTestFilesMap = getGradleProjectRootToTestFilesMap( testFiles, @@ -98,7 +104,7 @@ export const createNodesV2: CreateNodesV2 = [ targetsCache, gradleProjectRootToTestFilesMap ), - configFiles, + buildFiles, options, context ); @@ -151,15 +157,16 @@ export const makeCreateNodesForGradleConfigFile = */ export const createNodes: CreateNodes = [ gradleConfigGlob, - async (configFile, options, context) => { + 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.' ); - await populateGradleReport(context.workspaceRoot); + const { gradlewFiles } = splitConfigFiles(context.configFiles); + await populateGradleReport(context.workspaceRoot, gradlewFiles); const gradleReport = getCurrentGradleReport(); const internalCreateNodes = makeCreateNodesForGradleConfigFile(gradleReport); - return await internalCreateNodes(configFile, options, context); + return await internalCreateNodes(buildFile, options, context); }, ]; @@ -234,13 +241,16 @@ async function createGradleTargets( context: CreateNodesContext, outputDirs: Map, gradleProject: string, - gradleFilePath: 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 = {}; @@ -262,15 +272,20 @@ async function createGradleTargets( outputs, task.type, targets, - targetGroups + targetGroups, + gradlewFileDirectory ); } const taskCommandToRun = `${gradleProject ? gradleProject + ':' : ''}${ task.name }`; + targets[targetName] = { command: `${getGradleExecFile()} ${taskCommandToRun}`, + options: { + cwd: gradlewFileDirectory, + }, cache: cacheableTaskType.has(task.type), inputs: inputsMap[task.name], dependsOn: dependsOnMap[task.name], @@ -320,7 +335,8 @@ function getTestCiTargets( outputs: string[], targetGroupName: string, targets: Record, - targetGroups: Record + targetGroups: Record, + gradlewFileDirectory: string ): void { if (!testFiles || testFiles.length === 0 || !ciTargetName) { return; @@ -338,6 +354,9 @@ function getTestCiTargets( targets[targetName] = { command: `${getGradleExecFile()} ${taskCommandToRun} --tests ${testName}`, + options: { + cwd: gradlewFileDirectory, + }, cache: true, inputs, dependsOn: dependsOnMap['test'], @@ -386,26 +405,6 @@ function getTestCiTargets( targetGroups[targetGroupName].push(ciTargetName); } -function splitConfigFiles(files: readonly string[]): { - configFiles: string[]; - testFiles: string[]; - projectRoots: string[]; -} { - const configFiles = []; - const testFiles = []; - const projectRoots = new Set(); - files.forEach((file) => { - if (GRADLE_BUILD_FILES.has(basename(file))) { - configFiles.push(file); - projectRoots.add(dirname(file)); - } else { - testFiles.push(file); - } - }); - - return { configFiles, testFiles, projectRoots: Array.from(projectRoots) }; -} - function getGradleProjectRootToTestFilesMap( testFiles: string[], projectRoots: string[] diff --git a/packages/gradle/src/utils/__mocks__/gradle-properties-report-no-child-projects.txt b/packages/gradle/src/utils/__mocks__/gradle-properties-report-no-child-projects.txt new file mode 100644 index 0000000000000..eb4ba78915916 --- /dev/null +++ b/packages/gradle/src/utils/__mocks__/gradle-properties-report-no-child-projects.txt @@ -0,0 +1,160 @@ + +------------------------------------------------------------ +Project ':app' +------------------------------------------------------------ + +allprojects: [project ':app'] +ant: org.gradle.api.internal.project.DefaultAntBuilder@505598eb +antBuilderFactory: org.gradle.api.internal.project.DefaultAntBuilderFactory@259d836f +application: extension 'application' +applicationDefaultJvmArgs: [] +applicationDistribution: org.gradle.api.internal.file.copy.DefaultCopySpec_Decorated@3978059a +applicationName: app +archivesBaseName: app +artifacts: org.gradle.api.internal.artifacts.dsl.DefaultArtifactHandler_Decorated@32167ebb +asDynamicObject: DynamicObject for project ':app' +assemble: task ':app:assemble' +assembleDist: task ':app:assembleDist' +autoTargetJvmDisabled: false +base: extension 'base' +baseClassLoaderScope: org.gradle.api.internal.initialization.DefaultClassLoaderScope@3c4c27d8 +build: task ':app:build' +buildDependents: task ':app:buildDependents' +buildDir: /private/tmp/nx-e2e/nx/proj9912426/app/build +buildEnvironment: task ':app:buildEnvironment' +buildFile: /private/tmp/nx-e2e/nx/proj9912426/app/build.gradle +buildNeeded: task ':app:buildNeeded' +buildPath: : +buildScriptSource: org.gradle.groovy.scripts.TextResourceScriptSource@4ef82ad2 +buildTreePath: :app +buildscript: org.gradle.api.internal.initialization.DefaultScriptHandler_Decorated@7b75e99 +check: task ':app:check' +childProjects: {} +childProjectsUnchecked: {} +class: class org.gradle.api.internal.project.DefaultProject_Decorated +classLoaderScope: org.gradle.api.internal.initialization.DefaultClassLoaderScope@7e1acf20 +classes: task ':app:classes' +clean: task ':app:clean' +compileGroovy: task ':app:compileGroovy' +compileJava: task ':app:compileJava' +compileTestGroovy: task ':app:compileTestGroovy' +compileTestJava: task ':app:compileTestJava' +components: SoftwareComponent container +configurationActions: org.gradle.configuration.project.DefaultProjectConfigurationActionContainer@7f7885fe +configurationTargetIdentifier: org.gradle.configuration.ConfigurationTargetIdentifier$1@70598e79 +configurations: configuration container +convention: org.gradle.internal.extensibility.DefaultConvention@4d27557d +conventionMapping: org.gradle.internal.extensibility.ConventionAwareHelper@65ecf036 +crossProjectModelAccess: org.gradle.api.internal.project.DefaultCrossProjectModelAccess@7665f252 +defaultArtifacts: extension 'defaultArtifacts' +defaultTasks: [] +deferredProjectConfiguration: org.gradle.api.internal.project.DeferredProjectConfiguration@5e80ce00 +dependencies: org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler_Decorated@6216b9ef +dependencyFactory: org.gradle.api.internal.artifacts.DefaultDependencyFactory@15ed846d +dependencyInsight: task ':app:dependencyInsight' +dependencyLocking: org.gradle.internal.locking.DefaultDependencyLockingHandler_Decorated@1702aba6 +dependencyMetaDataProvider: org.gradle.internal.service.scopes.ProjectScopeServices$ProjectBackedModuleMetaDataProvider@4f2a8788 +dependencyReport: task ':app:dependencyReport' +dependentComponents: task ':app:dependentComponents' +depth: 1 +description: null +detachedState: false +displayName: project ':app' +distTar: task ':app:distTar' +distZip: task ':app:distZip' +distributions: Distribution container +distsDirName: distributions +distsDirectory: extension 'base' property 'distsDirectory' +docsDir: /private/tmp/nx-e2e/nx/proj9912426/app/build/docs +docsDirName: docs +executableDir: bin +ext: org.gradle.internal.extensibility.DefaultExtraPropertiesExtension@18249643 +extensions: org.gradle.internal.extensibility.DefaultConvention@4d27557d +fileOperations: org.gradle.api.internal.file.DefaultFileOperations@6193a7c6 +fileResolver: org.gradle.api.internal.file.BaseDirFileResolver@634e7fc9 +gradle: build 'my-gradle-project5165026' +groovyRuntime: extension 'groovyRuntime' +groovydoc: task ':app:groovydoc' +group: my-gradle-project5165026 +help: task ':app:help' +htmlDependencyReport: task ':app:htmlDependencyReport' +identityPath: :app +inheritedScope: org.gradle.internal.extensibility.ExtensibleDynamicObject$InheritedDynamicObject@7843a2ef +installDist: task ':app:installDist' +internalStatus: property(java.lang.Object, fixed(class java.lang.String, integration)) +jar: task ':app:jar' +java: extension 'java' +javaToolchains: extension 'javaToolchains' +javadoc: task ':app:javadoc' +layout: org.gradle.api.internal.file.DefaultProjectLayout@38b897f6 +libsDirName: libs +libsDirectory: extension 'base' property 'libsDirectory' +listenerBuildOperationDecorator: org.gradle.configuration.internal.DefaultListenerBuildOperationDecorator@58c7b14b +logger: org.gradle.internal.logging.slf4j.OutputEventListenerBackedLogger@2fb55781 +logging: org.gradle.internal.logging.services.DefaultLoggingManager@2818071f +mainClassName: null +model: project ':app' +modelIdentityDisplayName: null +modelRegistry: org.gradle.model.internal.registry.DefaultModelRegistry@4d299c29 +name: app +normalization: org.gradle.normalization.internal.DefaultInputNormalizationHandler_Decorated@4460a043 +objects: org.gradle.api.internal.model.DefaultObjectFactory@381b881e +outgoingVariants: task ':app:outgoingVariants' +owner: project ':app' +parent: root project 'my-gradle-project5165026' +parentIdentifier: root project 'my-gradle-project5165026' +path: :app +pluginContext: false +pluginManager: org.gradle.api.internal.plugins.DefaultPluginManager_Decorated@505350fd +plugins: [org.gradle.api.plugins.ReportingBasePlugin$Inject@363d344c, org.gradle.api.plugins.ProjectReportsPlugin$Inject@4ecb0bf4, org.gradle.api.plugins.HelpTasksPlugin$Inject@55c1f1c, org.gradle.buildinit.plugins.BuildInitPlugin$Inject@5154243b, org.gradle.buildinit.plugins.WrapperPlugin$Inject@4186a206, org.gradle.language.base.plugins.LifecycleBasePlugin$Inject@63b97a85, org.gradle.api.plugins.BasePlugin$Inject@46c915ee, org.gradle.api.plugins.JvmEcosystemPlugin$Inject@26901290, org.gradle.api.plugins.JvmToolchainsPlugin$Inject@1047a16b, org.gradle.api.plugins.JavaBasePlugin$Inject@219fc174, org.gradle.api.plugins.GroovyBasePlugin$Inject@cc0c00e, org.gradle.testing.base.plugins.TestSuiteBasePlugin$Inject@afcdb52, org.gradle.api.plugins.JvmTestSuitePlugin$Inject@4f2802e1, org.gradle.api.plugins.JavaPlugin$Inject@457c77b1, org.gradle.api.plugins.GroovyPlugin$Inject@6bea0b65, BuildlogicGroovyCommonConventionsPlugin@340f0f12, org.gradle.api.distribution.plugins.DistributionPlugin$Inject@41c5c0a5, org.gradle.api.plugins.ApplicationPlugin$Inject@4e797fdb, BuildlogicGroovyApplicationConventionsPlugin@502595f2] +processOperations: org.gradle.process.internal.DefaultExecActionFactory$DecoratingExecActionFactory@511a460a +processResources: task ':app:processResources' +processTestResources: task ':app:processTestResources' +project: project ':app' +projectConfigurator: org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator@21331883 +projectDir: /private/tmp/nx-e2e/nx/proj9912426/app +projectEvaluationBroadcaster: ProjectEvaluationListener broadcast +projectEvaluator: org.gradle.configuration.project.LifecycleProjectEvaluator@f6c3829 +projectPath: :app +projectReport: task ':app:projectReport' +projectReportDir: /private/tmp/nx-e2e/nx/proj9912426/app/build/reports/project +projectReportDirName: project +projects: [project ':app'] +properties: {...} +propertyReport: task ':app:propertyReport' +providers: org.gradle.api.internal.provider.DefaultProviderFactory_Decorated@1eb7e486 +publicType: org.gradle.api.plugins.ProjectReportsPluginConvention +reporting: extension 'reporting' +repositories: repository container +resolvableConfigurations: task ':app:resolvableConfigurations' +resources: org.gradle.api.internal.resources.DefaultResourceHandler@103986d2 +rootDir: /private/tmp/nx-e2e/nx/proj9912426 +rootProject: root project 'my-gradle-project5165026' +rootScript: false +run: task ':app:run' +script: false +scriptHandlerFactory: org.gradle.api.internal.initialization.DefaultScriptHandlerFactory@191cd667 +scriptPluginFactory: org.gradle.configuration.ScriptPluginFactorySelector@775f081d +serviceRegistryFactory: org.gradle.internal.service.scopes.BuildScopeServiceRegistryFactory@566d51a4 +services: Project services +sourceCompatibility: 21 +sourceSets: SourceSet container +standardOutputCapture: org.gradle.internal.logging.services.DefaultLoggingManager@2818071f +startScripts: task ':app:startScripts' +state: project state 'EXECUTED' +status: integration +subprojects: [] +targetCompatibility: 21 +taskDependencyFactory: org.gradle.api.internal.tasks.DefaultTaskDependencyFactory@7486b7b3 +taskReport: task ':app:taskReport' +taskThatOwnsThisObject: null +tasks: task set +test: task ':app:test' +testClasses: task ':app:testClasses' +testReportDir: /private/tmp/nx-e2e/nx/proj9912426/app/build/reports/tests +testReportDirName: tests +testResultsDir: /private/tmp/nx-e2e/nx/proj9912426/app/build/test-results +testResultsDirName: test-results +testing: extension 'testing' +version: unspecified +versionCatalogs: extension 'versionCatalogs' diff --git a/packages/gradle/src/utils/exec-gradle.spec.ts b/packages/gradle/src/utils/exec-gradle.spec.ts new file mode 100644 index 0000000000000..a8b2ae1286975 --- /dev/null +++ b/packages/gradle/src/utils/exec-gradle.spec.ts @@ -0,0 +1,87 @@ +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { findGraldewFile } from './exec-gradle'; + +describe('exec gradle', () => { + describe('findGraldewFile', () => { + let tempFs: TempFs; + let cwd: string; + + beforeEach(async () => { + tempFs = new TempFs('test'); + cwd = process.cwd(); + process.chdir(tempFs.tempDir); + }); + + afterEach(() => { + jest.resetModules(); + process.chdir(cwd); + }); + + it('should find gradlew with one gradlew file at root', async () => { + await tempFs.createFiles({ + 'proj/build.gradle': ``, + gradlew: '', + 'nested/nested/proj/build.gradle': ``, + 'nested/nested/proj/settings.gradle': ``, + 'nested/nested/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': ``, + }); + let gradlewFile = findGraldewFile('proj/build.gradle', tempFs.tempDir); + expect(gradlewFile).toEqual('gradlew'); + gradlewFile = findGraldewFile( + 'nested/nested/proj/build.gradle', + tempFs.tempDir + ); + expect(gradlewFile).toEqual('gradlew'); + gradlewFile = findGraldewFile( + 'nested/nested/proj/settings.gradle', + tempFs.tempDir + ); + expect(gradlewFile).toEqual('gradlew'); + }); + + it('should find gradlew with multiple gradlew files with nested project structure', async () => { + await tempFs.createFiles({ + 'proj/build.gradle': ``, + 'proj/gradlew': '', + 'proj/settings.gradle': ``, + 'nested/nested/proj/gradlew': '', + 'nested/nested/proj/build.gradle': ``, + 'nested/nested/proj/settings.gradle': ``, + 'nested/nested/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': ``, + }); + + let gradlewFile = findGraldewFile('proj/build.gradle', tempFs.tempDir); + expect(gradlewFile).toEqual('proj/gradlew'); + gradlewFile = findGraldewFile('proj/settings.gradle', tempFs.tempDir); + expect(gradlewFile).toEqual('proj/gradlew'); + gradlewFile = findGraldewFile( + 'nested/nested/proj/build.gradle', + tempFs.tempDir + ); + expect(gradlewFile).toEqual('nested/nested/proj/gradlew'); + gradlewFile = findGraldewFile( + 'nested/nested/proj/settings.gradle', + tempFs.tempDir + ); + expect(gradlewFile).toEqual('nested/nested/proj/gradlew'); + }); + + it('should throw an error if no gradlw in workspace', async () => { + await tempFs.createFiles({ + 'proj/build.gradle': ``, + 'nested/nested/proj/build.gradle': ``, + 'nested/nested/proj/settings.gradle': ``, + 'nested/nested/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': ``, + }); + expect(() => + findGraldewFile('proj/build.gradle', tempFs.tempDir) + ).toThrow(); + }); + }); +}); diff --git a/packages/gradle/src/utils/exec-gradle.ts b/packages/gradle/src/utils/exec-gradle.ts index c2327eebe60a3..d695737b84e34 100644 --- a/packages/gradle/src/utils/exec-gradle.ts +++ b/packages/gradle/src/utils/exec-gradle.ts @@ -1,36 +1,35 @@ -import { workspaceRoot } from '@nx/devkit'; +import { AggregateCreateNodesError, workspaceRoot } from '@nx/devkit'; import { ExecFileOptions, execFile } from 'node:child_process'; import { existsSync } from 'node:fs'; -import { join } from 'node:path'; - -export function getGradleBinaryPath(): string { - const gradleFile = process.platform.startsWith('win') - ? 'gradlew.bat' - : 'gradlew'; - const gradleBinaryPath = join(workspaceRoot, gradleFile); - if (!existsSync(gradleBinaryPath)) { - throw new Error('Gradle is not setup. Run "gradle init"'); - } - - return gradleBinaryPath; -} +import { dirname, join } from 'node:path'; +/** + * For gradle command, it needs to be run from the directory of the gradle binary + * @returns gradle binary file name + */ export function getGradleExecFile(): string { return process.platform.startsWith('win') ? '.\\gradlew.bat' : './gradlew'; } +/** + * This function executes gradle with the given arguments + * @param gradleBinaryPath absolute path to gradle binary + * @param args args passed to gradle + * @param execOptions exec options + * @returns promise with the stdout buffer + */ export function execGradleAsync( + gradleBinaryPath: string, args: ReadonlyArray, execOptions: ExecFileOptions = {} ): Promise { - const gradleBinaryPath = getGradleBinaryPath(); - return new Promise((res, rej) => { const cp = execFile(gradleBinaryPath, args, { - ...execOptions, + cwd: dirname(gradleBinaryPath), shell: true, windowsHide: true, env: process.env, + ...execOptions, }); let stdout = Buffer.from(''); @@ -53,3 +52,46 @@ export function execGradleAsync( }); }); } + +/** + * This function recursively finds the nearest gradlew file in the workspace + * @param originalFileToSearch the original file to search for + * @param wr workspace root + * @param currentSearchPath the path to start searching for gradlew file + * @returns the relative path of the gradlew file to workspace root, throws an error if gradlew file is not found + * It will return gradlew.bat file on windows and gradlew file on other platforms + */ +export function findGraldewFile( + originalFileToSearch: string, + wr: string = workspaceRoot, + currentSearchPath?: string +): string { + currentSearchPath ??= originalFileToSearch; + const parent = dirname(currentSearchPath); + if (currentSearchPath === parent) { + throw new AggregateCreateNodesError( + [ + [ + originalFileToSearch, + new Error('No Gradlew file found. Run "gradle init"'), + ], + ], + [] + ); + } + + const gradlewPath = join(parent, 'gradlew'); + const gradlewBatPath = join(parent, 'gradlew.bat'); + + if (process.platform.startsWith('win')) { + if (existsSync(join(wr, gradlewBatPath))) { + return gradlewBatPath; + } + } else { + if (existsSync(join(wr, gradlewPath))) { + return gradlewPath; + } + } + + return findGraldewFile(originalFileToSearch, wr, parent); +} diff --git a/packages/gradle/src/utils/get-gradle-report.spec.ts b/packages/gradle/src/utils/get-gradle-report.spec.ts index 04f746ffb0a7a..014e3cbac2cf2 100644 --- a/packages/gradle/src/utils/get-gradle-report.spec.ts +++ b/packages/gradle/src/utils/get-gradle-report.spec.ts @@ -36,4 +36,13 @@ describe('processProjectReports', () => { 'mylibrary', ]); }); + + it('should process properties report for projects without child projects', () => { + const report = processProjectReports([ + '> Task :propertyReport', + `See the report at: file://${__dirname}/__mocks__/gradle-properties-report-no-child-projects.txt`, + ]); + expect(report.gradleProjectToProjectName.get('')).toEqual('app'); + expect(report.gradleProjectToChildProjects.get('')).toEqual([]); + }); }); diff --git a/packages/gradle/src/utils/get-gradle-report.ts b/packages/gradle/src/utils/get-gradle-report.ts index 14ef960b93a53..1d689890e6ce5 100644 --- a/packages/gradle/src/utils/get-gradle-report.ts +++ b/packages/gradle/src/utils/get-gradle-report.ts @@ -3,23 +3,18 @@ import { join, relative } from 'node:path'; import { AggregateCreateNodesError, - logger, normalizePath, workspaceRoot, } from '@nx/devkit'; -import { combineGlobPatterns } from 'nx/src/utils/globs'; -import { execGradleAsync } from './exec-gradle'; import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context'; import { dirname } from 'path'; - -export const fileSeparator = process.platform.startsWith('win') - ? 'file:///' - : 'file://'; - -export const newLineSeparator = process.platform.startsWith('win') - ? '\r\n' - : '\n'; +import { gradleConfigAndTestGlob } from './split-config-files'; +import { + getProjectReportLines, + fileSeparator, + newLineSeparator, +} from './get-project-report-lines'; export interface GradleReport { gradleFileToGradleProjectMap: Map; @@ -34,37 +29,39 @@ export interface GradleReport { let gradleReportCache: GradleReport; let gradleCurrentConfigHash: string; -export const GRADLE_BUILD_FILES = new Set(['build.gradle', 'build.gradle.kts']); -export const GRADLE_TEST_FILES = [ - '**/src/test/java/**/*Test.java', - '**/src/test/kotlin/**/*Test.kt', - '**/src/test/java/**/*Tests.java', - '**/src/test/kotlin/**/*Tests.kt', -]; - -export const gradleConfigGlob = combineGlobPatterns( - ...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`) -); - -export const gradleConfigAndTestGlob = combineGlobPatterns( - ...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`), - ...GRADLE_TEST_FILES -); - export function getCurrentGradleReport() { if (!gradleReportCache) { - throw new Error( - 'Expected cached gradle report. Please open an issue at https://github.com/nrwl/nx/issues/new/choose' + throw new AggregateCreateNodesError( + [ + [ + null, + new Error( + `Expected cached gradle report. Please open an issue at https://github.com/nrwl/nx/issues/new/choose` + ), + ], + ], + [] ); } return gradleReportCache; } +/** + * 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 populateGradleReport( - workspaceRoot: string + workspaceRoot: string, + gradlewFiles: string[] ): Promise { const gradleConfigHash = await hashWithWorkspaceContext(workspaceRoot, [ - gradleConfigGlob, + gradleConfigAndTestGlob, ]); if (gradleReportCache && gradleConfigHash === gradleCurrentConfigHash) { return; @@ -73,37 +70,18 @@ export async function populateGradleReport( const gradleProjectReportStart = performance.mark( 'gradleProjectReport:start' ); - let projectReportLines; - try { - projectReportLines = await execGradleAsync(['projectReportAll'], { - cwd: workspaceRoot, - }); - } catch (e) { - try { - projectReportLines = await execGradleAsync(['projectReport'], { - cwd: workspaceRoot, - }); - logger.warn( - 'Could not run `projectReportAll` task. Ran `projectReport` instead. Please run `nx generate @nx/gradle:init` to generate the necessary tasks.' - ); - } catch (e) { - throw new AggregateCreateNodesError( - [ - [ - null, - new Error( - 'Could not run `projectReportAll` or `projectReport` task. Please run `nx generate @nx/gradle:init` to generate the necessary tasks.' - ), - ], - ], - [] - ); - } - } - projectReportLines = projectReportLines - .toString() - .split(newLineSeparator) - .filter((line) => line.trim() !== ''); + + const projectReportLines = await gradlewFiles.reduce( + async ( + projectReportLines: Promise, + gradlewFile: string + ): Promise => { + const allLines = await projectReportLines; + const currentLines = await getProjectReportLines(gradlewFile); + return [...allLines, ...currentLines]; + }, + Promise.resolve([]) + ); const gradleProjectReportEnd = performance.mark('gradleProjectReport:end'); performance.measure( @@ -111,6 +89,7 @@ export async function populateGradleReport( gradleProjectReportStart.name, gradleProjectReportEnd.name ); + gradleCurrentConfigHash = gradleConfigHash; gradleReportCache = processProjectReports(projectReportLines); } diff --git a/packages/gradle/src/utils/get-project-report-lines.ts b/packages/gradle/src/utils/get-project-report-lines.ts new file mode 100644 index 0000000000000..84be3187ccc93 --- /dev/null +++ b/packages/gradle/src/utils/get-project-report-lines.ts @@ -0,0 +1,65 @@ +import { AggregateCreateNodesError, logger } from '@nx/devkit'; +import { execGradleAsync } from './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 + * @returns project report lines + */ +export async function getProjectReportLines( + gradlewFile: string +): Promise { + let projectReportBuffer: Buffer; + + // if there is no build.gradle or build.gradle.kts file, we cannot run the projectReport nor projectReportAll 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 { + projectReportBuffer = await execGradleAsync(gradlewFile, [ + 'projectReportAll', + ]); + } catch (e) { + try { + projectReportBuffer = await execGradleAsync(gradlewFile, [ + 'projectReport', + ]); + logger.warn( + `Could not run 'projectReportAll' task. Ran 'projectReport' instead. Please run 'nx generate @nx/gradle:init' to generate the necessary tasks.` + ); + } catch (e) { + throw new AggregateCreateNodesError( + [ + [ + gradlewFile, + new Error( + `Could not run 'projectReportAll' or 'projectReport' task. Please run 'nx generate @nx/gradle:init' to generate the necessary tasks.` + ), + ], + ], + [] + ); + } + } + return projectReportBuffer + .toString() + .split(newLineSeparator) + .filter((line) => line.trim() !== ''); +} diff --git a/packages/gradle/src/utils/split-config-files.spec.ts b/packages/gradle/src/utils/split-config-files.spec.ts new file mode 100644 index 0000000000000..4bbf2fe866f37 --- /dev/null +++ b/packages/gradle/src/utils/split-config-files.spec.ts @@ -0,0 +1,88 @@ +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { splitConfigFiles } from './split-config-files'; + +describe('split config files', () => { + let tempFs: TempFs; + let cwd: string; + + beforeEach(async () => { + tempFs = new TempFs('test'); + cwd = process.cwd(); + process.chdir(tempFs.tempDir); + }); + + afterEach(() => { + jest.resetModules(); + process.chdir(cwd); + }); + + it('should split config files with one gradlew file', async () => { + await tempFs.createFiles({ + 'proj/build.gradle': ``, + gradlew: '', + 'nested/nested/proj/build.gradle': ``, + 'nested/nested/proj/settings.gradle': ``, + 'nested/nested/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': ``, + }); + const { buildFiles, gradlewFiles, testFiles, projectRoots } = + splitConfigFiles([ + 'proj/build.gradle', + 'gradlew', + 'nested/nested/proj/build.gradle', + 'nested/nested/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', + ]); + expect(buildFiles).toEqual([ + 'proj/build.gradle', + 'nested/nested/proj/build.gradle', + ]); + expect(gradlewFiles).toEqual(['gradlew']); + expect(testFiles).toEqual([ + 'nested/nested/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', + ]); + expect(projectRoots).toEqual(['proj', 'nested/nested/proj']); + }); + + it('should split config files with multiple gradlew files', async () => { + await tempFs.createFiles({ + 'proj/build.gradle': ``, + 'proj/gradlew': '', + 'proj/settings.gradle': ``, + 'nested/nested/proj/gradlew': '', + 'nested/nested/proj/build.gradle': ``, + 'nested/nested/proj/settings.gradle': ``, + 'nested/nested/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': ``, + }); + const { buildFiles, gradlewFiles, testFiles, projectRoots } = + splitConfigFiles([ + 'proj/build.gradle', + 'proj/gradlew', + 'nested/nested/proj/build.gradle', + 'nested/nested/proj/gradlew', + 'nested/nested/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', + ]); + expect(buildFiles).toEqual([ + 'proj/build.gradle', + 'nested/nested/proj/build.gradle', + ]); + expect(gradlewFiles).toEqual([ + 'proj/gradlew', + 'nested/nested/proj/gradlew', + ]); + expect(testFiles).toEqual([ + 'nested/nested/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', + ]); + expect(projectRoots).toEqual(['proj', 'nested/nested/proj']); + }); +}); diff --git a/packages/gradle/src/utils/split-config-files.ts b/packages/gradle/src/utils/split-config-files.ts new file mode 100644 index 0000000000000..c8b900e91568d --- /dev/null +++ b/packages/gradle/src/utils/split-config-files.ts @@ -0,0 +1,67 @@ +import { combineGlobPatterns } from 'nx/src/utils/globs'; +import { basename, dirname } from 'node:path'; + +export const GRADLE_BUILD_FILES = new Set(['build.gradle', 'build.gradle.kts']); +export const GRALDEW_FILES = new Set(['gradlew', 'gradlew.bat']); +export const GRADLE_TEST_FILES = [ + '**/src/test/java/**/*Test.java', + '**/src/test/kotlin/**/*Test.kt', + '**/src/test/java/**/*Tests.java', + '**/src/test/kotlin/**/*Tests.kt', +]; + +export const gradleConfigGlob = combineGlobPatterns( + ...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`) +); + +export const gradleConfigAndTestGlob = combineGlobPatterns( + ...Array.from(GRADLE_BUILD_FILES).map((file) => `**/${file}`), + ...Array.from(GRALDEW_FILES).map((file) => `**/${file}`), + ...GRADLE_TEST_FILES +); + +/** + * This function split config files into build files, settings files, test files and project roots + * @param files list of files to split + * @returns object with buildFiles, gradlewFiles, testFiles and projectRoots + * For gradlewFiles, it will start with settings files and find the nearest gradlew file in the workspace + */ +export function splitConfigFiles(files: readonly string[]): { + buildFiles: string[]; + gradlewFiles: string[]; + testFiles: string[]; + projectRoots: string[]; +} { + const buildFiles = []; + const testFiles = []; + const gradlewFiles = []; + const projectRoots = new Set(); + + files.forEach((file) => { + const filename = basename(file); + const fileDirectory = dirname(file); + if (GRADLE_BUILD_FILES.has(filename)) { + buildFiles.push(file); + projectRoots.add(fileDirectory); + } else if (GRALDEW_FILES.has(filename)) { + if (process.platform.startsWith('win')) { + if (filename === 'gradlew.bat') { + gradlewFiles.push(file); + } + } else { + if (filename === 'gradlew') { + gradlewFiles.push(file); + } + } + } else { + testFiles.push(file); + } + }); + + return { + buildFiles, + testFiles, + gradlewFiles, + projectRoots: Array.from(projectRoots), + }; +} diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index 457cdd99d4445..f47a1261c3842 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -220,7 +220,14 @@ export async function detectPlugins( } } } - if (existsSync('gradlew') || existsSync('gradlew.bat')) { + + let gradlewFiles = ['gradlew', 'gradlew.bat'].concat( + await globWithWorkspaceContext(process.cwd(), [ + '**/gradlew', + '**/gradlew.bat', + ]) + ); + if (gradlewFiles.some((f) => existsSync(f))) { detectedPlugins.add('@nx/gradle'); }