Skip to content

Commit 69a843e

Browse files
committed
feat(scaffold): 模板可以拒绝自依赖,因为有一个模板必须拒绝
⚠️ **`mcpp new k --template riscv-virt-rt:nolibc` 之后 `mcpp run` 直接失败**, 而那个模板的全部意义就是不依赖任何东西。 自依赖注入的存在是为了让模板不与它所属的库脱节,这对几乎所有模板都对。它对 「主题就是不依赖任何东西」的模板是错的:板级包自己的模块 `#include <stdio.h>`, 于是在一个**清单里没有任何依赖**的工程上,报错是 riscv_virt_rt.cppm:9: fatal error: 'stdio.h' file not found ⇒ `[template.inject] self = false`。布尔形式拒绝注入,既有的表形式(携带 features) 不受影响,默认仍然是开 —— 三个单测分别钉住这三种情形,其中「默认为开」那条是防止 将来悄悄翻转:每个没有声明的模板都依赖它。 ## 生态侧同批修复(riscv-virt-rt 0.4.1) 板级包此前把「没有 C 库」当成错误。它不是:处在零 libc 档的工程没有在向这块板子要 任何东西。现在该路径不再报错。 ⚠️ 但**模拟器照常发** —— 哪个模拟器能启动这台机器是板级事实,与哪份 C 库编出的镜像 无关。我第一版把它和其余部分一起早退掉了,那会让这类工程没有 runner。 实测:生成后 **362 字节**,qemu 中打印 `k: running with no C library` / `answer 42`。 ## ⚠️ 这是本轮唯一一次发布之后才发现的缺陷 0.4.0 的模板在索引里可见、可生成,而生成出来的工程跑不起来。发现它的方式是**用发布后 的包从零走一遍新用户流程** —— 与上一轮 `.3` 那次同一个方法,也同一个教训: **「我这儿能跑」用的从来不是新用户的路径。**
1 parent f2ea0b0 commit 69a843e

4 files changed

Lines changed: 97 additions & 6 deletions

File tree

.agents/docs/2026-08-20-freestanding-ecosystem-implementation-plan.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,13 @@ capability 作仲裁,实现在别的包。
135135

136136
| `nolibc` 也应做成 `std-freestanding` 的 feature(默认关),与 `alloc-*` 一致 | ⚠️ **我判断它「状态不可达」,而那个判断建立在一次测错的探针上**;正确测量后它**可达,但代价是一个独立项目**。见 §3.1 | 本轮不加;记为后续项 |
137137

138-
⇒ 十四条里有十三条**若不实测就会写进文档**
138+
| 零 libc 模板发布即可用 | ⚠️ **生成出来就是坏的。** `mcpp new --template` 强制注入自依赖,而板级包自己的模块 `#include <stdio.h>` ⇒ 那个从未被请求的依赖在工程本身之前就编不过 | 引擎加 `[template.inject] self = false`;板级包在零 libc 档改为**不贡献而非报错**,但**照常发 runner** |
139+
140+
⇒ 十五条里有十四条**若不实测就会写进文档**
141+
142+
⚠️ 最后这条是唯一一次**发布之后**才发现的:0.4.0 的模板在索引里可见、可生成,
143+
而生成出来的工程跑不起来。发现它的方式是**用发布后的包从零走一遍新用户流程** ——
144+
和上一轮 `.3` 那次同一个方法,也同一个教训:**「我这儿能跑」用的从来不是新用户的路径。**
139145

