consortium_builder/compile/
firmware.rs1use std::path::{Path, PathBuf};
18use thiserror::Error;
19use tokio::process::Command;
20
21use super::Compiler;
22use crate::compile::env::{DbgConfig, SharedMemConfig};
23use crate::compile::utils::{
24 elf_path, inject_dbg, inject_shared_mem, resolve_cargo, run_command, verify_elf,
25};
26use crate::progress::BuildReporter;
27
28#[derive(Error, Debug)]
30pub enum FirmwareCompileError {
31 #[error("Failed to resolve cargo: {0}")]
32 CargoResolveError(String),
33
34 #[error("Cargo build failed: {0}")]
35 BuildFailed(String),
36
37 #[error("ELF not found at expected path: {0:?}")]
38 ElfNotFound(PathBuf),
39
40 #[error("copying firmware ELF to {0:?}: {1}")]
41 Copy(PathBuf, std::io::Error),
42
43 #[error("objcopy post-processing failed: {0}")]
44 Objcopy(String),
45
46 #[error("{0}")]
47 UtilityError(String),
48}
49
50#[derive(Clone, Debug)]
52pub struct FirmwareArtifacts {
53 pub original: PathBuf,
57 pub deploy: PathBuf,
62}
63
64pub fn original_elf_path(raw: &Path) -> PathBuf {
69 sibling(raw, "_original.elf")
70}
71
72pub fn deploy_elf_path(raw: &Path) -> PathBuf {
75 sibling(raw, ".elf")
76}
77
78fn sibling(raw: &Path, suffix: &str) -> PathBuf {
79 let stem = raw
80 .file_name()
81 .and_then(|n| n.to_str())
82 .unwrap_or("firmware");
83 raw.with_file_name(format!("{stem}{suffix}"))
84}
85
86fn resolve_objcopy() -> String {
88 std::env::var("CONSORTIUM_OBJCOPY").unwrap_or_else(|_| "arm-none-eabi-objcopy".to_string())
89}
90
91async fn objcopy_firmware(
97 src: &Path,
98 dest: &Path,
99 reporter: Option<&dyn BuildReporter>,
100) -> Result<(), FirmwareCompileError> {
101 let objcopy = resolve_objcopy();
102 let mut cmd = Command::new(&objcopy);
103 cmd.arg("--rename-section")
104 .arg(".vector_table=.isr_vectors")
105 .arg("--remove-section")
106 .arg(".defmt")
107 .arg(src)
108 .arg(dest);
109
110 let status = run_command(&mut cmd, reporter)
111 .await
112 .map_err(|e| FirmwareCompileError::Objcopy(format!("{objcopy}: {e}")))?;
113
114 if !status.success() {
115 return Err(FirmwareCompileError::Objcopy(format!(
116 "{objcopy} exited with status: {status}"
117 )));
118 }
119 Ok(())
120}
121
122#[derive(Clone)]
124pub struct FirmwareConfiguration {
125 pub target: String,
130 pub path: PathBuf,
132 pub release: bool,
134 pub ipc: Option<SharedMemConfig>,
136 pub dbg: Option<DbgConfig>,
138}
139
140pub struct FirmwareCompiler;
142
143impl FirmwareCompiler {
144 pub async fn build_with_reporter(
145 config: FirmwareConfiguration,
146 reporter: Option<&dyn BuildReporter>,
147 ) -> Result<FirmwareArtifacts, FirmwareCompileError> {
148 let cargo = resolve_cargo()
149 .await
150 .map_err(FirmwareCompileError::CargoResolveError)?;
151
152 let mut cmd = Command::new(&cargo);
153 cmd.arg("build")
154 .arg("--target")
155 .arg(&config.target)
156 .current_dir(&config.path);
157
158 inject_shared_mem(&mut cmd, "IPC", config.ipc.as_ref());
159 inject_dbg(&mut cmd, config.dbg.as_ref());
160
161 if config.release {
162 cmd.arg("--release");
163 }
164
165 let status = run_command(&mut cmd, reporter)
166 .await
167 .map_err(|e| FirmwareCompileError::BuildFailed(e.to_string()))?;
168
169 if !status.success() {
170 return Err(FirmwareCompileError::BuildFailed(format!(
171 "cargo build failed with status: {status}"
172 )));
173 }
174
175 let elf = Self::output_path(&config).await?;
176
177 if !elf.exists() {
178 return Err(FirmwareCompileError::ElfNotFound(elf));
179 }
180
181 verify_elf(&elf)
182 .await
183 .map_err(FirmwareCompileError::UtilityError)?;
184
185 let original = original_elf_path(&elf);
188 tokio::fs::copy(&elf, &original)
189 .await
190 .map_err(|e| FirmwareCompileError::Copy(original.clone(), e))?;
191
192 let deploy = deploy_elf_path(&elf);
195 objcopy_firmware(&elf, &deploy, reporter).await?;
196
197 Ok(FirmwareArtifacts { original, deploy })
198 }
199}
200
201impl Compiler for FirmwareCompiler {
202 type Configuration = FirmwareConfiguration;
203 type Error = FirmwareCompileError;
204 type Output = FirmwareArtifacts;
205
206 async fn build(config: Self::Configuration) -> Result<Self::Output, Self::Error> {
207 Self::build_with_reporter(config, None).await
208 }
209
210 async fn output_path(config: &Self::Configuration) -> Result<PathBuf, Self::Error> {
211 elf_path(&config.path, &config.target, config.release)
212 .await
213 .map_err(FirmwareCompileError::UtilityError)
214 }
215}
216
217pub struct FirmwareBuilder {
219 pub target: String,
221 pub path: PathBuf,
222 pub release: bool,
223 pub ipc: Option<SharedMemConfig>,
224 pub dbg: Option<DbgConfig>,
225}
226
227impl FirmwareBuilder {
228 pub async fn build(&self) -> Result<PathBuf, String> {
231 FirmwareCompiler::build(FirmwareConfiguration {
232 target: self.target.clone(),
233 path: self.path.clone(),
234 release: self.release,
235 ipc: self.ipc,
236 dbg: self.dbg,
237 })
238 .await
239 .map(|artifacts| artifacts.deploy)
240 .map_err(|e| e.to_string())
241 }
242
243 pub async fn elf_path(&self) -> Result<PathBuf, String> {
245 FirmwareCompiler::output_path(&FirmwareConfiguration {
246 target: self.target.clone(),
247 path: self.path.clone(),
248 release: self.release,
249 ipc: self.ipc,
250 dbg: self.dbg,
251 })
252 .await
253 .map_err(|e| e.to_string())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn artifact_paths_derive_from_raw_cargo_output() {
263 let raw = Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu");
266 assert_eq!(
267 original_elf_path(raw),
268 Path::new(
269 "/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu_original.elf"
270 )
271 );
272 assert_eq!(
273 deploy_elf_path(raw),
274 Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu.elf")
275 );
276 }
277
278 #[test]
279 fn objcopy_resolution_honours_override() {
280 let prev = std::env::var("CONSORTIUM_OBJCOPY").ok();
282 unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", "riscv64-unknown-elf-objcopy") };
283 assert_eq!(resolve_objcopy(), "riscv64-unknown-elf-objcopy");
284 unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") };
285 assert_eq!(resolve_objcopy(), "arm-none-eabi-objcopy");
286 match prev {
287 Some(v) => unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", v) },
288 None => unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") },
289 }
290 }
291}