Skip to content
Merged
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
93 changes: 93 additions & 0 deletions .agents/docs/2026-07-09-project-index-scope-global-infra-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 项目本地模式下的索引作用域:别把默认全局的官方索引(xim)注入项目组(架构分析 + 修复设计)

**日期**: 2026-07-09
**范围**: `src/config.cppm`(`ensure_project_index_dir`)、`src/pm/index_spec.cppm`、`src/build/prepare.cppm`
**性质**: mcpp 侧架构缺陷分析与修复设计。这是 OpenCV 收录时 `mcpp test` 沙箱里 `cmake` 不可执行故障的**唯一正解所在**。
## 文档族与职责边界(三份配套,全部根治设计、无 workaround)

| # | 仓库 / 文档 | 角色 | 与本故障的关系 |
|---|---|---|---|
| ① | mcpp-index `2026-07-09-mcpp-builddep-loader-store-split-rootcause.md` | 根因总报告(现象/实证/归责) | 先读,了解全貌 |
| ② | **本篇** mcpp `2026-07-09-project-index-scope-global-infra-fix.md` | **本故障的唯一必需修复(根治)** | 修 ② 即可让 OpenCV 闭环 |
| ③ | openxlings/xlings `2026-07-09-scope-consistency-installed-check-and-loader-resolution.md` | xlings 侧**独立**健壮性缺口(根治) | 不由本故障触发、与 ② 互不依赖 |

**职责边界**:OpenCV/cmake 故障的**必需且唯一**修复是 ②(本篇)。③ 是一个**独立**的 additive loader 解析缺口(合法"项目消费者+全局 provider"场景),**不是本故障的必要条件**;两者可各自独立落地。**三份均采用根治设计,不提供也不采用任何 workaround。**

---

## 0. 结论(TL;DR)

- **缺陷**:mcpp 在项目本地模式下,把**官方全局基础设施索引 `xim`** 强注册为**项目作用域** index repo(`config.cppm:684-697`)。因 xlings 把"项目 index_repos 里的包"一律判 `PackageScope::Project`,导致 `xim` 的**全局工具**(cmake/glibc/gcc/make/binutils)被整体错误地"项目化"、装进项目 store —— 而它们本应装进全局 registry。由此引出 loader 不落项目 store、cmake interpreter 悬空、不可执行的连锁。
- **根**:**xlings 的作用域默认就是全局** —— 一个 repo 只有落进"项目组"(项目 `.xlings.json` 的 `index_repos` / 项目 discovered sub-index)才是 Project,其余(全局 repos、全局发现的 sub-index)恒 Global(`catalog.cppm:175-224`)。`xim` 及其 sub-index 本在全局组 → 本应 Global。**是 mcpp 主动把 `xim` 塞进了项目组**(`config.cppm:684-697` 把官方 xim append 进项目 `customRepos`),把一个默认全局的索引挪成了项目作用域。
- **多余**:xlings 原生就在全局解析 `xim`(全局 index_repos + registry 本地 clone `registry/data/xim-pkgindex/pkgs`),并经 additive 让项目可见。`xim:*` 本会 Global 解析 → 装 registry。mcpp 的注入不必要。
- **修复原则(不硬编码)**:**global 是默认;mcpp 只把用户在 `mcpp.toml [indices]` 声明的项目本地索引放进项目组,绝不注入用户未声明的官方 `xim`。** 删掉这次注入即根治;xim 及其 sub-index 留在全局 → registry;cmake/glibc/gcc/make 同店 → interpreter 天然指 registry glibc → 可执行。**xlings 无需改动。** 不建 `is_global_infra` 白名单(违背"global 默认"、且漏 sub-index)。

---

## 1. mcpp 项目本地模式的架构(先厘清哪些是对的)

- **触发**:工程 `mcpp.toml` 声明自定义 `[indices]`(如本地 path 索引)→ 进入项目本地模式(`make_project_xlings_env`,`config.cppm:100-105`;`prepare.cppm:1188` `useProjectEnv = idxSpec && !idxSpec->is_builtin()`)。
- **目的(正当)**:让**用户自定义索引**的包**项目作用域隔离**(不污染全局、可复现)。这本身没问题。
- **工具链走全局(正确)**:默认工具链 `gcc@16.1.0`(含 glibc)经 `make_xlings_env`(无 projectDir)装进 **registry**(`prepare.cppm:805-810`),global 作用域、共享。**对。**
- **一次调用挂双 store**:项目模式命令前缀同时设 `XLINGS_HOME=registry` + `XLINGS_PROJECT_DIR=<proj>/.mcpp`(`xlings.cppm:730-738`)。这本身也 OK —— xlings 支持 additive(project 叠加 global)。

