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.
101 lines
2.7 KiB
Rust
101 lines
2.7 KiB
Rust
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();
|
|
}
|
|
} |