mod signal;
use std::fs;
use std::io::{self, BufWriter, Write};
use std::path::PathBuf;
use anyhow::{Context, bail};
use clap::{Parser, Subcommand};
use rigless::coverage::Coverage;
use rigless::machine::Board;
use rigless::session::Session;
use rigless::sidecar::{Access, AccessKind, BackendError, SidecarOptions, StopReason, TargetStart};
use rigless::time::VirtualNanos;
const NANOS_PER_MICRO: u64 = 1_000;
const NANOS_PER_MILLI: u64 = 1_000_000;
const NANOS_PER_SECOND: u64 = 1_000_000_000;
const NO_DEADLINE: VirtualNanos = VirtualNanos(i64::MAX as u64);
const UNITS: [(&str, u64); 4] = [
("ns", 1),
("us", NANOS_PER_MICRO),
("ms", NANOS_PER_MILLI),
("s", NANOS_PER_SECOND),
];
#[derive(Parser)]
#[command(version, about)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Svd {
#[command(subcommand)]
command: Svd,
},
Run(Run),
}
#[derive(Subcommand)]
enum Svd {
Generate {
soc_directory: PathBuf,
},
}
#[derive(clap::Args)]
struct Run {
#[arg(long)]
machine: String,
#[arg(long, value_parser = parse_virtual_time)]
until: Option<VirtualNanos>,
#[arg(long, value_name = "PORT | MCU=PORT")]
gdb: Vec<String>,
#[arg(long, requires = "gdb")]
wait_for_debugger: bool,
#[arg(long, value_name = "PATH")]
sidecar: Option<PathBuf>,
#[arg(long, value_name = "MCU=PATH")]
firmware: Vec<String>,
#[arg(long, value_name = "PATH")]
coverage: Option<PathBuf>,
image: Option<PathBuf>,
}
fn main() -> anyhow::Result<()> {
match Cli::parse().command {
Command::Svd {
command: Svd::Generate { soc_directory },
} => rigless::svd::write_module(&soc_directory)
.with_context(|| format!("generating {}", soc_directory.display())),
Command::Run(run) => match run_firmware(&run) {
Err(error) if matches!(error.downcast_ref(), Some(BackendError::Cancelled)) => {
Err(signal::die_of_recorded_signal().into())
}
result => result,
},
}
}
fn run_firmware(run: &Run) -> anyhow::Result<()> {
let machines = rigless::machine::all();
let Some((_name, build_machine)) = machines.iter().find(|(name, _)| *name == run.machine)
else {
let names: Vec<&str> = machines.iter().map(|(name, _)| *name).collect();
bail!(
"no machine named {}; this build has {}",
run.machine,
names.join(", ")
);
};
let mut machine = build_machine().with_context(|| format!("building {}", run.machine))?;
let images = images_of(&*machine, run)?;
let ports = debugger_ports_of(&*machine, run)?;
let labels = labels_of(&*machine);
let starts: Vec<TargetStart<'_>> = images
.iter()
.zip(&ports)
.map(|(firmware, &debugger_port)| TargetStart {
firmware,
debugger_port,
wait_for_debugger: run.wait_for_debugger,
})
.collect();
signal::install().context("routing SIGINT and SIGTERM to the run")?;
let options = SidecarOptions {
binary: run.sidecar.clone(),
cancel: &signal::CANCEL,
};
for (label, port) in labels.iter().zip(&ports) {
if let Some(port) = port {
let state = if run.wait_for_debugger {
"waiting for a debugger"
} else {
"serving GDB"
};
writeln!(io::stderr(), "{label}sidecar {state} on :{port}")?;
}
}
let mut reports: Vec<Coverage> = machine
.targets()
.into_iter()
.map(|target| Coverage::of(target.mcu))
.collect();
let mut session = Session::spawn(machine, &starts, &options)?;
let mut output = BufWriter::new(io::stdout().lock());
let mut write_error = None;
let until = run.until.unwrap_or(NO_DEADLINE);
let stops = session.run(until, &mut |target, access, value| {
reports[target].saw(access);
if write_error.is_none() {
write_error = write_access(&mut output, &labels[target], access, value).err();
}
})?;
if let Some(error) = write_error {
return Err(error.into());
}
for (target, (label, stop)) in labels.iter().zip(&stops).enumerate() {
writeln!(
output,
"stop {label}t={} n={} reason={}",
stop.at.0,
stop.instruction_count,
stop_reason(stop.reason)
)?;
for (address, program_counter) in session.unmodeled(target) {
writeln!(
output,
"unmodeled {label}at={address:#010x} pc={program_counter:#010x}"
)?;
}
for (instance, interrupt) in session.unrouted(target) {
writeln!(
output,
"unrouted {label}instance={instance} interrupt={interrupt}"
)?;
}
}
if let Some(path) = &run.coverage {
let mut report = String::new();
for (label, coverage) in labels.iter().zip(&reports) {
report.push_str(label);
report.push_str(&coverage.to_string());
}
fs::write(path, report).with_context(|| format!("writing {}", path.display()))?;
}
output.flush()?;
session.shutdown()?;
Ok(())
}
fn labels_of(machine: &dyn Board) -> Vec<String> {
if machine.target_names().len() == 1 {
return vec![String::new()];
}
machine
.target_names()
.into_iter()
.map(|name| format!("target={name} "))
.collect()
}
fn images_of(machine: &dyn Board, run: &Run) -> anyhow::Result<Vec<PathBuf>> {
let named = by_target(machine, &run.firmware, |value| Ok(PathBuf::from(value)))?;
match (&run.image, machine.target_names().len()) {
(Some(image), 1) if run.firmware.is_empty() => Ok(vec![image.clone()]),
(Some(_), 1) => bail!("give {} one firmware image, not two", machine.name()),
(Some(_), _) => bail!(
"{} has the MCUs {}, so name each image as MCU=PATH",
machine.name(),
names_of(machine)
),
(None, _) => named
.into_iter()
.zip(machine.target_names())
.map(|(image, target)| image.with_context(|| format!("{target} has no firmware image")))
.collect(),
}
}
fn debugger_ports_of(machine: &dyn Board, run: &Run) -> anyhow::Result<Vec<Option<u16>>> {
if let [only] = &run.gdb[..]
&& machine.target_names().len() == 1
&& !only.contains('=')
{
let port = only.parse().context("the debugger port is not a port")?;
return Ok(vec![Some(port)]);
}
by_target(machine, &run.gdb, |value| {
value.parse().context("the debugger port is not a port")
})
}
fn by_target<T>(
machine: &dyn Board,
values: &[String],
parse: impl Fn(&str) -> anyhow::Result<T>,
) -> anyhow::Result<Vec<Option<T>>> {
let mut by_index: Vec<Option<T>> = machine.target_names().iter().map(|_| None).collect();
for value in values {
let (name, rest) = value.split_once('=').with_context(|| {
format!(
"{value} names no MCU; {} has {}",
machine.name(),
names_of(machine)
)
})?;
let target = machine.target(name).with_context(|| {
format!(
"{} has no MCU named {name}; it has {}",
machine.name(),
names_of(machine)
)
})?;
if by_index[target].is_some() {
bail!("{name} is named twice");
}
by_index[target] = Some(parse(rest)?);
}
Ok(by_index)
}
fn names_of(machine: &dyn Board) -> String {
machine.target_names().join(", ")
}
fn write_access(
output: &mut impl Write,
label: &str,
access: &Access,
value: u64,
) -> io::Result<()> {
let kind = match access.kind {
AccessKind::Read => "read ",
AccessKind::Write => "write",
};
writeln!(
output,
"{kind} {label}t={} n={} pc={:#010x} at={:#010x} w={} v={value:#010x}",
access.virtual_time.0,
access.instruction_count,
access.program_counter,
access.address,
access.width.bits(),
)
}
fn stop_reason(reason: StopReason) -> &'static str {
match reason {
StopReason::Deadline => "deadline",
StopReason::Idle => "idle",
}
}
fn parse_virtual_time(text: &str) -> Result<VirtualNanos, String> {
let (digits, scale) = UNITS
.iter()
.find_map(|(unit, scale)| text.strip_suffix(unit).map(|digits| (digits, *scale)))
.ok_or_else(|| format!("{text} has no unit; use ns, us, ms, or s"))?;
let count: u64 = digits
.parse()
.map_err(|error| format!("{digits} is not a whole number: {error}"))?;
let nanos = count
.checked_mul(scale)
.ok_or_else(|| format!("{text} does not fit the virtual clock"))?;
if nanos > NO_DEADLINE.0 {
return Err(format!(
"{text} is longer than the virtual clock runs; the most is {} ns",
NO_DEADLINE.0
));
}
Ok(VirtualNanos(nanos))
}
#[cfg(test)]
mod tests {
use super::*;
use rigless::machine::Target;
use rigless::mcu::Bare;
use rigless::sidecar::{InterruptLine, SidecarCommand};
use rigless::soc::{Bus, Cpu, Region};
const AN_IMAGE: &str = "firmware.elf";
static MEMORY: [Region; 1] = [Region::ram("sram", 0x2000_0000, 0x1000)];
#[derive(Debug)]
struct TestBoard {
name: &'static str,
names: Vec<&'static str>,
mcus: Vec<Bare>,
}
impl TestBoard {
fn of(name: &'static str, names: &[&'static str]) -> Box<dyn Board> {
let mcus = names
.iter()
.map(|_| {
Bare::new(Bus::builder(
Cpu {
name: "a-core",
fpu: false,
interrupt_lines: 1,
},
&MEMORY,
[],
))
.unwrap()
})
.collect();
Box::new(Self {
name,
names: names.to_vec(),
mcus,
})
}
}
impl Board for TestBoard {
fn name(&self) -> &'static str {
self.name
}
fn target_names(&self) -> Vec<&'static str> {
self.names.clone()
}
fn targets(&mut self) -> Vec<Target<'_>> {
let Self { names, mcus, .. } = self;
mcus.iter_mut()
.zip(names.iter())
.map(|(mcu, &name)| Target {
name,
mcu,
sidecar: SidecarCommand {
binary: "a-sidecar",
machine: "a-machine",
cpu: "a-core",
properties: Vec::new(),
icount_shift: 0,
aliases: Vec::new(),
},
interrupt_line: InterruptLine::new,
})
.collect()
}
}
fn one_mcu() -> Box<dyn Board> {
TestBoard::of("one", &["only"])
}
fn two_mcus() -> Box<dyn Board> {
TestBoard::of("two", &["first", "second"])
}
fn run_of(image: Option<&str>, firmware: &[&str], gdb: &[&str]) -> Run {
Run {
coverage: None,
machine: "test".to_owned(),
until: Some(VirtualNanos(1)),
wait_for_debugger: false,
gdb: gdb.iter().map(|value| (*value).to_owned()).collect(),
sidecar: None,
firmware: firmware.iter().map(|value| (*value).to_owned()).collect(),
image: image.map(PathBuf::from),
}
}
#[test]
fn a_machine_with_one_mcu_takes_the_image_on_its_own() {
let images = images_of(&*one_mcu(), &run_of(Some(AN_IMAGE), &[], &[])).unwrap();
assert_eq!(images, [PathBuf::from(AN_IMAGE)]);
let ports =
debugger_ports_of(&*one_mcu(), &run_of(Some(AN_IMAGE), &[], &["3333"])).unwrap();
assert_eq!(ports, [Some(3333)]);
}
#[test]
fn a_machine_with_several_mcus_takes_one_image_each_by_name() {
let run = run_of(None, &["second=two.elf", "first=one.elf"], &["second=3333"]);
assert_eq!(
images_of(&*two_mcus(), &run).unwrap(),
[PathBuf::from("one.elf"), PathBuf::from("two.elf")],
"the images come back in the machine's order, not the command line's"
);
assert_eq!(
debugger_ports_of(&*two_mcus(), &run).unwrap(),
[None, Some(3333)]
);
}
#[test]
fn an_image_that_names_no_mcu_of_the_machine_is_refused() {
let named = |values: &[&str]| {
images_of(&*two_mcus(), &run_of(None, values, &[]))
.unwrap_err()
.to_string()
};
assert!(
named(&["one.elf"]).contains("names no MCU"),
"a bare image names none of several"
);
assert!(named(&["third=one.elf"]).contains("no MCU named third"));
assert!(named(&["first=one.elf"]).contains("second has no firmware image"));
assert!(named(&["first=one.elf", "first=two.elf"]).contains("first is named twice"));
assert!(
images_of(&*two_mcus(), &run_of(Some(AN_IMAGE), &[], &[]))
.unwrap_err()
.to_string()
.contains("name each image as MCU=PATH"),
"the positional image belongs to a machine with one MCU"
);
let named_as_well = format!("{}=other.elf", one_mcu().target_names()[0]);
assert!(
images_of(&*one_mcu(), &run_of(Some(AN_IMAGE), &[&named_as_well], &[]))
.unwrap_err()
.to_string()
.contains("one firmware image, not two")
);
}
#[test]
fn a_duration_parses_with_each_unit_and_refuses_the_rest() {
assert_eq!(parse_virtual_time("48ns"), Ok(VirtualNanos(48)));
assert_eq!(parse_virtual_time("7us"), Ok(VirtualNanos(7_000)));
assert_eq!(parse_virtual_time("2ms"), Ok(VirtualNanos(2_000_000)));
assert_eq!(parse_virtual_time("1s"), Ok(VirtualNanos(NANOS_PER_SECOND)));
assert!(parse_virtual_time("10").is_err());
assert!(parse_virtual_time("1.5ms").is_err());
assert!(parse_virtual_time("99999999999999999999s").is_err());
assert_eq!(
parse_virtual_time(&format!("{}ns", NO_DEADLINE.0)),
Ok(NO_DEADLINE)
);
assert!(
parse_virtual_time(&format!("{}ns", NO_DEADLINE.0 + 1)).is_err(),
"a deadline the engine reads as the past runs nothing, so it is refused here"
);
}
#[test]
fn a_run_without_until_has_no_deadline_and_serves_gdb_without_holding_it() {
let run = Cli::parse_from(["rigless", "run", "--machine", "pixracer", AN_IMAGE]);
let Command::Run(run) = run.command else {
panic!("the run command")
};
assert_eq!(run.until, None, "--until has no default");
assert!(!run.wait_for_debugger);
let held = Cli::parse_from([
"rigless",
"run",
"--machine",
"pixracer",
"--gdb",
"3333",
"--wait-for-debugger",
AN_IMAGE,
]);
let Command::Run(held) = held.command else {
panic!("the run command")
};
assert!(held.wait_for_debugger);
assert!(
Cli::try_parse_from([
"rigless",
"run",
"--machine",
"pixracer",
"--wait-for-debugger",
AN_IMAGE
])
.is_err(),
"waiting needs a port to wait on"
);
}
}