v2 nearly done

This commit is contained in:
Jorijn van der Graaf 2026-04-27 07:04:42 +02:00
commit f13671b2be
24 changed files with 1467 additions and 314 deletions

View file

@ -20,12 +20,173 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
export module Crafter.Build:Clang_impl;
import std;
import :Clang;
import :Platform;
namespace fs = std::filesystem;
using namespace Crafter;
BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& depSet, std::mutex& depMutex) {
fs::path pwd = std::filesystem::path cwd = std::filesystem::current_path();
fs::path buildDir = pwd/"build";
fs::path outputDir = pwd/"bin";
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;
}
}
for(Configuration* depCfg : this->dependencies) {
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;
}
}
}
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++){
fs::path file = path / interfaces[i];
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) {
fs::path file = path / tempFile;
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) {
fs::path buildDir = config.path/"build"/std::format("{}-{}-{}", config.name, config.target, config.march);
fs::path outputDir = config.path/"bin"/std::format("{}-{}-{}", config.name, config.target, config.march);
if (!fs::exists(buildDir)) {
fs::create_directories(buildDir);
@ -35,6 +196,8 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
fs::create_directories(outputDir);
}
BuildResult buildResult;
std::vector<std::thread> threads;
threads.reserve(config.shaders.size() + 1 + config.interfaces.size() + config.implementations.size());
@ -106,7 +269,23 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
}
});
fs::path stdPcmDir = GetCacheDir()/config.target/"-"/config.march;
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]() {
if (buildCancelled.load(std::memory_order_relaxed)) return;
externalResults[i] = BuildExternal(config.externalDependencies[i], buildCancelled);
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);
if (!fs::exists(stdPcmDir)) {
fs::create_directories(stdPcmDir);
@ -114,7 +293,10 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
std::string stdPcmResult = BuildStdPcm(config, stdPcmDir/"std.pcm");
if(!stdPcmResult.empty()) {
return {result, false, {}};
buildCancelled.store(true);
for(std::thread& thread : threads) thread.join();
for(std::thread& thread : externalThreads) thread.join();
return {stdPcmResult, false, {}};
}
fs::path pcmDir;
@ -125,58 +307,93 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
pcmDir = buildDir;
}
std::string command = std::format("{} --target={} -march={} -mtune={} -std=c++26 -D CRAFTER_BUILD_CONFIGURATION_TARGET=\\\"{}\\\" -D CRAFTER_BUILD_CONFIGURATION_TARGET_{} -fprebuilt-module-path={} -fprebuilt-module-path={}", GetBaseCommand(), config.target, config.march, config.mtune, editedTarget, editedTarget, stdPcm.parent_path().string(), stdPcmDir);
fs::copy_file(stdPcmDir/"std.pcm", pcmDir/"std.pcm", fs::copy_options::update_existing);
if(config.type == CRAFTER_CONFIGURATION_TYPE_SHARED_LIBRARY) {
std::string editedTarget = config.target;
std::replace(editedTarget.begin(), editedTarget.end(), '-', '_');
std::string command = std::format("{} --target={} -march={} -mtune={} -std=c++26 -D CRAFTER_BUILD_CONFIGURATION_TARGET=\\\"{}\\\" -D CRAFTER_BUILD_CONFIGURATION_TARGET_{} -fprebuilt-module-path={} -fprebuilt-module-path={}", GetBaseCommand(config), config.target, config.march, config.mtune, editedTarget, editedTarget, stdPcmDir.string(), pcmDir.string());
if(config.type == ConfigurationType::LibraryDynamic) {
#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
} else if(config.type == CRAFTER_CONFIGURATION_TYPE_LIBRARY) {
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
} else {
} else if(config.type == ConfigurationType::Executable) {
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
} else {
command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
}
std::string files;
std::unordered_set<std::string> libSet;
std::mutex fileMutex;
std::vector<std::thread> depThreads;
depThreads.reserve(config.dependencies);
depThreads.reserve(config.dependencies.size());
std::atomic<bool> repack(false);
for(const Dependency& dep : config.dependencies) {
for (const auto& entry : fs::recursive_directory_iterator(dep.path)) {
for(Configuration* dep : config.dependencies) {
for (const auto& entry : fs::recursive_directory_iterator(dep->path)) {
if (entry.is_directory() && entry.path().filename() == "include") {
command += " -I" + entry.path().string();
}
}
command += std::format(" -I{} -fprebuilt-module-path={}", dep.path.string(), (dep.path/"bin"/(config.target+config.march)).string());
command += std::format(" -I{} -fprebuilt-module-path={}", dep->path.string(), dep->PcmDir().string());
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;
depThreads.emplace_back([&](){
if (!buildCancelled.load(std::memory_order_relaxed)) {
depMutex.lock();
if(!depSet.contains(dep.path)) {
depSet.insert(dep.path);
depMutex.unlock();
BuildResult result = Build(dep.path, depSet, depMutex);
fileMutex.lock();
libSet.merge(result.libs);
fileMutex.unlock();
if (buildCancelled.compare_exchange_strong(false, true)) {
buildError = std::move(result.result);
}
if(result.repack) {
repack = true;
}
auto it = depResults.find(dep->path);
if (it == depResults.end()) {
isBuilder = true;
promise = std::make_shared<std::promise<BuildResult>>();
resultFuture = promise->get_future().share();
depResults.emplace(dep->path, resultFuture);
} else {
depMutex.unlock();
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);
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());
}
}
});
});
}
for(const Define& define : config.defines) {
@ -187,6 +404,10 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
}
}
for(const std::string& flag : config.compileFlags) {
command += " " + flag;
}
std::string cmakeBuildType;
if(config.debug) {
@ -197,12 +418,12 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
command += " -O3";
}
for (const fs::path& cFile : config.c_files) {
for (const fs::path& cFile : config.cFiles) {
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";
if (!fs::exists(objPath) || fs::last_write_time(srcPath) > fs::last_write_time(objPath)) {
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled]() {
threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config]() {
if (buildCancelled.load(std::memory_order_relaxed)) return;
std::string result = RunCommand(std::format("clang {}.c --target={} -march={} -mtune={} -O3 -c -o {}_source.o", cFile.string(), config.target, config.march, config.mtune, (buildDir / cFile.filename()).string()));
@ -238,92 +459,70 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
for(std::thread& thread : depThreads) {
thread.join();
}
for(std::thread& thread : externalThreads) {
thread.join();
}
if(buildCancelled.load()) {
for(std::thread& thread : threads) thread.join();
return {buildError, false, {}};
}
std::vector<std::unique_ptr<Module>> interfaces;
std::vector<std::tuple<fs::path, std::string, ModulePartition*, Module*>> tempModulePaths = std::vector<std::tuple<fs::path, std::string, ModulePartition*, Module*>>(config.interfaces.size());
for(std::uint16_t i = 0; i < config.interfaces.size(); i++){
const std::filesystem::path file = workingDir / (config.interfaces[i]+".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};
if(repack.load()) {
buildResult.repack = true;
}
buildResult.libs = std::move(libSet);
for(const std::string& flag : config.linkFlags) {
buildResult.libs.insert(flag);
}
fs::file_time_type externalFloor = fs::file_time_type::min();
for(const ExternalBuildResult& ext : externalResults) {
for(const std::string& flag : ext.compileFlags) {
command += " " + flag;
}
for(const std::string& flag : ext.linkFlags) {
buildResult.libs.insert(flag);
}
if (ext.latestArtifact > externalFloor) externalFloor = ext.latestArtifact;
}
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("");
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 : 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;
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]() {
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());
}
}
}
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) {
std::regex pattern(R"(import :([a-zA-Z_\-0-9\.]+);)");
std::sregex_iterator currentMatch(std::get<1>(file).begin(), std::get<1>(file).end(), pattern);
std::sregex_iterator lastMatch;
while (currentMatch != lastMatch) {
std::smatch match = *currentMatch;
for(std::unique_ptr<ModulePartition>& partition2 : std::get<3>(file)->partitions) {
if(partition2->name == match[1]) {
std::get<2>(file)->partitionDependencies.push_back(partition2.get());
goto next2;
}
}
throw std::runtime_error(std::format("imported partition {}:{} not found, referenced in {}", std::get<3>(file)->name, match[1].str(), std::get<0>(file).string()));
next2: ++currentMatch;
}
}
for(Module& interface : interfaces) {
if(config.interfaces[i]->Check(pcmDir)) {
threads.emplace_back(&Module::Compile, interface, command, pcmDir, buildDir, buildCancelled, buildError);
});
buildResult.repack = true;
}
files += std::format(" {}/{}.o", buildDir.string(), interface.path.filename().string());
for(std::unique_ptr<ModulePartition>& part : interface.partitions) {
files += std::format(" {}/{}.o", buildDir.string(), interface->path.filename().string());
for(std::unique_ptr<ModulePartition>& part : interface->partitions) {
files += std::format(" {}/{}.o", buildDir.string(), part->path.filename().string());
}
}
for(Implementation& interface : implementations) {
if(config.implementations[i].Check(buildDir, pcmDir)) {
for(Implementation& implementation : config.implementations) {
if(implementation.Check(buildDir, pcmDir, externalFloor)) {
buildResult.repack = true;
threads.emplace_back(&Implementation::Compile, &config.implementations[i], command, buildDir, buildCancelled, buildError);
Implementation* impl = &implementation;
threads.emplace_back([impl, &command, &buildDir, &buildCancelled, &buildError]() {
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());
}
}
});
}
files += std::format(" {}/{}_impl.o", buildDir.string(), config.implementations[i].path.filename().string());
files += std::format(" {}/{}_impl.o", buildDir.string(), implementation.path.filename().string());
}
for(std::thread& thread : threads) {
@ -334,29 +533,95 @@ BuildResult Build(const Configuration& config, std::unordered_set<fs::path>& dep
return {buildError, false, {}};
}
std::string linkExtras;
for(const std::string& flag : buildResult.libs) {
linkExtras += " " + flag;
}
if(buildResult.repack) {
if(config.type == CRAFTER_CONFIGURATION_TYPE_EXECUTABLE) {
if(config.type == ConfigurationType::Executable) {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
buildResult.result = RunCommand(std::format("{}{} -o {} -fuse-ld=lld ", command, files, (binDir/outputName).string()));
if(config.target == "x86_64-w64-mingw32") {
try {
// Iterate over the source directory
for (const auto& entry : fs::directory_iterator("/usr/x86_64-w64-mingw32/bin/")) {
// Check if the file is a regular file and ends with ".dll"
if (fs::is_regular_file(entry) && entry.path().extension() == ".dll") {
// Construct the destination file path
fs::path dest_file = outputDir / entry.path().filename();
// Check if the destination file exists and if it is older than the source file
if (!fs::exists(dest_file) || fs::last_write_time(entry.path()) > 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);
}
}
}
} catch (const fs::filesystem_error& e) {
return {e.what(), false, {}};
}
}
buildResult.result = RunCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
system(std::format("copy \"%LIBCXX_DIR%\\lib\\c++.dll\" \"{}\\c++.dll\"", binDir.string()).c_str());
buildResult.result = RunCommand(std::format("{}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR%\\lib -lc++ -nostdinc++ -nostdlib++", command, files, (binDir/outputName).string()));
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 == CRAFTER_CONFIGURATION_TYPE_LIBRARY) {
} else if(config.type == ConfigurationType::LibraryStatic) {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
buildResult.result = RunCommand(std::format("ar r {}.a {}", (binDir/fs::path(std::string("lib")+outputName)).string(), files));
buildResult.result = RunCommand(std::format("ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
buildResult.result = RunCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (binDir/fs::path(outputName)).string()));
buildResult.result = RunCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string()));
#endif
} else {
buildResult.result = RunCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld", command, files, (binDir/(std::string("lib")+outputName)).string()));
buildResult.result = RunCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
}
}
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));
}
return buildResult;
}
}
int Crafter::Run(int argc, char** argv) {
try {
fs::path projectFile = "./project.cpp";
std::vector<std::string_view> projectArgs;
projectArgs.reserve(argc);
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg.starts_with("--project=")) {
projectFile = arg.substr(std::string_view("--project=").size());
} else {
projectArgs.push_back(arg);
}
}
if (!fs::exists(projectFile)) {
std::println(std::cerr, "No project file at {}", projectFile.string());
return 1;
}
Configuration config = LoadProject(projectFile, projectArgs);
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
std::mutex depMutex;
BuildResult result = Build(config, depResults, depMutex);
if (!result.result.empty()) {
std::println(std::cerr, "{}", result.result);
return 1;
}
return 0;
} catch (const std::exception& e) {
std::println(std::cerr, "{}", e.what());
return 1;
}
}

