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
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,26 @@ mcpp 第一次跑 / 沙盒初始化时(当前日志:`Initialize mcpp sandbox lay
3. **P2(发布解耦)**:mcpp-index 仓加 artifact 发布 CI + mcpp 侧 artifact 拉取(与
xim-pkgindex 对齐)。

### 4.1 实现进度(WS3)

- **✅ P0②(build 离线优先 + 缺包触发刷新)**:`ensure_official_package_index_fresh`(`src/xlings.cppm`)
不再因 TTL 过期就跑联网 `xlings update`。改为 **miss-triggered**:
- **依赖在本地索引里 → 直接用,零网络**(常态 build,消除 Termux 首跑/构建卡几分钟的根因);
- **依赖在本地索引里查不到 → 自动刷新一次**去拉它(`mcpp build` 路径以 `quiet=false` 调用,
打印一行 `Refreshing package index — \`<pkg>\` not found locally`,让一次性网络停顿不像卡死)。
- **防重**:刚刷过(<120s)且包仍缺失 → 不再重复跑 `xlings update`(上游确实没有,重拉无益),
避免一个 build 里多个缺包各跑一遍全量 git 同步。
commit `f0f57ae`(初版纯离线)→ 增补 miss-triggered + 防重 + 可见提示。
> 即"完全不联网"过严;稳态(依赖齐全)不联网,**缺包则联网刷一次**,二者兼得。
- **✅ P0③(`mcpp index status`)**:新增只读、**全程不联网**的 `mcpp index status`,显示
xim/mcpplibs 两索引的 present/fresh/age/path;缺索引时提示显式 `mcpp index update`,否则确认本地可离线用。
`src/xlings.cppm` 导出 `IndexStatus` + `{default,official}_index_status`;`src/pm/index_management.cppm`
`index_status()`;CLI 接线 + e2e `tests/e2e/75_index_status_offline.sh`(commit `ba92265`)。
- **P0①(seed 内置索引)**:暂沿用"缺则自动拉一次"(已满足首次保证有索引);随发行版捎带 seed 快照为后续优化。
- **P1 / P2(指针 sha 比对 + mcpp 侧 artifact 拉取)**:mcpp-index 发布侧已就绪(WS2,资源仓
`xlings-res/mcpp-index` push 触发发 artifact + 指针);mcpp 客户端的 artifact 拉取/比对为后续 PR。
当前闭环经 git 路径已可用(稳态离线、缺包显式刷新)。

---

