Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 72 additions & 6 deletions src/main/java/com/redhat/exhort/providers/JavaMavenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand All @@ -53,20 +54,21 @@ public final class JavaMavenProvider extends BaseJavaProvider {
private static final String PROP_JAVA_HOME = "JAVA_HOME";
private static final Logger log = LoggersFactory.getLogger(JavaMavenProvider.class.getName());
private final String mvnExecutable;
private static final String MVN = "mvn";
private static final String ARG_VERSION = "-v";

public JavaMavenProvider(Path manifest) {
super(Type.MAVEN, manifest);
// check for custom mvn executable
this.mvnExecutable = Operations.getExecutable("mvn", "-v");
// check for custom mvn executable mvn or mvn wrapper
this.mvnExecutable = selectMvnRuntime(manifest);
}

@Override
public Content provideStack() throws IOException {
// clean command used to clean build target
var mvnCleanCmd = new String[] {mvnExecutable, "clean", "-f", manifest.toString()};
var mvnEnvs = getMvnExecEnvs();
// execute the clean command
Operations.runProcess(mvnCleanCmd, mvnEnvs);
Operations.runProcess(manifest.getParent(), mvnCleanCmd, mvnEnvs);
// create a temp file for storing the dependency tree in
var tmpFile = Files.createTempFile("exhort_dot_graph_", null);
// the tree command will build the project and create the dependency tree in the temp file
Expand All @@ -90,7 +92,7 @@ public Content provideStack() throws IOException {
.map(PackageURL::getCoordinates)
.collect(Collectors.toList());
// execute the tree command
Operations.runProcess(mvnTreeCmd.toArray(String[]::new), mvnEnvs);
Operations.runProcess(manifest.getParent(), mvnTreeCmd.toArray(String[]::new), mvnEnvs);
if (debugLoggingIsNeeded()) {
String stackAnalysisDependencyTree = Files.readString(tmpFile);
log.info(
Expand Down Expand Up @@ -137,7 +139,7 @@ private Content generateSbomFromEffectivePom() throws IOException {
manifest.toString()
};
// execute the effective pom command
Operations.runProcess(mvnEffPomCmd, getMvnExecEnvs());
Operations.runProcess(manifest.getParent(), mvnEffPomCmd, getMvnExecEnvs());
if (debugLoggingIsNeeded()) {
String CaEffectivePoM = Files.readString(tmpEffPom);
log.info(
Expand Down Expand Up @@ -366,4 +368,68 @@ public int hashCode() {
return Objects.hash(groupId, artifactId, version);
}
}

private String selectMvnRuntime(final Path manifestPath) {
boolean preferWrapper = Operations.getWrapperPreference(MVN);
if (preferWrapper) {
String wrapperName = isWindows() ? "mvnw.cmd" : "mvnw";
String mvnw = traverseForMvnw(wrapperName, manifestPath.toString());
if (mvnw != null) {
try {
// verify maven wrapper is accessible
Operations.runProcess(manifest.getParent(), mvnw, ARG_VERSION);
log.fine("using maven wrapper from : " + mvnw);
return mvnw;
} catch (Exception e) {
log.warning(
"Failed to check for mvnw due to: " + e.getMessage() + " Fall back to use mvn");
}
}
}
// If maven wrapper is not requested or not accessible, fall back to use mvn
String mvn = Operations.getExecutable(MVN, ARG_VERSION);
log.fine("using mvn executable from : " + mvn);
return mvn;
}

private String traverseForMvnw(String wrapperName, String startingManifest) {
return traverseForMvnw(wrapperName, startingManifest, null);
}

public static String traverseForMvnw(
String wrapperName, String startingManifest, String repoRoot) {
String normalizedManifest = normalizePath(startingManifest);

Path path = Paths.get(normalizedManifest);
if (repoRoot == null) {
repoRoot =
Operations.getGitRootDir(path.getParent().toString())
.orElse(path.toAbsolutePath().getRoot().toString());
}

Path wrapperPath = path.getParent().resolve(wrapperName).toAbsolutePath();

if (Files.isExecutable(wrapperPath)) {
return wrapperPath.toString();
} else {
String currentDir = path.getParent().toAbsolutePath().toString();
if (currentDir.equals(repoRoot)) {
return null;
}
return traverseForMvnw(wrapperName, path.getParent().toString(), repoRoot);
}
}

public static String normalizePath(String thePath) {
Path normalized = Paths.get(thePath).toAbsolutePath().normalize();
String result = normalized.toString();
if (isWindows()) {
result = result.toLowerCase();
}
return result;
}

private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("win");
}
}
75 changes: 74 additions & 1 deletion src/main/java/com/redhat/exhort/tools/Operations.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,26 @@

import static java.lang.String.join;

import com.redhat.exhort.logging.LoggersFactory;
import com.redhat.exhort.utils.Environment;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/** Utility class used for executing process on the operating system. * */
public final class Operations {

private static final Logger log = LoggersFactory.getLogger(Operations.class.getName());

private Operations() {
// constructor not required for a utility class
}
Expand Down Expand Up @@ -56,12 +65,24 @@ public static String getCustomPathOrElse(String defaultExecutable) {
* @param cmdList list of command parts
*/
public static void runProcess(final String... cmdList) {
runProcess(cmdList, null);
runProcess(null, cmdList, null);
}

public static void runProcess(final Path workingDirectory, final String... cmdList) {
runProcess(workingDirectory, cmdList, null);
}

public static void runProcess(final String[] cmdList, final Map<String, String> envMap) {
runProcess(null, cmdList, null);
}

public static void runProcess(
final Path workingDirectory, final String[] cmdList, final Map<String, String> envMap) {
var processBuilder = new ProcessBuilder();
processBuilder.command(cmdList);
if (workingDirectory != null) {
processBuilder.directory(workingDirectory.toFile());
}
if (envMap != null) {
processBuilder.environment().putAll(envMap);
}
Expand Down Expand Up @@ -276,4 +297,56 @@ public static String getExecutable(
public static String getExecutable(String command, String args) {
return getExecutable(command, args, null);
}

/**
* Checks whether a wrapper preference is set for a given tool name.
*
* <p>This method looks for an environment variable with the name {@code
* EXHORT_PREFER_<TOOLNAME>W}, where {@code <TOOLNAME>} is the uppercase form of the provided
* {@code name} parameter. If the environment variable is present (i.e., not {@code null}), the
* method returns {@code true}.
*
* @param name the tool name for which to check the wrapper preference (e.g., "maven", "gradle")
* @return {@code true} if the corresponding environment variable is set; {@code false} otherwise
*/
public static boolean getWrapperPreference(String name) {
return Environment.get("EXHORT_PREFER_" + name.toUpperCase() + "W") != null;
}

/**
* Attempts to retrieve the root directory of a Git repository for the given working directory.
*
* <p>This method runs the command {@code git rev-parse --show-toplevel} in the specified
* directory, which returns the absolute path to the root of the current Git working tree. If the
* command executes successfully and produces output, the trimmed result is returned as an {@link
* Optional}. If the command fails or the output is empty, an empty {@code Optional} is returned.
*
* @param cwd the working directory in which to execute the Git command
* @return an {@code Optional} containing the Git root directory path if found, otherwise {@code
* Optional.empty()}
*/
public static Optional<String> getGitRootDir(String cwd) {
List<String> command = Arrays.asList("git", "rev-parse", "--show-toplevel");
ProcessBuilder builder =
new ProcessBuilder(command).directory(new File(cwd)).redirectErrorStream(true);
try {
Process process = builder.start();
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line = reader.readLine();
int exitCode = process.waitFor();
if (exitCode == 0) {
return Optional.ofNullable(line).map(String::trim);
} else {
log.warning("Git command exited with code " + exitCode + " in directory " + cwd);
}
}
} catch (IOException e) {
log.warning("I/O error executing git in " + cwd + ": " + e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warning("Git command interrupted in directory " + cwd + ": " + e.getMessage());
}
return Optional.empty();
}
}
4 changes: 2 additions & 2 deletions src/test/java/com/redhat/exhort/impl/ExhortApiIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOExc
depTree = new String(is.readAllBytes());
}
mockedOperations
.when(() -> Operations.runProcess(any(), any()))
.when(() -> Operations.runProcess(any(), any(), any()))
.thenAnswer(
invocationOnMock ->
getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"));
Expand All @@ -378,7 +378,7 @@ private void mockMavenDependencyTree(Ecosystem.Type packageManager) throws IOExc
public static String getOutputFileAndOverwriteItWithMock(
String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix)
throws IOException {
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0];
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[1];
Optional<String> outputFileArg =
Arrays.stream(rawArguments)
.filter(arg -> arg != null && arg.startsWith(parameterPrefix))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void test_the_provideStack(String testFolder) throws IOException {
}
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
mockedOperations
.when(() -> Operations.runProcess(any(), any()))
.when(() -> Operations.runProcess(any(), any(), any()))
.thenAnswer(
invocationOnMock ->
getOutputFileAndOverwriteItWithMock(depTree, invocationOnMock, "-DoutputFile"));
Expand All @@ -107,7 +107,7 @@ void test_the_provideStack(String testFolder) throws IOException {
public static String getOutputFileAndOverwriteItWithMock(
String outputFileContent, InvocationOnMock invocationOnMock, String parameterPrefix)
throws IOException {
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[0];
String[] rawArguments = (String[]) invocationOnMock.getRawArguments()[1];
Optional<String> outputFileArg =
Arrays.stream(rawArguments)
.filter(arg -> arg != null && arg.startsWith(parameterPrefix))
Expand Down Expand Up @@ -144,7 +144,7 @@ void test_the_provideComponent(String testFolder) throws IOException {
}
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
mockedOperations
.when(() -> Operations.runProcess(any(), any()))
.when(() -> Operations.runProcess(any(), any(), any()))
.thenAnswer(
invocationOnMock ->
getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"));
Expand Down Expand Up @@ -190,7 +190,7 @@ void test_the_provideComponent_With_Path(String testFolder) throws IOException {
}
try (MockedStatic<Operations> mockedOperations = mockStatic(Operations.class)) {
mockedOperations
.when(() -> Operations.runProcess(any(), any()))
.when(() -> Operations.runProcess(any(), any(), any()))
.thenAnswer(
invocationOnMock ->
getOutputFileAndOverwriteItWithMock(effectivePom, invocationOnMock, "-Doutput"));
Expand Down
Loading