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(())
|
||||
}
|
||||
7
src/lib.rs
Normal file
7
src/lib.rs
Normal 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
127
src/main.rs
Normal 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
82
src/project/config.rs
Normal 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
19
src/project/deployer.rs
Normal 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
455
src/project/mod.rs
Normal 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
142
src/sdk/android.rs
Normal 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
112
src/sdk/ftc.rs
Normal 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
379
src/sdk/gradle.rs
Normal 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
103
src/sdk/mod.rs
Normal 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
101
src/templates/mod.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user