#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #include #endif namespace MetaCore { namespace { using json = nlohmann::json; [[nodiscard]] bool CopyTree(const std::filesystem::path& source, const std::filesystem::path& target); std::unordered_set MetaCoreCollectCookablePhysicsMeshes( const std::filesystem::path& projectRoot, const MetaCoreTypeRegistry& registry, const std::filesystem::path& derivedOutputRoot ) { std::unordered_set 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(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(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(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(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(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(bytes.data()), static_cast(bytes.size())); output.close(); if (!output) { std::error_code removeError; std::filesystem::remove(outputPath, removeError); continue; } result.insert(mesh.AssetGuid); } } } } return result; } std::optional MetaCoreValidateScenePhysicsForCook( const MetaCoreSceneDocument& scene, const std::unordered_set& 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 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 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 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 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 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(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 CollectManifestFiles(const std::filesystem::path& root) { std::vector 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 libraries; bool dependencyMissing = false; std::array buffer{}; while (fgets(buffer.data(), static_cast(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 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 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()); } } 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 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{}); 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 MetaCoreValidateReleasePackage(const std::filesystem::path& packageRoot) { std::vector 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 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 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 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 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 environmentProfileGuids; std::unordered_set 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