feat: add proxy support for SDK downloads (v1.1.0)

Add --proxy and --no-proxy global flags to control HTTP/HTTPS proxy
usage for all network operations (SDK installs, FTC SDK clone/fetch,
Android SDK download).

Proxy resolution priority:
  1. --no-proxy          → go direct, ignore everything
  2. --proxy <url>       → use the specified proxy
  3. HTTPS_PROXY / HTTP_PROXY env vars (auto-detected)
  4. Nothing             → go direct

Key implementation details:
- reqwest client is always built through ProxyConfig::client() rather
  than Client::new(), so --no-proxy actively suppresses env-var
  auto-detection instead of just being a no-op.
- git2/libgit2 has its own HTTP transport that doesn't use reqwest.
  GitProxyGuard is an RAII guard that temporarily sets/clears the
  HTTPS_PROXY env vars around clone and fetch operations, then restores
  the previous state on drop. This avoids mutating ~/.gitconfig.
- Gradle wrapper reads HTTPS_PROXY natively; no programmatic
  intervention needed.
- All network failure paths now print offline/air-gapped installation
  instructions automatically, covering manual SDK installs and Gradle
  distribution download.

Closes: v1.1.0 proxy support milestone
This commit is contained in:
Eric Ratliff
2026-02-01 09:44:53 -06:00
parent 5596f5bade
commit 54647a47b1
9 changed files with 1161 additions and 48 deletions

View File

@@ -3,11 +3,19 @@ use colored::*;
use anyhow::Result;
use weevil::version::WEEVIL_VERSION;
// Import ProxyConfig through our own `mod sdk`, not through the `weevil`
// library crate. Both re-export the same source, but Rust treats
// `weevil::sdk::proxy::ProxyConfig` and `sdk::proxy::ProxyConfig` as
// distinct types when a binary and its lib are compiled together.
// The command modules already see the local-mod version, so main must match.
mod commands;
mod sdk;
mod project;
mod templates;
use sdk::proxy::ProxyConfig;
#[derive(Parser)]
#[command(name = "weevil")]
#[command(author = "Eric Ratliff <eric@nxlearn.net>")]
@@ -17,6 +25,14 @@ mod templates;
long_about = None
)]
struct Cli {
/// Use this HTTP/HTTPS proxy for all downloads
#[arg(long, value_name = "URL", global = true)]
proxy: Option<String>,
/// Skip proxy entirely — go direct even if HTTPS_PROXY is set
#[arg(long, global = true)]
no_proxy: bool,
#[command(subcommand)]
command: Commands,
}
@@ -119,15 +135,18 @@ fn main() -> Result<()> {
print_banner();
// Resolve proxy once at the top — every network-touching command uses it.
let proxy = ProxyConfig::resolve(cli.proxy.as_deref(), cli.no_proxy)?;
match cli.command {
Commands::New { name, ftc_sdk, android_sdk } => {
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref())
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref(), &proxy)
}
Commands::Doctor => {
commands::doctor::run_diagnostics()
}
Commands::Setup { path } => {
commands::setup::setup_environment(path.as_deref())
commands::setup::setup_environment(path.as_deref(), &proxy)
}
Commands::Uninstall { dry_run, only } => {
commands::uninstall::uninstall_dependencies(dry_run, only)
@@ -139,9 +158,9 @@ fn main() -> Result<()> {
commands::deploy::deploy_project(&path, usb, wifi, ip.as_deref())
}
Commands::Sdk { command } => match command {
SdkCommands::Install => commands::sdk::install_sdks(),
SdkCommands::Install => commands::sdk::install_sdks(&proxy),
SdkCommands::Status => commands::sdk::show_status(),
SdkCommands::Update => commands::sdk::update_sdks(),
SdkCommands::Update => commands::sdk::update_sdks(&proxy),
},
Commands::Config { path, set_sdk } => {
if let Some(sdk_path) = set_sdk {
@@ -164,4 +183,4 @@ fn print_banner() {
println!("{}", " Nexus Workshops LLC".bright_cyan());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
}
}