Crafter.Build/interfaces/Crafter.Build-Platform.cppm
Jorijn van der Graaf 561398013c
feat(script): scripts in project.cpp, run with crafter-build run
A project.cpp could describe how a project is built, tested and linted,
but not the chores around it: packaging, installing, publishing. Those
stayed shell lines in a README or in someone's history.

Configuration gains `scripts`, a plain list like `defines`. A Script is
a name and a C++ callback, as a lint rule is; there is no script
language and no config file. The callback gets a ScriptContext (the
project, the project file's directory, the arguments) and returns the
exit status. ctx.Run runs a shell command with the terminal attached
and returns its status. It is only stored when project.cpp loads, which
happens for every verb and for every project that depends on this one.

`crafter-build run <name> [args...]` runs one, a bare `run` lists them.
Only the scripts of the project crafter-build was pointed at are
runnable, never a dependency's.

Everything after the script's name is handed to the script untouched.
The two branches that do this sit ahead of every other branch of the
parse loop, --version and --help included. Below the verbs,
`run package clean` would delete build/, and any other word would land
in the project's args and so in the variant hash, moving the build to
another directory. Options before the name keep their meaning, so
project args go there.

The driver is its own partition, :Script. It refuses a project with no
scripts, a name that is empty or starts with '-' (it could never be
typed), a name declared twice (running either would be a guess) and an
unknown name, each with exit 1 and without running anything. A script
that throws exits 1. A return value outside 0..255 becomes 1: an exit
status is eight bits, and a raw wait status such as 256 would otherwise
wrap to success.

Two helpers in :Platform:

- RunCommandAttached runs a command with inherited stdio. Every helper
  so far captured through popen, which is right for a compile step and
  useless for a command that runs for half an hour or asks a question.
  It reads the wait status through DecodeWaitStatus, which
  RunCommandChecked and RunCommandWithTimeout now share.
- ShellQuote is exported. It existed twice, as ShellQuote in :External
  and ShellQuoteSh in :Test, with the same behaviour; both are gone. A
  script that puts a path into a command needs it too.

Not included: a string shorthand for one-command scripts (a one-line
lambda covers it) and a ctx.Build() helper (Build() is already
exported).

A new partition and new exported symbols: run ./build.sh.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-20 19:08:26 +02:00

83 lines
4.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;
};
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();
// 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);
}