|
| 1 | +import crypto from 'node:crypto' |
1 | 2 | import fs from 'node:fs' |
| 3 | +import os from 'node:os' |
2 | 4 | import path from 'node:path' |
3 | 5 | import { EOL } from 'os' |
4 | 6 |
|
5 | 7 | import TOML from 'fast-toml' |
6 | 8 |
|
7 | 9 | import { readLicenseFile } from '../license/license_utils.js' |
8 | 10 | import Sbom from '../sbom.js' |
| 11 | +import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from '../tools.js' |
| 12 | +import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js' |
9 | 13 |
|
10 | 14 | import Base_java, { ecosystem_gradle } from "./base_java.js"; |
11 | 15 |
|
@@ -466,3 +470,130 @@ export default class Java_gradle extends Base_java { |
466 | 470 | return undefined |
467 | 471 | } |
468 | 472 | } |
| 473 | + |
| 474 | +const DEFAULT_GRADLE_DISCOVERY_IGNORE = [ |
| 475 | + '**/build/**', |
| 476 | + '**/.gradle/**', |
| 477 | +] |
| 478 | + |
| 479 | +/** |
| 480 | + * Resolve the Gradle binary, respecting wrapper preference. |
| 481 | + * |
| 482 | + * @param {string} startDir - Directory from which to start the wrapper search |
| 483 | + * @param {import('../index.js').Options} [opts={}] |
| 484 | + * @returns {string} Path to the Gradle binary |
| 485 | + */ |
| 486 | +function resolveGradleBinary(startDir, opts = {}) { |
| 487 | + const localWrapper = 'gradlew' + (process.platform === 'win32' ? '.bat' : '') |
| 488 | + const useWrapper = getWrapperPreference('gradle', opts) |
| 489 | + if (useWrapper) { |
| 490 | + const wrapper = traverseForWrapper(startDir, localWrapper) |
| 491 | + if (wrapper !== undefined) { |
| 492 | + return wrapper |
| 493 | + } |
| 494 | + } |
| 495 | + return getCustomPath('gradle', opts) |
| 496 | +} |
| 497 | + |
| 498 | +/** Gradle init script that emits structured project listing. */ |
| 499 | +const GRADLE_INIT_SCRIPT = `allprojects { |
| 500 | + task daListProjects { |
| 501 | + doLast { |
| 502 | + println "::DA_PROJECT::\${project.path}::\${project.projectDir}" |
| 503 | + } |
| 504 | + } |
| 505 | +} |
| 506 | +` |
| 507 | + |
| 508 | +/** |
| 509 | + * Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build. |
| 510 | + * Uses a custom init script to get structured project listing. |
| 511 | + * |
| 512 | + * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain settings.gradle[.kts]) |
| 513 | + * @param {import('../index.js').Options} [opts={}] |
| 514 | + * @returns {Promise<string[]>} Paths to build.gradle[.kts] files (absolute) |
| 515 | + */ |
| 516 | +export async function discoverGradleSubprojects(workspaceRoot, opts = {}) { |
| 517 | + const root = path.resolve(workspaceRoot) |
| 518 | + const hasSettings = fs.existsSync(path.join(root, 'settings.gradle')) |
| 519 | + || fs.existsSync(path.join(root, 'settings.gradle.kts')) |
| 520 | + |
| 521 | + if (!hasSettings) { |
| 522 | + return [] |
| 523 | + } |
| 524 | + |
| 525 | + const gradleBin = resolveGradleBinary(root, opts) |
| 526 | + const manifestPaths = [] |
| 527 | + |
| 528 | + const rootBuildKts = path.join(root, 'build.gradle.kts') |
| 529 | + const rootBuild = path.join(root, 'build.gradle') |
| 530 | + if (fs.existsSync(rootBuildKts)) { |
| 531 | + manifestPaths.push(rootBuildKts) |
| 532 | + } else if (fs.existsSync(rootBuild)) { |
| 533 | + manifestPaths.push(rootBuild) |
| 534 | + } |
| 535 | + |
| 536 | + const initScriptPath = path.join(os.tmpdir(), `da-list-projects-${crypto.randomUUID()}.gradle`) |
| 537 | + try { |
| 538 | + fs.writeFileSync(initScriptPath, GRADLE_INIT_SCRIPT) |
| 539 | + let output |
| 540 | + try { |
| 541 | + output = invokeCommand(gradleBin, [ |
| 542 | + '-q', '--no-daemon', |
| 543 | + '--init-script', initScriptPath, |
| 544 | + 'daListProjects', |
| 545 | + ], { cwd: root }) |
| 546 | + } catch { |
| 547 | + const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE] |
| 548 | + return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) |
| 549 | + } |
| 550 | + |
| 551 | + const projects = parseGradleInitScriptOutput(output.toString()) |
| 552 | + for (const proj of projects) { |
| 553 | + if (proj.path === ':') { |
| 554 | + continue |
| 555 | + } |
| 556 | + const projDir = path.resolve(proj.dir) |
| 557 | + const buildKts = path.join(projDir, 'build.gradle.kts') |
| 558 | + const buildGroovy = path.join(projDir, 'build.gradle') |
| 559 | + if (fs.existsSync(buildKts)) { |
| 560 | + manifestPaths.push(buildKts) |
| 561 | + } else if (fs.existsSync(buildGroovy)) { |
| 562 | + manifestPaths.push(buildGroovy) |
| 563 | + } |
| 564 | + } |
| 565 | + } finally { |
| 566 | + try { fs.unlinkSync(initScriptPath) } catch { /* ignore */ } |
| 567 | + } |
| 568 | + |
| 569 | + const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_GRADLE_DISCOVERY_IGNORE] |
| 570 | + return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) |
| 571 | +} |
| 572 | + |
| 573 | +/** |
| 574 | + * Parse the structured output from the Gradle init script. |
| 575 | + * |
| 576 | + * @param {string} raw - Raw stdout from gradle |
| 577 | + * @returns {{ path: string, dir: string }[]} |
| 578 | + */ |
| 579 | +export function parseGradleInitScriptOutput(raw) { |
| 580 | + const projects = [] |
| 581 | + for (const rawLine of raw.split('\n')) { |
| 582 | + const line = rawLine.trimEnd() |
| 583 | + if (!line.startsWith('::DA_PROJECT::')) { |
| 584 | + continue |
| 585 | + } |
| 586 | + const prefix = '::DA_PROJECT::' |
| 587 | + const remainder = line.substring(prefix.length) |
| 588 | + const lastSep = remainder.lastIndexOf('::') |
| 589 | + if (lastSep < 0) { |
| 590 | + continue |
| 591 | + } |
| 592 | + const projPath = remainder.substring(0, lastSep) |
| 593 | + const dir = remainder.substring(lastSep + 2) |
| 594 | + if (projPath && dir) { |
| 595 | + projects.push({ path: projPath, dir }) |
| 596 | + } |
| 597 | + } |
| 598 | + return projects |
| 599 | +} |
0 commit comments