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

46
src/commands/config.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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(())
}

7
src/lib.rs Normal file
View File

@@ -0,0 +1,7 @@
// File: src/lib.rs
// Library interface for testing
pub mod sdk;
pub mod project;
pub mod commands;
pub mod templates;

127
src/main.rs Normal file
View File

@@ -0,0 +1,127 @@
use clap::{Parser, Subcommand};
use colored::*;
use anyhow::Result;
mod commands;
mod sdk;
mod project;
mod templates;
#[derive(Parser)]
#[command(name = "weevil")]
#[command(author = "Eric Barch <eric@intrepidfusion.com>")]
#[command(version = "1.0.0")]
#[command(about = "FTC robotics project generator - bores into complexity, emerges with clean code", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Create a new FTC robot project
New {
/// Name of the robot project
name: String,
/// Path to FTC SDK (optional, will auto-detect or download)
#[arg(long)]
ftc_sdk: Option<String>,
/// Path to Android SDK (optional, will auto-detect or download)
#[arg(long)]
android_sdk: Option<String>,
},
/// Upgrade an existing project to the latest generator version
Upgrade {
/// Path to the project directory
path: String,
},
/// Build and deploy project to Control Hub
Deploy {
/// Path to the project directory
path: String,
/// Force USB connection
#[arg(long)]
usb: bool,
/// Force WiFi connection
#[arg(long)]
wifi: bool,
/// Custom IP address
#[arg(short, long)]
ip: Option<String>,
},
/// Manage SDKs (FTC and Android)
Sdk {
#[command(subcommand)]
command: SdkCommands,
},
/// Show or update project configuration
Config {
/// Path to the project directory
path: String,
/// Set FTC SDK path for this project
#[arg(long, value_name = "PATH")]
set_sdk: Option<String>,
},
}
#[derive(Subcommand)]
enum SdkCommands {
/// Install required SDKs
Install,
/// Show SDK status and locations
Status,
/// Update SDKs to latest versions
Update,
}
fn main() -> Result<()> {
let cli = Cli::parse();
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::Upgrade { path } => {
commands::upgrade::upgrade_project(&path)
}
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::Config { path, set_sdk } => {
if let Some(sdk_path) = set_sdk {
commands::config::set_sdk(&path, &sdk_path)
} else {
commands::config::show_config(&path)
}
}
}
}
fn print_banner() {
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " 🪲 Weevil - FTC Project Generator v1.0.0".bright_cyan().bold());
println!("{}", " Nexus Workshops LLC".bright_cyan());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
}

82
src/project/config.rs Normal file
View File

