Skip to content

Commit 58196d7

Browse files
authored
feat: support mvnw if requested and exists (#170)
## Description Adds support for using [mvnw](https://maven.apache.org/wrapper/) instead of maven from PATH/EXHORT_MVN_PATH. If set, mvnw is searched for starting in the directory the passed manifest is in, traversing upwards until either the git repo root is hit, or the root of the drive. The `EXHORT_PREFER_MVNW` env var controls this preference. **Related issues (if any):** #169 ## Checklist - [x] I have followed this repository's contributing guidelines. - [x] I will adhere to the project's code of conduct. ## Additional information > Anything else?
1 parent ca74653 commit 58196d7

File tree

3 files changed

+125
-77
lines changed

3 files changed

+125
-77
lines changed

integration/run_its.sh

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,10 @@ cleanup() {
2020
exit "$1"
2121
}
2222

23-
# utility function takes file name and a command
24-
# used for matching the file content and the command output
25-
match() {
26-
if [[ $(< "$1") != "$(eval "$2")" ]]; then
27-
echo "- FAILED"
28-
cleanup 1
29-
fi
30-
echo "- PASSED"
31-
echo
32-
}
33-
3423
matchConstant() {
3524
TEST_MESSAGE="$3"
3625
sleep 1
37-
echo $TEST_MESSAGE
26+
echo "$TEST_MESSAGE"
3827
if [[ "$1" != "$2" ]]; then
3928
echo "- FAILED"
4029
echo "expected = $1, actual= $2"
@@ -49,25 +38,29 @@ matchConstant() {
4938
##########################################
5039
echo "VERIFYING Node and NPM availability"
5140
if ! node --version > /dev/null 2>&1; then
41+
RC="$?"
5242
echo "- FAILED Node not found"
53-
cleanup $?
43+
cleanup $RC
5444
fi
5545

5646
if ! npm --version > /dev/null 2>&1; then
47+
RC="$?"
5748
echo "- FAILED NPM not found"
58-
cleanup $?
49+
cleanup $RC
5950
fi
6051
echo "- SUCCESSFUL"
6152

6253
echo "VERIFYING Java and Maven availability"
6354
if ! java --version > /dev/null 2>&1; then
55+
RC="$?"
6456
echo "- FAILED Java not found"
65-
cleanup $?
57+
cleanup $RC
6658
fi
6759

6860
if ! mvn --version > /dev/null 2>&1; then
61+
RC="$?"
6962
echo "- FAILED Maven not found"
70-
cleanup $?
63+
cleanup $RC
7164
fi
7265
echo "- SUCCESSFUL"
7366

@@ -79,62 +72,61 @@ echo "PREPARING JavaScript CLI tests environment"
7972
rm -rf testers/cli/node_modules
8073
rm -f testers/cli/package-lock.json
8174
if ! npm --prefix testers/cli install --silent ; then
75+
RC="$?"
8276
echo "- FAILED Installing exhort-javascript-api environment for testing"
83-
cleanup $?
77+
cleanup $RC
8478
fi
8579
echo "- SUCCESSFUL"
8680
mkdir -p ./responses
8781
#### JAVA MAVEN
8882
echo "RUNNING JavaScript CLI integration test for Stack Analysis report in Html for Java Maven"
8983

90-
testers/cli/node_modules/.bin/exhort-javascript-api stack scenarios/maven/pom.xml --html &> ./responses/stack.html
91-
92-
if [ "$?" -ne 0 ]; then
93-
echo "- FAILED , return $RC from invocation"
84+
testers/cli/node_modules/.bin/exhort-javascript-api stack scenarios/maven/pom.xml --html > ./responses/stack.html
85+
RC="$?"
86+
if [ "$RC" -ne 0 ]; then
87+
echo "- FAILED, return $RC from invocation"
9488
cleanup $RC
9589
fi
9690
RESPONSE_CONTENT=$(grep -i "DOCTYPE html" ./responses/stack.html)
9791
if [[ -z "${RESPONSE_CONTENT}" ]]; then
98-
echo "- FAILED ,return code is ok ,but received doc is not HTML"
92+
echo "- FAILED, response is not valid html: $RESPONSE_CONTENT"
9993
cleanup 1
10094
fi
10195
echo "- PASSED"
10296
echo
10397

10498
echo 'RUNNING JavaScript CLI integration test for Stack Analysis report summary of snyk provider for Java Maven'
105-
10699
testers/cli/node_modules/.bin/exhort-javascript-api stack scenarios/maven/pom.xml --summary > ./responses/stack-summary.json
107-
108-
if [ "$?" -ne 0 ]; then
109-
echo "- FAILED , return $RC from invocation"
100+
RC="$?"
101+
if [ "$RC" -ne 0 ]; then
102+
echo "- FAILED, return $RC from invocation"
110103
cleanup $RC
111104
fi
112105

113-
RESPONSE_CONTENT=$(jq . ./responses/stack-summary.json)
114-
if [ "$?" -ne 0 ]; then
115-
echo "- FAILED , response is not a valid json"
106+
if ! RESPONSE_CONTENT=$(jq . ./responses/stack-summary.json); then
107+
RC="$?"
108+
echo "- FAILED, response is not a valid json: $RESPONSE_CONTENT"
116109
cleanup $RC
117110
fi
118111
echo
119-
echo $RESPONSE_CONTENT
112+
echo "$RESPONSE_CONTENT"
120113
echo "- PASSED"
121114
echo
122115

123-
124-
125-
126116
echo "RUNNING JavaScript CLI integration test for Stack Analysis report in Json for Java Maven"
127117
testers/cli/node_modules/.bin/exhort-javascript-api stack scenarios/maven/pom.xml > ./responses/stack.json
128-
129-
if [ "$?" -ne 0 ]; then
130-
echo "- FAILED , return $RC from invocation"
118+
RC="$?"
119+
if [ "$RC" -ne 0 ]; then
120+
echo "- FAILED, return $RC from invocation"
131121
cleanup $RC
132122
fi
133-
RESPONSE_CONTENT=$(jq . ./responses/stack.json)
134-
if [ "$?" -ne 0 ]; then
135-
echo "- FAILED , response is not a valid json"
123+
124+
if ! RESPONSE_CONTENT=$(jq . ./responses/stack.json); then
125+
RC="$?"
126+
echo "- FAILED, response is not a valid json: $RESPONSE_CONTENT"
136127
cleanup $RC
137128
fi
129+
138130
StatusCodeTC=$(jq '.providers["trusted-content"].status.code' ./responses/stack.json)
139131
matchConstant "200" "$StatusCodeTC" "Check that Response code from Trusted Content is OK ( Http Status = 200)..."
140132

@@ -143,14 +135,15 @@ matchConstant "200" "$StatusCodeTC" "Check that Response code from Trusted Conte
143135

144136
echo "RUNNING JavaScript CLI integration test for Component Analysis report for Java Maven"
145137
eval "testers/cli/node_modules/.bin/exhort-javascript-api component scenarios/maven/pom.xml" > ./responses/component.json
146-
147-
if [ "$?" -ne 0 ]; then
148-
echo "- FAILED , return $RC from invocation"
138+
RC="$?"
139+
if [ "$RC" -ne 0 ]; then
140+
echo "- FAILED, return $RC from invocation"
149141
cleanup $RC
150142
fi
151-
RESPONSE_CONTENT=$(jq . ./responses/component.json)
152-
if [ "$?" -ne 0 ]; then
153-
echo "- FAILED , response is not a valid json, got $RC from parsing the file"
143+
144+
if ! RESPONSE_CONTENT=$(jq . ./responses/component.json); then
145+
RC="$?"
146+
echo "- FAILED, response is not a valid json, got $RC from parsing the file"
154147
cleanup $RC
155148
fi
156149

@@ -161,11 +154,11 @@ matchConstant "200" "$StatusCodeTC" "Check that Response code from Trusted Conte
161154

162155
echo "RUNNING JavaScript CLI integration test for Validate Token Function With wrong token, expecting getting 401 http status code "
163156
answerAboutToken=$(testers/cli/node_modules/.bin/exhort-javascript-api validate-token snyk --value=veryBadTokenValue)
164-
matchConstant "401" "$answerAboutToken" "Checking That dummy Token is Invalid , Expecting Response Status of Authentication Failure( Http Status = 401)..."
157+
matchConstant "401" "$answerAboutToken" "Checking That dummy Token is Invalid, Expecting Response Status of Authentication Failure( Http Status = 401)..."
165158

166159
echo "RUNNING JavaScript CLI integration test for Validate Token Function With no token at all, Expecting getting 400 http status code"
167160
answerAboutToken=$(testers/cli/node_modules/.bin/exhort-javascript-api validate-token snyk )
168-
matchConstant "400" "$answerAboutToken" "Checking That Token is missing , Expecting Response Status of Bad Request( Http Status = 400)..."
161+
matchConstant "400" "$answerAboutToken" "Checking That Token is missing, Expecting Response Status of Bad Request( Http Status = 400)..."
169162
echo "==>SUCCESS!!"
170163

171164
cleanup 0

src/providers/java_maven.js

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { XMLParser } from 'fast-xml-parser'
22
import fs from 'node:fs'
3-
import { getCustomPath } from "../tools.js";
3+
import { getCustomPath, getGitRootDir, getWrapperPreference } from "../tools.js";
44
import os from 'node:os'
55
import path from 'node:path'
66
import Sbom from '../sbom.js'
@@ -71,16 +71,8 @@ export default class Java_maven extends Base_java {
7171
* @private
7272
*/
7373
#createSbomStackAnalysis(manifest, opts = {}) {
74-
// get custom maven path
75-
let mvn = getCustomPath('mvn', opts)
76-
// verify maven is accessible
77-
this._invokeCommand(mvn, ['--version'], error => {
78-
if (error.code === 'ENOENT') {
79-
throw new Error(`maven not accessible at "${mvn}"`)
80-
} else {
81-
throw new Error(`failed to check for maven`, {cause: error})
82-
}
83-
})
74+
const mvn = this.#selectMvnRuntime(manifest, opts)
75+
8476
// clean maven target
8577
this._invokeCommand(mvn, ['-q', 'clean', '-f', manifest], error => {
8678
throw new Error(`failed to clean maven target`, {cause: error})
@@ -107,7 +99,7 @@ export default class Java_maven extends Base_java {
10799
// read dependency tree from temp file
108100
let content = fs.readFileSync(`${tmpDepTree}`)
109101
if (process.env["EXHORT_DEBUG"] === "true") {
110-
console.log("Dependency tree that will be used as input for creating the BOM =>" + EOL + EOL + content.toString())
102+
console.error("Dependency tree that will be used as input for creating the BOM =>" + EOL + EOL + content.toString())
111103
}
112104
let sbom = this.createSbomFileFromTextFormat(content.toString(), ignoredDeps,opts);
113105
// delete temp file and directory
@@ -116,7 +108,6 @@ export default class Java_maven extends Base_java {
116108
return sbom
117109
}
118110

119-
120111
/**
121112
*
122113
* @param {String} textGraphList Text graph String of the manifest
@@ -141,16 +132,7 @@ export default class Java_maven extends Base_java {
141132
* @private
142133
*/
143134
#getSbomForComponentAnalysis(manifestPath, opts = {}) {
144-
// get custom maven path
145-
let mvn = getCustomPath('mvn', opts)
146-
// verify maven is accessible
147-
this._invokeCommand(mvn, ['--version'], error => {
148-
if (error.code === 'ENOENT') {
149-
throw new Error(`maven not accessible at "${mvn}"`)
150-
} else {
151-
throw new Error(`failed to check for maven`, {cause: error})
152-
}
153-
})
135+
const mvn = this.#selectMvnRuntime(manifestPath, opts)
154136

155137
const tmpEffectivePom = path.resolve(path.join(path.dirname(manifestPath), 'effective-pom.xml'))
156138
const targetPom = manifestPath
@@ -195,9 +177,7 @@ export default class Java_maven extends Base_java {
195177
let pomRoot
196178
if (effectivePomStruct['project']) {
197179
pomRoot = effectivePomStruct['project']
198-
}
199-
// if there is no project root tag, then it's a multi module/submodules aggregator parent POM
200-
else {
180+
} else { // if there is no project root tag, then it's a multi module/submodules aggregator parent POM
201181
for (let proj of effectivePomStruct['projects']['project']) {
202182
// need to choose the aggregate POM and not one of the modules.
203183
if (proj.packaging && proj.packaging === 'pom') {
@@ -216,6 +196,64 @@ export default class Java_maven extends Base_java {
216196
return rootDependency
217197
}
218198

199+
#selectMvnRuntime(manifestPath, opts) {
200+
// get custom maven path
201+
let mvn = getCustomPath('mvn', opts)
202+
203+
// check if mvnw is preferred and available
204+
let useMvnw = getWrapperPreference('mvn', opts)
205+
if (useMvnw) {
206+
const mvnw = this.#traverseForMvnw(manifestPath)
207+
if (mvnw !== undefined) {
208+
this._invokeCommand(mvnw, ['--version'], error => {
209+
if (error.code === 'ENOENT') {
210+
useMvnw = false
211+
} else {
212+
throw new Error(`failed to check for mvnw`, {cause: error})
213+
}
214+
})
215+
mvn = useMvnw ? mvnw : mvn
216+
}
217+
} else {
218+
// verify maven is accessible
219+
this._invokeCommand(mvn, ['--version'], error => {
220+
if (error.code === 'ENOENT') {
221+
throw new Error(`maven not accessible at "${mvn}"`)
222+
} else {
223+
throw new Error(`failed to check for maven`, {cause: error})
224+
}
225+
})
226+
}
227+
return mvn
228+
}
229+
230+
/**
231+
*
232+
* @param {string} startingManifest - the path of the manifest from which to start searching for mvnw
233+
* @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
234+
* to the root of the drive the manifest is on (assumes absolute path is given)
235+
* @returns
236+
*/
237+
#traverseForMvnw(startingManifest, repoRoot = undefined) {
238+
repoRoot = repoRoot || getGitRootDir(path.resolve(path.dirname(startingManifest))) || path.parse(path.resolve(startingManifest)).root
239+
240+
const wrapperName = 'mvnw' + (process.platform === 'win32' ? '.cmd' : '');
241+
const wrapperPath = path.join(path.resolve(path.dirname(startingManifest)), wrapperName);
242+
243+
try {
244+
fs.accessSync(wrapperPath, fs.constants.X_OK)
245+
} catch(error) {
246+
if (error.code === 'ENOENT') {
247+
if (path.resolve(path.dirname(startingManifest)) === repoRoot) {
248+
return undefined
249+
}
250+
return this.#traverseForMvnw(path.resolve(path.dirname(startingManifest)), repoRoot)
251+
}
252+
throw new Error(`failure searching for mvnw`, {cause: error})
253+
}
254+
return wrapperPath
255+
}
256+
219257
/**
220258
* Get a list of dependencies with marking of dependencies commented with <!--exhortignore-->.
221259
* @param {string} manifest - path for pom.xml
@@ -246,9 +284,7 @@ export default class Java_maven extends Base_java {
246284
} else {
247285
pomXml = []
248286
}
249-
}
250-
// project with modules
251-
else {
287+
} else { // project with modules
252288
pomXml = pomJson['projects']['project'].filter(project => project.dependencies !== undefined).flatMap(project => project.dependencies.dependency)
253289
}
254290

src/tools.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,15 @@ export function getCustomPath(name, opts = {}) {
5858
return getCustom(`EXHORT_${name.toUpperCase()}_PATH`, name, opts)
5959
}
6060

61+
/**
62+
* Utility function for determining whether wrappers for build tools such as gradlew/mvnw should be
63+
* preferred over invoking the binary directly.
64+
* @param {string} name - binary for which to search for its wrapper
65+
* @param {{}} opts - the options object to look for the key in if not found in environment
66+
* @returns {boolean} whether to prefer the wrapper if exists or not
67+
*/
6168
export function getWrapperPreference(name, opts = {}) {
62-
return getCustom(`EXHORT_PREFER_${name.toUpperCase()}W`, true, opts)
69+
return getCustom(`EXHORT_PREFER_${name.toUpperCase()}W`, 'true', opts) === 'true'
6370
}
6471

6572
export function environmentVariableIsPopulated(envVariableName) {
@@ -100,6 +107,18 @@ function hasSpaces(path) {
100107
}
101108

102109

110+
/**
111+
*
112+
* @param {string} cwd - directory for which to find the root of the git repository.
113+
*/
114+
export function getGitRootDir(cwd) {
115+
const root = invokeCommand('git', ['rev-parse', '--show-toplevel'], () => {}, {cwd: cwd})
116+
if (!root) {
117+
return undefined
118+
}
119+
return root.toString().trim()
120+
}
121+
103122
/** this method invokes command string in a process in a synchronous way.
104123
* @param {string} bin - the command to be invoked
105124
* @param {Array<string>} args - the args to pass to the binary

0 commit comments

Comments
 (0)