Files
weevil/src/project/mod.rs
Eric Ratliff df338987b6 feat: Add template system with testing showcase
Implements template-based project creation allowing teams to start with
professional example code instead of empty projects.

Features:
- Two templates: 'basic' (minimal) and 'testing' (45-test showcase)
- Template variable substitution ({{PROJECT_NAME}}, etc.)
- Template validation with helpful error messages
- `weevil new --list-templates` command
- Templates embedded in binary at compile time

Testing template includes:
- 3 complete subsystems (MotorCycler, WallApproach, TurnController)
- Hardware abstraction layer with mock implementations
- 45 comprehensive tests (unit, integration, system)
- Professional documentation (DESIGN_AND_TEST_PLAN.md, etc.)

Usage:
  weevil new my-robot                    # basic template
  weevil new my-robot --template testing # testing showcase
  weevil new --list-templates            # show available templates

This enables FTC teams to learn from working code and best practices
rather than starting from scratch.

All 62 tests passing.
2026-02-02 23:15:23 -06:00

791 lines
26 KiB
Rust

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;
const WEEVIL_VERSION: &str = env!("CARGO_PKG_VERSION");
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",
".idea/runConfigurations",
];
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", WEEVIL_VERSION);
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(), sdk_config.android_sdk_path.clone())?;
project_config.save(project_path)?;
// README.md
let readme = format!(
r#"# {}
FTC Robot Project generated by Weevil v{}
## 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, WEEVIL_VERSION
);
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"), WEEVIL_VERSION)?;
// build.gradle.kts - Pure Java with deployToSDK task
// Escape backslashes for Windows paths in Kotlin strings
let sdk_path = sdk_config.ftc_sdk_path.display().to_string().replace("\\", "\\\\");
let build_gradle = format!(r#"plugins {{
java
}}
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_path, sdk_path);
fs::write(project_path.join("build.gradle.kts"), build_gradle)?;
// settings.gradle.kts - Repositories go here in Gradle 8+
let settings_gradle = format!(r#"rootProject.name = "{}"
// Repository configuration (Gradle 8+ prefers repositories in settings)
dependencyResolutionManagement {{
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {{
mavenCentral()
google()
}}
}}
"#, 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 paths from config
for /f "tokens=2 delims==" %%a in ('findstr /c:"ftc_sdk_path" .weevil.toml') do set SDK_DIR=%%a
for /f "tokens=2 delims==" %%a in ('findstr /c:"android_sdk_path" .weevil.toml') do set ANDROID_SDK=%%a
REM Strip all quotes (both single and double)
set SDK_DIR=%SDK_DIR:"=%
set SDK_DIR=%SDK_DIR:'=%
set SDK_DIR=%SDK_DIR: =%
set ANDROID_SDK=%ANDROID_SDK:"=%
set ANDROID_SDK=%ANDROID_SDK:'=%
set ANDROID_SDK=%ANDROID_SDK: =%
if not defined SDK_DIR (
echo Error: Could not read FTC SDK path from .weevil.toml
exit /b 1
)
if not defined ANDROID_SDK (
echo Error: Could not read Android SDK path from .weevil.toml
exit /b 1
)
REM Set ADB path
set ADB_PATH=%ANDROID_SDK%\platform-tools\adb.exe
echo Building APK...
call gradlew.bat buildApk
echo.
echo Deploying to Control Hub...
REM Find APK - look for TeamCode-debug.apk
for /f "delims=" %%i in ('dir /s /b "%SDK_DIR%\TeamCode-debug.apk" 2^>nul') do set APK=%%i
if not defined APK (
echo Error: APK not found
exit /b 1
)
echo Found APK: %APK%
REM Check for adb
if not exist "%ADB_PATH%" (
echo Error: adb not found at %ADB_PATH%
echo Run: weevil sdk install
exit /b 1
)
echo Installing: %APK%
"%ADB_PATH%" install -r "%APK%"
if errorlevel 1 (
echo.
echo Deployment failed!
exit /b 1
)
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
)?;
// Android Studio integration: .idea/ files
self.generate_idea_files(project_path)?;
Ok(())
}
/// Generate .idea/ files for Android Studio integration.
///
/// The goal is for students to open the project in Android Studio and see
/// a clean file tree (just src/ and the scripts) with Run configurations
/// that invoke Weevil's shell scripts directly. All the internal plumbing
/// (sdk/, .gradle/, build/) is hidden from the IDE view.
///
/// Android Studio uses IntelliJ's run configuration XML format. The
/// ShellScript type invokes a script relative to the project root — exactly
/// what we want since deploy.sh and build.sh already live there.
fn generate_idea_files(&self, project_path: &Path) -> Result<()> {
// workspace.xml — controls the file-tree view and hides internals.
// We use a ProjectViewPane exclude pattern list rather than touching
// the module's source roots, so this works regardless of whether the
// student has opened the project before.
let workspace_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectViewManager">
<state>
<navigator currentProjector="ProjectFiles" hideEmptyMiddlePackages="true" sortByType="true">
<state>
<expand>
<file url="file://$PROJECT_DIR$/src" />
<file url="file://$PROJECT_DIR$/src/main" />
<file url="file://$PROJECT_DIR$/src/main/java" />
<file url="file://$PROJECT_DIR$/src/main/java/robot" />
<file url="file://$PROJECT_DIR$/src/test" />
<file url="file://$PROJECT_DIR$/src/test/java" />
<file url="file://$PROJECT_DIR$/src/test/java/robot" />
</expand>
</state>
</navigator>
</state>
</component>
<component name="ExcludedFiles">
<file url="file://$PROJECT_DIR$/build" reason="Build output" />
<file url="file://$PROJECT_DIR$/.gradle" reason="Gradle cache" />
<file url="file://$PROJECT_DIR$/gradle" reason="Gradle wrapper internals" />
</component>
</project>
"#;
fs::write(project_path.join(".idea/workspace.xml"), workspace_xml)?;
// Run configurations. Each is a ShellScript type that invokes one of
// Weevil's scripts. Android Studio shows these in the Run dropdown
// at the top of the IDE — no configuration needed by the student.
//
// We generate both Unix (.sh, ./gradlew) and Windows (.bat, gradlew.bat)
// variants. Android Studio automatically hides configs whose script files
// don't exist, so only the platform-appropriate ones appear in the dropdown.
// Build (Unix) — just builds the APK without deploying
let build_unix_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Build" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/build.sh" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Build.xml"),
build_unix_xml,
)?;
// Build (Windows) — same, but calls build.bat
let build_windows_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Build (Windows)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/build.bat" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="cmd.exe" />
<option name="INTERPRETER_OPTIONS" value="/c" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Build (Windows).xml"),
build_windows_xml,
)?;
// Deploy (auto) — no flags, deploy.sh auto-detects USB vs WiFi
let deploy_auto_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Deploy (auto)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.sh" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Deploy (auto).xml"),
deploy_auto_xml,
)?;
// Deploy (auto) (Windows)
let deploy_auto_windows_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Deploy (auto) (Windows)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.bat" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="cmd.exe" />
<option name="INTERPRETER_OPTIONS" value="/c" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Deploy (auto) (Windows).xml"),
deploy_auto_windows_xml,
)?;
// Deploy (USB) — forces USB connection
let deploy_usb_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Deploy (USB)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.sh" />
<option name="SCRIPT_OPTIONS" value="--usb" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Deploy (USB).xml"),
deploy_usb_xml,
)?;
// Deploy (USB) (Windows)
let deploy_usb_windows_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Deploy (USB) (Windows)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.bat" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="cmd.exe" />
<option name="INTERPRETER_OPTIONS" value="/c" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Deploy (USB) (Windows).xml"),
deploy_usb_windows_xml,
)?;
// Deploy (WiFi) — forces WiFi connection to default 192.168.43.1
let deploy_wifi_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Deploy (WiFi)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.sh" />
<option name="SCRIPT_OPTIONS" value="--wifi" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Deploy (WiFi).xml"),
deploy_wifi_xml,
)?;
// Deploy (WiFi) (Windows)
let deploy_wifi_windows_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Deploy (WiFi) (Windows)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.bat" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="cmd.exe" />
<option name="INTERPRETER_OPTIONS" value="/c" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Deploy (WiFi) (Windows).xml"),
deploy_wifi_windows_xml,
)?;
// Test — runs the unit test suite via Gradle
let test_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Test" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/gradlew" />
<option name="SCRIPT_OPTIONS" value="test" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Test.xml"),
test_xml,
)?;
// Test (Windows)
let test_windows_xml = r#"<component name="ProjectRunConfigurationManager">
<configuration name="Test (Windows)" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/gradlew.bat" />
<option name="SCRIPT_OPTIONS" value="test" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="cmd.exe" />
<option name="INTERPRETER_OPTIONS" value="/c" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="true" />
<envs />
<method v="2" />
</configuration>
</component>
"#;
fs::write(
project_path.join(".idea/runConfigurations/Test (Windows).xml"),
test_windows_xml,
)?;
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(())
}
}