Skip to content

Commit 3ef7be9

Browse files
authored
feat: support preferring gradlew (#195)
1 parent 1c04b35 commit 3ef7be9

File tree

5 files changed

+106
-73
lines changed

5 files changed

+106
-73
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,11 @@ following keys for setting custom paths for the said executables.
341341
<td>EXHORT_MVN_PATH</td>
342342
</tr>
343343
<tr>
344+
<td><a href="https://maven.apache.org/">Maven</a></td>
345+
<td><em>maven</em></td>
346+
<td>EXHORT_PREFER_MVNW</td>
347+
</tr>
348+
<tr>
344349
<td><a href="https://www.npmjs.com/">NPM</a></td>
345350
<td><em>npm</em></td>
346351
<td>EXHORT_NPM_PATH</td>
@@ -385,6 +390,11 @@ following keys for setting custom paths for the said executables.
385390
<td><em>gradle</em></td>
386391
<td>EXHORT_GRADLE_PATH</td>
387392
</tr>
393+
<tr>
394+
<td><a href="https://gradle.org/">Gradle</a></td>
395+
<td><em>gradle</em></td>
396+
<td>EXHORT_PREFER_GRADLEW</td>
397+
</tr>
388398
</table>
389399

390400
#### Match Manifest Versions Feature
@@ -463,7 +473,7 @@ Need to set environment variable/option - `EXHORT_PIP_USE_DEP_TREE` to true.
463473
### Known Issues
464474

465475
- For pip requirements.txt - It's been observed that for python versions 3.11.x, there might be slowness for invoking the analysis.
466-
If you encounter a performance issue with version >= 3.11.x, kindly try to set environment variable/option `EXHORT_PIP_USE_DEP_TREE`=true, before calling the analysis.
476+
If you encounter a performance issue with version >= 3.11.x, kindly try to set environment variable/option `EXHORT_PIP_USE_DEP_TREE=true`, before calling the analysis.
467477

468478

469479
- For maven pom.xml, it has been noticed that using java 17 might cause stack analysis to hang forever.

src/providers/base_java.js

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PackageURL } from 'packageurl-js'
2-
3-
import { invokeCommand } from "../tools.js"
2+
import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js"
3+
import fs from 'node:fs'
4+
import path from 'node:path'
45

56

67
/** @typedef {import('../provider').Provider} */
@@ -21,6 +22,19 @@ export default class Base_Java {
2122
DEP_REGEX = /(([-a-zA-Z0-9._]{2,})|[0-9])/g
2223
CONFLICT_REGEX = /.*omitted for conflict with (\S+)\)/
2324

25+
globalBinary
26+
localWrapper
27+
28+
/**
29+
*
30+
* @param {string} globalBinary name of the global binary
31+
* @param {string} localWrapper name of the local wrapper filename
32+
*/
33+
constructor(globalBinary, localWrapper) {
34+
this.globalBinary = globalBinary
35+
this.localWrapper = localWrapper
36+
}
37+
2438
/**
2539
* Recursively populates the SBOM instance with the parsed graph
2640
* @param {string} src - Source dependency to start the calculations from
@@ -107,10 +121,72 @@ export default class Base_Java {
107121
return new PackageURL('maven', group, artifact, version, undefined, undefined);
108122
}
109123

110-
/** this method invokes command string in a process in a synchronous way.
124+
/** This method invokes command string in a process in a synchronous way.
125+
* Exists for stubbing in tests.
111126
* @param bin - the command to be invoked
112127
* @param args - the args to pass to the binary
113128
* @protected
114129
*/
115130
_invokeCommand(bin, args, opts={}) { return invokeCommand(bin, args, opts) }
131+
132+
/**
133+
*
134+
* @param {string} manifestPath
135+
* @param {{}} opts
136+
* @returns string
137+
*/
138+
selectToolBinary(manifestPath, opts) {
139+
const toolPath = getCustomPath(this.globalBinary, opts)
140+
141+
const useWrapper = getWrapperPreference(toolPath, opts)
142+
if (useWrapper) {
143+
const wrapper = this.traverseForWrapper(manifestPath)
144+
if (wrapper !== undefined) {
145+
try {
146+
this._invokeCommand(wrapper, ['--version'])
147+
} catch (error) {
148+
throw new Error(`failed to check for ${this.localWrapper}`, {cause: error})
149+
}
150+
return wrapper
151+
}
152+
}
153+
// verify tool is accessible, if wrapper was not requested or not found
154+
try {
155+
this._invokeCommand(toolPath, ['--version'])
156+
} catch (error) {
157+
if (error.code === 'ENOENT') {
158+
throw new Error((useWrapper ? `${this.localWrapper} not found and ` : '') + `${this.globalBinary === 'mvn' ? 'maven' : 'gradle'} not found at ${toolPath}`)
159+
} else {
160+
throw new Error(`failed to check for ${this.globalBinary === 'mvn' ? 'maven' : 'gradle'}`, {cause: error})
161+
}
162+
}
163+
return toolPath
164+
}
165+
166+
/**
167+
*
168+
* @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
169+
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
170+
* to the root of the drive the manifest is on (assumes absolute path is given)
171+
* @returns {string|undefined}
172+
*/
173+
traverseForWrapper(startingManifest, repoRoot = undefined) {
174+
repoRoot = repoRoot || getGitRootDir(path.resolve(path.dirname(startingManifest))) || path.parse(path.resolve(startingManifest)).root
175+
176+
const wrapperName = this.localWrapper;
177+
const wrapperPath = path.join(path.resolve(path.dirname(startingManifest)), wrapperName);
178+
179+
try {
180+
fs.accessSync(wrapperPath, fs.constants.X_OK)
181+
} catch(error) {
182+
if (error.code === 'ENOENT') {
183+
if (path.resolve(path.dirname(startingManifest)) === repoRoot) {
184+
return undefined
185+
}
186+
return this.traverseForWrapper(path.resolve(path.dirname(startingManifest)), repoRoot)
187+
}
188+
throw new Error(`failure searching for ${this.localWrapper}`, {cause: error})
189+
}
190+
return wrapperPath
191+
}
116192
}

