Skip to content
Merged
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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ on:
env:
# Keep in step with the mcpp-index CI pin: this package's dependencies are
# index packages, and they are validated against that same mcpp.
# 2026.8.5.2 is the FLOOR, not a routine pin. Three things the codegen in
# 2026.8.6.2 is the FLOOR, not a routine pin. Four things the codegen in
# templates/ and examples/ is built on do not exist before it:
# 2026.8.5.1 `tools = [...]` host tools; `mcpp:action=` build-graph nodes
# 2026.8.5.2 a `host-module = true` rule package may use `import std;` and
# `import mcpp;` — rules/ needs both, and before .5.2 it failed
# with `module 'std' not found` / `module 'mcpp' not found`
MCPP_VERSION: "2026.8.5.2"
# 2026.8.6.2 `reexport = true`, which is what lets THIS package hand its
# user the whole codegen toolchain — one dependency, not four —
# and `rerun_if_changed_glob`, without which generate_all()
# would not re-run when a .proto is ADDED (no declared file's
# hash changes), silently never generating it
MCPP_VERSION: "2026.8.6.2"

jobs:
build:
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,28 +81,30 @@ you no longer supply them yourself. Declare them on the dependencies they belong
the codegen rule, and that is the entire setup:

```toml
[dependencies]
grpc.grpc = "1.83.0"
grpc.grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"] }
grpcgen = { version = "1.83.0", host-module = true }
compat.protobuf = { version = "35.1", tools = ["protoc"] }
[dependencies.grpc]
grpc = { version = "1.83.0", features = ["codegen"] }
```

```cpp
// build.mcpp — in full
import mcpp;
import grpcgen;
int main() { return grpcgen::generate({"helloworld"}) ? 0 : 1; }
int main() { return grpcgen::generate_all() ? 0 : 1; }
```

One dependency and one line. Which tools gRPC codegen needs — protoc, the C++
plugin, the rule that drives them — is this package's knowledge, and
`features = ["codegen"]` is where it says so. Adding a `.proto` means dropping
a file in `proto/`; there is no list to keep in sync.

That is `templates/greeter`, and `mcpp new --template grpc` gives it to you ready to
build (`--template` takes the PACKAGE; spell the template out as `grpc:greeter` when a
package ships more than one). **No generated file is checked in any more** — edit the `.proto` and rebuild.

`grpcgen` is an ordinary mcpp package holding the rule, written in C++ and versioned
alongside gRPC. Rules ship through the package manager you already have, so there is no
second language here the way xmake has Lua rules and Bazel has Starlark. It needs
**mcpp 2026.8.5.2**, which is where a rule package first became able to use `import std;`
**mcpp 2026.8.6.2** — 2026.8.5.2 is where a rule package first became able to use `import std;`
and `import mcpp;`.

