780 lines
45 KiB
C++
780 lines
45 KiB
C++
#include "MetaCoreDelivery/MetaCoreBuildPipeline.h"
|
|
|
|
#include "MetaCoreFoundation/MetaCoreHash.h"
|
|
#include "MetaCoreFoundation/MetaCorePackage.h"
|
|
#include "MetaCoreFoundation/MetaCoreProject.h"
|
|
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
|
|
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
|
|
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.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");
|
|
MetaCoreTypeRegistry inputRegistry;
|
|
MetaCoreRegisterRuntimeDataGeneratedTypes(inputRegistry);
|
|
MetaCoreRegisterRuntimeInputGeneratedTypes(inputRegistry);
|
|
const auto runtimeProjectPath = request.ProjectRoot / project->RuntimeDirectory / "ProjectRuntime.mcruntimecfg";
|
|
const auto runtimeProject = MetaCoreReadRuntimeProjectDocument(runtimeProjectPath, inputRegistry);
|
|
if (!runtimeProject) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime project configuration is missing or unreadable", runtimeProjectPath);
|
|
if (runtimeProject->InputMapPath.empty() || !IsSafeRelativePath(runtimeProject->InputMapPath))
|
|
return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime input map path is empty or unsafe", runtimeProjectPath);
|
|
const auto inputMapPath = request.ProjectRoot / runtimeProject->InputMapPath;
|
|
const auto inputMap = MetaCoreReadInputMap(inputMapPath, inputRegistry);
|
|
if (!inputMap) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime input map is missing or unreadable", inputMapPath);
|
|
const auto inputIssues = MetaCoreValidateInputMap(*inputMap);
|
|
if (std::any_of(inputIssues.begin(), inputIssues.end(), [](const auto& issue) { return issue.Error; }))
|
|
return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime input map validation failed", inputMapPath);
|
|
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
|