Skip to content

Commit beafdb3

Browse files
committed
feat: package-based project templates (design v2 T1-T5,T7)
mcpp new --template now accepts a multi-level SPEC: a bare name resolves the builtin registry first (frozen: bin; gui = transitional deprecation alias), then falls through to a package template — pkg | pkg:tmpl | pkg@ver | pkg@ver:tmpl. Packages opt in by shipping templates/<name>/ (template.toml metadata: description / default / post_message / [template.inject] self features). New mcpp.scaffold module: SPEC parsing, {{project.name}}/{{self.name}}/ {{self.version}} rendering (.in files; everything else verbatim; pure data — no hooks/scripts), self-dependency injection (template's own declaration wins), template listing. new --list-templates <pkg[@ver]> prints a package's templates with the default marked. Fetch reuses the dependency machinery (index lua -> semver latest -> install cache). Builtin gui keeps working with a deprecation pointer; its pinned imgui version bumps to current as a transitional stopgap (structural fix is the package template itself: {{self.version}} cannot rot). e2e 69 (hermetic: pre-seeded install cache + local index lua) covers L0/L3 resolution, rendering, inject-vs-declared, listing, error paths, gui deprecation.
1 parent 9efa721 commit beafdb3

4 files changed

Lines changed: 553 additions & 9 deletions

File tree

docs/00-getting-started.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ mcpp pack --mode bundle-all # 全自包含,含 libc 与 ld-linux
122122

123123
## 更多入口
124124

125-
- GUI 起步:`mcpp new myapp --template gui`(imgui.app 窗口骨架,构建后 `mcpp run` 直接出窗口)。
125+
- GUI 起步:`mcpp new myapp --template imgui`(模板随 imgui 库分发、版本自动对齐;
126+
`mcpp new --list-templates imgui` 查看库提供的全部模板,`--template imgui:docking` 选指定模板)。
126127
- 解释默认决策:`mcpp why [toolchain|runtime|deps]`;主机能力体检:`mcpp self doctor`;
127128
机器可读解析清单:构建产物 `target/<triple>/<fp>/resolution.json`

src/cli.cppm

Lines changed: 184 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import mcpp.fetcher;
3838
import mcpp.pm.resolver; // PR-R4: extracted from cli.cppm
3939
import mcpp.pm.commands; // PR-R5: cmd_add / cmd_remove / cmd_update live here now
4040
import mcpp.pm.index_spec; // IndexSpec for [indices] support
41+
import mcpp.scaffold; // package-based project templates
4142
import mcpp.pm.mangle; // Level 1 multi-version fallback (cross-major coexistence)
4243
import mcpp.pm.compat; // 0.0.6: namespace field + dotted-name compat shims
4344
import mcpp.pm.dep_spec;
@@ -1093,20 +1094,191 @@ void gcc_post_install_fixup(const mcpp::config::GlobalConfig& cfg,
10931094

10941095
// --- Commands ---
10951096

1097+
// ─── Package-based templates (design v2: multi-level --template) ──────
1098+
//
1099+
// Resolve SPEC's package@version through the index, ensure the package
1100+
// sources are installed (same cache as dependencies), and return the
1101+
// package root (the directory containing mcpp.toml).
1102+
struct FetchedTemplatePackage {
1103+
std::filesystem::path root;
1104+
std::string name; // short package name (e.g. "imgui")
1105+
std::string version; // resolved exact version
1106+
};
1107+
1108+
std::expected<FetchedTemplatePackage, std::string>
1109+
fetch_template_package(const mcpp::scaffold::TemplateSpec& spec) {
1110+
auto cfg = mcpp::config::load_or_init(/*quiet=*/false,
1111+
make_bootstrap_progress_callback());
1112+
if (!cfg) return std::unexpected(cfg.error().message);
1113+
mcpp::pm::Fetcher fetcher(*cfg);
1114+
1115+
// Namespace candidates mirror dependency lookup: index root first,
1116+
// then the compat namespace.
1117+
std::string ns;
1118+
std::optional<std::string> lua;
1119+
for (std::string cand : {std::string{}, std::string{"compat"}}) {
1120+
if (auto l = fetcher.read_xpkg_lua(cand, spec.pkg)) {
1121+
ns = cand;
1122+
lua = std::move(*l);
1123+
break;
1124+
}
1125+
}
1126+
if (!lua) {
1127+
return std::unexpected(std::format(
1128+
"template package '{}' not found in the index "
1129+
"(check the name, or run `mcpp index update`)", spec.pkg));
1130+
}
1131+
1132+
std::string version = spec.version;
1133+
if (version.empty()) {
1134+
auto v = mcpp::pm::resolve_semver(ns, spec.pkg, "*", fetcher);
1135+
if (!v) return std::unexpected(v.error());
1136+
version = *v;
1137+
}
1138+
1139+
auto installed = fetcher.install_path(ns, spec.pkg, version);
1140+
if (!installed) {
1141+
auto fq = ns.empty() ? spec.pkg : std::format("{}.{}", ns, spec.pkg);
1142+
mcpp::ui::info("Downloading", std::format("{} v{}", fq, version));
1143+
CliInstallProgress progress;
1144+
std::vector<std::string> targets{ std::format("{}@{}", fq, version) };
1145+
auto r = fetcher.install(targets, &progress);
1146+
if (!r) return std::unexpected(std::format(
1147+
"fetch '{}@{}': {}", fq, version, r.error().message));
1148+
if (r->exitCode != 0) return std::unexpected(std::format(
1149+
"fetch '{}@{}' failed (exit {})", fq, version, r->exitCode));
1150+
installed = fetcher.install_path(ns, spec.pkg, version);
1151+
if (!installed) return std::unexpected(std::format(
1152+
"package '{}@{}' install path missing after fetch", fq, version));
1153+
}
1154+
1155+
// Package root = the directory holding mcpp.toml (tarballs usually wrap
1156+
// everything in a single top-level directory).
1157+
std::filesystem::path root = *installed;
1158+
if (!std::filesystem::exists(root / "mcpp.toml")) {
1159+
std::error_code ec;
1160+
for (auto& e : std::filesystem::directory_iterator(root, ec)) {
1161+
if (e.is_directory()
1162+
&& std::filesystem::exists(e.path() / "mcpp.toml")) {
1163+
root = e.path();
1164+
break;
1165+
}
1166+
}
1167+
}
1168+
if (!std::filesystem::exists(root / "mcpp.toml")) {
1169+
return std::unexpected(std::format(
1170+
"package '{}@{}' has no mcpp.toml", spec.pkg, version));
1171+
}
1172+
return FetchedTemplatePackage{root, spec.pkg, version};
1173+
}
1174+
1175+
void print_template_listing(const FetchedTemplatePackage& pkg,
1176+
const std::vector<mcpp::scaffold::TemplateEntry>& entries) {
1177+
std::println("Templates in {}@{}:", pkg.name, pkg.version);
1178+
for (auto& t : entries) {
1179+
std::println(" {:<14}{}{}", t.name,
1180+
t.meta.isDefault ? "(default) " : " ",
1181+
t.meta.description);
1182+
}
1183+
std::println("");
1184+
std::println("usage: mcpp new <name> --template {}[@ver][:<template>]", pkg.name);
1185+
}
1186+
1187+
int list_package_templates(const mcpp::scaffold::TemplateSpec& spec) {
1188+
auto pkg = fetch_template_package(spec);
1189+
if (!pkg) { mcpp::ui::error(pkg.error()); return 1; }
1190+
auto entries = mcpp::scaffold::list_templates(pkg->root);
1191+
if (!entries) { mcpp::ui::error(entries.error()); return 1; }
1192+
print_template_listing(*pkg, *entries);
1193+
return 0;
1194+
}
1195+
1196+
int new_from_package_template(const std::string& name, const std::string& specStr) {
1197+
auto spec = mcpp::scaffold::parse_spec(specStr);
1198+
if (spec.listOnly) return list_package_templates(spec);
1199+
1200+
auto pkg = fetch_template_package(spec);
1201+
if (!pkg) { mcpp::ui::error(pkg.error()); return 1; }
1202+
auto entries = mcpp::scaffold::list_templates(pkg->root);
1203+
if (!entries) { mcpp::ui::error(entries.error()); return 1; }
1204+
1205+
const mcpp::scaffold::TemplateEntry* chosen = nullptr;
1206+
if (spec.tmpl.empty()) {
1207+
for (auto& t : *entries) if (t.meta.isDefault) { chosen = &t; break; }
1208+
if (!chosen) {
1209+
mcpp::ui::error(std::format(
1210+
"package '{}' declares no default template — pick one explicitly:",
1211+
pkg->name));
1212+
print_template_listing(*pkg, *entries);
1213+
return 1;
1214+
}
1215+
} else {
1216+
for (auto& t : *entries) if (t.name == spec.tmpl) { chosen = &t; break; }
1217+
if (!chosen) {
1218+
mcpp::ui::error(std::format(
1219+
"package '{}@{}' has no template '{}'",
1220+
pkg->name, pkg->version, spec.tmpl));
1221+
print_template_listing(*pkg, *entries);
1222+
return 1;
1223+
}
1224+
}
1225+
1226+
std::filesystem::path root = std::filesystem::current_path() / name;
1227+
if (std::filesystem::exists(root)) {
1228+
mcpp::ui::error(std::format("'{}' already exists", root.string()));
1229+
return 1;
1230+
}
1231+
std::error_code ec;
1232+
std::filesystem::create_directories(root, ec);
1233+
if (ec) {
1234+
mcpp::ui::error(std::format("cannot create '{}': {}",
1235+
root.string(), ec.message()));
1236+
return 1;
1237+
}
1238+
1239+
mcpp::scaffold::RenderVars vars{name, pkg->name, pkg->version};
1240+
if (auto err = mcpp::scaffold::instantiate(
1241+
pkg->root / "templates" / chosen->name, root, vars)) {
1242+
mcpp::ui::error(*err);
1243+
return 1;
1244+
}
1245+
if (auto err = mcpp::scaffold::inject_self_dependency(
1246+
root / "mcpp.toml", vars, chosen->meta.injectSelfFeatures)) {
1247+
mcpp::ui::error(*err);
1248+
return 1;
1249+
}
1250+
1251+
mcpp::ui::status("Created", std::format(
1252+
"{} (template {}@{}:{})", name, pkg->name, pkg->version, chosen->name));
1253+
if (!chosen->meta.postMessage.empty())
1254+
std::println("{}", chosen->meta.postMessage);
1255+
return 0;
1256+
}
1257+
10961258
int cmd_new(const mcpplibs::cmdline::ParsedArgs& parsed) {
1259+
// Discovery mode: `mcpp new --list-templates <pkg>[@ver]` — no project.
1260+
if (auto lt = parsed.value("list-templates")) {
1261+
return list_package_templates(mcpp::scaffold::parse_spec(*lt));
1262+
}
1263+
10971264
std::string name = parsed.positional(0);
10981265
if (name.empty()) {
10991266
std::println(stderr, "error: `mcpp new` requires a package name (e.g. `mcpp new hello`)");
11001267
return 2;
11011268
}
11021269

1103-
// `--template` selects the project skeleton: "bin" (default) or "gui"
1104-
// (an imgui.app starter — Tier-0 zero-boilerplate window).
1270+
// `--template` multi-level SPEC (design v2):
1271+
// builtin registry (frozen: bin; gui = transitional alias), else a
1272+
// package template: pkg | pkg:tmpl | pkg@ver | pkg@ver:tmpl.
11051273
std::string tmpl = "bin";
11061274
if (auto t = parsed.value("template")) tmpl = *t;
1275+
if (tmpl == "gui") {
1276+
mcpp::ui::warning(
1277+
"--template gui is deprecated; use `--template imgui` "
1278+
"(the template then ships with — and version-tracks — the library)");
1279+
}
11071280
if (tmpl != "bin" && tmpl != "gui") {
1108-
std::println(stderr, "error: unknown --template '{}' (expected: bin | gui)", tmpl);
1109-
return 2;
1281+
return new_from_package_template(name, tmpl);
11101282
}
11111283
const bool gui = (tmpl == "gui");
11121284

@@ -1130,7 +1302,7 @@ int cmd_new(const mcpplibs::cmdline::ParsedArgs& parsed) {
11301302
// The GUI template depends on the imgui module package. It does not
11311303
// pin a toolchain — mcpp resolves the environment/default toolchain
11321304
// and the GL runtime is closed by the ecosystem (compat.glx-runtime).
1133-
os << "\n[dependencies]\nimgui = \"0.0.2\"\n";
1305+
os << "\n[dependencies]\nimgui = \"0.0.5\"\n";
11341306
}
11351307
}
11361308
// src/main.cpp — template with PROJECT placeholder, replaced with `name`.
@@ -5763,9 +5935,13 @@ int run(int argc, char** argv) {
57635935
// ─── project commands ──────────────────────────────────────────
57645936
.subcommand(cl::App("new")
57655937
.description("Create a new mcpp package skeleton")
5766-
.arg(cl::Arg("name").help("Package directory name").required())
5767-
.option(cl::Option("template").short_name('t').takes_value().value_name("KIND")
5768-
.help("Project template: bin (default) | gui (imgui.app window starter)"))
5938+
// not .required(): `--list-templates` runs without a name
5939+
// (cmd_new validates presence for project creation itself).
5940+
.arg(cl::Arg("name").help("Package directory name"))
5941+
.option(cl::Option("template").short_name('t').takes_value().value_name("SPEC")
5942+
.help("bin (default) | <pkg>[@ver][:<template>] — package-shipped template"))
5943+
.option(cl::Option("list-templates").takes_value().value_name("PKG")
5944+
.help("List the templates a package ships (PKG[@ver])"))
57695945
.action(wrap_rc(cmd_new)))
57705946
.subcommand(cl::App("build")
57715947
.description("Build the current package")

0 commit comments

Comments
 (0)