Files
weevil/build.rs
Eric Ratliff 70a1acc2a1 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.
2026-01-25 00:17:51 -06:00

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