Compare commits
1 Commits
v1.1.0-bet
...
v1.0.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0b2482774 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -34,10 +34,6 @@ Cargo.lock text diff=toml
|
|||||||
*.ico binary
|
*.ico binary
|
||||||
*.svg text
|
*.svg text
|
||||||
|
|
||||||
# Test fixtures
|
|
||||||
.gitkeep text
|
|
||||||
tests/fixtures/mock-android-sdk/platform-tools/adb binary
|
|
||||||
|
|
||||||
# Fonts
|
# Fonts
|
||||||
*.ttf binary
|
*.ttf binary
|
||||||
*.otf binary
|
*.otf binary
|
||||||
|
|||||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,17 +0,0 @@
|
|||||||
# Changelog
|
|
||||||
|
|
||||||
## [1.0.0] - 2026-01-27
|
|
||||||
|
|
||||||
First stable release! 🎉
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- Complete Windows deployment support
|
|
||||||
- Android SDK path in project configuration
|
|
||||||
- Robust cross-platform build and deployment scripts
|
|
||||||
- Project upgrade command with config migration
|
|
||||||
- Comprehensive test suite
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Windows APK discovery and deployment
|
|
||||||
- Batch file path parsing (quote handling)
|
|
||||||
- ADB integration and error reporting
|
|
||||||
15
Cargo.toml
15
Cargo.toml
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "weevil"
|
name = "weevil"
|
||||||
version = "1.1.0-beta.2"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Eric Ratliff <eric@nxlearn.net>"]
|
authors = ["Eric Ratliff <eric@intrepidfusion.com>"]
|
||||||
description = "FTC robotics project generator - bores into complexity, emerges with clean code"
|
description = "FTC robotics project generator - bores into complexity, emerges with clean code"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
@@ -63,17 +63,6 @@ assert_cmd = "2.0"
|
|||||||
predicates = "3.1"
|
predicates = "3.1"
|
||||||
insta = "1.41"
|
insta = "1.41"
|
||||||
|
|
||||||
# Proxy integration tests: mockito is the mock origin server; hyper + friends
|
|
||||||
# are the forward proxy. All three are already in Cargo.lock as transitive
|
|
||||||
# deps of reqwest — we're just promoting them to explicit dev-deps with the
|
|
||||||
# features we actually need.
|
|
||||||
mockito = "1.7"
|
|
||||||
hyper = { version = "1", features = ["server", "http1", "client"] }
|
|
||||||
hyper-util = { version = "0.1", features = ["tokio", "client-legacy", "http1"] }
|
|
||||||
http-body-util = "0.1"
|
|
||||||
bytes = "1"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ureq = { version = "2.10", features = ["json"] }
|
ureq = { version = "2.10", features = ["json"] }
|
||||||
zip = "2.2"
|
zip = "2.2"
|
||||||
|
|||||||
282
README.md
282
README.md
@@ -26,8 +26,6 @@ This approach works against standard software engineering practices and creates
|
|||||||
- ✅ Generate all build/deploy scripts automatically
|
- ✅ Generate all build/deploy scripts automatically
|
||||||
- ✅ Enable proper version control workflows
|
- ✅ Enable proper version control workflows
|
||||||
- ✅ Are actually testable and maintainable
|
- ✅ Are actually testable and maintainable
|
||||||
- ✅ Work seamlessly with Android Studio
|
|
||||||
- ✅ Support proxy/air-gapped environments
|
|
||||||
|
|
||||||
Students focus on building robots, not navigating SDK internals.
|
Students focus on building robots, not navigating SDK internals.
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@ my-robot/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── main/java/robot/ # Your robot code lives here
|
│ ├── main/java/robot/ # Your robot code lives here
|
||||||
│ └── test/java/robot/ # Unit tests (run on PC!)
|
│ └── test/java/robot/ # Unit tests (run on PC!)
|
||||||
├── .idea/ # Android Studio integration (auto-generated)
|
|
||||||
├── build.sh / build.bat # One command to build
|
├── build.sh / build.bat # One command to build
|
||||||
├── deploy.sh / deploy.bat # One command to deploy
|
├── deploy.sh / deploy.bat # One command to deploy
|
||||||
└── .weevil.toml # Project configuration
|
└── .weevil.toml # Project configuration
|
||||||
@@ -49,9 +46,6 @@ my-robot/
|
|||||||
|
|
||||||
### 🚀 Simple Commands
|
### 🚀 Simple Commands
|
||||||
```bash
|
```bash
|
||||||
# Set up development environment
|
|
||||||
weevil setup
|
|
||||||
|
|
||||||
# Create a new robot project
|
# Create a new robot project
|
||||||
weevil new awesome-robot
|
weevil new awesome-robot
|
||||||
|
|
||||||
@@ -66,9 +60,6 @@ cd awesome-robot
|
|||||||
|
|
||||||
### 🔧 Project Management
|
### 🔧 Project Management
|
||||||
```bash
|
```bash
|
||||||
# Check system health
|
|
||||||
weevil doctor
|
|
||||||
|
|
||||||
# Upgrade project infrastructure
|
# Upgrade project infrastructure
|
||||||
weevil upgrade awesome-robot
|
weevil upgrade awesome-robot
|
||||||
|
|
||||||
@@ -78,36 +69,8 @@ weevil config awesome-robot --set-sdk /path/to/different/sdk
|
|||||||
|
|
||||||
# Check SDK status
|
# Check SDK status
|
||||||
weevil sdk status
|
weevil sdk status
|
||||||
|
|
||||||
# Remove installed components
|
|
||||||
weevil uninstall --dry-run
|
|
||||||
weevil uninstall
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🌐 Proxy Support (v1.1.0)
|
|
||||||
Work behind corporate firewalls or in air-gapped environments:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use HTTP proxy for all downloads
|
|
||||||
weevil --proxy http://proxy.company.com:8080 setup
|
|
||||||
weevil --proxy http://proxy.company.com:8080 new my-robot
|
|
||||||
|
|
||||||
# Bypass proxy (for local/direct connections)
|
|
||||||
weevil --no-proxy setup
|
|
||||||
|
|
||||||
# Proxy auto-detected from HTTPS_PROXY/HTTP_PROXY environment variables
|
|
||||||
export HTTPS_PROXY=http://proxy:8080
|
|
||||||
weevil setup # Uses proxy automatically
|
|
||||||
```
|
|
||||||
|
|
||||||
### 💻 Android Studio Integration (v1.1.0)
|
|
||||||
Projects work seamlessly with Android Studio:
|
|
||||||
- **One-click deployment** - Run configurations appear automatically in the Run dropdown
|
|
||||||
- **Clean file tree** - Internal directories hidden, only your code visible
|
|
||||||
- **No configuration needed** - Just open the project and hit Run
|
|
||||||
|
|
||||||
See [Android Studio Setup](#android-studio-setup) for details.
|
|
||||||
|
|
||||||
### ✨ Smart Features
|
### ✨ Smart Features
|
||||||
- **Per-project SDK configuration** - Different projects can use different SDK versions
|
- **Per-project SDK configuration** - Different projects can use different SDK versions
|
||||||
- **Automatic Gradle wrapper** - No manual setup required
|
- **Automatic Gradle wrapper** - No manual setup required
|
||||||
@@ -115,8 +78,6 @@ See [Android Studio Setup](#android-studio-setup) for details.
|
|||||||
- **Zero SDK modification** - Your SDK stays pristine
|
- **Zero SDK modification** - Your SDK stays pristine
|
||||||
- **Git-ready** - Projects initialize with proper `.gitignore`
|
- **Git-ready** - Projects initialize with proper `.gitignore`
|
||||||
- **Upgrade-safe** - Update build scripts without losing code
|
- **Upgrade-safe** - Update build scripts without losing code
|
||||||
- **System diagnostics** - `weevil doctor` checks your environment health
|
|
||||||
- **Selective uninstall** - Remove specific components without nuking everything
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -135,49 +96,30 @@ export PATH="$PATH:$(pwd)/target/release"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Rust 1.70+ (for building Weevil)
|
- Rust 1.70+ (for building)
|
||||||
- Java 11+ (for running Gradle)
|
- Java 11+ (for running Gradle)
|
||||||
- Android SDK with platform-tools (for deployment)
|
- Android SDK with platform-tools (for deployment)
|
||||||
- FTC SDK (Weevil can install it for you)
|
- FTC SDK (Weevil can download it for you)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Set Up Your Environment
|
### 1. Create Your First Project
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check what's installed
|
|
||||||
weevil doctor
|
|
||||||
|
|
||||||
# Install everything automatically
|
|
||||||
weevil setup
|
|
||||||
|
|
||||||
# Or install to custom location
|
|
||||||
weevil setup --ftc-sdk ~/my-sdks/ftc --android-sdk ~/my-sdks/android
|
|
||||||
```
|
|
||||||
|
|
||||||
Weevil will:
|
|
||||||
- Download and install FTC SDK
|
|
||||||
- Download and install Android SDK (if needed)
|
|
||||||
- Set up Gradle wrapper
|
|
||||||
- Verify all dependencies
|
|
||||||
|
|
||||||
### 2. Create Your First Project
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
weevil new my-robot
|
weevil new my-robot
|
||||||
cd my-robot
|
cd my-robot
|
||||||
```
|
```
|
||||||
|
|
||||||
Weevil generates:
|
Weevil will:
|
||||||
- Clean project structure
|
- Download the FTC SDK if needed (or use existing)
|
||||||
- Android Studio run configurations
|
- Generate your project structure
|
||||||
- Example test files
|
- Set up Gradle wrapper
|
||||||
- Build and deploy scripts
|
- Initialize git repository
|
||||||
- Git repository with `.gitignore`
|
- Create example test files
|
||||||
|
|
||||||
### 3. Write Some Code
|
### 2. Write Some Code
|
||||||
|
|
||||||
Create `src/main/java/robot/MyOpMode.java`:
|
Create `src/main/java/robot/MyOpMode.java`:
|
||||||
|
|
||||||
@@ -204,7 +146,7 @@ public class MyOpMode extends LinearOpMode {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Test Locally (No Robot!)
|
### 3. Test Locally (No Robot!)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew test
|
./gradlew test
|
||||||
@@ -212,7 +154,7 @@ public class MyOpMode extends LinearOpMode {
|
|||||||
|
|
||||||
Write unit tests in `src/test/java/robot/` that run on your PC. No need to deploy to a robot for every code change!
|
Write unit tests in `src/test/java/robot/` that run on your PC. No need to deploy to a robot for every code change!
|
||||||
|
|
||||||
### 5. Deploy to Robot
|
### 4. Deploy to Robot
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build APK
|
# Build APK
|
||||||
@@ -230,93 +172,8 @@ Write unit tests in `src/test/java/robot/` that run on your PC. No need to deplo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Android Studio Setup
|
|
||||||
|
|
||||||
### Opening a Weevil Project
|
|
||||||
|
|
||||||
1. Launch Android Studio
|
|
||||||
2. Choose **Open** (not "New Project")
|
|
||||||
3. Navigate to your project directory (e.g., `my-robot`)
|
|
||||||
4. Click OK
|
|
||||||
|
|
||||||
Android Studio will index the project. After a few seconds, you'll see:
|
|
||||||
- **Clean file tree** - Only `src/`, scripts, and essential files visible
|
|
||||||
- **Run configurations** - Dropdown next to the green play button shows:
|
|
||||||
- **Build** - Builds APK without deploying
|
|
||||||
- **Deploy (auto)** - Auto-detects USB or WiFi
|
|
||||||
- **Deploy (USB)** - Forces USB connection
|
|
||||||
- **Deploy (WiFi)** - Forces WiFi connection
|
|
||||||
- **Test** - Runs unit tests
|
|
||||||
|
|
||||||
### First-Time Setup: Shell Script Plugin
|
|
||||||
|
|
||||||
**Important:** Android Studio requires the Shell Script plugin to run Weevil's deployment scripts.
|
|
||||||
|
|
||||||
1. Go to **File → Settings** (or **Ctrl+Alt+S**)
|
|
||||||
2. Navigate to **Plugins**
|
|
||||||
3. Click the **Marketplace** tab
|
|
||||||
4. Search for **"Shell Script"**
|
|
||||||
5. Install the plugin (by JetBrains)
|
|
||||||
6. Restart Android Studio
|
|
||||||
|
|
||||||
After restart, the run configurations will work.
|
|
||||||
|
|
||||||
### Running from Android Studio
|
|
||||||
|
|
||||||
1. Select a configuration from the dropdown (e.g., "Deploy (auto)")
|
|
||||||
2. Click the green play button (▶) or press **Shift+F10**
|
|
||||||
3. Watch the output in the Run panel at the bottom
|
|
||||||
|
|
||||||
**That's it!** Students can now build and deploy without leaving the IDE.
|
|
||||||
|
|
||||||
### Platform Notes
|
|
||||||
|
|
||||||
- **Linux/macOS:** Uses the Unix run configurations (`.sh` scripts)
|
|
||||||
- **Windows:** Uses the Windows run configurations (`.bat` scripts)
|
|
||||||
- Android Studio automatically hides the configurations for the other platform
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
|
|
||||||
### Proxy Configuration
|
|
||||||
|
|
||||||
#### Corporate Environments
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Set proxy for all Weevil operations
|
|
||||||
weevil --proxy http://proxy.company.com:8080 setup
|
|
||||||
weevil --proxy http://proxy.company.com:8080 new robot-project
|
|
||||||
|
|
||||||
# Or use environment variables (auto-detected)
|
|
||||||
export HTTPS_PROXY=http://proxy:8080
|
|
||||||
export HTTP_PROXY=http://proxy:8080
|
|
||||||
weevil setup # Automatically uses proxy
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Air-Gapped / Offline Installation
|
|
||||||
|
|
||||||
If you're on an isolated network without internet:
|
|
||||||
|
|
||||||
1. **Download SDKs manually on a connected machine:**
|
|
||||||
- FTC SDK: `git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git`
|
|
||||||
- Android SDK: Download from https://developer.android.com/studio
|
|
||||||
- Gradle: Download distribution from https://gradle.org/releases/
|
|
||||||
|
|
||||||
2. **Transfer to isolated machine via USB drive**
|
|
||||||
|
|
||||||
3. **Install using local paths:**
|
|
||||||
```bash
|
|
||||||
weevil setup --ftc-sdk /path/to/FtcRobotController --android-sdk /path/to/android-sdk
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Bypass Proxy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Force direct connection (ignore proxy environment variables)
|
|
||||||
weevil --no-proxy setup
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multiple SDK Versions
|
### Multiple SDK Versions
|
||||||
|
|
||||||
Working with multiple SDK versions? No problem:
|
Working with multiple SDK versions? No problem:
|
||||||
@@ -346,27 +203,10 @@ This updates:
|
|||||||
- Build scripts
|
- Build scripts
|
||||||
- Deployment scripts
|
- Deployment scripts
|
||||||
- Gradle configuration
|
- Gradle configuration
|
||||||
- Android Studio run configurations
|
|
||||||
- Project templates
|
- Project templates
|
||||||
|
|
||||||
**Your code in `src/` is never touched.**
|
**Your code in `src/` is never touched.**
|
||||||
|
|
||||||
### System Maintenance
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check what's installed
|
|
||||||
weevil doctor
|
|
||||||
|
|
||||||
# See what can be uninstalled
|
|
||||||
weevil uninstall --dry-run
|
|
||||||
|
|
||||||
# Remove specific components
|
|
||||||
weevil uninstall --only 1 # Removes FTC SDK only
|
|
||||||
|
|
||||||
# Full uninstall (removes everything Weevil installed)
|
|
||||||
weevil uninstall
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cross-Platform Development
|
### Cross-Platform Development
|
||||||
|
|
||||||
All scripts work on Windows, Linux, and macOS:
|
All scripts work on Windows, Linux, and macOS:
|
||||||
@@ -380,11 +220,9 @@ All scripts work on Windows, Linux, and macOS:
|
|||||||
**Windows:**
|
**Windows:**
|
||||||
```cmd
|
```cmd
|
||||||
build.bat
|
build.bat
|
||||||
deploy.bat
|
deploy.bat --wifi
|
||||||
```
|
```
|
||||||
|
|
||||||
**Android Studio:** Works identically on all platforms
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Configuration
|
## Project Configuration
|
||||||
@@ -393,10 +231,9 @@ Each project has a `.weevil.toml` file:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
project_name = "my-robot"
|
project_name = "my-robot"
|
||||||
weevil_version = "1.1.0"
|
weevil_version = "1.0.0"
|
||||||
ftc_sdk_path = "/home/user/.weevil/ftc-sdk"
|
ftc_sdk_path = "/home/user/.weevil/ftc-sdk"
|
||||||
ftc_sdk_version = "v10.1.1"
|
ftc_sdk_version = "v10.1.1"
|
||||||
android_sdk_path = "/home/user/.weevil/android-sdk"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can edit this manually or use:
|
You can edit this manually or use:
|
||||||
@@ -436,7 +273,6 @@ git push
|
|||||||
1. **Unit Tests** - Test business logic on your PC
|
1. **Unit Tests** - Test business logic on your PC
|
||||||
```bash
|
```bash
|
||||||
./gradlew test
|
./gradlew test
|
||||||
# Or from Android Studio: select "Test" and click Run
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Integration Tests** - Test on actual hardware
|
2. **Integration Tests** - Test on actual hardware
|
||||||
@@ -457,7 +293,7 @@ cd robot
|
|||||||
# Check SDK location
|
# Check SDK location
|
||||||
weevil config .
|
weevil config .
|
||||||
|
|
||||||
# Set SDK to local path (if different from .weevil.toml)
|
# Set SDK to local path
|
||||||
weevil config . --set-sdk ~/ftc-sdk
|
weevil config . --set-sdk ~/ftc-sdk
|
||||||
|
|
||||||
# Build and deploy
|
# Build and deploy
|
||||||
@@ -465,29 +301,15 @@ weevil config . --set-sdk ~/ftc-sdk
|
|||||||
./deploy.sh
|
./deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Android Studio users:** Just open the project. The `.idea/` folder contains all run configurations.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Command Reference
|
## Command Reference
|
||||||
|
|
||||||
### Environment Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| `weevil doctor` | Check system health and dependencies |
|
|
||||||
| `weevil setup` | Install FTC SDK, Android SDK, and dependencies |
|
|
||||||
| `weevil setup --ftc-sdk <path>` | Install to custom FTC SDK location |
|
|
||||||
| `weevil uninstall` | Remove all Weevil-managed components |
|
|
||||||
| `weevil uninstall --dry-run` | Show what would be removed |
|
|
||||||
| `weevil uninstall --only <N>` | Remove specific component by index |
|
|
||||||
|
|
||||||
### Project Commands
|
### Project Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `weevil new <name>` | Create new FTC project |
|
| `weevil new <name>` | Create new FTC project |
|
||||||
| `weevil new <name> --ftc-sdk <path>` | Create with specific SDK |
|
|
||||||
| `weevil upgrade <path>` | Update project infrastructure |
|
| `weevil upgrade <path>` | Update project infrastructure |
|
||||||
| `weevil config <path>` | View project configuration |
|
| `weevil config <path>` | View project configuration |
|
||||||
| `weevil config <path> --set-sdk <sdk>` | Change FTC SDK path |
|
| `weevil config <path> --set-sdk <sdk>` | Change FTC SDK path |
|
||||||
@@ -500,13 +322,6 @@ weevil config . --set-sdk ~/ftc-sdk
|
|||||||
| `weevil sdk install` | Download and install SDKs |
|
| `weevil sdk install` | Download and install SDKs |
|
||||||
| `weevil sdk update` | Update SDKs to latest versions |
|
| `weevil sdk update` | Update SDKs to latest versions |
|
||||||
|
|
||||||
### Global Flags
|
|
||||||
|
|
||||||
| Flag | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `--proxy <url>` | Use HTTP proxy for all network operations |
|
|
||||||
| `--no-proxy` | Bypass proxy (ignore HTTPS_PROXY env vars) |
|
|
||||||
|
|
||||||
### Deployment Options
|
### Deployment Options
|
||||||
|
|
||||||
**`deploy.sh` / `deploy.bat` flags:**
|
**`deploy.sh` / `deploy.bat` flags:**
|
||||||
@@ -528,7 +343,6 @@ weevil config . --set-sdk ~/ftc-sdk
|
|||||||
- Creates standalone Java project structure
|
- Creates standalone Java project structure
|
||||||
- Generates Gradle build files that reference FTC SDK
|
- Generates Gradle build files that reference FTC SDK
|
||||||
- Sets up deployment scripts
|
- Sets up deployment scripts
|
||||||
- Creates Android Studio run configurations
|
|
||||||
|
|
||||||
2. **Build Process**
|
2. **Build Process**
|
||||||
- Runs `deployToSDK` Gradle task
|
- Runs `deployToSDK` Gradle task
|
||||||
@@ -541,18 +355,12 @@ weevil config . --set-sdk ~/ftc-sdk
|
|||||||
- Connects to Control Hub (USB or WiFi)
|
- Connects to Control Hub (USB or WiFi)
|
||||||
- Installs APK using `adb`
|
- Installs APK using `adb`
|
||||||
|
|
||||||
4. **Proxy Support**
|
|
||||||
- reqwest HTTP client respects `--proxy` flag and HTTPS_PROXY env vars
|
|
||||||
- git2/libgit2 gets temporary proxy env vars during clone/fetch
|
|
||||||
- Gradle wrapper reads HTTPS_PROXY natively
|
|
||||||
|
|
||||||
### Why This Approach?
|
### Why This Approach?
|
||||||
|
|
||||||
**Separation of Concerns:**
|
**Separation of Concerns:**
|
||||||
- Your code: `my-robot/src/`
|
- Your code: `my-robot/src/`
|
||||||
- Build infrastructure: `my-robot/*.gradle.kts`
|
- Build infrastructure: `my-robot/*.gradle.kts`
|
||||||
- FTC SDK: System-level installation
|
- FTC SDK: System-level installation
|
||||||
- IDE integration: Auto-generated, auto-upgraded
|
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
- Test code without SDK complications
|
- Test code without SDK complications
|
||||||
@@ -560,7 +368,6 @@ weevil config . --set-sdk ~/ftc-sdk
|
|||||||
- SDK updates don't break your projects
|
- SDK updates don't break your projects
|
||||||
- Proper version control (no massive SDK in repo)
|
- Proper version control (no massive SDK in repo)
|
||||||
- Industry-standard project structure
|
- Industry-standard project structure
|
||||||
- Students use familiar tools (Android Studio)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -575,7 +382,6 @@ cargo test
|
|||||||
# Run specific test suites
|
# Run specific test suites
|
||||||
cargo test --test integration
|
cargo test --test integration
|
||||||
cargo test --test project_lifecycle
|
cargo test --test project_lifecycle
|
||||||
cargo test --test proxy_integration
|
|
||||||
cargo test config_tests
|
cargo test config_tests
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -586,8 +392,6 @@ cargo test config_tests
|
|||||||
- ✅ Build script generation
|
- ✅ Build script generation
|
||||||
- ✅ Upgrade workflow
|
- ✅ Upgrade workflow
|
||||||
- ✅ CLI commands
|
- ✅ CLI commands
|
||||||
- ✅ Proxy configuration and network operations
|
|
||||||
- ✅ Environment setup and health checks
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -596,11 +400,11 @@ cargo test config_tests
|
|||||||
### "FTC SDK not found"
|
### "FTC SDK not found"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check system health
|
# Check SDK status
|
||||||
weevil doctor
|
weevil sdk status
|
||||||
|
|
||||||
# Install SDK
|
# Install SDK
|
||||||
weevil setup
|
weevil sdk install
|
||||||
|
|
||||||
# Or specify custom location
|
# Or specify custom location
|
||||||
weevil new my-robot --ftc-sdk /custom/path/to/sdk
|
weevil new my-robot --ftc-sdk /custom/path/to/sdk
|
||||||
@@ -612,10 +416,6 @@ Install Android platform-tools:
|
|||||||
|
|
||||||
**Linux:**
|
**Linux:**
|
||||||
```bash
|
```bash
|
||||||
# Weevil can install it for you
|
|
||||||
weevil setup
|
|
||||||
|
|
||||||
# Or install manually
|
|
||||||
sudo apt install android-tools-adb
|
sudo apt install android-tools-adb
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -625,7 +425,7 @@ brew install android-platform-tools
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Windows:**
|
**Windows:**
|
||||||
Download Android SDK Platform Tools from Google or run `weevil setup`.
|
Download Android SDK Platform Tools from Google.
|
||||||
|
|
||||||
### "Build failed"
|
### "Build failed"
|
||||||
|
|
||||||
@@ -637,9 +437,6 @@ cd my-robot
|
|||||||
|
|
||||||
# Check SDK path
|
# Check SDK path
|
||||||
weevil config .
|
weevil config .
|
||||||
|
|
||||||
# Verify system health
|
|
||||||
weevil doctor
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### "Deploy failed - No devices"
|
### "Deploy failed - No devices"
|
||||||
@@ -654,24 +451,6 @@ weevil doctor
|
|||||||
2. Find Control Hub IP (usually 192.168.43.1 or 192.168.49.1)
|
2. Find Control Hub IP (usually 192.168.43.1 or 192.168.49.1)
|
||||||
3. Try `./deploy.sh -i <ip>`
|
3. Try `./deploy.sh -i <ip>`
|
||||||
|
|
||||||
### Android Studio: "Unknown run configuration type ShellScript"
|
|
||||||
|
|
||||||
The Shell Script plugin is not installed. See [Android Studio Setup](#android-studio-setup) for installation instructions.
|
|
||||||
|
|
||||||
### Proxy Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test proxy connectivity
|
|
||||||
weevil --proxy http://proxy:8080 sdk status
|
|
||||||
|
|
||||||
# Bypass proxy if it's causing issues
|
|
||||||
weevil --no-proxy setup
|
|
||||||
|
|
||||||
# Check environment variables
|
|
||||||
echo $HTTPS_PROXY
|
|
||||||
echo $HTTP_PROXY
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
@@ -711,7 +490,6 @@ Like the boll weevil that bores through complex cotton bolls to reach the valuab
|
|||||||
3. **Testability** - Enable TDD and proper testing workflows
|
3. **Testability** - Enable TDD and proper testing workflows
|
||||||
4. **Simplicity** - One command should do one obvious thing
|
4. **Simplicity** - One command should do one obvious thing
|
||||||
5. **Transparency** - Students should understand what's happening
|
5. **Transparency** - Students should understand what's happening
|
||||||
6. **Tool compatibility** - Work with tools students already know
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -733,33 +511,31 @@ Built with frustration at unnecessarily complex robotics frameworks, and hope th
|
|||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
**Current Version:** 1.1.0
|
**Current Version:** 1.0.0-rc1
|
||||||
|
|
||||||
**What Works:**
|
**What Works:**
|
||||||
- ✅ Project generation
|
- ✅ Project generation
|
||||||
- ✅ Cross-platform build/deploy
|
- ✅ Cross-platform build/deploy (Linux, macOS, Windows)
|
||||||
- ✅ SDK management and auto-install
|
- ✅ SDK management
|
||||||
- ✅ Configuration management
|
- ✅ Configuration management
|
||||||
- ✅ Project upgrades
|
- ✅ Project upgrades
|
||||||
- ✅ Local unit testing
|
- ✅ Local testing
|
||||||
- ✅ System diagnostics (`weevil doctor`)
|
|
||||||
- ✅ Selective uninstall
|
|
||||||
- ✅ Proxy support for corporate/air-gapped environments
|
|
||||||
- ✅ Android Studio integration with one-click deployment
|
|
||||||
|
|
||||||
**Roadmap:**
|
**Roadmap:**
|
||||||
- 📋 Package management for FTC libraries
|
- 📋 Package management for FTC libraries
|
||||||
- 📋 Template system for common robot configurations
|
- 📋 Template system for common robot configurations
|
||||||
- 📋 VS Code integration
|
- 📋 IDE integration (VS Code, IntelliJ)
|
||||||
- 📋 Team collaboration features
|
- 📋 Team collaboration features
|
||||||
- 📋 Automated testing on robot hardware
|
- 📋 Automated testing on robot hardware
|
||||||
- 📋 Multi-robot support (manage multiple Control Hubs)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Support & Contact
|
||||||
|
|
||||||
**Questions? Issues? Suggestions?**
|
**Questions? Issues? Suggestions?**
|
||||||
|
|
||||||
📧 Email: [eric@nxlearn.net](mailto:eric@nxlearn.net)
|
- 📧 Email: [eric@nxws.dev](mailto:eric@nxws.dev)
|
||||||
🐛 Issues: Open an issue on the repository
|
- 🐛 Issues: Open an issue on the repository
|
||||||
|
- 💬 Community: Reach out via the FTC community
|
||||||
|
|
||||||
Building better tools so you can build better robots. 🤖
|
Building better tools so you can build better robots. 🤖
|
||||||
755
diff.txt
755
diff.txt
@@ -1,755 +0,0 @@
|
|||||||
diff --git i/src/commands/new.rs w/src/commands/new.rs
|
|
||||||
index aeed30a..4d219c6 100644
|
|
||||||
--- i/src/commands/new.rs
|
|
||||||
+++ w/src/commands/new.rs
|
|
||||||
@@ -3,12 +3,14 @@
|
|
||||||
use colored::*;
|
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
|
||||||
+use crate::sdk::proxy::ProxyConfig;
|
|
||||||
use crate::project::ProjectBuilder;
|
|
||||||
|
|
||||||
pub fn create_project(
|
|
||||||
name: &str,
|
|
||||||
ftc_sdk: Option<&str>,
|
|
||||||
android_sdk: Option<&str>,
|
|
||||||
+ proxy: &ProxyConfig,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Validate project name
|
|
||||||
if name.is_empty() {
|
|
||||||
@@ -32,6 +34,7 @@ pub fn create_project(
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{}", format!("Creating FTC project: {}", name).bright_green().bold());
|
|
||||||
+ proxy.print_status();
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Check system health FIRST
|
|
||||||
diff --git i/src/commands/sdk.rs w/src/commands/sdk.rs
|
|
||||||
index ddc3aa6..250b844 100644
|
|
||||||
--- i/src/commands/sdk.rs
|
|
||||||
+++ w/src/commands/sdk.rs
|
|
||||||
@@ -1,18 +1,22 @@
|
|
||||||
use anyhow::Result;
|
|
||||||
use colored::*;
|
|
||||||
use crate::sdk::SdkConfig;
|
|
||||||
+use crate::sdk::proxy::ProxyConfig;
|
|
||||||
|
|
||||||
-pub fn install_sdks() -> Result<()> {
|
|
||||||
+pub fn install_sdks(proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
println!("{}", "Installing SDKs...".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
+ proxy.print_status();
|
|
||||||
+ println!();
|
|
||||||
+
|
|
||||||
let config = SdkConfig::new()?;
|
|
||||||
|
|
||||||
// Install FTC SDK
|
|
||||||
- crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?;
|
|
||||||
+ crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path, proxy)?;
|
|
||||||
|
|
||||||
// Install Android SDK
|
|
||||||
- crate::sdk::android::install(&config.android_sdk_path)?;
|
|
||||||
+ crate::sdk::android::install(&config.android_sdk_path, proxy)?;
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{} All SDKs installed successfully", "✓".green().bold());
|
|
||||||
@@ -44,17 +48,20 @@ pub fn show_status() -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
-pub fn update_sdks() -> Result<()> {
|
|
||||||
+pub fn update_sdks(proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
println!("{}", "Updating SDKs...".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
+
|
|
||||||
+ proxy.print_status();
|
|
||||||
+ println!();
|
|
||||||
|
|
||||||
let config = SdkConfig::new()?;
|
|
||||||
|
|
||||||
// Update FTC SDK
|
|
||||||
- crate::sdk::ftc::update(&config.ftc_sdk_path)?;
|
|
||||||
+ crate::sdk::ftc::update(&config.ftc_sdk_path, proxy)?;
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{} SDKs updated successfully", "✓".green().bold());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
-}
|
|
||||||
+}
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git i/src/commands/setup.rs w/src/commands/setup.rs
|
|
||||||
index 975b814..cbb5871 100644
|
|
||||||
--- i/src/commands/setup.rs
|
|
||||||
+++ w/src/commands/setup.rs
|
|
||||||
@@ -4,22 +4,26 @@
|
|
||||||
use colored::*;
|
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
|
||||||
+use crate::sdk::proxy::ProxyConfig;
|
|
||||||
use crate::project::ProjectConfig;
|
|
||||||
|
|
||||||
/// Setup development environment - either system-wide or for a specific project
|
|
||||||
-pub fn setup_environment(project_path: Option<&str>) -> Result<()> {
|
|
||||||
+pub fn setup_environment(project_path: Option<&str>, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
match project_path {
|
|
||||||
- Some(path) => setup_project(path),
|
|
||||||
- None => setup_system(),
|
|
||||||
+ Some(path) => setup_project(path, proxy),
|
|
||||||
+ None => setup_system(proxy),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup system-wide development environment with default SDKs
|
|
||||||
-fn setup_system() -> Result<()> {
|
|
||||||
+fn setup_system(proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!("{}", " System Setup - Preparing FTC Development Environment".bright_cyan().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
+
|
|
||||||
+ proxy.print_status();
|
|
||||||
+ println!();
|
|
||||||
|
|
||||||
let mut issues = Vec::new();
|
|
||||||
let mut installed = Vec::new();
|
|
||||||
@@ -57,18 +61,34 @@ fn setup_system() -> Result<()> {
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
+ match crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path, proxy) {
|
|
||||||
+ Ok(_) => {
|
|
||||||
+ 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));
|
|
||||||
+ }
|
|
||||||
+ Err(e) => {
|
|
||||||
+ println!("{} {}", "✗".red(), e);
|
|
||||||
+ print_ftc_manual_fallback(&sdk_config);
|
|
||||||
+ issues.push(("FTC SDK", "See manual installation instructions above.".to_string()));
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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));
|
|
||||||
+ match crate::sdk::ftc::install(&sdk_config.ftc_sdk_path, &sdk_config.android_sdk_path, proxy) {
|
|
||||||
+ Ok(_) => {
|
|
||||||
+ 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));
|
|
||||||
+ }
|
|
||||||
+ Err(e) => {
|
|
||||||
+ println!("{} {}", "✗".red(), e);
|
|
||||||
+ print_ftc_manual_fallback(&sdk_config);
|
|
||||||
+ issues.push(("FTC SDK", "See manual installation instructions above.".to_string()));
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
|
|
||||||
@@ -85,14 +105,26 @@ fn setup_system() -> Result<()> {
|
|
||||||
}
|
|
||||||
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());
|
|
||||||
+ match crate::sdk::android::install(&sdk_config.android_sdk_path, proxy) {
|
|
||||||
+ Ok(_) => installed.push("Android SDK (installed)".to_string()),
|
|
||||||
+ Err(e) => {
|
|
||||||
+ println!("{} {}", "✗".red(), e);
|
|
||||||
+ print_android_manual_fallback(&sdk_config);
|
|
||||||
+ issues.push(("Android SDK", "See manual installation instructions above.".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());
|
|
||||||
+ match crate::sdk::android::install(&sdk_config.android_sdk_path, proxy) {
|
|
||||||
+ Ok(_) => installed.push("Android SDK (installed)".to_string()),
|
|
||||||
+ Err(e) => {
|
|
||||||
+ println!("{} {}", "✗".red(), e);
|
|
||||||
+ print_android_manual_fallback(&sdk_config);
|
|
||||||
+ issues.push(("Android SDK", "See manual installation instructions above.".to_string()));
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
|
|
||||||
@@ -132,7 +164,7 @@ fn setup_system() -> Result<()> {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup dependencies for a specific project by reading its .weevil.toml
|
|
||||||
-fn setup_project(project_path: &str) -> Result<()> {
|
|
||||||
+fn setup_project(project_path: &str, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
let project_path = PathBuf::from(project_path);
|
|
||||||
|
|
||||||
if !project_path.exists() {
|
|
||||||
@@ -143,6 +175,9 @@ fn setup_project(project_path: &str) -> Result<()> {
|
|
||||||
println!("{}", " Project Setup - Installing Dependencies".bright_cyan().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
+
|
|
||||||
+ proxy.print_status();
|
|
||||||
+ println!();
|
|
||||||
|
|
||||||
// Load project configuration
|
|
||||||
println!("{}", "Reading project configuration...".bright_yellow());
|
|
||||||
@@ -214,7 +249,7 @@ fn setup_project(project_path: &str) -> Result<()> {
|
|
||||||
|
|
||||||
// Try to install it automatically
|
|
||||||
println!("{}", "Attempting automatic installation...".bright_yellow());
|
|
||||||
- match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path) {
|
|
||||||
+ match crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path, proxy) {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("{} FTC SDK {} installed successfully",
|
|
||||||
"✓".green(),
|
|
||||||
@@ -224,13 +259,13 @@ fn setup_project(project_path: &str) -> Result<()> {
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
+ let sdk_config = SdkConfig {
|
|
||||||
+ ftc_sdk_path: config.ftc_sdk_path.clone(),
|
|
||||||
+ android_sdk_path: config.android_sdk_path.clone(),
|
|
||||||
+ cache_dir: dirs::home_dir().unwrap_or_default().join(".weevil"),
|
|
||||||
+ };
|
|
||||||
+ print_ftc_manual_fallback(&sdk_config);
|
|
||||||
+ issues.push(("FTC SDK", "See manual installation instructions above.".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -249,14 +284,36 @@ fn setup_project(project_path: &str) -> Result<()> {
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
println!("{} Android SDK found but incomplete, reinstalling...", "⚠".yellow());
|
|
||||||
- crate::sdk::android::install(&config.android_sdk_path)?;
|
|
||||||
- installed.push("Android SDK (installed)".to_string());
|
|
||||||
+ match crate::sdk::android::install(&config.android_sdk_path, proxy) {
|
|
||||||
+ Ok(_) => installed.push("Android SDK (installed)".to_string()),
|
|
||||||
+ Err(e) => {
|
|
||||||
+ println!("{} {}", "✗".red(), e);
|
|
||||||
+ let sdk_config = SdkConfig {
|
|
||||||
+ ftc_sdk_path: config.ftc_sdk_path.clone(),
|
|
||||||
+ android_sdk_path: config.android_sdk_path.clone(),
|
|
||||||
+ cache_dir: dirs::home_dir().unwrap_or_default().join(".weevil"),
|
|
||||||
+ };
|
|
||||||
+ print_android_manual_fallback(&sdk_config);
|
|
||||||
+ issues.push(("Android SDK", "See manual installation instructions above.".to_string()));
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("Android SDK not found. Installing...");
|
|
||||||
- crate::sdk::android::install(&config.android_sdk_path)?;
|
|
||||||
- installed.push("Android SDK (installed)".to_string());
|
|
||||||
+ match crate::sdk::android::install(&config.android_sdk_path, proxy) {
|
|
||||||
+ Ok(_) => installed.push("Android SDK (installed)".to_string()),
|
|
||||||
+ Err(e) => {
|
|
||||||
+ println!("{} {}", "✗".red(), e);
|
|
||||||
+ let sdk_config = SdkConfig {
|
|
||||||
+ ftc_sdk_path: config.ftc_sdk_path.clone(),
|
|
||||||
+ android_sdk_path: config.android_sdk_path.clone(),
|
|
||||||
+ cache_dir: dirs::home_dir().unwrap_or_default().join(".weevil"),
|
|
||||||
+ };
|
|
||||||
+ print_android_manual_fallback(&sdk_config);
|
|
||||||
+ issues.push(("Android SDK", "See manual installation instructions above.".to_string()));
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
|
|
||||||
@@ -511,4 +568,147 @@ fn print_project_summary(installed: &[String], issues: &[(&str, String)], config
|
|
||||||
println!(" Then run {} to verify", format!("weevil setup {}", project_path.display()).bright_white());
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
+// Manual fallback instructions — printed when automatic install fails.
|
|
||||||
+// These walk the user through doing everything by hand, with explicit steps
|
|
||||||
+// for Linux, macOS, and Windows.
|
|
||||||
+// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
+
|
|
||||||
+fn print_ftc_manual_fallback(sdk_config: &SdkConfig) {
|
|
||||||
+ let dest = sdk_config.ftc_sdk_path.display();
|
|
||||||
+
|
|
||||||
+ println!();
|
|
||||||
+ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow());
|
|
||||||
+ println!("{}", " Manual FTC SDK Installation".bright_yellow().bold());
|
|
||||||
+ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow());
|
|
||||||
+ println!();
|
|
||||||
+ println!(" Automatic installation failed. Follow the steps below to");
|
|
||||||
+ println!(" clone the FTC SDK by hand. If you are behind a proxy, set");
|
|
||||||
+ println!(" the environment variables shown before running git.");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" Target directory: {}", dest);
|
|
||||||
+ println!();
|
|
||||||
+
|
|
||||||
+ println!(" {} Linux / macOS:", "▸".bright_cyan());
|
|
||||||
+ println!(" # If behind a proxy, set these first (replace with your proxy):");
|
|
||||||
+ println!(" export HTTPS_PROXY=http://your-proxy:3128");
|
|
||||||
+ println!(" export HTTP_PROXY=http://your-proxy:3128");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" # If the proxy uses a custom CA certificate, add:");
|
|
||||||
+ println!(" export GIT_SSL_CAPATH=/path/to/ca-certificates");
|
|
||||||
+ println!(" # (ask your IT department for the CA cert if needed)");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git \\");
|
|
||||||
+ println!(" {}", dest);
|
|
||||||
+ println!(" cd {}", dest);
|
|
||||||
+ println!(" git checkout v10.1.1");
|
|
||||||
+ println!();
|
|
||||||
+
|
|
||||||
+ println!(" {} Windows (PowerShell):", "▸".bright_cyan());
|
|
||||||
+ println!(" # If behind a proxy, set these first:");
|
|
||||||
+ println!(" $env:HTTPS_PROXY = \"http://your-proxy:3128\"");
|
|
||||||
+ println!(" $env:HTTP_PROXY = \"http://your-proxy:3128\"");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" # If the proxy uses a custom CA certificate:");
|
|
||||||
+ println!(" git config --global http.sslCAInfo C:\\path\\to\\ca-bundle.crt");
|
|
||||||
+ println!(" # (ask your IT department for the CA cert if needed)");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git `");
|
|
||||||
+ println!(" {}", dest);
|
|
||||||
+ println!(" cd {}", dest);
|
|
||||||
+ println!(" git checkout v10.1.1");
|
|
||||||
+ println!();
|
|
||||||
+
|
|
||||||
+ println!(" Once done, run {} again.", "weevil setup".bright_white());
|
|
||||||
+ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow());
|
|
||||||
+ println!();
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
+fn print_android_manual_fallback(sdk_config: &SdkConfig) {
|
|
||||||
+ let dest = sdk_config.android_sdk_path.display();
|
|
||||||
+
|
|
||||||
+ // Pick the right download URL for the current OS
|
|
||||||
+ let (url, extract_note) = if cfg!(target_os = "windows") {
|
|
||||||
+ (
|
|
||||||
+ "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip",
|
|
||||||
+ "Extract the zip. You will get a cmdline-tools/ folder."
|
|
||||||
+ )
|
|
||||||
+ } else if cfg!(target_os = "macos") {
|
|
||||||
+ (
|
|
||||||
+ "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip",
|
|
||||||
+ "Extract the zip. You will get a cmdline-tools/ folder."
|
|
||||||
+ )
|
|
||||||
+ } else {
|
|
||||||
+ (
|
|
||||||
+ "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip",
|
|
||||||
+ "Extract the zip. You will get a cmdline-tools/ folder."
|
|
||||||
+ )
|
|
||||||
+ };
|
|
||||||
+
|
|
||||||
+ println!();
|
|
||||||
+ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow());
|
|
||||||
+ println!("{}", " Manual Android SDK Installation".bright_yellow().bold());
|
|
||||||
+ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow());
|
|
||||||
+ println!();
|
|
||||||
+ println!(" Automatic installation failed. Follow the steps below to");
|
|
||||||
+ println!(" download and set up the Android SDK by hand.");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" Target directory: {}", dest);
|
|
||||||
+ println!(" Download URL: {}", url);
|
|
||||||
+ println!();
|
|
||||||
+
|
|
||||||
+ println!(" {} Linux / macOS:", "▸".bright_cyan());
|
|
||||||
+ println!(" # If behind a proxy, set these first:");
|
|
||||||
+ println!(" export HTTPS_PROXY=http://your-proxy:3128");
|
|
||||||
+ println!(" export HTTP_PROXY=http://your-proxy:3128");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" # If the proxy uses a custom CA cert, add:");
|
|
||||||
+ println!(" export CURL_CA_BUNDLE=/path/to/ca-bundle.crt");
|
|
||||||
+ println!(" # (ask your IT department for the CA cert if needed)");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" mkdir -p {}", dest);
|
|
||||||
+ println!(" cd {}", dest);
|
|
||||||
+ println!(" curl -L -o cmdline-tools.zip \\");
|
|
||||||
+ println!(" \"{}\"", url);
|
|
||||||
+ println!(" unzip cmdline-tools.zip");
|
|
||||||
+ println!(" # {} Move into the expected layout:", extract_note);
|
|
||||||
+ println!(" mv cmdline-tools cmdline-tools-temp");
|
|
||||||
+ println!(" mkdir -p cmdline-tools/latest");
|
|
||||||
+ println!(" mv cmdline-tools-temp/* cmdline-tools/latest/");
|
|
||||||
+ println!(" rmdir cmdline-tools-temp");
|
|
||||||
+ println!(" # Accept licenses and install packages:");
|
|
||||||
+ println!(" ./cmdline-tools/latest/bin/sdkmanager --licenses");
|
|
||||||
+ println!(" ./cmdline-tools/latest/bin/sdkmanager platform-tools \"platforms;android-34\" \"build-tools;34.0.0\"");
|
|
||||||
+ println!();
|
|
||||||
+
|
|
||||||
+ println!(" {} Windows (PowerShell):", "▸".bright_cyan());
|
|
||||||
+ println!(" # If behind a proxy, set these first:");
|
|
||||||
+ println!(" $env:HTTPS_PROXY = \"http://your-proxy:3128\"");
|
|
||||||
+ println!(" $env:HTTP_PROXY = \"http://your-proxy:3128\"");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" # If the proxy uses a custom CA cert:");
|
|
||||||
+ println!(" # Download the CA cert from your IT department and note its path.");
|
|
||||||
+ println!(" # PowerShell's Invoke-WebRequest will use the system cert store;");
|
|
||||||
+ println!(" # you may need to import the cert: ");
|
|
||||||
+ println!(" # Import-Certificate -FilePath C:\\path\\to\\ca.crt -CertStoreLocation Cert:\\LocalMachine\\Root");
|
|
||||||
+ println!();
|
|
||||||
+ println!(" New-Item -ItemType Directory -Path \"{}\" -Force", dest);
|
|
||||||
+ println!(" cd \"{}\"", dest);
|
|
||||||
+ println!(" Invoke-WebRequest -Uri \"{}\" -OutFile cmdline-tools.zip", url);
|
|
||||||
+ println!(" Expand-Archive -Path cmdline-tools.zip -DestinationPath .");
|
|
||||||
+ println!(" # Move into the expected layout:");
|
|
||||||
+ println!(" Rename-Item cmdline-tools cmdline-tools-temp");
|
|
||||||
+ println!(" New-Item -ItemType Directory -Path cmdline-tools\\latest");
|
|
||||||
+ println!(" Move-Item cmdline-tools-temp\\* cmdline-tools\\latest\\");
|
|
||||||
+ println!(" Remove-Item cmdline-tools-temp");
|
|
||||||
+ println!(" # Accept licenses and install packages:");
|
|
||||||
+ println!(" .\\cmdline-tools\\latest\\bin\\sdkmanager.bat --licenses");
|
|
||||||
+ println!(" .\\cmdline-tools\\latest\\bin\\sdkmanager.bat platform-tools \"platforms;android-34\" \"build-tools;34.0.0\"");
|
|
||||||
+ println!();
|
|
||||||
+
|
|
||||||
+ println!(" Once done, run {} again.", "weevil setup".bright_white());
|
|
||||||
+ println!("{}", "═══════════════════════════════════════════════════════════".bright_yellow());
|
|
||||||
+ println!();
|
|
||||||
}
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git i/src/main.rs w/src/main.rs
|
|
||||||
index aa8fce4..35d11f7 100644
|
|
||||||
--- i/src/main.rs
|
|
||||||
+++ w/src/main.rs
|
|
||||||
@@ -35,6 +35,14 @@ enum Commands {
|
|
||||||
/// Path to Android SDK (optional, will auto-detect or download)
|
|
||||||
#[arg(long)]
|
|
||||||
android_sdk: Option<String>,
|
|
||||||
+
|
|
||||||
+ /// Use this proxy for all network operations (e.g. http://proxy:3128)
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ proxy: Option<String>,
|
|
||||||
+
|
|
||||||
+ /// Force direct connection, ignoring proxy env vars
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ no_proxy: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Check system health and diagnose issues
|
|
||||||
@@ -44,6 +52,14 @@ enum Commands {
|
|
||||||
Setup {
|
|
||||||
/// Path to project directory (optional - without it, sets up system)
|
|
||||||
path: Option<String>,
|
|
||||||
+
|
|
||||||
+ /// Use this proxy for all network operations (e.g. http://proxy:3128)
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ proxy: Option<String>,
|
|
||||||
+
|
|
||||||
+ /// Force direct connection, ignoring proxy env vars
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ no_proxy: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove Weevil-installed SDKs and dependencies
|
|
||||||
@@ -85,6 +101,14 @@ enum Commands {
|
|
||||||
Sdk {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: SdkCommands,
|
|
||||||
+
|
|
||||||
+ /// Use this proxy for all network operations (e.g. http://proxy:3128)
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ proxy: Option<String>,
|
|
||||||
+
|
|
||||||
+ /// Force direct connection, ignoring proxy env vars
|
|
||||||
+ #[arg(long)]
|
|
||||||
+ no_proxy: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Show or update project configuration
|
|
||||||
@@ -120,14 +144,16 @@ fn main() -> Result<()> {
|
|
||||||
print_banner();
|
|
||||||
|
|
||||||
match cli.command {
|
|
||||||
- Commands::New { name, ftc_sdk, android_sdk } => {
|
|
||||||
- commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref())
|
|
||||||
+ Commands::New { name, ftc_sdk, android_sdk, proxy, no_proxy } => {
|
|
||||||
+ let proxy_config = weevil::sdk::proxy::ProxyConfig::resolve(proxy.as_deref(), no_proxy)?;
|
|
||||||
+ commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref(), &proxy_config)
|
|
||||||
}
|
|
||||||
Commands::Doctor => {
|
|
||||||
commands::doctor::run_diagnostics()
|
|
||||||
}
|
|
||||||
- Commands::Setup { path } => {
|
|
||||||
- commands::setup::setup_environment(path.as_deref())
|
|
||||||
+ Commands::Setup { path, proxy, no_proxy } => {
|
|
||||||
+ let proxy_config = weevil::sdk::proxy::ProxyConfig::resolve(proxy.as_deref(), no_proxy)?;
|
|
||||||
+ commands::setup::setup_environment(path.as_deref(), &proxy_config)
|
|
||||||
}
|
|
||||||
Commands::Uninstall { dry_run, only } => {
|
|
||||||
commands::uninstall::uninstall_dependencies(dry_run, only)
|
|
||||||
@@ -138,11 +164,14 @@ fn main() -> Result<()> {
|
|
||||||
Commands::Deploy { path, usb, wifi, ip } => {
|
|
||||||
commands::deploy::deploy_project(&path, usb, wifi, ip.as_deref())
|
|
||||||
}
|
|
||||||
- Commands::Sdk { command } => match command {
|
|
||||||
- SdkCommands::Install => commands::sdk::install_sdks(),
|
|
||||||
- SdkCommands::Status => commands::sdk::show_status(),
|
|
||||||
- SdkCommands::Update => commands::sdk::update_sdks(),
|
|
||||||
- },
|
|
||||||
+ Commands::Sdk { command, proxy, no_proxy } => {
|
|
||||||
+ let proxy_config = weevil::sdk::proxy::ProxyConfig::resolve(proxy.as_deref(), no_proxy)?;
|
|
||||||
+ match command {
|
|
||||||
+ SdkCommands::Install => commands::sdk::install_sdks(&proxy_config),
|
|
||||||
+ SdkCommands::Status => commands::sdk::show_status(),
|
|
||||||
+ SdkCommands::Update => commands::sdk::update_sdks(&proxy_config),
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
Commands::Config { path, set_sdk } => {
|
|
||||||
if let Some(sdk_path) = set_sdk {
|
|
||||||
commands::config::set_sdk(&path, &sdk_path)
|
|
||||||
@@ -164,4 +193,4 @@ fn print_banner() {
|
|
||||||
println!("{}", " Nexus Workshops LLC".bright_cyan());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
-}
|
|
||||||
+}
|
|
||||||
\ No newline at end of file
|
|
||||||
diff --git i/src/sdk/android.rs w/src/sdk/android.rs
|
|
||||||
index 596ed74..b91701e 100644
|
|
||||||
--- i/src/sdk/android.rs
|
|
||||||
+++ w/src/sdk/android.rs
|
|
||||||
@@ -6,11 +6,13 @@
|
|
||||||
use std::io::Write;
|
|
||||||
use colored::*;
|
|
||||||
|
|
||||||
+use super::proxy::ProxyConfig;
|
|
||||||
+
|
|
||||||
const ANDROID_SDK_URL_LINUX: &str = "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip";
|
|
||||||
const ANDROID_SDK_URL_MAC: &str = "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip";
|
|
||||||
const ANDROID_SDK_URL_WINDOWS: &str = "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip";
|
|
||||||
|
|
||||||
-pub fn install(sdk_path: &Path) -> Result<()> {
|
|
||||||
+pub fn install(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
// Check if SDK exists AND is complete
|
|
||||||
if sdk_path.exists() {
|
|
||||||
match verify(sdk_path) {
|
|
||||||
@@ -42,10 +44,20 @@ pub fn install(sdk_path: &Path) -> Result<()> {
|
|
||||||
|
|
||||||
// Download
|
|
||||||
println!("Downloading from: {}", url);
|
|
||||||
- let client = Client::new();
|
|
||||||
+ let client = match &proxy.url {
|
|
||||||
+ Some(proxy_url) => {
|
|
||||||
+ println!(" via proxy: {}", proxy_url);
|
|
||||||
+ Client::builder()
|
|
||||||
+ .proxy(reqwest::Proxy::all(proxy_url.clone())
|
|
||||||
+ .context("Failed to configure proxy")?)
|
|
||||||
+ .build()
|
|
||||||
+ .context("Failed to build HTTP client")?
|
|
||||||
+ }
|
|
||||||
+ None => Client::new(),
|
|
||||||
+ };
|
|
||||||
let response = client.get(url)
|
|
||||||
.send()
|
|
||||||
- .context("Failed to download Android SDK")?;
|
|
||||||
+ .context("Failed to download Android SDK. If you are behind a proxy, try --proxy <url> or set HTTPS_PROXY")?;
|
|
||||||
|
|
||||||
let total_size = response.content_length().unwrap_or(0);
|
|
||||||
|
|
||||||
@@ -104,14 +116,14 @@ pub fn install(sdk_path: &Path) -> Result<()> {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install required packages
|
|
||||||
- install_packages(sdk_path)?;
|
|
||||||
+ install_packages(sdk_path, proxy)?;
|
|
||||||
|
|
||||||
println!("{} Android SDK installed successfully", "✓".green());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
-fn install_packages(sdk_path: &Path) -> Result<()> {
|
|
||||||
+fn install_packages(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
println!("Installing Android SDK packages...");
|
|
||||||
|
|
||||||
let sdkmanager_path = sdk_path.join("cmdline-tools").join("latest").join("bin");
|
|
||||||
@@ -132,10 +144,10 @@ fn install_packages(sdk_path: &Path) -> Result<()> {
|
|
||||||
|
|
||||||
println!("Found sdkmanager at: {}", sdkmanager.display());
|
|
||||||
|
|
||||||
- run_sdkmanager(&sdkmanager, sdk_path)
|
|
||||||
+ run_sdkmanager(&sdkmanager, sdk_path, proxy)
|
|
||||||
}
|
|
||||||
|
|
||||||
-fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> {
|
|
||||||
+fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
@@ -151,6 +163,13 @@ fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> {
|
|
||||||
Command::new(sdkmanager)
|
|
||||||
};
|
|
||||||
|
|
||||||
+ // Inject proxy env vars so sdkmanager picks them up
|
|
||||||
+ if let Some(proxy_url) = &proxy.url {
|
|
||||||
+ let url_str = proxy_url.as_str();
|
|
||||||
+ cmd.env("HTTPS_PROXY", url_str)
|
|
||||||
+ .env("HTTP_PROXY", url_str);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
cmd.arg(format!("--sdk_root={}", sdk_root.display()))
|
|
||||||
.arg("--licenses")
|
|
||||||
.stdin(Stdio::piped())
|
|
||||||
@@ -192,6 +211,13 @@ fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> {
|
|
||||||
} else {
|
|
||||||
Command::new(sdkmanager)
|
|
||||||
};
|
|
||||||
+
|
|
||||||
+ // Inject proxy env vars here too
|
|
||||||
+ if let Some(proxy_url) = &proxy.url {
|
|
||||||
+ let url_str = proxy_url.as_str();
|
|
||||||
+ cmd.env("HTTPS_PROXY", url_str)
|
|
||||||
+ .env("HTTP_PROXY", url_str);
|
|
||||||
+ }
|
|
||||||
|
|
||||||
let status = cmd
|
|
||||||
.arg(format!("--sdk_root={}", sdk_root.display()))
|
|
||||||
diff --git i/src/sdk/ftc.rs w/src/sdk/ftc.rs
|
|
||||||
index 778cece..3e982e8 100644
|
|
||||||
--- i/src/sdk/ftc.rs
|
|
||||||
+++ w/src/sdk/ftc.rs
|
|
||||||
@@ -4,10 +4,12 @@
|
|
||||||
use colored::*;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
+use super::proxy::ProxyConfig;
|
|
||||||
+
|
|
||||||
const FTC_SDK_URL: &str = "https://github.com/FIRST-Tech-Challenge/FtcRobotController.git";
|
|
||||||
const FTC_SDK_VERSION: &str = "v10.1.1";
|
|
||||||
|
|
||||||
-pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> {
|
|
||||||
+pub fn install(sdk_path: &Path, android_sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
if sdk_path.exists() {
|
|
||||||
println!("{} FTC SDK already installed at: {}",
|
|
||||||
"✓".green(),
|
|
||||||
@@ -22,8 +24,8 @@ pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> {
|
|
||||||
println!("Cloning from: {}", FTC_SDK_URL);
|
|
||||||
println!("Version: {}", FTC_SDK_VERSION);
|
|
||||||
|
|
||||||
- // Clone the repository
|
|
||||||
- let repo = Repository::clone(FTC_SDK_URL, sdk_path)
|
|
||||||
+ // Clone the repository, with proxy if configured
|
|
||||||
+ let repo = clone_with_proxy(FTC_SDK_URL, sdk_path, proxy)
|
|
||||||
.context("Failed to clone FTC SDK")?;
|
|
||||||
|
|
||||||
// Checkout specific version
|
|
||||||
@@ -39,6 +41,44 @@ pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
+/// Clone a git repo, injecting http.proxy into a git2 config if ProxyConfig has a URL.
|
|
||||||
+/// Returns a more helpful error message when a proxy was involved.
|
|
||||||
+fn clone_with_proxy(url: &str, dest: &Path, proxy: &ProxyConfig) -> Result<Repository> {
|
|
||||||
+ let mut opts = git2::CloneOptions::new();
|
|
||||||
+
|
|
||||||
+ if let Some(proxy_url) = &proxy.url {
|
|
||||||
+ // git2 reads http.proxy from a config object passed to the clone options.
|
|
||||||
+ // We build an in-memory config with just that one key.
|
|
||||||
+ let mut git_config = git2::Config::new()?;
|
|
||||||
+ git_config.set_str("http.proxy", proxy_url.as_str())?;
|
|
||||||
+ opts.local(false); // force network path even if URL looks local
|
|
||||||
+ // Unfortunately git2::CloneOptions doesn't have a direct .config() method,
|
|
||||||
+ // so we set the env var which libgit2 also respects as a fallback.
|
|
||||||
+ std::env::set_var("GIT_PROXY_COMMAND", ""); // clear any ssh proxy
|
|
||||||
+ std::env::set_var("HTTP_PROXY", proxy_url.as_str());
|
|
||||||
+ std::env::set_var("HTTPS_PROXY", proxy_url.as_str());
|
|
||||||
+ println!(" via proxy: {}", proxy_url);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ Repository::clone_with(url, dest, &opts).map_err(|e| {
|
|
||||||
+ if proxy.url.is_some() {
|
|
||||||
+ anyhow::anyhow!(
|
|
||||||
+ "{}\n\n\
|
|
||||||
+ This failure may be caused by your proxy. If you are behind a \
|
|
||||||
+ corporate or school network, see 'weevil setup' for manual \
|
|
||||||
+ fallback instructions.",
|
|
||||||
+ e
|
|
||||||
+ )
|
|
||||||
+ } else {
|
|
||||||
+ anyhow::anyhow!(
|
|
||||||
+ "{}\n\n\
|
|
||||||
+ If you are behind a proxy, try: weevil sdk install --proxy <url>",
|
|
||||||
+ e
|
|
||||||
+ )
|
|
||||||
+ }
|
|
||||||
+ })
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
fn create_local_properties(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> {
|
|
||||||
// Convert path to use forward slashes (works on both Windows and Unix)
|
|
||||||
let android_sdk_str = android_sdk_path
|
|
||||||
@@ -80,15 +120,39 @@ fn check_version(sdk_path: &Path) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
-pub fn update(sdk_path: &Path) -> Result<()> {
|
|
||||||
+pub fn update(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
println!("{}", "Updating FTC SDK...".bright_yellow());
|
|
||||||
|
|
||||||
+ // Set proxy env vars for the fetch if configured
|
|
||||||
+ if let Some(proxy_url) = &proxy.url {
|
|
||||||
+ std::env::set_var("HTTP_PROXY", proxy_url.as_str());
|
|
||||||
+ std::env::set_var("HTTPS_PROXY", proxy_url.as_str());
|
|
||||||
+ println!(" via proxy: {}", proxy_url);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
let repo = Repository::open(sdk_path)
|
|
||||||
.context("FTC SDK not found or not a git repository")?;
|
|
||||||
|
|
||||||
// Fetch latest
|
|
||||||
let mut remote = repo.find_remote("origin")?;
|
|
||||||
- remote.fetch(&["refs/tags/*:refs/tags/*"], None, None)?;
|
|
||||||
+ remote.fetch(&["refs/tags/*:refs/tags/*"], None, None)
|
|
||||||
+ .map_err(|e| {
|
|
||||||
+ if proxy.url.is_some() {
|
|
||||||
+ anyhow::anyhow!(
|
|
||||||
+ "Failed to fetch: {}\n\n\
|
|
||||||
+ This failure may be caused by your proxy. If you are behind a \
|
|
||||||
+ corporate or school network, see 'weevil setup' for manual \
|
|
||||||
+ fallback instructions.",
|
|
||||||
+ e
|
|
||||||
+ )
|
|
||||||
+ } else {
|
|
||||||
+ anyhow::anyhow!(
|
|
||||||
+ "Failed to fetch: {}\n\n\
|
|
||||||
+ If you are behind a proxy, try: weevil sdk update --proxy <url>",
|
|
||||||
+ e
|
|
||||||
+ )
|
|
||||||
+ }
|
|
||||||
+ })?;
|
|
||||||
|
|
||||||
// Checkout latest version
|
|
||||||
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
|
||||||
diff --git i/src/sdk/mod.rs w/src/sdk/mod.rs
|
|
||||||
index 080ce36..5d7c065 100644
|
|
||||||
--- i/src/sdk/mod.rs
|
|
||||||
+++ w/src/sdk/mod.rs
|
|
||||||
@@ -6,6 +6,7 @@
|
|
||||||
pub mod android;
|
|
||||||
pub mod ftc;
|
|
||||||
pub mod gradle;
|
|
||||||
+pub mod proxy;
|
|
||||||
|
|
||||||
pub struct SdkConfig {
|
|
||||||
pub ftc_sdk_path: PathBuf,
|
|
||||||
627
docs/ROADMAP.md
627
docs/ROADMAP.md
@@ -1,627 +0,0 @@
|
|||||||
# Weevil Roadmap
|
|
||||||
|
|
||||||
This document outlines the planned feature development for Weevil across multiple versions. Features are subject to change based on user feedback, technical constraints, and market needs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version 1.1.0 - Core Stability & Team Adoption
|
|
||||||
|
|
||||||
**Theme:** Making Weevil production-ready for FTC teams with essential operational features and reducing friction in existing workflows.
|
|
||||||
|
|
||||||
### System Audit & Diagnostics
|
|
||||||
|
|
||||||
**Feature:** `weevil status` or `weevil doctor` command
|
|
||||||
|
|
||||||
**Description:** Provides a comprehensive audit of the development environment, showing what's installed and what versions are present. This would display:
|
|
||||||
- FTC SDK versions (current and available)
|
|
||||||
- Android SDK installation status and version
|
|
||||||
- Gradle version and location
|
|
||||||
- Java/JDK version and location
|
|
||||||
- ADB availability and version
|
|
||||||
- Any other critical dependencies Weevil manages
|
|
||||||
|
|
||||||
**Rationale:** Teams need visibility into their environment to troubleshoot issues. Coaches working with multiple machines need to quickly verify setup consistency across laptops. This builds trust by making Weevil's actions transparent.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Straightforward to implement - query what `weevil setup` installed
|
|
||||||
- High value for troubleshooting
|
|
||||||
- Professional tooling feel
|
|
||||||
- Helps with team onboarding (new members can verify setup)
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Need to handle edge cases (partial installations, manual modifications)
|
|
||||||
- Version detection across platforms may be fragile
|
|
||||||
- Output formatting needs to be clear for non-technical users
|
|
||||||
|
|
||||||
**Priority:** HIGH - Essential for v1.1.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Dependency Cleanup
|
|
||||||
|
|
||||||
**Feature:** `weevil clean` or `weevil uninstall` command
|
|
||||||
|
|
||||||
**Description:** Removes dependencies that Weevil installed during setup. This includes:
|
|
||||||
- FTC SDK files
|
|
||||||
- Android SDK components (if Weevil installed them)
|
|
||||||
- Gradle distributions
|
|
||||||
- Configuration files Weevil created
|
|
||||||
|
|
||||||
Should offer options for selective cleanup (e.g., keep SDK but remove Gradle) or complete removal.
|
|
||||||
|
|
||||||
**Rationale:** Teams switch machines, need to free disk space, or want to start fresh. Without a clean uninstall, Weevil leaves artifacts behind. This is critical for maintaining system hygiene and building confidence that Weevil doesn't pollute the environment.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Demonstrates respect for users' systems
|
|
||||||
- Essential for testing and development
|
|
||||||
- Helps with troubleshooting (clean slate approach)
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Must track what Weevil installed vs. what user installed manually
|
|
||||||
- Risk of removing shared dependencies other tools need
|
|
||||||
- Need careful confirmation prompts to prevent accidental deletion
|
|
||||||
|
|
||||||
**Priority:** HIGH - Essential for v1.1.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Corporate/School Proxy Support
|
|
||||||
|
|
||||||
**Feature:** Transparent proxy configuration for all network operations
|
|
||||||
|
|
||||||
**Description:** Automatically detect and use system proxy settings (or allow manual configuration) for all network operations including:
|
|
||||||
- Gradle dependency downloads
|
|
||||||
- Android SDK downloads
|
|
||||||
- FTC SDK downloads
|
|
||||||
- Any HTTP/HTTPS requests Weevil makes
|
|
||||||
|
|
||||||
Handle `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` environment variables and write appropriate configuration into Gradle properties, Android SDK manager config, etc.
|
|
||||||
|
|
||||||
**Rationale:** Many FTC teams work in schools or corporate environments with mandatory proxy servers. Without proxy support, Weevil is unusable in these environments, cutting off a significant portion of the potential user base.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Unlocks enterprise/school environments
|
|
||||||
- Relatively well-understood problem space
|
|
||||||
- Shows professionalism and enterprise-readiness
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Proxy configurations vary widely
|
|
||||||
- Authentication (proxy username/password) adds complexity
|
|
||||||
- SSL/certificate issues in corporate environments
|
|
||||||
- Testing requires access to proxy environments
|
|
||||||
|
|
||||||
**Priority:** HIGH - Essential for v1.1.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Android Studio Integration
|
|
||||||
|
|
||||||
**Feature:** Seamless integration with Android Studio IDE
|
|
||||||
|
|
||||||
**Description:** Generate Android Studio project files and configurations that:
|
|
||||||
- Hide Weevil's internal directory structure from the IDE view
|
|
||||||
- Present a clean, minimal file tree to students
|
|
||||||
- Hook Weevil's build and deploy scripts into Android Studio's "Run" button
|
|
||||||
- Properly configure the IDE's indexing and code completion
|
|
||||||
- Support debugging integration
|
|
||||||
|
|
||||||
The goal: students work in Android Studio (the tool they know) but get Weevil's improved project structure and deployment workflow behind the scenes.
|
|
||||||
|
|
||||||
**Rationale:** This is the killer feature that bridges the gap between Weevil's better engineering practices and students' existing workflow. Kids already know Android Studio. Making Weevil "just work" with it removes adoption friction and lets them focus on robot code, not tooling.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Huge competitive advantage for Nexus Workshops
|
|
||||||
- Leverages existing student knowledge
|
|
||||||
- Reduces cognitive load (one less tool to learn)
|
|
||||||
- Makes Weevil invisible in the best way - it just works
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Android Studio project file format may change
|
|
||||||
- Complex to test across different Android Studio versions
|
|
||||||
- May conflict with students' existing Android Studio customizations
|
|
||||||
- Requires deep understanding of IDE configuration
|
|
||||||
|
|
||||||
**Priority:** HIGH - Strong candidate for v1.1.0 (killer feature)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Manual Installation Fallback Documentation
|
|
||||||
|
|
||||||
**Feature:** Comprehensive manual setup documentation
|
|
||||||
|
|
||||||
**Description:** Detailed, step-by-step instructions for manually installing every dependency when automation fails. This includes:
|
|
||||||
- Screenshots or terminal output examples
|
|
||||||
- Platform-specific variations (Windows vs. Linux)
|
|
||||||
- Common error messages and solutions
|
|
||||||
- Checksums for verifying downloads
|
|
||||||
- Fallback download URLs if primary sources are blocked
|
|
||||||
|
|
||||||
**Rationale:** Automation fails. Proxies block downloads. Firewalls interfere. Having a "guaranteed to work" manual path builds confidence and ensures teams aren't stuck. This is about providing an escape hatch and building trust.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Low effort (documentation, not code)
|
|
||||||
- High value when automation fails
|
|
||||||
- Educational - teaches students what's happening under the hood
|
|
||||||
- Demonstrates thoroughness and professionalism
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Requires maintenance as dependencies evolve
|
|
||||||
- Screenshots go stale quickly
|
|
||||||
- Platform variations multiply documentation burden
|
|
||||||
|
|
||||||
**Priority:** MEDIUM-HIGH - Strong candidate for v1.1.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Package Distribution (Debian/Ubuntu)
|
|
||||||
|
|
||||||
**Feature:** `.deb` package for easy installation on Debian-based systems
|
|
||||||
|
|
||||||
**Description:** Create Debian packages that can be installed via `sudo dpkg -i weevil_1.1.0_amd64.deb` or distributed through a personal APT repository. Package would:
|
|
||||||
- Install weevil binary to `/usr/bin`
|
|
||||||
- Include man pages and documentation
|
|
||||||
- Handle any system dependencies
|
|
||||||
- Support clean uninstallation
|
|
||||||
|
|
||||||
**Rationale:** Provides a "professional" distribution method for Linux users. Makes Weevil feel like real software, not just a script. Easier for schools/teams to deploy across multiple machines.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Professional appearance
|
|
||||||
- Standard Linux distribution method
|
|
||||||
- Can include in deployment automation (Ansible, etc.)
|
|
||||||
- `cargo-deb` makes this relatively easy
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Maintenance overhead for packaging
|
|
||||||
- Need to support multiple Ubuntu/Debian versions
|
|
||||||
- Most teams will just download the binary anyway
|
|
||||||
- Not essential for functionality
|
|
||||||
|
|
||||||
**Priority:** LOW - Nice to have, but not essential for v1.1.0
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version 1.2.0 - Polish & Accessibility
|
|
||||||
|
|
||||||
**Theme:** Making Weevil accessible to non-technical users and expanding platform support.
|
|
||||||
|
|
||||||
### Windows Installer (MSI)
|
|
||||||
|
|
||||||
**Feature:** Professional Windows installer package
|
|
||||||
|
|
||||||
**Description:** Create an MSI installer using WiX Toolset or `cargo-wix` that:
|
|
||||||
- Installs weevil.exe to Program Files
|
|
||||||
- Adds weevil to system PATH automatically
|
|
||||||
- Creates Start Menu entries
|
|
||||||
- Appears in "Programs and Features" for clean uninstall
|
|
||||||
- Optionally creates desktop shortcut
|
|
||||||
|
|
||||||
**Rationale:** Windows users expect installers, not loose executables. An MSI makes Weevil feel professional and legitimate. Start menu integration makes it discoverable.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Expected Windows UX
|
|
||||||
- Automatic PATH configuration (users don't need to understand this)
|
|
||||||
- Professional appearance
|
|
||||||
- Easy deployment in school environments
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- MSI creation and signing has complexity
|
|
||||||
- Code signing certificates cost money ($200+/year)
|
|
||||||
- Without code signing, Windows shows security warnings
|
|
||||||
- Testing across Windows versions (10, 11)
|
|
||||||
|
|
||||||
**Priority:** MEDIUM - Polish feature for Windows adoption
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Linux Program Launcher Integration
|
|
||||||
|
|
||||||
**Feature:** Desktop file and menu integration for Linux
|
|
||||||
|
|
||||||
**Description:** Include `.desktop` files and icon assets that integrate with Linux desktop environments (GNOME, KDE, XFCE). This makes Weevil appear in application menus and launchers. Likely bundled with the .deb package.
|
|
||||||
|
|
||||||
**Rationale:** Makes Weevil discoverable in the GUI for users who aren't comfortable with terminals. Fits the "reduce cognitive load" philosophy.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Low effort (just create .desktop files)
|
|
||||||
- Helps GUI users discover Weevil
|
|
||||||
- Standard Linux desktop integration
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Different desktop environments have quirks
|
|
||||||
- Icon design needed
|
|
||||||
- Only useful if there's a GUI to launch
|
|
||||||
|
|
||||||
**Priority:** MEDIUM - Pairs well with GUI development
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Non-Debian Linux Distribution Support
|
|
||||||
|
|
||||||
**Feature:** Support for Arch, Fedora, Slackware, and other distributions
|
|
||||||
|
|
||||||
**Description:** Adapt installation scripts to detect and use different package managers:
|
|
||||||
- Arch: pacman
|
|
||||||
- Fedora: dnf/yum
|
|
||||||
- Slackware: pkgtool
|
|
||||||
- Generic: compile from source instructions
|
|
||||||
|
|
||||||
May also include packaging for AUR (Arch User Repository), Fedora Copr, etc.
|
|
||||||
|
|
||||||
**Rationale:** Expands addressable market. Some teams use Arch or Fedora. Shows commitment to Linux ecosystem.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Broader Linux support
|
|
||||||
- Community contributions likely (Arch users love AUR packages)
|
|
||||||
- Demonstrates technical depth
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Significant testing burden across distros
|
|
||||||
- Each package manager has different quirks
|
|
||||||
- Low ROI - most teams use Ubuntu/Debian or Windows
|
|
||||||
- Maintenance overhead
|
|
||||||
|
|
||||||
**Priority:** LOW-MEDIUM - Nice to have, but niche
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Graphical User Interface (GUI)
|
|
||||||
|
|
||||||
**Feature:** GUI application for teams uncomfortable with terminals
|
|
||||||
|
|
||||||
**Description:** A graphical interface that wraps Weevil's functionality, allowing users to:
|
|
||||||
- Create new projects through forms/wizards
|
|
||||||
- Configure project settings visually
|
|
||||||
- Run builds and deployments with buttons
|
|
||||||
- View status and logs in a window
|
|
||||||
- Manage dependencies through checkboxes/dropdowns
|
|
||||||
|
|
||||||
**Technical Approaches:**
|
|
||||||
1. **Tauri** - Rust + web frontend (HTML/CSS/JS), native performance, small binary
|
|
||||||
2. **Local web server** - Weevil serves HTML, opens browser automatically
|
|
||||||
3. **Native GUI** - GTK, Qt, or egui (Rust native GUI)
|
|
||||||
|
|
||||||
**Rationale:** Reduces barrier to entry for students and coaches unfamiliar with terminals. Lowers cognitive load - students focus on robotics, not command syntax. Particularly valuable for younger teams or schools with limited technical resources.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Significantly lowers barrier to entry
|
|
||||||
- Appeals to visual learners
|
|
||||||
- Makes Weevil accessible to non-programmers (coaches, parents)
|
|
||||||
- Could include visual project templates/wizards
|
|
||||||
- Positions Weevil as "real software" vs. "developer tool"
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Substantial development effort
|
|
||||||
- GUI framework choice has long-term implications
|
|
||||||
- Need to maintain two interfaces (CLI + GUI)
|
|
||||||
- UI design and UX is its own skillset
|
|
||||||
- Testing GUI across platforms is complex
|
|
||||||
- May need separate binary or flag to launch GUI
|
|
||||||
|
|
||||||
**Priority:** MEDIUM-HIGH - Valuable for adoption, but requires careful planning
|
|
||||||
|
|
||||||
**Dependencies:** If building a GUI, having an API layer (see below) makes sense for architecture.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### REST API Layer
|
|
||||||
|
|
||||||
**Feature:** Internal API that both CLI and GUI can consume
|
|
||||||
|
|
||||||
**Description:** Refactor Weevil's core functionality behind a REST API:
|
|
||||||
- API could run as a local server or be embedded
|
|
||||||
- CLI becomes a thin client to the API
|
|
||||||
- GUI uses the same API endpoints
|
|
||||||
- Enables future integrations (VS Code extension, web dashboard, etc.)
|
|
||||||
|
|
||||||
Endpoints might include:
|
|
||||||
- `POST /project/create` - Create new project
|
|
||||||
- `GET /status` - System audit
|
|
||||||
- `POST /build` - Trigger build
|
|
||||||
- `GET /dependencies` - List installed dependencies
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
**Rationale:** Clean separation of concerns. Makes adding new interfaces (GUI, IDE plugins) easier. Enables potential future features like remote builds or team collaboration.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Clean architecture
|
|
||||||
- Multiple frontends share same backend logic
|
|
||||||
- Easier testing (API can be tested independently)
|
|
||||||
- Opens door to remote/distributed features
|
|
||||||
- Could enable web-based dashboard for teams
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Significant refactoring of existing code
|
|
||||||
- Adds complexity (serialization, HTTP layer, error handling)
|
|
||||||
- Local-only API doesn't provide much value initially
|
|
||||||
- May be over-engineering for current needs
|
|
||||||
- gRPC/Protobuf would be overkill unless remote features needed
|
|
||||||
|
|
||||||
**Priority:** LOW-MEDIUM - Nice architecture, but not essential unless building GUI or extensions
|
|
||||||
|
|
||||||
**Note:** If staying local-only, CLI calling library functions directly is simpler. Only build API if there's a concrete need (GUI, remote features, integrations).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version 1.3.0 - Extended Platform Support
|
|
||||||
|
|
||||||
(Features carried over from v1.2.0 if not completed, plus any new platform-specific enhancements)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version 1.4.0 - Ecosystem & Package Management
|
|
||||||
|
|
||||||
**Theme:** Transforming Weevil from a project generator into an ecosystem platform. This is where Weevil becomes more than what Android Studio offers.
|
|
||||||
|
|
||||||
### FTC Component Package Manager
|
|
||||||
|
|
||||||
**Feature:** Package manager for sharing and reusing FTC robot code components
|
|
||||||
|
|
||||||
**Description:** Enable teams to publish and consume reusable robot code components. Examples:
|
|
||||||
- Mechanim wheel controllers
|
|
||||||
- Sensor abstractions
|
|
||||||
- Autonomous routines
|
|
||||||
- Vision processing pipelines
|
|
||||||
- Hardware wrappers
|
|
||||||
|
|
||||||
**Potential Approaches:**
|
|
||||||
|
|
||||||
1. **Git Submodule Style (FreeBSD Ports):**
|
|
||||||
- Package index is a Git repository with manifests
|
|
||||||
- `weevil add mechanim-wheel` pulls code via Git into project
|
|
||||||
- Code is vendored locally, teams can modify
|
|
||||||
- Clean version control story
|
|
||||||
|
|
||||||
2. **Central Registry:**
|
|
||||||
- Nexus Workshops hosts package registry at nxgit.dev
|
|
||||||
- Teams publish packages with metadata (license, dependencies, version)
|
|
||||||
- `weevil search wheels` finds packages
|
|
||||||
- `weevil add team123/mechanim-wheel` installs
|
|
||||||
- Binary or source distribution
|
|
||||||
|
|
||||||
3. **Hybrid Approach:**
|
|
||||||
- Decentralized (anyone can host packages on Git)
|
|
||||||
- Nexus Workshops provides discovery/curation (searchable index)
|
|
||||||
- Teams can specify direct Git URLs or use curated registry
|
|
||||||
|
|
||||||
**Key Considerations:**
|
|
||||||
- **Licensing:** Must track and display licenses, ensure compliance
|
|
||||||
- **Namespacing:** Avoid collisions (team number prefixes? org namespaces?)
|
|
||||||
- **Versioning:** Semantic versioning, dependency resolution
|
|
||||||
- **Quality:** Curated vs. open submission, review process
|
|
||||||
- **Trust:** Code signing? Verified publishers?
|
|
||||||
|
|
||||||
**Rationale:** This is the network effect feature. Teams contribute back proven solutions. Nexus Workshops becomes the central hub for FTC software engineering knowledge. Competitive moat - no other tool offers this. Transforms FTC from "everyone reinvents wheels" to "community shares solutions."
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Massive competitive differentiation
|
|
||||||
- Creates community around Weevil/Nexus Workshops
|
|
||||||
- Direct value to teams (stop reinventing proven solutions)
|
|
||||||
- Positions Nexus Workshops as FTC software authority
|
|
||||||
- Revenue potential (premium packages? consulting on custom components?)
|
|
||||||
- Network effects - more users = more packages = more value
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Complex to implement correctly
|
|
||||||
- Licensing compliance is non-trivial
|
|
||||||
- Moderation/curation burden (prevent malicious code)
|
|
||||||
- Version conflicts and dependency hell
|
|
||||||
- Need critical mass of packages to be valuable
|
|
||||||
- Support burden (teams will ask for help with downloaded packages)
|
|
||||||
- Security concerns (code execution from third parties)
|
|
||||||
|
|
||||||
**Priority:** HIGH - This is the strategic differentiator for v1.4.0
|
|
||||||
|
|
||||||
**Success Criteria:**
|
|
||||||
- At least 10 high-quality packages at launch
|
|
||||||
- Clear licensing and attribution
|
|
||||||
- Simple `weevil add` and `weevil remove` workflow
|
|
||||||
- Nexus Workshops positions as curator/quality gatekeeper
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version 1.5.0 - Language Expansion
|
|
||||||
|
|
||||||
**Theme:** Future-proofing Weevil for FTC's evolution
|
|
||||||
|
|
||||||
### C++ Language Support
|
|
||||||
|
|
||||||
**Feature:** Support for C++ FTC projects alongside Java
|
|
||||||
|
|
||||||
**Description:** If/when FTC officially supports C++ for robot programming, Weevil should support creating and managing C++ projects:
|
|
||||||
- C++ project templates
|
|
||||||
- Build system integration (CMake? Gradle?)
|
|
||||||
- Android NDK integration
|
|
||||||
- Debugging support
|
|
||||||
- Mixed Java/C++ projects (JNI bridges)
|
|
||||||
|
|
||||||
**Rationale:** Stay ahead of FTC changes. C++ may offer performance benefits for vision processing or complex algorithms. Supporting multiple languages positions Weevil as the universal FTC development tool.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Future-proofing
|
|
||||||
- Potential performance benefits for teams
|
|
||||||
- Differentiator if other tools don't support C++
|
|
||||||
- Demonstrates technical sophistication
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Uncertain if FTC will actually support C++
|
|
||||||
- C++ toolchain complexity (NDK, build systems)
|
|
||||||
- Most teams won't need/want C++
|
|
||||||
- Significant development effort
|
|
||||||
- Testing burden (two language stacks)
|
|
||||||
|
|
||||||
**Priority:** LOW - Wait and see if FTC actually supports C++
|
|
||||||
|
|
||||||
**Trigger:** FTC officially announces C++ support
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Multi-Language Architecture
|
|
||||||
|
|
||||||
**Feature:** Plugin-based language support architecture
|
|
||||||
|
|
||||||
**Description:** If supporting multiple languages (Java, C++, potentially Kotlin), refactor Weevil to have a language-agnostic core with language-specific plugins:
|
|
||||||
- Core: project structure, build orchestration, deployment
|
|
||||||
- Plugins: language-specific templates, build rules, dependencies
|
|
||||||
|
|
||||||
This makes adding new languages easier and keeps core clean.
|
|
||||||
|
|
||||||
**Rationale:** Clean architecture for extensibility. Easier to maintain than language-specific code scattered throughout.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Cleaner codebase
|
|
||||||
- Community could contribute language plugins
|
|
||||||
- Future-proof for whatever FTC supports next
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Significant refactoring
|
|
||||||
- May be over-engineering if only supporting Java + maybe C++
|
|
||||||
- Plugin API needs careful design
|
|
||||||
|
|
||||||
**Priority:** LOW-MEDIUM - Only if supporting 3+ languages
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version 1.6.0+ - Advanced Tooling
|
|
||||||
|
|
||||||
**Theme:** Making Weevil an all-in-one development environment
|
|
||||||
|
|
||||||
### Troubleshooting Suite
|
|
||||||
|
|
||||||
**Feature:** Comprehensive diagnostic and debugging tools
|
|
||||||
|
|
||||||
**Description:** A suite of troubleshooting tools that help teams diagnose common problems:
|
|
||||||
|
|
||||||
**Potential Components:**
|
|
||||||
1. **Connectivity Diagnostics:**
|
|
||||||
- `weevil diagnose adb` - Check ADB connection to robot controller
|
|
||||||
- Detect USB vs. WiFi connection issues
|
|
||||||
- Test latency and connection stability
|
|
||||||
|
|
||||||
2. **Build Analysis:**
|
|
||||||
- Parse build errors and suggest fixes
|
|
||||||
- Detect common misconfigurations (wrong SDK version, missing dependencies)
|
|
||||||
- Gradle build cache issues
|
|
||||||
|
|
||||||
3. **Log Analysis:**
|
|
||||||
- `weevil logs analyze` - Parse robot logs for common errors
|
|
||||||
- Highlight crashes, exceptions, performance issues
|
|
||||||
- Suggest fixes based on error patterns
|
|
||||||
|
|
||||||
4. **Performance Profiling:**
|
|
||||||
- Measure loop times
|
|
||||||
- Identify performance bottlenecks in autonomous
|
|
||||||
- Memory usage analysis
|
|
||||||
|
|
||||||
5. **Code Quality Checks:**
|
|
||||||
- Static analysis for common mistakes
|
|
||||||
- Style guide compliance
|
|
||||||
- Anti-pattern detection (blocking operations in main loop, etc.)
|
|
||||||
|
|
||||||
6. **Interactive Troubleshooter:**
|
|
||||||
- Wizard-style troubleshooting ("What problem are you having?")
|
|
||||||
- Step-by-step guidance
|
|
||||||
- Link to documentation/solutions
|
|
||||||
|
|
||||||
**Rationale:** This is a game-changer for teams without experienced mentors. Most FTC teams struggle with debugging. Automated troubleshooting reduces frustration and keeps teams moving. Positions Nexus Workshops as the support resource.
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- High value - debugging is painful for teams
|
|
||||||
- Competitive advantage (no other tool offers this)
|
|
||||||
- Reduces support burden (self-service troubleshooting)
|
|
||||||
- Educational - teaches debugging skills
|
|
||||||
- Could integrate with package manager (suggest better packages)
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Requires deep knowledge of common FTC issues
|
|
||||||
- Error pattern recognition is complex
|
|
||||||
- May give wrong advice (false positives)
|
|
||||||
- Maintenance as FTC SDK evolves
|
|
||||||
- Difficult to test comprehensively
|
|
||||||
|
|
||||||
**Priority:** MEDIUM-HIGH - Valuable but complex, needs careful design
|
|
||||||
|
|
||||||
**Implementation Note:** Could start simple (common error pattern matching) and evolve based on real team issues encountered through Nexus Workshops.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Version 2.0.0 - Major Architectural Changes
|
|
||||||
|
|
||||||
**Theme:** Breaking changes that significantly improve Weevil but may require migration
|
|
||||||
|
|
||||||
**Potential Features:**
|
|
||||||
- Complete rewrite of core systems if architecture needs major overhaul
|
|
||||||
- Breaking changes to project structure
|
|
||||||
- New configuration format
|
|
||||||
- Major changes to CLI interface
|
|
||||||
- Integration with cloud services (build servers, team collaboration)
|
|
||||||
|
|
||||||
**Note:** Given the `weevil upgrade` command's ability to migrate projects, moving to 2.0.0 may not be necessary unless there are fundamental architecture changes that can't be backward compatible.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Unscheduled / Research Needed
|
|
||||||
|
|
||||||
### Cloud Build Services
|
|
||||||
**Description:** Remote build servers for teams with slow computers. Teams push code, Weevil builds in the cloud, streams back APK.
|
|
||||||
|
|
||||||
**Status:** Research - needs cost/benefit analysis, infrastructure planning
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VS Code Extension
|
|
||||||
**Description:** Extension for VS Code to provide similar integration as Android Studio.
|
|
||||||
|
|
||||||
**Status:** Research - depends on VS Code adoption in FTC community
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Team Collaboration Features
|
|
||||||
**Description:** Features for teams to coordinate across multiple developers - shared configurations, code review integration, task tracking.
|
|
||||||
|
|
||||||
**Status:** Research - needs market validation (do teams want this?)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Custom Hardware Support
|
|
||||||
**Description:** Templates and tools for teams using custom sensors or actuators beyond standard FTC parts.
|
|
||||||
|
|
||||||
**Status:** Research - depends on community need
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Versioning Philosophy
|
|
||||||
|
|
||||||
- **1.x.0 releases:** New features, backward compatible
|
|
||||||
- **1.x.y releases:** Bug fixes, patches, documentation
|
|
||||||
- **2.0.0:** Breaking changes requiring migration (avoid if possible)
|
|
||||||
|
|
||||||
The `weevil upgrade` command is designed to migrate projects forward across versions, making major version bumps less necessary.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Metrics
|
|
||||||
|
|
||||||
How we'll measure if Weevil is succeeding:
|
|
||||||
|
|
||||||
- **Adoption:** Number of teams using Weevil (tracked via downloads, GitHub stars)
|
|
||||||
- **Retention:** Teams continuing to use across seasons
|
|
||||||
- **Nexus Workshops impact:** Does Weevil drive workshop signups or consulting engagement?
|
|
||||||
- **Community:** Package contributions, GitHub issues/PRs, community discussions
|
|
||||||
- **Competitive outcomes:** Do Nexus Workshops teams using Weevil perform better?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Contributing to the Roadmap
|
|
||||||
|
|
||||||
This roadmap is subject to change based on:
|
|
||||||
- User feedback from FTC teams
|
|
||||||
- Technical feasibility discoveries
|
|
||||||
- Market competition
|
|
||||||
- Strategic priorities for Nexus Workshops LLC
|
|
||||||
|
|
||||||
Features may be accelerated, deferred, or cancelled as the project evolves.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last Updated: January 2026*
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::Command;
|
|
||||||
use colored::*;
|
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SystemHealth {
|
|
||||||
pub java_ok: bool,
|
|
||||||
pub java_version: Option<String>,
|
|
||||||
pub ftc_sdk_ok: bool,
|
|
||||||
pub ftc_sdk_version: Option<String>,
|
|
||||||
pub android_sdk_ok: bool,
|
|
||||||
pub adb_ok: bool,
|
|
||||||
pub adb_version: Option<String>,
|
|
||||||
pub gradle_ok: bool,
|
|
||||||
pub gradle_version: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SystemHealth {
|
|
||||||
pub fn is_healthy(&self) -> bool {
|
|
||||||
// Required: Java, FTC SDK, Android SDK
|
|
||||||
// Optional: ADB in PATH (can be in Android SDK), Gradle (projects have wrapper)
|
|
||||||
self.java_ok && self.ftc_sdk_ok && self.android_sdk_ok
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run system diagnostics and report health status
|
|
||||||
pub fn run_diagnostics() -> Result<()> {
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!("{}", " 🩺 Weevil Doctor - System Diagnostics".bright_cyan().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
let health = check_system_health()?;
|
|
||||||
print_diagnostics(&health);
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
|
|
||||||
if health.is_healthy() {
|
|
||||||
println!("{}", " ✓ System is healthy and ready for FTC development".bright_green().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
println!("{}", "You can now:".bright_yellow().bold());
|
|
||||||
println!(" - Create a new project: {}", "weevil new <project-name>".bright_cyan());
|
|
||||||
println!(" - Setup a cloned project: {}", "weevil setup <project-path>".bright_cyan());
|
|
||||||
} else {
|
|
||||||
println!("{}", " ⚠ Issues found - setup required".bright_yellow().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
println!("{}", "To fix issues, run:".bright_yellow().bold());
|
|
||||||
println!(" {}", "weevil setup".bright_cyan());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check system health and return a report
|
|
||||||
pub fn check_system_health() -> Result<SystemHealth> {
|
|
||||||
let sdk_config = SdkConfig::new()?;
|
|
||||||
|
|
||||||
// Check Java
|
|
||||||
let (java_ok, java_version) = match check_java() {
|
|
||||||
Ok(version) => (true, Some(version)),
|
|
||||||
Err(_) => (false, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check FTC SDK
|
|
||||||
let (ftc_sdk_ok, ftc_sdk_version) = 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());
|
|
||||||
(true, Some(version))
|
|
||||||
}
|
|
||||||
Err(_) => (false, None),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(false, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check Android SDK
|
|
||||||
let android_sdk_ok = if sdk_config.android_sdk_path.exists() {
|
|
||||||
crate::sdk::android::verify(&sdk_config.android_sdk_path).is_ok()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check ADB
|
|
||||||
let (adb_ok, adb_version) = match check_adb(&sdk_config.android_sdk_path) {
|
|
||||||
Ok(version) => (true, Some(version)),
|
|
||||||
Err(_) => (false, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check Gradle (optional)
|
|
||||||
let (gradle_ok, gradle_version) = match check_gradle() {
|
|
||||||
Ok(version) => (true, Some(version)),
|
|
||||||
Err(_) => (false, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(SystemHealth {
|
|
||||||
java_ok,
|
|
||||||
java_version,
|
|
||||||
ftc_sdk_ok,
|
|
||||||
ftc_sdk_version,
|
|
||||||
android_sdk_ok,
|
|
||||||
adb_ok,
|
|
||||||
adb_version,
|
|
||||||
gradle_ok,
|
|
||||||
gradle_version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_diagnostics(health: &SystemHealth) {
|
|
||||||
let sdk_config = SdkConfig::new().unwrap();
|
|
||||||
|
|
||||||
println!("{}", "Required Components:".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Java
|
|
||||||
if health.java_ok {
|
|
||||||
println!(" {} Java JDK {}",
|
|
||||||
"✓".green(),
|
|
||||||
health.java_version.as_ref().unwrap()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!(" {} Java JDK {}",
|
|
||||||
"✗".red(),
|
|
||||||
"not found".red()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FTC SDK
|
|
||||||
if health.ftc_sdk_ok {
|
|
||||||
println!(" {} FTC SDK {} at {}",
|
|
||||||
"✓".green(),
|
|
||||||
health.ftc_sdk_version.as_ref().unwrap(),
|
|
||||||
sdk_config.ftc_sdk_path.display()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!(" {} FTC SDK {} (expected at {})",
|
|
||||||
"✗".red(),
|
|
||||||
"not found".red(),
|
|
||||||
sdk_config.ftc_sdk_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android SDK
|
|
||||||
if health.android_sdk_ok {
|
|
||||||
println!(" {} Android SDK at {}",
|
|
||||||
"✓".green(),
|
|
||||||
sdk_config.android_sdk_path.display()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!(" {} Android SDK {} (expected at {})",
|
|
||||||
"✗".red(),
|
|
||||||
"not found".red(),
|
|
||||||
sdk_config.android_sdk_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{}", "Optional Components:".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// ADB
|
|
||||||
if health.adb_ok {
|
|
||||||
println!(" {} ADB {}",
|
|
||||||
"✓".green(),
|
|
||||||
health.adb_version.as_ref().unwrap()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!(" {} ADB {}",
|
|
||||||
"⚠".yellow(),
|
|
||||||
"not in PATH (included in Android SDK)".yellow()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gradle
|
|
||||||
if health.gradle_ok {
|
|
||||||
println!(" {} Gradle {}",
|
|
||||||
"✓".green(),
|
|
||||||
health.gradle_version.as_ref().unwrap()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!(" {} Gradle {}",
|
|
||||||
"⚠".yellow(),
|
|
||||||
"not in PATH (projects include wrapper)".yellow()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
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(_) => anyhow::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() {
|
|
||||||
anyhow::bail!("ADB found in Android SDK but not in PATH")
|
|
||||||
} else {
|
|
||||||
anyhow::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(_) => anyhow::bail!("Gradle not found in PATH"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,4 @@ pub mod new;
|
|||||||
pub mod upgrade;
|
pub mod upgrade;
|
||||||
pub mod deploy;
|
pub mod deploy;
|
||||||
pub mod sdk;
|
pub mod sdk;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod setup;
|
|
||||||
pub mod doctor;
|
|
||||||
pub mod uninstall;
|
|
||||||
@@ -3,17 +3,13 @@ use std::path::PathBuf;
|
|||||||
use colored::*;
|
use colored::*;
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
use crate::sdk::SdkConfig;
|
||||||
use crate::sdk::proxy::ProxyConfig;
|
|
||||||
use crate::project::ProjectBuilder;
|
use crate::project::ProjectBuilder;
|
||||||
|
|
||||||
pub fn create_project(
|
pub fn create_project(
|
||||||
name: &str,
|
name: &str,
|
||||||
ftc_sdk: Option<&str>,
|
ftc_sdk: Option<&str>,
|
||||||
android_sdk: Option<&str>,
|
android_sdk: Option<&str>,
|
||||||
_proxy: &ProxyConfig,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// _proxy is threaded through here so future flows (e.g. auto-install on
|
|
||||||
// missing SDK) can use it without changing the call site in main.
|
|
||||||
// Validate project name
|
// Validate project name
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
bail!("Project name cannot be empty");
|
bail!("Project name cannot be empty");
|
||||||
@@ -38,47 +34,14 @@ pub fn create_project(
|
|||||||
println!("{}", format!("Creating FTC project: {}", name).bright_green().bold());
|
println!("{}", format!("Creating FTC project: {}", name).bright_green().bold());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
// Check system health FIRST
|
|
||||||
println!("{}", "Checking system prerequisites...".bright_yellow());
|
|
||||||
let health = crate::commands::doctor::check_system_health()?;
|
|
||||||
|
|
||||||
if !health.is_healthy() {
|
|
||||||
println!();
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_red());
|
|
||||||
println!("{}", " ✗ System Setup Required".bright_red().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_red());
|
|
||||||
println!();
|
|
||||||
println!("{}", "Missing required components:".bright_yellow().bold());
|
|
||||||
|
|
||||||
if !health.java_ok {
|
|
||||||
println!(" {} Java JDK", "✗".red());
|
|
||||||
}
|
|
||||||
if !health.ftc_sdk_ok {
|
|
||||||
println!(" {} FTC SDK", "✗".red());
|
|
||||||
}
|
|
||||||
if !health.android_sdk_ok {
|
|
||||||
println!(" {} Android SDK", "✗".red());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{}", "Before creating a project, you need to set up your development environment.".bright_yellow());
|
|
||||||
println!();
|
|
||||||
println!("{}", "Run this command to install required components:".bright_yellow().bold());
|
|
||||||
println!(" {}", "weevil setup".bright_cyan());
|
|
||||||
println!();
|
|
||||||
println!("{}", "Then try creating your project again:".bright_yellow().bold());
|
|
||||||
println!(" {}", format!("weevil new {}", name).bright_cyan());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
bail!("System setup required");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{} All prerequisites met", "✓".green());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Setup or verify SDK configuration
|
// Setup or verify SDK configuration
|
||||||
let sdk_config = SdkConfig::with_paths(ftc_sdk, android_sdk)?;
|
let sdk_config = SdkConfig::with_paths(ftc_sdk, android_sdk)?;
|
||||||
|
|
||||||
|
// Install SDKs if needed
|
||||||
|
println!("{}", "Checking SDKs...".bright_yellow());
|
||||||
|
ensure_sdks(&sdk_config)?;
|
||||||
|
|
||||||
|
println!();
|
||||||
println!("{}", "Creating project structure...".bright_yellow());
|
println!("{}", "Creating project structure...".bright_yellow());
|
||||||
|
|
||||||
// Build the project
|
// Build the project
|
||||||
@@ -94,12 +57,34 @@ pub fn create_project(
|
|||||||
println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string()));
|
println!("Version: {}", crate::sdk::ftc::get_version(&sdk_config.ftc_sdk_path).unwrap_or_else(|_| "unknown".to_string()));
|
||||||
println!();
|
println!();
|
||||||
println!("{}", "Next steps:".bright_yellow().bold());
|
println!("{}", "Next steps:".bright_yellow().bold());
|
||||||
println!(" 1. {}", format!("cd {}", name).bright_cyan());
|
println!(" 1. cd {}", name);
|
||||||
println!(" 2. Review README.md for project structure");
|
println!(" 2. Review README.md for project structure");
|
||||||
println!(" 3. Start coding in src/main/java/robot/");
|
println!(" 3. Start coding in src/main/java/robot/");
|
||||||
println!(" 4. Run tests: {}", "./gradlew test".bright_cyan());
|
println!(" 4. Run: ./gradlew test");
|
||||||
println!(" 5. Deploy to robot: {}", format!("weevil deploy {}", name).bright_cyan());
|
println!(" 5. Deploy: weevil deploy {}", name);
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_sdks(config: &SdkConfig) -> Result<()> {
|
||||||
|
// Check FTC SDK
|
||||||
|
if !config.ftc_sdk_path.exists() {
|
||||||
|
println!("FTC SDK not found. Installing...");
|
||||||
|
crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?;
|
||||||
|
} else {
|
||||||
|
println!("{} FTC SDK found at: {}", "✓".green(), config.ftc_sdk_path.display());
|
||||||
|
crate::sdk::ftc::verify(&config.ftc_sdk_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Android SDK
|
||||||
|
if !config.android_sdk_path.exists() {
|
||||||
|
println!("Android SDK not found. Installing...");
|
||||||
|
crate::sdk::android::install(&config.android_sdk_path)?;
|
||||||
|
} else {
|
||||||
|
println!("{} Android SDK found at: {}", "✓".green(), config.android_sdk_path.display());
|
||||||
|
crate::sdk::android::verify(&config.android_sdk_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,61 +1,60 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use crate::sdk::SdkConfig;
|
use crate::sdk::SdkConfig;
|
||||||
use crate::sdk::proxy::ProxyConfig;
|
|
||||||
|
|
||||||
pub fn install_sdks(proxy: &ProxyConfig) -> Result<()> {
|
pub fn install_sdks() -> Result<()> {
|
||||||
println!("{}", "Installing SDKs...".bright_yellow().bold());
|
println!("{}", "Installing SDKs...".bright_yellow().bold());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let config = SdkConfig::new()?;
|
let config = SdkConfig::new()?;
|
||||||
|
|
||||||
// Install FTC SDK
|
// Install FTC SDK
|
||||||
crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path, proxy)?;
|
crate::sdk::ftc::install(&config.ftc_sdk_path, &config.android_sdk_path)?;
|
||||||
|
|
||||||
// Install Android SDK
|
// Install Android SDK
|
||||||
crate::sdk::android::install(&config.android_sdk_path, proxy)?;
|
crate::sdk::android::install(&config.android_sdk_path)?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("{} All SDKs installed successfully", "✓".green().bold());
|
println!("{} All SDKs installed successfully", "✓".green().bold());
|
||||||
config.print_status();
|
config.print_status();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_status() -> Result<()> {
|
pub fn show_status() -> Result<()> {
|
||||||
let config = SdkConfig::new()?;
|
let config = SdkConfig::new()?;
|
||||||
config.print_status();
|
config.print_status();
|
||||||
|
|
||||||
// Verify SDKs
|
// Verify SDKs
|
||||||
println!();
|
println!();
|
||||||
println!("{}", "Verification:".bright_yellow().bold());
|
println!("{}", "Verification:".bright_yellow().bold());
|
||||||
|
|
||||||
match crate::sdk::ftc::verify(&config.ftc_sdk_path) {
|
match crate::sdk::ftc::verify(&config.ftc_sdk_path) {
|
||||||
Ok(_) => println!("{} FTC SDK is valid", "✓".green()),
|
Ok(_) => println!("{} FTC SDK is valid", "✓".green()),
|
||||||
Err(e) => println!("{} FTC SDK: {}", "✗".red(), e),
|
Err(e) => println!("{} FTC SDK: {}", "✗".red(), e),
|
||||||
}
|
}
|
||||||
|
|
||||||
match crate::sdk::android::verify(&config.android_sdk_path) {
|
match crate::sdk::android::verify(&config.android_sdk_path) {
|
||||||
Ok(_) => println!("{} Android SDK is valid", "✓".green()),
|
Ok(_) => println!("{} Android SDK is valid", "✓".green()),
|
||||||
Err(e) => println!("{} Android SDK: {}", "✗".red(), e),
|
Err(e) => println!("{} Android SDK: {}", "✗".red(), e),
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_sdks(proxy: &ProxyConfig) -> Result<()> {
|
pub fn update_sdks() -> Result<()> {
|
||||||
println!("{}", "Updating SDKs...".bright_yellow().bold());
|
println!("{}", "Updating SDKs...".bright_yellow().bold());
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
let config = SdkConfig::new()?;
|
let config = SdkConfig::new()?;
|
||||||
|
|
||||||
// Update FTC SDK
|
// Update FTC SDK
|
||||||
crate::sdk::ftc::update(&config.ftc_sdk_path, proxy)?;
|
crate::sdk::ftc::update(&config.ftc_sdk_path)?;
|
||||||
|
|
||||||
println!();
|
println!();
|
||||||
println!("{} SDKs updated successfully", "✓".green().bold());
|
println!("{} SDKs updated successfully", "✓".green().bold());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,515 +0,0 @@
|
|||||||
use anyhow::{Result, Context, bail};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::Command;
|
|
||||||
use colored::*;
|
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
|
||||||
use crate::sdk::proxy::ProxyConfig;
|
|
||||||
use crate::project::ProjectConfig;
|
|
||||||
|
|
||||||
/// Setup development environment - either system-wide or for a specific project
|
|
||||||
pub fn setup_environment(project_path: Option<&str>, proxy: &ProxyConfig) -> Result<()> {
|
|
||||||
match project_path {
|
|
||||||
Some(path) => setup_project(path, proxy),
|
|
||||||
None => setup_system(proxy),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup system-wide development environment with default SDKs
|
|
||||||
fn setup_system(proxy: &ProxyConfig) -> 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, proxy)?;
|
|
||||||
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, proxy)?;
|
|
||||||
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, proxy)?;
|
|
||||||
installed.push("Android SDK (installed)".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("Android SDK not found. Installing...");
|
|
||||||
crate::sdk::android::install(&sdk_config.android_sdk_path, proxy)?;
|
|
||||||
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, proxy: &ProxyConfig) -> 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, proxy) {
|
|
||||||
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, proxy)?;
|
|
||||||
installed.push("Android SDK (installed)".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("Android SDK not found. Installing...");
|
|
||||||
crate::sdk::android::install(&config.android_sdk_path, proxy)?;
|
|
||||||
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!();
|
|
||||||
}
|
|
||||||
@@ -1,394 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use colored::*;
|
|
||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
enum RemoveTarget {
|
|
||||||
FtcSdk(PathBuf, String), // path, version label
|
|
||||||
AndroidSdk(PathBuf),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RemoveTarget {
|
|
||||||
fn label(&self) -> String {
|
|
||||||
match self {
|
|
||||||
RemoveTarget::FtcSdk(_, version) => format!("FTC SDK {}", version),
|
|
||||||
RemoveTarget::AndroidSdk(_) => "Android SDK".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path(&self) -> &PathBuf {
|
|
||||||
match self {
|
|
||||||
RemoveTarget::FtcSdk(path, _) => path,
|
|
||||||
RemoveTarget::AndroidSdk(path) => path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size(&self) -> u64 {
|
|
||||||
dir_size(self.path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uninstall Weevil-managed dependencies
|
|
||||||
///
|
|
||||||
/// - No args: removes ~/.weevil entirely
|
|
||||||
/// - --dry-run: shows what would be removed
|
|
||||||
/// - --only N [N ...]: selective removal of specific components
|
|
||||||
pub fn uninstall_dependencies(dry_run: bool, targets: Option<Vec<usize>>) -> Result<()> {
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!("{}", " 🗑️ Weevil Uninstall - Remove Dependencies".bright_cyan().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
let sdk_config = SdkConfig::new()?;
|
|
||||||
|
|
||||||
// No --only flag: full uninstall, just nuke .weevil
|
|
||||||
if targets.is_none() {
|
|
||||||
return full_uninstall(&sdk_config, dry_run);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --only flag: selective removal
|
|
||||||
let all_targets = scan_targets(&sdk_config);
|
|
||||||
|
|
||||||
if all_targets.is_empty() {
|
|
||||||
println!("{}", "No Weevil-managed components found.".bright_green());
|
|
||||||
println!();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show numbered list
|
|
||||||
println!("{}", "Found Weevil-managed components:".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
for (i, target) in all_targets.iter().enumerate() {
|
|
||||||
println!(" {}. {} — {}",
|
|
||||||
(i + 1).to_string().bright_cyan().bold(),
|
|
||||||
target.label(),
|
|
||||||
format!("{} at {}", format_size(target.size()), target.path().display()).dimmed()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Resolve selected indices
|
|
||||||
let indices = targets.unwrap();
|
|
||||||
let mut selected = Vec::new();
|
|
||||||
for idx in indices {
|
|
||||||
if idx == 0 || idx > all_targets.len() {
|
|
||||||
println!("{} Invalid selection: {}. Valid range is 1–{}",
|
|
||||||
"✗".red(), idx, all_targets.len());
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
selected.push(all_targets[idx - 1].clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if dry_run {
|
|
||||||
print_dry_run(&selected);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
print_removal_list(&selected);
|
|
||||||
|
|
||||||
if !confirm()? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
execute_removal(&selected);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Full uninstall — removes the entire .weevil directory
|
|
||||||
fn full_uninstall(sdk_config: &SdkConfig, dry_run: bool) -> Result<()> {
|
|
||||||
let all_targets = scan_targets(sdk_config);
|
|
||||||
|
|
||||||
if all_targets.is_empty() {
|
|
||||||
println!("{}", "No Weevil-managed components found.".bright_green());
|
|
||||||
println!();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = dir_size(&sdk_config.cache_dir);
|
|
||||||
|
|
||||||
if dry_run {
|
|
||||||
|
|
||||||
println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
println!("{}", format!("Contents of {}:", sdk_config.cache_dir.display()).bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
for (i, target) in all_targets.iter().enumerate() {
|
|
||||||
println!(" {}. {} — {}",
|
|
||||||
(i + 1).to_string().bright_cyan().bold(),
|
|
||||||
target.label(),
|
|
||||||
format!("{} at {}", format_size(target.size()), target.path().display()).dimmed()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note any system-installed dependencies that Weevil doesn't manage
|
|
||||||
let mut has_external = false;
|
|
||||||
|
|
||||||
if sdk_config.android_sdk_path.exists()
|
|
||||||
&& !sdk_config.android_sdk_path.to_string_lossy().contains(".weevil") {
|
|
||||||
if !has_external {
|
|
||||||
println!();
|
|
||||||
has_external = true;
|
|
||||||
}
|
|
||||||
println!(" {} Android SDK at {} — not managed by Weevil, will not be removed",
|
|
||||||
"ⓘ".bright_cyan(),
|
|
||||||
sdk_config.android_sdk_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(gradle_version) = check_gradle() {
|
|
||||||
if !has_external {
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
println!(" {} Gradle {} — not managed by Weevil, will not be removed",
|
|
||||||
"ⓘ".bright_cyan(),
|
|
||||||
gradle_version
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{}", format!("Total: {} ({})", sdk_config.cache_dir.display(), format_size(size)).bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
println!("{}", "To remove everything:".bright_yellow().bold());
|
|
||||||
println!(" {}", "weevil uninstall".bright_cyan());
|
|
||||||
println!();
|
|
||||||
println!("{}", "To remove specific items:".bright_yellow().bold());
|
|
||||||
println!(" {}", "weevil uninstall --only 1 2".bright_cyan());
|
|
||||||
println!();
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{}", "This will permanently remove:".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
println!(" {} {} ({})", "✗".red(), sdk_config.cache_dir.display(), format_size(size));
|
|
||||||
println!();
|
|
||||||
println!("{}", "Everything Weevil installed will be gone.".bright_yellow());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
if !confirm()? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
print!(" Removing {} ... ", sdk_config.cache_dir.display());
|
|
||||||
match fs::remove_dir_all(&sdk_config.cache_dir) {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("{}", "✓".green());
|
|
||||||
println!();
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!("{}", " ✓ Uninstall Complete".bright_green().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
println!("{}", "Weevil binary is still installed. To remove it, delete the weevil executable.".bright_yellow());
|
|
||||||
println!();
|
|
||||||
println!("{}", "To reinstall dependencies later:".bright_yellow().bold());
|
|
||||||
println!(" {}", "weevil setup".bright_cyan());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("{} ({})", "✗".red(), e);
|
|
||||||
println!();
|
|
||||||
println!("{}", "You may need to manually remove this directory.".bright_yellow());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_dry_run(selected: &[RemoveTarget]) {
|
|
||||||
println!("{}", "── Dry Run ─────────────────────────────────────────────────".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
println!("{}", "The following would be removed:".bright_yellow());
|
|
||||||
println!();
|
|
||||||
let mut total: u64 = 0;
|
|
||||||
for target in selected {
|
|
||||||
let size = target.size();
|
|
||||||
total += size;
|
|
||||||
println!(" {} {} ({})", "✗".red(), target.label(), format_size(size));
|
|
||||||
println!(" {}", target.path().display().to_string().dimmed());
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
println!("{}", format!("Total: {}", format_size(total)).bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
println!("{}", "Run without --dry-run to actually remove these components.".dimmed());
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_removal_list(selected: &[RemoveTarget]) {
|
|
||||||
println!("{}", "The following will be removed:".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
let mut total: u64 = 0;
|
|
||||||
for target in selected {
|
|
||||||
let size = target.size();
|
|
||||||
total += size;
|
|
||||||
println!(" {} {} ({})", "✗".red(), target.label(), format_size(size));
|
|
||||||
println!(" {}", target.path().display().to_string().dimmed());
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
println!("{}", format!("Total: {}", format_size(total)).bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm() -> Result<bool> {
|
|
||||||
print!("{}", "Are you sure you want to continue? (y/N): ".bright_yellow());
|
|
||||||
io::stdout().flush()?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin().read_line(&mut input)?;
|
|
||||||
let answer = input.trim().to_lowercase();
|
|
||||||
|
|
||||||
if answer != "y" && answer != "yes" {
|
|
||||||
println!();
|
|
||||||
println!("{}", "Uninstall cancelled.".bright_green());
|
|
||||||
println!();
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute_removal(selected: &[RemoveTarget]) {
|
|
||||||
println!();
|
|
||||||
println!("{}", "Removing components...".bright_yellow());
|
|
||||||
println!();
|
|
||||||
|
|
||||||
let mut removed = Vec::new();
|
|
||||||
let mut failed = Vec::new();
|
|
||||||
|
|
||||||
for target in selected {
|
|
||||||
print!(" Removing {}... ", target.label());
|
|
||||||
match fs::remove_dir_all(target.path()) {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("{}", "✓".green());
|
|
||||||
removed.push(target.clone());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("{} ({})", "✗".red(), e);
|
|
||||||
failed.push((target.clone(), e.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
|
|
||||||
if failed.is_empty() {
|
|
||||||
println!("{}", " ✓ Uninstall Complete".bright_green().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
println!("{}", "Removed:".bright_green().bold());
|
|
||||||
for target in &removed {
|
|
||||||
println!(" {} {}", "✓".green(), target.label());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!("{}", " ⚠ Uninstall Completed with Errors".bright_yellow().bold());
|
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
|
||||||
println!();
|
|
||||||
if !removed.is_empty() {
|
|
||||||
println!("{}", "Removed:".bright_green().bold());
|
|
||||||
for target in &removed {
|
|
||||||
println!(" {} {}", "✓".green(), target.label());
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
println!("{}", "Failed to remove:".bright_red().bold());
|
|
||||||
for (target, error) in &failed {
|
|
||||||
println!(" {} {}: {}", "✗".red(), target.label(), error);
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
println!("{}", "You may need to manually remove these directories.".bright_yellow());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
println!("{}", "To reinstall dependencies later:".bright_yellow().bold());
|
|
||||||
println!(" {}", "weevil setup".bright_cyan());
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan the cache directory for individual removable components (used by --only)
|
|
||||||
fn scan_targets(sdk_config: &SdkConfig) -> Vec<RemoveTarget> {
|
|
||||||
let mut targets = Vec::new();
|
|
||||||
|
|
||||||
if sdk_config.cache_dir.exists() {
|
|
||||||
if let Ok(entries) = fs::read_dir(&sdk_config.cache_dir) {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let name = entry.file_name().to_string_lossy().to_string();
|
|
||||||
let path = entry.path();
|
|
||||||
|
|
||||||
if !path.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if name.starts_with("ftc-sdk") {
|
|
||||||
let version = crate::sdk::ftc::get_version(&path)
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
if name == "ftc-sdk" {
|
|
||||||
"default".to_string()
|
|
||||||
} else {
|
|
||||||
name.trim_start_matches("ftc-sdk-").to_string()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
targets.push(RemoveTarget::FtcSdk(path, version));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android SDK — only if Weevil installed it (lives inside .weevil)
|
|
||||||
if sdk_config.android_sdk_path.exists()
|
|
||||||
&& sdk_config.android_sdk_path.to_string_lossy().contains(".weevil") {
|
|
||||||
targets.push(RemoveTarget::AndroidSdk(sdk_config.android_sdk_path.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
targets
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_gradle() -> Result<String> {
|
|
||||||
let output = std::process::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(_) => anyhow::bail!("Gradle not found"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dir_size(path: &PathBuf) -> u64 {
|
|
||||||
let mut size: u64 = 0;
|
|
||||||
if let Ok(entries) = fs::read_dir(path) {
|
|
||||||
for entry in entries.flatten() {
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
size += dir_size(&path);
|
|
||||||
} else if let Ok(metadata) = path.metadata() {
|
|
||||||
size += metadata.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
size
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_size(bytes: u64) -> String {
|
|
||||||
if bytes >= 1_073_741_824 {
|
|
||||||
format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
|
|
||||||
} else if bytes >= 1_048_576 {
|
|
||||||
format!("{:.1} MB", bytes as f64 / 1_048_576.0)
|
|
||||||
} else if bytes >= 1_024 {
|
|
||||||
format!("{:.1} KB", bytes as f64 / 1_024.0)
|
|
||||||
} else {
|
|
||||||
format!("{} B", bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,7 @@ pub fn upgrade_project(path: &str) -> Result<()> {
|
|||||||
let project_name = project_path.file_name()
|
let project_name = project_path.file_name()
|
||||||
.and_then(|n| n.to_str())
|
.and_then(|n| n.to_str())
|
||||||
.unwrap_or("unknown");
|
.unwrap_or("unknown");
|
||||||
crate::project::ProjectConfig::new(project_name, sdk_config.ftc_sdk_path.clone(), sdk_config.android_sdk_path.clone())?
|
crate::project::ProjectConfig::new(project_name, sdk_config.ftc_sdk_path.clone())?
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Current SDK: {}", project_config.ftc_sdk_path.display());
|
println!("Current SDK: {}", project_config.ftc_sdk_path.display());
|
||||||
@@ -52,19 +52,6 @@ pub fn upgrade_project(path: &str) -> Result<()> {
|
|||||||
"gradle/wrapper/gradle-wrapper.properties",
|
"gradle/wrapper/gradle-wrapper.properties",
|
||||||
"gradle/wrapper/gradle-wrapper.jar",
|
"gradle/wrapper/gradle-wrapper.jar",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
// Android Studio integration — regenerated so run configs stay in
|
|
||||||
// sync if deploy.sh flags or script names ever change.
|
|
||||||
".idea/workspace.xml",
|
|
||||||
".idea/runConfigurations/Build.xml",
|
|
||||||
".idea/runConfigurations/Build (Windows).xml",
|
|
||||||
".idea/runConfigurations/Deploy (auto).xml",
|
|
||||||
".idea/runConfigurations/Deploy (auto) (Windows).xml",
|
|
||||||
".idea/runConfigurations/Deploy (USB).xml",
|
|
||||||
".idea/runConfigurations/Deploy (USB) (Windows).xml",
|
|
||||||
".idea/runConfigurations/Deploy (WiFi).xml",
|
|
||||||
".idea/runConfigurations/Deploy (WiFi) (Windows).xml",
|
|
||||||
".idea/runConfigurations/Test.xml",
|
|
||||||
".idea/runConfigurations/Test (Windows).xml",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
println!("{}", "Updating infrastructure files...".bright_yellow());
|
println!("{}", "Updating infrastructure files...".bright_yellow());
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// File: src/lib.rs
|
// File: src/lib.rs
|
||||||
// Library interface for testing
|
// Library interface for testing
|
||||||
|
|
||||||
pub mod version;
|
|
||||||
pub mod sdk;
|
pub mod sdk;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
|||||||
109
src/main.rs
109
src/main.rs
@@ -1,38 +1,18 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use colored::*;
|
use colored::*;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use weevil::version::WEEVIL_VERSION;
|
|
||||||
|
|
||||||
// Import ProxyConfig through our own `mod sdk`, not through the `weevil`
|
|
||||||
// library crate. Both re-export the same source, but Rust treats
|
|
||||||
// `weevil::sdk::proxy::ProxyConfig` and `sdk::proxy::ProxyConfig` as
|
|
||||||
// distinct types when a binary and its lib are compiled together.
|
|
||||||
// The command modules already see the local-mod version, so main must match.
|
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod sdk;
|
mod sdk;
|
||||||
mod project;
|
mod project;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
use sdk::proxy::ProxyConfig;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "weevil")]
|
#[command(name = "weevil")]
|
||||||
#[command(author = "Eric Ratliff <eric@nxlearn.net>")]
|
#[command(author = "Eric Barch <eric@intrepidfusion.com>")]
|
||||||
#[command(version = WEEVIL_VERSION)]
|
#[command(version = "1.0.0")]
|
||||||
#[command(
|
#[command(about = "FTC robotics project generator - bores into complexity, emerges with clean code", long_about = None)]
|
||||||
about = "FTC robotics project generator - bores into complexity, emerges with clean code",
|
|
||||||
long_about = None
|
|
||||||
)]
|
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Use this HTTP/HTTPS proxy for all downloads
|
|
||||||
#[arg(long, value_name = "URL", global = true)]
|
|
||||||
proxy: Option<String>,
|
|
||||||
|
|
||||||
/// Skip proxy entirely — go direct even if HTTPS_PROXY is set
|
|
||||||
#[arg(long, global = true)]
|
|
||||||
no_proxy: bool,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
}
|
}
|
||||||
@@ -43,71 +23,51 @@ enum Commands {
|
|||||||
New {
|
New {
|
||||||
/// Name of the robot project
|
/// Name of the robot project
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
/// Path to FTC SDK (optional, will auto-detect or download)
|
/// Path to FTC SDK (optional, will auto-detect or download)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
ftc_sdk: Option<String>,
|
ftc_sdk: Option<String>,
|
||||||
|
|
||||||
/// Path to Android SDK (optional, will auto-detect or download)
|
/// Path to Android SDK (optional, will auto-detect or download)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
android_sdk: Option<String>,
|
android_sdk: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Check system health and diagnose issues
|
|
||||||
Doctor,
|
|
||||||
|
|
||||||
/// Setup development environment (system or project)
|
|
||||||
Setup {
|
|
||||||
/// Path to project directory (optional - without it, sets up system)
|
|
||||||
path: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove Weevil-installed SDKs and dependencies
|
|
||||||
Uninstall {
|
|
||||||
/// Show what would be removed without actually removing anything
|
|
||||||
#[arg(long)]
|
|
||||||
dry_run: bool,
|
|
||||||
|
|
||||||
/// Remove only specific items by number (use --dry-run first to see the list)
|
|
||||||
#[arg(long, value_name = "NUM", num_args = 1..)]
|
|
||||||
only: Option<Vec<usize>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Upgrade an existing project to the latest generator version
|
/// Upgrade an existing project to the latest generator version
|
||||||
Upgrade {
|
Upgrade {
|
||||||
/// Path to the project directory
|
/// Path to the project directory
|
||||||
path: String,
|
path: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Build and deploy project to Control Hub
|
/// Build and deploy project to Control Hub
|
||||||
Deploy {
|
Deploy {
|
||||||
/// Path to the project directory
|
/// Path to the project directory
|
||||||
path: String,
|
path: String,
|
||||||
|
|
||||||
/// Force USB connection
|
/// Force USB connection
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
usb: bool,
|
usb: bool,
|
||||||
|
|
||||||
/// Force WiFi connection
|
/// Force WiFi connection
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
wifi: bool,
|
wifi: bool,
|
||||||
|
|
||||||
/// Custom IP address
|
/// Custom IP address
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
ip: Option<String>,
|
ip: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Manage SDKs (FTC and Android)
|
/// Manage SDKs (FTC and Android)
|
||||||
Sdk {
|
Sdk {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: SdkCommands,
|
command: SdkCommands,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Show or update project configuration
|
/// Show or update project configuration
|
||||||
Config {
|
Config {
|
||||||
/// Path to the project directory
|
/// Path to the project directory
|
||||||
path: String,
|
path: String,
|
||||||
|
|
||||||
/// Set FTC SDK path for this project
|
/// Set FTC SDK path for this project
|
||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
set_sdk: Option<String>,
|
set_sdk: Option<String>,
|
||||||
@@ -118,10 +78,10 @@ enum Commands {
|
|||||||
enum SdkCommands {
|
enum SdkCommands {
|
||||||
/// Install required SDKs
|
/// Install required SDKs
|
||||||
Install,
|
Install,
|
||||||
|
|
||||||
/// Show SDK status and locations
|
/// Show SDK status and locations
|
||||||
Status,
|
Status,
|
||||||
|
|
||||||
/// Update SDKs to latest versions
|
/// Update SDKs to latest versions
|
||||||
Update,
|
Update,
|
||||||
}
|
}
|
||||||
@@ -130,26 +90,14 @@ fn main() -> Result<()> {
|
|||||||
// Enable colors on Windows
|
// Enable colors on Windows
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
colored::control::set_virtual_terminal(true).ok();
|
colored::control::set_virtual_terminal(true).ok();
|
||||||
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
print_banner();
|
print_banner();
|
||||||
|
|
||||||
// Resolve proxy once at the top — every network-touching command uses it.
|
|
||||||
let proxy = ProxyConfig::resolve(cli.proxy.as_deref(), cli.no_proxy)?;
|
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::New { name, ftc_sdk, android_sdk } => {
|
Commands::New { name, ftc_sdk, android_sdk } => {
|
||||||
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref(), &proxy)
|
commands::new::create_project(&name, ftc_sdk.as_deref(), android_sdk.as_deref())
|
||||||
}
|
|
||||||
Commands::Doctor => {
|
|
||||||
commands::doctor::run_diagnostics()
|
|
||||||
}
|
|
||||||
Commands::Setup { path } => {
|
|
||||||
commands::setup::setup_environment(path.as_deref(), &proxy)
|
|
||||||
}
|
|
||||||
Commands::Uninstall { dry_run, only } => {
|
|
||||||
commands::uninstall::uninstall_dependencies(dry_run, only)
|
|
||||||
}
|
}
|
||||||
Commands::Upgrade { path } => {
|
Commands::Upgrade { path } => {
|
||||||
commands::upgrade::upgrade_project(&path)
|
commands::upgrade::upgrade_project(&path)
|
||||||
@@ -157,11 +105,13 @@ fn main() -> Result<()> {
|
|||||||
Commands::Deploy { path, usb, wifi, ip } => {
|
Commands::Deploy { path, usb, wifi, ip } => {
|
||||||
commands::deploy::deploy_project(&path, usb, wifi, ip.as_deref())
|
commands::deploy::deploy_project(&path, usb, wifi, ip.as_deref())
|
||||||
}
|
}
|
||||||
Commands::Sdk { command } => match command {
|
Commands::Sdk { command } => {
|
||||||
SdkCommands::Install => commands::sdk::install_sdks(&proxy),
|
match command {
|
||||||
SdkCommands::Status => commands::sdk::show_status(),
|
SdkCommands::Install => commands::sdk::install_sdks(),
|
||||||
SdkCommands::Update => commands::sdk::update_sdks(&proxy),
|
SdkCommands::Status => commands::sdk::show_status(),
|
||||||
},
|
SdkCommands::Update => commands::sdk::update_sdks(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Commands::Config { path, set_sdk } => {
|
Commands::Config { path, set_sdk } => {
|
||||||
if let Some(sdk_path) = set_sdk {
|
if let Some(sdk_path) = set_sdk {
|
||||||
commands::config::set_sdk(&path, &sdk_path)
|
commands::config::set_sdk(&path, &sdk_path)
|
||||||
@@ -174,12 +124,7 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
fn print_banner() {
|
fn print_banner() {
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||||
println!(
|
println!("{}", " 🪲 Weevil - FTC Project Generator v1.0.0".bright_cyan().bold());
|
||||||
"{}",
|
|
||||||
format!(" 🪲 Weevil - FTC Project Generator v{}", WEEVIL_VERSION)
|
|
||||||
.bright_cyan()
|
|
||||||
.bold()
|
|
||||||
);
|
|
||||||
println!("{}", " Nexus Workshops LLC".bright_cyan());
|
println!("{}", " Nexus Workshops LLC".bright_cyan());
|
||||||
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
println!("{}", "═══════════════════════════════════════════════════════════".bright_cyan());
|
||||||
println!();
|
println!();
|
||||||
|
|||||||
@@ -3,33 +3,24 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use anyhow::{Result, Context, bail};
|
use anyhow::{Result, Context, bail};
|
||||||
|
|
||||||
const WEEVIL_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ProjectConfig {
|
pub struct ProjectConfig {
|
||||||
pub project_name: String,
|
pub project_name: String,
|
||||||
pub weevil_version: String,
|
pub weevil_version: String,
|
||||||
pub ftc_sdk_path: PathBuf,
|
pub ftc_sdk_path: PathBuf,
|
||||||
pub ftc_sdk_version: String,
|
pub ftc_sdk_version: String,
|
||||||
#[serde(default = "default_android_sdk_path")]
|
|
||||||
pub android_sdk_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_android_sdk_path() -> PathBuf {
|
|
||||||
PathBuf::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectConfig {
|
impl ProjectConfig {
|
||||||
pub fn new(project_name: &str, ftc_sdk_path: PathBuf, android_sdk_path: PathBuf) -> Result<Self> {
|
pub fn new(project_name: &str, ftc_sdk_path: PathBuf) -> Result<Self> {
|
||||||
let ftc_sdk_version = crate::sdk::ftc::get_version(&ftc_sdk_path)
|
let ftc_sdk_version = crate::sdk::ftc::get_version(&ftc_sdk_path)
|
||||||
.unwrap_or_else(|_| "unknown".to_string());
|
.unwrap_or_else(|_| "unknown".to_string());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
project_name: project_name.to_string(),
|
project_name: project_name.to_string(),
|
||||||
weevil_version: WEEVIL_VERSION.to_string(),
|
weevil_version: "1.0.0".to_string(),
|
||||||
ftc_sdk_path,
|
ftc_sdk_path,
|
||||||
ftc_sdk_version,
|
ftc_sdk_version,
|
||||||
android_sdk_path,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +34,9 @@ impl ProjectConfig {
|
|||||||
let contents = fs::read_to_string(&config_path)
|
let contents = fs::read_to_string(&config_path)
|
||||||
.context("Failed to read .weevil.toml")?;
|
.context("Failed to read .weevil.toml")?;
|
||||||
|
|
||||||
let mut config: ProjectConfig = toml::from_str(&contents)
|
let config: ProjectConfig = toml::from_str(&contents)
|
||||||
.context("Failed to parse .weevil.toml")?;
|
.context("Failed to parse .weevil.toml")?;
|
||||||
|
|
||||||
// Migrate old configs that don't have android_sdk_path
|
|
||||||
if config.android_sdk_path.as_os_str().is_empty() {
|
|
||||||
let sdk_config = crate::sdk::SdkConfig::new()?;
|
|
||||||
config.android_sdk_path = sdk_config.android_sdk_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +77,6 @@ impl ProjectConfig {
|
|||||||
println!();
|
println!();
|
||||||
println!("{:.<20} {}", "FTC SDK Path", self.ftc_sdk_path.display().to_string().bright_white());
|
println!("{:.<20} {}", "FTC SDK Path", self.ftc_sdk_path.display().to_string().bright_white());
|
||||||
println!("{:.<20} {}", "FTC SDK Version", self.ftc_sdk_version.bright_white());
|
println!("{:.<20} {}", "FTC SDK Version", self.ftc_sdk_version.bright_white());
|
||||||
println!("{:.<20} {}", "Android SDK Path", self.android_sdk_path.display().to_string().bright_white());
|
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,6 @@ use git2::Repository;
|
|||||||
|
|
||||||
use crate::sdk::SdkConfig;
|
use crate::sdk::SdkConfig;
|
||||||
|
|
||||||
const WEEVIL_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
|
|
||||||
pub mod deployer;
|
pub mod deployer;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ impl ProjectBuilder {
|
|||||||
"src/test/java/robot",
|
"src/test/java/robot",
|
||||||
"src/test/java/robot/subsystems",
|
"src/test/java/robot/subsystems",
|
||||||
"gradle/wrapper",
|
"gradle/wrapper",
|
||||||
".idea/runConfigurations",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for dir in dirs {
|
for dir in dirs {
|
||||||
@@ -71,7 +68,7 @@ impl ProjectBuilder {
|
|||||||
let mut _context = TeraContext::new();
|
let mut _context = TeraContext::new();
|
||||||
_context.insert("project_name", &self.name);
|
_context.insert("project_name", &self.name);
|
||||||
_context.insert("sdk_dir", &sdk_config.ftc_sdk_path.to_string_lossy());
|
_context.insert("sdk_dir", &sdk_config.ftc_sdk_path.to_string_lossy());
|
||||||
_context.insert("generator_version", WEEVIL_VERSION);
|
_context.insert("generator_version", "1.0.0");
|
||||||
|
|
||||||
self.create_project_files(project_path, sdk_config)?;
|
self.create_project_files(project_path, sdk_config)?;
|
||||||
|
|
||||||
@@ -80,14 +77,14 @@ impl ProjectBuilder {
|
|||||||
|
|
||||||
fn create_project_files(&self, project_path: &Path, sdk_config: &SdkConfig) -> Result<()> {
|
fn create_project_files(&self, project_path: &Path, sdk_config: &SdkConfig) -> Result<()> {
|
||||||
// Create .weevil.toml config
|
// Create .weevil.toml config
|
||||||
let project_config = ProjectConfig::new(&self.name, sdk_config.ftc_sdk_path.clone(), sdk_config.android_sdk_path.clone())?;
|
let project_config = ProjectConfig::new(&self.name, sdk_config.ftc_sdk_path.clone())?;
|
||||||
project_config.save(project_path)?;
|
project_config.save(project_path)?;
|
||||||
|
|
||||||
// README.md
|
// README.md
|
||||||
let readme = format!(
|
let readme = format!(
|
||||||
r#"# {}
|
r#"# {}
|
||||||
|
|
||||||
FTC Robot Project generated by Weevil v{}
|
FTC Robot Project generated by Weevil v1.0.0
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
```bash
|
```bash
|
||||||
@@ -114,7 +111,7 @@ deploy.bat
|
|||||||
2. Test locally: `./gradlew test`
|
2. Test locally: `./gradlew test`
|
||||||
3. Deploy: `./deploy.sh` (or `deploy.bat` on Windows)
|
3. Deploy: `./deploy.sh` (or `deploy.bat` on Windows)
|
||||||
"#,
|
"#,
|
||||||
self.name, WEEVIL_VERSION
|
self.name
|
||||||
);
|
);
|
||||||
fs::write(project_path.join("README.md"), readme)?;
|
fs::write(project_path.join("README.md"), readme)?;
|
||||||
|
|
||||||
@@ -123,7 +120,7 @@ deploy.bat
|
|||||||
fs::write(project_path.join(".gitignore"), gitignore)?;
|
fs::write(project_path.join(".gitignore"), gitignore)?;
|
||||||
|
|
||||||
// Version marker
|
// Version marker
|
||||||
fs::write(project_path.join(".weevil-version"), WEEVIL_VERSION)?;
|
fs::write(project_path.join(".weevil-version"), "1.0.0")?;
|
||||||
|
|
||||||
// build.gradle.kts - Pure Java with deployToSDK task
|
// build.gradle.kts - Pure Java with deployToSDK task
|
||||||
// Escape backslashes for Windows paths in Kotlin strings
|
// Escape backslashes for Windows paths in Kotlin strings
|
||||||
@@ -337,62 +334,34 @@ echo "✓ Deployed!"
|
|||||||
let deploy_bat = r#"@echo off
|
let deploy_bat = r#"@echo off
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
REM Read SDK paths from config
|
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
|
for /f "tokens=2 delims==" %%a in ('findstr /c:"ftc_sdk_path" .weevil.toml') do (
|
||||||
for /f "tokens=2 delims==" %%a in ('findstr /c:"android_sdk_path" .weevil.toml') do set ANDROID_SDK=%%a
|
set SDK_DIR=%%a
|
||||||
|
set SDK_DIR=!SDK_DIR:"=!
|
||||||
REM Strip all quotes (both single and double)
|
set SDK_DIR=!SDK_DIR: =!
|
||||||
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 (
|
if not defined SDK_DIR (
|
||||||
echo Error: Could not read FTC SDK path from .weevil.toml
|
echo Error: Could not read FTC SDK path from .weevil.toml
|
||||||
exit /b 1
|
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...
|
echo Building APK...
|
||||||
call gradlew.bat buildApk
|
call gradlew.bat buildApk
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Deploying to Control Hub...
|
echo Deploying to Control Hub...
|
||||||
|
|
||||||
REM Find APK - look for TeamCode-debug.apk
|
REM Find APK
|
||||||
for /f "delims=" %%i in ('dir /s /b "%SDK_DIR%\TeamCode-debug.apk" 2^>nul') do set APK=%%i
|
for /f "delims=" %%i in ('dir /s /b "%SDK_DIR%\*app-debug.apk" 2^>nul') do set APK=%%i
|
||||||
|
|
||||||
if not defined APK (
|
if not defined APK (
|
||||||
echo Error: APK not found
|
echo Error: APK not found
|
||||||
exit /b 1
|
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%
|
echo Installing: %APK%
|
||||||
"%ADB_PATH%" install -r "%APK%"
|
adb install -r "%APK%"
|
||||||
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo.
|
|
||||||
echo Deployment failed!
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Deployed!
|
echo Deployed!
|
||||||
@@ -417,307 +386,9 @@ class BasicTest {
|
|||||||
test_file
|
test_file
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Android Studio integration: .idea/ files
|
|
||||||
self.generate_idea_files(project_path)?;
|
|
||||||
|
|
||||||
Ok(())
|
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<()> {
|
fn setup_gradle(&self, project_path: &Path) -> Result<()> {
|
||||||
println!("Setting up Gradle wrapper...");
|
println!("Setting up Gradle wrapper...");
|
||||||
crate::sdk::gradle::setup_wrapper(project_path)?;
|
crate::sdk::gradle::setup_wrapper(project_path)?;
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use anyhow::{Result, Context};
|
use anyhow::{Result, Context};
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use reqwest::blocking::Client;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use colored::*;
|
use colored::*;
|
||||||
|
|
||||||
use super::proxy::ProxyConfig;
|
|
||||||
|
|
||||||
const ANDROID_SDK_URL_LINUX: &str = "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip";
|
const ANDROID_SDK_URL_LINUX: &str = "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip";
|
||||||
const ANDROID_SDK_URL_MAC: &str = "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip";
|
const ANDROID_SDK_URL_MAC: &str = "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip";
|
||||||
const ANDROID_SDK_URL_WINDOWS: &str = "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip";
|
const ANDROID_SDK_URL_WINDOWS: &str = "https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip";
|
||||||
|
|
||||||
pub fn install(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
pub fn install(sdk_path: &Path) -> Result<()> {
|
||||||
// Check if SDK exists AND is complete
|
// Check if SDK exists AND is complete
|
||||||
if sdk_path.exists() {
|
if sdk_path.exists() {
|
||||||
match verify(sdk_path) {
|
match verify(sdk_path) {
|
||||||
@@ -43,14 +42,10 @@ pub fn install(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
|||||||
|
|
||||||
// Download
|
// Download
|
||||||
println!("Downloading from: {}", url);
|
println!("Downloading from: {}", url);
|
||||||
proxy.print_status();
|
let client = Client::new();
|
||||||
let client = proxy.client()?;
|
|
||||||
let response = client.get(url)
|
let response = client.get(url)
|
||||||
.send()
|
.send()
|
||||||
.map_err(|e| {
|
.context("Failed to download Android SDK")?;
|
||||||
super::proxy::print_offline_instructions();
|
|
||||||
anyhow::anyhow!("Failed to download Android SDK: {}", e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let total_size = response.content_length().unwrap_or(0);
|
let total_size = response.content_length().unwrap_or(0);
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ use git2::Repository;
|
|||||||
use colored::*;
|
use colored::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use super::proxy::{ProxyConfig, GitProxyGuard};
|
|
||||||
|
|
||||||
const FTC_SDK_URL: &str = "https://github.com/FIRST-Tech-Challenge/FtcRobotController.git";
|
const FTC_SDK_URL: &str = "https://github.com/FIRST-Tech-Challenge/FtcRobotController.git";
|
||||||
const FTC_SDK_VERSION: &str = "v10.1.1";
|
const FTC_SDK_VERSION: &str = "v10.1.1";
|
||||||
|
|
||||||
pub fn install(sdk_path: &Path, android_sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
pub fn install(sdk_path: &Path, android_sdk_path: &Path) -> Result<()> {
|
||||||
if sdk_path.exists() {
|
if sdk_path.exists() {
|
||||||
println!("{} FTC SDK already installed at: {}",
|
println!("{} FTC SDK already installed at: {}",
|
||||||
"✓".green(),
|
"✓".green(),
|
||||||
@@ -23,18 +21,10 @@ pub fn install(sdk_path: &Path, android_sdk_path: &Path, proxy: &ProxyConfig) ->
|
|||||||
println!("{}", "Installing FTC SDK...".bright_yellow());
|
println!("{}", "Installing FTC SDK...".bright_yellow());
|
||||||
println!("Cloning from: {}", FTC_SDK_URL);
|
println!("Cloning from: {}", FTC_SDK_URL);
|
||||||
println!("Version: {}", FTC_SDK_VERSION);
|
println!("Version: {}", FTC_SDK_VERSION);
|
||||||
proxy.print_status();
|
|
||||||
|
|
||||||
// GitProxyGuard sets HTTPS_PROXY for the duration of the clone so that
|
|
||||||
// libgit2 honours --proxy / --no-proxy without touching ~/.gitconfig.
|
|
||||||
let _guard = GitProxyGuard::new(proxy);
|
|
||||||
|
|
||||||
// Clone the repository
|
// Clone the repository
|
||||||
let repo = Repository::clone(FTC_SDK_URL, sdk_path)
|
let repo = Repository::clone(FTC_SDK_URL, sdk_path)
|
||||||
.map_err(|e| {
|
.context("Failed to clone FTC SDK")?;
|
||||||
super::proxy::print_offline_instructions();
|
|
||||||
anyhow::anyhow!("Failed to clone FTC SDK: {}", e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Checkout specific version
|
// Checkout specific version
|
||||||
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
||||||
@@ -90,23 +80,15 @@ fn check_version(sdk_path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(sdk_path: &Path, proxy: &ProxyConfig) -> Result<()> {
|
pub fn update(sdk_path: &Path) -> Result<()> {
|
||||||
println!("{}", "Updating FTC SDK...".bright_yellow());
|
println!("{}", "Updating FTC SDK...".bright_yellow());
|
||||||
proxy.print_status();
|
|
||||||
|
|
||||||
let repo = Repository::open(sdk_path)
|
let repo = Repository::open(sdk_path)
|
||||||
.context("FTC SDK not found or not a git repository")?;
|
.context("FTC SDK not found or not a git repository")?;
|
||||||
|
|
||||||
// Guard env vars for the fetch
|
|
||||||
let _guard = GitProxyGuard::new(proxy);
|
|
||||||
|
|
||||||
// Fetch latest
|
// Fetch latest
|
||||||
let mut remote = repo.find_remote("origin")?;
|
let mut remote = repo.find_remote("origin")?;
|
||||||
remote.fetch(&["refs/tags/*:refs/tags/*"], None, None)
|
remote.fetch(&["refs/tags/*:refs/tags/*"], None, None)?;
|
||||||
.map_err(|e| {
|
|
||||||
super::proxy::print_offline_instructions();
|
|
||||||
anyhow::anyhow!("Failed to fetch FTC SDK updates: {}", e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Checkout latest version
|
// Checkout latest version
|
||||||
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
let obj = repo.revparse_single(FTC_SDK_VERSION)?;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use colored::*;
|
|||||||
pub mod android;
|
pub mod android;
|
||||||
pub mod ftc;
|
pub mod ftc;
|
||||||
pub mod gradle;
|
pub mod gradle;
|
||||||
pub mod proxy;
|
|
||||||
|
|
||||||
pub struct SdkConfig {
|
pub struct SdkConfig {
|
||||||
pub ftc_sdk_path: PathBuf,
|
pub ftc_sdk_path: PathBuf,
|
||||||
@@ -16,26 +15,15 @@ pub struct SdkConfig {
|
|||||||
|
|
||||||
impl SdkConfig {
|
impl SdkConfig {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
// Allow tests (or power users) to override the cache directory.
|
let home = dirs::home_dir()
|
||||||
// When WEEVIL_HOME is set, we also skip the system Android SDK
|
.context("Could not determine home directory")?;
|
||||||
// search so tests are fully isolated.
|
|
||||||
let (cache_dir, android_sdk_path) = if let Ok(weevil_home) = std::env::var("WEEVIL_HOME") {
|
|
||||||
let cache = PathBuf::from(weevil_home);
|
|
||||||
let android = cache.join("android-sdk");
|
|
||||||
(cache, android)
|
|
||||||
} else {
|
|
||||||
let home = dirs::home_dir()
|
|
||||||
.context("Could not determine home directory")?;
|
|
||||||
let cache = home.join(".weevil");
|
|
||||||
let android = Self::find_android_sdk().unwrap_or_else(|| cache.join("android-sdk"));
|
|
||||||
(cache, android)
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let cache_dir = home.join(".weevil");
|
||||||
fs::create_dir_all(&cache_dir)?;
|
fs::create_dir_all(&cache_dir)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ftc_sdk_path: cache_dir.join("ftc-sdk"),
|
ftc_sdk_path: cache_dir.join("ftc-sdk"),
|
||||||
android_sdk_path,
|
android_sdk_path: Self::find_android_sdk().unwrap_or_else(|| cache_dir.join("android-sdk")),
|
||||||
cache_dir,
|
cache_dir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
322
src/sdk/proxy.rs
322
src/sdk/proxy.rs
@@ -1,322 +0,0 @@
|
|||||||
use colored::*;
|
|
||||||
use reqwest::blocking;
|
|
||||||
use reqwest::Url;
|
|
||||||
|
|
||||||
/// Where the proxy URL came from — used for status messages.
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum ProxySource {
|
|
||||||
/// --proxy <url> on the command line
|
|
||||||
Flag,
|
|
||||||
/// HTTPS_PROXY or HTTP_PROXY environment variable
|
|
||||||
Env(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolved proxy configuration. A `None` url means "go direct".
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ProxyConfig {
|
|
||||||
pub url: Option<Url>,
|
|
||||||
pub source: Option<ProxySource>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProxyConfig {
|
|
||||||
/// Resolve proxy with this priority:
|
|
||||||
/// 1. --no-proxy → direct, ignore everything
|
|
||||||
/// 2. --proxy <url> → use that URL
|
|
||||||
/// 3. HTTPS_PROXY / HTTP_PROXY env vars
|
|
||||||
/// 4. Nothing → direct
|
|
||||||
pub fn resolve(proxy_flag: Option<&str>, no_proxy: bool) -> Result<Self, anyhow::Error> {
|
|
||||||
if no_proxy {
|
|
||||||
return Ok(Self { url: None, source: None });
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(raw) = proxy_flag {
|
|
||||||
let url = Url::parse(raw)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid --proxy URL '{}': {}", raw, e))?;
|
|
||||||
return Ok(Self { url: Some(url), source: Some(ProxySource::Flag) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk the env vars in priority order
|
|
||||||
for var in &["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"] {
|
|
||||||
if let Ok(val) = std::env::var(var) {
|
|
||||||
if val.is_empty() { continue; }
|
|
||||||
let url = Url::parse(&val)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid {} env var '{}': {}", var, val, e))?;
|
|
||||||
return Ok(Self {
|
|
||||||
url: Some(url),
|
|
||||||
source: Some(ProxySource::Env(var.to_string())),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self { url: None, source: None })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// True when the user explicitly passed --proxy (as opposed to env-var pickup).
|
|
||||||
/// Used for distinguishing "you asked for this proxy and it failed" from
|
|
||||||
/// "we picked up an ambient proxy from your environment" in error paths.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_explicit(&self) -> bool {
|
|
||||||
matches!(self.source, Some(ProxySource::Flag))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Human-readable description of where the proxy came from, for status output.
|
|
||||||
pub fn source_description(&self) -> String {
|
|
||||||
match &self.source {
|
|
||||||
Some(ProxySource::Flag) => "--proxy flag".to_string(),
|
|
||||||
Some(ProxySource::Env(var)) => format!("{} env var", var),
|
|
||||||
None => "none".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print a one-line proxy status line (used in setup output).
|
|
||||||
pub fn print_status(&self) {
|
|
||||||
match &self.url {
|
|
||||||
Some(url) => println!(
|
|
||||||
" {} Proxy: {} ({})",
|
|
||||||
"✓".green(),
|
|
||||||
url.as_str().bright_white(),
|
|
||||||
self.source_description()
|
|
||||||
),
|
|
||||||
None => println!(
|
|
||||||
" {} Proxy: direct (no proxy)",
|
|
||||||
"○".bright_black()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a reqwest blocking Client that honours this proxy config.
|
|
||||||
///
|
|
||||||
/// * `Some(url)` → all HTTP/HTTPS traffic goes through that proxy.
|
|
||||||
/// * `None` → direct, no proxy at all.
|
|
||||||
///
|
|
||||||
/// We always go through the builder (never plain `Client::new()`) because
|
|
||||||
/// `Client::new()` silently picks up env-var proxies. When the user says
|
|
||||||
/// `--no-proxy` we need to actively disable that.
|
|
||||||
pub fn client(&self) -> anyhow::Result<blocking::Client> {
|
|
||||||
let mut builder = blocking::ClientBuilder::new();
|
|
||||||
|
|
||||||
match &self.url {
|
|
||||||
Some(url) => {
|
|
||||||
builder = builder.proxy(reqwest::Proxy::all(url.clone())?);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Actively suppress env-var auto-detection.
|
|
||||||
builder = builder.no_proxy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.build().map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RAII guard that temporarily sets HTTPS_PROXY / HTTP_PROXY for the lifetime
|
|
||||||
/// of the guard, then restores the previous values on drop.
|
|
||||||
///
|
|
||||||
/// libgit2 (the C library behind the `git2` crate) reads these env vars
|
|
||||||
/// directly for its HTTP transport. This is the cleanest way to make
|
|
||||||
/// `git2::Repository::clone` and `remote.fetch()` honour a `--proxy` flag
|
|
||||||
/// without touching the user's global `~/.gitconfig`.
|
|
||||||
///
|
|
||||||
/// When the ProxyConfig has no URL (direct / --no-proxy) the guard *clears*
|
|
||||||
/// the vars so libgit2 won't accidentally pick up an ambient proxy the user
|
|
||||||
/// didn't intend for this operation.
|
|
||||||
pub struct GitProxyGuard {
|
|
||||||
prev_https: Option<String>,
|
|
||||||
prev_http: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitProxyGuard {
|
|
||||||
pub fn new(config: &ProxyConfig) -> Self {
|
|
||||||
let prev_https = std::env::var("HTTPS_PROXY").ok();
|
|
||||||
let prev_http = std::env::var("HTTP_PROXY").ok();
|
|
||||||
|
|
||||||
match &config.url {
|
|
||||||
Some(url) => {
|
|
||||||
let s = url.as_str();
|
|
||||||
std::env::set_var("HTTPS_PROXY", s);
|
|
||||||
std::env::set_var("HTTP_PROXY", s);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
std::env::remove_var("HTTP_PROXY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self { prev_https, prev_http }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for GitProxyGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
match &self.prev_https {
|
|
||||||
Some(v) => std::env::set_var("HTTPS_PROXY", v),
|
|
||||||
None => std::env::remove_var("HTTPS_PROXY"),
|
|
||||||
}
|
|
||||||
match &self.prev_http {
|
|
||||||
Some(v) => std::env::set_var("HTTP_PROXY", v),
|
|
||||||
None => std::env::remove_var("HTTP_PROXY"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print clear, actionable instructions for obtaining dependencies without
|
|
||||||
/// an internet connection. Called whenever a network download fails so the
|
|
||||||
/// user always sees their escape hatch.
|
|
||||||
pub fn print_offline_instructions() {
|
|
||||||
println!();
|
|
||||||
println!("{}", "── Offline / Air-Gapped Installation ──────────────────────".bright_yellow().bold());
|
|
||||||
println!();
|
|
||||||
println!("If you have no internet access (or the proxy is not working),");
|
|
||||||
println!("you can obtain the required SDKs on a connected machine and");
|
|
||||||
println!("copy them over.");
|
|
||||||
println!();
|
|
||||||
println!("{}", "1. FTC SDK (git clone)".bright_cyan().bold());
|
|
||||||
println!(" On a connected machine:");
|
|
||||||
println!(" git clone https://github.com/FIRST-Tech-Challenge/FtcRobotController.git");
|
|
||||||
println!(" cd FtcRobotController");
|
|
||||||
println!(" git checkout v10.1.1");
|
|
||||||
println!();
|
|
||||||
println!(" Copy the entire FtcRobotController/ directory to this machine");
|
|
||||||
println!(" at ~/.weevil/ftc-sdk/ (or wherever your .weevil.toml points).");
|
|
||||||
println!();
|
|
||||||
println!("{}", "2. Android SDK (command-line tools)".bright_cyan().bold());
|
|
||||||
println!(" Download the zip for your OS from a connected machine:");
|
|
||||||
println!(" Linux: https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip");
|
|
||||||
println!(" macOS: https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip");
|
|
||||||
println!(" Windows: https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip");
|
|
||||||
println!();
|
|
||||||
println!(" Extract to ~/.weevil/android-sdk/, then run sdkmanager:");
|
|
||||||
println!(" ./cmdline-tools/latest/bin/sdkmanager \\");
|
|
||||||
println!(" platform-tools platforms;android-34 build-tools;34.0.0");
|
|
||||||
println!();
|
|
||||||
println!(" Copy the resulting android-sdk/ directory to this machine.");
|
|
||||||
println!();
|
|
||||||
println!("{}", "3. Gradle distribution".bright_cyan().bold());
|
|
||||||
println!(" Gradle fetches its own distribution the first time ./gradlew");
|
|
||||||
println!(" runs. If that fails offline, download manually:");
|
|
||||||
println!(" https://services.gradle.org/distributions/gradle-8.9-bin.zip");
|
|
||||||
println!(" Extract into ~/.gradle/wrapper/dists/gradle-8.9-bin/");
|
|
||||||
println!(" (the exact subdirectory is printed by gradlew on failure).");
|
|
||||||
println!();
|
|
||||||
println!("{}", "4. Proxy quick reference".bright_cyan().bold());
|
|
||||||
println!(" • Use a specific proxy: weevil --proxy http://proxy:3128 sdk install");
|
|
||||||
println!(" • Skip the proxy entirely: weevil --no-proxy sdk install");
|
|
||||||
println!(" • Gradle also reads HTTPS_PROXY / HTTP_PROXY, so set those");
|
|
||||||
println!(" in your shell before running ./gradlew if the build needs a proxy.");
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
// Env vars are process-global. cargo test runs tests in parallel within
|
|
||||||
// a single binary, so any test that sets/removes HTTPS_PROXY or HTTP_PROXY
|
|
||||||
// must hold this lock for its entire duration.
|
|
||||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_proxy_flag_forces_direct() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
std::env::set_var("HTTPS_PROXY", "http://proxy.example.com:3128");
|
|
||||||
let config = ProxyConfig::resolve(None, true).unwrap();
|
|
||||||
assert!(config.url.is_none());
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn explicit_flag_overrides_env() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
std::env::set_var("HTTPS_PROXY", "http://env-proxy.example.com:3128");
|
|
||||||
let config = ProxyConfig::resolve(Some("http://flag-proxy.example.com:8080"), false).unwrap();
|
|
||||||
assert_eq!(config.url.as_ref().unwrap().host_str(), Some("flag-proxy.example.com"));
|
|
||||||
assert!(config.is_explicit());
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn picks_up_env_var() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
std::env::remove_var("https_proxy");
|
|
||||||
std::env::set_var("HTTP_PROXY", "http://env-proxy.example.com:3128");
|
|
||||||
let config = ProxyConfig::resolve(None, false).unwrap();
|
|
||||||
assert_eq!(config.url.as_ref().unwrap().host_str(), Some("env-proxy.example.com"));
|
|
||||||
assert_eq!(config.source, Some(ProxySource::Env("HTTP_PROXY".to_string())));
|
|
||||||
std::env::remove_var("HTTP_PROXY");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn direct_when_nothing_set() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
std::env::remove_var("https_proxy");
|
|
||||||
std::env::remove_var("HTTP_PROXY");
|
|
||||||
std::env::remove_var("http_proxy");
|
|
||||||
let config = ProxyConfig::resolve(None, false).unwrap();
|
|
||||||
assert!(config.url.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_garbage_url() {
|
|
||||||
let result = ProxyConfig::resolve(Some("not a url at all"), false);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn client_builds_with_proxy() {
|
|
||||||
let config = ProxyConfig::resolve(Some("http://proxy.example.com:3128"), false).unwrap();
|
|
||||||
assert!(config.client().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn client_builds_direct() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
std::env::remove_var("https_proxy");
|
|
||||||
std::env::remove_var("HTTP_PROXY");
|
|
||||||
std::env::remove_var("http_proxy");
|
|
||||||
let config = ProxyConfig::resolve(None, true).unwrap();
|
|
||||||
assert!(config.client().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn git_proxy_guard_sets_and_restores() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
std::env::set_var("HTTPS_PROXY", "http://original:1111");
|
|
||||||
std::env::set_var("HTTP_PROXY", "http://original:2222");
|
|
||||||
|
|
||||||
let config = ProxyConfig::resolve(Some("http://guarded:9999"), false).unwrap();
|
|
||||||
{
|
|
||||||
let _guard = GitProxyGuard::new(&config);
|
|
||||||
// Url::parse normalises — trailing slash is expected
|
|
||||||
assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://guarded:9999/");
|
|
||||||
assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://guarded:9999/");
|
|
||||||
}
|
|
||||||
assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://original:1111");
|
|
||||||
assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://original:2222");
|
|
||||||
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
std::env::remove_var("HTTP_PROXY");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn git_proxy_guard_clears_for_direct() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
std::env::set_var("HTTPS_PROXY", "http://should-be-cleared:1111");
|
|
||||||
std::env::set_var("HTTP_PROXY", "http://should-be-cleared:2222");
|
|
||||||
|
|
||||||
let config = ProxyConfig { url: None, source: None };
|
|
||||||
{
|
|
||||||
let _guard = GitProxyGuard::new(&config);
|
|
||||||
assert!(std::env::var("HTTPS_PROXY").is_err());
|
|
||||||
assert!(std::env::var("HTTP_PROXY").is_err());
|
|
||||||
}
|
|
||||||
assert_eq!(std::env::var("HTTPS_PROXY").unwrap(), "http://should-be-cleared:1111");
|
|
||||||
assert_eq!(std::env::var("HTTP_PROXY").unwrap(), "http://should-be-cleared:2222");
|
|
||||||
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
std::env::remove_var("HTTP_PROXY");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub const WEEVIL_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Intentionally hardcoded. When you bump the version in Cargo.toml,
|
|
||||||
// tests will fail here until you update this to match.
|
|
||||||
pub const EXPECTED_VERSION: &str = "1.1.0-beta.2";
|
|
||||||
@@ -1,17 +1,8 @@
|
|||||||
use assert_cmd::prelude::*;
|
use assert_cmd::prelude::*;
|
||||||
use predicates::prelude::*;
|
use predicates::prelude::*;
|
||||||
|
use tempfile::TempDir;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
#[path = "common.rs"]
|
|
||||||
mod common;
|
|
||||||
use common::EXPECTED_VERSION;
|
|
||||||
|
|
||||||
#[path = "integration/environment_tests.rs"]
|
|
||||||
mod environment_tests;
|
|
||||||
|
|
||||||
#[path = "integration/project_lifecycle_tests.rs"]
|
|
||||||
mod project_lifecycle_tests;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_help_command() {
|
fn test_help_command() {
|
||||||
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
|
||||||
@@ -29,7 +20,7 @@ fn test_version_command() {
|
|||||||
|
|
||||||
cmd.assert()
|
cmd.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(predicate::str::contains(EXPECTED_VERSION));
|
.stdout(predicate::str::contains("1.0.0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -40,4 +31,25 @@ fn test_sdk_status_command() {
|
|||||||
cmd.assert()
|
cmd.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(predicate::str::contains("SDK Configuration"));
|
.stdout(predicate::str::contains("SDK Configuration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project creation test - will need mock SDKs
|
||||||
|
#[test]
|
||||||
|
#[ignore] // Ignore until we have mock SDKs set up
|
||||||
|
fn test_project_creation() {
|
||||||
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
|
||||||
|
cmd.current_dir(&temp)
|
||||||
|
.arg("new")
|
||||||
|
.arg("test-robot");
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(predicate::str::contains("Project Created"));
|
||||||
|
|
||||||
|
// Verify project structure
|
||||||
|
assert!(temp.path().join("test-robot/README.md").exists());
|
||||||
|
assert!(temp.path().join("test-robot/build.gradle.kts").exists());
|
||||||
|
assert!(temp.path().join("test-robot/gradlew").exists());
|
||||||
}
|
}
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
// File: tests/integration/environment_tests.rs
|
|
||||||
// Integration tests for doctor, setup, uninstall, and new (v1.1.0 commands)
|
|
||||||
//
|
|
||||||
// Strategy: every test sets WEEVIL_HOME to a fresh TempDir. When WEEVIL_HOME
|
|
||||||
// is set, SdkConfig skips the system Android SDK search entirely, so nothing
|
|
||||||
// on the real system is visible or touched.
|
|
||||||
//
|
|
||||||
// We manually create the mock fixture structures in each test rather than
|
|
||||||
// using include_dir::extract, because include_dir doesn't preserve empty
|
|
||||||
// directories.
|
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::process::Command;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
/// Helper: returns a configured Command pointing at the weevil binary with
|
|
||||||
/// WEEVIL_HOME set to the given temp directory.
|
|
||||||
fn weevil_cmd(weevil_home: &TempDir) -> Command {
|
|
||||||
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
|
|
||||||
cmd.env("WEEVIL_HOME", weevil_home.path());
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: create a minimal mock FTC SDK at the given path.
|
|
||||||
/// Matches the structure that ftc::verify checks for.
|
|
||||||
fn create_mock_ftc_sdk(path: &std::path::Path) {
|
|
||||||
fs::create_dir_all(path.join("TeamCode/src/main/java")).unwrap();
|
|
||||||
fs::create_dir_all(path.join("FtcRobotController")).unwrap();
|
|
||||||
fs::write(path.join("build.gradle"), "// mock").unwrap();
|
|
||||||
fs::write(path.join(".version"), "v10.1.1\n").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: create a minimal mock Android SDK at the given path.
|
|
||||||
/// Matches the structure that android::verify checks for.
|
|
||||||
fn create_mock_android_sdk(path: &std::path::Path) {
|
|
||||||
fs::create_dir_all(path.join("platform-tools")).unwrap();
|
|
||||||
fs::write(path.join("platform-tools/adb"), "").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: populate a WEEVIL_HOME with both mock SDKs (fully healthy system)
|
|
||||||
fn populate_healthy(weevil_home: &TempDir) {
|
|
||||||
create_mock_ftc_sdk(&weevil_home.path().join("ftc-sdk"));
|
|
||||||
create_mock_android_sdk(&weevil_home.path().join("android-sdk"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: populate with only the FTC SDK (Android missing)
|
|
||||||
fn populate_ftc_only(weevil_home: &TempDir) {
|
|
||||||
create_mock_ftc_sdk(&weevil_home.path().join("ftc-sdk"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: print labeled output from a test so it's visually distinct from test assertions
|
|
||||||
fn print_output(test_name: &str, output: &std::process::Output) {
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
println!("\n╔══ {} ══════════════════════════════════════════════╗", test_name);
|
|
||||||
if !stdout.is_empty() {
|
|
||||||
println!("║ stdout:");
|
|
||||||
for line in stdout.lines() {
|
|
||||||
println!("║ {}", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
println!("║ stderr:");
|
|
||||||
for line in stderr.lines() {
|
|
||||||
println!("║ {}", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("╚════════════════════════════════════════════════════════╝\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── doctor ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn doctor_healthy_system() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
populate_healthy(&home);
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("doctor")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil doctor");
|
|
||||||
print_output("doctor_healthy_system", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("✓ FTC SDK"), "expected FTC SDK check to pass");
|
|
||||||
assert!(stdout.contains("✓ Android SDK"), "expected Android SDK check to pass");
|
|
||||||
assert!(stdout.contains("System is healthy"), "expected healthy verdict");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn doctor_missing_ftc_sdk() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
// Only Android SDK present
|
|
||||||
create_mock_android_sdk(&home.path().join("android-sdk"));
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("doctor")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil doctor");
|
|
||||||
print_output("doctor_missing_ftc_sdk", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("✗ FTC SDK"), "expected FTC SDK failure");
|
|
||||||
assert!(stdout.contains("Issues found"), "expected issues verdict");
|
|
||||||
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn doctor_missing_android_sdk() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
populate_ftc_only(&home);
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("doctor")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil doctor");
|
|
||||||
print_output("doctor_missing_android_sdk", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("✗ Android SDK"), "expected Android SDK failure");
|
|
||||||
assert!(stdout.contains("Issues found"), "expected issues verdict");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn doctor_completely_empty() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("doctor")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil doctor");
|
|
||||||
print_output("doctor_completely_empty", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("✗ FTC SDK"), "expected FTC SDK failure");
|
|
||||||
assert!(stdout.contains("✗ Android SDK"), "expected Android SDK failure");
|
|
||||||
assert!(stdout.contains("Issues found"), "expected issues verdict");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── uninstall ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn uninstall_dry_run_shows_contents() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
populate_healthy(&home);
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.args(&["uninstall", "--dry-run"])
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil uninstall --dry-run");
|
|
||||||
print_output("uninstall_dry_run_shows_contents", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("FTC SDK"), "expected FTC SDK in dry-run listing");
|
|
||||||
assert!(stdout.contains("weevil uninstall"), "expected full uninstall command");
|
|
||||||
assert!(stdout.contains("weevil uninstall --only"), "expected selective uninstall command");
|
|
||||||
// Nothing should actually be removed
|
|
||||||
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk should still exist after dry-run");
|
|
||||||
assert!(home.path().join("android-sdk").exists(), "android-sdk should still exist after dry-run");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn uninstall_dry_run_empty_system() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.args(&["uninstall", "--dry-run"])
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil uninstall --dry-run");
|
|
||||||
print_output("uninstall_dry_run_empty_system", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("No Weevil-managed components found"),
|
|
||||||
"expected empty message");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn uninstall_only_dry_run_shows_selection() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
populate_healthy(&home);
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.args(&["uninstall", "--only", "1", "--dry-run"])
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil uninstall --only 1 --dry-run");
|
|
||||||
print_output("uninstall_only_dry_run_shows_selection", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("Dry Run"), "expected dry run header");
|
|
||||||
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk should still exist after dry-run");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn uninstall_only_invalid_index() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
populate_healthy(&home);
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.args(&["uninstall", "--only", "99"])
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil uninstall --only 99");
|
|
||||||
print_output("uninstall_only_invalid_index", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("Invalid selection"), "expected invalid selection error");
|
|
||||||
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk should still exist after invalid selection");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── new (requires setup) ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_fails_when_system_not_setup() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
|
||||||
.arg("test-robot")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil new");
|
|
||||||
print_output("new_fails_when_system_not_setup", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(!output.status.success(), "weevil new should fail when system not set up");
|
|
||||||
assert!(stdout.contains("System Setup Required"), "expected setup required message");
|
|
||||||
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_fails_missing_ftc_sdk_only() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
create_mock_android_sdk(&home.path().join("android-sdk"));
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
|
||||||
.arg("test-robot")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil new");
|
|
||||||
print_output("new_fails_missing_ftc_sdk_only", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(!output.status.success(), "weevil new should fail with missing FTC SDK");
|
|
||||||
assert!(stdout.contains("FTC SDK"), "expected FTC SDK listed as missing");
|
|
||||||
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_fails_missing_android_sdk_only() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
populate_ftc_only(&home);
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
|
||||||
.arg("test-robot")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil new");
|
|
||||||
print_output("new_fails_missing_android_sdk_only", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(!output.status.success(), "weevil new should fail with missing Android SDK");
|
|
||||||
assert!(stdout.contains("Android SDK"), "expected Android SDK listed as missing");
|
|
||||||
assert!(stdout.contains("weevil setup"), "expected setup suggestion");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_shows_project_name_in_setup_suggestion() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
|
||||||
.arg("my-cool-robot")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil new");
|
|
||||||
print_output("new_shows_project_name_in_setup_suggestion", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("weevil new my-cool-robot"),
|
|
||||||
"expected retry command with project name");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── setup (project mode) ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn setup_project_missing_toml() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
populate_healthy(&home);
|
|
||||||
|
|
||||||
let project_dir = home.path().join("empty-project");
|
|
||||||
fs::create_dir_all(&project_dir).unwrap();
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("setup")
|
|
||||||
.arg(project_dir.to_str().unwrap())
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil setup <project>");
|
|
||||||
print_output("setup_project_missing_toml", &output);
|
|
||||||
|
|
||||||
assert!(!output.status.success(), "setup should fail on missing .weevil.toml");
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(stderr.contains(".weevil.toml"), "expected .weevil.toml error");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn setup_project_nonexistent_directory() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("setup")
|
|
||||||
.arg("/this/path/does/not/exist")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil setup");
|
|
||||||
print_output("setup_project_nonexistent_directory", &output);
|
|
||||||
|
|
||||||
assert!(!output.status.success(), "setup should fail on nonexistent directory");
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(stderr.contains("not found"), "expected not found error");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── full lifecycle round-trip ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lifecycle_new_uninstall_setup() {
|
|
||||||
let home = TempDir::new().unwrap();
|
|
||||||
let workspace = TempDir::new().unwrap(); // separate from WEEVIL_HOME
|
|
||||||
populate_healthy(&home);
|
|
||||||
|
|
||||||
// 1. Create a project — in workspace, not inside WEEVIL_HOME
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
|
||||||
.arg("my-robot")
|
|
||||||
.current_dir(workspace.path())
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil new");
|
|
||||||
print_output("lifecycle (new)", &output);
|
|
||||||
assert!(output.status.success(), "weevil new failed");
|
|
||||||
|
|
||||||
let project_dir = workspace.path().join("my-robot");
|
|
||||||
assert!(project_dir.join(".weevil.toml").exists(), "project not created");
|
|
||||||
assert!(project_dir.join("src/main/java/robot").exists(), "project structure incomplete");
|
|
||||||
|
|
||||||
// 2. Run gradlew test — skeleton project should compile and pass out of the box.
|
|
||||||
// gradlew/gradlew.bat is cross-platform; pick the right one at runtime.
|
|
||||||
let gradlew = if cfg!(target_os = "windows") { "gradlew.bat" } else { "gradlew" };
|
|
||||||
|
|
||||||
let output = Command::new(project_dir.join(gradlew))
|
|
||||||
.arg("test")
|
|
||||||
.current_dir(&project_dir)
|
|
||||||
.output()
|
|
||||||
.expect("failed to run gradlew test");
|
|
||||||
print_output("lifecycle (gradlew test)", &output);
|
|
||||||
assert!(output.status.success(),
|
|
||||||
"gradlew test failed — new project should pass its skeleton tests out of the box");
|
|
||||||
|
|
||||||
// 3. Run gradlew compileJava — verify the project builds cleanly
|
|
||||||
let output = Command::new(project_dir.join(gradlew))
|
|
||||||
.arg("compileJava")
|
|
||||||
.current_dir(&project_dir)
|
|
||||||
.output()
|
|
||||||
.expect("failed to run gradlew compileJava");
|
|
||||||
print_output("lifecycle (gradlew compileJava)", &output);
|
|
||||||
assert!(output.status.success(), "gradlew compileJava failed — new project should compile cleanly");
|
|
||||||
|
|
||||||
// 4. Uninstall dependencies — project must survive
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.args(&["uninstall", "--dry-run"])
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil uninstall --dry-run");
|
|
||||||
print_output("lifecycle (uninstall dry-run)", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("FTC SDK"), "dry-run should show FTC SDK");
|
|
||||||
|
|
||||||
// Confirm project is untouched by dry-run
|
|
||||||
assert!(project_dir.join(".weevil.toml").exists(), "project deleted by dry-run");
|
|
||||||
|
|
||||||
// Now actually uninstall — feed "y" via stdin
|
|
||||||
let mut child = weevil_cmd(&home)
|
|
||||||
.arg("uninstall")
|
|
||||||
.stdin(std::process::Stdio::piped())
|
|
||||||
.stdout(std::process::Stdio::piped())
|
|
||||||
.stderr(std::process::Stdio::piped())
|
|
||||||
.spawn()
|
|
||||||
.expect("failed to spawn weevil uninstall");
|
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
child.stdin.as_mut().unwrap().write_all(b"y\n").unwrap();
|
|
||||||
let output = child.wait_with_output().expect("failed to wait on uninstall");
|
|
||||||
print_output("lifecycle (uninstall)", &output);
|
|
||||||
|
|
||||||
// Dependencies gone
|
|
||||||
assert!(!home.path().join("ftc-sdk").exists(), "ftc-sdk not removed by uninstall");
|
|
||||||
assert!(!home.path().join("android-sdk").exists(), "android-sdk not removed by uninstall");
|
|
||||||
|
|
||||||
// Project still there, completely intact
|
|
||||||
assert!(project_dir.exists(), "project directory was deleted by uninstall");
|
|
||||||
assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml deleted by uninstall");
|
|
||||||
assert!(project_dir.join("src/main/java/robot").exists(), "project source deleted by uninstall");
|
|
||||||
assert!(project_dir.join("build.gradle.kts").exists(), "build.gradle.kts deleted by uninstall");
|
|
||||||
|
|
||||||
// 3. Doctor confirms system is unhealthy now
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("doctor")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil doctor");
|
|
||||||
print_output("lifecycle (doctor after uninstall)", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("✗ FTC SDK"), "doctor should show FTC SDK missing");
|
|
||||||
assert!(stdout.contains("✗ Android SDK"), "doctor should show Android SDK missing");
|
|
||||||
|
|
||||||
// 4. Setup brings dependencies back
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("setup")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil setup");
|
|
||||||
print_output("lifecycle (setup)", &output);
|
|
||||||
|
|
||||||
// Verify dependencies are back
|
|
||||||
assert!(home.path().join("ftc-sdk").exists(), "ftc-sdk not restored by setup");
|
|
||||||
|
|
||||||
// 5. Doctor confirms healthy again
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("doctor")
|
|
||||||
.output()
|
|
||||||
.expect("failed to run weevil doctor");
|
|
||||||
print_output("lifecycle (doctor after setup)", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
assert!(stdout.contains("✓ FTC SDK"), "doctor should show FTC SDK healthy after setup");
|
|
||||||
}
|
|
||||||
4
tests/integration/mod.rs
Normal file
4
tests/integration/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// File: tests/integration/mod.rs
|
||||||
|
// Integration tests module declarations
|
||||||
|
|
||||||
|
mod project_lifecycle_tests;
|
||||||
@@ -1,238 +1,185 @@
|
|||||||
// File: tests/integration/project_lifecycle_tests.rs
|
// File: tests/integration/project_lifecycle_tests.rs
|
||||||
// Integration tests - full project lifecycle
|
// Integration tests - full project lifecycle
|
||||||
//
|
|
||||||
// Same strategy as environment_tests: WEEVIL_HOME points to a TempDir,
|
|
||||||
// mock SDKs are created manually with fs, and we invoke the compiled
|
|
||||||
// binary directly rather than going through `cargo run`.
|
|
||||||
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use include_dir::{include_dir, Dir};
|
||||||
|
|
||||||
/// Helper: returns a configured Command pointing at the weevil binary with
|
// Embed test fixtures
|
||||||
/// WEEVIL_HOME set to the given temp directory.
|
static MOCK_SDK: Dir = include_dir!("$CARGO_MANIFEST_DIR/tests/fixtures/mock-ftc-sdk");
|
||||||
fn weevil_cmd(weevil_home: &TempDir) -> Command {
|
|
||||||
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("weevil"));
|
|
||||||
cmd.env("WEEVIL_HOME", weevil_home.path());
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: create a minimal mock FTC SDK at the given path.
|
|
||||||
fn create_mock_ftc_sdk(path: &std::path::Path) {
|
|
||||||
fs::create_dir_all(path.join("TeamCode/src/main/java")).unwrap();
|
|
||||||
fs::create_dir_all(path.join("FtcRobotController")).unwrap();
|
|
||||||
fs::write(path.join("build.gradle"), "// mock").unwrap();
|
|
||||||
fs::write(path.join(".version"), "v10.1.1\n").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: create a minimal mock Android SDK at the given path.
|
|
||||||
fn create_mock_android_sdk(path: &std::path::Path) {
|
|
||||||
fs::create_dir_all(path.join("platform-tools")).unwrap();
|
|
||||||
fs::write(path.join("platform-tools/adb"), "").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: populate a WEEVIL_HOME with both mock SDKs (fully healthy system)
|
|
||||||
fn populate_healthy(weevil_home: &TempDir) {
|
|
||||||
create_mock_ftc_sdk(&weevil_home.path().join("ftc-sdk"));
|
|
||||||
create_mock_android_sdk(&weevil_home.path().join("android-sdk"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: print labeled output from a test so it's visually distinct from test assertions
|
|
||||||
fn print_output(test_name: &str, output: &std::process::Output) {
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
println!("\n╔══ {} ══════════════════════════════════════════════╗", test_name);
|
|
||||||
if !stdout.is_empty() {
|
|
||||||
println!("║ stdout:");
|
|
||||||
for line in stdout.lines() {
|
|
||||||
println!("║ {}", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !stderr.is_empty() {
|
|
||||||
println!("║ stderr:");
|
|
||||||
for line in stderr.lines() {
|
|
||||||
println!("║ {}", line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("╚════════════════════════════════════════════════════════╝\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_project_creation_with_mock_sdk() {
|
fn test_project_creation_with_mock_sdk() {
|
||||||
let home = TempDir::new().unwrap();
|
let test_dir = TempDir::new().unwrap();
|
||||||
populate_healthy(&home);
|
let sdk_dir = test_dir.path().join("mock-sdk");
|
||||||
|
let project_dir = test_dir.path().join("test-robot");
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
// Extract mock SDK
|
||||||
.arg("test-robot")
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
.current_dir(home.path())
|
|
||||||
|
// Create project using weevil
|
||||||
|
let output = Command::new("cargo")
|
||||||
|
.args(&["run", "--", "new", "test-robot", "--ftc-sdk", sdk_dir.to_str().unwrap()])
|
||||||
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run weevil new");
|
.expect("Failed to run weevil");
|
||||||
print_output("test_project_creation_with_mock_sdk", &output);
|
|
||||||
|
// Verify project was created
|
||||||
let project_dir = home.path().join("test-robot");
|
assert!(output.status.success(), "weevil new failed: {}", String::from_utf8_lossy(&output.stderr));
|
||||||
assert!(output.status.success(), "weevil new failed");
|
assert!(project_dir.join(".weevil.toml").exists());
|
||||||
assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml missing");
|
assert!(project_dir.join("build.gradle.kts").exists());
|
||||||
assert!(project_dir.join("build.gradle.kts").exists(), "build.gradle.kts missing");
|
assert!(project_dir.join("src/main/java/robot").exists());
|
||||||
assert!(project_dir.join("src/main/java/robot").exists(), "src/main/java/robot missing");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_project_config_persistence() {
|
fn test_project_config_persistence() {
|
||||||
let home = TempDir::new().unwrap();
|
let test_dir = TempDir::new().unwrap();
|
||||||
populate_healthy(&home);
|
let sdk_dir = test_dir.path().join("mock-sdk");
|
||||||
|
let project_dir = test_dir.path().join("config-test");
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
// Extract mock SDK
|
||||||
.arg("config-test")
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
.current_dir(home.path())
|
|
||||||
|
// Create project
|
||||||
|
Command::new("cargo")
|
||||||
|
.args(&["run", "--", "new", "config-test", "--ftc-sdk", sdk_dir.to_str().unwrap()])
|
||||||
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run weevil new");
|
.expect("Failed to create project");
|
||||||
print_output("test_project_config_persistence", &output);
|
|
||||||
assert!(output.status.success(), "weevil new failed");
|
// Read config
|
||||||
|
let config_content = fs::read_to_string(project_dir.join(".weevil.toml")).unwrap();
|
||||||
let project_dir = home.path().join("config-test");
|
|
||||||
let config_content = fs::read_to_string(project_dir.join(".weevil.toml"))
|
assert!(config_content.contains("project_name = \"config-test\""));
|
||||||
.expect(".weevil.toml not found");
|
assert!(config_content.contains(&format!("ftc_sdk_path = \"{}\"", sdk_dir.display())));
|
||||||
|
|
||||||
assert!(config_content.contains("project_name = \"config-test\""),
|
|
||||||
"project_name missing from config:\n{}", config_content);
|
|
||||||
assert!(config_content.contains("ftc_sdk_path"),
|
|
||||||
"ftc_sdk_path missing from config:\n{}", config_content);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_project_upgrade_preserves_code() {
|
fn test_project_upgrade_preserves_code() {
|
||||||
let home = TempDir::new().unwrap();
|
let test_dir = TempDir::new().unwrap();
|
||||||
populate_healthy(&home);
|
let sdk_dir = test_dir.path().join("mock-sdk");
|
||||||
|
let project_dir = test_dir.path().join("upgrade-test");
|
||||||
|
|
||||||
|
// Extract mock SDK
|
||||||
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
|
|
||||||
// Create project
|
// Create project
|
||||||
let output = weevil_cmd(&home)
|
Command::new("cargo")
|
||||||
.arg("new")
|
.args(&["run", "--", "new", "upgrade-test", "--ftc-sdk", sdk_dir.to_str().unwrap()])
|
||||||
.arg("upgrade-test")
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.current_dir(home.path())
|
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run weevil new");
|
.expect("Failed to create project");
|
||||||
print_output("test_project_upgrade_preserves_code (new)", &output);
|
|
||||||
assert!(output.status.success(), "weevil new failed");
|
|
||||||
|
|
||||||
let project_dir = home.path().join("upgrade-test");
|
|
||||||
|
|
||||||
// Add custom code
|
// Add custom code
|
||||||
let custom_file = project_dir.join("src/main/java/robot/CustomCode.java");
|
let custom_file = project_dir.join("src/main/java/robot/CustomCode.java");
|
||||||
fs::write(&custom_file, "// My custom robot code").unwrap();
|
fs::write(&custom_file, "// My custom robot code").unwrap();
|
||||||
|
|
||||||
// Upgrade
|
// Upgrade project
|
||||||
let output = weevil_cmd(&home)
|
Command::new("cargo")
|
||||||
.arg("upgrade")
|
.args(&["run", "--", "upgrade", project_dir.to_str().unwrap()])
|
||||||
.arg(project_dir.to_str().unwrap())
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run weevil upgrade");
|
.expect("Failed to upgrade project");
|
||||||
print_output("test_project_upgrade_preserves_code (upgrade)", &output);
|
|
||||||
|
// Verify custom code still exists
|
||||||
// Custom code survives
|
assert!(custom_file.exists());
|
||||||
assert!(custom_file.exists(), "custom code file was deleted by upgrade");
|
|
||||||
let content = fs::read_to_string(&custom_file).unwrap();
|
let content = fs::read_to_string(&custom_file).unwrap();
|
||||||
assert!(content.contains("My custom robot code"), "custom code was overwritten");
|
assert!(content.contains("My custom robot code"));
|
||||||
|
|
||||||
// Config still present
|
// Verify config was updated
|
||||||
assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml missing after upgrade");
|
assert!(project_dir.join(".weevil.toml").exists());
|
||||||
|
assert!(!project_dir.join(".weevil-version").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_build_scripts_read_from_config() {
|
fn test_build_scripts_read_from_config() {
|
||||||
let home = TempDir::new().unwrap();
|
let test_dir = TempDir::new().unwrap();
|
||||||
populate_healthy(&home);
|
let sdk_dir = test_dir.path().join("mock-sdk");
|
||||||
|
let project_dir = test_dir.path().join("build-test");
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
// Extract mock SDK
|
||||||
.arg("build-test")
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
.current_dir(home.path())
|
|
||||||
|
// Create project
|
||||||
|
Command::new("cargo")
|
||||||
|
.args(&["run", "--", "new", "build-test", "--ftc-sdk", sdk_dir.to_str().unwrap()])
|
||||||
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run weevil new");
|
.expect("Failed to create project");
|
||||||
print_output("test_build_scripts_read_from_config", &output);
|
|
||||||
assert!(output.status.success(), "weevil new failed");
|
// Check build.sh contains config reading
|
||||||
|
let build_sh = fs::read_to_string(project_dir.join("build.sh")).unwrap();
|
||||||
let project_dir = home.path().join("build-test");
|
assert!(build_sh.contains(".weevil.toml"));
|
||||||
|
assert!(build_sh.contains("ftc_sdk_path"));
|
||||||
let build_sh = fs::read_to_string(project_dir.join("build.sh"))
|
|
||||||
.expect("build.sh not found");
|
// Check build.bat contains config reading
|
||||||
assert!(build_sh.contains(".weevil.toml"), "build.sh doesn't reference .weevil.toml");
|
let build_bat = fs::read_to_string(project_dir.join("build.bat")).unwrap();
|
||||||
assert!(build_sh.contains("ftc_sdk_path"), "build.sh doesn't reference ftc_sdk_path");
|
assert!(build_bat.contains(".weevil.toml"));
|
||||||
|
assert!(build_bat.contains("ftc_sdk_path"));
|
||||||
let build_bat = fs::read_to_string(project_dir.join("build.bat"))
|
|
||||||
.expect("build.bat not found");
|
|
||||||
assert!(build_bat.contains(".weevil.toml"), "build.bat doesn't reference .weevil.toml");
|
|
||||||
assert!(build_bat.contains("ftc_sdk_path"), "build.bat doesn't reference ftc_sdk_path");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_command_show() {
|
fn test_config_command_show() {
|
||||||
let home = TempDir::new().unwrap();
|
let test_dir = TempDir::new().unwrap();
|
||||||
populate_healthy(&home);
|
let sdk_dir = test_dir.path().join("mock-sdk");
|
||||||
|
let project_dir = test_dir.path().join("config-show-test");
|
||||||
|
|
||||||
|
// Extract mock SDK
|
||||||
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
|
|
||||||
// Create project
|
// Create project
|
||||||
let output = weevil_cmd(&home)
|
Command::new("cargo")
|
||||||
.arg("new")
|
.args(&["run", "--", "new", "config-show-test", "--ftc-sdk", sdk_dir.to_str().unwrap()])
|
||||||
.arg("config-show-test")
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.current_dir(home.path())
|
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run weevil new");
|
.expect("Failed to create project");
|
||||||
print_output("test_config_command_show (new)", &output);
|
|
||||||
assert!(output.status.success(), "weevil new failed");
|
|
||||||
|
|
||||||
let project_dir = home.path().join("config-show-test");
|
|
||||||
|
|
||||||
// Show config
|
// Show config
|
||||||
let output = weevil_cmd(&home)
|
let output = Command::new("cargo")
|
||||||
.arg("config")
|
.args(&["run", "--", "config", project_dir.to_str().unwrap()])
|
||||||
.arg(project_dir.to_str().unwrap())
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to run weevil config");
|
.expect("Failed to show config");
|
||||||
print_output("test_config_command_show (config)", &output);
|
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
assert!(stdout.contains("config-show-test"), "project name missing from config output");
|
assert!(stdout.contains("config-show-test"));
|
||||||
|
assert!(stdout.contains(&sdk_dir.display().to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_projects_different_sdks() {
|
fn test_multiple_projects_different_sdks() {
|
||||||
let home = TempDir::new().unwrap();
|
let test_dir = TempDir::new().unwrap();
|
||||||
populate_healthy(&home);
|
let sdk1 = test_dir.path().join("sdk-v10");
|
||||||
|
let sdk2 = test_dir.path().join("sdk-v11");
|
||||||
// Create a second FTC SDK with a different version
|
let project1 = test_dir.path().join("robot1");
|
||||||
let sdk2 = home.path().join("ftc-sdk-v11");
|
let project2 = test_dir.path().join("robot2");
|
||||||
create_mock_ftc_sdk(&sdk2);
|
|
||||||
fs::write(sdk2.join(".version"), "v11.0.0\n").unwrap();
|
// Create two different SDK versions
|
||||||
|
MOCK_SDK.extract(&sdk1).unwrap();
|
||||||
// Create first project (uses default ftc-sdk in WEEVIL_HOME)
|
MOCK_SDK.extract(&sdk2).unwrap();
|
||||||
let output = weevil_cmd(&home)
|
fs::write(sdk2.join(".version"), "v11.0.0").unwrap();
|
||||||
.arg("new")
|
|
||||||
.arg("robot1")
|
// Create two projects with different SDKs
|
||||||
.current_dir(home.path())
|
Command::new("cargo")
|
||||||
|
.args(&["run", "--", "new", "robot1", "--ftc-sdk", sdk1.to_str().unwrap()])
|
||||||
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to create robot1");
|
.expect("Failed to create project1");
|
||||||
print_output("test_multiple_projects_different_sdks (robot1)", &output);
|
|
||||||
assert!(output.status.success(), "weevil new robot1 failed");
|
Command::new("cargo")
|
||||||
|
.args(&["run", "--", "new", "robot2", "--ftc-sdk", sdk2.to_str().unwrap()])
|
||||||
// Create second project — would need --ftc-sdk flag if supported,
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
// otherwise both use the same default. Verify they each have valid configs.
|
|
||||||
let output = weevil_cmd(&home)
|
|
||||||
.arg("new")
|
|
||||||
.arg("robot2")
|
|
||||||
.current_dir(home.path())
|
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to create robot2");
|
.expect("Failed to create project2");
|
||||||
print_output("test_multiple_projects_different_sdks (robot2)", &output);
|
|
||||||
assert!(output.status.success(), "weevil new robot2 failed");
|
// Verify each project has correct SDK
|
||||||
|
let config1 = fs::read_to_string(project1.join(".weevil.toml")).unwrap();
|
||||||
let config1 = fs::read_to_string(home.path().join("robot1/.weevil.toml"))
|
let config2 = fs::read_to_string(project2.join(".weevil.toml")).unwrap();
|
||||||
.expect("robot1 .weevil.toml missing");
|
|
||||||
let config2 = fs::read_to_string(home.path().join("robot2/.weevil.toml"))
|
assert!(config1.contains(&sdk1.display().to_string()));
|
||||||
.expect("robot2 .weevil.toml missing");
|
assert!(config2.contains(&sdk2.display().to_string()));
|
||||||
|
assert!(config1.contains("v10.1.1"));
|
||||||
assert!(config1.contains("project_name = \"robot1\""), "robot1 config wrong");
|
assert!(config2.contains("v11.0.0"));
|
||||||
assert!(config2.contains("project_name = \"robot2\""), "robot2 config wrong");
|
|
||||||
assert!(config1.contains("ftc_sdk_path"), "robot1 missing ftc_sdk_path");
|
|
||||||
assert!(config2.contains("ftc_sdk_path"), "robot2 missing ftc_sdk_path");
|
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,6 @@ use std::fs;
|
|||||||
use weevil::project::{ProjectBuilder, ProjectConfig};
|
use weevil::project::{ProjectBuilder, ProjectConfig};
|
||||||
use weevil::sdk::SdkConfig;
|
use weevil::sdk::SdkConfig;
|
||||||
|
|
||||||
#[path = "common.rs"]
|
|
||||||
mod common;
|
|
||||||
use common::EXPECTED_VERSION;
|
|
||||||
|
|
||||||
// Note: These tests use the actual FTC SDK if available, or skip if not
|
// Note: These tests use the actual FTC SDK if available, or skip if not
|
||||||
// For true unit testing with mocks, we'd need to refactor to use dependency injection
|
// For true unit testing with mocks, we'd need to refactor to use dependency injection
|
||||||
|
|
||||||
@@ -17,7 +13,6 @@ use common::EXPECTED_VERSION;
|
|||||||
fn test_config_create_and_save() {
|
fn test_config_create_and_save() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let sdk_path = temp_dir.path().join("mock-sdk");
|
let sdk_path = temp_dir.path().join("mock-sdk");
|
||||||
let android_sdk_path = temp_dir.path().join("android-sdk");
|
|
||||||
|
|
||||||
// Create minimal SDK structure
|
// Create minimal SDK structure
|
||||||
fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap();
|
fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap();
|
||||||
@@ -25,12 +20,11 @@ fn test_config_create_and_save() {
|
|||||||
fs::write(sdk_path.join("build.gradle"), "// test").unwrap();
|
fs::write(sdk_path.join("build.gradle"), "// test").unwrap();
|
||||||
fs::write(sdk_path.join(".version"), "v10.1.1").unwrap();
|
fs::write(sdk_path.join(".version"), "v10.1.1").unwrap();
|
||||||
|
|
||||||
let config = ProjectConfig::new("test-robot", sdk_path.clone(), android_sdk_path.clone()).unwrap();
|
let config = ProjectConfig::new("test-robot", sdk_path.clone()).unwrap();
|
||||||
|
|
||||||
assert_eq!(config.project_name, "test-robot");
|
assert_eq!(config.project_name, "test-robot");
|
||||||
assert_eq!(config.ftc_sdk_path, sdk_path);
|
assert_eq!(config.ftc_sdk_path, sdk_path);
|
||||||
assert_eq!(config.android_sdk_path, android_sdk_path);
|
assert_eq!(config.weevil_version, "1.0.0");
|
||||||
assert_eq!(config.weevil_version, EXPECTED_VERSION);
|
|
||||||
|
|
||||||
// Save and reload
|
// Save and reload
|
||||||
let project_path = temp_dir.path().join("project");
|
let project_path = temp_dir.path().join("project");
|
||||||
@@ -40,14 +34,12 @@ fn test_config_create_and_save() {
|
|||||||
let loaded = ProjectConfig::load(&project_path).unwrap();
|
let loaded = ProjectConfig::load(&project_path).unwrap();
|
||||||
assert_eq!(loaded.project_name, config.project_name);
|
assert_eq!(loaded.project_name, config.project_name);
|
||||||
assert_eq!(loaded.ftc_sdk_path, config.ftc_sdk_path);
|
assert_eq!(loaded.ftc_sdk_path, config.ftc_sdk_path);
|
||||||
assert_eq!(loaded.android_sdk_path, config.android_sdk_path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_toml_format() {
|
fn test_config_toml_format() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
let sdk_path = temp_dir.path().join("sdk");
|
let sdk_path = temp_dir.path().join("sdk");
|
||||||
let android_sdk_path = temp_dir.path().join("android-sdk");
|
|
||||||
|
|
||||||
// Create minimal SDK
|
// Create minimal SDK
|
||||||
fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap();
|
fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap();
|
||||||
@@ -55,7 +47,7 @@ fn test_config_toml_format() {
|
|||||||
fs::write(sdk_path.join("build.gradle"), "// test").unwrap();
|
fs::write(sdk_path.join("build.gradle"), "// test").unwrap();
|
||||||
fs::write(sdk_path.join(".version"), "v10.1.1").unwrap();
|
fs::write(sdk_path.join(".version"), "v10.1.1").unwrap();
|
||||||
|
|
||||||
let config = ProjectConfig::new("my-robot", sdk_path, android_sdk_path).unwrap();
|
let config = ProjectConfig::new("my-robot", sdk_path).unwrap();
|
||||||
|
|
||||||
let project_path = temp_dir.path().join("project");
|
let project_path = temp_dir.path().join("project");
|
||||||
fs::create_dir_all(&project_path).unwrap();
|
fs::create_dir_all(&project_path).unwrap();
|
||||||
@@ -64,10 +56,9 @@ fn test_config_toml_format() {
|
|||||||
let content = fs::read_to_string(project_path.join(".weevil.toml")).unwrap();
|
let content = fs::read_to_string(project_path.join(".weevil.toml")).unwrap();
|
||||||
|
|
||||||
assert!(content.contains("project_name = \"my-robot\""));
|
assert!(content.contains("project_name = \"my-robot\""));
|
||||||
assert!(content.contains(&format!("weevil_version = \"{}\"", EXPECTED_VERSION)));
|
assert!(content.contains("weevil_version = \"1.0.0\""));
|
||||||
assert!(content.contains("ftc_sdk_path"));
|
assert!(content.contains("ftc_sdk_path"));
|
||||||
assert!(content.contains("ftc_sdk_version"));
|
assert!(content.contains("ftc_sdk_version"));
|
||||||
assert!(content.contains("android_sdk_path"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,390 +0,0 @@
|
|||||||
//! Integration tests for proxy support.
|
|
||||||
//!
|
|
||||||
//! Architecture per test:
|
|
||||||
//!
|
|
||||||
//! weevil (or ProxyConfig::client())
|
|
||||||
//! │ --proxy http://127.0.0.1:<proxy_port>
|
|
||||||
//! ▼
|
|
||||||
//! TestProxy ← real forwarding proxy, hyper HTTP/1.1 server
|
|
||||||
//! │ forwards absolute-form URI to origin
|
|
||||||
//! ▼
|
|
||||||
//! mockito origin ← fake dl.google.com / github.com, returns canned bytes
|
|
||||||
//!
|
|
||||||
//! This proves traffic actually traverses the proxy, not just that the download
|
|
||||||
//! works. The TestProxy struct is the only custom code; everything else is
|
|
||||||
//! standard mockito + assert_cmd.
|
|
||||||
|
|
||||||
use std::convert::Infallible;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::sync::Mutex;
|
|
||||||
|
|
||||||
// Tests that mutate HTTPS_PROXY / HTTP_PROXY must not run concurrently —
|
|
||||||
// those env vars are process-global. cargo test runs tests in parallel
|
|
||||||
// within a single binary by default, so we serialize access with this lock.
|
|
||||||
// Tests that don't touch env vars (or that only use --proxy flag) skip it.
|
|
||||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
|
||||||
|
|
||||||
use hyper::body::Bytes;
|
|
||||||
use hyper::server::conn::http1;
|
|
||||||
use hyper::service::service_fn;
|
|
||||||
use hyper_util::client::legacy::Client;
|
|
||||||
use hyper_util::client::legacy::connect::HttpConnector;
|
|
||||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
|
||||||
use http_body_util::{BodyExt, Full};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
|
|
||||||
use weevil::sdk::proxy::ProxyConfig;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// TestProxy — a minimal HTTP forward proxy for use in tests.
|
|
||||||
//
|
|
||||||
// Binds to 127.0.0.1:0 (OS picks the port), spawns a tokio task to serve
|
|
||||||
// connections, and shuts down when dropped. Also counts how many requests
|
|
||||||
// it forwarded so tests can assert the proxy was actually hit.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// A live forwarding proxy bound to a random local port.
|
|
||||||
struct TestProxy {
|
|
||||||
addr: SocketAddr,
|
|
||||||
request_count: Arc<AtomicU64>,
|
|
||||||
/// Dropping this handle shuts the server down.
|
|
||||||
_shutdown: tokio::sync::mpsc::Sender<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The actual proxy handler, extracted to a named async fn.
|
|
||||||
///
|
|
||||||
/// You cannot put a return-type annotation on an `async move { }` block, and
|
|
||||||
/// you cannot use `-> impl Future` on a closure inside `service_fn` because
|
|
||||||
/// the opaque future type is unnameable. A named `async fn` is the one place
|
|
||||||
/// Rust lets you write both `async` and an explicit return type in the same
|
|
||||||
/// spot — the compiler knows the concrete future type and can verify the
|
|
||||||
/// `Into<Box<dyn Error>>` bound that `serve_connection` requires.
|
|
||||||
async fn proxy_handler(
|
|
||||||
client: Client<HttpConnector, Full<Bytes>>,
|
|
||||||
counter: Arc<AtomicU64>,
|
|
||||||
req: hyper::Request<hyper::body::Incoming>,
|
|
||||||
) -> Result<hyper::Response<Full<Bytes>>, Infallible> {
|
|
||||||
counter.fetch_add(1, Ordering::Relaxed);
|
|
||||||
|
|
||||||
// The client sends the request in absolute-form:
|
|
||||||
// GET http://origin:PORT/path HTTP/1.1
|
|
||||||
// hyper parses that into req.uri() for us.
|
|
||||||
let uri = req.uri().clone();
|
|
||||||
|
|
||||||
// Collect the incoming body so we can forward it.
|
|
||||||
let body_bytes = req.into_body().collect().await
|
|
||||||
.map(|b| b.to_bytes())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let forwarded = hyper::Request::builder()
|
|
||||||
.method("GET")
|
|
||||||
.uri(uri)
|
|
||||||
.body(Full::new(body_bytes))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
match client.request(forwarded).await {
|
|
||||||
Ok(upstream_resp) => {
|
|
||||||
// Collect the upstream body and re-wrap so we return a concrete
|
|
||||||
// body type (not Incoming).
|
|
||||||
let status = upstream_resp.status();
|
|
||||||
let headers = upstream_resp.headers().clone();
|
|
||||||
let collected = upstream_resp.into_body().collect().await
|
|
||||||
.map(|b| b.to_bytes())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut resp = hyper::Response::builder()
|
|
||||||
.status(status)
|
|
||||||
.body(Full::new(collected))
|
|
||||||
.unwrap();
|
|
||||||
*resp.headers_mut() = headers;
|
|
||||||
Ok(resp)
|
|
||||||
}
|
|
||||||
Err(_) => Ok(
|
|
||||||
hyper::Response::builder()
|
|
||||||
.status(502)
|
|
||||||
.body(Full::new(Bytes::from("Bad Gateway")))
|
|
||||||
.unwrap()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestProxy {
|
|
||||||
async fn start() -> Self {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
||||||
let addr = listener.local_addr().unwrap();
|
|
||||||
|
|
||||||
let count = Arc::new(AtomicU64::new(0));
|
|
||||||
let count_clone = count.clone();
|
|
||||||
|
|
||||||
let (shutdown_tx, mut shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
|
||||||
|
|
||||||
let hyper_client: Client<HttpConnector, Full<Bytes>> =
|
|
||||||
Client::builder(TokioExecutor::new()).build_http();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let stream = tokio::select! {
|
|
||||||
Ok((stream, _peer)) = listener.accept() => stream,
|
|
||||||
_ = shutdown_rx.recv() => break,
|
|
||||||
else => break,
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = hyper_client.clone();
|
|
||||||
let counter = count_clone.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let io = TokioIo::new(stream);
|
|
||||||
// The closure just delegates to proxy_handler. The named
|
|
||||||
// fn's return type is visible to the compiler so it can
|
|
||||||
// resolve the Error associated type that serve_connection
|
|
||||||
// needs.
|
|
||||||
let _ = http1::Builder::new()
|
|
||||||
.serve_connection(io, service_fn(move |req| {
|
|
||||||
proxy_handler(client.clone(), counter.clone(), req)
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
addr,
|
|
||||||
request_count: count,
|
|
||||||
_shutdown: shutdown_tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn proxy_url(&self) -> String {
|
|
||||||
format!("http://{}", self.addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn requests_forwarded(&self) -> u64 {
|
|
||||||
self.request_count.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Tests: ProxyConfig::client() talking through TestProxy to mockito.
|
|
||||||
//
|
|
||||||
// reqwest's blocking::Client owns an internal tokio Runtime. Dropping it
|
|
||||||
// inside an async context panics on tokio ≥ 1.49 ("Cannot drop a runtime in
|
|
||||||
// a context where blocking is not allowed"). Each test therefore does its
|
|
||||||
// reqwest work — client creation, HTTP calls, and drop — inside
|
|
||||||
// spawn_blocking, which runs on a thread-pool thread where blocking is fine.
|
|
||||||
// The mockito origin and TestProxy stay in the async body because they need
|
|
||||||
// the tokio runtime for their listeners.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn proxy_forwards_request_to_origin() {
|
|
||||||
let mut origin = mockito::Server::new_async().await;
|
|
||||||
let _mock = origin
|
|
||||||
.mock("GET", "/sdk.zip")
|
|
||||||
.with_status(200)
|
|
||||||
.with_header("content-type", "application/octet-stream")
|
|
||||||
.with_body(b"fake-sdk-bytes".as_slice())
|
|
||||||
.create_async()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let proxy = TestProxy::start().await;
|
|
||||||
let proxy_url = proxy.proxy_url();
|
|
||||||
let origin_url = origin.url();
|
|
||||||
|
|
||||||
let (status, body) = tokio::task::spawn_blocking(move || {
|
|
||||||
let config = ProxyConfig::resolve(Some(&proxy_url), false).unwrap();
|
|
||||||
let client = config.client().unwrap();
|
|
||||||
let resp = client.get(format!("{}/sdk.zip", origin_url)).send().unwrap();
|
|
||||||
(resp.status().as_u16(), resp.text().unwrap())
|
|
||||||
}).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(status, 200);
|
|
||||||
assert_eq!(body, "fake-sdk-bytes");
|
|
||||||
assert_eq!(proxy.requests_forwarded(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn no_proxy_bypasses_proxy_entirely() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
|
|
||||||
let mut origin = mockito::Server::new_async().await;
|
|
||||||
let _mock = origin
|
|
||||||
.mock("GET", "/direct.txt")
|
|
||||||
.with_status(200)
|
|
||||||
.with_body("direct-hit")
|
|
||||||
.create_async()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let proxy = TestProxy::start().await;
|
|
||||||
let proxy_url = proxy.proxy_url();
|
|
||||||
let origin_url = origin.url();
|
|
||||||
|
|
||||||
let (status, body) = tokio::task::spawn_blocking(move || {
|
|
||||||
std::env::set_var("HTTPS_PROXY", &proxy_url);
|
|
||||||
let config = ProxyConfig::resolve(None, true).unwrap();
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
let client = config.client().unwrap();
|
|
||||||
let resp = client.get(format!("{}/direct.txt", origin_url)).send().unwrap();
|
|
||||||
(resp.status().as_u16(), resp.text().unwrap())
|
|
||||||
}).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(status, 200);
|
|
||||||
assert_eq!(body, "direct-hit");
|
|
||||||
assert_eq!(proxy.requests_forwarded(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn env_var_proxy_is_picked_up() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
|
|
||||||
let mut origin = mockito::Server::new_async().await;
|
|
||||||
let _mock = origin
|
|
||||||
.mock("GET", "/env.txt")
|
|
||||||
.with_status(200)
|
|
||||||
.with_body("via-env-proxy")
|
|
||||||
.create_async()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let proxy = TestProxy::start().await;
|
|
||||||
let proxy_url = proxy.proxy_url();
|
|
||||||
let origin_url = origin.url();
|
|
||||||
|
|
||||||
let (status, body) = tokio::task::spawn_blocking(move || {
|
|
||||||
std::env::remove_var("HTTP_PROXY");
|
|
||||||
std::env::remove_var("http_proxy");
|
|
||||||
std::env::remove_var("https_proxy");
|
|
||||||
std::env::set_var("HTTPS_PROXY", &proxy_url);
|
|
||||||
let config = ProxyConfig::resolve(None, false).unwrap();
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
let client = config.client().unwrap();
|
|
||||||
let resp = client.get(format!("{}/env.txt", origin_url)).send().unwrap();
|
|
||||||
(resp.status().as_u16(), resp.text().unwrap())
|
|
||||||
}).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(status, 200);
|
|
||||||
assert_eq!(body, "via-env-proxy");
|
|
||||||
assert_eq!(proxy.requests_forwarded(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn explicit_proxy_flag_overrides_env() {
|
|
||||||
let _env_lock = ENV_MUTEX.lock().unwrap();
|
|
||||||
|
|
||||||
let mut origin = mockito::Server::new_async().await;
|
|
||||||
let _mock = origin
|
|
||||||
.mock("GET", "/override.txt")
|
|
||||||
.with_status(200)
|
|
||||||
.with_body("flag-proxy-wins")
|
|
||||||
.create_async()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let decoy = TestProxy::start().await;
|
|
||||||
let real = TestProxy::start().await;
|
|
||||||
let decoy_url = decoy.proxy_url();
|
|
||||||
let real_url = real.proxy_url();
|
|
||||||
let origin_url = origin.url();
|
|
||||||
|
|
||||||
let (status, body) = tokio::task::spawn_blocking(move || {
|
|
||||||
std::env::set_var("HTTPS_PROXY", &decoy_url);
|
|
||||||
let config = ProxyConfig::resolve(Some(&real_url), false).unwrap();
|
|
||||||
std::env::remove_var("HTTPS_PROXY");
|
|
||||||
let client = config.client().unwrap();
|
|
||||||
let resp = client.get(format!("{}/override.txt", origin_url)).send().unwrap();
|
|
||||||
(resp.status().as_u16(), resp.text().unwrap())
|
|
||||||
}).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(status, 200);
|
|
||||||
assert_eq!(body, "flag-proxy-wins");
|
|
||||||
assert_eq!(real.requests_forwarded(), 1);
|
|
||||||
assert_eq!(decoy.requests_forwarded(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn proxy_returns_502_when_origin_is_unreachable() {
|
|
||||||
let proxy = TestProxy::start().await;
|
|
||||||
let proxy_url = proxy.proxy_url();
|
|
||||||
|
|
||||||
let status = tokio::task::spawn_blocking(move || {
|
|
||||||
let config = ProxyConfig::resolve(Some(&proxy_url), false).unwrap();
|
|
||||||
let client = config.client().unwrap();
|
|
||||||
let resp = client.get("http://127.0.0.1:1/unreachable").send().unwrap();
|
|
||||||
resp.status().as_u16()
|
|
||||||
}).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(status, 502);
|
|
||||||
assert_eq!(proxy.requests_forwarded(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn multiple_sequential_requests_all_forwarded() {
|
|
||||||
let mut origin = mockito::Server::new_async().await;
|
|
||||||
let _m1 = origin.mock("GET", "/a").with_status(200).with_body("aaa").create_async().await;
|
|
||||||
let _m2 = origin.mock("GET", "/b").with_status(200).with_body("bbb").create_async().await;
|
|
||||||
let _m3 = origin.mock("GET", "/c").with_status(200).with_body("ccc").create_async().await;
|
|
||||||
|
|
||||||
let proxy = TestProxy::start().await;
|
|
||||||
let proxy_url = proxy.proxy_url();
|
|
||||||
let origin_url = origin.url();
|
|
||||||
|
|
||||||
let (a, b, c) = tokio::task::spawn_blocking(move || {
|
|
||||||
let config = ProxyConfig::resolve(Some(&proxy_url), false).unwrap();
|
|
||||||
let client = config.client().unwrap();
|
|
||||||
let a = client.get(format!("{}/a", origin_url)).send().unwrap().text().unwrap();
|
|
||||||
let b = client.get(format!("{}/b", origin_url)).send().unwrap().text().unwrap();
|
|
||||||
let c = client.get(format!("{}/c", origin_url)).send().unwrap().text().unwrap();
|
|
||||||
(a, b, c)
|
|
||||||
}).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(a, "aaa");
|
|
||||||
assert_eq!(b, "bbb");
|
|
||||||
assert_eq!(c, "ccc");
|
|
||||||
assert_eq!(proxy.requests_forwarded(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// CLI-level tests
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[allow(deprecated)] // cargo_bin_cmd! requires assert_cmd ≥ 2.2; we're on 2.1.2
|
|
||||||
#[test]
|
|
||||||
fn cli_help_shows_proxy_flags() {
|
|
||||||
let mut cmd = assert_cmd::Command::cargo_bin("weevil").unwrap();
|
|
||||||
let assert = cmd.arg("--help").assert();
|
|
||||||
assert
|
|
||||||
.success()
|
|
||||||
.stdout(predicates::prelude::predicate::str::contains("--proxy"))
|
|
||||||
.stdout(predicates::prelude::predicate::str::contains("--no-proxy"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
#[test]
|
|
||||||
fn cli_rejects_garbage_proxy_url() {
|
|
||||||
let mut cmd = assert_cmd::Command::cargo_bin("weevil").unwrap();
|
|
||||||
let assert = cmd
|
|
||||||
.arg("--proxy")
|
|
||||||
.arg("not-a-url-at-all")
|
|
||||||
.arg("sdk")
|
|
||||||
.arg("install")
|
|
||||||
.assert();
|
|
||||||
assert
|
|
||||||
.failure()
|
|
||||||
.stderr(predicates::prelude::predicate::str::contains("Invalid --proxy URL"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
#[test]
|
|
||||||
fn cli_proxy_and_no_proxy_are_mutually_exclusive_in_effect() {
|
|
||||||
let mut cmd = assert_cmd::Command::cargo_bin("weevil").unwrap();
|
|
||||||
let out = cmd
|
|
||||||
.arg("--proxy")
|
|
||||||
.arg("http://127.0.0.1:9999")
|
|
||||||
.arg("--no-proxy")
|
|
||||||
.arg("sdk")
|
|
||||||
.arg("install")
|
|
||||||
.output()
|
|
||||||
.expect("weevil binary failed to execute");
|
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
||||||
assert!(!stderr.contains("panic"), "binary panicked: {}", stderr);
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,6 @@ use std::path::PathBuf;
|
|||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
#[path = "../common.rs"]
|
|
||||||
mod common;
|
|
||||||
use common::EXPECTED_VERSION;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_config_create_and_save() {
|
fn test_config_create_and_save() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
@@ -19,7 +15,7 @@ fn test_config_create_and_save() {
|
|||||||
|
|
||||||
assert_eq!(config.project_name, "test-robot");
|
assert_eq!(config.project_name, "test-robot");
|
||||||
assert_eq!(config.ftc_sdk_path, sdk_path);
|
assert_eq!(config.ftc_sdk_path, sdk_path);
|
||||||
assert_eq!(config.weevil_version, EXPECTED_VERSION);
|
assert_eq!(config.weevil_version, "1.0.0");
|
||||||
|
|
||||||
// Save and reload
|
// Save and reload
|
||||||
config.save(temp_dir.path()).unwrap();
|
config.save(temp_dir.path()).unwrap();
|
||||||
@@ -49,7 +45,7 @@ fn test_config_toml_format() {
|
|||||||
let content = fs::read_to_string(temp_dir.path().join(".weevil.toml")).unwrap();
|
let content = fs::read_to_string(temp_dir.path().join(".weevil.toml")).unwrap();
|
||||||
|
|
||||||
assert!(content.contains("project_name = \"my-robot\""));
|
assert!(content.contains("project_name = \"my-robot\""));
|
||||||
assert!(content.contains(&format!("weevil_version = \"{}\"", EXPECTED_VERSION)));
|
assert!(content.contains("weevil_version = \"1.0.0\""));
|
||||||
assert!(content.contains("ftc_sdk_path"));
|
assert!(content.contains("ftc_sdk_path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user