→ 问题**不在**"用了项目本地模式",而在**把哪些索引归入了项目作用域**。

## 2. 缺陷:把全局基础设施索引 `xim` 归进了项目作用域

`ensure_project_index_dir`(`config.cppm:662-767`)做了两件事:

1. **`config.cppm:684-697`(致命)**:把官方 `xim` 索引(`officialIndex = cfg.xlingsHome()/data/xim-pkgindex`)以名字 `xim` **append 进项目 `customRepos`**,随后 `seed_xlings_json`(`:707`)写进项目 `.mcpp/.xlings.json` 的 `index_repos`。
- 后果:xlings `catalog` 把 `project_index_repos()` 里的包判 `PackageScope::Project` → `storeRoot=project_data_dir/xpkgs`。→ **凡从 `xim` 解析到的包(cmake/glibc/gcc/make/binutils)全部项目化、装项目 store。**
2. **`config.cppm:745-765`(冗余)**:把 `xim` 索引 symlink/copy 进项目 data dir(`<proj>/.mcpp/.xlings/data/xim-pkgindex`)。意图注释 `:745-748`:"让 `xim:python@latest` 之类能解析,免于回退到 remote 索引更新"。

**根(违背"global 默认")**:xlings 的作用域默认全局——只有落进"项目组"才是 Project(`catalog.cppm:175-224`)。`config.cppm:684-697` 这段是**无条件**把官方 `xim` 注入项目 `customRepos`(不经 `is_builtin` 判定、也不是用户声明),等于**主动把一个默认全局的索引搬进项目组**。`xim` 本是 xlings 全局默认索引(`config.cppm:556-561`),连同其动态 sub-index 都应留在全局——不该被注入项目。

**证据(实测)**:项目 `.mcpp/.xlings.json` 的 `index_repos = [compat(本地,对), xim(官方全局,被项目化)]`;项目 store 里出现 `xim-x-cmake/gcc/glibc/make/binutils`,而 fresh registry **无 cmake**(即 cmake 只被装进了项目)。

## 3. 为什么"强加"既多余又有害

- **多余**:xlings 原生就把 `xim` auto-add 进 **globalIndexRepos_**(`config.cppm:556-561`),catalog 同时扫 project+global 两套 repo,项目模式 additive over global。registry 里已有 `xim-pkgindex` 本地 clone。→ **`xim:cmake` 本会 Global 解析 → 装 registry → 对项目可见,无需 mcpp 再把 xim 加进项目 repos。**
- **有害**:一旦项目化,全局工具装进项目 store;其 loader dep glibc 又被工具链 bootstrap 装在 registry 并记录,项目安装时被"已装"复用跳过 → 项目 store 的 glibc 空 → elfpatch 把 cmake interpreter 指向空目录 → 悬空。**故障链的起点就是这次错误的作用域归属。**

## 4. 修复设计

### 设计原则:**global 是默认,项目化是显式且仅限用户声明**
xlings 的作用域已由"repo 落在哪一组"决定(`catalog.cppm:175-224`):`project_index_repos()` → Project;`global_index_repos()` / discovered 全局 sub-index(`xim-indexrepos.json`)→ Global;discovered 项目 sub-index → Project。**即"不落进项目组的一切,默认全局"。** `xim` 及其 sub-index 本在全局组/全局发现 → 天然 Global。

因此**不建任何硬编码"全局基础设施"白名单**(既违背"global 默认",又漏掉动态发现的 sub-index)。正确原则只有一句:

> **mcpp 只把"用户在 `mcpp.toml [indices]` 里声明的项目本地索引"放进项目组;绝不注入用户未声明的索引(尤其官方 `xim`)。**

