From 2c09766ac463faea2ed996052e7c6a252cab663e Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Fri, 1 Dec 2023 17:21:28 -0500 Subject: [PATCH] feat(core): add gradle plugin --- .circleci/config.yml | 47 +++++ .github/workflows/e2e-matrix.yml | 4 + .nx/workflows/agents.yaml | 20 +++ CODEOWNERS | 4 + e2e/gradle/jest.config.ts | 14 ++ e2e/gradle/project.json | 10 ++ e2e/gradle/src/gradle.test.ts | 57 ++++++ e2e/gradle/tsconfig.json | 13 ++ e2e/gradle/tsconfig.spec.json | 20 +++ e2e/utils/create-project-utils.ts | 1 + e2e/utils/get-env-info.ts | 1 + nx-dev/nx-dev/public/images/icons/gradle.svg | 9 + nx-dev/ui-references/src/lib/icons-map.ts | 1 + packages/gradle/.eslintrc.json | 37 ++++ packages/gradle/README.md | 13 ++ packages/gradle/index.ts | 1 + packages/gradle/jest.config.ts | 10 ++ packages/gradle/migrations.json | 4 + packages/gradle/package.json | 35 ++++ packages/gradle/plugin.ts | 2 + packages/gradle/project.json | 66 +++++++ packages/gradle/src/plugin/dependencies.ts | 121 +++++++++++++ packages/gradle/src/plugin/nodes.ts | 164 ++++++++++++++++++ packages/gradle/src/utils/exec-gradle.ts | 60 +++++++ .../gradle/src/utils/get-gradle-report.ts | 162 +++++++++++++++++ packages/gradle/tsconfig.json | 14 ++ packages/gradle/tsconfig.lib.json | 10 ++ packages/gradle/tsconfig.spec.json | 9 + .../project-graph/project-graph-builder.ts | 12 +- tsconfig.base.json | 2 + 30 files changed, 918 insertions(+), 5 deletions(-) create mode 100644 e2e/gradle/jest.config.ts create mode 100644 e2e/gradle/project.json create mode 100644 e2e/gradle/src/gradle.test.ts create mode 100644 e2e/gradle/tsconfig.json create mode 100644 e2e/gradle/tsconfig.spec.json create mode 100644 nx-dev/nx-dev/public/images/icons/gradle.svg create mode 100644 packages/gradle/.eslintrc.json create mode 100644 packages/gradle/README.md create mode 100644 packages/gradle/index.ts create mode 100644 packages/gradle/jest.config.ts create mode 100644 packages/gradle/migrations.json create mode 100644 packages/gradle/package.json create mode 100644 packages/gradle/plugin.ts create mode 100644 packages/gradle/project.json create mode 100644 packages/gradle/src/plugin/dependencies.ts create mode 100644 packages/gradle/src/plugin/nodes.ts create mode 100644 packages/gradle/src/utils/exec-gradle.ts create mode 100644 packages/gradle/src/utils/get-gradle-report.ts create mode 100644 packages/gradle/tsconfig.json create mode 100644 packages/gradle/tsconfig.lib.json create mode 100644 packages/gradle/tsconfig.spec.json diff --git a/.circleci/config.yml b/.circleci/config.yml index c5dfc45b152ae5..fc583c885066fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -69,6 +69,50 @@ commands: - ~/.pnpm-store - ~/.cache/Cypress - node_modules + install-sdkman: + description: Install SDKMAN + steps: + - restore_cache: + name: Restore SDKMAN executable and binaries from cache + keys: + - sdkman-cli-{{ arch }}-v2 + - run: + name: Installing SDKMAN + command: | + if [ ! -d ~/.sdkman ] + then + curl -s "https://get.sdkman.io?rcupdate=false" | bash + sed -i -e 's/sdkman_auto_answer=false/sdkman_auto_answer=true/g' ~/.sdkman/etc/config + fi + echo -e '\nsource "/home/circleci/.sdkman/bin/sdkman-init.sh"' >> $BASH_ENV + source $BASH_ENV + sdk version + - save_cache: + name: Save SDKMAN executable and binaries to cache + key: sdkman-cli-{{ arch }}-v2 + paths: + - ~/.sdkman + install-gradle: + description: Install gradle + parameters: + gradle-version: + type: string + default: '' + steps: + - restore_cache: + name: Restore Gradle binary from cache + keys: + - gradle-cli-{{ arch }}-v1 + - run: + name: Installing Gradle + command: | + sdk install gradle << parameters.gradle-version >> + gradle --version + - save_cache: + name: Save Gradle binary to cache + key: gradle-cli-{{ arch }}-v1 + paths: + - ~/.sdkman/candidates/gradle/ # ------------------------- # JOBS # ------------------------- @@ -95,6 +139,9 @@ jobs: sudo apt-get install -y ca-certificates lsof - browser-tools/install-chrome - browser-tools/install-chromedriver + - install-sdkman + - install-gradle: + gradle-version: '8.5' - run-pnpm-install: os: linux - nx/set-shas: diff --git a/.github/workflows/e2e-matrix.yml b/.github/workflows/e2e-matrix.yml index ea305376d8561e..2d8845881fdd7b 100644 --- a/.github/workflows/e2e-matrix.yml +++ b/.github/workflows/e2e-matrix.yml @@ -165,6 +165,8 @@ jobs: codeowners: 'S04SJ6HHP0X' - project: e2e-expo codeowners: 'S04TNCNJG5N' + - project: e2e-gradle + codeowners: 'S04TNCNJG5N' - project: e2e-jest codeowners: 'S04T16BTJJY' - project: e2e-js @@ -242,6 +244,8 @@ jobs: project: e2e-esbuild - node_version: 18 project: e2e-expo + - node_version: 18 + project: e2e-gradle - node_version: 18 project: e2e-jest - node_version: 18 diff --git a/.nx/workflows/agents.yaml b/.nx/workflows/agents.yaml index 902127533a762f..dba51bcfd1d01f 100644 --- a/.nx/workflows/agents.yaml +++ b/.nx/workflows/agents.yaml @@ -21,6 +21,8 @@ launch-templates: node_modules ~/.cache/Cypress ~/.pnpm-store + ~/.sdkman + ~/.sdkman/candidates/gradle BASE_BRANCH: 'master' - name: Install Pnpm @@ -47,3 +49,21 @@ launch-templates: - name: Load Cargo Env script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV + + - name: Install zip and unzip + script: apt-get -yqq install zip unzip + + - name: Install SDKMAN and gradle + script: | + if [ ! -d $HOME/.sdkman ] + then + curl -s "https://get.sdkman.io" | bash + source "$HOME/.sdkman/bin/sdkman-init.sh" + fi + sdk version + if [ ! -d $HOME/.sdkman/candidates/gradle/8.5 ] + then + sdk install gradle 8.5 + fi + gradle --version + echo "PATH=$HOME/.sdkman/candidates/gradle/8.5/bin:$PATH" >> $NX_CLOUD_ENV diff --git a/CODEOWNERS b/CODEOWNERS index 1a742a3cc547be..df19614e10a65f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -144,6 +144,10 @@ rust-toolchain @nrwl/nx-native-reviewers /packages/devkit/public-api.ts @FrozenPandaz @vsavkin /packages/devkit/nx.ts @FrozenPandaz @vsavkin +## Gradle +/packages/gradle/** @FrozenPandaz @xiongemi +/e2e/gradle/** @FrozenPandaz @xiongemi + # Nx-Plugin /docs/generated/packages/plugin/** @nrwl/nx-devkit-reviewers @nrwl/nx-docs-reviewers /docs/shared/packages/plugin/** @nrwl/nx-devkit-reviewers @nrwl/nx-docs-reviewers diff --git a/e2e/gradle/jest.config.ts b/e2e/gradle/jest.config.ts new file mode 100644 index 00000000000000..735a04c47420f1 --- /dev/null +++ b/e2e/gradle/jest.config.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +export default { + transform: { + '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + maxWorkers: 1, + globals: {}, + globalSetup: '../utils/global-setup.ts', + globalTeardown: '../utils/global-teardown.ts', + displayName: 'e2e-gradle', + testTimeout: 600000, + preset: '../../jest.preset.js', +}; diff --git a/e2e/gradle/project.json b/e2e/gradle/project.json new file mode 100644 index 00000000000000..c20d6f05d778ea --- /dev/null +++ b/e2e/gradle/project.json @@ -0,0 +1,10 @@ +{ + "name": "e2e-gradle", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/gradle", + "projectType": "application", + "targets": { + "e2e": {} + }, + "implicitDependencies": ["eslint"] +} diff --git a/e2e/gradle/src/gradle.test.ts b/e2e/gradle/src/gradle.test.ts new file mode 100644 index 00000000000000..fe0518c141f155 --- /dev/null +++ b/e2e/gradle/src/gradle.test.ts @@ -0,0 +1,57 @@ +import { + cleanupProject, + createFile, + e2eConsoleLogger, + newProject, + runCLI, + runCommand, + uniq, + updateJson, +} from '@nx/e2e/utils'; +import { execSync } from 'child_process'; + +describe('Gradle', () => { + let gradleProjectName = uniq('my-gradle-project'); + + beforeAll(() => { + newProject(); + createGradleProject(gradleProjectName); + }); + afterAll(() => cleanupProject()); + + it('should build', () => { + const projects = runCLI(`show projects`); + e2eConsoleLogger(projects); + expect(projects).toContain('app'); + expect(projects).toContain('list'); + expect(projects).toContain('utilities'); + expect(projects).toContain(gradleProjectName); + + runCLI('build app'); + runCLI('build list'); + runCLI('build utilities'); + }); +}); + +function createGradleProject(projectName: string) { + e2eConsoleLogger(`Using java version: ${execSync('java --version')}`); + e2eConsoleLogger(`Using gradle version: ${execSync('gradle --version')}`); + e2eConsoleLogger(execSync(`gradle help --task :init`).toString()); + e2eConsoleLogger( + runCommand( + `gradle init --type kotlin-application --dsl kotlin --project-name ${projectName} --package gradleProject --no-incubating --split-project` + ) + ); + updateJson('nx.json', (nxJson) => { + nxJson.plugins = ['@nx/gradle']; + return nxJson; + }); + createFile( + 'build.gradle.kts', + `allprojects { + apply { + plugin("project-report") + } + }` + ); +} diff --git a/e2e/gradle/tsconfig.json b/e2e/gradle/tsconfig.json new file mode 100644 index 00000000000000..6d5abf84832009 --- /dev/null +++ b/e2e/gradle/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/e2e/gradle/tsconfig.spec.json b/e2e/gradle/tsconfig.spec.json new file mode 100644 index 00000000000000..1a24bfb0a13536 --- /dev/null +++ b/e2e/gradle/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "**/*.d.ts", + "jest.config.ts" + ] +} diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 4722f14af9ef5c..f78cf58fa71cc7 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -42,6 +42,7 @@ const nxPackages = [ `@nx/eslint-plugin`, `@nx/express`, `@nx/esbuild`, + `@nx/gradle`, `@nx/jest`, `@nx/js`, `@nx/eslint`, diff --git a/e2e/utils/get-env-info.ts b/e2e/utils/get-env-info.ts index cb834febdb1cbd..72a784543c5473 100644 --- a/e2e/utils/get-env-info.ts +++ b/e2e/utils/get-env-info.ts @@ -111,6 +111,7 @@ export const packageManagerLockFile = { }; export function ensureCypressInstallation() { + console.log(execSync('npx cypress version').toString().trim()); let cypressVerified = true; try { const r = execSync('npx cypress verify', { diff --git a/nx-dev/nx-dev/public/images/icons/gradle.svg b/nx-dev/nx-dev/public/images/icons/gradle.svg new file mode 100644 index 00000000000000..c667328188e8c2 --- /dev/null +++ b/nx-dev/nx-dev/public/images/icons/gradle.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/nx-dev/ui-references/src/lib/icons-map.ts b/nx-dev/ui-references/src/lib/icons-map.ts index 6f2d811b6df3e4..6536a5870720a8 100644 --- a/nx-dev/ui-references/src/lib/icons-map.ts +++ b/nx-dev/ui-references/src/lib/icons-map.ts @@ -8,6 +8,7 @@ export const iconsMap: Record = { 'eslint-plugin': '/images/icons/eslint.svg', expo: '/images/icons/expo.svg', express: '/images/icons/express.svg', + gradle: '/images/icons/gradle.svg', jest: '/images/icons/jest.svg', js: '/images/icons/javascript.svg', eslint: '/images/icons/eslint.svg', diff --git a/packages/gradle/.eslintrc.json b/packages/gradle/.eslintrc.json new file mode 100644 index 00000000000000..52b3d2c3351891 --- /dev/null +++ b/packages/gradle/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredDependencies": ["nx"] + } + ] + } + }, + { + "files": ["./package.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/nx-plugin-checks": "error" + } + } + ] +} diff --git a/packages/gradle/README.md b/packages/gradle/README.md new file mode 100644 index 00000000000000..d9248c1e08c121 --- /dev/null +++ b/packages/gradle/README.md @@ -0,0 +1,13 @@ +

