From a78cba802080dbeeb652196578aa0b35ef315e7d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Wed, 10 Jun 2026 10:58:27 +0800 Subject: [PATCH] fix(new): resolve namespaced packages in --template fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mcpp new --template llmapi` failed with `fetch 'llmapi@0.2.6' failed`: the index lookup finds pkgs/l/llmapi.lua by bare filename, but the namespace stayed "" while the descriptor declares namespace="mcpplibs" (legacy name="mcpplibs.llmapi") — and xlings resolves install targets by the descriptor's qualified name. fetch_template_package now derives the structured (namespace, shortName) from the descriptor fields via the new pm::compat::descriptor_coordinates helper instead of trusting the filename hit. Root-index packages (imgui) keep namespace "" and still install by bare name; legacy embedded/dotted names and canonical namespace fields all normalize to the same coordinates. Covered by unit tests in test_pm_compat. --- src/cli.cppm | 24 ++++++++++----- src/pm/compat.cppm | 41 ++++++++++++++++++++++++++ tests/unit/test_pm_compat.cpp | 55 +++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 7 deletions(-) diff --git a/src/cli.cppm b/src/cli.cppm index c9ae75cf..cc1aff9e 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -1068,11 +1068,9 @@ fetch_template_package(const mcpp::scaffold::TemplateSpec& spec) { // Namespace candidates mirror dependency lookup: index root first, // then the compat namespace. - std::string ns; std::optional lua; for (std::string cand : {std::string{}, std::string{"compat"}}) { if (auto l = fetcher.read_xpkg_lua(cand, spec.pkg)) { - ns = cand; lua = std::move(*l); break; } @@ -1083,16 +1081,28 @@ fetch_template_package(const mcpp::scaffold::TemplateSpec& spec) { "(check the name, or run `mcpp index update`)", spec.pkg)); } + // The filename hit alone does not carry the namespace: a bare spec + // like "llmapi" finds pkgs/l/llmapi.lua even though the descriptor + // declares `namespace = "mcpplibs"`, and xlings resolves install + // targets by the descriptor's qualified name. Derive the structured + // (namespace, shortName) from the descriptor fields. + auto coords = mcpp::pm::compat::descriptor_coordinates( + spec.pkg, + mcpp::manifest::extract_xpkg_namespace(*lua), + mcpp::manifest::extract_xpkg_name(*lua)); + const std::string& ns = coords.namespace_; + const std::string& shortName = coords.shortName; + std::string version = spec.version; if (version.empty()) { - auto v = mcpp::pm::resolve_semver(ns, spec.pkg, "*", fetcher); + auto v = mcpp::pm::resolve_semver(ns, shortName, "*", fetcher); if (!v) return std::unexpected(v.error()); version = *v; } - auto installed = fetcher.install_path(ns, spec.pkg, version); + auto installed = fetcher.install_path(ns, shortName, version); if (!installed) { - auto fq = ns.empty() ? spec.pkg : std::format("{}.{}", ns, spec.pkg); + auto fq = ns.empty() ? shortName : std::format("{}.{}", ns, shortName); mcpp::ui::info("Downloading", std::format("{} v{}", fq, version)); CliInstallProgress progress; std::vector targets{ std::format("{}@{}", fq, version) }; @@ -1101,7 +1111,7 @@ fetch_template_package(const mcpp::scaffold::TemplateSpec& spec) { "fetch '{}@{}': {}", fq, version, r.error().message)); if (r->exitCode != 0) return std::unexpected(std::format( "fetch '{}@{}' failed (exit {})", fq, version, r->exitCode)); - installed = fetcher.install_path(ns, spec.pkg, version); + installed = fetcher.install_path(ns, shortName, version); if (!installed) return std::unexpected(std::format( "package '{}@{}' install path missing after fetch", fq, version)); } @@ -1123,7 +1133,7 @@ fetch_template_package(const mcpp::scaffold::TemplateSpec& spec) { return std::unexpected(std::format( "package '{}@{}' has no mcpp.toml", spec.pkg, version)); } - return FetchedTemplatePackage{root, spec.pkg, version}; + return FetchedTemplatePackage{root, shortName, version}; } void print_template_listing(const FetchedTemplatePackage& pkg, diff --git a/src/pm/compat.cppm b/src/pm/compat.cppm index 21df2acd..eb1d2c9a 100644 --- a/src/pm/compat.cppm +++ b/src/pm/compat.cppm @@ -73,6 +73,47 @@ inline ResolvedName resolve_package_name(std::string_view name, return r; } +// ─── Descriptor-derived coordinates ────────────────────────────────── +// +// Derive the structured (namespace, shortName) for a package whose index +// descriptor was located by filename candidates from an unqualified spec +// (`mcpp new --template `). The filename hit alone is not enough: +// pkgs/l/llmapi.lua is found by the bare name "llmapi" while the +// descriptor declares `namespace = "mcpplibs"` (and possibly a legacy +// dotted `name = "mcpplibs.llmapi"`), and xlings resolves install targets +// by the descriptor's qualified name. +// +// Unlike resolve_package_name(), a bare name with no namespace field +// stays in the index root (namespace "") — root packages such as "imgui" +// install by their bare name. + +inline ResolvedName descriptor_coordinates(std::string_view specPkg, + std::string_view luaNs, + std::string_view luaName) +{ + ResolvedName r; + std::string name(luaName.empty() ? specPkg : luaName); + r.namespace_ = std::string(luaNs); + + if (!r.namespace_.empty()) { + // Legacy descriptors embed the namespace in the name too + // (namespace = "mcpplibs", name = "mcpplibs.llmapi"). + auto prefix = r.namespace_ + "."; + if (name.starts_with(prefix)) { + name = name.substr(prefix.size()); + r.usedLegacySplit = true; + } + } else if (auto dot = name.find('.'); dot != std::string::npos) { + // Legacy dotted name without a namespace field. + r.namespace_ = name.substr(0, dot); + name = name.substr(dot + 1); + r.usedLegacySplit = true; + } + + r.shortName = std::move(name); + return r; +} + // Reconstruct the fully-qualified name from (namespace, shortName). // Default-namespace packages use the bare short name; others use // "ns.short". diff --git a/tests/unit/test_pm_compat.cpp b/tests/unit/test_pm_compat.cpp index 6a553199..2ab17028 100644 --- a/tests/unit/test_pm_compat.cpp +++ b/tests/unit/test_pm_compat.cpp @@ -93,3 +93,58 @@ TEST(DependencySelector, ExplicitRootSelectorHasOnlyThatRoot) { EXPECT_EQ(selector.candidates[0].namespace_, "compat"); EXPECT_EQ(selector.candidates[0].shortName, "gtest"); } + +// ─── descriptor_coordinates (package-template fetch) ──────────────── + +TEST(PmCompat, DescriptorCoordinatesLegacyEmbeddedNamespace) { + // pkgs/l/llmapi.lua: namespace = "mcpplibs", name = "mcpplibs.llmapi" + auto r = mcpp::pm::compat::descriptor_coordinates( + "llmapi", "mcpplibs", "mcpplibs.llmapi"); + + EXPECT_EQ(r.namespace_, "mcpplibs"); + EXPECT_EQ(r.shortName, "llmapi"); + EXPECT_TRUE(r.usedLegacySplit); +} + +TEST(PmCompat, DescriptorCoordinatesCanonicalNamespaceField) { + auto r = mcpp::pm::compat::descriptor_coordinates( + "llmapi", "mcpplibs", "llmapi"); + + EXPECT_EQ(r.namespace_, "mcpplibs"); + EXPECT_EQ(r.shortName, "llmapi"); + EXPECT_FALSE(r.usedLegacySplit); +} + +TEST(PmCompat, DescriptorCoordinatesRootPackageStaysInRoot) { + // pkgs/i/imgui.lua: namespace = "", name = "imgui" — must NOT be + // promoted to the default namespace (it installs by its bare name). + auto r = mcpp::pm::compat::descriptor_coordinates("imgui", "", "imgui"); + + EXPECT_EQ(r.namespace_, ""); + EXPECT_EQ(r.shortName, "imgui"); + EXPECT_FALSE(r.usedLegacySplit); +} + +TEST(PmCompat, DescriptorCoordinatesLegacyDottedNameWithoutNamespace) { + auto r = mcpp::pm::compat::descriptor_coordinates( + "tinyhttps", "", "mcpplibs.tinyhttps"); + + EXPECT_EQ(r.namespace_, "mcpplibs"); + EXPECT_EQ(r.shortName, "tinyhttps"); + EXPECT_TRUE(r.usedLegacySplit); +} + +TEST(PmCompat, DescriptorCoordinatesFallsBackToSpecWhenNameMissing) { + auto r = mcpp::pm::compat::descriptor_coordinates("imgui", "", ""); + + EXPECT_EQ(r.namespace_, ""); + EXPECT_EQ(r.shortName, "imgui"); +} + +TEST(PmCompat, DescriptorCoordinatesCompatNamespace) { + auto r = mcpp::pm::compat::descriptor_coordinates( + "mbedtls", "compat", "compat.mbedtls"); + + EXPECT_EQ(r.namespace_, "compat"); + EXPECT_EQ(r.shortName, "mbedtls"); +}