### 根治方案(单一设计;不含 workaround)
1. **删除 `config.cppm:684-697`**(`ensure_project_index_dir` 里把官方 `xim` append 进项目 `customRepos` 的整段)。→ mcpp 不再把 xim 从全局组搬进项目组;xim 及其 sub-index 留在全局 → Global → registry。
2. **收敛 `config.cppm:745-765`**(项目 data dir 的 xim 副本暴露)——它是"项目化 xim"的配套产物,删注入后不再需要。
3. **保留 `:672` 遍历中"用户声明的本地自定义索引 → 项目组"的逻辑**(这是项目本地隔离的正当用途,不动)。
4. **全局侧保证 `xim:*` 本地可解析(回应原动机、免 remote)**:确保全局 `xim` 条目/发现指向 **registry 的本地 clone**(`registry/data/xim-pkgindex`),使 `xim:*` 以 Global 解析、装 registry、不触发 remote fetch。原 `:745-748` 的"避免 remote 索引更新"顾虑,在全局侧被正确满足。

落地后:`xim` 的全部工具(cmake/glibc/gcc/make/binutils)及其 sub-index → Global → registry(同一全局 store);项目模式经 xlings additive 天然可见并复用;工具链(§1 已正确 global)与它们同店 → interpreter 天然自洽。**完整根治,不依赖任何 xlings 改动,且对 sub-index 机制天然正确(mcpp 一概不碰全局发现的 sub-index)。**

### ❌ 不要做(明确排除的 workaround)
- **不建 `is_global_infra` 硬编码白名单**:硬编码 `{mcpplibs,xim,awesome,scode,d2x}` 违背"global 默认",且漏掉 xim 动态 sub-index —— 正解是"别注入",而非"枚举谁是全局"。
- **不做"删注入但仍以其它方式把 xim 拉进项目"**的变体。
- **不在 xlings 侧迁就**:不"把 glibc 物化进项目 store",不"改 elfpatch 指项目空目录"——那是坐实错误作用域(见文档 ③ 反模式)。**正解在本仓:别把默认全局的索引搬进项目组。**

## 5. 与 xlings 的关系(职责边界)
- 修好本缺陷后:cmake/glibc/gcc/make 全 Global → registry(同一全局 store)→ cmake interpreter 天然指 registry glibc(存在)→ 可执行。**xlings 的 additive 模型本就支持此,无需任何改动。**
- xlings 侧另有一个**相互独立**的健壮性缺口(合法"项目消费者 + 全局 loader-provider"时,elfpatch 的 provider 解析口径),记录在 xlings 仓文档里,**不是本故障的必要条件、也不由本修复触发**。

## 6. 验证与最小复现
- **修前(现状)**:含自定义 `[indices]` 的工程 + compat 包声明 build-dep `xim:cmake` 并在 install() 执行它 → 任意机器(含 fresh `MCPP_HOME`)`mcpp test` → `cmake: cannot execute`(项目 store 的 xim-x-glibc 只有 `.xpkg.lua`)。现成:`mcpp-index` 仓 `tests/examples/opencv` + `pkgs/c/compat.opencv.lua`。
- **修后预期**:项目 `.xlings.json` 的 `index_repos` 只含本地 `compat`;`xim-x-cmake/glibc/gcc/make` 出现在 **registry** 而非项目 store;`cmake` 可执行;`compat.opencv` 的 `mcpp test --workspace` 三平台闭环(descriptor 与测试工程零改动)。
- **回归点**:确认移除项目 xim 后,`xim:*` 依赖仍能解析(靠 global auto-add + registry 本地 clone),且不触发 remote 索引 fetch。

## 7. 影响
- **compat.opencv**:本修复是其闭环的前置;修好后 descriptor(已是零兼容代码的 `pkginfo.build_dep` 形式)与 `tests/examples/opencv` **零改动**即通过 `mcpp test --workspace`,可进 CI 选跑、开 PR。
- **面向未来**:确立"**global 默认、只项目化用户声明的本地索引、绝不注入官方索引**"这一不变式,避免后续任何"全局工具被项目化"的复发;对 xim 动态 sub-index 天然正确(mcpp 不碰全局发现)。

## 8. 改动点清单(单一根治设计,不硬编码)
- `src/config.cppm:684-697` —— **删除**把官方 `xim` append 进项目 `customRepos` 的整段(不再把默认全局的索引注入项目组)。
- `src/config.cppm:745-765` —— 收敛项目 data dir 的 xim 副本暴露(注入删除后不再需要);由全局 `xim` 指 registry 本地 clone 满足解析(免 remote fetch)。
- `src/config.cppm:662-682`(`ensure_project_index_dir` 遍历)—— **保持不变**:仅项目化用户在 `[indices]` 声明的本地自定义索引(项目隔离的正当用途)。
- **不改** `is_builtin()`,**不新增** `is_global_infra` 白名单(违背"global 默认"、漏 sub-index)。
- e2e/工作区:`mcpp test --workspace` 覆盖"含全局 build-dep 工具的 compat 包"(opencv 即样例)+ 断言 xim 工具落在 registry 而非项目 store,防回归。
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.87] — 2026-07-09

