Skip to content
Open
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
132 changes: 131 additions & 1 deletion src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.github.guacsec.trustifyda.image.ImageUtils;
import io.github.guacsec.trustifyda.license.LicenseCheck;
import io.github.guacsec.trustifyda.logging.LoggersFactory;
import io.github.guacsec.trustifyda.providers.JavaMavenProvider;
import io.github.guacsec.trustifyda.providers.javascript.workspace.JsWorkspaceDiscovery;
import io.github.guacsec.trustifyda.providers.rust.model.CargoMetadata;
import io.github.guacsec.trustifyda.tools.Ecosystem;
Expand Down Expand Up @@ -842,7 +843,7 @@ int resolveBatchConcurrency() {
}

private static final Set<String> DEFAULT_WORKSPACE_DISCOVERY_IGNORE =
Set.of("**/node_modules/**", "**/.git/**");
Set.of("**/node_modules/**", "**/.git/**", "**/target/**", "**/build/**", "**/.gradle/**");

/** Merges default ignore patterns, env var overrides, and caller-provided patterns. */
Set<String> resolveIgnorePatterns(Set<String> callerPatterns) {
Expand Down Expand Up @@ -875,6 +876,14 @@ List<Path> discoverWorkspaceManifests(Path workspaceDir, Set<String> ignorePatte
return discoverCargoManifests(workspaceDir, ignorePatterns);
}

// Gradle multi-project: settings.gradle or settings.gradle.kts
boolean hasGradleSettings =
Files.isRegularFile(workspaceDir.resolve("settings.gradle"))
|| Files.isRegularFile(workspaceDir.resolve("settings.gradle.kts"));
if (hasGradleSettings) {
return discoverGradleSubprojects(workspaceDir, ignorePatterns);
}

// JS workspace: require package.json + a lock file
Path packageJson = workspaceDir.resolve("package.json");
boolean hasJsLock =
Expand Down Expand Up @@ -930,6 +939,127 @@ private List<Path> discoverCargoManifests(Path workspaceDir, Set<String> ignoreP
}
}

private static final String GRADLE_INIT_SCRIPT =
"allprojects {\n"
+ " task daListProjects {\n"
+ " doLast {\n"
+ " println \"::DA_PROJECT::${project.path}::${project.projectDir}\"\n"
+ " }\n"
+ " }\n"
+ "}\n";

/**
* Resolve the Gradle binary, preferring gradlew wrapper when available and configured.
*
* @param startDir directory from which to start the wrapper search
* @return path to the Gradle binary
*/
private static String resolveGradleBinary(Path startDir) {
if (Operations.getWrapperPreference("gradle")) {
String wrapperName = Operations.isWindows() ? "gradlew.bat" : "gradlew";
String wrapper =
JavaMavenProvider.traverseForMvnw(
wrapperName, startDir.resolve("build.gradle").toString(), null);
if (wrapper != null) {
return wrapper;
}
}
return Operations.getCustomPathOrElse("gradle");
}

