Checkpoint 4: harden native paths, authoring workflows and Player lifecycle

This commit is contained in:
Emil
2026-09-18 05:14:50 +03:00
parent 5ac4db438d
commit 45bc352d46
88 changed files with 12606 additions and 504 deletions
+44 -37
View File
@@ -74,7 +74,7 @@ std::set<std::string> asset_references(const Json& scene) {
void copy_required_file(const fs::path& source, const fs::path& target) {
if (!fs::is_regular_file(source) || fs::is_symlink(source))
throw std::runtime_error("Required package file missing or not a regular file: " +
source.string());
path_to_utf8(source));
fs::create_directories(target.parent_path());
fs::copy_file(source, target, fs::copy_options::overwrite_existing);
}
@@ -248,9 +248,9 @@ struct BuildService::Impl {
fs::create_directories(native_directory);
std::vector<std::string> arguments = {config.cmake,
"-S",
config.engine_root.string(),
path_to_utf8(config.engine_root),
"-B",
native_directory.string(),
path_to_utf8(native_directory),
"-G",
config.generator,
"-DCMAKE_BUILD_TYPE=" + configuration,
@@ -260,7 +260,7 @@ struct BuildService::Impl {
"-DFASET_BUILD_RUNTIME=ON",
"-DFASET_BUILD_ASSETS=ON",
"-DFASET_GAMEPLAY_SOURCE_DIR=" +
(config.project_root / "Scripts").string()};
path_to_utf8(config.project_root / "Scripts")};
bool compiler_overridden = false;
for (const auto& arg : config.configure_arguments)
if (arg.starts_with("-DCMAKE_CXX_COMPILER="))
@@ -268,11 +268,11 @@ struct BuildService::Impl {
if (!compiler_overridden) {
#ifdef _WIN32
auto compiler = find_executable("clang-cl");
arguments.push_back("-DCMAKE_C_COMPILER=" + compiler.string());
arguments.push_back("-DCMAKE_CXX_COMPILER=" + compiler.string());
arguments.push_back("-DCMAKE_C_COMPILER=" + path_to_utf8(compiler));
arguments.push_back("-DCMAKE_CXX_COMPILER=" + path_to_utf8(compiler));
#else
arguments.push_back("-DCMAKE_C_COMPILER=" + find_executable("clang").string());
arguments.push_back("-DCMAKE_CXX_COMPILER=" + find_executable("clang++").string());
arguments.push_back("-DCMAKE_C_COMPILER=" + path_to_utf8(find_executable("clang")));
arguments.push_back("-DCMAKE_CXX_COMPILER=" + path_to_utf8(find_executable("clang++")));
#endif
}
arguments.insert(arguments.end(), config.configure_arguments.begin(),
@@ -281,7 +281,7 @@ struct BuildService::Impl {
run(job, std::move(arguments), config.project_root);
checkpoint(job, "Compiling and linking Player", .25);
run(job,
{config.cmake, "--build", native_directory.string(), "--config", configuration,
{config.cmake, "--build", path_to_utf8(native_directory), "--config", configuration,
"--parallel", "4", "--target", "faset_player", "faset_schema_exporter"},
config.project_root);
checkpoint(job, "Exporting gameplay schema", .58);
@@ -292,7 +292,8 @@ struct BuildService::Impl {
fs::create_directories(staging);
try {
const auto schema_file = staging / "schema.json";
run(job, {exporter.string(), "--output", schema_file.string()}, config.project_root);
run(job, {path_to_utf8(exporter), "--output", path_to_utf8(schema_file)},
config.project_root);
auto schema = read_json(schema_file);
if (schema.value("format", "") != "faset.schema" || schema.value("version", 0) != 1 ||
!schema.contains("types") || !schema.at("types").is_array())
@@ -324,11 +325,11 @@ struct BuildService::Impl {
atomic_write_json(config.cache_root / "last_build.json",
{{"generation", job.status.id}, {"fingerprint", fingerprint}});
return {{"generation", job.status.id},
{"directory", generation.string()},
{"build_directory", native_directory.string()},
{"directory", path_to_utf8(generation)},
{"build_directory", path_to_utf8(native_directory)},
{"configuration", configuration},
{"player", (generation / ("faset_player" + executable_suffix())).string()},
{"schema", (generation / "schema.json").string()},
{"player", path_to_utf8(generation / ("faset_player" + executable_suffix()))},
{"schema", path_to_utf8(generation / "schema.json")},
{"fingerprint", fingerprint}};
} catch (...) {
std::error_code error;
@@ -346,7 +347,7 @@ struct BuildService::Impl {
{executable.parent_path(), native_directory, native_directory / configuration})
if (fs::is_directory(root))
for (const auto& entry : fs::directory_iterator(root)) {
auto extension = entry.path().extension().string();
auto extension = path_to_utf8(entry.path().extension());
for (auto& c : extension)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (entry.is_regular_file() && extension == ".dll")
@@ -356,7 +357,8 @@ struct BuildService::Impl {
(void)job;
#else
const auto output =
run(job, {find_executable("ldd").string(), executable.string()}, config.project_root);
run(job, {path_to_utf8(find_executable("ldd")), path_to_utf8(executable)},
config.project_root);
if (output.find("not found") != std::string::npos)
throw std::runtime_error("Player has unresolved shared library dependencies");
// SDL/physics/gameplay are linked statically. glibc/libstdc++/Vulkan are the host baseline.
@@ -427,8 +429,8 @@ struct BuildService::Impl {
fs::rename(staging, directory);
atomic_write_json(config.cache_root / "last_cook.json", {{"generation", digest}});
return {{"generation", digest},
{"scene", (directory / "scene.fscene").string()},
{"directory", directory.string()}};
{"scene", path_to_utf8(directory / "scene.fscene")},
{"directory", path_to_utf8(directory)}};
} catch (...) {
std::error_code error;
fs::remove_all(staging, error);
@@ -451,13 +453,14 @@ struct BuildService::Impl {
for (const auto& entry : fs::directory_iterator(source)) {
if (!entry.is_regular_file())
continue;
auto filename = entry.path().filename().string();
auto filename = path_to_utf8(entry.path().filename());
std::string upper = filename;
for (auto& c : upper)
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
if (upper.starts_with("LICENSE") || upper.starts_with("COPYING") ||
upper.starts_with("NOTICE")) {
copy_required_file(entry.path(), destination / name / filename);
copy_required_file(entry.path(),
destination / name / entry.path().filename());
copied = true;
}
}
@@ -487,7 +490,7 @@ struct BuildService::Impl {
"generations" / generation);
auto target = destination / "assets" / id / "generations" / generation;
for (const auto& file : manifest.at("files")) {
auto relative = fs::path(file.at("path").get<std::string>());
auto relative = path_from_utf8(file.at("path").get<std::string>());
copy_required_file(project_path(source_directory, relative),
project_path(target, relative));
}
@@ -508,7 +511,8 @@ struct BuildService::Impl {
for (const auto& id : asset_references(job.scene))
job.asset_manifests[id] = source.current_manifest(id);
const auto built = build(job, true);
validate_component_types(job.scene, read_json(built.at("schema").get<std::string>()));
validate_component_types(job.scene,
read_json(path_from_utf8(built.at("schema").get<std::string>())));
auto output = fs::absolute(job.output);
if (output.empty())
throw std::runtime_error("An export destination is required");
@@ -519,7 +523,7 @@ struct BuildService::Impl {
try {
checkpoint(job, "Cooking export snapshot", .72);
write_cooked_scene(staging / "scene.fscene", job.scene);
auto build_directory = fs::path(built.at("directory").get<std::string>());
auto build_directory = path_from_utf8(built.at("directory").get<std::string>());
copy_required_file(build_directory / ("faset_player" + executable_suffix()),
staging / ("faset_player" + executable_suffix()));
for (const auto* shader : {"vertexMain.spv", "fragmentMain.spv", "shadowMain.spv",
@@ -528,7 +532,7 @@ struct BuildService::Impl {
copy_required_file(build_directory / "shaders" / shader,
staging / "shaders" / shader);
for (const auto& entry : fs::directory_iterator(build_directory)) {
auto extension = entry.path().extension().string();
auto extension = path_to_utf8(entry.path().extension());
for (auto& c : extension)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (entry.is_regular_file() && extension == ".dll")
@@ -536,7 +540,8 @@ struct BuildService::Impl {
}
checkpoint(job, "Packaging assets and notices", .80);
package_assets(job, staging);
package_notices(staging / "Notices", built.at("build_directory").get<std::string>());
package_notices(staging / "Notices",
path_from_utf8(built.at("build_directory").get<std::string>()));
atomic_write(
staging / "README.txt",
"Run faset_player" + executable_suffix() +
@@ -553,8 +558,9 @@ struct BuildService::Impl {
checkpoint(job, "Validating packaged Player", .90);
auto validation_output =
run(job,
{(staging / ("faset_player" + executable_suffix())).string(), "--validate",
"--scene", (staging / "scene.fscene").string(), "--assets", staging.string()},
{path_to_utf8(staging / ("faset_player" + executable_suffix())), "--validate",
"--scene", path_to_utf8(staging / "scene.fscene"), "--assets",
path_to_utf8(staging)},
staging);
Json files = Json::array();
for (const auto& entry : fs::recursive_directory_iterator(staging)) {
@@ -562,7 +568,7 @@ struct BuildService::Impl {
throw std::runtime_error("Export contains a symlink");
if (entry.is_regular_file())
files.push_back(
{{"path", entry.path().lexically_relative(staging).generic_string()},
{{"path", generic_path_to_utf8(entry.path().lexically_relative(staging))},
{"sha256", sha256_file(entry.path())},
{"size", entry.file_size()}});
}
@@ -613,15 +619,16 @@ struct BuildService::Impl {
{"version", 1},
{"generation", job.status.id},
{"directory", "generations/" + job.status.id}});
return {{"directory", generation.string()},
{"executable", (generation / ("faset_player" + executable_suffix())).string()},
{"manifest", (generation / "manifest.json").string()},
{"generation", job.status.id},
{"build", built},
{"schema", built.at("schema")},
{"player", built.at("player")},
{"build_directory", built.at("build_directory")},
{"configuration", built.at("configuration")}};
return {
{"directory", path_to_utf8(generation)},
{"executable", path_to_utf8(generation / ("faset_player" + executable_suffix()))},
{"manifest", path_to_utf8(generation / "manifest.json")},
{"generation", job.status.id},
{"build", built},
{"schema", built.at("schema")},
{"player", built.at("player")},
{"build_directory", built.at("build_directory")},
{"configuration", built.at("configuration")}};
} catch (...) {
std::error_code error;
fs::remove_all(staging, error);