Skip to content

Commit afb152a

Browse files
Strum355claude
andcommitted
refactor: move Gradle workspace discovery to provider layer
Extract ~120 lines of Gradle-specific code (init script, subproject parsing, binary resolution) from ExhortApi into a dedicated GradleWorkspaceDiscovery utility class in the provider layer, following the same pattern as JsWorkspaceDiscovery. Add wrapper-preference tests for gradlew resolution path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 81a38e0 commit afb152a

4 files changed

Lines changed: 280 additions & 160 deletions

File tree

src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java

Lines changed: 9 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.github.guacsec.trustifyda.logging.LoggersFactory;
3333
import io.github.guacsec.trustifyda.providers.JavaMavenProvider;
3434
import io.github.guacsec.trustifyda.providers.golang.model.GoWorkspace;
35+
import io.github.guacsec.trustifyda.providers.gradle.workspace.GradleWorkspaceDiscovery;
3536
import io.github.guacsec.trustifyda.providers.javascript.workspace.JsWorkspaceDiscovery;
3637
import io.github.guacsec.trustifyda.providers.rust.model.CargoMetadata;
3738
import io.github.guacsec.trustifyda.tools.Ecosystem;
@@ -852,7 +853,13 @@ int resolveBatchConcurrency() {
852853

853854
private static final Set<String> DEFAULT_WORKSPACE_DISCOVERY_IGNORE =
854855
Set.of(
855-
"**/node_modules/**", "**/.git/**", "**/target/**", "**/__pycache__/**", "**/.venv/**", "**/build/**", "**/.gradle/**");
856+
"**/node_modules/**",
857+
"**/.git/**",
858+
"**/target/**",
859+
"**/__pycache__/**",
860+
"**/.venv/**",
861+
"**/build/**",
862+
"**/.gradle/**");
856863

