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:
+128
-24
@@ -1,4 +1,4 @@
|
||||
//! Internal NVMe access is explicit and isolated in this process's mount namespace.
|
||||
//! Internal SD/NVMe access is explicit and isolated in this process's mount namespace.
|
||||
use fds_common::{
|
||||
Error, Result,
|
||||
machine::{self, Config},
|
||||
@@ -58,29 +58,55 @@ fn parent(part: &sysfs::BlockPartition) -> Result<PathBuf> {
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| Error("Partition has no parent disk".into()))
|
||||
}
|
||||
fn select() -> Result<(sysfs::BlockPartition, PathBuf, String)> {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Transport {
|
||||
Sd,
|
||||
Nvme,
|
||||
}
|
||||
impl Transport {
|
||||
fn source(self) -> &'static str {
|
||||
match self {
|
||||
Self::Sd => "internal_sd",
|
||||
Self::Nvme => "internal_nvme",
|
||||
}
|
||||
}
|
||||
}
|
||||
fn transport(disk: &Path) -> Result<Option<Transport>> {
|
||||
let buses: Vec<_> = disk
|
||||
.ancestors()
|
||||
.filter_map(|path| fs::read_link(path.join("subsystem")).ok())
|
||||
.filter_map(|path| path.file_name().map(|s| s.to_owned()))
|
||||
.collect();
|
||||
// A USB card reader is a workstation flashing destination, never internal
|
||||
// machine storage at runtime. Names such as mmcblk0 are not identities.
|
||||
if buses.iter().any(|b| b == "usb") {
|
||||
return Ok(None);
|
||||
}
|
||||
if buses.iter().any(|b| b == "mmc") {
|
||||
let kind = read_text(&disk.join("device/type"), 32)?;
|
||||
return Ok((kind.trim() == "SD").then_some(Transport::Sd));
|
||||
}
|
||||
if buses.iter().any(|b| b == "nvme") && read_text(&disk.join("removable"), 32)?.trim() == "0" {
|
||||
return Ok(Some(Transport::Nvme));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn select() -> Result<(sysfs::BlockPartition, PathBuf, String, Transport)> {
|
||||
let parts = sysfs::partitions(Path::new("/sys"))?;
|
||||
let mut candidates = Vec::new();
|
||||
for part in parts.iter().filter(|p| p.partition_name == "FDS_INTERNAL") {
|
||||
let disk = parent(part)?;
|
||||
let buses: Vec<_> = disk
|
||||
.ancestors()
|
||||
.filter_map(|path| fs::read_link(path.join("subsystem")).ok())
|
||||
.filter_map(|path| path.file_name().map(|s| s.to_owned()))
|
||||
.collect();
|
||||
if !buses.iter().any(|b| b == "nvme") || buses.iter().any(|b| b == "usb") {
|
||||
let Some(transport) = transport(&disk)? else {
|
||||
continue;
|
||||
}
|
||||
if read_text(&disk.join("removable"), 32)?.trim() != "0" {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let siblings: Vec<_> = parts
|
||||
.iter()
|
||||
.filter(|p| parent(p).ok().as_ref() == Some(&disk))
|
||||
.collect();
|
||||
if siblings.len() != 3 {
|
||||
return Err(Error(
|
||||
"Internal NVMe must contain exactly FDS_BOOT, FDS_RECOVERY and FDS_INTERNAL".into(),
|
||||
"Internal storage must contain exactly FDS_BOOT, FDS_RECOVERY and FDS_INTERNAL"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
for (number, label) in [(1, "FDS_BOOT"), (2, "FDS_RECOVERY"), (3, "FDS_INTERNAL")] {
|
||||
@@ -99,22 +125,22 @@ fn select() -> Result<(sysfs::BlockPartition, PathBuf, String)> {
|
||||
.trim()
|
||||
!= number.to_string()
|
||||
{
|
||||
return Err(Error("Invalid internal NVMe partition layout".into()));
|
||||
return Err(Error("Invalid internal storage partition layout".into()));
|
||||
}
|
||||
}
|
||||
let sequence = read_text(&disk.join("diskseq"), 32)?.trim().to_owned();
|
||||
sequence
|
||||
.parse::<u64>()
|
||||
.map_err(|_| Error("Invalid internal disk sequence".into()))?;
|
||||
candidates.push((part.clone(), disk, sequence));
|
||||
candidates.push((part.clone(), disk, sequence, transport));
|
||||
}
|
||||
match candidates.len() {
|
||||
0 => Err(Error(
|
||||
"No complete internal NVMe layout found; image defaults remain available".into(),
|
||||
"No complete internal SD/NVMe layout found; image defaults remain available".into(),
|
||||
)),
|
||||
1 => Ok(candidates.pop().unwrap()),
|
||||
_ => Err(Error(
|
||||
"Multiple internal NVMe layouts found; refusing to choose one".into(),
|
||||
"Multiple internal SD/NVMe layouts found; refusing to choose one".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -122,11 +148,12 @@ struct Internal {
|
||||
file: File,
|
||||
disk: PathBuf,
|
||||
sequence: String,
|
||||
transport: Transport,
|
||||
mounted: bool,
|
||||
}
|
||||
impl Internal {
|
||||
fn open(writable: bool) -> Result<Self> {
|
||||
let (part, disk, sequence) = select()?;
|
||||
let (part, disk, sequence, transport) = select()?;
|
||||
let mounts = read_text(Path::new("/proc/self/mountinfo"), 4 * 1024 * 1024)?;
|
||||
if mounts.lines().any(|line| {
|
||||
line.split_whitespace().nth(2) == Some(&format!("{}:{}", part.major, part.minor))
|
||||
@@ -180,6 +207,7 @@ impl Internal {
|
||||
file,
|
||||
disk,
|
||||
sequence,
|
||||
transport,
|
||||
mounted: false,
|
||||
};
|
||||
internal.identity()?;
|
||||
@@ -209,7 +237,9 @@ impl Internal {
|
||||
}
|
||||
fn identity(&self) -> Result<()> {
|
||||
if read_text(&self.disk.join("diskseq"), 32)?.trim() != self.sequence {
|
||||
return Err(Error("Internal NVMe changed during the operation".into()));
|
||||
return Err(Error(
|
||||
"Internal storage changed during the operation".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -309,15 +339,16 @@ pub fn load() -> Result<()> {
|
||||
if Path::new(machine::SNAPSHOT).exists() && Path::new(machine::STATUS).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let attempt = || -> Result<(Config, String)> {
|
||||
let attempt = || -> Result<(Config, String, Transport)> {
|
||||
let internal = Internal::open(false)?;
|
||||
let config = Config::parse(&machine::trusted_text(
|
||||
Path::new(&format!("{MOUNT}/config/machine.json")),
|
||||
machine::MAX_BUNDLE,
|
||||
)?)?;
|
||||
let sequence = internal.sequence.clone();
|
||||
let transport = internal.transport;
|
||||
internal.close(false)?;
|
||||
Ok((config, sequence))
|
||||
Ok((config, sequence, transport))
|
||||
};
|
||||
let emulated = emulator_config()?;
|
||||
let (config, status) = if let Some(config) = emulated {
|
||||
@@ -325,8 +356,8 @@ pub fn load() -> Result<()> {
|
||||
(config, status)
|
||||
} else {
|
||||
match attempt() {
|
||||
Ok((config, sequence)) => {
|
||||
let status = serde_json::json!({"source":"internal_nvme", "name":config.name, "disk_sequence":sequence, "error":null});
|
||||
Ok((config, sequence, transport)) => {
|
||||
let status = serde_json::json!({"source":transport.source(), "name":config.name, "disk_sequence":sequence, "error":null});
|
||||
(config, status)
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -410,7 +441,7 @@ fn store(name: &str, input: &Path) -> Result<()> {
|
||||
}
|
||||
atomic(&output, &bytes, 0o600)?;
|
||||
internal.close(true)?;
|
||||
println!("Saved diagnostics/{name} on internal NVMe; storage is flushed and unmounted.");
|
||||
println!("Saved diagnostics/{name} on internal storage; storage is flushed and unmounted.");
|
||||
Ok(())
|
||||
}
|
||||
pub fn run(command: crate::cli::MachineCommand, json: bool) -> Result<()> {
|
||||
@@ -508,3 +539,76 @@ pub fn run(command: crate::cli::MachineCommand, json: bool) -> Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod transport_tests {
|
||||
use super::{Transport, transport};
|
||||
use std::{fs, os::unix::fs::symlink, path::PathBuf};
|
||||
|
||||
struct Fixture(PathBuf);
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"fds-internal-transport-{}-{nonce}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&root).unwrap();
|
||||
Self(root)
|
||||
}
|
||||
fn disk(&self, bus: &str, kind: &str, removable: &str, usb: bool) -> PathBuf {
|
||||
let ancestor = self.0.join(format!("{bus}-{kind}-{removable}-{usb}"));
|
||||
let card = ancestor.join("card");
|
||||
let disk = card.join("block/arbitrary-disk-name");
|
||||
fs::create_dir_all(&disk).unwrap();
|
||||
symlink(format!("/sys/bus/{bus}"), card.join("subsystem")).unwrap();
|
||||
if usb {
|
||||
symlink("/sys/bus/usb", ancestor.join("subsystem")).unwrap();
|
||||
}
|
||||
symlink(&card, disk.join("device")).unwrap();
|
||||
fs::write(card.join("type"), kind).unwrap();
|
||||
fs::write(disk.join("removable"), removable).unwrap();
|
||||
disk
|
||||
}
|
||||
}
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_sd_uses_mmc_card_identity_not_removable_flag_or_device_name() {
|
||||
let fixture = Fixture::new();
|
||||
for removable in ["0", "1"] {
|
||||
let disk = fixture.disk("mmc", "SD", removable, false);
|
||||
assert_eq!(transport(&disk).unwrap(), Some(Transport::Sd));
|
||||
}
|
||||
assert_eq!(Transport::Sd.source(), "internal_sd");
|
||||
let disk = fixture.disk("nvme", "", "0", false);
|
||||
assert_eq!(transport(&disk).unwrap(), Some(Transport::Nvme));
|
||||
assert_eq!(Transport::Nvme.source(), "internal_nvme");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn usb_lookalikes_other_mmc_devices_and_unknown_transports_are_rejected() {
|
||||
let fixture = Fixture::new();
|
||||
for (bus, kind, removable, usb) in [
|
||||
("mmc", "SD", "0", true),
|
||||
("nvme", "", "0", true),
|
||||
("mmc", "MMC", "0", false),
|
||||
("mmc", "SDIO", "1", false),
|
||||
("scsi", "SD", "0", false),
|
||||
("nvme", "", "1", false),
|
||||
] {
|
||||
let disk = fixture.disk(bus, kind, removable, usb);
|
||||
assert_eq!(transport(&disk).unwrap(), None, "{}", disk.display());
|
||||
}
|
||||
let disk = fixture.disk("mmc", "SD", "1", false);
|
||||
fs::remove_file(disk.join("device/type")).unwrap();
|
||||
assert!(transport(&disk).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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