consortium_builder/compile/
firmware.rs1use std::path::{Path, PathBuf};
30use thiserror::Error;
31use tokio::process::Command;
32
33use super::Compiler;
34use crate::compile::env::{DbgConfig, SharedMemConfig};
35use crate::compile::utils::{
36 elf_path, 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 build 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("{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 reporter: Option<&dyn BuildReporter>,
112) -> Result<(), FirmwareCompileError> {
113 let objcopy = resolve_objcopy();
114 let mut cmd = Command::new(&objcopy);
115 cmd.arg("--rename-section")
116 .arg(".vector_table=.isr_vectors")
117 .arg("--remove-section")
118 .arg(".defmt")
119 .arg(src)
120 .arg(dest);
121
122 let status = run_command(&mut cmd, reporter)
123 .await
124 .map_err(|e| FirmwareCompileError::Objcopy(format!("{objcopy}: {e}")))?;
125
126 if !status.success() {
127 return Err(FirmwareCompileError::Objcopy(format!(
128 "{objcopy} exited with status: {status}"
129 )));
130 }
131 Ok(())
132}
133
134#[derive(Clone)]
136pub struct FirmwareConfiguration {
137 pub target: String,
142 pub path: PathBuf,
144 pub release: bool,
146 pub ipc: Option<SharedMemConfig>,
148 pub dbg: Option<DbgConfig>,
150}
151
152pub struct FirmwareCompiler;
154
155impl FirmwareCompiler {
156 pub async fn build_with_reporter(
157 config: FirmwareConfiguration,
158 reporter: Option<&dyn BuildReporter>,
159 ) -> Result<FirmwareArtifacts, FirmwareCompileError> {
160 let cargo = resolve_cargo()
161 .await
162 .map_err(FirmwareCompileError::CargoResolveError)?;
163
164 let mut cmd = Command::new(&cargo);
165 cmd.arg("build")
166 .arg("--target")
167 .arg(&config.target)
168 .current_dir(&config.path);
169
170 inject_shared_mem(&mut cmd, "IPC", config.ipc.as_ref());
171 inject_dbg(&mut cmd, config.dbg.as_ref());
172
173 if config.release {
174 cmd.arg("--release");
175 }
176
177 let status = run_command(&mut cmd, reporter)
178 .await
179 .map_err(|e| FirmwareCompileError::BuildFailed(e.to_string()))?;
180
181 if !status.success() {
182 return Err(FirmwareCompileError::BuildFailed(format!(
183 "cargo build failed with status: {status}"
184 )));
185 }
186
187 let elf = Self::output_path(&config).await?;
188
189 if !elf.exists() {
190 return Err(FirmwareCompileError::ElfNotFound(elf));
191 }
192
193 verify_elf(&elf)
194 .await
195 .map_err(FirmwareCompileError::UtilityError)?;
196
197 let original = original_elf_path(&elf);
200 tokio::fs::copy(&elf, &original)
201 .await
202 .map_err(|e| FirmwareCompileError::Copy(original.clone(), e))?;
203
204 let deploy = deploy_elf_path(&elf);
207 objcopy_firmware(&elf, &deploy, reporter).await?;
208
209 Ok(FirmwareArtifacts { original, deploy })
210 }
211}
212
213impl Compiler for FirmwareCompiler {
214 type Configuration = FirmwareConfiguration;
215 type Error = FirmwareCompileError;
216 type Output = FirmwareArtifacts;
217
218 async fn build(config: Self::Configuration) -> Result<Self::Output, Self::Error> {
219 Self::build_with_reporter(config, None).await
220 }
221
222 async fn output_path(config: &Self::Configuration) -> Result<PathBuf, Self::Error> {
223 elf_path(&config.path, &config.target, config.release)
224 .await
225 .map_err(FirmwareCompileError::UtilityError)
226 }
227}
228
229pub struct FirmwareBuilder {
231 pub target: String,
233 pub path: PathBuf,
234 pub release: bool,
235 pub ipc: Option<SharedMemConfig>,
236 pub dbg: Option<DbgConfig>,
237}
238
239impl FirmwareBuilder {
240 pub async fn build(&self) -> Result<PathBuf, String> {
243 FirmwareCompiler::build(FirmwareConfiguration {
244 target: self.target.clone(),
245 path: self.path.clone(),
246 release: self.release,
247 ipc: self.ipc,
248 dbg: self.dbg,
249 })
250 .await
251 .map(|artifacts| artifacts.deploy)
252 .map_err(|e| e.to_string())
253 }
254
255 pub async fn elf_path(&self) -> Result<PathBuf, String> {
257 FirmwareCompiler::output_path(&FirmwareConfiguration {
258 target: self.target.clone(),
259 path: self.path.clone(),
260 release: self.release,
261 ipc: self.ipc,
262 dbg: self.dbg,
263 })
264 .await
265 .map_err(|e| e.to_string())
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn artifact_paths_derive_from_raw_cargo_output() {
275 let raw = Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu");
278 assert_eq!(
279 original_elf_path(raw),
280 Path::new(
281 "/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu_original.elf"
282 )
283 );
284 assert_eq!(
285 deploy_elf_path(raw),
286 Path::new("/proj/firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu.elf")
287 );
288 }
289
290 #[test]
291 fn objcopy_resolution_honours_override() {
292 let prev = std::env::var("CONSORTIUM_OBJCOPY").ok();
294 unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", "riscv64-unknown-elf-objcopy") };
295 assert_eq!(resolve_objcopy(), "riscv64-unknown-elf-objcopy");
296 unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") };
297 assert_eq!(resolve_objcopy(), "arm-none-eabi-objcopy");
298 match prev {
299 Some(v) => unsafe { std::env::set_var("CONSORTIUM_OBJCOPY", v) },
300 None => unsafe { std::env::remove_var("CONSORTIUM_OBJCOPY") },
301 }
302 }
303}