Nx - Smart, Fast and Extensible Build System

+ +{{links}} + +
+ +# Nx: Smart, Fast and Extensible Build System + +Nx is a next generation build system with first class monorepo support and powerful integrations. + +This package is a [Gradle plugin for Nx](https://nx.dev/js/overview). + +{{content}} diff --git a/packages/gradle/index.ts b/packages/gradle/index.ts new file mode 100644 index 00000000000000..1110b6451f6a13 --- /dev/null +++ b/packages/gradle/index.ts @@ -0,0 +1 @@ +export * from './plugin'; diff --git a/packages/gradle/jest.config.ts b/packages/gradle/jest.config.ts new file mode 100644 index 00000000000000..6f81e3c9f8ec72 --- /dev/null +++ b/packages/gradle/jest.config.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +export default { + transform: { + '^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html', 'json'], + globals: {}, + displayName: 'gradle', + preset: '../../jest.preset.js', +}; diff --git a/packages/gradle/migrations.json b/packages/gradle/migrations.json new file mode 100644 index 00000000000000..f9d34cef28c2cd --- /dev/null +++ b/packages/gradle/migrations.json @@ -0,0 +1,4 @@ +{ + "generators": {}, + "packageJsonUpdates": {} +} diff --git a/packages/gradle/package.json b/packages/gradle/package.json new file mode 100644 index 00000000000000..a99524f07ab710 --- /dev/null +++ b/packages/gradle/package.json @@ -0,0 +1,35 @@ +{ + "name": "@nx/gradle", + "version": "0.0.1", + "private": true, + "description": "The Nx Plugin for gradle", + "repository": { + "type": "git", + "url": "https://github.com/nrwl/nx.git", + "directory": "packages/gradle" + }, + "keywords": [ + "Monorepo", + "Java", + "Gradle", + "CLI" + ], + "main": "./index", + "typings": "./index.d.ts", + "author": "Victor Savkin", + "license": "MIT", + "bugs": { + "url": "https://github.com/nrwl/nx/issues" + }, + "homepage": "https://nx.dev", + "ng-update": { + "requirements": {}, + "migrations": "./migrations.json" + }, + "dependencies": { + "@nx/devkit": "file:../devkit" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/gradle/plugin.ts b/packages/gradle/plugin.ts new file mode 100644 index 00000000000000..558233345f7aec --- /dev/null +++ b/packages/gradle/plugin.ts @@ -0,0 +1,2 @@ +export { createDependencies } from './src/plugin/dependencies'; +export { createNodes } from './src/plugin/nodes'; diff --git a/packages/gradle/project.json b/packages/gradle/project.json new file mode 100644 index 00000000000000..7eb44c92a83324 --- /dev/null +++ b/packages/gradle/project.json @@ -0,0 +1,66 @@ +{ + "name": "gradle", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/gradle/src", + "projectType": "library", + "targets": { + "build-base": { + "executor": "@nx/js:tsc", + "options": { + "assets": [ + { + "input": "packages/gradle", + "glob": "**/@(files|files-angular)/**", + "output": "/" + }, + { + "input": "packages/gradle", + "glob": "**/files/**/.gitkeep", + "output": "/" + }, + { + "input": "packages/gradle", + "glob": "**/*.json", + "ignore": ["**/tsconfig*.json", "project.json", ".eslintrc.json"], + "output": "/" + }, + { + "input": "packages/gradle", + "glob": "**/*.js", + "ignore": ["**/jest.config.js"], + "output": "/" + }, + { + "input": "packages/gradle", + "glob": "**/*.d.ts", + "output": "/" + }, + { + "input": "", + "glob": "LICENSE", + "output": "/" + } + ] + } + }, + "build": { + "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/build/packages/gradle"], + "options": { + "command": "node ./scripts/copy-readme.js gradle" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/gradle/jest.config.ts" + } + } + }, + "tags": [] +} diff --git a/packages/gradle/src/plugin/dependencies.ts b/packages/gradle/src/plugin/dependencies.ts new file mode 100644 index 00000000000000..8ed58ec6cb26a5 --- /dev/null +++ b/packages/gradle/src/plugin/dependencies.ts @@ -0,0 +1,121 @@ +import { + CreateDependenciesContext, + DependencyType, + RawProjectGraphDependency, + validateDependency, +} from '@nx/devkit'; +import { requireNx } from '@nx/devkit/nx'; +import { readFileSync } from 'node:fs'; +import { basename } from 'node:path'; + +import { getGradleReport } from '../utils/get-gradle-report'; + +const { + createProjectRootMappingsFromProjectConfigurations, + findProjectForPath, +} = requireNx(); + +export const createDependencies = async ( + _, + context: CreateDependenciesContext +) => { + const { filesToProcess, projects } = context; + + const gradleFiles = findGradleFiles(filesToProcess); + + if (gradleFiles.length === 0) { + return []; + } + + const projectRootMappings = + createProjectRootMappingsFromProjectConfigurations(projects); + let dependencies: RawProjectGraphDependency[] = []; + console.time('locating gradle dependencies'); + const { gradleFileToGradleProjectMap, buildFileToDepsMap } = + getGradleReport(); + /** + * Map of gradle project name to nx project name + */ + const gradleProjectToNxProjectMap = new Map(); + for (const [buildFile, gradleProject] of gradleFileToGradleProjectMap) { + const nxProject = findProjectForPath(buildFile, projectRootMappings); + gradleProjectToNxProjectMap.set(gradleProject, nxProject as string); + } + + for (const [source, gradleFile] of gradleFiles) { + const depsFile = buildFileToDepsMap.get(gradleFile) as string; + + if (depsFile) { + dependencies = dependencies.concat( + processGradleDependencies( + depsFile, + gradleProjectToNxProjectMap, + source, + gradleFile, + context + ) + ); + } + } + console.timeEnd('locating gradle dependencies'); + return dependencies; +}; +const gradleConfigFileNames = new Set(['build.gradle', 'build.gradle.kts']); + +function findGradleFiles( + filesToProcess: CreateDependenciesContext['filesToProcess'] +) { + const gradleFiles: [string, string][] = []; + + for (const [source, files] of Object.entries(filesToProcess.projectFileMap)) { + for (const file of files) { + if (gradleConfigFileNames.has(basename(file.file))) { + gradleFiles.push([source, file.file]); + } + } + } + return gradleFiles; +} +function processGradleDependencies( + depsFile: string, + gradleProjectToNxProjectMap: Map, + source: string, + gradleFile: string, + context: CreateDependenciesContext +) { + const dependencies: RawProjectGraphDependency[] = []; + const lines = readFileSync(depsFile).toString().split('\n'); + let inDeps = false; + for (const line of lines) { + if (line.startsWith('implementationDependenciesMetadata')) { + inDeps = true; + continue; + } + + if (inDeps) { + if (line === '') { + inDeps = false; + continue; + } + const [indents, dep] = line.split('--- '); + if ((indents === '\\' || indents === '+') && dep.startsWith('project ')) { + const gradleProjectName = dep + .substring('project '.length) + .replace(/ \(n\)$/, '') + .trim(); + const target = gradleProjectToNxProjectMap.get( + gradleProjectName + ) as string; + const dependency: RawProjectGraphDependency = { + source: source, + target, + type: DependencyType.static, + sourceFile: gradleFile, + }; + validateDependency(dependency, context); + dependencies.push(dependency); + } + } + } + return dependencies; +} diff --git a/packages/gradle/src/plugin/nodes.ts b/packages/gradle/src/plugin/nodes.ts new file mode 100644 index 00000000000000..19a0bc2882cf8c --- /dev/null +++ b/packages/gradle/src/plugin/nodes.ts @@ -0,0 +1,164 @@ +import { + CreateNodes, + CreateNodesContext, + TargetConfiguration, + readJsonFile, + writeJsonFile, +} from '@nx/devkit'; +import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; + +import { getGradleBinaryPath } from '../utils/exec-gradle'; +import { getGradleReport } from '../utils/get-gradle-report'; + +const nonCacheableGradleTaskTypes = new Set(['Application']); +const dependsOnMap = { + build: ['^build', 'classes'], + test: ['classes'], + classes: ['^classes'], +}; + +interface GradleTask { + type: string; + name: string; +} + +export interface GradlePluginOptions { + testTargetName?: string; + classesTargetName?: string; + buildTargetName?: string; + [taskTargetName: string]: string | undefined; +} + +const cachePath = join(projectGraphCacheDirectory, 'gradle.hash'); +const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; + +export const calculatedTargets: Record< + string, + Record +> = {}; + +function readTargetsCache(): Record< + string, + Record +> { + return readJsonFile(cachePath); +} + +export function writeTargetsToCache( + targets: Record> +) { + writeJsonFile(cachePath, targets); +} + +export const createNodes: CreateNodes = [ + '**/build.{gradle.kts,gradle}', + ( + gradleFilePath, + options: GradlePluginOptions | undefined, + context: CreateNodesContext + ) => { + const projectRoot = dirname(gradleFilePath); + + const hash = calculateHashForCreateNodes( + projectRoot, + options ?? {}, + context + ); + + try { + const { + tasksMap, + gradleProjectToTasksTypeMap, + gradleFileToOutputDirsMap, + gradleFileToGradleProjectMap, + gradleProjectToProjectName, + } = getGradleReport(); + + const gradleProject = gradleFileToGradleProjectMap.get( + gradleFilePath + ) as string; + const projectName = gradleProjectToProjectName.get(gradleProject); + if (!projectName) { + return; + } + + const availableTaskNames = tasksMap.get(gradleFilePath) as string[]; + const tasksTypeMap = gradleProjectToTasksTypeMap.get( + gradleProject + ) as Map; + const tasks: GradleTask[] = availableTaskNames.map((taskName) => { + return { + type: tasksTypeMap.get(taskName) ?? 'Unknown', + name: taskName, + }; + }); + + const outputDirs = gradleFileToOutputDirsMap.get(gradleFilePath) as Map< + string, + string + >; + + const targets = + targetsCache[hash] ?? + createGradleTargets(tasks, projectRoot, options, context, outputDirs); + calculatedTargets[hash] = targets; + + return { + projects: { + [projectRoot]: { + root: projectRoot, + name: projectName, + targets, + }, + }, + }; + } catch (e) { + console.error(e); + return {}; + } + }, +]; + +function createGradleTargets( + tasks: GradleTask[], + projectRoot: string, + options: GradlePluginOptions | undefined, + context: CreateNodesContext, + outputDirs: Map +): Record { + const inputsMap = createInputsMap(context); + + const targets: Record = {}; + for (const task of tasks) { + const targetName = options?.[`${task.name}TargetName`] ?? task.name; + + const outputs = outputDirs.get(task.name); + targets[targetName] = { + command: `${getGradleBinaryPath()} ${task.name}`, + options: { + cwd: projectRoot, + }, + cache: !nonCacheableGradleTaskTypes.has(task.type), + inputs: inputsMap[task.name], + outputs: outputs ? [outputs] : undefined, + dependsOn: dependsOnMap[task.name], + }; + } + return 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: ['default', '^default'], + }; +} diff --git a/packages/gradle/src/utils/exec-gradle.ts b/packages/gradle/src/utils/exec-gradle.ts new file mode 100644 index 00000000000000..cc0f120756e56d --- /dev/null +++ b/packages/gradle/src/utils/exec-gradle.ts @@ -0,0 +1,60 @@ +import { workspaceRoot } from '@nx/devkit'; +import { ExecFileOptions } from 'child_process'; +import { + ExecFileSyncOptionsWithBufferEncoding, + execFile, + execFileSync, +} from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; + +export function execGradle( + args: string[], + execOptions: ExecFileSyncOptionsWithBufferEncoding +) { + const gradleBinaryPath = getGradleBinaryPath(); + + return execFileSync(gradleBinaryPath, args, execOptions); +} + +export function getGradleBinaryPath() { + const gradleBinaryPath = join(workspaceRoot, './gradlew'); + if (!existsSync(gradleBinaryPath)) { + throw new Error('Gradle is not setup. Run "gradle init"'); + } + + return gradleBinaryPath; +} + +export function execGradleAsync( + args: ReadonlyArray, + execOptions: ExecFileOptions +) { + const gradleBinaryPath = join(workspaceRoot, './gradlew'); + if (!existsSync(gradleBinaryPath)) { + throw new Error('Gradle is not setup. Run "gradle init"'); + } + + return new Promise((res, rej) => { + const cp = execFile(gradleBinaryPath, args, execOptions); + + let stdout = Buffer.from(''); + cp.stdout?.on('data', (data) => { + stdout += data; + }); + + cp.on('exit', (code) => { + if (code === 0) { + res(stdout); + } else { + rej( + new Error( + `Executing Gradle with ${args.join( + ' ' + )} failed with code: ${code}. \nLogs: ${stdout}` + ) + ); + } + }); + }); +} diff --git a/packages/gradle/src/utils/get-gradle-report.ts b/packages/gradle/src/utils/get-gradle-report.ts new file mode 100644 index 00000000000000..c387b4fcf31b13 --- /dev/null +++ b/packages/gradle/src/utils/get-gradle-report.ts @@ -0,0 +1,162 @@ +import { readFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; + +import { workspaceRoot } from '@nx/devkit'; + +import { execGradle } from './exec-gradle'; + +let gradleReport: ReturnType; +export function getGradleReport() { + if (gradleReport) { + return gradleReport; + } + console.time('executing gradle commands'); + const projectReportLines = execGradle(['projectReport'], { + cwd: workspaceRoot, + }) + .toString() + .split('\n'); + console.timeEnd('executing gradle commands'); + gradleReport = processProjectReports(projectReportLines); + return gradleReport; +} + +function processProjectReports(projectReportLines: string[]) { + /** + * Map of Gradle File path to Gradle Project Name + */ + const gradleFileToGradleProjectMap = new Map(); + /** + * Map of Gradle Project Name to Gradle File + */ + const gradleProjectToGradleFileMap = new Map(); + const dependenciesMap = new Map(); + /** + * Map of Gradle Build File to available tasks + */ + const tasksMap = new Map(); + /** + * Map of Gradle Build File to tasks type map + */ + const gradleProjectToTasksTypeMap = new Map>(); + const gradleProjectToProjectName = new Map(); + /** + * Map of buildFile to dependencies report path + */ + const buildFileToDepsMap = new Map(); + /** + * Map fo possible output files of each gradle file + * e.g. {build.gradle.kts: { projectReportDir: '' testReportDir: '' }} + */ + const gradleFileToOutputDirsMap = new Map>(); + + projectReportLines.forEach((line, index) => { + if (line.startsWith('> Task ')) { + const nextLine = projectReportLines[index + 1]; + if (line.endsWith(':dependencyReport')) { + const gradleProject = line.substring( + '> Task '.length, + line.length - ':dependencyReport'.length + ); + const [_, file] = nextLine.split('file://'); + dependenciesMap.set(gradleProject, file); + } + if (line.endsWith('propertyReport')) { + const gradleProject = line.substring( + '> Task '.length, + line.length - ':propertyReport'.length + ); + const [_, file] = nextLine.split('file://'); + const propertyReportLines = readFileSync(file).toString().split('\n'); + + let projectName: string, + absBuildFilePath: string, + absBuildDirPath: string; + const tasks: string[] = []; + const outputDirMap = new Map(); + for (const line of propertyReportLines) { + if (line.startsWith('name: ')) { + projectName = line.substring('name: '.length); + } + if (line.startsWith('buildFile: ')) { + absBuildFilePath = line.substring('buildFile: '.length); + } + if (line.startsWith('buildDir: ')) { + absBuildDirPath = line.substring('buildDir: '.length); + } + if (line.includes(': task ')) { + const taskSegments = line.split(': task '); + tasks.push(taskSegments[0]); + } + if (line.includes('Dir: ')) { + const [dirName, dirPath] = line.split(': '); + const taskName = dirName.replace('Dir', ''); + outputDirMap.set( + taskName, + `{workspaceRoot}/${relative(workspaceRoot, dirPath)}` + ); + } + } + + if (!projectName || !absBuildFilePath || !absBuildDirPath) { + return; + } + const buildFile = relative(workspaceRoot, absBuildFilePath); + const buildDir = relative(workspaceRoot, absBuildDirPath); + buildFileToDepsMap.set( + buildFile, + dependenciesMap.get(gradleProject) as string + ); + + outputDirMap.set('build', `{workspaceRoot}/${buildDir}`); + outputDirMap.set( + 'classes', + `{workspaceRoot}/${join(buildDir, 'classes')}` + ); + + gradleFileToOutputDirsMap.set(buildFile, outputDirMap); + gradleFileToGradleProjectMap.set(buildFile, gradleProject); + gradleProjectToGradleFileMap.set(gradleProject, buildFile); + gradleProjectToProjectName.set(gradleProject, projectName); + tasksMap.set(buildFile, tasks); + } + if (line.endsWith('taskReport')) { + const gradleProject = line.substring( + '> Task '.length, + line.length - ':taskReport'.length + ); + const [_, file] = nextLine.split('file://'); + const taskTypeMap = new Map(); + const tasksFileLines = readFileSync(file).toString().split('\n'); + + let i = 0; + while (i < tasksFileLines.length) { + const line = tasksFileLines[i]; + + if (line.endsWith('tasks')) { + const dashes = new Array(line.length + 1).join('-'); + if (tasksFileLines[i + 1] === dashes) { + const type = line.substring(0, line.length - ' tasks'.length); + i++; + while (tasksFileLines[++i] !== '') { + const [taskName] = tasksFileLines[i].split(' - '); + taskTypeMap.set(taskName, type); + } + } + } + i++; + } + gradleProjectToTasksTypeMap.set(gradleProject, taskTypeMap); + } + } + }); + + return { + gradleFileToGradleProjectMap, + buildFileToDepsMap, + gradleFileToOutputDirsMap, + gradleProjectToTasksTypeMap, + tasksMap, + gradleProjectToProjectName, + }; +} diff --git a/packages/gradle/tsconfig.json b/packages/gradle/tsconfig.json new file mode 100644 index 00000000000000..f5a01e5713e0e2 --- /dev/null +++ b/packages/gradle/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": {}, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/gradle/tsconfig.lib.json b/packages/gradle/tsconfig.lib.json new file mode 100644 index 00000000000000..7bfc80f73e6fad --- /dev/null +++ b/packages/gradle/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/packages/gradle/tsconfig.spec.json b/packages/gradle/tsconfig.spec.json new file mode 100644 index 00000000000000..546f12877f7f05 --- /dev/null +++ b/packages/gradle/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/packages/nx/src/project-graph/project-graph-builder.ts b/packages/nx/src/project-graph/project-graph-builder.ts index aa09bfb5fdb0b1..a3ad761422cf6e 100644 --- a/packages/nx/src/project-graph/project-graph-builder.ts +++ b/packages/nx/src/project-graph/project-graph-builder.ts @@ -383,11 +383,13 @@ export class ProjectGraphBuilder { for (let f of files) { if (f.deps) { for (let d of f.deps) { - const target = fileDataDepTarget(d); - if (!fileDeps.has(target)) { - fileDeps.set(target, new Set([fileDataDepType(d)])); - } else { - fileDeps.get(target).add(fileDataDepType(d)); + if (d) { + const target = fileDataDepTarget(d); + if (!fileDeps.has(target)) { + fileDeps.set(target, new Set([fileDataDepType(d)])); + } else { + fileDeps.get(target).add(fileDataDepType(d)); + } } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index e28134d1b4d12c..9a89e9b5ba14e1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -35,6 +35,8 @@ "@nx/expo": ["packages/expo"], "@nx/expo/*": ["packages/expo/*"], "@nx/express": ["packages/express"], + "@nx/gradle": ["packages/gradle/src/index.ts"], + "@nx/gradle/*": ["packages/gradle/*"], "@nx/graph/project-details": ["graph/project-details/src/index.ts"], "@nx/graph/shared": ["graph/shared/src/index.ts"], "@nx/graph/ui-components": ["graph/ui-components/src/index.ts"],