V2: WASI, -r flag, CI pipeline, examples & tests cleanup
Some checks failed
CI / build-test-release (pull_request) Failing after 44s
Some checks failed
CI / build-test-release (pull_request) Failing after 44s
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cdfdb976c8
commit
eaee502e8c
102 changed files with 2211 additions and 686 deletions
|
|
@ -197,6 +197,40 @@ void Configuration::GetInterfacesAndImplementations(std::span<fs::path> interfac
|
|||
}
|
||||
|
||||
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
|
||||
// Reset per-build cached state on every Module/ModulePartition so that
|
||||
// successive Build() calls on the same Configuration re-evaluate mtimes
|
||||
// (incremental-rebuild test scenarios). Walks cfg.dependencies recursively
|
||||
// with a seen-set so diamond deps don't loop.
|
||||
{
|
||||
std::unordered_set<Configuration*> resetSeen;
|
||||
std::function<void(Configuration*)> reset = [&](Configuration* c) {
|
||||
if (!resetSeen.insert(c).second) return;
|
||||
for (auto& iface : c->interfaces) {
|
||||
iface->checked = false;
|
||||
iface->compiled.store(false);
|
||||
for (auto& part : iface->partitions) {
|
||||
part->checked = false;
|
||||
part->compiled.store(false);
|
||||
}
|
||||
}
|
||||
for (Configuration* dep : c->dependencies) {
|
||||
reset(dep);
|
||||
}
|
||||
};
|
||||
reset(&config);
|
||||
}
|
||||
|
||||
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
|
||||
// and the main compile command see the same value. Linux-only — Windows
|
||||
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
|
||||
// wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
|
||||
// subdirs handle the differences.
|
||||
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
||||
config.sysroot = "/usr/share/wasi-sysroot";
|
||||
}
|
||||
#endif
|
||||
|
||||
fs::path buildDir = config.path/"build"/std::format("{}-{}-{}", config.name, config.target, config.march);
|
||||
fs::path outputDir = config.path/"bin"/std::format("{}-{}-{}", config.name, config.target, config.march);
|
||||
|
||||
|
|
@ -287,7 +321,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
for (std::size_t i = 0; i < config.externalDependencies.size(); ++i) {
|
||||
externalThreads.emplace_back([&, i]() {
|
||||
if (buildCancelled.load(std::memory_order_relaxed)) return;
|
||||
externalResults[i] = BuildExternal(config.externalDependencies[i], buildCancelled);
|
||||
externalResults[i] = BuildExternal(config.externalDependencies[i], config.target, buildCancelled);
|
||||
if (!externalResults[i].error.empty()) {
|
||||
bool expected = false;
|
||||
if (buildCancelled.compare_exchange_strong(expected, true)) {
|
||||
|
|
@ -324,11 +358,31 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
std::string editedTarget = config.target;
|
||||
std::replace(editedTarget.begin(), editedTarget.end(), '-', '_');
|
||||
|
||||
std::string command = std::format("{} --target={} -march={} -mtune={} -std=c++26 -D CRAFTER_BUILD_CONFIGURATION_TARGET=\\\"{}\\\" -D CRAFTER_BUILD_CONFIGURATION_TARGET_{} -fprebuilt-module-path={} -fprebuilt-module-path={}", GetBaseCommand(config), config.target, config.march, config.mtune, editedTarget, editedTarget, stdPcmDir.string(), pcmDir.string());
|
||||
// wasm32 targets reject -march and silently ignore -mtune (clang errors on
|
||||
// the former). Skip both for any wasm32-* triple.
|
||||
bool isWasm = config.target.starts_with("wasm32");
|
||||
std::string archFlags = isWasm
|
||||
? std::string()
|
||||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||
std::string command = std::format("{} --target={}{} -std=c++26 -D CRAFTER_BUILD_CONFIGURATION_TARGET=\\\"{}\\\" -D CRAFTER_BUILD_CONFIGURATION_TARGET_{} -fprebuilt-module-path={} -fprebuilt-module-path={}", GetBaseCommand(config), config.target, archFlags, editedTarget, editedTarget, stdPcmDir.string(), pcmDir.string());
|
||||
|
||||
if (!config.sysroot.empty()) {
|
||||
command += std::format(" --sysroot={}", config.sysroot);
|
||||
}
|
||||
if (config.target.starts_with("wasm32")) {
|
||||
// -mllvm is consumed by codegen but not the link driver, which is the
|
||||
// same command line; quiet the unused-flag warning rather than split
|
||||
// compile and link commands.
|
||||
command += " -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument";
|
||||
}
|
||||
if (config.target == "x86_64-w64-mingw32") {
|
||||
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
|
||||
// -femulated-tls clang generates native-TLS references that don't
|
||||
// match. Symptom: undefined std::__once_callable / __once_call at
|
||||
// link time. Also -Wno-unused… because -femulated-tls is a codegen
|
||||
// flag the link driver doesn't consume.
|
||||
command += " -femulated-tls -Wno-unused-command-line-argument";
|
||||
}
|
||||
|
||||
if(config.type == ConfigurationType::LibraryDynamic) {
|
||||
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||
|
|
@ -565,6 +619,61 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
for(const std::string& flag : buildResult.libs) {
|
||||
linkExtras += " " + flag;
|
||||
}
|
||||
// mingw uses libstdc++; C++26 std::print/format extras live in libstdc++exp.
|
||||
// libstdc++ on mingw uses winpthreads for std::atomic_wait /
|
||||
// counting_semaphore / stop_token, so -lpthread is required as soon as
|
||||
// those primitives appear (they do, transitively, in any non-trivial std
|
||||
// import). -static-libstdc++ bundles libstdc++ into the exe so we don't
|
||||
// chase libstdc++-6.dll TLS symbol mismatches across mingw versions and
|
||||
// the resulting binary stands alone. Auto-link so user projects don't
|
||||
// carry boilerplate.
|
||||
if (config.target == "x86_64-w64-mingw32") {
|
||||
// mingw runtime DLLs (libstdc++-6.dll, libgcc_s_seh-1.dll,
|
||||
// libwinpthread-1.dll) get auto-copied next to the .exe by the
|
||||
// post-build step below, so dynamic linkage is fine. -lstdc++exp =
|
||||
// C++26 std::print/format extras; -lpthread = winpthreads symbols
|
||||
// that libstdc++ uses for std::atomic_wait, counting_semaphore,
|
||||
// stop_token. clang++ adds the main -lstdc++ implicitly.
|
||||
linkExtras += " -lstdc++exp -lpthread";
|
||||
}
|
||||
|
||||
// Force a relink if the expected output is missing or older than any dep
|
||||
// artifact. Missing covers: previous build produced a different outputName,
|
||||
// or the binary was deleted by hand. Older-than-dep covers: dep's library
|
||||
// was rebuilt by an earlier run (so dep.repack is false this time around)
|
||||
// but the consumer was never relinked against the new dep.
|
||||
{
|
||||
auto expectedOutputFor = [](const Configuration& c) -> fs::path {
|
||||
fs::path dir = c.path/"bin"/std::format("{}-{}-{}", c.name, c.target, c.march);
|
||||
if (c.type == ConfigurationType::Executable) {
|
||||
if (c.target.starts_with("wasm32"))
|
||||
return dir / (c.outputName + ".wasm");
|
||||
return c.target == "x86_64-w64-mingw32"
|
||||
? dir / (c.outputName + ".exe")
|
||||
: dir / c.outputName;
|
||||
}
|
||||
if (c.type == ConfigurationType::LibraryStatic) {
|
||||
return c.target == "x86_64-w64-mingw32" || c.target == "x86_64-pc-windows-msvc"
|
||||
? dir / std::format("{}.lib", c.outputName)
|
||||
: dir / std::format("lib{}.a", c.outputName);
|
||||
}
|
||||
return dir / std::format("lib{}.so", c.outputName);
|
||||
};
|
||||
|
||||
fs::path expected = expectedOutputFor(config);
|
||||
if (!fs::exists(expected)) {
|
||||
buildResult.repack = true;
|
||||
} else {
|
||||
auto consumerMtime = fs::last_write_time(expected);
|
||||
for (Configuration* dep : config.dependencies) {
|
||||
fs::path depArtifact = expectedOutputFor(*dep);
|
||||
if (fs::exists(depArtifact) && fs::last_write_time(depArtifact) > consumerMtime) {
|
||||
buildResult.repack = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(buildResult.repack) {
|
||||
if(config.type == ConfigurationType::Executable) {
|
||||
|
|
@ -589,7 +698,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
return {e.what(), false, {}};
|
||||
}
|
||||
}
|
||||
buildResult.result = RunCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||
if (config.target.starts_with("wasm32")) {
|
||||
buildResult.result = RunCommand(std::format("{}{} -o {}.wasm -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||
} else {
|
||||
buildResult.result = RunCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
|
|
@ -616,6 +729,33 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
|||
|
||||
return buildResult;
|
||||
}
|
||||
|
||||
void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
||||
fs::path runtimeDir = GetCrafterBuildHome() / "wasi-runtime";
|
||||
fs::path runtimeJs = runtimeDir / "runtime.js";
|
||||
fs::path htmlTemplate = runtimeDir / "index.html.in";
|
||||
if (!fs::exists(runtimeJs) || !fs::exists(htmlTemplate)) {
|
||||
throw std::runtime_error(std::format(
|
||||
"wasi-runtime assets missing under {} (set CRAFTER_BUILD_HOME or reinstall)",
|
||||
runtimeDir.string()));
|
||||
}
|
||||
|
||||
fs::path htmlOutDir = cfg.path / "build" / "wasi-runtime" / cfg.name;
|
||||
fs::create_directories(htmlOutDir);
|
||||
fs::path htmlPath = htmlOutDir / "index.html";
|
||||
|
||||
std::ifstream in(htmlTemplate);
|
||||
std::stringstream buf;
|
||||
buf << in.rdbuf();
|
||||
std::string html = std::regex_replace(buf.str(), std::regex(R"(\{\{WASM\}\})"), cfg.outputName + ".wasm");
|
||||
std::ofstream out(htmlPath);
|
||||
out << html;
|
||||
out.close();
|
||||
|
||||
cfg.files.push_back(runtimeJs);
|
||||
cfg.files.push_back(htmlPath);
|
||||
}
|
||||
|
||||
int Crafter::Run(int argc, char** argv) {
|
||||
try {
|
||||
fs::path projectFile = "./project.cpp";
|
||||
|
|
@ -623,12 +763,15 @@ int Crafter::Run(int argc, char** argv) {
|
|||
projectArgs.reserve(argc);
|
||||
|
||||
bool runTests = false;
|
||||
bool runAfterBuild = false;
|
||||
RunTestsOptions testOpts;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
std::string_view arg = argv[i];
|
||||
if (arg == "test") {
|
||||
runTests = true;
|
||||
} else if (arg == "-r") {
|
||||
runAfterBuild = true;
|
||||
} else if (arg.starts_with("--project=")) {
|
||||
projectFile = arg.substr(std::string_view("--project=").size());
|
||||
} else if (runTests && arg.starts_with("--jobs=")) {
|
||||
|
|
@ -637,6 +780,8 @@ int Crafter::Run(int argc, char** argv) {
|
|||
testOpts.timeoutOverride = std::chrono::seconds(std::stoi(std::string(arg.substr(std::string_view("--timeout=").size()))));
|
||||
} else if (runTests && arg == "--list") {
|
||||
testOpts.listOnly = true;
|
||||
} else if (runTests && arg.starts_with("--runner=")) {
|
||||
testOpts.runnerOverride = std::string(arg.substr(std::string_view("--runner=").size()));
|
||||
} else if (runTests && !arg.starts_with("-")) {
|
||||
testOpts.globs.emplace_back(arg);
|
||||
} else {
|
||||
|
|
@ -644,15 +789,25 @@ int Crafter::Run(int argc, char** argv) {
|
|||
}
|
||||
}
|
||||
|
||||
// The test run is target-scoped: only tests whose cfg.target equals
|
||||
// testOpts.targetFilter are included. Default = host triple, so a
|
||||
// bare `crafter-build test` runs everything that targets host.
|
||||
for (auto& a : projectArgs) {
|
||||
if (a.starts_with("--target=")) {
|
||||
testOpts.targetFilter = std::string(a.substr(std::string_view("--target=").size()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs::exists(projectFile)) {
|
||||
std::println(std::cerr, "No project file at {}", projectFile.string());
|
||||
return 1;
|
||||
}
|
||||
|
||||
Configuration config = LoadProject(projectFile, projectArgs);
|
||||
SetParentProject(&config);
|
||||
|
||||
if (runTests) {
|
||||
TestSummary summary = RunTests(config, testOpts);
|
||||
TestSummary summary = RunTests(config, testOpts, projectArgs);
|
||||
return summary.AllPassed() ? 0 : 1;
|
||||
}
|
||||
|
||||
|
|
@ -665,6 +820,21 @@ int Crafter::Run(int argc, char** argv) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
if (runAfterBuild) {
|
||||
if (config.type != ConfigurationType::Executable) {
|
||||
std::println(std::cerr, "-r: cannot run a library");
|
||||
return 1;
|
||||
}
|
||||
fs::path dir = config.path / "bin" / std::format("{}-{}-{}", config.name, config.target, config.march);
|
||||
fs::path artifact = dir / config.outputName;
|
||||
if (config.target.starts_with("wasm32")) {
|
||||
artifact += ".wasm";
|
||||
} else if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
|
||||
artifact += ".exe";
|
||||
}
|
||||
return std::system(artifact.string().c_str()) == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "{}", e.what());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue