feat(gradle): support composite build (#25990)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

## Current Behavior
<!-- This is the behavior we have today -->

## Expected Behavior
![Workspace Project Graph
(2)](https://github.com/nrwl/nx/assets/16211801/9776a642-16c6-45ee-a253-cfc4b86a12a8)



## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Emily Xiong 2024-06-24 13:05:32 -07:00 committed by GitHub
parent 3b2c42a8a5
commit 0e7e4690f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 450 additions and 70 deletions

Binary file not shown.

View File

@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
validateDistributionUrl=false
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -53,7 +53,7 @@ describe('Gradle', () => {
createFile(
`app2/build.gradle`,
`plugins {
id 'gradleProject.groovy-application-conventions'
id 'buildlogic.groovy-application-conventions'
}
dependencies {
@ -64,7 +64,7 @@ dependencies {
createFile(
`app2/build.gradle.kts`,
`plugins {
id("gradleProject.kotlin-library-conventions")
id("buildlogic.kotlin-application-conventions")
}
dependencies {

View File

@ -1,4 +1,11 @@
{
"generators": {},
"generators": {
"add-project-report-all": {
"version": "19.4.0-beta.1",
"cli": "nx",
"description": "Add task projectReportAll to build.gradle file",
"factory": "./src/migrations/19-4-0/add-project-report-all"
}
},
"packageJsonUpdates": {}
}

View File

@ -0,0 +1,8 @@
import json = require('./migrations.json');
import { assertValidMigrationPaths } from '@nx/devkit/internal-testing-utils';
import { MigrationsJson } from '@nx/devkit';
describe('gradle migrations', () => {
assertValidMigrationPaths(json as MigrationsJson, __dirname);
});

View File

@ -2,6 +2,7 @@ import {
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
globAsync,
logger,
readNxJson,
runTasksInSerial,
@ -12,6 +13,7 @@ import { execSync } from 'child_process';
import { nxVersion } from '../../utils/versions';
import { InitGeneratorSchema } from './schema';
import { hasGradlePlugin } from '../../utils/has-gradle-plugin';
import { dirname, join, basename } from 'path';
export async function initGenerator(tree: Tree, options: InitGeneratorSchema) {
const tasks: GeneratorCallback[] = [];
@ -36,10 +38,9 @@ Running 'gradle init':`);
)
);
}
await addBuildGradleFileNextToSettingsGradle(tree);
addPlugin(tree);
updateNxJsonConfiguration(tree);
addProjectReportToBuildGradle(tree);
if (!options.skipFormat) {
await formatFiles(tree);
@ -66,23 +67,39 @@ function addPlugin(tree: Tree) {
}
/**
* This function adds the project-report plugin to the build.gradle or build.gradle.kts file
* This function creates and populate build.gradle file next to the settings.gradle file.
*/
function addProjectReportToBuildGradle(tree: Tree) {
let buildGradleFile: string;
if (tree.exists('settings.gradle.kts')) {
buildGradleFile = 'build.gradle.kts';
} else if (tree.exists('settings.gradle')) {
buildGradleFile = 'build.gradle';
export async function addBuildGradleFileNextToSettingsGradle(tree: Tree) {
const settingsGradleFiles = await globAsync(tree, [
'**/settings.gradle?(.kts)',
]);
settingsGradleFiles.forEach((settingsGradleFile) => {
addProjectReportToBuildGradle(settingsGradleFile, tree);
});
}
/**
* - creates a build.gradle file next to the settings.gradle file if it does not exist.
* - adds the project-report plugin to the build.gradle file if it does not exist.
* - adds a task to generate project reports for all subprojects and included builds.
*/
function addProjectReportToBuildGradle(settingsGradleFile: string, tree: Tree) {
const filename = basename(settingsGradleFile);
let gradleFilePath = 'build.gradle';
if (filename.endsWith('.kts')) {
gradleFilePath = 'build.gradle.kts';
}
gradleFilePath = join(dirname(settingsGradleFile), gradleFilePath);
let buildGradleContent = '';
if (!tree.exists(gradleFilePath)) {
tree.write(gradleFilePath, buildGradleContent); // create a build.gradle file near settings.gradle file if it does not exist
} else {
buildGradleContent = tree.read(gradleFilePath).toString();
}
let buildGradleContent = '';
if (tree.exists(buildGradleFile)) {
buildGradleContent = tree.read(buildGradleFile).toString();
}
if (buildGradleContent.includes('allprojects')) {
if (!buildGradleContent.includes('"project-report')) {
logger.warn(`Please add the project-report plugin to your ${buildGradleFile}:
if (!buildGradleContent.includes('"project-report"')) {
logger.warn(`Please add the project-report plugin to your ${gradleFilePath}:
allprojects {
apply {
plugin("project-report")
@ -95,7 +112,37 @@ allprojects {
plugin("project-report")
}
}`;
tree.write(buildGradleFile, buildGradleContent);
}
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);
}
}

View File

@ -0,0 +1,28 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree } from '@nx/devkit';
import update from './add-project-report-all';
describe('AddProjectReportAll', () => {
let tree: Tree;
beforeAll(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should update build.gradle', async () => {
tree.write('settings.gradle', '');
await update(tree);
const buildGradle = tree.read('build.gradle').toString();
expect(buildGradle).toContain('project-report');
expect(buildGradle).toContain('projectReportAll');
});
it('should update build.gradle.kts', async () => {
tree.write('settings.gradle.kts', '');
await update(tree);
const buildGradle = tree.read('build.gradle.kts').toString();
expect(buildGradle).toContain('project-report');
expect(buildGradle).toContain('projectReportAll');
});
});

View File

@ -0,0 +1,9 @@
import { Tree } from '@nx/devkit';
import { addBuildGradleFileNextToSettingsGradle } from '../../generators/init/init';
/**
* This migration adds task `projectReportAll` to build.gradle files
*/
export default async function update(tree: Tree) {
await addBuildGradleFileNextToSettingsGradle(tree);
}

View File

@ -0,0 +1,72 @@
import { join } from 'path';
import { processGradleDependencies } from './dependencies';
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
validateDependency: jest.fn().mockReturnValue(true),
}));
describe('processGradleDependencies', () => {
it('should process gradle dependencies with composite build', () => {
const depFilePath = join(
__dirname,
'..',
'utils/__mocks__/gradle-composite-dependencies.txt'
);
const dependencies = new Set([]);
processGradleDependencies(
depFilePath,
new Map([
[':my-utils:number-utils', 'number-utils'],
[':my-utils:string-utils', 'string-utils'],
]),
'app',
'app',
{} as any,
dependencies
);
expect(Array.from(dependencies)).toEqual([
{
source: 'app',
sourceFile: 'app',
target: 'number-utils',
type: 'static',
},
{
source: 'app',
sourceFile: 'app',
target: 'string-utils',
type: 'static',
},
]);
});
it('should process gradle dependencies with regular build', () => {
const depFilePath = join(
__dirname,
'..',
'utils/__mocks__/gradle-dependencies.txt'
);
const dependencies = new Set([]);
processGradleDependencies(
depFilePath,
new Map([
[':my-utils:number-utils', 'number-utils'],
[':my-utils:string-utils', 'string-utils'],
[':utilities', 'utilities'],
]),
'app',
'app',
{} as any,
dependencies
);
expect(Array.from(dependencies)).toEqual([
{
source: 'app',
sourceFile: 'app',
target: 'utilities',
type: 'static',
},
]);
});
});

View File

@ -23,13 +23,13 @@ export const createDependencies: CreateDependencies = async (
return [];
}
let dependencies: RawProjectGraphDependency[] = [];
const gradleDependenciesStart = performance.mark('gradleDependencies:start');
const {
gradleFileToGradleProjectMap,
gradleProjectToProjectName,
buildFileToDepsMap,
} = getCurrentGradleReport();
const dependencies: Set<RawProjectGraphDependency> = new Set();
for (const gradleFile of gradleFiles) {
const gradleProject = gradleFileToGradleProjectMap.get(gradleFile);
@ -37,19 +37,17 @@ export const createDependencies: CreateDependencies = async (
const depsFile = buildFileToDepsMap.get(gradleFile);
if (projectName && depsFile) {
dependencies = dependencies.concat(
Array.from(
processGradleDependencies(
depsFile,
gradleProjectToProjectName,
projectName,
gradleFile,
context
)
)
context,
dependencies
);
}
}
const gradleDependenciesEnd = performance.mark('gradleDependencies:end');
performance.measure(
'gradleDependencies',
@ -57,7 +55,7 @@ export const createDependencies: CreateDependencies = async (
gradleDependenciesEnd.name
);
return dependencies;
return Array.from(dependencies);
};
const gradleConfigFileNames = new Set(['build.gradle', 'build.gradle.kts']);
@ -76,14 +74,14 @@ function findGradleFiles(fileMap: FileMap): string[] {
return gradleFiles;
}
function processGradleDependencies(
export function processGradleDependencies(
depsFile: string,
gradleProjectToProjectName: Map<string, string>,
sourceProjectName: string,
gradleFile: string,
context: CreateDependenciesContext
): Set<RawProjectGraphDependency> {
const dependencies: Set<RawProjectGraphDependency> = new Set();
context: CreateDependenciesContext,
dependencies: Set<RawProjectGraphDependency>
): void {
const lines = readFileSync(depsFile).toString().split(newLineSeparator);
let inDeps = false;
for (const line of lines) {
@ -101,14 +99,21 @@ function processGradleDependencies(
continue;
}
const [indents, dep] = line.split('--- ');
if ((indents === '\\' || indents === '+') && dep.startsWith('project ')) {
const gradleProjectName = dep
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 target = gradleProjectToProjectName.get(
gradleProjectName
) as string;
if (target) {
const dependency: RawProjectGraphDependency = {
source: sourceProjectName,
target,
@ -120,5 +125,5 @@ function processGradleDependencies(
}
}
}
return dependencies;
}
}

View File

@ -0,0 +1,60 @@
------------------------------------------------------------
Project ':my-app:app'
------------------------------------------------------------
annotationProcessor - Annotation processors and their dependencies for source set 'main'.
No dependencies
compileClasspath - Compile classpath for source set 'main'.
+--- org.sample:number-utils:1.0 -> project :my-utils:number-utils
\--- org.sample:string-utils:1.0 -> project :my-utils:string-utils
compileOnly - Compile-only dependencies for the 'main' feature. (n)
No dependencies
default - Configuration for default artifacts. (n)
No dependencies
implementation - Implementation dependencies for the 'main' feature. (n)
+--- org.sample:number-utils:1.0 (n)
\--- org.sample:string-utils:1.0 (n)
mainSourceElements - List of source directories contained in the Main SourceSet. (n)
No dependencies
runtimeClasspath - Runtime classpath of source set 'main'.
+--- org.sample:number-utils:1.0 -> project :my-utils:number-utils
\--- org.sample:string-utils:1.0 -> project :my-utils:string-utils
\--- org.apache.commons:commons-lang3:3.4
runtimeElements - Runtime elements for the 'main' feature. (n)
No dependencies
runtimeOnly - Runtime-only dependencies for the 'main' feature. (n)
No dependencies
testAnnotationProcessor - Annotation processors and their dependencies for source set 'test'.
No dependencies
testCompileClasspath - Compile classpath for source set 'test'.
+--- org.sample:number-utils:1.0 -> project :my-utils:number-utils
\--- org.sample:string-utils:1.0 -> project :my-utils:string-utils
testCompileOnly - Compile only dependencies for source set 'test'. (n)
No dependencies
testImplementation - Implementation only dependencies for source set 'test'. (n)
No dependencies
testRuntimeClasspath - Runtime classpath of source set 'test'.
+--- org.sample:number-utils:1.0 -> project :my-utils:number-utils
\--- org.sample:string-utils:1.0 -> project :my-utils:string-utils
\--- org.apache.commons:commons-lang3:3.4
testRuntimeOnly - Runtime only dependencies for source set 'test'. (n)
No dependencies
(n) - A dependency or dependency configuration that cannot be resolved.
A web-based, searchable dependency report is available by adding the --scan option.

View File

@ -0,0 +1,121 @@
------------------------------------------------------------
Project ':app'
------------------------------------------------------------
annotationProcessor - Annotation processors and their dependencies for source set 'main'.
No dependencies
compileClasspath - Compile classpath for source set 'main'.
+--- org.apache.commons:commons-text -> 1.11.0
| \--- org.apache.commons:commons-lang3:3.13.0
+--- project :utilities
| \--- project :list
\--- org.apache.commons:commons-text:1.11.0 (c)
compileOnly - Compile-only dependencies for the 'main' feature. (n)
No dependencies
default - Configuration for default artifacts. (n)
No dependencies
implementation - Implementation dependencies for the 'main' feature. (n)
+--- org.apache.commons:commons-text (n)
\--- project utilities (n)
mainSourceElements - List of source directories contained in the Main SourceSet. (n)
No dependencies
runtimeClasspath - Runtime classpath of source set 'main'.
+--- org.apache.commons:commons-text -> 1.11.0
| \--- org.apache.commons:commons-lang3:3.13.0
+--- project :utilities
| +--- project :list
| | \--- org.apache.commons:commons-text:1.11.0 (c)
| \--- org.apache.commons:commons-text:1.11.0 (c)
\--- org.apache.commons:commons-text:1.11.0 (c)
runtimeElements - Runtime elements for the 'main' feature. (n)
No dependencies
runtimeOnly - Runtime-only dependencies for the 'main' feature. (n)
No dependencies
testAnnotationProcessor - Annotation processors and their dependencies for source set 'test'.
No dependencies
testCompileClasspath - Compile classpath for source set 'test'.
+--- org.apache.commons:commons-text -> 1.11.0
| \--- org.apache.commons:commons-lang3:3.13.0
+--- project :utilities
| \--- project :list
+--- org.apache.commons:commons-text:1.11.0 (c)
\--- org.junit.jupiter:junit-jupiter:5.10.1
+--- org.junit:junit-bom:5.10.1
| +--- org.junit.jupiter:junit-jupiter:5.10.1 (c)
| +--- org.junit.jupiter:junit-jupiter-api:5.10.1 (c)
| +--- org.junit.jupiter:junit-jupiter-params:5.10.1 (c)
| \--- org.junit.platform:junit-platform-commons:1.10.1 (c)
+--- org.junit.jupiter:junit-jupiter-api:5.10.1
| +--- org.junit:junit-bom:5.10.1 (*)
| +--- org.opentest4j:opentest4j:1.3.0
| +--- org.junit.platform:junit-platform-commons:1.10.1
| | +--- org.junit:junit-bom:5.10.1 (*)
| | \--- org.apiguardian:apiguardian-api:1.1.2
| \--- org.apiguardian:apiguardian-api:1.1.2
\--- org.junit.jupiter:junit-jupiter-params:5.10.1
+--- org.junit:junit-bom:5.10.1 (*)
+--- org.junit.jupiter:junit-jupiter-api:5.10.1 (*)
\--- org.apiguardian:apiguardian-api:1.1.2
testCompileOnly - Compile only dependencies for source set 'test'. (n)
No dependencies
testImplementation - Implementation only dependencies for source set 'test'. (n)
\--- org.junit.jupiter:junit-jupiter:5.10.1 (n)
testRuntimeClasspath - Runtime classpath of source set 'test'.
+--- org.apache.commons:commons-text -> 1.11.0
| \--- org.apache.commons:commons-lang3:3.13.0
+--- project :utilities
| +--- project :list
| | \--- org.apache.commons:commons-text:1.11.0 (c)
| \--- org.apache.commons:commons-text:1.11.0 (c)
+--- org.apache.commons:commons-text:1.11.0 (c)
+--- org.junit.jupiter:junit-jupiter:5.10.1
| +--- org.junit:junit-bom:5.10.1
| | +--- org.junit.jupiter:junit-jupiter:5.10.1 (c)
| | +--- org.junit.jupiter:junit-jupiter-api:5.10.1 (c)
| | +--- org.junit.jupiter:junit-jupiter-engine:5.10.1 (c)
| | +--- org.junit.jupiter:junit-jupiter-params:5.10.1 (c)
| | +--- org.junit.platform:junit-platform-launcher:1.10.1 (c)
| | +--- org.junit.platform:junit-platform-commons:1.10.1 (c)
| | \--- org.junit.platform:junit-platform-engine:1.10.1 (c)
| +--- org.junit.jupiter:junit-jupiter-api:5.10.1
| | +--- org.junit:junit-bom:5.10.1 (*)
| | +--- org.opentest4j:opentest4j:1.3.0
| | \--- org.junit.platform:junit-platform-commons:1.10.1
| | \--- org.junit:junit-bom:5.10.1 (*)
| +--- org.junit.jupiter:junit-jupiter-params:5.10.1
| | +--- org.junit:junit-bom:5.10.1 (*)
| | \--- org.junit.jupiter:junit-jupiter-api:5.10.1 (*)
| \--- org.junit.jupiter:junit-jupiter-engine:5.10.1
| +--- org.junit:junit-bom:5.10.1 (*)
| +--- org.junit.platform:junit-platform-engine:1.10.1
| | +--- org.junit:junit-bom:5.10.1 (*)
| | +--- org.opentest4j:opentest4j:1.3.0
| | \--- org.junit.platform:junit-platform-commons:1.10.1 (*)
| \--- org.junit.jupiter:junit-jupiter-api:5.10.1 (*)
\--- org.junit.platform:junit-platform-launcher -> 1.10.1
+--- org.junit:junit-bom:5.10.1 (*)
\--- org.junit.platform:junit-platform-engine:1.10.1 (*)
testRuntimeOnly - Runtime only dependencies for source set 'test'. (n)
\--- org.junit.platform:junit-platform-launcher (n)
(c) - A dependency constraint, not a dependency. The dependency affected by the constraint occurs elsewhere in the tree.
(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.
(n) - A dependency or dependency configuration that cannot be resolved.
A web-based, searchable dependency report is available by adding the --scan option.

View File

@ -1,6 +1,6 @@
import { readFileSync } from 'fs';
import { join } from 'path';
import { processProjectReports, fileSeparator } from './get-gradle-report';
import { processProjectReports } from './get-gradle-report';
describe('processProjectReports', () => {
it('should process project reports', () => {

View File

@ -1,7 +1,12 @@
import { existsSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { normalizePath, workspaceRoot } from '@nx/devkit';
import {
AggregateCreateNodesError,
logger,
normalizePath,
workspaceRoot,
} from '@nx/devkit';
import { execGradleAsync } from './exec-gradle';
import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context';
@ -49,13 +54,38 @@ export async function populateGradleReport(
const gradleProjectReportStart = performance.mark(
'gradleProjectReport:start'
);
const projectReportLines = (
await execGradleAsync(['projectReport'], {
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);
.split(newLineSeparator)
.filter((line) => line.trim() !== '');
const gradleProjectReportEnd = performance.mark('gradleProjectReport:end');
performance.measure(
'gradleProjectReport',
@ -72,10 +102,6 @@ export function processProjectReports(
* Map of Gradle File path to Gradle Project Name
*/
const gradleFileToGradleProjectMap = new Map<string, string>();
/**
* Map of Gradle Project Name to Gradle File
*/
const gradleProjectToGradleFileMap = new Map<string, string>();
const dependenciesMap = new Map<string, string>();
/**
* Map of Gradle Build File to tasks type map
@ -170,7 +196,6 @@ export function processProjectReports(
gradleFileToOutputDirsMap.set(buildFile, outputDirMap);
gradleFileToGradleProjectMap.set(buildFile, gradleProject);
gradleProjectToGradleFileMap.set(gradleProject, buildFile);
gradleProjectToProjectName.set(gradleProject, projectName);
}
if (line.endsWith('taskReport')) {

View File

@ -84,12 +84,11 @@ export function writeMinimalNxJson(host: Tree, version: string) {
export function updateGitIgnore(host: Tree) {
let contents = host.read('.gitignore', 'utf-8') ?? '';
if (!contents.includes('.nx/installation')) {
contents = [contents, '.nx/installation'].join('\n');
}
if (!contents.includes('.nx/cache')) {
contents = [contents, '.nx/cache'].join('\n');
['.nx/installation', '.nx/cache', '.nx/workspace-data'].forEach((file) => {
if (!contents.includes(file)) {
contents = [contents, file].join('\n');
}
});
host.write('.gitignore', contents);
}