@@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::fs;
use anyhow::{Result, Context, bail};
#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectConfig {
pub project_name: String,
pub weevil_version: String,
pub ftc_sdk_path: PathBuf,
pub ftc_sdk_version: String,
}
impl ProjectConfig {
pub fn new(project_name: &str, ftc_sdk_path: PathBuf) -> Result<Self> {
let ftc_sdk_version = crate::sdk::ftc::get_version(&ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string());
Ok(Self {
project_name: project_name.to_string(),
weevil_version: "1.0.0".to_string(),
ftc_sdk_path,
ftc_sdk_version,
})
}
pub fn load(project_path: &Path) -> Result<Self> {
let config_path = project_path.join(".weevil.toml");
if !config_path.exists() {
bail!("Not a weevil project (missing .weevil.toml)");
}
let contents = fs::read_to_string(&config_path)
.context("Failed to read .weevil.toml")?;
let config: ProjectConfig = toml::from_str(&contents)
.context("Failed to parse .weevil.toml")?;
Ok(config)
}
pub fn save(&self, project_path: &Path) -> Result<()> {
let config_path = project_path.join(".weevil.toml");
let contents = toml::to_string_pretty(self)
.context("Failed to serialize config")?;
fs::write(&config_path, contents)
.context("Failed to write .weevil.toml")?;
Ok(())
}
pub fn update_sdk_path(&mut self, new_path: PathBuf) -> Result<()> {
// Verify the SDK exists
crate::sdk::ftc::verify(&new_path)?;
// Update version
self.ftc_sdk_version = crate::sdk::ftc::get_version(&new_path)
.unwrap_or_else(|_| "unknown".to_string());
self.ftc_sdk_path = new_path;
Ok(())
}
pub fn display(&self) {
use colored::*;
println!();
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!("{}", " Project Configuration".bright_cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
println!();
println!("{:.<20} {}", "Project Name", self.project_name.bright_white());
println!("{:.<20} {}", "Weevil Version", self.weevil_version.bright_white());
println!();
println!("{:.<20} {}", "FTC SDK Path", self.ftc_sdk_path.display().to_string().bright_white());
println!("{:.<20} {}", "FTC SDK Version", self.ftc_sdk_version.bright_white());
println!();
}
}

19
src/project/deployer.rs Normal file
View File

@@ -0,0 +1,19 @@
use anyhow::Result;
#[allow(dead_code)]
pub struct Deployer {
// Future: ADB communication, APK building, etc.
}
impl Deployer {
#[allow(dead_code)]
pub fn new() -> Self {
Self {}
}
#[allow(dead_code)]
pub fn deploy(&self) -> Result<()> {
// Coming soon!
Ok(())
}
}

455
src/project/mod.rs Normal file
View File

@@ -0,0 +1,455 @@
use std::path::Path;
use anyhow::{Result, Context};
use std::fs;
use colored::*;
use tera::Context as TeraContext;
use git2::Repository;
use crate::sdk::SdkConfig;
pub mod deployer;
pub mod config;
pub use config::ProjectConfig;
pub struct ProjectBuilder {
name: String,
}
impl ProjectBuilder {
pub fn new(name: &str, _sdk_config: &SdkConfig) -> Result<Self> {
Ok(Self {
name: name.to_string(),
})
}
pub fn create(&self, project_path: &Path, sdk_config: &SdkConfig) -> Result<()> {
// Create directory structure
self.create_directories(project_path)?;
// Generate files from templates
self.generate_files(project_path, sdk_config)?;
// Setup Gradle wrapper
self.setup_gradle(project_path)?;
// Initialize git repository
self.init_git(project_path)?;
// Make scripts executable
self.make_executable(project_path)?;
println!("{} Project structure created", "".green());
Ok(())
}
fn create_directories(&self, project_path: &Path) -> Result<()> {
let dirs = vec![
"src/main/java/robot",
"src/main/java/robot/subsystems",
"src/main/java/robot/hardware",
"src/main/java/robot/opmodes",
"src/test/java/robot",
"src/test/java/robot/subsystems",
"gradle/wrapper",
];
for dir in dirs {
let full_path = project_path.join(dir);
fs::create_dir_all(&full_path)
.context(format!("Failed to create directory: {}", dir))?;
}
Ok(())
}
fn generate_files(&self, project_path: &Path, sdk_config: &SdkConfig) -> Result<()> {
let mut _context = TeraContext::new();
_context.insert("project_name", &self.name);
_context.insert("sdk_dir", &sdk_config.ftc_sdk_path.to_string_lossy());
_context.insert("generator_version", "1.0.0");
self.create_project_files(project_path, sdk_config)?;
Ok(())
}
fn create_project_files(&self, project_path: &Path, sdk_config: &SdkConfig) -> Result<()> {
// Create .weevil.toml config
let project_config = ProjectConfig::new(&self.name, sdk_config.ftc_sdk_path.clone())?;
project_config.save(project_path)?;
// README.md
let readme = format!(
r#"# {}
FTC Robot Project generated by Weevil v1.0.0
## Quick Start
```bash
# Test your code (runs on PC, no robot needed)
./gradlew test
# Build and deploy (Linux/Mac)
./build.sh
./deploy.sh
# Build and deploy (Windows)
build.bat
deploy.bat
```
## Project Structure
- `src/main/java/robot/` - Your robot code
- `src/test/java/robot/` - Unit tests (run on PC)
## Development Workflow
1. Write code in `src/main/java/robot/`
2. Test locally: `./gradlew test`
3. Deploy: `./deploy.sh` (or `deploy.bat` on Windows)
"#,
self.name
);
fs::write(project_path.join("README.md"), readme)?;
// .gitignore
let gitignore = "build/\n.gradle/\n*.iml\n.idea/\nlocal.properties\n*.apk\n*.aab\n";
fs::write(project_path.join(".gitignore"), gitignore)?;
// Version marker
fs::write(project_path.join(".weevil-version"), "1.0.0")?;
// build.gradle.kts - Pure Java with deployToSDK task
let build_gradle = format!(r#"plugins {{
java
}}
repositories {{
mavenCentral()
google()
}}
dependencies {{
// Testing (runs on PC without SDK)
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.mockito:mockito-core:5.5.0")
}}
java {{
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}}
tasks.test {{
useJUnitPlatform()
testLogging {{
events("passed", "skipped", "failed")
showStandardStreams = false
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
}}
}}
// Task to deploy code to FTC SDK
tasks.register<Copy>("deployToSDK") {{
group = "ftc"
description = "Copy code to FTC SDK TeamCode for deployment"
val sdkDir = "{}"
from("src/main/java") {{
include("robot/**/*.java")
}}
into(layout.projectDirectory.dir("$sdkDir/TeamCode/src/main/java"))
doLast {{
println("✓ Code deployed to TeamCode")
}}
}}
// Task to build APK
tasks.register<Exec>("buildApk") {{
group = "ftc"
description = "Build APK using FTC SDK"
dependsOn("deployToSDK")
val sdkDir = "{}"
workingDir = file(sdkDir)
commandLine = if (System.getProperty("os.name").lowercase().contains("windows")) {{
listOf("cmd", "/c", "gradlew.bat", "assembleDebug")
}} else {{
listOf("./gradlew", "assembleDebug")
}}
doLast {{
println("✓ APK built successfully")
}}
}}
"#, sdk_config.ftc_sdk_path.display(), sdk_config.ftc_sdk_path.display());
fs::write(project_path.join("build.gradle.kts"), build_gradle)?;
// settings.gradle.kts
let settings_gradle = format!("rootProject.name = \"{}\"\n", self.name);
fs::write(project_path.join("settings.gradle.kts"), settings_gradle)?;
// build.sh (Linux/Mac)
let build_sh = r#"#!/bin/bash
set -e
# Read SDK path from config
SDK_DIR=$(grep '^ftc_sdk_path' .weevil.toml | sed 's/.*= "\(.*\)"/\1/')
if [ -z "$SDK_DIR" ]; then
echo "Error: Could not read FTC SDK path from .weevil.toml"
exit 1
fi
echo "Building project..."
echo "Using FTC SDK: $SDK_DIR"
./gradlew buildApk
echo ""
echo "✓ Build complete!"
echo ""
APK=$(find "$SDK_DIR" -path "*/outputs/apk/debug/*.apk" 2>/dev/null | head -1)
if [ -n "$APK" ]; then
echo "APK: $APK"
fi
"#;
let build_sh_path = project_path.join("build.sh");
fs::write(&build_sh_path, build_sh)?;
// build.bat (Windows)
let build_bat = r#"@echo off
setlocal enabledelayedexpansion
REM Read SDK path from config
for /f "tokens=2 delims==" %%a in ('findstr /c:"ftc_sdk_path" .weevil.toml') do (
set SDK_DIR=%%a
set SDK_DIR=!SDK_DIR:"=!
set SDK_DIR=!SDK_DIR: =!
)
if not defined SDK_DIR (
echo Error: Could not read FTC SDK path from .weevil.toml
exit /b 1
)
echo Building project...
echo Using FTC SDK: %SDK_DIR%
call gradlew.bat buildApk
echo.
echo Build complete!
"#;
fs::write(project_path.join("build.bat"), build_bat)?;
// deploy.sh with all the flags
let deploy_sh = r#"#!/bin/bash
set -e
# Read SDK path from config
SDK_DIR=$(grep '^ftc_sdk_path' .weevil.toml | sed 's/.*= "\(.*\)"/\1/')
if [ -z "$SDK_DIR" ]; then
echo "Error: Could not read FTC SDK path from .weevil.toml"
exit 1
fi
# Parse arguments
USE_USB=false
USE_WIFI=false
CUSTOM_IP=""
WIFI_TIMEOUT=5
while [[ $# -gt 0 ]]; do
case $1 in
--usb) USE_USB=true; shift ;;
--wifi) USE_WIFI=true; shift ;;
-i|--ip) CUSTOM_IP="$2"; USE_WIFI=true; shift 2 ;;
--timeout) WIFI_TIMEOUT="$2"; shift 2 ;;
*) echo "Unknown option: $1"; echo "Usage: $0 [--usb|--wifi] [-i IP] [--timeout SECONDS]"; exit 1 ;;
esac
done
echo "Building APK..."
./gradlew buildApk
echo ""
echo "Deploying to Control Hub..."
# Check for adb
if ! command -v adb &> /dev/null; then
echo "Error: adb not found. Install Android SDK platform-tools."
exit 1
fi
# Find the APK in FTC SDK
APK=$(find "$SDK_DIR" -path "*/outputs/apk/debug/*.apk" | head -1)
if [ -z "$APK" ]; then
echo "Error: APK not found in $SDK_DIR"
exit 1
fi
# Connection logic
if [ "$USE_USB" = true ]; then
echo "Using USB..."
adb devices
elif [ "$USE_WIFI" = true ]; then
TARGET_IP="${CUSTOM_IP:-192.168.43.1}"
echo "Connecting to $TARGET_IP..."
timeout ${WIFI_TIMEOUT}s adb connect "$TARGET_IP:5555" || {
echo "Failed to connect"
exit 1
}
else
# Auto-detect
if adb devices | grep -q "device$"; then
echo "Using USB (auto-detected)..."
else
echo "Trying WiFi..."
timeout ${WIFI_TIMEOUT}s adb connect "192.168.43.1:5555" || {
echo "No devices found"
exit 1
}
fi
fi
echo "Installing: $APK"
adb install -r "$APK"
echo ""
echo "✓ Deployed!"
"#;
fs::write(project_path.join("deploy.sh"), deploy_sh)?;
// deploy.bat
let deploy_bat = r#"@echo off
setlocal enabledelayedexpansion
REM Read SDK path from config
for /f "tokens=2 delims==" %%a in ('findstr /c:"ftc_sdk_path" .weevil.toml') do (
set SDK_DIR=%%a
set SDK_DIR=!SDK_DIR:"=!
set SDK_DIR=!SDK_DIR: =!
)
if not defined SDK_DIR (
echo Error: Could not read FTC SDK path from .weevil.toml
exit /b 1
)
echo Building APK...
call gradlew.bat buildApk
echo.
echo Deploying to Control Hub...
REM Find APK
for /f "delims=" %%i in ('dir /s /b "%SDK_DIR%\*app-debug.apk" 2^>nul') do set APK=%%i
if not defined APK (
echo Error: APK not found
exit /b 1
)
echo Installing: %APK%
adb install -r "%APK%"
echo.
echo Deployed!
"#;
fs::write(project_path.join("deploy.bat"), deploy_bat)?;
// Simple test file
let test_file = r#"package robot;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class BasicTest {
@Test
void testBasic() {
assertTrue(true, "Basic test should pass");
}
}
"#;
fs::write(
project_path.join("src/test/java/robot/BasicTest.java"),
test_file
)?;
Ok(())
}
fn setup_gradle(&self, project_path: &Path) -> Result<()> {
println!("Setting up Gradle wrapper...");
crate::sdk::gradle::setup_wrapper(project_path)?;
println!("{} Gradle wrapper configured", "".green());
Ok(())
}
fn init_git(&self, project_path: &Path) -> Result<()> {
println!("Initializing git repository...");
let repo = Repository::init(project_path)
.context("Failed to initialize git repository")?;
// Configure git
let mut config = repo.config()?;
// Only set if not already set globally
if config.get_string("user.email").is_err() {
config.set_str("user.email", "robot@example.com")?;
}
if config.get_string("user.name").is_err() {
config.set_str("user.name", "FTC Robot")?;
}
// Initial commit
let mut index = repo.index()?;
index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let signature = repo.signature()?;
repo.commit(
Some("HEAD"),
&signature,
&signature,
"Initial commit from Weevil",
&tree,
&[],
)?;
println!("{} Git repository initialized", "".green());
Ok(())
}
fn make_executable(&self, project_path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let scripts = vec!["gradlew", "build.sh", "deploy.sh"];
for script in scripts {
let path = project_path.join(script);
if path.exists() {
let mut perms = fs::metadata(&path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms)?;
}
}
}
Ok(())
}
}

