1pub mod application;
34pub mod bitbake;
35pub mod dist;
36pub mod dtc;
37pub mod dts;
38pub mod elf_map;
39pub mod firmware;
40pub mod tee;
41
42use std::collections::HashMap;
43use std::path::{Path, PathBuf};
44
45use consortium_cfg::Session;
46use consortium_cfg::bridge::LoweredIr;
47use consortium_cfg_common::config::{HmiEngine, VisibleCore, XcoreName};
48use consortium_cfg_common::database::Coprocessor;
49use thiserror::Error;
50
51use crate::compile::application::{ApplicationCompileError, ApplicationCompiler};
52use crate::compile::deploy::systemd::{self, SystemdRestartPolicy, SystemdUnitConfig};
53use crate::compile::env::SharedMemConfig;
54use crate::compile::firmware::{FirmwareCompileError, FirmwareCompiler};
55use crate::compile::optee::{OpteeCompileError, OpteeCompiler, OpteeConfiguration};
56use crate::compile::web::{HmiCompileError, HmiCompiler, HmiConfiguration};
57use crate::progress::{BuildReporter, BuildStep};
58
59use application::ApplicationBuildPlan;
60use bitbake::{BitbakeAction, BitbakeError, RecipeSpec};
61use dist::{DistAssemblyError, DistLayout};
62use firmware::FirmwareBuildPlan;
63use tee::TeeToolchain;
64
65const GENERATED_MODULE: &str = "consortium.gen.rs";
68
69#[derive(Error, Debug)]
71pub enum PipelineError {
72 #[error("reading manifest {path}: {source}")]
74 ManifestRead {
75 path: String,
77 source: std::io::Error,
79 },
80
81 #[error(transparent)]
83 Cfg(#[from] Box<consortium_cfg::CfgError>),
84
85 #[error(transparent)]
87 Firmware(#[from] FirmwareCompileError),
88
89 #[error(transparent)]
91 Application(#[from] ApplicationCompileError),
92
93 #[error(transparent)]
95 Optee(#[from] OpteeCompileError),
96
97 #[error(transparent)]
99 Hmi(#[from] HmiCompileError),
100
101 #[error(transparent)]
103 Dist(#[from] DistAssemblyError),
104
105 #[error(transparent)]
106 Dtc(#[from] dtc::DtcError),
107
108 #[error(transparent)]
110 Bitbake(#[from] BitbakeError),
111
112 #[error("writing generated module to {path}: {source}")]
114 GeneratedWrite {
115 path: String,
117 source: std::io::Error,
119 },
120
121 #[error(
123 "debug core '{0}' has no compiled firmware to embed: `[dbg.{0}]` names a \
124 core that is not a remote firmware target"
125 )]
126 DbgCoreNotBuilt(String),
127
128 #[error("manifest declares no endpoint for the required party '{0}'")]
130 MissingEndpoint(String),
131
132 #[error(
134 "manifest has a [tee] section but no OP-TEE toolchain was provided \
135 (set TA_DEV_KIT_DIR or pass a toolchain)"
136 )]
137 MissingTeeToolchain,
138
139 #[error("unknown core '{0}'")]
141 UnknownCore(String),
142
143 #[error("codegen error: {0}")]
145 Codegen(String),
146}
147
148impl From<consortium_cfg::CfgError> for PipelineError {
149 fn from(error: consortium_cfg::CfgError) -> Self {
150 Self::Cfg(Box::new(error))
151 }
152}
153
154pub struct PipelineConfig {
156 pub manifest_path: PathBuf,
158 pub dist_root: PathBuf,
160 pub release: bool,
162 pub hmi: Option<HmiConfiguration>,
165 pub tee_toolchain: Option<TeeToolchain>,
167 pub sign_tee: bool,
170 pub bitbake: Vec<BitbakeAction>,
175}
176
177pub struct PipelinePlan {
180 pub manifest_dir: PathBuf,
182 pub ty_crate: syn::Path,
184 pub firmware: Vec<FirmwareBuildPlan>,
186 pub firmware_outputs: HashMap<VisibleCore, PathBuf>,
188 pub dbg_elfs: HashMap<VisibleCore, PathBuf>,
190 pub hmi: Option<HmiConfiguration>,
192 pub hmi_dist: Option<PathBuf>,
194 pub ta: Option<OpteeConfiguration>,
196 pub application: ApplicationBuildPlan,
198 pub dts_files: Vec<(String, String)>,
202 pub kernel_files: Vec<(String, String)>,
207 pub boot_files: Vec<(String, String)>,
212 pub systemd: SystemdUnitConfig,
214}
215
216pub async fn load_session(manifest_path: &Path) -> Result<Session, PipelineError> {
218 consortium_log::trace!("loading manifest {}", manifest_path.display());
219 let toml = tokio::fs::read_to_string(manifest_path)
220 .await
221 .map_err(|source| {
222 consortium_log::trace!("manifest read failed: {}", manifest_path.display());
223 PipelineError::ManifestRead {
224 path: manifest_path.display().to_string(),
225 source,
226 }
227 })?;
228 Ok(Session::from_toml(&toml)?)
229}
230
231pub fn derive_hmi_configuration(
237 session: &Session,
238 manifest_dir: &Path,
239) -> Result<Option<HmiConfiguration>, PipelineError> {
240 let Some(hmi) = &session.config.hmi else {
241 return Ok(None);
242 };
243 hmi.validate().map_err(PipelineError::Codegen)?;
244
245 let project_dir = manifest_dir.join(&hmi.source);
246 let dist_dir = manifest_dir.join(&hmi.build);
247 let command = match hmi.engine {
248 HmiEngine::Cog => {
249 crate::compile::utils::resolve_hmi_build_command().map_err(PipelineError::Codegen)?
250 }
251 HmiEngine::Pocket => {
252 let app = hmi.app.as_deref().unwrap_or_default();
254 crate::compile::utils::resolve_pocket_build_command(app, &dist_dir)
255 .map_err(PipelineError::Codegen)?
256 }
257 };
258 Ok(Some(HmiConfiguration {
259 engine: hmi.engine,
260 app: hmi.app.clone(),
261 project_dir,
262 dist_dir,
263 command,
264 }))
265}
266
267pub async fn plan(
269 session: &Session,
270 config: &PipelineConfig,
271) -> Result<PipelinePlan, PipelineError> {
272 let manifest_dir = config
273 .manifest_path
274 .parent()
275 .map(Path::to_path_buf)
276 .unwrap_or_else(|| PathBuf::from("."));
277
278 let ty_crate = resolve_ty_crate(session, &manifest_dir).await?;
279
280 let firmware =
282 firmware::plan_firmware_builds(session, &manifest_dir, config.release, &ty_crate)?;
283 let mut firmware_outputs = HashMap::new();
284 for plan in &firmware {
285 let elf = firmware::firmware_elf_path(&plan.config).await?;
286 firmware_outputs.insert(plan.core, elf);
287 }
288
289 let ir = session.lower()?;
293 let dbg_sources: HashMap<VisibleCore, PathBuf> = firmware_outputs
294 .iter()
295 .map(|(core, raw)| (*core, crate::compile::firmware::original_elf_path(raw)))
296 .collect();
297 let dbg_elfs = elf_map::resolve_dbg_elf_paths(&dbg_sources, ir.dbg.rings().keys().copied())?;
298
299 let hmi_dist = config.hmi.as_ref().map(|h| h.dist_dir.clone());
301
302 let ta = tee::plan_ta_build(
304 session,
305 &manifest_dir,
306 config.tee_toolchain.as_ref(),
307 config.release,
308 config.sign_tee,
309 )?;
310
311 let application = application::plan_application_build(
313 session,
314 &manifest_dir,
315 hmi_dist.as_deref(),
316 &dbg_elfs,
317 config.release,
318 &ty_crate,
319 )?;
320
321 let profile = &session.config.profile;
325 let dts_cfg = &session.config.dts;
326 let target_cores: Vec<String> = firmware.iter().map(|p| p.core.to_string()).collect();
328 let dts_files = match dts::render_linux_dts(
329 &profile.chip,
330 profile.devkit.as_deref(),
331 dts_cfg.base.as_deref(),
332 dts_cfg.mode,
333 &ir,
334 &target_cores,
335 )? {
336 Some(rendered) => {
337 consortium_log::trace!(
338 "rendered linux dts {} with {} warnings",
339 rendered.file_name,
340 rendered.warnings.len()
341 );
342 for warning in &rendered.warnings {
343 consortium_log::trace!("dts warning: {}", warning);
344 eprintln!("warning: {warning}");
345 }
346 vec![(rendered.file_name, rendered.contents)]
347 }
348 None => {
349 consortium_log::trace!("no UIO devices; no linux dts rendered");
350 Vec::new()
351 }
352 };
353
354 let mut kernel_files = dts::render_uio_kconfig(&ir, dts_cfg.mode, dts_cfg.configfs)
357 .map(|contents| vec![(dts::UIO_KCONFIG_NAME.to_string(), contents)])
358 .unwrap_or_default();
359 if let Some(kernel_recipe) = &session.config.profile.kernel_recipe {
360 let fragment_names: Vec<String> = kernel_files.iter().map(|(n, _)| n.clone()).collect();
361 if let Some(bbappend) = dts::render_kernel_bbappend(&fragment_names) {
362 kernel_files.push((dts::kernel_bbappend_name(kernel_recipe), bbappend));
363 }
364 }
365
366 let boot_files = dts::render_boot(&profile.chip, &ir);
368
369 let systemd = systemd_config(session, &application.config.path).await?;
371
372 Ok(PipelinePlan {
373 manifest_dir,
374 ty_crate,
375 firmware,
376 firmware_outputs,
377 dbg_elfs,
378 hmi: config.hmi.clone(),
379 hmi_dist,
380 ta,
381 application,
382 dts_files,
383 kernel_files,
384 boot_files,
385 systemd,
386 })
387}
388
389pub async fn run(config: PipelineConfig) -> Result<PathBuf, PipelineError> {
391 run_inner(config, None).await
392}
393
394pub async fn run_with_reporter(
396 config: PipelineConfig,
397 reporter: &dyn BuildReporter,
398) -> Result<PathBuf, PipelineError> {
399 run_inner(config, Some(reporter)).await
400}
401
402async fn run_inner(
403 config: PipelineConfig,
404 reporter: Option<&dyn BuildReporter>,
405) -> Result<PathBuf, PipelineError> {
406 consortium_log::trace!("pipeline run: manifest {}", config.manifest_path.display());
407 let session = load_session(&config.manifest_path).await?;
408 let plan = plan(&session, &config).await?;
409 consortium_log::trace!(
410 "pipeline plan: {} firmware, ta={}, hmi={}, {} bitbake",
411 plan.firmware.len(),
412 plan.ta.is_some(),
413 config.hmi.is_some(),
414 config.bitbake.len()
415 );
416
417 let total_steps = plan.firmware.len()
418 + usize::from(config.hmi.is_some())
419 + usize::from(plan.ta.is_some())
420 + 2
421 + config.bitbake.len();
422 let mut steps = StepRunner::new(reporter, total_steps);
423
424 let mut firmware_outputs = HashMap::new();
429 let mut firmware_originals = HashMap::new();
430 for fw in &plan.firmware {
431 let detail = format!("{} ({})", fw.core, fw.config.target);
432 let (deploy, original) = steps
433 .run("Build firmware", Some(&detail), async {
434 write_generated_module(&fw.config.path, &fw.generated).await?;
435 let artifacts =
436 FirmwareCompiler::build_with_reporter(fw.config.clone(), reporter).await?;
437 Ok((artifacts.deploy, artifacts.original))
438 })
439 .await?;
440 firmware_outputs.insert(fw.core, deploy);
441 firmware_originals.insert(fw.core, original);
442 }
443
444 let hmi_dist = match &config.hmi {
445 Some(hmi) => {
446 let detail = hmi.project_dir.display().to_string();
447 Some(
448 steps
449 .run("Build HMI", Some(&detail), async {
450 let dist = HmiCompiler::build_with_reporter(hmi.clone(), reporter).await?;
451 Ok(dist)
452 })
453 .await?,
454 )
455 }
456 None => None,
457 };
458
459 let ta = match plan.ta {
460 Some(ta) => {
461 let detail = if ta.sign {
462 "signed trusted application"
463 } else {
464 "unsigned trusted application"
465 };
466 Some(
467 steps
468 .run("Build TEE", Some(detail), async {
469 let ta = OpteeCompiler::build_with_reporter(ta, reporter).await?;
470 Ok(ta)
471 })
472 .await?,
473 )
474 }
475 None => None,
476 };
477
478 let app_detail = plan.application.config.target.clone();
479 let application = steps
480 .run("Build application", Some(&app_detail), async {
481 write_generated_module(&plan.application.config.path, &plan.application.generated)
482 .await?;
483 let app =
484 ApplicationCompiler::build_with_reporter(plan.application.config.clone(), reporter)
485 .await?;
486 Ok(app)
487 })
488 .await?;
489
490 let systemd_units = vec![(
491 plan.systemd.file_name(),
492 systemd::render_unit(&plan.systemd),
493 )];
494
495 let dist_detail = config.dist_root.display().to_string();
496 let layout = DistLayout::new(&config.dist_root);
497 steps
498 .run("Assemble dist", Some(&dist_detail), async {
499 let inputs = dist::DistAssemblyInputs {
500 firmware: &firmware_outputs,
501 firmware_originals: &firmware_originals,
502 application: &application,
503 ta: ta.as_deref(),
504 hmi: config
505 .hmi
506 .as_ref()
507 .zip(hmi_dist.as_deref())
508 .map(|(cfg, dist)| dist::HmiStaging {
509 engine: cfg.engine,
510 app: cfg.app.as_deref(),
511 dist,
512 }),
513 dts_files: &plan.dts_files,
514 kconfig_fragments: &plan.kernel_files,
515 boot_files: &plan.boot_files,
516 systemd_units: &systemd_units,
517 };
518 dist::assemble(&layout, inputs).await?;
519 Ok(())
520 })
521 .await?;
522
523 if let Some((dts_name, _)) = plan.dts_files.first() {
527 let arch = dtc::arch_dir(&plan.application.config.target);
528 let dts_dir = layout.dts_linux_dir();
529 match &session.config.dts.linux {
530 Some(linux) => {
531 let linux_dir = plan.manifest_dir.join(linux);
532 let profile = &session.config.profile;
533 let closure = consortium_cfg::dts::base_closure(
534 &profile.chip,
535 profile.devkit.as_deref(),
536 session.config.dts.base.as_deref(),
537 )
538 .map_err(consortium_cfg::CfgError::from)?;
539 let detail = format!("dtc {dts_name}");
540 steps
541 .run("Compile device tree", Some(&detail), async {
542 let out = dtc::compile_device_tree(
543 &dts_dir, dts_name, &closure, &linux_dir, arch,
544 )
545 .await?;
546 Ok(out)
547 })
548 .await?;
549 }
550 None => {
551 let yocto =
552 !config.bitbake.is_empty() || session.config.profile.kernel_recipe.is_some();
553 eprintln!("note: {}", dtc::compilation_hint(dts_name, arch, yocto));
554 }
555 }
556 }
557
558 for action in &config.bitbake {
559 let recipe_spec = match action {
563 BitbakeAction::InvokeBitbake {
564 recipe, dry_run, ..
565 } if *dry_run => Some(recipe_spec(
566 recipe,
567 &session.config.profile.deploy_root,
568 &application,
569 &firmware_outputs,
570 ta.as_deref(),
571 hmi_dist.as_deref(),
572 &plan.systemd.file_name(),
573 &plan.dts_files,
574 &plan.boot_files,
575 )?),
576 _ => None,
577 };
578
579 let (label, detail) = match action {
580 BitbakeAction::CopyIntoRecipe { recipe_files_dir } => (
581 "Stage into recipe",
582 format!("copy to {}", recipe_files_dir.display()),
583 ),
584 BitbakeAction::InvokeBitbake {
585 build_dir,
586 recipe,
587 dry_run,
588 } => {
589 let dry = if *dry_run {
590 " (dry-run, recipe only)"
591 } else {
592 ""
593 };
594 (
595 "Run bitbake",
596 format!("{recipe} in {}{dry}", build_dir.display()),
597 )
598 }
599 };
600 steps
601 .run(label, Some(&detail), async {
602 bitbake::run_with_reporter(
603 action,
604 &config.dist_root,
605 recipe_spec.as_ref(),
606 reporter,
607 )
608 .await?;
609 Ok(())
610 })
611 .await?;
612 }
613
614 Ok(config.dist_root)
615}
616
617#[allow(clippy::too_many_arguments)]
621fn recipe_spec(
622 recipe_name: &str,
623 deploy_root: &str,
624 application: &Path,
625 firmware_outputs: &HashMap<VisibleCore, PathBuf>,
626 ta: Option<&Path>,
627 hmi_dist: Option<&Path>,
628 systemd_unit: &str,
629 dts_files: &[(String, String)],
630 boot_files: &[(String, String)],
631) -> Result<RecipeSpec, PipelineError> {
632 let file_name = |path: &Path| -> Result<String, PipelineError> {
633 path.file_name()
634 .and_then(|n| n.to_str())
635 .map(str::to_string)
636 .ok_or_else(|| PipelineError::Codegen(format!("no file name for {}", path.display())))
637 };
638
639 let firmware = firmware_outputs
641 .keys()
642 .map(|core| format!("{core}.elf"))
643 .collect();
644
645 Ok(RecipeSpec {
646 recipe_name: recipe_name.to_string(),
647 deploy_root: deploy_root.to_string(),
648 app_bin: file_name(application)?,
649 firmware,
650 ta: ta.map(file_name).transpose()?,
651 has_www: hmi_dist.is_some(),
652 systemd_unit: systemd_unit.to_string(),
653 dts_files: dts_files.iter().map(|(name, _)| name.clone()).collect(),
654 boot_files: boot_files.iter().map(|(name, _)| name.clone()).collect(),
655 })
656}
657
658struct StepRunner<'a> {
659 reporter: Option<&'a dyn BuildReporter>,
660 next: usize,
661 total: usize,
662}
663
664impl<'a> StepRunner<'a> {
665 fn new(reporter: Option<&'a dyn BuildReporter>, total: usize) -> Self {
666 Self {
667 reporter,
668 next: 0,
669 total,
670 }
671 }
672
673 async fn run<T, F>(
674 &mut self,
675 name: &str,
676 detail: Option<&str>,
677 future: F,
678 ) -> Result<T, PipelineError>
679 where
680 F: std::future::Future<Output = Result<T, PipelineError>>,
681 {
682 self.next += 1;
683 let step = BuildStep {
684 index: self.next,
685 total: self.total,
686 name,
687 detail,
688 };
689 if let Some(reporter) = self.reporter {
690 reporter.step_started(step);
691 }
692
693 let result = future.await;
694 if let Some(reporter) = self.reporter {
695 match &result {
696 Ok(_) => reporter.step_succeeded(step),
697 Err(_) => reporter.step_failed(step),
698 }
699 }
700 result
701 }
702}
703
704pub(crate) fn visible_core_of(cp: &Coprocessor) -> Result<VisibleCore, PipelineError> {
706 cp.name
707 .parse()
708 .map_err(|_| PipelineError::UnknownCore(cp.name.clone()))
709}
710
711pub(crate) fn core_rust_triple(core: VisibleCore) -> String {
713 match core {
714 VisibleCore::Mcore(name) => name.rust_triple(),
715 VisibleCore::Rcore(name) => name.rust_triple(),
716 VisibleCore::Rvcore(name) => name.rust_triple(),
717 VisibleCore::Ap => "aarch64-unknown-linux-gnu".to_string(),
720 }
721}
722
723pub(crate) fn ipc_config_for(ir: &LoweredIr, core: VisibleCore) -> Option<SharedMemConfig> {
729 let transceiver = ir
730 .shuhan
731 .transceivers()
732 .iter()
733 .find(|t| t.channel_primary.0 == core || t.channel_secondary.0 == core)?;
734 let uio = match core {
736 VisibleCore::Ap => transceiver.region.address.ap.uio_id as u32,
737 _ => 0,
738 };
739 Some(SharedMemConfig {
740 length: u64::from(transceiver.region.length),
741 uio,
742 mtu: transceiver.mtu,
743 })
744}
745
746async fn resolve_ty_crate(
752 session: &Session,
753 manifest_dir: &Path,
754) -> Result<syn::Path, PipelineError> {
755 let shared_dir = manifest_dir.join(&session.config.profile.shared);
756 let name = match crate::compile::utils::binary_name(&shared_dir).await {
757 Ok(name) => name,
758 Err(_) => shared_dir
759 .file_name()
760 .and_then(|n| n.to_str())
761 .unwrap_or("shared")
762 .to_string(),
763 };
764 let ident = name.replace('-', "_");
765 syn::parse_str(&format!("::{ident}"))
766 .map_err(|e| PipelineError::Codegen(format!("bad shared crate name '{ident}': {e}")))
767}
768
769async fn systemd_config(
772 session: &Session,
773 app_path: &Path,
774) -> Result<SystemdUnitConfig, PipelineError> {
775 let binary = crate::compile::utils::binary_name(app_path)
776 .await
777 .map_err(PipelineError::Codegen)?;
778 let deploy_root = &session.config.profile.deploy_root;
779 Ok(SystemdUnitConfig {
780 unit_name: "consortium-app".to_string(),
781 exec_start: format!("{deploy_root}/app/{binary}"),
782 working_directory: format!("{deploy_root}/app"),
783 description: None,
784 restart: SystemdRestartPolicy::OnFailure,
785 })
786}
787
788async fn write_generated_module(
793 project_dir: &Path,
794 source: &str,
795) -> Result<PathBuf, PipelineError> {
796 let src_dir = project_dir.join("src");
797 tokio::fs::create_dir_all(&src_dir)
798 .await
799 .map_err(|source| {
800 consortium_log::trace!("failed to create src dir {}", src_dir.display());
801 PipelineError::GeneratedWrite {
802 path: src_dir.display().to_string(),
803 source,
804 }
805 })?;
806 let path = src_dir.join(GENERATED_MODULE);
807 consortium_log::trace!(
808 "writing {} bytes of generated code to {}",
809 source.len(),
810 path.display()
811 );
812 tokio::fs::write(&path, source).await.map_err(|source| {
813 consortium_log::trace!("failed to write generated module {}", path.display());
814 PipelineError::GeneratedWrite {
815 path: path.display().to_string(),
816 source,
817 }
818 })?;
819 Ok(path)
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 const STM32: &str = include_str!("../../../consortium-cfg/Consortium.example.stm32mp257.toml");
827
828 fn fabricate_project(tag: &str) -> PathBuf {
832 let root =
833 std::env::temp_dir().join(format!("consortium-pipeline-{tag}-{}", std::process::id()));
834 let _ = std::fs::remove_dir_all(&root);
835 std::fs::create_dir_all(&root).unwrap();
836 std::fs::write(root.join("Consortium.toml"), STM32).unwrap();
837 for (dir, pkg) in [
838 ("app", "melt-pot-app"),
839 ("ta", "melt-pot-ta"),
840 ("firmware", "melt-pot-mcu"),
841 ("shared", "melt-pot-shared"),
842 ] {
843 let crate_dir = root.join(dir);
844 std::fs::create_dir_all(&crate_dir).unwrap();
845 std::fs::write(
846 crate_dir.join("Cargo.toml"),
847 format!("[package]\nname = \"{pkg}\"\nversion = \"0.0.0\"\n"),
848 )
849 .unwrap();
850 }
851 root
852 }
853
854 #[tokio::test]
855 async fn plan_threads_all_stages() {
856 let root = fabricate_project("plan");
857 let session = Session::from_toml(STM32).unwrap();
858 let config = PipelineConfig {
859 manifest_path: root.join("Consortium.toml"),
860 dist_root: root.join("dist"),
861 release: true,
862 hmi: Some(HmiConfiguration {
863 engine: HmiEngine::Cog,
864 app: None,
865 project_dir: root.join("website"),
866 dist_dir: root.join("website/dist"),
867 command: crate::compile::web::HmiBuildCommand::Custom(vec!["true".into()]),
868 }),
869 tee_toolchain: Some(TeeToolchain {
870 ta_dev_kit_dir: PathBuf::from("/opt/optee/export-ta_arm64"),
871 optee_signer: PathBuf::from("/opt/optee/scripts/sign_encrypt.py"),
872 linker: None,
873 }),
874 sign_tee: true,
875 bitbake: Vec::new(),
876 };
877
878 let plan = plan(&session, &config).await.expect("plan succeeds");
879
880 assert_eq!(
883 plan.ty_crate.segments.last().unwrap().ident.to_string(),
884 "melt_pot_shared"
885 );
886
887 assert_eq!(plan.firmware.len(), 1);
890 let cm33 = VisibleCore::Mcore("cm33".parse().unwrap());
891 let expected_elf =
893 root.join("./firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu");
894 assert_eq!(plan.firmware_outputs[&cm33], expected_elf);
895
896 let original_elf =
899 std::path::absolute(crate::compile::firmware::original_elf_path(&expected_elf))
900 .unwrap();
901 assert_eq!(plan.dbg_elfs[&cm33], original_elf);
902
903 assert!(plan.application.generated.contains("include_bytes!"));
908 assert!(
909 plan.application
910 .generated
911 .contains(&format!("\"{}\"", original_elf.display()))
912 );
913 assert_eq!(plan.application.config.target, "aarch64-unknown-linux-gnu");
914 assert_eq!(
915 plan.application.config.hmi.as_ref().unwrap(),
916 &crate::compile::env::HmiConfig::Cog {
917 dist: "/opt/consortium/www".to_string(),
918 debug_url: Some("http://127.0.0.1:5173/".to_string()),
919 }
920 );
921
922 assert!(plan.ta.is_some());
924
925 assert_eq!(plan.systemd.exec_start, "/opt/consortium/app/melt-pot-app");
927
928 assert_eq!(
932 plan.dts_files
933 .iter()
934 .map(|(n, _)| n.as_str())
935 .collect::<Vec<_>>(),
936 vec!["consortium-stm32mp257.dts"]
937 );
938 assert!(plan.dts_files[0].1.contains("compatible = \"generic-uio\""));
940 let kconfig = plan
941 .kernel_files
942 .iter()
943 .find(|(n, _)| n == dts::UIO_KCONFIG_NAME)
944 .expect("uio kconfig fragment planned");
945 assert!(kconfig.1.contains("CONFIG_UIO_PDRV_GENIRQ=y"));
946 assert!(kconfig.1.contains("CONFIG_UIO_DMEM_GENIRQ=y"));
947 assert!(!kconfig.1.contains("CONFIG_OF_OVERLAY"));
949 assert!(plan.boot_files.iter().any(|(n, _)| n == "extlinux-uio.cfg"));
951 assert!(
953 !plan
954 .kernel_files
955 .iter()
956 .any(|(n, _)| n.ends_with(".bbappend"))
957 );
958
959 let _ = std::fs::remove_dir_all(&root);
960 }
961}