Skip to content

Commit 42dfab5

Browse files
committed
feat(build): materialize package generated files
1 parent d3126c1 commit 42dfab5

5 files changed

Lines changed: 208 additions & 1 deletion

File tree

.agents/docs/2026-05-30-package-owned-build-flags-plan.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ xlings 现在可以用 `mcpp 0.0.34` 构建,但工程文件里有大量 C 库
2424
- [x] 依赖包描述中的 `mcpp.cflags` / `mcpp.cxxflags` 能作用到该包自己的 compile units。
2525
- [x] 包级 include dirs 在编译该包源码时优先于全局 include dirs。
2626
- [x] build fingerprint / cache key 包含包级 flags,避免 flags 变化后误用缓存。
27+
- [x] Form B 描述支持 `generated_files`,让官方索引包可以生成少量包私有配置头。
2728
- [x] 保持现有项目级 flags 兼容,不破坏已有 manifest。
2829
- [x] 用 xlings 当前项目验证 `mcpp build``mcpp build --target x86_64-linux-musl`
2930

@@ -32,6 +33,7 @@ xlings 现在可以用 `mcpp 0.0.34` 构建,但工程文件里有大量 C 库
3233
- `src/manifest.cppm`
3334
- 保持现有解析入口。
3435
- 若缺少字段,不改变默认行为。
36+
- 解析 `mcpp.generated_files = { ["path"] = "content" }`
3537
- `src/build/plan*.cppm` 或当前 build plan 生成位置
3638
- 在 compile unit 上记录 origin package 或直接保存 package-owned flags。
3739
- `src/build/flags.cppm`
@@ -41,6 +43,7 @@ xlings 现在可以用 `mcpp 0.0.34` 构建,但工程文件里有大量 C 库
4143
- 加入 package build metadata hash。
4244
- `tests/*`
4345
- 增加一个最小包级 cflags 测试: 依赖包源码必须依赖包内宏才能编译通过,主项目不声明该宏。
46+
- 增加一个最小 Form B `generated_files` 测试: 依赖包源码包含生成头,消费者不声明该头。
4447

4548
## 验证命令
4649

@@ -59,9 +62,12 @@ cd /home/speak/workspace/github/openxlings/xlings
5962
- [x] 文档 checkpoint commit: `9047604`.
6063
- [x] failing test added: `tests/e2e/50_package_owned_build_flags.sh`.
6164
- [x] package-owned flags implementation.
65+
- [x] package-owned generated support files implementation:
66+
- `tests/unit/test_manifest.cpp` 覆盖 Form B `generated_files` 解析。
67+
- `tests/e2e/51_package_generated_files.sh` 覆盖生成文件参与依赖包编译。
6268
- [x] xlings local validation:
6369
- `mcpp build` -> `target/x86_64-linux-gnu/ff952c89919589bb/bin/xlings --version` = `xlings 0.4.45`.
6470
- `mcpp build --target x86_64-linux-musl` -> `target/x86_64-linux-musl/7e48a312cd4dbb49/bin/xlings` static ELF.
65-
- [ ] PR draft 创建
71+
- [x] PR draft 创建: https://github.com/mcpp-community/mcpp/pull/88
6672
- [ ] CI 每 120s 检查一次直到完成。
6773
- [ ] 设计确认后 bump `0.0.35` 并发布。

src/cli.cppm

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,10 +623,59 @@ std::string canonical_package_build_metadata(
623623
s += " cxxflag:";
624624
s += flag;
625625
}
626+
for (auto const& [path, content] : pkg.manifest.buildConfig.generatedFiles) {
627+
s += " genfile:";
628+
s += path.generic_string();
629+
s += "=";
630+
s += content;
631+
}
626632
}
627633
return s;
628634
}
629635

