ForceHostTooling rewrites a `#pragma crafter dependency` and everything it loads in place: host target, march native, release, -fPIC. Those are entries of the project cache like any other, keyed by project file and args alone. A project that loaded the same sibling as its tool, with the same args, got the rewritten entry back. That happens whenever the project gets no args, since host tooling gets none either: a consumer built for x86-64-v2 then imported a sibling's BMI compiled with -march=native, and clang refused it. Any arg on the command line split the keys and hid it. A thread-local count of pragma loads now marks the tool's load and every load inside it, its project file's siblings included, and GitProject and LocalProject put that mark in the key. The clone stays shared; only the Configuration is separate, as for two arg lists. A module-internal function rather than a spec field, because the siblings of a tool are loaded by the tool's own project.cpp, which cannot know it is host tooling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.9 KiB
C++
104 lines
5.9 KiB
C++
// 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<const std::string_view> 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();
|
|
// True while this thread loads a `#pragma crafter dependency`: the tool's
|
|
// project file and every sibling it loads in turn. GitProject and
|
|
// LocalProject key the project cache on it, so host tooling, which
|
|
// ForceHostTooling rewrites in place, never shares a Configuration with a
|
|
// load for the target.
|
|
bool LoadingHostTooling();
|
|
// Resolves the directory holding distributed runtime assets (Crafter.Build
|
|
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
|
// derives <prefix>/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 <artifact>.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<const std::string> globs, std::string_view name);
|
|
}
|