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
|
|
@ -35,6 +35,22 @@ import :Clang;
|
|||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
fs::path Crafter::GetCrafterBuildHome() {
|
||||
if (const char* envHome = std::getenv("CRAFTER_BUILD_HOME")) {
|
||||
return fs::path(envHome);
|
||||
}
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
char buf[MAX_PATH];
|
||||
DWORD len = GetModuleFileNameA(nullptr, buf, MAX_PATH);
|
||||
if (len == 0 || len == MAX_PATH) {
|
||||
throw std::runtime_error("GetModuleFileName failed");
|
||||
}
|
||||
fs::path hostExe(std::string(buf, len));
|
||||
#else
|
||||
fs::path hostExe = fs::read_symlink("/proc/self/exe");
|
||||
#endif
|
||||
return hostExe.parent_path().parent_path() / "share" / "crafter-build";
|
||||
}
|
||||
|
||||
#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) {
|
||||
|
|
@ -76,8 +92,103 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|||
return result;
|
||||
}
|
||||
|
||||
CommandResult Crafter::RunCommandWithTimeout(std::string_view, std::chrono::seconds) {
|
||||
throw std::runtime_error("RunCommandWithTimeout not yet implemented on Windows");
|
||||
CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) {
|
||||
CommandResult result{};
|
||||
|
||||
SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, TRUE };
|
||||
HANDLE readEnd = nullptr, writeEnd = nullptr;
|
||||
if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) {
|
||||
throw std::runtime_error("CreatePipe failed");
|
||||
}
|
||||
SetHandleInformation(readEnd, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
// KILL_ON_JOB_CLOSE so the cmd.exe wrapper plus whatever it spawned
|
||||
// (the test binary, ssh, etc.) all die when the job handle goes away —
|
||||
// that's how we enforce the timeout reliably across the whole tree.
|
||||
HANDLE job = CreateJobObjectA(nullptr, nullptr);
|
||||
if (!job) {
|
||||
CloseHandle(readEnd);
|
||||
CloseHandle(writeEnd);
|
||||
throw std::runtime_error("CreateJobObject failed");
|
||||
}
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli{};
|
||||
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(job, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli));
|
||||
|
||||
STARTUPINFOA si{};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdOutput = writeEnd;
|
||||
si.hStdError = writeEnd;
|
||||
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
// cmd /C "...": gives users the same shell-syntax surface (redirects,
|
||||
// &&, ||) 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::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);
|
||||
if (!ok) {
|
||||
CloseHandle(readEnd);
|
||||
CloseHandle(writeEnd);
|
||||
CloseHandle(job);
|
||||
throw std::runtime_error("CreateProcess failed");
|
||||
}
|
||||
|
||||
AssignProcessToJobObject(job, pi.hProcess);
|
||||
ResumeThread(pi.hThread);
|
||||
// Drop our writer ref so the read end sees EOF once the child (its
|
||||
// own dup of writeEnd) exits.
|
||||
CloseHandle(writeEnd);
|
||||
|
||||
std::string output;
|
||||
std::jthread reader([&output, readEnd]() {
|
||||
char buf[4096];
|
||||
DWORD n = 0;
|
||||
while (ReadFile(readEnd, buf, sizeof(buf), &n, nullptr) && n > 0) {
|
||||
output.append(buf, n);
|
||||
}
|
||||
});
|
||||
|
||||
DWORD waitMs = static_cast<DWORD>(std::min<long long>(
|
||||
static_cast<long long>(timeout.count()) * 1000LL,
|
||||
static_cast<long long>(INFINITE) - 1));
|
||||
DWORD waitResult = WaitForSingleObject(pi.hProcess, waitMs);
|
||||
|
||||
if (waitResult == WAIT_TIMEOUT) {
|
||||
TerminateJobObject(job, 1);
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
result.timedOut = true;
|
||||
result.exitCode = 124;
|
||||
} else {
|
||||
DWORD exit = 0;
|
||||
GetExitCodeProcess(pi.hProcess, &exit);
|
||||
// NTSTATUS error severity (top two bits set) means the process
|
||||
// terminated by exception — STATUS_ACCESS_VIOLATION 0xC0000005,
|
||||
// STATUS_STACK_OVERFLOW 0xC00000FD, etc. Surface those as crashes
|
||||
// so the runner shows 💥 instead of a numeric exit code.
|
||||
if ((exit & 0xC0000000) == 0xC0000000) {
|
||||
result.crashed = true;
|
||||
result.signal = static_cast<int>(exit);
|
||||
}
|
||||
result.exitCode = static_cast<int>(exit);
|
||||
}
|
||||
|
||||
reader.join();
|
||||
CloseHandle(readEnd);
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(job);
|
||||
|
||||
result.output = std::move(output);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||
|
|
@ -155,10 +266,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
}
|
||||
fs::path hostExe(std::string(hostExeBuf, hostExeLen));
|
||||
|
||||
const char* envHome = std::getenv("CRAFTER_BUILD_HOME");
|
||||
fs::path sourceDir = envHome
|
||||
? fs::path(envHome)
|
||||
: hostExe.parent_path().parent_path() / "share" / "crafter-build";
|
||||
fs::path sourceDir = Crafter::GetCrafterBuildHome();
|
||||
|
||||
Configuration hostConfig;
|
||||
hostConfig.target = "x86_64-pc-windows-msvc";
|
||||
|
|
@ -318,19 +426,37 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|||
fs::path stdCc = fs::path(std::format("/usr/x86_64-w64-mingw32/include/c++/{}/bits/std.cc", mingWversion));
|
||||
|
||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) {
|
||||
return RunCommand(std::format("cp {} {}/std.cppm\nclang++ --target={} -march={} -mtune={} -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {}/std.cppm -o {}", stdCc.string(), stdPcm.parent_path().string(), config.target, config.march, config.mtune, stdPcm.parent_path().string(), stdPcm.string()));
|
||||
// -femulated-tls keeps PCM TLS access matching libstdc++'s emutls
|
||||
// definitions; mismatch surfaces as undefined std::__once_callable.
|
||||
return RunCommand(std::format("cp {} {}/std.cppm\nclang++ --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {}/std.cppm -o {}", stdCc.string(), stdPcm.parent_path().string(), config.target, config.march, config.mtune, stdPcm.parent_path().string(), stdPcm.string()));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} else {
|
||||
std::string stdCppm = config.sysroot.empty()
|
||||
? std::string("/usr/share/libc++/v1/std.cppm")
|
||||
: std::format("{}/usr/share/libc++/v1/std.cppm", config.sysroot);
|
||||
bool isWasm = config.target.starts_with("wasm32");
|
||||
// wasi-sdk drops std.cppm at <sysroot>/share/libc++/v1/, the rest of
|
||||
// the libc++ ecosystem (e.g. /opt/aarch64-rootfs) follows FHS at
|
||||
// <sysroot>/usr/share/libc++/v1/.
|
||||
std::string stdCppm;
|
||||
if (isWasm) {
|
||||
stdCppm = std::format("{}/share/libc++/v1/std.cppm", config.sysroot);
|
||||
} else if (config.sysroot.empty()) {
|
||||
stdCppm = "/usr/share/libc++/v1/std.cppm";
|
||||
} else {
|
||||
stdCppm = std::format("{}/usr/share/libc++/v1/std.cppm", config.sysroot);
|
||||
}
|
||||
std::string sysrootFlag = config.sysroot.empty()
|
||||
? std::string()
|
||||
: std::format(" --sysroot={}", config.sysroot);
|
||||
// wasm32 rejects -march. wasi-libc++ headers require these baselines
|
||||
// to compile: setjmp.h needs -mllvm -wasm-enable-sjlj (lowered to wasm
|
||||
// EH); signal.h requires the emulation define; and EH itself isn't
|
||||
// wired up so -fno-exceptions stays.
|
||||
std::string archFlags = isWasm
|
||||
? std::string(" -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL")
|
||||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
||||
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{} -march={} -mtune={} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, config.march, config.mtune, stdCppm, stdPcm.string()));
|
||||
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -406,10 +532,7 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
|||
|
||||
fs::path hostExe = fs::read_symlink("/proc/self/exe");
|
||||
|
||||
const char* envHome = std::getenv("CRAFTER_BUILD_HOME");
|
||||
fs::path sourceDir = envHome
|
||||
? fs::path(envHome)
|
||||
: hostExe.parent_path().parent_path() / "share" / "crafter-build";
|
||||
fs::path sourceDir = Crafter::GetCrafterBuildHome();
|
||||
|
||||
Configuration hostConfig;
|
||||
hostConfig.target = "x86_64-pc-linux-gnu";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue