Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -2094,6 +2094,64 @@ See [07 — build.mcpp](07-build-mcpp.md). Naming such a file in
command: nothing tracks it, and editing the file produces `ninja: no work to
do`.

### 2.16 `[hooks]` — Project Build Lifecycle Commands

`mcpp build` can run one host-shell command before the build and one command
for its terminal result:

```toml
[hooks]
build_start = "echo build started"
build_failed = "notify-send 'build failed'"
build_finished = "notify-send 'build finished'"

# Optional; these are the defaults.
timeout_seconds = 10
enabled = true
side_effect = true
```

| Key | Type | Default | Meaning |
|---|---|---:|---|
| `build_start` | string | — | Runs after project preparation, immediately before the build |
| `build_failed` | string | — | Runs when the build exits unsuccessfully |
| `build_finished` | string | — | Runs when the build exits successfully |
| `timeout_seconds` | positive integer | `10` | Maximum time for each command |
| `enabled` | bool | `true` | Enables all commands in this table |
| `side_effect` | bool | `true` | Whether a hook failure makes the overall build fail |

Commands run synchronously in the current project's root directory through the
host shell (`/bin/sh` or `cmd.exe`). Standard input/output/error keep their
ordinary terminal behaviour. Missing event commands are skipped.

The lifecycle is:

```text
build_start
├─ build succeeds → build_finished
└─ build fails → build_failed
```

`build_failed` and `build_finished` are mutually exclusive. A hook command
that cannot start, returns non-zero, or exceeds its timeout is a hook failure.
With `side_effect = false`, mcpp reports a warning and preserves the build's
result; with `true`, it returns failure. A hook's own failure does not trigger
another hook.

Hook programs can be installed as ordinary xlings dependencies. For example,
an audio notifier can keep its sound files inside its own executable rather
than adding media handling to mcpp:

```toml
[hooks]
build_finished = "mcpp-hooks-audioplayer niulai-mm"
build_failed = "mcpp-hooks-audioplayer niulai-niulai"
side_effect = false

[xlings]
deps = ["xim:mcpp-hooks-audioplayer@0.0.1"]
```

## Appendix A. Schema Ownership Principle (admission criteria for new fields)

> **Closed syntax, open vocabulary**: whoever owns the parsing semantics defines the keys; whoever owns the domain knowledge defines the values.
Expand Down
53 changes: 53 additions & 0 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,59 @@ o.arg("./mkblob.sh").arg("blob.bin").arg("${mcpp.out_dir}/blob.o")
但 ldflags 是链接命令里的一串字符:没有任何东西跟踪它,改了它得到的是
`ninja: no work to do`。

### 2.16 `[hooks]` —— 项目构建生命周期命令

`mcpp build` 可以在正式构建前执行一条宿主 Shell 命令,并根据最终结果再执行一条:

```toml
[hooks]
build_start = "echo build started"
build_failed = "notify-send 'build failed'"
build_finished = "notify-send 'build finished'"

# 可选;以下是默认值。
timeout_seconds = 10
enabled = true
side_effect = true
```

| 键 | 类型 | 默认值 | 含义 |
|---|---|---:|---|
| `build_start` | 字符串 | — | 项目准备完成、正式构建开始前执行 |
| `build_failed` | 字符串 | — | 构建以失败状态结束时执行 |
| `build_finished` | 字符串 | — | 构建成功结束时执行 |
| `timeout_seconds` | 正整数 | `10` | 每条命令的最长执行时间 |
| `enabled` | 布尔 | `true` | 是否启用本表中的全部命令 |
| `side_effect` | 布尔 | `true` | Hook 失败是否让本次构建失败 |

命令在当前项目根目录中同步执行,使用宿主 Shell(`/bin/sh` 或 `cmd.exe`),标准输入、
输出和错误沿用普通终端行为。没有配置的事件直接跳过。

生命周期为:

```text
build_start
├─ 构建成功 → build_finished
└─ 构建失败 → build_failed
```

`build_failed` 与 `build_finished` 互斥。命令无法启动、返回非零或超过时限均视为
Hook 失败。`side_effect = false` 时 mcpp 报 warning 并保留原构建结果;设为 `true`
时返回失败。Hook 自身失败不会再触发另一个 Hook。

Hook 程序可以作为普通 xlings 依赖安装。例如,音频通知程序可以把音频内置进自己的
可执行文件,无需让 mcpp 处理媒体资源:

```toml
[hooks]
build_finished = "mcpp-hooks-audioplayer niulai-mm"
build_failed = "mcpp-hooks-audioplayer niulai-niulai"
side_effect = false

[xlings]
deps = ["xim:mcpp-hooks-audioplayer@0.0.1"]
```

## 附录 A. Schema 所有权原则(新字段准入标准)

> **语法封闭,词汇开放**:谁拥有解析语义谁定义键;谁拥有领域知识谁定义值。
Expand Down
20 changes: 20 additions & 0 deletions modules/platform/src/process.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ int run_exec_deadline(const std::vector<std::string>& argv,
std::chrono::milliseconds deadline,
bool* timed_out);

// Run one host-shell command with inherited stdio and a real deadline.
// POSIX uses /bin/sh; Windows uses cmd.exe. This is for user-authored command
// strings such as project hooks — programmatic launches should keep using the
// argv-based run_exec_deadline API above.
int run_shell_deadline(std::string_view command,
std::chrono::milliseconds deadline,
bool* timed_out);

RunResult capture_exec_deadline(
const std::vector<std::string>& argv,
const std::vector<std::pair<std::string, std::string>>& extraEnv,
Expand Down Expand Up @@ -658,6 +666,18 @@ int run_exec_deadline(const std::vector<std::string>& argv,
return r.exit_code;
}

int run_shell_deadline(std::string_view command,
std::chrono::milliseconds deadline,
bool* timed_out)
{
std::vector<std::string> argv;
if constexpr (mcpp::platform::is_windows)
argv = {"cmd.exe", "/d", "/s", "/c", std::string(command)};
else
argv = {"/bin/sh", "-c", std::string(command)};
return run_exec_deadline(argv, {}, deadline, timed_out);
}

RunResult capture_exec_deadline(
const std::vector<std::string>& argv,
const std::vector<std::pair<std::string, std::string>>& extraEnv,
Expand Down
39 changes: 35 additions & 4 deletions src/cli/cmd_build.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import mcpp.build.stage;
import mcpp.build.schedule.detach_codegen;
import mcpp.build.test_targets;
import mcpp.dyndep;
import mcpp.hooks;
import mcpp.log;
import mcpp.project;
import mcpp.manifest;
Expand All @@ -42,6 +43,25 @@ workspace_fanout_members(bool wantAll, const std::string& package_filter) {
return std::nullopt;
}

int run_build_with_hooks(mcpp::build::BuildContext& ctx, bool verbose,
bool no_cache, std::string_view targetOverride) {
auto config = mcpp::hooks::load(ctx.projectRoot / "mcpp.toml");
if (!config) {
mcpp::ui::error(std::format("invalid hook configuration: {}", config.error()));
return 2;
}

if (!mcpp::hooks::invoke(*config, mcpp::hooks::Event::BuildStart,
ctx.projectRoot))
return 1;

int rc = mcpp::build::run_build_plan(ctx, verbose, no_cache, targetOverride);
auto terminalEvent = rc == 0 ? mcpp::hooks::Event::BuildFinished
: mcpp::hooks::Event::BuildFailed;
bool hookOk = mcpp::hooks::invoke(*config, terminalEvent, ctx.projectRoot);
return rc != 0 ? rc : (hookOk ? 0 : 1);
}

export int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
bool verbose = parsed.is_flag_set("verbose") || mcpp::log::is_verbose();
bool print_fp = parsed.is_flag_set("print-fingerprint");
Expand Down Expand Up @@ -118,7 +138,7 @@ export int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
auto ctx = mcpp::build::prepare_build(print_fp, /*includeDevDeps=*/false,
/*extraTargets=*/{}, mo);
if (!ctx) { std::println(stderr, "error: {}: {}", mp, ctx.error()); rc = 2; continue; }
int r = mcpp::build::run_build_plan(*ctx, verbose, no_cache, mo.target_triple);
int r = run_build_with_hooks(*ctx, verbose, no_cache, mo.target_triple);
if (r != 0) rc = r;
}
return rc;
Expand All @@ -137,8 +157,19 @@ export int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
&& ov.cache_mode.empty()) {
auto root = mcpp::project::find_manifest_root(std::filesystem::current_path());
if (root) {
if (auto rc = mcpp::build::try_fast_build(*root, verbose, no_cache)) {
return *rc;
auto config = mcpp::hooks::load(*root / "mcpp.toml");
if (!config) {
mcpp::ui::error(std::format(
"invalid hook configuration: {}", config.error()));
return 2;
}
// Hooked builds must first prepare the project so xlings-provided
// hook programs are available before build_start. Projects with no
// active hooks keep the existing fast path unchanged.
if (!mcpp::hooks::active(*config)) {
if (auto rc = mcpp::build::try_fast_build(*root, verbose, no_cache)) {
return *rc;
}
}
}
}
Expand All @@ -147,7 +178,7 @@ export int cmd_build(const mcpplibs::cmdline::ParsedArgs& parsed) {
/*extraTargets=*/{}, ov);
if (!ctx) { std::println(stderr, "error: {}", ctx.error()); return 2; }

return mcpp::build::run_build_plan(*ctx, verbose, no_cache, ov.target_triple);
return run_build_with_hooks(*ctx, verbose, no_cache, ov.target_triple);
}

export int cmd_run(const mcpplibs::cmdline::ParsedArgs& parsed,
Expand Down
Loading