From b0b2482774a669d73e31630a7737520abdd75202 Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Sun, 25 Jan 2026 16:09:17 -0600 Subject: [PATCH] feat: Add Windows support and stabilize SDK installation (v1.0.0-rc1) Complete Windows compatibility overhaul with robust cross-platform SDK management. This release candidate establishes feature freeze for the 1.0.0 release. Key improvements: - Fixed Android SDK installation on Windows * Use cmd.exe wrapper for sdkmanager.bat with piped stdin * Properly reorganize cmdline-tools directory structure * Write license acceptances synchronously to avoid hangs - Fixed FTC SDK configuration * Auto-generate local.properties with Android SDK path * Escape backslashes in Kotlin build.gradle.kts strings * Support both new installs and upgrades via ensure_local_properties() - Enhanced Windows console output * Enable ANSI color support via enable_ansi_support crate * Maintain color compatibility across Windows versions - Improved error handling and debugging * Added comprehensive logging throughout SDK installation * Better context messages for troubleshooting failures Cross-platform testing verified on: - Windows 11 with Eclipse Adoptium JDK 21 - Linux (existing support maintained) Breaking changes: None This RC introduces feature freeze - subsequent 1.0.x releases will be bug fixes only. New features deferred to 1.1.0. Closes Windows compatibility milestone. --- README.md | 12 ++- src/commands/new.rs | 2 +- src/commands/sdk.rs | 2 +- src/commands/upgrade.rs | 21 +++++ src/main.rs | 4 + src/project/mod.rs | 6 +- src/sdk/android.rs | 178 ++++++++++++++++++++++++++++++---------- src/sdk/ftc.rs | 26 +++++- 8 files changed, 200 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ea3ba19..d7e7629 100644 --- a/README.md +++ b/README.md @@ -511,11 +511,11 @@ Built with frustration at unnecessarily complex robotics frameworks, and hope th ## Project Status -**Current Version:** 1.0.0-beta2 +**Current Version:** 1.0.0-rc1 **What Works:** - ✅ Project generation -- ✅ Cross-platform build/deploy +- ✅ Cross-platform build/deploy (Linux, macOS, Windows) - ✅ SDK management - ✅ Configuration management - ✅ Project upgrades @@ -530,6 +530,12 @@ Built with frustration at unnecessarily complex robotics frameworks, and hope th --- +## Support & Contact + **Questions? Issues? Suggestions?** -Open an issue on NXGit or reach out to the FTC community. Let's make robot programming accessible for everyone! 🚀 \ No newline at end of file +- 📧 Email: [eric@nxws.dev](mailto:eric@nxws.dev) +- 🐛 Issues: Open an issue on the repository +- 💬 Community: Reach out via the FTC community + +Building better tools so you can build better robots. 🤖 \ No newline at end of file diff --git a/src/commands/new.rs b/src/commands/new.rs index 4cdb05c..ea1e338 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -71,7 +71,7 @@ fn ensure_sdks(config: &SdkConfig) -> Result<()> { // Check FTC SDK if !config.ftc_sdk_path.exists() { println!("FTC SDK not found. Installing..."); - crate::sdk::ftc::install(&config.ftc_sdk_path)?; + crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?; } else { println!("{} FTC SDK found at: {}", "✓".green(), config.ftc_sdk_path.display()); crate::sdk::ftc::verify(&config.ftc_sdk_path)?; diff --git a/src/commands/sdk.rs b/src/commands/sdk.rs index 3e7555c..ddc3aa6 100644 --- a/src/commands/sdk.rs +++ b/src/commands/sdk.rs @@ -9,7 +9,7 @@ pub fn install_sdks() -> Result<()> { let config = SdkConfig::new()?; // Install FTC SDK - crate::sdk::ftc::install(&config.ftc_sdk_path)?; + crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?; // Install Android SDK crate::sdk::android::install(&config.android_sdk_path)?; diff --git a/src/commands/upgrade.rs b/src/commands/upgrade.rs index 0cd0b91..e31c1e2 100644 --- a/src/commands/upgrade.rs +++ b/src/commands/upgrade.rs @@ -20,6 +20,9 @@ pub fn upgrade_project(path: &str) -> Result<()> { // Get SDK config let sdk_config = crate::sdk::SdkConfig::new()?; + // Ensure FTC SDK has local.properties (in case it was installed before this feature) + ensure_local_properties(&sdk_config)?; + // Load or create project config let project_config = if has_config { println!("Found existing .weevil.toml"); @@ -115,5 +118,23 @@ pub fn upgrade_project(path: &str) -> Result<()> { println!("Test it: ./gradlew test"); println!(); + Ok(()) +} + +fn ensure_local_properties(sdk_config: &crate::sdk::SdkConfig) -> Result<()> { + let local_properties_path = sdk_config.ftc_sdk_path.join("local.properties"); + + if !local_properties_path.exists() { + println!("Creating local.properties in FTC SDK..."); + let android_sdk_str = sdk_config.android_sdk_path + .display() + .to_string() + .replace("\\", "/"); + + let local_properties = format!("sdk.dir={}\n", android_sdk_str); + fs::write(&local_properties_path, local_properties)?; + println!("{} Created local.properties", "✓".green()); + } + Ok(()) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 43d44f1..08e9dac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,6 +87,10 @@ enum SdkCommands { } fn main() -> Result<()> { + // Enable colors on Windows + #[cfg(windows)] + colored::control::set_virtual_terminal(true).ok(); + let cli = Cli::parse(); print_banner(); diff --git a/src/project/mod.rs b/src/project/mod.rs index 561e6ab..cf45c24 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -87,7 +87,6 @@ impl ProjectBuilder { FTC Robot Project generated by Weevil v1.0.0 ## Quick Start - ```bash # Test your code (runs on PC, no robot needed) ./gradlew test @@ -124,6 +123,9 @@ deploy.bat fs::write(project_path.join(".weevil-version"), "1.0.0")?; // build.gradle.kts - Pure Java with deployToSDK task + // Escape backslashes for Windows paths in Kotlin strings + let sdk_path = sdk_config.ftc_sdk_path.display().to_string().replace("\\", "\\\\"); + let build_gradle = format!(r#"plugins {{ java }} @@ -192,7 +194,7 @@ tasks.register("buildApk") {{ println("✓ APK built successfully") }} }} -"#, sdk_config.ftc_sdk_path.display(), sdk_config.ftc_sdk_path.display()); +"#, sdk_path, sdk_path); fs::write(project_path.join("build.gradle.kts"), build_gradle)?; // settings.gradle.kts diff --git a/src/sdk/android.rs b/src/sdk/android.rs index e911da7..596ed74 100644 --- a/src/sdk/android.rs +++ b/src/sdk/android.rs @@ -8,19 +8,33 @@ use colored::*; 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<()> { + // Check if SDK exists AND is complete if sdk_path.exists() { - println!("{} Android SDK already installed at: {}", - "✓".green(), - sdk_path.display() - ); - return Ok(()); + match verify(sdk_path) { + Ok(_) => { + println!("{} Android SDK already installed at: {}", + "✓".green(), + sdk_path.display() + ); + return Ok(()); + } + Err(_) => { + println!("{} Android SDK found but incomplete, reinstalling...", + "⚠".yellow() + ); + // Continue with installation + } + } } println!("{}", "Installing Android SDK...".bright_yellow()); - let url = if cfg!(target_os = "macos") { + let url = if cfg!(target_os = "windows") { + ANDROID_SDK_URL_WINDOWS + } else if cfg!(target_os = "macos") { ANDROID_SDK_URL_MAC } else { ANDROID_SDK_URL_LINUX @@ -58,11 +72,37 @@ pub fn install(sdk_path: &Path) -> Result<()> { let mut archive = zip::ZipArchive::new(file)?; std::fs::create_dir_all(sdk_path)?; - archive.extract(sdk_path)?; + archive.extract(sdk_path) + .context("Failed to extract Android SDK")?; // Cleanup std::fs::remove_file(&temp_zip)?; + // The zip extracts to cmdline-tools/ but we need it in cmdline-tools/latest/ + let extracted_tools = sdk_path.join("cmdline-tools"); + let target_location = sdk_path.join("cmdline-tools").join("latest"); + + if extracted_tools.exists() && !target_location.exists() { + println!("Reorganizing cmdline-tools directory structure..."); + + let temp_dir = sdk_path.join("cmdline-tools-temp"); + std::fs::rename(&extracted_tools, &temp_dir) + .context("Failed to rename cmdline-tools to temp directory")?; + + std::fs::create_dir_all(&target_location) + .context("Failed to create cmdline-tools/latest directory")?; + + for entry in std::fs::read_dir(&temp_dir)? { + let entry = entry?; + let dest = target_location.join(entry.file_name()); + std::fs::rename(entry.path(), dest) + .with_context(|| format!("Failed to move {} to latest/", entry.file_name().to_string_lossy()))?; + } + + std::fs::remove_dir_all(&temp_dir) + .context("Failed to remove temporary directory")?; + } + // Install required packages install_packages(sdk_path)?; @@ -74,68 +114,120 @@ pub fn install(sdk_path: &Path) -> Result<()> { fn install_packages(sdk_path: &Path) -> Result<()> { println!("Installing Android SDK packages..."); - let sdkmanager = sdk_path - .join("cmdline-tools/bin/sdkmanager"); + let sdkmanager_path = sdk_path.join("cmdline-tools").join("latest").join("bin"); + + let sdkmanager = if cfg!(target_os = "windows") { + sdkmanager_path.join("sdkmanager.bat") + } else { + sdkmanager_path.join("sdkmanager") + }; if !sdkmanager.exists() { - // Try alternate location - let alt = sdk_path.join("cmdline-tools/latest/bin/sdkmanager"); - if alt.exists() { - return run_sdkmanager(&alt, sdk_path); - } - - // Need to move cmdline-tools to correct location - let from = sdk_path.join("cmdline-tools"); - let to = sdk_path.join("cmdline-tools/latest"); - if from.exists() { - std::fs::create_dir_all(sdk_path.join("cmdline-tools"))?; - std::fs::rename(&from, &to)?; - return run_sdkmanager(&to.join("bin/sdkmanager"), sdk_path); - } + anyhow::bail!( + "sdkmanager not found at expected location: {}\n\ + Directory structure may be incorrect.", + sdkmanager.display() + ); } + println!("Found sdkmanager at: {}", sdkmanager.display()); + run_sdkmanager(&sdkmanager, sdk_path) } fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> { - use std::process::Command; + use std::process::{Command, Stdio}; use std::io::Write; - // Accept licenses - let mut yes_cmd = Command::new("yes"); - let yes_output = yes_cmd.output()?; + println!("Accepting licenses..."); - let mut cmd = Command::new(sdkmanager); - cmd.arg("--sdk_root") - .arg(sdk_root) + // Build command based on OS + let mut cmd = if cfg!(target_os = "windows") { + let mut c = Command::new("cmd"); + c.arg("/c"); + c.arg(sdkmanager); + c + } else { + Command::new(sdkmanager) + }; + + cmd.arg(format!("--sdk_root={}", sdk_root.display())) .arg("--licenses") - .stdin(std::process::Stdio::piped()) - .spawn()? - .stdin - .as_mut() - .unwrap() - .write_all(&yes_output.stdout)?; + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); - // Install packages - Command::new(sdkmanager) - .arg("--sdk_root") - .arg(sdk_root) + let mut child = cmd.spawn() + .context("Failed to spawn sdkmanager for licenses")?; + + // Write 'y' responses to accept all licenses + if let Some(mut stdin) = child.stdin.take() { + // Create a string with many 'y' responses + let responses = "y\n".repeat(20); + stdin.write_all(responses.as_bytes()) + .context("Failed to write license responses")?; + // Explicitly drop stdin to close the pipe + drop(stdin); + } + + let output = child.wait_with_output() + .context("Failed to wait for license acceptance")?; + + if !output.status.success() { + eprintln!("License stderr: {}", String::from_utf8_lossy(&output.stderr)); + eprintln!("License stdout: {}", String::from_utf8_lossy(&output.stdout)); + println!("{} License acceptance may have failed, continuing anyway...", "⚠".yellow()); + } else { + println!("{} Licenses accepted", "✓".green()); + } + + println!("Installing SDK packages (this may take a few minutes)..."); + + // Build command for package installation + let mut cmd = if cfg!(target_os = "windows") { + let mut c = Command::new("cmd"); + c.arg("/c"); + c.arg(sdkmanager); + c + } else { + Command::new(sdkmanager) + }; + + let status = cmd + .arg(format!("--sdk_root={}", sdk_root.display())) .arg("platform-tools") .arg("platforms;android-34") .arg("build-tools;34.0.0") - .status()?; + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .status() + .context("Failed to run sdkmanager for package installation")?; + + if !status.success() { + anyhow::bail!("Failed to install Android SDK packages"); + } Ok(()) } pub fn verify(sdk_path: &Path) -> Result<()> { if !sdk_path.exists() { - anyhow::bail!("Android SDK not found at: {}", sdk_path.display()); + anyhow::bail!( + "Android SDK not found at: {}\n\ + Run 'weevil sdk install' to download it automatically,\n\ + or install manually from: https://developer.android.com/studio#command-tools", + sdk_path.display() + ); } let platform_tools = sdk_path.join("platform-tools"); if !platform_tools.exists() { - anyhow::bail!("Android SDK incomplete: platform-tools not found"); + anyhow::bail!( + "Android SDK incomplete: platform-tools not found\n\ + Expected at: {}\n\ + Run 'weevil sdk install' to complete the installation", + platform_tools.display() + ); } Ok(()) diff --git a/src/sdk/ftc.rs b/src/sdk/ftc.rs index 4dcb949..778cece 100644 --- a/src/sdk/ftc.rs +++ b/src/sdk/ftc.rs @@ -2,16 +2,19 @@ use std::path::Path; use anyhow::{Result, Context}; use git2::Repository; use colored::*; +use std::fs; 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) -> Result<()> { +pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> { if sdk_path.exists() { println!("{} FTC SDK already installed at: {}", "✓".green(), sdk_path.display() ); + // Make sure local.properties exists even if SDK was already installed + create_local_properties(sdk_path, android_sdk_path)?; return check_version(sdk_path); } @@ -28,11 +31,32 @@ pub fn install(sdk_path: &Path) -> Result<()> { repo.checkout_tree(&obj, None)?; repo.set_head_detached(obj.id())?; + // Create local.properties with Android SDK path + create_local_properties(sdk_path, android_sdk_path)?; + println!("{} FTC SDK installed successfully", "✓".green()); Ok(()) } +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 + .display() + .to_string() + .replace("\\", "/"); + + let local_properties = format!("sdk.dir={}\n", android_sdk_str); + + let properties_path = sdk_path.join("local.properties"); + fs::write(&properties_path, local_properties) + .context("Failed to create local.properties")?; + + println!("{} Created local.properties with Android SDK path", "✓".green()); + + Ok(()) +} + fn check_version(sdk_path: &Path) -> Result<()> { let repo = Repository::open(sdk_path)?;