Two examples, on purpose:
Expand Down
17 changes: 9 additions & 8 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,30 +75,31 @@ class Greeter final : public helloworld::Greeter::Service {
## 代码生成

gRPC 需要两个宿主工具 —— `protoc` 与 `grpc_cpp_plugin` —— 自 **mcpp 2026.8.5.1** 起
你不再需要自己准备它们。把它们声明在各自所属的依赖上、再加上 codegen 规则,配置就完了:
你不再需要自己准备它们,也不需要知道它们是谁:

```toml
[dependencies]
grpc.grpc = "1.83.0"
grpc.grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"] }
grpcgen = { version = "1.83.0", host-module = true }
compat.protobuf = { version = "35.1", tools = ["protoc"] }
[dependencies.grpc]
grpc = { version = "1.83.0", features = ["codegen"] }
```

```cpp
// build.mcpp —— 全文如此
import mcpp;
import grpcgen;
int main() { return grpcgen::generate({"helloworld"}) ? 0 : 1; }
int main() { return grpcgen::generate_all() ? 0 : 1; }
```

一条依赖,一行代码。gRPC 的代码生成需要哪些工具 —— protoc、C++ 插件、驱动它们
的那条规则 —— 是**这个包**的知识,`features = ["codegen"]` 就是它说出这件事的
地方。新增一个 `.proto` 只是往 `proto/` 里放一个文件,没有需要同步维护的清单。

这就是 `templates/greeter`,`mcpp new --template grpc` 直接给你一份能建的
(`--template` 接的是**包名**;一个包带多个模板时写成 `grpc:greeter`)。
**仓库里不再签入任何生成产物** —— 改 `.proto` 然后重新构建,就这样。

`grpcgen` 是一个普通的 mcpp 包,里面装着那条规则,用 C++ 写、与 gRPC 同版本发布。
规则走你已经在用的包管理器分发,所以这里**没有第二门语言** —— 不像 xmake 用 Lua rule、
Bazel 用 Starlark。它需要 **mcpp 2026.8.5.2**:规则包是从那一版起才真正能用
Bazel 用 Starlark。它需要 **mcpp 2026.8.6.2**;2026.8.5.2 是规则包从那一版起才真正能用
`import std;` 与 `import mcpp;` 的。

两个示例,是刻意的:
Expand Down
6 changes: 3 additions & 3 deletions examples/greeter/build.mcpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// gRPC codegen.
// gRPC codegen — every .proto under proto/, without naming any of them.
//
// To add a .proto: drop it in proto/ and add its name below.
// To add a .proto: drop it in proto/ and build. Nothing here changes.
//
// The rule lives in the `grpcgen` package rather than in this file, so it is
// versioned, tested and fixed in one place instead of copy-pasted into every
Expand All @@ -18,5 +18,5 @@ import mcpp;
import grpcgen;

int main() {
return grpcgen::generate({"helloworld"}) ? 0 : 1;
return grpcgen::generate_all() ? 0 : 1;
}
16 changes: 13 additions & 3 deletions examples/greeter/mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# greeter — what `mcpp new --template greeter` produces.
#
# This example exists to keep the TEMPLATE honest: it is the template
# instantiated, so CI building it is CI testing what users are handed. The only
# deliberate difference is that the three grpc-m packages are taken by `path`
# rather than from the index, so the example tests the working tree.
# instantiated, so CI building it is CI testing what users are handed. Two
# deliberate differences:
#
# * the three grpc-m packages are taken by `path` rather than from the index,
# so the example tests the WORKING TREE;
# * consequently it cannot use the template's one-line
# `features = ["codegen"]` form. That feature's [feature-deps] name index
# VERSIONS of grpc-plugin and grpcgen, which is right for a released
# package and wrong for testing an unreleased working tree. The one-line
# form is verified against the published index instead.
#
# What this example does share with the template is `generate_all()`: the rule
# discovers every .proto under proto/ rather than being handed a list.
#
# The sibling examples/helloworld does the same job with a hand-written
# build.mcpp instead of the rule package — that one demonstrates the mechanism,
Expand Down
33 changes: 33 additions & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,39 @@ sources = [
[feature-deps.ares]
compat.c-ares = "1.34.5"

# ── codegen ──────────────────────────────────────────────────────────────────
# Turning it on gives the consumer the whole toolchain — protoc, the
# gRPC C++ plugin, and the rule that drives them — so its mcpp.toml says
#
# grpc = { version = "1.83.0", features = ["codegen"] }
#
# and its build.mcpp says `grpcgen::generate_all()`. Nothing else. Which tools
# gRPC codegen needs is this package's knowledge, not its user's; before
# `reexport` existed the user had to write all four dependencies and know that
# gRPC codegen runs protobuf's protoc.
#
# Off by default because the cost is real: protoc drags in libprotoc's ~157
# extra TUs, and a project that only LINKS gRPC must not pay for them.
#
# Requires mcpp 2026.8.6.2+ (`reexport`). On an older mcpp the key parses as an
# unknown dep-spec key and the toolchain simply does not reach the consumer.
#
# The feature is registered by this table alone — a feature that only pulls
# dependencies needs no [features.<name>] section.
[feature-deps.codegen]
# protoc, pinned to the protobuf this package already depends on above — the
# single version axis is why "the generator does not match the runtime" is not
# expressible here. `tools` and `reexport` are ADDED to that existing edge;
# they are not a second, conflicting declaration of protobuf.
compat.protobuf = { version = "35.1", tools = ["protoc"], reexport = true }
# The gRPC C++ codegen plugin — a package of its own rather than a target
# inside gRPC, because a code generator needs a .proto parser and a C++
# emitter, not TLS, DNS and a regex engine.
grpc.grpc-plugin = { version = "1.83.0", tools = ["grpc_cpp_plugin"], reexport = true }
# The rule itself, as an importable host module. Not upstream code, so it
# lives in mcpplibs rather than in the grpc namespace.
mcpplibs.grpcgen = { version = "1.83.0", host-module = true, reexport = true }

[target.'cfg(windows)'.build]
cxxflags = [
# Upstream's own MSVC set (CMakeLists.txt add_definitions under if(MSVC)).
Expand Down
71 changes: 66 additions & 5 deletions rules/src/grpcgen.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
// what lets this file use `import mcpp;` — the typed wrapper over the `mcpp:`
// directive protocol — rather than hand-printing JSON.
//
// Requires mcpp >= 2026.8.5.2. Both of those properties are fixes in that
// Requires mcpp >= 2026.8.6.2 for generate_all() (see there) and >= 2026.8.5.2
// for everything else. Both of those properties are fixes in that
// release: before it, a rule was compiled before the std module existed
// (`module 'std' not found`) and was ALSO compiled as an ordinary library of
// the consumer, where the `mcpp` module does not exist.
Expand Down Expand Up @@ -61,15 +62,15 @@ struct options {
//
// Returns false after printing a diagnostic; a build program should propagate
// that as a non-zero exit.
bool generate(std::initializer_list<const char*> protos, options opt = {}) {
bool generate(std::vector<std::string> protos, options opt = {}) {
const std::string root = mcpp::manifest_dir();
const std::string out = mcpp::out_dir();
if (root.empty() || out.empty()) {
std::println(std::cerr,
"grpcgen: no mcpp build context — this runs from build.mcpp");
return false;
}
if (protos.size() == 0) {
if (protos.empty()) {
std::println(std::cerr, "grpcgen::generate() called with no .proto files");
return false;
}
Expand Down Expand Up @@ -111,10 +112,10 @@ bool generate(std::initializer_list<const char*> protos, options opt = {}) {
// too eagerly is far better than silently stale stubs.
std::vector<std::string> inputs;
inputs.reserve(protos.size());
for (const char* n : protos)
for (const auto& n : protos)
inputs.push_back(std::format("{}/{}.proto", protoDir, n));

for (const char* name : protos) {
for (const auto& name : protos) {
const std::string src = std::format("{}/{}.proto", protoDir, name);
const std::string base = std::format("{}/{}", out, name);

Expand Down Expand Up @@ -146,6 +147,15 @@ bool generate(std::initializer_list<const char*> protos, options opt = {}) {

for (const auto& in : inputs) gen.input(in.c_str());

// protoc mirrors the .proto's relative path under --cpp_out and does
// NOT create the intermediate directories. A flat proto/ never notices;
// proto/sub/x.proto fails with "No such file or directory" from inside
// protoc, which reads as a codegen bug rather than a missing mkdir.
{
std::error_code mkec;
std::filesystem::create_directories(
std::filesystem::path(base).parent_path(), mkec);
}
const std::string pbcc = base + ".pb.cc", pbh = base + ".pb.h";
gen.output(pbcc.c_str()).output(pbh.c_str());
const std::string gcc_ = base + ".grpc.pb.cc", gh = base + ".grpc.pb.h";
Expand All @@ -160,4 +170,55 @@ bool generate(std::initializer_list<const char*> protos, options opt = {}) {
return true;
}

// Every .proto under `opt.proto_dir`, without naming any of them.
//
// This is the form the greeter template uses, and it is only SAFE because the
// engine can express "my output depends on which files are here"
// (`rerun_if_changed_glob`, mcpp 2026.8.6.2+). Before that, a build program
// that globbed did not re-run when a .proto was added — no declared file's
// hash had changed — so the new file was silently never generated, which is
// worse than making the author list names. That is why this repository shipped
// the explicit list first.
//
// Names are relative to `opt.proto_dir` and keep their subdirectory, so
// proto/sub/x.proto generates sub/x.pb.cc and is imported as "sub/x.proto".
bool generate_all(options opt = {}) {
namespace fs = std::filesystem;
const std::string root = mcpp::manifest_dir();
if (root.empty()) {
std::println(std::cerr,
"grpcgen: no mcpp build context — this runs from build.mcpp");
return false;
}
const std::string pattern = std::string(opt.proto_dir) + "/**/*.proto";
mcpp::rerun_if_changed_glob(pattern.c_str());

const fs::path dir = fs::path(root) / opt.proto_dir;
std::error_code ec;
if (!fs::exists(dir, ec)) {
std::println(std::cerr,
"grpcgen::generate_all(): no '{}' directory under {}",
opt.proto_dir, root);
return false;
}
std::vector<std::string> names;
for (auto const& e : fs::recursive_directory_iterator(dir, ec)) {
if (ec) break;
if (!e.is_regular_file(ec)) continue;
if (e.path().extension() != ".proto") continue;
auto rel = e.path().lexically_relative(dir);
rel.replace_extension();
names.push_back(rel.generic_string());
}
if (names.empty()) {
std::println(std::cerr,
"grpcgen::generate_all(): no .proto files under {}", dir.string());
return false;
}
// Sorted so the declared edge set is stable run to run — an unstable order
// would churn build.ninja for no reason.
std::ranges::sort(names);
return generate(std::move(names), opt);
}

} // namespace grpcgen
6 changes: 3 additions & 3 deletions templates/greeter/build.mcpp.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// gRPC codegen.
// gRPC codegen — every .proto under proto/, without naming any of them.
//
// To add a .proto: drop it in proto/ and add its name below.
// To add a .proto: drop it in proto/ and build. Nothing here changes.
//
// The rule lives in the `grpcgen` package rather than in this file, so it is
// versioned, tested and fixed in one place instead of copy-pasted into every
Expand All @@ -18,5 +18,5 @@ import mcpp;
import grpcgen;

int main() {
return grpcgen::generate({"helloworld"}) ? 0 : 1;
return grpcgen::generate_all() ? 0 : 1;
}
29 changes: 8 additions & 21 deletions templates/greeter/mcpp.toml.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,12 @@ sources = ["src/main.cpp"]
kind = "bin"
main = "src/main.cpp"

# The `grpc` namespace, because both of these are UPSTREAM gRPC code — the
# runtime and, below, the codegen plugin's src/compiler/* sources. A namespace
# names whose library it is, not who packaged it.
# One dependency, and it is the library you actually use.
#
# `features = ["codegen"]` is what pulls in protoc, the gRPC C++ plugin and the
# `grpcgen` rule that drives them — which tools gRPC codegen needs is the gRPC
# package's knowledge, not yours. The `grpc` namespace, because this is
# UPSTREAM gRPC code: a namespace names whose library it is, not who packaged
# it.
[dependencies.grpc]
{{self.name}} = "{{self.version}}"
# The gRPC C++ codegen plugin — a package of its own rather than a target
# inside gRPC, because a code generator needs a .proto parser and a C++
# emitter, not TLS, DNS and a regex engine.
grpc-plugin = { version = "{{self.version}}", tools = ["grpc_cpp_plugin"] }

# grpcgen is NOT upstream — it is the codegen rule this repository wrote, so it
# lives in mcpplibs (which is also mcpp's default namespace, hence no prefix
# needed when writing it).
[dependencies.mcpplibs]
# The codegen RULE. `host-module = true` makes its module importable from
# build.mcpp, which is why build.mcpp is three lines instead of sixty. It is
# build-time only — not compiled into or linked with this project.
grpcgen = { version = "{{self.version}}", host-module = true }

[dependencies.compat]
# protoc, pinned to the same protobuf this project links.
protobuf = { version = "35.1", tools = ["protoc"] }
{{self.name}} = { version = "{{self.version}}", features = ["codegen"] }
2 changes: 1 addition & 1 deletion templates/greeter/template.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[template]
description = "Greeter service + client in one program — a real gRPC round trip over loopback"
post_message = "cd into the project and `mcpp run`. The protobuf/gRPC stubs are NOT checked in: mcpp builds protoc and grpc_cpp_plugin from the same packages you link against, then generates the stubs during the build. Edit proto/helloworld.proto and rebuild — that is the whole workflow, and the generator can never be a different version from the runtime. To add a .proto, drop it in proto/ and add its name to the list in build.mcpp."
post_message = "cd into the project and `mcpp run`. The protobuf/gRPC stubs are NOT checked in: mcpp builds protoc and grpc_cpp_plugin from the same packages you link against, then generates the stubs during the build. Edit proto/helloworld.proto and rebuild — that is the whole workflow, and the generator can never be a different version from the runtime. To add a .proto, drop it in proto/ and build — build.mcpp picks up every .proto under that directory, so there is no list to maintain."
Loading