frontend spec by workspace instead

This commit is contained in:
bQUARKz 2026-02-26 05:08:47 +00:00
parent f627914c9f
commit 9170104a12
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
17 changed files with 93 additions and 89 deletions

View File

@ -1,11 +1,9 @@
package p.studio.compiler.workspaces;
import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyCollection;
public interface PipelineStage {
BuildingIssues run(BuilderPipelineContext ctx, LogAggregator logs);
BuildingIssueSink run(BuilderPipelineContext ctx, LogAggregator logs);
}

View File

@ -3,7 +3,7 @@ package p.studio.compiler.workspaces.stages;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.models.SourceHandle;
import p.studio.compiler.workspaces.PipelineStage;
@ -26,15 +26,15 @@ import java.util.stream.Collectors;
public class LoadPipelineStage implements PipelineStage {
@Override
public BuildingIssues run(final BuilderPipelineContext ctx, final LogAggregator logs) {
final var issues = BuildingIssues.empty();
public BuildingIssueSink run(final BuilderPipelineContext ctx, final LogAggregator logs) {
final var issues = BuildingIssueSink.empty();
// Iterates projects; "loads sources"; registers files
ctx.resolvedWorkspace.topologicalOrder().forEach(pId -> {
final var pd = ctx.resolvedWorkspace.graph().projectTable().get(pId);
logs.using(log).info("Project [ " + pd.getName() + " ] source loading...");
final var allowedExtensions = normalize(pd.getFrontendSpec().getAllowedExtensions());
final var allowedExtensions = normalize(ctx.resolvedWorkspace.frontendSpec().getAllowedExtensions());
for (final var sourceRootPath : pd.getSourceRoots()) {
logs.using(log).debug("Walking source root [ " + sourceRootPath + " ]");
@ -52,14 +52,14 @@ public class LoadPipelineStage implements PipelineStage {
var fileId = ctx.fileTable.register(rawFile);
}
} catch (IOException e) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("Failed to load project [ " + pd.getName() + " ]")
.exception(e));
}
}
});
return BuildingIssues.empty();
return BuildingIssueSink.empty();
}
private static ReadOnlySet<String> normalize(final ReadOnlySet<String> extensions) {

View File

@ -1,7 +1,7 @@
package p.studio.compiler.workspaces.stages;
import lombok.extern.slf4j.Slf4j;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.workspaces.DependencyService;
@ -14,18 +14,18 @@ import java.nio.file.Paths;
@Slf4j
public class ResolvePipelineStage implements PipelineStage {
@Override
public BuildingIssues run(final BuilderPipelineContext ctx, LogAggregator logs) {
public BuildingIssueSink run(final BuilderPipelineContext ctx, LogAggregator logs) {
try {
ctx.rootProjectPathCanon = Paths.get(ctx.config.rootProjectPath()).toRealPath();
} catch (IOException e) {
return BuildingIssues.empty()
.add(builder -> builder
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[BUILD]: root project directory no found: " + ctx.config.rootProjectPath())
.exception(e));
}
final var dependencyConfig = new DependencyConfig(ctx.config.explain(), ctx.rootProjectPathCanon);
ctx.resolvedWorkspace = DependencyService.INSTANCE.run(dependencyConfig, logs);
return BuildingIssues.empty();
return BuildingIssueSink.empty();
}
}

View File

@ -5,18 +5,18 @@ import p.studio.utilities.structures.ReadOnlyCollection;
import java.util.ArrayList;
import java.util.function.Consumer;
public class BuildingIssues extends ReadOnlyCollection<BuildingIssue> {
public class BuildingIssueSink extends ReadOnlyCollection<BuildingIssue> {
private boolean hasErrors = false;
protected BuildingIssues() {
protected BuildingIssueSink() {
super(new ArrayList<>());
}
public static BuildingIssues empty() {
return new BuildingIssues();
public static BuildingIssueSink empty() {
return new BuildingIssueSink();
}
public BuildingIssues add(final Consumer<BuildingIssue.BuildingIssueBuilder> consumer) {
public BuildingIssueSink report(final Consumer<BuildingIssue.BuildingIssueBuilder> consumer) {
final var builder = BuildingIssue.builder();
consumer.accept(builder);
final var issue = builder.build();
@ -25,7 +25,7 @@ public class BuildingIssues extends ReadOnlyCollection<BuildingIssue> {
return this;
}
public BuildingIssues add(final BuildingIssues issues) {
public BuildingIssueSink report(final BuildingIssueSink issues) {
hasErrors |= issues.hasErrors();
collection.addAll(issues.collection);
return this;

View File

@ -11,4 +11,8 @@ public class FrontendSpec {
private final ReadOnlySet<String> allowedExtensions;
private final ReadOnlySet<String> sourceRoots;
private final boolean caseSensitive;
public String toString() {
return String.format("FrontendSpec(language=%s)", languageId);
}
}

View File

@ -12,6 +12,5 @@ public final class ProjectDescriptor {
private final Path rootPath; // canon root path
private final String name; // project name
private final String version; // project version
private final ReadOnlyList<Path> sourceRoots;
private final FrontendSpec frontendSpec;
private final ReadOnlyList<Path> sourceRoots; // source roots canon paths for the project
}

View File

@ -10,21 +10,18 @@ import java.nio.file.Path;
import java.util.*;
public final class DependencyContext {
private final DependencyConfig config;
// Internal state mirroring Rust ResolverState
public Path mainProjectRootPathCanon;
// Phase 1 (Discover)
public final DependencyConfig config;
public final Deque<Path> pending = new ArrayDeque<>();
public final ProjectInfoTable projectInfoTable = new ProjectInfoTable();
public final ProjectInfoTable projectInfoTable = new ProjectInfoTable();
public final ProjectTable projectTable = new ProjectTable();
public final Map<String, Set<String>> projectNameAndVersions = new HashMap<>();
public final List<ProjectId> projectIds = new ArrayList<>();
public final List<List<ProjectId>> dependenciesByProject = new ArrayList<>();
public Path mainProjectRootPathCanon;
public FrontendSpec frontendSpec;
public ProjectId rootProjectId;
public BuildStack stack;
@ -36,10 +33,6 @@ public final class DependencyContext {
return new DependencyContext(config);
}
public DependencyConfig config() {
return config;
}
private ReadOnlyList<ReadOnlyList<ProjectId>> buildDependenciesByProject() {
return ReadOnlyList.wrap(this
.dependenciesByProject
@ -54,7 +47,7 @@ public final class DependencyContext {
}
final var dependenciesByProject = buildDependenciesByProject();
final var workspaceGraph = new WorkspaceGraph(projectTable, dependenciesByProject);
return new ResolvedWorkspace(rootProjectId, workspaceGraph, stack);
return new ResolvedWorkspace(rootProjectId, frontendSpec, workspaceGraph, stack);
}

View File

@ -11,5 +11,4 @@ public final class ProjectInfo {
public final Path rootDirectory;
public final Path manifestPath;
public final PrometeuManifest manifest;
public final FrontendSpec frontendSpec;
}

View File

@ -6,6 +6,7 @@ import java.util.stream.Stream;
public record ResolvedWorkspace(
ProjectId projectId,
FrontendSpec frontendSpec,
WorkspaceGraph graph,
BuildStack stack) {

View File

@ -1,8 +1,8 @@
package p.studio.compiler.workspaces;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.DependencyContext;
public interface DependencyPhase {
BuildingIssues run(DependencyContext state);
BuildingIssueSink run(DependencyContext state);
}

View File

@ -4,7 +4,7 @@ import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.dtos.PrometeuManifestDTO;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.models.ProjectInfo;
import p.studio.compiler.models.ProjectInfoId;
@ -25,9 +25,9 @@ import java.util.*;
public class DiscoverPhase implements DependencyPhase {
@Override
public BuildingIssues run(final DependencyContext ctx) {
public BuildingIssueSink run(final DependencyContext ctx) {
final Map<Path, ProjectInfoId> projectIndexByDirectory = new HashMap<>();
final BuildingIssues issues = BuildingIssues.empty();
final BuildingIssueSink issues = BuildingIssueSink.empty();
// Discovers projects; registers them; adds dependencies to queue
while (!ctx.pending.isEmpty()) {
final var rootPathCanon = ctx.pending.pollFirst();
@ -40,14 +40,14 @@ public class DiscoverPhase implements DependencyPhase {
try {
manifestPathCanon = rootPathCanon.resolve("prometeu.json").toRealPath();
} catch (IOException e) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: manifest canonPath does not exist: " + rootPathCanon)
.exception(e));
continue;
}
if (!Files.exists(manifestPathCanon) || !Files.isRegularFile(manifestPathCanon)) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: manifest not found: expected a file " + manifestPathCanon));
continue;
@ -64,18 +64,27 @@ public class DiscoverPhase implements DependencyPhase {
final var frontendSpec = FrontendRegistryService.getFrontendSpec(manifest.language());
if (frontendSpec.isEmpty()) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: unknown language " + manifest.language() + " for project " + manifest.name()));
continue;
}
if (Objects.isNull(ctx.frontendSpec)) {
ctx.frontendSpec = frontendSpec.get();
} else if (!ctx.frontendSpec.getLanguageId().equals(frontendSpec.get().getLanguageId())) {
issues.report(builder -> builder
.error(true)
.message(String.format("[DEPS]: inconsistent language: [ %s ] has \"%s\" but should be \"%s\"",
manifest.name(), frontendSpec.get().getLanguageId(), ctx.frontendSpec.getLanguageId())));
continue;
}
final var projectInfo = ProjectInfo
.builder()
.rootDirectory(rootPathCanon)
.manifestPath(manifestPathCanon)
.manifest(manifest)
.frontendSpec(frontendSpec.get())
.build();
final var projectInfoId = ctx.projectInfoTable.register(projectInfo);
projectIndexByDirectory.put(rootPathCanon, projectInfoId);
@ -91,16 +100,16 @@ public class DiscoverPhase implements DependencyPhase {
public static Optional<PrometeuManifest> map(
final Path rootPath,
final PrometeuManifestDTO dto,
final BuildingIssues issues) {
final BuildingIssueSink issues) {
if (StringUtils.isBlank(dto.name())) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: manifest missing 'name': " + rootPath));
}
if (StringUtils.isBlank(dto.version())) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: manifest missing 'version': " + rootPath));
}
@ -121,14 +130,14 @@ public class DiscoverPhase implements DependencyPhase {
private static List<DependencyReference> resolveDependencies(
final Path rootProjectCanonPath,
final List<PrometeuManifestDTO.DependencyDeclaration> dependencies,
final BuildingIssues issues) {
final BuildingIssueSink issues) {
if (CollectionUtils.isEmpty(dependencies)) {
return List.of();
}
final var registryMaybe = RegistryStore.INSTANCE.load(rootProjectCanonPath);
if (registryMaybe.isEmpty()) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: failed to load registry from " + rootProjectCanonPath));
return List.of();
@ -148,7 +157,7 @@ public class DiscoverPhase implements DependencyPhase {
final Path pathCanon = pathResolve.toRealPath();
deps.add(new DependencyReference(pathCanon));
} catch (IOException e) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: failed to canonicalize dependency path: " + pathResolve)
.exception(e));

View File

@ -1,6 +1,6 @@
package p.studio.compiler.workspaces.phases;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.DependencyPhase;
@ -8,19 +8,19 @@ import java.io.IOException;
public final class SeedPhase implements DependencyPhase {
@Override
public BuildingIssues run(final DependencyContext ctx) {
public BuildingIssueSink run(final DependencyContext ctx) {
try {
ctx.mainProjectRootPathCanon = ctx.config().cacheDir().toRealPath();
ctx.mainProjectRootPathCanon = ctx.config.cacheDir().toRealPath();
} catch (IOException e) {
return BuildingIssues.empty()
.add(builder -> builder
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[DEPS]: failed to canonicalize rootProjectId directory: " + ctx.config().cacheDir())
.message("[DEPS]: failed to canonicalize rootProjectId directory: " + ctx.config.cacheDir())
.exception(e));
}
ctx.pending.add(ctx.mainProjectRootPathCanon);
return BuildingIssues.empty();
return BuildingIssueSink.empty();
}
}

View File

@ -1,6 +1,6 @@
package p.studio.compiler.workspaces.phases;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.BuildStack;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.models.ProjectInfoId;
@ -20,7 +20,7 @@ public final class StackPhase implements DependencyPhase {
* Implements topological sort; detects dependency cycles; sets build stack
*/
@Override
public BuildingIssues run(final DependencyContext ctx) {
public BuildingIssueSink run(final DependencyContext ctx) {
final int n = ctx.projectTable.size();
final int[] indeg = new int[n];
for (int from = 0; from < n; from++) {
@ -83,13 +83,13 @@ public final class StackPhase implements DependencyPhase {
)
.collect(joining("\n"));
return BuildingIssues.empty()
.add(builder -> builder.error(true).message(msg));
return BuildingIssueSink.empty()
.report(builder -> builder.error(true).message(msg));
}
ctx.stack = new BuildStack(ReadOnlyList.wrap(travesalOrder));
return BuildingIssues.empty();
return BuildingIssueSink.empty();
}
static final class TarjanScc {

View File

@ -1,24 +1,24 @@
package p.studio.compiler.workspaces.phases;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.DependencyPhase;
public final class ValidatePhase implements DependencyPhase {
@Override
public BuildingIssues run(final DependencyContext ctx) {
public BuildingIssueSink run(final DependencyContext ctx) {
// ensure rootProjectId is set
if (ctx.rootProjectId == null) {
return BuildingIssues.empty()
.add(builder -> builder
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[DEPS]: rootProjectId ProjectId not set"));
}
// ensure the dependenciesByProject matches the number of projectDescriptors
if (ctx.dependenciesByProject.size() != ctx.projectTable.size()) {
return BuildingIssues.empty()
.add(builder -> builder
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[DEPS]: internal error: dependenciesByProject and projectDescriptors size mismatch"));
}
@ -28,8 +28,8 @@ public final class ValidatePhase implements DependencyPhase {
final var name = entry.getKey();
final var versions = entry.getValue();
if (versions.size() > 1) {
return BuildingIssues.empty()
.add(builder -> builder
return BuildingIssueSink.empty()
.report(builder -> builder
.error(true)
.message("[DEPS]: inconsistent version for project: " + name + " (" + versions + ")"));
}
@ -37,6 +37,6 @@ public final class ValidatePhase implements DependencyPhase {
// run check over source policy (if any) from here (FrontedSpec)
return BuildingIssues.empty();
return BuildingIssueSink.empty();
}
}

View File

@ -1,6 +1,6 @@
package p.studio.compiler.workspaces.phases;
import p.studio.compiler.messages.BuildingIssues;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.models.ProjectDescriptor;
import p.studio.compiler.models.ProjectInfo;
@ -16,25 +16,25 @@ import java.util.List;
public final class WireProjectsPhase implements DependencyPhase {
@Override
public BuildingIssues run(final DependencyContext ctx) {
public BuildingIssueSink run(final DependencyContext ctx) {
// to start all over again, we will re-populate the project nodes and dependenciesByProjectId based on the project infos
ctx.rootProjectId = null;
ctx.projectIds.clear();
ctx.projectTable.clear();
ctx.dependenciesByProject.clear();
final BuildingIssues issues = BuildingIssues.empty();
final BuildingIssueSink issues = BuildingIssueSink.empty();
for (int index = 0; index < ctx.projectInfoTable.size(); index++) {
final var projectInfo = ctx.projectInfoTable.get(new ProjectInfoId(index));
final var projectDescriptor = buildProjectDescriptor(projectInfo, issues);
final var projectDescriptor = buildProjectDescriptor(ctx, projectInfo, issues);
final var projectId = ctx.projectTable.register(projectDescriptor);
ctx.projectIds.add(projectId);
}
final var rootProjectId = ctx.projectTable.optionalId(ctx.mainProjectRootPathCanon);
if (rootProjectId.isEmpty()) {
return issues.add(builder -> builder
return issues.report(builder -> builder
.message("[DEPS]: rootProjectId project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized"));
}
@ -47,7 +47,7 @@ public final class WireProjectsPhase implements DependencyPhase {
final var dependencyCanonPath = dependency.canonPath();
final var projectId = ctx.projectTable.optionalId(dependencyCanonPath);
if (projectId.isEmpty()) {
issues.add(builder -> builder
issues.report(builder -> builder
.error(true)
.message("[DEPS]: dependency not found: " + dependencyCanonPath));
continue;
@ -63,25 +63,26 @@ public final class WireProjectsPhase implements DependencyPhase {
}
private static ProjectDescriptor buildProjectDescriptor(
final DependencyContext ctx,
final ProjectInfo projectInfo,
final BuildingIssues issues) {
final BuildingIssues sourceRootIssues = BuildingIssues.empty();
final BuildingIssueSink issues) {
final BuildingIssueSink sourceRootIssues = BuildingIssueSink.empty();
final List<Path> sourceRoots = new ArrayList<>();
for (final var sourceRoot : projectInfo.getFrontendSpec().getSourceRoots()) {
for (final var sourceRoot : ctx.frontendSpec.getSourceRoots()) {
final var sourceRootPath = projectInfo.rootDirectory.resolve(sourceRoot);
try {
final var sourceRootPathCanon = sourceRootPath.toRealPath();
sourceRoots.add(sourceRootPathCanon);
} catch (IOException e) {
sourceRootIssues.add(builder -> builder
sourceRootIssues.report(builder -> builder
.error(true)
.message("[DEPS]: source project canonPath does not exist: " + sourceRootPath + " (from " + projectInfo.rootDirectory + ")")
.exception(e));
}
}
if (sourceRootIssues.size() == projectInfo.getFrontendSpec().getSourceRoots().size()) {
if (sourceRootIssues.size() == ctx.frontendSpec.getSourceRoots().size()) {
// no source roots were found at all
issues.add(sourceRootIssues);
issues.report(sourceRootIssues);
}
return ProjectDescriptor
@ -89,7 +90,6 @@ public final class WireProjectsPhase implements DependencyPhase {
.rootPath(projectInfo.rootDirectory)
.name(projectInfo.manifest.name())
.version(projectInfo.manifest.version())
.frontendSpec(projectInfo.getFrontendSpec())
.sourceRoots(ReadOnlyList.wrap(sourceRoots))
.build();
}

View File

@ -8,7 +8,7 @@ import java.util.Optional;
public class FrontendRegistryService {
private static final FrontendSpec[] FRONTEND_SPECS = {
PBSDefinitions.PBS,
PBSDefinitions.PBS,
};
private static final Map<String, FrontendSpec> FRONTENDS = new HashMap<>();
@ -17,7 +17,8 @@ public class FrontendRegistryService {
for (final var frontendSpec : FRONTEND_SPECS) {
FRONTENDS.put(frontendSpec.getLanguageId(), frontendSpec);
}
if (FRONTENDS.size() != FRONTEND_SPECS.length) throw new IllegalStateException("Duplicate frontend specs found");
if (FRONTENDS.size() != FRONTEND_SPECS.length)
throw new IllegalStateException("Duplicate frontend specs found");
}
public static FrontendSpec getDefaultFrontendSpec() {
@ -27,4 +28,4 @@ public class FrontendRegistryService {
public static Optional<FrontendSpec> getFrontendSpec(final String languageId) {
return Optional.ofNullable(FRONTENDS.get(languageId));
}
}
}