### 修复

- **项目本地模式:不再把官方全局索引 `xim` 注入项目作用域**。此前带自定义
`[indices]`(本地 path 索引)的工程进入项目本地模式时,`ensure_project_index_dir`
会无条件把官方 `xim` 索引 append 进项目 `.xlings.json` 的 `index_repos`
(`config.cppm`),意在让 `xim:*` 依赖在项目模式可解析。但 xlings 按"repo 落在
哪一组"决定作用域——项目 `index_repos` 里的包一律 `PackageScope::Project`,于是
`xim` 的**全局工具**(cmake/glibc/gcc/make/binutils 等)整体被错误地"项目化"、
装进项目 store 而非共享 registry。由此 build-dep 工具(如 `xim:cmake`)的 ELF
interpreter 被指向项目 store 里未物化的 glibc → `cannot execute`,任何在
`install()` 里执行 glibc-动态 build-dep 工具的 compat 包(如从源码 CMake 构建的
OpenCV)在 `mcpp test` 下必现,且与宿主历史无关(fresh `MCPP_HOME` 亦复现)。
**修复**:移除该注入及配套的项目 data dir `xim` 副本暴露。`xim`(及其动态发现的
sub-index)是 xlings 全局默认索引,**global 即默认作用域**——`xim:*` 经全局
index_repos + registry 本地 clone 正常解析、装 registry,并经 additive 对项目
可见;只有用户在 `[indices]` 声明的本地自定义索引才项目化。设计与分析见
`.agents/docs/2026-07-09-project-index-scope-global-infra-fix.md`。

## [0.0.84] — 2026-07-08

### 修复
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "0.0.86"
version = "0.0.87"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
50 changes: 16 additions & 34 deletions src/config.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -681,20 +681,18 @@ bool ensure_project_index_dir(
customRepos.emplace_back(name, spec.url);
}

auto officialIndex = cfg.xlingsHome() / "data" / "xim-pkgindex";
// NOTE: the official global `xim` index is deliberately NOT injected into
// the project's customRepos here. `xim` (and its dynamically-discovered
// sub-indexes) are xlings GLOBAL-default indices — global IS the default
// scope, and a repo is only project-scoped when it lands in the project's
// index_repos. Injecting `xim` as a project repo made every `xim:*` tool
// (cmake/glibc/gcc/make/…) resolve project-scoped and install into the
// project store instead of the shared registry, which broke ELF loader
// resolution for build-dep tools (cmake interpreter dangling). `xim:*`
// resolves fine at global scope via xlings' global index_repos + the
// registry-local clone, and stays visible to the project via additive.
// See .agents/docs/2026-07-09-project-index-scope-global-infra-fix.md.
std::error_code ec;
if (!customRepos.empty() && std::filesystem::exists(officialIndex / "pkgs", ec)) {
bool hasXim = false;
for (auto const& [name, _] : customRepos) {
if (name == "xim") {
hasXim = true;
break;
}
}
if (!hasXim) {
customRepos.emplace_back("xim", officialIndex.generic_string());
}
}

if (customRepos.empty() && penv.empty()) return false; // nothing to do

Expand Down Expand Up @@ -742,27 +740,11 @@ bool ensure_project_index_dir(
exposeLocalIndex(name, source, dotMcpp / "data");
}

// Project-scoped xlings installs custom-index packages in an additive
// project data dir. Expose the global official xim index there too, so
// package deps like `xim:python@latest` can resolve without falling back
// to unrelated remote index updates or system tools.
if (std::filesystem::exists(officialIndex / "pkgs", ec)) {
auto projectData = dotMcpp / ".xlings" / "data";
auto projectOfficial = projectData / "xim-pkgindex";
std::filesystem::create_directories(projectData, ec);
if (!std::filesystem::exists(projectOfficial, ec)) {
std::filesystem::create_directory_symlink(officialIndex, projectOfficial, ec);
if (ec) {
ec.clear();
std::filesystem::copy(
officialIndex,
projectOfficial,
std::filesystem::copy_options::recursive
| std::filesystem::copy_options::skip_existing,
ec);
}
}
}
// (removed) The official global `xim` index is no longer exposed/copied into
// the project data dir: it is not a project-scoped repo (see the note above),
// so `xim:*` resolves at GLOBAL scope via the registry-local xim clone, not
// from a project-local copy. Only genuinely user-declared local custom
// indices are exposed project-locally (the loop above).
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/toolchain/fingerprint.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import mcpp.toolchain.detect;