140146
⭐ 上面这条值得单独记:提法本身是对的(默认关的 feature 与主动加依赖等价,且能让包的
141147
表面一致),**我原来反对它的理由也确实站不住**(中间库无论经 feature 还是经直接依赖

src/scaffold/create.cppm

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,15 @@ export int new_from_package_template(
256256
mcpp::ui::error(instantiated.error());
257257
return 1;
258258
}
259-
auto injected = mcpp::scaffold::inject_self_dependency(
260-
root / "mcpp.toml", vars, chosen->meta.injectSelfFeatures);
261-
if (!injected) {
262-
mcpp::ui::error(injected.error());
263-
return 1;
259+
// A template may decline the self-dependency — see TemplateMeta::injectSelf
260+
// for the case that requires it.
261+
if (chosen->meta.injectSelf) {
262+
auto injected = mcpp::scaffold::inject_self_dependency(
263+
root / "mcpp.toml", vars, chosen->meta.injectSelfFeatures);
264+
if (!injected) {
265+
mcpp::ui::error(injected.error());
266+
return 1;
267+
}
264268
}
265269

266270
// Parsing the completed manifest is the final semantic gate. Nothing is

src/scaffold/template.cppm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ struct TemplateMeta {
118118
std::string postMessage;
119119
// [template.inject] self = { features = [...] }
120120
std::vector<std::string> injectSelfFeatures;
121+
// [template.inject] self = false
122+
//
123+
// ⚠️ A TEMPLATE THAT MUST NOT DEPEND ON ITS OWN PACKAGE IS NOT HYPOTHETICAL.
124+
//
125+
// The self-dependency exists so a template cannot drift from the library
126+
// that ships it, and for almost every template that is right. It is wrong
127+
// for one whose whole subject is depending on NOTHING: a bare-metal package
128+
// may ship a kernel starting point for a target with no C library, and its
129+
// own module includes <stdio.h> — so injecting it makes the generated
130+
// project fail to compile the dependency it never asked for.
131+
//
132+
// Measured: `mcpp new k --template riscv-virt-rt:nolibc` then `mcpp run`
133+
// failed on the board package's own module, in a project whose manifest
134+
// declares no dependencies at all.
135+
bool injectSelf = true;
121136
};
122137

123138
std::expected<TemplateMeta, std::string>
@@ -142,6 +157,12 @@ load_meta(const std::filesystem::path& templateDir) {
142157
meta.isDefault = it->second.as_bool();
143158
if (auto it = t->find("inject"); it != t->end() && it->second.is_table()) {
144159
auto& inj = it->second.as_table();
160+
// The bool form declines the injection outright; the table form
161+
// keeps it and carries features.
162+
if (auto self = inj.find("self");
163+
self != inj.end() && self->second.is_bool()) {
164+
meta.injectSelf = self->second.as_bool();
165+
}
145166
if (auto self = inj.find("self");
146167
self != inj.end() && self->second.is_table()) {
147168
auto& st = self->second.as_table();

tests/unit/test_scaffold.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,63 @@ widget = "1.0.0"
388388
}
389389

390390
} // namespace
391+
392+
// ── [template.inject] self = false ───────────────────────────────────────────
393+
//
394+
// ⚠️ A TEMPLATE THAT MUST NOT DEPEND ON ITS OWN PACKAGE IS NOT HYPOTHETICAL.
395+
//
396+
// The self-dependency keeps a template from drifting from the library that
397+
// ships it, and for almost every template that is right. It is wrong for one
398+
// whose subject is depending on NOTHING: a bare-metal package may ship a kernel
399+
// starting point for a target with no C library, while the package's own module
400+
// includes <stdio.h>. Measured: the injected dependency failed to compile
401+
// before the generated project was reached, in a project whose manifest
402+
// declares no dependencies at all.
403+
TEST(ScaffoldTemplate, InjectSelfCanBeDeclined) {
404+
auto dir = std::filesystem::temp_directory_path() / "mcpp_tmpl_noself";
405+
std::filesystem::remove_all(dir);
406+
std::filesystem::create_directories(dir);
407+
{
408+
std::ofstream f(dir / "template.toml");
409+
f << "[template]\ndescription = \"d\"\n[template.inject]\nself = false\n";
410+
}
411+
auto meta = mcpp::scaffold::load_meta(dir);
412+
ASSERT_TRUE(meta.has_value()) << (meta ? "" : meta.error());
413+
EXPECT_FALSE(meta->injectSelf);
414+
std::filesystem::remove_all(dir);
415+
}
416+
417+
// The default must stay ON: every template that does not say otherwise relies
418+
// on the injection, and a silent flip would leave them all without their own
419+
// package.
420+
TEST(ScaffoldTemplate, InjectSelfDefaultsToOn) {
421+
auto dir = std::filesystem::temp_directory_path() / "mcpp_tmpl_self_default";
422+
std::filesystem::remove_all(dir);
423+
std::filesystem::create_directories(dir);
424+
{
425+
std::ofstream f(dir / "template.toml");
426+
f << "[template]\ndescription = \"d\"\n";
427+
}
428+
auto meta = mcpp::scaffold::load_meta(dir);
429+
ASSERT_TRUE(meta.has_value()) << (meta ? "" : meta.error());
430+
EXPECT_TRUE(meta->injectSelf);
431+
std::filesystem::remove_all(dir);
432+
}
433+
434+
// The table form still carries features, and must not be read as "declined".
435+
TEST(ScaffoldTemplate, InjectSelfTableFormStillInjects) {
436+
auto dir = std::filesystem::temp_directory_path() / "mcpp_tmpl_self_table";
437+
std::filesystem::remove_all(dir);
438+
std::filesystem::create_directories(dir);
439+
{
440+
std::ofstream f(dir / "template.toml");
441+
f << "[template]\ndescription = \"d\"\n"
442+
"[template.inject]\nself = { features = [\"x\"] }\n";
443+
}
444+
auto meta = mcpp::scaffold::load_meta(dir);
445+
ASSERT_TRUE(meta.has_value()) << (meta ? "" : meta.error());
446+
EXPECT_TRUE(meta->injectSelf);
447+
ASSERT_EQ(meta->injectSelfFeatures.size(), 1u);
448+
EXPECT_EQ(meta->injectSelfFeatures[0], "x");
449+
std::filesystem::remove_all(dir);
450+
}

0 commit comments

Comments
 (0)