11 Commits

Author SHA1 Message Date
Eric Ratliff
78abe1d65c feat: Add weevil uninstall command
Adds `weevil uninstall` with three modes of operation:
- Full uninstall removes the entire .weevil directory
- --dry-run enumerates managed components and their sizes
- --only N removes specific components by index

Acknowledges system-installed dependencies (Android SDK, Gradle)
in dry-run output so users know what will and won't be touched.
2026-01-31 13:05:20 -06:00
Eric Ratliff
d8e3c54f3d refactor: Remove SDK installation from weevil new
Project creation now requires environment setup first.
Checks system health and directs users to `weevil setup` if needed.
Separates concerns: setup installs, new creates projects.
2026-01-31 10:56:34 -06:00
Eric Ratliff
df7ca091ec feat: Add system diagnostics command
Adds `weevil doctor` to check development environment health.
Reports status of Java, FTC SDK, Android SDK, ADB, and Gradle.
Provides clear next steps based on system state.
2026-01-31 10:56:22 -06:00
Eric Ratliff
4e9575cc4f feat: Add environment setup command
Adds `weevil setup` with two modes:
- System setup: installs default SDKs and dependencies
- Project setup: reads .weevil.toml and installs project requirements

Provides platform-specific installation instructions when auto-install fails.
Never leaves users wondering what to do next.
2026-01-31 10:55:53 -06:00
Eric Ratliff
6b6ba058b7 chore: Release v1.0.0 - First stable release
Update version references from 1.0.0-rc2 to 1.0.0 across documentation.

This marks the first production-ready stable release of Weevil with complete
cross-platform support, robust Windows deployment, and comprehensive project
management features.

Changes:
- Update README.md current version to 1.0.0
- Remove "Next Release" section from ROADMAP.md (now tracking post-1.0 features)

Weevil is now stable and ready for FTC teams to use in production. 🎉
2026-01-26 19:39:11 -06:00
Eric Ratliff
8add733514 Updated email to eric@nxlearn.net 2026-01-26 19:18:26 -06:00
Eric Ratliff
8f12a0a09d Updated versioning information to align to RC2 2026-01-26 19:16:02 -06:00
Eric Ratliff
655a213113 docs: Add project roadmap for v1.0.0 and beyond
Add ROADMAP.md documenting planned features and improvements for the
initial stable release and future versions.

Roadmap includes:
- Pre-1.0.0: Critical fixes for Windows deployment and documentation
- v1.0.0 features: Enhanced project templates, improved error handling
- Post-1.0.0: Advanced features like custom templates, project analytics,
  multi-robot support, and CI/CD integration

This provides transparency for users and contributors about the project's
direction and planned capabilities.
2026-01-26 18:43:44 -06:00
Eric Ratliff
64826e2ce2 fix: Complete Windows deployment pipeline
Fixes critical bugs in Windows APK deployment workflow including APK path
resolution, adb integration, and config file parsing.

Changes:
- Fix APK search to look for TeamCode-debug.apk instead of *app-debug.apk
- Strip both single and double quotes from batch file path parsing
- Add android_sdk_path to project configuration (.weevil.toml)
- Resolve adb.exe from Android SDK platform-tools directory
- Check adb install exit code and report deployment failures correctly
- Add migration support for old .weevil.toml files missing android_sdk_path
- Update all tests to use new ProjectConfig::new() signature

The deployment workflow now properly finds the generated APK, locates adb,
and reports success/failure accurately on Windows.
2026-01-26 18:42:06 -06:00
Eric Ratliff
6626ca83d1 feat: Add Windows support and stabilize SDK installation (v1.0.0-rc1)
Complete Windows compatibility overhaul with robust cross-platform SDK management.
This release candidate establishes feature freeze for the 1.0.0 release.

Key improvements:
- Fixed Android SDK installation on Windows
  * Use cmd.exe wrapper for sdkmanager.bat with piped stdin
  * Properly reorganize cmdline-tools directory structure
  * Write license acceptances synchronously to avoid hangs

- Fixed FTC SDK configuration
  * Auto-generate local.properties with Android SDK path
  * Escape backslashes in Kotlin build.gradle.kts strings
  * Support both new installs and upgrades via ensure_local_properties()

- Enhanced Windows console output
  * Enable ANSI color support via enable_ansi_support crate
  * Maintain color compatibility across Windows versions

- Improved error handling and debugging
  * Added comprehensive logging throughout SDK installation
  * Better context messages for troubleshooting failures

Cross-platform testing verified on:
- Windows 11 with Eclipse Adoptium JDK 21
- Linux (existing support maintained)

Breaking changes: None
This RC introduces feature freeze - subsequent 1.0.x releases will be
bug fixes only. New features deferred to 1.1.0.

Closes Windows compatibility milestone.
2026-01-25 18:10:18 -06:00
Eric Ratliff
90ed42b3c5 fix: Remove unused variable warning and add release build script
- Fix unused `project_path` parameter warning in make_executable()
- Add build-release.sh for automated binary packaging
- Update .gitignore to exclude release artifacts
- Support cross-compilation for Linux and Windows binaries

Release artifacts are now built with ./build-release.sh and uploaded
to Gitea releases separately, keeping the git repo clean.
2026-01-25 01:17:47 -06:00
18 changed files with 2240 additions and 101 deletions

5
.gitignore vendored
View File

@@ -22,6 +22,11 @@ Cargo.lock
*.so *.so
*.exe *.exe
# Release packaging (uploaded to releases, not checked in)
/release-artifacts/
*.tar.gz
*.zip
# OS # OS
Thumbs.db Thumbs.db
.AppleDouble .AppleDouble

17
CHANGELOG.md Normal file
View File

@@ -0,0 +1,17 @@
# 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

View File

@@ -86,7 +86,7 @@ weevil sdk status
### From Source ### From Source
```bash ```bash
git clone https://github.com/yourusername/weevil.git git clone https://www.nxgit.dev/nexus-workshops/weevil.git
cd weevil cd weevil
cargo build --release cargo build --release
sudo cp target/release/weevil /usr/local/bin/ sudo cp target/release/weevil /usr/local/bin/
@@ -255,7 +255,7 @@ weevil new competition-bot
cd competition-bot cd competition-bot
# Project is already a git repo! # Project is already a git repo!
git remote add origin https://github.com/team/robot.git git remote add origin https://nxgit.dev/team/robot.git
git push -u origin main git push -u origin main
# Make changes # Make changes
@@ -287,7 +287,7 @@ git push
**Project Structure is Portable:** **Project Structure is Portable:**
```bash ```bash
# Team member clones repo # Team member clones repo
git clone https://github.com/team/robot.git git clone https://nxgit.dev/team/robot.git
cd robot cd robot
# Check SDK location # Check SDK location
@@ -466,7 +466,7 @@ Contributions welcome! Please:
### Development Setup ### Development Setup
```bash ```bash
git clone https://github.com/yourusername/weevil.git git clone https://www.nxgit.dev/nexus-workshops/weevil.git
cd weevil cd weevil
cargo build cargo build
cargo test cargo test
@@ -511,7 +511,7 @@ Built with frustration at unnecessarily complex robotics frameworks, and hope th
## Project Status ## Project Status
**Current Version:** 1.0.0-beta1 **Current Version:** 1.0.0
**What Works:** **What Works:**
- ✅ Project generation - ✅ Project generation
@@ -532,4 +532,7 @@ Built with frustration at unnecessarily complex robotics frameworks, and hope th
**Questions? Issues? Suggestions?** **Questions? Issues? Suggestions?**
Open an issue on GitHub or reach out to the FTC community. Let's make robot programming accessible for everyone! 🚀 📧 Email: [eric@nxlearn.net](mailto:eric@nxlearn.net)
🐛 Issues: Open an issue on the repository
Building better tools so you can build better robots. 🤖

