use std::io::{self, IsTerminal, Write};
use std::path::PathBuf;
use std::sync::Mutex;
use clap::Args;
use consortium_builder::pipeline::{
self, PipelineConfig, bitbake::BitbakeAction, tee::TeeToolchain,
};
use consortium_builder::{BuildReporter, BuildStep};
#[derive(Args)]
pub struct BuildArgs {
#[arg(long, default_value = "Consortium.toml")]
manifest: PathBuf,
#[arg(long, default_value = "dist")]
dist: PathBuf,
#[arg(long)]
release: bool,
#[arg(long)]
skip_tee_sign: bool,
#[arg(long, value_name = "DIR")]
bitbake_files_dir: Option<PathBuf>,
#[arg(long, value_name = "RECIPE")]
bitbake_recipe: Option<String>,
#[arg(long, value_name = "DIR")]
bitbake_build_dir: Option<PathBuf>,
#[arg(long)]
bitbake_dry_run: bool,
}
fn resolve_bitbake(args: &BuildArgs) -> Result<Vec<BitbakeAction>, String> {
let mut actions = Vec::new();
if let Some(dir) = &args.bitbake_files_dir {
actions.push(BitbakeAction::CopyIntoRecipe {
recipe_files_dir: dir.clone(),
});
}
match (&args.bitbake_recipe, &args.bitbake_build_dir) {
(Some(recipe), Some(build_dir)) => actions.push(BitbakeAction::InvokeBitbake {
build_dir: build_dir.clone(),
recipe: recipe.clone(),
dry_run: args.bitbake_dry_run,
}),
(Some(_), None) => return Err("--bitbake-recipe requires --bitbake-build-dir".to_string()),
(None, Some(_)) => return Err("--bitbake-build-dir requires --bitbake-recipe".to_string()),
(None, None) if args.bitbake_dry_run => {
return Err("--bitbake-dry-run requires --bitbake-recipe".to_string());
}
(None, None) => {}
}
Ok(actions)
}
#[derive(Clone, Copy)]
enum StepStatus {
Running,
Done,
Failed,
}
struct CurrentStep {
index: usize,
total: usize,
name: String,
detail: Option<String>,
status: StepStatus,
}
struct ReporterState {
current: Option<CurrentStep>,
pending_output: String,
}
struct StickyReporter {
state: Mutex<ReporterState>,
interactive: bool,
color: bool,
}
impl StickyReporter {
fn new() -> Self {
let interactive = io::stderr().is_terminal();
let color = interactive
&& std::env::var_os("NO_COLOR").is_none()
&& std::env::var("TERM")
.map(|term| term != "dumb")
.unwrap_or(true);
Self {
state: Mutex::new(ReporterState {
current: None,
pending_output: String::new(),
}),
interactive,
color,
}
}
fn finish_success(&self) {
self.finish("ok", "Build complete", "32;1");
}
fn finish_failure(&self) {
self.finish("x", "Build failed", "31;1");
}
fn finish(&self, mark: &str, message: &str, color: &str) {
let Ok(mut state) = self.state.lock() else {
return;
};
let mut stderr = io::stderr().lock();
if self.interactive {
let _ = write!(stderr, "\r\x1b[2K");
}
self.flush_pending(&mut state, &mut stderr);
state.current = None;
let _ = writeln!(stderr, "{} {}", self.paint(color, mark), message);
let _ = stderr.flush();
}
fn update_step(&self, step: BuildStep<'_>, status: StepStatus) {
let Ok(mut state) = self.state.lock() else {
return;
};
let mut stderr = io::stderr().lock();
if self.interactive {
let _ = write!(stderr, "\r\x1b[2K");
}
self.flush_pending(&mut state, &mut stderr);
state.current = Some(CurrentStep {
index: step.index,
total: step.total,
name: step.name.to_string(),
detail: step.detail.map(str::to_string),
status,
});
if let Some(current) = state.current.as_ref() {
let prompt = self.render_prompt(current);
if self.interactive {
let _ = write!(stderr, "{prompt}");
} else {
let _ = writeln!(stderr, "{prompt}");
}
}
let _ = stderr.flush();
}
fn flush_pending(&self, state: &mut ReporterState, stderr: &mut impl Write) {
if state.pending_output.is_empty() {
return;
}
let pending = std::mem::take(&mut state.pending_output);
let _ = write!(stderr, "{pending}");
if !pending.ends_with('\n') && !pending.ends_with('\r') {
let _ = writeln!(stderr);
}
}
fn drain_complete_output(&self, state: &mut ReporterState) -> String {
let Some(end) = state
.pending_output
.char_indices()
.filter_map(|(idx, ch)| (ch == '\n' || ch == '\r').then_some(idx + ch.len_utf8()))
.next_back()
else {
return String::new();
};
let complete = state.pending_output[..end].to_string();
state.pending_output.replace_range(..end, "");
normalize_line_endings(&complete)
}
fn render_prompt(&self, current: &CurrentStep) -> String {
let (status, color) = match current.status {
StepStatus::Running => ("running", "36;1"),
StepStatus::Done => ("done", "32;1"),
StepStatus::Failed => ("failed", "31;1"),
};
let mut prompt = format!(
"{} {} {} {}",
self.paint("36;1", "> consortium"),
self.paint("33", format!("[{}/{}]", current.index, current.total)),
self.paint(color, status),
self.paint("1", ¤t.name),
);
if let Some(detail) = ¤t.detail {
prompt.push(' ');
prompt.push_str(&self.paint("2", detail));
}
prompt
}
fn paint(&self, color: &str, text: impl AsRef<str>) -> String {
let text = text.as_ref();
if self.color {
format!("\x1b[{color}m{text}\x1b[0m")
} else {
text.to_string()
}
}
}
impl BuildReporter for StickyReporter {
fn step_started(&self, step: BuildStep<'_>) {
self.update_step(step, StepStatus::Running);
}
fn step_succeeded(&self, step: BuildStep<'_>) {
self.update_step(step, StepStatus::Done);
}
fn step_failed(&self, step: BuildStep<'_>) {
self.update_step(step, StepStatus::Failed);
}
fn command_output(&self, output: &str) {
let Ok(mut state) = self.state.lock() else {
return;
};
state.pending_output.push_str(output);
let complete = self.drain_complete_output(&mut state);
if complete.is_empty() {
return;
}
let mut stderr = io::stderr().lock();
if self.interactive {
let _ = write!(stderr, "\r\x1b[2K");
}
let _ = write!(stderr, "{complete}");
if self.interactive
&& let Some(current) = state.current.as_ref()
{
let _ = write!(stderr, "\r\x1b[2K{}", self.render_prompt(current));
}
let _ = stderr.flush();
}
}
fn normalize_line_endings(text: &str) -> String {
let mut normalized = String::with_capacity(text.len());
let mut previous_was_cr = false;
for ch in text.chars() {
match ch {
'\r' => {
normalized.push('\n');
previous_was_cr = true;
}
'\n' if previous_was_cr => {
previous_was_cr = false;
}
_ => {
normalized.push(ch);
previous_was_cr = false;
}
}
}
normalized
}
pub async fn run(args: BuildArgs) -> Result<(), String> {
let session = pipeline::load_session(&args.manifest)
.await
.map_err(|e| e.to_string())?;
let manifest_dir = args
.manifest
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let hmi =
pipeline::derive_hmi_configuration(&session, &manifest_dir).map_err(|e| e.to_string())?;
let tee_toolchain = TeeToolchain::from_env();
let bitbake = resolve_bitbake(&args)?;
let config = PipelineConfig {
manifest_path: args.manifest.clone(),
dist_root: args.dist.clone(),
release: args.release,
hmi,
tee_toolchain,
sign_tee: !args.skip_tee_sign,
bitbake,
};
let plan = pipeline::plan(&session, &config)
.await
.map_err(|e| e.to_string())?;
println!(
"Building {} for chip {}",
args.manifest.display(),
session.database.name
);
for fw in &plan.firmware {
println!(" firmware: {} ({})", fw.core, fw.config.target);
}
if plan.hmi.is_some() {
let source = session
.config
.hmi
.as_ref()
.map(|h| h.source.as_str())
.unwrap_or("-");
println!(" hmi: {source}");
}
if plan.ta.is_some() {
let signed = if args.skip_tee_sign {
" (unsigned: --skip-tee-sign)"
} else {
""
};
println!(" tee: trusted application{signed}");
}
println!(" app: {}", plan.application.config.target);
let reporter = StickyReporter::new();
let dist = match pipeline::run_with_reporter(config, &reporter).await {
Ok(dist) => {
reporter.finish_success();
dist
}
Err(error) => {
reporter.finish_failure();
return Err(error.to_string());
}
};
println!("Assembled dist tree at {}", dist.display());
Ok(())
}