Status: approved, in implementation Target release: mcpp 0.0.53 Scope: make library / component downloads show the same live progress + speed that toolchain downloads already show, across Linux / macOS / Windows.
mcpp toolchain install shows a live progress bar with percent, bytes and
transfer speed. Installing library / component dependencies (during
mcpp build, dependency resolution, or mcpp new --template) prints a single
Downloading <pkg> v<ver> line and then appears to hang for a long time with
no percent, no speed and no sign of life — especially for large packages on slow
mirrors.
There are three download code paths. They were not equally instrumented.
| Path | Trigger | Transport | Symptom |
|---|---|---|---|
| ① Toolchain | mcpp toolchain install |
NDJSON interface install_packages + CliInstallProgress |
progress OK |
| ② Builtin-index deps | dep resolves via builtin index (useProjectEnv = false) |
same as ① | progress OK once bytes flow |
| ③ Custom/project-index deps | dep resolves via a project-added index (useProjectEnv = true) |
install_direct(projEnv, target, quiet=true) |
fully silent |
Two independent defects fall out of this:
src/cli.cppm routes project/custom-index dependency installs through
mcpp::xlings::install_direct(projEnv, target, /*quiet=*/true). quiet=true
redirects xlings' stdout/stderr to the platform null device, so xlings' own
progress output is discarded and no NDJSON events are parsed (the direct CLI
does not speak NDJSON). The Downloading <pkg> line is printed by mcpp before
this call, then the whole download runs dark.
This path was switched to the direct CLI in a prior change to guarantee that project-index packages land in the project-local xlings data root (so a package's install hook can find sibling packages from the same index). That change traded away progress as a side effect.
Why the NDJSON interface is in fact safe here (verified against xlings
source): in the pinned xlings, the install_packages capability and the
install CLI both call the same xim::cmd_install(...). The install
destination is chosen by package scope — storeRoot = (scope == Project ? project_data_dir() : global_data_dir()) / "xpkgs" — which is derived from
which index the package came from, not from interface-vs-CLI. download_progress
events are emitted unconditionally (no scope gate). So the NDJSON interface
installs project-scoped packages into the project-local data root and streams
progress. The earlier switch was working around index exposure, which is now
handled separately (the project index is symlinked/exposed into the project data
dir and targets are passed as indexName:fqname@version).
Captured NDJSON for a real package download shows the downloader emits
download_progress events with totalBytes = 0 (and often downloadedBytes = 0)
during the initial connect / TLS / redirect / pre-sizing window, before the
transfer's total size is known:
elapsed 0.2s–1.4s : downloadedBytes=0 totalBytes=0 sizesReady=false (connecting)
elapsed 1.6s : downloadedBytes=57344 totalBytes=1754511 (bytes flow)
elapsed 2.0s : downloadedBytes=1754511 totalBytes=1754511 finished
Both CliInstallProgress::on_data and make_bootstrap_progress_callback only
call ProgressBar::update_bytes when total > 0, so during the connecting
window the line shows nothing. For a large file on a slow mirror this window can
last many seconds and reads as a hang. Toolchain downloads hide this because the
connect window is tiny relative to a multi-minute transfer; library downloads do
not.
Three changes, all behind existing abstractions, all cross-platform.
In src/cli.cppm, the useProjectEnv branch of the dependency install lambda
calls the NDJSON interface with the project env and an CliInstallProgress
handler, instead of the silenced direct CLI:
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;
}This yields the same cyan Downloading … [bar] X/Y Z/s UI on all three
paths. Package scope (and therefore install location) is unchanged — packages
from a project index still install into the project-local data root.
Add ProgressBar::update_indeterminate(current_bytes, elapsed_sec): render a
swept/indeterminate bar plus an info suffix that ticks — connecting… Ns, or a
byte counter X.Y MB Ns once bytes arrive without a known total. This also
covers servers that stream without Content-Length.
Wire both progress consumers (CliInstallProgress::on_data and
make_bootstrap_progress_callback) to call update_indeterminate when the
active file has total == 0 (and update_bytes once total > 0).
Refactor render_line so the bar string is produced by a small callable; the
percent path and the indeterminate path share the terminal-width budgeting.
Bump pinned::kXlingsVersion in src/xlings.cppm to the latest xlings release
(the version that ships the unified cmd_install + scope-routed install +
unconditional download_progress). This removes any dependence on older xlings
behavior for the path ③ install location.
- Progress rendering already runs on Linux/macOS/Windows (toolchain path uses it today); no new platform branches are introduced.
mcpp::xlings::call/build_interface_commandalready handle the Windows vs POSIX command shape and env propagation; path ③ inherits that.update_indeterminateuses the samerender_linebudgeting (TIOCGWINSZ /$COLUMNS/ 80-col fallback) as the existing bar; non-TTY /--quietpaths remain silent via the existingg_quietguard.
- Unit-ish / build: project builds clean with the existing warning gate.
- E2E regression:
tests/e2e/52_local_path_namespaced_index.shandtests/e2e/58_preinstall_mcpp_deps_for_hooks.shcurrently assert path ③ uses the direct CLI. They are updated to assert the interface transport while keeping their existing invariants (project-local install location, andmcpp.depsinstalled before a dependent package's install hook runs). - Real integration check: with a real project-local custom index and a real
installable package, confirm (a) the package installs into the project-local
data root, (b) the install hook still sees its
mcpp.deps, and (c) theDownloading … [bar] X/Y Z/sUI renders. This is the empirical gate before merge.
- Implement Fix 1–3; update e2e tests.
- Local build + e2e + real integration check.
- Bump
MCPP_VERSION(src/toolchain/fingerprint.cppm) andmcpp.tomlto0.0.53; CHANGELOG entry. - PR → CI green → squash merge → trigger release
0.0.53. - Ecosystem: add mcpp
0.0.53to the package index (mcpp xpkg descriptor: version + per-platform sha256), publish release artifacts to the release-asset mirrors, PR + merge, verify an end-to-end install of the new version.
- No change to xlings' own native CLI progress UI.
- No change to the dependency-resolution / preinstall ordering logic (it already installs deps before dependents; only the per-install transport on path ③ changes).