diff --git a/Cargo.toml b/Cargo.toml
index dfa0619..6178c1f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,55 +1,56 @@
-[package]
-name = "spruceos-installer"
-version = "1.5.0"
-edition = "2021"
-description = "SpruceOS SD Card Installer"
-authors = ["SpruceOS Team (https://github.com/spruceUI)", "CMTag (https://github.com/CMTag)", "NextUI Team (https://github.com/LoveRetro)", "Helaas (https://github.com/Helaas)"]
-license = "CC-BY-NC-4.0"
-
-[features]
-default = []
-icon = []
-
-[dependencies]
-eframe = "0.33"
-egui = "0.33"
-egui_extras = { version = "0.33", features = ["all_loaders"] }
-egui-thematic = "0.1"
-tokio = { version = "1", features = ["rt-multi-thread", "fs", "process", "sync", "time", "io-util", "macros"] }
-reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
-sevenz-rust = "0.6"
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-futures-util = "0.3"
-tokio-util = "0.7"
-dirs = "5"
-tempfile = "3"
-image = { version = "0.25", default-features = false, features = ["png", "qoi"] }
-lazy_static = "1.4"
-libc = "0.2"
-sha2 = "0.10"
-flate2 = "1.0"
-arboard = "3.4"
-regex = "1"
-which = "6.0"
-
-[target.'cfg(windows)'.dependencies]
-windows = { version = "0.58", features = [
- "Win32_Foundation",
- "Win32_Storage_FileSystem",
- "Win32_System_IO",
- "Win32_System_Ioctl",
- "Win32_System_Registry",
- "Win32_Security",
-] }
-
-[target.'cfg(windows)'.build-dependencies]
-embed-resource = "2"
-
-[profile.release]
-opt-level = "z"
-lto = true
-strip = "debuginfo"
-
-
-
+[package]
+name = "spruceos-installer"
+version = "1.6.0"
+edition = "2021"
+description = "SpruceOS SD Card Installer"
+authors = ["SpruceOS Team (https://github.com/spruceUI)", "CMTag (https://github.com/CMTag)", "NextUI Team (https://github.com/LoveRetro)", "Helaas (https://github.com/Helaas)"]
+license = "CC-BY-NC-4.0"
+
+[features]
+default = []
+icon = []
+
+[dependencies]
+eframe = "0.33"
+egui = "0.33"
+egui_extras = { version = "0.33", features = ["all_loaders"] }
+egui-thematic = "0.1"
+tokio = { version = "1", features = ["rt-multi-thread", "fs", "process", "sync", "time", "io-util", "macros"] }
+reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
+sevenz-rust = "0.6"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+futures-util = "0.3"
+tokio-util = "0.7"
+dirs = "5"
+tempfile = "3"
+image = { version = "0.25", default-features = false, features = ["png", "qoi"] }
+lazy_static = "1.4"
+libc = "0.2"
+sha2 = "0.10"
+flate2 = "1.0"
+zip = { version = "2", default-features = false, features = ["deflate"] }
+arboard = "3.4"
+regex = "1"
+which = "6.0"
+
+[target.'cfg(windows)'.dependencies]
+windows = { version = "0.58", features = [
+ "Win32_Foundation",
+ "Win32_Storage_FileSystem",
+ "Win32_System_IO",
+ "Win32_System_Ioctl",
+ "Win32_System_Registry",
+ "Win32_Security",
+] }
+
+[target.'cfg(windows)'.build-dependencies]
+embed-resource = "2"
+
+[profile.release]
+opt-level = "z"
+lto = true
+strip = "debuginfo"
+
+
+
diff --git a/assets/Mac/Info.plist b/assets/Mac/Info.plist
index aa1b410..d8146a7 100644
--- a/assets/Mac/Info.plist
+++ b/assets/Mac/Info.plist
@@ -9,9 +9,9 @@
CFBundleIdentifier
com.spruceos.installer
CFBundleVersion
- 1.5.0
+ 1.6.0
CFBundleShortVersionString
- 1.5.0
+ 1.6.0
CFBundlePackageType
APPL
CFBundleExecutable
diff --git a/src/app/logic.rs b/src/app/logic.rs
index da2eeaa..63fa055 100644
--- a/src/app/logic.rs
+++ b/src/app/logic.rs
@@ -106,12 +106,23 @@ impl InstallerApp {
.collect()
}
+ /// Whether an asset is a raw disk image to burn, rather than an archive
+ /// whose contents get copied onto a formatted card.
+ ///
+ /// `.img.zip` is a raw image inside a zip container (BaseOS ships these) and
+ /// must be burned, so it is checked before the plain `.zip` archive case.
+ pub(super) fn is_raw_image_asset(name: &str) -> bool {
+ name.ends_with(".img.gz") ||
+ name.ends_with(".img.zip") ||
+ name.ends_with(".img")
+ }
+
/// Strip all known extensions from an asset name to get the base name
fn strip_extensions(name: &str) -> String {
let mut base = name.to_string();
// Remove known extensions in order of specificity
- for ext in &[".img.gz", ".tar.gz", ".7z", ".zip", ".img"] {
+ for ext in &[".img.gz", ".img.zip", ".tar.gz", ".7z", ".zip", ".img"] {
if base.ends_with(ext) {
base = base.strip_suffix(ext).unwrap_or(&base).to_string();
break; // Only strip one extension
@@ -136,8 +147,10 @@ impl InstallerApp {
if base_names.len() == 1 {
// Same base name, different extensions - pick by priority
- // Priority: .7z > .zip > .img.gz > .img
- const PRIORITY: &[&str] = &[".7z", ".zip", ".img.gz", ".img"];
+ // Priority: .7z > .img.zip > .zip > .img.gz > .img
+ // .img.zip is listed before .zip so the plain-archive entry does not
+ // shadow it — every .img.zip name also ends with .zip.
+ const PRIORITY: &[&str] = &[".7z", ".img.zip", ".zip", ".img.gz", ".img"];
for ext in PRIORITY {
if let Some((idx, _)) = assets.iter()
@@ -399,8 +412,7 @@ impl InstallerApp {
log(&format!("Disk space check passed: {} MB available", available_space / 1_048_576));
// Detect installation mode: raw image vs archive
- let is_raw_image = asset.name.ends_with(".img.gz") ||
- asset.name.ends_with(".img");
+ let is_raw_image = Self::is_raw_image_asset(&asset.name);
if is_raw_image {
crate::debug::log("Detected RAW IMAGE mode - will burn image to device");
diff --git a/src/app/ui.rs b/src/app/ui.rs
index 13f8725..419b904 100644
--- a/src/app/ui.rs
+++ b/src/app/ui.rs
@@ -159,13 +159,9 @@ impl eframe::App for InstallerApp {
}
// Check if selected asset is a raw image
- let is_raw_image = if let Some(idx) = auto_idx {
- let asset_name = &self.available_assets[idx].name;
- asset_name.ends_with(".img.gz") ||
- asset_name.ends_with(".img")
- } else {
- false
- };
+ let is_raw_image = auto_idx.is_some_and(|idx| {
+ Self::is_raw_image_asset(&self.available_assets[idx].name)
+ });
// If update mode and NOT a raw image, show preview modal; otherwise go to confirmation
if self.update_mode && !is_raw_image {
@@ -472,13 +468,9 @@ impl eframe::App for InstallerApp {
ui.add_enabled_ui(can_continue, |ui| {
if ui.button("Continue").clicked() {
// Check if selected asset is a raw image
- let is_raw_image = if let Some(idx) = self.selected_asset_idx {
- let asset_name = &self.available_assets[idx].name;
- asset_name.ends_with(".img.gz") ||
- asset_name.ends_with(".img")
- } else {
- false
- };
+ let is_raw_image = self.selected_asset_idx.is_some_and(|idx| {
+ Self::is_raw_image_asset(&self.available_assets[idx].name)
+ });
// If update mode and NOT a raw image, show preview; otherwise go to confirmation
if self.update_mode && !is_raw_image {
diff --git a/src/burn.rs b/src/burn.rs
index 1df85cc..ff34c89 100644
--- a/src/burn.rs
+++ b/src/burn.rs
@@ -9,6 +9,9 @@ use flate2::read::GzDecoder;
const CHUNK_SIZE: usize = 4 * 1024 * 1024; // 4MB chunks
+/// Chunk size used when pumping bytes out of a zip entry
+const ZIP_CHUNK_SIZE: usize = 1024 * 1024; // 1MB
+
#[derive(Debug, Clone)]
pub enum BurnProgress {
Started { total_bytes: u64 },
@@ -21,6 +24,179 @@ pub enum BurnProgress {
Error(String),
}
+/// How a downloaded raw image is compressed.
+///
+/// BaseOS publishes `.img.zip`; TwigUI publishes `.img.gz`. All forms are read
+/// back as a plain stream of raw image bytes.
+///
+/// NOTE: `Read` is written fully qualified throughout this section on purpose.
+/// Several functions below have their own `use std::io::Read;`, one of which is
+/// cfg-gated and guards the statement that follows it — importing `Read` at
+/// module scope here would make those look redundant and invite a cleanup that
+/// silently breaks the Windows build.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum ImageCompression {
+ None,
+ Gzip,
+ Zip,
+}
+
+fn detect_compression(image_path: &Path) -> ImageCompression {
+ let name = image_path
+ .file_name()
+ .and_then(|s| s.to_str())
+ .unwrap_or_default()
+ .to_ascii_lowercase();
+
+ if name.ends_with(".zip") {
+ ImageCompression::Zip
+ } else if name.ends_with(".gz") {
+ ImageCompression::Gzip
+ } else {
+ ImageCompression::None
+ }
+}
+
+/// Streams the first entry of a zip archive as a plain `Read`.
+///
+/// `ZipArchive::by_index` borrows the archive, so the resulting reader cannot
+/// escape into the `Box` the burn paths expect. A worker thread owns
+/// the archive instead and hands chunks over a bounded channel; the bound also
+/// caps how much decompressed data is buffered ahead of the writer.
+struct ZipEntryReader {
+ rx: std::sync::mpsc::Receiver, String>>,
+ current: Vec,
+ pos: usize,
+ finished: bool,
+}
+
+impl ZipEntryReader {
+ fn spawn(image_path: &Path) -> Result {
+ // Validate the archive up front so a bad file fails here rather than
+ // partway through writing to the device.
+ let entry_name = {
+ let file = std::fs::File::open(image_path)
+ .map_err(|e| format!("Failed to open zip image: {}", e))?;
+ let mut archive = zip::ZipArchive::new(file)
+ .map_err(|e| format!("Failed to read zip image: {}", e))?;
+ if archive.is_empty() {
+ return Err("Zip image contains no files".to_string());
+ }
+ let entry = archive
+ .by_index(0)
+ .map_err(|e| format!("Failed to open first zip entry: {}", e))?;
+ entry.name().to_string()
+ };
+
+ crate::debug::log(&format!("Zip entry: {}", entry_name));
+
+ let path = image_path.to_path_buf();
+ let (tx, rx) = std::sync::mpsc::sync_channel::, String>>(4);
+
+ std::thread::spawn(move || {
+ use std::io::Read;
+
+ let file = match std::fs::File::open(&path) {
+ Ok(f) => f,
+ Err(e) => {
+ let _ = tx.send(Err(format!("Failed to open zip image: {}", e)));
+ return;
+ }
+ };
+ let mut archive = match zip::ZipArchive::new(file) {
+ Ok(a) => a,
+ Err(e) => {
+ let _ = tx.send(Err(format!("Failed to read zip image: {}", e)));
+ return;
+ }
+ };
+ let mut entry = match archive.by_index(0) {
+ Ok(e) => e,
+ Err(e) => {
+ let _ = tx.send(Err(format!("Failed to open first zip entry: {}", e)));
+ return;
+ }
+ };
+
+ let mut buffer = vec![0u8; ZIP_CHUNK_SIZE];
+ loop {
+ match entry.read(&mut buffer) {
+ Ok(0) => break,
+ Ok(n) => {
+ // A send error means the reader was dropped (cancelled burn)
+ if tx.send(Ok(buffer[..n].to_vec())).is_err() {
+ return;
+ }
+ }
+ Err(e) => {
+ let _ = tx.send(Err(format!("Failed to decompress zip image: {}", e)));
+ return;
+ }
+ }
+ }
+ });
+
+ Ok(Self {
+ rx,
+ current: Vec::new(),
+ pos: 0,
+ finished: false,
+ })
+ }
+}
+
+impl std::io::Read for ZipEntryReader {
+ fn read(&mut self, out: &mut [u8]) -> std::io::Result {
+ while self.pos >= self.current.len() {
+ if self.finished {
+ return Ok(0);
+ }
+ match self.rx.recv() {
+ Ok(Ok(chunk)) => {
+ self.current = chunk;
+ self.pos = 0;
+ }
+ Ok(Err(e)) => {
+ self.finished = true;
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e));
+ }
+ // Sender dropped without an error: the entry was fully read
+ Err(_) => {
+ self.finished = true;
+ return Ok(0);
+ }
+ }
+ }
+
+ let n = std::cmp::min(self.current.len() - self.pos, out.len());
+ out[..n].copy_from_slice(&self.current[self.pos..self.pos + n]);
+ self.pos += n;
+ Ok(n)
+ }
+}
+
+/// Opens an image as a stream of raw bytes, decompressing `.gz` and `.zip`
+/// on the fly so the burn paths always see a plain image.
+fn open_image_reader(image_path: &Path) -> Result, String> {
+ match detect_compression(image_path) {
+ ImageCompression::Zip => {
+ crate::debug::log("Detected .zip image, decompressing on-the-fly during burn");
+ Ok(Box::new(ZipEntryReader::spawn(image_path)?))
+ }
+ ImageCompression::Gzip => {
+ crate::debug::log("Detected .gz image, decompressing on-the-fly during burn");
+ let file = std::fs::File::open(image_path)
+ .map_err(|e| format!("Failed to open image file: {}", e))?;
+ Ok(Box::new(GzDecoder::new(file)))
+ }
+ ImageCompression::None => {
+ let file = std::fs::File::open(image_path)
+ .map_err(|e| format!("Failed to open image file: {}", e))?;
+ Ok(Box::new(file))
+ }
+ }
+}
+
/// Burns a raw disk image to a device and verifies the write
pub async fn burn_image(
image_path: &Path,
@@ -32,19 +208,44 @@ pub async fn burn_image(
crate::debug::log(&format!("Image: {:?}", image_path));
crate::debug::log(&format!("Device: {}", device_path));
- // Get image size - for .gz files, we need to determine decompressed size
+ // Get image size - for compressed files, we need the decompressed size
let compressed_size = tokio::fs::metadata(image_path)
.await
.map_err(|e| format!("Failed to get image size: {}", e))?
.len();
- // Check if file is gzipped
- let is_gzipped = image_path.extension()
- .and_then(|s| s.to_str())
- .map(|s| s.eq_ignore_ascii_case("gz"))
- .unwrap_or(false);
+ let compression = detect_compression(image_path);
+
+ let image_size = if compression == ImageCompression::Zip {
+ crate::debug::log(&format!("Compressed size: {} bytes ({:.2} GB)", compressed_size, compressed_size as f64 / 1_073_741_824.0));
+
+ // Zip records the uncompressed size in its headers, so unlike .gz this
+ // needs no pre-scan of the whole file.
+ let decompressed_size = tokio::task::spawn_blocking({
+ let image_path = image_path.to_path_buf();
+ move || -> Result {
+ let file = std::fs::File::open(&image_path)
+ .map_err(|e| format!("Failed to open image for size check: {}", e))?;
+ let mut archive = zip::ZipArchive::new(file)
+ .map_err(|e| format!("Failed to read zip image: {}", e))?;
+ if archive.is_empty() {
+ return Err("Zip image contains no files".to_string());
+ }
+ let entry = archive
+ .by_index(0)
+ .map_err(|e| format!("Failed to open first zip entry: {}", e))?;
+ Ok(entry.size())
+ }
+ }).await
+ .map_err(|e| format!("Size lookup task failed: {}", e))??;
+
+ if decompressed_size == 0 {
+ return Err("Zip image reports an uncompressed size of zero".to_string());
+ }
- let image_size = if is_gzipped {
+ crate::debug::log(&format!("Decompressed size: {} bytes ({:.2} GB)", decompressed_size, decompressed_size as f64 / 1_073_741_824.0));
+ decompressed_size
+ } else if compression == ImageCompression::Gzip {
crate::debug::log(&format!("Compressed size: {} bytes ({:.2} GB)", compressed_size, compressed_size as f64 / 1_073_741_824.0));
crate::debug::log("Pre-scanning .gz file to determine decompressed size...");
@@ -392,26 +593,13 @@ async fn burn_image_windows(
crate::debug::log("File pointer reset, beginning image write...");
- // Check if file is gzipped and create appropriate reader
- let is_gzipped = image_path.extension()
- .and_then(|s| s.to_str())
- .map(|s| s.eq_ignore_ascii_case("gz"))
- .unwrap_or(false);
-
- let file = std::fs::File::open(&image_path)
+ let mut image_reader = open_image_reader(&image_path)
.map_err(|e| {
unsafe { let _ = CloseHandle(handle); }
cleanup_volumes(&volume_handles);
- format!("Failed to open image file: {}", e)
+ e
})?;
- let mut image_reader: Box = if is_gzipped {
- crate::debug::log("Detected .gz file, decompressing on-the-fly during burn");
- Box::new(GzDecoder::new(file))
- } else {
- Box::new(file)
- };
-
// Windows requires 512-byte sector-aligned writes for physical drives (SECTOR_SIZE already defined above)
// Allocate buffers: read buffer for decompression, sector buffer for aligned writes
let mut read_buffer = vec![0u8; CHUNK_SIZE];
@@ -611,21 +799,7 @@ async fn burn_image_linux(
.open(&device_path)
.map_err(|e| format!("Failed to open device {}: {}. Are you running with sudo/root?", device_path, e))?;
- // Check if file is gzipped and create appropriate reader
- let is_gzipped = image_path.extension()
- .and_then(|s| s.to_str())
- .map(|s| s.eq_ignore_ascii_case("gz"))
- .unwrap_or(false);
-
- let file = std::fs::File::open(&image_path)
- .map_err(|e| format!("Failed to open image file: {}", e))?;
-
- let mut image_reader: Box = if is_gzipped {
- crate::debug::log("Detected .gz file, decompressing on-the-fly during burn");
- Box::new(GzDecoder::new(file))
- } else {
- Box::new(file)
- };
+ let mut image_reader = open_image_reader(&image_path)?;
let mut buffer = vec![0u8; CHUNK_SIZE];
let mut total_written = 0u64;
@@ -777,21 +951,7 @@ async fn burn_image_macos(
crate::debug::log("Ready to write image");
- // Check if file is gzipped and create appropriate reader
- let is_gzipped = image_path.extension()
- .and_then(|s| s.to_str())
- .map(|s| s.eq_ignore_ascii_case("gz"))
- .unwrap_or(false);
-
- let file = std::fs::File::open(&image_path)
- .map_err(|e| format!("Failed to open image file: {}", e))?;
-
- let mut image_reader: Box = if is_gzipped {
- crate::debug::log("Detected .gz file, decompressing on-the-fly during burn");
- Box::new(GzDecoder::new(file))
- } else {
- Box::new(file)
- };
+ let mut image_reader = open_image_reader(&image_path)?;
// macOS raw devices with F_NOCACHE require sector-aligned writes
// Use similar buffering approach as Windows implementation
@@ -886,7 +1046,7 @@ async fn verify_image(
) -> Result<(), String> {
crate::debug::log("Computing image hash...");
- // Compute hash of original image (decompress if .gz)
+ // Compute hash of original image (decompressing .gz/.zip as needed)
let image_hash = tokio::task::spawn_blocking({
let image_path = image_path.to_path_buf();
let cancel_token = cancel_token.clone();
@@ -894,21 +1054,7 @@ async fn verify_image(
move || -> Result {
use std::io::Read;
- // Check if file is gzipped and create appropriate reader
- let is_gzipped = image_path.extension()
- .and_then(|s| s.to_str())
- .map(|s| s.eq_ignore_ascii_case("gz"))
- .unwrap_or(false);
-
- let file = std::fs::File::open(&image_path)
- .map_err(|e| format!("Failed to open image for verification: {}", e))?;
-
- let mut image_reader: Box = if is_gzipped {
- crate::debug::log("Decompressing .gz file for hash verification");
- Box::new(GzDecoder::new(file))
- } else {
- Box::new(file)
- };
+ let mut image_reader = open_image_reader(&image_path)?;
let mut hasher = Sha256::new();
let mut buffer = vec![0u8; CHUNK_SIZE];
diff --git a/src/config.rs b/src/config.rs
index d39e3c1..8b3708f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -292,6 +292,26 @@ pub const SPRUCE_UPDATE_DELETE_PATHS: &[&str] = &[
"README.md",
];
+/// Device name mappings for BaseOS release assets.
+///
+/// Assets are named `baseos--.img.zip`. Matching is a plain
+/// substring test against the filename and the FIRST match wins, so more
+/// specific patterns must come first — `-rg34xx` would otherwise swallow
+/// `-rg34xxsp`. The leading dash keeps patterns anchored to the device field.
+pub const BASEOS_DEVICE_MAPPINGS: &[AssetDisplayMapping] = &[
+ AssetDisplayMapping { pattern: "-rg35xxplus", display_name: "RG35XX Plus", devices: "Anbernic RG35XX Plus" },
+ AssetDisplayMapping { pattern: "-rg35xxpro", display_name: "RG35XX Pro", devices: "Anbernic RG35XX Pro" },
+ AssetDisplayMapping { pattern: "-rg35xxsp", display_name: "RG35XX SP", devices: "Anbernic RG35XX SP" },
+ AssetDisplayMapping { pattern: "-rg35xxh", display_name: "RG35XX H", devices: "Anbernic RG35XX H" },
+ AssetDisplayMapping { pattern: "-rg34xxsp", display_name: "RG34XX SP", devices: "Anbernic RG34XX SP" },
+ AssetDisplayMapping { pattern: "-rg34xx", display_name: "RG34XX", devices: "Anbernic RG34XX" },
+ AssetDisplayMapping { pattern: "-rg40xxh", display_name: "RG40XX H", devices: "Anbernic RG40XX H" },
+ AssetDisplayMapping { pattern: "-rg40xxv", display_name: "RG40XX V", devices: "Anbernic RG40XX V" },
+ AssetDisplayMapping { pattern: "-rgcubexx", display_name: "RG CubeXX", devices: "Anbernic RG CubeXX" },
+ AssetDisplayMapping { pattern: "-rg28xx", display_name: "RG28XX", devices: "Anbernic RG28XX" },
+ AssetDisplayMapping { pattern: "-rgsp", display_name: "RG SP", devices: "Anbernic RG SP" },
+];
+
pub const REPO_OPTIONS: &[RepoOption] = &[
RepoOption {
name: "Stable",
@@ -315,17 +335,6 @@ pub const REPO_OPTIONS: &[RepoOption] = &[
asset_display_mappings: None,
supports_preserve_mode: true,
},
- RepoOption {
- name: "SprigUI",
- url: "spruceUI/sprigUI",
- info: "SpruceOS for the Miyoo Mini Flip.",
- display_name: None, // Falls back to "SprigUI"
- supports_update_mode: true, // Archive-based (.7z)
- update_directories: &["Retroarch", "spruce"],
- allowed_extensions: Some(&[".7z"]), // Only show 7z archives
- asset_display_mappings: None,
- supports_preserve_mode: false,
- },
RepoOption {
name: "TwigUI",
url: "spruceUI/twigUI",
@@ -337,6 +346,17 @@ pub const REPO_OPTIONS: &[RepoOption] = &[
asset_display_mappings: None,
supports_preserve_mode: false,
},
+ RepoOption {
+ name: "BaseOS",
+ url: "pvaibhav/BaseOS",
+ info: "A minimal base OS for Anbernic RG XX devices. 3 second boot time.\nA third-party project by pvaibhav - not a spruceOS release.\nRaw disk image; erases the entire card.\n[Project page](https://github.com/pvaibhav/BaseOS)",
+ display_name: Some("BaseOS"),
+ supports_update_mode: false, // Raw disk images always do a full burn
+ update_directories: &[], // Not used for raw images
+ allowed_extensions: Some(&[".img.zip"]), // Excludes .bosupd update packages
+ asset_display_mappings: Some(BASEOS_DEVICE_MAPPINGS),
+ supports_preserve_mode: false,
+ },
];
/// Index of the default repository selection (0 = first option)