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

112
src/sdk/ftc.rs Normal file
View File

@@ -0,0 +1,112 @@
use std::path::Path;
use anyhow::{Result, Context};
use git2::Repository;
use colored::*;
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<()> {
if sdk_path.exists() {
println!("{} FTC SDK already installed at: {}",
"".green(),
sdk_path.display()
);
return check_version(sdk_path);
}
println!("{}", "Installing FTC SDK...".bright_yellow());
println!("Cloning from: {}", FTC_SDK_URL);
println!("Version: {}", FTC_SDK_VERSION);
// Clone the repository
let repo = Repository::clone(FTC_SDK_URL, sdk_path)
.context("Failed to clone FTC SDK")?;
// Checkout specific version
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
repo.checkout_tree(&obj, None)?;
repo.set_head_detached(obj.id())?;
println!("{} FTC SDK installed successfully", "".green());
Ok(())
}
fn check_version(sdk_path: &Path) -> Result<()> {
let repo = Repository::open(sdk_path)?;
// Get current HEAD
let head = repo.head()?;
let commit = head.peel_to_commit()?;
// Try to find a tag for this commit
let tag_names = repo.tag_names(None)?;
let tags: Vec<_> = tag_names
.iter()
.filter_map(|t| t)
.collect();
println!("Current version: {}",
tags.first()
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{}", commit.id()))
);
Ok(())
}
pub fn update(sdk_path: &Path) -> Result<()> {
println!("{}", "Updating FTC SDK...".bright_yellow());
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)?;
// Checkout latest version
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
repo.checkout_tree(&obj, None)?;
repo.set_head_detached(obj.id())?;
println!("{} FTC SDK updated to {}", "".green(), FTC_SDK_VERSION);
Ok(())
}
pub fn verify(sdk_path: &Path) -> Result<()> {
if !sdk_path.exists() {
anyhow::bail!("FTC SDK not found at: {}", sdk_path.display());
}
// Check for essential directories
let team_code = sdk_path.join("TeamCode");
let ftc_robot_controller = sdk_path.join("FtcRobotController");
if !team_code.exists() || !ftc_robot_controller.exists() {
anyhow::bail!("FTC SDK incomplete: missing essential directories");
}
Ok(())
}
pub fn get_version(sdk_path: &Path) -> Result<String> {
let repo = Repository::open(sdk_path)?;
let head = repo.head()?;
let commit = head.peel_to_commit()?;
// Try to find a tag
let tags = repo.tag_names(None)?;
for tag in tags.iter().flatten() {
let tag_ref = repo.find_reference(&format!("refs/tags/{}", tag))?;
if let Ok(tag_commit) = tag_ref.peel_to_commit() {
if tag_commit.id() == commit.id() {
return Ok(tag.to_string());
}
}
}
Ok(format!("commit-{}", &commit.id().to_string()[..8]))
}