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:
755
diff.txt
Normal file
755
diff.txt
Normal file
@@ -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<String>,
|
||||||
|
+
|
||||||
|
+ /// Use this proxy for all network operations (e.g. http://proxy:3128)
|
||||||
|
+ #[arg(long)]
|
||||||
|
+ proxy: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// 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<String>,
|
||||||
|
+
|
||||||
|
+ /// Use this proxy for all network operations (e.g. http://proxy:3128)
|
||||||
|
+ #[arg(long)]
|
||||||
|
+ proxy: Option<String>,
|
||||||
|
+
|
||||||
|
+ /// 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<String>,
|
||||||
|
+
|
||||||
|
+ /// 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 <url> 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<Repository> {
|
||||||
|
+ 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 <url>",
|
||||||
|
+ 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 <url>",
|
||||||
|
+ 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,
|
||||||
@@ -3,13 +3,17 @@ use std::path::PathBuf;
|
|||||||
use colored::*;
|
use colored::*;
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
use crate::sdk::SdkConfig;
|
||||||
|
use crate::sdk::proxy::ProxyConfig;
|
||||||
use crate::project::ProjectBuilder;
|
use crate::project::ProjectBuilder;
|
||||||
|
|
||||||
pub fn create_project(
|
pub fn create_project(
|
||||||
name: &str,
|
name: &str,
|
||||||
ftc_sdk: Option<&str>,
|
ftc_sdk: Option<&str>,
|
||||||
android_sdk: Option<&str>,
|
android_sdk: Option<&str>,
|
||||||
|
_proxy: &ProxyConfig,
|
||||||
) -> Result<()> {
|
) -> 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
|
// Validate project name
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
bail!("Project name cannot be empty");
|
bail!("Project name cannot be empty");
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use crate::sdk::SdkConfig;
|
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!("{}", "Installing SDKs...".bright_yellow().bold());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let config = SdkConfig::new()?;
|
let config = SdkConfig::new()?;
|
||||||
|
|
||||||
// Install FTC SDK
|
// 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
|
// Install Android SDK
|
||||||
crate::sdk::android::install(&config.android_sdk_path)?;
|
crate::sdk::android::install(&config.android_sdk_path, proxy)?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("{} All SDKs installed successfully", "✓".green().bold());
|
println!("{} All SDKs installed successfully", "✓".green().bold());
|
||||||
@@ -44,14 +45,14 @@ pub fn show_status() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_sdks() -> Result<()> {
|
pub fn update_sdks(proxy: &ProxyConfig) -> Result<()> {
|
||||||
println!("{}", "Updating SDKs...".bright_yellow().bold());
|
println!("{}", "Updating SDKs...".bright_yellow().bold());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let config = SdkConfig::new()?;
|
let config = SdkConfig::new()?;
|
||||||
|
|
||||||
// Update FTC SDK
|
// Update FTC SDK
|
||||||
crate::sdk::ftc::update(&config.ftc_sdk_path)?;
|
crate::sdk::ftc::update(&config.ftc_sdk_path, proxy)?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("{} SDKs updated successfully", "✓".green().bold());
|
println!("{} SDKs updated successfully", "✓".green().bold());
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ use std::process::Command;
|
|||||||
use colored::*;
|
use colored::*;
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
use crate::sdk::SdkConfig;
|
||||||
|
use crate::sdk::proxy::ProxyConfig;
|
||||||
use crate::project::ProjectConfig;
|
use crate::project::ProjectConfig;
|
||||||
|
|
||||||
/// Setup development environment - either system-wide or for a specific project
|
/// 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 {
|
match project_path {
|
||||||
Some(path) => setup_project(path),
|
Some(path) => setup_project(path, proxy),
|
||||||
None => setup_system(),
|
None => setup_system(proxy),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setup system-wide development environment with default SDKs
|
/// Setup system-wide development environment with default SDKs
|
||||||
fn setup_system() -> Result<()> {
|
fn setup_system(proxy: &ProxyConfig) -> Result<()> {
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||||
println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold());
|
println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold());
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||||
@@ -57,7 +58,7 @@ fn setup_system() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("{} FTC SDK found but incomplete, reinstalling...", "⚠".yellow());
|
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)
|
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
|
||||||
.unwrap_or_else(|_| "unknown".to_string());
|
.unwrap_or_else(|_| "unknown".to_string());
|
||||||
installed.push(format!("FTC SDK {} (installed)", version));
|
installed.push(format!("FTC SDK {} (installed)", version));
|
||||||
@@ -65,7 +66,7 @@ fn setup_system() -> Result<()> {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("FTC SDK not found. Installing...");
|
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)
|
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
|
||||||
.unwrap_or_else(|_| "unknown".to_string());
|
.unwrap_or_else(|_| "unknown".to_string());
|
||||||
installed.push(format!("FTC SDK {} (installed)", version));
|
installed.push(format!("FTC SDK {} (installed)", version));
|
||||||
@@ -85,13 +86,13 @@ fn setup_system() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow());
|
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());
|
installed.push("Android SDK (installed)".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("Android SDK not found. Installing...");
|
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());
|
installed.push("Android SDK (installed)".to_string());
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
@@ -132,7 +133,7 @@ fn setup_system() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Setup dependencies for a specific project by reading its .weevil.toml
|
/// 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);
|
let project_path = PathBuf::from(project_path);
|
||||||
|
|
||||||
if !project_path.exists() {
|
if !project_path.exists() {
|
||||||
@@ -214,7 +215,7 @@ fn setup_project(project_path: &str) -> Result<()> {
|
|||||||
|
|
||||||
// Try to install it automatically
|
// Try to install it automatically
|
||||||
println!("{}", "Attempting automatic installation...".bright_yellow());
|
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(_) => {
|
Ok(_) => {
|
||||||
println!("{} FTC SDK {} installed successfully",
|
println!("{} FTC SDK {} installed successfully",
|
||||||
"✓".green(),
|
"✓".green(),
|
||||||
@@ -249,13 +250,13 @@ fn setup_project(project_path: &str) -> Result<()> {
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow());
|
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());
|
installed.push("Android SDK (installed)".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("Android SDK not found. Installing...");
|
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());
|
installed.push("Android SDK (installed)".to_string());
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
|
|||||||
27
src/main.rs
27
src/main.rs
@@ -3,11 +3,19 @@ use colored::*;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use weevil::version::WEEVIL_VERSION;
|
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 commands;
|
||||||
mod sdk;
|
mod sdk;
|
||||||
mod project;
|
mod project;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
|
use sdk::proxy::ProxyConfig;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "weevil")]
|
#[command(name = "weevil")]
|
||||||
#[command(author = "Eric Ratliff <eric@nxlearn.net>")]
|
#[command(author = "Eric Ratliff <eric@nxlearn.net>")]
|
||||||
@@ -17,6 +25,14 @@ mod templates;
|
|||||||
long_about = None
|
long_about = None
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
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(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
}
|
}
|
||||||
@@ -119,15 +135,18 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
print_banner();
|
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 {
|
match cli.command {
|
||||||
Commands::New { name, ftc_sdk, android_sdk } => {
|
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 => {
|
||||||
commands::doctor::run_diagnostics()
|
commands::doctor::run_diagnostics()
|
||||||
}
|
}
|
||||||
Commands::Setup { path } => {
|
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 { dry_run, only } => {
|
||||||
commands::uninstall::uninstall_dependencies(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::deploy::deploy_project(&path, usb, wifi, ip.as_deref())
|
||||||
}
|
}
|
||||||
Commands::Sdk { command } => match command {
|
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::Status => commands::sdk::show_status(),
|
||||||
SdkCommands::Update => commands::sdk::update_sdks(),
|
SdkCommands::Update => commands::sdk::update_sdks(&proxy),
|
||||||
},
|
},
|
||||||
Commands::Config { path, set_sdk } => {
|
Commands::Config { path, set_sdk } => {
|
||||||
if let Some(sdk_path) = set_sdk {
|
if let Some(sdk_path) = set_sdk {
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use anyhow::{Result, Context};
|
use anyhow::{Result, Context};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use reqwest::blocking::Client;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use colored::*;
|
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_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_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";
|
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
|
// Check if SDK exists AND is complete
|
||||||
if sdk_path.exists() {
|
if sdk_path.exists() {
|
||||||
match verify(sdk_path) {
|
match verify(sdk_path) {
|
||||||
@@ -42,10 +43,14 @@ pub fn install(sdk_path: &Path) -> Result<()> {
|
|||||||
|
|
||||||
// Download
|
// Download
|
||||||
println!("Downloading from: {}", url);
|
println!("Downloading from: {}", url);
|
||||||
let client = Client::new();
|
proxy.print_status();
|
||||||
|
let client = proxy.client()?;
|
||||||
let response = client.get(url)
|
let response = client.get(url)
|
||||||
.send()
|
.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);
|
let total_size = response.content_length().unwrap_or(0);
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ use git2::Repository;
|
|||||||
use colored::*;
|
use colored::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
use super::proxy::{ProxyConfig, GitProxyGuard};
|
||||||
|
|
||||||
const FTC_SDK_URL: &str = "https://github.com/FIRST-Tech-Challenge/FtcRobotController.git";
|
const FTC_SDK_URL: &str = "https://github.com/FIRST-Tech-Challenge/FtcRobotController.git";
|
||||||
const FTC_SDK_VERSION: &str = "v10.1.1";
|
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() {
|
if sdk_path.exists() {
|
||||||
println!("{} FTC SDK already installed at: {}",
|
println!("{} FTC SDK already installed at: {}",
|
||||||
"✓".green(),
|
"✓".green(),
|
||||||
@@ -21,10 +23,18 @@ pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> {
|
|||||||
println!("{}", "Installing FTC SDK...".bright_yellow());
|
println!("{}", "Installing FTC SDK...".bright_yellow());
|
||||||
println!("Cloning from: {}", FTC_SDK_URL);
|
println!("Cloning from: {}", FTC_SDK_URL);
|
||||||
println!("Version: {}", FTC_SDK_VERSION);
|
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
|
// Clone the repository
|
||||||
let repo = Repository::clone(FTC_SDK_URL, sdk_path)
|
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
|
// Checkout specific version
|
||||||
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
||||||
@@ -80,15 +90,23 @@ fn check_version(sdk_path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(sdk_path: &Path) -> Result<()> {
|
pub fn update(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
||||||
println!("{}", "Updating FTC SDK...".bright_yellow());
|
println!("{}", "Updating FTC SDK...".bright_yellow());
|
||||||
|
proxy.print_status();
|
||||||
|
|
||||||
let repo = Repository::open(sdk_path)
|
let repo = Repository::open(sdk_path)
|
||||||
.context("FTC SDK not found or not a git repository")?;
|
.context("FTC SDK not found or not a git repository")?;
|
||||||
|
|
||||||
|
// Guard env vars for the fetch
|
||||||
|
let _guard = GitProxyGuard::new(proxy);
|
||||||
|
|
||||||
// Fetch latest
|
// Fetch latest
|
||||||
let mut remote = repo.find_remote("origin")?;
|
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
|
// Checkout latest version
|
||||||
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use colored::*;
|
|||||||
pub mod android;
|
pub mod android;
|
||||||
pub mod ftc;
|
pub mod ftc;
|
||||||
pub mod gradle;
|
pub mod gradle;
|
||||||
|
pub mod proxy;
|
||||||
|
|
||||||
pub struct SdkConfig {
|
pub struct SdkConfig {
|
||||||
pub ftc_sdk_path: PathBuf,
|
pub ftc_sdk_path: PathBuf,
|
||||||
|
|||||||
309
src/sdk/proxy.rs
Normal file
309
src/sdk/proxy.rs
Normal file
@@ -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 <url> 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<Url>,
|
||||||
|
pub source: Option<ProxySource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyConfig {
|
||||||
|
/// Resolve proxy with this priority:
|
||||||
|
/// 1. --no-proxy → direct, ignore everything
|
||||||
|
/// 2. --proxy <url> → use that URL
|
||||||
|
/// 3. HTTPS_PROXY / HTTP_PROXY env vars
|
||||||
|
/// 4. Nothing → direct
|
||||||
|
pub fn resolve(proxy_flag: Option<&str>, no_proxy: bool) -> Result<Self, anyhow::Error> {
|
||||||
|
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<blocking::Client> {
|
||||||
|
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<String>,
|
||||||
|
prev_http: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user