Crafter.Build/interfaces/Crafter.Build-Platform.cppm
Jorijn van der Graaf be88a194f4
fix: let a project file ask whether it is the top-level project
Crafter.LintRules declares a test that links crafter-build, fetched
with GitProject. crafter-build's own project.cpp loads Crafter.LintRules
as a `#pragma crafter dependency`, so the chain was: crafter-build loads
LintRules, LintRules' test wiring loads crafter-build's project.cpp from
the external cache, which loads LintRules again, and the cycle guard
threw. Every command that loads project.cpp in this repository has
failed since that test was added, including the last CI run on master.

A project file had no way to tell whether it was the one the user
pointed crafter-build at or a dependency being loaded for somebody
else, so it could not skip wiring that only its own build needs.
IsTopLevelProject() answers that from the loading-frame stack
ProjectDependency() already reads: one frame is the top-level project
file, a second is only ever pushed for a dependency. Outside any project
file it is true as well, which is what a test harness constructing
Configurations expects.

The ProjectDependency test records the answer from both the app and the
greeter project files and checks each.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:02:40 +02:00

73 lines
4.1 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
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;
};
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 CRAFTER_API CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
std::string GetBaseCommand(const Configuration& config);
export CRAFTER_API 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 CRAFTER_API 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 CRAFTER_API bool IsTopLevelProject();
// 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 CRAFTER_API 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);
}