Crafter.Build/implementations/Crafter.Build-Clang.cpp

1625 lines
80 KiB
C++
Raw Normal View History

2026-04-23 01:57:25 +02:00
/*
Crafter® Build
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
2026-05-19 00:50:06 +02:00
module;
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
#include <winsock2.h>
#include <ws2tcpip.h>
// <rpc.h> (pulled in transitively) defines `interface` as a macro for `struct`,
// which collides with local variables named `interface` in this TU.
#undef interface
2026-05-19 00:50:06 +02:00
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
2026-04-23 01:57:25 +02:00
export module Crafter.Build:Clang_impl;
import std;
import :Clang;
2026-04-27 07:04:42 +02:00
import :Platform;
import :Test;
2026-04-29 03:27:11 +02:00
import :Progress;
2026-05-12 01:16:40 +02:00
import :Asset;
2026-04-23 01:57:25 +02:00
namespace fs = std::filesystem;
2026-04-27 07:04:42 +02:00
using namespace Crafter;
2026-04-23 01:57:25 +02:00
2026-04-27 07:04:42 +02:00
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 {
for(const std::unique_ptr<Module>& interface : this->interfaces) {
if(interface->name == importName) {
localDeps.push_back(interface.get());
return true;
}
}
std::unordered_set<Configuration*> seen;
std::function<bool(Configuration*)> walk = [&](Configuration* depCfg) -> bool {
if (!seen.insert(depCfg).second) return false;
2026-04-27 07:04:42 +02:00
for(const std::unique_ptr<Module>& depInterface : depCfg->interfaces) {
if(depInterface->name == importName) {
fs::path depPcmPath = (depCfg->PcmDir() / depInterface->path.filename()).string() + ".pcm";
externalDeps.emplace_back(depInterface.get(), std::move(depPcmPath));
return true;
}
}
for(Configuration* sub : depCfg->dependencies) {
if (walk(sub)) return true;
}
return false;
};
for(Configuration* depCfg : this->dependencies) {
if (walk(depCfg)) return true;
2026-04-27 07:04:42 +02:00
}
return false;
};
std::vector<std::tuple<fs::path, std::string, ModulePartition*, Module*>> tempModulePaths = std::vector<std::tuple<fs::path, std::string, ModulePartition*, Module*>>(interfaces.size());
for(std::uint16_t i = 0; i < interfaces.size(); i++){
2026-04-30 02:20:19 +02:00
// Resolve to absolute now so the stored path survives cwd changes
// (matters for GitProject deps loaded from a different working dir).
fs::path file = fs::absolute(path / interfaces[i]).lexically_normal();
2026-04-27 07:04:42 +02:00
file += ".cppm";
std::ifstream t(file);
std::stringstream buffer;
buffer << t.rdbuf();
std::string fileContent = buffer.str();
fileContent = std::regex_replace(fileContent, std::regex(R"(//[^\n]*)"), "");
fileContent = std::regex_replace(fileContent, std::regex(R"(/\*.*?\*/)"), "");
tempModulePaths[i] = {file, fileContent, nullptr, nullptr};
}
std::erase_if(tempModulePaths, [this](std::tuple<fs::path, std::string, ModulePartition*, Module*>& file) {
std::smatch match;
if (std::regex_search(std::get<1>(file), match, std::regex(R"(export module ([a-zA-Z0-9_\.\-]+);)"))) {
std::get<0>(file).replace_extension("");
this->interfaces.push_back(std::make_unique<Module>(std::move(match[1].str()), std::move(std::get<0>(file))));
return true;
} else {
return false;
}
});
for(std::uint16_t i = 0; i < tempModulePaths.size(); i++) {
std::smatch match;
if (std::regex_search(std::get<1>(tempModulePaths[i]), match, std::regex(R"(export module ([a-zA-Z_0-9\.\-]+):([a-zA-Z_0-9\.\-]+);)"))) {
for(const std::unique_ptr<Module>& modulee : this->interfaces) {
if(modulee->name == match[1]) {
std::string name = match[2].str();
fs::path pthCpy = std::get<0>(tempModulePaths[i]);
pthCpy.replace_extension("");
std::unique_ptr<ModulePartition> partition = std::make_unique<ModulePartition>(std::move(name), std::move(pthCpy));
std::get<2>(tempModulePaths[i]) = partition.get();
modulee->partitions.push_back(std::move(partition));
std::get<3>(tempModulePaths[i]) = modulee.get();
goto next;
}
}
throw std::runtime_error(std::format("Module {} not found, referenced in {}", match[1].str(), std::get<0>(tempModulePaths[i]).string()));
} else {
throw std::runtime_error(std::format("No module declaration found in {}", std::get<0>(tempModulePaths[i]).string()));
}
next:;
}
for(std::tuple<fs::path, std::string, ModulePartition*, Module*>& file : tempModulePaths) {
ModulePartition* partition = std::get<2>(file);
Module* parentModule = std::get<3>(file);
const std::string& fileContent = std::get<1>(file);
std::regex partitionPattern(R"(import :([a-zA-Z_\-0-9\.]+);)");
std::sregex_iterator currentMatch(fileContent.begin(), fileContent.end(), partitionPattern);
std::sregex_iterator lastMatch;
while (currentMatch != lastMatch) {
std::smatch match = *currentMatch;
for(std::unique_ptr<ModulePartition>& sibling : parentModule->partitions) {
if(sibling->name == match[1]) {
partition->partitionDependencies.push_back(sibling.get());
goto next2;
}
}
throw std::runtime_error(std::format("imported partition {}:{} not found, referenced in {}", parentModule->name, match[1].str(), std::get<0>(file).string()));
next2: ++currentMatch;
}
std::regex modulePattern(R"(import ([a-zA-Z_0-9\.\-]+);)");
std::sregex_iterator modCurrent(fileContent.begin(), fileContent.end(), modulePattern);
while (modCurrent != lastMatch) {
std::smatch match = *modCurrent;
resolveImport(match[1].str(), partition->moduleDependencies, partition->externalModuleDependencies);
++modCurrent;
}
}
for(const fs::path& tempFile : implementations) {
2026-04-30 02:20:19 +02:00
fs::path file = fs::absolute(path / tempFile).lexically_normal();
2026-04-27 07:04:42 +02:00
file += ".cpp";
std::ifstream t(file);
std::stringstream buffer;
buffer << t.rdbuf();
std::string fileContent = buffer.str();
fileContent = std::regex_replace(fileContent, std::regex(R"(//[^\n]*)"), "");
fileContent = std::regex_replace(fileContent, std::regex(R"(/\*.*?\*/)"), "");
std::smatch match;
fs::path fileCopy = file;
fileCopy.replace_extension("");
Implementation& implementation = this->implementations.emplace_back(std::move(fileCopy));
if (std::regex_search(fileContent, match, std::regex(R"(module ([a-zA-Z0-9_\.\-]+)(:[a-zA-Z0-9_\.\-]+)?\s*;)"))) {
bool isPartitionImpl = match[2].length() > 0;
for(const std::unique_ptr<Module>& interface : this->interfaces) {
if(interface->name == match[1]) {
if (!isPartitionImpl) {
implementation.moduleDependencies.push_back(interface.get());
}
std::regex partitionPattern(R"(import :([a-zA-Z_\-0-9\.]+);)");
std::sregex_iterator currentMatch(fileContent.begin(), fileContent.end(), partitionPattern);
std::sregex_iterator lastMatch;
while (currentMatch != lastMatch) {
std::smatch match2 = *currentMatch;
for(const std::unique_ptr<ModulePartition>& partition : interface->partitions) {
if(partition->name == match2[1]) {
implementation.partitionDependencies.push_back(partition.get());
goto next3;
}
}
throw std::runtime_error(std::format("imported partition {}:{} not found, referenced in {}", match[1].str(), match2[1].str(), file.string()));
next3: ++currentMatch;
}
std::regex modulePattern(R"(import ([a-zA-Z_0-9\.\-]+);)");
std::sregex_iterator modCurrent(fileContent.begin(), fileContent.end(), modulePattern);
while (modCurrent != lastMatch) {
std::smatch match2 = *modCurrent;
if (match2[1] != match[1]) {
resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies);
}
++modCurrent;
}
goto next4;
}
}
throw std::runtime_error(std::format("Module {} not found not found, referenced in {}", match[1].str(), file.string()));
next4:;
} else {
std::regex pattern(R"(import ([a-zA-Z_\-0-9\.]+);)");
std::sregex_iterator currentMatch(fileContent.begin(), fileContent.end(), pattern);
std::sregex_iterator lastMatch;
while (currentMatch != lastMatch) {
std::smatch match2 = *currentMatch;
resolveImport(match2[1].str(), implementation.moduleDependencies, implementation.externalModuleDependencies);
++currentMatch;
}
}
}
}
BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex) {
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
// Reset per-build cached state on every Module/ModulePartition so that
// successive Build() calls on the same Configuration re-evaluate mtimes
fix: scope per-build module-state reset to the config being built Build() resets each Module/ModulePartition's per-build `compiled`/`checked` flags so a reused Configuration re-evaluates mtimes. That reset recursed into cfg.dependencies — but dependency Configurations are shared across the build DAG and each is compiled concurrently by its own Build() call. A parent/sibling's recursive reset could therefore clear a shared dependency's module `compiled` atomic *after* that dependency's module-compile thread had set it true and exited, but before an intra-config waiter (its impl, or a dependent partition) ran compiled.wait(false). The waiter then blocked forever on a flag nothing would re-signal: the build froze mid-compile, idle, with no compiler process alive — exactly the hang in issue #16. Reset only the current configuration's own modules. Every config in the tree already gets its own Build() call (the per-PcmDir builder registered in depResults), which resets its own state at the top of that call, sequenced before its compile threads spawn. Cross-config module state is consulted only via PCM file mtimes and the depResults futures, never via these flags, so the narrower reset is correct and removes the data race entirely. Adds ConcurrentDependencyReset: builds a static-lib dependency fully, then builds a consumer that depends on it while the dependency is already cached in depResults (so it is never rebuilt), and asserts the consumer build leaves the dependency's module `compiled` flag intact. Fails deterministically on the old recursive reset; passes with the fix. Resolves #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:32:46 +00:00
// (incremental-rebuild test scenarios).
//
// Reset ONLY this configuration's own modules — never recurse into
// dependencies. Configuration objects are shared across the dependency DAG
// (diamond deps point at the same Configuration*), and every config in the
// tree gets its own Build() call: the per-PcmDir builder registered in
// depResults resets its own state here, at the top of its own Build(),
// sequenced before that config's compile threads spawn. Recursing into
// dependencies from here would re-clear a shared dependency's `compiled`
// atomic from a parent/sibling thread *while that dependency's own Build()
// is concurrently compiling it* — after its module-compile thread set the
// flag true and exited but before its impl/partition waiter ran
// compiled.wait(false). The waiter would then block forever on a flag
// nothing re-signals: the build freezes mid-compile, idle, with no compiler
// process alive (issue #16). Cross-config module state is consulted only via
// PCM file mtimes and the depResults futures, never via these flags, so the
// narrower reset is correct.
for (auto& iface : config.interfaces) {
iface->checked = false;
iface->compiled.store(false);
for (auto& part : iface->partitions) {
part->checked = false;
part->compiled.store(false);
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
}
// 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
2026-04-30 02:20:19 +02:00
fs::path buildDir = config.BuildDir();
fs::path outputDir = config.BinDir();
2026-04-23 01:57:25 +02:00
if (!fs::exists(buildDir)) {
fs::create_directories(buildDir);
}
if (!fs::exists(outputDir)) {
fs::create_directories(outputDir);
}
2026-05-19 16:53:24 +02:00
BuildResult buildResult{};
2026-04-27 07:04:42 +02:00
2026-05-02 21:08:51 +02:00
// glslang #include search paths for every shader compiled in this
// configuration: each transitive (incl. self) buildFiles entry's parent
// dir, or the entry itself if it points at a directory. Collected once
// up front so the per-shader threads can capture the resulting span by
// reference. No file copy involved — glslang reads the includes in
// place from the dep's source tree.
std::vector<fs::path> shaderIncludeDirs;
{
std::unordered_set<std::string> seenDirs;
std::unordered_set<const Configuration*> seenCfg;
std::function<void(const Configuration*)> collect = [&](const Configuration* c) {
if (!seenCfg.insert(c).second) return;
for (const fs::path& bf : c->buildFiles) {
fs::path dir = fs::is_directory(bf) ? bf : bf.parent_path();
if (seenDirs.insert(dir.string()).second) {
shaderIncludeDirs.push_back(std::move(dir));
}
}
for (const Configuration* sub : c->dependencies) collect(sub);
};
collect(&config);
}
2026-04-23 01:57:25 +02:00
std::vector<std::thread> threads;
threads.reserve(config.shaders.size() + 1 + config.interfaces.size() + config.implementations.size());
std::string buildError;
std::atomic<bool> buildCancelled{false};
for (const Shader& shader : config.shaders) {
if (shader.Check(outputDir)) continue;
2026-05-02 21:08:51 +02:00
threads.emplace_back([&shader, &outputDir, &shaderIncludeDirs, &buildError, &buildCancelled]() {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Compiling shader {}", shader.path.filename().string()));
2026-04-23 01:57:25 +02:00
if (buildCancelled.load(std::memory_order_relaxed)) return;
2026-05-02 21:08:51 +02:00
std::string result = shader.Compile(outputDir, shaderIncludeDirs);
2026-04-23 01:57:25 +02:00
if (result.empty()) return;
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
});
}
2026-05-12 03:44:14 +02:00
// Asset compilation: each cfg.assets entry is either a single .png/.obj
// file (flat output: outputDir/<filename>.ctex/cmesh — preserves the
// original behavior) or a directory (recursed, with the relative tree
// mirrored under outputDir/<dirname>/; .png/.obj are compressed, every
// other file is copied through unchanged). Directory mode lets mod/map
// trees keep their nested layout so mod.json paths like
// "cannon/base.cmesh" resolve correctly at runtime.
// Skipped per-file if the output is newer than the source. Each
// compress runs in its own thread; passthrough copies are bundled into
// a single thread to match the cfg.files pattern.
auto compressedName = [](const fs::path& src) -> std::optional<fs::path> {
std::string ext = src.extension().string();
2026-05-19 00:50:06 +02:00
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
// stb_image (used by CompressAsset → TextureAsset::LoadPNG) handles
// png/tga/jpg/bmp; all map to .ctex.
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
return fs::path(src.filename()).replace_extension(".ctex");
}
2026-05-12 03:44:14 +02:00
if (ext == ".obj") return fs::path(src.filename()).replace_extension(".cmesh");
2026-05-12 01:16:40 +02:00
return std::nullopt;
};
2026-05-12 03:44:14 +02:00
auto submitCompress = [&](fs::path sourcePath, fs::path outRelative) {
fs::path out = outputDir / outRelative;
if (fs::exists(out) && fs::exists(sourcePath) && fs::last_write_time(sourcePath) <= fs::last_write_time(out)) return;
threads.emplace_back([sourcePath = std::move(sourcePath), out = std::move(out), &buildError, &buildCancelled]() {
Progress::Task task(std::format("Compressing asset {}", sourcePath.filename().string()));
2026-05-12 01:16:40 +02:00
if (buildCancelled.load(std::memory_order_relaxed)) return;
2026-05-12 03:44:14 +02:00
std::error_code ec;
fs::create_directories(out.parent_path(), ec);
std::string result = CompressAsset(sourcePath, out);
2026-05-12 01:16:40 +02:00
if (result.empty()) return;
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
});
2026-05-12 03:44:14 +02:00
};
std::vector<std::pair<fs::path, fs::path>> assetPassthroughs;
for (const fs::path& asset : config.assets) {
if (fs::is_directory(asset)) {
fs::path topName = asset.filename();
for (const auto& entry : fs::recursive_directory_iterator(asset)) {
if (!entry.is_regular_file()) continue;
fs::path rel = fs::relative(entry.path(), asset);
if (std::optional<fs::path> compName = compressedName(entry.path())) {
submitCompress(entry.path(), topName / rel.parent_path() / *compName);
} else {
assetPassthroughs.emplace_back(entry.path(), topName / rel);
}
}
} else {
std::optional<fs::path> outName = compressedName(asset);
if (!outName) {
buildCancelled.store(true);
2026-05-19 00:50:06 +02:00
buildError = std::format("{}: unsupported asset extension (expected .png/.tga/.jpg/.bmp/.obj, or a directory)", asset.string());
2026-05-12 03:44:14 +02:00
break;
}
submitCompress(asset, *outName);
}
}
if (!assetPassthroughs.empty()) {
threads.emplace_back([passthroughs = std::move(assetPassthroughs), &outputDir, &buildCancelled, &buildError]() {
Progress::Task task("Copying asset passthrough files");
if (buildCancelled.load(std::memory_order_relaxed)) return;
try {
for (const auto& [src, rel] : passthroughs) {
fs::path dst = outputDir / rel;
fs::create_directories(dst.parent_path());
if (!fs::exists(dst)) {
fs::copy_file(src, dst);
} else if (fs::last_write_time(src) > fs::last_write_time(dst)) {
fs::copy_file(src, dst, fs::copy_options::overwrite_existing);
}
}
} catch (const std::exception& e) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = e.what();
}
}
});
2026-05-12 01:16:40 +02:00
}
2026-04-23 01:57:25 +02:00
threads.emplace_back([&config, &outputDir, &buildCancelled, &buildError]() {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Copying files for {}", config.name));
2026-04-23 01:57:25 +02:00
if (buildCancelled.load(std::memory_order_relaxed)) return;
try {
2026-05-02 21:08:51 +02:00
for (const fs::path& additionalFile : config.files) {
2026-04-23 01:57:25 +02:00
fs::path destination = outputDir / additionalFile.filename();
if (fs::is_directory(additionalFile)) {
for (const auto& entry : fs::recursive_directory_iterator(additionalFile)) {
const fs::path& sourcePath = entry.path();
fs::path relativePath = fs::relative(sourcePath, additionalFile);
fs::path destPath = destination / relativePath;
if (entry.is_directory()) {
2026-05-02 21:08:51 +02:00
if (!fs::exists(destPath)) fs::create_directories(destPath);
2026-04-23 01:57:25 +02:00
} else if (entry.is_regular_file()) {
fs::create_directories(destPath.parent_path());
if (!fs::exists(destPath)) {
fs::copy_file(sourcePath, destPath);
2026-05-02 21:08:51 +02:00
} else if (fs::last_write_time(sourcePath) > fs::last_write_time(destPath)) {
2026-04-23 01:57:25 +02:00
fs::copy_file(sourcePath, destPath, fs::copy_options::overwrite_existing);
}
}
}
} else {
if (!fs::exists(destination)) {
fs::copy_file(additionalFile, destination);
2026-05-02 21:08:51 +02:00
} else if (fs::last_write_time(additionalFile) > fs::last_write_time(destination)) {
2026-04-23 01:57:25 +02:00
fs::copy_file(additionalFile, destination, fs::copy_options::overwrite_existing);
}
}
}
} catch(std::exception& e) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = e.what();
}
}
});
2026-04-27 07:04:42 +02:00
std::vector<ExternalBuildResult> externalResults(config.externalDependencies.size());
std::vector<std::thread> externalThreads;
externalThreads.reserve(config.externalDependencies.size());
for (std::size_t i = 0; i < config.externalDependencies.size(); ++i) {
externalThreads.emplace_back([&, i]() {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Building external dep {}", config.externalDependencies[i].name));
2026-04-27 07:04:42 +02:00
if (buildCancelled.load(std::memory_order_relaxed)) return;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
externalResults[i] = BuildExternal(config.externalDependencies[i], config.target, buildCancelled);
2026-04-27 07:04:42 +02:00
if (!externalResults[i].error.empty()) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = externalResults[i].error;
}
}
});
}
fs::path stdPcmDir = GetCacheDir()/(config.target+"-"+config.march);
2026-04-23 01:57:25 +02:00
if (!fs::exists(stdPcmDir)) {
fs::create_directories(stdPcmDir);
}
2026-04-29 03:27:11 +02:00
std::string stdPcmResult;
{
Progress::Task task(std::format("Building std PCM ({}-{})", config.target, config.march));
stdPcmResult = BuildStdPcm(config, stdPcmDir/"std.pcm");
}
2026-04-23 01:57:25 +02:00
if(!stdPcmResult.empty()) {
2026-04-27 07:04:42 +02:00
buildCancelled.store(true);
for(std::thread& thread : threads) thread.join();
for(std::thread& thread : externalThreads) thread.join();
return {stdPcmResult, false, {}};
2026-04-23 01:57:25 +02:00
}
fs::path pcmDir;
if(config.type != ConfigurationType::Executable) {
pcmDir = outputDir;
} else {
pcmDir = buildDir;
}
2026-04-27 07:04:42 +02:00
fs::copy_file(stdPcmDir/"std.pcm", pcmDir/"std.pcm", fs::copy_options::update_existing);
2026-04-23 01:57:25 +02:00
2026-04-27 07:04:42 +02:00
std::string editedTarget = config.target;
std::replace(editedTarget.begin(), editedTarget.end(), '-', '_');
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
// 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());
2026-04-27 07:04:42 +02:00
if (!config.sysroot.empty()) {
command += std::format(" --sysroot={}", config.sysroot);
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
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.
2026-05-18 05:23:11 +02:00
command += " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument";
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
}
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";
}
2026-04-27 07:04:42 +02:00
if(config.type == ConfigurationType::LibraryDynamic) {
2026-04-23 01:57:25 +02:00
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
#endif
2026-04-27 07:04:42 +02:00
} else if(config.type == ConfigurationType::Executable) {
2026-04-23 01:57:25 +02:00
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// On Windows targets the API uses __declspec(dllimport) when consuming
// a DLL. Set the macro for executables so CRAFTER_API resolves to
// dllimport in their PCM cache (separate from the lib's PCM cache,
// which gets dllexport). Harmless if the exe doesn't actually link a
// crafter DLL — CRAFTER_API only matters at API call sites.
if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
command += " -D CRAFTER_BUILD_DLL_IMPORT";
}
2026-04-27 07:04:42 +02:00
} else {
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
2026-04-23 01:57:25 +02:00
}
std::string files;
std::unordered_set<std::string> libSet;
2026-05-01 19:02:14 +02:00
std::unordered_set<std::string> publicFlagSet;
2026-04-23 01:57:25 +02:00
std::mutex fileMutex;
std::vector<std::thread> depThreads;
2026-04-27 07:04:42 +02:00
depThreads.reserve(config.dependencies.size());
2026-04-23 01:57:25 +02:00
std::atomic<bool> repack(false);
2026-05-12 01:16:40 +02:00
// -I propagation that's valid for both C and C++ compiles. Module-only
// bits (-fprebuilt-module-path) stay on `command` only.
std::string includeFlags;
{
std::unordered_set<Configuration*> seen;
std::function<void(Configuration*)> addFlags = [&](Configuration* dep) {
if (!seen.insert(dep).second) return;
for (const auto& entry : fs::recursive_directory_iterator(dep->path)) {
if (entry.is_directory() && entry.path().filename() == "include") {
2026-05-12 01:16:40 +02:00
includeFlags += " -I" + entry.path().string();
}
2026-04-23 01:57:25 +02:00
}
2026-05-12 01:16:40 +02:00
includeFlags += std::format(" -I{}", dep->path.string());
command += std::format(" -fprebuilt-module-path={}", dep->PcmDir().string());
for (Configuration* sub : dep->dependencies) {
addFlags(sub);
}
};
for (Configuration* dep : config.dependencies) {
addFlags(dep);
2026-04-23 01:57:25 +02:00
}
}
2026-05-12 01:16:40 +02:00
command += includeFlags;
2026-04-23 01:57:25 +02:00
for(Configuration* dep : config.dependencies) {
2026-04-27 07:04:42 +02:00
depThreads.emplace_back([&, dep](){
try {
if (buildCancelled.load(std::memory_order_relaxed)) return;
std::shared_ptr<std::promise<BuildResult>> promise;
std::shared_future<BuildResult> resultFuture;
bool isBuilder = false;
2026-04-23 01:57:25 +02:00
depMutex.lock();
fs::path cacheKey = dep->PcmDir();
auto it = depResults.find(cacheKey);
2026-04-27 07:04:42 +02:00
if (it == depResults.end()) {
isBuilder = true;
promise = std::make_shared<std::promise<BuildResult>>();
resultFuture = promise->get_future().share();
depResults.emplace(cacheKey, resultFuture);
2026-04-23 01:57:25 +02:00
} else {
2026-04-27 07:04:42 +02:00
resultFuture = it->second;
}
depMutex.unlock();
if (isBuilder) {
BuildResult built;
try {
built = Build(*dep, depResults, depMutex);
} catch (...) {
promise->set_exception(std::current_exception());
throw;
}
promise->set_value(std::move(built));
}
const BuildResult& result = resultFuture.get();
fileMutex.lock();
for (const std::string& lib : result.libs) libSet.insert(lib);
2026-05-01 19:02:14 +02:00
for (const std::string& f : result.publicCompileFlags) publicFlagSet.insert(f);
2026-04-27 07:04:42 +02:00
fileMutex.unlock();
if (!result.result.empty()) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = result.result;
}
}
if (result.repack) {
repack = true;
}
} catch (const std::exception& e) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::format("dep build for {} threw: {}", dep->path.string(), e.what());
2026-04-23 01:57:25 +02:00
}
}
2026-04-27 07:04:42 +02:00
});
2026-04-23 01:57:25 +02:00
}
2026-05-12 01:16:40 +02:00
// Defines belong on both C and C++ compiles so vendored C dependencies
// can see configuration-level macros consistently with module sources.
std::string defineFlags;
2026-04-23 01:57:25 +02:00
for(const Define& define : config.defines) {
if(define.value.empty()) {
2026-05-12 01:16:40 +02:00
defineFlags += std::format(" -D {}", define.name);
2026-04-23 01:57:25 +02:00
} else {
2026-05-12 01:16:40 +02:00
defineFlags += std::format(" -D {}={}", define.name, define.value);
2026-04-23 01:57:25 +02:00
}
}
2026-05-12 01:16:40 +02:00
command += defineFlags;
2026-04-23 01:57:25 +02:00
2026-05-12 01:16:40 +02:00
// Track caller-provided compileFlags separately so the .c compile can
// pick them up too (vendored C deps usually need -I from this set).
std::string userFlags;
2026-04-27 07:04:42 +02:00
for(const std::string& flag : config.compileFlags) {
2026-05-12 01:16:40 +02:00
userFlags += " " + flag;
2026-04-27 07:04:42 +02:00
}
2026-05-12 01:16:40 +02:00
command += userFlags;
2026-04-27 07:04:42 +02:00
2026-04-23 01:57:25 +02:00
std::string cmakeBuildType;
if(config.debug) {
cmakeBuildType = "Debug";
command += " -g -D CRAFTER_BUILD_CONFIGURATION_DEBUG";
} else {
cmakeBuildType = "Release";
command += " -O3";
}
2026-05-18 05:23:11 +02:00
// Same target-aware setup as the C++ compile path (line 459-): wasm32
// rejects -march, silently ignores -mtune, and needs --sysroot to find
// wasi-libc headers. Build the prefix once so all C compiles share it.
const bool cIsWasm = config.target.starts_with("wasm32");
std::string cArchFlags = cIsWasm
? std::string()
: std::format(" -march={} -mtune={}", config.march, config.mtune);
if (!config.sysroot.empty()) {
cArchFlags += std::format(" --sysroot={}", config.sysroot);
}
if (cIsWasm) {
// Matches the C++ path's wasi flag set so any libc shim defines
// (e.g. _WASI_EMULATED_SIGNAL → signal.h shims) are visible to
// C dependencies that drag in signal.h transitively.
cArchFlags += " -D_WASI_EMULATED_SIGNAL";
}
2026-04-27 07:04:42 +02:00
for (const fs::path& cFile : config.cFiles) {
2026-04-23 01:57:25 +02:00
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";
2026-04-30 02:20:19 +02:00
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) {
2026-05-18 05:23:11 +02:00
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config, &includeFlags, &defineFlags, &userFlags, &cArchFlags]() {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Compiling {}.c", cFile.filename().string()));
2026-04-23 01:57:25 +02:00
if (buildCancelled.load(std::memory_order_relaxed)) return;
2026-05-18 05:23:11 +02:00
std::string result = RunCommand(std::format("clang {}.c --target={}{} -O3 -c{}{}{} -o {}_source.o", cFile.string(), config.target, cArchFlags, includeFlags, defineFlags, userFlags, (buildDir / cFile.filename()).string()));
2026-04-23 01:57:25 +02:00
if (result.empty()) return;
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
});
}
}
for (const fs::path& cFile : config.cuda) {
files += std::format(" {}_source.o ", (buildDir / cFile.filename()).string());
const std::string objPath = (buildDir / cFile.filename()).string() + "_source.o";
const std::string srcPath = cFile.string() + ".cu";
2026-04-30 02:20:19 +02:00
if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) {
2026-04-23 01:57:25 +02:00
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled]() {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Compiling {}.cu", cFile.filename().string()));
2026-04-23 01:57:25 +02:00
if (buildCancelled.load(std::memory_order_relaxed)) return;
std::string result = RunCommand(std::format("nvcc {}.cu -c -o {}_source.o -O3 -arch=sm_89", cFile.string(), (buildDir / cFile.filename()).string()));
if (result.empty()) return;
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
});
}
}
for(std::thread& thread : depThreads) {
thread.join();
}
2026-04-27 07:04:42 +02:00
for(std::thread& thread : externalThreads) {
thread.join();
}
2026-04-23 01:57:25 +02:00
if(buildCancelled.load()) {
2026-04-27 07:04:42 +02:00
for(std::thread& thread : threads) thread.join();
2026-04-23 01:57:25 +02:00
return {buildError, false, {}};
}
2026-05-02 21:08:51 +02:00
// Ship runtime artifacts from transitive deps' bin dirs alongside the
// executable: compiled .spv files (cfg.shaders) and asset files/dirs
// (cfg.files). The lib already mirrored these into its own bin dir
// during its build, but a consumer exe loads them from its own dir at
// runtime. Only an exe is a deployment unit; intermediate libs don't
// need to forward since the exe walks all transitive deps. Runs outside
// the repack gate because the relink mtime check above only watches
// .so/.dll/.a, so a shader/file-only change in a dep wouldn't trigger
// repack but still needs the new artifact copied across.
// (cfg.buildFiles use a different mechanism — they're exposed to shader
// compiles as #include search paths in place, no copy.)
2026-05-01 19:16:13 +02:00
if (config.type == ConfigurationType::Executable) {
try {
2026-05-02 21:08:51 +02:00
auto copyTree = [](const fs::path& src, const fs::path& dest) {
if (fs::is_directory(src)) {
for (const auto& entry : fs::recursive_directory_iterator(src)) {
fs::path rel = fs::relative(entry.path(), src);
fs::path destPath = dest / rel;
if (entry.is_directory()) {
if (!fs::exists(destPath)) fs::create_directories(destPath);
} else if (entry.is_regular_file()) {
fs::create_directories(destPath.parent_path());
if (!fs::exists(destPath)) {
fs::copy_file(entry.path(), destPath);
} else if (fs::last_write_time(entry.path()) > fs::last_write_time(destPath)) {
fs::copy_file(entry.path(), destPath, fs::copy_options::overwrite_existing);
}
}
}
} else {
if (!fs::exists(dest)) {
fs::copy_file(src, dest);
} else if (fs::last_write_time(src) > fs::last_write_time(dest)) {
2026-05-01 19:16:13 +02:00
fs::copy_file(src, dest, fs::copy_options::overwrite_existing);
}
}
};
2026-05-02 21:08:51 +02:00
std::unordered_set<Configuration*> seen;
std::function<void(Configuration*)> forwardDepArtifacts = [&](Configuration* dep) {
if (!seen.insert(dep).second) return;
fs::path depBinDir = dep->BinDir();
for (const Shader& shader : dep->shaders) {
fs::path src = depBinDir / shader.path.filename().replace_extension("spv");
if (!fs::exists(src)) continue;
copyTree(src, outputDir / src.filename());
}
for (const fs::path& additionalFile : dep->files) {
fs::path src = depBinDir / additionalFile.filename();
if (!fs::exists(src)) continue;
copyTree(src, outputDir / additionalFile.filename());
}
2026-05-12 01:16:40 +02:00
for (const fs::path& asset : dep->assets) {
2026-05-12 03:44:14 +02:00
// Directory entry: the dep already mirrored the
// (compressed + passthrough) tree under
// depBinDir/<asset.filename()>/. Forward it wholesale
// so our bin dir gets the same layout.
if (fs::is_directory(asset)) {
fs::path src = depBinDir / asset.filename();
if (!fs::exists(src)) continue;
copyTree(src, outputDir / asset.filename());
continue;
}
2026-05-12 01:16:40 +02:00
std::string ext = asset.extension().string();
2026-05-19 00:50:06 +02:00
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
2026-05-12 01:16:40 +02:00
fs::path srcName = asset.filename();
2026-05-19 00:50:06 +02:00
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
srcName.replace_extension(".ctex");
} else if (ext == ".obj") {
srcName.replace_extension(".cmesh");
} else {
continue;
}
2026-05-12 01:16:40 +02:00
fs::path src = depBinDir / srcName;
if (!fs::exists(src)) continue;
copyTree(src, outputDir / srcName);
}
2026-05-02 21:08:51 +02:00
for (Configuration* sub : dep->dependencies) forwardDepArtifacts(sub);
};
for (Configuration* dep : config.dependencies) forwardDepArtifacts(dep);
2026-05-01 19:16:13 +02:00
} catch (const fs::filesystem_error& e) {
for(std::thread& thread : threads) thread.join();
return {e.what(), false, {}};
}
}
2026-04-27 07:04:42 +02:00
if(repack.load()) {
buildResult.repack = true;
}
buildResult.libs = std::move(libSet);
for(const std::string& flag : config.linkFlags) {
buildResult.libs.insert(flag);
}
2026-05-01 19:02:14 +02:00
// Public compile flags propagated from sub-deps. Add them to this build's
// command so config sees the headers its deps expose, and re-publish them
// so config's own consumers see them transitively.
for (const std::string& flag : publicFlagSet) command += " " + flag;
buildResult.publicCompileFlags = std::move(publicFlagSet);
2026-04-27 07:04:42 +02:00
fs::file_time_type externalFloor = fs::file_time_type::min();
for(const ExternalBuildResult& ext : externalResults) {
for(const std::string& flag : ext.compileFlags) {
command += " " + flag;
2026-05-01 19:02:14 +02:00
// Headers a dep links via ExternalDependency are part of its
// public surface (its modules can include them in declarations
// visible to consumers), so propagate the -I to consumers.
buildResult.publicCompileFlags.insert(flag);
2026-04-27 07:04:42 +02:00
}
for(const std::string& flag : ext.linkFlags) {
buildResult.libs.insert(flag);
}
if (ext.latestArtifact > externalFloor) externalFloor = ext.latestArtifact;
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
for(std::unique_ptr<Module>& interface : config.interfaces) {
if(interface->Check(pcmDir, externalFloor)) {
Module* mod = interface.get();
threads.emplace_back([mod, &command, &pcmDir, &buildDir, &buildCancelled, &buildError]() {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Compiling interface {}", mod->path.filename().string()));
2026-04-27 07:04:42 +02:00
try {
mod->Compile(command, pcmDir, buildDir, buildCancelled, buildError);
} catch (const std::exception& e) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::format("Module::Compile threw: {}", e.what());
}
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
});
2026-04-23 01:57:25 +02:00
buildResult.repack = true;
}
2026-04-27 07:04:42 +02:00
files += std::format(" {}/{}.o", buildDir.string(), interface->path.filename().string());
for(std::unique_ptr<ModulePartition>& part : interface->partitions) {
2026-04-23 01:57:25 +02:00
files += std::format(" {}/{}.o", buildDir.string(), part->path.filename().string());
}
}
2026-04-27 07:04:42 +02:00
for(Implementation& implementation : config.implementations) {
if(implementation.Check(buildDir, pcmDir, externalFloor)) {
2026-04-23 01:57:25 +02:00
buildResult.repack = true;
2026-04-27 07:04:42 +02:00
Implementation* impl = &implementation;
threads.emplace_back([impl, &command, &buildDir, &buildCancelled, &buildError]() {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Compiling {}.cpp", impl->path.filename().string()));
2026-04-27 07:04:42 +02:00
try {
impl->Compile(command, buildDir, buildCancelled, buildError);
} catch (const std::exception& e) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::format("Implementation::Compile threw: {}", e.what());
}
}
});
2026-04-23 01:57:25 +02:00
}
2026-04-27 07:04:42 +02:00
files += std::format(" {}/{}_impl.o", buildDir.string(), implementation.path.filename().string());
2026-04-23 01:57:25 +02:00
}
for(std::thread& thread : threads) {
thread.join();
}
if(buildCancelled.load()) {
return {buildError, false, {}};
}
2026-04-27 07:04:42 +02:00
std::string linkExtras;
for(const std::string& flag : buildResult.libs) {
linkExtras += " " + flag;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
// 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") {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// Static-link libstdc++/libgcc/libwinpthread so the resulting .exe
// or .dll doesn't depend on a specific runtime DLL being on the
// consumer's PATH. The mingw runtime ABI varies subtly between
// distributions (Arch UCRT vs msys2 UCRT vs msys2 MSVCRT) and
// STATUS_ENTRYPOINT_NOT_FOUND at LoadLibrary time is the symptom
// of a mismatch. Static linkage trades binary size for portability.
// The -Bstatic/-Bdynamic bracketing forces -lpthread to resolve
// against libwinpthread.a rather than the import lib; everything
// else (KERNEL32, UCRT) stays dynamic. -lstdc++exp adds C++26
// std::print/format extras. -femulated-tls is already on the
// compile so __once_callable et al resolve in static libstdc++.
linkExtras += " -lstdc++exp -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread -Wl,-Bdynamic";
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
}
// 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 {
2026-04-30 02:20:19 +02:00
fs::path dir = c.BinDir();
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
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);
}
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// LibraryDynamic — point at the .dll on Windows targets so the
// mtime check sees the newly-built DLL; on Unix the .so suffices.
if (c.target == "x86_64-w64-mingw32" || c.target == "x86_64-pc-windows-msvc") {
return dir / std::format("{}.dll", c.outputName);
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
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;
}
}
2026-05-19 16:53:24 +02:00
// Also relink if any .o this archive bundles is newer than the
// archive itself. Covers a build that compiled the .o but never
// reached the link step (interrupt, crash, or a source touched
// after compile but before link): on the next run the .cpp is
// already ≤ .o so Implementation/Module Check returns false and
// nothing else would notice the archive is stale.
if (!buildResult.repack) {
auto objNewer = [&](const fs::path& obj) {
std::error_code ec;
auto t = fs::last_write_time(obj, ec);
return !ec && t > consumerMtime;
};
for (const std::unique_ptr<Module>& iface : config.interfaces) {
if (objNewer(buildDir / (iface->path.filename().string() + ".o"))) {
buildResult.repack = true;
break;
}
bool partHit = false;
for (const std::unique_ptr<ModulePartition>& part : iface->partitions) {
if (objNewer(buildDir / (part->path.filename().string() + ".o"))) {
partHit = true;
break;
}
}
if (partHit) { buildResult.repack = true; break; }
}
if (!buildResult.repack) {
for (const Implementation& impl : config.implementations) {
if (objNewer(buildDir / (impl.path.filename().string() + "_impl.o"))) {
buildResult.repack = true;
break;
}
}
}
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
}
}
2026-04-27 07:04:42 +02:00
2026-04-23 01:57:25 +02:00
if(buildResult.repack) {
2026-04-29 03:27:11 +02:00
Progress::Task task(std::format("Linking {}", config.outputName));
2026-04-27 07:04:42 +02:00
if(config.type == ConfigurationType::Executable) {
2026-04-23 01:57:25 +02:00
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
2026-04-27 07:04:42 +02:00
if(config.target == "x86_64-w64-mingw32") {
try {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// Copy any LibraryDynamic dependency DLLs alongside
// the launcher exe — Windows resolves DLLs from the exe's
// own directory at load time, so this is the simplest
// equivalent of rpath $ORIGIN.
std::unordered_set<Configuration*> dllSeen;
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
if (!dllSeen.insert(dep).second) return;
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
2026-04-30 02:20:19 +02:00
fs::path depDir = dep->BinDir();
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// The DLL itself (Windows resolves it from the
// exe's directory at load time) and the mingw
// import lib (so a downstream `crafter-build.exe`
// can link a fresh project.dll against it without
// hunting through sibling output dirs).
for (auto fname : {std::format("{}.dll", dep->outputName),
std::format("lib{}.dll.a", dep->outputName)}) {
fs::path src = depDir / fname;
if (!fs::exists(src)) continue;
fs::path dest = outputDir / src.filename();
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
fs::copy(src, dest, fs::copy_options::overwrite_existing);
}
2026-04-27 07:04:42 +02:00
}
}
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
for (Configuration* sub : dep->dependencies) copyDepDlls(sub);
};
for (Configuration* dep : config.dependencies) copyDepDlls(dep);
2026-04-27 07:04:42 +02:00
} catch (const fs::filesystem_error& e) {
return {e.what(), false, {}};
}
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
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));
}
2026-04-23 01:57:25 +02:00
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
if (config.target == "x86_64-w64-mingw32") {
// Windows host, mingw target: same shape as the Linux→mingw
// path (no LIBCXX_DIR / -lc++ / -nostdlib++ — those are MSVC
// libc++ flags). Copy LibraryDynamic dep DLLs + import libs
// alongside the launcher exe so Windows resolves them from
// the exe's own directory at load time. Runtime DLLs (libstdc++,
// libgcc, libwinpthread) come from msys2 on PATH.
std::unordered_set<Configuration*> dllSeen;
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
if (!dllSeen.insert(dep).second) return;
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
2026-04-30 02:20:19 +02:00
fs::path depDir = dep->BinDir();
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
for (auto fname : {std::format("{}.dll", dep->outputName),
std::format("lib{}.dll.a", dep->outputName)}) {
fs::path src = depDir / fname;
if (!fs::exists(src)) continue;
fs::path dest = outputDir / src.filename();
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
fs::copy(src, dest, fs::copy_options::overwrite_existing);
}
}
}
for (Configuration* sub : dep->dependencies) copyDepDlls(sub);
};
for (Configuration* dep : config.dependencies) copyDepDlls(dep);
buildResult.result = RunCommand(std::format(
"{}{} -o {} -fuse-ld=lld{}",
command, files, (outputDir/config.outputName).string(), linkExtras));
} else {
std::system(std::format("copy \"%LIBCXX_DIR%\\lib\\c++.dll\" \"{}\\c++.dll\"", outputDir.string()).c_str());
buildResult.result = RunCommand(std::format("{}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR%\\lib -lc++ -nostdinc++ -nostdlib++{}", command, files, (outputDir/config.outputName).string(), linkExtras));
}
2026-04-23 01:57:25 +02:00
#endif
2026-04-27 07:04:42 +02:00
} else if(config.type == ConfigurationType::LibraryStatic) {
2026-04-23 01:57:25 +02:00
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
2026-04-27 07:04:42 +02:00
buildResult.result = RunCommand(std::format("ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
2026-04-23 01:57:25 +02:00
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
2026-04-27 07:04:42 +02:00
buildResult.result = RunCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string()));
2026-04-23 01:57:25 +02:00
#endif
} else {
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// LibraryDynamic. Output names follow each target's convention so
// consumers can link via the standard linker search:
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
// msvc: <name>.dll + <name>.lib (lld /IMPLIB)
// unix: lib<name>.so (rpath $ORIGIN)
if (config.target == "x86_64-w64-mingw32") {
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName);
buildResult.result = RunCommand(std::format(
"{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}",
command, files, dll.string(), implib.string(), linkExtras));
} else if (config.target == "x86_64-pc-windows-msvc") {
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
fs::path implib = outputDir / std::format("{}.lib", config.outputName);
buildResult.result = RunCommand(std::format(
"{}{} -shared -o {} -Wl,/IMPLIB:{} -fuse-ld=lld{}",
command, files, dll.string(), implib.string(), linkExtras));
} else {
buildResult.result = RunCommand(std::format(
"{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}",
command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
}
2026-04-23 01:57:25 +02:00
}
}
2026-04-27 07:04:42 +02:00
if (config.type == ConfigurationType::LibraryStatic || config.type == ConfigurationType::LibraryDynamic) {
buildResult.libs.insert(std::format("-L{}", outputDir.string()));
buildResult.libs.insert(std::format("-l{}", config.outputName));
}
2026-04-23 01:57:25 +02:00
return buildResult;
2026-04-27 07:04:42 +02:00
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
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";
2026-05-18 05:23:11 +02:00
// Walk the dep graph for env-style JS bridges that need to load BEFORE
// runtime.js so they can populate `window.crafter_webbuild_env`. Any
// `*.js` entry in a (transitive) dep's `cfg.files` qualifies — the
// file is already going to be copied into the consumer's bin dir, we
// just need its basename for a `<script src=...>` tag.
std::vector<std::string> envScripts;
std::unordered_set<Configuration*> seen;
std::function<void(Configuration*)> walk = [&](Configuration* c) {
if (!c || !seen.insert(c).second) return;
for (const fs::path& f : c->files) {
if (f.extension() == ".js" && f.filename() != "runtime.js") {
std::string name = f.filename().string();
if (std::find(envScripts.begin(), envScripts.end(), name) == envScripts.end()) {
envScripts.push_back(std::move(name));
}
}
}
for (Configuration* dep : c->dependencies) walk(dep);
};
walk(&cfg);
2026-05-26 22:50:08 +02:00
// Per-build cache-busting token. Stamped onto every script src + the
// wasm URL so a regular browser reload sees fresh files even though
// 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());
2026-05-18 05:23:11 +02:00
std::string envScriptTags;
for (const std::string& name : envScripts) {
2026-05-26 22:50:08 +02:00
envScriptTags += std::format(
" <script src=\"{}?v={}\" type=\"module\"></script>\n",
name, buildId);
2026-05-18 05:23:11 +02:00
}
// Walk the dep graph again for non-JS assets — these get pre-loaded by
// runtime.js into an in-memory VFS so the wasm's std::ifstream et al.
// can actually read them (the wasi-runtime in this repo otherwise
2026-05-19 03:28:27 +02:00
// stubs every fd syscall to zero).
//
// The manifest lists *relative paths* (e.g. "assets/Inter.ttf") so
// runtime.js's fetch() resolves against the bin-dir layout the asset
// copy step actually emits. The VFS is keyed by basename — path_open
// strips to basename on lookup, so subdir layouts collapse on the
// wasm side. Basename collisions across subdirs aren't supported on
// the wasi runtime today; if two assets share a basename, the last
// one preloaded wins. Avoid collisions in the source tree.
auto compressedExt = [](const fs::path& src) -> std::optional<std::string> {
2026-05-19 00:50:06 +02:00
std::string ext = src.extension().string();
for (char& c : ext) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (ext == ".png" || ext == ".tga" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") {
2026-05-19 03:28:27 +02:00
return std::string(".ctex");
2026-05-19 00:50:06 +02:00
}
2026-05-19 03:28:27 +02:00
if (ext == ".obj") return std::string(".cmesh");
return std::nullopt;
};
auto compressedRel = [&](const fs::path& rel) -> fs::path {
if (auto ext = compressedExt(rel)) {
fs::path out = rel;
out.replace_extension(*ext);
return out;
}
return rel;
2026-05-19 00:50:06 +02:00
};
2026-05-18 05:23:11 +02:00
std::vector<std::string> assetFiles;
2026-05-19 03:28:27 +02:00
auto pushUnique = [&](std::string name) {
if (name.empty()) return;
if (std::find(assetFiles.begin(), assetFiles.end(), name) == assetFiles.end()) {
assetFiles.push_back(std::move(name));
}
};
2026-05-18 05:23:11 +02:00
seen.clear();
std::function<void(Configuration*)> walkAssets = [&](Configuration* c) {
if (!c || !seen.insert(c).second) return;
for (const fs::path& f : c->files) {
std::string ext = f.extension().string();
if (ext == ".js" || ext == ".html") continue;
if (f.filename() == "runtime.js") continue;
2026-05-19 03:28:27 +02:00
// cfg.files lands flat next to the .wasm by `name = filename()`.
pushUnique(f.filename().string());
2026-05-18 05:23:11 +02:00
}
2026-05-19 03:28:27 +02:00
// cfg.assets — mirror the bin-dir layout the build emits: a
// directory entry becomes <topName>/<rel inside dir>, single
// files land flat at the bin root. .png/.obj are compressed in
// place; everything else passes through under its original name.
2026-05-19 00:50:06 +02:00
for (const fs::path& a : c->assets) {
if (fs::is_directory(a)) {
2026-05-19 03:28:27 +02:00
const fs::path topName = a.filename();
2026-05-19 00:50:06 +02:00
std::error_code ec;
for (const auto& entry : fs::recursive_directory_iterator(a, ec)) {
if (ec) break;
if (!entry.is_regular_file()) continue;
2026-05-19 03:28:27 +02:00
fs::path rel = fs::relative(entry.path(), a);
pushUnique((topName / compressedRel(rel)).generic_string());
2026-05-19 00:50:06 +02:00
}
} else {
2026-05-19 03:28:27 +02:00
pushUnique(compressedRel(a.filename()).generic_string());
2026-05-19 00:50:06 +02:00
}
}
2026-05-18 05:23:11 +02:00
for (Configuration* dep : c->dependencies) walkAssets(dep);
};
walkAssets(&cfg);
fs::path manifestPath = htmlOutDir / "files.json";
{
std::ofstream m(manifestPath);
m << "[";
for (std::size_t i = 0; i < assetFiles.size(); ++i) {
if (i) m << ",";
m << "\"" << assetFiles[i] << "\"";
}
m << "]";
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
std::ifstream in(htmlTemplate);
std::stringstream buf;
buf << in.rdbuf();
2026-05-18 05:23:11 +02:00
std::string html = buf.str();
html = std::regex_replace(html, std::regex(R"(\{\{WASM\}\})"), cfg.outputName + ".wasm");
html = std::regex_replace(html, std::regex(R"(\{\{ENV_SCRIPTS\}\})"), envScriptTags);
2026-05-26 22:50:08 +02:00
html = std::regex_replace(html, std::regex(R"(\{\{BUILDID\}\})"), buildId);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
std::ofstream out(htmlPath);
out << html;
out.close();
cfg.files.push_back(runtimeJs);
cfg.files.push_back(htmlPath);
2026-05-18 05:23:11 +02:00
cfg.files.push_back(manifestPath);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
}
2026-04-29 18:59:01 +02:00
std::string Crafter::HostTarget() {
static const std::string cached = []() -> std::string {
CommandResult r = RunCommandChecked("clang++ -print-target-triple");
if (r.exitCode != 0) return {};
std::string out = std::move(r.output);
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
return out;
}();
return cached;
}
bool Crafter::ArgQuery::Has(std::string_view flag) const {
for (std::string_view a : args) if (a == flag) return true;
return false;
}
std::optional<std::string> Crafter::ArgQuery::Get(std::string_view prefix) const {
for (std::string_view a : args) {
if (a.starts_with(prefix)) return std::string(a.substr(prefix.size()));
}
return std::nullopt;
}
2026-04-30 02:20:19 +02:00
ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args) {
2026-04-29 18:59:01 +02:00
if (const char* envMarch = std::getenv("CRAFTER_BUILD_MARCH"); envMarch && *envMarch) {
cfg.march = envMarch;
}
if (const char* envMtune = std::getenv("CRAFTER_BUILD_MTUNE"); envMtune && *envMtune) {
cfg.mtune = envMtune;
}
2026-04-30 02:20:19 +02:00
bool sawLib = false, sawShared = false;
2026-04-29 18:59:01 +02:00
for (std::string_view a : args) {
if (a == "--debug") cfg.debug = true;
2026-04-30 02:20:19 +02:00
else if (a == "--lib") sawLib = true;
else if (a == "--shared") sawShared = true;
2026-04-29 18:59:01 +02:00
else if (a.starts_with("--target=")) cfg.target = std::string(a.substr(std::string_view("--target=").size()));
else if (a.starts_with("--march=")) cfg.march = std::string(a.substr(std::string_view("--march=").size()));
else if (a.starts_with("--mtune=")) cfg.mtune = std::string(a.substr(std::string_view("--mtune=").size()));
}
2026-04-30 02:20:19 +02:00
if (sawLib && cfg.type == ConfigurationType::Executable) cfg.type = ConfigurationType::LibraryStatic;
if (sawShared && cfg.type == ConfigurationType::LibraryStatic) cfg.type = ConfigurationType::LibraryDynamic;
2026-05-18 05:23:11 +02:00
// WASI sysroot autodetect, applied at config-load time so the VariantId
// includes it. (Build() also runs this once more for callers that bypassed
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
// between the consumer's command-construction and the dep's own build.)
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
cfg.sysroot = "/usr/share/wasi-sysroot";
}
#endif
2026-04-30 02:20:19 +02:00
return ArgQuery{args};
2026-04-29 18:59:01 +02:00
}
2026-04-29 04:00:07 +02:00
static void PrintHelp(std::string_view argv0) {
std::println(
R"(Usage:
{0} [options] [-- project-args...] Build the project in the current directory
{0} test [test-options] [globs...] Build and run the project's tests
{0} help | -h | --help Show this help
Loads ./project.cpp (override with --project=<path>), compiles it to a shared
object, and invokes its CrafterBuildProject() to obtain a Configuration that
drives the build. Outputs land at bin/<name>-<target>-<march>/, intermediates
at build/<name>-<target>-<march>/.
Build options:
-r Run the produced executable after a successful build
(host targets only; libraries cannot be run).
-v, --verbose Verbose progress output.
-q, --quiet Suppress progress output.
--project=<path> Path to the project file (default: ./project.cpp).
Test options (after the `test` subcommand):
--list Enumerate matching tests without running them.
--jobs=<N> Parallel job count (default: hardware_concurrency).
--timeout=<seconds> Per-test timeout override.
--runner=<spec> Override the test runner for this run. Specs:
local
cmd:<command> (e.g. cmd:wine)
2026-05-27 19:45:05 +02:00
--target=<triple> Filter to tests whose cfg.target matches. Default:
sweep across every distinct target declared by the
project's tests plus the host triple.
2026-04-29 04:00:07 +02:00
<glob> One or more name globs to filter tests (e.g. 'Unit*').
Project args:
Any flag not consumed above is forwarded verbatim to CrafterBuildProject as
part of its `args` span. Project-specific flags (e.g. --target=, custom
feature toggles) live there.
Environment:
CRAFTER_BUILD_MARCH Override -march (default: native).
CRAFTER_BUILD_MTUNE Override -mtune (default: native).
CRAFTER_BUILD_RUNNER_<TARGET> Default test runner for a target triple.
Replace '-' and '.' with '_' in the
triple. CLI --runner= overrides this.
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
LIBCXX_DIR Windows libc++ install (MSVC ABI builds).
Exit status:
0 success / all non-skipped tests passed
1 build failure or one or more tests failed
)", argv0);
}
2026-04-27 07:04:42 +02:00
int Crafter::Run(int argc, char** argv) {
try {
2026-04-29 04:00:07 +02:00
std::string_view argv0 = argc > 0 ? argv[0] : "crafter-build";
2026-04-27 07:04:42 +02:00
fs::path projectFile = "./project.cpp";
std::vector<std::string_view> projectArgs;
projectArgs.reserve(argc);
bool runTests = false;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
bool runAfterBuild = false;
RunTestsOptions testOpts;
2026-04-29 03:27:11 +02:00
Progress::Verbosity verbosity = Progress::Verbosity::Default;
2026-04-27 07:04:42 +02:00
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
2026-04-29 04:00:07 +02:00
if (arg == "-h" || arg == "--help" || (!runTests && arg == "help")) {
PrintHelp(argv0);
return 0;
} else if (arg == "test") {
runTests = true;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
} else if (arg == "-r") {
runAfterBuild = true;
2026-04-29 03:27:11 +02:00
} else if (arg == "-v" || arg == "--verbose") {
verbosity = Progress::Verbosity::Verbose;
} else if (arg == "-q" || arg == "--quiet") {
verbosity = Progress::Verbosity::Quiet;
} else if (arg.starts_with("--project=")) {
2026-04-27 07:04:42 +02:00
projectFile = arg.substr(std::string_view("--project=").size());
} else if (runTests && arg.starts_with("--jobs=")) {
testOpts.jobs = std::stoi(std::string(arg.substr(std::string_view("--jobs=").size())));
} else if (runTests && arg.starts_with("--timeout=")) {
testOpts.timeoutOverride = std::chrono::seconds(std::stoi(std::string(arg.substr(std::string_view("--timeout=").size()))));
} else if (runTests && arg == "--list") {
testOpts.listOnly = true;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
} 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);
2026-04-27 07:04:42 +02:00
} else {
projectArgs.push_back(arg);
}
}
2026-04-29 03:27:11 +02:00
Progress::SetVerbosity(verbosity);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
// 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()));
}
}
2026-04-27 07:04:42 +02:00
if (!fs::exists(projectFile)) {
std::println(std::cerr, "No project file at {}", projectFile.string());
return 1;
}
Configuration config = LoadProject(projectFile, projectArgs);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
SetParentProject(&config);
2026-04-27 07:04:42 +02:00
if (runTests) {
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
TestSummary summary = RunTests(config, testOpts, projectArgs);
return summary.AllPassed() ? 0 : 1;
}
2026-04-27 07:04:42 +02:00
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
BuildResult result = Build(config, depResults, depMutex);
if (!result.result.empty()) {
2026-04-29 03:27:11 +02:00
Progress::Clear();
2026-04-27 07:04:42 +02:00
std::println(std::cerr, "{}", result.result);
return 1;
}
2026-04-29 03:27:11 +02:00
Progress::Finalize();
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
if (runAfterBuild) {
if (config.type != ConfigurationType::Executable) {
std::println(std::cerr, "-r: cannot run a library");
return 1;
}
2026-04-30 02:20:19 +02:00
fs::path dir = config.BinDir();
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
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";
}
2026-05-18 05:23:11 +02:00
artifact = fs::absolute(artifact);
fs::path absDir = fs::absolute(dir);
// wasm targets need either a wasm runtime (wasi-cli) or an HTTP
// server (browser build with index.html). std::system on the
// .wasm path goes nowhere useful — replace with detection.
if (config.target.starts_with("wasm32")) {
bool browserBuild = fs::exists(absDir / "index.html");
auto have = [](std::string_view exe) {
#ifdef _WIN32
std::string probe = std::format("where {} > NUL 2>&1", exe);
#else
std::string probe = std::format("command -v {} > /dev/null 2>&1", exe);
#endif
return std::system(probe.c_str()) == 0;
};
if (browserBuild) {
2026-05-19 00:50:06 +02:00
// Probe-bind to find a free port starting at 8080 — if the
// user has another dev server running we shift up rather
// than letting caddy/python exit with EADDRINUSE.
auto findFreePort = [](int basePort, int span) -> int {
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
static const bool wsaInit = []{ WSADATA d; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }();
if (!wsaInit) return basePort;
using sock_t = SOCKET;
const sock_t invalid = INVALID_SOCKET;
auto closesock = [](sock_t s){ closesocket(s); };
#else
using sock_t = int;
const sock_t invalid = -1;
auto closesock = [](sock_t s){ ::close(s); };
#endif
for (int p = basePort; p < basePort + span; ++p) {
sock_t s = ::socket(AF_INET, SOCK_STREAM, 0);
if (s == invalid) return basePort;
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(static_cast<std::uint16_t>(p));
2026-05-19 00:50:06 +02:00
bool ok = ::bind(s, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0;
closesock(s);
if (ok) return p;
}
return basePort;
};
const int port = findFreePort(8080, 16);
2026-05-26 22:50:08 +02:00
// Cross-origin isolation: the browser coarsens
// performance.now() (and chrono::steady_clock under wasi)
// to ~0.1ms unless the response carries COOP/COEP, which
// floors the --timing overlay's sub-ms phase counters to
// zero. Emitting same-origin / require-corp drops the
// resolution to ~5µs and also unlocks SharedArrayBuffer
// for any future threading work. CORP keeps the local
// asset fetches passing under require-corp.
auto writeFile = [](const fs::path& p, std::string_view contents) {
std::ofstream f(p, std::ios::binary | std::ios::trunc);
f.write(contents.data(), static_cast<std::streamsize>(contents.size()));
};
2026-05-18 05:23:11 +02:00
std::string cmd;
std::string_view picked;
2026-05-26 22:50:08 +02:00
bool isolated = false;
2026-05-18 05:23:11 +02:00
if (have("caddy")) {
picked = "caddy";
2026-05-26 22:50:08 +02:00
isolated = true;
// caddy file-server has no --header flag; write a
// Caddyfile next to the build output. Adapter is
// inferred from the .caddyfile extension when run
// via `caddy run`.
fs::path cf = absDir / "Caddyfile.coi";
writeFile(cf, std::format(
":{} {{\n"
" root * {}\n"
" header Cross-Origin-Opener-Policy \"same-origin\"\n"
" header Cross-Origin-Embedder-Policy \"require-corp\"\n"
" header Cross-Origin-Resource-Policy \"same-origin\"\n"
" header Cache-Control \"no-store\"\n"
" file_server\n"
"}}\n",
port, absDir.string()));
cmd = std::format("caddy run --config {} --adapter caddyfile",
cf.string());
} else if (have("python3") || have("python")) {
std::string_view py = have("python3") ? "python3" : "python";
picked = py;
isolated = true;
// Inline a tiny SimpleHTTPRequestHandler subclass
// that appends the COI headers on every response.
// Lives in absDir so the user can re-run it manually
// (`python3 absDir/.serve-coi.py 8080`).
fs::path sp = absDir / ".serve-coi.py";
writeFile(sp,
"import http.server, socketserver, sys, os\n"
"class H(http.server.SimpleHTTPRequestHandler):\n"
" def end_headers(self):\n"
" self.send_header('Cross-Origin-Opener-Policy', 'same-origin')\n"
" self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')\n"
" self.send_header('Cross-Origin-Resource-Policy', 'same-origin')\n"
" self.send_header('Cache-Control', 'no-store')\n"
" super().end_headers()\n"
"socketserver.TCPServer.allow_reuse_address = True\n"
"port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080\n"
"os.chdir(os.path.dirname(os.path.abspath(__file__)))\n"
"with socketserver.TCPServer(('', port), H) as s:\n"
" s.serve_forever()\n");
cmd = std::format("{} {} {}", py, sp.string(), port);
2026-05-18 05:23:11 +02:00
} else if (have("php")) {
picked = "php";
2026-05-26 22:50:08 +02:00
// php -S supports a router script — emit one that
// sets COI headers then delegates to the built-in
// static-file handler by returning false.
fs::path rp = absDir / ".serve-coi.php";
writeFile(rp,
"<?php\n"
"header('Cross-Origin-Opener-Policy: same-origin');\n"
"header('Cross-Origin-Embedder-Policy: require-corp');\n"
"header('Cross-Origin-Resource-Policy: same-origin');\n"
"header('Cache-Control: no-store');\n"
"return false;\n");
isolated = true;
cmd = std::format("php -S 0.0.0.0:{} -t {} {}",
port, absDir.string(), rp.string());
2026-05-18 05:23:11 +02:00
} else if (have("ruby")) {
picked = "ruby";
cmd = std::format("ruby -run -e httpd {} -p{}", absDir.string(), port);
} else if (have("busybox")) {
picked = "busybox httpd";
cmd = std::format("busybox httpd -f -p {} -h {}", port, absDir.string());
} else if (have("npx")) {
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).");
return 1;
}
2026-05-31 17:23:34 +02:00
std::println("listening on port :{}", port);
if (!isolated) {
2026-05-26 22:50:08 +02:00
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);
}
2026-05-31 17:23:34 +02:00
// Silence the backend's own banner/request logs so the
// experience is identical regardless of which server is
// picked — the line above is the only output.
#ifdef _WIN32
cmd += " > NUL 2>&1";
#else
cmd += " > /dev/null 2>&1";
#endif
2026-05-18 05:23:11 +02:00
return std::system(cmd.c_str()) == 0 ? 0 : 1;
}
// wasi-cli wasm — needs a standalone runtime.
if (have("wasmtime")) {
return std::system(std::format("wasmtime {}", artifact.string()).c_str()) == 0 ? 0 : 1;
}
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.");
return 1;
}
Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target Linux→mingw cross-compile now produces the same architectural shape as build.cmd (DLL + import lib + launcher exe) instead of a single static binary. The CI Windows artifact becomes a first-class drop-in: a user on Windows can run crafter-build.exe against any project.cpp and have it produce real Windows binaries — for either mingw or MSVC ABI. What changed: project.cpp: when target=mingw or target=msvc, crafter.build-lib is built as LibraryDynamic instead of LibraryStatic so the link emits a DLL + import lib (matching what build.cmd produces natively). Crafter.Build-Clang.cpp Build(): - LibraryDynamic now branches per target — mingw emits <name>.dll + lib<name>.dll.a via lld --out-implib; msvc emits <name>.dll + <name>.lib via /IMPLIB; unix unchanged. - expectedOutputFor returns .dll for Windows-target dynamic libs. - Executable on Windows host now branches per target: mingw target uses simple link (no -lc++/-nostdlib++/LIBCXX_DIR), msvc target keeps the existing path. Both auto-copy LibraryDynamic dep DLLs + import libs alongside the launcher exe (Windows resolves DLLs from the exe's own directory at load time). - Mingw-target Executables get -D CRAFTER_BUILD_DLL_IMPORT so CRAFTER_API resolves to dllimport in their PCMs. - mingw link adds -static-libstdc++ -static-libgcc -Wl,-Bstatic -lpthread so produced .exe/.dll don't depend on a particular libstdc++-6.dll / libwinpthread-1.dll being on the consumer's PATH (avoids the Arch UCRT vs msys2 UCRT vs msys2 MSVCRT ABI rabbit hole). Drops the old auto-copy of /usr/x86_64-w64-mingw32/bin/*.dll which is now dead weight. - -r flag resolves to an absolute path before std::system, otherwise cmd.exe rejects "./bin/..." with "'.' is not recognized...". Crafter.Build-Platform.cpp: - Split the Windows-host block into shared shell helpers (#if MSVC || MINGW) plus separate #if MSVC and #if MINGW blocks for LoadProject / EnsureCrafterBuildPcms / GetBaseCommand / BuildStdPcm. - Mingw-host LoadProject compiles project.cpp with --target=mingw, --sysroot=C:\msys64\ucrt64 (default; override with CRAFTER_MINGW_DIR), -femulated-tls, -Wl,--export-all-symbols (mingw-lld doesn't accept /EXPORT:NAME), and links against libcrafter-build.dll.a from the launcher's directory. - Mingw-host GetBaseCommand and BuildStdPcm dispatch on config.target so a mingw-host crafter-build can also build msvc-target outputs (uses LIBCXX_DIR + libc++ headers, same as native build.cmd) when the user sets cfg.target = "x86_64-pc-windows-msvc". README adds a Quick start (Windows) section covering both build paths (native MSVC via build.cmd and the cross-compiled mingw artifact), documenting the msys2 UCRT toolchain prerequisite. Verified end-to-end on the winvm: - mingw target: cross-compiled crafter-build.exe builds hello-world's project.cpp, compiles main.cpp, links a hello.exe that runs without any custom PATH (only Windows system DLLs needed). - msvc target: same crafter-build.exe builds an MSVC-ABI hello.exe linked against c++.dll (auto-copied from LIBCXX_DIR), runs cleanly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:23:42 +02:00
// Resolve to absolute — cmd.exe on Windows mishandles a leading
// "./" by trying to interpret it as a command. system() invokes
// through cmd /c, so the relative-prefixed path makes cmd error
// with "'.' is not recognized as an internal or external command".
2026-05-01 19:02:14 +02:00
// Run from the artifact's own directory so relative file opens
// (shaders, assets copied alongside the exe via cfg.files) resolve
// against the bin dir rather than the user's cwd. We exit the
// process immediately after, so no cwd restore needed.
fs::current_path(dir);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup 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>
2026-04-28 23:24:46 +02:00
return std::system(artifact.string().c_str()) == 0 ? 0 : 1;
}
2026-04-27 07:04:42 +02:00
return 0;
} catch (const std::exception& e) {
2026-04-29 03:27:11 +02:00
Progress::Clear();
2026-04-27 07:04:42 +02:00
std::println(std::cerr, "{}", e.what());
return 1;
}
}