diff --git a/e2e/gradle/src/gradle.test.ts b/e2e/gradle/src/gradle.test.ts index b2e48f44e4a9d..856f77b43c592 100644 --- a/e2e/gradle/src/gradle.test.ts +++ b/e2e/gradle/src/gradle.test.ts @@ -29,8 +29,7 @@ describe('Gradle', () => { expect(projects).toContain('utilities'); expect(projects).toContain(gradleProjectName); - let buildOutput = runCLI('build app', { verbose: true }); - // app depends on list and utilities + const buildOutput = runCLI('build app', { verbose: true }); expect(buildOutput).toContain('nx run list:build'); expect(buildOutput).toContain(':list:classes'); expect(buildOutput).toContain('nx run utilities:build'); @@ -41,15 +40,6 @@ describe('Gradle', () => { `list/build/libs/list.jar`, `utilities/build/libs/utilities.jar` ); - - buildOutput = runCLI(`build ${gradleProjectName}`, { verbose: true }); - // root project depends on app, list and utilities - expect(buildOutput).toContain('nx run app:build'); - expect(buildOutput).toContain(':app:classes'); - expect(buildOutput).toContain('nx run list:build'); - expect(buildOutput).toContain(':list:classes'); - expect(buildOutput).toContain('nx run utilities:build'); - expect(buildOutput).toContain(':utilities:classes'); }); it('should track dependencies for new app', () => { diff --git a/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.tsx b/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.tsx index bbb8c5031a9d8..218df08b2b444 100644 --- a/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.tsx +++ b/graph/ui-project-details/src/lib/target-configuration-details-group-list/target-configuration-details-group-list.tsx @@ -44,8 +44,11 @@ export function TargetConfigurationGroupList({ if (hasGroups) { return ( <> - {Object.entries(targetsGroup.groups).map( - ([targetGroupName, targets]) => { + {Object.entries(targetsGroup.groups) + .sort(([targetGroupName1], [targetGroupName2]) => + targetGroupName1.localeCompare(targetGroupName2) + ) + .map(([targetGroupName, targets]) => { return ( ); - } - )} + })} { "options": { "buildTargetName": "build", "classesTargetName": "classes", + "includeSubprojectsTasks": false, "testTargetName": "test", }, "plugin": "@nx/gradle", @@ -48,6 +49,7 @@ describe('@nx/gradle:init', () => { "options": { "buildTargetName": "build", "classesTargetName": "classes", + "includeSubprojectsTasks": false, "testTargetName": "test", }, "plugin": "@nx/gradle", diff --git a/packages/gradle/src/generators/init/init.ts b/packages/gradle/src/generators/init/init.ts index 77ba771b76656..2a7f9028ec457 100644 --- a/packages/gradle/src/generators/init/init.ts +++ b/packages/gradle/src/generators/init/init.ts @@ -52,6 +52,7 @@ function addPlugin(tree: Tree) { testTargetName: 'test', classesTargetName: 'classes', buildTargetName: 'build', + includeSubprojectsTasks: false, }, }); updateNxJson(tree, nxJson); diff --git a/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.spec.ts b/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.spec.ts new file mode 100644 index 0000000000000..ad989cbc00abe --- /dev/null +++ b/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.spec.ts @@ -0,0 +1,63 @@ +import { Tree, readNxJson } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import update from './add-include-subprojects-tasks'; + +describe('AddIncludeSubprojectsTasks', () => { + let tree: Tree; + + beforeAll(() => { + tree = createTreeWithEmptyWorkspace(); + }); + + it('should not change nx.json if @nx/gradle is not added', async () => { + tree.write('nx.json', JSON.stringify({ namedInputs: {} })); + update(tree); + expect(readNxJson(tree)).toMatchInlineSnapshot(` + { + "namedInputs": {}, + } + `); + }); + + it('should add includeSubprojectsTasks to @nx/gradle plugin', async () => { + tree.write('nx.json', JSON.stringify({ plugins: ['@nx/gradle'] })); + update(tree); + expect(readNxJson(tree)).toMatchInlineSnapshot(` + { + "plugins": [ + { + "options": { + "includeSubprojectsTasks": true, + }, + "plugin": "@nx/gradle", + }, + ], + } + `); + }); + + it('should add includeSubprojectsTasks to @nx/gradle plugin with options', async () => { + tree.write( + 'nx.json', + JSON.stringify({ + plugins: [ + { plugin: '@nx/gradle', options: { testTargetName: 'test' } }, + ], + }) + ); + update(tree); + expect(readNxJson(tree)).toMatchInlineSnapshot(` + { + "plugins": [ + { + "options": { + "includeSubprojectsTasks": true, + "testTargetName": "test", + }, + "plugin": "@nx/gradle", + }, + ], + } + `); + }); +}); 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 new file mode 100644 index 0000000000000..b81506bbdf62c --- /dev/null +++ b/packages/gradle/src/migrations/20-2-0/add-include-subprojects-tasks.ts @@ -0,0 +1,32 @@ +import { Tree, readNxJson, updateNxJson } from '@nx/devkit'; +import { hasGradlePlugin } from '../../utils/has-gradle-plugin'; +import { GradlePluginOptions } from '../../plugin/nodes'; + +// This function add options includeSubprojectsTasks as true in nx.json for gradle plugin +export default function update(tree: Tree) { + const nxJson = readNxJson(tree); + if (!nxJson) { + return; + } + if (!hasGradlePlugin(tree)) { + return; + } + let gradlePluginIndex = nxJson.plugins.findIndex((p) => + typeof p === 'string' ? p === '@nx/gradle' : p.plugin === '@nx/gradle' + ); + let gradlePlugin = nxJson.plugins[gradlePluginIndex]; + if (typeof gradlePlugin === 'string') { + gradlePlugin = { + plugin: '@nx/gradle', + options: { + includeSubprojectsTasks: true, + }, + }; + nxJson.plugins[gradlePluginIndex] = gradlePlugin; + } else { + gradlePlugin.options ??= {}; + (gradlePlugin.options as GradlePluginOptions).includeSubprojectsTasks = + true; + } + updateNxJson(tree, nxJson); +} diff --git a/packages/gradle/src/plugin/nodes.spec.ts b/packages/gradle/src/plugin/nodes.spec.ts index 47cbf615bcf03..2860777af9a02 100644 --- a/packages/gradle/src/plugin/nodes.spec.ts +++ b/packages/gradle/src/plugin/nodes.spec.ts @@ -30,8 +30,17 @@ describe('@nx/gradle/plugin', () => { gradleFileToOutputDirsMap: new Map>([ ['proj/build.gradle', new Map([['build', 'build']])], ]), + gradleProjectToTasksMap: new Map>([ + ['proj', new Set(['test'])], + ]), gradleProjectToTasksTypeMap: new Map>([ - ['proj', new Map([['test', 'Verification']])], + [ + 'proj', + new Map([ + ['test', 'Verification'], + ['build', 'Build'], + ]), + ], ]), gradleProjectToProjectName: new Map([['proj', 'proj']]), gradleProjectNameToProjectRootMap: new Map([ @@ -90,7 +99,113 @@ describe('@nx/gradle/plugin', () => { ], }, "name": "proj", + "projectType": "application", + "targets": { + "test": { + "cache": true, + "command": "./gradlew proj:test", + "dependsOn": [ + "testClasses", + ], + "inputs": [ + "default", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:test", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + }, + }, + }, + }, + }, + ], + ] + `); + }); + + it('should create nodes include subprojects tasks', async () => { + const results = await createNodesFunction( + ['proj/build.gradle'], + { + buildTargetName: 'build', + includeSubprojectsTasks: true, + }, + context + ); + + expect(results).toMatchInlineSnapshot(` + [ + [ + "proj/build.gradle", + { + "projects": { + "proj": { + "metadata": { + "targetGroups": { + "Build": [ + "build", + ], + "Verification": [ + "test", + ], + }, + "technologies": [ + "gradle", + ], + }, + "name": "proj", + "projectType": "application", "targets": { + "build": { + "cache": true, + "command": "./gradlew proj:build", + "dependsOn": [ + "^build", + "classes", + "test", + ], + "inputs": [ + "production", + "^production", + ], + "metadata": { + "help": { + "command": "./gradlew help --task proj:build", + "example": { + "options": { + "args": [ + "--rerun", + ], + }, + }, + }, + "technologies": [ + "gradle", + ], + }, + "options": { + "cwd": ".", + }, + "outputs": [ + "build", + ], + }, "test": { "cache": true, "command": "./gradlew proj:test", @@ -138,6 +253,9 @@ describe('@nx/gradle/plugin', () => { gradleFileToOutputDirsMap: new Map>([ ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], ]), + gradleProjectToTasksMap: new Map>([ + ['proj', new Set(['test'])], + ]), gradleProjectToTasksTypeMap: new Map>([ ['proj', new Map([['test', 'Verification']])], ]), @@ -177,6 +295,7 @@ describe('@nx/gradle/plugin', () => { ], }, "name": "proj", + "projectType": "application", "targets": { "test": { "cache": true, @@ -226,6 +345,9 @@ describe('@nx/gradle/plugin', () => { gradleFileToOutputDirsMap: new Map>([ ['nested/nested/proj/build.gradle', new Map([['build', 'build']])], ]), + gradleProjectToTasksMap: new Map>([ + ['proj', new Set(['test'])], + ]), gradleProjectToTasksTypeMap: new Map>([ ['proj', new Map([['test', 'Test']])], ]), @@ -290,6 +412,7 @@ describe('@nx/gradle/plugin', () => { ], }, "name": "proj", + "projectType": "application", "targets": { "test": { "cache": false, diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts index f17aae096cbff..cbedf477d1b37 100644 --- a/packages/gradle/src/plugin/nodes.ts +++ b/packages/gradle/src/plugin/nodes.ts @@ -31,7 +31,7 @@ import { getGradleExecFile, findGraldewFile } from '../utils/exec-gradle'; const cacheableTaskType = new Set(['Build', 'Verification']); const dependsOnMap = { - build: ['^build', 'classes'], + build: ['^build', 'classes', 'test'], testClasses: ['classes'], test: ['testClasses'], classes: ['^classes'], @@ -43,11 +43,12 @@ interface GradleTask { } 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; + [taskTargetName: string]: string | undefined | boolean; } function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions { @@ -58,14 +59,7 @@ function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions { return options; } -type GradleTargets = Record< - string, - { - name: string; - targets: Record; - metadata: ProjectConfiguration['metadata']; - } ->; +type GradleTargets = Record>; function readTargetsCache(cachePath: string): GradleTargets { return existsSync(cachePath) ? readJsonFile(cachePath) : {}; @@ -180,6 +174,7 @@ async function createGradleProject( try { const { gradleProjectToTasksTypeMap, + gradleProjectToTasksMap, gradleFileToOutputDirsMap, gradleFileToGradleProjectMap, gradleProjectToProjectName, @@ -193,16 +188,26 @@ async function createGradleProject( return; } - const tasksTypeMap = gradleProjectToTasksTypeMap.get(gradleProject) as Map< - string, - string - >; + const tasksTypeMap: Map = gradleProjectToTasksTypeMap.get( + gradleProject + ) as Map; + const tasksSet = gradleProjectToTasksMap.get(gradleProject) as Set; let tasks: GradleTask[] = []; - for (let [taskName, taskType] of tasksTypeMap.entries()) { + tasksSet.forEach((taskName) => { tasks.push({ - type: taskType, + 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< @@ -219,8 +224,9 @@ async function createGradleProject( gradleFilePath, testFiles ); - const project = { + const project: Partial = { name: projectName, + projectType: 'application', targets, metadata: { targetGroups, @@ -266,7 +272,7 @@ async function createGradleTargets( getTestCiTargets( testFiles, gradleProject, - targetName, + targetName as string, options.ciTargetName, inputsMap['test'], outputs, @@ -281,7 +287,7 @@ async function createGradleTargets( task.name }`; - targets[targetName] = { + targets[targetName as string] = { command: `${getGradleExecFile()} ${taskCommandToRun}`, options: { cwd: gradlewFileDirectory, @@ -303,10 +309,12 @@ async function createGradleTargets( ...(outputs && outputs.length ? { outputs } : {}), }; - if (!targetGroups[task.type]) { - targetGroups[task.type] = []; + if (task.type) { + if (!targetGroups[task.type]) { + targetGroups[task.type] = []; + } + targetGroups[task.type].push(targetName as string); } - targetGroups[task.type].push(targetName); } return { targetGroups, targets }; } diff --git a/packages/gradle/src/utils/get-gradle-report.ts b/packages/gradle/src/utils/get-gradle-report.ts index 1d689890e6ce5..254e8b16c36bc 100644 --- a/packages/gradle/src/utils/get-gradle-report.ts +++ b/packages/gradle/src/utils/get-gradle-report.ts @@ -21,6 +21,7 @@ export interface GradleReport { buildFileToDepsMap: Map; gradleFileToOutputDirsMap: Map>; gradleProjectToTasksTypeMap: Map>; + gradleProjectToTasksMap: Map>; gradleProjectToProjectName: Map; gradleProjectNameToProjectRootMap: Map; gradleProjectToChildProjects: Map; @@ -105,6 +106,7 @@ export function processProjectReports( * Map of Gradle Build File to tasks type map */ const gradleProjectToTasksTypeMap = new Map>(); + const gradleProjectToTasksMap = new Map>(); const gradleProjectToProjectName = new Map(); const gradleProjectNameToProjectRootMap = new Map(); /** @@ -159,6 +161,7 @@ export function processProjectReports( absBuildFilePath: string, absBuildDirPath: string; const outputDirMap = new Map(); + const tasks = new Set(); for (const line of propertyReportLines) { if (line.startsWith('name: ')) { projectName = line.substring('name: '.length); @@ -190,6 +193,10 @@ export function processProjectReports( `{workspaceRoot}/${relative(workspaceRoot, dirPath)}` ); } + if (line.includes(': task ')) { + const [task] = line.split(': task '); + tasks.add(task); + } } if (!projectName || !absBuildFilePath || !absBuildDirPath) { @@ -217,6 +224,7 @@ export function processProjectReports( gradleProject, dirname(buildFile) ); + gradleProjectToTasksMap.set(gradleProject, tasks); } if (line.endsWith('taskReport')) { const gradleProject = line.substring( @@ -267,6 +275,7 @@ export function processProjectReports( buildFileToDepsMap, gradleFileToOutputDirsMap, gradleProjectToTasksTypeMap, + gradleProjectToTasksMap, gradleProjectToProjectName, gradleProjectNameToProjectRootMap, gradleProjectToChildProjects,