feat: Add environment setup command
Adds `weevil setup` with two modes: - System setup: installs default SDKs and dependencies - Project setup: reads .weevil.toml and installs project requirements Provides platform-specific installation instructions when auto-install fails. Never leaves users wondering what to do next.
This commit is contained in:
@@ -3,3 +3,4 @@ pub mod upgrade;
|
||||
pub mod deploy;
|
||||
pub mod sdk;
|
||||
pub mod config;
|
||||
pub mod setup;
|
||||
514
src/commands/setup.rs
Normal file
514
src/commands/setup.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
use anyhow::{Result, Context, bail};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use colored::*;
|
||||
|
||||
use crate::sdk::SdkConfig;
|
||||
use crate::project::ProjectConfig;
|
||||
|
||||
/// Setup development environment - either system-wide or for a specific project
|
||||
pub fn setup_environment(project_path: Option<&str>) -> Result<()> {
|
||||
match project_path {
|
||||
Some(path) => setup_project(path),
|
||||
None => setup_system(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup system-wide development environment with default SDKs
|
||||
fn setup_system() -> Result<()> {
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||
println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||
println!();
|
||||
|
||||
let mut issues = Vec::new();
|
||||
let mut installed = Vec::new();
|
||||
|
||||
// Check and install SDKs
|
||||
let sdk_config = SdkConfig::new()?;
|
||||
|
||||
// 1. Check Java
|
||||
println!("{}", "Checking Java JDK...".bright_yellow());
|
||||
match check_java() {
|
||||
Ok(version) => {
|
||||
println!("{} Java JDK {} found", "✓".green(), version);
|
||||
installed.push(format!("Java JDK {}", version));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} {}", "✗".red(), e);
|
||||
issues.push(("Java JDK", get_java_install_instructions()));
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// 2. Check/Install FTC SDK
|
||||
println!("{}", "Checking FTC SDK...".bright_yellow());
|
||||
if sdk_config.ftc_sdk_path.exists() {
|
||||
match crate::sdk::ftc::verify(&sdk_config.ftc_sdk_path) {
|
||||
Ok(_) => {
|
||||
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
println!("{} FTC SDK {} found at: {}",
|
||||
"✓".green(),
|
||||
version,
|
||||
sdk_config.ftc_sdk_path.display()
|
||||
);
|
||||
installed.push(format!("FTC SDK {}", version));
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{} FTC SDK found but incomplete, reinstalling...", "⚠".yellow());
|
||||
crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?;
|
||||
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
installed.push(format!("FTC SDK {} (installed)", version));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("FTC SDK not found. Installing...");
|
||||
crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path)?;
|
||||
let version = crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path)
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
installed.push(format!("FTC SDK {} (installed)", version));
|
||||
}
|
||||
println!();
|
||||
|
||||
// 3. Check/Install Android SDK
|
||||
println!("{}", "Checking Android SDK...".bright_yellow());
|
||||
if sdk_config.android_sdk_path.exists() {
|
||||
match crate::sdk::android::verify(&sdk_config.android_sdk_path) {
|
||||
Ok(_) => {
|
||||
println!("{} Android SDK found at: {}",
|
||||
"✓".green(),
|
||||
sdk_config.android_sdk_path.display()
|
||||
);
|
||||
installed.push("Android SDK".to_string());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow());
|
||||
crate::sdk::android::install(&sdk_config.android_sdk_path)?;
|
||||
installed.push("Android SDK (installed)".to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Android SDK not found. Installing...");
|
||||
crate::sdk::android::install(&sdk_config.android_sdk_path)?;
|
||||
installed.push("Android SDK (installed)".to_string());
|
||||
}
|
||||
println!();
|
||||
|
||||
// 4. Check ADB
|
||||
println!("{}", "Checking ADB (Android Debug Bridge)...".bright_yellow());
|
||||
match check_adb(&sdk_config.android_sdk_path) {
|
||||
Ok(version) => {
|
||||
println!("{} ADB {} found", "✓".green(), version);
|
||||
installed.push(format!("ADB {}", version));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} {}", "⚠".yellow(), e);
|
||||
println!(" ADB is included in Android SDK platform-tools");
|
||||
println!(" Add to PATH: {}", sdk_config.android_sdk_path.join("platform-tools").display());
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// 5. Check Gradle
|
||||
println!("{}", "Checking Gradle...".bright_yellow());
|
||||
match check_gradle() {
|
||||
Ok(version) => {
|
||||
println!("{} Gradle {} found", "✓".green(), version);
|
||||
installed.push(format!("Gradle {}", version));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} {}", "⚠".yellow(), e);
|
||||
println!(" Note: Weevil projects include Gradle wrapper, so this is optional");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// Print summary
|
||||
print_system_summary(&installed, &issues, &sdk_config);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setup dependencies for a specific project by reading its .weevil.toml
|
||||
fn setup_project(project_path: &str) -> Result<()> {
|
||||
let project_path = PathBuf::from(project_path);
|
||||
|
||||
if !project_path.exists() {
|
||||
bail!("Project directory not found: {}", project_path.display());
|
||||
}
|
||||
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||
println!("{}", " Project Setup - Installing Dependencies".bright_cyan().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||
println!();
|
||||
|
||||
// Load project configuration
|
||||
println!("{}", "Reading project configuration...".bright_yellow());
|
||||
let config = ProjectConfig::load(&project_path)
|
||||
.context("Failed to load .weevil.toml")?;
|
||||
|
||||
println!();
|
||||
println!("{}", "Project Configuration:".bright_yellow().bold());
|
||||
println!(" Project: {}", config.project_name.bright_white());
|
||||
println!(" FTC SDK: {} ({})",
|
||||
config.ftc_sdk_version.bright_white(),
|
||||
config.ftc_sdk_path.display()
|
||||
);
|
||||
println!(" Android SDK: {}", config.android_sdk_path.display());
|
||||
println!();
|
||||
|
||||
let mut installed = Vec::new();
|
||||
let mut issues = Vec::new();
|
||||
|
||||
// 1. Check Java
|
||||
println!("{}", "Checking Java JDK...".bright_yellow());
|
||||
match check_java() {
|
||||
Ok(version) => {
|
||||
println!("{} Java JDK {} found", "✓".green(), version);
|
||||
installed.push(format!("Java JDK {}", version));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} {}", "✗".red(), e);
|
||||
issues.push(("Java JDK", get_java_install_instructions()));
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// 2. Check/Install project-specific FTC SDK
|
||||
println!("{}", format!("Checking FTC SDK {}...", config.ftc_sdk_version).bright_yellow());
|
||||
if config.ftc_sdk_path.exists() {
|
||||
match crate::sdk::ftc::verify(&config.ftc_sdk_path) {
|
||||
Ok(_) => {
|
||||
println!("{} FTC SDK {} found at: {}",
|
||||
"✓".green(),
|
||||
config.ftc_sdk_version,
|
||||
config.ftc_sdk_path.display()
|
||||
);
|
||||
installed.push(format!("FTC SDK {}", config.ftc_sdk_version));
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{} FTC SDK path exists but is invalid", "✗".red());
|
||||
println!(" Expected at: {}", config.ftc_sdk_path.display());
|
||||
println!();
|
||||
println!("{}", "Solution:".bright_yellow().bold());
|
||||
println!(" The .weevil.toml specifies an FTC SDK location that doesn't exist or is incomplete.");
|
||||
println!(" You have two options:");
|
||||
println!();
|
||||
println!(" 1. Update the project to use a different SDK:");
|
||||
println!(" weevil config {} --set-sdk <path-to-sdk>", project_path.display());
|
||||
println!();
|
||||
println!(" 2. Install the SDK at the expected location:");
|
||||
println!(" # Clone FTC SDK to the expected path");
|
||||
println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\");
|
||||
println!(" {}", config.ftc_sdk_path.display());
|
||||
println!(" cd {}", config.ftc_sdk_path.display());
|
||||
println!(" git checkout {}", config.ftc_sdk_version);
|
||||
bail!("FTC SDK verification failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{} FTC SDK not found at: {}", "✗".red(), config.ftc_sdk_path.display());
|
||||
println!();
|
||||
|
||||
// Try to install it automatically
|
||||
println!("{}", "Attempting automatic installation...".bright_yellow());
|
||||
match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path) {
|
||||
Ok(_) => {
|
||||
println!("{} FTC SDK {} installed successfully",
|
||||
"✓".green(),
|
||||
config.ftc_sdk_version
|
||||
);
|
||||
installed.push(format!("FTC SDK {} (installed)", config.ftc_sdk_version));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} Automatic installation failed: {}", "✗".red(), e);
|
||||
println!();
|
||||
println!("{}", "Manual Installation Required:".bright_yellow().bold());
|
||||
println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\");
|
||||
println!(" {}", config.ftc_sdk_path.display());
|
||||
println!(" cd {}", config.ftc_sdk_path.display());
|
||||
println!(" git checkout {}", config.ftc_sdk_version);
|
||||
bail!("FTC SDK installation failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// 3. Check/Install Android SDK
|
||||
println!("{}", "Checking Android SDK...".bright_yellow());
|
||||
if config.android_sdk_path.exists() {
|
||||
match crate::sdk::android::verify(&config.android_sdk_path) {
|
||||
Ok(_) => {
|
||||
println!("{} Android SDK found at: {}",
|
||||
"✓".green(),
|
||||
config.android_sdk_path.display()
|
||||
);
|
||||
installed.push("Android SDK".to_string());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow());
|
||||
crate::sdk::android::install(&config.android_sdk_path)?;
|
||||
installed.push("Android SDK (installed)".to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Android SDK not found. Installing...");
|
||||
crate::sdk::android::install(&config.android_sdk_path)?;
|
||||
installed.push("Android SDK (installed)".to_string());
|
||||
}
|
||||
println!();
|
||||
|
||||
// 4. Check ADB
|
||||
println!("{}", "Checking ADB...".bright_yellow());
|
||||
match check_adb(&config.android_sdk_path) {
|
||||
Ok(version) => {
|
||||
println!("{} ADB {} found", "✓".green(), version);
|
||||
installed.push(format!("ADB {}", version));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{} {}", "⚠".yellow(), e);
|
||||
println!(" Add to PATH: {}", config.android_sdk_path.join("platform-tools").display());
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// 5. Check Gradle wrapper in project
|
||||
println!("{}", "Checking Gradle wrapper...".bright_yellow());
|
||||
let gradlew = if cfg!(target_os = "windows") {
|
||||
project_path.join("gradlew.bat")
|
||||
} else {
|
||||
project_path.join("gradlew")
|
||||
};
|
||||
|
||||
if gradlew.exists() {
|
||||
println!("{} Gradle wrapper found in project", "✓".green());
|
||||
installed.push("Gradle wrapper".to_string());
|
||||
} else {
|
||||
println!("{} Gradle wrapper not found in project", "⚠".yellow());
|
||||
println!(" Run 'weevil upgrade {}' to regenerate project files", project_path.display());
|
||||
}
|
||||
println!();
|
||||
|
||||
// Print summary
|
||||
print_project_summary(&installed, &issues, &config, &project_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_java() -> Result<String> {
|
||||
let output = Command::new("java")
|
||||
.arg("-version")
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
// Java version is typically in stderr, format: java version "11.0.x" or openjdk version "11.0.x"
|
||||
for line in stderr.lines() {
|
||||
if line.contains("version") {
|
||||
if let Some(version_str) = line.split('"').nth(1) {
|
||||
return Ok(version_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok("installed (version unknown)".to_string())
|
||||
}
|
||||
Err(_) => bail!("Java JDK not found in PATH"),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_adb(android_sdk_path: &Path) -> Result<String> {
|
||||
// First try system PATH
|
||||
let output = Command::new("adb")
|
||||
.arg("version")
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if out.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
for line in stdout.lines() {
|
||||
if line.starts_with("Android Debug Bridge version") {
|
||||
return Ok(line.replace("Android Debug Bridge version ", ""));
|
||||
}
|
||||
}
|
||||
return Ok("installed (version unknown)".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Try Android SDK location
|
||||
let adb_path = if cfg!(target_os = "windows") {
|
||||
android_sdk_path.join("platform-tools").join("adb.exe")
|
||||
} else {
|
||||
android_sdk_path.join("platform-tools").join("adb")
|
||||
};
|
||||
|
||||
if adb_path.exists() {
|
||||
bail!("ADB found in Android SDK but not in PATH")
|
||||
} else {
|
||||
bail!("ADB not found")
|
||||
}
|
||||
}
|
||||
|
||||
fn check_gradle() -> Result<String> {
|
||||
let output = Command::new("gradle")
|
||||
.arg("--version")
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
for line in stdout.lines() {
|
||||
if line.starts_with("Gradle") {
|
||||
return Ok(line.replace("Gradle ", ""));
|
||||
}
|
||||
}
|
||||
Ok("installed (version unknown)".to_string())
|
||||
}
|
||||
Err(_) => bail!("Gradle not found in PATH (optional)"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_java_install_instructions() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!(
|
||||
"Java JDK is required but not found.\n\
|
||||
\n\
|
||||
To install Java 11 on Windows:\n\
|
||||
\n\
|
||||
1. Download from: {}\n\
|
||||
2. Run the installer\n\
|
||||
3. Add Java to your PATH (installer usually does this)\n\
|
||||
4. Run 'weevil setup' again to verify\n\
|
||||
\n\
|
||||
Verify installation: java -version",
|
||||
"https://adoptium.net/temurin/releases/?version=11".bright_white()
|
||||
)
|
||||
} else if cfg!(target_os = "macos") {
|
||||
format!(
|
||||
"Java JDK is required but not found.\n\
|
||||
\n\
|
||||
To install Java 11 on macOS:\n\
|
||||
\n\
|
||||
Using Homebrew (recommended):\n\
|
||||
{}\n\
|
||||
\n\
|
||||
Or download from: {}\n\
|
||||
\n\
|
||||
Verify installation: java -version",
|
||||
" brew install openjdk@11".bright_white(),
|
||||
"https://adoptium.net/temurin/releases/?version=11".bright_white()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Java JDK is required but not found.\n\
|
||||
\n\
|
||||
To install Java 11 on Ubuntu/Debian:\n\
|
||||
{}\n\
|
||||
{}\n\
|
||||
\n\
|
||||
To install on Fedora/RHEL:\n\
|
||||
{}\n\
|
||||
\n\
|
||||
Verify installation: java -version",
|
||||
" sudo apt update".bright_white(),
|
||||
" sudo apt install openjdk-11-jdk".bright_white(),
|
||||
" sudo dnf install java-11-openjdk-devel".bright_white()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn print_system_summary(installed: &[String], issues: &[(&str, String)], sdk_config: &SdkConfig) {
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!("{}", " System Setup Summary".bright_green().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!();
|
||||
|
||||
if !installed.is_empty() {
|
||||
println!("{}", "Installed Components:".bright_green().bold());
|
||||
for component in installed {
|
||||
println!(" {} {}", "✓".green(), component);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if !issues.is_empty() {
|
||||
println!("{}", "Manual Installation Required:".bright_yellow().bold());
|
||||
println!();
|
||||
for (name, instructions) in issues {
|
||||
println!("{} {}", "✗".red(), name.red().bold());
|
||||
println!();
|
||||
for line in instructions.lines() {
|
||||
println!(" {}", line);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "SDK Locations:".bright_cyan().bold());
|
||||
println!(" FTC SDK: {}", sdk_config.ftc_sdk_path.display());
|
||||
println!(" Android SDK: {}", sdk_config.android_sdk_path.display());
|
||||
println!(" Cache: {}", sdk_config.cache_dir.display());
|
||||
println!();
|
||||
|
||||
if issues.is_empty() {
|
||||
println!("{}", "✓ System is ready for FTC development!".bright_green().bold());
|
||||
println!();
|
||||
println!("{}", "Next steps:".bright_yellow().bold());
|
||||
println!(" Create a new project: {}", "weevil new my-robot".bright_white());
|
||||
println!(" Clone existing project: {}", "git clone <repo> && cd <repo> && weevil setup .".bright_white());
|
||||
} else {
|
||||
println!("{}", "⚠ Please install the required components listed above".bright_yellow().bold());
|
||||
println!(" Then run {} to verify", "weevil setup".bright_white());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_project_summary(installed: &[String], issues: &[(&str, String)], config: &ProjectConfig, project_path: &Path) {
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!("{}", " Project Setup Summary".bright_green().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_green());
|
||||
println!();
|
||||
|
||||
println!("{}", "Project Details:".bright_cyan().bold());
|
||||
println!(" Name: {}", config.project_name);
|
||||
println!(" Location: {}", project_path.display());
|
||||
println!(" FTC SDK: {} at {}", config.ftc_sdk_version, config.ftc_sdk_path.display());
|
||||
println!();
|
||||
|
||||
if !installed.is_empty() {
|
||||
println!("{}", "Installed Components:".bright_green().bold());
|
||||
for component in installed {
|
||||
println!(" {} {}", "✓".green(), component);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if !issues.is_empty() {
|
||||
println!("{}", "Manual Installation Required:".bright_yellow().bold());
|
||||
println!();
|
||||
for (name, instructions) in issues {
|
||||
println!("{} {}", "✗".red(), name.red().bold());
|
||||
println!();
|
||||
for line in instructions.lines() {
|
||||
println!(" {}", line);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
if issues.is_empty() {
|
||||
println!("{}", "✓ Project is ready for development!".bright_green().bold());
|
||||
println!();
|
||||
println!("{}", "Next steps:".bright_yellow().bold());
|
||||
println!(" 1. Review the code: {}", format!("cd {}", project_path.display()).bright_white());
|
||||
println!(" 2. Run tests: {}", "./gradlew test".bright_white());
|
||||
println!(" 3. Build: {}", "./build.sh (or build.bat on Windows)".bright_white());
|
||||
println!(" 4. Deploy to robot: {}", format!("weevil deploy {}", project_path.display()).bright_white());
|
||||
} else {
|
||||
println!("{}", "⚠ Please install the required components listed above".bright_yellow().bold());
|
||||
println!(" Then run {} to verify", format!("weevil setup {}", project_path.display()).bright_white());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
@@ -33,6 +33,12 @@ enum Commands {
|
||||
android_sdk: Option<String>,
|
||||
},
|
||||
|
||||
/// Setup development environment (system or project)
|
||||
Setup {
|
||||
/// Path to project directory (optional - without it, sets up system)
|
||||
path: Option<String>,
|
||||
},
|
||||
|
||||
/// Upgrade an existing project to the latest generator version
|
||||
Upgrade {
|
||||
/// Path to the project directory
|
||||
@@ -99,6 +105,9 @@ fn main() -> Result<()> {
|
||||
Commands::New { name, ftc_sdk, android_sdk } => {
|
||||
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref())
|
||||
}
|
||||
Commands::Setup { path } => {
|
||||
commands::setup::setup_environment(path.as_deref())
|
||||
}
|
||||
Commands::Upgrade { path } => {
|
||||
commands::upgrade::upgrade_project(&path)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user