recursive assets
Some checks failed
CI / build-test-release (push) Failing after 14m17s

This commit is contained in:
Jorijn van der Graaf 2026-05-12 03:44:14 +02:00
commit dea67ae5aa
2 changed files with 94 additions and 29 deletions

View file

@ -292,36 +292,82 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
});
}
// Asset compilation: each .png/.obj listed in cfg.assets is run through
// Crafter.Asset's library (linked into crafter-build directly — see
// :Asset_impl) to produce the SaveCompressed .ctex/.cmesh in outputDir.
// Skipped if the output is newer than the source. Parallel with shaders
// and module compiles; cancellation participates in the same flag.
auto assetOutput = [](const fs::path& asset) -> std::optional<fs::path> {
std::string ext = asset.extension().string();
if (ext == ".png") return fs::path(asset.filename()).replace_extension(".ctex");
if (ext == ".obj") return fs::path(asset.filename()).replace_extension(".cmesh");
// Asset compilation: each cfg.assets entry is either a single .png/.obj
// file (flat output: outputDir/<filename>.ctex/cmesh — preserves the
// original behavior) or a directory (recursed, with the relative tree
// mirrored under outputDir/<dirname>/; .png/.obj are compressed, every
// other file is copied through unchanged). Directory mode lets mod/map
// trees keep their nested layout so mod.json paths like
// "cannon/base.cmesh" resolve correctly at runtime.
// Skipped per-file if the output is newer than the source. Each
// compress runs in its own thread; passthrough copies are bundled into
// a single thread to match the cfg.files pattern.
auto compressedName = [](const fs::path& src) -> std::optional<fs::path> {
std::string ext = src.extension().string();
if (ext == ".png") return fs::path(src.filename()).replace_extension(".ctex");
if (ext == ".obj") return fs::path(src.filename()).replace_extension(".cmesh");
return std::nullopt;
};
for (const fs::path& asset : config.assets) {
std::optional<fs::path> outName = assetOutput(asset);
if (!outName) {
buildCancelled.store(true);
buildError = std::format("{}: unsupported asset extension (expected .png or .obj)", asset.string());
break;
}
fs::path out = outputDir / *outName;
if (fs::exists(out) && fs::exists(asset) && fs::last_write_time(asset) <= fs::last_write_time(out)) continue;
threads.emplace_back([&asset, out = std::move(out), &buildError, &buildCancelled]() {
Progress::Task task(std::format("Compressing asset {}", asset.filename().string()));
auto submitCompress = [&](fs::path sourcePath, fs::path outRelative) {
fs::path out = outputDir / outRelative;
if (fs::exists(out) && fs::exists(sourcePath) && fs::last_write_time(sourcePath) <= fs::last_write_time(out)) return;
threads.emplace_back([sourcePath = std::move(sourcePath), out = std::move(out), &buildError, &buildCancelled]() {
Progress::Task task(std::format("Compressing asset {}", sourcePath.filename().string()));
if (buildCancelled.load(std::memory_order_relaxed)) return;
std::string result = CompressAsset(asset, out);
std::error_code ec;
fs::create_directories(out.parent_path(), ec);
std::string result = CompressAsset(sourcePath, out);
if (result.empty()) return;
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
});
};
std::vector<std::pair<fs::path, fs::path>> assetPassthroughs;
for (const fs::path& asset : config.assets) {
if (fs::is_directory(asset)) {
fs::path topName = asset.filename();
for (const auto& entry : fs::recursive_directory_iterator(asset)) {
if (!entry.is_regular_file()) continue;
fs::path rel = fs::relative(entry.path(), asset);
if (std::optional<fs::path> compName = compressedName(entry.path())) {
submitCompress(entry.path(), topName / rel.parent_path() / *compName);
} else {
assetPassthroughs.emplace_back(entry.path(), topName / rel);
}
}
} else {
std::optional<fs::path> outName = compressedName(asset);
if (!outName) {
buildCancelled.store(true);
buildError = std::format("{}: unsupported asset extension (expected .png or .obj, or a directory)", asset.string());
break;
}
submitCompress(asset, *outName);
}
}
if (!assetPassthroughs.empty()) {
threads.emplace_back([passthroughs = std::move(assetPassthroughs), &outputDir, &buildCancelled, &buildError]() {
Progress::Task task("Copying asset passthrough files");
if (buildCancelled.load(std::memory_order_relaxed)) return;
try {
for (const auto& [src, rel] : passthroughs) {
fs::path dst = outputDir / rel;
fs::create_directories(dst.parent_path());
if (!fs::exists(dst)) {
fs::copy_file(src, dst);
} else if (fs::last_write_time(src) > fs::last_write_time(dst)) {
fs::copy_file(src, dst, fs::copy_options::overwrite_existing);
}
}
} catch (const std::exception& e) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = e.what();
}
}
});
}
threads.emplace_back([&config, &outputDir, &buildCancelled, &buildError]() {
@ -679,6 +725,16 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
copyTree(src, outputDir / additionalFile.filename());
}
for (const fs::path& asset : dep->assets) {
// Directory entry: the dep already mirrored the
// (compressed + passthrough) tree under
// depBinDir/<asset.filename()>/. Forward it wholesale
// so our bin dir gets the same layout.
if (fs::is_directory(asset)) {
fs::path src = depBinDir / asset.filename();
if (!fs::exists(src)) continue;
copyTree(src, outputDir / asset.filename());
continue;
}
std::string ext = asset.extension().string();
fs::path srcName = asset.filename();
if (ext == ".png") srcName.replace_extension(".ctex");

View file

@ -125,14 +125,23 @@ export namespace Crafter {
std::vector<fs::path> buildFiles;
std::vector<Define> defines;
std::vector<Shader> shaders;
// Source assets (.png, .obj) compressed via Crafter.Asset's
// SaveCompressed → .ctex/.cmesh in this configuration's bin dir.
// Requires Crafter.Asset in cfg.dependencies (transitively): the
// build engine locates it by name and uses its library API to do
// the conversion (an auxiliary host-target executable, generated
// once per build host, links Crafter.Asset and is invoked per
// asset). Forwarded to a consuming executable's bin dir alongside
// .spv shaders and cfg.files entries.
// Source assets compressed via Crafter.Asset's SaveCompressed →
// .ctex/.cmesh in this configuration's bin dir.
//
// Each entry is either:
// - A single .png/.obj file. Output lands flat in the bin dir:
// bin/<filename>.ctex or bin/<filename>.cmesh.
// - A directory. The build recurses; .png/.obj are compressed
// with the relative tree mirrored under bin/<dirname>/, and
// every other file in the tree is copied through unchanged.
// Lets mod/map trees (mod.json + cannon/base.obj +
// cannon/color.png) keep their nested layout so JSON paths
// like "cannon/base.cmesh" resolve at runtime.
//
// Crafter.Asset must be reachable through cfg.dependencies (the
// build engine links its library API into crafter-build via a
// self-host pass). Forwarded to a consuming executable's bin dir
// alongside .spv shaders and cfg.files entries.
std::vector<fs::path> assets;
std::vector<ExternalDependency> externalDependencies;
std::vector<std::string> compileFlags;