diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index c0ce96a..31780d4 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -1,4 +1,6 @@ #include "MetaCorePlatform/MetaCoreWindow.h" +#include "MetaCoreDelivery/MetaCoreBuildPipeline.h" +#include "MetaCoreDelivery/MetaCoreCrashReporter.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" @@ -12,6 +14,7 @@ #include "MetaCoreScene/MetaCoreScene.h" #include +#include #include #include #include @@ -19,6 +22,44 @@ namespace { +class MetaCoreTeeBuffer final : public std::streambuf { +public: + MetaCoreTeeBuffer(std::streambuf* first, std::streambuf* second) : First_(first), Second_(second) {} +protected: + int overflow(int character) override { + if (character == traits_type::eof()) return traits_type::not_eof(character); + const bool firstOk = First_ == nullptr || First_->sputc(static_cast(character)) != traits_type::eof(); + const bool secondOk = Second_ == nullptr || Second_->sputc(static_cast(character)) != traits_type::eof(); + return firstOk && secondOk ? character : traits_type::eof(); + } + int sync() override { + return (First_ == nullptr || First_->pubsync() == 0) && (Second_ == nullptr || Second_->pubsync() == 0) ? 0 : -1; + } +private: + std::streambuf* First_ = nullptr; + std::streambuf* Second_ = nullptr; +}; + +[[nodiscard]] std::filesystem::path MetaCoreGetExecutableRoot(int argc, char* argv[]) { +#if defined(__linux__) + std::error_code error; + const auto executable = std::filesystem::canonical("/proc/self/exe", error); + if (!error) return executable.parent_path(); +#endif + if (argc > 0) { + std::error_code error; + return std::filesystem::absolute(argv[0], error).parent_path(); + } + return std::filesystem::current_path(); +} + +void MetaCoreRotatePlayerLog(const std::filesystem::path& path) { + std::error_code error; + if (std::filesystem::is_regular_file(path, error) && std::filesystem::file_size(path, error) >= 5U * 1024U * 1024U) { + std::filesystem::rename(path, path.string() + ".1", error); + } +} + MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() { MetaCore::MetaCoreSceneView sceneView; sceneView.CameraPosition = {0.0F, 2.5F, 6.5F}; @@ -196,6 +237,9 @@ void MetaCoreResolveRuntimeSourcePathsForProject( int main(int argc, char* argv[]) { std::filesystem::path customProjectRoot{}; std::filesystem::path customScenePath{}; + std::filesystem::path runtimeProjectPath{}; + bool validateOnly = false; + std::uint64_t smokeTestFrames = 0U; for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "--project") == 0 && i + 1 < argc) { customProjectRoot = argv[i + 1]; @@ -203,38 +247,95 @@ int main(int argc, char* argv[]) { } else if (std::strcmp(argv[i], "--scene") == 0 && i + 1 < argc) { customScenePath = argv[i + 1]; ++i; + } else if (std::strcmp(argv[i], "--runtime-project") == 0 && i + 1 < argc) { + runtimeProjectPath = argv[++i]; + } else if (std::strcmp(argv[i], "--validate-only") == 0) { + validateOnly = true; + } else if (std::strcmp(argv[i], "--smoke-test-frames") == 0 && i + 1 < argc) { + smokeTestFrames = static_cast(std::strtoull(argv[++i], nullptr, 10)); } } - if (argc > 0) { - std::filesystem::current_path(std::filesystem::path(argv[0]).parent_path()); + const std::filesystem::path executableRoot = MetaCoreGetExecutableRoot(argc, argv); + if (runtimeProjectPath.empty() && customProjectRoot.empty() && + std::filesystem::is_regular_file(executableRoot / "Project" / "MetaCore.runtimeproject")) { + runtimeProjectPath = executableRoot / "Project" / "MetaCore.runtimeproject"; + } + std::optional deliveryDocument; + std::filesystem::path packageRoot; + if (!runtimeProjectPath.empty()) { + runtimeProjectPath = runtimeProjectPath.is_absolute() ? runtimeProjectPath : std::filesystem::absolute(runtimeProjectPath); + packageRoot = runtimeProjectPath.parent_path().parent_path(); + MetaCore::MetaCoreBuildIssue issue; + deliveryDocument = MetaCore::MetaCoreReadRuntimeDeliveryDocument(runtimeProjectPath, &issue); + if (!deliveryDocument) { + std::cerr << MetaCore::MetaCoreBuildErrorCodeName(issue.Code) << ": " << issue.Message << " path=" << issue.Path << '\n'; + return 2; + } + const auto validationIssues = MetaCore::MetaCoreValidateReleasePackage(packageRoot); + if (!validationIssues.empty()) { + for (const auto& validationIssue : validationIssues) { + std::cerr << MetaCore::MetaCoreBuildErrorCodeName(validationIssue.Code) << ": " << validationIssue.Message + << " path=" << validationIssue.Path << '\n'; + } + return 2; + } + if (validateOnly) { + std::cout << "MetaCorePlayer: delivery package validation passed build_id=" << deliveryDocument->BuildId << '\n'; + return 0; + } + } else if (validateOnly) { + std::cerr << "MCB1001: --validate-only requires a packaged runtime project\n"; + return 2; + } + + std::filesystem::current_path(deliveryDocument ? packageRoot : executableRoot); + const auto diagnosticsRoot = deliveryDocument ? packageRoot / "Diagnostics" : executableRoot / "Diagnostics"; + std::error_code logError; + std::filesystem::create_directories(diagnosticsRoot / "Logs", logError); + const auto logPath = diagnosticsRoot / "Logs" / "MetaCorePlayer.log"; + MetaCoreRotatePlayerLog(logPath); + std::ofstream logFile(logPath, std::ios::app); + MetaCoreTeeBuffer coutBuffer(std::cout.rdbuf(), logFile.rdbuf()); + MetaCoreTeeBuffer cerrBuffer(std::cerr.rdbuf(), logFile.rdbuf()); + std::ostream output(&coutBuffer); + std::ostream errors(&cerrBuffer); + output << "MetaCorePlayer: start version=1.0.0 platform=Linux-x86_64 build_id=" + << (deliveryDocument ? deliveryDocument->BuildId : "development") << '\n'; + if (!MetaCore::MetaCoreInitializeCrashReporter( + executableRoot, + diagnosticsRoot, + deliveryDocument ? deliveryDocument->BuildId : "development")) { + output << "MetaCorePlayer: Crashpad handler unavailable; Minidump capture disabled\n"; } MetaCore::MetaCoreWindow window; if (!window.Initialize(1280, 720, "MetaCore Player")) { - std::cerr << "MetaCorePlayer: window initialize failed\n"; + errors << "MetaCorePlayer: window initialize failed\n"; return 1; } MetaCore::MetaCoreRenderDevice renderDevice; if (!renderDevice.Initialize(window)) { - std::cerr << "MetaCorePlayer: render device initialize failed\n"; + errors << "MetaCorePlayer: render device initialize failed\n"; return 1; } MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; if (!viewportRenderer.Initialize(renderDevice, window, false)) { - std::cerr << "MetaCorePlayer: viewport renderer initialize failed\n"; + errors << "MetaCorePlayer: viewport renderer initialize failed\n"; return 1; } std::filesystem::path projectRoot; - if (!customProjectRoot.empty()) { + if (deliveryDocument) { + projectRoot = packageRoot / deliveryDocument->ContentRoot; + } else if (!customProjectRoot.empty()) { projectRoot = std::filesystem::absolute(customProjectRoot); } else { const auto discovered = MetaCore::MetaCoreDiscoverProjectRoot(); if (!discovered.has_value()) { - std::cerr << "MetaCorePlayer: failed to discover project root\n"; + errors << "MetaCorePlayer: failed to discover project root\n"; return 1; } projectRoot = *discovered; @@ -244,7 +345,7 @@ int main(int argc, char* argv[]) { MetaCore::MetaCoreTypeRegistry typeRegistry; MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(typeRegistry); const auto loadedRuntimeProjectDocument = MetaCore::MetaCoreReadRuntimeProjectDocument( - projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + deliveryDocument ? packageRoot / deliveryDocument->RuntimeConfig : projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", typeRegistry ); const auto runtimeProjectDocument = loadedRuntimeProjectDocument.value_or(MetaCoreBuildDefaultRuntimeProjectDocument()); @@ -254,10 +355,13 @@ int main(int argc, char* argv[]) { const auto absoluteScene = customScenePath.is_absolute() ? customScenePath : (projectRoot / customScenePath); startupSceneDocument = MetaCore::MetaCoreReadScenePackage(absoluteScene); } - if (!startupSceneDocument.has_value() && !runtimeProjectDocument.StartupScenePath.empty()) { + if (!startupSceneDocument.has_value() && deliveryDocument) { + startupSceneDocument = MetaCore::MetaCoreReadScenePackage(packageRoot / deliveryDocument->StartupScene); + } + if (!startupSceneDocument.has_value() && !deliveryDocument && !runtimeProjectDocument.StartupScenePath.empty()) { startupSceneDocument = MetaCore::MetaCoreReadScenePackage(projectRoot / runtimeProjectDocument.StartupScenePath); } - if (!startupSceneDocument.has_value()) { + if (!startupSceneDocument.has_value() && !deliveryDocument) { startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath); } MetaCore::MetaCoreScene scene; @@ -265,11 +369,14 @@ int main(int argc, char* argv[]) { MetaCore::MetaCoreSceneSnapshot snapshot; snapshot.GameObjects = startupSceneDocument->GameObjects; scene.RestoreSnapshot(snapshot); - std::cout << "MetaCorePlayer: loaded startup scene from project " + output << "MetaCorePlayer: loaded startup scene from project " << projectPath.string() << '\n'; - } else { + } else if (!deliveryDocument) { scene = MetaCore::MetaCoreCreateDefaultScene(); - std::cout << "MetaCorePlayer: startup scene unavailable, using built-in default scene\n"; + output << "MetaCorePlayer: startup scene unavailable, using built-in default scene\n"; + } else { + errors << "MCB6004: packaged startup scene is missing or unreadable path=" << deliveryDocument->StartupScene << '\n'; + return 2; } MetaCore::MetaCoreRuntimeDataDispatcher runtimeDataDispatcher(scene); @@ -289,25 +396,31 @@ int main(int argc, char* argv[]) { MetaCore::MetaCoreRuntimeUiSystem runtimeUi; if (loadedUiManifest.has_value()) { if (!runtimeUi.Initialize(window, projectRoot, projectRoot / "Ui", *loadedUiManifest)) { - std::cerr << "MetaCorePlayer: failed to initialize runtime UI\n"; + errors << "MetaCorePlayer: failed to initialize runtime UI\n"; } else { runtimeUi.AttachToViewportRenderer(viewportRenderer); } for (const std::string& error : runtimeUi.GetErrors()) { - std::cerr << "MetaCorePlayer UI: " << error << '\n'; + errors << "MetaCorePlayer UI: " << error << '\n'; } } if (loadedSourcesDocument.has_value() && loadedBindingsDocument.has_value()) { - std::cout << "MetaCorePlayer: loaded runtime config from binary .mcruntime documents\n"; + output << "MetaCorePlayer: loaded runtime config from binary .mcruntime documents\n"; } else { const bool sourcesExists = std::filesystem::exists(sourcesPath); const bool bindingsExists = std::filesystem::exists(bindingsPath); if ((sourcesExists && !loadedSourcesDocument.has_value()) || (bindingsExists && !loadedBindingsDocument.has_value())) { - std::cerr << "MetaCorePlayer: runtime config exists but is unreadable or corrupted, using built-in fallback config\n"; + errors << "MetaCorePlayer: runtime config exists but is unreadable or corrupted" + << (deliveryDocument ? "\n" : ", using built-in fallback config\n"); + if (deliveryDocument) return 2; } else { - std::cout << "MetaCorePlayer: runtime config missing, using built-in fallback config\n"; + if (deliveryDocument) { + errors << "MCB6004: packaged runtime configuration is missing\n"; + return 2; + } + output << "MetaCorePlayer: runtime config missing, using built-in fallback config\n"; } } @@ -318,17 +431,17 @@ int main(int argc, char* argv[]) { if (!sourcesDocument.Sources.empty()) { runtimeAdapter = MetaCore::MetaCoreCreateRuntimeDataSourceAdapter(sourcesDocument.Sources.front().AdapterType); if (runtimeAdapter == nullptr) { - std::cerr << "MetaCorePlayer: unsupported adapter type " + errors << "MetaCorePlayer: unsupported adapter type " << sourcesDocument.Sources.front().AdapterType << '\n'; return 1; } if (!runtimeAdapter->Configure(sourcesDocument.Sources.front())) { - std::cerr << "MetaCorePlayer: failed to configure runtime adapter error=" + errors << "MetaCorePlayer: failed to configure runtime adapter error=" << runtimeAdapter->GetStatus().LastError << '\n'; return 1; } if (!runtimeAdapter->Connect()) { - std::cerr << "MetaCorePlayer: failed to connect runtime adapter error=" + errors << "MetaCorePlayer: failed to connect runtime adapter error=" << runtimeAdapter->GetStatus().LastError << '\n'; return 1; } @@ -341,6 +454,7 @@ int main(int argc, char* argv[]) { std::unordered_map lastReportedBindingIssueKeys; std::uint64_t diagnosticsWriteFrame = 0; + std::uint64_t renderedFrames = 0U; while (!window.ShouldClose()) { window.BeginFrame(); const auto [windowWidth, windowHeight] = window.GetWindowSize(); @@ -367,7 +481,7 @@ int main(int argc, char* argv[]) { } ++diagnosticsWriteFrame; if (runtimeAdapter != nullptr && runtimeAdapter->GetStatus().State != lastReportedSourceState) { - std::cout << "MetaCorePlayer: data source state changed to " + output << "MetaCorePlayer: data source state changed to " << static_cast(runtimeAdapter->GetStatus().State) << " error=" << runtimeAdapter->GetStatus().LastError << '\n'; lastReportedSourceState = runtimeAdapter->GetStatus().State; @@ -384,7 +498,7 @@ int main(int argc, char* argv[]) { const auto previousIssue = lastReportedBindingIssueKeys.find(bindingStatus.BindingId); if (previousIssue == lastReportedBindingIssueKeys.end() || previousIssue->second != issueKey) { - std::cout << "MetaCorePlayer: binding issue id=" << bindingStatus.BindingId + output << "MetaCorePlayer: binding issue id=" << bindingStatus.BindingId << " stale=" << (bindingStatus.Stale ? "true" : "false") << " error=" << bindingStatus.LastError << '\n'; lastReportedBindingIssueKeys[bindingStatus.BindingId] = issueKey; @@ -392,7 +506,7 @@ int main(int argc, char* argv[]) { } else { const auto previousIssue = lastReportedBindingIssueKeys.find(bindingStatus.BindingId); if (previousIssue != lastReportedBindingIssueKeys.end()) { - std::cout << "MetaCorePlayer: binding recovered id=" << bindingStatus.BindingId << '\n'; + output << "MetaCorePlayer: binding recovered id=" << bindingStatus.BindingId << '\n'; lastReportedBindingIssueKeys.erase(previousIssue); } } @@ -404,6 +518,8 @@ int main(int argc, char* argv[]) { renderDevice.RenderFrame(); renderDevice.PresentFrame(); window.EndFrame(); + ++renderedFrames; + if (smokeTestFrames > 0U && renderedFrames >= smokeTestFrames) break; } runtimeUi.Shutdown(); diff --git a/CMakeLists.txt b/CMakeLists.txt index f9627fe..0f59997 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ endif() find_package(glm CONFIG REQUIRED) +find_package(crashpad CONFIG QUIET) set(METACORE_IMGUI_USES_PKG_CONFIG FALSE) if(NOT METACORE_BUILD_CORE_ONLY) include(MetaCoreFilament) @@ -477,6 +478,36 @@ target_link_libraries(MetaCoreScene target_compile_options(MetaCoreScene PRIVATE ${METACORE_COMMON_WARNINGS}) +add_library(MetaCoreDelivery STATIC + Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h + Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreCrashReporter.h + Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp + Source/MetaCoreDelivery/Private/MetaCoreCrashReporter.cpp +) +target_include_directories(MetaCoreDelivery + PUBLIC Source/MetaCoreDelivery/Public + PRIVATE third_party +) +target_link_libraries(MetaCoreDelivery + PUBLIC MetaCoreFoundation MetaCoreScene +) +target_compile_options(MetaCoreDelivery PRIVATE ${METACORE_COMMON_WARNINGS}) +if(crashpad_FOUND) + target_link_libraries(MetaCoreDelivery PUBLIC crashpad::crashpad) + target_compile_definitions(MetaCoreDelivery PUBLIC METACORE_HAS_CRASHPAD=1) +else() + target_compile_definitions(MetaCoreDelivery PUBLIC METACORE_HAS_CRASHPAD=0) + message(WARNING "Crashpad was not found; release packaging works but local Minidump capture is disabled.") +endif() + +add_executable(MetaCoreBuildTool tools/MetaCoreBuildTool/main.cpp) +target_link_libraries(MetaCoreBuildTool PRIVATE MetaCoreDelivery) +target_compile_options(MetaCoreBuildTool PRIVATE ${METACORE_COMMON_WARNINGS}) + +add_executable(MetaCoreDiagnosticsTool tools/MetaCoreDiagnosticsTool/main.cpp) +target_link_libraries(MetaCoreDiagnosticsTool PRIVATE MetaCoreDelivery) +target_compile_options(MetaCoreDiagnosticsTool PRIVATE ${METACORE_COMMON_WARNINGS}) + if(NOT METACORE_BUILD_CORE_ONLY) set(METACORE_RENDER_HEADERS Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h @@ -573,6 +604,7 @@ target_link_libraries(MetaCoreRuntimeConfigTool MetaCoreFoundation MetaCoreRuntimeData MetaCoreScene + MetaCoreDelivery ) endif() @@ -654,6 +686,7 @@ target_link_libraries(MetaCoreEditor MetaCoreRender MetaCoreRuntimeData MetaCoreScene + MetaCoreDelivery glm::glm imgui::imgui ) @@ -740,7 +773,14 @@ target_link_libraries(MetaCorePlayer MetaCoreRuntimeData MetaCoreScene MetaCoreRuntimeUi + MetaCoreDelivery ) +if(UNIX AND NOT APPLE) + set_target_properties(MetaCorePlayer PROPERTIES + BUILD_RPATH "\$ORIGIN/lib" + INSTALL_RPATH "\$ORIGIN/lib" + ) +endif() metacore_stage_ui_blit_material(MetaCorePlayer) metacore_stage_rml_ui_material(MetaCorePlayer) @@ -749,6 +789,11 @@ metacore_stage_rml_ui_material(MetaCorePlayer) if(METACORE_BUILD_TESTS) enable_testing() + add_executable(MetaCoreDeliveryTests tests/MetaCoreDeliveryTests.cpp) + target_link_libraries(MetaCoreDeliveryTests PRIVATE MetaCoreDelivery) + target_compile_options(MetaCoreDeliveryTests PRIVATE ${METACORE_COMMON_WARNINGS}) + add_test(NAME MetaCoreDeliveryTests COMMAND MetaCoreDeliveryTests) + add_executable(MetaCoreSmokeTests tests/MetaCoreSmokeTests.cpp Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp diff --git a/CMakePresets.json b/CMakePresets.json index 84b924c..0bdbb6b 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -32,6 +32,32 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } + }, + { + "name": "linux-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "CMAKE_CXX_STANDARD": "20", + "VCPKG_TARGET_TRIPLET": "x64-linux-clang-libcxx", + "VCPKG_OVERLAY_TRIPLETS": "${sourceDir}/vcpkg-triplets", + "METACORE_BUILD_TESTS": "ON" + }, + "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } + }, + { + "name": "linux-development", + "inherits": "linux-base", + "displayName": "Linux x64 Development", + "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" } + }, + { + "name": "linux-release", + "inherits": "linux-base", + "displayName": "Linux x64 Release", + "cacheVariables": { "CMAKE_BUILD_TYPE": "Release" } } ], "buildPresets": [ @@ -44,6 +70,14 @@ "name": "build-release", "configurePreset": "vs2022-release", "configuration": "Release" + }, + { + "name": "build-linux-development", + "configurePreset": "linux-development" + }, + { + "name": "build-linux-release", + "configurePreset": "linux-release" } ], "testPresets": [ @@ -54,6 +88,11 @@ "output": { "outputOnFailure": true } + }, + { + "name": "test-linux-development", + "configurePreset": "linux-development", + "output": { "outputOnFailure": true } } ] } diff --git a/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp b/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp new file mode 100644 index 0000000..92f7ef2 --- /dev/null +++ b/Source/MetaCoreDelivery/Private/MetaCoreBuildPipeline.cpp @@ -0,0 +1,690 @@ +#include "MetaCoreDelivery/MetaCoreBuildPipeline.h" + +#include "MetaCoreFoundation/MetaCoreHash.h" +#include "MetaCoreFoundation/MetaCorePackage.h" +#include "MetaCoreFoundation/MetaCoreProject.h" +#include "MetaCoreScene/MetaCoreScenePackage.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__linux__) +#include +#endif + +namespace MetaCore { +namespace { + +using json = nlohmann::json; + +[[nodiscard]] std::string ConfigurationName(MetaCoreBuildConfiguration configuration) { + switch (configuration) { + case MetaCoreBuildConfiguration::Debug: return "Debug"; + case MetaCoreBuildConfiguration::Development: return "Development"; + case MetaCoreBuildConfiguration::Release: return "Release"; + } + return "Development"; +} + +[[nodiscard]] std::string CMakeConfigurationName(MetaCoreBuildConfiguration configuration) { + switch (configuration) { + case MetaCoreBuildConfiguration::Debug: return "Debug"; + case MetaCoreBuildConfiguration::Development: return "RelWithDebInfo"; + case MetaCoreBuildConfiguration::Release: return "Release"; + } + return "RelWithDebInfo"; +} + +[[nodiscard]] bool IsSafeRelativePath(const std::filesystem::path& path) { + if (path.empty() || path.is_absolute()) return false; + const auto normalized = path.lexically_normal(); + return normalized != ".." && (normalized.empty() || *normalized.begin() != ".."); +} + +[[nodiscard]] bool IsSafeIdentifier(std::string_view value) { + if (value.empty()) return false; + return std::all_of(value.begin(), value.end(), [](unsigned char character) { + return std::isalnum(character) != 0 || character == '-' || character == '_' || character == '.'; + }); +} + +[[nodiscard]] std::string ShellQuote(const std::filesystem::path& path) { + std::string result = "'"; + for (const char character : path.string()) { + result += character == '\'' ? "'\\''" : std::string(1, character); + } + result += "'"; + return result; +} + +[[nodiscard]] bool RunCommand(const std::string& command) { + return std::system(command.c_str()) == 0; +} + +[[nodiscard]] std::optional 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 (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; + } + } + return document; +} + +bool MetaCoreWriteRuntimeDeliveryDocument(const std::filesystem::path& path, const MetaCoreRuntimeDeliveryDocument& document) { + 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()} + }); +} + +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}); + 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); + const auto outputRoot = request.ProjectRoot / profile->OutputDirectory / profile->TargetPlatform / project->Name / profile->Name; + const std::string packageName = project->Name + "-" + project->Version + "-" + result.BuildId; + const auto staging = outputRoot / (".staging-" + result.BuildId); + result.PackageDirectory = outputRoot / packageName; + result.ArchivePath = outputRoot / (packageName + ".tar.gz"); + result.SymbolsArchivePath = outputRoot / (packageName + ".symbols.tar.gz"); + std::error_code error; + if (request.Clean && std::filesystem::is_directory(outputRoot, error)) { + for (const auto& entry : std::filesystem::directory_iterator(outputRoot, error)) + if (entry.path().filename().string().rfind(".staging-", 0) == 0) std::filesystem::remove_all(entry.path(), error); + } + std::filesystem::remove_all(staging, error); + std::filesystem::create_directories(staging / "Content" / "Scenes", error); + std::filesystem::create_directories(staging / "Content" / "Assets", error); + std::filesystem::create_directories(staging / "Content" / "Ui", error); + std::filesystem::create_directories(staging / "Content" / "Runtime", error); + std::filesystem::create_directories(staging / "Engine" / "Materials", error); + std::filesystem::create_directories(staging / "Engine" / "Fonts", error); + std::filesystem::create_directories(staging / "Diagnostics" / "Logs", error); + if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to create staging directory", staging); + + report(MetaCoreBuildStage::Cook, "Cooking startup scene and project content"); + std::filesystem::path startupSource = profile->StartupScene.empty() ? project->StartupScenePath : profile->StartupScene; + if (!IsSafeRelativePath(startupSource)) return fail(MetaCoreBuildErrorCode::UnsafePath, "Startup scene path is unsafe", startupSource); + const auto absoluteStartup = request.ProjectRoot / startupSource; + std::filesystem::path cookedSceneName = startupSource.filename(); + if (cookedSceneName.extension() == ".json") cookedSceneName.replace_extension(""); + const auto cookedScene = staging / "Content" / "Scenes" / cookedSceneName; + if (startupSource.extension() == ".json") { + const auto registry = MetaCoreBuildScenePackageTypeRegistry(); + const auto scene = MetaCoreSceneSerializer::LoadSceneFromJson(absoluteStartup, registry); + if (!scene || !MetaCoreWriteScenePackage(cookedScene, *scene)) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to cook startup scene", absoluteStartup); + } else { + std::filesystem::copy_file(absoluteStartup, cookedScene, std::filesystem::copy_options::overwrite_existing, error); + if (error) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to copy startup scene", absoluteStartup); + } + const auto currentCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / profile->Name; + const auto editorCookRoot = request.ProjectRoot / "Library" / "Cooked" / profile->TargetPlatform / "Editor"; + const auto legacyCookRoot = request.ProjectRoot / "Library" / "Cooked" / "Windows"; + bool copiedCooked = false; + if (std::filesystem::is_directory(currentCookRoot)) copiedCooked = CopyTree(currentCookRoot, staging / "Content" / "Assets"); + else if (std::filesystem::is_directory(editorCookRoot)) copiedCooked = CopyTree(editorCookRoot, staging / "Content" / "Assets"); + else if (std::filesystem::is_directory(legacyCookRoot)) copiedCooked = CopyTree(legacyCookRoot, staging / "Content" / "Assets"); + if ((profile->ResourcePolicy == MetaCoreBuildResourcePolicy::AllProjectContent || !copiedCooked) && std::filesystem::is_directory(request.ProjectRoot / "Assets")) + if (!CopyTree(request.ProjectRoot / "Assets", staging / "Content" / "Assets")) return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to collect project assets", request.ProjectRoot / "Assets"); + if (std::filesystem::is_directory(request.ProjectRoot / project->UiDirectory) && + !CopyTree(request.ProjectRoot / project->UiDirectory, staging / "Content" / "Ui")) + return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to collect project UI", request.ProjectRoot / project->UiDirectory); + if (std::filesystem::is_directory(request.ProjectRoot / project->RuntimeDirectory) && + !CopyTree(request.ProjectRoot / project->RuntimeDirectory, staging / "Content" / "Runtime")) + return fail(MetaCoreBuildErrorCode::CookFailed, "Failed to collect project runtime configuration", request.ProjectRoot / project->RuntimeDirectory); + if (cancelled()) { std::filesystem::remove_all(staging, error); return fail(MetaCoreBuildErrorCode::Cancelled, "Build cancelled"); } + + report(MetaCoreBuildStage::Stage, "Staging executable and runtime dependencies"); + std::filesystem::copy_file(player, staging / player.filename(), std::filesystem::copy_options::overwrite_existing, error); + if (error) return fail(MetaCoreBuildErrorCode::StageFailed, "Failed to stage MetaCorePlayer", player); + std::filesystem::permissions(staging / player.filename(), std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error); + for (const char* material : {"uiBlit.filamat", "rml_ui.filamat"}) { + const auto source = request.EngineBuildRoot / material; + if (!std::filesystem::is_regular_file(source)) return fail(MetaCoreBuildErrorCode::StageFailed, "Required engine material is missing", source); + std::filesystem::copy_file(source, staging / "Engine" / "Materials" / material, std::filesystem::copy_options::overwrite_existing, error); + } + const std::array fontCandidates = { + sourceRoot / "third_party" / "filament_installed" / "bin" / "assets" / "fonts" / "Roboto-Medium.ttf", + sourceRoot / "third_party" / "filament_installed.before-linux-sdk" / "bin" / "assets" / "fonts" / "Roboto-Medium.ttf" + }; + bool fontCopied = false; + for (const auto& font : fontCandidates) if (!fontCopied && std::filesystem::is_regular_file(font)) { + std::filesystem::copy_file(font, staging / "Engine" / "Fonts" / font.filename(), std::filesystem::copy_options::overwrite_existing, error); + fontCopied = !error; + } + if (!fontCopied) return fail(MetaCoreBuildErrorCode::StageFailed, "Required runtime font is missing", sourceRoot); + if (!CopyLinuxRuntimeLibraries(player, staging / "lib")) return fail(MetaCoreBuildErrorCode::DependencyMissing, "Failed to inspect or stage Linux C++ runtime libraries", player); + const std::array crashpadCandidates = { + sourceRoot / "vcpkg_installed" / "x64-linux-clang-libcxx" / "tools" / "crashpad" / "crashpad_handler", + request.EngineBuildRoot / "vcpkg_installed" / "x64-linux-clang-libcxx" / "tools" / "crashpad" / "crashpad_handler" + }; + bool crashpadHandlerCopied = false; + for (const auto& handler : crashpadCandidates) { + if (!std::filesystem::is_regular_file(handler)) continue; + std::filesystem::create_directories(staging / "Crash", error); + std::filesystem::copy_file(handler, staging / "Crash" / "crashpad_handler", std::filesystem::copy_options::overwrite_existing, error); + std::filesystem::permissions(staging / "Crash" / "crashpad_handler", std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec | std::filesystem::perms::others_exec, std::filesystem::perm_options::add, error); + crashpadHandlerCopied = !error; + break; + } +#if defined(METACORE_HAS_CRASHPAD) && METACORE_HAS_CRASHPAD + if (!crashpadHandlerCopied) return fail(MetaCoreBuildErrorCode::DependencyMissing, "Crashpad is linked but crashpad_handler was not found", sourceRoot); +#else + (void)crashpadHandlerCopied; +#endif + + MetaCoreRuntimeDeliveryDocument delivery; + delivery.BuildId = result.BuildId; + delivery.StartupScene = std::filesystem::path("Content") / "Scenes" / cookedSceneName; + if (!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()); + } + } + + 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 diff --git a/Source/MetaCoreDelivery/Private/MetaCoreCrashReporter.cpp b/Source/MetaCoreDelivery/Private/MetaCoreCrashReporter.cpp new file mode 100644 index 0000000..a03b9bf --- /dev/null +++ b/Source/MetaCoreDelivery/Private/MetaCoreCrashReporter.cpp @@ -0,0 +1,45 @@ +#include "MetaCoreDelivery/MetaCoreCrashReporter.h" + +#if defined(METACORE_HAS_CRASHPAD) && METACORE_HAS_CRASHPAD +#include +#include +#include +#endif + +#include +#include +#include + +namespace MetaCore { + +bool MetaCoreInitializeCrashReporter( + const std::filesystem::path& executableRoot, + const std::filesystem::path& diagnosticsRoot, + std::string_view buildId +) { +#if defined(METACORE_HAS_CRASHPAD) && METACORE_HAS_CRASHPAD + const std::filesystem::path handler = executableRoot / "Crash" / "crashpad_handler"; + const std::filesystem::path database = diagnosticsRoot / "Crashpad"; + const std::filesystem::path metrics = diagnosticsRoot / "CrashpadMetrics"; + std::error_code error; + std::filesystem::create_directories(database, error); + std::filesystem::create_directories(metrics, error); + if (error || !std::filesystem::is_regular_file(handler)) return false; + std::map annotations = { + {"product", "MetaCorePlayer"}, {"version", "1.0.0"}, {"build_id", std::string(buildId)} + }; + std::vector arguments = {"--no-rate-limit"}; + static crashpad::CrashpadClient client; + return client.StartHandler( + base::FilePath(handler.string()), base::FilePath(database.string()), base::FilePath(metrics.string()), + "", annotations, arguments, false, false + ); +#else + (void)executableRoot; + (void)diagnosticsRoot; + (void)buildId; + return false; +#endif +} + +} // namespace MetaCore diff --git a/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h new file mode 100644 index 0000000..1872786 --- /dev/null +++ b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreBuildPipeline.h @@ -0,0 +1,162 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +inline constexpr std::uint32_t GMetaCoreReleaseManifestVersion = 1U; +inline constexpr std::uint32_t GMetaCoreRuntimeDeliveryVersion = 1U; +inline constexpr std::uint32_t GMetaCoreRuntimeAbiMajor = 1U; +inline constexpr std::uint32_t GMetaCoreRuntimeAbiMinor = 0U; + +enum class MetaCoreBuildConfiguration { Debug, Development, Release }; +enum class MetaCoreBuildResourcePolicy { ReferencedOnly, AllProjectContent }; +enum class MetaCoreBuildStage { Build, Cook, Stage, Package, Validate }; + +enum class MetaCoreBuildErrorCode { + None, + InvalidArguments, + InvalidProfile, + UnsupportedPlatform, + UnsafePath, + ProjectUnreadable, + BuildFailed, + CookFailed, + StageFailed, + PackageFailed, + ManifestUnreadable, + ManifestVersionUnsupported, + RuntimeAbiIncompatible, + RequiredFileMissing, + FileSizeMismatch, + FileHashMismatch, + DependencyMissing, + Cancelled +}; + +struct MetaCoreBuildProfile { + std::uint32_t FormatVersion = 1U; + std::string Name = "LinuxDevelopment"; + std::string TargetPlatform = "Linux-x86_64"; + MetaCoreBuildConfiguration Configuration = MetaCoreBuildConfiguration::Development; + std::string GraphicsBackend = "OpenGL"; + std::filesystem::path StartupScene{}; + std::filesystem::path OutputDirectory = "Build"; + MetaCoreBuildResourcePolicy ResourcePolicy = MetaCoreBuildResourcePolicy::ReferencedOnly; +}; + +struct MetaCoreRuntimeDeliveryDocument { + std::uint32_t FormatVersion = GMetaCoreRuntimeDeliveryVersion; + std::uint32_t RuntimeAbiMajor = GMetaCoreRuntimeAbiMajor; + std::uint32_t RuntimeAbiMinor = GMetaCoreRuntimeAbiMinor; + std::string BuildId{}; + std::filesystem::path ContentRoot = "Content"; + std::filesystem::path StartupScene{}; + std::filesystem::path RuntimeConfig = "Content/Runtime/ProjectRuntime.mcruntimecfg"; + std::filesystem::path UiManifest = "Content/Runtime/Ui.mcruntime"; + std::filesystem::path ReleaseManifest = "Manifest/MetaCore.release.json"; +}; + +struct MetaCoreReleaseFileEntry { + std::filesystem::path Path{}; + std::string Role{}; + std::uint64_t Size = 0U; + std::string Sha256{}; + bool Required = true; +}; + +struct MetaCoreReleaseManifestDocument { + std::uint32_t FormatVersion = GMetaCoreReleaseManifestVersion; + std::uint32_t RuntimeAbiMajor = GMetaCoreRuntimeAbiMajor; + std::uint32_t RuntimeAbiMinor = GMetaCoreRuntimeAbiMinor; + std::uint32_t PackageFormatVersion = 1U; + std::string EngineVersion = "1.0.0"; + std::string ProjectName{}; + std::string ProjectVersion{}; + std::string BuildId{}; + std::string GitCommit{}; + std::string TargetPlatform{}; + std::string Configuration{}; + std::string GraphicsBackend{}; + std::vector SystemDependencies{}; + std::vector Files{}; +}; + +struct MetaCoreBuildIssue { + MetaCoreBuildErrorCode Code = MetaCoreBuildErrorCode::None; + std::string Message{}; + std::filesystem::path Path{}; + std::string Expected{}; + std::string Actual{}; +}; + +struct MetaCoreBuildRequest { + std::filesystem::path ProjectRoot{}; + std::filesystem::path ProfilePath{}; + std::filesystem::path EngineBuildRoot{}; + bool Clean = false; + bool SkipBuild = false; +}; + +struct MetaCoreBuildResult { + bool Success = false; + std::string BuildId{}; + std::filesystem::path PackageDirectory{}; + std::filesystem::path ArchivePath{}; + std::filesystem::path SymbolsArchivePath{}; + std::vector Issues{}; +}; + +using MetaCoreBuildProgressCallback = std::function; + +[[nodiscard]] const char* MetaCoreBuildErrorCodeName(MetaCoreBuildErrorCode code); +[[nodiscard]] const char* MetaCoreBuildStageName(MetaCoreBuildStage stage); +[[nodiscard]] std::optional MetaCoreReadBuildProfile( + const std::filesystem::path& path, + MetaCoreBuildIssue* issue = nullptr +); +[[nodiscard]] bool MetaCoreWriteBuildProfile(const std::filesystem::path& path, const MetaCoreBuildProfile& profile); +[[nodiscard]] std::optional MetaCoreReadRuntimeDeliveryDocument( + const std::filesystem::path& path, + MetaCoreBuildIssue* issue = nullptr +); +[[nodiscard]] bool MetaCoreWriteRuntimeDeliveryDocument( + const std::filesystem::path& path, + const MetaCoreRuntimeDeliveryDocument& document +); +[[nodiscard]] std::optional MetaCoreReadReleaseManifest( + const std::filesystem::path& path, + MetaCoreBuildIssue* issue = nullptr +); +[[nodiscard]] bool MetaCoreWriteReleaseManifest( + const std::filesystem::path& path, + const MetaCoreReleaseManifestDocument& document +); +[[nodiscard]] std::vector MetaCoreValidateReleasePackage( + const std::filesystem::path& packageRoot +); +[[nodiscard]] bool MetaCoreExportDiagnostics( + const std::filesystem::path& packageRoot, + const std::filesystem::path& outputArchive, + MetaCoreBuildIssue* issue = nullptr +); + +class MetaCoreBuildPipeline { +public: + void RequestCancel(); + [[nodiscard]] MetaCoreBuildResult Run( + const MetaCoreBuildRequest& request, + MetaCoreBuildProgressCallback progress = {} + ); + +private: + std::atomic_bool CancelRequested_{false}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreCrashReporter.h b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreCrashReporter.h new file mode 100644 index 0000000..609299d --- /dev/null +++ b/Source/MetaCoreDelivery/Public/MetaCoreDelivery/MetaCoreCrashReporter.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace MetaCore { + +// Starts the local-only crash handler. No upload URL is configured. +[[nodiscard]] bool MetaCoreInitializeCrashReporter( + const std::filesystem::path& executableRoot, + const std::filesystem::path& diagnosticsRoot, + std::string_view buildId +); + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 23d692f..09006b3 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -6442,7 +6442,12 @@ bool MetaCoreBuiltinAssetDatabaseService::SetStartupScenePath(const std::filesys } [[nodiscard]] std::filesystem::path MetaCoreBuildCookManifestPath(const MetaCoreProjectDescriptor& project) { - return project.LibraryPath / "Cooked" / "Windows" / "CookManifest.bin"; +#if defined(_WIN32) + constexpr std::string_view platform = "Windows-x86_64"; +#else + constexpr std::string_view platform = "Linux-x86_64"; +#endif + return project.LibraryPath / "Cooked" / platform / "Editor" / "CookManifest.bin"; } [[nodiscard]] bool MetaCoreCopyDirectoryTree( @@ -6658,8 +6663,13 @@ bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) { return true; } +#if defined(_WIN32) + constexpr std::string_view cookPlatform = "Windows-x86_64"; +#else + constexpr std::string_view cookPlatform = "Linux-x86_64"; +#endif const std::filesystem::path relativeCookedPath = - std::filesystem::path("Library") / "Cooked" / "Windows" / + std::filesystem::path("Library") / "Cooked" / cookPlatform / "Editor" / (assetGuid.ToString() + "_" + MetaCoreFormatHex64(cookedKey) + ".mccooked"); const std::filesystem::path absoluteCookedPath = AssetDatabaseService_->GetProjectDescriptor().RootPath / relativeCookedPath; std::filesystem::create_directories(absoluteCookedPath.parent_path()); diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index 3098c8c..d40838f 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -7,6 +7,7 @@ #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" #include "MetaCoreEditor/MetaCoreEditorServices.h" +#include "MetaCoreDelivery/MetaCoreBuildPipeline.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreEditorCameraController.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" @@ -46,6 +47,8 @@ #include #include #include +#include +#include #include #include @@ -2575,7 +2578,9 @@ public: MetaCoreLaunchPlayer(editorContext, *assetDatabaseService, *scenePersistenceService); } ImGui::Separator(); - ImGui::MenuItem("构建设置", nullptr, false, false); + if (ImGui::MenuItem("构建设置")) { + editorContext.GetModuleRegistry().AccessPanelOpenState("BuildSettings") = true; + } ImGui::MenuItem("偏好设置", nullptr, false, false); ImGui::Separator(); if (ImGui::MenuItem("退出")) { @@ -9462,6 +9467,120 @@ private: std::array NameBuffer_{}; }; +class MetaCoreBuildSettingsPanelProvider final : public MetaCoreIEditorPanelProvider { +public: + std::string GetPanelId() const override { return "BuildSettings"; } + std::string GetPanelTitle() const override { return "构建设置"; } + bool IsOpenByDefault() const override { return false; } + + void DrawPanel(MetaCoreEditorContext& editorContext) override { + const auto assetDatabase = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabase == nullptr || !assetDatabase->HasProject()) { + ImGui::TextUnformatted("请先打开项目。"); + return; + } + const auto& project = assetDatabase->GetProjectDescriptor(); + const std::filesystem::path defaultProfile = project.RootPath / "BuildProfiles" / "LinuxDevelopment.mcbuildprofile.json"; + if (LoadedProjectRoot_ != project.RootPath) { + LoadedProjectRoot_ = project.RootPath; + ProfilePath_ = defaultProfile; + EngineBuildRoot_ = std::filesystem::current_path(); + MetaCoreBuildIssue issue; + if (const auto loaded = MetaCoreReadBuildProfile(ProfilePath_, &issue)) Profile_ = *loaded; + else Profile_ = MetaCoreBuildProfile{}; + } + + ImGui::Text("Profile: %s", ProfilePath_.generic_string().c_str()); + ImGui::Text("平台: %s", Profile_.TargetPlatform.c_str()); + ImGui::Text("图形后端: %s", Profile_.GraphicsBackend.c_str()); + int configuration = static_cast(Profile_.Configuration); + const char* configurations[] = {"Debug", "Development", "Release"}; + if (ImGui::Combo("配置", &configuration, configurations, 3)) Profile_.Configuration = static_cast(configuration); + bool allContent = Profile_.ResourcePolicy == MetaCoreBuildResourcePolicy::AllProjectContent; + if (ImGui::Checkbox("包含全部项目内容", &allContent)) + Profile_.ResourcePolicy = allContent ? MetaCoreBuildResourcePolicy::AllProjectContent : MetaCoreBuildResourcePolicy::ReferencedOnly; + ImGui::Text("启动场景: %s", Profile_.StartupScene.generic_string().c_str()); + ImGui::Text("引擎构建目录: %s", EngineBuildRoot_.generic_string().c_str()); + + if (ImGui::Button("保存 Profile")) { + if (MetaCoreWriteBuildProfile(ProfilePath_, Profile_)) + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Build", "Build Profile 已保存"); + else editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Build", "Build Profile 保存失败"); + } + ImGui::SameLine(); + const bool running = Future_.valid() && Future_.wait_for(std::chrono::seconds(0)) != std::future_status::ready; + if (running) ImGui::BeginDisabled(); + if (ImGui::Button("Build / Package")) { + (void)MetaCoreWriteBuildProfile(ProfilePath_, Profile_); + Pipeline_ = std::make_shared(); + MetaCoreBuildRequest request; + request.ProjectRoot = project.RootPath; + request.ProfilePath = ProfilePath_; + request.EngineBuildRoot = EngineBuildRoot_; + request.Clean = true; + const auto pipeline = Pipeline_; + Future_ = std::async(std::launch::async, [this, pipeline, request]() { + return pipeline->Run(request, [this](MetaCoreBuildStage stage, std::string_view message) { + std::scoped_lock lock(ProgressMutex_); + Progress_ = std::string(MetaCoreBuildStageName(stage)) + ": " + std::string(message); + }); + }); + } + if (running) ImGui::EndDisabled(); + if (running) { + ImGui::SameLine(); + if (ImGui::Button("取消")) Pipeline_->RequestCancel(); + std::string progress; + { + std::scoped_lock lock(ProgressMutex_); + progress = Progress_; + } + ImGui::TextUnformatted(progress.empty() ? "构建进行中……" : progress.c_str()); + } else if (Future_.valid()) { + LastResult_ = Future_.get(); + Pipeline_.reset(); + editorContext.AddConsoleMessage( + LastResult_.Success ? MetaCoreLogLevel::Info : MetaCoreLogLevel::Error, + "Build", + LastResult_.Success ? "发布包构建与校验成功" : "发布包构建失败" + ); + } + if (!LastResult_.PackageDirectory.empty()) { + ImGui::Separator(); + ImGui::TextWrapped("输出: %s", LastResult_.PackageDirectory.generic_string().c_str()); + if (ImGui::Button("Validate")) { + LastResult_.Issues = MetaCoreValidateReleasePackage(LastResult_.PackageDirectory); + LastResult_.Success = LastResult_.Issues.empty(); + } + ImGui::SameLine(); + if (ImGui::Button("打开输出目录")) { +#if defined(_WIN32) + ShellExecuteW(nullptr, L"open", LastResult_.PackageDirectory.wstring().c_str(), nullptr, nullptr, SW_SHOWNORMAL); +#else + const std::string outputPath = LastResult_.PackageDirectory.string(); + if (outputPath.find('\'') == std::string::npos) { + (void)std::system(("xdg-open '" + outputPath + "' >/dev/null 2>&1 &").c_str()); + } +#endif + } + for (const auto& issue : LastResult_.Issues) { + ImGui::TextWrapped("%s: %s (%s)", MetaCoreBuildErrorCodeName(issue.Code), issue.Message.c_str(), issue.Path.generic_string().c_str()); + } + } + } + +private: + std::filesystem::path LoadedProjectRoot_{}; + std::filesystem::path ProfilePath_{}; + std::filesystem::path EngineBuildRoot_{}; + MetaCoreBuildProfile Profile_{}; + std::shared_ptr Pipeline_{}; + MetaCoreBuildResult LastResult_{}; + std::mutex ProgressMutex_{}; + std::string Progress_{}; + std::future Future_{}; +}; + class MetaCoreBuiltinEditorViewsModule final : public MetaCoreIModule { public: std::string GetModuleName() const override { return "MetaCoreBuiltinEditorViewsModule"; } @@ -9479,6 +9598,7 @@ public: moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); + moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterInspectorDrawer(std::make_unique()); } diff --git a/Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp b/Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp index 330a5d0..3aa02f8 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreAssetDependencyGraph.cpp @@ -59,6 +59,27 @@ MetaCoreAssetDependencyGraph::AssetGuidSet MetaCoreAssetDependencyGraph::GetDepe return iterator == Dependents_.end() ? AssetGuidSet{} : iterator->second; } +MetaCoreAssetDependencyGraph::AssetGuidSet MetaCoreAssetDependencyGraph::GetDependencyClosure( + const std::vector& roots +) const { + std::scoped_lock lock(Mutex_); + AssetGuidSet closure; + std::vector pending; + for (const auto& root : roots) { + if (root.IsValid() && closure.insert(root).second) pending.push_back(root); + } + while (!pending.empty()) { + const MetaCoreAssetGuid current = pending.back(); + pending.pop_back(); + const auto dependencies = Dependencies_.find(current); + if (dependencies == Dependencies_.end()) continue; + for (const auto& dependency : dependencies->second) { + if (closure.insert(dependency).second) pending.push_back(dependency); + } + } + return closure; +} + void MetaCoreAssetDependencyGraph::RemoveAsset(const MetaCoreAssetGuid& assetGuid) { if (!assetGuid.IsValid()) { return; diff --git a/Source/MetaCoreFoundation/Private/MetaCoreHash.cpp b/Source/MetaCoreFoundation/Private/MetaCoreHash.cpp index 966256f..07e59f3 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreHash.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreHash.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include namespace MetaCore { @@ -10,6 +13,21 @@ namespace { constexpr std::uint64_t GMetaCoreFnvOffsetBasis = 1469598103934665603ULL; constexpr std::uint64_t GMetaCoreFnvPrime = 1099511628211ULL; +constexpr std::array GMetaCoreSha256RoundConstants = { + 0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U, + 0xd807aa98U, 0x12835b01U, 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, 0xc19bf174U, + 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU, + 0x983e5152U, 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, 0x06ca6351U, 0x14292967U, + 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, + 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, 0xd6990624U, 0xf40e3585U, 0x106aa070U, + 0x19a4c116U, 0x1e376c08U, 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, 0x682e6ff3U, + 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U +}; + +[[nodiscard]] constexpr std::uint32_t MetaCoreRotateRight(std::uint32_t value, std::uint32_t count) { + return (value >> count) | (value << (32U - count)); +} + } // namespace std::uint64_t MetaCoreHashBytes(std::span data) { @@ -56,4 +74,77 @@ std::uint64_t MetaCoreHashCombine(std::uint64_t seed, std::uint64_t value) { return seed; } +std::string MetaCoreSha256Bytes(std::span data) { + std::vector message; + message.reserve(data.size() + 72U); + for (const std::byte value : data) { + message.push_back(std::to_integer(value)); + } + const std::uint64_t bitLength = static_cast(message.size()) * 8ULL; + message.push_back(0x80U); + while ((message.size() % 64U) != 56U) { + message.push_back(0U); + } + for (int shift = 56; shift >= 0; shift -= 8) { + message.push_back(static_cast((bitLength >> shift) & 0xffULL)); + } + + std::array state = { + 0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU, + 0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U + }; + for (std::size_t offset = 0; offset < message.size(); offset += 64U) { + std::array words{}; + for (std::size_t index = 0; index < 16U; ++index) { + const std::size_t base = offset + index * 4U; + words[index] = (static_cast(message[base]) << 24U) | + (static_cast(message[base + 1U]) << 16U) | + (static_cast(message[base + 2U]) << 8U) | + static_cast(message[base + 3U]); + } + for (std::size_t index = 16U; index < words.size(); ++index) { + const std::uint32_t s0 = MetaCoreRotateRight(words[index - 15U], 7U) ^ MetaCoreRotateRight(words[index - 15U], 18U) ^ (words[index - 15U] >> 3U); + const std::uint32_t s1 = MetaCoreRotateRight(words[index - 2U], 17U) ^ MetaCoreRotateRight(words[index - 2U], 19U) ^ (words[index - 2U] >> 10U); + words[index] = words[index - 16U] + s0 + words[index - 7U] + s1; + } + std::uint32_t a = state[0]; + std::uint32_t b = state[1]; + std::uint32_t c = state[2]; + std::uint32_t d = state[3]; + std::uint32_t e = state[4]; + std::uint32_t f = state[5]; + std::uint32_t g = state[6]; + std::uint32_t h = state[7]; + for (std::size_t index = 0; index < words.size(); ++index) { + const std::uint32_t sum1 = MetaCoreRotateRight(e, 6U) ^ MetaCoreRotateRight(e, 11U) ^ MetaCoreRotateRight(e, 25U); + const std::uint32_t choice = (e & f) ^ ((~e) & g); + const std::uint32_t temporary1 = h + sum1 + choice + GMetaCoreSha256RoundConstants[index] + words[index]; + const std::uint32_t sum0 = MetaCoreRotateRight(a, 2U) ^ MetaCoreRotateRight(a, 13U) ^ MetaCoreRotateRight(a, 22U); + const std::uint32_t majority = (a & b) ^ (a & c) ^ (b & c); + const std::uint32_t temporary2 = sum0 + majority; + h = g; g = f; f = e; e = d + temporary1; + d = c; c = b; b = a; a = temporary1 + temporary2; + } + state[0] += a; state[1] += b; state[2] += c; state[3] += d; + state[4] += e; state[5] += f; state[6] += g; state[7] += h; + } + + std::ostringstream output; + output << std::hex << std::setfill('0'); + for (const std::uint32_t value : state) { + output << std::setw(8) << value; + } + return output.str(); +} + +std::optional MetaCoreSha256File(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input.is_open()) { + return std::nullopt; + } + std::vector contents((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + const auto* bytes = reinterpret_cast(contents.data()); + return MetaCoreSha256Bytes(std::span(bytes, contents.size())); +} + } // namespace MetaCore diff --git a/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp b/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp index eb51c8a..89716cb 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreProject.cpp @@ -161,13 +161,14 @@ bool MetaCoreCreateProjectSkeleton( } std::error_code errorCode; - const std::array directories = { + const std::array directories = { normalizedRoot / "Assets", normalizedRoot / "Scenes", normalizedRoot / "Runtime", normalizedRoot / "Ui", normalizedRoot / "Library", normalizedRoot / "Build", + normalizedRoot / "BuildProfiles", normalizedRoot / "Assets" / "Models", normalizedRoot / "Assets" / "Materials", normalizedRoot / "Assets" / "Textures", @@ -220,6 +221,23 @@ input { width: 150dp; padding: 6dp; background-color: #0f1720; color: #ffffff; } return false; } + constexpr std::string_view defaultBuildProfile = R"json({ + "format_version": 1, + "name": "LinuxDevelopment", + "target_platform": "Linux-x86_64", + "configuration": "Development", + "graphics_backend": "OpenGL", + "startup_scene": "Scenes/Main.mcscene.json", + "output_directory": "Build", + "resource_policy": "ReferencedOnly" +} +)json"; + if (!MetaCoreWriteTextFileIfMissing( + normalizedRoot / "BuildProfiles" / "LinuxDevelopment.mcbuildprofile.json", + defaultBuildProfile)) { + return false; + } + MetaCoreProjectFileDocument document; if (!projectName.empty()) { document.Name = std::string(projectName); diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h index 8a5fe51..e9be9b4 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetDependencyGraph.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace MetaCore { @@ -17,6 +18,7 @@ public: void SetDependencies(const MetaCoreAssetGuid& assetGuid, const AssetGuidSet& dependencies); [[nodiscard]] AssetGuidSet GetDependencies(const MetaCoreAssetGuid& assetGuid) const; [[nodiscard]] AssetGuidSet GetDependents(const MetaCoreAssetGuid& assetGuid) const; + [[nodiscard]] AssetGuidSet GetDependencyClosure(const std::vector& roots) const; void RemoveAsset(const MetaCoreAssetGuid& assetGuid); void Clear(); diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h index 810de7b..e5d5db1 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreHash.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace MetaCore { @@ -13,6 +14,7 @@ namespace MetaCore { [[nodiscard]] std::uint64_t MetaCoreHashString(std::string_view value); [[nodiscard]] std::optional MetaCoreHashFile(const std::filesystem::path& path); [[nodiscard]] std::uint64_t MetaCoreHashCombine(std::uint64_t seed, std::uint64_t value); +[[nodiscard]] std::string MetaCoreSha256Bytes(std::span data); +[[nodiscard]] std::optional MetaCoreSha256File(const std::filesystem::path& path); } // namespace MetaCore - diff --git a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp index 42d41ee..a086a6f 100644 --- a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp @@ -45,6 +45,9 @@ MetaCoreImGuiHelper::MetaCoreImGuiHelper(Engine* engine, filament::View* view, c // 加载我们编译好的材质 std::ifstream file("uiBlit.filamat", std::ios::binary); + if (!file.is_open()) { + file.open("Engine/Materials/uiBlit.filamat", std::ios::binary); + } if (!file.is_open()) { file.open("../uiBlit.filamat", std::ios::binary); } diff --git a/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp b/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp index e04ecb5..a3fc60b 100644 --- a/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp +++ b/Source/MetaCoreRuntimeUi/Private/MetaCoreRuntimeUiSystem.cpp @@ -117,8 +117,10 @@ namespace { [[nodiscard]] std::vector MetaCoreLoadRuntimeUiMaterialPackage() { for (const std::filesystem::path& candidate : { std::filesystem::path("rml_ui.filamat"), + std::filesystem::path("Engine/Materials/rml_ui.filamat"), std::filesystem::path("../rml_ui.filamat"), std::filesystem::path("uiBlit.filamat"), + std::filesystem::path("Engine/Materials/uiBlit.filamat"), std::filesystem::path("../uiBlit.filamat") }) { std::ifstream input(candidate, std::ios::binary); diff --git a/TestProject/BuildProfiles/LinuxDevelopment.mcbuildprofile.json b/TestProject/BuildProfiles/LinuxDevelopment.mcbuildprofile.json new file mode 100644 index 0000000..2056b88 --- /dev/null +++ b/TestProject/BuildProfiles/LinuxDevelopment.mcbuildprofile.json @@ -0,0 +1,10 @@ +{ + "format_version": 1, + "name": "LinuxDevelopment", + "target_platform": "Linux-x86_64", + "configuration": "Development", + "graphics_backend": "OpenGL", + "startup_scene": "Scenes/Main.mcscene.json", + "output_directory": "Build", + "resource_policy": "ReferencedOnly" +} diff --git a/ci/linux-runtime-validation.Dockerfile b/ci/linux-runtime-validation.Dockerfile new file mode 100644 index 0000000..2d7d3b7 --- /dev/null +++ b/ci/linux-runtime-validation.Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:22.04 + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates libgl1 libglx0 libopengl0 libx11-6 libxcb1 libxau6 libxdmcp6 \ + libbsd0 libmd0 mesa-utils xvfb \ + && rm -rf /var/lib/apt/lists/* + +ENV LIBGL_ALWAYS_SOFTWARE=1 +WORKDIR /app diff --git a/docs/designs/metacore-mature-engine-gap-and-work-plan.md b/docs/designs/metacore-mature-engine-gap-and-work-plan.md index 2fa6262..90f44ea 100644 --- a/docs/designs/metacore-mature-engine-gap-and-work-plan.md +++ b/docs/designs/metacore-mature-engine-gap-and-work-plan.md @@ -54,6 +54,18 @@ MetaCore 已经超过单纯编辑器 Demo 阶段,当前具备: ### 1. 发布与部署还不具备正式交付厚度 +#### Linux 首发执行状态(2026-07-13) + +- [x] 建立 Linux x86_64 `BuildProfile`、公共发布服务以及 CLI / Editor 共用入口。 +- [x] 建立 `Build -> Cook -> Stage -> Package -> Validate` 命令链和原子 staging。 +- [x] 生成运行时项目描述、SHA-256 Release Manifest、便携目录、归档和独立符号包。 +- [x] Player 支持严格交付模式、任意工作目录启动、版本/缺失/损坏错误码和有限帧 smoke 参数。 +- [x] 增加日志轮转、Crashpad 本地 Minidump 接口、handler staging 和现场诊断包导出。 +- [ ] 解决 Crashpad vcpkg 端口与当前 libc++ 15 的兼容问题,并完成真实 Minidump/符号回溯验收。 +- [x] 增加 Ubuntu 22.04 clean-container 验证脚本与镜像定义。 +- [ ] 在正式 Ubuntu 22.04 clean image 完成 GPU/UI smoke 实机验收。 +- [ ] 实现并验收 Windows 对等后端;完成前本问题不能全平台关闭。 + #### 问题 - Cook 已存在,但尚未形成统一的一键 Build / Package 工作流。 diff --git a/docs/designs/metacore-packaging-runtime-delivery-design.md b/docs/designs/metacore-packaging-runtime-delivery-design.md index fd896ef..39d3d84 100644 --- a/docs/designs/metacore-packaging-runtime-delivery-design.md +++ b/docs/designs/metacore-packaging-runtime-delivery-design.md @@ -1,7 +1,7 @@ # MetaCore 打包与交付运行时工作流设计 生成时间:2026-03-28 -状态:草案 +状态:Linux 首发实现中 范围:M4 UI、持久化与打包循环 ## 目的 @@ -15,6 +15,31 @@ - 打包后客户端如何在独立环境运行 - 哪些文件属于开发期,哪些属于交付期 +## 2026-07 Linux 首发实现 + +当前主干已经落地以下正式发布接口: + +- `BuildProfiles/*.mcbuildprofile.json` 定义 Linux x86_64 的 Debug、Development、Release 构建。 +- `MetaCoreBuildTool build` 执行 `Build -> Cook -> Stage -> Package -> Validate`,使用临时目录并在成功后提交。 +- `MetaCoreBuildTool validate` 根据 SHA-256 Manifest 检查必需文件、大小、Hash、Runtime ABI 和路径安全。 +- `MetaCoreDiagnosticsTool export` 导出 Manifest、校验结果、日志、Runtime 诊断和本地 Crashpad 数据。 +- Player 默认从可执行文件旁的 `Project/MetaCore.runtimeproject` 进入严格交付模式;开发目录猜测只在显式开发模式保留。 +- `scripts/validate_linux_release.sh` 和 `ci/linux-runtime-validation.Dockerfile` 提供 Ubuntu 22.04 隔离验证入口。 + +Crashpad 集成位于可选 vcpkg feature `crash-reporting`。当前固定端口在 libc++ 15 下因 `std::ranges::copy` 支持不足无法完成构建,因此默认构建保留接口但不宣称 Minidump 已验收;升级 libc++ 或加入经过维护的端口补丁后,发布流水线会强制收集 `crashpad_handler`。 + +标准命令: + +```bash +MetaCoreBuildTool build \ + --project /path/to/project \ + --profile /path/to/project/BuildProfiles/LinuxDevelopment.mcbuildprofile.json \ + --engine-build /path/to/metacore/build/linux-development \ + --clean +``` + +Linux 首发完成不代表 Windows 已验收;Windows 仍需复用同一 Profile、Manifest、Player 严格模式和验证协议完成独立平台验收。 + ## 结论先说 MetaCore 第一阶段应明确采用下面这条交付链: diff --git a/scripts/validate_linux_release.sh b/scripts/validate_linux_release.sh new file mode 100755 index 0000000..9a2abf0 --- /dev/null +++ b/scripts/validate_linux_release.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "Usage: validate_linux_release.sh [--smoke]" >&2 + exit 2 +fi + +build_tool="$(realpath "$1")" +package="$(realpath "$2")" +"$build_tool" validate --package "$package" + +docker run --rm --network none --read-only \ + --tmpfs /tmp --tmpfs /app/Diagnostics \ + -v "$package:/app:ro" \ + metacore-runtime-validation:ubuntu22.04 \ + /app/MetaCorePlayer --validate-only + +if [[ "${3:-}" == "--smoke" ]]; then + docker run --rm --network none \ + --tmpfs /tmp --tmpfs /app/Diagnostics \ + -v "$package:/app:ro" \ + metacore-runtime-validation:ubuntu22.04 \ + xvfb-run -a /app/MetaCorePlayer --smoke-test-frames 3 +fi diff --git a/tests/MetaCoreDeliveryTests.cpp b/tests/MetaCoreDeliveryTests.cpp new file mode 100644 index 0000000..eca3deb --- /dev/null +++ b/tests/MetaCoreDeliveryTests.cpp @@ -0,0 +1,87 @@ +#include "MetaCoreDelivery/MetaCoreBuildPipeline.h" +#include "MetaCoreFoundation/MetaCoreAssetDependencyGraph.h" +#include "MetaCoreFoundation/MetaCoreHash.h" + +#include +#include +#include +#include + +namespace { + +int Failures = 0; + +void Expect(bool condition, const char* message) { + if (!condition) { + std::cerr << "FAILED: " << message << '\n'; + ++Failures; + } +} + +void WriteText(const std::filesystem::path& path, std::string_view value) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary); + output << value; +} + +} // namespace + +int main() { + const std::string abc = "abc"; + const auto* abcBytes = reinterpret_cast(abc.data()); + Expect( + MetaCore::MetaCoreSha256Bytes(std::span(abcBytes, abc.size())) == + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + "SHA-256 known vector" + ); + + const auto temporary = std::filesystem::temp_directory_path() / "MetaCoreDeliveryTests"; + std::error_code error; + std::filesystem::remove_all(temporary, error); + WriteText(temporary / "unsafe.mcbuildprofile.json", R"({ + "format_version":1,"name":"unsafe","target_platform":"Linux-x86_64","configuration":"Development", + "graphics_backend":"OpenGL","startup_scene":"../outside","output_directory":"Build","resource_policy":"ReferencedOnly" + })"); + MetaCore::MetaCoreBuildIssue profileIssue; + Expect(!MetaCore::MetaCoreReadBuildProfile(temporary / "unsafe.mcbuildprofile.json", &profileIssue), "unsafe profile rejected"); + Expect(profileIssue.Code == MetaCore::MetaCoreBuildErrorCode::UnsafePath, "unsafe profile error code"); + + const auto package = temporary / "Package"; + WriteText(package / "payload.bin", "payload"); + MetaCore::MetaCoreRuntimeDeliveryDocument delivery; + delivery.BuildId = "test-build"; + delivery.StartupScene = "payload.bin"; + Expect(MetaCore::MetaCoreWriteRuntimeDeliveryDocument(package / "Project" / "MetaCore.runtimeproject", delivery), "write runtime delivery"); + MetaCore::MetaCoreReleaseManifestDocument manifest; + manifest.BuildId = delivery.BuildId; + const auto payloadHash = MetaCore::MetaCoreSha256File(package / "payload.bin"); + const auto deliveryHash = MetaCore::MetaCoreSha256File(package / "Project" / "MetaCore.runtimeproject"); + manifest.Files.push_back({"payload.bin", "asset", 7U, payloadHash.value_or(""), true}); + manifest.Files.push_back({"Project/MetaCore.runtimeproject", "runtime_project", + std::filesystem::file_size(package / "Project" / "MetaCore.runtimeproject"), deliveryHash.value_or(""), true}); + Expect(MetaCore::MetaCoreWriteReleaseManifest(package / "Manifest" / "MetaCore.release.json", manifest), "write release manifest"); + Expect(MetaCore::MetaCoreValidateReleasePackage(package).empty(), "valid package accepted"); + WriteText(package / "payload.bin", "tampered"); + const auto corruptIssues = MetaCore::MetaCoreValidateReleasePackage(package); + Expect(!corruptIssues.empty(), "corrupt package rejected"); + Expect(corruptIssues.front().Code == MetaCore::MetaCoreBuildErrorCode::FileSizeMismatch || + corruptIssues.front().Code == MetaCore::MetaCoreBuildErrorCode::FileHashMismatch, "corrupt package error code"); + std::filesystem::remove(package / "payload.bin", error); + const auto missingIssues = MetaCore::MetaCoreValidateReleasePackage(package); + Expect(!missingIssues.empty() && missingIssues.front().Code == MetaCore::MetaCoreBuildErrorCode::RequiredFileMissing, "missing file reported"); + + auto& graph = MetaCore::MetaCoreAssetDependencyGraph::Get(); + graph.Clear(); + const auto root = MetaCore::MetaCoreAssetGuid::Generate(); + const auto middle = MetaCore::MetaCoreAssetGuid::Generate(); + const auto leaf = MetaCore::MetaCoreAssetGuid::Generate(); + graph.SetDependencies(root, {middle}); + graph.SetDependencies(middle, {leaf}); + const auto closure = graph.GetDependencyClosure({root}); + Expect(closure.size() == 3U && closure.contains(root) && closure.contains(middle) && closure.contains(leaf), "transitive dependency closure"); + graph.Clear(); + + std::filesystem::remove_all(temporary, error); + if (Failures == 0) std::cout << "MetaCoreDeliveryTests passed\n"; + return Failures == 0 ? 0 : 1; +} diff --git a/tools/MetaCoreBuildTool/main.cpp b/tools/MetaCoreBuildTool/main.cpp new file mode 100644 index 0000000..8b50f51 --- /dev/null +++ b/tools/MetaCoreBuildTool/main.cpp @@ -0,0 +1,73 @@ +#include "MetaCoreDelivery/MetaCoreBuildPipeline.h" + +#include +#include +#include + +namespace { + +void PrintIssue(const MetaCore::MetaCoreBuildIssue& issue) { + std::cerr << MetaCore::MetaCoreBuildErrorCodeName(issue.Code) << ": " << issue.Message; + if (!issue.Path.empty()) std::cerr << " path=" << issue.Path.generic_string(); + if (!issue.Expected.empty()) std::cerr << " expected=" << issue.Expected; + if (!issue.Actual.empty()) std::cerr << " actual=" << issue.Actual; + std::cerr << '\n'; +} + +[[nodiscard]] const char* NextValue(int& index, int argc, char* argv[]) { + if (index + 1 >= argc) return nullptr; + return argv[++index]; +} + +void PrintUsage() { + std::cerr + << "Usage:\n" + << " MetaCoreBuildTool build --project --profile --engine-build [--clean] [--skip-build]\n" + << " MetaCoreBuildTool validate --package \n"; +} + +} // namespace + +int main(int argc, char* argv[]) { + if (argc < 2) { PrintUsage(); return 2; } + const std::string_view command(argv[1]); + if (command == "validate") { + std::filesystem::path package; + for (int index = 2; index < argc; ++index) { + if (std::string_view(argv[index]) == "--package") { + const char* value = NextValue(index, argc, argv); + if (value != nullptr) package = value; + } + } + if (package.empty()) { PrintUsage(); return 2; } + const auto issues = MetaCore::MetaCoreValidateReleasePackage(package); + for (const auto& issue : issues) PrintIssue(issue); + if (!issues.empty()) return 1; + std::cout << "MetaCore release package is valid: " << package.generic_string() << '\n'; + return 0; + } + if (command != "build") { PrintUsage(); return 2; } + + MetaCore::MetaCoreBuildRequest request; + for (int index = 2; index < argc; ++index) { + const std::string_view argument(argv[index]); + if (argument == "--project") { + const char* value = NextValue(index, argc, argv); if (value != nullptr) request.ProjectRoot = value; + } else if (argument == "--profile") { + const char* value = NextValue(index, argc, argv); if (value != nullptr) request.ProfilePath = value; + } else if (argument == "--engine-build") { + const char* value = NextValue(index, argc, argv); if (value != nullptr) request.EngineBuildRoot = value; + } else if (argument == "--clean") request.Clean = true; + else if (argument == "--skip-build") request.SkipBuild = true; + } + MetaCore::MetaCoreBuildPipeline pipeline; + const auto result = pipeline.Run(request, [](MetaCore::MetaCoreBuildStage stage, std::string_view message) { + std::cout << '[' << MetaCore::MetaCoreBuildStageName(stage) << "] " << message << '\n'; + }); + for (const auto& issue : result.Issues) PrintIssue(issue); + if (!result.Success) return 1; + std::cout << "package=" << result.PackageDirectory.generic_string() << '\n' + << "archive=" << result.ArchivePath.generic_string() << '\n'; + if (!result.SymbolsArchivePath.empty()) std::cout << "symbols=" << result.SymbolsArchivePath.generic_string() << '\n'; + return 0; +} diff --git a/tools/MetaCoreDiagnosticsTool/main.cpp b/tools/MetaCoreDiagnosticsTool/main.cpp new file mode 100644 index 0000000..66db63b --- /dev/null +++ b/tools/MetaCoreDiagnosticsTool/main.cpp @@ -0,0 +1,23 @@ +#include "MetaCoreDelivery/MetaCoreBuildPipeline.h" + +#include +#include +#include + +int main(int argc, char* argv[]) { + if (argc != 6 || std::string_view(argv[1]) != "export" || std::string_view(argv[2]) != "--package" || + std::string_view(argv[4]) != "--output") { + std::cerr << "Usage: MetaCoreDiagnosticsTool export --package --output \n"; + return 2; + } + const std::filesystem::path package = argv[3]; + const std::filesystem::path output = argv[5]; + MetaCore::MetaCoreBuildIssue issue; + if (!MetaCore::MetaCoreExportDiagnostics(package, output, &issue)) { + std::cerr << MetaCore::MetaCoreBuildErrorCodeName(issue.Code) << ": " << issue.Message + << " path=" << issue.Path.generic_string() << '\n'; + return 1; + } + std::cout << "diagnostics=" << output.generic_string() << '\n'; + return 0; +} diff --git a/vcpkg.json b/vcpkg.json index 65fce87..02e9257 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -25,5 +25,11 @@ ], "platform": "!windows" } - ] + ], + "features": { + "crash-reporting": { + "description": "Enable local Crashpad Minidump capture and package crashpad_handler.", + "dependencies": ["crashpad"] + } + } }