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:
46
src/commands/config.rs
Normal file
46
src/commands/config.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
use crate::project::ProjectConfig;
|
||||
|
||||
pub fn show_config(path: &str) -> Result<()> {
|
||||
let project_path = PathBuf::from(path);
|
||||
let config = ProjectConfig::load(&project_path)
|
||||
.context("Failed to load project config")?;
|
||||
|
||||
config.display();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_sdk(path: &str, sdk_path: &str) -> Result<()> {
|
||||
let project_path = PathBuf::from(path);
|
||||
let new_sdk_path = PathBuf::from(sdk_path);
|
||||
|
||||
println!("{}", "Updating FTC SDK path...".bright_yellow());
|
||||
println!();
|
||||
|
||||
// Load existing config
|
||||
let mut config = ProjectConfig::load(&project_path)
|
||||
.context("Failed to load project config")?;
|
||||
|
||||
println!("Current SDK: {}", config.ftc_sdk_path.display());
|
||||
println!("New SDK: {}", new_sdk_path.display());
|
||||
println!();
|
||||
|
||||
// Update and validate
|
||||
config.update_sdk_path(new_sdk_path.clone())
|
||||
.context("Failed to update SDK path")?;
|
||||
|
||||
// Save config
|
||||
config.save(&project_path)
|
||||
.context("Failed to save config")?;
|
||||
|
||||
println!("{} FTC SDK updated to: {}", "✓".green(), new_sdk_path.display());
|
||||
println!("{} Version: {}", "✓".green(), config.ftc_sdk_version);
|
||||
println!();
|
||||
println!("Note: Run {} to update build files", "weevil upgrade .".bright_cyan());
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
35
src/commands/deploy.rs
Normal file
35
src/commands/deploy.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
|
||||
pub fn deploy_project(
|
||||
path: &str,
|
||||
usb: bool,
|
||||
wifi: bool,
|
||||
ip: Option<&str>,
|
||||
) -> Result<()> {
|
||||
println!("{}", format!("Deploying project: {}", path).bright_yellow());
|
||||
println!();
|
||||
|
||||
if usb {
|
||||
println!("Mode: USB");
|
||||
} else if wifi {
|
||||
println!("Mode: WiFi");
|
||||
} else {
|
||||
println!("Mode: Auto-detect");
|
||||
}
|
||||
|
||||
if let Some(ip_addr) = ip {
|
||||
println!("Target IP: {}", ip_addr);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "⚠ Deploy functionality coming soon!".yellow());
|
||||
println!();
|
||||
println!("This will:");
|
||||
println!(" • Build the APK");
|
||||
println!(" • Detect Control Hub (USB or WiFi)");
|
||||
println!(" • Install to robot");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
5
src/commands/mod.rs
Normal file
5
src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod new;
|
||||
pub mod upgrade;
|
||||
pub mod deploy;
|
||||
pub mod sdk;
|
||||
pub mod config;
|
||||
90
src/commands/new.rs
Normal file
90
src/commands/new.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use anyhow::{Result, bail};
|
||||
use std::path::PathBuf;
|
||||
use colored::*;
|
||||
|
||||
use crate::sdk::SdkConfig;
|
||||
use crate::project::ProjectBuilder;
|
||||
|
||||
pub fn create_project(
|
||||
name: &str,
|
||||
ftc_sdk: Option<&str>,
|
||||
android_sdk: Option<&str>,
|
||||
) -> Result<()> {
|
||||
// Validate project name
|
||||
if name.is_empty() {
|
||||
bail!("Project name cannot be empty");
|
||||
}
|
||||
|
||||
if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
|
||||
bail!("Project name must contain only alphanumeric characters, hyphens, and underscores");
|
||||
}
|
||||
|
||||
let project_path = PathBuf::from(name);
|
||||
|
||||
// Check if project already exists
|
||||
if project_path.exists() {
|
||||
bail!(
|
||||
"{}\n\nDirectory already exists: {}\n\nTo upgrade: weevil upgrade {}",
|
||||
"Project Already Exists".red().bold(),
|
||||
project_path.display(),
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
println!("{}", format!("Creating FTC project: {}", name).bright_green().bold());
|
||||
println!();
|
||||
|
||||
// Setup or verify SDK configuration
|
||||
let sdk_config = SdkConfig::with_paths(ftc_sdk, android_sdk)?;
|
||||
|
||||
// Install SDKs if needed
|
||||
println!("{}", "Checking SDKs...".bright_yellow());
|
||||
ensure_sdks(&sdk_config)?;
|
||||
|
||||
println!();
|
||||
println!("{}", "Creating project structure...".bright_yellow());
|
||||
|
||||
// Build the project
|
||||
let builder = ProjectBuilder::new(name, &sdk_config)?;
|
||||
builder.create(&project_path, &sdk_config)?;
|
||||
|
||||
println!();
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!("{}", format!(" ✓ Project Created: {}", name).bright_green().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!();
|
||||
println!("FTC SDK: {}", sdk_config.ftc_sdk_path.display());
|
||||
println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string()));
|
||||
println!();
|
||||
println!("{}", "Next steps:".bright_yellow().bold());
|
||||
println!(" 1. cd {}", name);
|
||||
println!(" 2. Review README.md for project structure");
|
||||
println!(" 3. Start coding in src/main/java/robot/");
|
||||
println!(" 4. Run: ./gradlew test");
|
||||
println!(" 5. Deploy: weevil deploy {}", name);
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)?;
|
||||
} else {
|
||||
println!("{} FTC SDK found at: {}", "✓".green(), config.ftc_sdk_path.display());
|
||||
crate::sdk::ftc::verify(&config.ftc_sdk_path)?;
|
||||
}
|
||||
|
||||
// Check Android SDK
|
||||
if !config.android_sdk_path.exists() {
|
||||
println!("Android SDK not found. Installing...");
|
||||
crate::sdk::android::install(&config.android_sdk_path)?;
|
||||
} else {
|
||||
println!("{} Android SDK found at: {}", "✓".green(), config.android_sdk_path.display());
|
||||
crate::sdk::android::verify(&config.android_sdk_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
60
src/commands/sdk.rs
Normal file
60
src/commands/sdk.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use crate::sdk::SdkConfig;
|
||||
|
||||
pub fn install_sdks() -> Result<()> {
|
||||
println!("{}", "Installing SDKs...".bright_yellow().bold());
|
||||
println!();
|
||||
|
||||
let config = SdkConfig::new()?;
|
||||
|
||||
// Install FTC SDK
|
||||
crate::sdk::ftc::install(&config.ftc_sdk_path)?;
|
||||
|
||||
// Install Android SDK
|
||||
crate::sdk::android::install(&config.android_sdk_path)?;
|
||||
|
||||
println!();
|
||||
println!("{} All SDKs installed successfully", "✓".green().bold());
|
||||
config.print_status();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_status() -> Result<()> {
|
||||
let config = SdkConfig::new()?;
|
||||
config.print_status();
|
||||
|
||||
// Verify SDKs
|
||||
println!();
|
||||
println!("{}", "Verification:".bright_yellow().bold());
|
||||
|
||||
match crate::sdk::ftc::verify(&config.ftc_sdk_path) {
|
||||
Ok(_) => println!("{} FTC SDK is valid", "✓".green()),
|
||||
Err(e) => println!("{} FTC SDK: {}", "✗".red(), e),
|
||||
}
|
||||
|
||||
match crate::sdk::android::verify(&config.android_sdk_path) {
|
||||
Ok(_) => println!("{} Android SDK is valid", "✓".green()),
|
||||
Err(e) => println!("{} Android SDK: {}", "✗".red(), e),
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_sdks() -> Result<()> {
|
||||
println!("{}", "Updating SDKs...".bright_yellow().bold());
|
||||
println!();
|
||||
|
||||
let config = SdkConfig::new()?;
|
||||
|
||||
// Update FTC SDK
|
||||
crate::sdk::ftc::update(&config.ftc_sdk_path)?;
|
||||
|
||||
println!();
|
||||
println!("{} SDKs updated successfully", "✓".green().bold());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
119
src/commands/upgrade.rs
Normal file
119
src/commands/upgrade.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use anyhow::{Result, bail};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
pub fn upgrade_project(path: &str) -> Result<()> {
|
||||
let project_path = PathBuf::from(path);
|
||||
|
||||
// Verify it's a weevil project (check for old .weevil-version or new .weevil.toml)
|
||||
let has_old_version = project_path.join(".weevil-version").exists();
|
||||
let has_config = project_path.join(".weevil.toml").exists();
|
||||
|
||||
if !has_old_version && !has_config {
|
||||
bail!("Not a weevil project: {} (missing .weevil-version or .weevil.toml)", path);
|
||||
}
|
||||
|
||||
println!("{}", format!("Upgrading project: {}", path).bright_yellow());
|
||||
println!();
|
||||
|
||||
// Get SDK config
|
||||
let sdk_config = crate::sdk::SdkConfig::new()?;
|
||||
|
||||
// Load or create project config
|
||||
let project_config = if has_config {
|
||||
println!("Found existing .weevil.toml");
|
||||
crate::project::ProjectConfig::load(&project_path)?
|
||||
} else {
|
||||
println!("Creating .weevil.toml (migrating from old format)");
|
||||
let project_name = project_path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("unknown");
|
||||
crate::project::ProjectConfig::new(project_name, sdk_config.ftc_sdk_path.clone())?
|
||||
};
|
||||
|
||||
println!("Current SDK: {}", project_config.ftc_sdk_path.display());
|
||||
println!("SDK Version: {}", project_config.ftc_sdk_version);
|
||||
println!();
|
||||
|
||||
// Files that are safe to overwrite (infrastructure, not user code)
|
||||
let safe_to_overwrite = vec![
|
||||
"build.gradle.kts",
|
||||
"settings.gradle.kts",
|
||||
"build.sh",
|
||||
"build.bat",
|
||||
"deploy.sh",
|
||||
"deploy.bat",
|
||||
"gradlew",
|
||||
"gradlew.bat",
|
||||
"gradle/wrapper/gradle-wrapper.properties",
|
||||
"gradle/wrapper/gradle-wrapper.jar",
|
||||
".gitignore",
|
||||
];
|
||||
|
||||
println!("{}", "Updating infrastructure files...".bright_yellow());
|
||||
|
||||
// Create a modified SDK config that uses the project's configured FTC SDK
|
||||
let project_sdk_config = crate::sdk::SdkConfig {
|
||||
ftc_sdk_path: project_config.ftc_sdk_path.clone(),
|
||||
android_sdk_path: sdk_config.android_sdk_path.clone(),
|
||||
cache_dir: sdk_config.cache_dir.clone(),
|
||||
};
|
||||
|
||||
// Regenerate safe files using the project's configured SDK
|
||||
let builder = crate::project::ProjectBuilder::new(
|
||||
&project_config.project_name,
|
||||
&project_sdk_config
|
||||
)?;
|
||||
|
||||
// Temporarily create files in a temp location
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
builder.create(temp_dir.path(), &project_sdk_config)?;
|
||||
|
||||
// Copy only the safe files
|
||||
for file in safe_to_overwrite {
|
||||
let src = temp_dir.path().join(file);
|
||||
let dst = project_path.join(file);
|
||||
|
||||
if src.exists() {
|
||||
if let Some(parent) = dst.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(&src, &dst)?;
|
||||
println!(" {} {}", "✓".green(), file);
|
||||
|
||||
// Make executable if needed
|
||||
#[cfg(unix)]
|
||||
if file.ends_with(".sh") || file == "gradlew" {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&dst)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&dst, perms)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save/update the config
|
||||
project_config.save(&project_path)?;
|
||||
println!(" {} {}", "✓".green(), ".weevil.toml");
|
||||
|
||||
// Remove old version marker if it exists
|
||||
let old_version_file = project_path.join(".weevil-version");
|
||||
if old_version_file.exists() {
|
||||
fs::remove_file(old_version_file)?;
|
||||
println!(" {} {}", "✓".green(), "Removed old .weevil-version");
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!("{}", " ✓ Project Upgraded!".bright_green().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!();
|
||||
println!("{}", "Your code in src/ was preserved.".green());
|
||||
println!("{}", "Build scripts and gradle configuration updated.".green());
|
||||
println!();
|
||||
println!("Test it: ./gradlew test");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user