100 lines
3.7 KiB
C++
100 lines
3.7 KiB
C++
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
module Crafter.Build:Script_impl;
|
||
|
|
import std;
|
||
|
|
import :Script;
|
||
|
|
import :Clang;
|
||
|
|
import :Platform;
|
||
|
|
import :Progress;
|
||
|
|
namespace fs = std::filesystem;
|
||
|
|
using namespace Crafter;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
std::string JoinNames(std::span<const Script> scripts) {
|
||
|
|
std::string out;
|
||
|
|
for (const Script& script : scripts) {
|
||
|
|
if (!out.empty()) out += ", ";
|
||
|
|
out += script.name;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::int32_t ScriptContext::Run(std::string_view command) const {
|
||
|
|
// Echoed by default, unlike a compile step: a script's commands are the
|
||
|
|
// only account of what it is doing to the machine.
|
||
|
|
if (Progress::GetVerbosity() != Progress::Verbosity::Quiet) std::println("$ {}", command);
|
||
|
|
return RunCommandAttached(command);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::int32_t Crafter::RunScript(Configuration& projectCfg, const RunScriptOptions& opts) {
|
||
|
|
// Loading the project may have left an in-place status line behind.
|
||
|
|
Progress::Clear();
|
||
|
|
|
||
|
|
const std::span<const Script> scripts = projectCfg.scripts;
|
||
|
|
if (scripts.empty()) {
|
||
|
|
std::println(std::cerr,
|
||
|
|
R"msg(No scripts defined.
|
||
|
|
Add them in project.cpp before returning the Configuration:
|
||
|
|
|
||
|
|
cfg.scripts.push_back({{"docs", [](Crafter::ScriptContext& ctx) {{
|
||
|
|
return ctx.Run("python3 tools/gen-docs.py");
|
||
|
|
}}}});
|
||
|
|
|
||
|
|
`crafter-build run docs [args...]` then runs it and exits with what it
|
||
|
|
returned; ctx.args holds the arguments.)msg");
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Checked for every script on every run, the list included, so a bad
|
||
|
|
// declaration is reported the first time anyone uses `run` and not only
|
||
|
|
// when that one name is asked for.
|
||
|
|
std::unordered_set<std::string_view> seen;
|
||
|
|
for (const Script& script : scripts) {
|
||
|
|
if (script.name.empty() || script.name.starts_with('-')) {
|
||
|
|
std::println(std::cerr, "script name '{}' cannot be run: a name must not be empty or start with '-'", script.name);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
// Running the first or the last of two would both be a guess, and
|
||
|
|
// the usual cause is a pasted line whose name was never changed.
|
||
|
|
if (!seen.insert(script.name).second) {
|
||
|
|
std::println(std::cerr, "script '{}' is defined twice", script.name);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
if (!script.run) {
|
||
|
|
std::println(std::cerr, "script '{}' has no callback", script.name);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (opts.listOnly || !opts.named) {
|
||
|
|
for (const Script& script : scripts) std::println("{}", script.name);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
const auto found = std::ranges::find(scripts, opts.name, &Script::name);
|
||
|
|
if (found == scripts.end()) {
|
||
|
|
std::println(std::cerr, "No script named '{}'. Scripts: {}", opts.name, JoinNames(scripts));
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
ScriptContext ctx{
|
||
|
|
.project = projectCfg,
|
||
|
|
// lexically_normal: fs::absolute keeps a "./" component.
|
||
|
|
.projectDir = opts.projectFile.empty() ? fs::absolute(projectCfg.path).lexically_normal() : opts.projectFile.parent_path(),
|
||
|
|
.args = opts.args,
|
||
|
|
};
|
||
|
|
try {
|
||
|
|
const std::int32_t code = found->run(ctx);
|
||
|
|
if (code == 0 || (code >= 1 && code <= 255)) return code;
|
||
|
|
std::println(std::cerr, "script '{}' returned {}, which is not an exit status (0..255); exiting with 1", found->name, code);
|
||
|
|
return 1;
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
// Never let a script's exception unwind across the project.so
|
||
|
|
// boundary and out of Run: report it as this script's failure.
|
||
|
|
Progress::Clear();
|
||
|
|
std::println(std::cerr, "script '{}' failed: {}", found->name, e.what());
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|