80
build-release.sh Executable file
View File

@@ -0,0 +1,80 @@
#!/bin/bash
# Build release binaries for distribution
# This script builds both Linux and Windows binaries (cross-compile)
#
# For Windows-only builds, use build-release.ps1 on Windows
# For Linux-only builds, comment out the Windows section below
set -e
VERSION=${1:-$(git describe --tags --always)}
RELEASE_DIR="release-artifacts"
echo "Building Weevil $VERSION release binaries..."
echo ""
# Clean previous artifacts
rm -rf "$RELEASE_DIR"
mkdir -p "$RELEASE_DIR"
# Build Linux binary (optimized)
echo "Building Linux x86_64 binary..."
cargo build --release
strip target/release/weevil
# Package Linux binary
echo "Packaging Linux binaries..."
cd target/release
tar -czf "../../$RELEASE_DIR/weevil-${VERSION}-linux-x86_64.tar.gz" weevil
zip -q "../../$RELEASE_DIR/weevil-${VERSION}-linux-x86_64.zip" weevil
cd ../..
# Build Windows binary (cross-compile)
echo ""
echo "Building Windows x86_64 binary..."
# Check if Windows target is installed
if ! rustup target list | grep -q "x86_64-pc-windows-gnu (installed)"; then
echo "Installing Windows target..."
rustup target add x86_64-pc-windows-gnu
fi
# Check if MinGW is installed
if ! command -v x86_64-w64-mingw32-gcc &> /dev/null; then
echo "Warning: MinGW not found. Install with: sudo apt install mingw-w64"
echo "Skipping Windows build."
else
cargo build --release --target x86_64-pc-windows-gnu
x86_64-w64-mingw32-strip target/x86_64-pc-windows-gnu/release/weevil.exe
# Package Windows binary
echo "Packaging Windows binary..."
cd target/x86_64-pc-windows-gnu/release
zip -q "../../../$RELEASE_DIR/weevil-${VERSION}-windows-x86_64.zip" weevil.exe
cd ../../..
fi
# Generate checksums
echo ""
echo "Generating checksums..."
cd "$RELEASE_DIR"
sha256sum * > SHA256SUMS
cd ..
# Display results
echo ""
echo "═══════════════════════════════════════════════════════════"
echo " ✓ Release artifacts built successfully!"
echo "═══════════════════════════════════════════════════════════"
echo ""
echo "Artifacts in $RELEASE_DIR/:"
ls -lh "$RELEASE_DIR"
echo ""
echo "Checksums:"
cat "$RELEASE_DIR/SHA256SUMS"
echo ""
echo "Upload these files to your Gitea release:"
echo " 1. Go to: Releases → $VERSION → Edit Release"
echo " 2. Drag and drop files from $RELEASE_DIR/"
echo " 3. Save"
echo ""

