Skip to content

Commit b1afec7

Browse files
Strum355claude
andcommitted
refactor: move Gradle discovery logic from workspace.js to java_gradle.js
Address review feedback: provider-specific discovery functions don't belong in workspace.js. Extract traverseForWrapper to tools.js as shared utility, update base_java.js to delegate to it, and move all Gradle workspace discovery functions (discoverGradleSubprojects, resolveGradleBinary, parseGradleInitScriptOutput, GRADLE_INIT_SCRIPT) to their provider file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 84e4163 commit b1afec7

6 files changed

Lines changed: 176 additions & 204 deletions

File tree

src/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import analysis from './analysis.js'
77
import fs from 'node:fs'
88
import { getCustom } from "./tools.js";
99
import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js'
10+
import { discoverGradleSubprojects } from './providers/java_gradle.js'
1011
import {
11-
discoverGradleSubprojects,
1212
discoverWorkspaceCrates,
1313
discoverWorkspacePackages,
1414
filterManifestPathsByDiscoveryIgnore,
@@ -100,7 +100,7 @@ export {
100100
* @param {any} valueToBePrinted - The value to log.
101101
* @private
102102
*/
103-
function logOptionsAndEnvironmentsVariables(alongsideText,valueToBePrinted) {
103+
function logOptionsAndEnvironmentsVariables(alongsideText, valueToBePrinted) {
104104
if (process.env["TRUSTIFY_DA_DEBUG"] === "true") {
105105
console.log(`${alongsideText}: ${valueToBePrinted} ${EOL}`)
106106
}
@@ -112,9 +112,9 @@ function logOptionsAndEnvironmentsVariables(alongsideText,valueToBePrinted) {
112112
*/
113113
function readAndPrintVersionFromPackageJson() {
114114
let dirName
115-
// new ESM way in nodeJS ( since node version 22 ) to bring module directory.
115+
// new ESM way in nodeJS ( since node version 22 ) to bring module directory.
116116
dirName = import.meta.dirname
117-
// old ESM way in nodeJS ( before node versions 22.00 to bring module directory)
117+
// old ESM way in nodeJS ( before node versions 22.00 to bring module directory)
118118
if (!dirName) {
119119
dirName = url.fileURLToPath(new URL('.', import.meta.url));
120120
}

src/providers/base_java.js

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import fs from 'node:fs'
21
import path from 'node:path'
32

43
import { PackageURL } from 'packageurl-js'
54

6-
import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js"
5+
import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from "../tools.js"
76

87
/** @typedef {import('../provider').Provider} */
98

@@ -145,7 +144,8 @@ export default class Base_Java {
145144

146145
const useWrapper = getWrapperPreference(this.globalBinary, opts)
147146
if (useWrapper) {
148-
const wrapper = this.traverseForWrapper(manifestPath)
147+
const manifestPath = path.dirname(this.normalizePath(manifestPath))
148+
const wrapper = traverseForWrapper(manifestPath)
149149
if (wrapper !== undefined) {
150150
try {
151151
this._invokeCommand(wrapper, ['--version'], {cwd: manifestDir})
@@ -168,39 +168,6 @@ export default class Base_Java {
168168
return toolPath
169169
}
170170

171-
/**
172-
*
173-
* @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
174-
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
175-
* to the root of the drive the manifest is on (assumes absolute path is given)
176-
* @returns {string|undefined}
177-
*/
178-
traverseForWrapper(startingManifest, repoRoot = undefined) {
179-
const normalizedManifest = this.normalizePath(startingManifest);
180-
const currentDir = this.normalizePath(path.dirname(normalizedManifest));
181-
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(normalizedManifest).root;
182-
const wrapperPath = path.join(currentDir, this.localWrapper);
183-
184-
try {
185-
fs.accessSync(wrapperPath, fs.constants.X_OK);
186-
return wrapperPath;
187-
}
188-
catch (error) {
189-
if (error.code === 'ENOENT') {
190-
const rootDir = path.parse(currentDir).root;
191-
if (currentDir === repoRoot || currentDir === rootDir) {
192-
return undefined;
193-
}
194-
const parentDir = path.dirname(currentDir);
195-
if (parentDir === currentDir || parentDir === rootDir) {
196-
return undefined;
197-
}
198-
return this.traverseForWrapper(path.join(parentDir, path.basename(normalizedManifest)), repoRoot);
199-
}
200-
throw new Error(`failure searching for ${this.localWrapper}`, { cause: error });
201-
}
202-
}
203-
204171
normalizePath(thePath) {
205172
const normalized = path.resolve(thePath).normalize();
206173
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;

src/providers/java_gradle.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import crypto from 'node:crypto'
12
import fs from 'node:fs'
3+
import os from 'node:os'
24
import path from 'node:path'
35
import { EOL } from 'os'
46

57
import TOML from 'fast-toml'
68

79
import { readLicenseFile } from '../license/license_utils.js'
810
import Sbom from '../sbom.js'
11+
import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from '../tools.js'
12+
import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore } from '../workspace.js'
913

1014
import Base_java, { ecosystem_gradle } from "./base_java.js";
1115

@@ -466,3 +470,130 @@ export default class Java_gradle extends Base_java {
466470
return undefined
467471
}
468472
}
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+
}

src/tools.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { execFileSync } from "child_process";
2+
import fs from 'node:fs'
3+
import path from 'node:path'
24
import { EOL } from "os";
35

46
import { HttpsProxyAgent } from "https-proxy-agent";
@@ -137,6 +139,34 @@ export function getGitRootDir(cwd) {
137139
}
138140
}
139141

142+
/**
143+
* Walk up from `startDir` to `repoRoot` looking for an executable wrapper script.
144+
*
145+
* @param {string} startDir - Absolute directory to start from
146+
* @param {string} wrapperName - Wrapper filename (e.g. `mvnw`, `gradlew`)
147+
* @param {string} [repoRoot] - Stop boundary (defaults to git root or filesystem root)
148+
* @returns {string | undefined}
149+
*/
150+
export function traverseForWrapper(startDir, wrapperName, repoRoot = undefined) {
151+
const currentDir = path.resolve(startDir)
152+
repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(currentDir).root
153+
const wrapperPath = path.join(currentDir, wrapperName)
154+
try {
155+
fs.accessSync(wrapperPath, fs.constants.X_OK)
156+
return wrapperPath
157+
} catch {
158+
const rootDir = path.parse(currentDir).root
159+
if (currentDir === repoRoot || currentDir === rootDir) {
160+
return undefined
161+
}
162+
const parentDir = path.dirname(currentDir)
163+
if (parentDir === currentDir || parentDir === rootDir) {
164+
return undefined
165+
}
166+
return traverseForWrapper(parentDir, wrapperName, repoRoot)
167+
}
168+
}
169+
140170
/** this method invokes command string in a process in a synchronous way.
141171
* @param {string} bin - the command to be invoked
142172
* @param {Array<string>} args - the args to pass to the binary

0 commit comments

Comments
 (0)