636+
std::expected<void, std::string>
637+
materialize_generated_files(const std::filesystem::path& root,
638+
const mcpp::manifest::Manifest& manifest)
639+
{
640+
for (auto const& [relPath, content] : manifest.buildConfig.generatedFiles) {
641+
if (relPath.empty()) {
642+
return std::unexpected("generated_files contains an empty path");
643+
}
644+
if (relPath.is_absolute()) {
645+
return std::unexpected(std::format(
646+
"generated_files path '{}' must be relative", relPath.generic_string()));
647+
}
648+
for (auto const& part : relPath) {
649+
if (part == "..") {
650+
return std::unexpected(std::format(
651+
"generated_files path '{}' must not escape the package root",
652+
relPath.generic_string()));
653+
}
654+
}
655+
656+
auto out = root / relPath.lexically_normal();
657+
std::error_code ec;
658+
std::filesystem::create_directories(out.parent_path(), ec);
659+
if (ec) {
660+
return std::unexpected(std::format(
661+
"cannot create directory for generated file '{}': {}",
662+
out.string(), ec.message()));
663+
}
664+
665+
std::ofstream os(out, std::ios::binary);
666+
if (!os) {
667+
return std::unexpected(std::format(
668+
"cannot write generated file '{}'", out.string()));
669+
}
670+
os << content;
671+
if (!os) {
672+
return std::unexpected(std::format(
673+
"failed while writing generated file '{}'", out.string()));
674+
}
675+
}
676+
return {};
677+
}
678+
630679
bool is_std_module(std::string_view name) {
631680
return name == "std" || name == "std.compat";
632681
}
@@ -1714,6 +1763,11 @@ prepare_build(bool print_fingerprint,
17141763
}
17151764
}
17161765

1766+
if (auto r = materialize_generated_files(effRoot, *manifest); !r) {
1767+
return std::unexpected(std::format(
1768+
"dependency '{}': {}", depName, r.error()));
1769+
}
1770+
17171771
return std::pair{effRoot, std::move(*manifest)};
17181772
};
17191773

