diff --git a/diff.txt b/diff.txt new file mode 100644 index 0000000..8a59842 --- /dev/null +++ b/diff.txt @@ -0,0 +1,755 @@ +diff --git i/src/commands/new.rs w/src/commands/new.rs +index aeed30a..4d219c6 100644 +--- i/src/commands/new.rs ++++ w/src/commands/new.rs +@@ -3,12 +3,14 @@ + use colored::*; + + use crate::sdk::SdkConfig; ++use crate::sdk::proxy::ProxyConfig; + use crate::project::ProjectBuilder; + + pub fn create_project( + name: &str, + ftc_sdk: Option<&str>, + android_sdk: Option<&str>, ++ proxy: &ProxyConfig, + ) -> Result<()> { + // Validate project name + if name.is_empty() { +@@ -32,6 +34,7 @@ pub fn create_project( + } + + println!("{}", format!("Creating FTC project: {}", name).bright_green().bold()); ++ proxy.print_status(); + println!(); + + // Check system health FIRST +diff --git i/src/commands/sdk.rs w/src/commands/sdk.rs +index ddc3aa6..250b844 100644 +--- i/src/commands/sdk.rs ++++ w/src/commands/sdk.rs +@@ -1,18 +1,22 @@ + use anyhow::Result; + use colored::*; + use crate::sdk::SdkConfig; ++use crate::sdk::proxy::ProxyConfig; + +-pub fn install_sdks() -> Result<()> { ++pub fn install_sdks(proxy: &ProxyConfig) -> Result<()> { + println!("{}", "Installing SDKs...".bright_yellow().bold()); + println!(); + ++ proxy.print_status(); ++ println!(); ++ + let config = SdkConfig::new()?; + + // Install FTC SDK +- crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?; ++ crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path, proxy)?; + + // Install Android SDK +- crate::sdk::android::install(&config.android_sdk_path)?; ++ crate::sdk::android::install(&config.android_sdk_path, proxy)?; + + println!(); + println!("{} All SDKs installed successfully", "✓".green().bold()); +@@ -44,17 +48,20 @@ pub fn show_status() -> Result<()> { + Ok(()) + } + +-pub fn update_sdks() -> Result<()> { ++pub fn update_sdks(proxy: &ProxyConfig) -> Result<()> { + println!("{}", "Updating SDKs...".bright_yellow().bold()); + println!(); ++ ++ proxy.print_status(); ++ println!(); + + let config = SdkConfig::new()?; + + // Update FTC SDK +- crate::sdk::ftc::update(&config.ftc_sdk_path)?; ++ crate::sdk::ftc::update(&config.ftc_sdk_path, proxy)?; + + println!(); + println!("{} SDKs updated successfully", "✓".green().bold()); + + Ok(()) +-} ++} +\ No newline at end of file +diff --git i/src/commands/setup.rs w/src/commands/setup.rs +index 975b814..cbb5871 100644 +--- i/src/commands/setup.rs ++++ w/src/commands/setup.rs +@@ -4,22 +4,26 @@ + use colored::*; + + use crate::sdk::SdkConfig; ++use crate::sdk::proxy::ProxyConfig; + use crate::project::ProjectConfig; + + /// Setup development environment - either system-wide or for a specific project +-pub fn setup_environment(project_path: Option<&str>) -> Result<()> { ++pub fn setup_environment(project_path: Option<&str>, proxy: &ProxyConfig) -> Result<()> { + match project_path { +- Some(path) => setup_project(path), +- None => setup_system(), ++ Some(path) => setup_project(path, proxy), ++ None => setup_system(proxy), + } + } + + /// Setup system-wide development environment with default SDKs +-fn setup_system() -> Result<()> { ++fn setup_system(proxy: &ProxyConfig) -> Result<()> { + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold()); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); ++ ++ proxy.print_status(); ++ println!(); + + let mut issues = Vec::new(); + let mut installed = Vec::new(); +@@ -57,18 +61,34 @@ fn setup_system() -> Result<()> { + } + Err(_) => { + println!("{} FTC SDK found but incomplete, reinstalling...", "⚠".yellow()); +- crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?; +- let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path) +- .unwrap_or_else(|_| "unknown".to_string()); +- installed.push(format!("FTC SDK {} (installed)", version)); ++ match crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path, proxy) { ++ Ok(_) => { ++ let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path) ++ .unwrap_or_else(|_| "unknown".to_string()); ++ installed.push(format!("FTC SDK {} (installed)", version)); ++ } ++ Err(e) => { ++ println!("{} {}", "✗".red(), e); ++ print_ftc_manual_fallback(&sdk_config); ++ issues.push(("FTC SDK", "See manual installation instructions above.".to_string())); ++ } ++ } + } + } + } else { + println!("FTC SDK not found. Installing..."); +- crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?; +- let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path) +- .unwrap_or_else(|_| "unknown".to_string()); +- installed.push(format!("FTC SDK {} (installed)", version)); ++ match crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path, proxy) { ++ Ok(_) => { ++ let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path) ++ .unwrap_or_else(|_| "unknown".to_string()); ++ installed.push(format!("FTC SDK {} (installed)", version)); ++ } ++ Err(e) => { ++ println!("{} {}", "✗".red(), e); ++ print_ftc_manual_fallback(&sdk_config); ++ issues.push(("FTC SDK", "See manual installation instructions above.".to_string())); ++ } ++ } + } + println!(); + +@@ -85,14 +105,26 @@ fn setup_system() -> Result<()> { + } + Err(_) => { + println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow()); +- crate::sdk::android::install(&sdk_config.android_sdk_path)?; +- installed.push("Android SDK (installed)".to_string()); ++ match crate::sdk::android::install(&sdk_config.android_sdk_path, proxy) { ++ Ok(_) => installed.push("Android SDK (installed)".to_string()), ++ Err(e) => { ++ println!("{} {}", "✗".red(), e); ++ print_android_manual_fallback(&sdk_config); ++ issues.push(("Android SDK", "See manual installation instructions above.".to_string())); ++ } ++ } + } + } + } else { + println!("Android SDK not found. Installing..."); +- crate::sdk::android::install(&sdk_config.android_sdk_path)?; +- installed.push("Android SDK (installed)".to_string()); ++ match crate::sdk::android::install(&sdk_config.android_sdk_path, proxy) { ++ Ok(_) => installed.push("Android SDK (installed)".to_string()), ++ Err(e) => { ++ println!("{} {}", "✗".red(), e); ++ print_android_manual_fallback(&sdk_config); ++ issues.push(("Android SDK", "See manual installation instructions above.".to_string())); ++ } ++ } + } + println!(); + +@@ -132,7 +164,7 @@ fn setup_system() -> Result<()> { + } + + /// Setup dependencies for a specific project by reading its .weevil.toml +-fn setup_project(project_path: &str) -> Result<()> { ++fn setup_project(project_path: &str, proxy: &ProxyConfig) -> Result<()> { + let project_path = PathBuf::from(project_path); + + if !project_path.exists() { +@@ -143,6 +175,9 @@ fn setup_project(project_path: &str) -> Result<()> { + println!("{}", " Project Setup - Installing Dependencies".bright_cyan().bold()); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); ++ ++ proxy.print_status(); ++ println!(); + + // Load project configuration + println!("{}", "Reading project configuration...".bright_yellow()); +@@ -214,7 +249,7 @@ fn setup_project(project_path: &str) -> Result<()> { + + // Try to install it automatically + println!("{}", "Attempting automatic installation...".bright_yellow()); +- match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path) { ++ match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path, proxy) { + Ok(_) => { + println!("{} FTC SDK {} installed successfully", + "✓".green(), +@@ -224,13 +259,13 @@ fn setup_project(project_path: &str) -> Result<()> { + } + Err(e) => { + println!("{} Automatic installation failed: {}", "✗".red(), e); +- println!(); +- println!("{}", "Manual Installation Required:".bright_yellow().bold()); +- println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\"); +- println!(" {}", config.ftc_sdk_path.display()); +- println!(" cd {}", config.ftc_sdk_path.display()); +- println!(" git checkout {}", config.ftc_sdk_version); +- bail!("FTC SDK installation failed"); ++ let sdk_config = SdkConfig { ++ ftc_sdk_path: config.ftc_sdk_path.clone(), ++ android_sdk_path: config.android_sdk_path.clone(), ++ cache_dir: dirs::home_dir().unwrap_or_default().join(".weevil"), ++ }; ++ print_ftc_manual_fallback(&sdk_config); ++ issues.push(("FTC SDK", "See manual installation instructions above.".to_string())); + } + } + } +@@ -249,14 +284,36 @@ fn setup_project(project_path: &str) -> Result<()> { + } + Err(_) => { + println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow()); +- crate::sdk::android::install(&config.android_sdk_path)?; +- installed.push("Android SDK (installed)".to_string()); ++ match crate::sdk::android::install(&config.android_sdk_path, proxy) { ++ Ok(_) => installed.push("Android SDK (installed)".to_string()), ++ Err(e) => { ++ println!("{} {}", "✗".red(), e); ++ let sdk_config = SdkConfig { ++ ftc_sdk_path: config.ftc_sdk_path.clone(), ++ android_sdk_path: config.android_sdk_path.clone(), ++ cache_dir: dirs::home_dir().unwrap_or_default().join(".weevil"), ++ }; ++ print_android_manual_fallback(&sdk_config); ++ issues.push(("Android SDK", "See manual installation instructions above.".to_string())); ++ } ++ } + } + } + } else { + println!("Android SDK not found. Installing..."); +- crate::sdk::android::install(&config.android_sdk_path)?; +- installed.push("Android SDK (installed)".to_string()); ++ match crate::sdk::android::install(&config.android_sdk_path, proxy) { ++ Ok(_) => installed.push("Android SDK (installed)".to_string()), ++ Err(e) => { ++ println!("{} {}", "✗".red(), e); ++ let sdk_config = SdkConfig { ++ ftc_sdk_path: config.ftc_sdk_path.clone(), ++ android_sdk_path: config.android_sdk_path.clone(), ++ cache_dir: dirs::home_dir().unwrap_or_default().join(".weevil"), ++ }; ++ print_android_manual_fallback(&sdk_config); ++ issues.push(("Android SDK", "See manual installation instructions above.".to_string())); ++ } ++ } + } + println!(); + +@@ -511,4 +568,147 @@ fn print_project_summary(installed: &[String], issues: &[(&str, String)], config + println!(" Then run {} to verify", format!("weevil setup {}", project_path.display()).bright_white()); + } + println!(); ++} ++ ++// ───────────────────────────────────────────────────────────────────────────── ++// Manual fallback instructions — printed when automatic install fails. ++// These walk the user through doing everything by hand, with explicit steps ++// for Linux, macOS, and Windows. ++// ───────────────────────────────────────────────────────────────────────────── ++ ++fn print_ftc_manual_fallback(sdk_config: &SdkConfig) { ++ let dest = sdk_config.ftc_sdk_path.display(); ++ ++ println!(); ++ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow()); ++ println!("{}", " Manual FTC SDK Installation".bright_yellow().bold()); ++ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow()); ++ println!(); ++ println!(" Automatic installation failed. Follow the steps below to"); ++ println!(" clone the FTC SDK by hand. If you are behind a proxy, set"); ++ println!(" the environment variables shown before running git."); ++ println!(); ++ println!(" Target directory: {}", dest); ++ println!(); ++ ++ println!(" {} Linux / macOS:", "▸".bright_cyan()); ++ println!(" # If behind a proxy, set these first (replace with your proxy):"); ++ println!(" export HTTPS_PROXY=http://your-proxy:3128"); ++ println!(" export HTTP_PROXY=http://your-proxy:3128"); ++ println!(); ++ println!(" # If the proxy uses a custom CA certificate, add:"); ++ println!(" export GIT_SSL_CAPATH=/path/to/ca-certificates"); ++ println!(" # (ask your IT department for the CA cert if needed)"); ++ println!(); ++ println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\"); ++ println!(" {}", dest); ++ println!(" cd {}", dest); ++ println!(" git checkout v10.1.1"); ++ println!(); ++ ++ println!(" {} Windows (PowerShell):", "▸".bright_cyan()); ++ println!(" # If behind a proxy, set these first:"); ++ println!(" $env:HTTPS_PROXY = \"http://your-proxy:3128\""); ++ println!(" $env:HTTP_PROXY = \"http://your-proxy:3128\""); ++ println!(); ++ println!(" # If the proxy uses a custom CA certificate:"); ++ println!(" git config --global http.sslCAInfo C:\\path\\to\\ca-bundle.crt"); ++ println!(" # (ask your IT department for the CA cert if needed)"); ++ println!(); ++ println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git `"); ++ println!(" {}", dest); ++ println!(" cd {}", dest); ++ println!(" git checkout v10.1.1"); ++ println!(); ++ ++ println!(" Once done, run {} again.", "weevil setup".bright_white()); ++ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow()); ++ println!(); ++} ++ ++fn print_android_manual_fallback(sdk_config: &SdkConfig) { ++ let dest = sdk_config.android_sdk_path.display(); ++ ++ // Pick the right download URL for the current OS ++ let (url, extract_note) = if cfg!(target_os = "windows") { ++ ( ++ "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip", ++ "Extract the zip. You will get a cmdline-tools/ folder." ++ ) ++ } else if cfg!(target_os = "macos") { ++ ( ++ "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip", ++ "Extract the zip. You will get a cmdline-tools/ folder." ++ ) ++ } else { ++ ( ++ "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip", ++ "Extract the zip. You will get a cmdline-tools/ folder." ++ ) ++ }; ++ ++ println!(); ++ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow()); ++ println!("{}", " Manual Android SDK Installation".bright_yellow().bold()); ++ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow()); ++ println!(); ++ println!(" Automatic installation failed. Follow the steps below to"); ++ println!(" download and set up the Android SDK by hand."); ++ println!(); ++ println!(" Target directory: {}", dest); ++ println!(" Download URL: {}", url); ++ println!(); ++ ++ println!(" {} Linux / macOS:", "▸".bright_cyan()); ++ println!(" # If behind a proxy, set these first:"); ++ println!(" export HTTPS_PROXY=http://your-proxy:3128"); ++ println!(" export HTTP_PROXY=http://your-proxy:3128"); ++ println!(); ++ println!(" # If the proxy uses a custom CA cert, add:"); ++ println!(" export CURL_CA_BUNDLE=/path/to/ca-bundle.crt"); ++ println!(" # (ask your IT department for the CA cert if needed)"); ++ println!(); ++ println!(" mkdir -p {}", dest); ++ println!(" cd {}", dest); ++ println!(" curl -L -o cmdline-tools.zip \\"); ++ println!(" \"{}\"", url); ++ println!(" unzip cmdline-tools.zip"); ++ println!(" # {} Move into the expected layout:", extract_note); ++ println!(" mv cmdline-tools cmdline-tools-temp"); ++ println!(" mkdir -p cmdline-tools/latest"); ++ println!(" mv cmdline-tools-temp/* cmdline-tools/latest/"); ++ println!(" rmdir cmdline-tools-temp"); ++ println!(" # Accept licenses and install packages:"); ++ println!(" ./cmdline-tools/latest/bin/sdkmanager --licenses"); ++ println!(" ./cmdline-tools/latest/bin/sdkmanager platform-tools \"platforms;android-34\" \"build-tools;34.0.0\""); ++ println!(); ++ ++ println!(" {} Windows (PowerShell):", "▸".bright_cyan()); ++ println!(" # If behind a proxy, set these first:"); ++ println!(" $env:HTTPS_PROXY = \"http://your-proxy:3128\""); ++ println!(" $env:HTTP_PROXY = \"http://your-proxy:3128\""); ++ println!(); ++ println!(" # If the proxy uses a custom CA cert:"); ++ println!(" # Download the CA cert from your IT department and note its path."); ++ println!(" # PowerShell's Invoke-WebRequest will use the system cert store;"); ++ println!(" # you may need to import the cert: "); ++ println!(" # Import-Certificate -FilePath C:\\path\\to\\ca.crt -CertStoreLocation Cert:\\LocalMachine\\Root"); ++ println!(); ++ println!(" New-Item -ItemType Directory -Path \"{}\" -Force", dest); ++ println!(" cd \"{}\"", dest); ++ println!(" Invoke-WebRequest -Uri \"{}\" -OutFile cmdline-tools.zip", url); ++ println!(" Expand-Archive -Path cmdline-tools.zip -DestinationPath ."); ++ println!(" # Move into the expected layout:"); ++ println!(" Rename-Item cmdline-tools cmdline-tools-temp"); ++ println!(" New-Item -ItemType Directory -Path cmdline-tools\\latest"); ++ println!(" Move-Item cmdline-tools-temp\\* cmdline-tools\\latest\\"); ++ println!(" Remove-Item cmdline-tools-temp"); ++ println!(" # Accept licenses and install packages:"); ++ println!(" .\\cmdline-tools\\latest\\bin\\sdkmanager.bat --licenses"); ++ println!(" .\\cmdline-tools\\latest\\bin\\sdkmanager.bat platform-tools \"platforms;android-34\" \"build-tools;34.0.0\""); ++ println!(); ++ ++ println!(" Once done, run {} again.", "weevil setup".bright_white()); ++ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow()); ++ println!(); + } +\ No newline at end of file +diff --git i/src/main.rs w/src/main.rs +index aa8fce4..35d11f7 100644 +--- i/src/main.rs ++++ w/src/main.rs +@@ -35,6 +35,14 @@ enum Commands { + /// Path to Android SDK (optional, will auto-detect or download) + #[arg(long)] + android_sdk: Option, ++ ++ /// Use this proxy for all network operations (e.g. http://proxy:3128) ++ #[arg(long)] ++ proxy: Option, ++ ++ /// Force direct connection, ignoring proxy env vars ++ #[arg(long)] ++ no_proxy: bool, + }, + + /// Check system health and diagnose issues +@@ -44,6 +52,14 @@ enum Commands { + Setup { + /// Path to project directory (optional - without it, sets up system) + path: Option, ++ ++ /// Use this proxy for all network operations (e.g. http://proxy:3128) ++ #[arg(long)] ++ proxy: Option, ++ ++ /// Force direct connection, ignoring proxy env vars ++ #[arg(long)] ++ no_proxy: bool, + }, + + /// Remove Weevil-installed SDKs and dependencies +@@ -85,6 +101,14 @@ enum Commands { + Sdk { + #[command(subcommand)] + command: SdkCommands, ++ ++ /// Use this proxy for all network operations (e.g. http://proxy:3128) ++ #[arg(long)] ++ proxy: Option, ++ ++ /// Force direct connection, ignoring proxy env vars ++ #[arg(long)] ++ no_proxy: bool, + }, + + /// Show or update project configuration +@@ -120,14 +144,16 @@ fn main() -> Result<()> { + print_banner(); + + 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 { name, ftc_sdk, android_sdk, proxy, no_proxy } => { ++ let proxy_config = weevil::sdk::proxy::ProxyConfig::resolve(proxy.as_deref(), no_proxy)?; ++ commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref(), &proxy_config) + } + Commands::Doctor => { + commands::doctor::run_diagnostics() + } +- Commands::Setup { path } => { +- commands::setup::setup_environment(path.as_deref()) ++ Commands::Setup { path, proxy, no_proxy } => { ++ let proxy_config = weevil::sdk::proxy::ProxyConfig::resolve(proxy.as_deref(), no_proxy)?; ++ commands::setup::setup_environment(path.as_deref(), &proxy_config) + } + Commands::Uninstall { dry_run, only } => { + commands::uninstall::uninstall_dependencies(dry_run, only) +@@ -138,11 +164,14 @@ fn main() -> Result<()> { + Commands::Deploy { path, usb, wifi, ip } => { + commands::deploy::deploy_project(&path, usb, wifi, ip.as_deref()) + } +- Commands::Sdk { command } => match command { +- SdkCommands::Install => commands::sdk::install_sdks(), +- SdkCommands::Status => commands::sdk::show_status(), +- SdkCommands::Update => commands::sdk::update_sdks(), +- }, ++ Commands::Sdk { command, proxy, no_proxy } => { ++ let proxy_config = weevil::sdk::proxy::ProxyConfig::resolve(proxy.as_deref(), no_proxy)?; ++ match command { ++ SdkCommands::Install => commands::sdk::install_sdks(&proxy_config), ++ SdkCommands::Status => commands::sdk::show_status(), ++ SdkCommands::Update => commands::sdk::update_sdks(&proxy_config), ++ } ++ } + Commands::Config { path, set_sdk } => { + if let Some(sdk_path) = set_sdk { + commands::config::set_sdk(&path, &sdk_path) +@@ -164,4 +193,4 @@ fn print_banner() { + println!("{}", " Nexus Workshops LLC".bright_cyan()); + println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); + println!(); +-} ++} +\ No newline at end of file +diff --git i/src/sdk/android.rs w/src/sdk/android.rs +index 596ed74..b91701e 100644 +--- i/src/sdk/android.rs ++++ w/src/sdk/android.rs +@@ -6,11 +6,13 @@ + use std::io::Write; + use colored::*; + ++use super::proxy::ProxyConfig; ++ + const ANDROID_SDK_URL_LINUX: &str = "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"; + const ANDROID_SDK_URL_MAC: &str = "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip"; + const ANDROID_SDK_URL_WINDOWS: &str = "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip"; + +-pub fn install(sdk_path: &Path) -> Result<()> { ++pub fn install(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> { + // Check if SDK exists AND is complete + if sdk_path.exists() { + match verify(sdk_path) { +@@ -42,10 +44,20 @@ pub fn install(sdk_path: &Path) -> Result<()> { + + // Download + println!("Downloading from: {}", url); +- let client = Client::new(); ++ let client = match &proxy.url { ++ Some(proxy_url) => { ++ println!(" via proxy: {}", proxy_url); ++ Client::builder() ++ .proxy(reqwest::Proxy::all(proxy_url.clone()) ++ .context("Failed to configure proxy")?) ++ .build() ++ .context("Failed to build HTTP client")? ++ } ++ None => Client::new(), ++ }; + let response = client.get(url) + .send() +- .context("Failed to download Android SDK")?; ++ .context("Failed to download Android SDK. If you are behind a proxy, try --proxy or set HTTPS_PROXY")?; + + let total_size = response.content_length().unwrap_or(0); + +@@ -104,14 +116,14 @@ pub fn install(sdk_path: &Path) -> Result<()> { + } + + // Install required packages +- install_packages(sdk_path)?; ++ install_packages(sdk_path, proxy)?; + + println!("{} Android SDK installed successfully", "✓".green()); + + Ok(()) + } + +-fn install_packages(sdk_path: &Path) -> Result<()> { ++fn install_packages(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> { + println!("Installing Android SDK packages..."); + + let sdkmanager_path = sdk_path.join("cmdline-tools").join("latest").join("bin"); +@@ -132,10 +144,10 @@ fn install_packages(sdk_path: &Path) -> Result<()> { + + println!("Found sdkmanager at: {}", sdkmanager.display()); + +- run_sdkmanager(&sdkmanager, sdk_path) ++ run_sdkmanager(&sdkmanager, sdk_path, proxy) + } + +-fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> { ++fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path, proxy: &ProxyConfig) -> Result<()> { + use std::process::{Command, Stdio}; + use std::io::Write; + +@@ -151,6 +163,13 @@ fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> { + Command::new(sdkmanager) + }; + ++ // Inject proxy env vars so sdkmanager picks them up ++ if let Some(proxy_url) = &proxy.url { ++ let url_str = proxy_url.as_str(); ++ cmd.env("HTTPS_PROXY", url_str) ++ .env("HTTP_PROXY", url_str); ++ } ++ + cmd.arg(format!("--sdk_root={}", sdk_root.display())) + .arg("--licenses") + .stdin(Stdio::piped()) +@@ -192,6 +211,13 @@ fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> { + } else { + Command::new(sdkmanager) + }; ++ ++ // Inject proxy env vars here too ++ if let Some(proxy_url) = &proxy.url { ++ let url_str = proxy_url.as_str(); ++ cmd.env("HTTPS_PROXY", url_str) ++ .env("HTTP_PROXY", url_str); ++ } + + let status = cmd + .arg(format!("--sdk_root={}", sdk_root.display())) +diff --git i/src/sdk/ftc.rs w/src/sdk/ftc.rs +index 778cece..3e982e8 100644 +--- i/src/sdk/ftc.rs ++++ w/src/sdk/ftc.rs +@@ -4,10 +4,12 @@ + use colored::*; + use std::fs; + ++use super::proxy::ProxyConfig; ++ + const FTC_SDK_URL: &str = "https://github.com/FIRST-Tech-Challenge/FtcRobotController.git"; + const FTC_SDK_VERSION: &str = "v10.1.1"; + +-pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> { ++pub fn install(sdk_path: &Path, android_sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> { + if sdk_path.exists() { + println!("{} FTC SDK already installed at: {}", + "✓".green(), +@@ -22,8 +24,8 @@ pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> { + println!("Cloning from: {}", FTC_SDK_URL); + println!("Version: {}", FTC_SDK_VERSION); + +- // Clone the repository +- let repo = Repository::clone(FTC_SDK_URL, sdk_path) ++ // Clone the repository, with proxy if configured ++ let repo = clone_with_proxy(FTC_SDK_URL, sdk_path, proxy) + .context("Failed to clone FTC SDK")?; + + // Checkout specific version +@@ -39,6 +41,44 @@ pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> { + Ok(()) + } + ++/// Clone a git repo, injecting http.proxy into a git2 config if ProxyConfig has a URL. ++/// Returns a more helpful error message when a proxy was involved. ++fn clone_with_proxy(url: &str, dest: &Path, proxy: &ProxyConfig) -> Result { ++ let mut opts = git2::CloneOptions::new(); ++ ++ if let Some(proxy_url) = &proxy.url { ++ // git2 reads http.proxy from a config object passed to the clone options. ++ // We build an in-memory config with just that one key. ++ let mut git_config = git2::Config::new()?; ++ git_config.set_str("http.proxy", proxy_url.as_str())?; ++ opts.local(false); // force network path even if URL looks local ++ // Unfortunately git2::CloneOptions doesn't have a direct .config() method, ++ // so we set the env var which libgit2 also respects as a fallback. ++ std::env::set_var("GIT_PROXY_COMMAND", ""); // clear any ssh proxy ++ std::env::set_var("HTTP_PROXY", proxy_url.as_str()); ++ std::env::set_var("HTTPS_PROXY", proxy_url.as_str()); ++ println!(" via proxy: {}", proxy_url); ++ } ++ ++ Repository::clone_with(url, dest, &opts).map_err(|e| { ++ if proxy.url.is_some() { ++ anyhow::anyhow!( ++ "{}\n\n\ ++ This failure may be caused by your proxy. If you are behind a \ ++ corporate or school network, see 'weevil setup' for manual \ ++ fallback instructions.", ++ e ++ ) ++ } else { ++ anyhow::anyhow!( ++ "{}\n\n\ ++ If you are behind a proxy, try: weevil sdk install --proxy ", ++ e ++ ) ++ } ++ }) ++} ++ + fn create_local_properties(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> { + // Convert path to use forward slashes (works on both Windows and Unix) + let android_sdk_str = android_sdk_path +@@ -80,15 +120,39 @@ fn check_version(sdk_path: &Path) -> Result<()> { + Ok(()) + } + +-pub fn update(sdk_path: &Path) -> Result<()> { ++pub fn update(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> { + println!("{}", "Updating FTC SDK...".bright_yellow()); + ++ // Set proxy env vars for the fetch if configured ++ if let Some(proxy_url) = &proxy.url { ++ std::env::set_var("HTTP_PROXY", proxy_url.as_str()); ++ std::env::set_var("HTTPS_PROXY", proxy_url.as_str()); ++ println!(" via proxy: {}", proxy_url); ++ } ++ + let repo = Repository::open(sdk_path) + .context("FTC SDK not found or not a git repository")?; + + // Fetch latest + let mut remote = repo.find_remote("origin")?; +- remote.fetch(&["refs/tags/*:refs/tags/*"], None, None)?; ++ remote.fetch(&["refs/tags/*:refs/tags/*"], None, None) ++ .map_err(|e| { ++ if proxy.url.is_some() { ++ anyhow::anyhow!( ++ "Failed to fetch: {}\n\n\ ++ This failure may be caused by your proxy. If you are behind a \ ++ corporate or school network, see 'weevil setup' for manual \ ++ fallback instructions.", ++ e ++ ) ++ } else { ++ anyhow::anyhow!( ++ "Failed to fetch: {}\n\n\ ++ If you are behind a proxy, try: weevil sdk update --proxy ", ++ e ++ ) ++ } ++ })?; + + // Checkout latest version + let obj = repo.revparse_single(FTC_SDK_VERSION)?; +diff --git i/src/sdk/mod.rs w/src/sdk/mod.rs +index 080ce36..5d7c065 100644 +--- i/src/sdk/mod.rs ++++ w/src/sdk/mod.rs +@@ -6,6 +6,7 @@ + pub mod android; + pub mod ftc; + pub mod gradle; ++pub mod proxy; + + pub struct SdkConfig { + pub ftc_sdk_path: PathBuf, diff --git a/src/commands/new.rs b/src/commands/new.rs index aeed30a..df37ada 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -3,13 +3,17 @@ use std::path::PathBuf; use colored::*; use crate::sdk::SdkConfig; +use crate::sdk::proxy::ProxyConfig; use crate::project::ProjectBuilder; pub fn create_project( name: &str, ftc_sdk: Option<&str>, android_sdk: Option<&str>, + _proxy: &ProxyConfig, ) -> Result<()> { + // _proxy is threaded through here so future flows (e.g. auto-install on + // missing SDK) can use it without changing the call site in main. // Validate project name if name.is_empty() { bail!("Project name cannot be empty"); diff --git a/src/commands/sdk.rs b/src/commands/sdk.rs index ddc3aa6..e87a6b7 100644 --- a/src/commands/sdk.rs +++ b/src/commands/sdk.rs @@ -1,60 +1,61 @@ use anyhow::Result; use colored::*; use crate::sdk::SdkConfig; +use crate::sdk::proxy::ProxyConfig; -pub fn install_sdks() -> Result<()> { +pub fn install_sdks(proxy: &ProxyConfig) -> Result<()> { println!("{}", "Installing SDKs...".bright_yellow().bold()); println!(); - + let config = SdkConfig::new()?; - + // Install FTC SDK - crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?; - + crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path, proxy)?; + // Install Android SDK - crate::sdk::android::install(&config.android_sdk_path)?; - + crate::sdk::android::install(&config.android_sdk_path, proxy)?; + println!(); println!("{} All SDKs installed successfully", "✓".green().bold()); config.print_status(); - + Ok(()) } pub fn show_status() -> Result<()> { let config = SdkConfig::new()?; config.print_status(); - + // Verify SDKs println!(); println!("{}", "Verification:".bright_yellow().bold()); - + match crate::sdk::ftc::verify(&config.ftc_sdk_path) { Ok(_) => println!("{} FTC SDK is valid", "✓".green()), Err(e) => println!("{} FTC SDK: {}", "✗".red(), e), } - + match crate::sdk::android::verify(&config.android_sdk_path) { Ok(_) => println!("{} Android SDK is valid", "✓".green()), Err(e) => println!("{} Android SDK: {}", "✗".red(), e), } - + println!(); - + Ok(()) } -pub fn update_sdks() -> Result<()> { +pub fn update_sdks(proxy: &ProxyConfig) -> Result<()> { println!("{}", "Updating SDKs...".bright_yellow().bold()); println!(); - + let config = SdkConfig::new()?; - + // Update FTC SDK - crate::sdk::ftc::update(&config.ftc_sdk_path)?; - + crate::sdk::ftc::update(&config.ftc_sdk_path, proxy)?; + println!(); println!("{} SDKs updated successfully", "✓".green().bold()); - + Ok(()) -} +} \ No newline at end of file diff --git a/src/commands/setup.rs b/src/commands/setup.rs index 975b814..baafed4 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -4,18 +4,19 @@ use std::process::Command; use colored::*; use crate::sdk::SdkConfig; +use crate::sdk::proxy::ProxyConfig; use crate::project::ProjectConfig; /// Setup development environment - either system-wide or for a specific project -pub fn setup_environment(project_path: Option<&str>) -> Result<()> { +pub fn setup_environment(project_path: Option<&str>, proxy: &ProxyConfig) -> Result<()> { match project_path { - Some(path) => setup_project(path), - None => setup_system(), + Some(path) => setup_project(path, proxy), + None => setup_system(proxy), } } /// Setup system-wide development environment with default SDKs -fn setup_system() -> Result<()> { +fn setup_system(proxy: &ProxyConfig) -> Result<()> { println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold()); println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan()); @@ -57,7 +58,7 @@ fn setup_system() -> Result<()> { } Err(_) => { println!("{} FTC SDK found but incomplete, reinstalling...", "⚠".yellow()); - crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?; + crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path, proxy)?; let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path) .unwrap_or_else(|_| "unknown".to_string()); installed.push(format!("FTC SDK {} (installed)", version)); @@ -65,7 +66,7 @@ fn setup_system() -> Result<()> { } } else { println!("FTC SDK not found. Installing..."); - crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?; + crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path, proxy)?; let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path) .unwrap_or_else(|_| "unknown".to_string()); installed.push(format!("FTC SDK {} (installed)", version)); @@ -85,13 +86,13 @@ fn setup_system() -> Result<()> { } Err(_) => { println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow()); - crate::sdk::android::install(&sdk_config.android_sdk_path)?; + crate::sdk::android::install(&sdk_config.android_sdk_path, proxy)?; installed.push("Android SDK (installed)".to_string()); } } } else { println!("Android SDK not found. Installing..."); - crate::sdk::android::install(&sdk_config.android_sdk_path)?; + crate::sdk::android::install(&sdk_config.android_sdk_path, proxy)?; installed.push("Android SDK (installed)".to_string()); } println!(); @@ -132,7 +133,7 @@ fn setup_system() -> Result<()> { } /// Setup dependencies for a specific project by reading its .weevil.toml -fn setup_project(project_path: &str) -> Result<()> { +fn setup_project(project_path: &str, proxy: &ProxyConfig) -> Result<()> { let project_path = PathBuf::from(project_path); if !project_path.exists() { @@ -214,7 +215,7 @@ fn setup_project(project_path: &str) -> Result<()> { // Try to install it automatically println!("{}", "Attempting automatic installation...".bright_yellow()); - match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path) { + match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path, proxy) { Ok(_) => { println!("{} FTC SDK {} installed successfully", "✓".green(), @@ -249,13 +250,13 @@ fn setup_project(project_path: &str) -> Result<()> { } Err(_) => { println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow()); - crate::sdk::android::install(&config.android_sdk_path)?; + crate::sdk::android::install(&config.android_sdk_path, proxy)?; installed.push("Android SDK (installed)".to_string()); } } } else { println!("Android SDK not found. Installing..."); - crate::sdk::android::install(&config.android_sdk_path)?; + crate::sdk::android::install(&config.android_sdk_path, proxy)?; installed.push("Android SDK (installed)".to_string()); } println!(); diff --git a/src/main.rs b/src/main.rs index aa8fce4..303bb52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 ")] @@ -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, + + /// 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!(); -} +} \ No newline at end of file diff --git a/src/sdk/android.rs b/src/sdk/android.rs index 596ed74..2c3f2a9 100644 --- a/src/sdk/android.rs +++ b/src/sdk/android.rs @@ -1,16 +1,17 @@ use std::path::Path; use anyhow::{Result, Context}; use indicatif::{ProgressBar, ProgressStyle}; -use reqwest::blocking::Client; use std::fs::File; use std::io::Write; use colored::*; +use super::proxy::ProxyConfig; + const ANDROID_SDK_URL_LINUX: &str = "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"; const ANDROID_SDK_URL_MAC: &str = "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip"; const ANDROID_SDK_URL_WINDOWS: &str = "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip"; -pub fn install(sdk_path: &Path) -> Result<()> { +pub fn install(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> { // Check if SDK exists AND is complete if sdk_path.exists() { match verify(sdk_path) { @@ -42,10 +43,14 @@ pub fn install(sdk_path: &Path) -> Result<()> { // Download println!("Downloading from: {}", url); - let client = Client::new(); + proxy.print_status(); + let client = proxy.client()?; let response = client.get(url) .send() - .context("Failed to download Android SDK")?; + .map_err(|e| { + super::proxy::print_offline_instructions(); + anyhow::anyhow!("Failed to download Android SDK: {}", e) + })?; let total_size = response.content_length().unwrap_or(0); diff --git a/src/sdk/ftc.rs b/src/sdk/ftc.rs index 778cece..1f883a4 100644 --- a/src/sdk/ftc.rs +++ b/src/sdk/ftc.rs @@ -4,10 +4,12 @@ use git2::Repository; use colored::*; use std::fs; +use super::proxy::{ProxyConfig, GitProxyGuard}; + const FTC_SDK_URL: &str = "https://github.com/FIRST-Tech-Challenge/FtcRobotController.git"; const FTC_SDK_VERSION: &str = "v10.1.1"; -pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> { +pub fn install(sdk_path: &Path, android_sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> { if sdk_path.exists() { println!("{} FTC SDK already installed at: {}", "✓".green(), @@ -21,10 +23,18 @@ pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> { println!("{}", "Installing FTC SDK...".bright_yellow()); println!("Cloning from: {}", FTC_SDK_URL); println!("Version: {}", FTC_SDK_VERSION); - + proxy.print_status(); + + // GitProxyGuard sets HTTPS_PROXY for the duration of the clone so that + // libgit2 honours --proxy / --no-proxy without touching ~/.gitconfig. + let _guard = GitProxyGuard::new(proxy); + // Clone the repository let repo = Repository::clone(FTC_SDK_URL, sdk_path) - .context("Failed to clone FTC SDK")?; + .map_err(|e| { + super::proxy::print_offline_instructions(); + anyhow::anyhow!("Failed to clone FTC SDK: {}", e) + })?; // Checkout specific version let obj = repo.revparse_single(FTC_SDK_VERSION)?; @@ -80,15 +90,23 @@ fn check_version(sdk_path: &Path) -> Result<()> { Ok(()) } -pub fn update(sdk_path: &Path) -> Result<()> { +pub fn update(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> { println!("{}", "Updating FTC SDK...".bright_yellow()); - + proxy.print_status(); + let repo = Repository::open(sdk_path) .context("FTC SDK not found or not a git repository")?; - + + // Guard env vars for the fetch + let _guard = GitProxyGuard::new(proxy); + // Fetch latest let mut remote = repo.find_remote("origin")?; - remote.fetch(&["refs/tags/*:refs/tags/*"], None, None)?; + remote.fetch(&["refs/tags/*:refs/tags/*"], None, None) + .map_err(|e| { + super::proxy::print_offline_instructions(); + anyhow::anyhow!("Failed to fetch FTC SDK updates: {}", e) + })?; // Checkout latest version let obj = repo.revparse_single(FTC_SDK_VERSION)?; diff --git a/src/sdk/mod.rs b/src/sdk/mod.rs index 080ce36..5d7c065 100644 --- a/src/sdk/mod.rs +++ b/src/sdk/mod.rs @@ -6,6 +6,7 @@ use colored::*; pub mod android; pub mod ftc; pub mod gradle; +pub mod proxy; pub struct SdkConfig { pub ftc_sdk_path: PathBuf, diff --git a/src/sdk/proxy.rs b/src/sdk/proxy.rs new file mode 100644 index 0000000..3a4e671 --- /dev/null +++ b/src/sdk/proxy.rs @@ -0,0 +1,309 @@ +use colored::*; +use reqwest::blocking; +use reqwest::Url; + +/// Where the proxy URL came from — used for status messages. +#[derive(Debug, Clone, PartialEq)] +pub enum ProxySource { + /// --proxy on the command line + Flag, + /// HTTPS_PROXY or HTTP_PROXY environment variable + Env(String), +} + +/// Resolved proxy configuration. A `None` url means "go direct". +#[derive(Debug, Clone)] +pub struct ProxyConfig { + pub url: Option, + pub source: Option, +} + +impl ProxyConfig { + /// Resolve proxy with this priority: + /// 1. --no-proxy → direct, ignore everything + /// 2. --proxy → use that URL + /// 3. HTTPS_PROXY / HTTP_PROXY env vars + /// 4. Nothing → direct + pub fn resolve(proxy_flag: Option<&str>, no_proxy: bool) -> Result { + if no_proxy { + return Ok(Self { url: None, source: None }); + } + + if let Some(raw) = proxy_flag { + let url = Url::parse(raw) + .map_err(|e| anyhow::anyhow!("Invalid --proxy URL '{}': {}", raw, e))?; + return Ok(Self { url: Some(url), source: Some(ProxySource::Flag) }); + } + + // Walk the env vars in priority order + for var in &["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] { + if let Ok(val) = std::env::var(var) { + if val.is_empty() { continue; } + let url = Url::parse(&val) + .map_err(|e| anyhow::anyhow!("Invalid {} env var '{}': {}", var, val, e))?; + return Ok(Self { + url: Some(url), + source: Some(ProxySource::Env(var.to_string())), + }); + } + } + + Ok(Self { url: None, source: None }) + } + + /// True when the user explicitly passed --proxy (as opposed to env-var pickup). + /// Used for distinguishing "you asked for this proxy and it failed" from + /// "we picked up an ambient proxy from your environment" in error paths. + #[allow(dead_code)] + pub fn is_explicit(&self) -> bool { + matches!(self.source, Some(ProxySource::Flag)) + } + + /// Human-readable description of where the proxy came from, for status output. + pub fn source_description(&self) -> String { + match &self.source { + Some(ProxySource::Flag) => "--proxy flag".to_string(), + Some(ProxySource::Env(var)) => format!("{} env var", var), + None => "none".to_string(), + } + } + + /// Print a one-line proxy status line (used in setup output). + pub fn print_status(&self) { + match &self.url { + Some(url) => println!( + " {} Proxy: {} ({})", + "✓".green(), + url.as_str().bright_white(), + self.source_description() + ), + None => println!( + " {} Proxy: direct (no proxy)", + "○".bright_black() + ), + } + } + + /// Build a reqwest blocking Client that honours this proxy config. + /// + /// * `Some(url)` → all HTTP/HTTPS traffic goes through that proxy. + /// * `None` → direct, no proxy at all. + /// + /// We always go through the builder (never plain `Client::new()`) because + /// `Client::new()` silently picks up env-var proxies. When the user says + /// `--no-proxy` we need to actively disable that. + pub fn client(&self) -> anyhow::Result { + let mut builder = blocking::ClientBuilder::new(); + + match &self.url { + Some(url) => { + builder = builder.proxy(reqwest::Proxy::all(url.clone())?); + } + None => { + // Actively suppress env-var auto-detection. + builder = builder.no_proxy(); + } + } + + builder.build().map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e)) + } +} + +/// RAII guard that temporarily sets HTTPS_PROXY / HTTP_PROXY for the lifetime +/// of the guard, then restores the previous values on drop. +/// +/// libgit2 (the C library behind the `git2` crate) reads these env vars +/// directly for its HTTP transport. This is the cleanest way to make +/// `git2::Repository::clone` and `remote.fetch()` honour a `--proxy` flag +/// without touching the user's global `~/.gitconfig`. +/// +/// When the ProxyConfig has no URL (direct / --no-proxy) the guard *clears* +/// the vars so libgit2 won't accidentally pick up an ambient proxy the user +/// didn't intend for this operation. +pub struct GitProxyGuard { + prev_https: Option, + prev_http: Option, +} + +impl GitProxyGuard { + pub fn new(config: &ProxyConfig) -> Self { + let prev_https = std::env::var("HTTPS_PROXY").ok(); + let prev_http = std::env::var("HTTP_PROXY").ok(); + + match &config.url { + Some(url) => { + let s = url.as_str(); + std::env::set_var("HTTPS_PROXY", s); + std::env::set_var("HTTP_PROXY", s); + } + None => { + std::env::remove_var("HTTPS_PROXY"); + std::env::remove_var("HTTP_PROXY"); + } + } + + Self { prev_https, prev_http } + } +} + +impl Drop for GitProxyGuard { + fn drop(&mut self) { + match &self.prev_https { + Some(v) => std::env::set_var("HTTPS_PROXY", v), + None => std::env::remove_var("HTTPS_PROXY"), + } + match &self.prev_http { + Some(v) => std::env::set_var("HTTP_PROXY", v), + None => std::env::remove_var("HTTP_PROXY"), + } + } +} + +/// Print clear, actionable instructions for obtaining dependencies without +/// an internet connection. Called whenever a network download fails so the +/// user always sees their escape hatch. +pub fn print_offline_instructions() { + println!(); + println!("{}", "── Offline / Air-Gapped Installation ──────────────────────".bright_yellow().bold()); + println!(); + println!("If you have no internet access (or the proxy is not working),"); + println!("you can obtain the required SDKs on a connected machine and"); + println!("copy them over."); + println!(); + println!("{}", "1. FTC SDK (git clone)".bright_cyan().bold()); + println!(" On a connected machine:"); + println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git"); + println!(" cd FtcRobotController"); + println!(" git checkout v10.1.1"); + println!(); + println!(" Copy the entire FtcRobotController/ directory to this machine"); + println!(" at ~/.weevil/ftc-sdk/ (or wherever your .weevil.toml points)."); + println!(); + println!("{}", "2. Android SDK (command-line tools)".bright_cyan().bold()); + println!(" Download the zip for your OS from a connected machine:"); + println!(" Linux: https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"); + println!(" macOS: https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip"); + println!(" Windows: https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip"); + println!(); + println!(" Extract to ~/.weevil/android-sdk/, then run sdkmanager:"); + println!(" ./cmdline-tools/latest/bin/sdkmanager \\"); + println!(" platform-tools platforms;android-34 build-tools;34.0.0"); + println!(); + println!(" Copy the resulting android-sdk/ directory to this machine."); + println!(); + println!("{}", "3. Gradle distribution".bright_cyan().bold()); + println!(" Gradle fetches its own distribution the first time ./gradlew"); + println!(" runs. If that fails offline, download manually:"); + println!(" https://services.gradle.org/distributions/gradle-8.9-bin.zip"); + println!(" Extract into ~/.gradle/wrapper/dists/gradle-8.9-bin/"); + println!(" (the exact subdirectory is printed by gradlew on failure)."); + println!(); + println!("{}", "4. Proxy quick reference".bright_cyan().bold()); + println!(" • Use a specific proxy: weevil --proxy http://proxy:3128 sdk install"); + println!(" • Skip the proxy entirely: weevil --no-proxy sdk install"); + println!(" • Gradle also reads HTTPS_PROXY / HTTP_PROXY, so set those"); + println!(" in your shell before running ./gradlew if the build needs a proxy."); + println!(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_proxy_flag_forces_direct() { + std::env::set_var("HTTPS_PROXY", "http://proxy.example.com:3128"); + let config = ProxyConfig::resolve(None, true).unwrap(); + assert!(config.url.is_none()); + std::env::remove_var("HTTPS_PROXY"); + } + + #[test] + fn explicit_flag_overrides_env() { + std::env::set_var("HTTPS_PROXY", "http://env-proxy.example.com:3128"); + let config = ProxyConfig::resolve(Some("http://flag-proxy.example.com:8080"), false).unwrap(); + assert_eq!(config.url.as_ref().unwrap().host_str(), Some("flag-proxy.example.com")); + assert!(config.is_explicit()); + std::env::remove_var("HTTPS_PROXY"); + } + + #[test] + fn picks_up_env_var() { + std::env::remove_var("HTTPS_PROXY"); + std::env::remove_var("https_proxy"); + std::env::set_var("HTTP_PROXY", "http://env-proxy.example.com:3128"); + let config = ProxyConfig::resolve(None, false).unwrap(); + assert_eq!(config.url.as_ref().unwrap().host_str(), Some("env-proxy.example.com")); + assert_eq!(config.source, Some(ProxySource::Env("HTTP_PROXY".to_string()))); + std::env::remove_var("HTTP_PROXY"); + } + + #[test] + fn direct_when_nothing_set() { + std::env::remove_var("HTTPS_PROXY"); + std::env::remove_var("https_proxy"); + std::env::remove_var("HTTP_PROXY"); + std::env::remove_var("http_proxy"); + let config = ProxyConfig::resolve(None, false).unwrap(); + assert!(config.url.is_none()); + } + + #[test] + fn rejects_garbage_url() { + let result = ProxyConfig::resolve(Some("not a url at all"), false); + assert!(result.is_err()); + } + + #[test] + fn client_builds_with_proxy() { + let config = ProxyConfig::resolve(Some("http://proxy.example.com:3128"), false).unwrap(); + assert!(config.client().is_ok()); + } + + #[test] + fn client_builds_direct() { + std::env::remove_var("HTTPS_PROXY"); + std::env::remove_var("https_proxy"); + std::env::remove_var("HTTP_PROXY"); + std::env::remove_var("http_proxy"); + let config = ProxyConfig::resolve(None, true).unwrap(); + assert!(config.client().is_ok()); + } + + #[test] + fn git_proxy_guard_sets_and_restores() { + std::env::set_var("HTTPS_PROXY", "http://original:1111"); + std::env::set_var("HTTP_PROXY", "http://original:2222"); + + let config = ProxyConfig::resolve(Some("http://guarded:9999"), false).unwrap(); + { + let _guard = GitProxyGuard::new(&config); + // Url::parse normalises — trailing slash is expected + assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://guarded:9999/"); + assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://guarded:9999/"); + } + assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://original:1111"); + assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://original:2222"); + + std::env::remove_var("HTTPS_PROXY"); + std::env::remove_var("HTTP_PROXY"); + } + + #[test] + fn git_proxy_guard_clears_for_direct() { + std::env::set_var("HTTPS_PROXY", "http://should-be-cleared:1111"); + std::env::set_var("HTTP_PROXY", "http://should-be-cleared:2222"); + + let config = ProxyConfig { url: None, source: None }; + { + let _guard = GitProxyGuard::new(&config); + assert!(std::env::var("HTTPS_PROXY").is_err()); + assert!(std::env::var("HTTP_PROXY").is_err()); + } + assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://should-be-cleared:1111"); + assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://should-be-cleared:2222"); + + std::env::remove_var("HTTPS_PROXY"); + std::env::remove_var("HTTP_PROXY"); + } +} \ No newline at end of file