// 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 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 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 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; } }