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.
791 lines
26 KiB
Rust
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(())
|
|
}
|
|
} |