use crate::run::usage;
use voltaic_driver::diagnose;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
pub(crate) fn build_command(args: &[String]) -> Result<(), String> {
let parsed = parse_build_args(args)?;
let shown = parsed.input.display().to_string();
let source = std::fs::read_to_string(&parsed.input)
.map_err(|e| format!("error: cannot read {}: {}", shown, e))?;
let program = voltaic_parser::parse_source(&source).map_err(|failure| {
voltaic_diag::render(&diagnose(&parsed.input, &source, &failure), parsed.plain)
})?;
let warnings = voltaic_driver::lint(&parsed.input, &source, Some(&program));
if !warnings.is_empty() {
let rendered = voltaic_diag::render(&warnings, parsed.plain);
eprint!("{}", rendered);
}
let emitted = voltaic_emit::emit_with_map(&program, &source, &shown);
let out_shown = parsed.output.display().to_string();
std::fs::write(&parsed.output, &emitted.0)
.map_err(|e| format!("error: cannot write {}: {}", out_shown, e))?;
let sidecar = map_path(&parsed.output);
let sidecar_shown = sidecar.display().to_string();
std::fs::write(&sidecar, voltaic_diag::render_map(&emitted.1))
.map_err(|e| format!("error: cannot write {}: {}", sidecar_shown, e))?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BuildArgs {
pub(crate) input: PathBuf,
pub(crate) output: PathBuf,
pub(crate) plain: bool,
}
pub(crate) fn default_output(input: &Path) -> PathBuf {
if input.extension().is_some_and(|e| e == "sdx") {
return input.with_extension("xrs");
}
let mut name = input.as_os_str().to_os_string();
name.push(".xrs");
PathBuf::from(name)
}
pub(crate) fn map_path(output: &Path) -> PathBuf {
let mut name = output
.file_name()
.map_or_else(OsString::new, OsString::from);
name.push(".map");
output.with_file_name(name)
}
pub(crate) fn parse_build_args(args: &[String]) -> Result<BuildArgs, String> {
let mut input: Option<PathBuf> = None;
let mut output: Option<PathBuf> = None;
let mut plain = false;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-o" | "--output" => {
let path = args
.get(i + 1)
.ok_or_else(|| "error: -o requires a path".to_string())?;
output = Some(PathBuf::from(path));
i += 2;
}
"--plain" => {
plain = true;
i += 1;
}
flag if flag.starts_with('-') => {
let help = usage();
return Err(format!("error: unknown flag `{}`\n\n{}", flag, help));
}
path => {
if input.is_some() {
return Err(format!("error: unexpected second input `{}`", path));
}
input = Some(PathBuf::from(path));
i += 1;
}
};
}
let Some(resolved) = input else {
let help = usage();
return Err(format!("error: no input file\n\n{}", help));
};
let out = output.unwrap_or_else(|| default_output(&resolved));
Ok(BuildArgs {
input: resolved,
output: out,
plain,
})
}
#[cfg(test)]
mod __rdx_tests {
use super::*;
#[test]
fn maps_sdx_to_xrs() {
assert_eq!(
default_output(Path::new("main.sdx")),
PathBuf::from("main.xrs")
);
assert_eq!(
default_output(Path::new("a/b/main.sdx")),
PathBuf::from("a/b/main.xrs")
);
}
#[test]
fn appends_to_an_unexpected_extension_rather_than_replacing_it() {
assert_eq!(
default_output(Path::new("notes.txt")),
PathBuf::from("notes.txt.xrs")
);
}
#[test]
fn appends_rather_than_replacing() {
assert_eq!(map_path(Path::new("main.xrs")), Path::new("main.xrs.map"));
}
#[test]
fn keeps_the_directory() {
assert_eq!(
map_path(Path::new("/tmp/out/main.xrs")),
Path::new("/tmp/out/main.xrs.map")
);
}
#[test]
fn handles_a_name_with_no_extension() {
assert_eq!(map_path(Path::new("out/main")), Path::new("out/main.map"));
}
#[test]
fn rejects_an_unknown_flag() {
let args = ["--nope".to_string()];
let err = parse_build_args(&args).expect_err("should fail");
assert!(err.contains("unknown flag"), "{}", err);
}
#[test]
fn rejects_a_missing_input() {
let empty: Vec<String> = Vec::new();
let err = parse_build_args(&empty).expect_err("should fail");
assert!(err.contains("no input file"), "{}", err);
}
#[test]
fn rejects_a_second_input() {
let args = ["a.sdx".to_string(), "b.sdx".to_string()];
let err = parse_build_args(&args).expect_err("should fail");
assert!(err.contains("second input"), "{}", err);
}
#[test]
fn rejects_a_dangling_output_flag() {
let args = ["-o".to_string()];
let err = parse_build_args(&args).expect_err("should fail");
assert!(err.contains("requires a path"), "{}", err);
}
#[test]
fn accepts_the_plain_flag_before_or_after_the_input() {
let before = ["--plain".to_string(), "a.sdx".to_string()];
let after = ["a.sdx".to_string(), "--plain".to_string()];
for args in [before, after] {
let parsed = parse_build_args(&args).expect("should parse");
assert!(parsed.plain);
assert_eq!(parsed.input.to_str(), Some("a.sdx"));
}
}
#[test]
fn defaults_to_graphical_rendering() {
let args = ["a.sdx".to_string()];
let parsed = parse_build_args(&args).expect("should parse");
assert!(!parsed.plain);
}
}