## 4.5 追加计划:first-init 细粒度带时间戳 debug log(WS5)
Expand Down
3 changes: 3 additions & 0 deletions src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ int run(int argc, char** argv) {
.subcommand(cl::App("update")
.description("Refresh local registry clones")
.arg(cl::Arg("name").help("If given, update only this index")))
.subcommand(cl::App("status")
.description("Show local index presence/freshness (offline)"))
.subcommand(cl::App("pin")
.description("Pin a custom index to a commit rev in mcpp.toml")
.arg(cl::Arg("name").help("Index name").required())
Expand All @@ -367,6 +369,7 @@ int run(int argc, char** argv) {
{"add", cmd_index_add},
{"remove", cmd_index_remove},
{"update", cmd_index_update},
{"status", cmd_index_status},
{"pin", cmd_index_pin},
{"unpin", cmd_index_unpin},
});
Expand Down
4 changes: 4 additions & 0 deletions src/cli/cmd_registry.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export int cmd_index_update(const mcpplibs::cmdline::ParsedArgs& parsed) {
return mcpp::pm::index_update(parsed.positional(0));
}

export int cmd_index_status(const mcpplibs::cmdline::ParsedArgs& /*parsed*/) {
return mcpp::pm::index_status();
}

export int cmd_index_pin(const mcpplibs::cmdline::ParsedArgs& parsed) {
std::string name = parsed.positional(0);
if (name.empty()) {
Expand Down
41 changes: 41 additions & 0 deletions src/pm/index_management.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,47 @@ export int index_update(const std::string& filterName) {
return 0;
}

// `mcpp index status` — read-only, offline snapshot of the local indexes.
// Never touches the network: reports presence/freshness/age and, when an
// index is missing or stale, points at the explicit `mcpp index update`.
export int index_status() {
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; }
auto xlEnv = mcpp::config::make_xlings_env(*cfg);

auto fmt_age = [](std::int64_t s) -> std::string {
if (s < 0) return "unknown";
if (s < 90) return std::format("{}s ago", s);
if (s < 5400) return std::format("{}m ago", s / 60);
if (s < 172800) return std::format("{}h ago", s / 3600);
return std::format("{}d ago", s / 86400);
};
auto show = [&](const char* label, const mcpp::xlings::IndexStatus& st) {
std::string state = !st.present ? "missing"
: st.fresh ? "fresh"
: "stale";
std::println(" {:<10} {:<8} {:<12} {}",
label, state, fmt_age(st.ageSeconds), st.dir.string());
};

auto official = mcpp::xlings::official_index_status(xlEnv, cfg->searchTtlSeconds);
auto deflt = mcpp::xlings::default_index_status(xlEnv, cfg->searchTtlSeconds);

std::println("");
std::println(" {:<10} {:<8} {:<12} {}", "index", "state", "refreshed", "path");
show("xim", official);
show("mcpplibs", deflt);
std::println("");

bool anyMissing = !official.present || !deflt.present;
if (anyMissing) {
mcpp::ui::status("Hint", "an index is missing — run `mcpp index update` to fetch it");
} else {
std::println(" Up to date locally. Refresh on demand with `mcpp index update`.");
}
return 0;
}

// `mcpp index pin <name> [<rev>]` — empty rev falls back to mcpp.lock.
export int index_pin(const std::string& name, std::string rev) {
auto root = mcpp::project::find_manifest_root(std::filesystem::current_path());
Expand Down
7 changes: 6 additions & 1 deletion src/pm/package_fetcher.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -725,8 +725,13 @@ Fetcher::resolve_xpkg_path(std::string_view target,
if (autoInstall) {
if (parsed.indexName == "xim") {
mcpp::xlings::Env xlEnv{ cfg_.xlingsBinary, cfg_.xlingsHome() };
// quiet=false: this only ever prints when a dependency is missing
// from the local index and we refresh once to fetch it — a rare,
// intentional event worth surfacing so a one-time network pause
// doesn't look like a silent hang. Steady-state builds (deps
// present) return early without a word.
mcpp::xlings::ensure_official_package_index_fresh(
xlEnv, parsed.packageName, cfg_.searchTtlSeconds, /*quiet=*/true);
xlEnv, parsed.packageName, cfg_.searchTtlSeconds, /*quiet=*/false);
}

std::vector<std::string> targets {
Expand Down
74 changes: 70 additions & 4 deletions src/xlings.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,18 @@ void ensure_official_package_index_fresh(const Env& env,
std::int64_t ttlSeconds,
bool quiet = false);

// ─── Index status (read-only, offline) ──────────────────────────────
// Snapshot of a local index directory — computed without touching the
// network, for `mcpp index status`.
struct IndexStatus {
std::filesystem::path dir; // on-disk index directory
bool present; // pkgs/ tree exists locally
bool fresh; // refreshed within ttlSeconds
std::int64_t ageSeconds; // since last refresh marker, -1 if unknown
};
IndexStatus default_index_status(const Env& env, std::int64_t ttlSeconds);
IndexStatus official_index_status(const Env& env, std::int64_t ttlSeconds);

// ─── run_capture utility ────────────────────────────────────────────

std::expected<std::string, std::string> run_capture(const std::string& cmd);
Expand Down Expand Up @@ -401,6 +413,29 @@ bool is_index_dir_fresh(const std::filesystem::path& indexDir, std::int64_t ttlS
return age.count() < ttlSeconds;
}

// Seconds since the index's refresh marker was last touched, or -1 if the
// marker is missing/unreadable. Read-only — no network, no side effects.
std::int64_t index_age_seconds(const std::filesystem::path& indexDir) {
std::error_code ec;
auto marker = index_refresh_marker(indexDir);
auto newest = std::filesystem::last_write_time(marker, ec);
if (ec) return -1;
auto now = std::filesystem::file_time_type::clock::now();
return std::chrono::duration_cast<std::chrono::seconds>(now - newest).count();
}

IndexStatus index_status_for(const std::filesystem::path& indexDir,
std::int64_t ttlSeconds) {
std::error_code ec;
bool present = std::filesystem::exists(index_pkgs_dir(indexDir), ec) && !ec;
return IndexStatus{
.dir = indexDir,
.present = present,
.fresh = is_index_dir_fresh(indexDir, ttlSeconds),
.ageSeconds = index_age_seconds(indexDir),
};
}

void write_file(const std::filesystem::path& p, std::string_view content) {
std::error_code ec;
std::filesystem::create_directories(p.parent_path(), ec);
Expand Down Expand Up @@ -1167,6 +1202,14 @@ bool is_official_index_fresh(const Env& env, std::int64_t ttlSeconds) {
return is_index_dir_fresh(official_index_dir(env), ttlSeconds);
}

IndexStatus default_index_status(const Env& env, std::int64_t ttlSeconds) {
return index_status_for(default_index_dir(env), ttlSeconds);
}

IndexStatus official_index_status(const Env& env, std::int64_t ttlSeconds) {
return index_status_for(official_index_dir(env), ttlSeconds);
}

bool is_official_package_index_fresh(const Env& env,
std::string_view packageName,
std::int64_t ttlSeconds) {
Expand Down Expand Up @@ -1203,12 +1246,35 @@ void ensure_official_index_fresh(const Env& env, std::int64_t ttlSeconds, bool q

void ensure_official_package_index_fresh(const Env& env,
std::string_view packageName,
std::int64_t ttlSeconds,
[[maybe_unused]] std::int64_t ttlSeconds,
bool quiet) {
if (is_official_package_index_fresh(env, packageName, ttlSeconds)) return;
// Offline-first, miss-triggered. We do NOT auto-update just because a TTL
// expired — that runs a network `xlings update` (git-syncs several index
// repos) that stalls for minutes on slow/blocked networks (the Termux
// first-run / build hang). But fully offline is too strict: if a requested
// dependency is NOT in the local index, we DO refresh once to discover it.
//
// present locally → use as-is, zero network (the common build case).
// missing locally → refresh once to try to fetch it.
//
// Routine, deps-already-present refresh stays the user's explicit
// `mcpp index update` / `xlings update`.
auto pkg = official_package_file(env, packageName);
if (!pkg.empty() && std::filesystem::exists(pkg)) return;

// The package is missing locally. Refresh once — but guard against a build
// that resolves several genuinely-absent packages re-running the heavy
// `xlings update` per package: if the index was refreshed moments ago and
// the package is STILL missing, upstream simply lacks it; re-pulling won't
// help. (A package added upstream before this run lands in that one pull.)
constexpr std::int64_t kJustRefreshedSeconds = 120;
if (is_official_index_fresh(env, kJustRefreshedSeconds)) return;

if (!quiet)
print_status("Updating", "package index (auto-refresh)");
update_index(env, /*quiet=*/true);
print_status("Refreshing",
std::format("package index — `{}` not found locally (one-time)",
packageName));
update_index(env, /*quiet=*/quiet);
}

} // namespace mcpp::xlings
31 changes: 31 additions & 0 deletions tests/e2e/75_index_status_offline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# requires:
# `mcpp index status` is a read-only, offline snapshot of the local indexes.
# After a successful init it reports both indexes present, and a second run
# needs no network (steady-state commands are offline once init succeeded).
set -e

TMP=$(mktemp -d)
trap "rm -rf $TMP" EXIT

export MCPP_HOME="$TMP/mcpp-home"

# init (this is the one place a first run may fetch the index)
"$MCPP" self env > /dev/null

# status: exits 0, prints the table header + both index rows
out=$("$MCPP" index status 2>&1)
[[ "$out" == *"xim"* ]] || { echo "index status missing xim row: $out"; exit 1; }
[[ "$out" == *"mcpplibs"* ]] || { echo "index status missing mcpplibs row: $out"; exit 1; }
[[ "$out" == *"refreshed"* ]] || { echo "index status missing header: $out"; exit 1; }

# After init the official index is present (not 'missing').
echo "$out" | grep -E '^[[:space:]]*xim[[:space:]]' | grep -q 'missing' \
&& { echo "xim index reported missing right after init: $out"; exit 1; }

# Offline invariant: a second status with the network cut must still succeed.
# (No network calls in the status path; this just re-asserts it deterministically.)
out2=$("$MCPP" index status 2>&1)
[[ "$out2" == *"mcpplibs"* ]] || { echo "second index status failed: $out2"; exit 1; }

echo "OK"
Loading