627
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,627 @@
# Weevil Roadmap
This document outlines the planned feature development for Weevil across multiple versions. Features are subject to change based on user feedback, technical constraints, and market needs.
---
## Version 1.1.0 - Core Stability & Team Adoption
**Theme:** Making Weevil production-ready for FTC teams with essential operational features and reducing friction in existing workflows.
### System Audit & Diagnostics
**Feature:** `weevil status` or `weevil doctor` command
**Description:** Provides a comprehensive audit of the development environment, showing what's installed and what versions are present. This would display:
- FTC SDK versions (current and available)
- Android SDK installation status and version
- Gradle version and location
- Java/JDK version and location
- ADB availability and version
- Any other critical dependencies Weevil manages
**Rationale:** Teams need visibility into their environment to troubleshoot issues. Coaches working with multiple machines need to quickly verify setup consistency across laptops. This builds trust by making Weevil's actions transparent.
**Pros:**
- Straightforward to implement - query what `weevil setup` installed
- High value for troubleshooting
- Professional tooling feel
- Helps with team onboarding (new members can verify setup)
**Cons:**
- Need to handle edge cases (partial installations, manual modifications)
- Version detection across platforms may be fragile
- Output formatting needs to be clear for non-technical users
**Priority:** HIGH - Essential for v1.1.0
---
### Dependency Cleanup
**Feature:** `weevil clean` or `weevil uninstall` command
**Description:** Removes dependencies that Weevil installed during setup. This includes:
- FTC SDK files
- Android SDK components (if Weevil installed them)
- Gradle distributions
- Configuration files Weevil created
Should offer options for selective cleanup (e.g., keep SDK but remove Gradle) or complete removal.
**Rationale:** Teams switch machines, need to free disk space, or want to start fresh. Without a clean uninstall, Weevil leaves artifacts behind. This is critical for maintaining system hygiene and building confidence that Weevil doesn't pollute the environment.
**Pros:**
- Demonstrates respect for users' systems
- Essential for testing and development
- Helps with troubleshooting (clean slate approach)
**Cons:**
- Must track what Weevil installed vs. what user installed manually
- Risk of removing shared dependencies other tools need
- Need careful confirmation prompts to prevent accidental deletion
**Priority:** HIGH - Essential for v1.1.0
---
### Corporate/School Proxy Support
**Feature:** Transparent proxy configuration for all network operations
**Description:** Automatically detect and use system proxy settings (or allow manual configuration) for all network operations including:
- Gradle dependency downloads
- Android SDK downloads
- FTC SDK downloads
- Any HTTP/HTTPS requests Weevil makes
Handle `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` environment variables and write appropriate configuration into Gradle properties, Android SDK manager config, etc.
**Rationale:** Many FTC teams work in schools or corporate environments with mandatory proxy servers. Without proxy support, Weevil is unusable in these environments, cutting off a significant portion of the potential user base.
**Pros:**
- Unlocks enterprise/school environments
- Relatively well-understood problem space
- Shows professionalism and enterprise-readiness
**Cons:**
- Proxy configurations vary widely
- Authentication (proxy username/password) adds complexity
- SSL/certificate issues in corporate environments
- Testing requires access to proxy environments
**Priority:** HIGH - Essential for v1.1.0
---
### Android Studio Integration
**Feature:** Seamless integration with Android Studio IDE
**Description:** Generate Android Studio project files and configurations that:
- Hide Weevil's internal directory structure from the IDE view
- Present a clean, minimal file tree to students
- Hook Weevil's build and deploy scripts into Android Studio's "Run" button
- Properly configure the IDE's indexing and code completion
- Support debugging integration
The goal: students work in Android Studio (the tool they know) but get Weevil's improved project structure and deployment workflow behind the scenes.
**Rationale:** This is the killer feature that bridges the gap between Weevil's better engineering practices and students' existing workflow. Kids already know Android Studio. Making Weevil "just work" with it removes adoption friction and lets them focus on robot code, not tooling.
**Pros:**
- Huge competitive advantage for Nexus Workshops
- Leverages existing student knowledge
- Reduces cognitive load (one less tool to learn)
- Makes Weevil invisible in the best way - it just works
**Cons:**
- Android Studio project file format may change
- Complex to test across different Android Studio versions
- May conflict with students' existing Android Studio customizations
- Requires deep understanding of IDE configuration
**Priority:** HIGH - Strong candidate for v1.1.0 (killer feature)
---
### Manual Installation Fallback Documentation
**Feature:** Comprehensive manual setup documentation
**Description:** Detailed, step-by-step instructions for manually installing every dependency when automation fails. This includes:
- Screenshots or terminal output examples
- Platform-specific variations (Windows vs. Linux)
- Common error messages and solutions
- Checksums for verifying downloads
- Fallback download URLs if primary sources are blocked
**Rationale:** Automation fails. Proxies block downloads. Firewalls interfere. Having a "guaranteed to work" manual path builds confidence and ensures teams aren't stuck. This is about providing an escape hatch and building trust.
**Pros:**
- Low effort (documentation, not code)
- High value when automation fails
- Educational - teaches students what's happening under the hood
- Demonstrates thoroughness and professionalism
**Cons:**
- Requires maintenance as dependencies evolve
- Screenshots go stale quickly
- Platform variations multiply documentation burden
**Priority:** MEDIUM-HIGH - Strong candidate for v1.1.0
---
### Package Distribution (Debian/Ubuntu)
**Feature:** `.deb` package for easy installation on Debian-based systems
**Description:** Create Debian packages that can be installed via `sudo dpkg -i weevil_1.1.0_amd64.deb` or distributed through a personal APT repository. Package would:
- Install weevil binary to `/usr/bin`
- Include man pages and documentation
- Handle any system dependencies
- Support clean uninstallation
**Rationale:** Provides a "professional" distribution method for Linux users. Makes Weevil feel like real software, not just a script. Easier for schools/teams to deploy across multiple machines.
**Pros:**
- Professional appearance
- Standard Linux distribution method
- Can include in deployment automation (Ansible, etc.)
- `cargo-deb` makes this relatively easy
**Cons:**
- Maintenance overhead for packaging
- Need to support multiple Ubuntu/Debian versions
- Most teams will just download the binary anyway
- Not essential for functionality
**Priority:** LOW - Nice to have, but not essential for v1.1.0
---
## Version 1.2.0 - Polish & Accessibility
**Theme:** Making Weevil accessible to non-technical users and expanding platform support.
### Windows Installer (MSI)
**Feature:** Professional Windows installer package
**Description:** Create an MSI installer using WiX Toolset or `cargo-wix` that:
- Installs weevil.exe to Program Files
- Adds weevil to system PATH automatically
- Creates Start Menu entries
- Appears in "Programs and Features" for clean uninstall
- Optionally creates desktop shortcut
**Rationale:** Windows users expect installers, not loose executables. An MSI makes Weevil feel professional and legitimate. Start menu integration makes it discoverable.
**Pros:**
- Expected Windows UX
- Automatic PATH configuration (users don't need to understand this)
- Professional appearance
- Easy deployment in school environments
**Cons:**
- MSI creation and signing has complexity
- Code signing certificates cost money ($200+/year)
- Without code signing, Windows shows security warnings
- Testing across Windows versions (10, 11)
**Priority:** MEDIUM - Polish feature for Windows adoption
---
### Linux Program Launcher Integration
**Feature:** Desktop file and menu integration for Linux
**Description:** Include `.desktop` files and icon assets that integrate with Linux desktop environments (GNOME, KDE, XFCE). This makes Weevil appear in application menus and launchers. Likely bundled with the .deb package.
**Rationale:** Makes Weevil discoverable in the GUI for users who aren't comfortable with terminals. Fits the "reduce cognitive load" philosophy.
**Pros:**
- Low effort (just create .desktop files)
- Helps GUI users discover Weevil
- Standard Linux desktop integration
**Cons:**
- Different desktop environments have quirks
- Icon design needed
- Only useful if there's a GUI to launch
**Priority:** MEDIUM - Pairs well with GUI development
---
### Non-Debian Linux Distribution Support
**Feature:** Support for Arch, Fedora, Slackware, and other distributions
**Description:** Adapt installation scripts to detect and use different package managers:
- Arch: pacman
- Fedora: dnf/yum
- Slackware: pkgtool
- Generic: compile from source instructions
May also include packaging for AUR (Arch User Repository), Fedora Copr, etc.
**Rationale:** Expands addressable market. Some teams use Arch or Fedora. Shows commitment to Linux ecosystem.
**Pros:**
- Broader Linux support
- Community contributions likely (Arch users love AUR packages)
- Demonstrates technical depth
**Cons:**
- Significant testing burden across distros
- Each package manager has different quirks
- Low ROI - most teams use Ubuntu/Debian or Windows
- Maintenance overhead
**Priority:** LOW-MEDIUM - Nice to have, but niche
---
### Graphical User Interface (GUI)
**Feature:** GUI application for teams uncomfortable with terminals
**Description:** A graphical interface that wraps Weevil's functionality, allowing users to:
- Create new projects through forms/wizards
- Configure project settings visually
- Run builds and deployments with buttons
- View status and logs in a window
- Manage dependencies through checkboxes/dropdowns
**Technical Approaches:**
1. **Tauri** - Rust + web frontend (HTML/CSS/JS), native performance, small binary
2. **Local web server** - Weevil serves HTML, opens browser automatically
3. **Native GUI** - GTK, Qt, or egui (Rust native GUI)
**Rationale:** Reduces barrier to entry for students and coaches unfamiliar with terminals. Lowers cognitive load - students focus on robotics, not command syntax. Particularly valuable for younger teams or schools with limited technical resources.
**Pros:**
- Significantly lowers barrier to entry
- Appeals to visual learners
- Makes Weevil accessible to non-programmers (coaches, parents)
- Could include visual project templates/wizards
- Positions Weevil as "real software" vs. "developer tool"
**Cons:**
- Substantial development effort
- GUI framework choice has long-term implications
- Need to maintain two interfaces (CLI + GUI)
- UI design and UX is its own skillset
- Testing GUI across platforms is complex
- May need separate binary or flag to launch GUI
**Priority:** MEDIUM-HIGH - Valuable for adoption, but requires careful planning
**Dependencies:** If building a GUI, having an API layer (see below) makes sense for architecture.
---
### REST API Layer
**Feature:** Internal API that both CLI and GUI can consume
**Description:** Refactor Weevil's core functionality behind a REST API:
- API could run as a local server or be embedded
- CLI becomes a thin client to the API
- GUI uses the same API endpoints
- Enables future integrations (VS Code extension, web dashboard, etc.)
Endpoints might include:
- `POST /project/create` - Create new project
- `GET /status` - System audit
- `POST /build` - Trigger build
- `GET /dependencies` - List installed dependencies
- etc.
**Rationale:** Clean separation of concerns. Makes adding new interfaces (GUI, IDE plugins) easier. Enables potential future features like remote builds or team collaboration.
**Pros:**
- Clean architecture
- Multiple frontends share same backend logic
- Easier testing (API can be tested independently)
- Opens door to remote/distributed features
- Could enable web-based dashboard for teams
**Cons:**
- Significant refactoring of existing code
- Adds complexity (serialization, HTTP layer, error handling)
- Local-only API doesn't provide much value initially
- May be over-engineering for current needs
- gRPC/Protobuf would be overkill unless remote features needed
**Priority:** LOW-MEDIUM - Nice architecture, but not essential unless building GUI or extensions
**Note:** If staying local-only, CLI calling library functions directly is simpler. Only build API if there's a concrete need (GUI, remote features, integrations).
---
## Version 1.3.0 - Extended Platform Support
(Features carried over from v1.2.0 if not completed, plus any new platform-specific enhancements)
---
## Version 1.4.0 - Ecosystem & Package Management
**Theme:** Transforming Weevil from a project generator into an ecosystem platform. This is where Weevil becomes more than what Android Studio offers.
### FTC Component Package Manager
**Feature:** Package manager for sharing and reusing FTC robot code components
**Description:** Enable teams to publish and consume reusable robot code components. Examples:
- Mechanim wheel controllers
- Sensor abstractions
- Autonomous routines
- Vision processing pipelines
- Hardware wrappers
**Potential Approaches:**
1. **Git Submodule Style (FreeBSD Ports):**
- Package index is a Git repository with manifests
- `weevil add mechanim-wheel` pulls code via Git into project
- Code is vendored locally, teams can modify
- Clean version control story
2. **Central Registry:**
- Nexus Workshops hosts package registry at nxgit.dev
- Teams publish packages with metadata (license, dependencies, version)
- `weevil search wheels` finds packages
- `weevil add team123/mechanim-wheel` installs
- Binary or source distribution
3. **Hybrid Approach:**
- Decentralized (anyone can host packages on Git)
- Nexus Workshops provides discovery/curation (searchable index)
- Teams can specify direct Git URLs or use curated registry
**Key Considerations:**
- **Licensing:** Must track and display licenses, ensure compliance
- **Namespacing:** Avoid collisions (team number prefixes? org namespaces?)
- **Versioning:** Semantic versioning, dependency resolution
- **Quality:** Curated vs. open submission, review process
- **Trust:** Code signing? Verified publishers?
**Rationale:** This is the network effect feature. Teams contribute back proven solutions. Nexus Workshops becomes the central hub for FTC software engineering knowledge. Competitive moat - no other tool offers this. Transforms FTC from "everyone reinvents wheels" to "community shares solutions."
**Pros:**
- Massive competitive differentiation
- Creates community around Weevil/Nexus Workshops
- Direct value to teams (stop reinventing proven solutions)
- Positions Nexus Workshops as FTC software authority
- Revenue potential (premium packages? consulting on custom components?)
- Network effects - more users = more packages = more value
**Cons:**
- Complex to implement correctly
- Licensing compliance is non-trivial
- Moderation/curation burden (prevent malicious code)
- Version conflicts and dependency hell
- Need critical mass of packages to be valuable
- Support burden (teams will ask for help with downloaded packages)
- Security concerns (code execution from third parties)
**Priority:** HIGH - This is the strategic differentiator for v1.4.0
**Success Criteria:**
- At least 10 high-quality packages at launch
- Clear licensing and attribution
- Simple `weevil add` and `weevil remove` workflow
- Nexus Workshops positions as curator/quality gatekeeper
---
## Version 1.5.0 - Language Expansion
**Theme:** Future-proofing Weevil for FTC's evolution
### C++ Language Support
**Feature:** Support for C++ FTC projects alongside Java
**Description:** If/when FTC officially supports C++ for robot programming, Weevil should support creating and managing C++ projects:
- C++ project templates
- Build system integration (CMake? Gradle?)
- Android NDK integration
- Debugging support
- Mixed Java/C++ projects (JNI bridges)
**Rationale:** Stay ahead of FTC changes. C++ may offer performance benefits for vision processing or complex algorithms. Supporting multiple languages positions Weevil as the universal FTC development tool.
**Pros:**
- Future-proofing
- Potential performance benefits for teams
- Differentiator if other tools don't support C++
- Demonstrates technical sophistication
**Cons:**
- Uncertain if FTC will actually support C++
- C++ toolchain complexity (NDK, build systems)
- Most teams won't need/want C++
- Significant development effort
- Testing burden (two language stacks)
**Priority:** LOW - Wait and see if FTC actually supports C++
**Trigger:** FTC officially announces C++ support
---
### Multi-Language Architecture
**Feature:** Plugin-based language support architecture
**Description:** If supporting multiple languages (Java, C++, potentially Kotlin), refactor Weevil to have a language-agnostic core with language-specific plugins:
- Core: project structure, build orchestration, deployment
- Plugins: language-specific templates, build rules, dependencies
This makes adding new languages easier and keeps core clean.
**Rationale:** Clean architecture for extensibility. Easier to maintain than language-specific code scattered throughout.
**Pros:**
- Cleaner codebase
- Community could contribute language plugins
- Future-proof for whatever FTC supports next
**Cons:**
- Significant refactoring
- May be over-engineering if only supporting Java + maybe C++
- Plugin API needs careful design
**Priority:** LOW-MEDIUM - Only if supporting 3+ languages
---
## Version 1.6.0+ - Advanced Tooling
**Theme:** Making Weevil an all-in-one development environment
### Troubleshooting Suite
**Feature:** Comprehensive diagnostic and debugging tools
**Description:** A suite of troubleshooting tools that help teams diagnose common problems:
**Potential Components:**
1. **Connectivity Diagnostics:**
- `weevil diagnose adb` - Check ADB connection to robot controller
- Detect USB vs. WiFi connection issues
- Test latency and connection stability
2. **Build Analysis:**
- Parse build errors and suggest fixes
- Detect common misconfigurations (wrong SDK version, missing dependencies)
- Gradle build cache issues
3. **Log Analysis:**
- `weevil logs analyze` - Parse robot logs for common errors
- Highlight crashes, exceptions, performance issues
- Suggest fixes based on error patterns
4. **Performance Profiling:**
- Measure loop times
- Identify performance bottlenecks in autonomous
- Memory usage analysis
5. **Code Quality Checks:**
- Static analysis for common mistakes
- Style guide compliance
- Anti-pattern detection (blocking operations in main loop, etc.)
6. **Interactive Troubleshooter:**
- Wizard-style troubleshooting ("What problem are you having?")
- Step-by-step guidance
- Link to documentation/solutions
**Rationale:** This is a game-changer for teams without experienced mentors. Most FTC teams struggle with debugging. Automated troubleshooting reduces frustration and keeps teams moving. Positions Nexus Workshops as the support resource.
**Pros:**
- High value - debugging is painful for teams
- Competitive advantage (no other tool offers this)
- Reduces support burden (self-service troubleshooting)
- Educational - teaches debugging skills
- Could integrate with package manager (suggest better packages)
**Cons:**
- Requires deep knowledge of common FTC issues
- Error pattern recognition is complex
- May give wrong advice (false positives)
- Maintenance as FTC SDK evolves
- Difficult to test comprehensively
**Priority:** MEDIUM-HIGH - Valuable but complex, needs careful design
**Implementation Note:** Could start simple (common error pattern matching) and evolve based on real team issues encountered through Nexus Workshops.
---
## Version 2.0.0 - Major Architectural Changes
**Theme:** Breaking changes that significantly improve Weevil but may require migration
**Potential Features:**
- Complete rewrite of core systems if architecture needs major overhaul
- Breaking changes to project structure
- New configuration format
- Major changes to CLI interface
- Integration with cloud services (build servers, team collaboration)
**Note:** Given the `weevil upgrade` command's ability to migrate projects, moving to 2.0.0 may not be necessary unless there are fundamental architecture changes that can't be backward compatible.
---
## Unscheduled / Research Needed
### Cloud Build Services
**Description:** Remote build servers for teams with slow computers. Teams push code, Weevil builds in the cloud, streams back APK.
**Status:** Research - needs cost/benefit analysis, infrastructure planning
---
### VS Code Extension
**Description:** Extension for VS Code to provide similar integration as Android Studio.
**Status:** Research - depends on VS Code adoption in FTC community
---
### Team Collaboration Features
**Description:** Features for teams to coordinate across multiple developers - shared configurations, code review integration, task tracking.
**Status:** Research - needs market validation (do teams want this?)
---
### Custom Hardware Support
**Description:** Templates and tools for teams using custom sensors or actuators beyond standard FTC parts.
**Status:** Research - depends on community need
---
## Versioning Philosophy
- **1.x.0 releases:** New features, backward compatible
- **1.x.y releases:** Bug fixes, patches, documentation
- **2.0.0:** Breaking changes requiring migration (avoid if possible)
The `weevil upgrade` command is designed to migrate projects forward across versions, making major version bumps less necessary.
---
## Success Metrics
How we'll measure if Weevil is succeeding:
- **Adoption:** Number of teams using Weevil (tracked via downloads, GitHub stars)
- **Retention:** Teams continuing to use across seasons
- **Nexus Workshops impact:** Does Weevil drive workshop signups or consulting engagement?
- **Community:** Package contributions, GitHub issues/PRs, community discussions
- **Competitive outcomes:** Do Nexus Workshops teams using Weevil perform better?
---
## Contributing to the Roadmap
This roadmap is subject to change based on:
- User feedback from FTC teams
- Technical feasibility discoveries
- Market competition
- Strategic priorities for Nexus Workshops LLC
Features may be accelerated, deferred, or cancelled as the project evolves.
---
*Last Updated: January 2026*

267
src/commands/doctor.rs Normal file
View File

@@ -0,0 +1,267 @@
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"),
}
}

View File

@@ -3,3 +3,6 @@ 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;

View File

@@ -34,14 +34,47 @@ 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
@@ -57,34 +90,12 @@ 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. cd {}", name); println!(" 1. {}", format!("cd {}", name).bright_cyan());
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: ./gradlew test"); println!(" 4. Run tests: {}", "./gradlew test".bright_cyan());
println!(" 5. Deploy: weevil deploy {}", name); println!(" 5. Deploy to robot: {}", format!("weevil deploy {}", name).bright_cyan());
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)?;
} 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(())
}

View File

@@ -9,7 +9,7 @@ pub fn install_sdks() -> Result<()> {
let config = SdkConfig::new()?; let config = SdkConfig::new()?;
// Install FTC SDK // Install FTC SDK
crate::sdk::ftc::install(&config.ftc_sdk_path)?; 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)?; crate::sdk::android::install(&config.android_sdk_path)?;

514
src/commands/setup.rs Normal file
View File

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

393
src/commands/uninstall.rs Normal file
View File

@@ -0,0 +1,393 @@
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<()> {
if !sdk_config.cache_dir.exists() {
println!("{}", "No Weevil-managed components found.".bright_green());
println!();
return Ok(());
}
let size = dir_size(&sdk_config.cache_dir);
if dry_run {
let all_targets = scan_targets(sdk_config);
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)
}
}

