- Checkboxes for additional packages (themes, ports, games)
- Backup and restore functionality
- Scrape boxart for ROMs
SpruceOS Installer is an all-in-one downloader, extractor, formatter, and installer for SpruceOS and other custom firmware projects.
- ✓ Download releases directly from GitHub
- ✓ Format SD cards (FAT32, supports >32GB on Windows)
- ✓ Extract archives (.7z, .zip) or burn raw images (.img, .img.gz, .img.xz)
- ✓ Cross-platform: Windows, Linux, macOS
- ✓ Update mode: preserve saves/ROMs while updating system files
- ✓ Multi-repository support with asset filtering
GitHub Actions automatically build releases per branch. If you'd like to use this installer for your own CFW project, let us know—we can create a branch for you or add you directly to the repository.
Please do not remove the Spruce or NextUI teams from the authors section. Instead, add your name alongside the existing credits.
-
Download the installer for your platform
-
On Linux you will need to mark the app as executable. When launched the app will automatically request privileges via
pkexecif needed
The installer is distributed as a .zip containing a self-contained .app bundle.
IMPORTANT: macOS requires Terminal to have "Full Disk Access" to write to SD cards. Follow these steps:
NOT ALL MAC ARE THE SAME, VARIOUS VERSIONS ETC MAY MAKE THE BELOW INSTRUCTIONS DIFFERENT FOR YOU!
https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
https://ordonez.tv/2024/11/04/how-to-run-unsigned-apps-in-macos-15-1/
-
Grant Terminal Full Disk Access:
- Open System Settings (or System Preferences on older macOS)
- Go to Privacy & Security → Full Disk Access
- Click the lock icon (bottom left) and enter your password
- Click the + button to add an application
- Navigate to Applications → Utilities → select Terminal.app
- Check the box next to Terminal in the list
- Quit and reopen Terminal (important!)
Why? macOS security prevents apps from accessing removable drives without this permission. Terminal needs access because it spawns the installer process.
-
Download and Run the Installer:
- Download and extract the ZIP file
- Easy method: Double-click
launch-installer.commandto automatically remove quarantine and launch - Alternative: Right-click "SpruceOSInstaller.app" and select "Open", then click "Open" in the dialog
-
Authorization During Install:
- When writing to SD cards, you'll see a native macOS authorization dialog requesting your admin password (via
authopen) - This is normal and required for disk operations
- When writing to SD cards, you'll see a native macOS authorization dialog requesting your admin password (via
If the installer can't access your SD card:
- Verify Terminal has Full Disk Access (see step 1 above)
- Quit Terminal completely and reopen it (changes don't apply to running Terminal sessions)
- Try running from Terminal manually:
cd ~/Downloads/SpruceOSInstaller.app/Contents/MacOS ./spruceos-installer
Note: This app is not code-signed.
This guide walks you through every single file that needs changing to rebrand this installer for your own CFW project.
Minimum viable rebrand (~15 minutes):
- 1. Edit
src/config.rs- ChangeAPP_NAME,VOLUME_LABEL,WINDOW_TITLE, andREPO_OPTIONS - 2. Edit
Cargo.toml- Updatename,description,authors - 3. Replace
assets/Icons/icon.pngandicon.ico- Your branding - 4. Edit
assets/Mac/Info.plist- macOS bundle identifiers - 5. Edit
app.manifest- Windows application name
Full rebrand with custom theme (~45 minutes):
- Complete the 5 steps above
- 6. Edit
src/app/theme.rs- Customize all colors - 7. Update
src/app/ui.rs- Search forColor32::from_rgband update button colors - 8. Test locally -
cargo build --release --features icon - 9. Push to GitHub - Automated builds create releases
This is the most important file - it controls all branding and functionality.
Click to expand detailed instructions
Location: src/config.rs
Search for these constants in the "BRANDING" section:
// Your OS name (shown throughout the UI)
pub const APP_NAME: &str = "SpruceOS"; // ← Change to "YourOS"
// SD card volume label (MAX 11 CHARS, UPPERCASE)
pub const VOLUME_LABEL: &str = "SPRUCEOS"; // ← Change to "YOUROS" (11 char max!)
// Window title bar text
pub const WINDOW_TITLE: &str = "SpruceOS Installer"; // ← Change to "YourOS Installer"VOLUME_LABEL has a hard 11-character limit (FAT32 limitation). Use uppercase only.
Search for pub const REPO_OPTIONS - this is where you define which GitHub repos to download from:
pub const REPO_OPTIONS: &[RepoOption] = &[
RepoOption {
name: "Stable", // ← Button label in UI
url: "spruceUI/spruceOS", // ← YOUR GitHub repo (owner/repo format)
info: "Stable releases of SpruceOS.\nSupported devices: Miyoo A30", // ← Info text (use \n for line breaks)
supports_update_mode: true, // ← Show update mode checkbox (true for archives, false for raw images)
update_directories: &["Retroarch", "spruce"], // ← Folders deleted during updates
allowed_extensions: Some(&[".7z"]), // ← File types to show (None = all)
asset_display_mappings: None, // ← User-friendly names (see advanced below)
},
// Add more repos as needed...
];Example for your project:
pub const REPO_OPTIONS: &[RepoOption] = &[
RepoOption {
name: "Stable",
url: "yourorg/yourrepo", // ← Your GitHub username/repo
info: "Official stable builds.\nSupported: Device X, Y, Z",
supports_update_mode: true, // Archives support updates
update_directories: &["System", "Apps"], // What gets replaced during updates
allowed_extensions: None, // Show all file types
asset_display_mappings: None,
},
RepoOption {
name: "Beta",
url: "yourorg/yourrepo-beta",
info: "Beta builds - may be unstable!\nTesting new features.",
supports_update_mode: true, // Archives support updates
update_directories: &["System"],
allowed_extensions: Some(&[".7z", ".zip"]), // Only show archives
asset_display_mappings: None,
},
RepoOption {
name: "Raw Images",
url: "yourorg/yourrepo-images",
info: "Full disk images for fresh installs only.",
supports_update_mode: false, // Raw images (.img.gz) don't support updates
update_directories: &[], // Not used for raw images
allowed_extensions: Some(&[".img.gz", ".img"]), // Only raw images
asset_display_mappings: None,
},
];Search for DEFAULT_REPO_INDEX - which repo button is selected by default:
// Which repo button is selected by default (0 = first, 1 = second, etc.)
pub const DEFAULT_REPO_INDEX: usize = 0; // ← Change if neededIf your releases have technical filenames like MyOS-RK3326.img.gz, use display mappings to show user-friendly names:
asset_display_mappings: Some(&[
AssetDisplayMapping {
pattern: "RK3326", // Matches filenames containing this string
display_name: "RK3326 Chipset", // Friendly name shown to users
devices: "Anbernic RG351P/V/M, Odroid Go Advance", // Compatible devices
},
AssetDisplayMapping {
pattern: "RK3588",
display_name: "RK3588 Chipset",
devices: "Gameforce Ace, Orange Pi 5",
},
]),Result: Users see "RK3326 Chipset - Compatible: Anbernic RG351P/V/M" instead of "MyOS-RK3326.img.gz"
Control which file types users see per repository:
allowed_extensions: Some(&[".7z", ".zip"]), // Only archives
allowed_extensions: Some(&[".img.gz"]), // Only compressed images
allowed_extensions: None, // Show everythingCommon use cases:
- Separate "full installer" repos (show only
.7z) from "update package" repos (show only.zip) - Hide experimental formats from stable releases
- Simplify UI when releases have many file types
The supports_update_mode field controls whether the "Update Mode" checkbox appears for a repository:
supports_update_mode: true, // Show checkbox - for archive-based installs (.7z, .zip)
supports_update_mode: false, // Hide checkbox - for raw disk images (.img.gz, .img)When to use each:
true: Archive files (.7z, .zip) that can be extracted over existing filesfalse: Raw disk images (.img.gz, .img) that always do full disk burns
When update mode is enabled (archives only), these directories get deleted before extraction:
update_directories: &["Retroarch", "spruce", "System"], // These get deleted
// Everything else (Roms/, Saves/, etc.) is preserved!How it works:
- User checks "Update Mode" checkbox (only visible when
supports_update_mode: true) - Installer mounts existing SD card (no format!)
- Only deletes the specified directories
- Extracts new files
- User's saves/ROMs stay intact
Location: Cargo.toml
Find the [package] section and update these fields:
[package]
name = "spruceos-installer" # ← Change to "yourname-installer" (lowercase, hyphens only)
version = "1.0.0"
edition = "2021"
description = "SpruceOS SD Card Installer" # ← Change description
authors = ["SpruceOS Team", "NextUI Team"] # ← ADD your name (keep credits!)Example:
name = "retrobox-installer"
description = "RetroBox CFW Installer"
authors = ["SpruceOS Team", "NextUI Team", "Your Name <you@example.com>"]Replace these files with your own:
| File | Format | Recommended Size | Usage |
|---|---|---|---|
assets/Icons/icon.png |
PNG with transparency | 128x128 or 256x256 | Window icon (all platforms), macOS icon source |
assets/Icons/icon.ico |
Multi-resolution ICO | 16x16, 32x32, 48x48, 256x256 | Windows taskbar, file explorer |
How to create a multi-resolution ICO:
- Create PNGs at multiple sizes (16x16, 32x32, 48x48, 256x256)
- Use online converter (e.g., https://convertio.co/png-ico/) or ImageMagick:
convert icon-16.png icon-32.png icon-48.png icon-256.png icon.ico
- PNG without transparency (use RGBA, not RGB)
- Wrong ICO format (must be valid multi-res .ico, not renamed .png)
- Too small (minimum 64x64, recommended 128x128+)
Location: assets/Mac/Info.plist
Search for each key and update its corresponding string value:
<!-- Bundle name (no spaces) -->
<key>CFBundleName</key>
<string>SpruceOSInstaller</string> ← Change to YourOSInstaller
<!-- Display name (shown in Finder) -->
<key>CFBundleDisplayName</key>
<string>SpruceOS Installer</string> ← Change to "YourOS Installer"
<!-- Bundle identifier (reverse DNS, must be unique) -->
<key>CFBundleIdentifier</key>
<string>com.spruceos.installer</string> ← Change to com.yourcompany.installer
<!-- Executable name (MUST match binary from Cargo.toml!) -->
<key>CFBundleExecutable</key>
<string>spruceos-installer</string> ← Change to match Cargo.toml name
<!-- Permission description shown to users -->
<key>NSSystemAdministrationUsageDescription</key>
<string>This app needs administrator privileges to write firmware images to SD cards.</string> ← Update to reference your firmware
<!-- Removable volumes permission description -->
<key>NSRemovableVolumesUsageDescription</key>
<string>This app needs access to removable drives to install firmware.</string> ← Update as neededCFBundleExecutable MUST exactly match the name field in Cargo.toml or macOS won't launch the app!
Location: app.manifest (root directory)
Update these fields:
<!-- Application identifier -->
<assemblyIdentity name="SpruceOS.Installer" ... />
↑ Change to "YourOS.Installer"
<!-- Description (shown in UAC prompt) -->
<description>SpruceOS SD Card Installer</description>
↑ Change to your descriptionThis controls how Windows displays your app in:
- UAC (User Account Control) elevation prompts
- Task Manager
- Windows Registry entries
Location: src/app/theme.rs
All color values are in RGBA format: [Red, Green, Blue, Alpha] (0-255)
Click to expand theme customization guide
- Build and run locally:
cargo run - Press Ctrl+T to open the live theme editor
- Adjust colors visually with color pickers
- Copy the generated
ThemeConfigcode - Paste into
src/app/theme.rs(replace entireget_theme_config()method)
Find the get_theme_config() method and update the ThemeConfig fields:
Most important colors to change:
// Theme name (cosmetic)
name: "SpruceOS".to_string(), // ← Change to your project name
// Primary text color
override_text_color: Some([251, 241, 199, 255]), // Cream - change to your brand
// Window background
override_extreme_bg_color: Some([29, 32, 33, 255]), // Dark gray
// Accent/highlight color (selections, checkboxes)
override_selection_bg: Some([215, 180, 95, 255]), // Gold - your brand color!
// Warning messages
override_warn_fg_color: Some([214, 93, 14, 255]), // Orange
// Error messages
override_error_fg_color: Some([204, 36, 29, 255]), // RedFull color reference:
| Field | Current Color | Purpose |
|---|---|---|
override_text_color |
[251, 241, 199, 255] | Main UI text |
override_weak_text_color |
[124, 111, 100, 255] | Secondary/dimmed text |
override_hyperlink_color |
[131, 165, 152, 255] | Clickable links |
override_faint_bg_color |
[48, 48, 48, 255] | Input fields, panels |
override_extreme_bg_color |
[29, 32, 33, 255] | Window background |
override_warn_fg_color |
[214, 93, 14, 255] | Warning text |
override_error_fg_color |
[204, 36, 29, 255] | Error text |
override_selection_bg |
[215, 180, 95, 255] | Highlight/accent |
Button/widget colors:
override_widget_inactive_fg_stroke_color- Checkbox/button bordersoverride_widget_active_bg_fill- Checked checkbox backgroundoverride_widget_active_fg_stroke_color- Checkmark coloroverride_widget_hovered_bg_stroke_color- Hover border
Location: src/app/ui.rs
Some UI elements use hardcoded colors outside the theme system. Search for Color32::from_rgb and update:
// Success messages (search for "Color32::from_rgb(104, 157, 106)")
Color32::from_rgb(104, 157, 106) // Green
// Install button (search for install button color)
.fill(egui::Color32::from_rgb(104, 157, 106)) // Green
// Cancel button (search for cancel button color)
.fill(egui::Color32::from_rgb(251, 73, 52)) // RedHow to find them:
- Open
src/app/ui.rs - Search for
Color32::from_rgb - Update RGB values to match your brand
Location: assets/Fonts/nunwen.ttf
To use a custom font:
- Replace
assets/Fonts/nunwen.ttfwith your TTF/OTF file - If renaming the file, search for
CUSTOM_FONT_NAMEinsrc/config.rsand update it:pub const CUSTOM_FONT_NAME: &str = "YourFont"; // ← Change to match your font file
Update artifact names for consistency (search for the old names and replace):
.github/workflows/build-windows.yml:
- Search for
spruceos-installer-windows.exe→ Change toyourname-installer-windows.exe - Update the corresponding artifact name
.github/workflows/build-macos.yml:
- Search for
SpruceOSInstaller.app→ Change toYourOSInstaller.app - Update the corresponding artifact name
.github/workflows/build-linux.yml:
- Search for
spruceos-installer→ Update artifact names for all 4 architectures
Update mode allows users to preserve ROMs/saves while updating system files. You have several options for controlling this feature:
The supports_update_mode field in each RepoOption controls whether the update mode checkbox appears:
RepoOption {
name: "Stable",
supports_update_mode: true, // Show checkbox for archives
// ...
},
RepoOption {
name: "Raw Images",
supports_update_mode: false, // Hide checkbox for disk images
// ...
},When to use:
- Set
truefor archive-based repositories (.7z, .zip) that support updates - Set
falsefor raw disk images (.img.gz) that always do full burns - This is automatically configured correctly in the default SpruceOS repos
To disable update mode for ALL repositories, hide the checkbox from users:
- Open
src/app/ui.rs - Search for
"Update existing installation (skip format)" - Comment out the entire block containing the checkbox
- Look for the comment
// Update mode checkbox (only show when not in progress AND repo supports it) - Comment from that line through the matching
// END HIDE UPDATE MODEcomment
- Look for the comment
Result: Users won't see the update mode option on any repository.
For a thorough removal, delete update mode code from these files (search for update_mode in each):
Files to modify:
src/app/state.rs- Remove theupdate_mode: boolfieldsrc/app/ui.rs- Remove checkbox UI and conditional display logicsrc/app/logic.rs- Remove update mode conditional checkssrc/config.rs- Optionally removeupdate_directoriesfield fromRepoOption
All update mode code can be found by searching for:
update_mode(the boolean flag)update_directories(in config.rs)"Update existing installation"(the UI text)PreviewingUpdate(the preview modal state)
Files are marked with // HIDE UPDATE MODE comments for easy identification.
# Clone your fork/branch
git clone https://github.com/yourorg/yourrepo-installer.git
cd yourrepo-installer
# Build with icon support
cargo build --release --features icon
# Binary location:
# Windows: target/release/yourname-installer.exe
# Linux: target/release/yourname-installer
# macOS: target/release/yourname-installer- Window title shows your custom name
- Icons display correctly (taskbar, window)
- Repository dropdown shows your repos
- Colors match your brand
- Update Mode: If enabled, checkbox lists correct directories; if disabled, checkbox is hidden
- Download works from your GitHub repo
- SD card gets labeled with your
VOLUME_LABEL - macOS: Terminal has Full Disk Access granted (if testing on macOS)
- macOS: App bundle opens and can access SD card (if testing on macOS)
- Push changes to GitHub
- Go to Actions tab
- Manually trigger "Build All Platforms" workflow
- Check artifacts:
- Windows:
yourname-installer-windows.exe - macOS:
YourOS-Installer-macOS-Universal.zip - Linux: 4 binaries for different architectures
- Windows:
| Problem | Cause | Solution |
|---|---|---|
| macOS can't access SD card | Terminal doesn't have Full Disk Access permission | Grant Terminal Full Disk Access in System Settings → Privacy & Security, then quit/reopen Terminal |
| macOS app won't launch | CFBundleExecutable doesn't match Cargo.toml name |
Make them identical |
| Volume label too long | VOLUME_LABEL > 11 characters |
Shorten to 11 chars max |
| Wrong files in dropdown | GitHub repo URL format wrong | Use "owner/repo" format (no https://) |
| Colors don't apply | Updated theme.rs but not ui.rs hardcoded colors |
Search Color32::from_rgb in ui.rs |
| Build fails on GitHub | Binary name changed but workflows not updated | Update .github/workflows/*.yml artifact names |
| Icon not showing | PNG doesn't have transparency or wrong format | Use RGBA PNG, valid multi-res ICO |
Critical (must change):
- ✅
src/config.rs- App name, repos, volume label - ✅
Cargo.toml- Package metadata - ✅
assets/Icons/- Both PNG and ICO files - ✅
assets/Mac/Info.plist- macOS bundle config - ✅
app.manifest- Windows app identifier
Recommended (for full rebrand):
6. ✅ src/app/theme.rs - All UI colors
7. ✅ src/app/ui.rs - Hardcoded button colors
Optional (cosmetic/advanced):
8. ⬜ assets/Fonts/nunwen.ttf - Custom font
9. ⬜ .github/workflows/*.yml - Artifact names
10. ⬜ .vscode/launch.json - Debug config (if using VS Code)
GitHub Actions automatically builds for:
- Windows: x64
- Linux: x64, ARM64, i686 (32-bit), ARMv7
- macOS: Universal binary (Apple Silicon + Intel)
No local build environment needed - just push to GitHub!
- Rust (via rustup.rs)
- Platform-specific dependencies:
- Windows: MSVC build tools
- Linux: Standard build tools
- macOS: Xcode Command Line Tools
# Debug build (fast compilation)
cargo build
# Release build (optimized)
cargo build --release --features icon
# Run directly (debug mode)
cargo runTips:
- Press Ctrl+T while running to open the theme editor
- Debug builds are in
target/debug/ - Release builds are in
target/release/
The installer uses a modular architecture (refactored from a single ~2300 line file):
src/
├── main.rs - Entry point, privilege escalation
├── config.rs - ⚠️ BRANDING: App name, repos, constants
├── app/ - Main application (modular)
│ ├── mod.rs - Module coordinator
│ ├── state.rs - AppState enum, InstallerApp struct
│ ├── theme.rs - ⚠️ COLORS: Theme configuration
│ ├── logic.rs - Installation orchestration
│ └── ui.rs - ⚠️ COLORS: UI rendering
├── drives.rs - Cross-platform drive detection
├── format.rs - FAT32 formatting (>32GB support on Windows)
├── extract.rs - 7z extraction with embedded binaries
├── burn.rs - Raw image burning (.img/.gz) with sector alignment
├── copy.rs - File copying with progress tracking
├── delete.rs - Selective directory deletion (update mode)
├── eject.rs - Safe drive ejection
├── github.rs - GitHub API integration
├── fat32.rs - Custom FAT32 formatter (Windows >32GB)
├── debug.rs - Debug logging to file
└── mac/
└── authopen.rs - macOS privileged disk access
Cross-platform drive detection:
- Windows:
GetLogicalDrives+IOCTL_STORAGE_GET_DEVICE_NUMBER - Linux:
/sys/block+/proc/mounts+ label detection - macOS:
diskutil list -plistwith multi-heuristic filtering
FAT32 formatting:
- Windows: Custom formatter bypasses 32GB OS limit, diskpart partitioning
- Linux:
parted+mkfs.vfat - macOS:
diskutil eraseDiskwith automatic retry logic
Raw image burning:
- On-the-fly
.gzdecompression - Pre-scans to determine decompressed size
- SHA256 verification (Linux only; disabled on Windows/macOS for reliability)
- Sector-aligned writes (Windows: 512-byte, macOS: 512-byte with F_NOCACHE)
- Direct hardware I/O on macOS (F_NOCACHE + O_SYNC flags prevent buffer cache stalls)
GitHub integration:
- Fetches latest releases via GitHub API
- Chunked streaming for large downloads
- Rate limit detection and timeout handling
- Automatic filtering of source code archives
macOS privileged access:
- Uses native
authopenutility (no code signing required!) - Unix domain socketpair for file descriptor passing (based on Raspberry Pi Imager)
- F_NOCACHE flag bypasses kernel buffer cache for direct hardware writes (prevents 99% freeze)
- O_SYNC flag ensures synchronous writes (data written before returning)
- 512-byte sector-aligned buffering for .gz decompression compatibility
- Proper error differentiation (cancelled, denied, system error)
- SpruceOS Team - Core development
- NextUI Team - Design and GUI enhancements
- Tag - Mac app bundles and so much more!
- Helaas - macOS testing, debugging, and research
- 7-Zip - We bundle the 7z binary (LGPL) for seamless archive extraction
- Raspberry Pi Imager - macOS authopen implementation patterns
- balenaEtcher - Inspiration and methodology
