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.
123 lines
4.1 KiB
Rust
123 lines
4.1 KiB
Rust
// 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(())
|
|
} |