Cross-compiled mingw artifact: full DLL+launcher pattern + MSVC target
Some checks failed
CI / build-test-release (pull_request) Has been cancelled
Some checks failed
CI / build-test-release (pull_request) Has been cancelled
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>
This commit is contained in:
parent
f1199429b7
commit
bed4a7c9e4
4 changed files with 363 additions and 37 deletions
27
README.md
27
README.md
|
|
@ -24,6 +24,33 @@ To build the system as a distro package on Arch:
|
||||||
makepkg -si # uses CRAFTER_BUILD_MARCH=x86-64-v3 by default for portability
|
makepkg -si # uses CRAFTER_BUILD_MARCH=x86-64-v3 by default for portability
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Quick start (Windows)
|
||||||
|
|
||||||
|
Two ways to get a working `crafter-build` on Windows: native build via `build.cmd` (MSVC ABI), or download the cross-compiled mingw artifact from CI (mingw ABI). They produce different binaries with different ABI requirements; pick one and stick with it.
|
||||||
|
|
||||||
|
**Native MSVC build** — what `build.cmd` does, what CI doesn't ship.
|
||||||
|
|
||||||
|
Need clang+lld targeting `x86_64-pc-windows-msvc` and a Windows libc++ install pointed to by `LIBCXX_DIR`. Then `build.cmd` produces `bin/{crafter-build.exe, crafter-build.dll, crafter-build.lib}`.
|
||||||
|
|
||||||
|
**Cross-compiled mingw artifact** — what CI ships from the Linux runner.
|
||||||
|
|
||||||
|
The CI's Windows zip contains `crafter-build.exe` + `crafter-build.dll` + `libcrafter-build.dll.a`. The DLL is **statically linked against libstdc++/libgcc/libwinpthread**, so it doesn't depend on a particular runtime DLL being on the consumer's PATH. To use it for builds the consumer needs the mingw-w64 sysroot installed, since `crafter-build.exe` invokes the user's clang to compile their `project.cpp`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# one-time setup
|
||||||
|
winget install MSYS2.MSYS2
|
||||||
|
C:\msys64\usr\bin\pacman.exe -S --noconfirm mingw-w64-ucrt-x86_64-toolchain
|
||||||
|
# add C:\msys64\ucrt64\bin to PATH (so user-built executables can find msys2 clang)
|
||||||
|
|
||||||
|
# then any project with a project.cpp:
|
||||||
|
cd <project-dir>
|
||||||
|
crafter-build.exe -r
|
||||||
|
```
|
||||||
|
|
||||||
|
Match the toolchain flavor to the artifact: **UCRT mingw** (`mingw-w64-ucrt-x86_64-toolchain`, sysroot at `C:\msys64\ucrt64`) — the artifact is cross-compiled with UCRT, the older MSVCRT mingw won't ABI-match. Override the auto-detected sysroot path with `CRAFTER_MINGW_DIR=...` if you have mingw-w64 installed somewhere else.
|
||||||
|
|
||||||
|
A project.cpp built on Windows by this artifact gets compiled with `--target=x86_64-w64-mingw32` by default, producing self-contained mingw exes (libstdc++/libgcc/libwinpthread statically linked, no runtime DLLs alongside). For MSVC-ABI output set `cfg.target = "x86_64-pc-windows-msvc"` in your `project.cpp` *and* point `LIBCXX_DIR` at a Windows libc++ install — same prerequisite as the native build.
|
||||||
|
|
||||||
## Writing a project.cpp
|
## Writing a project.cpp
|
||||||
|
|
||||||
Crafter Build loads a `project.cpp` from the current directory. The file exports one function that returns a populated `Configuration`:
|
Crafter Build loads a `project.cpp` from the current directory. The file exports one function that returns a populated `Configuration`:
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,14 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
#endif
|
#endif
|
||||||
} else if(config.type == ConfigurationType::Executable) {
|
} else if(config.type == ConfigurationType::Executable) {
|
||||||
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
|
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
|
||||||
|
// 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";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
|
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
|
||||||
}
|
}
|
||||||
|
|
@ -628,13 +636,18 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
// the resulting binary stands alone. Auto-link so user projects don't
|
// the resulting binary stands alone. Auto-link so user projects don't
|
||||||
// carry boilerplate.
|
// carry boilerplate.
|
||||||
if (config.target == "x86_64-w64-mingw32") {
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
// mingw runtime DLLs (libstdc++-6.dll, libgcc_s_seh-1.dll,
|
// Static-link libstdc++/libgcc/libwinpthread so the resulting .exe
|
||||||
// libwinpthread-1.dll) get auto-copied next to the .exe by the
|
// or .dll doesn't depend on a specific runtime DLL being on the
|
||||||
// post-build step below, so dynamic linkage is fine. -lstdc++exp =
|
// consumer's PATH. The mingw runtime ABI varies subtly between
|
||||||
// C++26 std::print/format extras; -lpthread = winpthreads symbols
|
// distributions (Arch UCRT vs msys2 UCRT vs msys2 MSVCRT) and
|
||||||
// that libstdc++ uses for std::atomic_wait, counting_semaphore,
|
// STATUS_ENTRYPOINT_NOT_FOUND at LoadLibrary time is the symptom
|
||||||
// stop_token. clang++ adds the main -lstdc++ implicitly.
|
// of a mismatch. Static linkage trades binary size for portability.
|
||||||
linkExtras += " -lstdc++exp -lpthread";
|
// 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force a relink if the expected output is missing or older than any dep
|
// Force a relink if the expected output is missing or older than any dep
|
||||||
|
|
@ -657,6 +670,11 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
? dir / std::format("{}.lib", c.outputName)
|
? dir / std::format("{}.lib", c.outputName)
|
||||||
: dir / std::format("lib{}.a", c.outputName);
|
: dir / std::format("lib{}.a", c.outputName);
|
||||||
}
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
return dir / std::format("lib{}.so", c.outputName);
|
return dir / std::format("lib{}.so", c.outputName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -680,20 +698,33 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||||
if(config.target == "x86_64-w64-mingw32") {
|
if(config.target == "x86_64-w64-mingw32") {
|
||||||
try {
|
try {
|
||||||
// Iterate over the source directory
|
// Copy any LibraryDynamic dependency DLLs alongside
|
||||||
for (const auto& entry : fs::directory_iterator("/usr/x86_64-w64-mingw32/bin/")) {
|
// the launcher exe — Windows resolves DLLs from the exe's
|
||||||
// Check if the file is a regular file and ends with ".dll"
|
// own directory at load time, so this is the simplest
|
||||||
if (fs::is_regular_file(entry) && entry.path().extension() == ".dll") {
|
// equivalent of rpath $ORIGIN.
|
||||||
// Construct the destination file path
|
std::unordered_set<Configuration*> dllSeen;
|
||||||
fs::path dest_file = outputDir / entry.path().filename();
|
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
|
||||||
|
if (!dllSeen.insert(dep).second) return;
|
||||||
// Check if the destination file exists and if it is older than the source file
|
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
|
||||||
if (!fs::exists(dest_file) || fs::last_write_time(entry.path()) > fs::last_write_time(dest_file)) {
|
fs::path depDir = dep->path / "bin" / std::format("{}-{}-{}", dep->name, dep->target, dep->march);
|
||||||
// Copy the file if it doesn't exist or is older
|
// The DLL itself (Windows resolves it from the
|
||||||
fs::copy(entry.path(), dest_file, fs::copy_options::overwrite_existing);
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (Configuration* sub : dep->dependencies) copyDepDlls(sub);
|
||||||
|
};
|
||||||
|
for (Configuration* dep : config.dependencies) copyDepDlls(dep);
|
||||||
} catch (const fs::filesystem_error& e) {
|
} catch (const fs::filesystem_error& e) {
|
||||||
return {e.what(), false, {}};
|
return {e.what(), false, {}};
|
||||||
}
|
}
|
||||||
|
|
@ -706,8 +737,39 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||||
std::system(std::format("copy \"%LIBCXX_DIR%\\lib\\c++.dll\" \"{}\\c++.dll\"", outputDir.string()).c_str());
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
buildResult.result = RunCommand(std::format("{}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR%\\lib -lc++ -nostdinc++ -nostdlib++{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
// 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") {
|
||||||
|
fs::path depDir = dep->path / "bin" / std::format("{}-{}-{}", dep->name, dep->target, dep->march);
|
||||||
|
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));
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
} else if(config.type == ConfigurationType::LibraryStatic) {
|
} else if(config.type == ConfigurationType::LibraryStatic) {
|
||||||
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||||
|
|
@ -718,7 +780,28 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
buildResult.result = RunCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string()));
|
buildResult.result = RunCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string()));
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} 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));
|
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -832,6 +915,11 @@ int Crafter::Run(int argc, char** argv) {
|
||||||
} else if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
|
} else if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
|
||||||
artifact += ".exe";
|
artifact += ".exe";
|
||||||
}
|
}
|
||||||
|
// 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".
|
||||||
|
artifact = fs::absolute(artifact);
|
||||||
return std::system(artifact.string().c_str()) == 0 ? 0 : 1;
|
return std::system(artifact.string().c_str()) == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,16 +191,6 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|
||||||
std::string libcxx = std::getenv("LIBCXX_DIR");
|
|
||||||
std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxx);
|
|
||||||
|
|
||||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdcppm)) {
|
|
||||||
return RunCommand(std::format("clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 -nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::path Crafter::GetCacheDir() {
|
fs::path Crafter::GetCacheDir() {
|
||||||
if (const char* local = std::getenv("LOCALAPPDATA")) {
|
if (const char* local = std::getenv("LOCALAPPDATA")) {
|
||||||
return fs::path(local) / "crafter.build";
|
return fs::path(local) / "crafter.build";
|
||||||
|
|
@ -209,10 +199,6 @@ fs::path Crafter::GetCacheDir() {
|
||||||
throw std::runtime_error("LOCALAPPDATA not set");
|
throw std::runtime_error("LOCALAPPDATA not set");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|
||||||
return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1");
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr std::array<std::string_view, 8> kCrafterBuildModules = {
|
constexpr std::array<std::string_view, 8> kCrafterBuildModules = {
|
||||||
"Crafter.Build-Shader",
|
"Crafter.Build-Shader",
|
||||||
|
|
@ -224,7 +210,27 @@ namespace {
|
||||||
"Crafter.Build-Test",
|
"Crafter.Build-Test",
|
||||||
"Crafter.Build",
|
"Crafter.Build",
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc
|
||||||
|
// Native Windows build via build.cmd: MSVC ABI + libc++ from %LIBCXX_DIR%.
|
||||||
|
|
||||||
|
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||||
|
std::string libcxx = std::getenv("LIBCXX_DIR");
|
||||||
|
std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxx);
|
||||||
|
|
||||||
|
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdcppm)) {
|
||||||
|
return RunCommand(std::format("clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 -nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Crafter::GetBaseCommand(const Configuration& config) {
|
||||||
|
return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1");
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||||
for (std::string_view name : kCrafterBuildModules) {
|
for (std::string_view name : kCrafterBuildModules) {
|
||||||
fs::path cppmPath = sourceDir / (std::string(name) + ".cppm");
|
fs::path cppmPath = sourceDir / (std::string(name) + ".cppm");
|
||||||
|
|
@ -321,6 +327,206 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32
|
||||||
|
// Linux→mingw cross-compile run on Windows: mingw ABI + libstdc++ from msys2.
|
||||||
|
// Resolves the mingw sysroot at C:\msys64\ucrt64 by default (UCRT runtime,
|
||||||
|
// matching the Arch cross-compile that produced this binary). User can
|
||||||
|
// override with CRAFTER_MINGW_DIR if they have mingw-w64 installed elsewhere
|
||||||
|
// or want the legacy MSVCRT mingw at C:\msys64\mingw64. clang itself is the
|
||||||
|
// user's existing standalone clang on PATH.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
fs::path MingwPrefix() {
|
||||||
|
if (const char* env = std::getenv("CRAFTER_MINGW_DIR"); env && *env) {
|
||||||
|
return fs::path(env);
|
||||||
|
}
|
||||||
|
return "C:\\msys64\\ucrt64";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MingwGccVersion() {
|
||||||
|
fs::path includeRoot = MingwPrefix() / "include" / "c++";
|
||||||
|
if (!fs::exists(includeRoot)) {
|
||||||
|
throw std::runtime_error(std::format(
|
||||||
|
"mingw-w64 not found at {} (install msys2 mingw-w64-x86_64-toolchain or set CRAFTER_MINGW_DIR)",
|
||||||
|
MingwPrefix().string()));
|
||||||
|
}
|
||||||
|
std::vector<std::string> versions;
|
||||||
|
for (const auto& entry : fs::directory_iterator(includeRoot)) {
|
||||||
|
if (entry.is_directory()) versions.push_back(entry.path().filename().string());
|
||||||
|
}
|
||||||
|
if (versions.empty()) {
|
||||||
|
throw std::runtime_error(std::format("no C++ versions under {}", includeRoot.string()));
|
||||||
|
}
|
||||||
|
std::sort(versions.begin(), versions.end(), std::greater<>());
|
||||||
|
return versions.front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||||
|
if (config.target == "x86_64-pc-windows-msvc") {
|
||||||
|
// MSVC target on mingw host: same MSVC libc++ logic as the
|
||||||
|
// native-MSVC LoadProject path. User must have LIBCXX_DIR pointing
|
||||||
|
// at a Windows libc++ install.
|
||||||
|
const char* libcxxEnv = std::getenv("LIBCXX_DIR");
|
||||||
|
if (!libcxxEnv || !*libcxxEnv) {
|
||||||
|
return "MSVC target requires LIBCXX_DIR pointing at a Windows libc++ install";
|
||||||
|
}
|
||||||
|
std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxxEnv);
|
||||||
|
if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdcppm)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return RunCommand(std::format(
|
||||||
|
"clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
|
||||||
|
"-nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier "
|
||||||
|
"--precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}",
|
||||||
|
config.target, config.march, config.mtune, stdPcm.string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: mingw target. Look up mingw-w64 libstdc++ via the msys2 prefix.
|
||||||
|
fs::path prefix = MingwPrefix();
|
||||||
|
fs::path stdCc = prefix / "include" / "c++" / MingwGccVersion() / "bits" / "std.cc";
|
||||||
|
if (!fs::exists(stdCc)) {
|
||||||
|
return std::format("std.cc not found at {}", stdCc.string());
|
||||||
|
}
|
||||||
|
if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdCc)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// Copy std.cc → std.cppm in C++ rather than via cmd's `copy /Y` because
|
||||||
|
// `copy` always prints "1 file(s) copied." to stdout and RunCommand
|
||||||
|
// treats any output as an error.
|
||||||
|
fs::path stdCppm = stdPcm.parent_path() / "std.cppm";
|
||||||
|
std::error_code ec;
|
||||||
|
fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, ec);
|
||||||
|
if (ec) {
|
||||||
|
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), ec.message());
|
||||||
|
}
|
||||||
|
return RunCommand(std::format(
|
||||||
|
"clang++ --target={} -march={} -mtune={} "
|
||||||
|
"--sysroot=\"{}\" -femulated-tls "
|
||||||
|
"-O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier "
|
||||||
|
"--precompile \"{}\" -o {}",
|
||||||
|
config.target, config.march, config.mtune,
|
||||||
|
prefix.string(),
|
||||||
|
stdCppm.string(),
|
||||||
|
stdPcm.string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Crafter::GetBaseCommand(const Configuration& config) {
|
||||||
|
if (config.target == "x86_64-pc-windows-msvc") {
|
||||||
|
return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1");
|
||||||
|
}
|
||||||
|
return std::format("clang++ --sysroot=\"{}\" -femulated-tls", MingwPrefix().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
|
||||||
|
fs::path prefix = MingwPrefix();
|
||||||
|
for (std::string_view name : kCrafterBuildModules) {
|
||||||
|
fs::path cppmPath = sourceDir / (std::string(name) + ".cppm");
|
||||||
|
fs::path pcmPath = cacheDir / (std::string(name) + ".pcm");
|
||||||
|
if (!fs::exists(cppmPath)) {
|
||||||
|
throw std::runtime_error(std::format(
|
||||||
|
"module source {} not found in {} (set CRAFTER_BUILD_HOME)",
|
||||||
|
name, sourceDir.string()));
|
||||||
|
}
|
||||||
|
if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string cmd = std::format(
|
||||||
|
"clang++ --target=x86_64-w64-mingw32 -march=native -mtune=native "
|
||||||
|
"--sysroot=\"{}\" -femulated-tls "
|
||||||
|
"-std=c++26 -O3 -D CRAFTER_BUILD_DLL_IMPORT "
|
||||||
|
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
|
||||||
|
"-fprebuilt-module-path={} "
|
||||||
|
"--precompile {} -o {}",
|
||||||
|
prefix.string(), cacheDir.string(), cppmPath.string(), pcmPath.string());
|
||||||
|
CommandResult r = Crafter::RunCommandChecked(cmd);
|
||||||
|
if (r.exitCode != 0) {
|
||||||
|
throw std::runtime_error(std::format(
|
||||||
|
"Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
|
||||||
|
fs::path absProject = fs::canonical(projectFile);
|
||||||
|
fs::path buildDir = absProject.parent_path() / "build";
|
||||||
|
if (!fs::exists(buildDir)) fs::create_directories(buildDir);
|
||||||
|
fs::path dllPath = buildDir / (absProject.stem().string() + ".dll");
|
||||||
|
|
||||||
|
char hostExeBuf[MAX_PATH];
|
||||||
|
DWORD hostExeLen = GetModuleFileNameA(nullptr, hostExeBuf, MAX_PATH);
|
||||||
|
if (hostExeLen == 0 || hostExeLen == MAX_PATH) {
|
||||||
|
throw std::runtime_error("GetModuleFileName failed");
|
||||||
|
}
|
||||||
|
fs::path hostExe(std::string(hostExeBuf, hostExeLen));
|
||||||
|
|
||||||
|
fs::path sourceDir = Crafter::GetCrafterBuildHome();
|
||||||
|
|
||||||
|
Configuration hostConfig;
|
||||||
|
hostConfig.target = "x86_64-w64-mingw32";
|
||||||
|
hostConfig.march = "native";
|
||||||
|
hostConfig.mtune = "native";
|
||||||
|
fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march);
|
||||||
|
if (!fs::exists(cacheDir)) fs::create_directories(cacheDir);
|
||||||
|
|
||||||
|
std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm");
|
||||||
|
if (!stdResult.empty()) {
|
||||||
|
throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult));
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCrafterBuildPcms(sourceDir, cacheDir);
|
||||||
|
|
||||||
|
bool stale = !fs::exists(dllPath)
|
||||||
|
|| fs::last_write_time(dllPath) < fs::last_write_time(absProject)
|
||||||
|
|| fs::last_write_time(dllPath) < fs::last_write_time(hostExe);
|
||||||
|
|
||||||
|
if (stale) {
|
||||||
|
// The import lib lives next to the launcher exe (Build()'s mingw
|
||||||
|
// copy step puts libcrafter-build.dll.a there) so -L<exe-dir>
|
||||||
|
// -lcrafter-build resolves it.
|
||||||
|
fs::path prefix = MingwPrefix();
|
||||||
|
std::string compileCmd = std::format(
|
||||||
|
"clang++ --target=x86_64-w64-mingw32 -march=native -mtune=native "
|
||||||
|
"--sysroot=\"{}\" -femulated-tls "
|
||||||
|
"-std=c++26 -shared -O3 -Wno-return-type-c-linkage -fuse-ld=lld "
|
||||||
|
"-D CRAFTER_BUILD_DLL_IMPORT "
|
||||||
|
"-fprebuilt-module-path={} "
|
||||||
|
// mingw-lld doesn't accept /EXPORT:NAME — use the catch-all so
|
||||||
|
// CrafterBuildProject is reachable via GetProcAddress without
|
||||||
|
// forcing project.cpp templates to add __declspec(dllexport).
|
||||||
|
"-Wl,--export-all-symbols "
|
||||||
|
"{} -o {} -L\"{}\" -lcrafter-build -lstdc++exp -lpthread",
|
||||||
|
prefix.string(),
|
||||||
|
cacheDir.string(),
|
||||||
|
absProject.string(), dllPath.string(),
|
||||||
|
hostExe.parent_path().string());
|
||||||
|
|
||||||
|
std::string result = RunCommand(compileCmd);
|
||||||
|
if (!result.empty()) {
|
||||||
|
throw std::runtime_error(std::format(
|
||||||
|
"Failed to compile project {}: {}", absProject.string(), result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HMODULE handle = LoadLibraryA(dllPath.string().c_str());
|
||||||
|
if (!handle) {
|
||||||
|
throw std::runtime_error(std::format(
|
||||||
|
"Failed to load project {}: error {}", dllPath.string(), GetLastError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
using ProjectFn = Configuration (*)(std::span<const std::string_view>);
|
||||||
|
auto fn = reinterpret_cast<ProjectFn>(GetProcAddress(handle, "CrafterBuildProject"));
|
||||||
|
if (!fn) {
|
||||||
|
throw std::runtime_error(std::format(
|
||||||
|
"CrafterBuildProject not found in {}: error {}", dllPath.string(), GetLastError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(args);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
|
||||||
|
|
||||||
std::string Crafter::RunCommand(const std::string_view cmd) {
|
std::string Crafter::RunCommand(const std::string_view cmd) {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,12 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
crafterBuildLib->target = target;
|
crafterBuildLib->target = target;
|
||||||
crafterBuildLib->march = march;
|
crafterBuildLib->march = march;
|
||||||
crafterBuildLib->mtune = mtune;
|
crafterBuildLib->mtune = mtune;
|
||||||
crafterBuildLib->type = ConfigurationType::LibraryStatic;
|
// Windows builds (native msvc via build.cmd or cross-compiled mingw from
|
||||||
|
// Linux) need a DLL + import lib + launcher exe so LoadProject can
|
||||||
|
// compile project.cpp against a stable ABI boundary. Linux is monolithic.
|
||||||
|
crafterBuildLib->type = (target == "x86_64-w64-mingw32" || target == "x86_64-pc-windows-msvc")
|
||||||
|
? ConfigurationType::LibraryDynamic
|
||||||
|
: ConfigurationType::LibraryStatic;
|
||||||
crafterBuildLib->debug = debug;
|
crafterBuildLib->debug = debug;
|
||||||
{
|
{
|
||||||
std::array<fs::path, 8> interfaces = {
|
std::array<fs::path, 8> interfaces = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue