use std::path::{Path, PathBuf};
use std::process::Command;
use super::CommandRun;
use crate::cli::GlobalArgs;
use crate::envelope::{Envelope, Issue, Project};
use crate::exit::ExitCode;
const ALL_EMIT_MODES: [&str; 4] = ["zephyr-conf", "dts-overlay", "cmake-args", "yocto-conf"];
#[derive(serde::Serialize)]
struct GenerateData {
#[serde(rename = "schemaVersion")]
schema_version: String,
targets: Vec<String>,
written: Vec<String>,
failed: Vec<String>,
}
pub fn run(g: &GlobalArgs) -> CommandRun {
let workspace_root = resolve_workspace_root(g);
let board_path = resolve_board_path(g, &workspace_root);
let project_str = g.project.clone().unwrap_or_else(|| ".".to_string());
let board_yaml_str = g
.board_yaml
.clone()
.unwrap_or_else(|| "board.yaml".to_string());
let project = Project {
root: Some(project_str),
board_yaml: Some(board_yaml_str),
};
if !board_path.exists() {
return failure(
g,
project,
ExitCode::ValidationFailure,
"board-yaml-missing",
"board.yaml path could not be resolved or the file does not exist.",
empty_data(),
vec!["generate: board.yaml path is unresolved or missing.".to_string()],
);
}
let Some(sdk_root) = resolve_sdk_root(g, &workspace_root) else {
return failure(
g,
project,
ExitCode::ValidationFailure,
"sdk-root-unresolved",
"alp-sdk root is unresolved. Use --sdk-root or place project near alp-sdk checkout.",
empty_data(),
vec!["generate: alp-sdk root is unresolved.".to_string()],
);
};
let targets = match resolve_generate_targets(g.target.as_deref(), g.all) {
Ok(t) => t,
Err(message) => {
let copy = message.clone();
return failure(
g,
project,
ExitCode::InternalFailure,
"internal-failure",
&message,
empty_data(),
vec!["generate: internal failure".to_string(), copy],
);
}
};
let python = default_python_binary();
let script_path = sdk_root.join("scripts").join("alp_project.py");
let mut written = Vec::<String>::new();
let mut failed = Vec::<String>::new();
let mut issues = Vec::<Issue>::new();
for emit in &targets {
let output_path = output_path_for_emit(&workspace_root, emit);
let output_str = output_path.to_string_lossy().to_string();
let status = Command::new(python)
.arg(&script_path)
.arg("--input")
.arg(&board_path)
.arg("--emit")
.arg(emit)
.arg("--output")
.arg(&output_str)
.output();
match status {
Ok(out) if out.status.success() => {
written.push(relative_or_full(&workspace_root, &output_path));
}
Ok(out) => {
failed.push((*emit).to_string());
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
issues.push(Issue {
code: "generate.emit-failed".to_string(),
severity: "error".to_string(),
message: if stderr.is_empty() {
format!("Generation failed for target '{emit}'.")
} else {
stderr
},
});
}
Err(err) => {
failed.push((*emit).to_string());
issues.push(Issue {
code: "generate.emit-failed".to_string(),
severity: "error".to_string(),
message: format!("Generation failed for target '{emit}': {err}"),
});
}
}
}
let exit = if failed.is_empty() {
ExitCode::Success
} else {
ExitCode::WriteFailure
};
let data = GenerateData {
schema_version: "1".to_string(),
targets: targets.iter().map(|s| (*s).to_string()).collect(),
written,
failed,
};
let text = if g.is_json() {
Vec::new()
} else {
generate_text_lines(g, &data)
};
let json = g
.is_json()
.then(|| Envelope::new("generate", project, data, issues, exit.code()).to_json());
CommandRun { exit, text, json }
}
fn resolve_workspace_root(g: &GlobalArgs) -> PathBuf {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
match &g.project {
Some(project) => cwd.join(project),
None => cwd,
}
}
fn resolve_board_path(g: &GlobalArgs, workspace_root: &Path) -> PathBuf {
if let Some(board) = &g.board_yaml {
let board_path = PathBuf::from(board);
if board_path.is_absolute() {
return board_path;
}
return workspace_root.join(board_path);
}
workspace_root.join("board.yaml")
}
fn resolve_sdk_root(g: &GlobalArgs, workspace_root: &Path) -> Option<PathBuf> {
if let Some(root) = &g.sdk_root {
let candidate = PathBuf::from(root);
if has_loader_script(&candidate) {
return Some(candidate);
}
return None;
}
let parent = workspace_root.parent().map(Path::to_path_buf);
let candidates = [
workspace_root.to_path_buf(),
parent
.as_ref()
.map(|p| p.join("alp-sdk"))
.unwrap_or_else(|| PathBuf::from("alp-sdk")),
parent
.as_ref()
.map(|p| p.join("alp-sdk-upstream"))
.unwrap_or_else(|| PathBuf::from("alp-sdk-upstream")),
];
candidates.into_iter().find(|c| has_loader_script(c))
}
fn has_loader_script(root: &Path) -> bool {
root.join("scripts").join("alp_project.py").exists()
}
fn resolve_generate_targets(target: Option<&str>, all: bool) -> Result<Vec<&'static str>, String> {
if all || target.is_none() {
return Ok(ALL_EMIT_MODES.to_vec());
}
let target = target.unwrap_or_default();
if let Some(mode) = ALL_EMIT_MODES.iter().copied().find(|mode| *mode == target) {
return Ok(vec![mode]);
}
Err(format!("Unsupported generate target '{target}'."))
}
fn output_path_for_emit(workspace_root: &Path, emit: &str) -> PathBuf {
let file_name = match emit {
"zephyr-conf" => "alp.conf",
"dts-overlay" => "alp.overlay",
"cmake-args" => "alp-cmake-args.txt",
"yocto-conf" => "alp-yocto.conf",
_ => "alp.out",
};
workspace_root
.join("build")
.join("generated")
.join(file_name)
}
fn relative_or_full(workspace_root: &Path, output_path: &Path) -> String {
output_path
.strip_prefix(workspace_root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| output_path.to_string_lossy().to_string())
}
fn default_python_binary() -> &'static str {
if cfg!(target_os = "windows") {
"python"
} else {
"python3"
}
}
fn generate_text_lines(g: &GlobalArgs, data: &GenerateData) -> Vec<String> {
let mut lines = Vec::<String>::new();
if data.failed.is_empty() {
lines.push(format!(
"generate: wrote {}/{} targets",
data.written.len(),
data.targets.len()
));
} else {
lines.push(format!(
"generate: wrote {}/{}; failed: {}",
data.written.len(),
data.targets.len(),
data.failed.join(", ")
));
}
if g.verbose {
for target in &data.targets {
lines.push(format!("target: {target}"));
}
}
lines
}
fn empty_data() -> GenerateData {
GenerateData {
schema_version: "1".to_string(),
targets: Vec::new(),
written: Vec::new(),
failed: Vec::new(),
}
}
fn failure(
g: &GlobalArgs,
project: Project,
exit: ExitCode,
code: &str,
message: &str,
data: GenerateData,
text_lines: Vec<String>,
) -> CommandRun {
let issues = vec![Issue {
code: format!("generate.{code}"),
severity: "error".to_string(),
message: message.to_string(),
}];
let text = if g.is_json() { Vec::new() } else { text_lines };
let json = g
.is_json()
.then(|| Envelope::new("generate", project, data, issues, exit.code()).to_json());
CommandRun { exit, text, json }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn target_resolution_defaults_to_all() {
let resolved = resolve_generate_targets(None, false).unwrap();
assert_eq!(resolved, ALL_EMIT_MODES.to_vec());
}
#[test]
fn target_resolution_accepts_single_target() {
let resolved = resolve_generate_targets(Some("cmake-args"), false).unwrap();
assert_eq!(resolved, vec!["cmake-args"]);
}
#[test]
fn target_resolution_rejects_unknown_target() {
let err = resolve_generate_targets(Some("unknown"), false).unwrap_err();
assert!(err.contains("Unsupported generate target"));
}
}