Skip to content

Commit bf42636

Browse files
authored
feat: support configuring maven wrapper (#147)
## Description feat: support configuring maven wrapper This is the java api part for redhat-developer/intellij-dependency-analytics#171 ## Checklist - [x] I have followed this repository's contributing guidelines. - [x] I will adhere to the project's code of conduct. --------- Signed-off-by: Chao Wang <chaowan@redhat.com>
1 parent 2577c0c commit bf42636

4 files changed

Lines changed: 152 additions & 13 deletions

File tree

src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.io.IOException;
3131
import java.nio.file.Files;
3232
import java.nio.file.Path;
33+
import java.nio.file.Paths;
3334
import java.util.ArrayList;
3435
import java.util.Collections;
3536
import java.util.List;
@@ -53,20 +54,21 @@ public final class JavaMavenProvider extends BaseJavaProvider {
5354
private static final String PROP_JAVA_HOME = "JAVA_HOME";
5455
private static final Logger log = LoggersFactory.getLogger(JavaMavenProvider.class.getName());
5556
private final String mvnExecutable;
57+
private static final String MVN = "mvn";
58+
private static final String ARG_VERSION = "-v";
5659

5760
public JavaMavenProvider(Path manifest) {
5861
super(Type.MAVEN, manifest);
59-
// check for custom mvn executable
60-
this.mvnExecutable = Operations.getExecutable("mvn", "-v");
62+
// check for custom mvn executable mvn or mvn wrapper
63+
this.mvnExecutable = selectMvnRuntime(manifest);
6164
}
6265

6366
@Override
6467
public Content provideStack() throws IOException {
65-
// clean command used to clean build target
6668
var mvnCleanCmd = new String[] {mvnExecutable, "clean", "-f", manifest.toString()};
6769
var mvnEnvs = getMvnExecEnvs();
6870
// execute the clean command
69-
Operations.runProcess(mvnCleanCmd, mvnEnvs);
71+
Operations.runProcess(manifest.getParent(), mvnCleanCmd, mvnEnvs);
7072
// create a temp file for storing the dependency tree in
7173
var tmpFile = Files.createTempFile("exhort_dot_graph_", null);
7274
// the tree command will build the project and create the dependency tree in the temp file
@@ -90,7 +92,7 @@ public Content provideStack() throws IOException {
9092
.map(PackageURL::getCoordinates)
9193
.collect(Collectors.toList());
9294
// execute the tree command
93-
Operations.runProcess(mvnTreeCmd.toArray(String[]::new), mvnEnvs);
95+
Operations.runProcess(manifest.getParent(), mvnTreeCmd.toArray(String[]::new), mvnEnvs);
9496
if (debugLoggingIsNeeded()) {
9597
String stackAnalysisDependencyTree = Files.readString(tmpFile);
9698
log.info(
@@ -137,7 +139,7 @@ private Content generateSbomFromEffectivePom() throws IOException {
137139
manifest.toString()
138140
};
139141
// execute the effective pom command
140-
Operations.runProcess(mvnEffPomCmd, getMvnExecEnvs());
142+
Operations.runProcess(manifest.getParent(), mvnEffPomCmd, getMvnExecEnvs());
141143
if (debugLoggingIsNeeded()) {
142144
String CaEffectivePoM = Files.readString(tmpEffPom);
143145
log.info(
@@ -366,4 +368,68 @@ public int hashCode() {
366368
return Objects.hash(groupId, artifactId, version);
367369
}
368370
}
371+
372+
private String selectMvnRuntime(final Path manifestPath) {
373+
boolean preferWrapper = Operations.getWrapperPreference(MVN);
374+
if (preferWrapper) {
375+
String wrapperName = isWindows() ? "mvnw.cmd" : "mvnw";
376+
String mvnw = traverseForMvnw(wrapperName, manifestPath.toString());
377+
if (mvnw != null) {
378+
try {
379+
// verify maven wrapper is accessible
380+
Operations.runProcess(manifest.getParent(), mvnw, ARG_VERSION);
381+
log.fine("using maven wrapper from : " + mvnw);
382+
return mvnw;
383+
} catch (Exception e) {
384+
log.warning(
385+
"Failed to check for mvnw due to: " + e.getMessage() + " Fall back to use mvn");
386+
}
387+
}
388+
}
389+
// If maven wrapper is not requested or not accessible, fall back to use mvn
390+
String mvn = Operations.getExecutable(MVN, ARG_VERSION);
391+
log.fine("using mvn executable from : " + mvn);
392+
return mvn;
393+
}
394+
395+
private String traverseForMvnw(String wrapperName, String startingManifest) {
396+
return traverseForMvnw(wrapperName, startingManifest, null);
397+
}
398+
399+
public static String traverseForMvnw(
400+
String wrapperName, String startingManifest, String repoRoot) {
401+
String normalizedManifest = normalizePath(startingManifest);
402+
403+
Path path = Paths.get(normalizedManifest);
404+
if (repoRoot == null) {
405+
repoRoot =
406+
Operations.getGitRootDir(path.getParent().toString())
407+
.orElse(path.toAbsolutePath().getRoot().toString());
408+
}
409+
410+
Path wrapperPath = path.getParent().resolve(wrapperName).toAbsolutePath();
411+
412+
if (Files.isExecutable(wrapperPath)) {
413+
return wrapperPath.toString();
414+
} else {
415+
String currentDir = path.getParent().toAbsolutePath().toString();
416+
if (currentDir.equals(repoRoot)) {
417+
return null;
418+
}
419+
return traverseForMvnw(wrapperName, path.getParent().toString(), repoRoot);
420+
}
421+
}
422+
423+
public static String normalizePath(String thePath) {
424+
Path normalized = Paths.get(thePath).toAbsolutePath().normalize();
425+
String result = normalized.toString();
426+
if (isWindows()) {
427+
result = result.toLowerCase();
428+
}
429+
return result;
430+
}
431+
432+
private static boolean isWindows() {
433+
return System.getProperty("os.name").toLowerCase().contains("win");
434+
}
369435
}

src/main/java/com/redhat/exhort/tools/Operations.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,26 @@
1717

1818
import static java.lang.String.join;
1919

20+
import com.redhat.exhort.logging.LoggersFactory;
2021
import com.redhat.exhort.utils.Environment;
2122
import java.io.BufferedReader;
23+
import java.io.File;
2224
import java.io.IOException;
2325
import java.io.InputStreamReader;
2426
import java.nio.file.Path;
27+
import java.util.Arrays;
28+
import java.util.List;
2529
import java.util.Map;
30+
import java.util.Optional;
2631
import java.util.concurrent.TimeUnit;
32+
import java.util.logging.Logger;
2733
import java.util.stream.Collectors;
2834

2935
/** Utility class used for executing process on the operating system. * */
3036
public final class Operations {
37+
38+
private static final Logger log = LoggersFactory.getLogger(Operations.class.getName());
39+
3140
private Operations() {
3241
// constructor not required for a utility class
3342
}
@@ -56,12 +65,24 @@ public static String getCustomPathOrElse(String defaultExecutable) {
5665
* @param cmdList list of command parts
5766
*/
5867
public static void runProcess(final String... cmdList) {
59-
runProcess(cmdList, null);
68+
runProcess(null, cmdList, null);
69+
}
70+
71+
public static void runProcess(final Path workingDirectory, final String... cmdList) {
72+
runProcess(workingDirectory, cmdList, null);
6073
}
6174

6275
public static void runProcess(final String[] cmdList, final Map<String, String> envMap) {
76+
runProcess(null, cmdList, null);
77+
}
78+
79+
public static void runProcess(
80+
final Path workingDirectory, final String[] cmdList, final Map<String, String> envMap) {
6381
var processBuilder = new ProcessBuilder();
6482
processBuilder.command(cmdList);
83+
if (workingDirectory != null) {
84+
processBuilder.directory(workingDirectory.toFile());
85+
}
6586
if (envMap != null) {
6687
processBuilder.environment().putAll(envMap);
6788
}
@@ -276,4 +297,56 @@ public static String getExecutable(
276297
public static String getExecutable(String command, String args) {
277298
return getExecutable(command, args, null);
278299
}
300+
301+
/**
302+
* Checks whether a wrapper preference is set for a given tool name.
303+
*
304+
* <p>This method looks for an environment variable with the name {@code
305+
* EXHORT_PREFER_<TOOLNAME>W}, where {@code <TOOLNAME>} is the uppercase form of the provided
306+
* {@code name} parameter. If the environment variable is present (i.e., not {@code null}), the
307+
* method returns {@code true}.
308+
*
309+
* @param name the tool name for which to check the wrapper preference (e.g., "maven", "gradle")
310+
* @return {@code true} if the corresponding environment variable is set; {@code false} otherwise
311+
*/
312+
public static boolean getWrapperPreference(String name) {
313+
return Environment.get("EXHORT_PREFER_" + name.toUpperCase() + "W") != null;
314+
}
315+
316+
/**
317+
* Attempts to retrieve the root directory of a Git repository for the given working directory.
318+
*
319+
* <p>This method runs the command {@code git rev-parse --show-toplevel} in the specified
320+
* directory, which returns the absolute path to the root of the current Git working tree. If the
321+
* command executes successfully and produces output, the trimmed result is returned as an {@link
322+
* Optional}. If the command fails or the output is empty, an empty {@code Optional} is returned.
323+
*
324+
* @param cwd the working directory in which to execute the Git command
325+
* @return an {@code Optional} containing the Git root directory path if found, otherwise {@code
326+
* Optional.empty()}
327+
*/
328+
public static Optional<String> getGitRootDir(String cwd) {
329+
List<String> command = Arrays.asList("git", "rev-parse", "--show-toplevel");
330+
ProcessBuilder builder =
331+
new ProcessBuilder(command).directory(new File(cwd)).redirectErrorStream(true);
332+
try {
333+
Process process = builder.start();
334+
try (BufferedReader reader =
335+
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
336+
String line = reader.readLine();
337+
int exitCode = process.waitFor();
338+
if (exitCode == 0) {
339+
return Optional.ofNullable(line).map(String::trim);
340+
} else {
341+
log.warning("Git command exited with code " + exitCode + " in directory " + cwd);
342+
}
343+
}
344+
} catch (IOException e) {
345+
log.warning("I/O error executing git in " + cwd + ": " + e.getMessage());
346+
} catch (InterruptedException e) {
347+
Thread.currentThread().interrupt();
348+
log.warning("Git command interrupted in directory " + cwd + ": " + e.getMessage());
349+
}
350+
return Optional.empty();
351+
}
279352
}

src/test/java/com/redhat/exhort/impl/ExhortApiIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOExc
362362
depTree = new String(is.readAllBytes());
363363
}
364364
mockedOperations
365-
.when(() -> Operations.runProcess(any(), any()))
365+
.when(() -> Operations.runProcess(any(), any(), any()))
366366
.thenAnswer(
367367
invocationOnMock ->
368368
getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"));
@@ -378,7 +378,7 @@ private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOExc
378378
public static String getOutputFileAndOverwriteItWithMock(
379379
String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix)
380380
throws IOException {
381-
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0];
381+
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[1];
382382
Optional<String> outputFileArg =
383383
Arrays.stream(rawArguments)
384384
.filter(arg -> arg != null && arg.startsWith(parameterPrefix))

src/test/java/com/redhat/exhort/providers/Java_Maven_Provider_Test.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ void test_the_provideStack(String testFolder) throws IOException {
8484
}
8585
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
8686
mockedOperations
87-
.when(() -> Operations.runProcess(any(), any()))
87+
.when(() -> Operations.runProcess(any(), any(), any()))
8888
.thenAnswer(
8989
invocationOnMock ->
9090
getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"));
@@ -107,7 +107,7 @@ void test_the_provideStack(String testFolder) throws IOException {
107107
public static String getOutputFileAndOverwriteItWithMock(
108108
String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix)
109109
throws IOException {
110-
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0];
110+
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[1];
111111
Optional<String> outputFileArg =
112112
Arrays.stream(rawArguments)
113113
.filter(arg -> arg != null && arg.startsWith(parameterPrefix))
@@ -144,7 +144,7 @@ void test_the_provideComponent(String testFolder) throws IOException {
144144
}
145145
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
146146
mockedOperations
147-
.when(() -> Operations.runProcess(any(), any()))
147+
.when(() -> Operations.runProcess(any(), any(), any()))
148148
.thenAnswer(
149149
invocationOnMock ->
150150
getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"));
@@ -190,7 +190,7 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException {
190190
}
191191
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
192192
mockedOperations
193-
.when(() -> Operations.runProcess(any(), any()))
193+
.when(() -> Operations.runProcess(any(), any(), any()))
194194
.thenAnswer(
195195
invocationOnMock ->
196196
getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"));

0 commit comments

Comments
 (0)