linting
Some checks failed
CI / build-test-release (push) Failing after 7m15s

This commit is contained in:
Jorijn van der Graaf 2026-07-23 01:24:42 +02:00
commit 8892154b28
70 changed files with 2780 additions and 596 deletions

View file

@ -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, &ltoCompileFlags]() {
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;
}