diff --git a/CHANGELOG.md b/CHANGELOG.md index 7736bfb..d40859a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ### 新增 diff --git a/mcpp.toml b/mcpp.toml index fb7e5bd..ddcb866 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -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"] diff --git a/src/cli.cppm b/src/cli.cppm index 3d06926..ba113f3 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -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 preinstallStack; + std::set preinstallDone; + + std::function( + 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 { auto cfg = get_cfg(); @@ -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 { + 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 installed; if (useProjectEnv) { @@ -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). @@ -1680,12 +1747,10 @@ prepare_build(bool print_fingerprint, auto install_one = [&](std::string target) -> std::expected { 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 targets{ std::move(target) }; CliInstallProgress progress; @@ -1730,17 +1795,8 @@ prepare_build(bool print_fingerprint, std::filesystem::path verRoot = *installed; // Route xpkg.lua reading through the appropriate index. - std::optional 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)); diff --git a/src/config.cppm b/src/config.cppm index 632413e..3f65848 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -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 diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 30f3d01..c79c4a2 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -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; diff --git a/tests/e2e/52_local_path_namespaced_index.sh b/tests/e2e/52_local_path_namespaced_index.sh index 6864e01..7cf73f9 100755 --- a/tests/e2e/52_local_path_namespaced_index.sh +++ b/tests/e2e/52_local_path_namespaced_index.sh @@ -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" @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh b/tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh new file mode 100644 index 0000000..3e96458 --- /dev/null +++ b/tests/e2e/58_preinstall_mcpp_deps_for_hooks.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# requires: gcc fresh-sandbox +# Packages installed from an mcpp index may need their mcpp.deps available +# before the package install hook runs. Library deps belong in mcpp.deps; only +# hook-time tools should be declared as xpm deps. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +source "$(dirname "$0")/_inherit_toolchain.sh" + +INDEX_DIR="$TMP/local-index" +mkdir -p "$INDEX_DIR/pkgs/c" + +cat > "$INDEX_DIR/pkgs/c/compat.proto.lua" <<'EOF' +package = { + spec = "1", + namespace = "compat", + name = "compat.proto", + description = "Protocol helper dependency", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/proto-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = false, + sources = { "src/proto.c" }, + targets = { ["proto"] = { kind = "lib" } }, + deps = {}, + }, +} +EOF + +cat > "$INDEX_DIR/pkgs/c/compat.appdep.lua" <<'EOF' +package = { + spec = "1", + namespace = "compat", + name = "compat.appdep", + description = "Package whose install hook needs mcpp deps", + licenses = {"MIT"}, + type = "package", + xpm = { + linux = { + ["1.0.0"] = { + url = "https://example.invalid/appdep-1.0.0.tar.gz", + sha256 = "0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + mcpp = { + language = "c++23", + import_std = false, + sources = { "src/appdep.c" }, + targets = { ["appdep"] = { kind = "lib" } }, + deps = { + ["compat.proto"] = "1.0.0", + }, + }, +} +EOF + +mkdir -p "$TMP/fake-bin" +FAKE_REGISTRY="$TMP/fake-registry" +FAKE_DIRECT_LOG="$TMP/fake-xlings-direct.log" +mkdir -p "$FAKE_REGISTRY/data" +USER_MCPP="${HOME}/.mcpp" +if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then + ln -s "$USER_MCPP/registry/data/xpkgs" "$FAKE_REGISTRY/data/xpkgs" +fi + +cat > "$TMP/fake-bin/xlings" <<'EOF' +#!/usr/bin/env bash +set -e + +if [[ "${1:-}" == "self" && "${2:-}" == "init" ]]; then + mkdir -p "${XLINGS_HOME:?}/subos/default" + printf '{}\n' > "$XLINGS_HOME/subos/default/.xlings.json" + exit 0 +fi + +if [[ "${1:-}" == "update" ]]; then + exit 0 +fi + +if [[ "${1:-}" == "interface" && "${2:-}" == "install_packages" ]]; then + echo "interface install should not be used for project path indices" >&2 + exit 31 +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 + + case " $* " in + *" compat:compat.proto@1.0.0 "*) + install_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.proto/1.0.0" + mkdir -p "$install_root/src" + cat > "$install_root/src/proto.c" <<'SRC' +int proto_value(void) { + return 42; +} +SRC + exit 0 + ;; + *" compat:compat.appdep@1.0.0 "*) + proto_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.proto/1.0.0" + if [[ ! -d "$proto_root" ]]; then + echo "mcpp.deps were not installed before appdep install hook" >&2 + exit 24 + fi + install_root="${XLINGS_PROJECT_DIR:?}/.xlings/data/xpkgs/compat-x-compat.appdep/1.0.0" + mkdir -p "$install_root/src" + cat > "$install_root/src/appdep.c" <<'SRC' +extern int proto_value(void); +int app_value(void) { + return proto_value(); +} +SRC + exit 0 + ;; + esac +fi + +exit 0 +EOF +chmod +x "$TMP/fake-bin/xlings" + +mkdir -p "$TMP/project/app/src" +cd "$TMP/project/app" + +cat > "$MCPP_HOME/config.toml" < src/app.cpp <<'EOF' +extern "C" int app_value(void); +int app_smoke(void) { + return app_value(); +} +EOF + +cat > mcpp.toml < build.log 2>&1; then + cat build.log + exit 1 +fi + +if ! grep -Fq 'install compat:compat.proto@1.0.0 -y' "$FAKE_DIRECT_LOG"; then + echo "FAIL: compat.proto was not installed from mcpp.deps" + cat "$FAKE_DIRECT_LOG" 2>/dev/null || true + exit 1 +fi + +if ! awk ' + /install compat:compat.proto@1\.0\.0 -y/ { proto = NR } + /install compat:compat.appdep@1\.0\.0 -y/ { appdep = NR } + END { exit !(proto > 0 && appdep > 0 && proto < appdep) } +' "$FAKE_DIRECT_LOG"; then + echo "FAIL: mcpp.deps should be installed before dependent package hook" + cat "$FAKE_DIRECT_LOG" 2>/dev/null || true + exit 1 +fi + +echo "OK"