You've already forked archr-flasher
mirror of
https://github.com/archr-linux/archr-flasher.git
synced 2026-07-12 18:19:47 -07:00
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:
@@ -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/**/*
|
||||
@@ -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 |
@@ -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
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user