857864
/** Merges default ignore patterns, env var overrides, and caller-provided patterns. */
858865
Set<String> resolveIgnorePatterns(Set<String> callerPatterns) {
@@ -917,7 +924,7 @@ List<Path> discoverWorkspaceManifests(Path workspaceDir, Set<String> ignorePatte
917924
Files.isRegularFile(workspaceDir.resolve("settings.gradle"))
918925
|| Files.isRegularFile(workspaceDir.resolve("settings.gradle.kts"));
919926
if (hasGradleSettings) {
920-
return discoverGradleSubprojects(workspaceDir, ignorePatterns);
927+
return GradleWorkspaceDiscovery.discoverSubprojects(workspaceDir, ignorePatterns);
921928
}
922929

923930
// JS workspace: require package.json + a lock file
@@ -975,131 +982,6 @@ private List<Path> discoverCargoManifests(Path workspaceDir, Set<String> ignoreP
975982
}
976983
}
977984

978-
private static final String GRADLE_INIT_SCRIPT =
979-
"allprojects {\n"
980-
+ " task daListProjects {\n"
981-
+ " doLast {\n"
982-
+ " println \"::DA_PROJECT::${project.path}::${project.projectDir}\"\n"
983-
+ " }\n"
984-
+ " }\n"
985-
+ "}\n";
986-
987-
/**
988-
* Resolve the Gradle binary, preferring gradlew wrapper when available and configured.
989-
*
990-
* @param startDir directory from which to start the wrapper search
991-
* @return path to the Gradle binary
992-
*/
993-
private static String resolveGradleBinary(Path startDir) {
994-
if (Operations.getWrapperPreference("gradle")) {
995-
String wrapperName = Operations.isWindows() ? "gradlew.bat" : "gradlew";
996-
String wrapper =
997-
JavaMavenProvider.traverseForMvnw(
998-
wrapperName, startDir.resolve("build.gradle").toString(), null);
999-
if (wrapper != null) {
1000-
return wrapper;
1001-
}
1002-
}
1003-
return Operations.getCustomPathOrElse("gradle");
1004-
}
1005-
1006-
/**
1007-
* Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build. Uses a custom
1008-
* init script to get a structured project listing.
1009-
*/
1010-
private List<Path> discoverGradleSubprojects(Path workspaceDir, Set<String> ignorePatterns) {
1011-
Path rootBuildKts = workspaceDir.resolve("build.gradle.kts");
1012-
Path rootBuild = workspaceDir.resolve("build.gradle");
1013-
1014-
List<Path> manifestPaths = new ArrayList<>();
1015-
if (Files.isRegularFile(rootBuildKts)) {
1016-
manifestPaths.add(rootBuildKts);
1017-
} else if (Files.isRegularFile(rootBuild)) {
1018-
manifestPaths.add(rootBuild);
1019-
}
1020-
1021-
String gradleBin = resolveGradleBinary(workspaceDir);
1022-
Path initScriptPath = null;
1023-
try {
1024-
initScriptPath = Files.createTempFile("da-list-projects-", ".gradle");
1025-
Files.writeString(initScriptPath, GRADLE_INIT_SCRIPT);
1026-
1027-
Operations.ProcessExecOutput output =
1028-
Operations.runProcessGetFullOutput(
1029-
workspaceDir,
1030-
new String[] {
1031-
gradleBin,
1032-
"-q",
1033-
"--no-daemon",
1034-
"--init-script",
1035-
initScriptPath.toString(),
1036-
"daListProjects"
1037-
},
1038-
null);
1039-
1040-
if (output.getExitCode() != 0) {
1041-
LOG.warning(
1042-
"gradle daListProjects failed with exit code "
1043-
+ output.getExitCode()
1044-
+ ": "
1045-
+ output.getError());
1046-
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
1047-
}
1048-
1049-
for (var proj : parseGradleInitScriptOutput(output.getOutput())) {
1050-
if (":".equals(proj.path())) {
1051-
continue;
1052-
}
1053-
Path projDir = Path.of(proj.dir()).toAbsolutePath().normalize();
1054-
Path buildKts = projDir.resolve("build.gradle.kts");
1055-
Path buildGroovy = projDir.resolve("build.gradle");
1056-
if (Files.isRegularFile(buildKts)) {
1057-
manifestPaths.add(buildKts);
1058-
} else if (Files.isRegularFile(buildGroovy)) {
1059-
manifestPaths.add(buildGroovy);
1060-
}
1061-
}
1062-
} catch (Exception e) {
1063-
LOG.warning("Failed to discover Gradle subprojects: " + e.getMessage());
1064-
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
1065-
} finally {
1066-
if (initScriptPath != null) {
1067-
try {
1068-
Files.deleteIfExists(initScriptPath);
1069-
} catch (IOException ignored) {
1070-
}
1071-
}
1072-
}
1073-
1074-
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
1075-
}
1076-
1077-
record GradleProject(String path, String dir) {}
1078-
1079-
static List<GradleProject> parseGradleInitScriptOutput(String raw) {
1080-
if (raw == null || raw.isBlank()) {
1081-
return List.of();
1082-
}
1083-
String prefix = "::DA_PROJECT::";
1084-
List<GradleProject> projects = new ArrayList<>();
1085-
for (String line : raw.lines().toList()) {
1086-
if (!line.startsWith(prefix)) {
1087-
continue;
1088-
}
1089-
String remainder = line.substring(prefix.length());
1090-
int lastSep = remainder.lastIndexOf("::");
1091-
if (lastSep < 0) {
1092-
continue;
1093-
}
1094-
String path = remainder.substring(0, lastSep);
1095-
String dir = remainder.substring(lastSep + 2);
1096-
if (!path.isEmpty() && !dir.isEmpty()) {
1097-
projects.add(new GradleProject(path, dir));
1098-
}
1099-
}
1100-
return projects;
1101-
}
1102-
1103985
/**
1104986
* Discover all go.mod manifest paths in a Go workspace. Uses {@code go work edit -json} to get
1105987
* workspace members.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2023-2025 Trustify Dependency Analytics Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
*
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package io.github.guacsec.trustifyda.providers.gradle.workspace;
18+
19+
import io.github.guacsec.trustifyda.logging.LoggersFactory;
20+
import io.github.guacsec.trustifyda.providers.JavaMavenProvider;
21+
import io.github.guacsec.trustifyda.tools.Operations;
22+
import io.github.guacsec.trustifyda.utils.WorkspaceUtils;
23+
import java.io.IOException;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
import java.util.Set;
29+
import java.util.logging.Level;
30+
import java.util.logging.Logger;
31+
32+
/** Discovers Gradle multi-project build manifest paths using a custom init script. */
33+
public final class GradleWorkspaceDiscovery {
34+
35+
private static final Logger LOG =
36+
LoggersFactory.getLogger(GradleWorkspaceDiscovery.class.getName());
37+
38+
private static final String GRADLE_INIT_SCRIPT =
39+
"allprojects {\n"
40+
+ " task daListProjects {\n"
41+
+ " doLast {\n"
42+
+ " println \"::DA_PROJECT::${project.path}::${project.projectDir}\"\n"
43+
+ " }\n"
44+
+ " }\n"
45+
+ "}\n";
46+
47+
private GradleWorkspaceDiscovery() {}
48+
49+
public static List<Path> discoverSubprojects(Path workspaceDir, Set<String> ignorePatterns) {
50+
Path rootBuildKts = workspaceDir.resolve("build.gradle.kts");
51+
Path rootBuild = workspaceDir.resolve("build.gradle");
52+
53+
List<Path> manifestPaths = new ArrayList<>();
54+
if (Files.isRegularFile(rootBuildKts)) {
55+
manifestPaths.add(rootBuildKts);
56+
} else if (Files.isRegularFile(rootBuild)) {
57+
manifestPaths.add(rootBuild);
58+
}
59+
60+
String gradleBin = resolveGradleBinary(workspaceDir);
61+
Path initScriptPath = null;
62+
try {
63+
initScriptPath = Files.createTempFile("da-list-projects-", ".gradle");
64+
Files.writeString(initScriptPath, GRADLE_INIT_SCRIPT);
65+
66+
Operations.ProcessExecOutput output =
67+
Operations.runProcessGetFullOutput(
68+
workspaceDir,
69+
new String[] {
70+
gradleBin,
71+
"-q",
72+
"--no-daemon",
73+
"--init-script",
74+
initScriptPath.toString(),
75+
"daListProjects"
76+
},
77+
null);
78+
79+
if (output.getExitCode() != 0) {
80+
LOG.warning(
81+
"gradle daListProjects failed with exit code "
82+
+ output.getExitCode()
83+
+ ": "
84+
+ output.getError());
85+
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
86+
}
87+
88+
for (var proj : parseGradleInitScriptOutput(output.getOutput())) {
89+
if (":".equals(proj.path())) {
90+
continue;
91+
}
92+
Path projDir = Path.of(proj.dir()).toAbsolutePath().normalize();
93+
Path buildKts = projDir.resolve("build.gradle.kts");
94+
Path buildGroovy = projDir.resolve("build.gradle");
95+
if (Files.isRegularFile(buildKts)) {
96+
manifestPaths.add(buildKts);
97+
} else if (Files.isRegularFile(buildGroovy)) {
98+
manifestPaths.add(buildGroovy);
99+
}
100+
}
101+
} catch (Exception e) {
102+
LOG.log(Level.WARNING, "Failed to discover Gradle subprojects", e);
103+
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
104+
} finally {
105+
if (initScriptPath != null) {
106+
try {
107+
Files.deleteIfExists(initScriptPath);
108+
} catch (IOException ignored) {
109+
}
110+
}
111+
}
112+
113+
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
114+
}
115+
116+
static String resolveGradleBinary(Path startDir) {
117+
if (Operations.getWrapperPreference("gradle")) {
118+
String wrapperName = Operations.isWindows() ? "gradlew.bat" : "gradlew";
119+
String wrapper =
120+
JavaMavenProvider.traverseForMvnw(
121+
wrapperName, startDir.resolve("build.gradle").toString(), null);
122+
if (wrapper != null) {
123+
return wrapper;
124+
}
125+
}
126+
return Operations.getCustomPathOrElse("gradle");
127+
}
128+
129+
record GradleProject(String path, String dir) {}
130+
131+
static List<GradleProject> parseGradleInitScriptOutput(String raw) {
132+
if (raw == null || raw.isBlank()) {
133+
return List.of();
134+
}
135+
String prefix = "::DA_PROJECT::";
136+
List<GradleProject> projects = new ArrayList<>();
137+
for (String line : raw.lines().toList()) {
138+
if (!line.startsWith(prefix)) {
139+
continue;
140+
}
141+
String remainder = line.substring(prefix.length());
142+
int lastSep = remainder.lastIndexOf("::");
143+
if (lastSep < 0) {
144+
continue;
145+
}
146+
String path = remainder.substring(0, lastSep);
147+
String dir = remainder.substring(lastSep + 2);
148+
if (!path.isEmpty() && !dir.isEmpty()) {
149+
projects.add(new GradleProject(path, dir));
150+
}
151+
}
152+
return projects;
153+
}
154+
}

src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
exports io.github.guacsec.trustifyda.providers;
3737
exports io.github.guacsec.trustifyda.providers.javascript.model;
38+
exports io.github.guacsec.trustifyda.providers.gradle.workspace;
3839
exports io.github.guacsec.trustifyda.providers.javascript.workspace;
3940
exports io.github.guacsec.trustifyda.providers.rust.model;
4041
exports io.github.guacsec.trustifyda.providers.golang.model;

0 commit comments

Comments
 (0)