This commit is contained in:
parent
a25b0a1ded
commit
8892154b28
70 changed files with 2780 additions and 596 deletions
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Build:Asset_impl;
|
||||
import :Asset;
|
||||
|
|
@ -15,7 +15,7 @@ namespace Crafter {
|
|||
std::string ext = input.extension().string();
|
||||
// Case-insensitive extension match — Sponza's textures dir mixes
|
||||
// case (.tga and .TGA both appear in the wild).
|
||||
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<std::uint8_t>(c)));
|
||||
#ifdef CRAFTER_BUILD_HAS_ASSET
|
||||
try {
|
||||
// stb_image (the loader behind LoadPNG) handles all of these,
|
||||
|
|
@ -35,20 +35,11 @@ namespace Crafter {
|
|||
}
|
||||
return {};
|
||||
#else
|
||||
return std::format(
|
||||
"{}: crafter-build was bootstrapped without Crafter.Asset linkage; "
|
||||
"rebuild via `./bin/crafter-build` so the self-host pass picks up the "
|
||||
"Crafter.Asset dep declared in project.cpp",
|
||||
input.string());
|
||||
return std::format("{}: crafter-build was bootstrapped without Crafter.Asset linkage; " "rebuild via `./bin/crafter-build` so the self-host pass picks up the " "Crafter.Asset dep declared in project.cpp", input.string());
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string BuildOBJBundle(
|
||||
const fs::path& objPath,
|
||||
const fs::path& mtlPath,
|
||||
const fs::path& outDir,
|
||||
std::uint16_t albedoSize)
|
||||
{
|
||||
std::string BuildOBJBundle(const fs::path& objPath, const fs::path& mtlPath, const fs::path& outDir, std::uint16_t albedoSize) {
|
||||
#ifdef CRAFTER_BUILD_HAS_ASSET
|
||||
const fs::path manifest = outDir / "scene.txt";
|
||||
if (fs::exists(manifest)) return {};
|
||||
|
|
@ -75,9 +66,7 @@ namespace Crafter {
|
|||
if (mesh.vertexes.empty() || mesh.indexes.empty()) continue;
|
||||
mesh.SaveCompressed(outDir / std::format("mesh_{}.cmesh", emitted));
|
||||
std::int32_t a = -1;
|
||||
if (auto it = materials.find(matName);
|
||||
it != materials.end() && !it->second.mapKd.empty())
|
||||
a = static_cast<std::int32_t>(albedoIndex.at(it->second.mapKd));
|
||||
if (auto it = materials.find(matName); it != materials.end() && !it->second.mapKd.empty()) a = static_cast<std::int32_t>(albedoIndex.at(it->second.mapKd));
|
||||
meshAlbedoIdx.push_back(a);
|
||||
++emitted;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
|
|
@ -18,6 +18,7 @@ import std;
|
|||
import :Clang;
|
||||
import :Platform;
|
||||
import :Test;
|
||||
import :Lint;
|
||||
import :Progress;
|
||||
import :Asset;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -25,9 +26,7 @@ using namespace Crafter;
|
|||
|
||||
|
||||
void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations) {
|
||||
auto resolveImport = [this](const std::string& importName,
|
||||
std::vector<Module*>& localDeps,
|
||||
std::vector<std::pair<Module*, fs::path>>& externalDeps) -> bool {
|
||||
auto resolveImport = [this](const std::string& importName, std::vector<Module*>& localDeps, std::vector<std::pair<Module*, fs::path>>& externalDeps) -> bool {
|
||||
for(const std::unique_ptr<Module>& interface : this->interfaces) {
|
||||
if(interface->name == importName) {
|
||||
localDeps.push_back(interface.get());
|
||||
|
|
@ -40,7 +39,7 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
if (!seen.insert(depCfg).second) return false;
|
||||
for(const std::unique_ptr<Module>& depInterface : depCfg->interfaces) {
|
||||
if(depInterface->name == importName) {
|
||||
fs::path depPcmPath = (depCfg->PcmDir() / depInterface->path.filename()).string() + ".pcm";
|
||||
fs::path depPcmPath = std::format("{}.pcm", (depCfg->PcmDir() / depInterface->path.filename()).string());
|
||||
externalDeps.emplace_back(depInterface.get(), std::move(depPcmPath));
|
||||
return true;
|
||||
}
|
||||
|
|
@ -98,9 +97,9 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
goto next;
|
||||
}
|
||||
}
|
||||
throw std::runtime_error(std::format("Module {} not found, referenced in {}", match[1].str(), std::get<0>(tempModulePaths[i]).string()));
|
||||
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()));
|
||||
throw std::runtime_error(std::format("No module declaration found in {}", std::get<0>(tempModulePaths[i]).string()));
|
||||
}
|
||||
next:;
|
||||
}
|
||||
|
|
@ -305,7 +304,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// 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();
|
||||
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<std::uint8_t>(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") {
|
||||
|
|
@ -435,7 +434,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// it with extra codegen flags (e.g. -mrelaxed-simd) — its BMI must not be
|
||||
// shared with the baseline's, or the consuming TUs see a target-feature
|
||||
// mismatch. Suffix the cache dir with the variant flags when present.
|
||||
std::string stdPcmKey = config.target + "-" + config.march;
|
||||
std::string stdPcmKey = std::format("{}-{}", config.target, config.march);
|
||||
for (const std::string& f : config.wasmVariantFlags) {
|
||||
stdPcmKey += "+";
|
||||
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
|
||||
|
|
@ -492,7 +491,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// for the baseline build. Part of VariantId, so these objects/PCMs
|
||||
// land in their own dir.
|
||||
for (const std::string& f : config.wasmVariantFlags) {
|
||||
command += " " + f;
|
||||
command += std::format(" {}", f);
|
||||
}
|
||||
}
|
||||
if (config.target == "x86_64-w64-mingw32") {
|
||||
|
|
@ -542,7 +541,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
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{}", entry.path().string());
|
||||
}
|
||||
}
|
||||
includeFlags += std::format(" -I{}", dep->path.string());
|
||||
|
|
@ -629,7 +628,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// 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;
|
||||
userFlags += std::format(" {}", flag);
|
||||
}
|
||||
command += userFlags;
|
||||
|
||||
|
|
@ -676,8 +675,8 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
}
|
||||
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";
|
||||
const std::string objPath = std::format("{}_source.o", (buildDir / cFile.filename()).string());
|
||||
const std::string srcPath = std::format("{}.c", cFile.string());
|
||||
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, <oCompileFlags]() {
|
||||
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
|
||||
|
|
@ -696,8 +695,8 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
|
||||
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";
|
||||
const std::string objPath = std::format("{}_source.o", (buildDir / cFile.filename()).string());
|
||||
const std::string srcPath = std::format("{}.cu", cFile.string());
|
||||
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()));
|
||||
|
|
@ -789,7 +788,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
continue;
|
||||
}
|
||||
std::string ext = asset.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<std::uint8_t>(c)));
|
||||
fs::path srcName = asset.filename();
|
||||
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
|
||||
srcName.replace_extension(".ctex");
|
||||
|
|
@ -821,12 +820,12 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// 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;
|
||||
for (const std::string& flag : publicFlagSet) command += std::format(" {}", 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;
|
||||
command += std::format(" {}", 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.
|
||||
|
|
@ -889,7 +888,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
|
||||
std::string linkExtras;
|
||||
for(const std::string& flag : buildResult.libs) {
|
||||
linkExtras += " " + flag;
|
||||
linkExtras += std::format(" {}", flag);
|
||||
}
|
||||
// Link-only LTO/section flags (empty in Debug and for wasm). Every exe/lib
|
||||
// link below ends with linkExtras, so this reaches them all; the static-lib
|
||||
|
|
@ -927,10 +926,9 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
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");
|
||||
if (c.target.starts_with("wasm32")) return dir / (std::format("{}.wasm", c.outputName));
|
||||
return c.target == "x86_64-w64-mingw32"
|
||||
? dir / (c.outputName + ".exe")
|
||||
? dir / (std::format("{}.exe", c.outputName))
|
||||
: dir / c.outputName;
|
||||
}
|
||||
if (c.type == ConfigurationType::LibraryStatic) {
|
||||
|
|
@ -971,13 +969,13 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
return !ec && t > consumerMtime;
|
||||
};
|
||||
for (const std::unique_ptr<Module>& iface : config.interfaces) {
|
||||
if (objNewer(buildDir / (iface->path.filename().string() + ".o"))) {
|
||||
if (objNewer(buildDir / std::format("{}.o", iface->path.filename().string()))) {
|
||||
buildResult.repack = true;
|
||||
break;
|
||||
}
|
||||
bool partHit = false;
|
||||
for (const std::unique_ptr<ModulePartition>& part : iface->partitions) {
|
||||
if (objNewer(buildDir / (part->path.filename().string() + ".o"))) {
|
||||
if (objNewer(buildDir / std::format("{}.o", part->path.filename().string()))) {
|
||||
partHit = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -986,7 +984,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
}
|
||||
if (!buildResult.repack) {
|
||||
for (const Implementation& impl : config.implementations) {
|
||||
if (objNewer(buildDir / (impl.path.filename().string() + "_impl.o"))) {
|
||||
if (objNewer(buildDir / std::format("{}_impl.o", impl.path.filename().string()))) {
|
||||
buildResult.repack = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -1016,8 +1014,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// 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)}) {
|
||||
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();
|
||||
|
|
@ -1053,8 +1050,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
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)}) {
|
||||
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();
|
||||
|
|
@ -1067,9 +1063,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
};
|
||||
for (Configuration* dep : config.dependencies) copyDepDlls(dep);
|
||||
|
||||
buildResult.result = RunCommand(std::format(
|
||||
"{}{} -o {} -fuse-ld=lld{}",
|
||||
command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||
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));
|
||||
|
|
@ -1095,19 +1089,13 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
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));
|
||||
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));
|
||||
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));
|
||||
buildResult.result = RunCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1127,11 +1115,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// Guard: only fires on the top-level config that *declared* wasmVariants
|
||||
// (deps leave it empty) and never on a variant pass itself (those carry
|
||||
// wasmVariantFlags), so there's no recursion. Skipped on build error.
|
||||
if (buildResult.result.empty()
|
||||
&& config.target.starts_with("wasm32")
|
||||
&& config.type == ConfigurationType::Executable
|
||||
&& !config.wasmVariants.empty()
|
||||
&& config.wasmVariantFlags.empty()) {
|
||||
if (buildResult.result.empty() && config.target.starts_with("wasm32") && config.type == ConfigurationType::Executable && !config.wasmVariants.empty() && config.wasmVariantFlags.empty()) {
|
||||
fs::path baselineBin = config.BinDir();
|
||||
|
||||
// The whole transitive graph, so the variant flags reach every TU.
|
||||
|
|
@ -1151,8 +1135,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
// another name — nothing extra to compile.
|
||||
if (variant.label.empty() || variant.flags.empty()) continue;
|
||||
|
||||
Progress::Task task(std::format("Building wasm variant {} ({})",
|
||||
variant.label, config.outputName));
|
||||
Progress::Task task(std::format("Building wasm variant {} ({})", variant.label, config.outputName));
|
||||
|
||||
for (Configuration* c : graph) c->wasmVariantFlags = variant.flags;
|
||||
fs::path variantBin = config.BinDir();
|
||||
|
|
@ -1164,19 +1147,16 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
for (Configuration* c : graph) c->wasmVariantFlags.clear();
|
||||
|
||||
if (!vr.result.empty()) {
|
||||
buildResult.result = std::format(
|
||||
"wasm variant '{}' build failed: {}", variant.label, vr.result);
|
||||
buildResult.result = std::format("wasm variant '{}' build failed: {}", variant.label, vr.result);
|
||||
break;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::path src = variantBin / (config.outputName + ".wasm");
|
||||
fs::path src = variantBin / (std::format("{}.wasm", config.outputName));
|
||||
fs::path dst = baselineBin / std::format("{}.{}.wasm", config.outputName, variant.label);
|
||||
fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
buildResult.result = std::format(
|
||||
"copy wasm variant '{}' {} -> {}: {}",
|
||||
variant.label, src.string(), dst.string(), ec.message());
|
||||
buildResult.result = std::format("copy wasm variant '{}' {} -> {}: {}", variant.label, src.string(), dst.string(), ec.message());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1190,9 +1170,7 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
|||
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()));
|
||||
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;
|
||||
|
|
@ -1225,15 +1203,11 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
|||
// the dev server (python -m http.server) sends no Cache-Control
|
||||
// headers. Using ms-since-epoch is enough to be unique per build
|
||||
// without invoking any version-control machinery.
|
||||
const std::string buildId = std::to_string(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
const std::string buildId = std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
|
||||
std::string envScriptTags;
|
||||
for (const std::string& name : envScripts) {
|
||||
envScriptTags += std::format(
|
||||
" <script src=\"{}?v={}\" type=\"module\"></script>\n",
|
||||
name, buildId);
|
||||
envScriptTags += std::format(" <script src=\"{}?v={}\" type=\"module\"></script>\n", name, buildId);
|
||||
}
|
||||
|
||||
// Walk the dep graph again for non-JS assets — these get pre-loaded by
|
||||
|
|
@ -1250,7 +1224,7 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
|||
// 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();
|
||||
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<std::uint8_t>(c)));
|
||||
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
|
||||
return std::string(".ctex");
|
||||
}
|
||||
|
|
@ -1340,7 +1314,7 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
|||
if (!first) v << ",";
|
||||
first = false;
|
||||
std::string url = label.empty()
|
||||
? cfg.outputName + ".wasm"
|
||||
? std::format("{}.wasm", cfg.outputName)
|
||||
: std::format("{}.{}.wasm", cfg.outputName, label);
|
||||
v << "{\"label\":\"" << jsonEscape(label) << "\""
|
||||
<< ",\"url\":\"" << jsonEscape(url) << "\""
|
||||
|
|
@ -1363,7 +1337,7 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
|||
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"(\{\{WASM\}\})"), std::format("{}.wasm", cfg.outputName));
|
||||
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);
|
||||
|
|
@ -1388,14 +1362,14 @@ void Crafter::EnableWasiRelaxedSimdVariant(Configuration& cfg) {
|
|||
}
|
||||
|
||||
std::string Crafter::HostTarget() {
|
||||
static const std::string cached = []() -> std::string {
|
||||
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;
|
||||
return Cached;
|
||||
}
|
||||
|
||||
bool Crafter::ArgQuery::Has(std::string_view flag) const {
|
||||
|
|
@ -1417,7 +1391,8 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
|
|||
if (const char* envMtune = std::getenv("CRAFTER_BUILD_MTUNE"); envMtune && *envMtune) {
|
||||
cfg.mtune = envMtune;
|
||||
}
|
||||
bool sawLib = false, sawShared = false;
|
||||
bool sawLib = false;
|
||||
bool sawShared = false;
|
||||
for (std::string_view a : args) {
|
||||
if (a == "--debug") cfg.debug = true;
|
||||
else if (a == "--lib") sawLib = true;
|
||||
|
|
@ -1446,6 +1421,8 @@ static void PrintHelp(std::string_view argv0) {
|
|||
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} lint [lint-options] [globs...] Run the project's lint rules over its sources
|
||||
{0} format [format-options] [globs...] Apply the project's transform rules (rewrites files)
|
||||
{0} help | -h | --help Show this help
|
||||
|
||||
Loads ./project.cpp (override with --project=<path>), compiles it to a shared
|
||||
|
|
@ -1472,6 +1449,22 @@ Test options (after the `test` subcommand):
|
|||
project's tests plus the host triple.
|
||||
<glob> One or more name globs to filter tests (e.g. 'Unit*').
|
||||
|
||||
Lint options (after the `lint` subcommand):
|
||||
--list Enumerate matching lint rules without running them.
|
||||
<glob> One or more name globs to filter rules (e.g. 'spdx*').
|
||||
|
||||
Lint rules are defined in project.cpp via cfg.AddLintRule(name, callback) —
|
||||
C++ callbacks run once per source file. No rules ship by default.
|
||||
|
||||
Format options (after the `format` subcommand):
|
||||
--check Dry run: list files that would change, exit 1 if any.
|
||||
--list Enumerate matching rules without running them.
|
||||
<glob> One or more name globs to filter rules.
|
||||
|
||||
Rules are shared with lint — a rule that calls ctx.SetContent is a
|
||||
transform. `format` rewrites changed files in place; `lint` reports the
|
||||
same transforms as would-reformat findings without writing.
|
||||
|
||||
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
|
||||
|
|
@ -1487,8 +1480,9 @@ Environment:
|
|||
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
|
||||
0 success / all non-skipped tests passed / lint clean / format applied
|
||||
1 build failure, one or more tests failed, lint findings, no rules defined,
|
||||
format write errors, or `format --check` found files that would change
|
||||
)", argv0);
|
||||
}
|
||||
|
||||
|
|
@ -1500,17 +1494,27 @@ int Crafter::Run(int argc, char** argv) {
|
|||
projectArgs.reserve(argc);
|
||||
|
||||
bool runTests = false;
|
||||
bool runLint = false;
|
||||
bool runFormat = false;
|
||||
bool runAfterBuild = false;
|
||||
RunTestsOptions testOpts;
|
||||
RunLintOptions lintOpts;
|
||||
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")) {
|
||||
if (arg == "-h" || arg == "--help" || (!runTests && !runLint && !runFormat && arg == "help")) {
|
||||
PrintHelp(argv0);
|
||||
return 0;
|
||||
} else if (arg == "test") {
|
||||
} else if (!runLint && !runFormat && arg == "test") {
|
||||
runTests = true;
|
||||
} else if (!runTests && !runFormat && arg == "lint") {
|
||||
runLint = true;
|
||||
} else if (!runTests && !runLint && arg == "format") {
|
||||
runFormat = true;
|
||||
lintOpts.mode = LintMode::Apply;
|
||||
} else if (runFormat && arg == "--check") {
|
||||
lintOpts.mode = LintMode::Check;
|
||||
} else if (arg == "-r") {
|
||||
runAfterBuild = true;
|
||||
} else if (arg == "-v" || arg == "--verbose") {
|
||||
|
|
@ -1529,6 +1533,10 @@ int Crafter::Run(int argc, char** argv) {
|
|||
testOpts.runnerOverride = std::string(arg.substr(std::string_view("--runner=").size()));
|
||||
} else if (runTests && !arg.starts_with("-")) {
|
||||
testOpts.globs.emplace_back(arg);
|
||||
} else if ((runLint || runFormat) && arg == "--list") {
|
||||
lintOpts.listOnly = true;
|
||||
} else if ((runLint || runFormat) && !arg.starts_with("-")) {
|
||||
lintOpts.globs.emplace_back(arg);
|
||||
} else {
|
||||
projectArgs.push_back(arg);
|
||||
}
|
||||
|
|
@ -1553,6 +1561,18 @@ int Crafter::Run(int argc, char** argv) {
|
|||
Configuration config = LoadProject(projectFile, projectArgs);
|
||||
SetParentProject(&config);
|
||||
|
||||
if (runLint || runFormat) {
|
||||
// lexically_normal: fs::absolute keeps a "./" component, which
|
||||
// would leak into displayed paths and the project-root derivation.
|
||||
lintOpts.projectFile = fs::absolute(projectFile).lexically_normal();
|
||||
LintSummary lintSummary = RunLint(config, lintOpts);
|
||||
if (!runFormat) return lintSummary.Clean() ? 0 : 1;
|
||||
if (lintSummary.noRulesDefined || lintSummary.errors > 0) return 1;
|
||||
// Apply: formatting files is success (gofmt convention).
|
||||
// Check: a would-change file is the CI failure signal.
|
||||
return lintOpts.mode == LintMode::Check && !lintSummary.changedFiles.empty() ? 1 : 0;
|
||||
}
|
||||
|
||||
if (runTests) {
|
||||
TestSummary summary = RunTests(config, testOpts, projectArgs);
|
||||
return summary.AllPassed() ? 0 : 1;
|
||||
|
|
@ -1602,20 +1622,20 @@ int Crafter::Run(int argc, char** argv) {
|
|||
// 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 {
|
||||
auto findFreePort = [](std::int32_t basePort, std::int32_t span) -> std::int32_t {
|
||||
#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); };
|
||||
static const bool WsaInit = []{ WSADATA d; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }();
|
||||
if (!WsaInit) return basePort;
|
||||
using Sock = SOCKET;
|
||||
const Sock invalid = INVALID_SOCKET;
|
||||
auto closesock = [](Sock s){ closesocket(s); };
|
||||
#else
|
||||
using sock_t = int;
|
||||
const sock_t invalid = -1;
|
||||
auto closesock = [](sock_t s){ ::close(s); };
|
||||
using Sock = std::int32_t;
|
||||
const Sock invalid = -1;
|
||||
auto closesock = [](Sock s){ ::close(s); };
|
||||
#endif
|
||||
for (int p = basePort; p < basePort + span; ++p) {
|
||||
sock_t s = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
for (std::int32_t p = basePort; p < basePort + span; ++p) {
|
||||
Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (s == invalid) return basePort;
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
|
|
@ -1627,7 +1647,7 @@ int Crafter::Run(int argc, char** argv) {
|
|||
}
|
||||
return basePort;
|
||||
};
|
||||
const int port = findFreePort(8080, 16);
|
||||
const std::int32_t 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
|
||||
|
|
@ -1661,8 +1681,7 @@ int Crafter::Run(int argc, char** argv) {
|
|||
" file_server\n"
|
||||
"}}\n",
|
||||
port, absDir.string()));
|
||||
cmd = std::format("caddy run --config {} --adapter caddyfile",
|
||||
cf.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;
|
||||
|
|
@ -1701,8 +1720,7 @@ int Crafter::Run(int argc, char** argv) {
|
|||
"header('Cache-Control: no-store');\n"
|
||||
"return false;\n");
|
||||
isolated = true;
|
||||
cmd = std::format("php -S 0.0.0.0:{} -t {} {}",
|
||||
port, absDir.string(), rp.string());
|
||||
cmd = std::format("php -S 0.0.0.0:{} -t {} {}", port, absDir.string(), rp.string());
|
||||
} else if (have("ruby")) {
|
||||
picked = "ruby";
|
||||
cmd = std::format("ruby -run -e httpd {} -p{}", absDir.string(), port);
|
||||
|
|
@ -1713,17 +1731,12 @@ int Crafter::Run(int argc, char** argv) {
|
|||
picked = "npx http-server";
|
||||
cmd = std::format("npx --yes http-server {} -p {} --silent", absDir.string(), port);
|
||||
} else {
|
||||
std::println(std::cerr,
|
||||
"-r wasm: no HTTP server found in PATH. Install one of: "
|
||||
"caddy, python3, python, php, ruby, busybox, npx (Node.js).");
|
||||
std::println(std::cerr, "-r wasm: no HTTP server found in PATH. Install one of: " "caddy, python3, python, php, ruby, busybox, npx (Node.js).");
|
||||
return 1;
|
||||
}
|
||||
std::println("listening on port :{}", port);
|
||||
if (!isolated) {
|
||||
std::println(std::cerr,
|
||||
"warning: {} does not emit COOP/COEP — performance.now() will be coarse "
|
||||
"(~0.1ms). Install caddy, python3, or php for cross-origin isolation.",
|
||||
picked);
|
||||
std::println(std::cerr, "warning: {} does not emit COOP/COEP — performance.now() will be coarse " "(~0.1ms). Install caddy, python3, or php for cross-origin isolation.", picked);
|
||||
}
|
||||
// Silence the backend's own banner/request logs so the
|
||||
// experience is identical regardless of which server is
|
||||
|
|
@ -1742,9 +1755,7 @@ int Crafter::Run(int argc, char** argv) {
|
|||
if (have("wasmer")) {
|
||||
return std::system(std::format("wasmer run {}", artifact.string()).c_str()) == 0 ? 0 : 1;
|
||||
}
|
||||
std::println(std::cerr,
|
||||
"-r wasm: no wasm runtime found in PATH. Install wasmtime or wasmer, "
|
||||
"or call EnableWasiBrowserRuntime(cfg) in project.cpp for a browser build.");
|
||||
std::println(std::cerr, "-r wasm: no wasm runtime found in PATH. Install wasmtime or wasmer, " "or call EnableWasiBrowserRuntime(cfg) in project.cpp for a browser build.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Build:External_impl;
|
||||
import std;
|
||||
|
|
@ -175,10 +175,7 @@ std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildD
|
|||
|
||||
if (!fs::exists(cmakeBuildDir)) fs::create_directories(cmakeBuildDir);
|
||||
|
||||
std::string cmd = std::format("cmake -S {} -B {}{}",
|
||||
ShellQuote(fs::absolute(cloneDir).string()),
|
||||
ShellQuote(fs::absolute(cmakeBuildDir).string()),
|
||||
BuildInjectedCMakeFlags(cmakeBuildDir, target));
|
||||
std::string cmd = std::format("cmake -S {} -B {}{}", ShellQuote(fs::absolute(cloneDir).string()), ShellQuote(fs::absolute(cmakeBuildDir).string()), BuildInjectedCMakeFlags(cmakeBuildDir, target));
|
||||
if (!options.empty()) {
|
||||
cmd += " ";
|
||||
cmd += optionsKey;
|
||||
|
|
@ -198,9 +195,8 @@ std::string BuildCMake(const fs::path& cmakeBuildDir) {
|
|||
// unit at a time, leaving every core but one idle. Pass an explicit job
|
||||
// count (a bare --parallel maps to an unbounded `make -j` fork bomb on the
|
||||
// Makefiles generator) so deps like DPP/msquic/glslang compile ~N× faster.
|
||||
unsigned jobs = std::max(1u, std::thread::hardware_concurrency());
|
||||
std::string cmd = std::format("cmake --build {} --parallel {}",
|
||||
ShellQuote(fs::absolute(cmakeBuildDir).string()), jobs);
|
||||
std::uint32_t jobs = std::max(1u, std::thread::hardware_concurrency());
|
||||
std::string cmd = std::format("cmake --build {} --parallel {}", ShellQuote(fs::absolute(cmakeBuildDir).string()), jobs);
|
||||
CommandResult r = RunCommandChecked(cmd);
|
||||
if (r.exitCode != 0) {
|
||||
return std::format("cmake --build failed (exit {}): {}", r.exitCode, r.output);
|
||||
|
|
@ -210,10 +206,7 @@ std::string BuildCMake(const fs::path& cmakeBuildDir) {
|
|||
|
||||
} // namespace
|
||||
|
||||
ExternalBuildResult Crafter::BuildExternal(
|
||||
const ExternalDependency& dep,
|
||||
std::string_view target,
|
||||
std::atomic<bool>& cancelled) {
|
||||
ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled) {
|
||||
ExternalBuildResult result;
|
||||
|
||||
if (cancelled.load(std::memory_order_relaxed)) return result;
|
||||
|
|
@ -234,8 +227,7 @@ ExternalBuildResult Crafter::BuildExternal(
|
|||
}
|
||||
}
|
||||
|
||||
std::string keyMaterial = std::format("{}|{}|{}|{}|{}",
|
||||
dep.source.url, dep.source.branch, dep.source.commit, JoinOptions(dep.options), target);
|
||||
std::string keyMaterial = std::format("{}|{}|{}|{}|{}", dep.source.url, dep.source.branch, dep.source.commit, JoinOptions(dep.options), target);
|
||||
std::size_t key = std::hash<std::string>{}(keyMaterial);
|
||||
fs::path cloneDir = externalRoot / std::format("{}-{:016x}", name, key);
|
||||
|
||||
|
|
@ -303,8 +295,8 @@ ExternalBuildResult Crafter::BuildExternal(
|
|||
}
|
||||
|
||||
namespace {
|
||||
std::mutex projectCacheMutex;
|
||||
std::unordered_map<std::string, std::unique_ptr<Configuration>> projectCache;
|
||||
std::mutex ProjectCacheMutex;
|
||||
std::unordered_map<std::string, std::unique_ptr<Configuration>> ProjectCache;
|
||||
|
||||
// Run the dep's project.cpp from its own directory so its relative paths
|
||||
// (cfg.path = "./", interfaces/, lib/, ...) resolve against the dep
|
||||
|
|
@ -342,8 +334,7 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
|||
// how the dep's project.cpp interprets them at runtime), while the
|
||||
// in-memory Configuration is keyed by the full spec so each (args)
|
||||
// permutation gets its own Configuration object.
|
||||
std::string srcKey = std::format("git|{}|{}|{}|{}",
|
||||
spec.source.url, spec.source.branch, spec.source.commit, spec.projectFile.string());
|
||||
std::string srcKey = std::format("git|{}|{}|{}|{}", spec.source.url, spec.source.branch, spec.source.commit, spec.projectFile.string());
|
||||
std::string srcHash = std::format("{:016x}", std::hash<std::string>{}(srcKey));
|
||||
|
||||
std::string fullKey = srcKey;
|
||||
|
|
@ -354,8 +345,8 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
|||
std::string fullHash = std::format("{:016x}", std::hash<std::string>{}(fullKey));
|
||||
|
||||
{
|
||||
std::lock_guard lock(projectCacheMutex);
|
||||
if (auto it = projectCache.find(fullHash); it != projectCache.end()) {
|
||||
std::lock_guard lock(ProjectCacheMutex);
|
||||
if (auto it = ProjectCache.find(fullHash); it != ProjectCache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -379,38 +370,34 @@ Configuration* Crafter::GitProject(const GitProjectSpec& spec) {
|
|||
|
||||
fs::path projectPath = cloneDir / spec.projectFile;
|
||||
if (!fs::exists(projectPath)) {
|
||||
throw std::runtime_error(std::format(
|
||||
"GitProject({}): {} not found in clone", spec.source.url, spec.projectFile.string()));
|
||||
throw std::runtime_error(std::format("GitProject({}): {} not found in clone", spec.source.url, spec.projectFile.string()));
|
||||
}
|
||||
|
||||
std::vector<std::string_view> argViews(spec.args.begin(), spec.args.end());
|
||||
Configuration cfg = LoadProjectFromRoot(projectPath, cloneDir, argViews);
|
||||
|
||||
std::lock_guard lock(projectCacheMutex);
|
||||
if (auto it = projectCache.find(fullHash); it != projectCache.end()) {
|
||||
std::lock_guard lock(ProjectCacheMutex);
|
||||
if (auto it = ProjectCache.find(fullHash); it != ProjectCache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
auto stored = std::make_unique<Configuration>(std::move(cfg));
|
||||
Configuration* ptr = stored.get();
|
||||
projectCache.emplace(std::move(fullHash), std::move(stored));
|
||||
ProjectCache.emplace(std::move(fullHash), std::move(stored));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
fs::path Crafter::GitFetch(const GitSource& source) {
|
||||
std::string name = DeriveName(source);
|
||||
if (name.empty()) {
|
||||
throw std::runtime_error(std::format(
|
||||
"GitFetch: could not derive name from URL '{}'", source.url));
|
||||
throw std::runtime_error(std::format("GitFetch: could not derive name from URL '{}'", source.url));
|
||||
}
|
||||
fs::path externalRoot = GetCacheDir() / "external";
|
||||
std::error_code ec;
|
||||
fs::create_directories(externalRoot, ec);
|
||||
if (ec) {
|
||||
throw std::runtime_error(std::format(
|
||||
"GitFetch: failed to create {}: {}", externalRoot.string(), ec.message()));
|
||||
throw std::runtime_error(std::format("GitFetch: failed to create {}: {}", externalRoot.string(), ec.message()));
|
||||
}
|
||||
std::string keyMaterial = std::format("git|{}|{}|{}",
|
||||
source.url, source.branch, source.commit);
|
||||
std::string keyMaterial = std::format("git|{}|{}|{}", source.url, source.branch, source.commit);
|
||||
std::size_t key = std::hash<std::string>{}(keyMaterial);
|
||||
fs::path cloneDir = externalRoot / std::format("{}-{:016x}", name, key);
|
||||
|
||||
|
|
@ -439,8 +426,8 @@ Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) {
|
|||
std::string fullHash = std::format("{:016x}", std::hash<std::string>{}(fullKey));
|
||||
|
||||
{
|
||||
std::lock_guard lock(projectCacheMutex);
|
||||
if (auto it = projectCache.find(fullHash); it != projectCache.end()) {
|
||||
std::lock_guard lock(ProjectCacheMutex);
|
||||
if (auto it = ProjectCache.find(fullHash); it != ProjectCache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
}
|
||||
|
|
@ -449,12 +436,12 @@ Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) {
|
|||
std::vector<std::string_view> argViews(spec.args.begin(), spec.args.end());
|
||||
Configuration cfg = LoadProjectFromRoot(canonical, depRoot, argViews);
|
||||
|
||||
std::lock_guard lock(projectCacheMutex);
|
||||
if (auto it = projectCache.find(fullHash); it != projectCache.end()) {
|
||||
std::lock_guard lock(ProjectCacheMutex);
|
||||
if (auto it = ProjectCache.find(fullHash); it != ProjectCache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
auto stored = std::make_unique<Configuration>(std::move(cfg));
|
||||
Configuration* ptr = stored.get();
|
||||
projectCache.emplace(std::move(fullHash), std::move(stored));
|
||||
ProjectCache.emplace(std::move(fullHash), std::move(stored));
|
||||
return ptr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Build:Implementation_impl;
|
||||
import std;
|
||||
|
|
@ -13,8 +13,8 @@ namespace Crafter {
|
|||
|
||||
}
|
||||
bool Implementation::Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor) const {
|
||||
std::string objPath = (buildDir/path.filename()).string()+"_impl.o";
|
||||
std::string cppPath = path.string()+".cpp";
|
||||
std::string objPath = std::format("{}_impl.o", (buildDir/path.filename()).string());
|
||||
std::string cppPath = std::format("{}.cpp", path.string());
|
||||
if(!fs::exists(objPath) || std::max(fs::last_write_time(cppPath), sourceFloor) >= fs::last_write_time(objPath)) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Build:Interface_impl;
|
||||
import std;
|
||||
|
|
@ -13,8 +13,8 @@ namespace Crafter {
|
|||
bool ModulePartition::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
|
||||
if(!checked) {
|
||||
checked = true;
|
||||
std::string pcmPath = (pcmDir/path.filename()).generic_string()+".pcm";
|
||||
std::string cppmPath = path.generic_string()+".cppm";
|
||||
std::string pcmPath = std::format("{}.pcm", (pcmDir/path.filename()).generic_string());
|
||||
std::string cppmPath = std::format("{}.cppm", path.generic_string());
|
||||
if(fs::exists(pcmPath) && std::max(fs::last_write_time(cppmPath), sourceFloor) < fs::last_write_time(pcmPath)) {
|
||||
fs::file_time_type pcmTime = fs::last_write_time(pcmPath);
|
||||
for(ModulePartition* dependency : partitionDependencies) {
|
||||
|
|
@ -97,8 +97,8 @@ namespace Crafter {
|
|||
bool Module::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
|
||||
if(!checked) {
|
||||
checked = true;
|
||||
std::string pcmPath = (pcmDir/path.filename()).generic_string()+".pcm";
|
||||
std::string cppmPath = path.generic_string()+".cppm";
|
||||
std::string pcmPath = std::format("{}.pcm", (pcmDir/path.filename()).generic_string());
|
||||
std::string cppmPath = std::format("{}.cppm", path.generic_string());
|
||||
if(fs::exists(pcmPath) && std::max(fs::last_write_time(cppmPath), sourceFloor) < fs::last_write_time(pcmPath)) {
|
||||
bool depCheck = false;
|
||||
for(std::unique_ptr<ModulePartition>& partition : partitions) {
|
||||
|
|
@ -126,7 +126,7 @@ namespace Crafter {
|
|||
}
|
||||
}
|
||||
|
||||
void Module::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
|
||||
void Module::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(partitions.size());
|
||||
for(std::unique_ptr<ModulePartition>& part : partitions) {
|
||||
|
|
|
|||
444
implementations/Crafter.Build-Lint.cpp
Normal file
444
implementations/Crafter.Build-Lint.cpp
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
export module Crafter.Build:Lint_impl;
|
||||
import std;
|
||||
import :Lint;
|
||||
import :Clang;
|
||||
import :Platform;
|
||||
import :Progress;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
// Blank //-comments, /*...*/ comments and string/char literal bodies to
|
||||
// spaces while copying '\n' through, so byte offsets and line numbers in
|
||||
// the result match the original text. Raw string literals are not
|
||||
// recognized (documented v1 limitation) — their bodies pass through as
|
||||
// ordinary string content until the first '"'.
|
||||
std::string StripComments(std::string_view src) {
|
||||
enum class State { Code, LineComment, BlockComment, String, Char };
|
||||
std::string out;
|
||||
out.reserve(src.size());
|
||||
State state = State::Code;
|
||||
for (std::size_t i = 0; i < src.size(); ++i) {
|
||||
char c = src[i];
|
||||
char next = i + 1 < src.size() ? src[i + 1] : '\0';
|
||||
switch (state) {
|
||||
case State::Code:
|
||||
if (c == '/' && next == '/') {
|
||||
state = State::LineComment;
|
||||
out += " ";
|
||||
++i;
|
||||
} else if (c == '/' && next == '*') {
|
||||
state = State::BlockComment;
|
||||
out += " ";
|
||||
++i;
|
||||
} else if (c == '"') {
|
||||
state = State::String;
|
||||
out += c; // keep the delimiter so quoting stays visible
|
||||
} else if (c == '\'') {
|
||||
state = State::Char;
|
||||
out += c;
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
break;
|
||||
case State::LineComment:
|
||||
if (c == '\n') {
|
||||
state = State::Code;
|
||||
out += c;
|
||||
} else {
|
||||
out += ' ';
|
||||
}
|
||||
break;
|
||||
case State::BlockComment:
|
||||
if (c == '*' && next == '/') {
|
||||
state = State::Code;
|
||||
out += " ";
|
||||
++i;
|
||||
} else {
|
||||
out += c == '\n' ? '\n' : ' ';
|
||||
}
|
||||
break;
|
||||
case State::String:
|
||||
case State::Char: {
|
||||
char delim = state == State::String ? '"' : '\'';
|
||||
if (c == '\\' && next != '\0') {
|
||||
out += " ";
|
||||
++i;
|
||||
if (next == '\n') out.back() = '\n';
|
||||
} else if (c == delim) {
|
||||
state = State::Code;
|
||||
out += c;
|
||||
} else {
|
||||
out += c == '\n' ? '\n' : ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> SplitLines(std::string_view content) {
|
||||
std::vector<std::string_view> lines;
|
||||
std::size_t start = 0;
|
||||
while (start <= content.size()) {
|
||||
std::size_t end = content.find('\n', start);
|
||||
if (end == std::string_view::npos) {
|
||||
// Skip a phantom empty final line after a trailing '\n'.
|
||||
if (start < content.size()) lines.push_back(content.substr(start));
|
||||
break;
|
||||
}
|
||||
lines.push_back(content.substr(start, end - start));
|
||||
start = end + 1;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
bool PathInsideRoot(const fs::path& p, const fs::path& root) {
|
||||
fs::path rel = fs::weakly_canonical(p).lexically_relative(fs::weakly_canonical(root));
|
||||
return !rel.empty() && *rel.begin() != "..";
|
||||
}
|
||||
|
||||
// Depth-first walk over the dependency graph keeping only Configurations
|
||||
// whose path lies inside the project root — GitProject / external deps
|
||||
// live under the global cache and are foreign code: they contribute
|
||||
// neither rules nor files. Root-first order so the root's rules win the
|
||||
// by-name dedup.
|
||||
std::vector<Configuration*> CollectLocalConfigs(Configuration& root, const fs::path& projectRoot) {
|
||||
std::vector<Configuration*> local;
|
||||
std::unordered_set<Configuration*> seen;
|
||||
std::function<void(Configuration*)> walk = [&](Configuration* c) {
|
||||
if (!seen.insert(c).second) return;
|
||||
if (PathInsideRoot(fs::absolute(c->path), projectRoot)) {
|
||||
local.push_back(c);
|
||||
}
|
||||
for (Configuration* dep : c->dependencies) walk(dep);
|
||||
};
|
||||
walk(&root);
|
||||
return local;
|
||||
}
|
||||
|
||||
void CollectConfigSources(const Configuration& c, std::set<fs::path>& files) {
|
||||
for (const std::unique_ptr<Module>& mod : c.interfaces) {
|
||||
files.insert(fs::path(std::format("{}.cppm", mod->path.string())));
|
||||
for (const std::unique_ptr<ModulePartition>& part : mod->partitions) {
|
||||
files.insert(fs::path(std::format("{}.cppm", part->path.string())));
|
||||
}
|
||||
}
|
||||
for (const Implementation& impl : c.implementations) {
|
||||
files.insert(fs::path(std::format("{}.cpp", impl.path.string())));
|
||||
}
|
||||
// cFiles/cuda resolve against cwd at build time (see Build's compile
|
||||
// loops); mirror that here.
|
||||
for (const fs::path& cf : c.cFiles) {
|
||||
files.insert(fs::absolute(fs::path(std::format("{}.c", cf.string()))).lexically_normal());
|
||||
}
|
||||
for (const fs::path& cu : c.cuda) {
|
||||
files.insert(fs::absolute(fs::path(std::format("{}.cu", cu.string()))).lexically_normal());
|
||||
}
|
||||
for (const Shader& shader : c.shaders) {
|
||||
files.insert(fs::absolute(shader.path).lexically_normal());
|
||||
}
|
||||
// files/buildFiles/assets are deliberately excluded: data shipped or
|
||||
// referenced by the build, not source code.
|
||||
}
|
||||
}
|
||||
|
||||
std::string LintContext::Extension() const {
|
||||
return file.extension().string();
|
||||
}
|
||||
|
||||
std::string_view LintContext::Line(std::size_t n) const {
|
||||
if (n == 0 || n > lines.size()) return {};
|
||||
return lines[n - 1];
|
||||
}
|
||||
|
||||
const std::string& LintContext::CommentStripped() {
|
||||
if (!commentStrippedCache) {
|
||||
commentStrippedCache = StripComments(content);
|
||||
}
|
||||
return *commentStrippedCache;
|
||||
}
|
||||
|
||||
void LintContext::Report(std::size_t line, std::string message) {
|
||||
sink->push_back({file, line, activeRule, std::move(message)});
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Scan raw lines for suppression comments. Raw, not stripped: the
|
||||
// directives ARE comments. A next-line directive on (0-based) line i
|
||||
// targets 1-based line i + 2 — the line below it.
|
||||
LintSuppressions ParseSuppressions(std::span<const std::string_view> lines) {
|
||||
LintSuppressions s;
|
||||
constexpr std::string_view NextLineMarker = "lint-disable-next-line";
|
||||
constexpr std::string_view FileMarker = "lint-disable-file";
|
||||
for (std::size_t i = 0; i < lines.size(); ++i) {
|
||||
std::size_t slash = lines[i].find("//");
|
||||
if (slash == std::string_view::npos) continue;
|
||||
bool nextLine = true;
|
||||
std::size_t marker = lines[i].find(NextLineMarker, slash);
|
||||
std::size_t markerLen = NextLineMarker.size();
|
||||
if (marker == std::string_view::npos) {
|
||||
nextLine = false;
|
||||
marker = lines[i].find(FileMarker, slash);
|
||||
markerLen = FileMarker.size();
|
||||
}
|
||||
if (marker == std::string_view::npos) continue;
|
||||
// Everything after the marker is rule names; none = all rules.
|
||||
std::string_view rest = lines[i].substr(marker + markerLen);
|
||||
std::vector<std::string> names;
|
||||
std::size_t pos = 0;
|
||||
while (pos < rest.size()) {
|
||||
if (rest[pos] == ' ' || rest[pos] == '\t' || rest[pos] == ',' || rest[pos] == '\r') { ++pos; continue; }
|
||||
std::size_t end = rest.find_first_of(" \t,\r", pos);
|
||||
if (end == std::string_view::npos) end = rest.size();
|
||||
names.emplace_back(rest.substr(pos, end - pos));
|
||||
pos = end;
|
||||
}
|
||||
if (nextLine) {
|
||||
if (names.empty()) s.lineAll.insert(i + 2);
|
||||
else for (std::string& n : names) s.lineRules[i + 2].insert(std::move(n));
|
||||
} else {
|
||||
if (names.empty()) s.fileAll = true;
|
||||
else for (std::string& n : names) s.fileRules.insert(std::move(n));
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
bool LintContext::Suppressed(std::string_view rule, std::size_t line) {
|
||||
if (!suppressionsCache) suppressionsCache = ParseSuppressions(lines);
|
||||
const LintSuppressions& s = *suppressionsCache;
|
||||
if (s.fileAll || s.fileRules.contains(std::string(rule))) return true;
|
||||
if (line == 0) return false;
|
||||
if (s.lineAll.contains(line)) return true;
|
||||
if (auto it = s.lineRules.find(line); it != s.lineRules.end()) return it->second.contains(std::string(rule));
|
||||
return false;
|
||||
}
|
||||
|
||||
void LintContext::SetContent(std::string newContent) {
|
||||
content = std::move(newContent);
|
||||
lines = SplitLines(content);
|
||||
commentStrippedCache.reset();
|
||||
suppressionsCache.reset(); // line numbers may have shifted — re-parse
|
||||
}
|
||||
|
||||
void Configuration::AddLintRule(std::string name, std::function<void(LintContext&)> check) {
|
||||
lintRules.push_back({std::move(name), std::move(check)});
|
||||
}
|
||||
|
||||
LintSummary Crafter::RunLint(Configuration& projectCfg, const RunLintOptions& opts) {
|
||||
LintSummary summary;
|
||||
|
||||
fs::path projectRoot = opts.projectFile.empty()
|
||||
? fs::absolute(projectCfg.path)
|
||||
: opts.projectFile.parent_path();
|
||||
|
||||
std::vector<Configuration*> localConfigs = CollectLocalConfigs(projectCfg, projectRoot);
|
||||
|
||||
// Collect rules root-first, dedup by name (first registration wins).
|
||||
std::vector<const LintRule*> rules;
|
||||
std::unordered_set<std::string_view> ruleNames;
|
||||
for (Configuration* c : localConfigs) {
|
||||
for (const LintRule& rule : c->lintRules) {
|
||||
if (ruleNames.insert(rule.name).second) rules.push_back(&rule);
|
||||
}
|
||||
}
|
||||
|
||||
if (rules.empty()) {
|
||||
summary.noRulesDefined = true;
|
||||
std::println(std::cerr,
|
||||
R"msg(No lint rules defined.
|
||||
Register rules in project.cpp before returning the Configuration:
|
||||
|
||||
cfg.AddLintRule("no-tabs", [](Crafter::LintContext& ctx) {{
|
||||
if (ctx.Extension() != ".cpp" && ctx.Extension() != ".cppm") return;
|
||||
for (std::size_t n = 1; n <= ctx.lines.size(); ++n) {{
|
||||
if (ctx.Line(n).contains('\t')) ctx.Report(n, "tab character (use spaces)");
|
||||
}}
|
||||
}});
|
||||
|
||||
A rule that calls ctx.SetContent(newContent) is a transform: `crafter-build
|
||||
format` applies it to disk, and `crafter-build lint` reports where it would.
|
||||
`crafter-build lint` runs every rule over the project's own sources.)msg");
|
||||
return summary;
|
||||
}
|
||||
|
||||
std::erase_if(rules, [&](const LintRule* r) { return !MatchAny(opts.globs, r->name); });
|
||||
summary.rulesRun = rules.size();
|
||||
|
||||
if (opts.listOnly) {
|
||||
for (const LintRule* rule : rules) std::println("{}", rule->name);
|
||||
return summary;
|
||||
}
|
||||
if (rules.empty()) {
|
||||
std::println("No lint rules matched.");
|
||||
return summary;
|
||||
}
|
||||
|
||||
std::set<fs::path> files;
|
||||
for (Configuration* c : localConfigs) {
|
||||
CollectConfigSources(*c, files);
|
||||
for (const Test& t : c->tests) CollectConfigSources(t.config, files);
|
||||
}
|
||||
if (!opts.projectFile.empty()) files.insert(opts.projectFile);
|
||||
|
||||
fs::path cwd = fs::current_path();
|
||||
auto shown = [&cwd](const fs::path& p) {
|
||||
return PathInsideRoot(p, cwd) ? p.lexically_relative(cwd) : p;
|
||||
};
|
||||
|
||||
for (const fs::path& file : files) {
|
||||
std::ifstream in(file, std::ios::binary);
|
||||
if (!in) continue; // config parse already read it; a vanished file fails the build first
|
||||
std::stringstream buffer;
|
||||
buffer << in.rdbuf();
|
||||
LintContext ctx;
|
||||
ctx.file = file;
|
||||
ctx.content = std::move(buffer).str();
|
||||
ctx.lines = SplitLines(ctx.content);
|
||||
ctx.sink = &summary.findings;
|
||||
++summary.filesLinted;
|
||||
const std::string original = ctx.content;
|
||||
for (const LintRule* rule : rules) {
|
||||
ctx.activeRule = rule->name;
|
||||
// Snapshot for transform diffing — and the revert point if the
|
||||
// rule throws, so a half-applied transform never reaches disk
|
||||
// and chained rules see clean input.
|
||||
std::string before = ctx.content;
|
||||
try {
|
||||
rule->check(ctx);
|
||||
} catch (const std::exception& e) {
|
||||
// Never let a rule's exception unwind across the project
|
||||
// DLL boundary — surface it as a finding instead.
|
||||
ctx.SetContent(std::move(before));
|
||||
ctx.Report(0, std::format("rule '{}' threw: {}", rule->name, e.what()));
|
||||
++summary.errors;
|
||||
if (opts.mode != LintMode::Report) {
|
||||
// Report mode prints it with the findings; the other
|
||||
// modes don't print findings, so surface it here.
|
||||
std::println(std::cerr, "{}: rule '{}' threw: {}", shown(file).string(), rule->name, e.what());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Transform detection is compare-by-value: a rule that SetContents
|
||||
// identical bytes is not a change. The mutated content carries
|
||||
// forward in every mode so chained rules compose identically
|
||||
// whether or not this run writes.
|
||||
if (ctx.content == before) continue;
|
||||
// File-level suppression disables the transform outright — in
|
||||
// every mode, so `format` never rewrites a suppressed file.
|
||||
if (ctx.Suppressed(rule->name, 0)) {
|
||||
ctx.SetContent(std::move(before));
|
||||
continue;
|
||||
}
|
||||
// Diff before/after. Same line count → per-line handling:
|
||||
// suppressed changed lines are REVERTED (all modes — suppression
|
||||
// must also stop `format`), the rest yield would-reformat
|
||||
// findings in the dry modes. Different count (or no differing
|
||||
// line — SplitLines hides a trailing '\n', the final-newline
|
||||
// case) → one whole-file finding; count-changing transforms
|
||||
// handle per-line suppression themselves (see
|
||||
// LintContext::Suppressed).
|
||||
std::vector<std::string_view> beforeLines = SplitLines(before);
|
||||
bool anyLineDiffers = false;
|
||||
if (beforeLines.size() == ctx.lines.size()) {
|
||||
std::vector<std::size_t> reverted;
|
||||
for (std::size_t i = 0; i < beforeLines.size(); ++i) {
|
||||
if (beforeLines[i] == ctx.lines[i]) continue;
|
||||
anyLineDiffers = true;
|
||||
if (ctx.Suppressed(rule->name, i + 1)) {
|
||||
reverted.push_back(i);
|
||||
} else if (opts.mode != LintMode::Apply) {
|
||||
summary.findings.push_back({file, i + 1, rule->name, "would reformat"});
|
||||
}
|
||||
}
|
||||
if (!reverted.empty()) {
|
||||
std::string rebuilt;
|
||||
rebuilt.reserve(ctx.content.size());
|
||||
std::size_t next = 0;
|
||||
for (std::size_t i = 0; i < ctx.lines.size(); ++i) {
|
||||
rebuilt += (next < reverted.size() && reverted[next] == i) ? beforeLines[i] : ctx.lines[i];
|
||||
if (next < reverted.size() && reverted[next] == i) ++next;
|
||||
if (i + 1 < ctx.lines.size() || ctx.content.ends_with('\n')) rebuilt += '\n';
|
||||
}
|
||||
ctx.SetContent(std::move(rebuilt));
|
||||
}
|
||||
}
|
||||
if (!anyLineDiffers && opts.mode != LintMode::Apply && ctx.content != before) {
|
||||
summary.findings.push_back({file, 0, rule->name, "would reformat"});
|
||||
}
|
||||
}
|
||||
// Drop findings the file's directives suppress — covers Report()
|
||||
// calls from any rule (custom ones included) plus the derived
|
||||
// would-reformat findings above. Line-0 findings only match
|
||||
// file-level directives.
|
||||
std::erase_if(summary.findings, [&](const LintFinding& f) {
|
||||
return f.file == file && ctx.Suppressed(f.rule, f.line);
|
||||
});
|
||||
if (ctx.content != original) {
|
||||
summary.changedFiles.push_back(file);
|
||||
if (opts.mode == LintMode::Apply) {
|
||||
std::ofstream out(file, std::ios::binary | std::ios::trunc);
|
||||
out.write(ctx.content.data(), static_cast<std::streamsize>(ctx.content.size()));
|
||||
out.close();
|
||||
if (!out) {
|
||||
std::println(std::cerr, "failed to write {}", shown(file).string());
|
||||
++summary.errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(summary.findings.begin(), summary.findings.end(),
|
||||
[](const LintFinding& a, const LintFinding& b) {
|
||||
return std::tie(a.file, a.line) < std::tie(b.file, b.line);
|
||||
});
|
||||
|
||||
Progress::Clear();
|
||||
switch (opts.mode) {
|
||||
case LintMode::Report: {
|
||||
std::unordered_set<std::string> filesWithFindings;
|
||||
for (const LintFinding& f : summary.findings) {
|
||||
filesWithFindings.insert(f.file.string());
|
||||
std::println("{}:{}: warning: {} [{}]", shown(f.file).string(), f.line, f.message, f.rule);
|
||||
}
|
||||
if (summary.findings.empty()) {
|
||||
std::println("Lint clean: {} files, {} rules", summary.filesLinted, summary.rulesRun);
|
||||
} else {
|
||||
std::println("{} finding(s) in {} of {} files ({} rules)", summary.findings.size(), filesWithFindings.size(), summary.filesLinted, summary.rulesRun);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LintMode::Check: {
|
||||
// gofmt -l style: the paths alone, then a one-line verdict.
|
||||
// Report-only findings are lint's business, not printed here.
|
||||
for (const fs::path& f : summary.changedFiles) {
|
||||
std::println("{}", shown(f).string());
|
||||
}
|
||||
if (summary.changedFiles.empty()) {
|
||||
std::println("Format check clean: {} files, {} rules", summary.filesLinted, summary.rulesRun);
|
||||
} else {
|
||||
std::println("{} file(s) would be reformatted", summary.changedFiles.size());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LintMode::Apply: {
|
||||
for (const fs::path& f : summary.changedFiles) {
|
||||
std::println("formatted: {}", shown(f).string());
|
||||
}
|
||||
if (summary.changedFiles.empty()) {
|
||||
std::println("Nothing to format: {} files, {} rules", summary.filesLinted, summary.rulesRun);
|
||||
} else {
|
||||
std::println("Formatted {} of {} files ({} rules)", summary.changedFiles.size(), summary.filesLinted, summary.rulesRun);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
|
|
@ -42,7 +42,7 @@ namespace {
|
|||
// don't keep the lock alive past our own release.
|
||||
class CacheLock {
|
||||
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||
int fd_ = -1;
|
||||
std::int32_t fd_ = -1;
|
||||
public:
|
||||
explicit CacheLock(const fs::path& cacheDir) {
|
||||
fd_ = ::open((cacheDir / ".lock").c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644);
|
||||
|
|
@ -59,9 +59,7 @@ namespace {
|
|||
HANDLE handle_ = INVALID_HANDLE_VALUE;
|
||||
public:
|
||||
explicit CacheLock(const fs::path& cacheDir) {
|
||||
handle_ = CreateFileW((cacheDir / ".lock").wstring().c_str(),
|
||||
GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
handle_ = CreateFileW((cacheDir / ".lock").wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (handle_ != INVALID_HANDLE_VALUE) {
|
||||
OVERLAPPED ov{};
|
||||
LockFileEx(handle_, LOCKFILE_EXCLUSIVE_LOCK, 0, MAXDWORD, MAXDWORD, &ov);
|
||||
|
|
@ -111,15 +109,43 @@ fs::path Crafter::GetCrafterBuildHome() {
|
|||
if (parent == dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
std::string msg = std::format(
|
||||
"could not locate crafter-build runtime assets relative to {} (set CRAFTER_BUILD_HOME). Tried:",
|
||||
hostExe.string());
|
||||
std::string msg = std::format("could not locate crafter-build runtime assets relative to {} (set CRAFTER_BUILD_HOME). Tried:", hostExe.string());
|
||||
for (const auto& p : tried) {
|
||||
msg += "\n " + p.string();
|
||||
msg += std::format("\n {}", p.string());
|
||||
}
|
||||
throw std::runtime_error(msg);
|
||||
}
|
||||
|
||||
bool Crafter::MatchGlob(std::string_view glob, std::string_view name) {
|
||||
std::size_t gi = 0;
|
||||
std::size_t ni = 0;
|
||||
std::size_t star = std::string_view::npos;
|
||||
std::size_t mark = 0;
|
||||
while (ni < name.size()) {
|
||||
if (gi < glob.size() && (glob[gi] == '?' || glob[gi] == name[ni])) {
|
||||
++gi; ++ni;
|
||||
} else if (gi < glob.size() && glob[gi] == '*') {
|
||||
star = gi++;
|
||||
mark = ni;
|
||||
} else if (star != std::string_view::npos) {
|
||||
gi = star + 1;
|
||||
ni = ++mark;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
while (gi < glob.size() && glob[gi] == '*') ++gi;
|
||||
return gi == glob.size();
|
||||
}
|
||||
|
||||
bool Crafter::MatchAny(std::span<const std::string> globs, std::string_view name) {
|
||||
if (globs.empty()) return true;
|
||||
for (const auto& g : globs) {
|
||||
if (MatchGlob(g, name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
std::string Crafter::RunCommand(const std::string_view cmd) {
|
||||
Progress::EchoCommand(cmd);
|
||||
|
|
@ -127,14 +153,14 @@ std::string Crafter::RunCommand(const std::string_view cmd) {
|
|||
std::string result;
|
||||
|
||||
// Use cmd.exe to interpret redirection
|
||||
std::string with = "cmd /C \"" + std::string(cmd) + " 2>&1\"";
|
||||
std::string with = std::format("cmd /C \"{} 2>&1\"", std::string(cmd));
|
||||
|
||||
FILE* pipe = _popen(with.c_str(), "r");
|
||||
if (!pipe) {
|
||||
throw std::runtime_error("_popen() failed!");
|
||||
}
|
||||
|
||||
while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
while (fgets(buffer.data(), static_cast<std::int32_t>(buffer.size()), pipe) != nullptr) {
|
||||
result += buffer.data();
|
||||
}
|
||||
|
||||
|
|
@ -146,14 +172,14 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|||
std::array<char, 128> buffer;
|
||||
CommandResult result{};
|
||||
|
||||
std::string with = "cmd /C \"" + std::string(cmd) + " 2>&1\"";
|
||||
std::string with = std::format("cmd /C \"{} 2>&1\"", std::string(cmd));
|
||||
|
||||
FILE* pipe = _popen(with.c_str(), "r");
|
||||
if (!pipe) {
|
||||
throw std::runtime_error("_popen() failed!");
|
||||
}
|
||||
|
||||
while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
while (fgets(buffer.data(), static_cast<std::int32_t>(buffer.size()), pipe) != nullptr) {
|
||||
result.output += buffer.data();
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +191,8 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
CommandResult result{};
|
||||
|
||||
SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, TRUE };
|
||||
HANDLE readEnd = nullptr, writeEnd = nullptr;
|
||||
HANDLE readEnd = nullptr;
|
||||
HANDLE writeEnd = nullptr;
|
||||
if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) {
|
||||
throw std::runtime_error("CreatePipe failed");
|
||||
}
|
||||
|
|
@ -196,14 +223,11 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
// &&, ||) as popen("/bin/sh -c …") does on Linux. cmd's special-case
|
||||
// /C parsing strips the outer quote pair when the inner text contains
|
||||
// additional quotes, which is the common case here.
|
||||
std::string wrapped = "cmd /C \"" + std::string(cmd) + "\"";
|
||||
std::string wrapped = std::format("cmd /C \"{}\"", std::string(cmd));
|
||||
std::vector<char> cmdBuf(wrapped.begin(), wrapped.end());
|
||||
cmdBuf.push_back('\0');
|
||||
|
||||
BOOL ok = CreateProcessA(
|
||||
nullptr, cmdBuf.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_NO_WINDOW | CREATE_SUSPENDED,
|
||||
nullptr, nullptr, &si, &pi);
|
||||
BOOL ok = CreateProcessA(nullptr, cmdBuf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &si, &pi);
|
||||
if (!ok) {
|
||||
CloseHandle(readEnd);
|
||||
CloseHandle(writeEnd);
|
||||
|
|
@ -226,9 +250,7 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
}
|
||||
});
|
||||
|
||||
DWORD waitMs = static_cast<DWORD>(std::min<long long>(
|
||||
static_cast<long long>(timeout.count()) * 1000LL,
|
||||
static_cast<long long>(INFINITE) - 1));
|
||||
DWORD waitMs = static_cast<DWORD>(std::min<std::int64_t>(static_cast<std::int64_t>(timeout.count()) * 1000LL, static_cast<std::int64_t>(INFINITE) - 1));
|
||||
DWORD waitResult = WaitForSingleObject(pi.hProcess, waitMs);
|
||||
|
||||
if (waitResult == WAIT_TIMEOUT) {
|
||||
|
|
@ -245,9 +267,9 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
// so the runner shows 💥 instead of a numeric exit code.
|
||||
if ((exit & 0xC0000000) == 0xC0000000) {
|
||||
result.crashed = true;
|
||||
result.signal = static_cast<int>(exit);
|
||||
result.signal = static_cast<std::int32_t>(exit);
|
||||
}
|
||||
result.exitCode = static_cast<int>(exit);
|
||||
result.exitCode = static_cast<std::int32_t>(exit);
|
||||
}
|
||||
|
||||
reader.join();
|
||||
|
|
@ -269,7 +291,7 @@ fs::path Crafter::GetCacheDir() {
|
|||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 10> kCrafterBuildModules = {
|
||||
constexpr std::array<std::string_view, 11> CrafterBuildModules = {
|
||||
"Crafter.Build-Shader",
|
||||
"Crafter.Build-Platform",
|
||||
"Crafter.Build-Interface",
|
||||
|
|
@ -277,6 +299,7 @@ namespace {
|
|||
"Crafter.Build-External",
|
||||
"Crafter.Build-Clang",
|
||||
"Crafter.Build-Test",
|
||||
"Crafter.Build-Lint",
|
||||
"Crafter.Build-Progress",
|
||||
"Crafter.Build-Asset",
|
||||
"Crafter.Build",
|
||||
|
|
@ -305,9 +328,9 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
namespace {
|
||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||
CacheLock lock(cacheDir);
|
||||
for (std::string_view name : kCrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / (std::string(name) + ".cppm");
|
||||
fs::path pcmPath = cacheDir / (std::string(name) + ".pcm");
|
||||
for (std::string_view name : CrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
||||
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
||||
if (!fs::exists(cppmPath)) {
|
||||
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
|
||||
}
|
||||
|
|
@ -336,7 +359,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
if (!fs::exists(buildDir)) {
|
||||
fs::create_directories(buildDir);
|
||||
}
|
||||
fs::path dllPath = buildDir / (absProject.stem().string() + ".dll");
|
||||
fs::path dllPath = buildDir / std::format("{}.dll", absProject.stem().string());
|
||||
|
||||
char hostExeBuf[MAX_PATH];
|
||||
DWORD hostExeLen = GetModuleFileNameA(nullptr, hostExeBuf, MAX_PATH);
|
||||
|
|
@ -361,9 +384,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
|
||||
EnsureCrafterBuildPcms(sourceDir, cacheDir);
|
||||
|
||||
bool stale = !fs::exists(dllPath)
|
||||
|| fs::last_write_time(dllPath) < fs::last_write_time(absProject)
|
||||
|| fs::last_write_time(dllPath) < fs::last_write_time(hostExe);
|
||||
bool stale = !fs::exists(dllPath) || fs::last_write_time(dllPath) < fs::last_write_time(absProject) || fs::last_write_time(dllPath) < fs::last_write_time(hostExe);
|
||||
|
||||
if (stale) {
|
||||
fs::path crafterBuildLib = hostExe.parent_path() / "crafter-build.lib";
|
||||
|
|
@ -427,9 +448,7 @@ namespace {
|
|||
std::string MingwGccVersion() {
|
||||
fs::path includeRoot = MingwPrefix() / "include" / "c++";
|
||||
if (!fs::exists(includeRoot)) {
|
||||
throw std::runtime_error(std::format(
|
||||
"mingw-w64 not found at {} (install msys2 mingw-w64-x86_64-toolchain or set CRAFTER_MINGW_DIR)",
|
||||
MingwPrefix().string()));
|
||||
throw std::runtime_error(std::format("mingw-w64 not found at {} (install msys2 mingw-w64-x86_64-toolchain or set CRAFTER_MINGW_DIR)", MingwPrefix().string()));
|
||||
}
|
||||
std::vector<std::string> versions;
|
||||
for (const auto& entry : fs::directory_iterator(includeRoot)) {
|
||||
|
|
@ -505,13 +524,11 @@ namespace {
|
|||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||
CacheLock lock(cacheDir);
|
||||
fs::path prefix = MingwPrefix();
|
||||
for (std::string_view name : kCrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / (std::string(name) + ".cppm");
|
||||
fs::path pcmPath = cacheDir / (std::string(name) + ".pcm");
|
||||
for (std::string_view name : CrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
||||
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
||||
if (!fs::exists(cppmPath)) {
|
||||
throw std::runtime_error(std::format(
|
||||
"module source {} not found in {} (set CRAFTER_BUILD_HOME)",
|
||||
name, sourceDir.string()));
|
||||
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
|
||||
}
|
||||
if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) {
|
||||
continue;
|
||||
|
|
@ -526,8 +543,7 @@ namespace {
|
|||
prefix.string(), cacheDir.string(), cppmPath.string(), pcmPath.string());
|
||||
CommandResult r = Crafter::RunCommandChecked(cmd);
|
||||
if (r.exitCode != 0) {
|
||||
throw std::runtime_error(std::format(
|
||||
"Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
||||
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -537,7 +553,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
fs::path absProject = fs::canonical(projectFile);
|
||||
fs::path buildDir = absProject.parent_path() / "build";
|
||||
if (!fs::exists(buildDir)) fs::create_directories(buildDir);
|
||||
fs::path dllPath = buildDir / (absProject.stem().string() + ".dll");
|
||||
fs::path dllPath = buildDir / std::format("{}.dll", absProject.stem().string());
|
||||
|
||||
char hostExeBuf[MAX_PATH];
|
||||
DWORD hostExeLen = GetModuleFileNameA(nullptr, hostExeBuf, MAX_PATH);
|
||||
|
|
@ -562,9 +578,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
|
||||
EnsureCrafterBuildPcms(sourceDir, cacheDir);
|
||||
|
||||
bool stale = !fs::exists(dllPath)
|
||||
|| fs::last_write_time(dllPath) < fs::last_write_time(absProject)
|
||||
|| fs::last_write_time(dllPath) < fs::last_write_time(hostExe);
|
||||
bool stale = !fs::exists(dllPath) || fs::last_write_time(dllPath) < fs::last_write_time(absProject) || fs::last_write_time(dllPath) < fs::last_write_time(hostExe);
|
||||
|
||||
if (stale) {
|
||||
// The import lib lives next to the launcher exe (Build()'s mingw
|
||||
|
|
@ -589,8 +603,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
|
||||
std::string result = RunCommand(compileCmd);
|
||||
if (!result.empty()) {
|
||||
throw std::runtime_error(std::format(
|
||||
"Failed to compile project {}: {}", absProject.string(), result));
|
||||
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -604,15 +617,13 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
|
||||
HMODULE handle = LoadLibraryA(dllPath.string().c_str());
|
||||
if (!handle) {
|
||||
throw std::runtime_error(std::format(
|
||||
"Failed to load project {}: error {}", dllPath.string(), GetLastError()));
|
||||
throw std::runtime_error(std::format("Failed to load project {}: error {}", dllPath.string(), GetLastError()));
|
||||
}
|
||||
|
||||
using ProjectFn = Configuration (*)(std::span<const std::string_view>);
|
||||
auto fn = reinterpret_cast<ProjectFn>(GetProcAddress(handle, "CrafterBuildProject"));
|
||||
if (!fn) {
|
||||
throw std::runtime_error(std::format(
|
||||
"CrafterBuildProject not found in {}: error {}", dllPath.string(), GetLastError()));
|
||||
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: error {}", dllPath.string(), GetLastError()));
|
||||
}
|
||||
|
||||
return fn(args);
|
||||
|
|
@ -626,7 +637,7 @@ std::string Crafter::RunCommand(const std::string_view cmd) {
|
|||
std::array<char, 128> buffer;
|
||||
std::string result;
|
||||
|
||||
std::string with = std::string(cmd) + " 2>&1";
|
||||
std::string with = std::format("{} 2>&1", cmd);
|
||||
// Open pipe to file
|
||||
FILE* pipe = popen(with.c_str(), "r");
|
||||
if (!pipe) throw std::runtime_error("popen() failed!");
|
||||
|
|
@ -645,7 +656,7 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|||
std::array<char, 128> buffer;
|
||||
CommandResult result{};
|
||||
|
||||
std::string with = std::string(cmd) + " 2>&1";
|
||||
std::string with = std::format("{} 2>&1", cmd);
|
||||
FILE* pipe = popen(with.c_str(), "r");
|
||||
if (!pipe) throw std::runtime_error("popen() failed!");
|
||||
|
||||
|
|
@ -653,7 +664,7 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|||
result.output += buffer.data();
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
std::int32_t status = pclose(pipe);
|
||||
if (WIFEXITED(status)) {
|
||||
result.exitCode = WEXITSTATUS(status);
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
|
|
@ -670,9 +681,7 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
std::array<char, 128> buffer;
|
||||
CommandResult result{};
|
||||
|
||||
std::string wrapped = std::format(
|
||||
"timeout --kill-after=2 {} {} 2>&1",
|
||||
timeout.count(), cmd);
|
||||
std::string wrapped = std::format("timeout --kill-after=2 {} {} 2>&1", timeout.count(), cmd);
|
||||
|
||||
FILE* pipe = popen(wrapped.c_str(), "r");
|
||||
if (!pipe) throw std::runtime_error("popen() failed!");
|
||||
|
|
@ -681,9 +690,9 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
|||
result.output += buffer.data();
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
std::int32_t status = pclose(pipe);
|
||||
if (WIFEXITED(status)) {
|
||||
int code = WEXITSTATUS(status);
|
||||
std::int32_t code = WEXITSTATUS(status);
|
||||
if (code == 124) {
|
||||
result.timedOut = true;
|
||||
result.exitCode = 124;
|
||||
|
|
@ -774,7 +783,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
// (e.g. -mrelaxed-simd) so the std BMI is feature-compatible. wasm only;
|
||||
// wasmVariantFlags is empty on every other path.
|
||||
if (isWasm) {
|
||||
for (const std::string& f : config.wasmVariantFlags) archFlags += " " + f;
|
||||
for (const std::string& f : config.wasmVariantFlags) archFlags += std::format(" {}", f);
|
||||
}
|
||||
// non-wasm cross builds: the driver ranks the host toolchain's own
|
||||
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
|
||||
|
|
@ -814,7 +823,7 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|||
}
|
||||
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 10> kCrafterBuildModules = {
|
||||
constexpr std::array<std::string_view, 11> CrafterBuildModules = {
|
||||
"Crafter.Build-Shader",
|
||||
"Crafter.Build-Platform",
|
||||
"Crafter.Build-Interface",
|
||||
|
|
@ -822,6 +831,7 @@ namespace {
|
|||
"Crafter.Build-External",
|
||||
"Crafter.Build-Clang",
|
||||
"Crafter.Build-Test",
|
||||
"Crafter.Build-Lint",
|
||||
"Crafter.Build-Progress",
|
||||
"Crafter.Build-Asset",
|
||||
"Crafter.Build",
|
||||
|
|
@ -829,9 +839,9 @@ namespace {
|
|||
|
||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||
CacheLock lock(cacheDir);
|
||||
for (std::string_view name : kCrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / (std::string(name) + ".cppm");
|
||||
fs::path pcmPath = cacheDir / (std::string(name) + ".pcm");
|
||||
for (std::string_view name : CrafterBuildModules) {
|
||||
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
||||
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
||||
if (!fs::exists(cppmPath)) {
|
||||
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
|
||||
}
|
||||
|
|
@ -859,7 +869,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
if (!fs::exists(buildDir)) {
|
||||
fs::create_directories(buildDir);
|
||||
}
|
||||
fs::path soPath = buildDir / (absProject.stem().string() + ".so");
|
||||
fs::path soPath = buildDir / std::format("{}.so", absProject.stem().string());
|
||||
|
||||
fs::path hostExe = fs::read_symlink("/proc/self/exe");
|
||||
|
||||
|
|
@ -879,9 +889,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
|
||||
EnsureCrafterBuildPcms(sourceDir, cacheDir);
|
||||
|
||||
bool stale = !fs::exists(soPath)
|
||||
|| fs::last_write_time(soPath) < fs::last_write_time(absProject)
|
||||
|| fs::last_write_time(soPath) < fs::last_write_time(hostExe);
|
||||
bool stale = !fs::exists(soPath) || fs::last_write_time(soPath) < fs::last_write_time(absProject) || fs::last_write_time(soPath) < fs::last_write_time(hostExe);
|
||||
|
||||
if (stale) {
|
||||
std::string compileCmd = std::format(
|
||||
|
|
@ -914,4 +922,4 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
return fn(args);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include <stdio.h>
|
||||
|
|
@ -22,21 +22,21 @@ import std;
|
|||
import :Progress;
|
||||
|
||||
namespace {
|
||||
std::mutex g_mutex;
|
||||
std::atomic<int> g_total{0};
|
||||
std::atomic<int> g_done{0};
|
||||
Crafter::Progress::Verbosity g_verbosity = Crafter::Progress::Verbosity::Default;
|
||||
bool g_isTty = false;
|
||||
bool g_lineDirty = false; // status line has uncleared content
|
||||
bool g_finalized = false;
|
||||
std::chrono::steady_clock::time_point g_startTime = std::chrono::steady_clock::now();
|
||||
std::mutex StateMutex;
|
||||
std::atomic<std::int32_t> Total{0};
|
||||
std::atomic<std::int32_t> Done{0};
|
||||
Crafter::Progress::Verbosity ActiveVerbosity = Crafter::Progress::Verbosity::Default;
|
||||
bool IsTty = false;
|
||||
bool LineDirty = false; // status line has uncleared content
|
||||
bool Finalized = false;
|
||||
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
|
||||
|
||||
int TerminalWidth() {
|
||||
std::int32_t TerminalWidth() {
|
||||
#if defined(_WIN32)
|
||||
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||
if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &info)) {
|
||||
int w = info.srWindow.Right - info.srWindow.Left + 1;
|
||||
std::int32_t w = info.srWindow.Right - info.srWindow.Left + 1;
|
||||
if (w > 0) return w;
|
||||
}
|
||||
#else
|
||||
|
|
@ -46,21 +46,21 @@ namespace {
|
|||
}
|
||||
#endif
|
||||
if (const char* col = std::getenv("COLUMNS")) {
|
||||
try { int c = std::stoi(col); if (c > 0) return c; } catch (...) {}
|
||||
try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {}
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
// Caller holds g_mutex.
|
||||
// Caller holds StateMutex.
|
||||
void RenderStatus(std::string_view label) {
|
||||
if (g_verbosity != Crafter::Progress::Verbosity::Default || !g_isTty) return;
|
||||
int done = g_done.load(std::memory_order_relaxed);
|
||||
int total = g_total.load(std::memory_order_relaxed);
|
||||
if (ActiveVerbosity != Crafter::Progress::Verbosity::Default || !IsTty) return;
|
||||
std::int32_t done = Done.load(std::memory_order_relaxed);
|
||||
std::int32_t total = Total.load(std::memory_order_relaxed);
|
||||
std::string prefix = std::format("[{}/{}] ", done, total);
|
||||
int width = TerminalWidth();
|
||||
int avail = width - static_cast<int>(prefix.size()) - 1;
|
||||
std::int32_t width = TerminalWidth();
|
||||
std::int32_t avail = width - static_cast<std::int32_t>(prefix.size()) - 1;
|
||||
std::string trimmed{label};
|
||||
if (avail > 0 && static_cast<int>(trimmed.size()) > avail) {
|
||||
if (avail > 0 && static_cast<std::int32_t>(trimmed.size()) > avail) {
|
||||
trimmed.resize(static_cast<std::size_t>(avail));
|
||||
}
|
||||
// \r returns to col 0, \033[2K erases the whole line. No newline.
|
||||
|
|
@ -68,15 +68,15 @@ namespace {
|
|||
std::fputs(prefix.c_str(), stdout);
|
||||
std::fputs(trimmed.c_str(), stdout);
|
||||
std::fflush(stdout);
|
||||
g_lineDirty = true;
|
||||
LineDirty = true;
|
||||
}
|
||||
|
||||
// Caller holds g_mutex.
|
||||
// Caller holds StateMutex.
|
||||
void ClearLineLocked() {
|
||||
if (g_lineDirty) {
|
||||
if (LineDirty) {
|
||||
std::fputs("\r\033[2K", stdout);
|
||||
std::fflush(stdout);
|
||||
g_lineDirty = false;
|
||||
LineDirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -84,66 +84,64 @@ namespace {
|
|||
namespace Crafter::Progress {
|
||||
|
||||
void SetVerbosity(Verbosity v) {
|
||||
std::lock_guard lock(g_mutex);
|
||||
g_verbosity = v;
|
||||
g_isTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0;
|
||||
g_startTime = std::chrono::steady_clock::now();
|
||||
g_total.store(0);
|
||||
g_done.store(0);
|
||||
g_finalized = false;
|
||||
std::lock_guard lock(StateMutex);
|
||||
ActiveVerbosity = v;
|
||||
IsTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0;
|
||||
StartTime = std::chrono::steady_clock::now();
|
||||
Total.store(0);
|
||||
Done.store(0);
|
||||
Finalized = false;
|
||||
}
|
||||
|
||||
Verbosity GetVerbosity() {
|
||||
std::lock_guard lock(g_mutex);
|
||||
return g_verbosity;
|
||||
std::lock_guard lock(StateMutex);
|
||||
return ActiveVerbosity;
|
||||
}
|
||||
|
||||
Task::Task(std::string label) : label_(std::move(label)) {
|
||||
g_total.fetch_add(1, std::memory_order_relaxed);
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (g_verbosity == Verbosity::Default && g_isTty) {
|
||||
Total.fetch_add(1, std::memory_order_relaxed);
|
||||
std::lock_guard lock(StateMutex);
|
||||
if (ActiveVerbosity == Verbosity::Default && IsTty) {
|
||||
RenderStatus(label_);
|
||||
}
|
||||
}
|
||||
|
||||
Task::~Task() {
|
||||
int done = g_done.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (g_verbosity == Verbosity::Default) {
|
||||
if (g_isTty) {
|
||||
std::int32_t done = Done.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
std::lock_guard lock(StateMutex);
|
||||
if (ActiveVerbosity == Verbosity::Default) {
|
||||
if (IsTty) {
|
||||
RenderStatus(label_);
|
||||
} else {
|
||||
// Non-TTY: one append-only line per completed task (ninja-style).
|
||||
int total = g_total.load(std::memory_order_relaxed);
|
||||
std::int32_t total = Total.load(std::memory_order_relaxed);
|
||||
std::println("[{}/{}] {}", done, total, label_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EchoCommand(std::string_view command) {
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (g_verbosity == Verbosity::Verbose) {
|
||||
std::lock_guard lock(StateMutex);
|
||||
if (ActiveVerbosity == Verbosity::Verbose) {
|
||||
std::println("$ {}", command);
|
||||
}
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
std::lock_guard lock(g_mutex);
|
||||
std::lock_guard lock(StateMutex);
|
||||
ClearLineLocked();
|
||||
}
|
||||
|
||||
void Finalize() {
|
||||
std::lock_guard lock(g_mutex);
|
||||
if (g_finalized) return;
|
||||
g_finalized = true;
|
||||
std::lock_guard lock(StateMutex);
|
||||
if (Finalized) return;
|
||||
Finalized = true;
|
||||
ClearLineLocked();
|
||||
if (g_verbosity == Verbosity::Quiet) return;
|
||||
int done = g_done.load();
|
||||
if (ActiveVerbosity == Verbosity::Quiet) return;
|
||||
std::int32_t done = Done.load();
|
||||
if (done == 0) return; // Nothing happened (cached build); stay silent.
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - g_startTime);
|
||||
std::println("Built {} step{} in {}ms",
|
||||
done, done == 1 ? "" : "s", elapsed.count());
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - StartTime);
|
||||
std::println("Built {} step{} in {}ms", done, done == 1 ? "" : "s", elapsed.count());
|
||||
}
|
||||
|
||||
} // namespace Crafter::Progress
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include "SPIRV/GlslangToSpv.h"
|
||||
|
|
@ -72,11 +72,11 @@ namespace Crafter {
|
|||
|
||||
std::string pathStr = path.string();
|
||||
const char* file_name_list[1] = { pathStr.c_str() };
|
||||
const char* shader_source = src.data();
|
||||
const int shader_source_len = static_cast<int>(src.size());
|
||||
const char* shaderSource = src.data();
|
||||
const std::int32_t shaderSourceLen = static_cast<std::int32_t>(src.size());
|
||||
|
||||
glslang::TShader shader(glslangType);
|
||||
shader.setStringsWithLengthsAndNames(&shader_source, &shader_source_len, file_name_list, 1);
|
||||
shader.setStringsWithLengthsAndNames(&shaderSource, &shaderSourceLen, file_name_list, 1);
|
||||
shader.setEntryPoint(entrypoint.c_str());
|
||||
shader.setSourceEntryPoint(entrypoint.c_str());
|
||||
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_4);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
export module Crafter.Build:Test_impl;
|
||||
|
|
@ -13,41 +13,15 @@ using namespace Crafter;
|
|||
|
||||
namespace {
|
||||
bool TargetIsWindows(std::string_view target) {
|
||||
return target.find("windows") != std::string_view::npos
|
||||
|| target.find("mingw") != std::string_view::npos;
|
||||
return target.find("windows") != std::string_view::npos || target.find("mingw") != std::string_view::npos;
|
||||
}
|
||||
|
||||
fs::path TestBinaryPath(const Configuration& cfg) {
|
||||
fs::path outputDir = cfg.BinDir();
|
||||
return outputDir / (TargetIsWindows(cfg.target) ? cfg.outputName + ".exe" : cfg.outputName);
|
||||
return outputDir / (TargetIsWindows(cfg.target) ? std::format("{}.exe", cfg.outputName) : cfg.outputName);
|
||||
}
|
||||
|
||||
bool MatchGlob(std::string_view glob, std::string_view name) {
|
||||
std::size_t gi = 0, ni = 0, star = std::string_view::npos, mark = 0;
|
||||
while (ni < name.size()) {
|
||||
if (gi < glob.size() && (glob[gi] == '?' || glob[gi] == name[ni])) {
|
||||
++gi; ++ni;
|
||||
} else if (gi < glob.size() && glob[gi] == '*') {
|
||||
star = gi++;
|
||||
mark = ni;
|
||||
} else if (star != std::string_view::npos) {
|
||||
gi = star + 1;
|
||||
ni = ++mark;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
while (gi < glob.size() && glob[gi] == '*') ++gi;
|
||||
return gi == glob.size();
|
||||
}
|
||||
|
||||
bool MatchAny(std::span<const std::string> globs, std::string_view name) {
|
||||
if (globs.empty()) return true;
|
||||
for (const auto& g : globs) {
|
||||
if (MatchGlob(g, name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// MatchGlob/MatchAny live in :Platform — shared with the lint verb.
|
||||
|
||||
std::string ShellQuoteSh(std::string_view s) {
|
||||
std::string out;
|
||||
|
|
@ -118,7 +92,7 @@ namespace {
|
|||
return out;
|
||||
}
|
||||
|
||||
std::string SignalName(int sig) {
|
||||
std::string SignalName(std::int32_t sig) {
|
||||
switch (sig) {
|
||||
case 1: return "SIGHUP";
|
||||
case 2: return "SIGINT";
|
||||
|
|
@ -139,7 +113,7 @@ namespace {
|
|||
std::error_code ec;
|
||||
fs::create_directories(logDir, ec);
|
||||
if (ec) return;
|
||||
std::ofstream(logDir / (name + ".log")) << output;
|
||||
std::ofstream(logDir / (std::format("{}.log", name))) << output;
|
||||
}
|
||||
|
||||
void PrintResult(const TestResult& r, std::string_view runnerName) {
|
||||
|
|
@ -174,19 +148,16 @@ namespace {
|
|||
std::println("⏱ {}{} ({}ms) timeout", r.name, runnerSuffix, ms);
|
||||
break;
|
||||
case TestOutcome::Skipped:
|
||||
std::println("⏭ {}{} skipped: {}", r.name, runnerSuffix,
|
||||
r.output.empty() ? std::string("(no reason)") : r.output);
|
||||
std::println("⏭ {}{} skipped: {}", r.name, runnerSuffix, r.output.empty() ? std::string("(no reason)") : r.output);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::atomic<Configuration*> g_parentProject{nullptr};
|
||||
std::atomic<Configuration*> ParentProject{nullptr};
|
||||
|
||||
Configuration* FindLibInTree(Configuration* root,
|
||||
std::string_view name,
|
||||
std::unordered_set<Configuration*>& seen) {
|
||||
Configuration* FindLibInTree(Configuration* root, std::string_view name, std::unordered_set<Configuration*>& seen) {
|
||||
if (!seen.insert(root).second) return nullptr;
|
||||
if (root->name == name) return root;
|
||||
for (Configuration* dep : root->dependencies) {
|
||||
|
|
@ -197,20 +168,17 @@ namespace {
|
|||
}
|
||||
|
||||
void Crafter::SetParentProject(Configuration* parent) {
|
||||
g_parentProject.store(parent);
|
||||
ParentProject.store(parent);
|
||||
}
|
||||
|
||||
Configuration* Crafter::ParentLib(std::string_view name) {
|
||||
Configuration* root = g_parentProject.load();
|
||||
Configuration* root = ParentProject.load();
|
||||
if (!root) {
|
||||
throw std::runtime_error(std::format(
|
||||
"Crafter::ParentLib('{}'): no parent project set", name));
|
||||
throw std::runtime_error(std::format("Crafter::ParentLib('{}'): no parent project set", name));
|
||||
}
|
||||
std::unordered_set<Configuration*> seen;
|
||||
if (auto found = FindLibInTree(root, name, seen)) return found;
|
||||
throw std::runtime_error(std::format(
|
||||
"Crafter::ParentLib('{}'): not found in parent project '{}'",
|
||||
name, root->name));
|
||||
throw std::runtime_error(std::format("Crafter::ParentLib('{}'): not found in parent project '{}'", name, root->name));
|
||||
}
|
||||
|
||||
TestRunner TestRunner::Local() {
|
||||
|
|
@ -286,9 +254,7 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
|
|||
// emulated loader then searches ITS default paths against the
|
||||
// host filesystem, picking up host-arch libraries from /lib —
|
||||
// LD_LIBRARY_PATH steers it back into the sysroot.
|
||||
r.exec = std::format(
|
||||
"env QEMU_LD_PREFIX={0} LD_LIBRARY_PATH={0}/lib:{0}/usr/lib {1}",
|
||||
cfg.sysroot, r.exec);
|
||||
r.exec = std::format("env QEMU_LD_PREFIX={0} LD_LIBRARY_PATH={0}/lib:{0}/usr/lib {1}", cfg.sysroot, r.exec);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
|
@ -317,9 +283,7 @@ namespace {
|
|||
if (spec.starts_with("cmd:") && spec.size() > 4) {
|
||||
return TestRunner::Cmd(std::string(spec.substr(4)));
|
||||
}
|
||||
throw std::runtime_error(std::format(
|
||||
"TestRunner::FromSpec: unrecognized runner spec '{}' "
|
||||
"(expected 'local' or 'cmd:<binary>')", spec));
|
||||
throw std::runtime_error(std::format("TestRunner::FromSpec: unrecognized runner spec '{}' " "(expected 'local' or 'cmd:<binary>')", spec));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,8 +375,7 @@ namespace {
|
|||
return {false, std::format("env '{}' unset", arg)};
|
||||
}
|
||||
} else {
|
||||
return {false, std::format(
|
||||
"unknown require kind '{}' (expected tool/file/env)", kind)};
|
||||
return {false, std::format("unknown require kind '{}' (expected tool/file/env)", kind)};
|
||||
}
|
||||
}
|
||||
return {true, ""};
|
||||
|
|
@ -479,9 +442,7 @@ TestBuilder Configuration::AddTest(std::string_view name, std::span<fs::path> in
|
|||
return TestBuilder{this, tests.size() - 1};
|
||||
}
|
||||
|
||||
void Configuration::AddMarchVariants(std::string_view name,
|
||||
std::span<fs::path> interfaces,
|
||||
std::span<const MarchTier> tiers) {
|
||||
void Configuration::AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const MarchTier> tiers) {
|
||||
for (const auto& tier : tiers) {
|
||||
Test t;
|
||||
t.config.path = "./";
|
||||
|
|
@ -507,25 +468,25 @@ void Configuration::AddMarchVariants(std::string_view name,
|
|||
}
|
||||
}
|
||||
|
||||
TestBuilder& TestBuilder::Path(fs::path p) { test().config.path = std::move(p); return *this; }
|
||||
TestBuilder& TestBuilder::Target(std::string t) { test().config.target = std::move(t); return *this; }
|
||||
TestBuilder& TestBuilder::March(std::string m) { test().config.march = std::move(m); return *this; }
|
||||
TestBuilder& TestBuilder::Mtune(std::string m) { test().config.mtune = std::move(m); return *this; }
|
||||
TestBuilder& TestBuilder::Sysroot(fs::path s) { test().config.sysroot = s.string(); return *this; }
|
||||
TestBuilder& TestBuilder::Debug(bool d) { test().config.debug = d; return *this; }
|
||||
TestBuilder& TestBuilder::Path(fs::path p) { Ref().config.path = std::move(p); return *this; }
|
||||
TestBuilder& TestBuilder::Target(std::string t) { Ref().config.target = std::move(t); return *this; }
|
||||
TestBuilder& TestBuilder::March(std::string m) { Ref().config.march = std::move(m); return *this; }
|
||||
TestBuilder& TestBuilder::Mtune(std::string m) { Ref().config.mtune = std::move(m); return *this; }
|
||||
TestBuilder& TestBuilder::Sysroot(fs::path s) { Ref().config.sysroot = s.string(); return *this; }
|
||||
TestBuilder& TestBuilder::Debug(bool d) { Ref().config.debug = d; return *this; }
|
||||
TestBuilder& TestBuilder::Define(std::string n, std::string v) {
|
||||
test().config.defines.push_back({std::move(n), std::move(v)});
|
||||
Ref().config.defines.push_back({std::move(n), std::move(v)});
|
||||
return *this;
|
||||
}
|
||||
TestBuilder& TestBuilder::Timeout(std::chrono::seconds s) { test().timeout = s; return *this; }
|
||||
TestBuilder& TestBuilder::Args(std::vector<std::string> a) { test().args = std::move(a); return *this; }
|
||||
TestBuilder& TestBuilder::Requires(std::string r) { test().requires_.push_back(std::move(r)); return *this; }
|
||||
TestBuilder& TestBuilder::Timeout(std::chrono::seconds s) { Ref().timeout = s; return *this; }
|
||||
TestBuilder& TestBuilder::Args(std::vector<std::string> a) { Ref().args = std::move(a); return *this; }
|
||||
TestBuilder& TestBuilder::Requires(std::string r) { Ref().requires_.push_back(std::move(r)); return *this; }
|
||||
TestBuilder& TestBuilder::Dependencies(std::vector<Configuration*> d) {
|
||||
test().config.dependencies = std::move(d);
|
||||
Ref().config.dependencies = std::move(d);
|
||||
return *this;
|
||||
}
|
||||
TestBuilder& TestBuilder::LinkFlag(std::string f) { test().config.linkFlags.push_back(std::move(f)); return *this; }
|
||||
TestBuilder& TestBuilder::CompileFlag(std::string f) { test().config.compileFlags.push_back(std::move(f)); return *this; }
|
||||
TestBuilder& TestBuilder::LinkFlag(std::string f) { Ref().config.linkFlags.push_back(std::move(f)); return *this; }
|
||||
TestBuilder& TestBuilder::CompileFlag(std::string f) { Ref().config.compileFlags.push_back(std::move(f)); return *this; }
|
||||
|
||||
TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs) {
|
||||
// Multi-target sweep: when no --target= was given, the run covers every
|
||||
|
|
@ -590,10 +551,10 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
|
|||
return summary;
|
||||
}
|
||||
|
||||
int jobs = opts.jobs > 0
|
||||
std::int32_t jobs = opts.jobs > 0
|
||||
? opts.jobs
|
||||
: std::max(1u, std::thread::hardware_concurrency());
|
||||
jobs = std::min(jobs, static_cast<int>(filtered.size()));
|
||||
jobs = std::min(jobs, static_cast<std::int32_t>(filtered.size()));
|
||||
|
||||
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
||||
std::mutex depMutex;
|
||||
|
|
@ -651,10 +612,7 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
|
|||
if (!tool.empty() && !RequiresMentionsTool(t.requires_, tool)) {
|
||||
r.outcome = TestOutcome::Fail;
|
||||
r.exitCode = -1;
|
||||
r.output = std::format(
|
||||
"runner '{}' unavailable and not declared in requires "
|
||||
"(add .Requires(\"tool:{}\") to permit skipping)",
|
||||
t.runner.name, tool);
|
||||
r.output = std::format("runner '{}' unavailable and not declared in requires " "(add .Requires(\"tool:{}\") to permit skipping)", t.runner.name, tool);
|
||||
} else {
|
||||
r.outcome = TestOutcome::Skipped;
|
||||
r.output = std::format("runner '{}' not available", t.runner.name);
|
||||
|
|
@ -718,7 +676,7 @@ TestSummary Crafter::RunTests(Configuration& projectCfg, const RunTestsOptions&
|
|||
|
||||
std::vector<std::jthread> threads;
|
||||
threads.reserve(jobs);
|
||||
for (int j = 0; j < jobs; ++j) {
|
||||
for (std::int32_t j = 0; j < jobs; ++j) {
|
||||
threads.emplace_back(worker);
|
||||
}
|
||||
threads.clear(); // joins all jthreads
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//SPDX-License-Identifier: LGPL-3.0-only
|
||||
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
#include <cstdio>
|
||||
#if defined(_WIN32)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue