feat: Weevil v1.0.0-beta1 - FTC Project Generator

Cross-platform tool for generating clean, testable FTC robot projects
without editing the SDK installation.

Features:
- Standalone project generation with proper separation from SDK
- Per-project SDK configuration via .weevil.toml
- Local unit testing support (no robot required)
- Cross-platform build/deploy scripts (Linux/macOS/Windows)
- Project upgrade system preserving user code
- Configuration management commands
- Comprehensive test suite (11 passing tests)
- Zero-warning builds

Architecture:
- Pure Rust implementation with embedded Gradle wrapper
- Projects use deployToSDK task to copy code to FTC SDK TeamCode
- Git-ready projects with automatic initialization
- USB and WiFi deployment with auto-detection

Commands:
- weevil new <name> - Create new project
- weevil upgrade <path> - Update project infrastructure
- weevil config <path> - View/modify project configuration
- weevil sdk status/install/update - Manage SDKs

Addresses the core problem: FTC's SDK structure forces students to
edit framework internals instead of separating concerns like industry
standard practices. Weevil enables proper software engineering workflows
for robotics education.
This commit is contained in:
Eric Ratliff
2026-01-24 15:20:18 -06:00
commit 70a1acc2a1
35 changed files with 3558 additions and 0 deletions

103
src/sdk/mod.rs Normal file
View File

@@ -0,0 +1,103 @@
use std::path::{Path, PathBuf};
use anyhow::{Result, Context, bail};
use std::fs;
use colored::*;
pub mod android;
pub mod ftc;
pub mod gradle;
pub struct SdkConfig {
pub ftc_sdk_path: PathBuf,
pub android_sdk_path: PathBuf,
pub cache_dir: PathBuf,
}
impl SdkConfig {
pub fn new() -> Result<Self> {
let home = dirs::home_dir()
.context("Could not determine home directory")?;
let cache_dir = home.join(".weevil");
fs::create_dir_all(&cache_dir)?;
Ok(Self {
ftc_sdk_path: cache_dir.join("ftc-sdk"),
android_sdk_path: Self::find_android_sdk().unwrap_or_else(|| cache_dir.join("android-sdk")),
cache_dir,
})
}
pub fn with_paths(ftc_sdk: Option<&str>, android_sdk: Option<&str>) -> Result<Self> {
let mut config = Self::new()?;
if let Some(path) = ftc_sdk {
config.ftc_sdk_path = PathBuf::from(path);
}
if let Some(path) = android_sdk {
config.android_sdk_path = PathBuf::from(path);
}
Ok(config)
}
fn find_android_sdk() -> Option<PathBuf> {
// Check common locations
let home = dirs::home_dir()?;
let candidates = vec![
home.join("Android/Sdk"),
home.join(".android-sdk"),
PathBuf::from("/usr/lib/android-sdk"),
];
for candidate in candidates {
if candidate.join("platform-tools").exists() {
return Some(candidate);
}
}
None
}
#[allow(dead_code)]
pub fn validate(&self) -> Result<()> {
if !self.ftc_sdk_path.exists() {
bail!(
"FTC SDK not found at: {}\nRun: weevil sdk install",
self.ftc_sdk_path.display()
);
}
if !self.android_sdk_path.exists() {
bail!(
"Android SDK not found at: {}\nRun: weevil sdk install",
self.android_sdk_path.display()
);
}
Ok(())
}
pub fn print_status(&self) {
println!("{}", "SDK Configuration:".bright_yellow().bold());
println!();
self.print_sdk_status("FTC SDK", &self.ftc_sdk_path);
self.print_sdk_status("Android SDK", &self.android_sdk_path);
println!();
println!("{}: {}", "Cache Directory".bright_yellow(), self.cache_dir.display());
}
fn print_sdk_status(&self, name: &str, path: &Path) {
let status = if path.exists() {
format!("{} {}", "".green(), path.display())
} else {
format!("{} {} {}", "".red(), path.display(), "(not found)".red())
};
println!("{}: {}", name.bright_yellow(), status);
}
}