Validate gameplay metadata before publishing build generations
This commit is contained in:
+1
-1
@@ -56,7 +56,7 @@ endif()
|
|||||||
if(TARGET faset_authoring AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/EditorCommands.cmake")
|
if(TARGET faset_authoring AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/EditorCommands.cmake")
|
||||||
include(cmake/EditorCommands.cmake)
|
include(cmake/EditorCommands.cmake)
|
||||||
endif()
|
endif()
|
||||||
if(TARGET faset_assets AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/BuildService.cmake")
|
if(TARGET faset_assets AND TARGET faset_authoring AND EXISTS "${PROJECT_SOURCE_DIR}/cmake/BuildService.cmake")
|
||||||
include(cmake/BuildService.cmake)
|
include(cmake/BuildService.cmake)
|
||||||
endif()
|
endif()
|
||||||
if(TARGET faset_editor_commands AND TARGET faset_build_service)
|
if(TARGET faset_editor_commands AND TARGET faset_build_service)
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
add_library(faset_build_service STATIC ${PROJECT_SOURCE_DIR}/src/editor/build_service.cpp)
|
add_library(faset_build_service STATIC ${PROJECT_SOURCE_DIR}/src/editor/build_service.cpp)
|
||||||
target_include_directories(faset_build_service PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
target_include_directories(faset_build_service PUBLIC ${PROJECT_SOURCE_DIR}/include)
|
||||||
target_compile_features(faset_build_service PUBLIC cxx_std_20)
|
target_compile_features(faset_build_service PUBLIC cxx_std_20)
|
||||||
target_link_libraries(faset_build_service PUBLIC faset_core PRIVATE faset_asset_data Threads::Threads)
|
target_link_libraries(faset_build_service PUBLIC faset_core PRIVATE faset_asset_data faset_authoring Threads::Threads)
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
add_executable(faset_build_service_tests ${PROJECT_SOURCE_DIR}/tests/build_service_tests.cpp)
|
add_executable(faset_build_service_tests ${PROJECT_SOURCE_DIR}/tests/build_service_tests.cpp)
|
||||||
target_link_libraries(faset_build_service_tests PRIVATE faset_build_service faset_assets)
|
target_link_libraries(faset_build_service_tests PRIVATE faset_build_service faset_assets)
|
||||||
target_compile_definitions(faset_build_service_tests PRIVATE FASET_ENGINE_SOURCE="${PROJECT_SOURCE_DIR}")
|
target_compile_definitions(faset_build_service_tests PRIVATE FASET_ENGINE_SOURCE="${PROJECT_SOURCE_DIR}")
|
||||||
add_test(NAME process_and_cook COMMAND faset_build_service_tests)
|
add_test(NAME process_and_cook COMMAND faset_build_service_tests)
|
||||||
|
add_executable(faset_build_schema_tool ${PROJECT_SOURCE_DIR}/tests/build_schema_tool.cpp)
|
||||||
|
target_link_libraries(faset_build_schema_tool PRIVATE faset_core)
|
||||||
|
add_executable(faset_build_schema_tests ${PROJECT_SOURCE_DIR}/tests/build_schema_tests.cpp)
|
||||||
|
target_link_libraries(faset_build_schema_tests PRIVATE faset_build_service faset_authoring)
|
||||||
|
add_dependencies(faset_build_schema_tests faset_build_schema_tool)
|
||||||
|
add_test(NAME build_schema_publication COMMAND faset_build_schema_tests $<TARGET_FILE:faset_build_schema_tool> ${PROJECT_SOURCE_DIR})
|
||||||
|
set_tests_properties(build_schema_publication PROPERTIES TIMEOUT 60)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -56,5 +56,8 @@ template <class T> class TypeRegistration {
|
|||||||
Json schema_;
|
Json schema_;
|
||||||
};
|
};
|
||||||
SchemaRegistry builtin_schemas();
|
SchemaRegistry builtin_schemas();
|
||||||
|
// Validate a gameplay schema array/types manifest, reserving native TypeIds and
|
||||||
|
// rejecting repeated gameplay TypeIds. The result includes built-in schemas.
|
||||||
|
SchemaRegistry gameplay_schemas(const Json& manifest);
|
||||||
void validate_field(const Json& value, const Json& descriptor);
|
void validate_field(const Json& value, const Json& descriptor);
|
||||||
} // namespace faset::authoring
|
} // namespace faset::authoring
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <faset/authoring/schema.hpp>
|
#include <faset/authoring/schema.hpp>
|
||||||
|
#include <limits>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
namespace faset::authoring {
|
namespace faset::authoring {
|
||||||
@@ -52,13 +53,25 @@ void SchemaRegistry::register_schema(const Json& value) {
|
|||||||
Json normalized = value;
|
Json normalized = value;
|
||||||
const auto id = value.at("id").get<std::string>();
|
const auto id = value.at("id").get<std::string>();
|
||||||
require(!id.empty(), "schema.invalid", "TypeId cannot be empty");
|
require(!id.empty(), "schema.invalid", "TypeId cannot be empty");
|
||||||
require(value.value("version", 1) > 0, "schema.invalid", "Schema version must be positive");
|
if (value.contains("version")) {
|
||||||
|
const auto& version = value.at("version");
|
||||||
|
require(version.is_number_integer() && version > 0 &&
|
||||||
|
version <= std::numeric_limits<int>::max(),
|
||||||
|
"schema.invalid", "Schema version must be a positive supported integer");
|
||||||
|
}
|
||||||
for (auto& [key, field] : normalized["fields"].items()) {
|
for (auto& [key, field] : normalized["fields"].items()) {
|
||||||
require(field.is_object() && field.contains("default"), "schema.invalid",
|
require(field.is_object() && field.contains("default"), "schema.invalid",
|
||||||
"Each field requires a typed default");
|
"Each field requires a typed default");
|
||||||
require(field.value("id", key) == key, "schema.field_id",
|
require(field.value("id", key) == key, "schema.field_id",
|
||||||
"Field map keys must be stable FieldIds");
|
"Field map keys must be stable FieldIds");
|
||||||
field["id"] = key;
|
field["id"] = key;
|
||||||
|
for (const auto* limit : {"min", "max"})
|
||||||
|
if (field.contains(limit))
|
||||||
|
require(field.at(limit).is_number() && std::isfinite(field.at(limit).get<double>()),
|
||||||
|
"schema.invalid", "Field limits must be finite numbers");
|
||||||
|
if (field.contains("enum"))
|
||||||
|
require(field.at("enum").is_array(), "schema.invalid",
|
||||||
|
"Field enum choices must be an array");
|
||||||
validate_field(field["default"], field);
|
validate_field(field["default"], field);
|
||||||
}
|
}
|
||||||
if (auto found = schemas_.find(id); found != schemas_.end())
|
if (auto found = schemas_.find(id); found != schemas_.end())
|
||||||
@@ -73,6 +86,22 @@ void SchemaRegistry::register_schemas(const Json& values) {
|
|||||||
candidate.register_schema(schema);
|
candidate.register_schema(schema);
|
||||||
*this = std::move(candidate);
|
*this = std::move(candidate);
|
||||||
}
|
}
|
||||||
|
SchemaRegistry gameplay_schemas(const Json& manifest) {
|
||||||
|
require(manifest.is_array() || (manifest.is_object() && manifest.contains("types") &&
|
||||||
|
manifest.at("types").is_array()),
|
||||||
|
"schema.invalid", "Gameplay schemas require an array or a types manifest");
|
||||||
|
const auto& types = manifest.is_array() ? manifest : manifest.at("types");
|
||||||
|
auto result = builtin_schemas();
|
||||||
|
for (const auto& schema : types) {
|
||||||
|
require(schema.is_object() && schema.contains("id") && schema.at("id").is_string(),
|
||||||
|
"schema.invalid", "Gameplay schema requires a TypeId");
|
||||||
|
const auto id = schema.at("id").get<std::string>();
|
||||||
|
require(!result.contains(id), "schema.duplicate_type",
|
||||||
|
"Gameplay schema duplicates a builtin or gameplay TypeId: " + id);
|
||||||
|
result.register_schema(schema);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
bool SchemaRegistry::contains(const std::string& type) const {
|
bool SchemaRegistry::contains(const std::string& type) const {
|
||||||
return schemas_.contains(type);
|
return schemas_.contains(type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,9 +311,7 @@ void AuthoringService::register_schemas(const Json& manifest) {
|
|||||||
}
|
}
|
||||||
void AuthoringService::replace_external_schemas(const Json& manifest) {
|
void AuthoringService::replace_external_schemas(const Json& manifest) {
|
||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
auto candidate = builtin_schemas();
|
schemas_ = gameplay_schemas(manifest);
|
||||||
candidate.register_schemas(manifest);
|
|
||||||
schemas_ = std::move(candidate);
|
|
||||||
}
|
}
|
||||||
void AuthoringService::apply(Json& scene, const Json& command) {
|
void AuthoringService::apply(Json& scene, const Json& command) {
|
||||||
require(command.is_object() && command.contains("op") && command["op"].is_string(),
|
require(command.is_object() && command.contains("op") && command["op"].is_string(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <faset/assets/asset_data.hpp>
|
#include <faset/assets/asset_data.hpp>
|
||||||
|
#include <faset/authoring/schema.hpp>
|
||||||
#include <faset/core/hash.hpp>
|
#include <faset/core/hash.hpp>
|
||||||
#include <faset/core/io.hpp>
|
#include <faset/core/io.hpp>
|
||||||
#include <faset/core/process.hpp>
|
#include <faset/core/process.hpp>
|
||||||
@@ -33,23 +34,14 @@ void validate_scene(const Json& scene) {
|
|||||||
throw std::runtime_error("Scene dimension must be 2 or 3");
|
throw std::runtime_error("Scene dimension must be 2 or 3");
|
||||||
}
|
}
|
||||||
void validate_component_types(const Json& scene, const Json& schema) {
|
void validate_component_types(const Json& scene, const Json& schema) {
|
||||||
std::map<std::string, int> versions;
|
const auto registry =
|
||||||
for (const auto* type : {"faset.transform", "faset.sprite", "faset.mesh", "faset.camera",
|
schema.is_null() ? authoring::builtin_schemas() : authoring::gameplay_schemas(schema);
|
||||||
"faset.light", "faset.rigid_body_2d", "faset.rigid_body_3d"})
|
|
||||||
versions[type] = 1;
|
|
||||||
for (const auto& type : schema.value("types", Json::array())) {
|
|
||||||
auto id = type.at("id").get<std::string>();
|
|
||||||
if (versions.contains(id))
|
|
||||||
throw std::runtime_error("Gameplay schema duplicates a component type: " + id);
|
|
||||||
versions[id] = type.value("version", 1);
|
|
||||||
}
|
|
||||||
for (const auto& entity : scene.at("entities"))
|
for (const auto& entity : scene.at("entities"))
|
||||||
for (const auto& component : entity.value("components", Json::array())) {
|
for (const auto& component : entity.value("components", Json::array())) {
|
||||||
const auto id = component.at("type").get<std::string>();
|
const auto id = component.at("type").get<std::string>();
|
||||||
auto found = versions.find(id);
|
if (!registry.contains(id))
|
||||||
if (found == versions.end())
|
|
||||||
throw std::runtime_error("Cannot cook unresolved component type: " + id);
|
throw std::runtime_error("Cannot cook unresolved component type: " + id);
|
||||||
if (component.value("version", 1) != found->second)
|
if (component.value("version", 1) != registry.schema(id).value("version", 1))
|
||||||
throw std::runtime_error("Migrate component '" + id +
|
throw std::runtime_error("Migrate component '" + id +
|
||||||
"' to the current gameplay schema before cooking");
|
"' to the current gameplay schema before cooking");
|
||||||
}
|
}
|
||||||
@@ -298,6 +290,9 @@ struct BuildService::Impl {
|
|||||||
if (schema.value("format", "") != "faset.schema" || schema.value("version", 0) != 1 ||
|
if (schema.value("format", "") != "faset.schema" || schema.value("version", 0) != 1 ||
|
||||||
!schema.contains("types") || !schema.at("types").is_array())
|
!schema.contains("types") || !schema.at("types").is_array())
|
||||||
throw std::runtime_error("SchemaExporter returned an invalid manifest");
|
throw std::runtime_error("SchemaExporter returned an invalid manifest");
|
||||||
|
// Validate the complete metadata before publishing either the schema or
|
||||||
|
// its Player generation. Session uses this same authoring contract.
|
||||||
|
(void)authoring::gameplay_schemas(schema);
|
||||||
std::string fingerprint = sha256_file(player) + sha256_file(exporter) +
|
std::string fingerprint = sha256_file(player) + sha256_file(exporter) +
|
||||||
read_text(native_directory / "CMakeCache.txt");
|
read_text(native_directory / "CMakeCache.txt");
|
||||||
for (const auto& file : {"Gameplay.cpp", "Gameplay.hpp"})
|
for (const auto& file : {"Gameplay.cpp", "Gameplay.hpp"})
|
||||||
@@ -393,7 +388,7 @@ struct BuildService::Impl {
|
|||||||
checkpoint(job, "Validating scene and assets", .1);
|
checkpoint(job, "Validating scene and assets", .1);
|
||||||
validate_scene(job.scene);
|
validate_scene(job.scene);
|
||||||
validate_assets(job.scene);
|
validate_assets(job.scene);
|
||||||
Json schema = Json::object();
|
Json schema;
|
||||||
if (fs::exists(config.cache_root / "last_build.json")) {
|
if (fs::exists(config.cache_root / "last_build.json")) {
|
||||||
auto pointer = read_json(config.cache_root / "last_build.json");
|
auto pointer = read_json(config.cache_root / "last_build.json");
|
||||||
auto schema_path = project_path(
|
auto schema_path = project_path(
|
||||||
@@ -403,7 +398,9 @@ struct BuildService::Impl {
|
|||||||
}
|
}
|
||||||
validate_component_types(job.scene, schema);
|
validate_component_types(job.scene, schema);
|
||||||
auto source_hash = sha256(job.scene.dump());
|
auto source_hash = sha256(job.scene.dump());
|
||||||
auto schema_fingerprint = schema.value("build_fingerprint", std::string("builtin-v1"));
|
auto schema_fingerprint =
|
||||||
|
schema.is_null() ? std::string("builtin-v1")
|
||||||
|
: schema.value("build_fingerprint", std::string("builtin-v1"));
|
||||||
auto digest = sha256(source_hash + schema_fingerprint);
|
auto digest = sha256(source_hash + schema_fingerprint);
|
||||||
auto directory = config.cache_root / "cooked" / digest;
|
auto directory = config.cache_root / "cooked" / digest;
|
||||||
auto staging = config.cache_root / "cooked" / (".staging-" + job.status.id);
|
auto staging = config.cache_root / "cooked" / (".staging-" + job.status.id);
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#include <faset/authoring/service.hpp>
|
||||||
|
#include <faset/core/hash.hpp>
|
||||||
|
#include <faset/core/io.hpp>
|
||||||
|
#include <faset/editor/build_service.hpp>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
using namespace faset;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
namespace {
|
||||||
|
void check(bool value, std::string_view message) {
|
||||||
|
if (!value)
|
||||||
|
throw std::runtime_error(std::string(message));
|
||||||
|
}
|
||||||
|
Json manifest() {
|
||||||
|
return {
|
||||||
|
{"format", "faset.schema"},
|
||||||
|
{"version", 1},
|
||||||
|
{"types",
|
||||||
|
Json::array(
|
||||||
|
{{{"id", "game.mover"},
|
||||||
|
{"version", 2},
|
||||||
|
{"fields",
|
||||||
|
{{"speed", {{"type", "number"}, {"default", 2.5}, {"min", 0}, {"max", 10}}}}}}})}};
|
||||||
|
}
|
||||||
|
int test_main(int argc, char** argv) {
|
||||||
|
const auto root =
|
||||||
|
fs::temp_directory_path() / path_from_utf8("Faset schema Café 世界 " + new_id());
|
||||||
|
try {
|
||||||
|
check(argc == 3, "Expected native fixture tool and engine root");
|
||||||
|
editor::BuildConfig config;
|
||||||
|
config.project_root = root / "project";
|
||||||
|
config.engine_root = fs::absolute(path_from_utf8(argv[2]));
|
||||||
|
config.cmake = path_to_utf8(fs::absolute(path_from_utf8(argv[1])));
|
||||||
|
config.configure_arguments = {"-DCMAKE_CXX_COMPILER=fixture-does-not-compile"};
|
||||||
|
editor::BuildService builds(config);
|
||||||
|
builds.scaffold("Schema publication", 2);
|
||||||
|
authoring::AuthoringService authoring(config.project_root, authoring::builtin_schemas());
|
||||||
|
const auto valid = manifest();
|
||||||
|
atomic_write_json(config.project_root / "schema-fixture.json", valid);
|
||||||
|
const auto first = builds.wait(builds.start_build());
|
||||||
|
check(first.state == "succeeded", "Valid custom schema v2 publishes: " + first.error);
|
||||||
|
const auto directory = path_from_utf8(first.result.at("directory").get<std::string>());
|
||||||
|
const auto player = path_from_utf8(first.result.at("player").get<std::string>());
|
||||||
|
const auto schema = path_from_utf8(first.result.at("schema").get<std::string>());
|
||||||
|
const auto last_build = builds.config().cache_root / "last_build.json";
|
||||||
|
const auto previous_pointer = read_text(last_build);
|
||||||
|
const auto previous_player = sha256_file(player);
|
||||||
|
const auto previous_schema = read_text(schema);
|
||||||
|
const auto previous_manifest = read_text(directory / "manifest.json");
|
||||||
|
authoring.replace_external_schemas(read_json(schema));
|
||||||
|
const auto previous_registry = authoring.schemas().manifest();
|
||||||
|
check(authoring.schemas().schema("game.mover").at("version") == 2,
|
||||||
|
"Matching custom v2 metadata reaches authoring");
|
||||||
|
|
||||||
|
Json scene{
|
||||||
|
{"format", "faset.scene"},
|
||||||
|
{"version", 1},
|
||||||
|
{"id", "fixture-scene"},
|
||||||
|
{"dimension", 2},
|
||||||
|
{"instances", Json::array()},
|
||||||
|
{"entities",
|
||||||
|
Json::array({{{"id", "mover"},
|
||||||
|
{"components", Json::array({{{"id", "behavior"},
|
||||||
|
{"type", "game.mover"},
|
||||||
|
{"version", 2},
|
||||||
|
{"fields", {{"speed", 2.5}}}}})}}})}};
|
||||||
|
check(builds.wait(builds.start_cook(scene)).state == "succeeded",
|
||||||
|
"Matching component v2 cooks against the published schema");
|
||||||
|
|
||||||
|
std::vector<Json> invalid;
|
||||||
|
auto candidate = valid;
|
||||||
|
candidate["types"][0]["fields"]["speed"]["default"] = "not a number";
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"][0]["fields"]["speed"]["type"] = "unsupported-kind";
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"][0]["fields"]["speed"]["min"] = "not a number";
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"][0]["fields"]["speed"]["enum"] = 2.5;
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"][0]["fields"]["speed"]["id"] = "different-field-id";
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"].push_back(candidate["types"][0]);
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"] = Json::array({authoring::builtin_schemas().schema("faset.transform")});
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
for (const Json& version : {Json(0), Json(2.5), Json("2")}) {
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"][0]["version"] = version;
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
}
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"][0].erase("fields");
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
candidate = valid;
|
||||||
|
candidate["types"] = Json::object();
|
||||||
|
invalid.push_back(candidate);
|
||||||
|
for (std::size_t index = 0; index < invalid.size(); ++index) {
|
||||||
|
atomic_write_json(config.project_root / "schema-fixture.json", invalid[index]);
|
||||||
|
atomic_write(config.project_root / "Scripts/Gameplay.cpp",
|
||||||
|
"// Invalid metadata fixture " + std::to_string(index));
|
||||||
|
const auto failed = builds.wait(builds.start_build());
|
||||||
|
check(failed.state == "failed" && failed.result.empty() && !failed.error.empty(),
|
||||||
|
"Malformed schema must fail before a successful build result is published");
|
||||||
|
check(read_text(last_build) == previous_pointer,
|
||||||
|
"Last good build pointer is preserved");
|
||||||
|
check(sha256_file(player) == previous_player && read_text(schema) == previous_schema &&
|
||||||
|
read_text(directory / "manifest.json") == previous_manifest,
|
||||||
|
"Last good binary, schema and build manifest remain unchanged");
|
||||||
|
std::size_t generations{};
|
||||||
|
for (const auto& entry : fs::directory_iterator(directory.parent_path())) {
|
||||||
|
++generations;
|
||||||
|
check(entry.path() == directory, "Invalid build leaves no staging or generation");
|
||||||
|
}
|
||||||
|
check(generations == 1, "Only the validated build generation remains");
|
||||||
|
bool rejected{};
|
||||||
|
try {
|
||||||
|
authoring.replace_external_schemas(invalid[index]);
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
rejected = true;
|
||||||
|
}
|
||||||
|
check(rejected && authoring.schemas().manifest() == previous_registry,
|
||||||
|
"Cached-schema validation uses the same contract and preserves the registry");
|
||||||
|
}
|
||||||
|
std::cout << "Valid v2 schema and atomic rejection of " << invalid.size()
|
||||||
|
<< " malformed metadata generations passed\n";
|
||||||
|
fs::remove_all(root);
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::cerr << error.what() << '\n';
|
||||||
|
std::error_code ignored;
|
||||||
|
fs::remove_all(root, ignored);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#ifdef _WIN32
|
||||||
|
int wmain(int argc, wchar_t** argv) {
|
||||||
|
return run_utf8_main(argc, argv, test_main);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
return test_main(argc, argv);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#include <faset/core/io.hpp>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
// Native stand-in for CMake and SchemaExporter. Tests exercise the real
|
||||||
|
// asynchronous BuildService and publication code without compiling a game.
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace faset;
|
||||||
|
int tool_main(int argc, char** argv) {
|
||||||
|
try {
|
||||||
|
if (argc == 3 && std::string_view(argv[1]) == "--output") {
|
||||||
|
atomic_write_json(path_from_utf8(argv[2]), read_json("schema-fixture.json"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (argc > 2 && std::string_view(argv[1]) == "--build")
|
||||||
|
return 0;
|
||||||
|
fs::path build;
|
||||||
|
for (int i = 1; i + 1 < argc; ++i)
|
||||||
|
if (std::string_view(argv[i]) == "-B")
|
||||||
|
build = path_from_utf8(argv[i + 1]);
|
||||||
|
if (build.empty())
|
||||||
|
throw std::runtime_error("Fixture expects CMake configure or SchemaExporter arguments");
|
||||||
|
fs::create_directories(build / "shaders");
|
||||||
|
atomic_write(build / "CMakeCache.txt", "Native schema publication fixture\n");
|
||||||
|
const auto self = fs::absolute(path_from_utf8(argv[0]));
|
||||||
|
#ifdef _WIN32
|
||||||
|
constexpr auto suffix = ".exe";
|
||||||
|
#else
|
||||||
|
constexpr auto suffix = "";
|
||||||
|
#endif
|
||||||
|
for (const auto* target : {"faset_player", "faset_schema_exporter"})
|
||||||
|
fs::copy_file(self, build / (std::string(target) + suffix),
|
||||||
|
fs::copy_options::overwrite_existing);
|
||||||
|
for (const auto* entry : {"vertexMain", "fragmentMain", "shadowMain"})
|
||||||
|
for (const auto* extension : {".spv", ".reflection.json"})
|
||||||
|
atomic_write(build / "shaders" / (std::string(entry) + extension), "fixture\n");
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
std::cerr << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef _WIN32
|
||||||
|
int wmain(int argc, wchar_t** argv) {
|
||||||
|
return run_utf8_main(argc, argv, tool_main);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
return tool_main(argc, argv);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user