// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® export module Crafter.Build:Platform; import std; namespace fs = std::filesystem; namespace Crafter { struct Configuration; struct CommandResult { std::int32_t exitCode = 0; std::string output; bool crashed = false; bool timedOut = false; std::int32_t signal = 0; }; // An exclusive lock on the file `path` (created if missing), held from // construction to destruction. An flock, so it keeps out the other // threads of this process (each opens the file anew) and other // crafter-build processes alike. The OS drops it when the descriptor // closes, process death included, so an interrupted build never leaves a // stale lock; the descriptor is close-on-exec, so the tools a holder // spawns don't keep it past its release. class FileLock { std::int32_t fd = -1; public: explicit FileLock(const fs::path& path); ~FileLock(); FileLock(const FileLock&) = delete; FileLock& operator=(const FileLock&) = delete; }; std::string BuildStdPcm(const Configuration& config, fs::path stdPcm); fs::path GetCacheDir(); CommandResult RunCommandChecked(std::string_view command); // A compile / link / archive step. Success is the command's exit status, // never whether it printed anything: a warning does not stop the compiler // from writing a valid object, and warnings are only emitted when a // translation unit is actually recompiled — so keying failure off the // output made the same unchanged source pass or fail depending on whether // its object happened to be up to date. // // Returns an empty string on success (any warnings go to the user via // Progress::Diagnostic), otherwise the compiler's diagnostics. Callers // therefore keep the `if (!result.empty())` shape they already had. std::string RunBuildCommand(std::string_view command); export CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout); // Runs `command` through sh with this process's own stdin, stdout and // stderr, so its output appears as it is produced and it can prompt. // Every helper above captures instead, which suits a compile step and // not a command that runs for half an hour. Returns the exit code, or // 128+signal when the command was killed; a nonzero exit is the caller's // to judge and never throws. While it runs, Ctrl-C goes to the command // and not to crafter-build, which then sees it as exit code 130. // Project scripts reach this through ScriptContext::Run. std::int32_t RunCommandAttached(std::string_view command); // `s` as one single-quoted sh word, safe to splice into a command line // whatever it contains: paths with spaces, arguments a user typed. export std::string ShellQuote(std::string_view s); std::string GetBaseCommand(const Configuration& config); export Configuration LoadProject(const fs::path& projectFile, std::span args); // A dependency the project file currently running its CrafterBuildProject // declared with `#pragma crafter dependency`, so it can also be wired // into the build graph (a test that links the same module, say) without // spelling the source twice. `name` is the dependency's Configuration // name or any module it exports — whatever the project file `import`s. // Throws when no such dependency was declared, or when called from // anywhere other than inside LoadProject. export Configuration* ProjectDependency(std::string_view name); // True while the project file crafter-build was pointed at runs its // CrafterBuildProject, and when no project file is running at all; false // inside a dependency's project file (a `#pragma crafter dependency`, // GitProject or LocalProject target). Wire anything only the project's // own build or test run needs behind it. The case that forced it: a // library's test links crafter-build itself, so when crafter-build loads // that library as a dependency, the test's GitProject loads crafter-build's // project file in turn and the two recurse until the cycle guard throws. export bool IsTopLevelProject(); // Resolves the directory holding distributed runtime assets (Crafter.Build // module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise // derives /share/crafter-build from the running executable's path. export fs::path GetCrafterBuildHome(); // Newest mtime among the prerequisites a compiler recorded in `depFile` // (clang's `-MD -MF .d`). This is how a build step learns which // headers its source pulled in: the module scanner only reads `import` // lines, so an `#include`d file is invisible to it until the compiler // reports what it actually opened. // // Returns file_time_type::max() when freshness cannot be proven — no // depfile (nothing has compiled this artifact since depfiles were emitted, // including every object left over from an older crafter-build) or a // prerequisite that no longer exists. Callers compare the result against // their artifact's mtime, so max() reads as "rebuild". fs::file_time_type NewestPrerequisite(const fs::path& depFile); // Wildcard name matching ('*', '?') shared by the test and lint verbs. bool MatchGlob(std::string_view glob, std::string_view name); // Empty `globs` matches everything. bool MatchAny(std::span globs, std::string_view name); }