organize and clean up
This commit is contained in:
parent
e8afc8ea0d
commit
f7101fec4d
@ -22,7 +22,7 @@ public class DepsPipelineStage implements PipelineStage {
|
||||
return ReadOnlyCollection.wrap(List.of(BuildingIssue
|
||||
.builder()
|
||||
.error(true)
|
||||
.message("[DEPS]: root directory no found: " + ctx.getConfig().getRootProjectPath())
|
||||
.message("[DEPS]: rootProjectId directory no found: " + ctx.getConfig().getRootProjectPath())
|
||||
.build()));
|
||||
}
|
||||
final var cfg = new DependencyConfig(false, rootCanonPath);
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
package p.studio.compiler.dtos;
|
||||
|
||||
import com.fasterxml.jackson.annotation.*;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record PrometeuManifestDTO(
|
||||
String name,
|
||||
String version,
|
||||
String language,
|
||||
List<DependencyDeclaration> dependencies) {
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = DependencyDeclaration.Local.class),
|
||||
@JsonSubTypes.Type(value = DependencyDeclaration.Git.class)
|
||||
})
|
||||
public interface DependencyDeclaration {
|
||||
@Getter
|
||||
class Local implements DependencyDeclaration {
|
||||
private final String path;
|
||||
|
||||
@JsonCreator
|
||||
public Local(@JsonProperty("path") final String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
class Git implements DependencyDeclaration {
|
||||
private final String url;
|
||||
private final String rev;
|
||||
|
||||
@JsonCreator
|
||||
public Git(@JsonProperty("url") final String url, @JsonProperty("rev") final String rev) {
|
||||
this.url = url;
|
||||
this.rev = rev;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,4 +10,5 @@ public class FrontendSpec {
|
||||
private final String languageId;
|
||||
private final ReadOnlySet<String> allowedExtensions;
|
||||
private final ReadOnlySet<String> sourceRoots;
|
||||
private final boolean caseSensitive;
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package p.studio.compiler.source.identifiers;
|
||||
|
||||
public class FileId extends AbstractSourceIdentifier {
|
||||
public static final FileId NONE = new FileId(-1L);
|
||||
public class FileId extends SourceIdentifier {
|
||||
public static final FileId NONE = new FileId(-1);
|
||||
|
||||
public FileId(long id) {
|
||||
public FileId(int id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package p.studio.compiler.source.identifiers;
|
||||
|
||||
public class ModuleId extends AbstractSourceIdentifier {
|
||||
public ModuleId(long id) {
|
||||
public class ModuleId extends SourceIdentifier {
|
||||
public ModuleId(int id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package p.studio.compiler.source.identifiers;
|
||||
|
||||
public class NameId extends AbstractSourceIdentifier {
|
||||
public NameId(long id) {
|
||||
public class NameId extends SourceIdentifier {
|
||||
public NameId(int id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package p.studio.compiler.source.identifiers;
|
||||
|
||||
public class NodeId extends AbstractSourceIdentifier {
|
||||
public NodeId(long id) {
|
||||
public class NodeId extends SourceIdentifier {
|
||||
public NodeId(int id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
package p.studio.compiler.source.identifiers;
|
||||
|
||||
public class ProjectId extends AbstractSourceIdentifier {
|
||||
public class ProjectId extends SourceIdentifier {
|
||||
public ProjectId(int id) {
|
||||
super(Integer.toUnsignedLong(id));
|
||||
}
|
||||
|
||||
public ProjectId(long id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,18 +3,24 @@ package p.studio.compiler.source.identifiers;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
abstract class AbstractSourceIdentifier {
|
||||
public abstract class SourceIdentifier {
|
||||
@EqualsAndHashCode.Include
|
||||
private final long id;
|
||||
private final int id;
|
||||
|
||||
AbstractSourceIdentifier(long id) {
|
||||
public SourceIdentifier(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return (int) id;
|
||||
if (id < 0) {
|
||||
throw new IllegalArgumentException("index should be valid (positive)");
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1,6 +1,6 @@
|
||||
package p.studio.compiler.source.identifiers;
|
||||
|
||||
public class SymbolId extends AbstractSourceIdentifier {
|
||||
public class SymbolId extends SourceIdentifier {
|
||||
public SymbolId(int id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package p.studio.compiler.source.identifiers;
|
||||
|
||||
public class TypeId extends AbstractSourceIdentifier {
|
||||
public TypeId(long id) {
|
||||
public class TypeId extends SourceIdentifier {
|
||||
public TypeId(int id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
package p.studio.compiler.source.tables;
|
||||
|
||||
import p.studio.compiler.source.identifiers.SourceIdentifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class DenseTable<IDENTIFIER extends SourceIdentifier, VALUE> {
|
||||
private final List<VALUE> values = new ArrayList<>();
|
||||
private final Function<Integer, IDENTIFIER> identifierGenerator;
|
||||
|
||||
protected DenseTable(final Function<Integer, IDENTIFIER> identifierGenerator) {
|
||||
this.identifierGenerator = Objects.requireNonNull(identifierGenerator);
|
||||
}
|
||||
|
||||
public final int size() { return values.size(); }
|
||||
|
||||
public final VALUE get(IDENTIFIER id) {
|
||||
return values.get(id.getIndex());
|
||||
}
|
||||
|
||||
public IDENTIFIER register(VALUE value) {
|
||||
final int idx = values.size();
|
||||
values.add(value);
|
||||
return identifierGenerator.apply(idx);
|
||||
}
|
||||
|
||||
protected void clear() {
|
||||
values.clear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package p.studio.compiler.source.tables;
|
||||
|
||||
import p.studio.compiler.source.identifiers.FileId;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class FileTable extends InternTable<FileId, Path> {
|
||||
public FileTable() {
|
||||
super(FileId::new);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package p.studio.compiler.source.tables;
|
||||
|
||||
import p.studio.compiler.source.identifiers.SourceIdentifier;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class InternTable<IDENTIFIER extends SourceIdentifier, VALUE>
|
||||
extends DenseTable<IDENTIFIER, VALUE> {
|
||||
|
||||
private final Map<VALUE, IDENTIFIER> identifierByValue = new HashMap<>();
|
||||
|
||||
public InternTable(Function<Integer, IDENTIFIER> identifierGenerator) {
|
||||
super(identifierGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IDENTIFIER register(final VALUE value) {
|
||||
return identifierByValue.computeIfAbsent(value, super::register);
|
||||
}
|
||||
|
||||
public IDENTIFIER get(final VALUE value) {
|
||||
return identifierByValue.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
super.clear();
|
||||
identifierByValue.clear();
|
||||
}
|
||||
|
||||
public boolean containsKey(VALUE value) {
|
||||
return identifierByValue.containsKey(value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package p.studio.compiler.source.tables;
|
||||
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ProjectTable extends InternTable<ProjectId, Path> {
|
||||
public ProjectTable() {
|
||||
super(ProjectId::new);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.studio.compiler.utilities;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import p.studio.compiler.dtos.PrometeuManifestDTO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class PrometeuManifestMapper {
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public static PrometeuManifestDTO read(Path manifestPath) {
|
||||
try {
|
||||
return mapper.readValue(manifestPath.toFile(), PrometeuManifestDTO.class);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to read manifest " + manifestPath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import p.studio.compiler.exceptions.BuildException;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.exceptions.BuildException;
|
||||
import p.studio.compiler.source.tables.ProjectTable;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@ -15,16 +16,16 @@ public final class DependencyContext {
|
||||
public Path mainProjectRootPathCanon;
|
||||
|
||||
// Phase 1 (Discover)
|
||||
public final List<ProjectInfo> projectInfos = new ArrayList<>();
|
||||
public final Map<Path, Long> projectIndexByDirectory = new HashMap<>();
|
||||
public final Deque<Path> pending = new ArrayDeque<>();
|
||||
public final ProjectInfoTable projectInfoTable = new ProjectInfoTable();
|
||||
|
||||
// Phase 2+
|
||||
public final List<ProjectNode> projectNodes = new ArrayList<>();
|
||||
public final Map<Path, ProjectId> projectIdByDirectoryRoot = new HashMap<>();
|
||||
public final ProjectTable projectTable = new ProjectTable();
|
||||
|
||||
public final Map<String, Set<String>> projectNameAndVersions = new HashMap<>();
|
||||
public final List<ProjectDescriptor> projectDescriptors = new ArrayList<>();
|
||||
public final List<List<ProjectId>> dependenciesByProject = new ArrayList<>();
|
||||
|
||||
public ProjectId root;
|
||||
public ProjectId rootProjectId;
|
||||
public BuildStack stack;
|
||||
|
||||
private DependencyContext(DependencyConfig config) {
|
||||
@ -40,32 +41,16 @@ public final class DependencyContext {
|
||||
}
|
||||
|
||||
public ResolvedWorkspace toResolvedWorkspace() {
|
||||
if (root == null) {
|
||||
throw new BuildException("dependencies: internal error: root ProjectId not set");
|
||||
if (rootProjectId == null) {
|
||||
throw new BuildException("dependenciesByProjectId: internal error: rootProjectId ProjectId not set");
|
||||
}
|
||||
final var projectDescriptors = ReadOnlyList.wrap(projectNodes
|
||||
.stream()
|
||||
.map(n -> {
|
||||
final var languageId = n.getLanguageId();
|
||||
final var sourcePolicy = new SourcePolicy(ReadOnlyList.empty(), true); // TODO: source policy should come from a frontend registry anginst language id
|
||||
return ProjectDescriptor
|
||||
.builder()
|
||||
.projectId(n.getProjectId())
|
||||
.name(n.getName())
|
||||
.version(n.getVersion())
|
||||
.projectDir(n.getProjectRootPath())
|
||||
.sourceRoots(n.getSourceRoots())
|
||||
.languageId(languageId)
|
||||
.sourcePolicy(sourcePolicy)
|
||||
.build();
|
||||
})
|
||||
.toList());
|
||||
final var edges = ReadOnlyList.wrap(this
|
||||
final var projectDescriptors = ReadOnlyList.wrap(this.projectDescriptors);
|
||||
final var dependenciesByProject = ReadOnlyList.wrap(this
|
||||
.dependenciesByProject
|
||||
.stream()
|
||||
.map(ReadOnlyList::wrap)
|
||||
.toList());
|
||||
final var graph = new ResolvedGraph(root, projectDescriptors, edges);
|
||||
return new ResolvedWorkspace(root, graph, stack);
|
||||
final var workspaceGraph = new WorkspaceGraph(projectDescriptors, dependenciesByProject);
|
||||
return new ResolvedWorkspace(rootProjectId, workspaceGraph, stack);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
public record LoadedFile(String uri, String text) {
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sources already loaded by dependencies (IO happens in dependencies, not in pipeline).
|
||||
*/
|
||||
public record LoadedSources(
|
||||
/**
|
||||
* For each project in the stack, a list of files (uri + text).
|
||||
*/
|
||||
List<ProjectSources> perProject
|
||||
) {
|
||||
public LoadedSources {
|
||||
perProject = List.copyOf(perProject);
|
||||
}
|
||||
}
|
||||
@ -3,18 +3,19 @@ package p.studio.compiler.models;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.workspaces.DependencyReference;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public class ProjectDescriptor {
|
||||
public final class ProjectDescriptor {
|
||||
private final ProjectId projectId;
|
||||
private final Path projectRootPath;
|
||||
private final String name;
|
||||
private final String version;
|
||||
private final Path projectDir;
|
||||
private final ReadOnlyList<Path> sourceRoots;
|
||||
private final String languageId;
|
||||
private final SourcePolicy sourcePolicy;
|
||||
private final ReadOnlyList<DependencyReference> dependencies;
|
||||
private final FrontendSpec frontendSpec;
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import p.studio.compiler.source.identifiers.SourceIdentifier;
|
||||
|
||||
public class ProjectInfoId extends SourceIdentifier {
|
||||
public ProjectInfoId(int id) {
|
||||
super(id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import p.studio.compiler.source.tables.DenseTable;
|
||||
|
||||
public class ProjectInfoTable extends DenseTable<ProjectInfoId, ProjectInfo> {
|
||||
protected ProjectInfoTable() {
|
||||
super(ProjectInfoId::new);
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.workspaces.DependencyReference;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public final class ProjectNode {
|
||||
private final FrontendSpec frontendSpec;
|
||||
private final ProjectId projectId;
|
||||
private final Path projectRootPath;
|
||||
private final String name;
|
||||
private final String version;
|
||||
private final ReadOnlyList<Path> sourceRoots;
|
||||
private final String languageId;
|
||||
private final ReadOnlyList<DependencyReference> dependencies;
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
public record ProjectSources(
|
||||
ProjectId projectId,
|
||||
ReadOnlyList<LoadedFile> files) {
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public record PrometeuLock(
|
||||
long schema,
|
||||
List<LockMapping> mappings) {
|
||||
|
||||
public PrometeuLock {
|
||||
mappings = mappings != null ? List.copyOf(mappings) : List.of();
|
||||
}
|
||||
|
||||
public static PrometeuLock blank() {
|
||||
return new PrometeuLock(0, List.of());
|
||||
}
|
||||
|
||||
public Optional<String> lookupGitLocalDir(
|
||||
final String url,
|
||||
final String rev) {
|
||||
return mappings
|
||||
.stream()
|
||||
.filter(m -> m instanceof LockMapping.Git g && g.url().equals(url) && g.rev().equals(rev))
|
||||
.map(m -> ((LockMapping.Git) m).localDir())
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
property = "kind"
|
||||
)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = LockMapping.Git.class, name = "git")
|
||||
})
|
||||
public interface LockMapping {
|
||||
record Git(
|
||||
String url,
|
||||
String rev,
|
||||
@JsonProperty("local-dir") String localDir
|
||||
) implements LockMapping {}
|
||||
}
|
||||
}
|
||||
@ -1,88 +1,11 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import com.fasterxml.jackson.annotation.*;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.Getter;
|
||||
import p.studio.compiler.FrontendRegistryService;
|
||||
import p.studio.compiler.exceptions.BuildException;
|
||||
import p.studio.compiler.workspaces.DependencyReference;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public record PrometeuManifest(
|
||||
String name,
|
||||
String version,
|
||||
String language,
|
||||
ReadOnlyList<DependencyDeclaration> dependencies) {
|
||||
public static PrometeuManifest extract(final Path path, final ObjectMapper mapper) {
|
||||
try {
|
||||
final var root = mapper.readTree(path.toFile());
|
||||
final var name = text(root, "name");
|
||||
final var version = text(root, "version");
|
||||
|
||||
final var language = Optional
|
||||
.ofNullable(root.get("language"))
|
||||
.map(JsonNode::asText)
|
||||
.orElseGet(() -> FrontendRegistryService.getDefaultFrontendSpec().getLanguageId());
|
||||
|
||||
final List<PrometeuManifest.DependencyDeclaration> dependencies = new ArrayList<>();
|
||||
final var dependencyNodes = root.get("dependencies");
|
||||
if (dependencyNodes != null && dependencyNodes.isArray()) {
|
||||
for (final var d : dependencyNodes) {
|
||||
if (d.has("path")) {
|
||||
dependencies.add(new PrometeuManifest.DependencyDeclaration.Local(d.get("path").asText()));
|
||||
} else if (d.has("url")) {
|
||||
final var url = d.get("url").asText();
|
||||
final var rev = d.has("rev") ? d.get("rev").asText(null) : null;
|
||||
dependencies.add(new PrometeuManifest.DependencyDeclaration.Git(url, rev));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new PrometeuManifest(name, version, language, ReadOnlyList.wrap(dependencies));
|
||||
} catch (IOException e) {
|
||||
throw new BuildException("dependencies: failed to read or parse prometeu manifest " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(final JsonNode root, final String field) {
|
||||
JsonNode n = root.get(field);
|
||||
return n != null ? n.asText() : null;
|
||||
}
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = DependencyDeclaration.Local.class),
|
||||
@JsonSubTypes.Type(value = DependencyDeclaration.Git.class)
|
||||
})
|
||||
public interface DependencyDeclaration {
|
||||
@Getter
|
||||
class Local implements DependencyDeclaration {
|
||||
private final String path;
|
||||
|
||||
@JsonCreator
|
||||
public Local(@JsonProperty("path") final String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@Getter
|
||||
class Git implements DependencyDeclaration {
|
||||
private final String git;
|
||||
private final String rev;
|
||||
|
||||
@JsonCreator
|
||||
public Git(@JsonProperty("url") final String git, @JsonProperty("rev") final String rev) {
|
||||
this.git = git;
|
||||
this.rev = rev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReadOnlyList<DependencyReference> dependencies) {
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
public record ResolvedGraph(
|
||||
ProjectId root,
|
||||
ReadOnlyList<ProjectDescriptor> projects,
|
||||
ReadOnlyList<ReadOnlyList<ProjectId>> edges) {
|
||||
public ProjectDescriptor project(ProjectId id) {
|
||||
return projects.get(id.getIndex());
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,6 @@ import p.studio.compiler.source.identifiers.ProjectId;
|
||||
|
||||
public record ResolvedWorkspace(
|
||||
ProjectId projectId,
|
||||
ResolvedGraph graph,
|
||||
WorkspaceGraph graph,
|
||||
BuildStack stack) {
|
||||
}
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
public record SourcePolicy(ReadOnlyList<String> extensions, boolean caseSensitive) {
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.compiler.models;
|
||||
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
public record WorkspaceGraph(
|
||||
ReadOnlyList<ProjectDescriptor> projects,
|
||||
ReadOnlyList<ReadOnlyList<ProjectId>> dependenciesByProjectId) {
|
||||
|
||||
public ProjectDescriptor getProjectDescriptor(final ProjectId projectId) {
|
||||
return projects.get(projectId.getIndex());
|
||||
}
|
||||
|
||||
public ReadOnlyList<ProjectId> getDependencies(final ProjectId projectId) {
|
||||
return dependenciesByProjectId.get(projectId.getIndex());
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,35 @@
|
||||
package p.studio.compiler.workspaces.phases;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.BuildingIssue;
|
||||
import p.studio.compiler.models.PrometeuManifest;
|
||||
import p.studio.compiler.workspaces.DependencyPhase;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.models.ProjectInfo;
|
||||
import p.studio.compiler.models.ProjectInfoId;
|
||||
import p.studio.compiler.models.PrometeuManifest;
|
||||
import p.studio.compiler.utilities.PrometeuManifestMapper;
|
||||
import p.studio.compiler.workspaces.DependencyPhase;
|
||||
import p.studio.compiler.workspaces.DependencyReference;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
public class DiscoverPhase implements DependencyPhase {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
|
||||
final Map<Path, ProjectInfoId> projectIndexByDirectory = new HashMap<>();
|
||||
final List<BuildingIssue> issues = new ArrayList<>();
|
||||
while (!ctx.pending.isEmpty()) {
|
||||
final var rootPathCanon = ctx.pending.pollFirst();
|
||||
|
||||
if (ctx.projectIndexByDirectory.containsKey(rootPathCanon)) {
|
||||
if (projectIndexByDirectory.containsKey(rootPathCanon)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -52,7 +56,14 @@ public class DiscoverPhase implements DependencyPhase {
|
||||
continue;
|
||||
}
|
||||
|
||||
final var manifest = PrometeuManifest.extract(manifestPathCanon, mapper);
|
||||
final var prometeuManifestDTO = PrometeuManifestMapper.read(manifestPathCanon);
|
||||
final var manifestMaybe = map(rootPathCanon, prometeuManifestDTO, issues);
|
||||
|
||||
if (manifestMaybe.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final var manifest = manifestMaybe.get();
|
||||
|
||||
final var frontendSpec = FrontendRegistryService.getFrontendSpec(manifest.language());
|
||||
if (frontendSpec.isEmpty()) {
|
||||
@ -65,7 +76,6 @@ public class DiscoverPhase implements DependencyPhase {
|
||||
continue;
|
||||
}
|
||||
|
||||
final long projectIndex = ctx.projectInfos.size();
|
||||
final var projectInfo = ProjectInfo
|
||||
.builder()
|
||||
.rootDirectory(rootPathCanon)
|
||||
@ -73,33 +83,92 @@ public class DiscoverPhase implements DependencyPhase {
|
||||
.manifest(manifest)
|
||||
.frontendSpec(frontendSpec.get())
|
||||
.build();
|
||||
ctx.projectInfos.add(projectInfo);
|
||||
ctx.projectIndexByDirectory.put(rootPathCanon, projectIndex);
|
||||
final var projectInfoId = ctx.projectInfoTable.register(projectInfo);
|
||||
projectIndexByDirectory.put(rootPathCanon, projectInfoId);
|
||||
|
||||
for (final var dependencyDeclaration : manifest.dependencies()) {
|
||||
if (dependencyDeclaration instanceof PrometeuManifest.DependencyDeclaration.Local local) {
|
||||
final var dependencyPath = rootPathCanon.resolve(local.getPath());
|
||||
try {
|
||||
final var dependencyPathCanon = dependencyPath.toRealPath();
|
||||
ctx.pending.add(dependencyPathCanon);
|
||||
} catch (IOException e) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: dep canonPath does not exist: " + dependencyPath + " (" + manifest.name() + ")")
|
||||
.exception(e)
|
||||
.build();
|
||||
issues.add(issue);
|
||||
}
|
||||
} else if (dependencyDeclaration instanceof PrometeuManifest.DependencyDeclaration.Git) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: url dependencies not yet supported " + manifest.name() + " (" + rootPathCanon + ")")
|
||||
.build();
|
||||
issues.add(issue);
|
||||
}
|
||||
}
|
||||
manifest.dependencies().forEach(depRef -> ctx.pending.add(depRef.canonPath()));
|
||||
|
||||
ctx.projectNameAndVersions.computeIfAbsent(manifest.name(), ignore -> new HashSet<>()).add(manifest.version());
|
||||
}
|
||||
|
||||
return ReadOnlyCollection.wrap(issues);
|
||||
}
|
||||
|
||||
public static Optional<PrometeuManifest> map(
|
||||
final Path rootPath,
|
||||
final PrometeuManifestDTO dto,
|
||||
final List<BuildingIssue> buildingIssues) {
|
||||
|
||||
if (StringUtils.isBlank(dto.name())) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.error(true)
|
||||
.message("[DEPS]: manifest missing 'name': " + rootPath)
|
||||
.build();
|
||||
buildingIssues.add(issue);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(dto.version())) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.error(true)
|
||||
.message("[DEPS]: manifest missing 'version': " + rootPath)
|
||||
.build();
|
||||
buildingIssues.add(issue);
|
||||
}
|
||||
|
||||
final var language = StringUtils.isBlank(dto.language())
|
||||
? FrontendRegistryService.getDefaultFrontendSpec().getLanguageId()
|
||||
: dto.language();
|
||||
|
||||
final var dependencies = resolveDependencies(rootPath, dto.dependencies(), buildingIssues);
|
||||
|
||||
if (CollectionUtils.isNotEmpty(buildingIssues)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(new PrometeuManifest(dto.name(), dto.version(), language, ReadOnlyList.wrap(dependencies)));
|
||||
}
|
||||
|
||||
private static List<DependencyReference> resolveDependencies(
|
||||
final Path rootProjectCanonPath,
|
||||
final List<PrometeuManifestDTO.DependencyDeclaration> dependencies,
|
||||
final List<BuildingIssue> buildingIssues) {
|
||||
if (CollectionUtils.isEmpty(dependencies)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final var deps = new ArrayList<DependencyReference>(dependencies.size());
|
||||
for (var dependency : dependencies) {
|
||||
switch (dependency) {
|
||||
case PrometeuManifestDTO.DependencyDeclaration.Local local -> {
|
||||
try {
|
||||
final Path dependencyPathCanon = rootProjectCanonPath.resolve(local.getPath()).toRealPath();
|
||||
deps.add(new DependencyReference(dependencyPathCanon));
|
||||
} catch (IOException e) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.error(true)
|
||||
.message("[DEPS]: failed to canonicalize dependency path: " + local.getPath() + " from (" + rootProjectCanonPath + ")")
|
||||
.exception(e)
|
||||
.build();
|
||||
buildingIssues.add(issue);
|
||||
}
|
||||
}
|
||||
|
||||
case PrometeuManifestDTO.DependencyDeclaration.Git git -> {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.error(true)
|
||||
.message("[DEPS]: git dependencies are not supported yet: " + git.getUrl() + " from (" + rootProjectCanonPath + ")")
|
||||
.build();
|
||||
buildingIssues.add(issue);
|
||||
}
|
||||
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package p.studio.compiler.workspaces.phases;
|
||||
|
||||
import p.studio.compiler.messages.BuildingIssue;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.*;
|
||||
import p.studio.compiler.workspaces.DependencyPhase;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -11,13 +11,13 @@ import java.util.Objects;
|
||||
|
||||
public final class LocalizePhase implements DependencyPhase {
|
||||
@Override
|
||||
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext state) {
|
||||
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
|
||||
final List<BuildingIssue> issues = new ArrayList<>();
|
||||
for (int i = 0; i < state.projectNodes.size(); i++) {
|
||||
final var fromProjectNode = state.projectNodes.get(i);
|
||||
for (int i = 0; i < ctx.projectDescriptors.size(); i++) {
|
||||
final var fromProjectNode = ctx.projectDescriptors.get(i);
|
||||
for (final var dependencyReference : fromProjectNode.getDependencies()) {
|
||||
final var dependencyReferenceCanonPath = dependencyReference.canonPath();
|
||||
final var projectId = state.projectIdByDirectoryRoot.get(dependencyReferenceCanonPath);
|
||||
final var projectId = ctx.projectTable.get(dependencyReferenceCanonPath);
|
||||
if (Objects.isNull(projectId)) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
@ -27,7 +27,7 @@ public final class LocalizePhase implements DependencyPhase {
|
||||
issues.add(issue);
|
||||
continue;
|
||||
}
|
||||
state.dependenciesByProject.get(fromProjectNode.getProjectId().getIndex()).add(projectId);
|
||||
ctx.dependenciesByProject.get(fromProjectNode.getProjectId().getIndex()).add(projectId);
|
||||
}
|
||||
}
|
||||
return ReadOnlyCollection.wrap(issues);
|
||||
|
||||
@ -1,49 +1,53 @@
|
||||
package p.studio.compiler.workspaces.phases;
|
||||
|
||||
import p.studio.compiler.messages.BuildingIssue;
|
||||
import p.studio.compiler.models.*;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.models.ProjectDescriptor;
|
||||
import p.studio.compiler.models.ProjectInfo;
|
||||
import p.studio.compiler.models.ProjectInfoId;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.workspaces.*;
|
||||
import p.studio.compiler.workspaces.DependencyPhase;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
import p.studio.utilities.structures.ReadOnlyList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class MaterializePhase implements DependencyPhase {
|
||||
@Override
|
||||
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
|
||||
// start all over again, we will re-populate the project nodes and edges based on the project infos
|
||||
ctx.projectNodes.clear();
|
||||
ctx.projectIdByDirectoryRoot.clear();
|
||||
// to start all over again, we will re-populate the project nodes and dependenciesByProjectId based on the project infos
|
||||
ctx.rootProjectId = null;
|
||||
ctx.projectDescriptors.clear();
|
||||
ctx.projectTable.clear();
|
||||
ctx.dependenciesByProject.clear();
|
||||
ctx.root = null;
|
||||
|
||||
final List<BuildingIssue> issues = new ArrayList<>();
|
||||
for (int index = 0; index < ctx.projectInfos.size(); index++) {
|
||||
final var projectId = new ProjectId(index);
|
||||
final var projectInfo = ctx.projectInfos.get(index);
|
||||
ctx.projectNodes.add(buildProjectNode(projectId, projectInfo, issues));
|
||||
ctx.projectIdByDirectoryRoot.put(projectInfo.rootDirectory, projectId);
|
||||
for (int index = 0; index < ctx.projectInfoTable.size(); index++) {
|
||||
final var projectInfo = ctx.projectInfoTable.get(new ProjectInfoId(index));
|
||||
final var projectId = ctx.projectTable.register(projectInfo.rootDirectory);
|
||||
ctx.projectDescriptors.add(buildProjectDescriptor(projectId, projectInfo, issues));
|
||||
ctx.dependenciesByProject.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
final var rootProjectId = ctx.projectIdByDirectoryRoot.get(ctx.mainProjectRootPathCanon);
|
||||
final var rootProjectId = ctx.projectTable.get(ctx.mainProjectRootPathCanon);
|
||||
if (rootProjectId == null) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: root project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized")
|
||||
.message("[DEPS]: rootProjectId project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized")
|
||||
.build();
|
||||
issues.add(issue);
|
||||
return ReadOnlyCollection.wrap(issues);
|
||||
}
|
||||
ctx.root = rootProjectId;
|
||||
|
||||
ctx.rootProjectId = rootProjectId;
|
||||
|
||||
return ReadOnlyCollection.wrap(issues);
|
||||
}
|
||||
|
||||
private static ProjectNode buildProjectNode(
|
||||
private static ProjectDescriptor buildProjectDescriptor(
|
||||
final ProjectId projectId,
|
||||
final ProjectInfo projectInfo,
|
||||
final List<BuildingIssue> issues) {
|
||||
@ -57,7 +61,8 @@ public final class MaterializePhase implements DependencyPhase {
|
||||
} catch (IOException e) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: source root canonPath does not exist: " + sourceRootPath + " (from " + projectInfo.rootDirectory + ")")
|
||||
.error(true)
|
||||
.message("[DEPS]: source rootProjectId canonPath does not exist: " + sourceRootPath + " (from " + projectInfo.rootDirectory + ")")
|
||||
.exception(e)
|
||||
.build();
|
||||
sourceRootIssues.add(issue);
|
||||
@ -68,39 +73,15 @@ public final class MaterializePhase implements DependencyPhase {
|
||||
issues.addAll(sourceRootIssues);
|
||||
}
|
||||
|
||||
final List<DependencyReference> dependencyReferencies = new ArrayList<>();
|
||||
for (PrometeuManifest.DependencyDeclaration d : projectInfo.manifest.dependencies()) {
|
||||
if (d instanceof PrometeuManifest.DependencyDeclaration.Local l) {
|
||||
final var dependencyPath = projectInfo.rootDirectory.resolve(l.getPath());
|
||||
try {
|
||||
final var dependencyPathCanon = dependencyPath.toRealPath();
|
||||
dependencyReferencies.add(new DependencyReference(dependencyPathCanon));
|
||||
} catch (IOException e) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: local dep canonPath does not exist: " + dependencyPath + " (from " + projectInfo.rootDirectory + ")")
|
||||
.exception(e)
|
||||
.build();
|
||||
issues.add(issue);
|
||||
}
|
||||
} else if (d instanceof PrometeuManifest.DependencyDeclaration.Git g) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: url dependency '" + g.getGit() + "' requires an explicit 'rev' and lock mapping (not supported yet)")
|
||||
.build();
|
||||
issues.add(issue);
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectNode
|
||||
return ProjectDescriptor
|
||||
.builder()
|
||||
.projectId(projectId)
|
||||
.projectRootPath(projectInfo.rootDirectory)
|
||||
.name(projectInfo.manifest.name())
|
||||
.version(projectInfo.manifest.version())
|
||||
.languageId(projectInfo.manifest.language())
|
||||
.frontendSpec(projectInfo.getFrontendSpec())
|
||||
.sourceRoots(ReadOnlyList.wrap(sourceRoots))
|
||||
.dependencies(ReadOnlyList.wrap(dependencyReferencies))
|
||||
.dependencies(projectInfo.manifest.dependencies())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ public final class SeedPhase implements DependencyPhase {
|
||||
} catch (IOException e) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: failed to canonicalize root directory: " + ctx.config().cacheDir())
|
||||
.message("[DEPS]: failed to canonicalize rootProjectId directory: " + ctx.config().cacheDir())
|
||||
.build();
|
||||
issues.add(issue);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package p.studio.compiler.workspaces.phases;
|
||||
|
||||
import p.studio.compiler.models.BuildStack;
|
||||
import p.studio.compiler.messages.BuildingIssue;
|
||||
import p.studio.compiler.models.ProjectInfoId;
|
||||
import p.studio.compiler.source.identifiers.ProjectId;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.DependencyPhase;
|
||||
@ -20,8 +21,8 @@ public final class StackPhase implements DependencyPhase {
|
||||
* Implements topological sort; detects dependency cycles; sets build stack
|
||||
*/
|
||||
@Override
|
||||
public ReadOnlyCollection<BuildingIssue> run(DependencyContext ctx) {
|
||||
final int n = ctx.projectNodes.size();
|
||||
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
|
||||
final int n = ctx.projectDescriptors.size();
|
||||
final int[] indeg = new int[n];
|
||||
for (int from = 0; from < n; from++) {
|
||||
for (final ProjectId to : ctx.dependenciesByProject.get(from)) {
|
||||
@ -38,7 +39,7 @@ public final class StackPhase implements DependencyPhase {
|
||||
// Performs topological sort using Kahn's algorithm
|
||||
while (!q.isEmpty()) {
|
||||
final int u = q.removeFirst();
|
||||
travesalOrder.add(ctx.projectNodes.get(u).getProjectId());
|
||||
travesalOrder.add(ctx.projectDescriptors.get(u).getProjectId());
|
||||
for (final ProjectId v : ctx.dependenciesByProject.get(u)) {
|
||||
if (--indeg[(int) v.getId()] == 0) {
|
||||
q.addLast((int) v.getId());
|
||||
@ -58,13 +59,13 @@ public final class StackPhase implements DependencyPhase {
|
||||
if (scc.size() > 1) {
|
||||
final var cycle = scc
|
||||
.stream()
|
||||
.map(i -> ctx.projectNodes.get(i).getProjectId())
|
||||
.map(i -> ctx.projectDescriptors.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();
|
||||
final var projectId = ctx.projectDescriptors.get(u).getProjectId();
|
||||
boolean selfLoop = false;
|
||||
for (final var pu : ctx.dependenciesByProject.get(u)) {
|
||||
if (pu.getIndex() == u) {
|
||||
@ -80,10 +81,7 @@ public final class StackPhase implements DependencyPhase {
|
||||
|
||||
final var msg = "[DEPS]: cycle(s) detected:\n" + cycles.stream()
|
||||
.map(projectIds -> " * " + projectIds.stream()
|
||||
.map(pId -> ctx.projectInfos
|
||||
.get(pId.getIndex())
|
||||
.manifest
|
||||
.name())
|
||||
.map(pId -> ctx.projectInfoTable.get(new ProjectInfoId(pId.getIndex())).manifest.name())
|
||||
.collect(joining(" -> "))
|
||||
)
|
||||
.collect(joining("\n"));
|
||||
|
||||
@ -9,24 +9,40 @@ import java.util.List;
|
||||
|
||||
public final class ValidatePhase implements DependencyPhase {
|
||||
@Override
|
||||
public ReadOnlyCollection<BuildingIssue> run(DependencyContext state) {
|
||||
if (state.root == null) {
|
||||
public ReadOnlyCollection<BuildingIssue> run(DependencyContext ctx) {
|
||||
if (ctx.rootProjectId == null) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: root ProjectId not set")
|
||||
.error(true)
|
||||
.message("[DEPS]: rootProjectId ProjectId not set")
|
||||
.build();
|
||||
return ReadOnlyCollection.wrap(List.of(issue));
|
||||
}
|
||||
|
||||
// Ensure the edges list matches the number of nodes
|
||||
if (state.dependenciesByProject.size() != state.projectNodes.size()) {
|
||||
// Ensure the dependenciesByProjectId list matches the number of project descriptors
|
||||
if (ctx.dependenciesByProject.size() != ctx.projectDescriptors.size()) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.message("[DEPS]: internal error: edges list size mismatch")
|
||||
.error(true)
|
||||
.message("[DEPS]: internal error: dependenciesByProjectId list size mismatch")
|
||||
.build();
|
||||
return ReadOnlyCollection.wrap(List.of(issue));
|
||||
}
|
||||
|
||||
// we should check and ensure uniformity to version across the same projects
|
||||
for (final var entry : ctx.projectNameAndVersions.entrySet()) {
|
||||
final var name = entry.getKey();
|
||||
final var versions = entry.getValue();
|
||||
if (versions.size() > 1) {
|
||||
final var issue = BuildingIssue
|
||||
.builder()
|
||||
.error(true)
|
||||
.message("[DEPS]: inconsistent version for project: " + name + " (" + versions + ")")
|
||||
.build();
|
||||
return ReadOnlyCollection.wrap(List.of(issue));
|
||||
}
|
||||
}
|
||||
|
||||
return ReadOnlyCollection.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
package p.studio.compiler.workspaces;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.ResolvedWorkspace;
|
||||
import p.studio.compiler.workspaces.phases.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class DependencyServiceTest {
|
||||
|
||||
@Test
|
||||
void canonical_pipeline_e2e(@TempDir Path tempDir) throws IOException {
|
||||
final Path root = tempDir;
|
||||
final Path depA = root.resolve("depA");
|
||||
Files.createDirectories(depA);
|
||||
Files.createDirectories(root.resolve("src"));
|
||||
Files.createDirectories(depA.resolve("src"));
|
||||
|
||||
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
|
||||
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
|
||||
|
||||
final var cfg = new DependencyConfig(false, root, List.of());
|
||||
final var phases = List.of(
|
||||
new SeedPhase(),
|
||||
new DiscoverPhase(),
|
||||
new MaterializePhase(),
|
||||
new LocalizePhase(),
|
||||
new ValidatePhase(),
|
||||
new StackPhase(),
|
||||
new PolicyPhase()
|
||||
);
|
||||
|
||||
final var service = new DependencyService(phases);
|
||||
final ResolvedWorkspace ws = service.run(cfg);
|
||||
|
||||
assertNotNull(ws);
|
||||
assertNotNull(ws.graph());
|
||||
assertEquals(2, ws.graph().projects().size());
|
||||
|
||||
// Obter ids por nome
|
||||
var rootDesc = ws.graph().projects().stream().filter(p -> p.getName().equals("root-proj")).findFirst().orElseThrow();
|
||||
var depDesc = ws.graph().projects().stream().filter(p -> p.getName().equals("dep-a")).findFirst().orElseThrow();
|
||||
final var rootId = rootDesc.getProjectId();
|
||||
final var depId = depDesc.getProjectId();
|
||||
|
||||
// Edges: root -> dep
|
||||
assertTrue(ws.graph().edges().get(rootId.getIndex()).contains(depId));
|
||||
assertTrue(ws.graph().edges().get(depId.getIndex()).isEmpty());
|
||||
|
||||
// Stack: de acordo com a implementação atual (arestas root->dep), root vem antes
|
||||
assertEquals(rootId, ws.stack().projects().get(0));
|
||||
assertEquals(depId, ws.stack().projects().get(1));
|
||||
|
||||
// workspace.root é o projeto root
|
||||
assertEquals(rootId, ws.projectId());
|
||||
}
|
||||
|
||||
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
|
||||
final StringBuilder deps = new StringBuilder();
|
||||
deps.append("[");
|
||||
for (int i = 0; i < localDeps.length; i++) {
|
||||
if (i > 0) deps.append(",");
|
||||
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
|
||||
}
|
||||
deps.append("]");
|
||||
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
|
||||
Files.writeString(dir.resolve("prometeu.json"), json);
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
package p.studio.compiler.workspaces;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.phases.DiscoverPhase;
|
||||
import p.studio.compiler.workspaces.phases.SeedPhase;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class DiscoverPhaseTest {
|
||||
|
||||
@Test
|
||||
void discover_finds_root_and_local_dependency(@TempDir Path tempDir) throws IOException {
|
||||
// Estrutura: root/ (prometeu.json) -> deps: ["depA/"] e cria depA/(prometeu.json)
|
||||
final Path root = tempDir;
|
||||
final Path depA = root.resolve("depA");
|
||||
Files.createDirectories(depA);
|
||||
|
||||
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"}, null);
|
||||
writeManifest(depA, "dep-a", "0.1.0", new String[]{}, null);
|
||||
|
||||
final var cfg = new DependencyConfig(false, root, java.util.List.of());
|
||||
final var ctx = DependencyContext.seed(cfg);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
|
||||
|
||||
final var issues = new DiscoverPhase().run(ctx);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(issues), "Descoberta deve ocorrer sem issues");
|
||||
|
||||
assertEquals(2, ctx.projectInfos.size(), "Dois projetos devem ser descobertos");
|
||||
assertTrue(ctx.projectIndexByDirectory.containsKey(root.toRealPath()), "Projeto raiz indexado");
|
||||
assertTrue(ctx.projectIndexByDirectory.containsKey(depA.toRealPath()), "Projeto depA indexado");
|
||||
assertEquals(0L, ctx.projectIndexByDirectory.get(root.toRealPath()));
|
||||
assertEquals(1L, ctx.projectIndexByDirectory.get(depA.toRealPath()));
|
||||
}
|
||||
|
||||
private static void writeManifest(Path dir, String name, String version, String[] localDeps, String language) throws IOException {
|
||||
final StringBuilder deps = new StringBuilder();
|
||||
deps.append("[");
|
||||
for (int i = 0; i < localDeps.length; i++) {
|
||||
if (i > 0) deps.append(",");
|
||||
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
|
||||
}
|
||||
deps.append("]");
|
||||
final String langField = language == null ? "" : ",\n \"language\": \"" + language + "\"";
|
||||
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + langField + "\n}";
|
||||
Files.writeString(dir.resolve("prometeu.json"), json);
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
package p.studio.compiler.workspaces;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.phases.*;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class LocalizePhaseTest {
|
||||
|
||||
@Test
|
||||
void localize_resolves_dependency_references(@TempDir Path tempDir) throws IOException {
|
||||
final Path root = tempDir;
|
||||
final Path depA = root.resolve("depA");
|
||||
Files.createDirectories(depA);
|
||||
Files.createDirectories(root.resolve("src"));
|
||||
Files.createDirectories(depA.resolve("src"));
|
||||
|
||||
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
|
||||
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
|
||||
|
||||
final var cfg = new DependencyConfig(false, root, java.util.List.of());
|
||||
final var ctx = DependencyContext.seed(cfg);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new MaterializePhase().run(ctx)));
|
||||
|
||||
final var issues = new LocalizePhase().run(ctx);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(issues), "Localização deve ocorrer sem issues");
|
||||
|
||||
final var depId = ctx.projectIdByDirectoryRoot.get(depA.toRealPath());
|
||||
assertNotNull(depId);
|
||||
final var rootId = ctx.root;
|
||||
assertTrue(ctx.dependenciesByProject.get(rootId.getIndex()).contains(depId), "Root deve depender de depA");
|
||||
}
|
||||
|
||||
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
|
||||
final StringBuilder deps = new StringBuilder();
|
||||
deps.append("[");
|
||||
for (int i = 0; i < localDeps.length; i++) {
|
||||
if (i > 0) deps.append(",");
|
||||
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
|
||||
}
|
||||
deps.append("]");
|
||||
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
|
||||
Files.writeString(dir.resolve("prometeu.json"), json);
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
package p.studio.compiler.workspaces;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.phases.DiscoverPhase;
|
||||
import p.studio.compiler.workspaces.phases.MaterializePhase;
|
||||
import p.studio.compiler.workspaces.phases.SeedPhase;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class MaterializePhaseTest {
|
||||
|
||||
@Test
|
||||
void materialize_builds_nodes_and_sets_root(@TempDir Path tempDir) throws IOException {
|
||||
final Path root = tempDir;
|
||||
final Path depA = root.resolve("depA");
|
||||
Files.createDirectories(depA);
|
||||
Files.createDirectories(root.resolve("src"));
|
||||
Files.createDirectories(depA.resolve("src"));
|
||||
|
||||
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
|
||||
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
|
||||
|
||||
final var cfg = new DependencyConfig(false, root, java.util.List.of());
|
||||
final var ctx = DependencyContext.seed(cfg);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
|
||||
|
||||
final var issues = new MaterializePhase().run(ctx);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(issues), "Materialização sem issues esperadas");
|
||||
|
||||
assertNotNull(ctx.root, "Root ProjectId deve ser definido");
|
||||
assertEquals(2, ctx.projectNodes.size());
|
||||
assertEquals(2, ctx.dependenciesByProject.size());
|
||||
assertTrue(ctx.projectIdByDirectoryRoot.containsKey(root.toRealPath()));
|
||||
assertTrue(ctx.projectIdByDirectoryRoot.containsKey(depA.toRealPath()));
|
||||
|
||||
final var rootNode = ctx.projectNodes.get(ctx.root.getIndex());
|
||||
assertEquals("root-proj", rootNode.getName());
|
||||
assertEquals("0.1.0", rootNode.getVersion());
|
||||
assertEquals("pbs", rootNode.getLanguageId());
|
||||
assertFalse(rootNode.getSourceRoots().isEmpty(), "Deve haver pelo menos um source root (src)");
|
||||
}
|
||||
|
||||
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
|
||||
final StringBuilder deps = new StringBuilder();
|
||||
deps.append("[");
|
||||
for (int i = 0; i < localDeps.length; i++) {
|
||||
if (i > 0) deps.append(",");
|
||||
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
|
||||
}
|
||||
deps.append("]");
|
||||
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
|
||||
Files.writeString(dir.resolve("prometeu.json"), json);
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
package p.studio.compiler.workspaces;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.phases.SeedPhase;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SeedPhaseTest {
|
||||
|
||||
@Test
|
||||
void seed_initializes_root_and_pending(@TempDir Path tempDir) throws Exception {
|
||||
final var cfg = new DependencyConfig(false, tempDir, java.util.List.of());
|
||||
final var ctx = DependencyContext.seed(cfg);
|
||||
|
||||
final var issues = new SeedPhase().run(ctx);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(issues), "Nenhum issue esperado na SeedPhase");
|
||||
|
||||
assertNotNull(ctx.mainProjectRootPathCanon, "Caminho canônico do projeto raiz deve ser inicializado");
|
||||
assertEquals(1, ctx.pending.size(), "Uma entrada deve ser enfileirada para descoberta inicial");
|
||||
assertEquals(tempDir.toRealPath(), ctx.mainProjectRootPathCanon);
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
package p.studio.compiler.workspaces;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.phases.*;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class StackPhaseTest {
|
||||
|
||||
@Test
|
||||
void stack_topologically_sorts_projects(@TempDir Path tempDir) throws IOException {
|
||||
final Path root = tempDir;
|
||||
final Path depA = root.resolve("depA");
|
||||
Files.createDirectories(depA);
|
||||
Files.createDirectories(root.resolve("src"));
|
||||
Files.createDirectories(depA.resolve("src"));
|
||||
|
||||
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
|
||||
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
|
||||
|
||||
final var cfg = new DependencyConfig(false, root, java.util.List.of());
|
||||
final var ctx = DependencyContext.seed(cfg);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new MaterializePhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new LocalizePhase().run(ctx)));
|
||||
|
||||
final var issues = new StackPhase().run(ctx);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(issues), "Ordenação sem issues");
|
||||
|
||||
assertNotNull(ctx.stack);
|
||||
assertEquals(2, ctx.stack.projects().size());
|
||||
final var depId = ctx.projectIdByDirectoryRoot.get(depA.toRealPath());
|
||||
final var rootId = ctx.root;
|
||||
assertEquals(rootId, ctx.stack.projects().get(0), "Root (sem entradas) vem primeiro pelo algoritmo atual");
|
||||
assertEquals(depId, ctx.stack.projects().get(1));
|
||||
}
|
||||
|
||||
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
|
||||
final StringBuilder deps = new StringBuilder();
|
||||
deps.append("[");
|
||||
for (int i = 0; i < localDeps.length; i++) {
|
||||
if (i > 0) deps.append(",");
|
||||
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
|
||||
}
|
||||
deps.append("]");
|
||||
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
|
||||
Files.writeString(dir.resolve("prometeu.json"), json);
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
package p.studio.compiler.workspaces;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.studio.compiler.messages.DependencyConfig;
|
||||
import p.studio.compiler.models.DependencyContext;
|
||||
import p.studio.compiler.workspaces.phases.*;
|
||||
import p.studio.utilities.structures.ReadOnlyCollection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ValidateAndPolicyPhasesTest {
|
||||
|
||||
@Test
|
||||
void validate_checks_consistency_without_issues(@TempDir Path tempDir) throws IOException {
|
||||
final Path root = tempDir;
|
||||
final Path depA = root.resolve("depA");
|
||||
Files.createDirectories(depA);
|
||||
Files.createDirectories(root.resolve("src"));
|
||||
Files.createDirectories(depA.resolve("src"));
|
||||
|
||||
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
|
||||
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
|
||||
|
||||
final var cfg = new DependencyConfig(false, root, java.util.List.of());
|
||||
final var ctx = DependencyContext.seed(cfg);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new MaterializePhase().run(ctx)));
|
||||
assertTrue(ReadOnlyCollection.isEmpty(new LocalizePhase().run(ctx)));
|
||||
|
||||
final var issues = new ValidatePhase().run(ctx);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(issues), "Validação não deve reportar issues no cenário feliz");
|
||||
}
|
||||
|
||||
@Test
|
||||
void policy_is_noop(@TempDir Path tempDir) {
|
||||
final var cfg = new DependencyConfig(false, tempDir, java.util.List.of());
|
||||
final var ctx = DependencyContext.seed(cfg);
|
||||
final var issues = new PolicyPhase().run(ctx);
|
||||
assertTrue(ReadOnlyCollection.isEmpty(issues), "PolicyPhase atualmente é no-op");
|
||||
}
|
||||
|
||||
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
|
||||
final StringBuilder deps = new StringBuilder();
|
||||
deps.append("[");
|
||||
for (int i = 0; i < localDeps.length; i++) {
|
||||
if (i > 0) deps.append(",");
|
||||
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
|
||||
}
|
||||
deps.append("]");
|
||||
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
|
||||
Files.writeString(dir.resolve("prometeu.json"), json);
|
||||
}
|
||||
}
|
||||
@ -4,5 +4,6 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
api(libs.jackson.databind)
|
||||
api(libs.apache.commons.lang3)
|
||||
api(libs.apache.commons.collections)
|
||||
}
|
||||
@ -5,6 +5,14 @@ import org.slf4j.Logger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public interface LogAggregator {
|
||||
static LogAggregator empty() {
|
||||
return with(s -> {});
|
||||
}
|
||||
|
||||
static LogAggregator stdout() {
|
||||
return with(System.out::println);
|
||||
}
|
||||
|
||||
static LogAggregator with(final Consumer<String> consumer) {
|
||||
return new LogAggregatorImpl(consumer);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user