From 8713b64827c0c869969dc624253e7225f05cb24f Mon Sep 17 00:00:00 2001 From: steven Date: Thu, 11 Jun 2026 13:20:00 -0600 Subject: [PATCH 1/5] fix: emit --list to stdout and honor FOUNDRYUP_MAX_RETRIES --- src/download.rs | 20 +++++++++++++++++--- src/install.rs | 14 +++++++------- src/main.rs | 8 ++++++++ tests/it/main.rs | 20 ++++++++++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/download.rs b/src/download.rs index a3305e3..3945e85 100644 --- a/src/download.rs +++ b/src/download.rs @@ -5,8 +5,22 @@ use indicatif::{ProgressBar, ProgressStyle}; use sha2::{Digest, Sha256}; use std::{io::Write, path::Path}; -/// Number of retries (after the initial attempt) for transient HTTP failures. -const MAX_RETRIES: u32 = 5; +/// Default number of retries (after the initial attempt) for transient HTTP +/// failures, used when `FOUNDRYUP_MAX_RETRIES` is unset or unparseable. +const DEFAULT_MAX_RETRIES: u32 = 5; + +/// Number of retries for transient HTTP failures, honoring the +/// `FOUNDRYUP_MAX_RETRIES` environment variable (matching the install script). +/// +/// The script's `FOUNDRYUP_RETRY_DELAY` / `FOUNDRYUP_RETRY_MAX_TIME` are not +/// supported here: reqwest's retry layer manages its own backoff and does not +/// expose delay or total-time knobs. +fn max_retries() -> u32 { + std::env::var("FOUNDRYUP_MAX_RETRIES") + .ok() + .and_then(|v| v.trim().parse().ok()) + .unwrap_or(DEFAULT_MAX_RETRIES) +} /// Transient HTTP statuses that may recover on retry (e.g. GitHub rate limiting /// or temporary outages). Other errors (e.g. 404) are treated as permanent. @@ -56,7 +70,7 @@ impl Downloader { // otherwise block retries on a CLI that issues only a few requests. let retry = reqwest::retry::for_host(GitHubHosts) .no_budget() - .max_retries_per_request(MAX_RETRIES) + .max_retries_per_request(max_retries()) .classify_fn(|req_rep| { if req_rep.error().is_some() || req_rep.status().is_some_and(is_retryable_status) { req_rep.retryable() diff --git a/src/install.rs b/src/install.rs index 38f5843..b02e969 100644 --- a/src/install.rs +++ b/src/install.rs @@ -3,7 +3,7 @@ use crate::{ config::Config, download::{Downloader, compute_sha256, extract_tar_gz, extract_zip}, platform::{Platform, Target}, - say, warn, + say, tell, warn, }; use eyre::{Result, WrapErr, bail}; use fs_err as fs; @@ -549,18 +549,18 @@ pub(crate) fn list(config: &Config) -> Result<()> { let version_name = version_entry.file_name(); let version_name = version_name.to_string_lossy(); - say!("{owner_name}/{repo_name} {version_name}"); + tell!("{owner_name}/{repo_name} {version_name}"); for bin in bins { let bin_path = version_path.join(bin_name(bin)); if bin_path.exists() { match get_bin_version(&bin_path) { - Ok(v) => say!("- {v}"), - Err(_) => say!("- {bin} (unknown version)"), + Ok(v) => tell!("- {v}"), + Err(_) => tell!("- {bin} (unknown version)"), } } } - eprintln!(); + println!(); } } } @@ -569,8 +569,8 @@ pub(crate) fn list(config: &Config) -> Result<()> { let bin_path = config.bin_path(bin); if bin_path.exists() { match get_bin_version(&bin_path) { - Ok(v) => say!("- {v}"), - Err(_) => say!("- {bin} (unknown version)"), + Ok(v) => tell!("- {v}"), + Err(_) => tell!("- {bin} (unknown version)"), } } } diff --git a/src/main.rs b/src/main.rs index 6f51eb6..dd052bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -152,6 +152,14 @@ macro_rules! say { }; } +/// Like [`say`], but writes to stdout instead of stderr. +#[macro_export] +macro_rules! tell { + ($($arg:tt)*) => { + println!("foundryup: {}", format_args!($($arg)*)) + }; +} + #[macro_export] macro_rules! warn { ($($arg:tt)*) => { diff --git a/tests/it/main.rs b/tests/it/main.rs index 6da5e0e..295675f 100644 --- a/tests/it/main.rs +++ b/tests/it/main.rs @@ -225,6 +225,26 @@ fn list_empty() { .success(); } +// `--list` results are written to stdout. +#[test] +fn list_writes_to_stdout() { + let temp_dir = tempfile::Builder::new().tempdir().unwrap(); + let foundry_dir = temp_dir.path().join(".foundry"); + let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.0.0"); + std::fs::create_dir_all(&version_dir).unwrap(); + + for bin in BINS { + std::fs::write(version_dir.join(format!("{bin}{EXE_SUFFIX}")), "fake binary").unwrap(); + } + + foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success().stdout_eq(str![ + [r#" +foundryup: foundry-rs/foundry v1.0.0 +... +"#] + ]); +} + #[test] fn migrate_legacy_versions() { let temp_dir = tempfile::Builder::new().tempdir().unwrap(); From 8df53c0342a2489dedd3a707860f3f2b34089228 Mon Sep 17 00:00:00 2001 From: steven Date: Thu, 11 Jun 2026 13:34:06 -0600 Subject: [PATCH 2/5] typo --- src/download.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/download.rs b/src/download.rs index 3945e85..e921856 100644 --- a/src/download.rs +++ b/src/download.rs @@ -6,7 +6,7 @@ use sha2::{Digest, Sha256}; use std::{io::Write, path::Path}; /// Default number of retries (after the initial attempt) for transient HTTP -/// failures, used when `FOUNDRYUP_MAX_RETRIES` is unset or unparseable. +/// failures, used when `FOUNDRYUP_MAX_RETRIES` is unset or unparsable. const DEFAULT_MAX_RETRIES: u32 = 5; /// Number of retries for transient HTTP failures, honoring the From 56fa678e36f033248b1224f5a24216c21e42b050 Mon Sep 17 00:00:00 2001 From: steven Date: Thu, 11 Jun 2026 13:51:57 -0600 Subject: [PATCH 3/5] fix --- tests/it/foundry_bins.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/it/foundry_bins.rs b/tests/it/foundry_bins.rs index 9b12242..53d22d6 100644 --- a/tests/it/foundry_bins.rs +++ b/tests/it/foundry_bins.rs @@ -42,7 +42,7 @@ fn test_install(version: &str) { run_forge_test(&foundry_dir, temp_dir.path()); - foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success().stderr_eq(str![ + foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success().stdout_eq(str![ [r#" foundryup: foundry-rs/foundry [..] foundryup: - forge [..] From a04eca44927e7bce6f6c9f1412ca9e91b9734f40 Mon Sep 17 00:00:00 2001 From: steven Date: Fri, 12 Jun 2026 08:51:18 -0600 Subject: [PATCH 4/5] test --- src/download.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/download.rs b/src/download.rs index e921856..2eed84c 100644 --- a/src/download.rs +++ b/src/download.rs @@ -259,6 +259,38 @@ mod tests { assert!(!api("https://objects.githubusercontent.com/x")); } + #[test] + fn max_retries_honors_env_var() { + // Single test so the process-global env var can't race across threads. + let key = "FOUNDRYUP_MAX_RETRIES"; + let original = std::env::var_os(key); + + // Unset falls back to the default. + unsafe { std::env::remove_var(key) }; + assert_eq!(max_retries(), DEFAULT_MAX_RETRIES); + + // A custom value (with surrounding whitespace) is parsed. + unsafe { std::env::set_var(key, " 9 ") }; + assert_eq!(max_retries(), 9); + + // Zero is honored (disables retries). + unsafe { std::env::set_var(key, "0") }; + assert_eq!(max_retries(), 0); + + // Unparsable values fall back to the default. + unsafe { std::env::set_var(key, "not-a-number") }; + assert_eq!(max_retries(), DEFAULT_MAX_RETRIES); + + // Negative values aren't valid u32 and fall back to the default. + unsafe { std::env::set_var(key, "-1") }; + assert_eq!(max_retries(), DEFAULT_MAX_RETRIES); + + match original { + Some(val) => unsafe { std::env::set_var(key, val) }, + None => unsafe { std::env::remove_var(key) }, + } + } + #[test] fn github_hosts_scope_matches_github_cdns() { assert!(GitHubHosts == "github.com"); From 69d49bc558118d9b0e9660d73f84d9a827b66942 Mon Sep 17 00:00:00 2001 From: steven Date: Fri, 12 Jun 2026 19:47:50 -0600 Subject: [PATCH 5/5] rm test --- src/download.rs | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/download.rs b/src/download.rs index 2eed84c..e921856 100644 --- a/src/download.rs +++ b/src/download.rs @@ -259,38 +259,6 @@ mod tests { assert!(!api("https://objects.githubusercontent.com/x")); } - #[test] - fn max_retries_honors_env_var() { - // Single test so the process-global env var can't race across threads. - let key = "FOUNDRYUP_MAX_RETRIES"; - let original = std::env::var_os(key); - - // Unset falls back to the default. - unsafe { std::env::remove_var(key) }; - assert_eq!(max_retries(), DEFAULT_MAX_RETRIES); - - // A custom value (with surrounding whitespace) is parsed. - unsafe { std::env::set_var(key, " 9 ") }; - assert_eq!(max_retries(), 9); - - // Zero is honored (disables retries). - unsafe { std::env::set_var(key, "0") }; - assert_eq!(max_retries(), 0); - - // Unparsable values fall back to the default. - unsafe { std::env::set_var(key, "not-a-number") }; - assert_eq!(max_retries(), DEFAULT_MAX_RETRIES); - - // Negative values aren't valid u32 and fall back to the default. - unsafe { std::env::set_var(key, "-1") }; - assert_eq!(max_retries(), DEFAULT_MAX_RETRIES); - - match original { - Some(val) => unsafe { std::env::set_var(key, val) }, - None => unsafe { std::env::remove_var(key) }, - } - } - #[test] fn github_hosts_scope_matches_github_cdns() { assert!(GitHubHosts == "github.com");