use std::ffi::OsString;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use serde_yaml::{Value, from_reader};
use crate::config::load::{LoadError, load_config};
use crate::config::schema::FileConfig;
use crate::core::cli::{Cli, Command};
use crate::core::config::{ResolveError, ResolvedConfig, discover, resolve_single};
use crate::core::exec::{Cmd, ExecError, Runner};
#[derive(Debug)]
pub enum KazeError {
Io(std::io::Error),
Exec(ExecError),
Load(LoadError),
Resolve(ResolveError),
Msg(String),
}
impl From<std::io::Error> for KazeError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<ExecError> for KazeError {
fn from(value: ExecError) -> Self {
Self::Exec(value)
}
}
impl From<LoadError> for KazeError {
fn from(value: LoadError) -> Self {
Self::Load(value)
}
}
impl From<ResolveError> for KazeError {
fn from(value: ResolveError) -> Self {
Self::Resolve(value)
}
}
impl From<serde_yaml::Error> for KazeError {
fn from(value: serde_yaml::Error) -> Self {
Self::Msg(format!("failed to load yaml: {value:?}"))
}
}
pub fn run(cli: &Cli) -> Result<(), KazeError> {
let ctx = discover(cli.project.as_deref(), cli)
.map_err(|e| KazeError::Msg(format!("project_dir discovery failed: {e:?}")))?;
let command = &cli.command;
let file_cfg = match *command {
Command::Init | Command::Boards => FileConfig::default(),
_ => load_config(&ctx.project_dir)
.map_err(|e| KazeError::Msg(format!("load failed: {e:?}")))?,
};
let rc = resolve_single(cli, &ctx, &file_cfg)
.map_err(|e| KazeError::Msg(format!("resolve failed: {e:?}")))?;
let runner = Runner {
verbose: cli.verbose,
dry_run: cli.dry_run,
};
if cli.preclean
&& matches!(
command,
&Command::Clean
| &Command::Conf(_)
| &Command::Build(_)
| &Command::Run(_)
| &Command::Flash(_)
)
{
clean_dir_safe(&rc.build_dir, &rc.ctx.project_dir, cli.dry_run)?;
}
match *command {
Command::Init => cmd_init(&rc.ctx.project_dir)?,
Command::Profiles => cmd_profiles(&file_cfg),
Command::Clean => cmd_clean_root(&rc.build_root, &rc.ctx.project_dir, cli.dry_run)?,
Command::Conf(ref p) => cmd_conf(runner, &rc, &p.extra)?,
Command::Build(ref p) => cmd_build(runner, &rc, &p.extra)?,
Command::Flash(ref f) => cmd_flash(runner, &rc, &f.phase.extra)?,
Command::Run(ref r) => cmd_run(runner, &rc, r.norebuild, &r.phase.extra)?,
Command::Boards => cmd_west_boards(runner)?,
Command::Runners => cmd_runners(&rc)?,
}
Ok(())
}
fn cmd_init(project_dir: &Path) -> Result<(), KazeError> {
let path = project_dir.join("kaze.toml");
if path.exists() {
return Err(KazeError::Msg("kaze.toml already exists".into()));
}
let template = r#"[project]
board = "nucleo_f767zi"
runner = "openocd"
[build]
root = "build"
"#;
fs::write(&path, template)?;
eprintln!("created {}", path.display());
Ok(())
}
fn cmd_profiles(file_cfg: &FileConfig) {
if file_cfg.profiles.is_empty() {
println!("(no profiles configured)");
return;
}
println!("Configured profiles:");
for (i, name) in file_cfg.profiles.keys().enumerate() {
println!("{i:>2}: {name}");
}
}
fn cmd_clean_root(build_root: &Path, project_dir: &Path, dry_run: bool) -> Result<(), KazeError> {
clean_dir_safe(build_root, project_dir, dry_run)?;
Ok(())
}
fn cmd_conf(runner: Runner, rc: &ResolvedConfig, extra: &[String]) -> Result<(), KazeError> {
fs::create_dir_all(&rc.build_dir)?;
let mut cmd = Cmd::new("cmake")
.arg("-B")
.arg(os(rc.build_dir.as_path()))
.arg("-S")
.arg(os(rc.ctx.project_dir.as_path()))
.arg("-G")
.arg("Ninja")
.arg(format!("-DBOARD={}", rc.board));
if let Some(zb) = rc.zephyr_base.as_ref() {
cmd = cmd.arg(format!("-DZEPHYR_BASE={}", zb.display()));
}
if let Some(rn) = rc.runner.as_ref() {
cmd = cmd.arg(format!("-DBOARD_FLASH_RUNNER={rn}"));
}
cmd = cmd.args(extra.iter().cloned());
runner.run(&cmd)?;
Ok(())
}
fn ensure_configured(runner: Runner, rc: &ResolvedConfig) -> Result<(), KazeError> {
if !is_configured(&rc.build_dir) {
cmd_conf(runner, rc, &rc.args_conf)?;
}
Ok(())
}
fn cmd_build(runner: Runner, rc: &ResolvedConfig, extra: &[String]) -> Result<(), KazeError> {
ensure_configured(runner, rc)?;
let mut cmd = Cmd::new("ninja").arg("-C").arg(os(rc.build_dir.as_path()));
cmd = cmd.args(extra.iter().cloned());
runner.run(&cmd)?;
Ok(())
}
fn ensure_built(runner: Runner, rc: &ResolvedConfig) -> Result<(), KazeError> {
cmd_build(runner, rc, &rc.args_build)?;
Ok(())
}
fn cmd_flash(runner: Runner, rc: &ResolvedConfig, extra: &[String]) -> Result<(), KazeError> {
ensure_built(runner, rc)?;
let mut cmd = Cmd::new("west")
.arg("flash")
.arg("-d")
.arg(os(rc.build_dir.as_path()));
if let Some(rn) = rc.runner.as_ref() {
cmd = cmd.arg("--runner").arg(rn);
}
cmd = cmd.args(extra.iter().cloned());
runner.run(&cmd)?;
Ok(())
}
fn cmd_run(
runner: Runner,
rc: &ResolvedConfig,
norebuild: bool,
extra: &[String],
) -> Result<(), KazeError> {
if !norebuild {
ensure_built(runner, rc)?;
}
let candidates = [
PathBuf::from("zephyr/zephyr.exe"),
PathBuf::from("zephyr/zephyr"),
];
for rel in candidates {
let full = rc.build_dir.join(&rel);
if full.is_file() {
let cmd = Cmd::new(os(&rel))
.cwd(rc.build_dir.clone())
.args(extra.iter().cloned());
runner.run(&cmd)?;
return Ok(());
}
}
let cmd = Cmd::new("ninja")
.arg("-C")
.arg(os(rc.build_dir.as_path()))
.arg("run");
runner.run(&cmd)?;
Ok(())
}
fn cmd_runners(rc: &ResolvedConfig) -> Result<(), KazeError> {
let yaml_path = &rc.build_dir.join("zephyr/runners.yaml");
if !yaml_path.is_file() {
return Err(KazeError::Msg(
"Runner config not found in build dir. Try buidling the project first with `kaze \
build`."
.into(),
));
}
let runners_yml = read_runners_yaml(yaml_path.as_path())?;
let runners = yaml_str_list(&runners_yml, "runners");
let flash_runner = yaml_str(&runners_yml, "flash-runner");
let board = rc.board.clone();
println!("Runners available for `{board}`:\n");
if runners.is_empty() {
println!("None.\n");
} else {
for (i, rn) in runners.iter().enumerate() {
println!("{i:>2}: {rn}");
}
println!();
}
if let Some(rn) = flash_runner {
println!("Configured flash runner: {rn}");
}
Ok(())
}
fn read_runners_yaml(path: &Path) -> Result<Value, KazeError> {
let f = File::open(path)?;
Ok(from_reader(f)?)
}
fn yaml_str(v: &Value, key: &str) -> Option<String> {
v.get(key)?.as_str().map(std::convert::Into::into)
}
fn yaml_str_list(v: &Value, key: &str) -> Vec<String> {
v.get(key)
.and_then(|x| x.as_sequence())
.into_iter()
.flatten()
.filter_map(|x| x.as_str().map(std::convert::Into::into))
.collect()
}
fn cmd_west_boards(runner: Runner) -> Result<(), KazeError> {
let cmd = Cmd::new("west").arg("boards");
runner.run(&cmd)?;
Ok(())
}
fn is_configured(build_dir: &Path) -> bool {
build_dir.join("CMakeCache.txt").is_file()
}
fn clean_dir_safe(dir: &Path, project_dir: &Path, dry_run: bool) -> Result<(), KazeError> {
if !dir.exists() && !dry_run {
return Ok(());
}
let proj = canonical_or(project_dir);
let target = canonical_or(dir);
if !target.starts_with(&proj) {
return Err(KazeError::Msg(format!(
"refusing to delete outside project dir: {}",
target.display()
)));
}
if proj == target {
return Err(KazeError::Msg(format!(
"refusing to delete project_root: {}",
target.display()
)));
}
if dry_run {
println!("Cleaning {}", target.display());
} else {
fs::remove_dir_all(&target)?;
eprintln!("removed {}", target.display());
}
Ok(())
}
fn canonical_or(p: &Path) -> PathBuf {
p.canonicalize().unwrap_or_else(|_| p.to_path_buf())
}
fn os(p: &Path) -> OsString {
p.as_os_str().to_os_string()
}