142
src/sdk/android.rs Normal file
View File

@@ -0,0 +1,142 @@
use std::path::Path;
use anyhow::{Result, Context};
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::blocking::Client;
use std::fs::File;
use std::io::Write;
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";
pub fn install(sdk_path: &Path) -> Result<()> {
if sdk_path.exists() {
println!("{} Android SDK already installed at: {}",
"".green(),
sdk_path.display()
);
return Ok(());
}
println!("{}", "Installing Android SDK...".bright_yellow());
let url = if cfg!(target_os = "macos") {
ANDROID_SDK_URL_MAC
} else {
ANDROID_SDK_URL_LINUX
};
// Download
println!("Downloading from: {}", url);
let client = Client::new();
let response = client.get(url)
.send()
.context("Failed to download Android SDK")?;
let total_size = response.content_length().unwrap_or(0);
let pb = ProgressBar::new(total_size);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.progress_chars("#>-"),
);
let temp_zip = sdk_path.parent().unwrap().join("android-sdk-temp.zip");
let mut file = File::create(&temp_zip)?;
let content = response.bytes()?;
pb.inc(content.len() as u64);
file.write_all(&content)?;
pb.finish_with_message("Download complete");
// Extract
println!("Extracting...");
let file = File::open(&temp_zip)?;
let mut archive = zip::ZipArchive::new(file)?;
std::fs::create_dir_all(sdk_path)?;
archive.extract(sdk_path)?;
// Cleanup
std::fs::remove_file(&temp_zip)?;
// Install required packages
install_packages(sdk_path)?;
println!("{} Android SDK installed successfully", "".green());
Ok(())
}
fn install_packages(sdk_path: &Path) -> Result<()> {
println!("Installing Android SDK packages...");
let sdkmanager = sdk_path
.join("cmdline-tools/bin/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);
}
}
run_sdkmanager(&sdkmanager, sdk_path)
}
fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> {
use std::process::Command;
use std::io::Write;
// Accept licenses
let mut yes_cmd = Command::new("yes");
let yes_output = yes_cmd.output()?;
let mut cmd = Command::new(sdkmanager);
cmd.arg("--sdk_root")
.arg(sdk_root)
.arg("--licenses")
.stdin(std::process::Stdio::piped())
.spawn()?
.stdin
.as_mut()
.unwrap()
.write_all(&yes_output.stdout)?;
// Install packages
Command::new(sdkmanager)
.arg("--sdk_root")
.arg(sdk_root)
.arg("platform-tools")
.arg("platforms;android-34")
.arg("build-tools;34.0.0")
.status()?;
Ok(())
}
pub fn verify(sdk_path: &Path) -> Result<()> {
if !sdk_path.exists() {
anyhow::bail!("Android SDK not found at: {}", sdk_path.display());
}
let platform_tools = sdk_path.join("platform-tools");
if !platform_tools.exists() {
anyhow::bail!("Android SDK incomplete: platform-tools not found");
}
Ok(())
}

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]))
}

