Skip to content
Open
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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [2026.8.22.1] — 在飞

### 新增

- **`mcpp search` 显示包的可用版本,`mcpp add` 的建议同样携带。**

search 的命中行追加该包描述符 per-OS 版本表的并集:semver 降序、按键去重,
默认显示最新 3 个并以 `, ...` 标记截断,`--all-versions` 显示全部。描述符不可读
或未发布任何版本的包保持原两列输出——富化是尽力而为的展示,不是新的失败路径。

```
$ mcpp search imgui
compat:imgui Dear ImGui immediate-mode GUI library core sources (1.92.8, 1.92.8-docking)
mcpplibs:imgui C++23 module package for Dear ImGui core and GLFW/OpenGL3 backends (0.0.6, 0.0.5, 0.0.4)
```

`mcpp add` 未命中时的跨命名空间建议从裸 FQN 升级为带版本:
`compat.eui-neo (0.5.6, 0.5.5, 0.5.3)`。数据是白捡的——did-you-mean 扫描本就要
打开每个候选 `.lua` 读身份(#278),版本只是同一段文本的再一次遍历;排序复用
SemVer 解析(`version_req`),不可解析的键保留原文排在最后(#363 的教训:任意的
索引键无法从解析形态复原)。build 失败路径的同款提示同步升级,两条路径不说两套话。

排序与扫描各有单测钉住;e2e 162 断言 build 与 add 两侧的建议都带版本。
(#487,#324 的遗留半边)

## [2026.8.20.2] — 2026-08-20

### 新增
Expand Down
12 changes: 7 additions & 5 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -2646,17 +2646,19 @@ prepare_build(bool print_fingerprint,

// T12 — did-you-mean. DIAGNOSTIC ONLY: the scan runs solely on
// this already-failed path and its result never leaves the
// error string (see Fetcher::scan_fqns_with_short_name).
// error string (see Fetcher::scan_short_name_matches).
std::string hint;
if (auto cfg = get_cfg()) {
auto fqns = mcpp::pm::cross_namespace_matches(
auto suggestions = mcpp::pm::cross_namespace_suggestions(
index_route(*cfg), candidates.front().shortName);
if (!fqns.empty()) {
if (!suggestions.empty()) {
hint += "\n a package with this name exists under "
"another namespace:";
for (auto& fqn : fqns) hint += "\n " + fqn;
for (auto& suggestion : suggestions)
hint += "\n " + suggestion.fqn
+ suggestion.versions_label();
if (auto suggested = mcpp::pm::parse_package_selector(
fqns.front()); suggested
suggestions.front().fqn); suggested
&& suggested->namespace_) {
hint += std::format(
"\n namespace omission means `{}`. write the "
Expand Down
2 changes: 2 additions & 0 deletions src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,8 @@ int run(int argc, char** argv) {
.subcommand(cl::App("search")
.description("Search packages in configured registries")
.arg(cl::Arg("keyword").help("Search keyword (substring match)").required())
.option(cl::Option("all-versions")
.help("List every published version instead of the latest few"))
.action(wrap_rc(cmd_search)))
.subcommand(cl::App("publish")
.description("Publish package to default registry")
Expand Down
2 changes: 1 addition & 1 deletion src/cli/cmd_registry.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export int cmd_search(const mcpplibs::cmdline::ParsedArgs& parsed) {
std::println(stderr, "error: `mcpp search` requires a keyword");
return 2;
}
return mcpp::pm::search_packages(keyword);
return mcpp::pm::search_packages(keyword, parsed.is_flag_set("all-versions"));
}

export int cmd_index_list(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) {
Expand Down
43 changes: 43 additions & 0 deletions src/manifest/xpkg.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import mcpp.pm.dep_spec;
import mcpp.pm.dependency_selector;
import mcpp.platform;
import mcpp.platform.axis;
import mcpp.version_req;

export namespace mcpp::manifest {

Expand Down Expand Up @@ -53,6 +54,12 @@ list_xpkg_version_entries(std::string_view luaContent,
std::vector<std::string>
list_xpkg_versions(std::string_view luaContent,
const mcpp::platform::PlatformKey& platform);

// Union of per-OS version-key lists, semver-descending, deduplicated — the
// display shape behind `mcpp add` suggestions and `mcpp search` (#487). See
// the definition for the ordering rules applied to unparsable keys.
std::vector<std::string> merge_xpkg_versions_desc(
const std::vector<std::vector<std::string>>& perPlatform);
// Extract the `namespace` field from an xpkg .lua's `package = { ... }` block.
// Returns empty string if the field is absent (legacy descriptors).
std::string extract_xpkg_namespace(std::string_view luaContent);
Expand Down Expand Up @@ -1062,6 +1069,42 @@ list_xpkg_versions(std::string_view luaContent,
return out;
}

// #487 — the union view over a descriptor's per-OS version tables, for
// display in `mcpp add` suggestions and `mcpp search`. Sorted
// semver-descending and deduplicated by exact key.
//
// Ordering parses each key with the SemVer grammar; a key that does not parse
// keeps its original text (#363's lesson: an arbitrary index key cannot be
// reproduced from its parsed form), sorts AFTER every parsable key, and orders
// among itself lexicographically. Equal-Version keys written differently
// (e.g. "1.0" vs "1.0.0") keep their first-seen input order.
std::vector<std::string> merge_xpkg_versions_desc(
const std::vector<std::vector<std::string>>& perPlatform) {
std::vector<std::string> out;
for (auto& list : perPlatform)
for (auto& key : list)
if (std::find(out.begin(), out.end(), key) == out.end())
out.push_back(key);

auto parsed = [](const std::string& s)
-> std::optional<mcpp::version_req::Version> {
auto v = mcpp::version_req::parse_version(s);
if (!v) return std::nullopt;
return std::move(*v);
};

std::stable_sort(out.begin(), out.end(),
[&](const std::string& a, const std::string& b) {
auto pa = parsed(a);
auto pb = parsed(b);
if (pa && pb) return *pb < *pa; // desc
if (pa) return true; // parsable first
if (pb) return false;
return a < b; // both opaque
});
return out;
}

// Parses the `{ { glob = "...", cflags/cxxflags/asmflags/defines = {...} },
// ... }` array-of-tables shape shared by `[build]`-level `flags` and #253's
// `features.<name>.flags` — one entry grammar, two anchoring keys. `ctxLabel`
Expand Down
4 changes: 2 additions & 2 deletions src/pm/commands.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,9 @@ inline int cmd_add(const mcpplibs::cmdline::ParsedArgs& parsed) {
if (!found.hit && found.conclusive) {
std::string hint;
if (!selector.candidates.empty()) {
for (auto& fqn : mcpp::pm::cross_namespace_matches(
for (auto& suggestion : mcpp::pm::cross_namespace_suggestions(
route, selector.candidates.front().shortName)) {
hint += "\n " + fqn;
hint += "\n " + suggestion.fqn + suggestion.versions_label();
}
}
if (!hint.empty()) {
Expand Down
36 changes: 33 additions & 3 deletions src/pm/index_management.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,28 @@ import mcpp.platform.xlings;

namespace mcpp::pm {

// `mcpp search <keyword>`.
export int search_packages(const std::string& keyword) {
namespace {
// #487 — "1.0.0, 0.9.0" from an already-sorted-desc list. `keep` bounds the
// default view; --all-versions passes std::numeric_limits<std::size_t>::max().
std::string join_versions(const std::vector<std::string>& versions,
std::size_t keep) {
std::string joined;
auto n = std::min(versions.size(), keep);
for (std::size_t i = 0; i < n; ++i) {
if (i) joined += ", ";
joined += versions[i];
}
return joined;
}
} // namespace

// `mcpp search <keyword> [--all-versions]`.
//
// Each hit line appends the versions the package publishes, merged across
// its descriptor's per-OS tables (#487): the latest three by default, all of
// them under --all-versions. A hit with no readable descriptor prints exactly
// as before — enrichment is best-effort display, never a failure.
export int search_packages(const std::string& keyword, bool allVersions = false) {
auto cfg = mcpp::config::load_or_init(/*quiet=*/false, mcpp::fetcher::make_bootstrap_progress_callback());
if (!cfg) { mcpp::ui::error(cfg.error().message); return 4; }

Expand Down Expand Up @@ -51,9 +71,19 @@ export int search_packages(const std::string& keyword) {
std::println("No packages match `{}`.", keyword);
return 0;
}
constexpr std::size_t kAllVersions =
std::numeric_limits<std::size_t>::max();
std::println("");
for (auto& h : *hits) {
std::println(" {:<20} {}", h.name, h.description);
auto versions = f.versions_for_hit(h);
if (versions.empty()) {
std::println(" {:<20} {}", h.name, h.description);
} else {
std::println(" {:<20} {} ({})", h.name, h.description,
join_versions(versions, allVersions
? kAllVersions
: 3));
}
}
return 0;
}
Expand Down
17 changes: 10 additions & 7 deletions src/pm/index_route.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,12 @@ struct Lookup {
Lookup lookup_descriptor(const IndexRoute& route,
const std::vector<DependencyCoordinate>& candidates);

// Diagnostic only: fully-qualified names carrying `shortName` under some other
// namespace, for did-you-mean text on an already-failed lookup.
std::vector<std::string> cross_namespace_matches(const IndexRoute& route,
std::string_view shortName);
// Diagnostic only: descriptors carrying `shortName` under some other
// namespace, for did-you-mean text on an already-failed lookup. Each record
// keeps the FQN plus the versions that package publishes (#487) — display
// only, never a resolution input (see Fetcher::scan_short_name_matches).
std::vector<mcpp::fetcher::Fetcher::ShortNameMatch> cross_namespace_suggestions(
const IndexRoute& route, std::string_view shortName);

// The `[indices]` a project at `root` effectively sees, including the
// workspace-root inheritance a member gets for free (#224).
Expand Down Expand Up @@ -229,8 +231,9 @@ Lookup lookup_descriptor(const IndexRoute& route,
return out;
}

std::vector<std::string> cross_namespace_matches(const IndexRoute& route,
std::string_view shortName) {
std::vector<mcpp::fetcher::Fetcher::ShortNameMatch>
cross_namespace_suggestions(
const IndexRoute& route, std::string_view shortName) {
if (!route.cfg) return {};
mcpp::fetcher::Fetcher fetcher(*route.cfg);
auto roots = fetcher.builtin_index_roots();
Expand All @@ -242,7 +245,7 @@ std::vector<std::string> cross_namespace_matches(const IndexRoute& route,
}
}
}
return mcpp::fetcher::Fetcher::scan_fqns_with_short_name(roots, shortName);
return mcpp::fetcher::Fetcher::scan_short_name_matches(roots, shortName);
}

IndexMap effective_indices(const std::filesystem::path& root) {
Expand Down
Loading
Loading