consortium_builder/pipeline/
check.rs1use std::collections::HashMap;
43use std::path::{Path, PathBuf};
44
45use consortium_cfg::Session;
46use consortium_cfg_common::config::VisibleCore;
47
48use super::application::{ApplicationBuildPlan, plan_application_build};
49use super::firmware::{FirmwareBuildPlan, firmware_elf_path, plan_firmware_builds};
50use super::tee::{TeeToolchain, plan_ta_build};
51use super::{
52 CargoOptions, PipelineError, StepRunner, dts, elf_map, load_session, resolve_ty_crate,
53 write_generated_memory_x, write_generated_module,
54};
55use crate::compile::application::ApplicationCompiler;
56use crate::compile::firmware::{FirmwareCompiler, original_elf_path};
57use crate::compile::optee::{OpteeCompiler, OpteeConfiguration};
58use crate::progress::BuildReporter;
59
60pub struct CheckConfig {
62 pub manifest_path: PathBuf,
64 pub linux: Option<PathBuf>,
67 pub release: bool,
69 pub offline: bool,
71 pub tee_toolchain: Option<TeeToolchain>,
73 pub strict_remoteproc: bool,
76}
77
78impl CheckConfig {
79 pub fn cargo(&self) -> CargoOptions {
81 CargoOptions {
82 release: self.release,
83 offline: self.offline,
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum TaSkip {
91 NoToolchain,
93}
94
95pub struct CheckPlan {
97 pub firmware: Vec<FirmwareBuildPlan>,
99 pub ta: Option<OpteeConfiguration>,
101 pub ta_skipped: Option<TaSkip>,
103 pub application: ApplicationBuildPlan,
105 pub placeholder_elfs: Vec<PathBuf>,
108 pub dts_files: Vec<String>,
111}
112
113pub fn placeholder_elf_path(raw: &Path) -> PathBuf {
119 let stem = raw
120 .file_name()
121 .and_then(|n| n.to_str())
122 .unwrap_or("firmware");
123 raw.with_file_name(format!("{stem}_check.elf"))
124}
125
126pub async fn plan_check(
132 session: &Session,
133 config: &CheckConfig,
134) -> Result<CheckPlan, PipelineError> {
135 let manifest_dir = config
136 .manifest_path
137 .parent()
138 .map(Path::to_path_buf)
139 .unwrap_or_else(|| PathBuf::from("."));
140 let ty_crate = resolve_ty_crate(session, &manifest_dir).await?;
141 let cargo = config.cargo();
142 let workspace = super::resolve_workspace(session, &manifest_dir)?;
143
144 let firmware = plan_firmware_builds(session, &workspace, cargo, &ty_crate)?;
145
146 let mut dbg_sources: HashMap<VisibleCore, PathBuf> = HashMap::new();
149 for plan in &firmware {
150 let raw = firmware_elf_path(&plan.config).await?;
151 let original = original_elf_path(&raw);
152 let source = if original.exists() {
153 original
154 } else {
155 placeholder_elf_path(&raw)
156 };
157 dbg_sources.insert(plan.core, source);
158 }
159 let ir = session.lower()?;
160 let dbg_elfs = elf_map::resolve_dbg_elf_paths(&dbg_sources, ir.dbg.rings().keys().copied())?;
161 let placeholder_elfs = dbg_elfs.values().filter(|p| !p.exists()).cloned().collect();
162
163 let (ta, ta_skipped) = match (&session.config.tee, &config.tee_toolchain) {
164 (Some(_), None) => (None, Some(TaSkip::NoToolchain)),
165 _ => (
166 plan_ta_build(
167 session,
168 &workspace,
169 config.tee_toolchain.as_ref(),
170 cargo,
171 false,
172 )?,
173 None,
174 ),
175 };
176
177 let hmi_dist = session
180 .config
181 .hmi
182 .as_ref()
183 .map(|hmi| manifest_dir.join(&hmi.build));
184 let application = plan_application_build(
185 session,
186 &workspace,
187 hmi_dist.as_deref(),
188 &dbg_elfs,
189 cargo,
190 &ty_crate,
191 )?;
192
193 let mut dts_files = Vec::new();
194 if let Some(linux) = &config.linux {
195 let profile = &session.config.profile;
196 let target_cores: Vec<String> = firmware.iter().map(|p| p.core.to_string()).collect();
197 if let Some(rendered) = dts::render_linux_dts(
198 &profile.chip,
199 profile.devkit.as_deref(),
200 session.config.dts.base.as_deref(),
201 session.config.dts.mode,
202 &ir,
203 &target_cores,
204 linux,
205 config.strict_remoteproc,
206 )? {
207 for warning in &rendered.warnings {
208 eprintln!("warning: {warning}");
209 }
210 dts_files.push(rendered.file_name);
211 }
212 }
213
214 Ok(CheckPlan {
215 firmware,
216 ta,
217 ta_skipped,
218 application,
219 placeholder_elfs,
220 dts_files,
221 })
222}
223
224pub async fn run(config: CheckConfig) -> Result<CheckPlan, PipelineError> {
226 run_inner(config, None).await
227}
228
229pub async fn run_with_reporter(
231 config: CheckConfig,
232 reporter: &dyn BuildReporter,
233) -> Result<CheckPlan, PipelineError> {
234 run_inner(config, Some(reporter)).await
235}
236
237async fn run_inner(
238 config: CheckConfig,
239 reporter: Option<&dyn BuildReporter>,
240) -> Result<CheckPlan, PipelineError> {
241 consortium_log::trace!("check run: manifest {}", config.manifest_path.display());
242 let session = load_session(&config.manifest_path).await?;
243 let plan = plan_check(&session, &config).await?;
244
245 let total = plan.firmware.len() + usize::from(plan.ta.is_some()) + 1;
246 let mut steps = StepRunner::new(reporter, total);
247
248 for fw in &plan.firmware {
249 let detail = format!("{} ({})", fw.core, fw.config.target);
250 steps
251 .run("Check firmware", Some(&detail), async {
252 write_generated_module(&fw.config.path, &fw.generated).await?;
253 write_generated_memory_x(&fw.config).await?;
254 FirmwareCompiler::check_with_reporter(&fw.config, reporter).await?;
255 Ok(())
256 })
257 .await?;
258 }
259
260 if let Some(ta) = &plan.ta {
261 steps
262 .run("Check TEE", Some("trusted application"), async {
263 OpteeCompiler::check_with_reporter(ta, reporter).await?;
264 Ok(())
265 })
266 .await?;
267 }
268
269 let app_detail = plan.application.config.target.clone();
270 steps
271 .run("Check application", Some(&app_detail), async {
272 for path in &plan.placeholder_elfs {
273 write_placeholder_elf(path).await?;
274 }
275 write_generated_module(&plan.application.config.path, &plan.application.generated)
276 .await?;
277 ApplicationCompiler::check_with_reporter(&plan.application.config, reporter).await?;
278 Ok(())
279 })
280 .await?;
281
282 Ok(plan)
283}
284
285async fn write_placeholder_elf(path: &Path) -> Result<(), PipelineError> {
288 let io_error = |source| PipelineError::GeneratedWrite {
289 path: path.display().to_string(),
290 source,
291 };
292 if let Some(parent) = path.parent() {
293 tokio::fs::create_dir_all(parent).await.map_err(io_error)?;
294 }
295 tokio::fs::write(path, []).await.map_err(io_error)
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 const STM32: &str = include_str!("../../../consortium-cfg/Consortium.example.stm32mp257.toml");
303
304 fn fabricate_project(tag: &str) -> PathBuf {
307 let root =
308 std::env::temp_dir().join(format!("consortium-check-{tag}-{}", std::process::id()));
309 let _ = std::fs::remove_dir_all(&root);
310 std::fs::create_dir_all(&root).unwrap();
311 std::fs::write(root.join("Consortium.toml"), STM32).unwrap();
312 for (dir, pkg) in [
313 ("app", "melt-pot-app"),
314 ("ta", "melt-pot-ta"),
315 ("firmware", "melt-pot-mcu"),
316 ("shared", "melt-pot-shared"),
317 ] {
318 let crate_dir = root.join(dir);
319 std::fs::create_dir_all(&crate_dir).unwrap();
320 std::fs::write(
321 crate_dir.join("Cargo.toml"),
322 format!("[package]\nname = \"{pkg}\"\nversion = \"0.0.0\"\n"),
323 )
324 .unwrap();
325 }
326 root
327 }
328
329 fn config(root: &Path, tee_toolchain: Option<TeeToolchain>) -> CheckConfig {
330 CheckConfig {
331 manifest_path: root.join("Consortium.toml"),
332 linux: None,
333 release: false,
334 offline: true,
335 tee_toolchain,
336 strict_remoteproc: true,
337 }
338 }
339
340 fn raw_mcu_elf(root: &Path) -> PathBuf {
341 root.join("./firmware/target/thumbv8m.main-none-eabihf/debug/melt-pot-mcu")
342 }
343
344 #[tokio::test]
345 async fn unbuilt_debug_firmware_gets_a_placeholder() {
346 let root = fabricate_project("placeholder");
347 let session = Session::from_toml(STM32).unwrap();
348
349 let plan = plan_check(&session, &config(&root, None))
350 .await
351 .expect("check plans without --linux or a TEE toolchain");
352
353 assert_eq!(plan.firmware.len(), 1);
354 assert!(plan.firmware[0].config.offline);
355 assert!(plan.dts_files.is_empty());
357 assert!(plan.ta.is_none());
359 assert_eq!(plan.ta_skipped, Some(TaSkip::NoToolchain));
360
361 let placeholder = std::path::absolute(placeholder_elf_path(&raw_mcu_elf(&root))).unwrap();
365 assert_eq!(plan.placeholder_elfs, vec![placeholder.clone()]);
366 let generated = &plan.application.generated;
367 assert!(
368 generated.contains("::core::env!(\"CSTI_MCU_PATH\")"),
369 "{generated}"
370 );
371 assert!(
372 generated
373 .contains("\"/target/thumbv8m.main-none-eabihf/debug/melt-pot-mcu_check.elf\"")
374 );
375 assert!(!generated.contains(&root.display().to_string()));
376
377 let _ = std::fs::remove_dir_all(&root);
378 }
379
380 #[tokio::test]
381 async fn built_debug_firmware_is_embedded_as_is() {
382 let root = fabricate_project("built");
383 let original = original_elf_path(&raw_mcu_elf(&root));
384 std::fs::create_dir_all(original.parent().unwrap()).unwrap();
385 std::fs::write(&original, b"\x7fELF").unwrap();
386 let session = Session::from_toml(STM32).unwrap();
387 let toolchain = TeeToolchain {
388 ta_dev_kit_dir: PathBuf::from("/opt/optee/export-ta_arm64"),
389 optee_signer: PathBuf::from("/opt/optee/scripts/sign_encrypt.py"),
390 linker: None,
391 };
392
393 let plan = plan_check(&session, &config(&root, Some(toolchain)))
394 .await
395 .expect("check plans");
396
397 assert!(plan.placeholder_elfs.is_empty());
398 assert!(
399 plan.application
400 .generated
401 .contains("\"/target/thumbv8m.main-none-eabihf/debug/melt-pot-mcu_original.elf\"")
402 );
403 let ta = plan.ta.expect("TA planned");
405 assert!(!ta.sign);
406 assert!(plan.ta_skipped.is_none());
407
408 let _ = std::fs::remove_dir_all(&root);
409 }
410}