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>
This commit is contained in:
parent
6dcdad4515
commit
561398013c
12 changed files with 313 additions and 56 deletions
|
|
@ -12,6 +12,7 @@ import :Platform;
|
|||
import :Toolchain;
|
||||
import :Test;
|
||||
import :Lint;
|
||||
import :Script;
|
||||
import :Progress;
|
||||
import :Asset;
|
||||
namespace fs = std::filesystem;
|
||||
|
|
@ -1594,6 +1595,7 @@ R"(Usage:
|
|||
{0} test [test-options] [globs...] Build and run the project's tests
|
||||
{0} lint [lint-options] [globs...] Run the project's lint rules over its sources
|
||||
{0} format [format-options] [globs...] Apply the project's transform rules (rewrites files)
|
||||
{0} run [script [args...]] Run a script the project defines, or list them
|
||||
{0} clean Delete the project's bin/ and build/ trees
|
||||
{0} help | -h | --help Show this help
|
||||
{0} --version Print the version and the compiler in use
|
||||
|
|
@ -1646,6 +1648,23 @@ Format options (after the `format` subcommand):
|
|||
transform. `format` rewrites changed files in place; `lint` reports the
|
||||
same transforms as would-reformat findings without writing.
|
||||
|
||||
Run (after the `run` subcommand):
|
||||
<script> [args...] Run the named script. Everything after the name is
|
||||
handed to the script untouched, including what
|
||||
looks like an option of crafter-build itself, so
|
||||
-v, -q, --project= and project args go before it.
|
||||
--list List the project's scripts. A bare `run` does too.
|
||||
|
||||
Scripts are defined in project.cpp, as entries of cfg.scripts:
|
||||
cfg.scripts.push_back({{"docs", [](Crafter::ScriptContext& ctx) {{
|
||||
return ctx.Run("python3 tools/gen-docs.py");
|
||||
}}}});
|
||||
The callback returns the exit status. ctx.Run runs a shell command with the
|
||||
terminal attached and returns its status; ctx.args holds the arguments and
|
||||
ctx.projectDir the project's directory. Commands run in the directory
|
||||
crafter-build was started from. Only the scripts of the project being run
|
||||
are available, never a dependency's.
|
||||
|
||||
Clean (after the `clean` subcommand):
|
||||
Removes bin/ and build/ next to the project file. Does not load project.cpp,
|
||||
so it works when the project itself no longer compiles. Every target, variant
|
||||
|
|
@ -1677,6 +1696,8 @@ Exit status:
|
|||
0 success / all non-skipped tests passed / lint clean / format applied
|
||||
1 build failure, one or more tests failed, lint findings, no rules defined,
|
||||
format write errors, or `format --check` found files that would change
|
||||
`run` exits with the status its script returned, and with 1 when there is no
|
||||
such script, when the script throws, or when it returns a value outside 0..255
|
||||
)", argv0);
|
||||
}
|
||||
|
||||
|
|
@ -1692,14 +1713,29 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
bool runLint = false;
|
||||
bool runFormat = false;
|
||||
bool runClean = false;
|
||||
bool runScript = false;
|
||||
bool runAfterBuild = false;
|
||||
RunTestsOptions testOpts;
|
||||
RunLintOptions lintOpts;
|
||||
RunScriptOptions scriptOpts;
|
||||
Progress::Verbosity verbosity = Progress::Verbosity::Default;
|
||||
|
||||
for (std::int32_t i = 1; i < argc; ++i) {
|
||||
std::string_view arg = argv[i];
|
||||
if (arg == "--version") {
|
||||
// The two `run` branches come before everything else on purpose.
|
||||
// What follows a script's name belongs to the script, whatever
|
||||
// it looks like: `run package --help` must not print this
|
||||
// tool's help, `run package clean` must not delete build/, and
|
||||
// none of it may reach projectArgs, where it would change the
|
||||
// variant hash and so the directory the project builds into.
|
||||
if (scriptOpts.named) {
|
||||
scriptOpts.args.emplace_back(arg);
|
||||
} else if (runScript && !arg.starts_with("-")) {
|
||||
// The first word after `run`, even one that is a verb
|
||||
// elsewhere. Options before it keep their usual meaning.
|
||||
scriptOpts.name = arg;
|
||||
scriptOpts.named = true;
|
||||
} else if (arg == "--version") {
|
||||
// Answered here, before the project file is looked for, so
|
||||
// it works in any directory and is never forwarded to
|
||||
// project.cpp as an unknown argument.
|
||||
|
|
@ -1717,6 +1753,10 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
} else if (!runTests && !runLint && !runClean && arg == "format") {
|
||||
runFormat = true;
|
||||
lintOpts.mode = LintMode::Apply;
|
||||
} else if (!runTests && !runLint && !runFormat && !runClean && arg == "run") {
|
||||
runScript = true;
|
||||
} else if (runScript && arg == "--list") {
|
||||
scriptOpts.listOnly = true;
|
||||
} else if (runFormat && arg == "--check") {
|
||||
lintOpts.mode = LintMode::Check;
|
||||
} else if (arg == "-r") {
|
||||
|
|
@ -1778,6 +1818,11 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
|||
Configuration config = LoadProject(projectFile, projectArgs);
|
||||
SetParentProject(&config);
|
||||
|
||||
if (runScript) {
|
||||
scriptOpts.projectFile = fs::absolute(projectFile).lexically_normal();
|
||||
return RunScript(config, scriptOpts);
|
||||
}
|
||||
|
||||
if (runLint || runFormat) {
|
||||
// lexically_normal: fs::absolute keeps a "./" component, which
|
||||
// would leak into displayed paths and the project-root derivation.
|
||||
|
|
|
|||
Loading…
Reference in a new issue