View file

@ -0,0 +1,251 @@
/*
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
*/
module Crafter.Build:External_impl;
import std;
import :External;
import :Platform;
namespace fs = std::filesystem;
using namespace Crafter;
namespace {
std::string DeriveName(const GitSource& source) {
std::string url = source.url;
while (!url.empty() && (url.back() == '/' || url.back() == '\\')) url.pop_back();
if (url.ends_with(".git")) url.resize(url.size() - 4);
auto slash = url.find_last_of("/\\");
return slash == std::string::npos ? url : url.substr(slash + 1);
}
std::string ShellQuote(std::string_view s) {
std::string out = "'";
for (char c : s) {
if (c == '\'') out += "'\\''";
else out += c;
}
out += "'";
return out;
}
std::string JoinOptions(std::span<const std::string> options) {
std::string out;
for (const std::string& opt : options) {
if (!out.empty()) out += ' ';
out += ShellQuote(opt);
}
return out;
}
std::string FetchGit(const GitSource& source, const fs::path& cloneDir) {
auto runGit = [](std::string_view cmd) -> std::optional<std::string> {
CommandResult r = RunCommandChecked(cmd);
if (r.exitCode != 0) return std::format("`{}` failed (exit {}): {}", cmd, r.exitCode, r.output);
return std::nullopt;
};
if (fs::exists(cloneDir)) {
CommandResult remote = RunCommandChecked(std::format("git -C {} remote get-url origin", ShellQuote(cloneDir.string())));
std::string currentUrl = remote.output;
while (!currentUrl.empty() && (currentUrl.back() == '\n' || currentUrl.back() == '\r')) currentUrl.pop_back();
if (remote.exitCode != 0 || currentUrl != source.url) {
fs::remove_all(cloneDir);
} else if (!source.commit.empty()) {
CommandResult head = RunCommandChecked(std::format("git -C {} rev-parse HEAD", ShellQuote(cloneDir.string())));
std::string currentHead = head.output;
while (!currentHead.empty() && (currentHead.back() == '\n' || currentHead.back() == '\r')) currentHead.pop_back();
if (head.exitCode == 0 && currentHead != source.commit) {
if (auto err = runGit(std::format("git -C {} fetch origin", ShellQuote(cloneDir.string())))) return *err;
if (auto err = runGit(std::format("git -C {} checkout {}", ShellQuote(cloneDir.string()), ShellQuote(source.commit)))) return *err;
}
return "";
} else {
return "";
}
}
fs::path tmpDir = cloneDir;
tmpDir += ".tmp";
if (fs::exists(tmpDir)) fs::remove_all(tmpDir);
if (auto err = runGit(std::format("git clone --recursive {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) {
if (fs::exists(tmpDir)) fs::remove_all(tmpDir);
return *err;
}
if (!source.branch.empty()) {
if (auto err = runGit(std::format("git -C {} switch {}", ShellQuote(tmpDir.string()), ShellQuote(source.branch)))) {
fs::remove_all(tmpDir);
return *err;
}
}
if (!source.commit.empty()) {
if (auto err = runGit(std::format("git -C {} checkout {}", ShellQuote(tmpDir.string()), ShellQuote(source.commit)))) {
fs::remove_all(tmpDir);
return *err;
}
}
std::error_code ec;
fs::rename(tmpDir, cloneDir, ec);
if (ec) {
fs::remove_all(tmpDir);
return std::format("rename {} -> {} failed: {}", tmpDir.string(), cloneDir.string(), ec.message());
}
return "";
}
std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir) {
std::string out;
out += " -DCMAKE_C_COMPILER=clang";
out += " -DCMAKE_CXX_COMPILER=clang++";
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
#endif
fs::path absBuildDir = fs::absolute(cmakeBuildDir);
out += std::format(" -DCMAKE_ARCHIVE_OUTPUT_DIRECTORY={}", ShellQuote(absBuildDir.string()));
out += std::format(" -DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}", ShellQuote(absBuildDir.string()));
return out;
}
std::string ConfigureCMake(const fs::path& cloneDir, const fs::path& cmakeBuildDir, std::span<const std::string> options) {
fs::path optionsFile = cmakeBuildDir / ".crafter-options";
std::string optionsKey = JoinOptions(options);
bool needsConfigure = !fs::exists(cmakeBuildDir / "CMakeCache.txt");
if (!needsConfigure) {
std::ifstream existing(optionsFile);
if (!existing) {
needsConfigure = true;
} else {
std::stringstream buf;
buf << existing.rdbuf();
if (buf.str() != optionsKey) needsConfigure = true;
}
}
if (!needsConfigure) return "";
if (!fs::exists(cmakeBuildDir)) fs::create_directories(cmakeBuildDir);
std::string cmd = std::format("cmake -S {} -B {}{}",
ShellQuote(fs::absolute(cloneDir).string()),
ShellQuote(fs::absolute(cmakeBuildDir).string()),
BuildInjectedCMakeFlags(cmakeBuildDir));
if (!options.empty()) {
cmd += " ";
cmd += optionsKey;
}
CommandResult r = RunCommandChecked(cmd);
if (r.exitCode != 0) {
return std::format("cmake configure failed (exit {}): {}", r.exitCode, r.output);
}
std::ofstream(optionsFile) << optionsKey;
return "";
}
std::string BuildCMake(const fs::path& cmakeBuildDir) {
std::string cmd = std::format("cmake --build {}", ShellQuote(fs::absolute(cmakeBuildDir).string()));
CommandResult r = RunCommandChecked(cmd);
if (r.exitCode != 0) {
return std::format("cmake --build failed (exit {}): {}", r.exitCode, r.output);
}
return "";
}
} // namespace
ExternalBuildResult Crafter::BuildExternal(
const ExternalDependency& dep,
std::atomic<bool>& cancelled) {
ExternalBuildResult result;
if (cancelled.load(std::memory_order_relaxed)) return result;
std::string name = dep.name.empty() ? DeriveName(dep.source) : dep.name;
if (name.empty()) {
result.error = std::format("Could not derive name for external dependency from URL '{}'", dep.source.url);
return result;
}
fs::path externalRoot = GetCacheDir() / "external";
if (!fs::exists(externalRoot)) {
std::error_code ec;
fs::create_directories(externalRoot, ec);
if (ec) {
result.error = std::format("Failed to create {}: {}", externalRoot.string(), ec.message());
return result;
}
}
std::string keyMaterial = std::format("{}|{}|{}|{}",
dep.source.url, dep.source.branch, dep.source.commit, JoinOptions(dep.options));
std::size_t key = std::hash<std::string>{}(keyMaterial);
fs::path cloneDir = externalRoot / std::format("{}-{:016x}", name, key);
std::string fetchErr = FetchGit(dep.source, cloneDir);
if (!fetchErr.empty()) {
result.error = std::format("git fetch for '{}': {}", name, fetchErr);
return result;
}
if (cancelled.load(std::memory_order_relaxed)) return result;
fs::path cmakeBuildDir;
if (dep.builder == ExternalBuilder::CMake) {
cmakeBuildDir = cloneDir / "build";
if (std::string err = ConfigureCMake(cloneDir, cmakeBuildDir, dep.options); !err.empty()) {
result.error = std::format("cmake configure for '{}': {}", name, err);
return result;
}
if (cancelled.load(std::memory_order_relaxed)) return result;
if (std::string err = BuildCMake(cmakeBuildDir); !err.empty()) {
result.error = std::format("cmake build for '{}': {}", name, err);
return result;
}
}
for (const fs::path& include : dep.includeDirs) {
fs::path full = include.empty() ? cloneDir : cloneDir / include;
result.compileFlags.push_back(std::format("-I{}", fs::absolute(full).string()));
}
if (dep.builder == ExternalBuilder::CMake) {
result.linkFlags.push_back(std::format("-L{}", fs::absolute(cmakeBuildDir).string()));
for (const std::string& lib : dep.libs) {
result.linkFlags.push_back(std::format("-l{}", lib));
}
if (fs::exists(cmakeBuildDir)) {
for (const auto& entry : fs::directory_iterator(cmakeBuildDir)) {
if (!entry.is_regular_file()) continue;
const fs::path& p = entry.path();
if (p.extension() != ".a" && p.extension() != ".so") continue;
std::error_code ec;
fs::file_time_type t = entry.last_write_time(ec);
if (!ec && t > result.latestArtifact) result.latestArtifact = t;
}
}
}
return result;
}

View file

@ -20,49 +20,55 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module Crafter.Build:Implementation_impl;
import std;
import :Implementation;
import :Module;
import :Command;
import :Interface;
import :Platform;
namespace fs = std::filesystem;
namespace Crafter {
Implementation::Implementation(fs::path&& path) : path(std::move(path)) {
}
bool Implementation::Check(const fs::path& buildDir, const fs::path& pcmDir) const {
if(fs::exists((buildDir/path.filename()).string()+"_impl.o") && fs::last_write_time(path.string()+".cpp") < fs::last_write_time((buildDir/path.filename()).string()+"_impl.o")) {
for(ModulePartition* dependency : partitionDependencies) {
if(dependency->Check(pcmDir)) {
return true;
}
}
for(Module* dependency : moduleDependencies) {
if(dependency->Check(pcmDir)) {
return true;
}
}
return false;
} else {
bool Implementation::Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor) const {
std::string objPath = (buildDir/path.filename()).string()+"_impl.o";
std::string cppPath = path.string()+".cpp";
if(!fs::exists(objPath) || std::max(fs::last_write_time(cppPath), sourceFloor) >= fs::last_write_time(objPath)) {
return true;
}
fs::file_time_type objTime = fs::last_write_time(objPath);
for(ModulePartition* dependency : partitionDependencies) {
if(dependency->Check(pcmDir, sourceFloor)) return true;
}
for(Module* dependency : moduleDependencies) {
if(dependency->Check(pcmDir, sourceFloor)) return true;
}
for(const auto& [externalMod, externalPcmPath] : externalModuleDependencies) {
std::error_code ec;
fs::file_time_type pcmTime = fs::last_write_time(externalPcmPath, ec);
if (!ec && pcmTime >= objTime) return true;
}
return false;
}
void Implementation::Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const {
for(ModulePartition* dependency : partitionDependencies) {
if(!dependency->compiled.load()) {
dependency->compiled.wait(true);
dependency->compiled.wait(false);
}
}
for(Module* dependency : moduleDependencies) {
if(!dependency->compiled.load()) {
dependency->compiled.wait(true);
dependency->compiled.wait(false);
}
}
if (!buildCancelled.load(std::memory_order_relaxed)) {
if (buildCancelled.load(std::memory_order_relaxed)) {
return;
}
std::string result = RunCommand(std::format("{} {}.cpp -c -o {}_impl.o", clang, path.string(), (buildDir/path.filename()).string()));
bool expected = false;
if(!result.empty() && buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
}
}

View file

@ -20,30 +20,41 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
module Crafter.Build:Interface_impl;
import std;
import :Interface;
import :Command;
import :Platform;
namespace fs = std::filesystem;
namespace Crafter {
ModulePartition::ModulePartition(std::string&& name, fs::path&& path) : name(std::move(name)), path(std::move(path)), compiled(false), checked(false) {}
bool ModulePartition::Check(const fs::path& pcmDir) {
bool ModulePartition::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
if(!checked) {
checked = true;
if(fs::exists((pcmDir/path.filename()).generic_string()+".pcm") && fs::last_write_time(path.generic_string()+".cppm") < fs::last_write_time((pcmDir/path.filename()).generic_string()+".pcm")) {
std::string pcmPath = (pcmDir/path.filename()).generic_string()+".pcm";
std::string cppmPath = path.generic_string()+".cppm";
if(fs::exists(pcmPath) && std::max(fs::last_write_time(cppmPath), sourceFloor) < fs::last_write_time(pcmPath)) {
fs::file_time_type pcmTime = fs::last_write_time(pcmPath);
for(ModulePartition* dependency : partitionDependencies) {
if(dependency->Check(pcmDir)) {
if(dependency->Check(pcmDir, sourceFloor)) {
needsRecompiling = true;
return true;
}
}
for(Module* dependency : moduleDependencies) {
if(dependency->Check(pcmDir)) {
if(dependency->Check(pcmDir, sourceFloor)) {
needsRecompiling = true;
return true;
}
}
for(const auto& [externalMod, externalPcmPath] : externalModuleDependencies) {
std::error_code ec;
fs::file_time_type t = fs::last_write_time(externalPcmPath, ec);
if (!ec && t >= pcmTime) {
needsRecompiling = true;
return true;
}
}
needsRecompiling = false;
compiled.store(CRAFTER_COMPILE_STATUS_COMPLETED);
compiled.store(true);
return false;
} else {
needsRecompiling = true;
@ -54,10 +65,15 @@ namespace Crafter {
}
}
void ModulePartition::Compile(const std::string_view clang, const fs::path& pcmDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
void ModulePartition::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
for(ModulePartition* dependency : partitionDependencies) {
if(!dependency->compiled.load()) {
dependency->compiled.wait(true);
dependency->compiled.wait(false);
}
}
for(Module* dependency : moduleDependencies) {
if(!dependency->compiled.load()) {
dependency->compiled.wait(false);
}
}
@ -70,7 +86,8 @@ namespace Crafter {
std::string result = RunCommand(std::format("{} {}.cppm --precompile -o {}.pcm", clang, path.string(), (pcmDir/path.filename()).string()));
if (!result.empty()) {
if (buildCancelled.compare_exchange_strong(false, true)) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
compiled.store(true);
compiled.notify_all();
@ -81,10 +98,11 @@ namespace Crafter {
compiled.store(true);
compiled.notify_all();
result = RunClang(std::format("{} -Wno-unused-command-line-argument {}.pcm -c -o {}.o", clang, (pcmDir/path.filename()).string(), (buildDir/path.filename()).string()));
result = RunCommand(std::format("{} -Wno-unused-command-line-argument {}.pcm -c -o {}.o", clang, (pcmDir/path.filename()).string(), (buildDir/path.filename()).string()));
if (!result.empty()) {
if (buildCancelled.compare_exchange_strong(false, true)) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
}
@ -92,13 +110,15 @@ namespace Crafter {
Module::Module(std::string&& name, fs::path&& path) : name(std::move(name)), path(std::move(path)), compiled(false), checked(false) {}
bool Module::Check(const fs::path& pcmDir) {
bool Module::Check(const fs::path& pcmDir, fs::file_time_type sourceFloor) {
if(!checked) {
checked = true;
if(fs::exists((pcmDir/path.filename()).generic_string()+".pcm") && fs::last_write_time(path.generic_string()+".cppm") < fs::last_write_time((pcmDir/path.filename()).generic_string()+".pcm")) {
std::string pcmPath = (pcmDir/path.filename()).generic_string()+".pcm";
std::string cppmPath = path.generic_string()+".cppm";
if(fs::exists(pcmPath) && std::max(fs::last_write_time(cppmPath), sourceFloor) < fs::last_write_time(pcmPath)) {
bool depCheck = false;
for(std::unique_ptr<ModulePartition>& partition : partitions) {
if(partition->Check(pcmDir)) {
if(partition->Check(pcmDir, sourceFloor)) {
depCheck = true;
}
}
@ -112,7 +132,7 @@ namespace Crafter {
}
} else {
for(std::unique_ptr<ModulePartition>& partition : partitions) {
partition->Check(pcmDir);
partition->Check(pcmDir, sourceFloor);
}
needsRecompiling = true;
return true;
@ -124,10 +144,10 @@ namespace Crafter {
void Module::Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) {
std::vector<std::thread> threads;
threads.reserve(interface.partitions.size());
for(std::unique_ptr<ModulePartition>& part : interface.partitions) {
if(part.needsRecompiling) {
threads.emplace_back(&ModulePartition::Compile, part.get(), clang, pcmDir, buildDir, buildCancelled, buildError);
threads.reserve(partitions.size());
for(std::unique_ptr<ModulePartition>& part : partitions) {
if(part->needsRecompiling) {
threads.emplace_back(&ModulePartition::Compile, part.get(), clang, pcmDir, buildDir, std::ref(buildCancelled), std::ref(buildError));
}
}
@ -144,7 +164,8 @@ namespace Crafter {
std::string result = RunCommand(std::format("{} {}.cppm --precompile -o {}.pcm", clang, path.string(), (pcmDir/path.filename()).string()));
if (!result.empty()) {
if (buildCancelled.compare_exchange_strong(false, true)) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
compiled.store(true);
compiled.notify_all();
@ -158,7 +179,8 @@ namespace Crafter {
result = RunCommand(std::format("{} -Wno-unused-command-line-argument {}.pcm -c -o {}.o", clang, (pcmDir/path.filename()).string(), (buildDir/path.filename()).string()));
if (!result.empty()) {
if (buildCancelled.compare_exchange_strong(false, true)) {
bool expected = false;
if (buildCancelled.compare_exchange_strong(expected, true)) {
buildError = std::move(result);
}
}

View file

@ -16,8 +16,19 @@ 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
*/
export module Crafter.Build:Platform_impl;
module;
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
#include <windows.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
#include <unistd.h>
#include <dlfcn.h>
#include <sys/wait.h>
#endif
module Crafter.Build:Platform_impl;
import std;
import :Platform;
import :Clang;
@ -26,7 +37,7 @@ using namespace Crafter;
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
std::string RunCommand(const std::string_view cmd) {
std::string Crafter::RunCommand(const std::string_view cmd) {
std::array<char, 128> buffer;
std::string result;
@ -46,17 +57,36 @@ std::string RunCommand(const std::string_view cmd) {
return result;
}
fs::path BuildStdPcm(Configuration& config, fs::path stdPcm) {
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{0, ""};
std::string with = "cmd /C \"" + std::string(cmd) + " 2>&1\"";
FILE* pipe = _popen(with.c_str(), "r");
if (!pipe) {
throw std::runtime_error("_popen() failed!");
}
while (fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
result.output += buffer.data();
}
result.exitCode = _pclose(pipe);
return result;
}
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
std::string libcxx = std::getenv("LIBCXX_DIR");
std::string stdPcm = std::format("{}\\{}{}\\std.pcm", exeDir.string(), config.target, config.march);
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));
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 GetCacheDir() {
fs::path Crafter::GetCacheDir() {
if (const char* local = std::getenv("LOCALAPPDATA")) {
return fs::path(local) / "crafter.build";
}
@ -64,14 +94,121 @@ fs::path GetCacheDir() {
throw std::runtime_error("LOCALAPPDATA not set");
}
std::string GetBaseCommand(Configuration& config) {
std::string Crafter::GetBaseCommand(const Configuration& config) {
return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1");
}
namespace {
constexpr std::array<std::string_view, 7> kCrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
"Crafter.Build-Implementation",
"Crafter.Build-External",
"Crafter.Build-Clang",
"Crafter.Build",
};
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
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-pc-windows-msvc -march=native -mtune=native "
"-std=c++26 -O3 "
"-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
"-fprebuilt-module-path={} "
"--precompile {} -o {}",
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));
const char* envHome = std::getenv("CRAFTER_BUILD_HOME");
fs::path sourceDir = envHome
? fs::path(envHome)
: hostExe.parent_path().parent_path() / "share" / "crafter-build";
Configuration hostConfig;
hostConfig.target = "x86_64-pc-windows-msvc";
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) {
fs::path crafterBuildLib = hostExe.parent_path() / "crafter-build.lib";
std::string compileCmd = std::format(
"clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native "
"-std=c++26 -shared -O3 -Wno-return-type-c-linkage "
"-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 "
"-fprebuilt-module-path={} "
"{} {} -o {} -L %LIBCXX_DIR%\\lib -lc++",
cacheDir.string(),
absProject.string(), crafterBuildLib.string(), dllPath.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
std::string RunCommand(const std::string_view cmd) {
std::string Crafter::RunCommand(const std::string_view cmd) {
std::array<char, 128> buffer;
std::string result;
@ -90,7 +227,30 @@ std::string RunCommand(const std::string_view cmd) {
return result;
}
fs::path BuildStdPcm(Configuration& config, fs::path stdPcm) {
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{0, ""};
std::string with = std::string(cmd) + " 2>&1";
FILE* pipe = popen(with.c_str(), "r");
if (!pipe) throw std::runtime_error("popen() failed!");
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
result.output += buffer.data();
}
int status = pclose(pipe);
if (WIFEXITED(status)) {
result.exitCode = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
result.exitCode = 128 + WTERMSIG(status);
} else {
result.exitCode = -1;
}
return result;
}
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
if(config.target == "x86_64-w64-mingw32") {
std::vector<std::string> folders;
@ -111,57 +271,139 @@ fs::path BuildStdPcm(Configuration& config, fs::path stdPcm) {
fs::path stdCc = fs::path(std::format("/usr/x86_64-w64-mingw32/include/c++/{}/bits/std.cc", mingWversion));
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) {
std::string result = RunCommand(std::format("cp {} {}/{}{}/std.cppm\nclang++ --target={} -march={} -mtune={} -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {}/{}{}/std.cppm -o {}", stdCc.string(), exeDir.string(), config.target, config.march, config.target, config.march, config.mtune, exeDir.string(), config.target, config.march, stdPcm));
if(!result.empty()) {
return result;
}
}
try {
// Iterate over the source directory
for (const auto& entry : fs::directory_iterator("/usr/x86_64-w64-mingw32/bin/")) {
// Check if the file is a regular file and ends with ".dll"
if (fs::is_regular_file(entry) && entry.path().extension() == ".dll") {
// Construct the destination file path
fs::path dest_file = project.binDir / config.name / entry.path().filename();
// Check if the destination file exists and if it is older than the source file
if (!fs::exists(dest_file) || fs::last_write_time(entry.path()) > 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);
}
}
}
} catch (const fs::filesystem_error& e) {
return e.what();
return RunCommand(std::format("cp {} {}/std.cppm\nclang++ --target={} -march={} -mtune={} -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {}/std.cppm -o {}", stdCc.string(), stdPcm.parent_path().string(), config.target, config.march, config.mtune, stdPcm.parent_path().string(), stdPcm.string()));
} else {
return "";
}
} else {
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time("/usr/share/libc++/v1/std.cppm")) {
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++ -march={} -mtune={} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile /usr/share/libc++/v1/std.cppm -o {}", config.target, config.march, config.mtune, stdPcm));
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++ -march={} -mtune={} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile /usr/share/libc++/v1/std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
} else {
return "";
}
}
}
fs::path GetCacheDir() {
fs::path Crafter::GetCacheDir() {
if (const char* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
return fs::path(xdg) / "crafter.build";
}
if (const char* home = std::getenv("HOME")) {
return fs::path(home) / ".cache" / "crafter.build";
}
throw std::runtime_error("Neither XDG_CACHE_HOME nor HOME set");
}
std::string GetBaseCommand(Configuration& config) {
std::string Crafter::GetBaseCommand(const Configuration& config) {
std::string stdlib;
if(config.target == "x86_64-w64-mingw32") {
stdlib = "";
} else {
stdlib = "-stdlib=libc++";
}
std::string command = std::format("clang++ {}", stdlib);
return std::format("clang++ {}", stdlib);
}
namespace {
constexpr std::array<std::string_view, 7> kCrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
"Crafter.Build-Implementation",
"Crafter.Build-External",
"Crafter.Build-Clang",
"Crafter.Build",
};
void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) {
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-pc-linux-gnu -march=native -mtune=native "
"-std=c++26 -stdlib=libc++ -O3 "
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
"-fprebuilt-module-path={} "
"--precompile {} -o {}",
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 soPath = buildDir / (absProject.stem().string() + ".so");
fs::path hostExe = fs::read_symlink("/proc/self/exe");
const char* envHome = std::getenv("CRAFTER_BUILD_HOME");
fs::path sourceDir = envHome
? fs::path(envHome)
: hostExe.parent_path().parent_path() / "share" / "crafter-build";
Configuration hostConfig;
hostConfig.target = "x86_64-pc-linux-gnu";
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(soPath)
|| fs::last_write_time(soPath) < fs::last_write_time(absProject)
|| fs::last_write_time(soPath) < fs::last_write_time(hostExe);
if (stale) {
std::string compileCmd = std::format(
"clang++ --target=x86_64-pc-linux-gnu -march=native -mtune=native "
"-std=c++26 -stdlib=libc++ -shared -fPIC -O3 "
"-Wno-return-type-c-linkage "
"-fprebuilt-module-path={} "
"{} -o {}",
cacheDir.string(),
absProject.string(), soPath.string());
std::string result = RunCommand(compileCmd);
if (!result.empty()) {
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
}
}
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
if (!handle) {
throw std::runtime_error(std::format("Failed to load project {}: {}", soPath.string(), dlerror()));
}
using ProjectFn = Configuration (*)(std::span<const std::string_view>);
dlerror();
auto fn = reinterpret_cast<ProjectFn>(dlsym(handle, "CrafterBuildProject"));
if (const char* err = dlerror()) {
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: {}", soPath.string(), err));
}
return fn(args);
}
#endif

View file

@ -27,14 +27,37 @@ import :Shader;
import std;
namespace fs = std::filesystem;
namespace {
EShLanguage ToEShLanguage(Crafter::ShaderType t) {
switch (t) {
case Crafter::ShaderType::Vertex: return EShLangVertex;
case Crafter::ShaderType::TessControl: return EShLangTessControl;
case Crafter::ShaderType::TessEvaluation: return EShLangTessEvaluation;
case Crafter::ShaderType::Geometry: return EShLangGeometry;
case Crafter::ShaderType::Fragment: return EShLangFragment;
case Crafter::ShaderType::Compute: return EShLangCompute;
case Crafter::ShaderType::RayGen: return EShLangRayGen;
case Crafter::ShaderType::Intersect: return EShLangIntersect;
case Crafter::ShaderType::AnyHit: return EShLangAnyHit;
case Crafter::ShaderType::ClosestHit: return EShLangClosestHit;
case Crafter::ShaderType::Miss: return EShLangMiss;
case Crafter::ShaderType::Callable: return EShLangCallable;
case Crafter::ShaderType::Task: return EShLangTask;
case Crafter::ShaderType::Mesh: return EShLangMesh;
}
return EShLangVertex;
}
}
namespace Crafter {
Shader::Shader(fs::path&& path, std::string&& entrypoint, EShLanguage type) : path(std::move(path)), entrypoint(std::move(entrypoint)), type(type) {
Shader::Shader(fs::path&& path, std::string&& entrypoint, ShaderType type) : path(std::move(path)), entrypoint(std::move(entrypoint)), type(type) {
}
bool Shader::Check(const fs::path& outputDir) const {
return fs::exists((outputDir/path.filename()).generic_string()+".spv") && fs::last_write_time(path.generic_string()+".glsl") < fs::last_write_time((outputDir/path.filename()).generic_string()+".spv");
}
std::string Shader::Compile(const fs::path& outputDir) const {
EShLanguage glslangType = ToEShLanguage(type);
glslang::InitializeProcess();
EShMessages messages = static_cast<EShMessages>(EShMsgDefault | EShMsgVulkanRules | EShMsgSpvRules);
std::ifstream fileStream(path, std::ios::in | std::ios::binary);
@ -44,11 +67,11 @@ namespace Crafter {
std::ostringstream contents;
contents << fileStream.rdbuf();
std::string src = contents.str();
const char *file_name_list[1] = {""};
const char *shader_source = reinterpret_cast<const char *>(src.data());
glslang::TShader shader(type);
glslang::TShader shader(glslangType);
shader.setStringsWithLengthsAndNames(&shader_source, nullptr, file_name_list, 1);
shader.setEntryPoint(entrypoint.c_str());
shader.setSourceEntryPoint(entrypoint.c_str());
@ -63,30 +86,30 @@ namespace Crafter {
// Add shader to new program object.
glslang::TProgram program;
program.addShader(&shader);
// Link program.
if (!program.link(messages))
{
info_log = std::string(program.getInfoLog()) + std::string(program.getInfoDebugLog());
}
// Save any info log that was generated.
if (shader.getInfoLog())
{
info_log += std::string(shader.getInfoLog()) + std::string(shader.getInfoDebugLog());
}
if (program.getInfoLog())
{
info_log += std::string(program.getInfoLog()) + std::string(program.getInfoDebugLog());
}
glslang::TIntermediate* intermediate = program.getIntermediate(type);
glslang::TIntermediate* intermediate = program.getIntermediate(glslangType);
if (!intermediate)
{
info_log += "Failed to get shared intermediate code.";
}
spv::SpvBuildLogger logger;
std::vector<std::uint32_t> spirv;
glslang::GlslangToSpv(*intermediate, spirv, &logger);

View file

@ -0,0 +1,25 @@
/*
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
*/
import std;
import Crafter.Build;
int main(int argc, char** argv) {
return Crafter::Run(argc, argv);
}