View File

@@ -20,6 +20,9 @@ pub fn upgrade_project(path: &str) -> Result<()> {
// Get SDK config // Get SDK config
let sdk_config = crate::sdk::SdkConfig::new()?; let sdk_config = crate::sdk::SdkConfig::new()?;
// Ensure FTC SDK has local.properties (in case it was installed before this feature)
ensure_local_properties(&sdk_config)?;
// Load or create project config // Load or create project config
let project_config = if has_config { let project_config = if has_config {
println!("Found existing .weevil.toml"); println!("Found existing .weevil.toml");
@@ -29,7 +32,7 @@ pub fn upgrade_project(path: &str) -> Result<()> {
let project_name = project_path.file_name() let project_name = project_path.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.unwrap_or("unknown"); .unwrap_or("unknown");
crate::project::ProjectConfig::new(project_name, sdk_config.ftc_sdk_path.clone())? crate::project::ProjectConfig::new(project_name, sdk_config.ftc_sdk_path.clone(), sdk_config.android_sdk_path.clone())?
}; };
println!("Current SDK: {}", project_config.ftc_sdk_path.display()); println!("Current SDK: {}", project_config.ftc_sdk_path.display());
@@ -117,3 +120,21 @@ pub fn upgrade_project(path: &str) -> Result<()> {
Ok(()) Ok(())
} }
fn ensure_local_properties(sdk_config: &crate::sdk::SdkConfig) -> Result<()> {
let local_properties_path = sdk_config.ftc_sdk_path.join("local.properties");
if !local_properties_path.exists() {
println!("Creating local.properties in FTC SDK...");
let android_sdk_str = sdk_config.android_sdk_path
.display()
.to_string()
.replace("\\", "/");
let local_properties = format!("sdk.dir={}\n", android_sdk_str);
fs::write(&local_properties_path, local_properties)?;
println!("{} Created local.properties", "".green());
}
Ok(())
}

View File

@@ -33,6 +33,26 @@ 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
@@ -87,6 +107,10 @@ enum SdkCommands {
} }
fn main() -> Result<()> { fn main() -> Result<()> {
// Enable colors on Windows
#[cfg(windows)]
colored::control::set_virtual_terminal(true).ok();
let cli = Cli::parse(); let cli = Cli::parse();
print_banner(); print_banner();
@@ -95,6 +119,15 @@ fn main() -> Result<()> {
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()) 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())
}
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)
} }

View File

