diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 5642d53..c17f6db 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -62,7 +62,9 @@ void Configuration::GetInterfacesAndImplementations(std::span interfac std::vector> tempModulePaths = std::vector>(interfaces.size()); for(std::uint16_t i = 0; i < interfaces.size(); i++){ - fs::path file = path / interfaces[i]; + // Resolve to absolute now so the stored path survives cwd changes + // (matters for GitProject deps loaded from a different working dir). + fs::path file = fs::absolute(path / interfaces[i]).lexically_normal(); file += ".cppm"; std::ifstream t(file); std::stringstream buffer; @@ -136,7 +138,7 @@ void Configuration::GetInterfacesAndImplementations(std::span interfac } for(const fs::path& tempFile : implementations) { - fs::path file = path / tempFile; + fs::path file = fs::absolute(path / tempFile).lexically_normal(); file += ".cpp"; std::ifstream t(file); std::stringstream buffer; @@ -232,8 +234,8 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map fs::last_write_time(objPath)) { + if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) { threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled, &config]() { Progress::Task task(std::format("Compiling {}.c", cFile.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; @@ -540,7 +542,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map fs::last_write_time(objPath)) { + if (!fs::exists(objPath) || (fs::exists(srcPath) && fs::last_write_time(srcPath) > fs::last_write_time(objPath))) { threads.emplace_back([&cFile, &buildDir, &buildError, &buildCancelled]() { Progress::Task task(std::format("Compiling {}.cu", cFile.filename().string())); if (buildCancelled.load(std::memory_order_relaxed)) return; @@ -669,7 +671,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map fs::path { - fs::path dir = c.path/"bin"/std::format("{}-{}-{}", c.name, c.target, c.march); + fs::path dir = c.BinDir(); if (c.type == ConfigurationType::Executable) { if (c.target.starts_with("wasm32")) return dir / (c.outputName + ".wasm"); @@ -719,7 +721,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map 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); + fs::path depDir = dep->BinDir(); // 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` @@ -761,7 +763,7 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map 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); + fs::path depDir = dep->BinDir(); for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) { fs::path src = depDir / fname; @@ -863,19 +865,25 @@ std::string Crafter::HostTarget() { return cached; } -void Crafter::ApplyStandardArgs(Configuration& cfg, std::span args) { +ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span args) { if (const char* envMarch = std::getenv("CRAFTER_BUILD_MARCH"); envMarch && *envMarch) { cfg.march = envMarch; } if (const char* envMtune = std::getenv("CRAFTER_BUILD_MTUNE"); envMtune && *envMtune) { cfg.mtune = envMtune; } + bool sawLib = false, sawShared = false; for (std::string_view a : args) { if (a == "--debug") cfg.debug = true; + else if (a == "--lib") sawLib = true; + else if (a == "--shared") sawShared = true; else if (a.starts_with("--target=")) cfg.target = std::string(a.substr(std::string_view("--target=").size())); else if (a.starts_with("--march=")) cfg.march = std::string(a.substr(std::string_view("--march=").size())); else if (a.starts_with("--mtune=")) cfg.mtune = std::string(a.substr(std::string_view("--mtune=").size())); } + if (sawLib && cfg.type == ConfigurationType::Executable) cfg.type = ConfigurationType::LibraryStatic; + if (sawShared && cfg.type == ConfigurationType::LibraryStatic) cfg.type = ConfigurationType::LibraryDynamic; + return ArgQuery{args}; } static void PrintHelp(std::string_view argv0) { @@ -1015,7 +1023,7 @@ int Crafter::Run(int argc, char** argv) { std::println(std::cerr, "-r: cannot run a library"); return 1; } - fs::path dir = config.path / "bin" / std::format("{}-{}-{}", config.name, config.target, config.march); + fs::path dir = config.BinDir(); fs::path artifact = dir / config.outputName; if (config.target.starts_with("wasm32")) { artifact += ".wasm"; diff --git a/implementations/Crafter.Build-External.cpp b/implementations/Crafter.Build-External.cpp index 57d2829..d9dbba5 100644 --- a/implementations/Crafter.Build-External.cpp +++ b/implementations/Crafter.Build-External.cpp @@ -21,6 +21,8 @@ module Crafter.Build:External_impl; import std; import :External; import :Platform; +import :Progress; +import :Clang; namespace fs = std::filesystem; using namespace Crafter; @@ -76,6 +78,14 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) { } return ""; } else { + // No commit pinned — pull latest. The user opted into "track latest" + // by not pinning; the cost is one network round-trip per build. + // Pin source.commit to a SHA for reproducible, offline-capable builds. + if (!source.branch.empty()) { + if (auto err = runGit(std::format("git -C {} pull origin {}", ShellQuote(cloneDir.string()), ShellQuote(source.branch)))) return *err; + } else { + if (auto err = runGit(std::format("git -C {} pull", ShellQuote(cloneDir.string())))) return *err; + } return ""; } } @@ -265,3 +275,131 @@ ExternalBuildResult Crafter::BuildExternal( return result; } + +namespace { + std::mutex projectCacheMutex; + std::unordered_map> projectCache; + + // Run the dep's project.cpp from its own directory so its relative paths + // (cfg.path = "./", interfaces/, lib/, ...) resolve against the dep + // root, then rebase cfg.path and all other relative path collections to + // absolute so Build() can run from any cwd without misresolving them. + // The mutex is NOT held across LoadProject — the dep may itself call + // GitProject/LocalProject, and a non-recursive lock would deadlock. + Configuration LoadProjectFromRoot(const fs::path& projectPath, const fs::path& depRoot, std::span args) { + fs::path savedCwd = fs::current_path(); + Configuration cfg; + fs::current_path(depRoot); + try { + cfg = LoadProject(projectPath, args); + } catch (...) { + fs::current_path(savedCwd); + throw; + } + fs::current_path(savedCwd); + cfg.path = fs::absolute(depRoot / cfg.path).lexically_normal(); + auto abs = [&](const fs::path& p) -> fs::path { + return p.is_absolute() ? p : fs::absolute(depRoot / p).lexically_normal(); + }; + for (fs::path& p : cfg.cFiles) p = abs(p); + for (fs::path& p : cfg.cuda) p = abs(p); + for (fs::path& p : cfg.files) p = abs(p); + for (Shader& s : cfg.shaders) s.path = abs(s.path); + return cfg; + } +} + +Configuration* Crafter::GitProject(const GitProjectSpec& spec) { + // Two hashes: the clone is shared across specs that point at the same + // source revision (forwarded args don't change what gets cloned, just + // how the dep's project.cpp interprets them at runtime), while the + // in-memory Configuration is keyed by the full spec so each (args) + // permutation gets its own Configuration object. + std::string srcKey = std::format("git|{}|{}|{}|{}", + spec.source.url, spec.source.branch, spec.source.commit, spec.projectFile.string()); + std::string srcHash = std::format("{:016x}", std::hash{}(srcKey)); + + std::string fullKey = srcKey; + for (const std::string& a : spec.args) { + fullKey += '|'; + fullKey += a; + } + std::string fullHash = std::format("{:016x}", std::hash{}(fullKey)); + + { + std::lock_guard lock(projectCacheMutex); + if (auto it = projectCache.find(fullHash); it != projectCache.end()) { + return it->second.get(); + } + } + + std::string name = DeriveName(spec.source); + if (name.empty()) { + throw std::runtime_error(std::format("GitProject: could not derive name from URL '{}'", spec.source.url)); + } + + fs::path externalRoot = GetCacheDir() / "external"; + fs::create_directories(externalRoot); + fs::path cloneDir = externalRoot / std::format("{}-{}", name, srcHash); + + { + bool exists = fs::exists(cloneDir); + Progress::Task task(std::format("{} {}", exists ? "Updating" : "Cloning", name)); + if (std::string err = FetchGit(spec.source, cloneDir); !err.empty()) { + throw std::runtime_error(std::format("GitProject({}): {}", spec.source.url, err)); + } + } + + fs::path projectPath = cloneDir / spec.projectFile; + if (!fs::exists(projectPath)) { + throw std::runtime_error(std::format( + "GitProject({}): {} not found in clone", spec.source.url, spec.projectFile.string())); + } + + std::vector argViews(spec.args.begin(), spec.args.end()); + Configuration cfg = LoadProjectFromRoot(projectPath, cloneDir, argViews); + + std::lock_guard lock(projectCacheMutex); + if (auto it = projectCache.find(fullHash); it != projectCache.end()) { + return it->second.get(); + } + auto stored = std::make_unique(std::move(cfg)); + Configuration* ptr = stored.get(); + projectCache.emplace(std::move(fullHash), std::move(stored)); + return ptr; +} + +Configuration* Crafter::LocalProject(const LocalProjectSpec& spec) { + fs::path projectPath = fs::absolute(spec.projectFile).lexically_normal(); + if (!fs::exists(projectPath)) { + throw std::runtime_error(std::format("LocalProject: {} not found", projectPath.string())); + } + fs::path canonical = fs::canonical(projectPath); + + std::string fullKey = std::format("local|{}", canonical.string()); + for (const std::string& a : spec.args) { + fullKey += '|'; + fullKey += a; + } + std::string fullHash = std::format("{:016x}", std::hash{}(fullKey)); + + { + std::lock_guard lock(projectCacheMutex); + if (auto it = projectCache.find(fullHash); it != projectCache.end()) { + return it->second.get(); + } + } + + fs::path depRoot = canonical.parent_path(); + std::vector argViews(spec.args.begin(), spec.args.end()); + Configuration cfg = LoadProjectFromRoot(canonical, depRoot, argViews); + + std::lock_guard lock(projectCacheMutex); + if (auto it = projectCache.find(fullHash); it != projectCache.end()) { + return it->second.get(); + } + auto stored = std::make_unique(std::move(cfg)); + Configuration* ptr = stored.get(); + projectCache.emplace(std::move(fullHash), std::move(stored)); + return ptr; +} diff --git a/implementations/Crafter.Build-Test.cpp b/implementations/Crafter.Build-Test.cpp index 67a2b09..8ad80fb 100644 --- a/implementations/Crafter.Build-Test.cpp +++ b/implementations/Crafter.Build-Test.cpp @@ -33,7 +33,7 @@ namespace { } fs::path TestBinaryPath(const Configuration& cfg) { - fs::path outputDir = cfg.path/"bin"/std::format("{}-{}-{}", cfg.name, cfg.target, cfg.march); + fs::path outputDir = cfg.BinDir(); return outputDir / (TargetIsWindows(cfg.target) ? cfg.outputName + ".exe" : cfg.outputName); } diff --git a/interfaces/Crafter.Build-Clang.cppm b/interfaces/Crafter.Build-Clang.cppm index bdb49d1..7d22d72 100644 --- a/interfaces/Crafter.Build-Clang.cppm +++ b/interfaces/Crafter.Build-Clang.cppm @@ -117,10 +117,35 @@ export namespace Crafter { std::vector linkFlags; std::vector tests; CRAFTER_API void GetInterfacesAndImplementations(std::span interfaces, std::span implementations); + // Suffix that uniquely identifies this Configuration's compile state. + // target+march+mtune are spelled out for readability; the rest + // (type, debug, sysroot, defines, compileFlags) collapse into a short + // hash so two Configurations sharing a path but with different + // compile state can't clobber each other's outputs. + std::string VariantId() const { + std::string compileKey; + compileKey += std::to_string(static_cast(type)); + compileKey += '|'; + compileKey += debug ? '1' : '0'; + compileKey += '|'; + compileKey += sysroot; + for (const Define& d : defines) { + compileKey += "|D:"; + compileKey += d.name; + compileKey += '='; + compileKey += d.value; + } + for (const std::string& f : compileFlags) { + compileKey += "|F:"; + compileKey += f; + } + std::size_t configHash = std::hash{}(compileKey); + return std::format("{}-{}-{}-{}-{:08x}", name, target, march, mtune, configHash); + } + fs::path BuildDir() const { return path / "build" / VariantId(); } + fs::path BinDir() const { return path / "bin" / VariantId(); } fs::path PcmDir() const { - return path - / (type == ConfigurationType::Executable ? "build" : "bin") - / std::format("{}-{}-{}", name, target, march); + return type == ConfigurationType::Executable ? BuildDir() : BinDir(); } }; @@ -143,14 +168,39 @@ export namespace Crafter { // outputName so renaming the binary later requires another call. CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg); + // View over the project's args with simple query helpers — kept inline + // so it works across the DLL boundary without an extra ABI hop. Use + // Has(flag) for boolean switches and Get(prefix) for valued options + // (e.g. Get("--prefix=") returns the substring after the equals). + struct ArgQuery { + std::span args; + bool Has(std::string_view flag) const { + for (std::string_view a : args) if (a == flag) return true; + return false; + } + std::optional Get(std::string_view prefix) const { + for (std::string_view a : args) { + if (a.starts_with(prefix)) return std::string(a.substr(prefix.size())); + } + return std::nullopt; + } + }; + // Apply the framework's standard CLI args + env vars onto cfg: // --debug cfg.debug = true // --target= cfg.target = // --march= cfg.march = // --mtune= cfg.mtune = + // --lib cfg.type promoted Executable → LibraryStatic + // --shared cfg.type promoted LibraryStatic → LibraryDynamic + // Promotions chain in priority order so `--lib --shared` lands on + // LibraryDynamic regardless of arg order; each is a no-op when the + // baseline doesn't match (e.g. --shared on an Executable, or --lib on a + // pre-set library). // $CRAFTER_BUILD_MARCH / $CRAFTER_BUILD_MTUNE seed march/mtune. // Env applies first, then args, so CLI wins over env wins over caller's - // pre-set defaults. Project-specific flags (e.g. --shared, --timing) are - // not handled here — parse them in your own loop alongside this call. - CRAFTER_API void ApplyStandardArgs(Configuration& cfg, std::span args); + // pre-set defaults. Returns an ArgQuery over the same span so projects + // can query their own flags (`--timing`, ...) without re-rolling the + // for-arg-in-args loop. + CRAFTER_API ArgQuery ApplyStandardArgs(Configuration& cfg, std::span args); } \ No newline at end of file diff --git a/interfaces/Crafter.Build-External.cppm b/interfaces/Crafter.Build-External.cppm index 70379fb..64303da 100644 --- a/interfaces/Crafter.Build-External.cppm +++ b/interfaces/Crafter.Build-External.cppm @@ -24,6 +24,8 @@ import std; namespace fs = std::filesystem; export namespace Crafter { + struct Configuration; + struct GitSource { std::string url; std::string branch; @@ -55,4 +57,37 @@ export namespace Crafter { const ExternalDependency& dep, std::string_view target, std::atomic& cancelled); + + // Specification for a sibling crafter-build project to fetch and depend on. + // GitSource picks the revision: leave branch + commit empty for the + // remote's default branch HEAD (sloppy but convenient), set branch to + // track a branch tip, or set commit to pin to an immutable SHA. args are + // forwarded verbatim to the dep's CrafterBuildProject — typically you + // pass through the parent's args so target/march/debug propagate. + struct GitProjectSpec { + GitSource source; + fs::path projectFile = "project.cpp"; + std::vector args; + }; + + // Clones the spec's git URL into the per-project external cache (keyed by + // url+branch+commit+args+projectFile), recursively LoadProject's the + // remote's project.cpp, and returns a stable Configuration* you can drop + // into cfg.dependencies. The Configuration is owned by an internal cache + // for the lifetime of the build; identical specs share one Configuration. + CRAFTER_API Configuration* GitProject(const GitProjectSpec& spec); + + // Specification for a local sibling crafter-build project (e.g. an + // example folder depending on its parent project, or a workspace where + // multiple projects live side-by-side without going through git). + struct LocalProjectSpec { + fs::path projectFile; // relative to cwd or absolute + std::vector args; // forwarded to dep's CrafterBuildProject + }; + + // Same as GitProject but for a local on-disk project — no fetch, no + // cache copy. Resolves projectFile to its canonical absolute path and + // recursively LoadProject's it. Returns a stable Configuration* owned + // by the same internal cache as GitProject. + CRAFTER_API Configuration* LocalProject(const LocalProjectSpec& spec); }