Compare commits
7 Commits
v1.1.0-dev
...
9db1408528
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9db1408528 | ||
|
|
39aa6f60be | ||
|
|
0188439c08 | ||
|
|
7420f8bda4 | ||
|
|
eca488f703 | ||
|
|
bcdf62ffa9 | ||
|
|
2419334f72 |
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.1"
|
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"
|
||||||
|
|||||||
275
README.md
275
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,28 @@ 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
|
||||||
- ✅ 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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**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
|
||||||
|
|
||||||
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,
|
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
**Current Version:** 1.0.0-rc1
|
||||||
|
**Next Release:** 1.1.0 (Target: TBD)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Version 1.1.0 - Core Stability & Team Adoption
|
## Version 1.1.0 - Core Stability & Team Adoption
|
||||||
|
|||||||
@@ -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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,3 @@ 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(())
|
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(())
|
||||||
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
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());
|
||||||
@@ -45,14 +44,14 @@ pub fn show_status() -> Result<()> {
|
|||||||
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());
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
77
src/main.rs
77
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,
|
||||||
}
|
}
|
||||||
@@ -53,26 +33,6 @@ enum Commands {
|
|||||||
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
|
||||||
@@ -135,21 +95,9 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
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::Install => commands::sdk::install_sdks(),
|
||||||
SdkCommands::Status => commands::sdk::show_status(),
|
SdkCommands::Status => commands::sdk::show_status(),
|
||||||
SdkCommands::Update => commands::sdk::update_sdks(&proxy),
|
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,8 +3,6 @@ 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,
|
||||||
@@ -26,7 +24,7 @@ impl ProjectConfig {
|
|||||||
|
|
||||||
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,
|
android_sdk_path,
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|
||||||
@@ -87,7 +84,7 @@ impl ProjectBuilder {
|
|||||||
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
|
||||||
@@ -417,304 +414,6 @@ class BasicTest {
|
|||||||
test_file
|
test_file
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Android Studio integration: .idea/ files
|
|
||||||
self.generate_idea_files(project_path)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate .idea/ files for Android Studio integration.
|
|
||||||
///
|
|
||||||
/// The goal is for students to open the project in Android Studio and see
|
|
||||||
/// a clean file tree (just src/ and the scripts) with Run configurations
|
|
||||||
/// that invoke Weevil's shell scripts directly. All the internal plumbing
|
|
||||||
/// (sdk/, .gradle/, build/) is hidden from the IDE view.
|
|
||||||
///
|
|
||||||
/// Android Studio uses IntelliJ's run configuration XML format. The
|
|
||||||
/// ShellScript type invokes a script relative to the project root — exactly
|
|
||||||
/// what we want since deploy.sh and build.sh already live there.
|
|
||||||
fn generate_idea_files(&self, project_path: &Path) -> Result<()> {
|
|
||||||
// workspace.xml — controls the file-tree view and hides internals.
|
|
||||||
// We use a ProjectViewPane exclude pattern list rather than touching
|
|
||||||
// the module's source roots, so this works regardless of whether the
|
|
||||||
// student has opened the project before.
|
|
||||||
let workspace_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectViewManager">
|
|
||||||
<state>
|
|
||||||
<navigator currentProjector="ProjectFiles" hideEmptyMiddlePackages="true" sortByType="true">
|
|
||||||
<state>
|
|
||||||
<expand>
|
|
||||||
<file url="file://$PROJECT_DIR$/src" />
|
|
||||||
<file url="file://$PROJECT_DIR$/src/main" />
|
|
||||||
<file url="file://$PROJECT_DIR$/src/main/java" />
|
|
||||||
<file url="file://$PROJECT_DIR$/src/main/java/robot" />
|
|
||||||
<file url="file://$PROJECT_DIR$/src/test" />
|
|
||||||
<file url="file://$PROJECT_DIR$/src/test/java" />
|
|
||||||
<file url="file://$PROJECT_DIR$/src/test/java/robot" />
|
|
||||||
</expand>
|
|
||||||
</state>
|
|
||||||
</navigator>
|
|
||||||
</state>
|
|
||||||
</component>
|
|
||||||
<component name="ExcludedFiles">
|
|
||||||
<file url="file://$PROJECT_DIR$/build" reason="Build output" />
|
|
||||||
<file url="file://$PROJECT_DIR$/.gradle" reason="Gradle cache" />
|
|
||||||
<file url="file://$PROJECT_DIR$/gradle" reason="Gradle wrapper internals" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
"#;
|
|
||||||
fs::write(project_path.join(".idea/workspace.xml"), workspace_xml)?;
|
|
||||||
|
|
||||||
// Run configurations. Each is a ShellScript type that invokes one of
|
|
||||||
// Weevil's scripts. Android Studio shows these in the Run dropdown
|
|
||||||
// at the top of the IDE — no configuration needed by the student.
|
|
||||||
//
|
|
||||||
// We generate both Unix (.sh, ./gradlew) and Windows (.bat, gradlew.bat)
|
|
||||||
// variants. Android Studio automatically hides configs whose script files
|
|
||||||
// don't exist, so only the platform-appropriate ones appear in the dropdown.
|
|
||||||
|
|
||||||
// Build (Unix) — just builds the APK without deploying
|
|
||||||
let build_unix_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Build" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/build.sh" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Build.xml"),
|
|
||||||
build_unix_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Build (Windows) — same, but calls build.bat
|
|
||||||
let build_windows_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Build (Windows)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/build.bat" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="cmd.exe" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="/c" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Build (Windows).xml"),
|
|
||||||
build_windows_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Deploy (auto) — no flags, deploy.sh auto-detects USB vs WiFi
|
|
||||||
let deploy_auto_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Deploy (auto)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.sh" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Deploy (auto).xml"),
|
|
||||||
deploy_auto_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Deploy (auto) (Windows)
|
|
||||||
let deploy_auto_windows_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Deploy (auto) (Windows)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.bat" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="cmd.exe" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="/c" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Deploy (auto) (Windows).xml"),
|
|
||||||
deploy_auto_windows_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Deploy (USB) — forces USB connection
|
|
||||||
let deploy_usb_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Deploy (USB)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.sh" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="--usb" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Deploy (USB).xml"),
|
|
||||||
deploy_usb_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Deploy (USB) (Windows)
|
|
||||||
let deploy_usb_windows_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Deploy (USB) (Windows)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.bat" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="cmd.exe" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="/c" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Deploy (USB) (Windows).xml"),
|
|
||||||
deploy_usb_windows_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Deploy (WiFi) — forces WiFi connection to default 192.168.43.1
|
|
||||||
let deploy_wifi_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Deploy (WiFi)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.sh" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="--wifi" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Deploy (WiFi).xml"),
|
|
||||||
deploy_wifi_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Deploy (WiFi) (Windows)
|
|
||||||
let deploy_wifi_windows_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Deploy (WiFi) (Windows)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/deploy.bat" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="cmd.exe" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="/c" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Deploy (WiFi) (Windows).xml"),
|
|
||||||
deploy_wifi_windows_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Test — runs the unit test suite via Gradle
|
|
||||||
let test_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Test" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/gradlew" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="test" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="/bin/bash" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Test.xml"),
|
|
||||||
test_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Test (Windows)
|
|
||||||
let test_windows_xml = r#"<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration name="Test (Windows)" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/gradlew.bat" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="test" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="cmd.exe" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="/c" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="true" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
"#;
|
|
||||||
fs::write(
|
|
||||||
project_path.join(".idea/runConfigurations/Test (Windows).xml"),
|
|
||||||
test_windows_xml,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
// When WEEVIL_HOME is set, we also skip the system Android SDK
|
|
||||||
// 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()
|
let home = dirs::home_dir()
|
||||||
.context("Could not determine home directory")?;
|
.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.1";
|
|
||||||
@@ -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]
|
||||||
@@ -41,3 +32,24 @@ fn test_sdk_status_command() {
|
|||||||
.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)
|
// Extract mock SDK
|
||||||
.arg("new")
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
.arg("test-robot")
|
|
||||||
.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);
|
|
||||||
|
|
||||||
let project_dir = home.path().join("test-robot");
|
// Verify project was created
|
||||||
assert!(output.status.success(), "weevil new failed");
|
assert!(output.status.success(), "weevil new failed: {}", String::from_utf8_lossy(&output.stderr));
|
||||||
assert!(project_dir.join(".weevil.toml").exists(), ".weevil.toml missing");
|
assert!(project_dir.join(".weevil.toml").exists());
|
||||||
assert!(project_dir.join("build.gradle.kts").exists(), "build.gradle.kts missing");
|
assert!(project_dir.join("build.gradle.kts").exists());
|
||||||
assert!(project_dir.join("src/main/java/robot").exists(), "src/main/java/robot missing");
|
assert!(project_dir.join("src/main/java/robot").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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)
|
// Extract mock SDK
|
||||||
.arg("new")
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
.arg("config-test")
|
|
||||||
.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");
|
|
||||||
|
|
||||||
let project_dir = home.path().join("config-test");
|
// Read config
|
||||||
let config_content = fs::read_to_string(project_dir.join(".weevil.toml"))
|
let config_content = fs::read_to_string(project_dir.join(".weevil.toml")).unwrap();
|
||||||
.expect(".weevil.toml not found");
|
|
||||||
|
|
||||||
assert!(config_content.contains("project_name = \"config-test\""),
|
assert!(config_content.contains("project_name = \"config-test\""));
|
||||||
"project_name missing from config:\n{}", config_content);
|
assert!(config_content.contains(&format!("ftc_sdk_path = \"{}\"", sdk_dir.display())));
|
||||||
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);
|
|
||||||
|
|
||||||
// Custom code survives
|
// Verify custom code still exists
|
||||||
assert!(custom_file.exists(), "custom code file was deleted by upgrade");
|
assert!(custom_file.exists());
|
||||||
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)
|
// Extract mock SDK
|
||||||
.arg("new")
|
MOCK_SDK.extract(&sdk_dir).unwrap();
|
||||||
.arg("build-test")
|
|
||||||
.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");
|
|
||||||
|
|
||||||
let project_dir = home.path().join("build-test");
|
// Check build.sh contains config reading
|
||||||
|
let build_sh = fs::read_to_string(project_dir.join("build.sh")).unwrap();
|
||||||
|
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"))
|
// Check build.bat contains config reading
|
||||||
.expect("build.sh not found");
|
let build_bat = fs::read_to_string(project_dir.join("build.bat")).unwrap();
|
||||||
assert!(build_sh.contains(".weevil.toml"), "build.sh doesn't reference .weevil.toml");
|
assert!(build_bat.contains(".weevil.toml"));
|
||||||
assert!(build_sh.contains("ftc_sdk_path"), "build.sh doesn't reference ftc_sdk_path");
|
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");
|
||||||
|
let project1 = test_dir.path().join("robot1");
|
||||||
|
let project2 = test_dir.path().join("robot2");
|
||||||
|
|
||||||
// Create a second FTC SDK with a different version
|
// Create two different SDK versions
|
||||||
let sdk2 = home.path().join("ftc-sdk-v11");
|
MOCK_SDK.extract(&sdk1).unwrap();
|
||||||
create_mock_ftc_sdk(&sdk2);
|
MOCK_SDK.extract(&sdk2).unwrap();
|
||||||
fs::write(sdk2.join(".version"), "v11.0.0\n").unwrap();
|
fs::write(sdk2.join(".version"), "v11.0.0").unwrap();
|
||||||
|
|
||||||
// Create first project (uses default ftc-sdk in WEEVIL_HOME)
|
// Create two projects with different SDKs
|
||||||
let output = weevil_cmd(&home)
|
Command::new("cargo")
|
||||||
.arg("new")
|
.args(&["run", "--", "new", "robot1", "--ftc-sdk", sdk1.to_str().unwrap()])
|
||||||
.arg("robot1")
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.current_dir(home.path())
|
|
||||||
.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");
|
|
||||||
|
|
||||||
// Create second project — would need --ftc-sdk flag if supported,
|
Command::new("cargo")
|
||||||
// otherwise both use the same default. Verify they each have valid configs.
|
.args(&["run", "--", "new", "robot2", "--ftc-sdk", sdk2.to_str().unwrap()])
|
||||||
let output = weevil_cmd(&home)
|
.current_dir(env!("CARGO_MANIFEST_DIR"))
|
||||||
.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");
|
|
||||||
|
|
||||||
let config1 = fs::read_to_string(home.path().join("robot1/.weevil.toml"))
|
// Verify each project has correct SDK
|
||||||
.expect("robot1 .weevil.toml missing");
|
let config1 = fs::read_to_string(project1.join(".weevil.toml")).unwrap();
|
||||||
let config2 = fs::read_to_string(home.path().join("robot2/.weevil.toml"))
|
let config2 = fs::read_to_string(project2.join(".weevil.toml")).unwrap();
|
||||||
.expect("robot2 .weevil.toml missing");
|
|
||||||
|
|
||||||
assert!(config1.contains("project_name = \"robot1\""), "robot1 config wrong");
|
assert!(config1.contains(&sdk1.display().to_string()));
|
||||||
assert!(config2.contains("project_name = \"robot2\""), "robot2 config wrong");
|
assert!(config2.contains(&sdk2.display().to_string()));
|
||||||
assert!(config1.contains("ftc_sdk_path"), "robot1 missing ftc_sdk_path");
|
assert!(config1.contains("v10.1.1"));
|
||||||
assert!(config2.contains("ftc_sdk_path"), "robot2 missing ftc_sdk_path");
|
assert!(config2.contains("v11.0.0"));
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -30,7 +26,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.android_sdk_path, android_sdk_path);
|
assert_eq!(config.android_sdk_path, android_sdk_path);
|
||||||
assert_eq!(config.weevil_version, EXPECTED_VERSION);
|
assert_eq!(config.weevil_version, "1.0.0");
|
||||||
|
|
||||||
// Save and reload
|
// Save and reload
|
||||||
let project_path = temp_dir.path().join("project");
|
let project_path = temp_dir.path().join("project");
|
||||||
@@ -64,7 +60,7 @@ 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"));
|
assert!(content.contains("android_sdk_path"));
|
||||||
|
|||||||
@@ -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