Skip to content

Commit 83abc4e

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 5252d4b commit 83abc4e

4 files changed

Lines changed: 273 additions & 160 deletions

File tree

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

Lines changed: 2 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import io.github.guacsec.trustifyda.image.ImageUtils;
3131
import io.github.guacsec.trustifyda.license.LicenseCheck;
3232
import io.github.guacsec.trustifyda.logging.LoggersFactory;
33-
import io.github.guacsec.trustifyda.providers.JavaMavenProvider;
33+
import io.github.guacsec.trustifyda.providers.gradle.workspace.GradleWorkspaceDiscovery;
3434
import io.github.guacsec.trustifyda.providers.javascript.workspace.JsWorkspaceDiscovery;
3535
import io.github.guacsec.trustifyda.providers.rust.model.CargoMetadata;
3636
import io.github.guacsec.trustifyda.tools.Ecosystem;
@@ -881,7 +881,7 @@ List<Path> discoverWorkspaceManifests(Path workspaceDir, Set<String> ignorePatte
881881
Files.isRegularFile(workspaceDir.resolve("settings.gradle"))
882882
|| Files.isRegularFile(workspaceDir.resolve("settings.gradle.kts"));
883883
if (hasGradleSettings) {
884-
return discoverGradleSubprojects(workspaceDir, ignorePatterns);
884+
return GradleWorkspaceDiscovery.discoverSubprojects(workspaceDir, ignorePatterns);
885885
}
886886

887887
// JS workspace: require package.json + a lock file
@@ -939,131 +939,6 @@ private List<Path> discoverCargoManifests(Path workspaceDir, Set<String> ignoreP
939939
}
940940
}
941941

942-
private static final String GRADLE_INIT_SCRIPT =
943-
"allprojects {\n"
944-
+ " task daListProjects {\n"
945-
+ " doLast {\n"
946-
+ " println \"::DA_PROJECT::${project.path}::${project.projectDir}\"\n"
947-
+ " }\n"
948-
+ " }\n"
949-
+ "}\n";
950-
951-
/**
952-
* Resolve the Gradle binary, preferring gradlew wrapper when available and configured.
953-
*
954-
* @param startDir directory from which to start the wrapper search
955-
* @return path to the Gradle binary
956-
*/
957-
private static String resolveGradleBinary(Path startDir) {
958-
if (Operations.getWrapperPreference("gradle")) {
959-
String wrapperName = Operations.isWindows() ? "gradlew.bat" : "gradlew";
960-
String wrapper =
961-
JavaMavenProvider.traverseForMvnw(
962-
wrapperName, startDir.resolve("build.gradle").toString(), null);
963-
if (wrapper != null) {
964-
return wrapper;
965-
}
966-
}
967-
return Operations.getCustomPathOrElse("gradle");
968-
}
969-
970-
/**
971-
* Discover all build.gradle[.kts] manifest paths in a Gradle multi-project build. Uses a custom
972-
* init script to get a structured project listing.
973-
*/
974-
private List<Path> discoverGradleSubprojects(Path workspaceDir, Set<String> ignorePatterns) {
975-
Path rootBuildKts = workspaceDir.resolve("build.gradle.kts");
976-
Path rootBuild = workspaceDir.resolve("build.gradle");
977-
978-
List<Path> manifestPaths = new ArrayList<>();
979-
if (Files.isRegularFile(rootBuildKts)) {
980-
manifestPaths.add(rootBuildKts);
981-
} else if (Files.isRegularFile(rootBuild)) {
982-
manifestPaths.add(rootBuild);
983-
}
984-
985-
String gradleBin = resolveGradleBinary(workspaceDir);
986-
Path initScriptPath = null;
987-
try {
988-
initScriptPath = Files.createTempFile("da-list-projects-", ".gradle");
989-
Files.writeString(initScriptPath, GRADLE_INIT_SCRIPT);
990-
991-
Operations.ProcessExecOutput output =
992-
Operations.runProcessGetFullOutput(
993-
workspaceDir,
994-
new String[] {
995-
gradleBin,
996-
"-q",
997-
"--no-daemon",
998-
"--init-script",
999-
initScriptPath.toString(),
1000-
"daListProjects"
1001-
},
1002-
null);
1003-
1004-
if (output.getExitCode() != 0) {
1005-
LOG.warning(
1006-
"gradle daListProjects failed with exit code "
1007-
+ output.getExitCode()
1008-
+ ": "
1009-
+ output.getError());
1010-
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
1011-
}
1012-
1013-
for (var proj : parseGradleInitScriptOutput(output.getOutput())) {
1014-
if (":".equals(proj.path())) {
1015-
continue;
1016-
}
1017-
Path projDir = Path.of(proj.dir()).toAbsolutePath().normalize();
1018-
Path buildKts = projDir.resolve("build.gradle.kts");
1019-
Path buildGroovy = projDir.resolve("build.gradle");
1020-
if (Files.isRegularFile(buildKts)) {
1021-
manifestPaths.add(buildKts);
1022-
} else if (Files.isRegularFile(buildGroovy)) {
1023-
manifestPaths.add(buildGroovy);
1024-
}
1025-
}
1026-
} catch (Exception e) {
1027-
LOG.warning("Failed to discover Gradle subprojects: " + e.getMessage());
1028-
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
1029-
} finally {
1030-
if (initScriptPath != null) {
1031-
try {
1032-
Files.deleteIfExists(initScriptPath);
1033-
} catch (IOException ignored) {
1034-
}
1035-
}
1036-
}
1037-
1038-
return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifestPaths, ignorePatterns);
1039-
}
1040-
1041-
record GradleProject(String path, String dir) {}
1042-
1043-
static List<GradleProject> parseGradleInitScriptOutput(String raw) {
1044-
if (raw == null || raw.isBlank()) {
1045-
return List.of();
1046-
}
1047-
String prefix = "::DA_PROJECT::";
1048-
List<GradleProject> projects = new ArrayList<>();
1049-
for (String line : raw.lines().toList()) {
1050-
if (!line.startsWith(prefix)) {
1051-
continue;
1052-
}
1053-
String remainder = line.substring(prefix.length());
1054-
int lastSep = remainder.lastIndexOf("::");
1055-
if (lastSep < 0) {
1056-
continue;
1057-
}
1058-
String path = remainder.substring(0, lastSep);
1059-
String dir = remainder.substring(lastSep + 2);
1060-
if (!path.isEmpty() && !dir.isEmpty()) {
1061-
projects.add(new GradleProject(path, dir));
1062-
}
1063-
}
1064-
return projects;
1065-
}
1066-
1067942
/**
1068943
* Checks whether a package.json has "private": true, meaning it should not be analyzed as a
1069944
* publishable package.
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
@@ -33,6 +33,7 @@
3333

3434
exports io.github.guacsec.trustifyda.providers;
3535
exports io.github.guacsec.trustifyda.providers.javascript.model;
36+
exports io.github.guacsec.trustifyda.providers.gradle.workspace;
3637
exports io.github.guacsec.trustifyda.providers.javascript.workspace;
3738
exports io.github.guacsec.trustifyda.providers.rust.model;
3839
exports io.github.guacsec.trustifyda.logging;

0 commit comments

Comments
 (0)