consortium_builder/compile/
firmware.rs1use consortium_cfg::components::remoteproc::RemoteprocMemoryLayout;
28use object::{Object, ObjectSection, ObjectSegment};
29use std::path::{Path, PathBuf};
30use thiserror::Error;
31use tokio::process::Command;
32
33use super::Compiler;
34use crate::compile::env::{CstiWorkspace, DbgConfig, SharedMemConfig};
35use crate::compile::utils::{
36 elf_path, inject_csti, inject_dbg, inject_shared_mem, resolve_cargo, run_command, verify_elf,
37};
38use crate::progress::BuildReporter;
39
40#[derive(Error, Debug)]
42pub enum FirmwareCompileError {
43 #[error("Failed to resolve cargo: {0}")]
44 CargoResolveError(String),
45
46 #[error("Cargo failed: {0}")]
47 BuildFailed(String),
48
49 #[error("ELF not found at expected path: {0:?}")]
50 ElfNotFound(PathBuf),
51
52 #[error("copying firmware ELF to {0:?}: {1}")]
53 Copy(PathBuf, std::io::Error),
54
55 #[error("objcopy post-processing failed: {0}")]
56 Objcopy(String),
57
58 #[error("remoteproc ELF validation failed: {0}")]
59 ElfLayout(String),
60
61 #[error("{0}")]
62 UtilityError(String),
63}
64
65#[derive(Clone, Debug)]
67pub struct FirmwareArtifacts {
68 pub original: PathBuf,
72 pub deploy: PathBuf,
77}
78
79pub fn original_elf_path(raw: &Path) -> PathBuf {
84 sibling(raw, "_original.elf")
85}
86
87pub fn deploy_elf_path(raw: &Path) -> PathBuf {
90 sibling(raw, ".elf")
91}
92
93fn sibling(raw: &Path, suffix: &str) -> PathBuf {
94 let stem = raw
95 .file_name()
96 .and_then(|n| n.to_str())
97 .unwrap_or("firmware");
98 raw.with_file_name(format!("{stem}{suffix}"))
99}
100
101fn resolve_objcopy() -> String {
103 std::env::var("CONSORTIUM_OBJCOPY").unwrap_or_else(|_| "arm-none-eabi-objcopy".to_string())
104}
105
106async fn objcopy_firmware(
112 src: &Path,
113 dest: &Path,
114 layout: Option<&RemoteprocMemoryLayout>,
115 reporter: Option<&dyn BuildReporter>,
116) -> Result<(), FirmwareCompileError> {
117 let vector_section = layout.map_or(".isr_vectors", |layout| layout.vector_section.as_str());
118 if let Some(layout) = layout {
119 validate_firmware_elf(src, ".vector_table", layout).await?;
120 }
121 let objcopy = resolve_objcopy();
122 let mut cmd = Command::new(&objcopy);
123 cmd.arg("--rename-section")
124 .arg(format!(".vector_table={vector_section}"))
125 .arg("--remove-section")
126 .arg(".defmt")
127 .arg(src)
128 .arg(dest);
129
130 let status = run_command(&mut cmd, reporter)
131 .await
132 .map_err(|e| FirmwareCompileError::Objcopy(format!("{objcopy}: {e}")))?;
133
134 if !status.success() {
135 return Err(FirmwareCompileError::Objcopy(format!(
136 "{objcopy} exited with status: {status}"
137 )));
138 }
139 if let Some(layout) = layout {
140 validate_firmware_elf(dest, vector_section, layout).await?;
141 }
142 Ok(())
143}
144
145async fn validate_firmware_elf(
146 path: &Path,
147 vector_section: &str,
148 layout: &RemoteprocMemoryLayout,
149) -> Result<(), FirmwareCompileError> {
150 let bytes = tokio::fs::read(path)
151 .await
152 .map_err(|error| FirmwareCompileError::ElfLayout(format!("{}: {error}", path.display())))?;
153 let file = object::File::parse(bytes.as_slice())
154 .map_err(|error| FirmwareCompileError::ElfLayout(format!("{}: {error}", path.display())))?;
155 let section = file.section_by_name(vector_section).ok_or_else(|| {
156 FirmwareCompileError::ElfLayout(format!(
157 "{} has no '{vector_section}' section",
158 path.display()
159 ))
160 })?;
161 if section.size() == 0 {
162 return Err(FirmwareCompileError::ElfLayout(format!(
163 "{} section '{vector_section}' is empty",
164 path.display()
165 )));
166 }
167 if section.address() != layout.vector.window.origin {
168 return Err(FirmwareCompileError::ElfLayout(format!(
169 "{} section '{vector_section}' starts at {:#x}, expected vector origin {:#x}",
170 path.display(),
171 section.address(),
172 layout.vector.window.origin
173 )));
174 }
175 if section.size() > layout.vector.window.length {
176 return Err(FirmwareCompileError::ElfLayout(format!(
177 "{} section '{vector_section}' is {:#x} bytes, exceeding vector region '{}' ({:#x} bytes)",
178 path.display(),
179 section.size(),
180 layout.vector.name,
181 layout.vector.window.length
182 )));
183 }
184 let resource_table = file.section_by_name(".resource_table").ok_or_else(|| {
185 FirmwareCompileError::ElfLayout(format!(
186 "{} has no mandatory '.resource_table' section",
187 path.display()
188 ))
189 })?;
190 let resource_window = layout.shmem.window;
191 if resource_table.size() == 0
192 || !resource_window.contains(resource_table.address(), resource_table.size())
193 {
194 return Err(FirmwareCompileError::ElfLayout(format!(
195 "{} '.resource_table' at {:#x}..{:#x} is outside mandatory region '{}'",
196 path.display(),
197 resource_table.address(),
198 resource_table
199 .address()
200 .saturating_add(resource_table.size()),
201 layout.shmem.name
202 )));
203 }
204 for segment in file.segments().filter(|segment| segment.size() != 0) {
205 let address = segment.address();
206 let size = segment.size();
207 if !layout
208 .load_windows()
209 .any(|window| window.contains(address, size))
210 {
211 return Err(FirmwareCompileError::ElfLayout(format!(
212 "{} PT_LOAD segment {address:#x}..{:#x} is outside configured remoteproc memory regions",
213 path.display(),
214 address.saturating_add(size)
215 )));
216 }
217 }
218 Ok(())
219}
220
221#[derive(Clone)]
223pub struct FirmwareConfiguration {
224 pub target: String,
229 pub path: PathBuf,
231 pub release: bool,
233 pub offline: bool,
236 pub ipc: Option<SharedMemConfig>,
238 pub dbg: Option<DbgConfig>,
240 pub remoteproc: Option<RemoteprocMemoryLayout>,
243 pub workspace: Option<CstiWorkspace>,
247}
248
249pub struct FirmwareCompiler;
251
252impl FirmwareCompiler {
253 pub async fn build_with_reporter(
254 config: FirmwareConfiguration,
255 reporter: Option<&dyn BuildReporter>,
256 ) -> Result<FirmwareArtifacts, FirmwareCompileError> {
257 Self::cargo(&config, "build", reporter).await?;
258
259 let elf = Self::output_path(&config).await?;
260
261 if !elf.exists() {
262 return Err(FirmwareCompileError::ElfNotFound(elf));
263 }
264
265 verify_elf(&elf)
266 .await
267 .map_err(FirmwareCompileError::UtilityError)?;
268
269 let original = original_elf_path(&elf);
272 tokio::fs::copy(&elf, &original)
273 .await
274 .map_err(|e| FirmwareCompileError::Copy(original.clone(), e))?;
275
276 let deploy = deploy_elf_path(&elf);
279 objcopy_firmware(&elf, &deploy, config.remoteproc.as_ref(), reporter).await?;
280
281 Ok(FirmwareArtifacts { original, deploy })
282 }
283
284 pub async fn check_with_reporter(
288 config: &FirmwareConfiguration,
289 reporter: Option<&dyn BuildReporter>,
290 ) -> Result<(), FirmwareCompileError> {
291 Self::cargo(config, "check", reporter).await
292 }
293
294 async fn cargo(
296 config: &FirmwareConfiguration,
297 subcommand: &str,
298 reporter: Option<&dyn BuildReporter>,
299 ) -> Result<(), FirmwareCompileError> {
300 let cargo = resolve_cargo()
301 .await
302 .map_err(FirmwareCompileError::CargoResolveError)?;
303
304 let mut cmd = Command::new(&cargo);
305 cmd.arg(subcommand)
306 .arg("--target")
307 .arg(&config.target)
308 .current_dir(&config.path);
309
310 inject_shared_mem(&mut cmd, "IPC", config.ipc.as_ref());
311 inject_dbg(&mut cmd, config.dbg.as_ref());
312 inject_csti(&mut cmd, config.workspace.as_ref());
313
314 if config.release {
315 cmd.arg("--release");
316 }
317
318 if config.offline {
319 cmd.arg("--offline");
320 }
321
322 let status = run_command(&mut cmd, reporter)
323 .await
324 .map_err(|e| FirmwareCompileError::BuildFailed(e.to_string()))?;
325
326 if !status.success() {
327 return Err(FirmwareCompileError::BuildFailed(format!(
328 "cargo {subcommand} failed with status: {status}"
329 )));
330 }
331 Ok(())
332 }
333}
334
335impl Compiler for FirmwareCompiler {
336 type Configuration = FirmwareConfiguration;
337 type Error = FirmwareCompileError;
338 type Output = FirmwareArtifacts;
339
340 async fn build(config: Self::Configuration) -> Result<Self::Output, Self::Error> {
341 Self::build_with_reporter(config, None).await
342 }
343
344 async fn output_path(config: &Self::Configuration) -> Result<PathBuf, Self::Error> {
345 elf_path(
346 &config.path,
347 config.workspace.as_ref(),
348 &config.target,
349 config.release,
350 )
351 .await
352 .map_err(FirmwareCompileError::UtilityError)
353 }
354}
355
356pub struct FirmwareBuilder {
361 pub target: String,
363 pub path: PathBuf,
364 pub release: bool,
365 pub ipc: Option<SharedMemConfig>,
366 pub dbg: Option<DbgConfig>,
367}
368
369impl FirmwareBuilder {
370 pub async fn build(&self) -> Result<PathBuf, String> {
373 FirmwareCompiler::build(FirmwareConfiguration {
374 target: self.target.clone(),
375 path: self.path.clone(),
376 release: self.release,
377 offline: false,
378 ipc: self.ipc,
379 dbg: self.dbg,
380 remoteproc: None,
381 workspace: None,
382 })
383 .await
384 .map(|artifacts| artifacts.deploy)
385 .map_err(|e| e.to_string())
386 }
387
388 pub async fn elf_path(&self) -> Result<PathBuf, String> {
390 FirmwareCompiler::output_path(&FirmwareConfiguration {
391 target: self.target.clone(),
392 path: self.path.clone(),
393 release: self.release,
394 offline: false,
395 ipc: self.ipc,
396 dbg: self.dbg,
397 remoteproc: None,
398 workspace: None,
399 })
400 .await
401 .map_err(|e| e.to_string())
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn artifact_paths_derive_from_raw_cargo_output() {
411 let raw = Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu");
414 assert_eq!(
415 original_elf_path(raw),
416 Path::new(
417 "/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu_original.elf"
418 )
419 );
420 assert_eq!(
421 deploy_elf_path(raw),
422 Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu.elf")
423 );
424 }
425
426 #[test]
427 fn objcopy_resolution_honours_override() {
428 let prev = std::env::var("CONSORTIUM_OBJCOPY").ok();
430 unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", "riscv64-unknown-elf-objcopy") };
431 assert_eq!(resolve_objcopy(), "riscv64-unknown-elf-objcopy");
432 unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") };
433 assert_eq!(resolve_objcopy(), "arm-none-eabi-objcopy");
434 match prev {
435 Some(v) => unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", v) },
436 None => unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") },
437 }
438 }
439}