379
src/sdk/gradle.rs Normal file
View File

@@ -0,0 +1,379 @@
use std::path::Path;
use anyhow::Result;
use std::fs;
const GRADLE_VERSION: &str = "8.9";
// Gradle wrapper JAR is downloaded at build time by build.rs
// It's compiled into the binary here
const GRADLE_WRAPPER_JAR: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/gradle-wrapper.jar"));
pub fn setup_wrapper(project_path: &Path) -> Result<()> {
// Create gradle wrapper directory
let wrapper_dir = project_path.join("gradle/wrapper");
fs::create_dir_all(&wrapper_dir)?;
// Create gradle-wrapper.properties
let properties = format!(
r#"distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-{}-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
"#,
GRADLE_VERSION
);
fs::write(wrapper_dir.join("gradle-wrapper.properties"), properties)?;
// Write embedded gradle-wrapper.jar
fs::write(wrapper_dir.join("gradle-wrapper.jar"), GRADLE_WRAPPER_JAR)?;
// Create gradlew script
create_gradlew_script(project_path)?;
// Create gradlew.bat for Windows
create_gradlew_bat(project_path)?;
Ok(())
}
fn create_gradlew_script(project_path: &Path) -> Result<()> {
// Use the official Gradle wrapper script - exact copy from Gradle 8.9
let gradlew = r#"#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
"#;
let gradlew_path = project_path.join("gradlew");
fs::write(&gradlew_path, gradlew)?;
// Make executable on Unix
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&gradlew_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&gradlew_path, perms)?;
}
Ok(())
}
fn create_gradlew_bat(project_path: &Path) -> Result<()> {
let gradlew_bat = r#"@rem Gradle startup script for Windows
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
"#;
fs::write(project_path.join("gradlew.bat"), gradlew_bat)?;
Ok(())
}
#[allow(dead_code)]
pub fn verify_wrapper(project_path: &Path) -> bool {
project_path.join("gradlew").exists()
}

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);
}
}