export namespace mcpp::toolchain {

inline constexpr std::string_view MCPP_VERSION = "0.0.86";
inline constexpr std::string_view MCPP_VERSION = "0.0.87";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
8 changes: 6 additions & 2 deletions tests/e2e/52_local_path_namespaced_index.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,12 @@ if [[ "${1:-}" == "interface" && "${2:-}" == "install_packages" ]]; then
fi
shift
done
if ! grep -q '"name": "xim"' "${XLINGS_PROJECT_DIR:?}/.xlings.json"; then
echo "missing official xim index in project .xlings.json" >&2
# The official global `xim` index must NOT be injected into the project's
# .xlings.json — it's a global-default index (global IS the default scope);
# injecting it project-scoped mis-scopes every xim:* tool into the project
# store. Only user-declared local custom indices belong here.
if grep -q '"name": "xim"' "${XLINGS_PROJECT_DIR:?}/.xlings.json"; then
echo "official xim index must NOT be injected into project .xlings.json" >&2
cat "${XLINGS_PROJECT_DIR:?}/.xlings.json" >&2 2>/dev/null || true
exit 24
fi
Expand Down
27 changes: 25 additions & 2 deletions tests/unit/test_config.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include <gtest/gtest.h>
#include <fstream>
#include <sstream>
#include <string>

import std;
import mcpp.config;
Expand Down Expand Up @@ -85,7 +88,14 @@ TEST(Config, ProjectIndexJsonEscapesLocalIndexPath) {
std::filesystem::remove_all(project);
}

TEST(Config, ProjectIndexDirExposesOfficialXimIndex) {
// The official global `xim` index must NOT be injected/exposed into the project
// scope. `xim` (and its dynamic sub-indexes) are xlings GLOBAL-default indices —
// global IS the default scope; injecting xim as a project repo made every `xim:*`
// tool (cmake/glibc/gcc/make) resolve project-scoped and install into the project
// store instead of the shared registry, breaking ELF loader resolution for
// build-dep tools. `xim:*` resolves at global scope via the registry-local clone.
// See .agents/docs/2026-07-09-project-index-scope-global-infra-fix.md.
TEST(Config, ProjectIndexDirDoesNotInjectOfficialXimIndex) {
auto project = make_tempdir("mcpp-config-project-xim-index");
auto registry = make_tempdir("mcpp-config-registry");
auto official = registry / "data" / "xim-pkgindex";
Expand All @@ -109,9 +119,22 @@ TEST(Config, ProjectIndexDirExposesOfficialXimIndex) {
cfg.registryDir = registry;
ASSERT_TRUE(mcpp::config::ensure_project_index_dir(cfg, project, indices));

// xim is NOT copied/exposed into the project data dir (it stays global).
auto projectOfficial =
project / ".mcpp" / ".xlings" / "data" / "xim-pkgindex";
EXPECT_TRUE(std::filesystem::exists(projectOfficial / "pkgs" / "p" / "python.lua"));
EXPECT_FALSE(std::filesystem::exists(projectOfficial / "pkgs" / "p" / "python.lua"));

// The user-declared local `compat` index IS exposed project-locally.
auto projectLocal = project / ".mcpp" / ".xlings" / "data" / "compat";
EXPECT_TRUE(std::filesystem::exists(projectLocal / "pkgs"));

// The seeded project .xlings.json index_repos must not contain `xim`.
auto projJson = project / ".mcpp" / ".xlings.json";
if (std::filesystem::exists(projJson)) {
std::ifstream is(projJson);
std::stringstream ss; ss << is.rdbuf();
EXPECT_EQ(ss.str().find("\"xim\""), std::string::npos);
}

std::filesystem::remove_all(project);
std::filesystem::remove_all(registry);
Expand Down
Loading