@@ -9,10 +9,16 @@ pub struct ProjectConfig {
pub weevil_version: String, pub weevil_version: String,
pub ftc_sdk_path: PathBuf, pub ftc_sdk_path: PathBuf,
pub ftc_sdk_version: String, pub ftc_sdk_version: String,
#[serde(default = "default_android_sdk_path")]
pub android_sdk_path: PathBuf,
}
fn default_android_sdk_path() -> PathBuf {
PathBuf::new()
} }
impl ProjectConfig { impl ProjectConfig {
pub fn new(project_name: &str, ftc_sdk_path: PathBuf) -> Result<Self> { pub fn new(project_name: &str, ftc_sdk_path: PathBuf, android_sdk_path: PathBuf) -> Result<Self> {
let ftc_sdk_version = crate::sdk::ftc::get_version(&ftc_sdk_path) let ftc_sdk_version = crate::sdk::ftc::get_version(&ftc_sdk_path)
.unwrap_or_else(|_| "unknown".to_string()); .unwrap_or_else(|_| "unknown".to_string());
@@ -21,6 +27,7 @@ impl ProjectConfig {
weevil_version: "1.0.0".to_string(), weevil_version: "1.0.0".to_string(),
ftc_sdk_path, ftc_sdk_path,
ftc_sdk_version, ftc_sdk_version,
android_sdk_path,
}) })
} }
@@ -34,9 +41,15 @@ impl ProjectConfig {
let contents = fs::read_to_string(&config_path) let contents = fs::read_to_string(&config_path)
.context("Failed to read .weevil.toml")?; .context("Failed to read .weevil.toml")?;
let config: ProjectConfig = toml::from_str(&contents) let mut config: ProjectConfig = toml::from_str(&contents)
.context("Failed to parse .weevil.toml")?; .context("Failed to parse .weevil.toml")?;
// Migrate old configs that don't have android_sdk_path
if config.android_sdk_path.as_os_str().is_empty() {
let sdk_config = crate::sdk::SdkConfig::new()?;
config.android_sdk_path = sdk_config.android_sdk_path;
}
Ok(config) Ok(config)
} }
@@ -77,6 +90,7 @@ impl ProjectConfig {
println!(); println!();
println!("{:.<20} {}", "FTC SDK Path", self.ftc_sdk_path.display().to_string().bright_white()); println!("{:.<20} {}", "FTC SDK Path", self.ftc_sdk_path.display().to_string().bright_white());
println!("{:.<20} {}", "FTC SDK Version", self.ftc_sdk_version.bright_white()); println!("{:.<20} {}", "FTC SDK Version", self.ftc_sdk_version.bright_white());
println!("{:.<20} {}", "Android SDK Path", self.android_sdk_path.display().to_string().bright_white());
println!(); println!();
} }
} }

View File

