Plugins follow a strict directory organization to separate user-facing configuration from internal plugin implementation:
The config/ directory contains only files that are copied to the user's project directory (devbox.d/{platform}). This is user-facing configuration that users can modify.
Allowed in config/:
devices/- Default device definitions (min.json, max.json)- Any other user-editable configuration files
NOT allowed in config/:
- Internal plugin files (flake.nix, scripts, tests)
- Process management files (process-compose.yaml)
- Test suites (test-suite.yaml)
- Generated files (android.json, devices.lock)
Internal plugin files that get copied to .devbox/virtenv/{platform} should be at the plugin root or in their respective directories:
plugins/{platform}/
├── config/
│ └── devices/ # User config → devbox.d/{platform}/devices/
│ ├── min.json
│ └── max.json
├── scripts/ # Plugin scripts → .devbox/virtenv/{platform}/scripts/
│ ├── lib/
│ ├── platform/
│ ├── domain/
│ ├── user/
│ └── init/
├── tests/ # Plugin tests → .devbox/virtenv/{platform}/tests/
│ ├── test-lib.sh
│ └── test-devices.sh
├── flake.nix # Nix SDK composition → .devbox/virtenv/{platform}/
├── process-compose.yaml # Development services → .devbox/virtenv/{platform}/
├── test-suite.yaml # E2E test suite → .devbox/virtenv/{platform}/
├── test-summary.sh # Test summary script → .devbox/virtenv/{platform}/
├── plugin.json # Plugin manifest
├── README.md # User documentation
└── REFERENCE.md # Complete API reference
Key principle: If it goes to devbox.d, it belongs in config/. If it goes to .devbox/virtenv, it belongs at the root or in its functional directory (scripts/, tests/).
All plugins follow these patterns:
{PLATFORM}_CONFIG_DIR- Project configuration directory (devbox.d/{platform}){PLATFORM}_DEVICES_DIR- Device definitions directory{PLATFORM}_SCRIPTS_DIR- Runtime scripts directory (.devbox/virtenv/{platform}/scripts)
{PLATFORM}_DEFAULT_DEVICE- Default device name when none specifiedANDROID_DEVICES- Array of device names to evaluate (empty = all)
{PLATFORM}_APP_*- Application-specific paths (APK, bundle, derived data, etc.)
plugin.json- Plugin manifestREADME.md- User-facing plugin overviewREFERENCE.md- Complete API reference{platform}.json- Platform configuration defaultsdevices.lock- Generated lock file (optimization for CI)process-compose.yaml- Service definitions
- All scripts use
set -euo pipefailfor safety (orset -eufor POSIX compatibility) - Functions prefixed with platform namespace (
android_,ios_) - Debug logging controlled by
{PLATFORM}_DEBUG=1orDEBUG=1 - Validation functions warn but never block (non-zero exit with
|| true)
Device definitions are stored as JSON files in devbox.d/{platform}/devices/:
Android: device_name.json
{
"name": "pixel",
"api": 28,
"device": "pixel",
"tag": "google_apis",
"preferred_abi": "x86_64"
}iOS: device_name.json
{
"name": "iphone15",
"runtime": "17.5"
}Lock files are generated from device definitions to optimize CI builds:
Purpose: CI optimization - only evaluate/download SDK for selected device APIs instead of all devices
Android: devices.lock
{
"api_versions": [28, 35, 36],
"checksum": "abc123..."
}iOS: devices.lock
{
"devices": ["min", "max"],
"checksum": "def456..."
}Generation: Run devbox run {platform}.sh devices eval to regenerate lock file
All plugins implement TTL-based caching (1 hour) for expensive operations:
- Xcode Discovery (iOS): Cache location of Xcode developer directory
- Nix SDK Evaluation (Android): Cache Nix flake evaluation results
- DevBox ShellEnv (iOS): Cache devbox shellenv output
- Time-based: 1 hour TTL for all caches
- Event-based: Android Nix cache invalidates when
devices.lockchanges - Location: Cache files stored in
.devbox/virtenv/{platform}/(git-ignored)
.xcode_dev_dir.cache- Xcode developer directory path- Note: Nix handles flake evaluation caching internally - no custom cache files needed
.shellenv.cache- DevBox shellenv export commands
All validation functions follow these principles:
- Warn, don't block: Validation warnings never prevent execution
- Actionable messages: Include command to fix the issue
- Environment-aware: Skip validation when appropriate (e.g., CI, missing tools)
{platform}_validate_lock_file()- Check if lock file matches device definitions{platform}_validate_sdk()/{platform}_validate_xcode()- Verify toolchain availability{platform}_validate_*()- Additional platform-specific checks
Lock files include SHA-256 checksums of device definition files:
# Compute checksum (cross-platform)
if command -v sha256sum >/dev/null 2>&1; then
checksum=$(find "$devices_dir" -name "*.json" -type f -exec cat {} \; | sha256sum | cut -d' ' -f1)
elif command -v shasum >/dev/null 2>&1; then
checksum=$(find "$devices_dir" -name "*.json" -type f -exec cat {} \; | shasum -a 256 | cut -d' ' -f1)
fiEach platform provides a unified CLI: {platform}.sh <command> [args]
Common Commands:
devices list- List available device definitionsdevices create <name> [options]- Create new device definitiondevices select <name>- Select device(s) for evaluationdevices eval- Regenerate lock file from selected devicesconfig show- Display platform configurationconfig set KEY=VALUE- Update configuration valuesconfig reset- Reset to default configurationinfo- Display resolved SDK/toolchain information
Android-specific:
android.sh devices create pixel_api28 --api 28 --device pixel --tag google_apis
android.sh devices update pixel_api28 --api 29
android.sh devices delete pixel_api28iOS-specific:
ios.sh devices create iphone15 --runtime 17.5
ios.sh devices update iphone15 --runtime 18.0
ios.sh devices delete iphone15SDK Management:
- Uses Nix flake for reproducible SDK composition
- Flake located at
devbox.d/android/flake.nix - SDK configured based on API versions in lock file
- Supports local SDK override with
ANDROID_LOCAL_SDK=1
Key Variables:
ANDROID_SDK_ROOT- SDK installation pathANDROID_HOME- Alias for SDK root (compatibility)ANDROID_SDK_FLAKE_OUTPUT- Nix flake output nameANDROID_BUILD_TOOLS_VERSION- Build tools versionANDROID_SYSTEM_IMAGE_TAG- System image type (google_apis, etc.)
Toolchain Management:
- Relies on system Xcode installation
- Discovers Xcode via multiple strategies (IOS_DEVELOPER_DIR, xcode-select, /Applications)
- Version selection: Latest Xcode by version number
Key Variables:
DEVELOPER_DIR- Xcode developer directory pathIOS_DEFAULT_RUNTIME- iOS simulator runtime versionIOS_APP_PROJECT- Xcode project pathIOS_APP_SCHEME- Xcode build schemeIOS_DOWNLOAD_RUNTIME- Auto-download missing runtimes (0/1)
- Test framework: Shell scripts with assertion helpers
- Location:
devbox/plugins/tests/{platform}/ - Test files:
test-*.sh - CI workflows:
.github/workflows/{platform}-plugin-tests.yml
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/../../examples/{platform}"
. "../tests/test-framework.sh"
# Assertions
assert_equal "expected" "actual" "Test description"
assert_file_exists "path/to/file" "File exists"
# Summary
test_summary # Prints pass/fail counts, exits 1 if failuresAvoid breaking changes. When necessary:
- Provide migration path in documentation
- Support old and new patterns during transition
- Use deprecation warnings before removal
- Never remove environment variables
- Mark deprecated variables with warning messages
- Maintain aliases for renamed variables
- Support both old and new config formats
- Auto-migrate when possible
- Provide manual migration commands
# Platform-specific
ANDROID_DEBUG=1 devbox shell
IOS_DEBUG=1 devbox shell
# Global
DEBUG=1 devbox shell{platform}_debug_enabled() # Check if debug mode is on
{platform}_debug_log "message" # Log debug message
{platform}_debug_dump_vars VAR1 VAR2 # Dump variable values- Prefix: Platform name in brackets
[android]or[ios] - Stderr: All debug output goes to stderr
- Conditional: Only output when debug mode enabled
- Use lock files to minimize SDK downloads
- Skip interactive prompts with environment detection
- Disable summary printing in CI (
CI=1orGITHUB_ACTIONS=1) - Run with
--pureflag for reproducible environments
- name: Setup Devbox
uses: jetify-com/devbox-install-action@v0.11.0
- name: Test Android Plugin
run: |
EMU_HEADLESS=1 devbox run --pure start:emu max
devbox run --pure stop:emu- Select specific devices:
devices select minreduces evaluation time - Use lock files: Commit
devices.lockto avoid regeneration - Cache Devbox: Cache
.devboxdirectory in CI for faster startups - Parallel jobs: Test different platforms in parallel