/* Crafter® Build Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) #include #include // (pulled in transitively) defines `interface` as a macro for `struct`, // which collides with local variables named `interface` in this TU. #undef interface #else #include #include #include #endif export module Crafter.Build:Clang_impl; import std; import :Clang; import :Platform; import :Test; import :Progress; import :Asset; namespace fs = std::filesystem; using namespace Crafter; void Configuration::GetInterfacesAndImplementations(std::span interfaces, std::span implementations) { auto resolveImport = [this](const std::string& importName, std::vector& localDeps, std::vector>& externalDeps) -> bool { for(const std::unique_ptr& interface : this->interfaces) { if(interface->name == importName) { localDeps.push_back(interface.get()); return true; } } std::unordered_set seen; std::function walk = [&](Configuration* depCfg) -> bool { if (!seen.insert(depCfg).second) return false; for(const std::unique_ptr& depInterface : depCfg->interfaces) { if(depInterface->name == importName) { fs::path depPcmPath = (depCfg->PcmDir() / depInterface->path.filename()).string() + ".pcm"; externalDeps.emplace_back(depInterface.get(), std::move(depPcmPath)); return true; } } for(Configuration* sub : depCfg->dependencies) { if (walk(sub)) return true; } return false; }; for(Configuration* depCfg : this->dependencies) { if (walk(depCfg)) return true; } return false; }; std::vector> tempModulePaths = std::vector>(interfaces.size()); for(std::uint16_t i = 0; i < interfaces.size(); i++){ // Resolve to absolute now so the stored path survives cwd changes // (matters for GitProject deps loaded from a different working dir). fs::path file = fs::absolute(path / interfaces[i]).lexically_normal(); file += ".cppm"; std::ifstream t(file); std::stringstream buffer; buffer << t.rdbuf(); std::string fileContent = buffer.str(); fileContent = std::regex_replace(fileContent, std::regex(R"(//[^\n]*)"), ""); fileContent = std::regex_replace(fileContent, std::regex(R"(/\*.*?\*/)"), ""); tempModulePaths[i] = {file, fileContent, nullptr, nullptr}; } std::erase_if(tempModulePaths, [this](std::tuple& file) { std::smatch match; if (std::regex_search(std::get<1>(file), match, std::regex(R"(export module ([a-zA-Z0-9_\.\-]+);)"))) { std::get<0>(file).replace_extension(""); this->interfaces.push_back(std::make_unique(std::move(match[1].str()), std::move(std::get<0>(file)))); return true; } else { return false; } }); for(std::uint16_t i = 0; i < tempModulePaths.size(); i++) { std::smatch match; if (std::regex_search(std::get<1>(tempModulePaths[i]), match, std::regex(R"(export module ([a-zA-Z_0-9\.\-]+):([a-zA-Z_0-9\.\-]+);)"))) { for(const std::unique_ptr& modulee : this->interfaces) { if(modulee->name == match[1]) { std::string name = match[2].str(); fs::path pthCpy = std::get<0>(tempModulePaths[i]); pthCpy.replace_extension(""); std::unique_ptr partition = std::make_unique(std::move(name), std::move(pthCpy)); std::get<2>(tempModulePaths[i]) = partition.get(); modulee->partitions.push_back(std::move(partition)); std::get<3>(tempModulePaths[i]) = modulee.get(); goto next; } } throw std::runtime_error(std::format("Module {} not found, referenced in {}", match[1].str(), std::get<0>(tempModulePaths[i]).string())); } else { throw std::runtime_error(std::format("No module declaration found in {}", std::get<0>(tempModulePaths[i]).string())); } next:; } for(std::tuple& file : tempModulePaths) { ModulePartition* partition = std::get<2>(file); Module* parentModule = std::get<3>(file); const std::string& fileContent = std::get<1>(file); std::regex partitionPattern(R"(import :([a-zA-Z_\-0-9\.]+);)"); std::sregex_iterator currentMatch(fileContent.begin(), fileContent.end(), partitionPattern); std::sregex_iterator lastMatch; while (currentMatch != lastMatch) { std::smatch match = *currentMatch; for(std::unique_ptr& sibling : parentModule->partitions) { if(sibling->name == match[1]) { partition->partitionDependencies.push_back(sibling.get()); goto next2; } } throw std::runtime_error(std::format("imported partition {}:{} not found, referenced in {}", parentModule->name, match[1].str(), std::get<0>(file).string())); next2: ++currentMatch; } std::regex modulePattern(R"(import ([a-zA-Z_0-9\.\-]+);)"); std::sregex_iterator modCurrent(fileContent.begin(), fileContent.end(), modulePattern); while (modCurrent != lastMatch) { std::smatch match = *modCurrent; resolveImport(match[1].str(), partition->moduleDependencies, partition->externalModuleDependencies); ++modCurrent; } } for(const fs::path& tempFile : implementations) { fs::path file = fs::absolute(path / tempFile).lexically_normal(); file += ".cpp"; std::ifstream t(file); std::stringstream buffer; buffer << t.rdbuf(); std::string fileContent = buffer.str(); fileContent = std::regex_replace(fileContent, std::regex(R"(//[^\n]*)"), ""); fileContent = std::regex_replace(fileContent, std::regex(R"(/\*.*?\*/)"), ""); std::smatch match; fs::path fileCopy = file; fileCopy.replace_extension(""); Implementation& implementation = this->implementations.emplace_back(std::move(fileCopy)); if (std::regex_search(fileContent, match, std::regex(R"(module ([a-zA-Z0-9_\.\-]+)(:[a-zA-Z0-9_\.\-]+)?\s*;)"))) { bool isPartitionImpl = match[2].length() > 0; for(const std::unique_ptr& interface : this->interfaces) { if(interface->name == match[1]) { if (!isPartitionImpl) { implementation.moduleDependencies.push_back(interface.get()); } std::regex partitionPattern(R"(import :([a-zA-Z_\-0-9\.]+);)"); std::sregex_iterator currentMatch(fileContent.begin(), fileContent.end(), partitionPattern); std::sregex_iterator lastMatch; while (currentMatch != lastMatch) { std::smatch match2 = *currentMatch; for(const std::unique_ptr& partition : interface->partitions) { if(partition->name == match2[1]) { implementation.partitionDependencies.push_back(partition.get()); goto next3; } } throw std::runtime_error(std::format("imported partition {}:{} not found, referenced in {}", match[1].str(), match2[1].str(), file.string())); next3: ++currentMatch; } std::regex modulePattern(R"(import ([a-zA-Z_0-9\.\-]+);)"); std::sregex_iterator modCurrent(fileContent.begin(), fileContent.end(), modulePattern); while (modCurrent != lastMatch) { std::smatch match2 = *modCurrent; if (match2[1] != match[1]) { resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies); } ++modCurrent; } goto next4; } } throw std::runtime_error(std::format("Module {} not found not found, referenced in {}", match[1].str(), file.string())); next4:; } else { std::regex pattern(R"(import ([a-zA-Z_\-0-9\.]+);)"); std::sregex_iterator currentMatch(fileContent.begin(), fileContent.end(), pattern); std::sregex_iterator lastMatch; while (currentMatch != lastMatch) { std::smatch match2 = *currentMatch; resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies); ++currentMatch; } } } } BuildResult Crafter::Build(Configuration& config, std::unordered_map>& depResults, std::mutex& depMutex) { // Reset per-build cached state on every Module/ModulePartition so that // successive Build() calls on the same Configuration re-evaluate mtimes // (incremental-rebuild test scenarios). Walks cfg.dependencies recursively // with a seen-set so diamond deps don't loop. { std::unordered_set resetSeen; std::function reset = [&](Configuration* c) { if (!resetSeen.insert(c).second) return; for (auto& iface : c->interfaces) { iface->checked = false; iface->compiled.store(false); for (auto& part : iface->partitions) { part->checked = false; part->compiled.store(false); } } for (Configuration* dep : c->dependencies) { reset(dep); } }; reset(&config); } // Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm // and the main compile command see the same value. Linux-only — Windows // users supply cfg.sysroot pointing at their wasi-sdk install. Covers all // wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple // subdirs handle the differences. #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu if (config.sysroot.empty() && config.target.starts_with("wasm32")) { config.sysroot = "/usr/share/wasi-sysroot"; } #endif fs::path buildDir = config.BuildDir(); fs::path outputDir = config.BinDir(); if (!fs::exists(buildDir)) { fs::create_directories(buildDir); } if (!fs::exists(outputDir)) { fs::create_directories(outputDir); } BuildResult buildResult{}; // glslang #include search paths for every shader compiled in this // configuration: each transitive (incl. self) buildFiles entry's parent // dir, or the entry itself if it points at a directory. Collected once // up front so the per-shader threads can capture the resulting span by // reference. No file copy involved — glslang reads the includes in // place from the dep's source tree. std::vector shaderIncludeDirs; { std::unordered_set seenDirs; std::unordered_set seenCfg; std::function collect = [&](const Configuration* c) { if (!seenCfg.insert(c).second) return; for (const fs::path& bf : c->buildFiles) { fs::path dir = fs::is_directory(bf) ? bf : bf.parent_path(); if (seenDirs.insert(dir.string()).second) { shaderIncludeDirs.push_back(std::move(dir)); } } for (const Configuration* sub : c->dependencies) collect(sub); }; collect(&config); } std::vector threads; threads.reserve(config.shaders.size() + 1 + config.interfaces.size() + config.implementations.size()); std::string buildError; std::atomic buildCancelled{false}; for (const Shader& shader : config.shaders) { if (shader.Check(outputDir)) continue; threads.emplace_back([&shader, &outputDir, &shaderIncludeDirs, &buildError, &buildCancelled]() { Progress::Task task(std::format("Compiling shader {}", shader.path.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; std::string result = shader.Compile(outputDir, shaderIncludeDirs); if (result.empty()) return; bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = std::move(result); } }); } // Asset compilation: each cfg.assets entry is either a single .png/.obj // file (flat output: outputDir/.ctex/cmesh — preserves the // original behavior) or a directory (recursed, with the relative tree // mirrored under outputDir//; .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 { std::string ext = src.extension().string(); for (char& c : ext) c = static_cast(std::tolower(static_cast(c))); // stb_image (used by CompressAsset → TextureAsset::LoadPNG) handles // png/tga/jpg/bmp; all map to .ctex. if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") { return fs::path(src.filename()).replace_extension(".ctex"); } if (ext == ".obj") return fs::path(src.filename()).replace_extension(".cmesh"); return std::nullopt; }; 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::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> 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 compName = compressedName(entry.path())) { submitCompress(entry.path(), topName / rel.parent_path() / *compName); } else { assetPassthroughs.emplace_back(entry.path(), topName / rel); } } } else { std::optional outName = compressedName(asset); if (!outName) { buildCancelled.store(true); buildError = std::format("{}: unsupported asset extension (expected .png/.tga/.jpg/.bmp/.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]() { Progress::Task task(std::format("Copying files for {}", config.name)); if (buildCancelled.load(std::memory_order_relaxed)) return; try { for (const fs::path& additionalFile : config.files) { fs::path destination = outputDir / additionalFile.filename(); if (fs::is_directory(additionalFile)) { for (const auto& entry : fs::recursive_directory_iterator(additionalFile)) { const fs::path& sourcePath = entry.path(); fs::path relativePath = fs::relative(sourcePath, additionalFile); fs::path destPath = destination / relativePath; if (entry.is_directory()) { if (!fs::exists(destPath)) fs::create_directories(destPath); } else if (entry.is_regular_file()) { fs::create_directories(destPath.parent_path()); if (!fs::exists(destPath)) { fs::copy_file(sourcePath, destPath); } else if (fs::last_write_time(sourcePath) > fs::last_write_time(destPath)) { fs::copy_file(sourcePath, destPath, fs::copy_options::overwrite_existing); } } } } else { if (!fs::exists(destination)) { fs::copy_file(additionalFile, destination); } else if (fs::last_write_time(additionalFile) > fs::last_write_time(destination)) { fs::copy_file(additionalFile, destination, fs::copy_options::overwrite_existing); } } } } catch(std::exception& e) { bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = e.what(); } } }); std::vector externalResults(config.externalDependencies.size()); std::vector externalThreads; externalThreads.reserve(config.externalDependencies.size()); for (std::size_t i = 0; i < config.externalDependencies.size(); ++i) { externalThreads.emplace_back([&, i]() { Progress::Task task(std::format("Building external dep {}", config.externalDependencies[i].name)); if (buildCancelled.load(std::memory_order_relaxed)) return; externalResults[i] = BuildExternal(config.externalDependencies[i], config.target, buildCancelled); if (!externalResults[i].error.empty()) { bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = externalResults[i].error; } } }); } fs::path stdPcmDir = GetCacheDir()/(config.target+"-"+config.march); if (!fs::exists(stdPcmDir)) { fs::create_directories(stdPcmDir); } std::string stdPcmResult; { Progress::Task task(std::format("Building std PCM ({}-{})", config.target, config.march)); stdPcmResult = BuildStdPcm(config, stdPcmDir/"std.pcm"); } if(!stdPcmResult.empty()) { buildCancelled.store(true); for(std::thread& thread : threads) thread.join(); for(std::thread& thread : externalThreads) thread.join(); return {stdPcmResult, false, {}}; } fs::path pcmDir; if(config.type != ConfigurationType::Executable) { pcmDir = outputDir; } else { pcmDir = buildDir; } fs::copy_file(stdPcmDir/"std.pcm", pcmDir/"std.pcm", fs::copy_options::update_existing); std::string editedTarget = config.target; std::replace(editedTarget.begin(), editedTarget.end(), '-', '_'); // wasm32 targets reject -march and silently ignore -mtune (clang errors on // the former). Skip both for any wasm32-* triple. bool isWasm = config.target.starts_with("wasm32"); std::string archFlags = isWasm ? std::string() : std::format(" -march={} -mtune={}", config.march, config.mtune); std::string command = std::format("{} --target={}{} -std=c++26 -D CRAFTER_BUILD_CONFIGURATION_TARGET=\\\"{}\\\" -D CRAFTER_BUILD_CONFIGURATION_TARGET_{} -fprebuilt-module-path={} -fprebuilt-module-path={}", GetBaseCommand(config), config.target, archFlags, editedTarget, editedTarget, stdPcmDir.string(), pcmDir.string()); if (!config.sysroot.empty()) { command += std::format(" --sysroot={}", config.sysroot); } if (config.target.starts_with("wasm32")) { // -mllvm is consumed by codegen but not the link driver, which is the // same command line; quiet the unused-flag warning rather than split // compile and link commands. command += " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument"; } if (config.target == "x86_64-w64-mingw32") { // mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without // -femulated-tls clang generates native-TLS references that don't // match. Symptom: undefined std::__once_callable / __once_call at // link time. Also -Wno-unused… because -femulated-tls is a codegen // flag the link driver doesn't consume. command += " -femulated-tls -Wno-unused-command-line-argument"; } if(config.type == ConfigurationType::LibraryDynamic) { #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY"; #endif #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY"; #endif } else if(config.type == ConfigurationType::Executable) { command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE"; // On Windows targets the API uses __declspec(dllimport) when consuming // a DLL. Set the macro for executables so CRAFTER_API resolves to // dllimport in their PCM cache (separate from the lib's PCM cache, // which gets dllexport). Harmless if the exe doesn't actually link a // crafter DLL — CRAFTER_API only matters at API call sites. if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") { command += " -D CRAFTER_BUILD_DLL_IMPORT"; } } else { command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY"; } std::string files; std::unordered_set libSet; std::unordered_set publicFlagSet; std::mutex fileMutex; std::vector depThreads; depThreads.reserve(config.dependencies.size()); std::atomic repack(false); // -I propagation that's valid for both C and C++ compiles. Module-only // bits (-fprebuilt-module-path) stay on `command` only. std::string includeFlags; { std::unordered_set seen; std::function addFlags = [&](Configuration* dep) { if (!seen.insert(dep).second) return; for (const auto& entry : fs::recursive_directory_iterator(dep->path)) { if (entry.is_directory() && entry.path().filename() == "include") { includeFlags += " -I" + entry.path().string(); } } includeFlags += std::format(" -I{}", dep->path.string()); command += std::format(" -fprebuilt-module-path={}", dep->PcmDir().string()); for (Configuration* sub : dep->dependencies) { addFlags(sub); } }; for (Configuration* dep : config.dependencies) { addFlags(dep); } } command += includeFlags; for(Configuration* dep : config.dependencies) { depThreads.emplace_back([&, dep](){ try { if (buildCancelled.load(std::memory_order_relaxed)) return; std::shared_ptr> promise; std::shared_future resultFuture; bool isBuilder = false; depMutex.lock(); fs::path cacheKey = dep->PcmDir(); auto it = depResults.find(cacheKey); if (it == depResults.end()) { isBuilder = true; promise = std::make_shared>(); resultFuture = promise->get_future().share(); depResults.emplace(cacheKey, resultFuture); } else { resultFuture = it->second; } depMutex.unlock(); if (isBuilder) { BuildResult built; try { built = Build(*dep, depResults, depMutex); } catch (...) { promise->set_exception(std::current_exception()); throw; } promise->set_value(std::move(built)); } const BuildResult& result = resultFuture.get(); fileMutex.lock(); for (const std::string& lib : result.libs) libSet.insert(lib); for (const std::string& f : result.publicCompileFlags) publicFlagSet.insert(f); fileMutex.unlock(); if (!result.result.empty()) { bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = result.result; } } if (result.repack) { repack = true; } } catch (const std::exception& e) { bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = std::format("dep build for {} threw: {}", dep->path.string(), e.what()); } } }); } // Defines belong on both C and C++ compiles so vendored C dependencies // can see configuration-level macros consistently with module sources. std::string defineFlags; for(const Define& define : config.defines) { if(define.value.empty()) { defineFlags += std::format(" -D {}", define.name); } else { defineFlags += std::format(" -D {}={}", define.name, define.value); } } command += defineFlags; // Track caller-provided compileFlags separately so the .c compile can // pick them up too (vendored C deps usually need -I from this set). std::string userFlags; for(const std::string& flag : config.compileFlags) { userFlags += " " + flag; } command += userFlags; std::string cmakeBuildType; if(config.debug) { cmakeBuildType = "Debug"; command += " -g -D CRAFTER_BUILD_CONFIGURATION_DEBUG"; } else { cmakeBuildType = "Release"; command += " -O3"; } // Same target-aware setup as the C++ compile path (line 459-): wasm32 // rejects -march, silently ignores -mtune, and needs --sysroot to find // wasi-libc headers. Build the prefix once so all C compiles share it. const bool cIsWasm = config.target.starts_with("wasm32"); std::string cArchFlags = cIsWasm ? std::string() : std::format(" -march={} -mtune={}", config.march, config.mtune); if (!config.sysroot.empty()) { cArchFlags += std::format(" --sysroot={}", config.sysroot); } if (cIsWasm) { // Matches the C++ path's wasi flag set so any libc shim defines // (e.g. _WASI_EMULATED_SIGNAL → signal.h shims) are visible to // C dependencies that drag in signal.h transitively. cArchFlags += " -D_WASI_EMULATED_SIGNAL"; } for (const fs::path& cFile : config.cFiles) { files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string()); const std::string objPath = (buildDir / cFile.filename()).string() + "_source.o"; const std::string srcPath = cFile.string() + ".c"; if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) { threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config, &includeFlags, &defineFlags, &userFlags, &cArchFlags]() { Progress::Task task(std::format("Compiling {}.c", cFile.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; std::string result = RunCommand(std::format("clang {}.c --target={}{} -O3 -c{}{}{} -o {}_source.o", cFile.string(), config.target, cArchFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string())); if (result.empty()) return; bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = std::move(result); } }); } } for (const fs::path& cFile : config.cuda) { files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string()); const std::string objPath = (buildDir / cFile.filename()).string() + "_source.o"; const std::string srcPath = cFile.string() + ".cu"; if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) { threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled]() { Progress::Task task(std::format("Compiling {}.cu", cFile.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; std::string result = RunCommand(std::format("nvcc {}.cu -c -o {}_source.o -O3 -arch=sm_89", cFile.string(), (buildDir / cFile.filename()).string())); if (result.empty()) return; bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = std::move(result); } }); } } for(std::thread& thread : depThreads) { thread.join(); } for(std::thread& thread : externalThreads) { thread.join(); } if(buildCancelled.load()) { for(std::thread& thread : threads) thread.join(); return {buildError, false, {}}; } // Ship runtime artifacts from transitive deps' bin dirs alongside the // executable: compiled .spv files (cfg.shaders) and asset files/dirs // (cfg.files). The lib already mirrored these into its own bin dir // during its build, but a consumer exe loads them from its own dir at // runtime. Only an exe is a deployment unit; intermediate libs don't // need to forward since the exe walks all transitive deps. Runs outside // the repack gate because the relink mtime check above only watches // .so/.dll/.a, so a shader/file-only change in a dep wouldn't trigger // repack but still needs the new artifact copied across. // (cfg.buildFiles use a different mechanism — they're exposed to shader // compiles as #include search paths in place, no copy.) if (config.type == ConfigurationType::Executable) { try { auto copyTree = [](const fs::path& src, const fs::path& dest) { if (fs::is_directory(src)) { for (const auto& entry : fs::recursive_directory_iterator(src)) { fs::path rel = fs::relative(entry.path(), src); fs::path destPath = dest / rel; if (entry.is_directory()) { if (!fs::exists(destPath)) fs::create_directories(destPath); } else if (entry.is_regular_file()) { fs::create_directories(destPath.parent_path()); if (!fs::exists(destPath)) { fs::copy_file(entry.path(), destPath); } else if (fs::last_write_time(entry.path()) > fs::last_write_time(destPath)) { fs::copy_file(entry.path(), destPath, fs::copy_options::overwrite_existing); } } } } else { if (!fs::exists(dest)) { fs::copy_file(src, dest); } else if (fs::last_write_time(src) > fs::last_write_time(dest)) { fs::copy_file(src, dest, fs::copy_options::overwrite_existing); } } }; std::unordered_set seen; std::function forwardDepArtifacts = [&](Configuration* dep) { if (!seen.insert(dep).second) return; fs::path depBinDir = dep->BinDir(); for (const Shader& shader : dep->shaders) { fs::path src = depBinDir / shader.path.filename().replace_extension("spv"); if (!fs::exists(src)) continue; copyTree(src, outputDir / src.filename()); } for (const fs::path& additionalFile : dep->files) { fs::path src = depBinDir / additionalFile.filename(); if (!fs::exists(src)) continue; copyTree(src, outputDir / additionalFile.filename()); } for (const fs::path& asset : dep->assets) { // Directory entry: the dep already mirrored the // (compressed + passthrough) tree under // depBinDir//. 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(); for (char& c : ext) c = static_cast(std::tolower(static_cast(c))); fs::path srcName = asset.filename(); if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") { srcName.replace_extension(".ctex"); } else if (ext == ".obj") { srcName.replace_extension(".cmesh"); } else { continue; } fs::path src = depBinDir / srcName; if (!fs::exists(src)) continue; copyTree(src, outputDir / srcName); } for (Configuration* sub : dep->dependencies) forwardDepArtifacts(sub); }; for (Configuration* dep : config.dependencies) forwardDepArtifacts(dep); } catch (const fs::filesystem_error& e) { for(std::thread& thread : threads) thread.join(); return {e.what(), false, {}}; } } if(repack.load()) { buildResult.repack = true; } buildResult.libs = std::move(libSet); for(const std::string& flag : config.linkFlags) { buildResult.libs.insert(flag); } // Public compile flags propagated from sub-deps. Add them to this build's // command so config sees the headers its deps expose, and re-publish them // so config's own consumers see them transitively. for (const std::string& flag : publicFlagSet) command += " " + flag; buildResult.publicCompileFlags = std::move(publicFlagSet); fs::file_time_type externalFloor = fs::file_time_type::min(); for(const ExternalBuildResult& ext : externalResults) { for(const std::string& flag : ext.compileFlags) { command += " " + flag; // Headers a dep links via ExternalDependency are part of its // public surface (its modules can include them in declarations // visible to consumers), so propagate the -I to consumers. buildResult.publicCompileFlags.insert(flag); } for(const std::string& flag : ext.linkFlags) { buildResult.libs.insert(flag); } if (ext.latestArtifact > externalFloor) externalFloor = ext.latestArtifact; } for(std::unique_ptr& interface : config.interfaces) { if(interface->Check(pcmDir, externalFloor)) { Module* mod = interface.get(); threads.emplace_back([mod, &command, &pcmDir, &buildDir, &buildCancelled, &buildError]() { Progress::Task task(std::format("Compiling interface {}", mod->path.filename().string())); try { mod->Compile(command, pcmDir, buildDir, buildCancelled, buildError); } catch (const std::exception& e) { bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = std::format("Module::Compile threw: {}", e.what()); } } }); buildResult.repack = true; } files += std::format(" {}/{}.o", buildDir.string(), interface->path.filename().string()); for(std::unique_ptr& part : interface->partitions) { files += std::format(" {}/{}.o", buildDir.string(), part->path.filename().string()); } } for(Implementation& implementation : config.implementations) { if(implementation.Check(buildDir, pcmDir, externalFloor)) { buildResult.repack = true; Implementation* impl = &implementation; threads.emplace_back([impl, &command, &buildDir, &buildCancelled, &buildError]() { Progress::Task task(std::format("Compiling {}.cpp", impl->path.filename().string())); try { impl->Compile(command, buildDir, buildCancelled, buildError); } catch (const std::exception& e) { bool expected = false; if (buildCancelled.compare_exchange_strong(expected, true)) { buildError = std::format("Implementation::Compile threw: {}", e.what()); } } }); } files += std::format(" {}/{}_impl.o", buildDir.string(), implementation.path.filename().string()); } for(std::thread& thread : threads) { thread.join(); } if(buildCancelled.load()) { return {buildError, false, {}}; } std::string linkExtras; for(const std::string& flag : buildResult.libs) { linkExtras += " " + flag; } // mingw uses libstdc++; C++26 std::print/format extras live in libstdc++exp. // libstdc++ on mingw uses winpthreads for std::atomic_wait / // counting_semaphore / stop_token, so -lpthread is required as soon as // those primitives appear (they do, transitively, in any non-trivial std // import). -static-libstdc++ bundles libstdc++ into the exe so we don't // chase libstdc++-6.dll TLS symbol mismatches across mingw versions and // the resulting binary stands alone. Auto-link so user projects don't // carry boilerplate. if (config.target == "x86_64-w64-mingw32") { // Static-link libstdc++/libgcc/libwinpthread so the resulting .exe // or .dll doesn't depend on a specific runtime DLL being on the // consumer's PATH. The mingw runtime ABI varies subtly between // distributions (Arch UCRT vs msys2 UCRT vs msys2 MSVCRT) and // STATUS_ENTRYPOINT_NOT_FOUND at LoadLibrary time is the symptom // of a mismatch. Static linkage trades binary size for portability. // The -Bstatic/-Bdynamic bracketing forces -lpthread to resolve // against libwinpthread.a rather than the import lib; everything // else (KERNEL32, UCRT) stays dynamic. -lstdc++exp adds C++26 // std::print/format extras. -femulated-tls is already on the // compile so __once_callable et al resolve in static libstdc++. linkExtras += " -lstdc++exp -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread -Wl,-Bdynamic"; } // Force a relink if the expected output is missing or older than any dep // artifact. Missing covers: previous build produced a different outputName, // or the binary was deleted by hand. Older-than-dep covers: dep's library // was rebuilt by an earlier run (so dep.repack is false this time around) // but the consumer was never relinked against the new dep. { auto expectedOutputFor = [](const Configuration& c) -> fs::path { fs::path dir = c.BinDir(); if (c.type == ConfigurationType::Executable) { if (c.target.starts_with("wasm32")) return dir / (c.outputName + ".wasm"); return c.target == "x86_64-w64-mingw32" ? dir / (c.outputName + ".exe") : dir / c.outputName; } if (c.type == ConfigurationType::LibraryStatic) { return c.target == "x86_64-w64-mingw32" || c.target == "x86_64-pc-windows-msvc" ? dir / std::format("{}.lib", c.outputName) : dir / std::format("lib{}.a", c.outputName); } // LibraryDynamic — point at the .dll on Windows targets so the // mtime check sees the newly-built DLL; on Unix the .so suffices. if (c.target == "x86_64-w64-mingw32" || c.target == "x86_64-pc-windows-msvc") { return dir / std::format("{}.dll", c.outputName); } return dir / std::format("lib{}.so", c.outputName); }; fs::path expected = expectedOutputFor(config); if (!fs::exists(expected)) { buildResult.repack = true; } else { auto consumerMtime = fs::last_write_time(expected); for (Configuration* dep : config.dependencies) { fs::path depArtifact = expectedOutputFor(*dep); if (fs::exists(depArtifact) && fs::last_write_time(depArtifact) > consumerMtime) { buildResult.repack = true; break; } } // Also relink if any .o this archive bundles is newer than the // archive itself. Covers a build that compiled the .o but never // reached the link step (interrupt, crash, or a source touched // after compile but before link): on the next run the .cpp is // already ≤ .o so Implementation/Module Check returns false and // nothing else would notice the archive is stale. if (!buildResult.repack) { auto objNewer = [&](const fs::path& obj) { std::error_code ec; auto t = fs::last_write_time(obj, ec); return !ec && t > consumerMtime; }; for (const std::unique_ptr& iface : config.interfaces) { if (objNewer(buildDir / (iface->path.filename().string() + ".o"))) { buildResult.repack = true; break; } bool partHit = false; for (const std::unique_ptr& part : iface->partitions) { if (objNewer(buildDir / (part->path.filename().string() + ".o"))) { partHit = true; break; } } if (partHit) { buildResult.repack = true; break; } } if (!buildResult.repack) { for (const Implementation& impl : config.implementations) { if (objNewer(buildDir / (impl.path.filename().string() + "_impl.o"))) { buildResult.repack = true; break; } } } } } } if(buildResult.repack) { Progress::Task task(std::format("Linking {}", config.outputName)); if(config.type == ConfigurationType::Executable) { #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu if(config.target == "x86_64-w64-mingw32") { try { // Copy any LibraryDynamic dependency DLLs alongside // the launcher exe — Windows resolves DLLs from the exe's // own directory at load time, so this is the simplest // equivalent of rpath $ORIGIN. std::unordered_set dllSeen; std::function copyDepDlls = [&](Configuration* dep) { if (!dllSeen.insert(dep).second) return; if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") { fs::path depDir = dep->BinDir(); // The DLL itself (Windows resolves it from the // exe's directory at load time) and the mingw // import lib (so a downstream `crafter-build.exe` // can link a fresh project.dll against it without // hunting through sibling output dirs). for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) { fs::path src = depDir / fname; if (!fs::exists(src)) continue; fs::path dest = outputDir / src.filename(); if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) { fs::copy(src, dest, fs::copy_options::overwrite_existing); } } } for (Configuration* sub : dep->dependencies) copyDepDlls(sub); }; for (Configuration* dep : config.dependencies) copyDepDlls(dep); } catch (const fs::filesystem_error& e) { return {e.what(), false, {}}; } } if (config.target.starts_with("wasm32")) { buildResult.result = RunCommand(std::format("{}{} -o {}.wasm -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras)); } else { buildResult.result = RunCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras)); } #endif #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) if (config.target == "x86_64-w64-mingw32") { // Windows host, mingw target: same shape as the Linux→mingw // path (no LIBCXX_DIR / -lc++ / -nostdlib++ — those are MSVC // libc++ flags). Copy LibraryDynamic dep DLLs + import libs // alongside the launcher exe so Windows resolves them from // the exe's own directory at load time. Runtime DLLs (libstdc++, // libgcc, libwinpthread) come from msys2 on PATH. std::unordered_set dllSeen; std::function copyDepDlls = [&](Configuration* dep) { if (!dllSeen.insert(dep).second) return; if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") { fs::path depDir = dep->BinDir(); for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) { fs::path src = depDir / fname; if (!fs::exists(src)) continue; fs::path dest = outputDir / src.filename(); if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) { fs::copy(src, dest, fs::copy_options::overwrite_existing); } } } for (Configuration* sub : dep->dependencies) copyDepDlls(sub); }; for (Configuration* dep : config.dependencies) copyDepDlls(dep); buildResult.result = RunCommand(std::format( "{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras)); } else { std::system(std::format("copy \"%LIBCXX_DIR%\\lib\\c++.dll\" \"{}\\c++.dll\"", outputDir.string()).c_str()); buildResult.result = RunCommand(std::format("{}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR%\\lib -lc++ -nostdinc++ -nostdlib++{}", command, files, (outputDir/config.outputName).string(), linkExtras)); } #endif } else if(config.type == ConfigurationType::LibraryStatic) { #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu buildResult.result = RunCommand(std::format("ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files)); #endif #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) buildResult.result = RunCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string())); #endif } else { // LibraryDynamic. Output names follow each target's convention so // consumers can link via the standard linker search: // mingw: .dll + lib.dll.a (lld --out-implib) // msvc: .dll + .lib (lld /IMPLIB) // unix: lib.so (rpath $ORIGIN) if (config.target == "x86_64-w64-mingw32") { fs::path dll = outputDir / std::format("{}.dll", config.outputName); fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName); buildResult.result = RunCommand(std::format( "{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras)); } else if (config.target == "x86_64-pc-windows-msvc") { fs::path dll = outputDir / std::format("{}.dll", config.outputName); fs::path implib = outputDir / std::format("{}.lib", config.outputName); buildResult.result = RunCommand(std::format( "{}{} -shared -o {} -Wl,/IMPLIB:{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras)); } else { buildResult.result = RunCommand(std::format( "{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras)); } } } if (config.type == ConfigurationType::LibraryStatic || config.type == ConfigurationType::LibraryDynamic) { buildResult.libs.insert(std::format("-L{}", outputDir.string())); buildResult.libs.insert(std::format("-l{}", config.outputName)); } return buildResult; } void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) { fs::path runtimeDir = GetCrafterBuildHome() / "wasi-runtime"; fs::path runtimeJs = runtimeDir / "runtime.js"; fs::path htmlTemplate = runtimeDir / "index.html.in"; if (!fs::exists(runtimeJs) || !fs::exists(htmlTemplate)) { throw std::runtime_error(std::format( "wasi-runtime assets missing under {} (set CRAFTER_BUILD_HOME or reinstall)", runtimeDir.string())); } fs::path htmlOutDir = cfg.path / "build" / "wasi-runtime" / cfg.name; fs::create_directories(htmlOutDir); fs::path htmlPath = htmlOutDir / "index.html"; // Walk the dep graph for env-style JS bridges that need to load BEFORE // runtime.js so they can populate `window.crafter_webbuild_env`. Any // `*.js` entry in a (transitive) dep's `cfg.files` qualifies — the // file is already going to be copied into the consumer's bin dir, we // just need its basename for a `\n", name, buildId); } // 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. // can actually read them (the wasi-runtime in this repo otherwise // stubs every fd syscall to zero). // // The manifest lists *relative paths* (e.g. "assets/Inter.ttf") so // runtime.js's fetch() resolves against the bin-dir layout the asset // 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 ext = src.extension().string(); for (char& c : ext) c = static_cast(std::tolower(static_cast(c))); if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") { return std::string(".ctex"); } 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 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(); std::function walkAssets = [&](Configuration* c) { if (!c || !seen.insert(c).second) return; for (const fs::path& f : c->files) { std::string ext = f.extension().string(); if (ext == ".js" || ext == ".html") continue; if (f.filename() == "runtime.js") continue; // cfg.files lands flat next to the .wasm by `name = filename()`. pushUnique(f.filename().string()); } // cfg.assets — mirror the bin-dir layout the build emits: a // directory entry becomes /, single // files land flat at the bin root. .png/.obj are compressed in // place; everything else passes through under its original name. for (const fs::path& a : c->assets) { if (fs::is_directory(a)) { const fs::path topName = a.filename(); std::error_code ec; for (const auto& entry : fs::recursive_directory_iterator(a, ec)) { if (ec) break; if (!entry.is_regular_file()) continue; fs::path rel = fs::relative(entry.path(), a); pushUnique((topName / compressedRel(rel)).generic_string()); } } else { pushUnique(compressedRel(a.filename()).generic_string()); } } for (Configuration* dep : c->dependencies) walkAssets(dep); }; walkAssets(&cfg); fs::path manifestPath = htmlOutDir / "files.json"; { std::ofstream m(manifestPath); m << "["; for (std::size_t i = 0; i < assetFiles.size(); ++i) { if (i) m << ","; m << "\"" << assetFiles[i] << "\""; } m << "]"; } std::ifstream in(htmlTemplate); std::stringstream buf; buf << in.rdbuf(); std::string html = buf.str(); html = std::regex_replace(html, std::regex(R"(\{\{WASM\}\})"), cfg.outputName + ".wasm"); html = std::regex_replace(html, std::regex(R"(\{\{ENV_SCRIPTS\}\})"), envScriptTags); html = std::regex_replace(html, std::regex(R"(\{\{BUILDID\}\})"), buildId); std::ofstream out(htmlPath); out << html; out.close(); cfg.files.push_back(runtimeJs); cfg.files.push_back(htmlPath); cfg.files.push_back(manifestPath); } std::string Crafter::HostTarget() { static const std::string cached = []() -> std::string { CommandResult r = RunCommandChecked("clang++ -print-target-triple"); if (r.exitCode != 0) return {}; std::string out = std::move(r.output); while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back(); return out; }(); return cached; } bool Crafter::ArgQuery::Has(std::string_view flag) const { for (std::string_view a : args) if (a == flag) return true; return false; } std::optional Crafter::ArgQuery::Get(std::string_view prefix) const { for (std::string_view a : args) { if (a.starts_with(prefix)) return std::string(a.substr(prefix.size())); } return std::nullopt; } ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span args) { if (const char* envMarch = std::getenv("CRAFTER_BUILD_MARCH"); envMarch && *envMarch) { cfg.march = envMarch; } if (const char* envMtune = std::getenv("CRAFTER_BUILD_MTUNE"); envMtune && *envMtune) { cfg.mtune = envMtune; } bool sawLib = false, sawShared = false; for (std::string_view a : args) { if (a == "--debug") cfg.debug = true; else if (a == "--lib") sawLib = true; else if (a == "--shared") sawShared = true; else if (a.starts_with("--target=")) cfg.target = std::string(a.substr(std::string_view("--target=").size())); else if (a.starts_with("--march=")) cfg.march = std::string(a.substr(std::string_view("--march=").size())); else if (a.starts_with("--mtune=")) cfg.mtune = std::string(a.substr(std::string_view("--mtune=").size())); } if (sawLib && cfg.type == ConfigurationType::Executable) cfg.type = ConfigurationType::LibraryStatic; if (sawShared && cfg.type == ConfigurationType::LibraryStatic) cfg.type = ConfigurationType::LibraryDynamic; // WASI sysroot autodetect, applied at config-load time so the VariantId // includes it. (Build() also runs this once more for callers that bypassed // ApplyStandardArgs, but doing it here makes dep PcmDirs consistent // between the consumer's command-construction and the dep's own build.) #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) { cfg.sysroot = "/usr/share/wasi-sysroot"; } #endif return ArgQuery{args}; } static void PrintHelp(std::string_view argv0) { std::println( R"(Usage: {0} [options] [-- project-args...] Build the project in the current directory {0} test [test-options] [globs...] Build and run the project's tests {0} help | -h | --help Show this help Loads ./project.cpp (override with --project=), compiles it to a shared object, and invokes its CrafterBuildProject() to obtain a Configuration that drives the build. Outputs land at bin/--/, intermediates at build/--/. Build options: -r Run the produced executable after a successful build (host targets only; libraries cannot be run). -v, --verbose Verbose progress output. -q, --quiet Suppress progress output. --project= Path to the project file (default: ./project.cpp). Test options (after the `test` subcommand): --list Enumerate matching tests without running them. --jobs= Parallel job count (default: hardware_concurrency). --timeout= Per-test timeout override. --runner= Override the test runner for this run. Specs: local cmd: (e.g. cmd:wine) ssh:[:] sshwin:[:] wsl[:] --target= Filter to tests whose cfg.target matches; this is also forwarded to project-args so per-target tests build for that triple. Default: host triple. One or more name globs to filter tests (e.g. 'Unit*'). Project args: Any flag not consumed above is forwarded verbatim to CrafterBuildProject as part of its `args` span. Project-specific flags (e.g. --target=, custom feature toggles) live there. Environment: CRAFTER_BUILD_MARCH Override -march (default: native). CRAFTER_BUILD_MTUNE Override -mtune (default: native). CRAFTER_BUILD_RUNNER_ Default test runner for a target triple. Replace '-' and '.' with '_' in the triple. CLI --runner= overrides this. CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect. LIBCXX_DIR Windows libc++ install (MSVC ABI builds). Exit status: 0 success / all non-skipped tests passed 1 build failure or one or more tests failed )", argv0); } int Crafter::Run(int argc, char** argv) { try { std::string_view argv0 = argc > 0 ? argv[0] : "crafter-build"; fs::path projectFile = "./project.cpp"; std::vector projectArgs; projectArgs.reserve(argc); bool runTests = false; bool runAfterBuild = false; RunTestsOptions testOpts; Progress::Verbosity verbosity = Progress::Verbosity::Default; for (int i = 1; i < argc; ++i) { std::string_view arg = argv[i]; if (arg == "-h" || arg == "--help" || (!runTests && arg == "help")) { PrintHelp(argv0); return 0; } else if (arg == "test") { runTests = true; } else if (arg == "-r") { runAfterBuild = true; } else if (arg == "-v" || arg == "--verbose") { verbosity = Progress::Verbosity::Verbose; } else if (arg == "-q" || arg == "--quiet") { verbosity = Progress::Verbosity::Quiet; } else if (arg.starts_with("--project=")) { projectFile = arg.substr(std::string_view("--project=").size()); } else if (runTests && arg.starts_with("--jobs=")) { testOpts.jobs = std::stoi(std::string(arg.substr(std::string_view("--jobs=").size()))); } else if (runTests && arg.starts_with("--timeout=")) { testOpts.timeoutOverride = std::chrono::seconds(std::stoi(std::string(arg.substr(std::string_view("--timeout=").size())))); } else if (runTests && arg == "--list") { testOpts.listOnly = true; } else if (runTests && arg.starts_with("--runner=")) { testOpts.runnerOverride = std::string(arg.substr(std::string_view("--runner=").size())); } else if (runTests && !arg.starts_with("-")) { testOpts.globs.emplace_back(arg); } else { projectArgs.push_back(arg); } } Progress::SetVerbosity(verbosity); // The test run is target-scoped: only tests whose cfg.target equals // testOpts.targetFilter are included. Default = host triple, so a // bare `crafter-build test` runs everything that targets host. for (auto& a : projectArgs) { if (a.starts_with("--target=")) { testOpts.targetFilter = std::string(a.substr(std::string_view("--target=").size())); } } if (!fs::exists(projectFile)) { std::println(std::cerr, "No project file at {}", projectFile.string()); return 1; } Configuration config = LoadProject(projectFile, projectArgs); SetParentProject(&config); if (runTests) { TestSummary summary = RunTests(config, testOpts, projectArgs); return summary.AllPassed() ? 0 : 1; } std::unordered_map> depResults; std::mutex depMutex; BuildResult result = Build(config, depResults, depMutex); if (!result.result.empty()) { Progress::Clear(); std::println(std::cerr, "{}", result.result); return 1; } Progress::Finalize(); if (runAfterBuild) { if (config.type != ConfigurationType::Executable) { std::println(std::cerr, "-r: cannot run a library"); return 1; } fs::path dir = config.BinDir(); fs::path artifact = dir / config.outputName; if (config.target.starts_with("wasm32")) { artifact += ".wasm"; } else if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") { artifact += ".exe"; } artifact = fs::absolute(artifact); fs::path absDir = fs::absolute(dir); // wasm targets need either a wasm runtime (wasi-cli) or an HTTP // server (browser build with index.html). std::system on the // .wasm path goes nowhere useful — replace with detection. if (config.target.starts_with("wasm32")) { bool browserBuild = fs::exists(absDir / "index.html"); auto have = [](std::string_view exe) { #ifdef _WIN32 std::string probe = std::format("where {} > NUL 2>&1", exe); #else std::string probe = std::format("command -v {} > /dev/null 2>&1", exe); #endif return std::system(probe.c_str()) == 0; }; if (browserBuild) { // Probe-bind to find a free port starting at 8080 — if the // user has another dev server running we shift up rather // than letting caddy/python exit with EADDRINUSE. auto findFreePort = [](int basePort, int span) -> int { #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) static const bool wsaInit = []{ WSADATA d; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }(); if (!wsaInit) return basePort; using sock_t = SOCKET; const sock_t invalid = INVALID_SOCKET; auto closesock = [](sock_t s){ closesocket(s); }; #else using sock_t = int; const sock_t invalid = -1; auto closesock = [](sock_t s){ ::close(s); }; #endif for (int p = basePort; p < basePort + span; ++p) { sock_t s = ::socket(AF_INET, SOCK_STREAM, 0); if (s == invalid) return basePort; sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(static_cast(p)); bool ok = ::bind(s, reinterpret_cast(&addr), sizeof(addr)) == 0; closesock(s); if (ok) return p; } return basePort; }; const int port = findFreePort(8080, 16); // Cross-origin isolation: the browser coarsens // performance.now() (and chrono::steady_clock under wasi) // to ~0.1ms unless the response carries COOP/COEP, which // floors the --timing overlay's sub-ms phase counters to // zero. Emitting same-origin / require-corp drops the // resolution to ~5µs and also unlocks SharedArrayBuffer // for any future threading work. CORP keeps the local // asset fetches passing under require-corp. auto writeFile = [](const fs::path& p, std::string_view contents) { std::ofstream f(p, std::ios::binary | std::ios::trunc); f.write(contents.data(), static_cast(contents.size())); }; std::string cmd; std::string_view picked; bool isolated = false; if (have("caddy")) { picked = "caddy"; isolated = true; // caddy file-server has no --header flag; write a // Caddyfile next to the build output. Adapter is // inferred from the .caddyfile extension when run // via `caddy run`. fs::path cf = absDir / "Caddyfile.coi"; writeFile(cf, std::format( ":{} {{\n" " root * {}\n" " header Cross-Origin-Opener-Policy \"same-origin\"\n" " header Cross-Origin-Embedder-Policy \"require-corp\"\n" " header Cross-Origin-Resource-Policy \"same-origin\"\n" " header Cache-Control \"no-store\"\n" " file_server\n" "}}\n", port, absDir.string())); cmd = std::format("caddy run --config {} --adapter caddyfile", cf.string()); } else if (have("python3") || have("python")) { std::string_view py = have("python3") ? "python3" : "python"; picked = py; isolated = true; // Inline a tiny SimpleHTTPRequestHandler subclass // that appends the COI headers on every response. // Lives in absDir so the user can re-run it manually // (`python3 absDir/.serve-coi.py 8080`). fs::path sp = absDir / ".serve-coi.py"; writeFile(sp, "import http.server, socketserver, sys, os\n" "class H(http.server.SimpleHTTPRequestHandler):\n" " def end_headers(self):\n" " self.send_header('Cross-Origin-Opener-Policy', 'same-origin')\n" " self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')\n" " self.send_header('Cross-Origin-Resource-Policy', 'same-origin')\n" " self.send_header('Cache-Control', 'no-store')\n" " super().end_headers()\n" "socketserver.TCPServer.allow_reuse_address = True\n" "port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080\n" "os.chdir(os.path.dirname(os.path.abspath(__file__)))\n" "with socketserver.TCPServer(('', port), H) as s:\n" " s.serve_forever()\n"); cmd = std::format("{} {} {}", py, sp.string(), port); } else if (have("php")) { picked = "php"; // php -S supports a router script — emit one that // sets COI headers then delegates to the built-in // static-file handler by returning false. fs::path rp = absDir / ".serve-coi.php"; writeFile(rp, "