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

## [0.0.39] — 2026-06-01

### 修复

- 修复 project-local index 包安装时没有走项目 xlings 数据根的问题,本地 path
索引现在通过 xlings CLI 直接安装到项目数据目录,避免 hook 查找不到同索引包。
- 修复包 install hook 运行前 `mcpp.deps` 尚未安装的问题,库/头文件依赖可以继续
留在 `mcpp.deps`,只有 hook 执行工具需要放入 xpm deps。

## [0.0.38] — 2026-05-31

### 新增
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.38"
version = "0.0.39"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
122 changes: 89 additions & 33 deletions src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -1628,10 +1628,19 @@ prepare_build(bool print_fingerprint,
// 0.0.10+: loadVersionDep accepts structured (ns, shortName) for
// namespace-aware lookup. depName is the map key (qualified or bare),
// kept for install() target formatting and error messages.
auto loadVersionDep = [&](const std::string& depName,
const std::string& ns,
const std::string& shortName,
const std::string& version)
std::set<std::string> preinstallStack;
std::set<std::string> preinstallDone;

std::function<std::expected<LoadedDep, std::string>(
const std::string&,
const std::string&,
const std::string&,
const std::string&)> loadVersionDep;

loadVersionDep = [&](const std::string& depName,
const std::string& ns,
const std::string& shortName,
const std::string& version)
-> std::expected<LoadedDep, std::string>
{
auto cfg = get_cfg();
Expand All @@ -1641,24 +1650,29 @@ prepare_build(bool print_fingerprint,
// ─── Routing: check if this dep's namespace maps to a custom index ──
auto* idxSpec = findIndexForNs(ns);

// For local path indices, verify the xpkg.lua exists in the index.
// The local PATH index is for DISCOVERY only (finding the xpkg.lua
// descriptor); the actual package artifacts come from the URLs
// declared inside the lua, installed via global xlings. So we
// validate the lua exists, then fall through to the normal install
// flow below.
if (idxSpec && idxSpec->is_local()) {
const bool useProjectEnv = idxSpec && !idxSpec->is_builtin();

auto readLuaContent = [&]() -> std::optional<std::string> {
if (idxSpec && idxSpec->is_local()) {
auto indexPath = mcpp::config::resolve_project_index_path(*root, *idxSpec);
return mcpp::fetcher::Fetcher::read_xpkg_lua_from_path(
indexPath, ns, shortName);
}
if (idxSpec && !idxSpec->is_builtin()) {
return mcpp::fetcher::Fetcher::read_xpkg_lua_from_project_data(
*root, ns, shortName);
}
return fetcher.read_xpkg_lua(ns, shortName);
};

auto luaContent = readLuaContent();
if (idxSpec && idxSpec->is_local() && !luaContent) {
auto indexPath = mcpp::config::resolve_project_index_path(*root, *idxSpec);
auto luaCheck = mcpp::fetcher::Fetcher::read_xpkg_lua_from_path(
indexPath, ns, shortName);
if (!luaCheck) return std::unexpected(std::format(
return std::unexpected(std::format(
"dependency '{}': not found in local index at '{}'",
depName, indexPath.string()));
// lua found — fall through to normal install path resolution.
}

const bool useProjectEnv = idxSpec && !idxSpec->is_builtin();

// For custom indices, try project-level xlings data roots first.
std::optional<std::filesystem::path> installed;
if (useProjectEnv) {
Expand All @@ -1670,6 +1684,59 @@ prepare_build(bool print_fingerprint,
}

if (!installed) {
if (luaContent) {
auto field = mcpp::manifest::extract_mcpp_field(*luaContent);
if (field.kind == mcpp::manifest::McppField::TableBody) {
auto depManifest = mcpp::manifest::synthesize_from_xpkg_lua(
*luaContent, depName, version);
if (!depManifest) {
return std::unexpected(std::format(
"dependency '{}': {}", depName, depManifest.error().format()));
}

auto preinstallKey = std::format("{}:{}@{}", ns, shortName, version);
if (preinstallStack.contains(preinstallKey)) {
return std::unexpected(std::format(
"dependency '{}': cyclic mcpp.deps while preparing install hooks",
depName));
}

if (!preinstallDone.contains(preinstallKey)) {
preinstallStack.insert(preinstallKey);
for (auto [childName, childSpec] : depManifest->dependencies) {
mcpp::pm::compat::normalize_nested_namespace(
childSpec.namespace_,
childSpec.shortName,
childSpec.legacyDottedKey);

if (auto r = resolveSemver(childSpec, childName); !r) {
preinstallStack.erase(preinstallKey);
return std::unexpected(r.error());
}

if (!childSpec.isVersion()) continue;

ResolvedKey childKey{
childSpec.namespace_.empty()
? std::string{mcpp::manifest::kDefaultNamespace}
: childSpec.namespace_,
childSpec.shortName.empty() ? childName : childSpec.shortName,
};
if (auto child = loadVersionDep(
childName,
childKey.ns,
childKey.shortName,
childSpec.version); !child) {
preinstallStack.erase(preinstallKey);
return std::unexpected(child.error());
}
}
preinstallStack.erase(preinstallKey);
preinstallDone.insert(preinstallKey);
}
}
}

// xlings resolves packages by the full qualified name (ns.shortName)
// as it appears in the index's name field. Use fqname, not the
// map key (which may be a bare short name for default-ns deps).
Expand All @@ -1680,12 +1747,10 @@ prepare_build(bool print_fingerprint,
auto install_one = [&](std::string target) -> std::expected<mcpp::xlings::CallResult, mcpp::pm::CallError> {
if (useProjectEnv) {
auto projEnv = mcpp::config::make_project_xlings_env(**cfg, *root);
auto argsJson = std::format(
R"({{"targets":["{}"],"yes":true}})", target);
CliInstallProgress progress;
auto r = mcpp::xlings::call(projEnv, "install_packages", argsJson, &progress);
if (!r) return std::unexpected(mcpp::pm::CallError{r.error()});
return *r;
int directRc = mcpp::xlings::install_direct(projEnv, target, /*quiet=*/true);
mcpp::xlings::CallResult result;
result.exitCode = directRc;
return result;
}
std::vector<std::string> targets{ std::move(target) };
CliInstallProgress progress;
Expand Down Expand Up @@ -1730,17 +1795,8 @@ prepare_build(bool print_fingerprint,
std::filesystem::path verRoot = *installed;

// Route xpkg.lua reading through the appropriate index.
std::optional<std::string> luaContent;
if (idxSpec && idxSpec->is_local()) {
auto indexPath = mcpp::config::resolve_project_index_path(*root, *idxSpec);
luaContent = mcpp::fetcher::Fetcher::read_xpkg_lua_from_path(
indexPath, ns, shortName);
} else if (idxSpec && !idxSpec->is_builtin()) {
luaContent = mcpp::fetcher::Fetcher::read_xpkg_lua_from_project_data(
*root, ns, shortName);
}
if (!luaContent) {
luaContent = fetcher.read_xpkg_lua(ns, shortName);
luaContent = readLuaContent();
}
if (!luaContent) return std::unexpected(std::format(
"dependency '{}': index entry not found in local clone", depName));
Expand Down
36 changes: 36 additions & 0 deletions src/config.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,42 @@ bool ensure_project_index_dir(
env.home = dotMcpp;
mcpp::xlings::seed_xlings_json(env, customRepos);

auto exposeLocalIndex = [&](const std::string& name,
const std::filesystem::path& source,
const std::filesystem::path& dataRoot)
{
std::error_code ec2;
auto target = dataRoot / name;
std::filesystem::create_directories(dataRoot, ec2);
if (std::filesystem::exists(target / "pkgs", ec2)) {
return;
}
ec2.clear();
if (std::filesystem::exists(target, ec2) || std::filesystem::is_symlink(target, ec2)) {
std::filesystem::remove_all(target, ec2);
}
ec2.clear();
std::filesystem::create_directory_symlink(source, target, ec2);
if (!ec2 && std::filesystem::exists(target / "pkgs", ec2)) {
return;
}
ec2.clear();
std::filesystem::copy(
source,
target,
std::filesystem::copy_options::recursive
| std::filesystem::copy_options::skip_existing,
ec2);
};

for (auto& [name, spec] : indices) {
if (spec.is_builtin() || !spec.is_local()) continue;
auto source = resolve_project_index_path(projectDir, spec);
if (!std::filesystem::exists(source / "pkgs", ec)) continue;
exposeLocalIndex(name, source, dotMcpp / ".xlings" / "data");
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
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.38";
inline constexpr std::string_view MCPP_VERSION = "0.0.39";

struct FingerprintInputs {
Toolchain toolchain;
Expand Down
50 changes: 39 additions & 11 deletions tests/e2e/52_local_path_namespaced_index.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ EOF
mkdir -p "$TMP/fake-bin"
FAKE_REGISTRY="$TMP/fake-registry"
FAKE_LOG="$TMP/fake-xlings.log"
FAKE_DIRECT_LOG="$TMP/fake-xlings-direct.log"
mkdir -p "$FAKE_REGISTRY/data"
if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then
ln -s "$USER_MCPP/registry/data/xpkgs" "$FAKE_REGISTRY/data/xpkgs"
Expand Down Expand Up @@ -114,7 +115,25 @@ if [[ "${1:-}" == "interface" && "${2:-}" == "install_packages" ]]; then
fi
shift
done
printf '{"kind":"result","exitCode":1}\n'
printf '{"kind":"result","exitCode":0}\n'
exit 0
fi

if [[ "${1:-}" == "install" ]]; then
printf '%s\n' "$*" > "${FAKE_XLINGS_DIRECT_LOG:?}"
if [[ ! -d "${XLINGS_PROJECT_DIR:?}/.xlings/data/compat/pkgs" \
&& ! -d "${XLINGS_PROJECT_DIR:?}/data/compat/pkgs" ]]; then
echo "missing project local path index link" >&2
find "${XLINGS_PROJECT_DIR:?}" -maxdepth 4 -type d -print >&2 2>/dev/null || true
exit 23
fi
install_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.cfg/1.0.0"
mkdir -p "$install_root/src"
cat > "$install_root/src/cfg.c" <<'SRC'
int cfg_value(void) {
return 42;
}
SRC
exit 0
fi

Expand Down Expand Up @@ -147,10 +166,10 @@ EOF
mkdir -p "$TMP/project/clean/src"
cd "$TMP/project/clean"

cat > src/main.cpp <<'EOF'
cat > src/clean.cpp <<'EOF'
extern "C" int cfg_value(void);
int main() {
return cfg_value() == 42 ? 0 : 1;
int clean_value(void) {
return cfg_value();
}
EOF

Expand All @@ -166,13 +185,15 @@ compat = { path = "$INDEX_DIR" }
cfg = "1.0.0"

[targets.clean]
kind = "bin"
main = "src/main.cpp"
kind = "lib"
EOF

UPDATE_LOG="$TMP/fake-xlings-update.log"
if FAKE_XLINGS_LOG="$FAKE_LOG" FAKE_XLINGS_UPDATE_LOG="$UPDATE_LOG" "$MCPP" build > fetch.log 2>&1; then
echo "FAIL: clean local path dependency unexpectedly built without package install"
if ! FAKE_XLINGS_LOG="$FAKE_LOG" \
FAKE_XLINGS_DIRECT_LOG="$FAKE_DIRECT_LOG" \
FAKE_XLINGS_UPDATE_LOG="$UPDATE_LOG" \
"$MCPP" build > fetch.log 2>&1; then
echo "FAIL: clean local path dependency should use direct xlings install"
cat fetch.log
exit 1
fi
Expand All @@ -183,10 +204,17 @@ if [[ -f "$UPDATE_LOG" ]]; then
exit 1
fi

grep -Fq '"compat:compat.cfg@1.0.0"' "$FAKE_LOG" || {
echo "FAIL: clean local path install target should use full package name"
if [[ -f "$FAKE_LOG" ]]; then
echo "FAIL: project local path install should use direct xlings install, not interface"
cat "$FAKE_LOG"
cat fetch.log
exit 1
fi

grep -Fq 'install compat:compat.cfg@1.0.0 -y' "$FAKE_DIRECT_LOG" || {
echo "FAIL: clean local path install target should use direct xlings with full package name"
echo "recorded:"
cat "$FAKE_LOG" 2>/dev/null || true
cat "$FAKE_DIRECT_LOG" 2>/dev/null || true
echo "build log:"
cat fetch.log
exit 1
Expand Down
Loading
Loading