MetaCore/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp
2026-07-16 09:01:25 +08:00

763 lines
44 KiB
C++

#include "MetaCoreDelivery/MetaCoreBuildPipeline.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include "MetaCoreFoundation/MetaCorePackage.h"
#include "MetaCoreFoundation/MetaCoreProject.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScripting/MetaCoreScripting.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <set>
#include <sstream>
#include <system_error>
#if defined(__linux__)
#include <sys/utsname.h>
#endif
namespace MetaCore {
namespace {
using json = nlohmann::json;
[[nodiscard]] std::string ConfigurationName(MetaCoreBuildConfiguration configuration) {
switch (configuration) {
case MetaCoreBuildConfiguration::Debug: return "Debug";
case MetaCoreBuildConfiguration::Development: return "Development";
case MetaCoreBuildConfiguration::Release: return "Release";
}
return "Development";
}
[[nodiscard]] std::string CMakeConfigurationName(MetaCoreBuildConfiguration configuration) {
switch (configuration) {
case MetaCoreBuildConfiguration::Debug: return "Debug";
case MetaCoreBuildConfiguration::Development: return "RelWithDebInfo";
case MetaCoreBuildConfiguration::Release: return "Release";
}
return "RelWithDebInfo";
}
[[nodiscard]] bool IsSafeRelativePath(const std::filesystem::path& path) {
if (path.empty() || path.is_absolute()) return false;
const auto normalized = path.lexically_normal();
return normalized != ".." && (normalized.empty() || *normalized.begin() != "..");
}
[[nodiscard]] bool IsSafeIdentifier(std::string_view value) {
if (value.empty()) return false;
return std::all_of(value.begin(), value.end(), [](unsigned char character) {
return std::isalnum(character) != 0 || character == '-' || character == '_' || character == '.';
});
}
[[nodiscard]] std::string ShellQuote(const std::filesystem::path& path) {
std::string result = "'";
for (const char character : path.string()) {
result += character == '\'' ? "'\\''" : std::string(1, character);
}
result += "'";
return result;
}
[[nodiscard]] bool RunCommand(const std::string& command) {
return std::system(command.c_str()) == 0;
}
[[nodiscard]] std::optional<json> ReadJson(const std::filesystem::path& path) {
try {
std::ifstream input(path);
if (!input.is_open()) return std::nullopt;
json value;
input >> value;
return value;
} catch (...) {
return std::nullopt;
}
}
[[nodiscard]] bool WriteJson(const std::filesystem::path& path, const json& value) {
std::error_code error;
std::filesystem::create_directories(path.parent_path(), error);
if (error) return false;
std::ofstream output(path, std::ios::trunc);
if (!output.is_open()) return false;
output << value.dump(2) << '\n';
return output.good();
}
void SetIssue(MetaCoreBuildIssue* issue, MetaCoreBuildErrorCode code, std::string message, const std::filesystem::path& path = {}) {
if (issue == nullptr) return;
issue->Code = code;
issue->Message = std::move(message);
issue->Path = path;
}
[[nodiscard]] std::string MakeBuildId() {
const auto now = std::chrono::system_clock::now();
const std::time_t time = std::chrono::system_clock::to_time_t(now);
std::tm utc{};
#if defined(_WIN32)
gmtime_s(&utc, &time);
#else
gmtime_r(&time, &utc);
#endif
const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() % 1000;
std::ostringstream output;
output << std::put_time(&utc, "%Y%m%dT%H%M%S") << std::setw(3) << std::setfill('0') << milliseconds << "Z";
return output.str();
}
[[nodiscard]] std::filesystem::path FindSourceRoot(std::filesystem::path start) {
std::error_code error;
start = std::filesystem::absolute(start, error);
while (!start.empty()) {
if (std::filesystem::exists(start / "CMakeLists.txt", error) && std::filesystem::exists(start / "Source", error)) return start;
const auto parent = start.parent_path();
if (parent == start) break;
start = parent;
}
return {};
}
[[nodiscard]] bool CopyTree(const std::filesystem::path& source, const std::filesystem::path& target) {
std::error_code error;
if (!std::filesystem::is_directory(source, error)) return false;
std::filesystem::create_directories(target, error);
if (error) return false;
for (std::filesystem::recursive_directory_iterator iterator(source, error), end; iterator != end; iterator.increment(error)) {
if (error) return false;
const auto relative = iterator->path().lexically_relative(source);
if (!IsSafeRelativePath(relative)) return false;
const auto destination = target / relative;
if (iterator->is_directory(error)) {
std::filesystem::create_directories(destination, error);
} else if (iterator->is_regular_file(error)) {
std::filesystem::create_directories(destination.parent_path(), error);
std::filesystem::copy_file(iterator->path(), destination, std::filesystem::copy_options::overwrite_existing, error);
}
if (error) return false;
}
return true;
}
[[nodiscard]] std::filesystem::path FindPlayer(const std::filesystem::path& buildRoot, MetaCoreBuildConfiguration configuration) {
const std::array candidates = {
buildRoot / "MetaCorePlayer",
buildRoot / CMakeConfigurationName(configuration) / "MetaCorePlayer",
buildRoot / "MetaCorePlayer.exe",
buildRoot / CMakeConfigurationName(configuration) / "MetaCorePlayer.exe"
};
for (const auto& candidate : candidates) if (std::filesystem::is_regular_file(candidate)) return candidate;
return {};
}
[[nodiscard]] std::string DetectGitCommit(const std::filesystem::path& sourceRoot) {
const auto head = ReadJson(sourceRoot / ".metacore-build-metadata.json");
if (head && head->contains("git_commit")) return head->value("git_commit", "unknown");
std::ifstream input(sourceRoot / ".git" / "HEAD");
std::string value;
std::getline(input, value);
if (value.rfind("ref: ", 0) == 0) {
std::ifstream reference(sourceRoot / ".git" / value.substr(5));
std::getline(reference, value);
}
return value.empty() ? "unknown" : value;
}
[[nodiscard]] std::string RoleForPath(const std::filesystem::path& path) {
const std::string value = path.generic_string();
if (value == "MetaCorePlayer" || value == "MetaCorePlayer.exe") return "player";
if (value.rfind("lib/", 0) == 0) return "runtime_library";
if (value.rfind("Engine/Materials/", 0) == 0) return "engine_material";
if (value.rfind("Engine/Fonts/", 0) == 0) return "font";
if (value.rfind("Content/Scenes/", 0) == 0) return "scene";
if (value.rfind("Content/Ui/", 0) == 0) return "ui";
if (value.rfind("Content/Runtime/", 0) == 0) return "runtime_config";
if (value.rfind("Content/Assets/", 0) == 0) return "asset";
if (value == "Project/MetaCore.runtimeproject") return "runtime_project";
return "content";
}
[[nodiscard]] std::vector<MetaCoreReleaseFileEntry> CollectManifestFiles(const std::filesystem::path& root) {
std::vector<MetaCoreReleaseFileEntry> files;
std::error_code error;
for (std::filesystem::recursive_directory_iterator iterator(root, error), end; iterator != end; iterator.increment(error)) {
if (error) break;
if (!iterator->is_regular_file(error)) continue;
const auto relative = iterator->path().lexically_relative(root);
if (relative.generic_string().rfind("Manifest/", 0) == 0 || relative.generic_string().rfind("Diagnostics/", 0) == 0) continue;
const auto hash = MetaCoreSha256File(iterator->path());
if (!hash) continue;
files.push_back({relative, RoleForPath(relative), iterator->file_size(error), *hash, true});
}
std::sort(files.begin(), files.end(), [](const auto& left, const auto& right) {
return left.Path.generic_string() < right.Path.generic_string();
});
return files;
}
[[nodiscard]] bool CopyLinuxRuntimeLibraries(const std::filesystem::path& player, const std::filesystem::path& libraryRoot) {
#if defined(__linux__)
const std::string command = "ldd " + ShellQuote(player);
FILE* pipe = popen(command.c_str(), "r");
if (pipe == nullptr) return false;
std::set<std::filesystem::path> libraries;
bool dependencyMissing = false;
std::array<char, 4096> buffer{};
while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
const std::string line(buffer.data());
if (line.find("=> not found") != std::string::npos) dependencyMissing = true;
const std::size_t arrow = line.find("=> /");
if (arrow == std::string::npos) continue;
const std::size_t begin = arrow + 3U;
const std::size_t end = line.find(' ', begin);
const std::filesystem::path dependency = line.substr(begin, end - begin);
const std::string filename = dependency.filename().string();
if (filename.rfind("libc++", 0) == 0 || filename.rfind("libunwind", 0) == 0) libraries.insert(dependency);
}
const int status = pclose(pipe);
if (status != 0 || dependencyMissing) return false;
std::error_code error;
std::filesystem::create_directories(libraryRoot, error);
for (const auto& library : libraries) {
const auto canonical = std::filesystem::canonical(library, error);
if (error) return false;
std::filesystem::copy_file(canonical, libraryRoot / library.filename(), std::filesystem::copy_options::overwrite_existing, error);
if (error) return false;
}
#else
(void)player;
(void)libraryRoot;
#endif
return true;
}
[[nodiscard]] bool CreateArchive(const std::filesystem::path& archive, const std::filesystem::path& directory) {
const std::string command = "cd " + ShellQuote(directory.parent_path()) + " && cmake -E tar czf " +
ShellQuote(std::filesystem::absolute(archive)) + " -- " + ShellQuote(directory.filename());
return RunCommand(command);
}
} // namespace
const char* MetaCoreBuildErrorCodeName(MetaCoreBuildErrorCode code) {
switch (code) {
case MetaCoreBuildErrorCode::None: return "MCB0000";
case MetaCoreBuildErrorCode::InvalidArguments: return "MCB1001";
case MetaCoreBuildErrorCode::InvalidProfile: return "MCB1002";
case MetaCoreBuildErrorCode::UnsupportedPlatform: return "MCB1003";
case MetaCoreBuildErrorCode::UnsafePath: return "MCB1004";
case MetaCoreBuildErrorCode::ProjectUnreadable: return "MCB1005";
case MetaCoreBuildErrorCode::BuildFailed: return "MCB2001";
case MetaCoreBuildErrorCode::CookFailed: return "MCB3001";
case MetaCoreBuildErrorCode::StageFailed: return "MCB4001";
case MetaCoreBuildErrorCode::PackageFailed: return "MCB5001";
case MetaCoreBuildErrorCode::ManifestUnreadable: return "MCB6001";
case MetaCoreBuildErrorCode::ManifestVersionUnsupported: return "MCB6002";
case MetaCoreBuildErrorCode::RuntimeAbiIncompatible: return "MCB6003";
case MetaCoreBuildErrorCode::RequiredFileMissing: return "MCB6004";
case MetaCoreBuildErrorCode::FileSizeMismatch: return "MCB6005";
case MetaCoreBuildErrorCode::FileHashMismatch: return "MCB6006";
case MetaCoreBuildErrorCode::DependencyMissing: return "MCB6007";
case MetaCoreBuildErrorCode::Cancelled: return "MCB9001";
}
return "MCB9999";
}
const char* MetaCoreBuildStageName(MetaCoreBuildStage stage) {
switch (stage) {
case MetaCoreBuildStage::Build: return "Build";
case MetaCoreBuildStage::Cook: return "Cook";
case MetaCoreBuildStage::Stage: return "Stage";
case MetaCoreBuildStage::Package: return "Package";
case MetaCoreBuildStage::Validate: return "Validate";
}
return "Unknown";
}
std::optional<MetaCoreBuildProfile> MetaCoreReadBuildProfile(const std::filesystem::path& path, MetaCoreBuildIssue* issue) {
const auto value = ReadJson(path);
if (!value) {
SetIssue(issue, MetaCoreBuildErrorCode::InvalidProfile, "Build profile is missing or invalid JSON", path);
return std::nullopt;
}
MetaCoreBuildProfile profile;
profile.FormatVersion = value->value("format_version", 0U);
profile.Name = value->value("name", "");
profile.TargetPlatform = value->value("target_platform", "");
profile.GraphicsBackend = value->value("graphics_backend", "");
profile.StartupScene = value->value("startup_scene", "");
profile.OutputDirectory = value->value("output_directory", "Build");
const std::string configuration = value->value("configuration", "Development");
if (configuration == "Debug") profile.Configuration = MetaCoreBuildConfiguration::Debug;
else if (configuration == "Release") profile.Configuration = MetaCoreBuildConfiguration::Release;
else if (configuration == "Development") profile.Configuration = MetaCoreBuildConfiguration::Development;
else {
SetIssue(issue, MetaCoreBuildErrorCode::InvalidProfile, "Unknown build configuration", path);
return std::nullopt;
}
const std::string policy = value->value("resource_policy", "ReferencedOnly");
if (policy == "AllProjectContent") profile.ResourcePolicy = MetaCoreBuildResourcePolicy::AllProjectContent;
else if (policy != "ReferencedOnly") {
SetIssue(issue, MetaCoreBuildErrorCode::InvalidProfile, "Unknown resource policy", path);
return std::nullopt;
}
if (profile.FormatVersion != 1U || !IsSafeIdentifier(profile.Name) || !IsSafeRelativePath(profile.OutputDirectory) ||
(!profile.StartupScene.empty() && !IsSafeRelativePath(profile.StartupScene))) {
SetIssue(issue, MetaCoreBuildErrorCode::UnsafePath, "Build profile contains an unsupported version or unsafe path", path);
return std::nullopt;
}
return profile;
}
bool MetaCoreWriteBuildProfile(const std::filesystem::path& path, const MetaCoreBuildProfile& profile) {
return WriteJson(path, {
{"format_version", profile.FormatVersion}, {"name", profile.Name}, {"target_platform", profile.TargetPlatform},
{"configuration", ConfigurationName(profile.Configuration)}, {"graphics_backend", profile.GraphicsBackend},
{"startup_scene", profile.StartupScene.generic_string()}, {"output_directory", profile.OutputDirectory.generic_string()},
{"resource_policy", profile.ResourcePolicy == MetaCoreBuildResourcePolicy::ReferencedOnly ? "ReferencedOnly" : "AllProjectContent"}
});
}
std::optional<MetaCoreRuntimeDeliveryDocument> MetaCoreReadRuntimeDeliveryDocument(const std::filesystem::path& path, MetaCoreBuildIssue* issue) {
const auto value = ReadJson(path);
if (!value) {
SetIssue(issue, MetaCoreBuildErrorCode::ManifestUnreadable, "Runtime delivery document is missing or invalid", path);
return std::nullopt;
}
MetaCoreRuntimeDeliveryDocument document;
document.FormatVersion = value->value("format_version", 0U);
document.RuntimeAbiMajor = value->value("runtime_abi_major", 0U);
document.RuntimeAbiMinor = value->value("runtime_abi_minor", 0U);
document.BuildId = value->value("build_id", "");
document.ContentRoot = value->value("content_root", "");
document.StartupScene = value->value("startup_scene", "");
document.RuntimeConfig = value->value("runtime_config", "");
document.UiManifest = value->value("ui_manifest", "");
document.ReleaseManifest = value->value("release_manifest", "");
if (value->contains("script_modules") && (*value)["script_modules"].is_array()) {
for (const auto& module : (*value)["script_modules"]) {
if (module.is_string()) document.ScriptModules.emplace_back(module.get<std::string>());
}
}
if (document.FormatVersion != GMetaCoreRuntimeDeliveryVersion) {
SetIssue(issue, MetaCoreBuildErrorCode::ManifestVersionUnsupported, "Runtime delivery format is unsupported", path);
return std::nullopt;
}
if (document.RuntimeAbiMajor != GMetaCoreRuntimeAbiMajor) {
SetIssue(issue, MetaCoreBuildErrorCode::RuntimeAbiIncompatible, "Runtime ABI major is incompatible", path);
return std::nullopt;
}
for (const auto& relative : {document.ContentRoot, document.StartupScene, document.RuntimeConfig, document.UiManifest, document.ReleaseManifest}) {
if (!relative.empty() && !IsSafeRelativePath(relative)) {
SetIssue(issue, MetaCoreBuildErrorCode::UnsafePath, "Runtime delivery document contains an unsafe path", path);
return std::nullopt;
}
}
for (const auto& relative : document.ScriptModules) {
if (!IsSafeRelativePath(relative)) {
SetIssue(issue, MetaCoreBuildErrorCode::UnsafePath, "Runtime delivery document contains an unsafe script module path", path);
return std::nullopt;
}
}
return document;
}
bool MetaCoreWriteRuntimeDeliveryDocument(const std::filesystem::path& path, const MetaCoreRuntimeDeliveryDocument& document) {
json modules = json::array();
for (const auto& module : document.ScriptModules) modules.push_back(module.generic_string());
return WriteJson(path, {
{"format_version", document.FormatVersion}, {"runtime_abi_major", document.RuntimeAbiMajor},
{"runtime_abi_minor", document.RuntimeAbiMinor}, {"build_id", document.BuildId},
{"content_root", document.ContentRoot.generic_string()}, {"startup_scene", document.StartupScene.generic_string()},
{"runtime_config", document.RuntimeConfig.generic_string()}, {"ui_manifest", document.UiManifest.generic_string()},
{"release_manifest", document.ReleaseManifest.generic_string()}, {"script_modules", std::move(modules)}
});
}
std::optional<MetaCoreReleaseManifestDocument> MetaCoreReadReleaseManifest(const std::filesystem::path& path, MetaCoreBuildIssue* issue) {
const auto value = ReadJson(path);
if (!value) {
SetIssue(issue, MetaCoreBuildErrorCode::ManifestUnreadable, "Release manifest is missing or invalid", path);
return std::nullopt;
}
MetaCoreReleaseManifestDocument document;
document.FormatVersion = value->value("format_version", 0U);
document.RuntimeAbiMajor = value->value("runtime_abi_major", 0U);
document.RuntimeAbiMinor = value->value("runtime_abi_minor", 0U);
document.PackageFormatVersion = value->value("package_format_version", 0U);
document.EngineVersion = value->value("engine_version", "");
document.ProjectName = value->value("project_name", "");
document.ProjectVersion = value->value("project_version", "");
document.BuildId = value->value("build_id", "");
document.GitCommit = value->value("git_commit", "");
document.TargetPlatform = value->value("target_platform", "");
document.Configuration = value->value("configuration", "");
document.GraphicsBackend = value->value("graphics_backend", "");
document.SystemDependencies = value->value("system_dependencies", std::vector<std::string>{});
if (value->contains("files") && (*value)["files"].is_array()) {
for (const auto& file : (*value)["files"]) {
MetaCoreReleaseFileEntry entry;
entry.Path = file.value("path", ""); entry.Role = file.value("role", "");
entry.Size = file.value("size", 0ULL); entry.Sha256 = file.value("sha256", ""); entry.Required = file.value("required", true);
document.Files.push_back(std::move(entry));
}
}
if (document.FormatVersion != GMetaCoreReleaseManifestVersion) {
SetIssue(issue, MetaCoreBuildErrorCode::ManifestVersionUnsupported, "Release manifest format is unsupported", path);
return std::nullopt;
}
if (document.RuntimeAbiMajor != GMetaCoreRuntimeAbiMajor) {
SetIssue(issue, MetaCoreBuildErrorCode::RuntimeAbiIncompatible, "Release runtime ABI major is incompatible", path);
return std::nullopt;
}
return document;
}
bool MetaCoreWriteReleaseManifest(const std::filesystem::path& path, const MetaCoreReleaseManifestDocument& document) {
json files = json::array();
for (const auto& file : document.Files) files.push_back({
{"path", file.Path.generic_string()}, {"role", file.Role}, {"size", file.Size}, {"sha256", file.Sha256}, {"required", file.Required}
});
return WriteJson(path, {
{"format_version", document.FormatVersion}, {"runtime_abi_major", document.RuntimeAbiMajor},
{"runtime_abi_minor", document.RuntimeAbiMinor}, {"package_format_version", document.PackageFormatVersion},
{"engine_version", document.EngineVersion}, {"project_name", document.ProjectName}, {"project_version", document.ProjectVersion},
{"build_id", document.BuildId}, {"git_commit", document.GitCommit}, {"target_platform", document.TargetPlatform},
{"configuration", document.Configuration}, {"graphics_backend", document.GraphicsBackend},
{"system_dependencies", document.SystemDependencies}, {"files", files}
});
}
std::vector<MetaCoreBuildIssue> MetaCoreValidateReleasePackage(const std::filesystem::path& packageRoot) {
std::vector<MetaCoreBuildIssue> issues;
MetaCoreBuildIssue manifestIssue;
const auto manifest = MetaCoreReadReleaseManifest(packageRoot / "Manifest" / "MetaCore.release.json", &manifestIssue);
if (!manifest) {
issues.push_back(std::move(manifestIssue));
return issues;
}
for (const auto& entry : manifest->Files) {
if (!IsSafeRelativePath(entry.Path)) {
issues.push_back({MetaCoreBuildErrorCode::UnsafePath, "Manifest contains an unsafe file path", entry.Path});
continue;
}
const auto absolute = packageRoot / entry.Path;
std::error_code error;
if (!std::filesystem::is_regular_file(absolute, error)) {
if (entry.Required) issues.push_back({MetaCoreBuildErrorCode::RequiredFileMissing, "Required release file is missing", entry.Path, entry.Role, "missing"});
continue;
}
const auto size = std::filesystem::file_size(absolute, error);
if (error || size != entry.Size) {
issues.push_back({MetaCoreBuildErrorCode::FileSizeMismatch, "Release file size does not match manifest", entry.Path, std::to_string(entry.Size), error ? "unreadable" : std::to_string(size)});
continue;
}
const auto hash = MetaCoreSha256File(absolute);
if (!hash || *hash != entry.Sha256) {
issues.push_back({MetaCoreBuildErrorCode::FileHashMismatch, "Release file SHA-256 does not match manifest", entry.Path, entry.Sha256, hash.value_or("unreadable")});
}
}
MetaCoreBuildIssue deliveryIssue;
const auto delivery = MetaCoreReadRuntimeDeliveryDocument(packageRoot / "Project" / "MetaCore.runtimeproject", &deliveryIssue);
if (!delivery) issues.push_back(std::move(deliveryIssue));
else {
if (delivery->BuildId != manifest->BuildId) issues.push_back({MetaCoreBuildErrorCode::RuntimeAbiIncompatible, "Runtime project and release manifest Build IDs differ", {}, manifest->BuildId, delivery->BuildId});
for (const auto& module : delivery->ScriptModules) {
if (!std::filesystem::is_regular_file(packageRoot / module))
issues.push_back({MetaCoreBuildErrorCode::RequiredFileMissing, "Required project script module is missing", module, "script_module", "missing"});
}
}
return issues;
}
bool MetaCoreExportDiagnostics(const std::filesystem::path& packageRoot, const std::filesystem::path& outputArchive, MetaCoreBuildIssue* issue) {
const auto manifest = packageRoot / "Manifest" / "MetaCore.release.json";
if (!std::filesystem::is_regular_file(manifest)) {
SetIssue(issue, MetaCoreBuildErrorCode::ManifestUnreadable, "Cannot export diagnostics without a release manifest", manifest);
return false;
}
const auto temporary = std::filesystem::temp_directory_path() / ("MetaCoreDiagnostics-" + MakeBuildId());
std::error_code error;
std::filesystem::create_directories(temporary / "Manifest", error);
std::filesystem::copy_file(manifest, temporary / "Manifest" / manifest.filename(), std::filesystem::copy_options::overwrite_existing, error);
if (std::filesystem::is_directory(packageRoot / "Diagnostics") &&
!CopyTree(packageRoot / "Diagnostics", temporary / "Diagnostics")) {
SetIssue(issue, MetaCoreBuildErrorCode::PackageFailed, "Failed to collect diagnostics directory", packageRoot / "Diagnostics");
std::filesystem::remove_all(temporary, error);
return false;
}
const auto runtimeDiagnostics = packageRoot / "Content" / "Runtime" / "Diagnostics.mcruntimestate";
if (std::filesystem::is_regular_file(runtimeDiagnostics)) {
std::filesystem::create_directories(temporary / "Runtime", error);
std::filesystem::copy_file(runtimeDiagnostics, temporary / "Runtime" / runtimeDiagnostics.filename(), std::filesystem::copy_options::overwrite_existing, error);
}
const auto validation = MetaCoreValidateReleasePackage(packageRoot);
json report = json::array();
for (const auto& value : validation) report.push_back({{"code", MetaCoreBuildErrorCodeName(value.Code)}, {"message", value.Message}, {"path", value.Path.generic_string()}});
if (!WriteJson(temporary / "validation.json", report)) {
SetIssue(issue, MetaCoreBuildErrorCode::PackageFailed, "Failed to write diagnostics validation report", temporary);
std::filesystem::remove_all(temporary, error);
return false;
}
std::ofstream systemInfo(temporary / "system.txt");
#if defined(__linux__)
struct utsname systemName{};
systemInfo << "platform=Linux\n";
if (uname(&systemName) == 0) {
systemInfo << "kernel=" << systemName.release << "\narchitecture=" << systemName.machine << '\n';
}
std::ifstream osRelease("/etc/os-release");
systemInfo << osRelease.rdbuf();
std::error_code drmError;
if (std::filesystem::is_directory("/sys/class/drm", drmError)) {
for (const auto& entry : std::filesystem::directory_iterator("/sys/class/drm", drmError)) {
if (entry.path().filename().string().rfind("card", 0) == 0)
systemInfo << "drm_device=" << entry.path().filename().string() << '\n';
}
}
#else
systemInfo << "platform=unknown\n";
#endif
systemInfo << "runtime_abi=" << GMetaCoreRuntimeAbiMajor << '.' << GMetaCoreRuntimeAbiMinor << '\n';
const bool archived = CreateArchive(outputArchive, temporary);
std::filesystem::remove_all(temporary, error);
if (!archived) SetIssue(issue, MetaCoreBuildErrorCode::PackageFailed, "Failed to create diagnostics archive", outputArchive);
return archived;
}
void MetaCoreBuildPipeline::RequestCancel() { CancelRequested_.store(true); }
MetaCoreBuildResult MetaCoreBuildPipeline::Run(const MetaCoreBuildRequest& request, MetaCoreBuildProgressCallback progress) {
CancelRequested_.store(false);
MetaCoreBuildResult result;
auto fail = [&](MetaCoreBuildErrorCode code, const std::string& message, const std::filesystem::path& path = {}) {
result.Issues.push_back({code, message, path});
return result;
};
auto report = [&](MetaCoreBuildStage stage, std::string_view message) { if (progress) progress(stage, message); };
auto cancelled = [&]() { return CancelRequested_.load(); };
if (request.ProjectRoot.empty() || request.ProfilePath.empty() || request.EngineBuildRoot.empty())
return fail(MetaCoreBuildErrorCode::InvalidArguments, "Project, profile, and engine build paths are required");
MetaCoreBuildIssue profileIssue;
const auto profile = MetaCoreReadBuildProfile(request.ProfilePath, &profileIssue);
if (!profile) { result.Issues.push_back(std::move(profileIssue)); return result; }
if (profile->TargetPlatform != "Linux-x86_64" || profile->GraphicsBackend != "OpenGL")
return fail(MetaCoreBuildErrorCode::UnsupportedPlatform, "Only Linux-x86_64 with OpenGL is supported by this release backend", request.ProfilePath);
const auto projectFile = MetaCoreGetProjectFilePath(request.ProjectRoot);
const auto project = MetaCoreReadProjectFile(projectFile);
if (!project) return fail(MetaCoreBuildErrorCode::ProjectUnreadable, "Project descriptor is missing or invalid", projectFile);
if (!IsSafeIdentifier(project->Name) || !IsSafeIdentifier(project->Version))
return fail(MetaCoreBuildErrorCode::UnsafePath, "Project name or version is unsafe for a release path", projectFile);
report(MetaCoreBuildStage::Build, "Building MetaCorePlayer");
if (!request.SkipBuild) {
const std::string command = "cmake --build " + ShellQuote(request.EngineBuildRoot) + " --target MetaCorePlayer --config " + CMakeConfigurationName(profile->Configuration);
if (!RunCommand(command)) return fail(MetaCoreBuildErrorCode::BuildFailed, "MetaCorePlayer build failed", request.EngineBuildRoot);
}
if (cancelled()) return fail(MetaCoreBuildErrorCode::Cancelled, "Build cancelled");
const auto player = FindPlayer(request.EngineBuildRoot, profile->Configuration);
if (player.empty()) return fail(MetaCoreBuildErrorCode::BuildFailed, "MetaCorePlayer executable was not found", request.EngineBuildRoot);
result.BuildId = MakeBuildId();
const auto sourceRoot = FindSourceRoot(request.EngineBuildRoot);
std::optional<std::filesystem::path> projectScriptModule;
if (std::filesystem::is_regular_file(request.ProjectRoot / "Scripts" / "CMakeLists.txt")) {
report(MetaCoreBuildStage::Build, "Building project script module");
if (!request.SkipBuild) {
const auto scriptBuild = MetaCoreBuildScriptProject(
request.ProjectRoot, CMakeConfigurationName(profile->Configuration), sourceRoot
);
if (!scriptBuild.Success) {
return fail(MetaCoreBuildErrorCode::BuildFailed, "Project script module build failed: " + scriptBuild.Output, request.ProjectRoot / "Scripts");
}
projectScriptModule = scriptBuild.ModulePath;
} else {
projectScriptModule = MetaCoreFindLatestScriptModule(request.ProjectRoot, CMakeConfigurationName(profile->Configuration));
if (!projectScriptModule.has_value()) {
return fail(MetaCoreBuildErrorCode::RequiredFileMissing, "Project script module has not been built", request.ProjectRoot / "Scripts");
}
}
}
const auto outputRoot = request.ProjectRoot / profile->OutputDirectory / profile->TargetPlatform / project->Name / profile->Name;
const std::string packageName = project->Name + "-" + project->Version + "-" + result.BuildId;
const auto staging = outputRoot / (".staging-" + result.BuildId);
result.PackageDirectory = outputRoot / packageName;
result.ArchivePath = outputRoot / (packageName + ".tar.gz");
result.SymbolsArchivePath = outputRoot / (packageName + ".symbols.tar.gz");
std::error_code error;
if (request.Clean && std::filesystem::is_directory(outputRoot, error)) {
for (const auto& entry : std::filesystem::directory_iterator(outputRoot, error))
if (entry.path().filename().string().rfind(".staging-", 0) == 0) std::filesystem::remove_all(entry.path(), error);
}
std::filesystem::remove_all(staging, error);
std::filesystem::create_directories(staging / "Content" / "Scenes", error);
std::filesystem::create_directories(staging / "Content" / "Assets", error);
std::filesystem::create_directories(staging / "Content" / "Ui", error);
std::filesystem::create_directories(staging / "Content" / "Runtime", error);
std::filesystem::create_directories(staging / "Engine" / "Materials", error);
std::filesystem::create_directories(staging / "Engine" / "Fonts", error);
std::filesystem::create_directories(staging / "Diagnostics" / "Logs", error);
std::filesystem::create_directories(staging / "Project" / "Scripts", error);
if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to create staging directory", staging);
report(MetaCoreBuildStage::Cook, "Cooking startup scene and project content");
std::filesystem::path startupSource = profile->StartupScene.empty() ? project->StartupScenePath : profile->StartupScene;
if (!IsSafeRelativePath(startupSource)) return fail(MetaCoreBuildErrorCode::UnsafePath, "Startup scene path is unsafe", startupSource);
const auto absoluteStartup = request.ProjectRoot / startupSource;
std::filesystem::path cookedSceneName = startupSource.filename();
if (cookedSceneName.extension() == ".json") cookedSceneName.replace_extension("");
const auto cookedScene = staging / "Content" / "Scenes" / cookedSceneName;
if (startupSource.extension() == ".json") {
const auto registry = MetaCoreBuildScenePackageTypeRegistry();
const auto scene = MetaCoreSceneSerializer::LoadSceneFromJson(absoluteStartup, registry);
if (!scene || !MetaCoreWriteScenePackage(cookedScene, *scene)) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to cook startup scene", absoluteStartup);
} else {
std::filesystem::copy_file(absoluteStartup, cookedScene, std::filesystem::copy_options::overwrite_existing, error);
if (error) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to copy startup scene", absoluteStartup);
}
const auto currentCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / profile->Name;
const auto editorCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / "Editor";
const auto legacyCookRoot = request.ProjectRoot / "Library" / "Cooked" / "Windows";
bool copiedCooked = false;
if (std::filesystem::is_directory(currentCookRoot)) copiedCooked = CopyTree(currentCookRoot, staging / "Content" / "Assets");
else if (std::filesystem::is_directory(editorCookRoot)) copiedCooked = CopyTree(editorCookRoot, staging / "Content" / "Assets");
else if (std::filesystem::is_directory(legacyCookRoot)) copiedCooked = CopyTree(legacyCookRoot, staging / "Content" / "Assets");
if ((profile->ResourcePolicy == MetaCoreBuildResourcePolicy::AllProjectContent || !copiedCooked) && std::filesystem::is_directory(request.ProjectRoot / "Assets"))
if (!CopyTree(request.ProjectRoot / "Assets", staging / "Content" / "Assets")) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to collect project assets", request.ProjectRoot / "Assets");
if (std::filesystem::is_directory(request.ProjectRoot / project->UiDirectory) &&
!CopyTree(request.ProjectRoot / project->UiDirectory, staging / "Content" / "Ui"))
return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to collect project UI", request.ProjectRoot / project->UiDirectory);
if (std::filesystem::is_directory(request.ProjectRoot / project->RuntimeDirectory) &&
!CopyTree(request.ProjectRoot / project->RuntimeDirectory, staging / "Content" / "Runtime"))
return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to collect project runtime configuration", request.ProjectRoot / project->RuntimeDirectory);
if (cancelled()) { std::filesystem::remove_all(staging, error); return fail(MetaCoreBuildErrorCode::Cancelled, "Build cancelled"); }
report(MetaCoreBuildStage::Stage, "Staging executable and runtime dependencies");
std::filesystem::copy_file(player, staging / player.filename(), std::filesystem::copy_options::overwrite_existing, error);
if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to stage MetaCorePlayer", player);
std::filesystem::permissions(staging / player.filename(), std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error);
std::filesystem::path stagedScriptModule;
if (projectScriptModule.has_value()) {
stagedScriptModule = staging / "Project" / "Scripts" / projectScriptModule->filename();
std::filesystem::copy_file(*projectScriptModule, stagedScriptModule, std::filesystem::copy_options::overwrite_existing, error);
if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to stage project script module", *projectScriptModule);
std::filesystem::path scriptManifest = *projectScriptModule;
scriptManifest += ".mcscriptmodule.json";
if (!std::filesystem::is_regular_file(scriptManifest))
return fail(MetaCoreBuildErrorCode::RequiredFileMissing, "Project script module manifest is missing", scriptManifest);
std::filesystem::copy_file(scriptManifest, staging / "Project" / "Scripts" / scriptManifest.filename(), std::filesystem::copy_options::overwrite_existing, error);
if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to stage project script module manifest", scriptManifest);
}
for (const char* material : {"uiBlit.filamat", "rml_ui.filamat"}) {
const auto source = request.EngineBuildRoot / material;
if (!std::filesystem::is_regular_file(source)) return fail(MetaCoreBuildErrorCode::StageFailed, "Required engine material is missing", source);
std::filesystem::copy_file(source, staging / "Engine" / "Materials" / material, std::filesystem::copy_options::overwrite_existing, error);
}
const std::array fontCandidates = {
sourceRoot / "third_party" / "filament_installed" / "bin" / "assets" / "fonts" / "Roboto-Medium.ttf",
sourceRoot / "third_party" / "filament_installed.before-linux-sdk" / "bin" / "assets" / "fonts" / "Roboto-Medium.ttf"
};
bool fontCopied = false;
for (const auto& font : fontCandidates) if (!fontCopied && std::filesystem::is_regular_file(font)) {
std::filesystem::copy_file(font, staging / "Engine" / "Fonts" / font.filename(), std::filesystem::copy_options::overwrite_existing, error);
fontCopied = !error;
}
if (!fontCopied) return fail(MetaCoreBuildErrorCode::StageFailed, "Required runtime font is missing", sourceRoot);
if (!CopyLinuxRuntimeLibraries(player, staging / "lib")) return fail(MetaCoreBuildErrorCode::DependencyMissing, "Failed to inspect or stage Linux C++ runtime libraries", player);
const std::array crashpadCandidates = {
sourceRoot / "vcpkg_installed" / "x64-linux-clang-libcxx" / "tools" / "crashpad" / "crashpad_handler",
request.EngineBuildRoot / "vcpkg_installed" / "x64-linux-clang-libcxx" / "tools" / "crashpad" / "crashpad_handler"
};
bool crashpadHandlerCopied = false;
for (const auto& handler : crashpadCandidates) {
if (!std::filesystem::is_regular_file(handler)) continue;
std::filesystem::create_directories(staging / "Crash", error);
std::filesystem::copy_file(handler, staging / "Crash" / "crashpad_handler", std::filesystem::copy_options::overwrite_existing, error);
std::filesystem::permissions(staging / "Crash" / "crashpad_handler", std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error);
crashpadHandlerCopied = !error;
break;
}
#if defined(METACORE_HAS_CRASHPAD) && METACORE_HAS_CRASHPAD
if (!crashpadHandlerCopied) return fail(MetaCoreBuildErrorCode::DependencyMissing, "Crashpad is linked but crashpad_handler was not found", sourceRoot);
#else
(void)crashpadHandlerCopied;
#endif
MetaCoreRuntimeDeliveryDocument delivery;
delivery.BuildId = result.BuildId;
delivery.StartupScene = std::filesystem::path("Content") / "Scenes" / cookedSceneName;
if (!stagedScriptModule.empty()) delivery.ScriptModules.push_back(std::filesystem::path("Project") / "Scripts" / stagedScriptModule.filename());
if (!MetaCoreWriteRuntimeDeliveryDocument(staging / "Project" / "MetaCore.runtimeproject", delivery))
return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to write runtime project document", staging);
const auto symbolsRoot = outputRoot / (".symbols-" + result.BuildId);
std::filesystem::remove_all(symbolsRoot, error);
if (profile->Configuration != MetaCoreBuildConfiguration::Debug) {
std::filesystem::create_directories(symbolsRoot, error);
const auto debugFile = symbolsRoot / "MetaCorePlayer.debug";
const std::string keepDebug = "objcopy --only-keep-debug " + ShellQuote(staging / player.filename()) + " " + ShellQuote(debugFile);
if (!RunCommand(keepDebug)) return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to create detached debug symbols", staging / player.filename());
if (profile->Configuration == MetaCoreBuildConfiguration::Release) {
if (!RunCommand("strip --strip-unneeded " + ShellQuote(staging / player.filename())) ||
!RunCommand("objcopy --add-gnu-debuglink=" + ShellQuote(debugFile) + " " + ShellQuote(staging / player.filename())))
return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to strip or link release symbols", staging / player.filename());
}
if (!stagedScriptModule.empty()) {
const auto scriptDebug = symbolsRoot / (stagedScriptModule.filename().string() + ".debug");
if (!RunCommand("objcopy --only-keep-debug " + ShellQuote(stagedScriptModule) + " " + ShellQuote(scriptDebug)))
return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to create project script debug symbols", stagedScriptModule);
if (profile->Configuration == MetaCoreBuildConfiguration::Release &&
(!RunCommand("strip --strip-unneeded " + ShellQuote(stagedScriptModule)) ||
!RunCommand("objcopy --add-gnu-debuglink=" + ShellQuote(scriptDebug) + " " + ShellQuote(stagedScriptModule))))
return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to strip project script module", stagedScriptModule);
if (profile->Configuration == MetaCoreBuildConfiguration::Release) {
std::filesystem::path scriptManifest = stagedScriptModule;
scriptManifest += ".mcscriptmodule.json";
auto manifestJson = ReadJson(scriptManifest);
const auto strippedHash = MetaCoreSha256File(stagedScriptModule);
if (!manifestJson.has_value() || !strippedHash.has_value())
return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to refresh stripped project script manifest", scriptManifest);
(*manifestJson)["sha256"] = *strippedHash;
if (!WriteJson(scriptManifest, *manifestJson))
return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to write stripped project script manifest", scriptManifest);
}
}
}
report(MetaCoreBuildStage::Package, "Writing release manifest and archives");
MetaCoreReleaseManifestDocument manifest;
manifest.ProjectName = project->Name; manifest.ProjectVersion = project->Version; manifest.BuildId = result.BuildId;
manifest.GitCommit = DetectGitCommit(sourceRoot); manifest.TargetPlatform = profile->TargetPlatform;
manifest.Configuration = ConfigurationName(profile->Configuration); manifest.GraphicsBackend = profile->GraphicsBackend;
manifest.SystemDependencies = {"Ubuntu 22.04 x86_64", "glibc >= 2.35", "OpenGL/GLX", "X11"};
manifest.Files = CollectManifestFiles(staging);
if (!MetaCoreWriteReleaseManifest(staging / "Manifest" / "MetaCore.release.json", manifest))
return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to write release manifest", staging);
std::filesystem::rename(staging, result.PackageDirectory, error);
if (error) return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to commit staging directory", result.PackageDirectory);
if (!CreateArchive(result.ArchivePath, result.PackageDirectory)) return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to create release archive", result.ArchivePath);
if (profile->Configuration != MetaCoreBuildConfiguration::Debug) {
if (!CreateArchive(result.SymbolsArchivePath, symbolsRoot)) return fail(MetaCoreBuildErrorCode::PackageFailed, "Failed to create symbols archive", result.SymbolsArchivePath);
std::filesystem::remove_all(symbolsRoot, error);
} else result.SymbolsArchivePath.clear();
report(MetaCoreBuildStage::Validate, "Validating release package");
result.Issues = MetaCoreValidateReleasePackage(result.PackageDirectory);
result.Success = result.Issues.empty();
return result;
}
} // namespace MetaCore