/**
* Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build. Uses a custom
* init script to get a structured project listing.
*/
private List<Path> discoverGradleSubprojects(Path workspaceDir, Set<String> ignorePatterns) {
Path rootBuildKts = workspaceDir.resolve("build.gradle.kts");
Path rootBuild = workspaceDir.resolve("build.gradle");

List<Path> manifestPaths = new ArrayList<>();
if (Files.isRegularFile(rootBuildKts)) {
manifestPaths.add(rootBuildKts);
} else if (Files.isRegularFile(rootBuild)) {
manifestPaths.add(rootBuild);
}

String gradleBin = resolveGradleBinary(workspaceDir);
Path initScriptPath = null;
try {
initScriptPath = Files.createTempFile("da-list-projects-", ".gradle");
Files.writeString(initScriptPath, GRADLE_INIT_SCRIPT);

Operations.ProcessExecOutput output =
Operations.runProcessGetFullOutput(
workspaceDir,
new String[] {
gradleBin,
"-q",
"--no-daemon",
"--init-script",
initScriptPath.toString(),
"daListProjects"
Comment on lines +991 to +1000
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Include stderr (and possibly stdout) in the warning when the Gradle task fails to aid diagnosability.

Right now the warning only includes the exit code. Please also log output.getErrorOutput() (and possibly a truncated getOutput()) to make remote diagnosis easier (e.g., wrong Gradle version, missing wrapper, init script issues). In the catch block, prefer logging the full exception (e.g., LOG.log(Level.WARNING, msg, e)) instead of only e.getMessage() so the stack trace is preserved.

Suggested implementation:

      if (output.getExitCode() != 0) {
        StringBuilder msg =
            new StringBuilder("gradle daListProjects failed with exit code ")
                .append(output.getExitCode());

        String stderr = output.getErrorOutput();
        if (stderr != null && !stderr.isBlank()) {
          msg.append(", stderr: ").append(truncateForLog(stderr));
        }

        String stdout = output.getOutput();
        if (stdout != null && !stdout.isBlank()) {
          msg.append(", stdout: ").append(truncateForLog(stdout));
        }

        LOG.warning(msg.toString());
        return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
      }
public class ExhortApi {

  private static String truncateForLog(String text) {
    if (text == null) {
      return "";
    }
    final int max = 4000;
    String normalized = text.strip();
    if (normalized.length() <= max) {
      return normalized;
    }
    return normalized.substring(0, max) + "...[truncated]";
  }

To fully implement your review comment, you should also update the catch block that surrounds this try to log the full exception instead of only e.getMessage(). For example, change any pattern like:

  • LOG.warning("...: " + e.getMessage());
    to:
  • LOG.log(Level.WARNING, "...", e);

You may need to ensure java.util.logging.Level is imported if it is not already (import java.util.logging.Level; near the top of the file).

},
null);

if (output.getExitCode() != 0) {
LOG.warning("gradle daListProjects failed with exit code " + output.getExitCode());
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
}

for (var proj : parseGradleInitScriptOutput(output.getOutput())) {
if (":".equals(proj.path())) {
continue;
}
Path projDir = Path.of(proj.dir()).toAbsolutePath().normalize();
Path buildKts = projDir.resolve("build.gradle.kts");
Path buildGroovy = projDir.resolve("build.gradle");
if (Files.isRegularFile(buildKts)) {
manifestPaths.add(buildKts);
} else if (Files.isRegularFile(buildGroovy)) {
manifestPaths.add(buildGroovy);
}
}
} catch (Exception e) {
LOG.warning("Failed to discover Gradle subprojects: " + e.getMessage());
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
} finally {
if (initScriptPath != null) {
try {
Files.deleteIfExists(initScriptPath);
} catch (IOException ignored) {
}
}
}

return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
}

record GradleProject(String path, String dir) {}

static List<GradleProject> parseGradleInitScriptOutput(String raw) {
if (raw == null || raw.isBlank()) {
return List.of();
}
String prefix = "::DA_PROJECT::";
List<GradleProject> projects = new ArrayList<>();
for (String line : raw.split("\n")) {
if (!line.startsWith(prefix)) {
continue;
}
String remainder = line.substring(prefix.length());
int lastSep = remainder.lastIndexOf("::");
if (lastSep < 0) {
continue;
}
String path = remainder.substring(0, lastSep);
String dir = remainder.substring(lastSep + 2);
if (!path.isEmpty() && !dir.isEmpty()) {
projects.add(new GradleProject(path, dir));
}
}
return projects;
}

/**
* Checks whether a package.json has "private": true, meaning it should not be analyzed as a
* publishable package.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,17 @@ public static List<Path> filterByIgnorePatterns(

List<PathMatcher> matchers =
ignorePatterns.stream()
.map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p))
.flatMap(
p -> {
var fs = FileSystems.getDefault();
java.util.stream.Stream.Builder<PathMatcher> b =
java.util.stream.Stream.builder();
b.add(fs.getPathMatcher("glob:" + p));
if (p.startsWith("**/")) {
b.add(fs.getPathMatcher("glob:" + p.substring(3)));
}
return b.build();
})
.toList();

return manifests.stream()
Expand Down
Loading
Loading