use std::path::Path;
use std::process::Command;
use super::Cause;
use super::typegen::{self, TypegenError};
use super::uag_source;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stage {
Graph,
Typegen,
Cargo,
Vite,
}
impl Stage {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Graph => "graph",
Self::Typegen => "typegen",
Self::Cargo => "cargo",
Self::Vite => "vite",
}
}
}
impl std::fmt::Display for Stage {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
pub fn run() -> Result<(), BuildError> {
let cwd = std::env::current_dir().map_err(|source| BuildError::Cwd { source })?;
println!("[1/4] graph");
let loaded = uag_source::load(&cwd).map_err(|source| BuildError::Source {
source: Box::new(source),
})?;
println!(" read from {}", loaded.source);
println!("[2/4] typegen");
let written = typegen::emit(&loaded.artifact, &loaded.root)
.map_err(|source| BuildError::Typegen { source })?;
println!(
" {} file{} in {}/",
written.len(),
if written.len() == 1 { "" } else { "s" },
typegen::OUTPUT_DIR
);
println!("[3/4] cargo build --release");
stage(Stage::Cargo, &loaded.root, "cargo", &["build", "--release"])?;
println!("[4/4] npm run build");
stage(Stage::Vite, &loaded.root, "npm", &["run", "build"])?;
println!("Build finished.");
Ok(())
}
fn spawnable(program: &str) -> Command {
if cfg!(windows) {
let mut command = Command::new("cmd");
command.args(["/C", program]);
command
} else {
Command::new(program)
}
}
fn stage(stage: Stage, root: &Path, program: &str, args: &[&str]) -> Result<(), BuildError> {
let status = spawnable(program)
.args(args)
.current_dir(root)
.stdin(std::process::Stdio::null())
.status()
.map_err(|source| BuildError::Spawn {
stage,
program: program.to_owned(),
source,
})?;
if status.success() {
Ok(())
} else {
Err(BuildError::Failed { stage, status })
}
}
#[derive(Debug)]
pub enum BuildError {
Cwd {
source: std::io::Error,
},
Source {
source: Cause,
},
Typegen {
source: TypegenError,
},
Spawn {
stage: Stage,
program: String,
source: std::io::Error,
},
Failed {
stage: Stage,
status: std::process::ExitStatus,
},
}
impl BuildError {
#[must_use]
pub fn stage(&self) -> Stage {
match self {
Self::Cwd { .. } | Self::Source { .. } => Stage::Graph,
Self::Typegen { .. } => Stage::Typegen,
Self::Spawn { stage, .. } | Self::Failed { stage, .. } => *stage,
}
}
}
impl std::fmt::Display for BuildError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "build failed at the {} stage: ", self.stage())?;
match self {
Self::Cwd { source } => {
write!(formatter, "could not read the working directory: {source}")
}
Self::Source { source } => write!(formatter, "{source}"),
Self::Typegen { source } => write!(formatter, "{source}"),
Self::Spawn {
program, source, ..
} => write!(
formatter,
"could not run `{program}`: {source}. It has to be on PATH for this stage."
),
Self::Failed { status, .. } => write!(
formatter,
"the command exited with {status}. Its output is above."
),
}
}
}
impl std::error::Error for BuildError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Cwd { source } | Self::Spawn { source, .. } => Some(source),
Self::Source { source } => Some(source.as_ref()),
Self::Typegen { source } => Some(source),
Self::Failed { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_error_names_the_stage_it_failed_at() {
let spawn = BuildError::Spawn {
stage: Stage::Vite,
program: String::from("npm"),
source: std::io::Error::other("not found"),
};
assert_eq!(spawn.stage(), Stage::Vite);
let message = spawn.to_string();
assert!(message.contains("at the vite stage"), "{message}");
assert!(message.contains("npm"), "{message}");
}
#[test]
fn a_graph_failure_is_reported_as_the_first_stage_rather_than_as_the_build() {
let error = BuildError::Cwd {
source: std::io::Error::other("gone"),
};
assert_eq!(error.stage(), Stage::Graph);
assert!(error.to_string().contains("at the graph stage"));
}
#[test]
fn the_stages_are_named_in_the_order_they_run() {
let names: Vec<&str> = [Stage::Graph, Stage::Typegen, Stage::Cargo, Stage::Vite]
.iter()
.map(|stage| stage.as_str())
.collect();
assert_eq!(names, ["graph", "typegen", "cargo", "vite"]);
}
}