diff --git a/README.md b/README.md index 77c5fab..470b38e 100644 --- a/README.md +++ b/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 ``` +## 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 +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 Crafter Build loads a `project.cpp` from the current directory. The file exports one function that returns a populated `Configuration`: diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 910c2a3..885c779 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -393,6 +393,14 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map fs::last_write_time(dest_file)) { - // Copy the file if it doesn't exist or is older - fs::copy(entry.path(), dest_file, fs::copy_options::overwrite_existing); + // 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 dllSeen; + std::function 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); + // 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); + } } } - } + for (Configuration* sub : dep->dependencies) copyDepDlls(sub); + }; + for (Configuration* dep : config.dependencies) copyDepDlls(dep); } catch (const fs::filesystem_error& e) { return {e.what(), false, {}}; } @@ -706,8 +737,39 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map dllSeen; + std::function 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 } else if(config.type == ConfigurationType::LibraryStatic) { #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu @@ -718,7 +780,28 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map.dll + lib.dll.a (lld --out-implib) + // msvc: .dll + .lib (lld /IMPLIB) + // unix: lib.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") { 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; } diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index e0fc8d1..080fa29 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -191,16 +191,6 @@ CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono:: 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() { if (const char* local = std::getenv("LOCALAPPDATA")) { return fs::path(local) / "crafter.build"; @@ -209,10 +199,6 @@ fs::path Crafter::GetCacheDir() { 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 { constexpr std::array kCrafterBuildModules = { "Crafter.Build-Shader", @@ -224,7 +210,27 @@ namespace { "Crafter.Build-Test", "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) { for (std::string_view name : kCrafterBuildModules) { fs::path cppmPath = sourceDir / (std::string(name) + ".cppm"); @@ -321,6 +327,206 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span 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 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 + // -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); + auto fn = reinterpret_cast(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 std::string Crafter::RunCommand(const std::string_view cmd) { diff --git a/project.cpp b/project.cpp index 54ac2c0..54e484a 100644 --- a/project.cpp +++ b/project.cpp @@ -27,7 +27,12 @@ extern "C" Configuration CrafterBuildProject(std::span a crafterBuildLib->target = target; crafterBuildLib->march = march; 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; { std::array interfaces = {