src/providers/java_gradle.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import fs from 'node:fs'
2-
import {getCustomPath} from "../tools.js";
32
import path from 'node:path'
43
import Sbom from '../sbom.js'
54
import { EOL } from 'os'
@@ -88,6 +87,9 @@ const stackAnalysisConfigs = ["runtimeClasspath","compileClasspath"];
8887
* This class provides common functionality for Groovy and Kotlin DSL files.
8988
*/
9089
export default class Java_gradle extends Base_java {
90+
constructor() {
91+
super('gradle', 'gradlew' + (process.platform === 'win32' ? '.bat' : ''))
92+
}
9193

9294
_getManifestName() {
9395
throw new Error('implement getManifestName method')
@@ -154,7 +156,7 @@ export default class Java_gradle extends Base_java {
154156
* @private
155157
*/
156158
#createSbomStackAnalysis(manifest, opts = {}) {
157-
let content = this.#getDependencies(manifest)
159+
let content = this.#getDependencies(manifest, opts)
158160
let properties = this.#extractProperties(manifest, opts)
159161
// read dependency tree from temp file
160162
if (process.env["EXHORT_DEBUG"] === "true") {
@@ -192,7 +194,7 @@ export default class Java_gradle extends Base_java {
192194
* @return {string} string content of the properties
193195
*/
194196
#getProperties(manifestPath, opts) {
195-
let gradle = getCustomPath("gradle", opts);
197+
let gradle = this.selectToolBinary(manifestPath, opts)
196198
try {
197199
let properties = this._invokeCommand(gradle, ['properties'], {cwd: path.dirname(manifestPath)})
198200
return properties.toString()
@@ -208,7 +210,7 @@ export default class Java_gradle extends Base_java {
208210
* @private
209211
*/
210212
#getSbomForComponentAnalysis(manifestPath, opts = {}) {
211-
let content = this.#getDependencies(manifestPath)
213+
let content = this.#getDependencies(manifestPath, opts)
212214
let properties = this.#extractProperties(manifestPath, opts)
213215
let configurationNames = componentAnalysisConfigs
214216

@@ -234,8 +236,8 @@ export default class Java_gradle extends Base_java {
234236
* @private
235237
*/
236238

237-
#getDependencies(manifest) {
238-
const gradle = getCustomPath("gradle")
239+
#getDependencies(manifest, opts={}) {
240+
const gradle = this.selectToolBinary(manifest, opts)
239241
try {
240242
const commandResult = this._invokeCommand(gradle, ['dependencies'], {cwd: path.dirname(manifest)})
241243
return commandResult.toString()

src/providers/java_maven.js

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { XMLParser } from 'fast-xml-parser'
22
import fs from 'node:fs'
3-
import { getCustomPath, getGitRootDir, getWrapperPreference } from "../tools.js";
43
import os from 'node:os'
54
import path from 'node:path'
65
import Sbom from '../sbom.js'
@@ -17,6 +16,9 @@ import Base_java, { ecosystem_maven } from "./base_java.js";
1716
/** @typedef {{groupId: string, artifactId: string, version: string, scope: string, ignore: boolean}} Dependency */
1817

1918
export default class Java_maven extends Base_java {
19+
constructor() {
20+
super('mvn', 'mvnw' + (process.platform === 'win32' ? '.cmd' : ''))
21+
}
2022

2123
/**
2224
* @param {string} manifestName - the subject manifest name-type
@@ -71,7 +73,7 @@ export default class Java_maven extends Base_java {
7173
* @private
7274
*/
7375
#createSbomStackAnalysis(manifest, opts = {}) {
74-
const mvn = this.#selectMvnRuntime(manifest, opts)
76+
const mvn = this.selectToolBinary(manifest, opts)
7577

7678
// clean maven target
7779
try {
@@ -136,7 +138,7 @@ export default class Java_maven extends Base_java {
136138
* @private
137139
*/
138140
#getSbomForComponentAnalysis(manifestPath, opts = {}) {
139-
const mvn = this.#selectMvnRuntime(manifestPath, opts)
141+
const mvn = this.selectToolBinary(manifestPath, opts)
140142

141143
const tmpEffectivePom = path.resolve(path.join(path.dirname(manifestPath), 'effective-pom.xml'))
142144
const targetPom = manifestPath
@@ -201,63 +203,6 @@ export default class Java_maven extends Base_java {
201203
return rootDependency
202204
}
203205

204-
#selectMvnRuntime(manifestPath, opts) {
205-
// get custom maven path
206-
let mvn = getCustomPath('mvn', opts)
207-
208-
// check if mvnw is preferred and available
209-
let useMvnw = getWrapperPreference('mvn', opts)
210-
if (useMvnw) {
211-
const mvnw = this.#traverseForMvnw(manifestPath)
212-
if (mvnw !== undefined) {
213-
try {
214-
this._invokeCommand(mvnw, ['--version'])
215-
} catch (error) {
216-
throw new Error(`failed to check for mvnw`, {cause: error})
217-
}
218-
return mvnw
219-
}
220-
}
221-
// verify maven is accessible, if mvnw was not requested or not found
222-
try {
223-
this._invokeCommand(mvn, ['--version'])
224-
} catch (error) {
225-
if (error.code === 'ENOENT') {
226-
throw new Error((useMvnw ? 'mvnw not found and ' : '') + `maven not accessible at "${mvn}"`)
227-
} else {
228-
throw new Error(`failed to check for maven`, {cause: error})
229-
}
230-
}
231-
return mvn
232-
}
233-
234-
/**
235-
*
236-
* @param {string} startingManifest - the path of the manifest from which to start searching for mvnw
237-
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
238-
* to the root of the drive the manifest is on (assumes absolute path is given)
239-
* @returns
240-
*/
241-
#traverseForMvnw(startingManifest, repoRoot = undefined) {
242-
repoRoot = repoRoot || getGitRootDir(path.resolve(path.dirname(startingManifest))) || path.parse(path.resolve(startingManifest)).root
243-
244-
const wrapperName = 'mvnw' + (process.platform === 'win32' ? '.cmd' : '');
245-
const wrapperPath = path.join(path.resolve(path.dirname(startingManifest)), wrapperName);
246-
247-
try {
248-
fs.accessSync(wrapperPath, fs.constants.X_OK)
249-
} catch(error) {
250-
if (error.code === 'ENOENT') {
251-
if (path.resolve(path.dirname(startingManifest)) === repoRoot) {
252-
return undefined
253-
}
254-
return this.#traverseForMvnw(path.resolve(path.dirname(startingManifest)), repoRoot)
255-
}
256-
throw new Error(`failure searching for mvnw`, {cause: error})
257-
}
258-
return wrapperPath
259-
}
260-
261206
/**
262207
* Get a list of dependencies with marking of dependencies commented with <!--exhortignore-->.
263208
* @param {string} manifest - path for pom.xml

test/it/end-to-end.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ suite('Integration Tests', () => {
5252
let pomPath = `test/it/test_manifests/${packageManager}/${manifestName}`
5353
let providedDataForStack = await index.stackAnalysis(pomPath)
5454
console.log(JSON.stringify(providedDataForStack,null , 4))
55-
let providers = ["osv"]
55+
let providers = ["tpa"]
5656
providers.forEach(provider => expect(extractTotalsGeneralOrFromProvider(providedDataForStack, provider)).greaterThan(0))
5757
// TODO: if sources doesn't exist, add "scanned" instead
5858
// python transitive count for stack analysis is awaiting fix in exhort backend
@@ -84,7 +84,7 @@ suite('Integration Tests', () => {
8484
reportParsedFromHtml = JSON.parse("{" + startOfJson.substring(0,startOfJson.indexOf("};") + 1))
8585
reportParsedFromHtml = reportParsedFromHtml.report
8686
} finally {
87-
parsedStatusFromHtmlOsvNvd = reportParsedFromHtml.providers["osv"].status
87+
parsedStatusFromHtmlOsvNvd = reportParsedFromHtml.providers["tpa"].status
8888
expect(parsedStatusFromHtmlOsvNvd.code).equals(200)
8989
parsedScannedFromHtml = reportParsedFromHtml.scanned
9090
expect( typeof html).equals("string")
@@ -101,7 +101,7 @@ suite('Integration Tests', () => {
101101

102102
expect(analysisReport.scanned.total).greaterThan(0)
103103
expect(analysisReport.scanned.transitive).equal(0)
104-
let providers = ["osv"]
104+
let providers = ["tpa"]
105105
providers.forEach(provider => expect(extractTotalsGeneralOrFromProvider(analysisReport, provider)).greaterThan(0))
106106
providers.forEach(provider => expect(analysisReport.providers[provider].status.code).equals(200))
107107
}).timeout(20000);

0 commit comments

Comments
 (0)