System disk protection, macOS flash, CI/CD, app icon

- disk.rs: block system disks (/proc/mounts -> strip partition suffix)
- flash.rs: macOS implementation (diskutil unmount + osascript admin
  privileges + rdisk raw write + auto-mount for panel injection)
- flash.rs: decompress_xz shared between linux and macos (cfg any)
- tauri.conf.json: full icon set (32, 128, 128@2x, ico, icns, png)
- .github/workflows/build.yml: CI/CD for Linux, macOS, Windows
  (Tauri CLI build + artifact upload + draft release on tag)
- .gitignore: track icons/ (custom branding, not auto-generated)
- icons/: generated from Quantico font (AR FLASHER branding)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Douglas Teles
2026-03-02 00:10:55 -03:00
parent eb715baa6b
commit 8aa385f04d
11 changed files with 269 additions and 9 deletions
+98
View File
@@ -0,0 +1,98 @@
name: Build & Release
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: write
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- platform: macos-latest
target: aarch64-apple-darwin
- platform: macos-latest
target: x86_64-apple-darwin
- platform: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
libxdo-dev \
libssl-dev \
libglib2.0-dev \
libgtk-3-dev
- name: Install Tauri CLI
run: cargo install tauri-cli --version "^2"
- name: Build
run: cargo tauri build --target ${{ matrix.target }}
- name: Upload artifacts (Linux)
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: archr-flasher-linux-${{ matrix.target }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage
- name: Upload artifacts (macOS)
if: runner.os == 'macOS'
uses: actions/upload-artifact@v4
with:
name: archr-flasher-macos-${{ matrix.target }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
- name: Upload artifacts (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: archr-flasher-windows-${{ matrix.target }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/msi/*.msi
src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
release:
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create Release
uses: softprops/action-gh-release@v2
with:
draft: true
generate_release_notes: true
files: artifacts/**/*
-1
View File
@@ -7,7 +7,6 @@ node_modules/
# Tauri
/src-tauri/WixTools/
/src-tauri/icons/
# OS
.DS_Store
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+55
View File
@@ -23,6 +23,7 @@ fn format_size(bytes: u64) -> String {
#[cfg(target_os = "linux")]
pub fn list_removable_disks() -> Vec<DiskInfo> {
let mut disks = Vec::new();
let system_disks = get_system_disk_names();
let block_dir = Path::new("/sys/block");
let entries = match fs::read_dir(block_dir) {
@@ -38,6 +39,11 @@ pub fn list_removable_disks() -> Vec<DiskInfo> {
continue;
}
// Never list system disk (where / is mounted)
if system_disks.iter().any(|sd| name == *sd) {
continue;
}
let sys_path = entry.path();
// Must be removable
@@ -83,6 +89,55 @@ pub fn list_removable_disks() -> Vec<DiskInfo> {
disks
}
/// Read /proc/mounts to find which block devices back /, /home, /boot.
/// Returns the parent disk names (e.g. "sda" from "/dev/sda1", "nvme0n1" from "/dev/nvme0n1p2").
#[cfg(target_os = "linux")]
fn get_system_disk_names() -> Vec<String> {
let mut names = Vec::new();
let mounts = fs::read_to_string("/proc/mounts").unwrap_or_default();
for line in mounts.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 { continue; }
let mount_point = parts[1];
// Only protect critical mount points
if mount_point != "/" && mount_point != "/home" && mount_point != "/boot" {
continue;
}
let dev = parts[0];
if !dev.starts_with("/dev/") { continue; }
let dev_name = &dev[5..]; // strip "/dev/"
// Strip partition number to get parent disk name
// "sda1" → "sda", "nvme0n1p2" → "nvme0n1", "mmcblk0p1" → "mmcblk0"
let parent = strip_partition_suffix(dev_name);
if !parent.is_empty() && !names.contains(&parent) {
names.push(parent);
}
}
names
}
/// Strip partition suffix: "sda1" → "sda", "nvme0n1p2" → "nvme0n1", "mmcblk0p1" → "mmcblk0"
#[cfg(target_os = "linux")]
fn strip_partition_suffix(dev: &str) -> String {
if dev.contains("nvme") || dev.contains("mmcblk") {
// These use "p" + number suffix: nvme0n1p2 → nvme0n1, mmcblk0p1 → mmcblk0
if let Some(idx) = dev.rfind('p') {
if dev[idx + 1..].chars().all(|c| c.is_ascii_digit()) && !dev[idx + 1..].is_empty() {
return dev[..idx].to_string();
}
}
dev.to_string()
} else {
// sd* devices: sda1 → sda (strip trailing digits)
dev.trim_end_matches(|c: char| c.is_ascii_digit()).to_string()
}
}
#[cfg(target_os = "macos")]
pub fn list_removable_disks() -> Vec<DiskInfo> {
use std::process::Command;
+111 -8
View File
@@ -86,7 +86,7 @@ pub fn flash_image_privileged(
Ok(())
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn decompress_xz(app: &AppHandle, src: &str, dst: &Path) -> Result<(), String> {
let src_file = File::open(src)
.map_err(|e| format!("Cannot open image: {}", e))?;
@@ -168,16 +168,119 @@ rmdir "$MOUNT_DIR"
// ---------------------------------------------------------------------------
#[cfg(target_os = "macos")]
pub fn flash_image_privileged(
_app: &AppHandle,
_image_path: &str,
_device: &str,
_panel_dtb: &str,
_panel_id: &str,
_variant: &str,
app: &AppHandle,
image_path: &str,
device: &str,
panel_dtb: &str,
panel_id: &str,
variant: &str,
) -> Result<(), String> {
Err("macOS flash not yet implemented".into())
use std::process::Command;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
let is_xz = image_path.ends_with(".xz");
// Step 1: Decompress .xz in user space (with progress)
let img_to_flash = if is_xz {
emit_progress(app, 0.0, "decompressing");
let temp_img = std::env::temp_dir().join("archr-flash-temp.img");
decompress_xz(app, image_path, &temp_img)?;
temp_img
} else {
PathBuf::from(image_path)
};
// Step 2: Unmount disk (macOS auto-mounts SD cards)
let _ = Command::new("diskutil")
.args(["unmountDisk", device])
.status();
// Step 3: Write helper script
let script_path = std::env::temp_dir().join("archr-flash.sh");
fs::write(&script_path, FLASH_SCRIPT_MACOS)
.map_err(|e| format!("Cannot write helper script: {}", e))?;
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))
.map_err(|e| format!("Cannot set script permissions: {}", e))?;
// Step 4: Run via osascript with administrator privileges
emit_progress(app, 60.0, "writing");
let cmd = format!(
"do shell script \"bash '{}' '{}' '{}' '{}' '{}' '{}'\" with administrator privileges",
script_path.display(),
img_to_flash.display(), device, panel_dtb, panel_id, variant
);
let output = Command::new("osascript")
.args(["-e", &cmd])
.output()
.map_err(|e| format!("osascript error: {}", e))?;
// Cleanup
let _ = fs::remove_file(&script_path);
if is_xz {
let _ = fs::remove_file(&img_to_flash);
}
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("User canceled") || stderr.contains("-128") {
return Err("cancelled".into());
}
return Err(format!("Flash failed: {}", stderr));
}
emit_progress(app, 95.0, "configuring");
emit_progress(app, 100.0, "done");
Ok(())
}
#[cfg(target_os = "macos")]
const FLASH_SCRIPT_MACOS: &str = r#"#!/bin/bash
set -e
IMAGE="$1"
DEVICE="$2"
PANEL_DTB="$3"
PANEL_ID="$4"
VARIANT="$5"
# Unmount all partitions (in case auto-mounted again)
diskutil unmountDisk "$DEVICE" 2>/dev/null || true
# Write raw image to device (macOS uses rdisk for raw access = faster)
RDISK=$(echo "$DEVICE" | sed 's|/dev/disk|/dev/rdisk|')
dd if="$IMAGE" of="$RDISK" bs=4m
sync
# Re-mount disk so we can access boot partition
sleep 2
diskutil mountDisk "$DEVICE" 2>/dev/null || true
sleep 1
# Find the mounted boot partition (FAT32, typically partition 1)
BOOT_VOL=$(diskutil info "${DEVICE}s1" 2>/dev/null | grep "Mount Point:" | awk -F: '{print $2}' | xargs)
if [ -n "$BOOT_VOL" ] && [ -d "$BOOT_VOL" ]; then
# Copy selected panel DTB as kernel.dtb
if [ -f "$BOOT_VOL/$PANEL_DTB" ]; then
cp "$BOOT_VOL/$PANEL_DTB" "$BOOT_VOL/kernel.dtb"
fi
# Write panel configuration
printf 'PanelNum=%s\nPanelDTB=%s\n' "$PANEL_ID" "$PANEL_DTB" > "$BOOT_VOL/panel.txt"
echo "confirmed" > "$BOOT_VOL/panel-confirmed"
echo "$VARIANT" > "$BOOT_VOL/variant"
sync
fi
# Eject disk safely
diskutil eject "$DEVICE" 2>/dev/null || true
"#;
// ---------------------------------------------------------------------------
// Windows: privilege escalation via UAC
// ---------------------------------------------------------------------------
+5
View File
@@ -28,6 +28,11 @@
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico",
"icons/icon.png"
]
}