wasm vfs path fix
Some checks failed
CI / build-test-release (push) Failing after 14m41s

This commit is contained in:
Jorijn van der Graaf 2026-05-19 03:28:27 +02:00
commit b8dc380c33
2 changed files with 49 additions and 34 deletions

View file

@ -1098,24 +1098,39 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
// Walk the dep graph again for non-JS assets — these get pre-loaded by // Walk the dep graph again for non-JS assets — these get pre-loaded by
// runtime.js into an in-memory VFS so the wasm's std::ifstream et al. // runtime.js into an in-memory VFS so the wasm's std::ifstream et al.
// can actually read them (the wasi-runtime in this repo otherwise // can actually read them (the wasi-runtime in this repo otherwise
// stubs every fd syscall to zero). The manifest lists basenames only; // stubs every fd syscall to zero).
// each file lives next to the .wasm in the bin dir. wasi-runtime //
// reduces every path_open path to its basename, so subdir layouts // The manifest lists *relative paths* (e.g. "assets/Inter.ttf") so
// collapse — that's fine for our flat-bin convention. // runtime.js's fetch() resolves against the bin-dir layout the asset
auto compressedBasename = [](const fs::path& src) -> std::optional<std::string> { // copy step actually emits. The VFS is keyed by basename — path_open
// strips to basename on lookup, so subdir layouts collapse on the
// wasm side. Basename collisions across subdirs aren't supported on
// the wasi runtime today; if two assets share a basename, the last
// one preloaded wins. Avoid collisions in the source tree.
auto compressedExt = [](const fs::path& src) -> std::optional<std::string> {
std::string ext = src.extension().string(); std::string ext = src.extension().string();
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
fs::path out = src.filename();
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") { if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
out.replace_extension(".ctex"); return std::string(".ctex");
} else if (ext == ".obj") {
out.replace_extension(".cmesh");
} else {
return std::nullopt;
} }
return out.string(); if (ext == ".obj") return std::string(".cmesh");
return std::nullopt;
};
auto compressedRel = [&](const fs::path& rel) -> fs::path {
if (auto ext = compressedExt(rel)) {
fs::path out = rel;
out.replace_extension(*ext);
return out;
}
return rel;
}; };
std::vector<std::string> assetFiles; std::vector<std::string> assetFiles;
auto pushUnique = [&](std::string name) {
if (name.empty()) return;
if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) {
assetFiles.push_back(std::move(name));
}
};
seen.clear(); seen.clear();
std::function<void(Configuration*)> walkAssets = [&](Configuration* c) { std::function<void(Configuration*)> walkAssets = [&](Configuration* c) {
if (!c || !seen.insert(c).second) return; if (!c || !seen.insert(c).second) return;
@ -1123,35 +1138,25 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
std::string ext = f.extension().string(); std::string ext = f.extension().string();
if (ext == ".js" || ext == ".html") continue; if (ext == ".js" || ext == ".html") continue;
if (f.filename() == "runtime.js") continue; if (f.filename() == "runtime.js") continue;
std::string name = f.filename().string(); // cfg.files lands flat next to the .wasm by `name = filename()`.
if (name.empty()) continue; pushUnique(f.filename().string());
if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) {
assetFiles.push_back(std::move(name));
}
} }
// cfg.assets — emit the *compressed* output basename. Directory // cfg.assets — mirror the bin-dir layout the build emits: a
// entries get the same compressed-basename treatment per file; // directory entry becomes <topName>/<rel inside dir>, single
// unrecognized extensions are treated as passthrough (kept under // files land flat at the bin root. .png/.obj are compressed in
// their original name, matching the build-side passthrough copy). // place; everything else passes through under its original name.
for (const fs::path& a : c->assets) { for (const fs::path& a : c->assets) {
if (fs::is_directory(a)) { if (fs::is_directory(a)) {
const fs::path topName = a.filename();
std::error_code ec; std::error_code ec;
for (const auto& entry : fs::recursive_directory_iterator(a, ec)) { for (const auto& entry : fs::recursive_directory_iterator(a, ec)) {
if (ec) break; if (ec) break;
if (!entry.is_regular_file()) continue; if (!entry.is_regular_file()) continue;
std::string name = compressedBasename(entry.path()) fs::path rel = fs::relative(entry.path(), a);
.value_or(entry.path().filename().string()); pushUnique((topName / compressedRel(rel)).generic_string());
if (name.empty()) continue;
if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) {
assetFiles.push_back(std::move(name));
}
} }
} else { } else {
std::optional<std::string> name = compressedBasename(a); pushUnique(compressedRel(a.filename()).generic_string());
if (!name) continue;
if (std::find(assetFiles.begin(), assetFiles.end(), *name) == assetFiles.end()) {
assetFiles.push_back(std::move(*name));
}
} }
} }
for (Configuration* dep : c->dependencies) walkAssets(dep); for (Configuration* dep : c->dependencies) walkAssets(dep);

View file

@ -301,6 +301,12 @@ if (!wasmUrl) {
// EnableWasiBrowserRuntime) into an in-memory VFS so wasi-libc's file // EnableWasiBrowserRuntime) into an in-memory VFS so wasi-libc's file
// syscalls work against them. Browser builds otherwise can't open assets // syscalls work against them. Browser builds otherwise can't open assets
// shipped alongside the .wasm — sync XHR is too deprecated to rely on. // shipped alongside the .wasm — sync XHR is too deprecated to rely on.
//
// Manifest entries are relative paths under the bin dir (the layout
// `cfg.assets` produces — e.g. "assets/Inter.ttf",
// "mods/3DForts_Base/foo.cmesh"). We fetch each at its full path but
// key the VFS by basename so `path_open`'s basename-reduction can find
// it regardless of the C++ side's cwd or prefix.
const vfs = new Map(); const vfs = new Map();
try { try {
const manifestResp = await fetch("files.json"); const manifestResp = await fetch("files.json");
@ -308,8 +314,12 @@ try {
const names = await manifestResp.json(); const names = await manifestResp.json();
await Promise.all(names.map(async (name) => { await Promise.all(names.map(async (name) => {
const r = await fetch(name); const r = await fetch(name);
if (r.ok) vfs.set(name, new Uint8Array(await r.arrayBuffer())); if (!r.ok) {
else console.warn(`[wasi] failed to preload ${name}: HTTP ${r.status}`); console.warn(`[wasi] failed to preload ${name}: HTTP ${r.status}`);
return;
}
const base = name.split(/[\\/]/).filter(Boolean).pop() || name;
vfs.set(base, new Uint8Array(await r.arrayBuffer()));
})); }));
} }
} catch (e) { } catch (e) {