Skip to content
11 changes: 10 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.gradle.workspace.GradleWorkspaceDiscovery;
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 GradleWorkspaceDiscovery.discoverSubprojects(workspaceDir, ignorePatterns);
}

// JS workspace: require package.json + a lock file
Path packageJson = workspaceDir.resolve("package.json");
boolean hasJsLock =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright 2023-2025 Trustify Dependency Analytics Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.guacsec.trustifyda.providers.gradle.workspace;

import io.github.guacsec.trustifyda.logging.LoggersFactory;
import io.github.guacsec.trustifyda.providers.JavaMavenProvider;
import io.github.guacsec.trustifyda.tools.Operations;
import io.github.guacsec.trustifyda.utils.WorkspaceUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

/** Discovers Gradle multi-project build manifest paths using a custom init script. */
public final class GradleWorkspaceDiscovery {

private static final Logger LOG =
LoggersFactory.getLogger(GradleWorkspaceDiscovery.class.getName());

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";

private GradleWorkspaceDiscovery() {}

public static List<Path> discoverSubprojects(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"
},
null);

if (output.getExitCode() != 0) {
LOG.warning(
"gradle daListProjects failed with exit code "
+ output.getExitCode()
+ ": "
+ output.getError());
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.log(Level.WARNING, "Failed to discover Gradle subprojects", e);
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
} finally {
if (initScriptPath != null) {
try {
Files.deleteIfExists(initScriptPath);
} catch (IOException ignored) {
}
}
}

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

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");
}

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.lines().toList()) {
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;
}
}
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
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

exports io.github.guacsec.trustifyda.providers;
exports io.github.guacsec.trustifyda.providers.javascript.model;
exports io.github.guacsec.trustifyda.providers.gradle.workspace;
exports io.github.guacsec.trustifyda.providers.javascript.workspace;
exports io.github.guacsec.trustifyda.providers.rust.model;
exports io.github.guacsec.trustifyda.logging;
Expand Down
Loading
Loading