|
32 | 32 | import io.github.guacsec.trustifyda.logging.LoggersFactory; |
33 | 33 | import io.github.guacsec.trustifyda.providers.JavaMavenProvider; |
34 | 34 | import io.github.guacsec.trustifyda.providers.golang.model.GoWorkspace; |
| 35 | +import io.github.guacsec.trustifyda.providers.gradle.workspace.GradleWorkspaceDiscovery; |
35 | 36 | import io.github.guacsec.trustifyda.providers.javascript.workspace.JsWorkspaceDiscovery; |
36 | 37 | import io.github.guacsec.trustifyda.providers.rust.model.CargoMetadata; |
37 | 38 | import io.github.guacsec.trustifyda.tools.Ecosystem; |
38 | 39 | import io.github.guacsec.trustifyda.tools.Operations; |
39 | 40 | import io.github.guacsec.trustifyda.utils.Environment; |
| 41 | +import io.github.guacsec.trustifyda.utils.PyprojectTomlUtils; |
40 | 42 | import io.github.guacsec.trustifyda.utils.WorkspaceUtils; |
41 | 43 | import jakarta.mail.MessagingException; |
42 | 44 | import jakarta.mail.internet.MimeMultipart; |
|
51 | 53 | import java.net.http.HttpRequest; |
52 | 54 | import java.net.http.HttpResponse; |
53 | 55 | import java.nio.charset.StandardCharsets; |
| 56 | +import java.nio.file.FileSystems; |
54 | 57 | import java.nio.file.Files; |
55 | 58 | import java.nio.file.Path; |
| 59 | +import java.nio.file.PathMatcher; |
56 | 60 | import java.time.LocalDateTime; |
57 | 61 | import java.time.temporal.ChronoUnit; |
58 | 62 | import java.util.AbstractMap; |
|
68 | 72 | import java.util.concurrent.CompletionException; |
69 | 73 | import java.util.function.Function; |
70 | 74 | import java.util.function.Supplier; |
| 75 | +import java.util.logging.Level; |
71 | 76 | import java.util.logging.Logger; |
72 | 77 | import java.util.stream.Collectors; |
| 78 | +import org.tomlj.TomlArray; |
| 79 | +import org.tomlj.TomlParseResult; |
| 80 | +import org.tomlj.TomlTable; |
73 | 81 |
|
74 | 82 | /** Concrete implementation of the Exhort {@link Api} Service. */ |
75 | 83 | public final class ExhortApi implements Api { |
@@ -844,7 +852,14 @@ int resolveBatchConcurrency() { |
844 | 852 | } |
845 | 853 |
|
846 | 854 | private static final Set<String> DEFAULT_WORKSPACE_DISCOVERY_IGNORE = |
847 | | - Set.of("**/node_modules/**", "**/.git/**", "**/target/**"); |
| 855 | + Set.of( |
| 856 | + "**/node_modules/**", |
| 857 | + "**/.git/**", |
| 858 | + "**/target/**", |
| 859 | + "**/__pycache__/**", |
| 860 | + "**/.venv/**", |
| 861 | + "**/build/**", |
| 862 | + "**/.gradle/**"); |
848 | 863 |
|
849 | 864 | /** Merges default ignore patterns, env var overrides, and caller-provided patterns. */ |
850 | 865 | Set<String> resolveIgnorePatterns(Set<String> callerPatterns) { |
@@ -895,6 +910,23 @@ List<Path> discoverWorkspaceManifests(Path workspaceDir, Set<String> ignorePatte |
895 | 910 | } |
896 | 911 | } |
897 | 912 |
|
| 913 | + // uv workspace: pyproject.toml with [tool.uv.workspace] + uv.lock |
| 914 | + if (Files.isRegularFile(workspaceDir.resolve("pyproject.toml")) |
| 915 | + && Files.isRegularFile(workspaceDir.resolve("uv.lock"))) { |
| 916 | + List<Path> uvManifests = discoverUvWorkspaceMembers(workspaceDir, ignorePatterns); |
| 917 | + if (!uvManifests.isEmpty()) { |
| 918 | + return uvManifests; |
| 919 | + } |
| 920 | + } |
| 921 | + |
| 922 | + // Gradle multi-project: settings.gradle or settings.gradle.kts |
| 923 | + boolean hasGradleSettings = |
| 924 | + Files.isRegularFile(workspaceDir.resolve("settings.gradle")) |
| 925 | + || Files.isRegularFile(workspaceDir.resolve("settings.gradle.kts")); |
| 926 | + if (hasGradleSettings) { |
| 927 | + return GradleWorkspaceDiscovery.discoverSubprojects(workspaceDir, ignorePatterns); |
| 928 | + } |
| 929 | + |
898 | 930 | // JS workspace: require package.json + a lock file |
899 | 931 | Path packageJson = workspaceDir.resolve("package.json"); |
900 | 932 | boolean hasJsLock = |
@@ -987,6 +1019,86 @@ private List<Path> discoverGoWorkspaceModules(Path workspaceDir, Set<String> ign |
987 | 1019 | } |
988 | 1020 | } |
989 | 1021 |
|
| 1022 | + /** |
| 1023 | + * Discover all pyproject.toml manifest paths in a uv workspace. Parses the root pyproject.toml |
| 1024 | + * for {@code [tool.uv.workspace]} member and exclude globs, then walks the filesystem to find |
| 1025 | + * matching member directories. |
| 1026 | + */ |
| 1027 | + private List<Path> discoverUvWorkspaceMembers(Path workspaceDir, Set<String> ignorePatterns) { |
| 1028 | + try { |
| 1029 | + Path rootPyproject = workspaceDir.resolve("pyproject.toml"); |
| 1030 | + TomlParseResult toml = PyprojectTomlUtils.parseToml(rootPyproject); |
| 1031 | + |
| 1032 | + TomlTable workspaceConfig = toml.getTable("tool.uv.workspace"); |
| 1033 | + if (workspaceConfig == null) { |
| 1034 | + return Collections.emptyList(); |
| 1035 | + } |
| 1036 | + |
| 1037 | + TomlArray membersArray = workspaceConfig.getArray("members"); |
| 1038 | + if (membersArray == null || membersArray.isEmpty()) { |
| 1039 | + return Collections.emptyList(); |
| 1040 | + } |
| 1041 | + |
| 1042 | + List<String> memberPatterns = toStringList(membersArray); |
| 1043 | + if (memberPatterns.isEmpty()) { |
| 1044 | + return Collections.emptyList(); |
| 1045 | + } |
| 1046 | + |
| 1047 | + List<String> excludePatterns = toStringList(workspaceConfig.getArray("exclude")); |
| 1048 | + |
| 1049 | + List<PathMatcher> memberMatchers = |
| 1050 | + memberPatterns.stream() |
| 1051 | + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) |
| 1052 | + .toList(); |
| 1053 | + |
| 1054 | + List<PathMatcher> excludeMatchers = |
| 1055 | + excludePatterns.stream() |
| 1056 | + .map(p -> FileSystems.getDefault().getPathMatcher("glob:" + p)) |
| 1057 | + .toList(); |
| 1058 | + |
| 1059 | + List<Path> manifests = new ArrayList<>(); |
| 1060 | + try (var stream = Files.walk(workspaceDir)) { |
| 1061 | + stream |
| 1062 | + .filter(p -> p.getFileName().toString().equals("pyproject.toml")) |
| 1063 | + .filter(p -> !p.equals(rootPyproject)) |
| 1064 | + .forEach( |
| 1065 | + p -> { |
| 1066 | + Path relative = workspaceDir.relativize(p.getParent()); |
| 1067 | + boolean matchesMember = |
| 1068 | + memberMatchers.stream().anyMatch(m -> m.matches(relative)); |
| 1069 | + boolean matchesExclude = |
| 1070 | + excludeMatchers.stream().anyMatch(m -> m.matches(relative)); |
| 1071 | + if (matchesMember && !matchesExclude) { |
| 1072 | + manifests.add(p); |
| 1073 | + } |
| 1074 | + }); |
| 1075 | + } |
| 1076 | + |
| 1077 | + if (PyprojectTomlUtils.getProjectName(toml) != null) { |
| 1078 | + manifests.addFirst(rootPyproject); |
| 1079 | + } |
| 1080 | + |
| 1081 | + return WorkspaceUtils.filterByIgnorePatterns(workspaceDir, manifests, ignorePatterns); |
| 1082 | + } catch (Exception e) { |
| 1083 | + LOG.log(Level.WARNING, "Failed to discover uv workspace members", e); |
| 1084 | + return Collections.emptyList(); |
| 1085 | + } |
| 1086 | + } |
| 1087 | + |
| 1088 | + static List<String> toStringList(TomlArray array) { |
| 1089 | + if (array == null) { |
| 1090 | + return List.of(); |
| 1091 | + } |
| 1092 | + List<String> result = new ArrayList<>(); |
| 1093 | + for (int i = 0; i < array.size(); i++) { |
| 1094 | + String s = array.getString(i); |
| 1095 | + if (s != null && !s.isBlank()) { |
| 1096 | + result.add(s.trim()); |
| 1097 | + } |
| 1098 | + } |
| 1099 | + return result; |
| 1100 | + } |
| 1101 | + |
990 | 1102 | /** |
991 | 1103 | * Discovers Maven multi-module workspace manifests by invoking {@code mvn help:evaluate} to list |
992 | 1104 | * declared modules, then recursively checking each module for nested aggregators. |
|
0 commit comments