MetaCore/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp
2026-07-22 16:08:48 +08:00

1025 lines
65 KiB
C++

#include "MetaCoreDelivery/MetaCoreBuildPipeline.h"
#include "MetaCoreFoundation/MetaCoreHash.h"
#include "MetaCoreFoundation/MetaCoreArchive.h"
#include "MetaCoreFoundation/MetaCoreAssetTypes.h"
#include "MetaCoreFoundation/MetaCorePackage.h"
#include "MetaCoreFoundation/MetaCoreProject.h"
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
#include "MetaCoreRuntimeInput/MetaCoreInputMap.h"
#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h"
#include "MetaCoreRendering/MetaCoreRendering.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 <cstring>
#include <fstream>
#include <iomanip>
#include <set>
#include <sstream>
#include <system_error>
#include <unordered_set>
#if defined(__linux__)
#include <sys/utsname.h>
#endif
namespace MetaCore {
namespace {
using json = nlohmann::json;
[[nodiscard]] bool CopyTree(const std::filesystem::path& source, const std::filesystem::path& target);
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> MetaCoreCollectCookablePhysicsMeshes(
const std::filesystem::path& projectRoot,
const MetaCoreTypeRegistry& registry,
const std::filesystem::path& derivedOutputRoot
) {
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> result;
for (const auto& relativeRoot : {std::filesystem::path("Assets"), std::filesystem::path("Library")}) {
const auto root = projectRoot / relativeRoot; std::error_code error;
if (!std::filesystem::is_directory(root, error)) continue;
for (const auto& entry : std::filesystem::recursive_directory_iterator(root, error)) {
if (error || !entry.is_regular_file() || entry.path().extension() != ".mcasset") continue;
const auto package = MetaCoreReadPackageFile(entry.path(), registry);
if (!package || package->PayloadSections.empty()) continue;
MetaCoreModelAssetDocument model;
if (!MetaCoreDeserializeFromBytes(package->PayloadSections.front(), model, registry)) continue;
for (const auto& mesh : model.GeneratedMeshAssets) {
if (mesh.AssetGuid.IsValid() && mesh.PayloadIndex < package->PayloadSections.size() &&
package->PayloadSections[static_cast<std::size_t>(mesh.PayloadIndex)].size() >= sizeof(std::uint32_t) * 2U) {
struct PackedVertex { float px,py,pz,nx,ny,nz,u,v; };
const auto& payload = package->PayloadSections[static_cast<std::size_t>(mesh.PayloadIndex)];
std::uint32_t vertexCount = 0, indexCount = 0;
std::memcpy(&vertexCount, payload.data(), sizeof(vertexCount));
std::memcpy(&indexCount, payload.data() + sizeof(vertexCount), sizeof(indexCount));
if (vertexCount == 0U || indexCount < 3U || indexCount % 3U != 0U ||
vertexCount > (payload.size() - 8U) / sizeof(PackedVertex)) continue;
const std::size_t verticesSize = sizeof(PackedVertex) * static_cast<std::size_t>(vertexCount);
if (indexCount > (payload.size() - 8U - verticesSize) / sizeof(std::uint32_t)) continue;
const std::size_t required = 8U + verticesSize + sizeof(std::uint32_t) * static_cast<std::size_t>(indexCount);
if (required > payload.size()) continue;
MetaCorePhysicsMeshData collision;
collision.Positions.resize(vertexCount);
collision.Indices.resize(indexCount);
const std::byte* cursor = payload.data() + 8U;
for (std::uint32_t index = 0; index < vertexCount; ++index) {
PackedVertex vertex{};
std::memcpy(&vertex, cursor, sizeof(vertex));
cursor += sizeof(vertex);
collision.Positions[index] = {vertex.px, vertex.pz, -vertex.py};
}
std::memcpy(collision.Indices.data(), cursor, sizeof(std::uint32_t) * static_cast<std::size_t>(indexCount));
if (std::any_of(collision.Indices.begin(), collision.Indices.end(),
[vertexCount](std::uint32_t index) { return index >= vertexCount; })) continue;
const auto bytes = MetaCoreEncodePhysicsCollisionDerivedData(collision);
const auto outputPath = derivedOutputRoot / (mesh.AssetGuid.ToString() + ".mccollision");
std::ofstream output(outputPath, std::ios::binary | std::ios::trunc);
if (!output) continue;
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
output.close();
if (!output) { std::error_code removeError; std::filesystem::remove(outputPath, removeError); continue; }
result.insert(mesh.AssetGuid);
}
}
}
}
return result;
}
std::optional<std::string> MetaCoreValidateScenePhysicsForCook(
const MetaCoreSceneDocument& scene,
const std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher>& cookableMeshes
) {
for (const auto& object : scene.GameObjects) {
if (object.CharacterController && object.RigidBody) {
return "Object " + std::to_string(object.Id) + " has both CharacterController and RigidBody";
}
if (!object.Collider || !object.Collider->Enabled) continue;
const auto shape = object.Collider->Shape;
if (shape != MetaCoreColliderShape::ConvexHull && shape != MetaCoreColliderShape::TriangleMesh) continue;
if (!object.Collider->MeshAssetGuid.IsValid() || !cookableMeshes.contains(object.Collider->MeshAssetGuid)) {
return "Object " + std::to_string(object.Id) + " is missing collision mesh payload " + object.Collider->MeshAssetGuid.ToString();
}
if (shape == MetaCoreColliderShape::TriangleMesh && object.RigidBody &&
object.RigidBody->BodyType != MetaCoreRigidBodyType::Static) {
return "Object " + std::to_string(object.Id) + " uses a concave TriangleMesh on a non-static body";
}
}
return std::nullopt;
}
std::optional<std::string> MetaCoreStageSceneRenderingForCook(
const MetaCoreSceneDocument& scene,
const std::filesystem::path& projectRoot,
const std::filesystem::path& outputRoot
) {
std::error_code error;
std::filesystem::create_directories(outputRoot / "ReflectionProbes", error);
if (error) return "Cannot create rendering derived-data output: " + error.message();
for (const auto& object : scene.GameObjects) {
if (object.LodGroup && object.LodGroup->Enabled) {
const auto& levels = object.LodGroup->Levels;
if (levels.empty() || levels.size() > 8U)
return "Object " + std::to_string(object.Id) + " LODGroup must contain 1-8 levels";
for (std::size_t index = 0; index < levels.size(); ++index) {
if (levels[index].ScreenHeight < 0.0F || levels[index].ScreenHeight > 1.0F ||
(index > 0U && levels[index].ScreenHeight >= levels[index - 1U].ScreenHeight))
return "Object " + std::to_string(object.Id) + " has invalid or non-descending LOD thresholds";
}
}
if (object.Terrain && object.Terrain->Enabled && object.Terrain->Layers.size() > 4U)
return "Object " + std::to_string(object.Id) + " Terrain exceeds the four-layer limit";
if (!object.ReflectionProbe || !object.ReflectionProbe->Enabled) continue;
const auto& probe = *object.ReflectionProbe;
if (!probe.BakedCubemapGuid.IsValid())
return "Object " + std::to_string(object.Id) + " ReflectionProbe has no baked cubemap GUID";
const auto sourceRoot = projectRoot / "Library" / "DerivedData" / "Rendering" / "Probes" / probe.BakedCubemapGuid.ToString();
const auto headerPath = sourceRoot / "probe.mcrprobe";
const auto header = MetaCoreReadReflectionProbeDerivedHeader(headerPath);
if (!header || header->ProbeGuid != probe.BakedCubemapGuid)
return "Object " + std::to_string(object.Id) + " ReflectionProbe derived header is missing, corrupt, or version-incompatible: " + probe.BakedCubemapGuid.ToString();
const std::filesystem::path iblRelativePath = header->IblRelativePath;
const std::filesystem::path skyboxRelativePath = header->SkyboxRelativePath;
const auto iblPath = sourceRoot / iblRelativePath;
const auto skyboxPath = sourceRoot / skyboxRelativePath;
if (header->IblRelativePath.empty() || header->SkyboxRelativePath.empty() ||
iblRelativePath.is_absolute() || skyboxRelativePath.is_absolute() ||
iblRelativePath.string().find("..") != std::string::npos || skyboxRelativePath.string().find("..") != std::string::npos ||
!std::filesystem::is_regular_file(iblPath) || !std::filesystem::is_regular_file(skyboxPath))
return "Object " + std::to_string(object.Id) + " ReflectionProbe KTX payload is missing or unsafe: " + probe.BakedCubemapGuid.ToString();
if (!probe.SourceEnvironmentPath.empty()) {
const std::filesystem::path sourceEnvironment = probe.SourceEnvironmentPath;
if (sourceEnvironment.is_absolute() || sourceEnvironment.string().find("..") != std::string::npos)
return "Object " + std::to_string(object.Id) + " ReflectionProbe source path is unsafe";
const auto sourceHash = MetaCoreSha256File(projectRoot / sourceEnvironment);
if (!sourceHash || *sourceHash != header->SourceHash)
return "Object " + std::to_string(object.Id) + " ReflectionProbe source hash changed; rebake probe " + probe.BakedCubemapGuid.ToString();
}
const auto destination = outputRoot / "ReflectionProbes" / probe.BakedCubemapGuid.ToString();
if (!CopyTree(sourceRoot, destination))
return "Failed to stage ReflectionProbe " + probe.BakedCubemapGuid.ToString();
}
return std::nullopt;
}
std::optional<std::string> MetaCoreValidateMaterialAssetsForCook(
const std::filesystem::path& projectRoot, const MetaCoreTypeRegistry& registry) {
MetaCoreMaterialTemplateRegistry templates;
for(const auto& relativeRoot:{std::filesystem::path("Assets"),std::filesystem::path("Library")}){
const auto root=projectRoot/relativeRoot;std::error_code error;if(!std::filesystem::is_directory(root,error))continue;
for(std::filesystem::recursive_directory_iterator iterator(root,std::filesystem::directory_options::skip_permission_denied,error),end;!error&&iterator!=end;iterator.increment(error)){
if(!iterator->is_regular_file(error))continue;const auto& path=iterator->path();
if(path.filename().string().ends_with(".mcmaterial.json")){
auto material=MetaCoreSceneSerializer::LoadMaterialFromJson(path,registry);std::vector<std::string> issues;
if(!material||!templates.ValidateAndMigrate(*material,&issues))return "Material asset is missing, invalid, or template-incompatible: "+path.generic_string()+(issues.empty()?std::string{}:"; "+issues.front());
}else if(path.extension()==".mcasset"){
const auto package=MetaCoreReadPackageFile(path,registry);if(!package||package->PayloadSections.empty())continue;MetaCoreModelAssetDocument model;if(!MetaCoreDeserializeFromBytes(package->PayloadSections.front(),model,registry))continue;
for(auto material:model.GeneratedMaterialAssets){std::vector<std::string> issues;if(!templates.ValidateAndMigrate(material,&issues))return "Generated material is template-incompatible: asset="+material.AssetGuid.ToString()+" package="+path.generic_string()+(issues.empty()?std::string{}:"; "+issues.front());}
}
}
}
return std::nullopt;
}
[[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 / "Content" / "Physics", 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;
MetaCoreRegisterFoundationGeneratedTypes(inputRegistry);
MetaCoreRegisterRenderingGeneratedTypes(inputRegistry);
MetaCoreRegisterRuntimeDataGeneratedTypes(inputRegistry);
MetaCoreRegisterRuntimeInputGeneratedTypes(inputRegistry);
MetaCoreRegisterPhysicsGeneratedTypes(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);
const auto physicsRelativePath = runtimeProject->PhysicsSettingsPath.empty() ? std::filesystem::path("Runtime/Physics.mcruntime") : runtimeProject->PhysicsSettingsPath;
if (!IsSafeRelativePath(physicsRelativePath)) return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime physics settings path is unsafe", physicsRelativePath);
const auto physicsPath = request.ProjectRoot / physicsRelativePath;
auto physicsSettings = MetaCoreReadPhysicsSettings(physicsPath, inputRegistry);
if (!physicsSettings) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime physics settings are missing or unreadable", physicsPath);
std::vector<std::string> physicsIssues;
if (!MetaCoreValidatePhysicsSettings(*physicsSettings, &physicsIssues)) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime physics settings validation failed", physicsPath);
const auto renderingRelativePath = runtimeProject->RenderingSettingsPath.empty() ? std::filesystem::path("Runtime/Rendering.mcruntime") : runtimeProject->RenderingSettingsPath;
if (!IsSafeRelativePath(renderingRelativePath)) return fail(MetaCoreBuildErrorCode::UnsafePath, "Runtime rendering settings path is unsafe", renderingRelativePath);
const auto renderingPath = request.ProjectRoot / renderingRelativePath;
auto renderingSettings = MetaCoreReadRenderSettings(renderingPath, inputRegistry);
if (!renderingSettings) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime rendering settings are missing, unreadable, or newer than this engine", renderingPath);
std::vector<std::string> renderingIssues;
if (!MetaCoreValidateRenderSettings(*renderingSettings, &renderingIssues)) return fail(MetaCoreBuildErrorCode::CookFailed, "Runtime rendering settings validation failed", renderingPath);
if(const auto materialIssue=MetaCoreValidateMaterialAssetsForCook(request.ProjectRoot,inputRegistry))
return fail(MetaCoreBuildErrorCode::CookFailed,*materialIssue,request.ProjectRoot/"Assets");
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;
const auto cookablePhysicsMeshes = MetaCoreCollectCookablePhysicsMeshes(request.ProjectRoot, inputRegistry, staging / "Content" / "Physics");
std::optional<MetaCoreSceneDocument> startupSceneDocument;
if (startupSource.extension() == ".json") {
const auto registry = MetaCoreBuildScenePackageTypeRegistry();
const auto scene = MetaCoreSceneSerializer::LoadSceneFromJson(absoluteStartup, registry);
if (scene) if (const auto issue = MetaCoreValidateScenePhysicsForCook(*scene, cookablePhysicsMeshes))
return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
if (!scene || !MetaCoreWriteScenePackage(cookedScene, *scene)) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to cook startup scene", absoluteStartup);
startupSceneDocument = *scene;
} else {
if (const auto scene = MetaCoreReadScenePackage(absoluteStartup); scene.has_value()) {
if (const auto issue = MetaCoreValidateScenePhysicsForCook(*scene, cookablePhysicsMeshes))
return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
else startupSceneDocument = *scene;
}
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 (startupSceneDocument) {
if (const auto issue = MetaCoreStageSceneRenderingForCook(
*startupSceneDocument, request.ProjectRoot, staging / "Content" / "Rendering"))
return fail(MetaCoreBuildErrorCode::CookFailed, *issue, absoluteStartup);
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> environmentProfileGuids;
std::unordered_set<MetaCoreAssetGuid, MetaCoreAssetGuidHasher> postProcessProfileGuids;
if (startupSceneDocument->Environment.EnvironmentProfileGuid.IsValid()) environmentProfileGuids.insert(startupSceneDocument->Environment.EnvironmentProfileGuid);
if (startupSceneDocument->Environment.PostProcessProfileGuid.IsValid()) postProcessProfileGuids.insert(startupSceneDocument->Environment.PostProcessProfileGuid);
for (const auto& object : startupSceneDocument->GameObjects) if (object.Camera && object.Camera->PostProcessProfileGuid.IsValid()) postProcessProfileGuids.insert(object.Camera->PostProcessProfileGuid);
const auto profileOutputRoot = staging / "Content" / "Assets" / "RenderingProfiles";
std::filesystem::create_directories(profileOutputRoot, error);
const auto stageRenderingProfile = [&](const MetaCoreAssetGuid& guid, bool environmentProfile) {
for (const auto& relativeRoot : {std::filesystem::path("Assets"), std::filesystem::path("Runtime")}) {
const auto root = request.ProjectRoot / relativeRoot;
std::error_code scanError;
if (!std::filesystem::is_directory(root, scanError)) continue;
for (std::filesystem::recursive_directory_iterator iterator(root, std::filesystem::directory_options::skip_permission_denied, scanError), end;
!scanError && iterator != end; iterator.increment(scanError)) {
if (!iterator->is_regular_file(scanError)) continue;
if (environmentProfile && iterator->path().extension() == ".mcenvironment") {
const auto document = MetaCoreReadEnvironmentProfile(iterator->path(), inputRegistry);
if (document && document->AssetGuid == guid) {
std::filesystem::copy_file(iterator->path(), profileOutputRoot / (guid.ToString() + ".mcenvironment"), std::filesystem::copy_options::overwrite_existing, scanError);
return !scanError;
}
} else if (!environmentProfile && iterator->path().extension() == ".mcpostprocess") {
const auto document = MetaCoreReadPostProcessProfile(iterator->path(), inputRegistry);
if (document && document->AssetGuid == guid) {
std::filesystem::copy_file(iterator->path(), profileOutputRoot / (guid.ToString() + ".mcpostprocess"), std::filesystem::copy_options::overwrite_existing, scanError);
return !scanError;
}
}
}
}
return false;
};
for (const auto& guid : environmentProfileGuids) if (!stageRenderingProfile(guid, true))
return fail(MetaCoreBuildErrorCode::CookFailed, "Referenced Environment Profile is missing, corrupt, or version-incompatible: " + guid.ToString(), absoluteStartup);
for (const auto& guid : postProcessProfileGuids) if (!stageRenderingProfile(guid, false))
return fail(MetaCoreBuildErrorCode::CookFailed, "Referenced Post-process Profile is missing, corrupt, or version-incompatible: " + guid.ToString(), absoluteStartup);
}
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", "grid.filamat", "render_debug.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 auto templateSourceRoot=request.EngineBuildRoot/"material_templates";
const auto templateOutputRoot=staging/"Engine"/"Materials"/"Templates";
if(!CopyTree(templateSourceRoot,templateOutputRoot))return fail(MetaCoreBuildErrorCode::StageFailed,"Compiled material templates are missing",templateSourceRoot);
MetaCoreMaterialTemplateRegistry materialTemplates;json templateManifest=json::array();
for(const auto& descriptor:materialTemplates.GetTemplates()){
const auto compiled=templateOutputRoot/descriptor.CompiledMaterialName;if(!std::filesystem::is_regular_file(compiled))return fail(MetaCoreBuildErrorCode::StageFailed,"Compiled material template is missing: "+descriptor.TemplateId,compiled);
const auto source=sourceRoot/"Source"/"MetaCoreRender"/"Private"/"Materials"/"Templates"/(std::filesystem::path(descriptor.CompiledMaterialName).stem().string()+".mat");
const auto sourceHash=MetaCoreSha256File(source);const auto compiledHash=MetaCoreSha256File(compiled);if(!sourceHash||!compiledHash)return fail(MetaCoreBuildErrorCode::StageFailed,"Failed to hash material template: "+descriptor.TemplateId,source);
MetaCoreMaterialCookRequest cookKey{descriptor.TemplateId,descriptor.Version,*sourceHash,profile->TargetPlatform,profile->GraphicsBackend,"matc-v71;platform=desktop"};
templateManifest.push_back({{"template_id",descriptor.TemplateId},{"template_version",descriptor.Version},{"source_sha256",*sourceHash},{"compiled_sha256",*compiledHash},{"cook_key",MetaCoreBuildMaterialCookKey(cookKey)},{"file",descriptor.CompiledMaterialName}});
}
if(!WriteJson(templateOutputRoot/"templates.manifest.json",templateManifest))return fail(MetaCoreBuildErrorCode::StageFailed,"Failed to write material template manifest",templateOutputRoot);
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