@@ -756,6 +756,46 @@ sysroot_override(const mcpp::manifest::Manifest& m,
756756// shape this codebase keeps paying for. A board package that got the right
757757// answer as a root project and a stale one as a dependency would fail only in
758758// the consuming build, which is the harder direction to debug.
759+ // ⚠️⚠️ A NETWORK STEP OF A BUILD, RETRIED — AND IT HAD NO RETRY AT ALL.
760+ //
761+ // A dependency resolved by `git` is fetched on every machine that has not
762+ // cached it, and a transport that hiccups once failed the whole build:
763+ //
764+ // error: git clone of 'https://github.com/…' failed:
765+ // Cloning into '/home/runner/.mcpp/git/63269d80b47f71e6'...
766+ //
767+ // — no message from git, which is what a connection that dies mid-transfer
768+ // looks like. Measured twice on 2026-08-23: once in continuous integration and
769+ // once locally as `TLS connect error: … unexpected eof while reading`.
770+ //
771+ // ⚠️ THREE ATTEMPTS, AND THE LAST FAILURE IS REPORTED UNCHANGED. A wrong URL
772+ // and a missing branch fail exactly as a transient fault does, so this cannot
773+ // tell them apart and does not try: a permanent failure costs three seconds and
774+ // produces the message it always did. Hiding a real error behind a retry is the
775+ // worse trade, which is why the count is small and the report is untouched.
776+ //
777+ // ⚠️ BOTH NETWORK STEPS, not one. The first version retried only the clone —
778+ // and a probe with a nonexistent repository failed in ONE second, because the
779+ // step that runs first is `git ls-remote` and it was still bare. A retry on
780+ // half of a path is a retry that reports success at having been added.
781+ //
782+ // `between` runs after a failed attempt: the clone needs the partial directory
783+ // removed, or git's next attempt fails with "already exists and is not an empty
784+ // directory" — a second, different error that says nothing about the first.
785+ mcpp::platform::process::RunResult run_with_network_retry (
786+ std::string_view command,
787+ const std::function<void ()>& between = {}) {
788+ mcpp::platform::process::RunResult r{};
789+ for (int attempt = 1 ; attempt <= 3 ; ++attempt) {
790+ r = mcpp::platform::process::capture (command);
791+ if (r.exit_code == 0 ) return r;
792+ if (between) between ();
793+ if (attempt < 3 )
794+ std::this_thread::sleep_for (std::chrono::seconds (attempt));
795+ }
796+ return r;
797+ }
798+
759799void fill_target_build_env (mcpp::build::BuildProgramEnv& e,
760800 const mcpp::toolchain::Toolchain* tc)
761801{
@@ -4275,7 +4315,9 @@ prepare_build(bool print_fingerprint,
42754315 std::format (" mcpp.lock records no commit for branch "
42764316 " '{}'" , spec.gitRev ),
42774317 " resolve" );
4278- auto r = mcpp::platform::process::capture (std::format (
4318+ // The FIRST network step of a git dependency, and therefore
4319+ // the one a transient fault is most likely to meet.
4320+ auto r = run_with_network_retry (std::format (
42794321 " git ls-remote {} {} 2>&1" ,
42804322 mcpp::platform::shell::quote (spec.git ),
42814323 mcpp::platform::shell::quote (
@@ -4357,7 +4399,11 @@ prepare_build(bool print_fingerprint,
43574399 mcpp::platform::shell::quote (gitRoot.string ()),
43584400 mcpp::platform::shell::quote (gitRoot.string ()),
43594401 mcpp::platform::shell::quote (resolvedGitRev));
4360- auto r = mcpp::platform::process::capture (cloneCmd);
4402+ // See `run_with_network_retry` for why, and for what the
4403+ // callback is removing between attempts.
4404+ auto r = run_with_network_retry (cloneCmd, [&] {
4405+ std::filesystem::remove_all (gitRoot, ec);
4406+ });
43614407 if (r.exit_code != 0 ) {
43624408 std::filesystem::remove_all (gitRoot, ec);
43634409 return std::unexpected (std::format (
0 commit comments