@@ -77,7 +77,7 @@ impl ProjectBuilder {
fn create_project_files(&self, project_path: &Path, sdk_config: &SdkConfig) -> Result<()> { fn create_project_files(&self, project_path: &Path, sdk_config: &SdkConfig) -> Result<()> {
// Create .weevil.toml config // Create .weevil.toml config
let project_config = ProjectConfig::new(&self.name, sdk_config.ftc_sdk_path.clone())?; let project_config = ProjectConfig::new(&self.name, sdk_config.ftc_sdk_path.clone(), sdk_config.android_sdk_path.clone())?;
project_config.save(project_path)?; project_config.save(project_path)?;
// README.md // README.md
@@ -87,7 +87,6 @@ impl ProjectBuilder {
FTC Robot Project generated by Weevil v1.0.0 FTC Robot Project generated by Weevil v1.0.0
## Quick Start ## Quick Start
```bash ```bash
# Test your code (runs on PC, no robot needed) # Test your code (runs on PC, no robot needed)
./gradlew test ./gradlew test
@@ -124,6 +123,9 @@ deploy.bat
fs::write(project_path.join(".weevil-version"), "1.0.0")?; 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
let sdk_path = sdk_config.ftc_sdk_path.display().to_string().replace("\\", "\\\\");
let build_gradle = format!(r#"plugins {{ let build_gradle = format!(r#"plugins {{
java java
}} }}
@@ -192,7 +194,7 @@ tasks.register<Exec>("buildApk") {{
println("✓ APK built successfully") println("✓ APK built successfully")
}} }}
}} }}
"#, sdk_config.ftc_sdk_path.display(), sdk_config.ftc_sdk_path.display()); "#, sdk_path, sdk_path);
fs::write(project_path.join("build.gradle.kts"), build_gradle)?; fs::write(project_path.join("build.gradle.kts"), build_gradle)?;
// settings.gradle.kts // settings.gradle.kts
@@ -332,34 +334,62 @@ echo "✓ Deployed!"
let deploy_bat = r#"@echo off let deploy_bat = r#"@echo off
setlocal enabledelayedexpansion setlocal enabledelayedexpansion
REM Read SDK path from config REM Read SDK paths from config
for /f "tokens=2 delims==" %%a in ('findstr /c:"ftc_sdk_path" .weevil.toml') do ( for /f "tokens=2 delims==" %%a in ('findstr /c:"ftc_sdk_path" .weevil.toml') do set SDK_DIR=%%a
set SDK_DIR=%%a for /f "tokens=2 delims==" %%a in ('findstr /c:"android_sdk_path" .weevil.toml') do set ANDROID_SDK=%%a
set SDK_DIR=!SDK_DIR:"=!
set SDK_DIR=!SDK_DIR: =! REM Strip all quotes (both single and double)
) set SDK_DIR=%SDK_DIR:"=%
set SDK_DIR=%SDK_DIR:'=%
set SDK_DIR=%SDK_DIR: =%
set ANDROID_SDK=%ANDROID_SDK:"=%
set ANDROID_SDK=%ANDROID_SDK:'=%
set ANDROID_SDK=%ANDROID_SDK: =%
if not defined SDK_DIR ( if not defined SDK_DIR (
echo Error: Could not read FTC SDK path from .weevil.toml echo Error: Could not read FTC SDK path from .weevil.toml
exit /b 1 exit /b 1
) )
if not defined ANDROID_SDK (
echo Error: Could not read Android SDK path from .weevil.toml
exit /b 1
)
REM Set ADB path
set ADB_PATH=%ANDROID_SDK%\platform-tools\adb.exe
echo Building APK... echo Building APK...
call gradlew.bat buildApk call gradlew.bat buildApk
echo. echo.
echo Deploying to Control Hub... echo Deploying to Control Hub...
REM Find APK REM Find APK - look for TeamCode-debug.apk
for /f "delims=" %%i in ('dir /s /b "%SDK_DIR%\*app-debug.apk" 2^>nul') do set APK=%%i for /f "delims=" %%i in ('dir /s /b "%SDK_DIR%\TeamCode-debug.apk" 2^>nul') do set APK=%%i
if not defined APK ( if not defined APK (
echo Error: APK not found echo Error: APK not found
exit /b 1 exit /b 1
) )
echo Found APK: %APK%
REM Check for adb
if not exist "%ADB_PATH%" (
echo Error: adb not found at %ADB_PATH%
echo Run: weevil sdk install
exit /b 1
)
echo Installing: %APK% echo Installing: %APK%
adb install -r "%APK%" "%ADB_PATH%" install -r "%APK%"
if errorlevel 1 (
echo.
echo Deployment failed!
exit /b 1
)
echo. echo.
echo Deployed! echo Deployed!
@@ -434,14 +464,14 @@ class BasicTest {
Ok(()) Ok(())
} }
fn make_executable(&self, project_path: &Path) -> Result<()> { fn make_executable(&self, _project_path: &Path) -> Result<()> {
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
let scripts = vec!["gradlew", "build.sh", "deploy.sh"]; let scripts = vec!["gradlew", "build.sh", "deploy.sh"];
for script in scripts { for script in scripts {
let path = project_path.join(script); let path = _project_path.join(script);
if path.exists() { if path.exists() {
let mut perms = fs::metadata(&path)?.permissions(); let mut perms = fs::metadata(&path)?.permissions();
perms.set_mode(0o755); perms.set_mode(0o755);

View File

@@ -8,19 +8,33 @@ use colored::*;
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";
pub fn install(sdk_path: &Path) -> Result<()> { pub fn install(sdk_path: &Path) -> Result<()> {
// Check if SDK exists AND is complete
if sdk_path.exists() { if sdk_path.exists() {
println!("{} Android SDK already installed at: {}", match verify(sdk_path) {
"".green(), Ok(_) => {
sdk_path.display() println!("{} Android SDK already installed at: {}",
); "".green(),
return Ok(()); sdk_path.display()
);
return Ok(());
}
Err(_) => {
println!("{} Android SDK found but incomplete, reinstalling...",
"".yellow()
);
// Continue with installation
}
}
} }
println!("{}", "Installing Android SDK...".bright_yellow()); println!("{}", "Installing Android SDK...".bright_yellow());
let url = if cfg!(target_os = "macos") { let url = if cfg!(target_os = "windows") {
ANDROID_SDK_URL_WINDOWS
} else if cfg!(target_os = "macos") {
ANDROID_SDK_URL_MAC ANDROID_SDK_URL_MAC
} else { } else {
ANDROID_SDK_URL_LINUX ANDROID_SDK_URL_LINUX
@@ -58,11 +72,37 @@ pub fn install(sdk_path: &Path) -> Result<()> {
let mut archive = zip::ZipArchive::new(file)?; let mut archive = zip::ZipArchive::new(file)?;
std::fs::create_dir_all(sdk_path)?; std::fs::create_dir_all(sdk_path)?;
archive.extract(sdk_path)?; archive.extract(sdk_path)
.context("Failed to extract Android SDK")?;
// Cleanup // Cleanup
std::fs::remove_file(&temp_zip)?; std::fs::remove_file(&temp_zip)?;
// The zip extracts to cmdline-tools/ but we need it in cmdline-tools/latest/
let extracted_tools = sdk_path.join("cmdline-tools");
let target_location = sdk_path.join("cmdline-tools").join("latest");
if extracted_tools.exists() && !target_location.exists() {
println!("Reorganizing cmdline-tools directory structure...");
let temp_dir = sdk_path.join("cmdline-tools-temp");
std::fs::rename(&extracted_tools, &temp_dir)
.context("Failed to rename cmdline-tools to temp directory")?;
std::fs::create_dir_all(&target_location)
.context("Failed to create cmdline-tools/latest directory")?;
for entry in std::fs::read_dir(&temp_dir)? {
let entry = entry?;
let dest = target_location.join(entry.file_name());
std::fs::rename(entry.path(), dest)
.with_context(|| format!("Failed to move {} to latest/", entry.file_name().to_string_lossy()))?;
}
std::fs::remove_dir_all(&temp_dir)
.context("Failed to remove temporary directory")?;
}
// Install required packages // Install required packages
install_packages(sdk_path)?; install_packages(sdk_path)?;
@@ -74,68 +114,120 @@ pub fn install(sdk_path: &Path) -> Result<()> {
fn install_packages(sdk_path: &Path) -> Result<()> { fn install_packages(sdk_path: &Path) -> Result<()> {
println!("Installing Android SDK packages..."); println!("Installing Android SDK packages...");
let sdkmanager = sdk_path let sdkmanager_path = sdk_path.join("cmdline-tools").join("latest").join("bin");
.join("cmdline-tools/bin/sdkmanager");
let sdkmanager = if cfg!(target_os = "windows") {
sdkmanager_path.join("sdkmanager.bat")
} else {
sdkmanager_path.join("sdkmanager")
};
if !sdkmanager.exists() { if !sdkmanager.exists() {
// Try alternate location anyhow::bail!(
let alt = sdk_path.join("cmdline-tools/latest/bin/sdkmanager"); "sdkmanager not found at expected location: {}\n\
if alt.exists() { Directory structure may be incorrect.",
return run_sdkmanager(&alt, sdk_path); sdkmanager.display()
} );
// Need to move cmdline-tools to correct location
let from = sdk_path.join("cmdline-tools");
let to = sdk_path.join("cmdline-tools/latest");
if from.exists() {
std::fs::create_dir_all(sdk_path.join("cmdline-tools"))?;
std::fs::rename(&from, &to)?;
return run_sdkmanager(&to.join("bin/sdkmanager"), sdk_path);
}
} }
println!("Found sdkmanager at: {}", sdkmanager.display());
run_sdkmanager(&sdkmanager, sdk_path) run_sdkmanager(&sdkmanager, sdk_path)
} }
fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> { fn run_sdkmanager(sdkmanager: &Path, sdk_root: &Path) -> Result<()> {
use std::process::Command; use std::process::{Command, Stdio};
use std::io::Write; use std::io::Write;
// Accept licenses println!("Accepting licenses...");
let mut yes_cmd = Command::new("yes");
let yes_output = yes_cmd.output()?;
let mut cmd = Command::new(sdkmanager); // Build command based on OS
cmd.arg("--sdk_root") let mut cmd = if cfg!(target_os = "windows") {
.arg(sdk_root) let mut c = Command::new("cmd");
c.arg("/c");
c.arg(sdkmanager);
c
} else {
Command::new(sdkmanager)
};
cmd.arg(format!("--sdk_root={}", sdk_root.display()))
.arg("--licenses") .arg("--licenses")
.stdin(std::process::Stdio::piped()) .stdin(Stdio::piped())
.spawn()? .stdout(Stdio::piped())
.stdin .stderr(Stdio::piped());
.as_mut()
.unwrap()
.write_all(&yes_output.stdout)?;
// Install packages let mut child = cmd.spawn()
Command::new(sdkmanager) .context("Failed to spawn sdkmanager for licenses")?;
.arg("--sdk_root")
.arg(sdk_root) // Write 'y' responses to accept all licenses
if let Some(mut stdin) = child.stdin.take() {
// Create a string with many 'y' responses
let responses = "y\n".repeat(20);
stdin.write_all(responses.as_bytes())
.context("Failed to write license responses")?;
// Explicitly drop stdin to close the pipe
drop(stdin);
}
let output = child.wait_with_output()
.context("Failed to wait for license acceptance")?;
if !output.status.success() {
eprintln!("License stderr: {}", String::from_utf8_lossy(&output.stderr));
eprintln!("License stdout: {}", String::from_utf8_lossy(&output.stdout));
println!("{} License acceptance may have failed, continuing anyway...", "".yellow());
} else {
println!("{} Licenses accepted", "".green());
}
println!("Installing SDK packages (this may take a few minutes)...");
// Build command for package installation
let mut cmd = if cfg!(target_os = "windows") {
let mut c = Command::new("cmd");
c.arg("/c");
c.arg(sdkmanager);
c
} else {
Command::new(sdkmanager)
};
let status = cmd
.arg(format!("--sdk_root={}", sdk_root.display()))
.arg("platform-tools") .arg("platform-tools")
.arg("platforms;android-34") .arg("platforms;android-34")
.arg("build-tools;34.0.0") .arg("build-tools;34.0.0")
.status()?; .stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.context("Failed to run sdkmanager for package installation")?;
if !status.success() {
anyhow::bail!("Failed to install Android SDK packages");
}
Ok(()) Ok(())
} }
pub fn verify(sdk_path: &Path) -> Result<()> { pub fn verify(sdk_path: &Path) -> Result<()> {
if !sdk_path.exists() { if !sdk_path.exists() {
anyhow::bail!("Android SDK not found at: {}", sdk_path.display()); anyhow::bail!(
"Android SDK not found at: {}\n\
Run 'weevil sdk install' to download it automatically,\n\
or install manually from: https://developer.android.com/studio#command-tools",
sdk_path.display()
);
} }
let platform_tools = sdk_path.join("platform-tools"); let platform_tools = sdk_path.join("platform-tools");
if !platform_tools.exists() { if !platform_tools.exists() {
anyhow::bail!("Android SDK incomplete: platform-tools not found"); anyhow::bail!(
"Android SDK incomplete: platform-tools not found\n\
Expected at: {}\n\
Run 'weevil sdk install' to complete the installation",
platform_tools.display()
);
} }
Ok(()) Ok(())

View File

@@ -2,16 +2,19 @@ use std::path::Path;
use anyhow::{Result, Context}; use anyhow::{Result, Context};
use git2::Repository; use git2::Repository;
use colored::*; use colored::*;
use std::fs;
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) -> 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(),
sdk_path.display() sdk_path.display()
); );
// Make sure local.properties exists even if SDK was already installed
create_local_properties(sdk_path, android_sdk_path)?;
return check_version(sdk_path); return check_version(sdk_path);
} }
@@ -28,11 +31,32 @@ pub fn install(sdk_path: &Path) -> Result<()> {
repo.checkout_tree(&obj, None)?; repo.checkout_tree(&obj, None)?;
repo.set_head_detached(obj.id())?; repo.set_head_detached(obj.id())?;
// Create local.properties with Android SDK path
create_local_properties(sdk_path, android_sdk_path)?;
println!("{} FTC SDK installed successfully", "".green()); println!("{} FTC SDK installed successfully", "".green());
Ok(()) Ok(())
} }
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
.display()
.to_string()
.replace("\\", "/");
let local_properties = format!("sdk.dir={}\n", android_sdk_str);
let properties_path = sdk_path.join("local.properties");
fs::write(&properties_path, local_properties)
.context("Failed to create local.properties")?;
println!("{} Created local.properties with Android SDK path", "".green());
Ok(())
}
fn check_version(sdk_path: &Path) -> Result<()> { fn check_version(sdk_path: &Path) -> Result<()> {
let repo = Repository::open(sdk_path)?; let repo = Repository::open(sdk_path)?;

View File

@@ -13,6 +13,7 @@ use weevil::sdk::SdkConfig;
fn test_config_create_and_save() { fn test_config_create_and_save() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let sdk_path = temp_dir.path().join("mock-sdk"); let sdk_path = temp_dir.path().join("mock-sdk");
let android_sdk_path = temp_dir.path().join("android-sdk");
// Create minimal SDK structure // Create minimal SDK structure
fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap(); fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap();
@@ -20,10 +21,11 @@ fn test_config_create_and_save() {
fs::write(sdk_path.join("build.gradle"), "// test").unwrap(); fs::write(sdk_path.join("build.gradle"), "// test").unwrap();
fs::write(sdk_path.join(".version"), "v10.1.1").unwrap(); fs::write(sdk_path.join(".version"), "v10.1.1").unwrap();
let config = ProjectConfig::new("test-robot", sdk_path.clone()).unwrap(); let config = ProjectConfig::new("test-robot", sdk_path.clone(), android_sdk_path.clone()).unwrap();
assert_eq!(config.project_name, "test-robot"); assert_eq!(config.project_name, "test-robot");
assert_eq!(config.ftc_sdk_path, sdk_path); assert_eq!(config.ftc_sdk_path, sdk_path);
assert_eq!(config.android_sdk_path, android_sdk_path);
assert_eq!(config.weevil_version, "1.0.0"); assert_eq!(config.weevil_version, "1.0.0");
// Save and reload // Save and reload
@@ -34,12 +36,14 @@ fn test_config_create_and_save() {
let loaded = ProjectConfig::load(&project_path).unwrap(); let loaded = ProjectConfig::load(&project_path).unwrap();
assert_eq!(loaded.project_name, config.project_name); assert_eq!(loaded.project_name, config.project_name);
assert_eq!(loaded.ftc_sdk_path, config.ftc_sdk_path); assert_eq!(loaded.ftc_sdk_path, config.ftc_sdk_path);
assert_eq!(loaded.android_sdk_path, config.android_sdk_path);
} }
#[test] #[test]
fn test_config_toml_format() { fn test_config_toml_format() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let sdk_path = temp_dir.path().join("sdk"); let sdk_path = temp_dir.path().join("sdk");
let android_sdk_path = temp_dir.path().join("android-sdk");
// Create minimal SDK // Create minimal SDK
fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap(); fs::create_dir_all(sdk_path.join("TeamCode/src/main/java")).unwrap();
@@ -47,7 +51,7 @@ fn test_config_toml_format() {
fs::write(sdk_path.join("build.gradle"), "// test").unwrap(); fs::write(sdk_path.join("build.gradle"), "// test").unwrap();
fs::write(sdk_path.join(".version"), "v10.1.1").unwrap(); fs::write(sdk_path.join(".version"), "v10.1.1").unwrap();
let config = ProjectConfig::new("my-robot", sdk_path).unwrap(); let config = ProjectConfig::new("my-robot", sdk_path, android_sdk_path).unwrap();
let project_path = temp_dir.path().join("project"); let project_path = temp_dir.path().join("project");
fs::create_dir_all(&project_path).unwrap(); fs::create_dir_all(&project_path).unwrap();
@@ -59,6 +63,7 @@ fn test_config_toml_format() {
assert!(content.contains("weevil_version = \"1.0.0\"")); assert!(content.contains("weevil_version = \"1.0.0\""));
assert!(content.contains("ftc_sdk_path")); assert!(content.contains("ftc_sdk_path"));
assert!(content.contains("ftc_sdk_version")); assert!(content.contains("ftc_sdk_version"));
assert!(content.contains("android_sdk_path"));
} }
#[test] #[test]