101
src/templates/mod.rs Normal file
View File

@@ -0,0 +1,101 @@
use include_dir::{include_dir, Dir};
use std::path::Path;
use anyhow::{Result, Context};
use tera::{Tera, Context as TeraContext};
use std::fs;
// Embed all template files at compile time
static TEMPLATES_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates");
pub struct TemplateEngine {
#[allow(dead_code)]
tera: Tera,
}
impl TemplateEngine {
#[allow(dead_code)]
pub fn new() -> Result<Self> {
let mut tera = Tera::default();
// Load all templates from embedded directory
for file in TEMPLATES_DIR.files() {
let path = file.path().to_string_lossy();
let contents = file.contents_utf8()
.context("Template must be valid UTF-8")?;
tera.add_raw_template(&path, contents)?;
}
Ok(Self { tera })
}
#[allow(dead_code)]
pub fn render_to_file(
&self,
template_name: &str,
output_path: &Path,
context: &TeraContext,
) -> Result<()> {
let rendered = self.tera.render(template_name, context)?;
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(output_path, rendered)?;
Ok(())
}
#[allow(dead_code)]
pub fn extract_static_file(&self, template_path: &str, output_path: &Path) -> Result<()> {
let file = TEMPLATES_DIR
.get_file(template_path)
.context(format!("Template not found: {}", template_path))?;
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(output_path, file.contents())?;
Ok(())
}
#[allow(dead_code)]
pub fn list_templates(&self) -> Vec<String> {
TEMPLATES_DIR
.files()
.map(|f| f.path().to_string_lossy().to_string())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_template_engine_creation() {
let engine = TemplateEngine::new();
assert!(engine.is_ok());
}
#[test]
fn test_list_templates() {
let engine = TemplateEngine::new().unwrap();
let templates = engine.list_templates();
assert!(!templates.is_empty());
}
#[test]
fn test_render_template() {
let _engine = TemplateEngine::new().unwrap();
let temp = TempDir::new().unwrap();
let _output = temp.path().join("test.txt");
let mut context = TeraContext::new();
context.insert("project_name", "TestRobot");
// This will fail until we add templates, but shows the pattern
// engine.render_to_file("README.md", &output, &context).unwrap();
}
}