use consortium_cfg::components::remoteproc::RemoteprocMemoryLayout;
use object::{Object, ObjectSection, ObjectSegment};
use std::path::{Path, PathBuf};
use thiserror::Error;
use tokio::process::Command;
use super::Compiler;
use crate::compile::env::CstiWorkspace;
use crate::compile::utils::{elf_path, inject_csti, resolve_cargo, run_command, verify_elf};
use crate::progress::BuildReporter;
#[derive(Error, Debug)]
pub enum FirmwareCompileError {
#[error("Failed to resolve cargo: {0}")]
CargoResolveError(String),
#[error("Cargo failed: {0}")]
BuildFailed(String),
#[error("ELF not found at expected path: {0:?}")]
ElfNotFound(PathBuf),
#[error("copying firmware ELF to {0:?}: {1}")]
Copy(PathBuf, std::io::Error),
#[error("objcopy post-processing failed: {0}")]
Objcopy(String),
#[error("remoteproc ELF validation failed: {0}")]
ElfLayout(String),
#[error("{0}")]
UtilityError(String),
}
#[derive(Clone, Debug)]
pub struct FirmwareArtifacts {
pub original: PathBuf,
pub deploy: PathBuf,
}
pub fn original_elf_path(raw: &Path) -> PathBuf {
sibling(raw, "_original.elf")
}
pub fn deploy_elf_path(raw: &Path) -> PathBuf {
sibling(raw, ".elf")
}
fn sibling(raw: &Path, suffix: &str) -> PathBuf {
let stem = raw
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("firmware");
raw.with_file_name(format!("{stem}{suffix}"))
}
fn resolve_objcopy() -> String {
std::env::var("CONSORTIUM_OBJCOPY").unwrap_or_else(|_| "arm-none-eabi-objcopy".to_string())
}
async fn objcopy_firmware(
src: &Path,
dest: &Path,
layout: Option<&RemoteprocMemoryLayout>,
strict: bool,
reporter: Option<&dyn BuildReporter>,
) -> Result<(), FirmwareCompileError> {
let vector_section = layout.map_or(".isr_vectors", |layout| layout.vector_section.as_str());
if let Some(layout) = layout {
validate_firmware_elf(src, ".vector_table", layout, strict).await?;
}
let objcopy = resolve_objcopy();
let mut cmd = Command::new(&objcopy);
cmd.arg("--rename-section")
.arg(format!(".vector_table={vector_section}"))
.arg("--remove-section")
.arg(".defmt")
.arg(src)
.arg(dest);
let status = run_command(&mut cmd, reporter)
.await
.map_err(|e| FirmwareCompileError::Objcopy(format!("{objcopy}: {e}")))?;
if !status.success() {
return Err(FirmwareCompileError::Objcopy(format!(
"{objcopy} exited with status: {status}"
)));
}
if let Some(layout) = layout {
validate_firmware_elf(dest, vector_section, layout, strict).await?;
}
Ok(())
}
async fn validate_firmware_elf(
path: &Path,
vector_section: &str,
layout: &RemoteprocMemoryLayout,
strict: bool,
) -> Result<(), FirmwareCompileError> {
let bytes = tokio::fs::read(path)
.await
.map_err(|error| FirmwareCompileError::ElfLayout(format!("{}: {error}", path.display())))?;
let file = object::File::parse(bytes.as_slice())
.map_err(|error| FirmwareCompileError::ElfLayout(format!("{}: {error}", path.display())))?;
let section = file.section_by_name(vector_section).ok_or_else(|| {
FirmwareCompileError::ElfLayout(format!(
"{} has no '{vector_section}' section",
path.display()
))
})?;
if section.size() == 0 {
return Err(FirmwareCompileError::ElfLayout(format!(
"{} section '{vector_section}' is empty",
path.display()
)));
}
if section.address() != layout.vector.window.origin {
if !strict {
consortium_log::warn!(
"{} section '{}' starts at {:#x}, not vector origin {:#x}; continuing (--skip-remoteproc-check)",
path.display(),
vector_section,
section.address(),
layout.vector.window.origin
);
return Ok(());
}
return Err(FirmwareCompileError::ElfLayout(format!(
"{} section '{vector_section}' starts at {:#x}, expected vector origin {:#x}",
path.display(),
section.address(),
layout.vector.window.origin
)));
}
if section.size() > layout.vector.window.length {
return Err(FirmwareCompileError::ElfLayout(format!(
"{} section '{vector_section}' is {:#x} bytes, exceeding vector region '{}' ({:#x} bytes)",
path.display(),
section.size(),
layout.vector.name,
layout.vector.window.length
)));
}
let resource_table = file.section_by_name(".resource_table").ok_or_else(|| {
FirmwareCompileError::ElfLayout(format!(
"{} has no mandatory '.resource_table' section",
path.display()
))
})?;
let resource_window = layout.shmem.window;
if resource_table.size() == 0
|| !resource_window.contains(resource_table.address(), resource_table.size())
{
return Err(FirmwareCompileError::ElfLayout(format!(
"{} '.resource_table' at {:#x}..{:#x} is outside mandatory region '{}'",
path.display(),
resource_table.address(),
resource_table
.address()
.saturating_add(resource_table.size()),
layout.shmem.name
)));
}
for segment in file.segments().filter(|segment| segment.size() != 0) {
let address = segment.address();
let size = segment.size();
if !layout
.load_windows()
.any(|window| window.contains(address, size))
{
return Err(FirmwareCompileError::ElfLayout(format!(
"{} PT_LOAD segment {address:#x}..{:#x} is outside configured remoteproc memory regions",
path.display(),
address.saturating_add(size)
)));
}
}
Ok(())
}
#[derive(Clone)]
pub struct FirmwareConfiguration {
pub target: String,
pub path: PathBuf,
pub release: bool,
pub offline: bool,
pub remoteproc: Option<RemoteprocMemoryLayout>,
pub strict_remoteproc: bool,
pub workspace: Option<CstiWorkspace>,
}
pub struct FirmwareCompiler;
impl FirmwareCompiler {
pub async fn build_with_reporter(
config: FirmwareConfiguration,
reporter: Option<&dyn BuildReporter>,
) -> Result<FirmwareArtifacts, FirmwareCompileError> {
Self::cargo(&config, "build", reporter).await?;
let elf = Self::output_path(&config).await?;
if !elf.exists() {
return Err(FirmwareCompileError::ElfNotFound(elf));
}
verify_elf(&elf)
.await
.map_err(FirmwareCompileError::UtilityError)?;
let original = original_elf_path(&elf);
tokio::fs::copy(&elf, &original)
.await
.map_err(|e| FirmwareCompileError::Copy(original.clone(), e))?;
let deploy = deploy_elf_path(&elf);
objcopy_firmware(
&elf,
&deploy,
config.remoteproc.as_ref(),
config.strict_remoteproc,
reporter,
)
.await?;
Ok(FirmwareArtifacts { original, deploy })
}
pub async fn check_with_reporter(
config: &FirmwareConfiguration,
reporter: Option<&dyn BuildReporter>,
) -> Result<(), FirmwareCompileError> {
Self::cargo(config, "check", reporter).await
}
async fn cargo(
config: &FirmwareConfiguration,
subcommand: &str,
reporter: Option<&dyn BuildReporter>,
) -> Result<(), FirmwareCompileError> {
let cargo = resolve_cargo()
.await
.map_err(FirmwareCompileError::CargoResolveError)?;
let mut cmd = Command::new(&cargo);
cmd.arg(subcommand)
.arg("--target")
.arg(&config.target)
.current_dir(&config.path);
inject_csti(&mut cmd, config.workspace.as_ref());
if config.release {
cmd.arg("--release");
}
if config.offline {
cmd.arg("--offline");
}
let status = run_command(&mut cmd, reporter)
.await
.map_err(|e| FirmwareCompileError::BuildFailed(e.to_string()))?;
if !status.success() {
return Err(FirmwareCompileError::BuildFailed(format!(
"cargo {subcommand} failed with status: {status}"
)));
}
Ok(())
}
}
impl Compiler for FirmwareCompiler {
type Configuration = FirmwareConfiguration;
type Error = FirmwareCompileError;
type Output = FirmwareArtifacts;
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.workspace.as_ref(),
&config.target,
config.release,
)
.await
.map_err(FirmwareCompileError::UtilityError)
}
}
pub struct FirmwareBuilder {
pub target: String,
pub path: PathBuf,
pub release: bool,
}
impl FirmwareBuilder {
pub async fn build(&self) -> Result<PathBuf, String> {
FirmwareCompiler::build(FirmwareConfiguration {
target: self.target.clone(),
path: self.path.clone(),
release: self.release,
offline: false,
remoteproc: None,
strict_remoteproc: true,
workspace: None,
})
.await
.map(|artifacts| artifacts.deploy)
.map_err(|e| e.to_string())
}
pub async fn elf_path(&self) -> Result<PathBuf, String> {
FirmwareCompiler::output_path(&FirmwareConfiguration {
target: self.target.clone(),
path: self.path.clone(),
release: self.release,
offline: false,
remoteproc: None,
strict_remoteproc: true,
workspace: None,
})
.await
.map_err(|e| e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn artifact_paths_derive_from_raw_cargo_output() {
let raw = Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu");
assert_eq!(
original_elf_path(raw),
Path::new(
"/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu_original.elf"
)
);
assert_eq!(
deploy_elf_path(raw),
Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu.elf")
);
}
#[test]
fn objcopy_resolution_honours_override() {
let prev = std::env::var("CONSORTIUM_OBJCOPY").ok();
unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", "riscv64-unknown-elf-objcopy") };
assert_eq!(resolve_objcopy(), "riscv64-unknown-elf-objcopy");
unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") };
assert_eq!(resolve_objcopy(), "arm-none-eabi-objcopy");
match prev {
Some(v) => unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", v) },
None => unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") },
}
}
}