use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::process::Command;
use super::Compiler;
use crate::compile::env::{DbgConfig, HmiConfig, SharedMemConfig};
use crate::compile::utils::{
clean_path, elf_path, inject_dbg, inject_hmi, inject_shared_mem, resolve_cargo, resolve_rustc,
run_command, verify_elf,
};
use crate::progress::BuildReporter;
#[derive(Error, Debug)]
pub enum ApplicationCompileError {
#[error("Failed to resolve sysroot: {0}")]
SysrootError(String),
#[error("Failed to resolve rustup tool: {0}")]
RustupError(String),
#[error("Cargo build failed: {0}")]
BuildFailed(String),
#[error("ELF not found at expected path: {0:?}")]
ElfNotFound(PathBuf),
#[error("{0}")]
UtilityError(String),
}
struct CrossCompiler {
cc: Option<String>,
cxx: Option<String>,
}
impl CrossCompiler {
fn detect() -> Self {
if let Ok(cc) = std::env::var("CC") {
let cxx = std::env::var("CXX").unwrap_or_else(|_| cc.clone());
return CrossCompiler {
cc: Some(cc),
cxx: Some(cxx),
};
}
CrossCompiler {
cc: None,
cxx: None,
}
}
}
struct SdkEnv {
sysroot: String,
cross: CrossCompiler,
arch_flags: String,
native_sysroot: String,
}
impl SdkEnv {
fn resolve(sysroot_override: Option<&Path>) -> Result<Self, ApplicationCompileError> {
let sysroot = match sysroot_override {
Some(path) => path.to_string_lossy().into_owned(),
None => std::env::var("SDKTARGETSYSROOT")
.or_else(|_| std::env::var("OECORE_TARGET_SYSROOT"))
.map_err(|_| {
ApplicationCompileError::SysrootError(
"Could not determine target sysroot. Set the [profile] sysroot in \
Consortium.toml, export SDKTARGETSYSROOT, or source a Yocto \
environment-setup script."
.to_string(),
)
})?,
};
if !PathBuf::from(&sysroot).exists() {
return Err(ApplicationCompileError::SysrootError(format!(
"Target sysroot does not exist: {sysroot}"
)));
}
let native_sysroot = std::env::var("OECORE_NATIVE_SYSROOT").unwrap_or_default();
let arch_flags = std::env::var("OECORE_TUNE_CCARGS")
.unwrap_or_else(|_| "-mcpu=cortex-a35+crc -mbranch-protection=standard".to_string());
Ok(SdkEnv {
sysroot,
cross: CrossCompiler::detect(),
arch_flags,
native_sysroot,
})
}
fn rustflags(&self) -> String {
let arch_link_args: String = self
.arch_flags
.split_whitespace()
.map(|f| format!(" -C link-arg={f}"))
.collect();
match &self.cross.cc {
Some(cc) => format!(
"-C linker={cc} -C link-arg=--sysroot={sysroot}{arch_link_args}",
sysroot = self.sysroot,
),
None => format!(
"-C link-arg=--sysroot={sysroot}{arch_link_args}",
sysroot = self.sysroot,
),
}
}
}
#[derive(Clone)]
pub struct ApplicationConfiguration {
pub path: PathBuf,
pub target: String,
pub release: bool,
pub hmi: Option<HmiConfig>,
pub ipc: Option<SharedMemConfig>,
pub dbg: Option<DbgConfig>,
pub sysroot: Option<PathBuf>,
}
pub struct ApplicationCompiler;
impl ApplicationCompiler {
pub async fn build_with_reporter(
config: ApplicationConfiguration,
reporter: Option<&dyn BuildReporter>,
) -> Result<PathBuf, ApplicationCompileError> {
let sdk = SdkEnv::resolve(config.sysroot.as_deref())?;
let rustc = resolve_rustc()
.await
.map_err(ApplicationCompileError::RustupError)?;
let cargo = resolve_cargo()
.await
.map_err(ApplicationCompileError::RustupError)?;
let rustflags = sdk.rustflags();
let sanitized_path = clean_path(&sdk.native_sysroot);
let mut cmd = Command::new(&cargo);
cmd.arg("build")
.arg("--target")
.arg(&config.target)
.current_dir(&config.path)
.env("PATH", &sanitized_path)
.env("RUSTC", &rustc)
.env("RUSTFLAGS", &rustflags)
.env("SYSROOT", &sdk.sysroot)
.env("PKG_CONFIG_SYSROOT_DIR", &sdk.sysroot)
.env(
"PKG_CONFIG_PATH",
format!(
"{s}/usr/lib/pkgconfig:{s}/usr/share/pkgconfig",
s = sdk.sysroot,
),
);
if let Some(ref cc) = sdk.cross.cc {
cmd.arg("--config")
.arg(format!("{}.linker={cc}", config.target));
cmd.env("CC", format!("{cc} --sysroot={}", sdk.sysroot));
}
if let Some(ref cxx) = sdk.cross.cxx {
cmd.env("CXX", format!("{cxx} --sysroot={}", sdk.sysroot));
}
inject_hmi(&mut cmd, config.hmi.as_ref());
inject_shared_mem(&mut cmd, "IPC", config.ipc.as_ref());
inject_dbg(&mut cmd, config.dbg.as_ref());
if config.release {
cmd.arg("--release");
}
let status = run_command(&mut cmd, reporter)
.await
.map_err(|e| ApplicationCompileError::BuildFailed(e.to_string()))?;
if !status.success() {
return Err(ApplicationCompileError::BuildFailed(format!(
"cargo build failed with status: {status}"
)));
}
let elf = Self::output_path(&config).await?;
if !elf.exists() {
return Err(ApplicationCompileError::ElfNotFound(elf));
}
verify_elf(&elf)
.await
.map_err(ApplicationCompileError::UtilityError)?;
Ok(elf)
}
}
impl Compiler for ApplicationCompiler {
type Configuration = ApplicationConfiguration;
type Error = ApplicationCompileError;
type Output = PathBuf;
async fn build(config: Self::Configuration) -> Result<Self::Output, Self::Error> {
Self::build_with_reporter(config, None).await
}
async fn output_path(config: &Self::Configuration) -> Result<PathBuf, Self::Error> {
elf_path(&config.path, &config.target, config.release)
.await
.map_err(ApplicationCompileError::UtilityError)
}
}
pub struct ApplicationBuilder {
pub path: PathBuf,
pub release: bool,
pub target: String,
pub hmi: Option<HmiConfig>,
pub ipc: Option<SharedMemConfig>,
pub dbg: Option<DbgConfig>,
pub sysroot: Option<PathBuf>,
}
impl ApplicationBuilder {
pub async fn build(&self) -> Result<PathBuf, String> {
ApplicationCompiler::build(ApplicationConfiguration {
path: self.path.clone(),
target: self.target.clone(),
release: self.release,
hmi: self.hmi.clone(),
ipc: self.ipc,
dbg: self.dbg,
sysroot: self.sysroot.clone(),
})
.await
.map_err(|e| e.to_string())
}
pub async fn elf_path(&self) -> Result<PathBuf, String> {
ApplicationCompiler::output_path(&ApplicationConfiguration {
path: self.path.clone(),
target: self.target.clone(),
release: self.release,
hmi: self.hmi.clone(),
ipc: self.ipc,
dbg: self.dbg,
sysroot: self.sysroot.clone(),
})
.await
.map_err(|e| e.to_string())
}
}