From 523dbd76a464905ee5d94f9358561b4bdab81174 Mon Sep 17 00:00:00 2001 From: Emil <65846814+emil28092005@users.noreply.github.com> Date: Thu, 24 Sep 2026 04:01:46 +0300 Subject: [PATCH] Reverify staged gameplay sources before build publication --- src/editor/build_service.cpp | 26 ++++++++++++++++++++------ tests/build_schema_tests.cpp | 9 +++++---- tests/build_schema_tool.cpp | 16 ++++++++++++++-- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/editor/build_service.cpp b/src/editor/build_service.cpp index d694edb..894d606 100644 --- a/src/editor/build_service.cpp +++ b/src/editor/build_service.cpp @@ -19,9 +19,11 @@ #include #include #include +#include #include #include #include +#include namespace faset::editor { namespace fs = std::filesystem; @@ -472,14 +474,21 @@ struct BuildService::Impl { const auto compiled = std::chrono::steady_clock::now(); auto player = build_executable(native_directory, configuration, "faset_player"); auto exporter = build_executable(native_directory, configuration, "faset_schema_exporter"); - const auto package_key = - build_package_key(inputs, native_directory, configuration, player, exporter); const auto source_unchanged = [&] { + const auto snapshot_root = staged_scripts.parent_path(); + if (fs::is_symlink(fs::symlink_status(snapshot_root)) || + !fs::is_directory(staged_scripts) || + gameplay_source_hash(snapshot_root, job.lua) != inputs.source_hash) + throw std::runtime_error( + "Gameplay source snapshot changed during the build; build again"); if (gameplay_source_hash(config.project_root, scripting::loadLuaProject(config.project_root)) != inputs.source_hash) throw std::runtime_error("Gameplay sources changed during the build; build again"); }; + source_unchanged(); + const auto package_key = + build_package_key(inputs, native_directory, configuration, player, exporter); const auto result_for = [&](const std::string& id, const std::string& fingerprint, bool reused) -> Json { const auto directory = config.cache_root / "builds" / id; @@ -502,6 +511,7 @@ struct BuildService::Impl { {"total", milliseconds(started, finished)}}}}; }; const auto pointer_file = config.cache_root / "last_build.json"; + std::optional> cached_generation; if (fs::is_regular_file(pointer_file)) try { const auto pointer = read_json(pointer_file); @@ -509,15 +519,19 @@ struct BuildService::Impl { const auto candidate = project_path(config.cache_root, fs::path("builds") / id); if (validate_build_generation(candidate, package_key)) { - source_unchanged(); - checkpoint(job, "Reusing verified build generation", .68); const auto manifest = read_json(candidate / "manifest.json"); - log(job, "Verified schema/package cache hit: " + id + "\n"); - return result_for(id, manifest.at("fingerprint").get(), true); + cached_generation = + {id, manifest.at("fingerprint").get()}; } } catch (const std::exception&) { // A bad pointer or old/corrupt generation is a cache miss. } + if (cached_generation) { + source_unchanged(); + checkpoint(job, "Reusing verified build generation", .68); + log(job, "Verified schema/package cache hit: " + cached_generation->first + "\n"); + return result_for(cached_generation->first, cached_generation->second, true); + } checkpoint(job, "Exporting gameplay schema", .58); const auto staging = config.cache_root / "builds" / (".staging-" + job.status.id); const auto generation = config.cache_root / "builds" / job.status.id; diff --git a/tests/build_schema_tests.cpp b/tests/build_schema_tests.cpp index 8805f83..5183b21 100644 --- a/tests/build_schema_tests.cpp +++ b/tests/build_schema_tests.cpp @@ -246,11 +246,12 @@ int test_main(int argc, char** argv) { fs::remove(config.project_root / "fail-unparseable-build"); atomic_write(config.project_root / "Scripts/Extensions/BuildOnly.hpp", "#define BUILD_ONLY 3\n"); - atomic_write(config.project_root / "mutate-cpp-header-during-build", "fixture\n"); + atomic_write(config.project_root / "mutate-cpp-snapshot-during-schema-export", "fixture\n"); const auto raced_header = builds.wait(builds.start_build()); - check(raced_header.state == "failed" && read_text(last_build) == previous_pointer, - "Header changed during schema export cannot publish a mixed build"); - fs::remove(config.project_root / "mutate-cpp-header-during-build"); + check(raced_header.state == "failed" && read_text(last_build) == previous_pointer && + raced_header.error.find("snapshot") != std::string::npos, + "A mutated staged C++ header cannot publish a falsely keyed binary"); + fs::remove(config.project_root / "mutate-cpp-snapshot-during-schema-export"); atomic_write(config.project_root / "Scripts/Extensions/BuildOnly.hpp", "#define BUILD_ONLY 1\n"); check(!first.result.at("lua_enabled").get() && diff --git a/tests/build_schema_tool.cpp b/tests/build_schema_tool.cpp index 13aa18c..20db216 100644 --- a/tests/build_schema_tool.cpp +++ b/tests/build_schema_tool.cpp @@ -5,6 +5,17 @@ // asynchronous BuildService and publication code without compiling a game. namespace fs = std::filesystem; using namespace faset; +namespace { +fs::path configured_gameplay_scripts() { + for (const auto& argument : read_json("configure-fixture.json")) { + const auto text = argument.get(); + constexpr std::string_view prefix = "-DFASET_GAMEPLAY_SOURCE_DIR="; + if (text.starts_with(prefix)) + return path_from_utf8(text.substr(prefix.size())); + } + throw std::runtime_error("Fixture has no configured gameplay source snapshot"); +} +} // namespace int tool_main(int argc, char** argv) { try { if (argc >= 3 && std::string_view(argv[1]) == "--output") { @@ -24,8 +35,9 @@ int tool_main(int argc, char** argv) { if (fs::exists("mutate-lua-snapshot")) atomic_write(snapshot / "Scripts/main.lua", "-- corrupt snapshot\n"); } - if (fs::exists("mutate-cpp-header-during-build")) - atomic_write("Scripts/Extensions/BuildOnly.hpp", "#define BUILD_ONLY 2\n"); + if (fs::exists("mutate-cpp-snapshot-during-schema-export")) + atomic_write(configured_gameplay_scripts() / "Extensions/BuildOnly.hpp", + "#define BUILD_ONLY 2\n"); atomic_write_json(path_from_utf8(argv[2]), read_json("schema-fixture.json")); return 0; }