use anyhow::{anyhow, Context, Result};
use std::path::Path;
use toml::Value;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Config {
pub build_command: Vec<String>,
pub run_command: Vec<String>,
pub run_args: Option<Vec<String>>,
pub test_args: Option<Vec<String>>,
pub test_timeout: u32,
pub test_success_exit_code: Option<i32>,
pub test_no_reboot: bool,
}
pub fn read_config(manifest_path: &Path) -> Result<Config> {
read_config_inner(manifest_path).context("Failed to read bootimage configuration")
}
fn read_config_inner(manifest_path: &Path) -> Result<Config> {
use std::{fs::File, io::Read};
let cargo_toml: Value = {
let mut content = String::new();
File::open(manifest_path)
.context("Failed to open Cargo.toml")?
.read_to_string(&mut content)
.context("Failed to read Cargo.toml")?;
content
.parse::<Value>()
.context("Failed to parse Cargo.toml")?
};
let metadata = cargo_toml
.get("package")
.and_then(|table| table.get("metadata"))
.and_then(|table| table.get("bootimage"));
let metadata = match metadata {
None => {
return Ok(ConfigBuilder::default().into());
}
Some(metadata) => metadata
.as_table()
.ok_or_else(|| anyhow!("Bootimage configuration invalid: {:?}", metadata))?,
};
let mut config = ConfigBuilder::default();
for (key, value) in metadata {
match (key.as_str(), value.clone()) {
("test-timeout", Value::Integer(timeout)) if timeout.is_negative() => {
return Err(anyhow!("test-timeout must not be negative"))
}
("test-timeout", Value::Integer(timeout)) => {
config.test_timeout = Some(timeout as u32);
}
("test-success-exit-code", Value::Integer(exit_code)) => {
config.test_success_exit_code = Some(exit_code as i32);
}
("build-command", Value::Array(array)) => {
config.build_command = Some(parse_string_array(array, "build-command")?);
}
("run-command", Value::Array(array)) => {
config.run_command = Some(parse_string_array(array, "run-command")?);
}
("run-args", Value::Array(array)) => {
config.run_args = Some(parse_string_array(array, "run-args")?);
}
("test-args", Value::Array(array)) => {
config.test_args = Some(parse_string_array(array, "test-args")?);
}
("test-no-reboot", Value::Boolean(no_reboot)) => {
config.test_no_reboot = Some(no_reboot);
}
(key, value) => {
return Err(anyhow!(
"unexpected `package.metadata.bootimage` \
key `{}` with value `{}`",
key,
value
))
}
}
}
Ok(config.into())
}
fn parse_string_array(array: Vec<Value>, prop_name: &str) -> Result<Vec<String>> {
let mut parsed = Vec::new();
for value in array {
match value {
Value::String(s) => parsed.push(s),
_ => return Err(anyhow!("{} must be a list of strings", prop_name)),
}
}
Ok(parsed)
}
#[derive(Default)]
struct ConfigBuilder {
build_command: Option<Vec<String>>,
run_command: Option<Vec<String>>,
run_args: Option<Vec<String>>,
test_args: Option<Vec<String>>,
test_timeout: Option<u32>,
test_success_exit_code: Option<i32>,
test_no_reboot: Option<bool>,
}
impl Into<Config> for ConfigBuilder {
fn into(self) -> Config {
Config {
build_command: self.build_command.unwrap_or_else(|| vec!["build".into()]),
run_command: self.run_command.unwrap_or_else(|| {
vec![
"qemu-system-x86_64".into(),
"-drive".into(),
"format=raw,file={}".into(),
]
}),
run_args: self.run_args,
test_args: self.test_args,
test_timeout: self.test_timeout.unwrap_or(60 * 5),
test_success_exit_code: self.test_success_exit_code,
test_no_reboot: self.test_no_reboot.unwrap_or(true),
}
}
}