implements PLN-0041 shipper preparation foundation
This commit is contained in:
parent
6649b11ec4
commit
70b9a183ba
@ -10,10 +10,12 @@ dependencies {
|
|||||||
implementation(project(":prometeu-packer:prometeu-packer-api"))
|
implementation(project(":prometeu-packer:prometeu-packer-api"))
|
||||||
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
implementation(project(":prometeu-compiler:prometeu-compiler-core"))
|
||||||
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
implementation(project(":prometeu-compiler:prometeu-build-pipeline"))
|
||||||
|
implementation(project(":prometeu-compiler:prometeu-frontend-api"))
|
||||||
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
|
implementation(project(":prometeu-compiler:prometeu-frontend-registry"))
|
||||||
implementation(libs.javafx.controls)
|
implementation(libs.javafx.controls)
|
||||||
implementation(libs.javafx.fxml)
|
implementation(libs.javafx.fxml)
|
||||||
implementation(libs.richtextfx)
|
implementation(libs.richtextfx)
|
||||||
|
testImplementation(project(":prometeu-packer:prometeu-packer-v1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx {
|
javafx {
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
package p.studio.shipper;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record StudioShipperLogEntry(
|
||||||
|
StudioShipperLogSource source,
|
||||||
|
StudioShipperLogLevel level,
|
||||||
|
String message,
|
||||||
|
Instant timestamp) {
|
||||||
|
|
||||||
|
public StudioShipperLogEntry {
|
||||||
|
Objects.requireNonNull(source, "source");
|
||||||
|
Objects.requireNonNull(level, "level");
|
||||||
|
message = Objects.requireNonNull(message, "message").trim();
|
||||||
|
timestamp = timestamp == null ? Instant.now() : timestamp;
|
||||||
|
if (message.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("message must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StudioShipperLogEntry info(
|
||||||
|
final StudioShipperLogSource source,
|
||||||
|
final String message) {
|
||||||
|
return new StudioShipperLogEntry(source, StudioShipperLogLevel.INFO, message, Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StudioShipperLogEntry error(
|
||||||
|
final StudioShipperLogSource source,
|
||||||
|
final String message) {
|
||||||
|
return new StudioShipperLogEntry(source, StudioShipperLogLevel.ERROR, message, Instant.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package p.studio.shipper;
|
||||||
|
|
||||||
|
public enum StudioShipperLogLevel {
|
||||||
|
INFO,
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package p.studio.shipper;
|
||||||
|
|
||||||
|
public enum StudioShipperLogSource {
|
||||||
|
BUILD,
|
||||||
|
PACK_VALIDATION,
|
||||||
|
PACK,
|
||||||
|
MANIFEST
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package p.studio.shipper;
|
||||||
|
|
||||||
|
public enum StudioShipperPreparationStatus {
|
||||||
|
SUCCESS,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package p.studio.shipper;
|
||||||
|
|
||||||
|
import p.packer.messages.PackWorkspaceResult;
|
||||||
|
import p.packer.messages.ValidatePackWorkspaceResult;
|
||||||
|
import p.studio.compiler.models.BuildResult;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public record StudioShipperPrepareResult(
|
||||||
|
StudioShipperPreparationStatus status,
|
||||||
|
List<StudioShipperLogEntry> logs,
|
||||||
|
BuildResult buildResult,
|
||||||
|
ValidatePackWorkspaceResult validationResult,
|
||||||
|
PackWorkspaceResult packResult,
|
||||||
|
Path manifestPath) {
|
||||||
|
|
||||||
|
public StudioShipperPrepareResult {
|
||||||
|
Objects.requireNonNull(status, "status");
|
||||||
|
logs = List.copyOf(Objects.requireNonNull(logs, "logs"));
|
||||||
|
manifestPath = manifestPath == null ? null : manifestPath.toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean success() {
|
||||||
|
return status == StudioShipperPreparationStatus.SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,275 @@
|
|||||||
|
package p.studio.shipper;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import p.packer.PackerWorkspaceService;
|
||||||
|
import p.packer.dtos.PackerDiagnosticDTO;
|
||||||
|
import p.packer.dtos.PackerPackValidationAssetDTO;
|
||||||
|
import p.packer.messages.PackWorkspaceRequest;
|
||||||
|
import p.packer.messages.PackWorkspaceResult;
|
||||||
|
import p.packer.messages.PackerOperationStatus;
|
||||||
|
import p.packer.messages.ValidatePackWorkspaceRequest;
|
||||||
|
import p.packer.messages.ValidatePackWorkspaceResult;
|
||||||
|
import p.studio.Container;
|
||||||
|
import p.studio.compiler.messages.BuilderPipelineConfig;
|
||||||
|
import p.studio.compiler.models.BuildResult;
|
||||||
|
import p.studio.compiler.models.BuilderPipelineContext;
|
||||||
|
import p.studio.compiler.workspaces.BuilderPipelineService;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.utilities.logs.LogAggregator;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public final class StudioShipperService {
|
||||||
|
private static final String BUILD_DIR = "build";
|
||||||
|
private static final String MANIFEST_FILE = "manifest.json";
|
||||||
|
private static final String PROJECT_MANIFEST_FILE = "prometeu.json";
|
||||||
|
private static final String ASSET_TABLE_FILE = "asset_table.json";
|
||||||
|
private static final String PRELOAD_FILE = "preload.json";
|
||||||
|
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||||
|
private final PackerWorkspaceService packerWorkspaceService;
|
||||||
|
|
||||||
|
public StudioShipperService() {
|
||||||
|
this(Container.packer().workspaceService());
|
||||||
|
}
|
||||||
|
|
||||||
|
public StudioShipperService(final PackerWorkspaceService packerWorkspaceService) {
|
||||||
|
this.packerWorkspaceService = Objects.requireNonNull(packerWorkspaceService, "packerWorkspaceService");
|
||||||
|
}
|
||||||
|
|
||||||
|
public StudioShipperPrepareResult prepare(final ProjectReference projectReference) {
|
||||||
|
return prepare(projectReference, ignored -> { });
|
||||||
|
}
|
||||||
|
|
||||||
|
public StudioShipperPrepareResult prepare(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final Consumer<StudioShipperLogEntry> logSink) {
|
||||||
|
final ProjectReference safeProjectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||||
|
final Consumer<StudioShipperLogEntry> safeLogSink = Objects.requireNonNull(logSink, "logSink");
|
||||||
|
final ArrayList<StudioShipperLogEntry> logs = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final BuildResult buildResult = build(safeProjectReference, logs, safeLogSink);
|
||||||
|
final ValidatePackWorkspaceResult validationResult = validatePack(safeProjectReference, logs, safeLogSink);
|
||||||
|
if (!validationResult.canPack()) {
|
||||||
|
emit(logs, safeLogSink, StudioShipperLogEntry.error(
|
||||||
|
StudioShipperLogSource.PACK_VALIDATION,
|
||||||
|
validationResult.summary()));
|
||||||
|
logBlockedAssets(validationResult.assets(), logs, safeLogSink);
|
||||||
|
return new StudioShipperPrepareResult(
|
||||||
|
StudioShipperPreparationStatus.FAILED,
|
||||||
|
logs,
|
||||||
|
buildResult,
|
||||||
|
validationResult,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final PackWorkspaceResult packResult = pack(safeProjectReference, logs, safeLogSink);
|
||||||
|
if (packResult.status() != PackerOperationStatus.SUCCESS) {
|
||||||
|
emit(logs, safeLogSink, logFor(packResult.status(), StudioShipperLogSource.PACK, packResult.summary()));
|
||||||
|
return new StudioShipperPrepareResult(
|
||||||
|
StudioShipperPreparationStatus.FAILED,
|
||||||
|
logs,
|
||||||
|
buildResult,
|
||||||
|
validationResult,
|
||||||
|
packResult,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Path manifestPath = writeManifest(safeProjectReference, buildResult, logs, safeLogSink);
|
||||||
|
return new StudioShipperPrepareResult(
|
||||||
|
StudioShipperPreparationStatus.SUCCESS,
|
||||||
|
logs,
|
||||||
|
buildResult,
|
||||||
|
validationResult,
|
||||||
|
packResult,
|
||||||
|
manifestPath);
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
emit(logs, safeLogSink, StudioShipperLogEntry.error(
|
||||||
|
StudioShipperLogSource.BUILD,
|
||||||
|
runtimeException.getMessage() == null ? runtimeException.getClass().getSimpleName() : runtimeException.getMessage()));
|
||||||
|
return new StudioShipperPrepareResult(
|
||||||
|
StudioShipperPreparationStatus.FAILED,
|
||||||
|
logs,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BuildResult build(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final List<StudioShipperLogEntry> logs,
|
||||||
|
final Consumer<StudioShipperLogEntry> logSink) {
|
||||||
|
final var config = new BuilderPipelineConfig(false, projectReference.rootPath().toString());
|
||||||
|
final var ctx = BuilderPipelineContext.fromConfig(config);
|
||||||
|
final var logAggregator = LogAggregator.with(message -> emit(logs, logSink, StudioShipperLogEntry.info(
|
||||||
|
StudioShipperLogSource.BUILD,
|
||||||
|
normalizeMessage(message))));
|
||||||
|
final BuildResult result = BuilderPipelineService.INSTANCE.build(ctx, logAggregator);
|
||||||
|
if (result.bytecodeArtifactPath() != null) {
|
||||||
|
emit(logs, logSink, StudioShipperLogEntry.info(
|
||||||
|
StudioShipperLogSource.BUILD,
|
||||||
|
"Prepared program bytecode at " + result.bytecodeArtifactPath().toAbsolutePath().normalize()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidatePackWorkspaceResult validatePack(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final List<StudioShipperLogEntry> logs,
|
||||||
|
final Consumer<StudioShipperLogEntry> logSink) {
|
||||||
|
final ValidatePackWorkspaceResult result = packerWorkspaceService
|
||||||
|
.validatePackWorkspace(new ValidatePackWorkspaceRequest(projectReference.toPackerProjectContext()));
|
||||||
|
// keep the validation source explicit even when the downstream packer reports partial status
|
||||||
|
emit(logs, logSink, logFor(result.status(), StudioShipperLogSource.PACK_VALIDATION, result.summary()));
|
||||||
|
if (!result.canPack()) {
|
||||||
|
logBlockedAssets(result.assets(), logs, logSink);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PackWorkspaceResult pack(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final List<StudioShipperLogEntry> logs,
|
||||||
|
final Consumer<StudioShipperLogEntry> logSink) {
|
||||||
|
final PackWorkspaceResult result = packerWorkspaceService
|
||||||
|
.packWorkspace(new PackWorkspaceRequest(projectReference.toPackerProjectContext()));
|
||||||
|
emit(logs, logSink, logFor(result.status(), StudioShipperLogSource.PACK, result.summary()));
|
||||||
|
result.result().emittedArtifacts().forEach(artifact -> emit(logs, logSink, StudioShipperLogEntry.info(
|
||||||
|
StudioShipperLogSource.PACK,
|
||||||
|
"Emitted " + artifact.label() + " at " + artifact.path())));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path writeManifest(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final BuildResult buildResult,
|
||||||
|
final List<StudioShipperLogEntry> logs,
|
||||||
|
final Consumer<StudioShipperLogEntry> logSink) {
|
||||||
|
final Path buildRoot = projectReference.rootPath().resolve(BUILD_DIR).toAbsolutePath().normalize();
|
||||||
|
final Path manifestPath = buildRoot.resolve(MANIFEST_FILE);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(buildRoot);
|
||||||
|
final ObjectNode manifest = createManifest(projectReference, buildResult, buildRoot);
|
||||||
|
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||||
|
emit(logs, logSink, StudioShipperLogEntry.info(
|
||||||
|
StudioShipperLogSource.MANIFEST,
|
||||||
|
"Generated manifest at " + manifestPath));
|
||||||
|
return manifestPath;
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
emit(logs, logSink, StudioShipperLogEntry.error(
|
||||||
|
StudioShipperLogSource.MANIFEST,
|
||||||
|
"Failed to write manifest: " + manifestPath));
|
||||||
|
throw new RuntimeException("failed to write build manifest: " + manifestPath, ioException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectNode createManifest(
|
||||||
|
final ProjectReference projectReference,
|
||||||
|
final BuildResult buildResult,
|
||||||
|
final Path buildRoot) throws IOException {
|
||||||
|
final ObjectNode manifest = MAPPER.createObjectNode();
|
||||||
|
final JsonNode projectManifest = MAPPER.readTree(projectReference.rootPath().resolve(PROJECT_MANIFEST_FILE).toFile());
|
||||||
|
manifest.put("magic", "PMTU");
|
||||||
|
manifest.put("cartridge_version", 1);
|
||||||
|
manifest.put("app_id", stableAppId(projectReference));
|
||||||
|
manifest.put("title", projectReference.name());
|
||||||
|
manifest.put("app_version", projectReference.version());
|
||||||
|
manifest.put("app_mode", "game");
|
||||||
|
manifest.set("capabilities", capabilitiesNode(buildResult));
|
||||||
|
manifest.set("asset_table", readArrayOrEmpty(buildRoot.resolve(ASSET_TABLE_FILE)));
|
||||||
|
manifest.set("preload", readArrayOrEmpty(buildRoot.resolve(PRELOAD_FILE)));
|
||||||
|
if (projectManifest != null) {
|
||||||
|
putIfTextual(manifest, "project_name", projectManifest.get("name"));
|
||||||
|
putIfTextual(manifest, "language", projectManifest.get("language"));
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayNode capabilitiesNode(final BuildResult buildResult) {
|
||||||
|
final ArrayNode capabilities = MAPPER.createArrayNode();
|
||||||
|
final var irBackend = buildResult.compileResult().analysisSnapshot().irBackend();
|
||||||
|
final LinkedHashSet<String> distinct = new LinkedHashSet<>(irBackend.getReservedMetadata().requiredCapabilities().asList());
|
||||||
|
distinct.forEach(capabilities::add);
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode readArrayOrEmpty(final Path path) throws IOException {
|
||||||
|
if (!Files.isRegularFile(path)) {
|
||||||
|
return MAPPER.createArrayNode();
|
||||||
|
}
|
||||||
|
final JsonNode loaded = MAPPER.readTree(path.toFile());
|
||||||
|
return loaded != null && loaded.isArray() ? loaded : MAPPER.createArrayNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int stableAppId(final ProjectReference projectReference) {
|
||||||
|
final int hash = Objects.hash(
|
||||||
|
projectReference.name(),
|
||||||
|
projectReference.version(),
|
||||||
|
projectReference.languageId(),
|
||||||
|
projectReference.stdlibVersion());
|
||||||
|
return hash == Integer.MIN_VALUE ? 1 : Math.max(1, Math.abs(hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putIfTextual(
|
||||||
|
final ObjectNode node,
|
||||||
|
final String fieldName,
|
||||||
|
final JsonNode value) {
|
||||||
|
if (value != null && value.isTextual() && !value.asText().isBlank()) {
|
||||||
|
node.put(fieldName, value.asText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logBlockedAssets(
|
||||||
|
final List<PackerPackValidationAssetDTO> assets,
|
||||||
|
final List<StudioShipperLogEntry> logs,
|
||||||
|
final Consumer<StudioShipperLogEntry> logSink) {
|
||||||
|
for (final PackerPackValidationAssetDTO asset : assets) {
|
||||||
|
emit(logs, logSink, StudioShipperLogEntry.error(
|
||||||
|
StudioShipperLogSource.PACK_VALIDATION,
|
||||||
|
"Blocked asset #" + asset.assetId() + " (" + asset.assetName() + ")"));
|
||||||
|
for (final PackerDiagnosticDTO diagnostic : asset.diagnostics()) {
|
||||||
|
emit(logs, logSink, StudioShipperLogEntry.error(
|
||||||
|
StudioShipperLogSource.PACK_VALIDATION,
|
||||||
|
diagnostic.message()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StudioShipperLogEntry logFor(
|
||||||
|
final PackerOperationStatus status,
|
||||||
|
final StudioShipperLogSource source,
|
||||||
|
final String message) {
|
||||||
|
return status == PackerOperationStatus.FAILED || status == PackerOperationStatus.PARTIAL
|
||||||
|
? StudioShipperLogEntry.error(source, message)
|
||||||
|
: StudioShipperLogEntry.info(source, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emit(
|
||||||
|
final List<StudioShipperLogEntry> logs,
|
||||||
|
final Consumer<StudioShipperLogEntry> logSink,
|
||||||
|
final StudioShipperLogEntry entry) {
|
||||||
|
logs.add(entry);
|
||||||
|
logSink.accept(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeMessage(final String message) {
|
||||||
|
if (message == null) {
|
||||||
|
return "(empty build log entry)";
|
||||||
|
}
|
||||||
|
final String trimmed = message.strip();
|
||||||
|
return trimmed.isEmpty() ? "(empty build log entry)" : trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,12 +6,9 @@ import javafx.scene.control.*;
|
|||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import p.studio.Container;
|
import p.studio.Container;
|
||||||
import p.studio.compiler.messages.BuilderPipelineConfig;
|
|
||||||
import p.studio.compiler.models.BuilderPipelineContext;
|
|
||||||
import p.studio.compiler.workspaces.BuilderPipelineService;
|
|
||||||
import p.studio.projects.ProjectReference;
|
import p.studio.projects.ProjectReference;
|
||||||
|
import p.studio.shipper.StudioShipperService;
|
||||||
import p.studio.utilities.i18n.I18n;
|
import p.studio.utilities.i18n.I18n;
|
||||||
import p.studio.utilities.logs.LogAggregator;
|
|
||||||
import p.studio.workspaces.Workspace;
|
import p.studio.workspaces.Workspace;
|
||||||
import p.studio.workspaces.WorkspaceId;
|
import p.studio.workspaces.WorkspaceId;
|
||||||
|
|
||||||
@ -20,6 +17,7 @@ public class ShipperWorkspace extends Workspace {
|
|||||||
private final TextArea logs = new TextArea();
|
private final TextArea logs = new TextArea();
|
||||||
private final Button buildButton = new Button();
|
private final Button buildButton = new Button();
|
||||||
private final Button clearButton = new Button();
|
private final Button clearButton = new Button();
|
||||||
|
private final StudioShipperService shipperService = new StudioShipperService();
|
||||||
|
|
||||||
public ShipperWorkspace(ProjectReference projectReference) {
|
public ShipperWorkspace(ProjectReference projectReference) {
|
||||||
super(projectReference);
|
super(projectReference);
|
||||||
@ -66,10 +64,8 @@ public class ShipperWorkspace extends Workspace {
|
|||||||
buildButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
buildButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||||
buildButton.setOnAction(e -> {
|
buildButton.setOnAction(e -> {
|
||||||
logs.clear();
|
logs.clear();
|
||||||
final var logAggregator = LogAggregator.with(logs::appendText);
|
shipperService.prepare(projectReference, entry -> logs.appendText(
|
||||||
final var config = new BuilderPipelineConfig(false, projectReference.rootPath().toString());
|
"[%s] %s%s".formatted(entry.source().name(), entry.message(), System.lineSeparator())));
|
||||||
final var ctx = BuilderPipelineContext.fromConfig(config);
|
|
||||||
BuilderPipelineService.INSTANCE.build(ctx, logAggregator);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
clearButton.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_SHIPPER_BUTTON_CLEAR));
|
clearButton.textProperty().bind(Container.i18n().bind(I18n.WORKSPACE_SHIPPER_BUTTON_CLEAR));
|
||||||
|
|||||||
@ -0,0 +1,99 @@
|
|||||||
|
package p.studio.shipper;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import p.packer.Packer;
|
||||||
|
import p.studio.projects.ProjectCatalogService;
|
||||||
|
import p.studio.projects.ProjectReference;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
final class StudioShipperServiceTest {
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void prepareBuildsPackAndManifestForMainFixture() throws Exception {
|
||||||
|
final Path projectRoot = copyFixture(fixturePath("test-projects/main"), tempDir.resolve("main-project"));
|
||||||
|
final ProjectReference projectReference = new ProjectCatalogService(tempDir).openProject(projectRoot);
|
||||||
|
final ArrayList<StudioShipperLogEntry> streamedLogs = new ArrayList<>();
|
||||||
|
|
||||||
|
final StudioShipperPrepareResult result;
|
||||||
|
try (final Packer packer = Packer.bootstrap(new ObjectMapper(), ignored -> { })) {
|
||||||
|
final StudioShipperService service = new StudioShipperService(packer.workspaceService());
|
||||||
|
result = service.prepare(projectReference, streamedLogs::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
assertEquals(StudioShipperPreparationStatus.SUCCESS, result.status());
|
||||||
|
assertNotNull(result.buildResult());
|
||||||
|
assertNotNull(result.validationResult());
|
||||||
|
assertNotNull(result.packResult());
|
||||||
|
assertNotNull(result.manifestPath());
|
||||||
|
assertEquals(result.logs(), streamedLogs);
|
||||||
|
assertTrue(Files.isRegularFile(projectRoot.resolve("build").resolve("program.pbx")));
|
||||||
|
assertTrue(Files.isRegularFile(projectRoot.resolve("build").resolve("assets.pa")));
|
||||||
|
assertTrue(Files.isRegularFile(projectRoot.resolve("build").resolve("manifest.json")));
|
||||||
|
assertTrue(result.logs().stream().anyMatch(entry -> entry.source() == StudioShipperLogSource.BUILD));
|
||||||
|
assertTrue(result.logs().stream().anyMatch(entry -> entry.source() == StudioShipperLogSource.PACK_VALIDATION));
|
||||||
|
assertTrue(result.logs().stream().anyMatch(entry -> entry.source() == StudioShipperLogSource.PACK));
|
||||||
|
assertTrue(result.logs().stream().anyMatch(entry -> entry.source() == StudioShipperLogSource.MANIFEST));
|
||||||
|
final String manifestJson = Files.readString(result.manifestPath());
|
||||||
|
assertTrue(manifestJson.contains("\"magic\""));
|
||||||
|
assertTrue(manifestJson.contains("\"capabilities\""));
|
||||||
|
assertTrue(manifestJson.contains("\"asset_table\""));
|
||||||
|
assertTrue(manifestJson.contains("\"preload\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void prepareManifestPreservesCompilerCapabilities() throws Exception {
|
||||||
|
final Path projectRoot = copyFixture(fixturePath("test-projects/main"), tempDir.resolve("main-capabilities"));
|
||||||
|
final ProjectReference projectReference = new ProjectCatalogService(tempDir).openProject(projectRoot);
|
||||||
|
final StudioShipperPrepareResult result;
|
||||||
|
try (final Packer packer = Packer.bootstrap(new ObjectMapper(), ignored -> { })) {
|
||||||
|
final StudioShipperService service = new StudioShipperService(packer.workspaceService());
|
||||||
|
result = service.prepare(projectReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(result.success());
|
||||||
|
final String manifestJson = Files.readString(result.manifestPath());
|
||||||
|
assertTrue(manifestJson.contains("\"log\""));
|
||||||
|
assertTrue(manifestJson.contains("\"gfx\""));
|
||||||
|
assertTrue(manifestJson.contains("\"asset\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path copyFixture(final Path sourceRoot, final Path targetRoot) throws IOException {
|
||||||
|
Files.walk(sourceRoot).forEach(source -> {
|
||||||
|
try {
|
||||||
|
final Path target = targetRoot.resolve(sourceRoot.relativize(source).toString());
|
||||||
|
if (Files.isDirectory(source)) {
|
||||||
|
Files.createDirectories(target);
|
||||||
|
} else {
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
Files.copy(source, target);
|
||||||
|
}
|
||||||
|
} catch (IOException ioException) {
|
||||||
|
throw new RuntimeException(ioException);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return targetRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path fixturePath(final String relativePath) {
|
||||||
|
final Path direct = Path.of(relativePath).toAbsolutePath().normalize();
|
||||||
|
if (Files.exists(direct)) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
final Path parent = Path.of("..").resolve(relativePath).toAbsolutePath().normalize();
|
||||||
|
if (Files.exists(parent)) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("fixture not found: " + relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user