src/manifest.cppm

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ struct Toolchain {
7979
struct BuildConfig {
8080
std::vector<std::string> sources; // glob patterns
8181
std::vector<std::filesystem::path> includeDirs; // relative to package root
82+
std::map<std::filesystem::path, std::string> generatedFiles; // Form B package-owned support files
8283
bool staticStdlib = true;
8384
// "" (default = dynamic), "static", "dynamic" — chosen at resolve
8485
// time from --static / --target / [target.<triple>].linkage. Wired
@@ -1296,6 +1297,27 @@ synthesize_from_xpkg_lua(std::string_view luaContent,
12961297
}
12971298
cur.consume('}');
12981299
}
1300+
else if (key == "generated_files") {
1301+
// `{ ["relative/path"] = "contents", ... }`
1302+
if (!cur.consume('{')) {
1303+
return std::unexpected(ManifestError{
1304+
"expected '{' after `generated_files =`", m.sourcePath, 0, 0});
1305+
}
1306+
cur.skip_ws_and_comments();
1307+
while (!cur.eof() && cur.peek() != '}') {
1308+
auto path = cur.read_key();
1309+
if (path.empty()) break;
1310+
cur.skip_ws_and_comments();
1311+
if (!cur.consume('=')) {
1312+
return std::unexpected(ManifestError{
1313+
"expected '=' in `generated_files` entry", m.sourcePath, 0, 0});
1314+
}
1315+
auto content = cur.read_string();
1316+
m.buildConfig.generatedFiles.emplace(path, std::move(content));
1317+
cur.skip_ws_and_comments();
1318+
}
1319+
cur.consume('}');
1320+
}
12991321
else if (key == "targets") {
13001322
// `{ ["name"] = { kind = "lib" }, ... }`
13011323
if (!cur.consume('{')) {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env bash
2+
# requires: gcc fresh-sandbox
3+
# Form B package descriptors can materialize small package-owned support
4+
# files before scanning and compiling the package.
5+
set -e
6+
7+
TMP=$(mktemp -d)
8+
trap "rm -rf $TMP" EXIT
9+
10+
export MCPP_HOME="$TMP/mcpp-home"
11+
source "$(dirname "$0")/_inherit_toolchain.sh"
12+
13+
INDEX_DIR="$TMP/local-index"
14+
mkdir -p "$INDEX_DIR/pkgs/t"
15+
cat > "$INDEX_DIR/pkgs/t/tinycfg.lua" <<'EOF'
16+
package = {
17+
spec = "1",
18+
name = "tinycfg",
19+
description = "Generated config header package",
20+
licenses = {"MIT"},
21+
type = "package",
22+
xpm = {
23+
linux = {
24+
["1.0.0"] = {
25+
url = "https://example.invalid/tinycfg-1.0.0.tar.gz",
26+
sha256 = "0000000000000000000000000000000000000000000000000000000000000000",
27+
},
28+
},
29+
},
30+
mcpp = {
31+
language = "c++23",
32+
import_std = false,
33+
sources = { "src/*.c" },
34+
include_dirs = { "mcpp_generated/include" },
35+
generated_files = {
36+
["mcpp_generated/include/generated_config.h"] = "#pragma once\n#define TINYCFG_VALUE 42\n",
37+
},
38+
targets = { ["tinycfg"] = { kind = "lib" } },
39+
deps = {},
40+
},
41+
}
42+
EOF
43+
44+
mkdir -p "$TMP/project/app/src" \
45+
"$TMP/project/app/.mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/src"
46+
cd "$TMP/project/app"
47+
48+
cat > .mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/src/tinycfg.c <<'EOF'
49+
#include "generated_config.h"
50+
int tinycfg_value(void) {
51+
return TINYCFG_VALUE;
52+
}
53+
EOF
54+
55+
cat > src/main.cpp <<'EOF'
56+
extern "C" int tinycfg_value(void);
57+
int main() {
58+
return tinycfg_value() == 42 ? 0 : 1;
59+
}
60+
EOF
61+
62+
cat > mcpp.toml <<EOF
63+
[package]
64+
name = "app"
65+
version = "0.1.0"
66+
67+
[indices]
68+
local-dev = { path = "$INDEX_DIR" }
69+
70+
[dependencies]
71+
"local-dev.tinycfg" = "1.0.0"
72+
73+
[targets.app]
74+
kind = "bin"
75+
main = "src/main.cpp"
76+
EOF
77+
78+
"$MCPP" build > build.log 2>&1 || {
79+
cat build.log
80+
exit 1
81+
}
82+
83+
generated=".mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0/mcpp_generated/include/generated_config.h"
84+
[[ -f "$generated" ]] || {
85+
echo "FAIL: generated file was not materialized"
86+
find .mcpp/.xlings/data/xpkgs/local-dev.tinycfg/1.0.0 -maxdepth 4 -type f | sort
87+
exit 1
88+
}
89+
90+
grep -q "TINYCFG_VALUE 42" "$generated" || {
91+
echo "FAIL: generated file content mismatch"
92+
cat "$generated"
93+
exit 1
94+
}
95+
96+
"$MCPP" run > run.log 2>&1 || {
97+
cat run.log
98+
exit 1
99+
}
100+
101+
echo "OK"

tests/unit/test_manifest.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,30 @@ package = {
194194
EXPECT_EQ(m->modules.sources[0], "*/src/*.c");
195195
}
196196

197+
TEST(SynthesizeFromXpkgLua, GeneratedFiles) {
198+
constexpr auto src = R"(
199+
package = {
200+
spec = "1",
201+
name = "tinyc",
202+
xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } },
203+
mcpp = {
204+
sources = { "*/src/*.c" },
205+
generated_files = {
206+
["mcpp_generated/include/config.h"] = "#pragma once\n#define TINYC_OK 1\n",
207+
},
208+
include_dirs = { "mcpp_generated/include" },
209+
targets = { ["tinyc"] = { kind = "lib" } },
210+
},
211+
}
212+
)";
213+
auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0");
214+
ASSERT_TRUE(m.has_value()) << m.error().format();
215+
ASSERT_EQ(m->buildConfig.generatedFiles.size(), 1u);
216+
auto it = m->buildConfig.generatedFiles.find("mcpp_generated/include/config.h");
217+
ASSERT_NE(it, m->buildConfig.generatedFiles.end());
218+
EXPECT_EQ(it->second, "#pragma once\n#define TINYC_OK 1\n");
219+
}
220+
197221
TEST(Manifest, DependenciesFlatDefaultNamespace) {
198222
constexpr auto src = R"(
199223
[package]

0 commit comments

Comments
 (0)