Support native SD storage and consolidate fds-flash tooling
Accept native SD cards for internal settings alongside legacy NVMe, reject USB ancestry and ambiguous disks, and preserve read-only boot loading with explicit recovery writes. Require built-in MMC drivers, validate cached kernel configuration, and provide SD-only, SD-first and USB-first EEPROM profiles. Expose typed create and inspect commands through fds-flash, reuse the existing cartridge creation code, and document the optional e2fsprogs package dependency. Extend storage, EEPROM, flashing and hardware-test coverage and advance the wiki reference to the published SD guide. Validation: rootfs checks for CLI/development, boot matrix, internal storage, EEPROM, flash, workstation, emulator, make check and wiki checks passed. The full internal suite passed on an unchanged rerun after one unexplained VM shutdown stall. Standalone init-test was blocked by its unavailable pinned upstream kernel. Physical Pi checks remain pending.
This commit is contained in:
@@ -211,7 +211,7 @@ pub fn inspect(path: &Path, runner: Option<&Path>) -> Result<Inspection> {
|
||||
let mut info = image::inspect(&file, file.metadata()?.len())?;
|
||||
if info.filesystem != "erofs" {
|
||||
return Err(Error(
|
||||
"Use fds-burn inspect for DATA geometry; this inspector validates software cartridges"
|
||||
"Use fds-flash inspect for DATA geometry; this inspector validates software cartridges"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{Prepared, device, prepare};
|
||||
use clap::{Parser, Subcommand};
|
||||
use fds_common::{Error, Result};
|
||||
use super::{Inspection, Prepared, device, inspect, prepare};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use fds_common::{Error, Result, manifest::Class};
|
||||
use std::{
|
||||
io::{self, BufRead, IsTerminal, Write},
|
||||
path::{Path, PathBuf},
|
||||
@@ -15,8 +15,8 @@ fn digest(value: &str) -> std::result::Result<String, String> {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "fds-flash", version = env!("FDS_BUILD_VERSION"),
|
||||
about = "Flash complete FDS internal and cartridge disk images on Linux",
|
||||
after_help = "With no flags, choose an image and disk interactively. Physical writes need root. Every write flushes and verifies readback; no partition or filesystem is expanded. Verify downloaded release signatures separately.",
|
||||
about = "Create, inspect and flash FDS internal and cartridge disk images on Linux",
|
||||
after_help = "With no flags, choose an image and disk interactively. Physical writes need root. Every write flushes and verifies readback; no partition or filesystem is expanded. Create and inspect work on regular files without a destination disk. Build new PROGRAM images with fds-cartridge. Verify downloaded release signatures separately.",
|
||||
args_conflicts_with_subcommands = true)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
@@ -49,6 +49,19 @@ pub struct Cli {
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Action {
|
||||
/// Inspect a complete internal or cartridge image and report its SHA-256.
|
||||
Inspect {
|
||||
image: PathBuf,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Create a cartridge image from a prepared tree containing FDS/CARTRIDGE.TOML.
|
||||
Create {
|
||||
#[command(subcommand)]
|
||||
command: CreateCommand,
|
||||
#[arg(long, global = true)]
|
||||
json: bool,
|
||||
},
|
||||
/// List physical whole disks, identities and reasons they cannot be flashed.
|
||||
List {
|
||||
#[arg(long)]
|
||||
@@ -56,6 +69,58 @@ enum Action {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct ImageTree {
|
||||
/// Prepared tree, including its FDS/CARTRIDGE.TOML manifest.
|
||||
source_directory: PathBuf,
|
||||
/// New regular image file outside the source tree; never overwritten.
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum CreateCommand {
|
||||
/// Create a writable DATA cartridge with an ext4 payload (requires e2fsprogs).
|
||||
Data {
|
||||
#[command(flatten)]
|
||||
tree: ImageTree,
|
||||
/// DATA filesystem size in MiB; the complete GPT image is slightly larger.
|
||||
#[arg(long, default_value_t = 128, value_parser = clap::value_parser!(u64).range(32..=1048576))]
|
||||
size_mib: u64,
|
||||
},
|
||||
/// Create an ENVIRONMENT descriptor cartridge (requires erofs-utils).
|
||||
Environment(ImageTree),
|
||||
/// Wrap a fully prepared FDS SYSTEM root (requires erofs-utils).
|
||||
System(ImageTree),
|
||||
}
|
||||
|
||||
fn image_result(status: &str, inspection: &Inspection, json: bool) -> Result<()> {
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"format": 1, "status": status, "inspection": inspection
|
||||
}))
|
||||
.map_err(|e| Error(e.to_string()))?
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}: {}\nKind: {}\nImage bytes: {}\nSHA-256: {}",
|
||||
status.to_uppercase(),
|
||||
inspection.image_path.display(),
|
||||
inspection.image.kind,
|
||||
inspection.image.bytes,
|
||||
inspection.sha256
|
||||
);
|
||||
for partition in &inspection.image.partitions {
|
||||
println!(
|
||||
" Partition {}: {} ({} bytes)",
|
||||
partition.number, partition.name, partition.bytes
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(text: &str) -> Result<String> {
|
||||
eprint!("{text}");
|
||||
io::stderr().flush()?;
|
||||
@@ -158,26 +223,41 @@ fn show(prepared: &Prepared) {
|
||||
}
|
||||
|
||||
pub fn run(cli: Cli) -> Result<()> {
|
||||
if let Some(Action::List { json }) = cli.command {
|
||||
let disks = device::list()?;
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&disks).map_err(|e| Error(e.to_string()))?
|
||||
);
|
||||
} else {
|
||||
for disk in disks {
|
||||
println!(
|
||||
"{}\n target_id: {}\n {}",
|
||||
describe(&disk.target),
|
||||
disk.target_id,
|
||||
disk.blocked
|
||||
.map(|r| format!("BLOCKED: {r}"))
|
||||
.unwrap_or_else(|| "Available for explicit selection".into())
|
||||
);
|
||||
}
|
||||
match cli.command {
|
||||
Some(Action::Inspect { image, json }) => {
|
||||
return image_result("inspected", &inspect(&image)?, json);
|
||||
}
|
||||
return Ok(());
|
||||
Some(Action::Create { command, json }) => {
|
||||
let (class, tree, size) = match command {
|
||||
CreateCommand::Data { tree, size_mib } => (Class::Data, tree, Some(size_mib)),
|
||||
CreateCommand::Environment(tree) => (Class::Environment, tree, None),
|
||||
CreateCommand::System(tree) => (Class::System, tree, None),
|
||||
};
|
||||
fds_burn::create::create(class, &tree.source_directory, &tree.output, size)?;
|
||||
return image_result("created", &inspect(&tree.output)?, json);
|
||||
}
|
||||
Some(Action::List { json }) => {
|
||||
let disks = device::list()?;
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&disks).map_err(|e| Error(e.to_string()))?
|
||||
);
|
||||
} else {
|
||||
for disk in disks {
|
||||
println!(
|
||||
"{}\n target_id: {}\n {}",
|
||||
describe(&disk.target),
|
||||
disk.target_id,
|
||||
disk.blocked
|
||||
.map(|r| format!("BLOCKED: {r}"))
|
||||
.unwrap_or_else(|| "Available for explicit selection".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
if !cli.unattended && !cli.dry_run && !io::stdin().is_terminal() {
|
||||
return Err(Error("Interactive flashing requires a terminal; use --dry-run or explicit --unattended options".into()));
|
||||
@@ -279,4 +359,42 @@ mod tests {
|
||||
assert!(Cli::try_parse_from(args).is_ok());
|
||||
assert!(digest("wrong").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_commands_are_typed_and_do_not_accept_disk_write_options() {
|
||||
for args in [
|
||||
vec!["inspect", "card.img", "--json"],
|
||||
vec![
|
||||
"create",
|
||||
"data",
|
||||
"tree",
|
||||
"card.img",
|
||||
"--size-mib",
|
||||
"32",
|
||||
"--json",
|
||||
],
|
||||
vec!["create", "--json", "environment", "tree", "card.img"],
|
||||
vec!["create", "system", "tree", "card.img"],
|
||||
] {
|
||||
assert!(Cli::try_parse_from(std::iter::once("fds-flash").chain(args)).is_ok());
|
||||
}
|
||||
for args in [
|
||||
vec!["inspect", "card.img", "--device", "/dev/sda"],
|
||||
vec!["--unattended", "create", "data", "tree", "card.img"],
|
||||
vec!["create", "program", "tree", "card.img"],
|
||||
vec!["create", "data", "tree"],
|
||||
vec!["create", "data", "tree", "card.img", "--size-mib", "31"],
|
||||
vec!["create", "data", "tree", "card.img", "--size-mib", "bad"],
|
||||
vec![
|
||||
"create",
|
||||
"environment",
|
||||
"tree",
|
||||
"card.img",
|
||||
"--size-mib",
|
||||
"32",
|
||||
],
|
||||
] {
|
||||
assert!(Cli::try_parse_from(std::iter::once("fds-flash").chain(args)).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Guided and unattended installation of complete disk images on Linux.
|
||||
//! Image inspection and guided/unattended installation of complete FDS disks.
|
||||
pub mod cli;
|
||||
pub mod device;
|
||||
mod image;
|
||||
@@ -25,12 +25,14 @@ pub struct Prepared {
|
||||
source: File,
|
||||
}
|
||||
|
||||
pub fn prepare(
|
||||
image_path: &Path,
|
||||
device_path: &Path,
|
||||
file_target: bool,
|
||||
expected_sha256: Option<&str>,
|
||||
) -> Result<Prepared> {
|
||||
#[derive(Serialize)]
|
||||
pub struct Inspection {
|
||||
pub image_path: PathBuf,
|
||||
pub image: image::Geometry,
|
||||
pub sha256: String,
|
||||
}
|
||||
|
||||
fn inspect_source(image_path: &Path, expected_sha256: Option<&str>) -> Result<(File, Inspection)> {
|
||||
let image_path = image_path.canonicalize()?;
|
||||
let source = crate::cartridge::open_image(&image_path)?;
|
||||
let geometry = image::inspect(&source, source.metadata()?.len())?;
|
||||
@@ -43,6 +45,33 @@ pub fn prepare(
|
||||
if image::inspect(&source, source.metadata()?.len())? != geometry {
|
||||
return Err(Error("Image geometry changed during inspection".into()));
|
||||
}
|
||||
Ok((
|
||||
source,
|
||||
Inspection {
|
||||
image_path,
|
||||
image: geometry,
|
||||
sha256,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Inspect a complete internal or cartridge image without selecting a target.
|
||||
pub fn inspect(image_path: &Path) -> Result<Inspection> {
|
||||
Ok(inspect_source(image_path, None)?.1)
|
||||
}
|
||||
|
||||
pub fn prepare(
|
||||
image_path: &Path,
|
||||
device_path: &Path,
|
||||
file_target: bool,
|
||||
expected_sha256: Option<&str>,
|
||||
) -> Result<Prepared> {
|
||||
let (source, inspection) = inspect_source(image_path, expected_sha256)?;
|
||||
let Inspection {
|
||||
image_path,
|
||||
image: geometry,
|
||||
sha256,
|
||||
} = inspection;
|
||||
let target = device::inspect(device_path, file_target)?;
|
||||
let metadata = std::fs::metadata(target.path())?;
|
||||
if source.metadata()?.dev() == metadata.dev() && source.metadata()?.ino() == metadata.ino() {
|
||||
|
||||
Reference in New Issue
Block a user