diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/BuildingIssue.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/BuildingIssue.java index 331d3565..bfa8be15 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/BuildingIssue.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/BuildingIssue.java @@ -6,6 +6,7 @@ import lombok.Getter; @Builder @Getter public class BuildingIssue { + private final boolean error; private final String message; private final Throwable exception; } diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/ResolvedGraph.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/ResolvedGraph.java index 659d6f05..25994160 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/ResolvedGraph.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/model/ResolvedGraph.java @@ -3,8 +3,6 @@ package p.studio.compiler.model; import p.studio.compiler.source.identifiers.ProjectId; import p.studio.utilities.structures.ReadOnlyList; -import java.util.List; - public record ResolvedGraph( ProjectId root, ReadOnlyList projects, diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyPipelineService.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyPipelineService.java index d46fc2fe..cb4d037e 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyPipelineService.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyPipelineService.java @@ -4,32 +4,59 @@ import lombok.extern.slf4j.Slf4j; import p.studio.compiler.exceptions.BuildException; import p.studio.compiler.model.DependencyPipelineConfig; import p.studio.compiler.model.ResolvedWorkspace; +import p.studio.compiler.workspaces.phases.*; +import p.studio.utilities.logs.LogAggregator; import p.studio.utilities.structures.ReadOnlyCollection; import java.util.List; @Slf4j public final class DependencyPipelineService { + public final static DependencyPipelineService INSTANCE; + + static { + final var phases = List.of( + new SeedPhase(), + new DiscoverPhase(), + new MaterializePhase(), + new LocalizePhase(), + new ValidatePhase(), + new StackPhase(), + new PolicyPhase() + ); + INSTANCE = new DependencyPipelineService(phases); + } + private final List phases; - public DependencyPipelineService(List phases) { + DependencyPipelineService(List phases) { this.phases = phases; } - public ResolvedWorkspace run(DependencyPipelineConfig config) { + public ResolvedWorkspace run( + final DependencyPipelineConfig config, + final LogAggregator logs) { final var ctx = DependencyPipelineContext.seed(config); for (final var dependencyPipelinePhase : phases) { final var issues = dependencyPipelinePhase.run(ctx); + var error = false; if (ReadOnlyCollection.isNotEmpty(issues)) { for (final var issue : issues) { - log.error(issue.getMessage(), issue.getException()); + if (issue.isError()) { + error = true; + logs.using(log).error(issue.getMessage(), issue.getException()); + continue; + } + logs.using(log).warn(issue.getMessage()); } + } + if (error) { throw new BuildException("issues found during dependency pipeline phase: " + dependencyPipelinePhase.getClass().getSimpleName()); } } - log.info("dependency pipeline completed successfully, going to resolve workspace"); + logs.using(log).info("dependency pipeline completed successfully, going to resolve workspace"); return ctx.toResolvedWorkspace(); } diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyReference.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyReference.java index 86013041..7b14fb5e 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyReference.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/DependencyReference.java @@ -2,5 +2,5 @@ package p.studio.compiler.workspaces; import java.nio.file.Path; -public record DependencyReference(Path path) { +public record DependencyReference(Path canonPath) { } diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/DiscoverPhase.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/DiscoverPhase.java index 28ba889d..9f8525fb 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/DiscoverPhase.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/DiscoverPhase.java @@ -1,7 +1,6 @@ package p.studio.compiler.workspaces.phases; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.extern.slf4j.Slf4j; import p.studio.compiler.FrontendRegistryService; import p.studio.compiler.model.BuildingIssue; import p.studio.compiler.model.PrometeuManifest; @@ -12,10 +11,11 @@ import p.studio.utilities.structures.ReadOnlyCollection; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -@Slf4j + public class DiscoverPhase implements DependencyPipelinePhase { private final ObjectMapper mapper = new ObjectMapper(); @@ -29,36 +29,47 @@ public class DiscoverPhase implements DependencyPipelinePhase { continue; } - final var manifestPath = rootPathCanon.resolve("prometeu.json"); - if (!Files.exists(manifestPath) || !Files.isRegularFile(manifestPath)) { + final Path manifestPathCanon; + try { + manifestPathCanon = rootPathCanon.resolve("prometeu.json").toRealPath(); + } catch (IOException e) { final var issue = BuildingIssue .builder() - .message("dependencies: manifest not found: expected a file " + manifestPath + " (" + rootPathCanon + ")") + .error(true) + .message("[DEPS]: manifest canonPath does not exist: " + rootPathCanon) + .exception(e) + .build(); + issues.add(issue); + continue; + } + if (!Files.exists(manifestPathCanon) || !Files.isRegularFile(manifestPathCanon)) { + final var issue = BuildingIssue + .builder() + .error(true) + .message("[DEPS]: manifest not found: expected a file " + manifestPathCanon) .build(); issues.add(issue); continue; } - final var manifest = PrometeuManifest.extract(manifestPath, mapper); + final var manifest = PrometeuManifest.extract(manifestPathCanon, mapper); final var frontendSpec = FrontendRegistryService.getFrontendSpec(manifest.language()); if (frontendSpec.isEmpty()) { final var issue = BuildingIssue .builder() - .message("dependencies: unknown language " + manifest.language() + " for project " + manifest.name()) + .error(true) + .message("[DEPS]: unknown language " + manifest.language() + " for project " + manifest.name()) .build(); issues.add(issue); continue; } - if (ReadOnlyCollection.isEmpty(manifest.dependencies())) { - log.warn("dependencies: no dependencies specified for project {}", manifest.name()); - } final long projectIndex = ctx.projectInfos.size(); final var projectInfo = ProjectInfo .builder() .rootDirectory(rootPathCanon) - .manifestPath(manifestPath) + .manifestPath(manifestPathCanon) .manifest(manifest) .frontendSpec(frontendSpec.get()) .build(); @@ -74,7 +85,7 @@ public class DiscoverPhase implements DependencyPipelinePhase { } catch (IOException e) { final var issue = BuildingIssue .builder() - .message("dependencies: dep path does not exist: " + dependencyPath + " (" + rootPathCanon + ")") + .message("[DEPS]: dep canonPath does not exist: " + dependencyPath + " (" + manifest.name() + ")") .exception(e) .build(); issues.add(issue); @@ -82,7 +93,7 @@ public class DiscoverPhase implements DependencyPipelinePhase { } else if (dependencyDeclaration instanceof PrometeuManifest.DependencyDeclaration.Git) { final var issue = BuildingIssue .builder() - .message("dependencies: url dependencies not yet supported " + manifest.name() + " (" + rootPathCanon + ")") + .message("[DEPS]: url dependencies not yet supported " + manifest.name() + " (" + rootPathCanon + ")") .build(); issues.add(issue); } diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/LocalizePhase.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/LocalizePhase.java index cd8d9242..031ceac1 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/LocalizePhase.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/LocalizePhase.java @@ -15,12 +15,13 @@ public final class LocalizePhase implements DependencyPipelinePhase { for (int i = 0; i < state.projectNodes.size(); i++) { final var fromProjectNode = state.projectNodes.get(i); for (final var dependencyReference : fromProjectNode.getDependencies()) { - final var dependencyReferencePath = dependencyReference.path(); - final var projectId = state.projectIdByDirectoryRoot.get(dependencyReferencePath); + final var dependencyReferenceCanonPath = dependencyReference.canonPath(); + final var projectId = state.projectIdByDirectoryRoot.get(dependencyReferenceCanonPath); if (Objects.isNull(projectId)) { final var issue = BuildingIssue .builder() - .message("dependencies: dependency not found: " + dependencyReferencePath + " (referenced from " + fromProjectNode.getProjectRootPath() + ")") + .error(true) + .message("[DEPS]: dependency not found: " + dependencyReferenceCanonPath) .build(); issues.add(issue); continue; diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/MaterializePhase.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/MaterializePhase.java index 8394bbd9..e974f60e 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/MaterializePhase.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/MaterializePhase.java @@ -33,7 +33,7 @@ public final class MaterializePhase implements DependencyPipelinePhase { if (rootProjectId == null) { final var issue = BuildingIssue .builder() - .message("dependencies: root project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized") + .message("[DEPS]: root project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized") .build(); issues.add(issue); return ReadOnlyCollection.wrap(issues); @@ -57,7 +57,7 @@ public final class MaterializePhase implements DependencyPipelinePhase { } catch (IOException e) { final var issue = BuildingIssue .builder() - .message("dependencies: source root path does not exist: " + sourceRootPath + " (from " + projectInfo.rootDirectory + ")") + .message("[DEPS]: source root canonPath does not exist: " + sourceRootPath + " (from " + projectInfo.rootDirectory + ")") .exception(e) .build(); sourceRootIssues.add(issue); @@ -78,7 +78,7 @@ public final class MaterializePhase implements DependencyPipelinePhase { } catch (IOException e) { final var issue = BuildingIssue .builder() - .message("dependencies: local dep path does not exist: " + dependencyPath + " (from " + projectInfo.rootDirectory + ")") + .message("[DEPS]: local dep canonPath does not exist: " + dependencyPath + " (from " + projectInfo.rootDirectory + ")") .exception(e) .build(); issues.add(issue); @@ -86,7 +86,7 @@ public final class MaterializePhase implements DependencyPipelinePhase { } else if (d instanceof PrometeuManifest.DependencyDeclaration.Git g) { final var issue = BuildingIssue .builder() - .message("dependencies: url dependency '" + g.getGit() + "' requires an explicit 'rev' and lock mapping (not supported yet)") + .message("[DEPS]: url dependency '" + g.getGit() + "' requires an explicit 'rev' and lock mapping (not supported yet)") .build(); issues.add(issue); } diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/SeedPhase.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/SeedPhase.java index 3e9a3027..097c4abb 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/SeedPhase.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/SeedPhase.java @@ -18,7 +18,7 @@ public final class SeedPhase implements DependencyPipelinePhase { } catch (IOException e) { final var issue = BuildingIssue .builder() - .message("Failed to canonicalize root directory: " + ctx.config().cacheDir()) + .message("[DEPS]: failed to canonicalize root directory: " + ctx.config().cacheDir()) .build(); issues.add(issue); } diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/StackPhase.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/StackPhase.java index bf08d008..9a3ae6c9 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/StackPhase.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/StackPhase.java @@ -13,18 +13,18 @@ import java.util.ArrayList; import java.util.Deque; import java.util.List; +import static java.util.stream.Collectors.joining; + public final class StackPhase implements DependencyPipelinePhase { /** * Implements topological sort; detects dependency cycles; sets build stack */ @Override - public ReadOnlyCollection run(DependencyPipelineContext state) { - final List issues = new ArrayList<>(); - - final int n = state.projectNodes.size(); + public ReadOnlyCollection run(DependencyPipelineContext ctx) { + final int n = ctx.projectNodes.size(); final int[] indeg = new int[n]; for (int from = 0; from < n; from++) { - for (final ProjectId to : state.dependenciesByProject.get(from)) { + for (final ProjectId to : ctx.dependenciesByProject.get(from)) { indeg[to.getIndex()]++; } } @@ -38,8 +38,8 @@ public final class StackPhase implements DependencyPipelinePhase { // Performs topological sort using Kahn's algorithm while (!q.isEmpty()) { final int u = q.removeFirst(); - travesalOrder.add(state.projectNodes.get(u).getProjectId()); - for (final ProjectId v : state.dependenciesByProject.get(u)) { + travesalOrder.add(ctx.projectNodes.get(u).getProjectId()); + for (final ProjectId v : ctx.dependenciesByProject.get(u)) { if (--indeg[(int) v.getId()] == 0) { q.addLast((int) v.getId()); } @@ -47,15 +47,110 @@ public final class StackPhase implements DependencyPipelinePhase { } if (travesalOrder.size() != n) { - issues.add(BuildingIssue - .builder() - .message("dependencies: cycle detected in dependency graph") - .build()); - return ReadOnlyCollection.wrap(issues); + boolean[] stuck = new boolean[n]; + for (int i = 0; i < n; i++) stuck[i] = indeg[i] > 0; + + var tarjan = new TarjanScc(n, ctx.dependenciesByProject); + final List> sccs = tarjan.run(stuck); + + final List> cycles = new ArrayList<>(); + for (var scc : sccs) { + if (scc.size() > 1) { + final var cycle = scc + .stream() + .map(i -> ctx.projectNodes.get(i).getProjectId()) + .toList(); + cycles.add(cycle); + } else { + // size==1: cycle only if self-loop exists + final var u = scc.getFirst(); + final var projectId = ctx.projectNodes.get(u).getProjectId(); + boolean selfLoop = false; + for (final var pu : ctx.dependenciesByProject.get(u)) { + if (pu.getIndex() == u) { + selfLoop = true; + break; + } + } + if (selfLoop) { + cycles.add(List.of(projectId)); + } + } + } + + final var msg = "[DEPS]: cycle(s) detected:\n" + cycles.stream() + .map(projectIds -> " * " + projectIds.stream() + .map(pId -> ctx.projectInfos + .get(pId.getIndex()) + .manifest + .name()) + .collect(joining(" -> ")) + ) + .collect(joining("\n")); + + var issue = BuildingIssue.builder().error(true).message(msg).build(); + return ReadOnlyCollection.wrap(List.of(issue)); } - state.stack = new BuildStack(ReadOnlyList.wrap(travesalOrder)); + ctx.stack = new BuildStack(ReadOnlyList.wrap(travesalOrder)); - return ReadOnlyCollection.wrap(issues); + return ReadOnlyCollection.empty(); + } + + static final class TarjanScc { + private final int n; + private final List> adj; + + private int time = 0; + private final int[] disc; + private final int[] low; + private final boolean[] inStack; + private final Deque stack = new ArrayDeque<>(); + + TarjanScc(int n, List> adj) { + this.n = n; + this.adj = adj; + this.disc = new int[n]; + this.low = new int[n]; + this.inStack = new boolean[n]; + java.util.Arrays.fill(disc, -1); + } + + List> run(boolean[] allowed) { + List> sccs = new ArrayList<>(); + for (int i = 0; i < n; i++) { + if (allowed[i] && disc[i] == -1) dfs(i, allowed, sccs); + } + return sccs; + } + + private void dfs(int u, boolean[] allowed, List> out) { + disc[u] = low[u] = time++; + stack.push(u); + inStack[u] = true; + + for (ProjectId pid : adj.get(u)) { + int v = pid.getIndex(); + if (!allowed[v]) continue; + + if (disc[v] == -1) { + dfs(v, allowed, out); + low[u] = Math.min(low[u], low[v]); + } else if (inStack[v]) { + low[u] = Math.min(low[u], disc[v]); + } + } + + if (low[u] == disc[u]) { + List scc = new ArrayList<>(); + while (true) { + int w = stack.pop(); + inStack[w] = false; + scc.add(w); + if (w == u) break; + } + out.add(scc); + } + } } } diff --git a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/ValidatePhase.java b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/ValidatePhase.java index 14e39fe3..6b02acf6 100644 --- a/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/ValidatePhase.java +++ b/prometeu-compiler/prometeu-deps/src/main/java/p/studio/compiler/workspaces/phases/ValidatePhase.java @@ -1,30 +1,32 @@ package p.studio.compiler.workspaces.phases; import p.studio.compiler.model.BuildingIssue; -import p.studio.compiler.workspaces.*; -import p.studio.compiler.exceptions.BuildException; +import p.studio.compiler.workspaces.DependencyPipelineContext; +import p.studio.compiler.workspaces.DependencyPipelinePhase; import p.studio.utilities.structures.ReadOnlyCollection; -import java.util.ArrayList; import java.util.List; public final class ValidatePhase implements DependencyPipelinePhase { @Override public ReadOnlyCollection run(DependencyPipelineContext state) { - final List issues = new ArrayList<>(); if (state.root == null) { - issues.add(BuildingIssue + final var issue = BuildingIssue .builder() - .message("dependencies: root ProjectId not set") - .build()); - return ReadOnlyCollection.wrap(issues); + .message("[DEPS]: root ProjectId not set") + .build(); + return ReadOnlyCollection.wrap(List.of(issue)); } - // Ensure edges list matches number of nodes + // Ensure the edges list matches the number of nodes if (state.dependenciesByProject.size() != state.projectNodes.size()) { - throw new BuildException("dependencies: internal error: edges list size mismatch"); + final var issue = BuildingIssue + .builder() + .message("[DEPS]: internal error: edges list size mismatch") + .build(); + return ReadOnlyCollection.wrap(List.of(issue)); } - return ReadOnlyCollection.wrap(issues); + return ReadOnlyCollection.empty(); } } diff --git a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DependencyPipelineServiceTest.java b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DependencyPipelineServiceTest.java index 7db0418f..2864c829 100644 --- a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DependencyPipelineServiceTest.java +++ b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DependencyPipelineServiceTest.java @@ -2,7 +2,6 @@ package p.studio.compiler.workspaces; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import p.studio.compiler.exceptions.BuildException; import p.studio.compiler.model.DependencyPipelineConfig; import p.studio.compiler.model.ResolvedWorkspace; import p.studio.compiler.workspaces.phases.*; @@ -68,7 +67,7 @@ class DependencyPipelineServiceTest { deps.append("["); for (int i = 0; i < localDeps.length; i++) { if (i > 0) deps.append(","); - deps.append("{\"path\":\"").append(localDeps[i]).append("\"}"); + deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}"); } deps.append("]"); final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}"; diff --git a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DiscoverPhaseTest.java b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DiscoverPhaseTest.java index 857452c7..2203f15a 100644 --- a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DiscoverPhaseTest.java +++ b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/DiscoverPhaseTest.java @@ -44,7 +44,7 @@ class DiscoverPhaseTest { deps.append("["); for (int i = 0; i < localDeps.length; i++) { if (i > 0) deps.append(","); - deps.append("{\"path\":\"").append(localDeps[i]).append("\"}"); + deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}"); } deps.append("]"); final String langField = language == null ? "" : ",\n \"language\": \"" + language + "\""; diff --git a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/LocalizePhaseTest.java b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/LocalizePhaseTest.java index eb69c120..471075dc 100644 --- a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/LocalizePhaseTest.java +++ b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/LocalizePhaseTest.java @@ -45,7 +45,7 @@ class LocalizePhaseTest { deps.append("["); for (int i = 0; i < localDeps.length; i++) { if (i > 0) deps.append(","); - deps.append("{\"path\":\"").append(localDeps[i]).append("\"}"); + deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}"); } deps.append("]"); final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}"; diff --git a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/MaterializePhaseTest.java b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/MaterializePhaseTest.java index 9c827e27..87bbdc1b 100644 --- a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/MaterializePhaseTest.java +++ b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/MaterializePhaseTest.java @@ -53,7 +53,7 @@ class MaterializePhaseTest { deps.append("["); for (int i = 0; i < localDeps.length; i++) { if (i > 0) deps.append(","); - deps.append("{\"path\":\"").append(localDeps[i]).append("\"}"); + deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}"); } deps.append("]"); final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}"; diff --git a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/StackPhaseTest.java b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/StackPhaseTest.java index 193cf8bc..9588c716 100644 --- a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/StackPhaseTest.java +++ b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/StackPhaseTest.java @@ -48,7 +48,7 @@ class StackPhaseTest { deps.append("["); for (int i = 0; i < localDeps.length; i++) { if (i > 0) deps.append(","); - deps.append("{\"path\":\"").append(localDeps[i]).append("\"}"); + deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}"); } deps.append("]"); final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}"; diff --git a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/ValidateAndPolicyPhasesTest.java b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/ValidateAndPolicyPhasesTest.java index edcdced2..e453526f 100644 --- a/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/ValidateAndPolicyPhasesTest.java +++ b/prometeu-compiler/prometeu-deps/src/test/java/p/studio/compiler/workspaces/ValidateAndPolicyPhasesTest.java @@ -49,7 +49,7 @@ class ValidateAndPolicyPhasesTest { deps.append("["); for (int i = 0; i < localDeps.length; i++) { if (i > 0) deps.append(","); - deps.append("{\"path\":\"").append(localDeps[i]).append("\"}"); + deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}"); } deps.append("]"); final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}"; diff --git a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/FrontendRegistryService.java b/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/FrontendRegistryService.java deleted file mode 100644 index 82c55762..00000000 --- a/prometeu-compiler/prometeu-frontend-api/src/main/java/p/studio/compiler/FrontendRegistryService.java +++ /dev/null @@ -1,4 +0,0 @@ -package p.studio.compiler; - -public class FrontendRegistryService { -} diff --git a/prometeu-infra/src/main/java/p/studio/utilities/logs/LogAggregator.java b/prometeu-infra/src/main/java/p/studio/utilities/logs/LogAggregator.java new file mode 100644 index 00000000..20c9f9a2 --- /dev/null +++ b/prometeu-infra/src/main/java/p/studio/utilities/logs/LogAggregator.java @@ -0,0 +1,19 @@ +package p.studio.utilities.logs; + +import org.slf4j.Logger; + +import java.util.function.Consumer; + +public interface LogAggregator { + static LogAggregator with(final Consumer consumer) { + return new LogAggregatorImpl(consumer); + } + + LogAggregator using(Logger logger); + void trace(String msg); + void debug(String msg); + void info(String msg); + void warn(String msg); + void error(String msg); + void error(String msg, Throwable t); +} diff --git a/prometeu-infra/src/main/java/p/studio/utilities/logs/LogAggregatorImpl.java b/prometeu-infra/src/main/java/p/studio/utilities/logs/LogAggregatorImpl.java new file mode 100644 index 00000000..09abc3f2 --- /dev/null +++ b/prometeu-infra/src/main/java/p/studio/utilities/logs/LogAggregatorImpl.java @@ -0,0 +1,104 @@ +package p.studio.utilities.logs; + +import org.slf4j.Logger; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +class LogAggregatorImpl implements LogAggregator { + private final static String LINE_PATTERN = "%s %s \n"; + + private final Consumer logConsumer; + private final Map cache = new ConcurrentHashMap<>(); + + public LogAggregatorImpl(final Consumer logConsumer) { + this.logConsumer = logConsumer; + } + + private void flush(final String kind, final String msg) { + logConsumer.accept(LINE_PATTERN.formatted(kind, msg)); + } + + @Override + public void trace(String msg) { + flush("[TRACE]", msg); + } + + @Override + public void debug(String msg) { + flush("[DEBUG]", msg); + } + + @Override + public void info(String msg) { + flush("[INFO]", msg); + } + + @Override + public void warn(String msg) { + flush("[WARN]", msg); + } + + @Override + public void error(String msg) { + flush("[ERR]", msg); + } + + @Override + public void error(String msg, Throwable t) { + error(msg); + } + + @Override + public LogAggregator using(final Logger logger) { + return cache + .computeIfAbsent(logger.getName(), __ -> buildNewLogAggregator(logger)); + } + + private LogAggregator buildNewLogAggregator(Logger logger) { + final var aggregator = this; + return new LogAggregator() { + @Override + public LogAggregator using(Logger logger) { + return aggregator.using(logger); + } + + @Override + public void trace(String msg) { + logger.trace(msg); + aggregator.trace(msg); + } + + @Override + public void debug(String msg) { + logger.debug(msg); + aggregator.debug(msg); + } + + @Override + public void info(String msg) { + logger.info(msg); + aggregator.info(msg); + } + + @Override + public void warn(String msg) { + logger.warn(msg); + aggregator.warn(msg); + } + + @Override + public void error(String msg) { + logger.error(msg); + aggregator.error(msg); + } + + @Override + public void error(String msg, Throwable t) { + logger.error(msg, t); + aggregator.error(msg, t); + } + }; + } +} diff --git a/prometeu-studio/build.gradle.kts b/prometeu-studio/build.gradle.kts index 7a1766d4..859d5a85 100644 --- a/prometeu-studio/build.gradle.kts +++ b/prometeu-studio/build.gradle.kts @@ -5,6 +5,8 @@ plugins { dependencies { implementation(project(":prometeu-infra")) + implementation(project(":prometeu-compiler:prometeu-compiler-core")) + implementation(project(":prometeu-compiler:prometeu-deps")) implementation(libs.javafx.controls) implementation(libs.javafx.fxml) implementation(libs.richtextfx) diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index 18db2085..4431eda4 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -19,10 +19,15 @@ public enum I18n { TOOLBAR_EXPORT("toolbar.export"), WORKSPACE_CODE("workspace.code"), - WORKSPACE_ASSETS("workspace.assets"), - WORKSPACE_DEBUG("workspace.debug"), + + WORKSPACE_BUILDER("workspace.builder"), + WORKSPACE_BUILDER_LOGS("workspace.builder.logs"), + WORKSPACE_BUILDER_BUTTON_RUN("workspace.builder.button.run"), + + WORKSPACE_ASSETS("workspace.assets"), + WORKSPACE_DEBUG("workspace.debug"); + - ; @Getter private final String key; diff --git a/prometeu-studio/src/main/java/p/studio/window/MainView.java b/prometeu-studio/src/main/java/p/studio/window/MainView.java index f52542c5..2d602bc1 100644 --- a/prometeu-studio/src/main/java/p/studio/window/MainView.java +++ b/prometeu-studio/src/main/java/p/studio/window/MainView.java @@ -4,6 +4,7 @@ import javafx.scene.layout.BorderPane; import p.studio.workspaces.PlaceholderWorkspace; import p.studio.workspaces.WorkspaceHost; import p.studio.workspaces.WorkspaceId; +import p.studio.workspaces.builder.BuilderWorkspace; import p.studio.workspaces.editor.EditorWorkspace; public final class MainView extends BorderPane { @@ -15,7 +16,7 @@ public final class MainView extends BorderPane { HOST.register(new EditorWorkspace()); HOST.register(new PlaceholderWorkspace(WorkspaceId.ASSETS, "Assets")); - HOST.register(new PlaceholderWorkspace(WorkspaceId.BUILD, "Build")); + HOST.register(new BuilderWorkspace()); HOST.register(new PlaceholderWorkspace(WorkspaceId.DEVICE, "Device")); var bar = new WorkspaceBar(HOST::show); diff --git a/prometeu-studio/src/main/java/p/studio/window/WorkspaceBar.java b/prometeu-studio/src/main/java/p/studio/window/WorkspaceBar.java index e9232b14..077bf748 100644 --- a/prometeu-studio/src/main/java/p/studio/window/WorkspaceBar.java +++ b/prometeu-studio/src/main/java/p/studio/window/WorkspaceBar.java @@ -21,7 +21,7 @@ public final class WorkspaceBar extends VBox { addBtn(WorkspaceId.EDITOR, "📝", "Editor", onSelect); addBtn(WorkspaceId.ASSETS, "📦", "Assets", onSelect); - addBtn(WorkspaceId.BUILD, "⚙️", "Build", onSelect); + addBtn(WorkspaceId.BUILDER, "⚙️", "Builder", onSelect); addBtn(WorkspaceId.DEVICE, "🎮", "Device", onSelect); } diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java b/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java index ba6bc3c0..8fee6d9f 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java @@ -3,6 +3,6 @@ package p.studio.workspaces; public enum WorkspaceId { EDITOR, ASSETS, - BUILD, + BUILDER, DEVICE } \ No newline at end of file diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/builder/BuilderWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/builder/BuilderWorkspace.java new file mode 100644 index 00000000..9680805d --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/workspaces/builder/BuilderWorkspace.java @@ -0,0 +1,85 @@ +package p.studio.workspaces.builder; + +import javafx.geometry.Orientation; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import p.studio.Container; +import p.studio.compiler.model.DependencyPipelineConfig; +import p.studio.compiler.workspaces.DependencyPipelineService; +import p.studio.utilities.i18n.I18n; +import p.studio.utilities.logs.LogAggregator; +import p.studio.workspaces.Workspace; +import p.studio.workspaces.WorkspaceId; + +import java.nio.file.Paths; +import java.util.List; + +public class BuilderWorkspace implements Workspace { + private final BorderPane root = new BorderPane(); + private final TextArea logs = new TextArea(); + private final Button buildButton = new Button(); + + @Override + public WorkspaceId id() { + return WorkspaceId.BUILDER; + } + + @Override + public I18n title() { + return I18n.WORKSPACE_BUILDER; + } + + @Override + public Node root() { + return root; + } + + public BuilderWorkspace() { + final var toolbar = buildToolBar(); + root.setTop(toolbar); + + final var projectArea = buildProjectArea(); + final var logsPane = buildLogsPane(); + + final var split = new SplitPane(projectArea, logsPane); + split.setOrientation(Orientation.VERTICAL); + split.setDividerPositions(0.5); + + root.setCenter(split); + } + + private ToolBar buildToolBar() { + buildButton.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_BUILDER_BUTTON_RUN)); + buildButton.setOnAction(e -> { + final var rootPath = Paths.get("../test-projects/main").toAbsolutePath(); + final var cfg = new DependencyPipelineConfig(false, rootPath, List.of()); + final var logAggregator = LogAggregator.with(logs::appendText); + final var resolvedWorkspace = DependencyPipelineService.INSTANCE.run(cfg, logAggregator); + for (final var p : resolvedWorkspace.graph().projects()) { + logs.appendText("Project [ " + p.getName() + " ] read\n"); + } + }); + return new ToolBar(buildButton); + } + + private StackPane buildProjectArea() { + final var tmpLabel = new Label("Builder project area (WIP)"); + return new StackPane(tmpLabel); + } + + private TitledPane buildLogsPane() { + logs.setEditable(false); + logs.setWrapText(true); + logs.setMinHeight(350); + + final var pane = new TitledPane(); + pane.setContent(logs); + pane.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_BUILDER_LOGS)); + pane.setCollapsible(true); + pane.setExpanded(true); + + return pane; + } +} diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index 0d209de0..93ee6ab4 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -12,5 +12,9 @@ toolbar.play=Play toolbar.stop=Stop toolbar.export=Export workspace.code=Code +workspace.builder=Builder +workspace.builder.logs=Logs +workspace.builder.button.run=Build + workspace.assets=Assets -workspace.debug=Debug +workspace.debug=Debug \ No newline at end of file diff --git a/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties b/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties index 4846ace3..41e530f5 100644 --- a/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties +++ b/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties @@ -12,5 +12,8 @@ toolbar.play=Executar toolbar.stop=Parar toolbar.export=Exportar workspace.code=Cdigo +workspace.builder=Construtor +workspace.builder.logs=Logs +workspace.builder.button.run=Construir workspace.assets=Assets -workspace.debug=Depurar +workspace.debug=Depurar \ No newline at end of file diff --git a/test-projects/dep1/prometeu.json b/test-projects/dep1/prometeu.json new file mode 100644 index 00000000..ff976c06 --- /dev/null +++ b/test-projects/dep1/prometeu.json @@ -0,0 +1,16 @@ +{ + "name": "dep1", + "version": "1.0.0", + "language": "pbs", + "dependencies": [ + { + "path": "../dep2" + }, + { + "path": "../sdk" + }, + { + "path": "../main" + } + ] +} \ No newline at end of file diff --git a/test-projects/dep2/prometeu.json b/test-projects/dep2/prometeu.json new file mode 100644 index 00000000..11acec3c --- /dev/null +++ b/test-projects/dep2/prometeu.json @@ -0,0 +1,13 @@ +{ + "name": "dep2", + "version": "1.0.0", + "language": "pbs", + "dependencies": [ + { + "path": "../dep1" + }, + { + "path": "../sdk" + } + ] +} \ No newline at end of file diff --git a/test-projects/main/prometeu.json b/test-projects/main/prometeu.json new file mode 100644 index 00000000..484a513a --- /dev/null +++ b/test-projects/main/prometeu.json @@ -0,0 +1,13 @@ +{ + "name": "main", + "version": "1.0.0", + "language": "pbs", + "dependencies": [ + { + "path": "../dep1" + }, + { + "path": "../sdk" + } + ] +} \ No newline at end of file diff --git a/test-projects/sdk/prometeu.json b/test-projects/sdk/prometeu.json new file mode 100644 index 00000000..92bfd195 --- /dev/null +++ b/test-projects/sdk/prometeu.json @@ -0,0 +1,7 @@ +{ + "name": "sdk", + "version": "1.0.0", + "language": "pbs", + "dependencies": [ + ] +} \ No newline at end of file