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:
123
build.rs
Normal file
123
build.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
// build.rs - Download and merge gradle-wrapper jars at build time
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
use std::io::{Read, Write, Cursor};
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||
let gradle_wrapper_path = out_dir.join("gradle-wrapper.jar");
|
||||
|
||||
// Check if gradle-wrapper.jar already exists in resources/ (manually prepared)
|
||||
if let Ok(content) = fs::read("resources/gradle-wrapper.jar") {
|
||||
eprintln!("Using existing resources/gradle-wrapper.jar");
|
||||
fs::write(&gradle_wrapper_path, content)
|
||||
.expect("Failed to copy gradle-wrapper.jar");
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, download and merge at build time
|
||||
eprintln!("Downloading and merging gradle-wrapper jars...");
|
||||
|
||||
if let Err(e) = download_and_merge_wrapper(&gradle_wrapper_path) {
|
||||
eprintln!("\nError downloading gradle-wrapper: {}\n", e);
|
||||
eprintln!("You can manually create resources/gradle-wrapper.jar and rebuild.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
eprintln!("✓ gradle-wrapper.jar ready");
|
||||
}
|
||||
|
||||
fn download_and_merge_wrapper(output_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::io::Cursor;
|
||||
|
||||
let url = "https://services.gradle.org/distributions/gradle-8.9-bin.zip";
|
||||
|
||||
// Download the distribution
|
||||
let response = ureq::get(url)
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.call()?;
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
response.into_reader().read_to_end(&mut bytes)?;
|
||||
|
||||
// Open the zip
|
||||
let cursor = Cursor::new(bytes);
|
||||
let mut archive = zip::ZipArchive::new(cursor)?;
|
||||
|
||||
// Extract ALL jars from lib/ (except sources)
|
||||
let mut all_jar_bytes = Vec::new();
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let file = archive.by_index(i)?;
|
||||
let name = file.name().to_string();
|
||||
|
||||
// Include all .jar files from lib/ or lib/plugins/ (but not sources)
|
||||
if (name.starts_with("gradle-8.9/lib/") || name.starts_with("gradle-8.9/lib/plugins/"))
|
||||
&& name.ends_with(".jar")
|
||||
&& !name.contains("-sources")
|
||||
&& !name.contains("-src") {
|
||||
|
||||
drop(file);
|
||||
let jar_bytes = extract_file(&mut archive, &name)?;
|
||||
all_jar_bytes.push(jar_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
if all_jar_bytes.is_empty() {
|
||||
return Err("No gradle jars found".into());
|
||||
}
|
||||
|
||||
eprintln!("Merging {} gradle jars into wrapper...", all_jar_bytes.len());
|
||||
|
||||
// Merge all jars
|
||||
merge_multiple_jars(all_jar_bytes, output_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_file(archive: &mut zip::ZipArchive<Cursor<Vec<u8>>>, path: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let mut file = archive.by_name(path)?;
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn merge_multiple_jars(jar_bytes_list: Vec<Vec<u8>>, output_path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::io::Cursor;
|
||||
use std::collections::HashSet;
|
||||
|
||||
let output_file = fs::File::create(output_path)?;
|
||||
let mut zip_writer = zip::ZipWriter::new(output_file);
|
||||
|
||||
let options = zip::write::FileOptions::<()>::default()
|
||||
.compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
let mut added_files = HashSet::new();
|
||||
|
||||
// Process each jar
|
||||
for jar_bytes in jar_bytes_list {
|
||||
let cursor = Cursor::new(jar_bytes);
|
||||
let mut archive = zip::ZipArchive::new(cursor)?;
|
||||
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
let name = file.name().to_string();
|
||||
|
||||
// Skip META-INF, directories, and duplicates
|
||||
if !name.starts_with("META-INF/") && !name.ends_with('/') && !added_files.contains(&name) {
|
||||
let mut contents = Vec::new();
|
||||
file.read_to_end(&mut contents)?;
|
||||
|
||||
zip_writer.start_file(&name, options)?;
|
||||
zip_writer.write_all(&contents)?;
|
||||
added_files.insert(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zip_writer.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user