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