1pub mod application;
42pub mod bitbake;
43pub mod check;
44pub mod dist;
45pub mod dtc;
46pub mod dts;
47pub mod elf_map;
48pub mod fetch;
49pub mod firmware;
50pub mod tee;
51
52use std::collections::HashMap;
53use std::path::{Path, PathBuf};
54
55use consortium_cfg::Session;
56use consortium_cfg::bridge::LoweredIr;
57use consortium_cfg_common::config::{HmiEngine, VisibleCore, Workspace, XcoreName};
58use consortium_cfg_common::database::Coprocessor;
59use thiserror::Error;
60
61use crate::compile::application::{ApplicationCompileError, ApplicationCompiler};
62use crate::compile::deploy::systemd::{self, SystemdRestartPolicy, SystemdUnitConfig};
63use crate::compile::env::{CstiWorkspace, SharedMemConfig};
64use crate::compile::firmware::{FirmwareCompileError, FirmwareCompiler};
65use crate::compile::optee::{OpteeCompileError, OpteeCompiler, OpteeConfiguration};
66use crate::compile::web::{HmiCompileError, HmiCompiler, HmiConfiguration};
67use crate::progress::{BuildReporter, BuildStep};
68
69use application::ApplicationBuildPlan;
70use bitbake::{BitbakeAction, BitbakeError, RecipeSpec};
71use dist::{DistAssemblyError, DistLayout};
72use firmware::FirmwareBuildPlan;
73use tee::TeeToolchain;
74
75const GENERATED_MODULE: &str = "consortium.gen.rs";
78const GENERATED_MEMORY_X: &str = "memory.x";
79
80#[derive(Error, Debug)]
82pub enum PipelineError {
83 #[error("reading manifest {path}: {source}")]
85 ManifestRead {
86 path: String,
88 source: std::io::Error,
90 },
91
92 #[error(
95 "[profile] workspace = \"unified\" requires a workspace root Cargo.toml beside the manifest, but {0} {1}"
96 )]
97 UnifiedWorkspace(String, &'static str),
98
99 #[error(transparent)]
101 Cfg(#[from] Box<consortium_cfg::CfgError>),
102
103 #[error(transparent)]
105 Firmware(#[from] FirmwareCompileError),
106
107 #[error(transparent)]
109 Application(#[from] ApplicationCompileError),
110
111 #[error(transparent)]
113 Optee(#[from] OpteeCompileError),
114
115 #[error(transparent)]
117 Hmi(#[from] HmiCompileError),
118
119 #[error(transparent)]
121 Dist(#[from] DistAssemblyError),
122
123 #[error(transparent)]
124 Dtc(#[from] dtc::DtcError),
125
126 #[error(transparent)]
128 Bitbake(#[from] BitbakeError),
129
130 #[error("writing generated module to {path}: {source}")]
132 GeneratedWrite {
133 path: String,
135 source: std::io::Error,
137 },
138
139 #[error(
141 "debug core '{0}' has no compiled firmware to embed: `[dbg.{0}]` names a \
142 core that is not a remote firmware target"
143 )]
144 DbgCoreNotBuilt(String),
145
146 #[error("manifest declares no endpoint for the required party '{0}'")]
148 MissingEndpoint(String),
149
150 #[error(
152 "manifest has a [tee] section but no OP-TEE toolchain was provided \
153 (set TA_DEV_KIT_DIR or pass a toolchain)"
154 )]
155 MissingTeeToolchain,
156
157 #[error("unknown core '{0}'")]
159 UnknownCore(String),
160
161 #[error("codegen error: {0}")]
163 Codegen(String),
164
165 #[error("fetching dependencies for {label} ({target}): {message}")]
167 Fetch {
168 label: String,
170 target: String,
172 message: String,
174 },
175
176 #[error("installing rust target(s) {targets}: {message}")]
178 RustTarget {
179 targets: String,
181 message: String,
183 },
184
185 #[error("installing HMI dependencies in {dir}: {message}")]
187 HmiFetch {
188 dir: String,
190 message: String,
192 },
193}
194
195impl From<consortium_cfg::CfgError> for PipelineError {
196 fn from(error: consortium_cfg::CfgError) -> Self {
197 Self::Cfg(Box::new(error))
198 }
199}
200
201#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
207pub struct CargoOptions {
208 pub release: bool,
210 pub offline: bool,
214}
215
216pub struct PipelineConfig {
218 pub manifest_path: PathBuf,
220 pub dist_root: PathBuf,
222 pub linux: PathBuf,
225 pub release: bool,
227 pub offline: bool,
230 pub hmi: Option<HmiConfiguration>,
233 pub tee_toolchain: Option<TeeToolchain>,
235 pub sign_tee: bool,
238 pub strict_remoteproc: bool,
245 pub bitbake: Vec<BitbakeAction>,
250}
251
252impl PipelineConfig {
253 pub fn cargo(&self) -> CargoOptions {
255 CargoOptions {
256 release: self.release,
257 offline: self.offline,
258 }
259 }
260}
261
262pub struct PipelinePlan {
265 pub manifest_dir: PathBuf,
267 pub ty_crate: syn::Path,
269 pub firmware: Vec<FirmwareBuildPlan>,
271 pub firmware_outputs: HashMap<VisibleCore, PathBuf>,
273 pub dbg_elfs: HashMap<VisibleCore, PathBuf>,
275 pub hmi: Option<HmiConfiguration>,
277 pub hmi_dist: Option<PathBuf>,
279 pub ta: Option<OpteeConfiguration>,
281 pub application: ApplicationBuildPlan,
283 pub dts_files: Vec<(String, String)>,
287 pub dts_source_closure: Vec<(String, String)>,
289 pub kernel_files: Vec<(String, String)>,
294 pub boot_files: Vec<(String, String)>,
299 pub systemd: SystemdUnitConfig,
301}
302
303pub async fn load_session(manifest_path: &Path) -> Result<Session, PipelineError> {
305 consortium_log::trace!("loading manifest {}", manifest_path.display());
306 let toml = tokio::fs::read_to_string(manifest_path)
307 .await
308 .map_err(|source| {
309 consortium_log::trace!("manifest read failed: {}", manifest_path.display());
310 PipelineError::ManifestRead {
311 path: manifest_path.display().to_string(),
312 source,
313 }
314 })?;
315 Ok(Session::from_toml(&toml)?)
316}
317
318pub fn resolve_workspace(
326 session: &Session,
327 manifest_dir: &Path,
328) -> Result<CstiWorkspace, PipelineError> {
329 let absolute = |p: &Path| std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf());
330 let manifest_dir = absolute(manifest_dir);
331 let layout = session.config.profile.workspace;
332
333 if layout == Workspace::Unified {
334 let cargo_toml = manifest_dir.join("Cargo.toml");
335 let shown = cargo_toml.display().to_string();
336 let contents = std::fs::read_to_string(&cargo_toml)
337 .map_err(|_| PipelineError::UnifiedWorkspace(shown.clone(), "does not exist"))?;
338 let is_workspace = contents
339 .parse::<toml::Table>()
340 .is_ok_and(|table| table.contains_key("workspace"));
341 if !is_workspace {
342 return Err(PipelineError::UnifiedWorkspace(
343 shown,
344 "has no [workspace] table",
345 ));
346 }
347 }
348
349 let firmware = firmware::firmware_endpoints(session)?;
350 let mcu_dir = match firmware.as_slice() {
351 [(_, _, root)] => Some(absolute(&manifest_dir.join(root))),
352 _ => None,
353 };
354
355 Ok(CstiWorkspace {
356 layout,
357 manifest_dir,
358 mcu_dir,
359 })
360}
361
362pub fn derive_hmi_configuration(
368 session: &Session,
369 manifest_dir: &Path,
370) -> Result<Option<HmiConfiguration>, PipelineError> {
371 let Some(hmi) = &session.config.hmi else {
372 return Ok(None);
373 };
374 hmi.validate().map_err(PipelineError::Codegen)?;
375
376 let project_dir = manifest_dir.join(&hmi.source);
377 let dist_dir = manifest_dir.join(&hmi.build);
378 let command = match hmi.engine {
379 HmiEngine::Cog => {
380 crate::compile::utils::resolve_hmi_build_command().map_err(PipelineError::Codegen)?
381 }
382 HmiEngine::Pocket => {
383 let app = hmi.app.as_deref().unwrap_or_default();
385 crate::compile::utils::resolve_pocket_build_command(app, &dist_dir)
386 .map_err(PipelineError::Codegen)?
387 }
388 };
389 Ok(Some(HmiConfiguration {
390 engine: hmi.engine,
391 app: hmi.app.clone(),
392 project_dir,
393 dist_dir,
394 command,
395 }))
396}
397
398pub async fn plan(
400 session: &Session,
401 config: &PipelineConfig,
402) -> Result<PipelinePlan, PipelineError> {
403 let manifest_dir = config
404 .manifest_path
405 .parent()
406 .map(Path::to_path_buf)
407 .unwrap_or_else(|| PathBuf::from("."));
408
409 let ty_crate = resolve_ty_crate(session, &manifest_dir).await?;
410 let cargo = config.cargo();
411 let workspace = resolve_workspace(session, &manifest_dir)?;
412
413 let firmware = firmware::plan_firmware_builds(session, &workspace, cargo, &ty_crate)?;
415 let mut firmware_outputs = HashMap::new();
416 for plan in &firmware {
417 let elf = firmware::firmware_elf_path(&plan.config).await?;
418 firmware_outputs.insert(plan.core, elf);
419 }
420
421 let ir = session.lower()?;
425 let dbg_sources: HashMap<VisibleCore, PathBuf> = firmware_outputs
426 .iter()
427 .map(|(core, raw)| (*core, crate::compile::firmware::original_elf_path(raw)))
428 .collect();
429 let dbg_elfs = elf_map::resolve_dbg_elf_paths(&dbg_sources, ir.dbg.rings().keys().copied())?;
430
431 let hmi_dist = config.hmi.as_ref().map(|h| h.dist_dir.clone());
433
434 let ta = tee::plan_ta_build(
436 session,
437 &workspace,
438 config.tee_toolchain.as_ref(),
439 cargo,
440 config.sign_tee,
441 )?;
442
443 let application = application::plan_application_build(
445 session,
446 &workspace,
447 hmi_dist.as_deref(),
448 &dbg_elfs,
449 cargo,
450 &ty_crate,
451 )?;
452
453 let profile = &session.config.profile;
457 let dts_cfg = &session.config.dts;
458 let target_cores: Vec<String> = firmware.iter().map(|p| p.core.to_string()).collect();
460 let (dts_files, dts_source_closure) = match dts::render_linux_dts(
461 &profile.chip,
462 profile.devkit.as_deref(),
463 dts_cfg.base.as_deref(),
464 dts_cfg.mode,
465 &ir,
466 &target_cores,
467 &config.linux,
468 config.strict_remoteproc,
469 )? {
470 Some(rendered) => {
471 consortium_log::trace!(
472 "rendered linux dts {} with {} warnings",
473 rendered.file_name,
474 rendered.warnings.len()
475 );
476 for warning in &rendered.warnings {
477 consortium_log::trace!("dts warning: {}", warning);
478 eprintln!("warning: {warning}");
479 }
480 (
481 vec![(rendered.file_name, rendered.contents)],
482 rendered.source_closure,
483 )
484 }
485 None => {
486 consortium_log::trace!("no UIO or remoteproc devices; no linux dts rendered");
487 (Vec::new(), Vec::new())
488 }
489 };
490
491 let mut kernel_files = dts::render_kernel_kconfig(&profile.chip, &ir)
494 .map(|contents| vec![(dts::KCONFIG_NAME.to_string(), contents)])
495 .unwrap_or_default();
496 if let Some(kernel_recipe) = &session.config.profile.kernel_recipe {
497 let fragment_names: Vec<String> = kernel_files.iter().map(|(n, _)| n.clone()).collect();
498 if let Some(bbappend) = dts::render_kernel_bbappend(&fragment_names) {
499 kernel_files.push((dts::kernel_bbappend_name(kernel_recipe), bbappend));
500 }
501 }
502
503 let boot_files = dts::render_boot(&profile.chip, &ir);
505
506 let systemd = systemd_config(session, &application.config.path).await?;
508
509 Ok(PipelinePlan {
510 manifest_dir,
511 ty_crate,
512 firmware,
513 firmware_outputs,
514 dbg_elfs,
515 hmi: config.hmi.clone(),
516 hmi_dist,
517 ta,
518 application,
519 dts_files,
520 dts_source_closure,
521 kernel_files,
522 boot_files,
523 systemd,
524 })
525}
526
527pub async fn run(config: PipelineConfig) -> Result<PathBuf, PipelineError> {
529 run_inner(config, None).await
530}
531
532pub async fn run_with_reporter(
534 config: PipelineConfig,
535 reporter: &dyn BuildReporter,
536) -> Result<PathBuf, PipelineError> {
537 run_inner(config, Some(reporter)).await
538}
539
540async fn run_inner(
541 config: PipelineConfig,
542 reporter: Option<&dyn BuildReporter>,
543) -> Result<PathBuf, PipelineError> {
544 consortium_log::trace!("pipeline run: manifest {}", config.manifest_path.display());
545 let session = load_session(&config.manifest_path).await?;
546 let plan = plan(&session, &config).await?;
547 consortium_log::trace!(
548 "pipeline plan: {} firmware, ta={}, hmi={}, {} bitbake",
549 plan.firmware.len(),
550 plan.ta.is_some(),
551 config.hmi.is_some(),
552 config.bitbake.len()
553 );
554
555 let total_steps = plan.firmware.len()
556 + usize::from(config.hmi.is_some())
557 + usize::from(plan.ta.is_some())
558 + 2
559 + config.bitbake.len();
560 let mut steps = StepRunner::new(reporter, total_steps);
561
562 let mut firmware_outputs = HashMap::new();
567 let mut firmware_originals = HashMap::new();
568 for fw in &plan.firmware {
569 let detail = format!("{} ({})", fw.core, fw.config.target);
570 let (deploy, original) = steps
571 .run("Build firmware", Some(&detail), async {
572 write_generated_module(&fw.config.path, &fw.generated).await?;
573 write_generated_memory_x(&fw.config).await?;
574 let artifacts =
575 FirmwareCompiler::build_with_reporter(fw.config.clone(), reporter).await?;
576 Ok((artifacts.deploy, artifacts.original))
577 })
578 .await?;
579 firmware_outputs.insert(fw.core, deploy);
580 firmware_originals.insert(fw.core, original);
581 }
582
583 let hmi_dist = match &config.hmi {
584 Some(hmi) => {
585 let detail = hmi.project_dir.display().to_string();
586 Some(
587 steps
588 .run("Build HMI", Some(&detail), async {
589 let dist = HmiCompiler::build_with_reporter(hmi.clone(), reporter).await?;
590 Ok(dist)
591 })
592 .await?,
593 )
594 }
595 None => None,
596 };
597
598 let ta = match plan.ta {
599 Some(ta) => {
600 let detail = if ta.sign {
601 "signed trusted application"
602 } else {
603 "unsigned trusted application"
604 };
605 Some(
606 steps
607 .run("Build TEE", Some(detail), async {
608 let ta = OpteeCompiler::build_with_reporter(ta, reporter).await?;
609 Ok(ta)
610 })
611 .await?,
612 )
613 }
614 None => None,
615 };
616
617 let app_detail = plan.application.config.target.clone();
618 let application = steps
619 .run("Build application", Some(&app_detail), async {
620 write_generated_module(&plan.application.config.path, &plan.application.generated)
621 .await?;
622 let app =
623 ApplicationCompiler::build_with_reporter(plan.application.config.clone(), reporter)
624 .await?;
625 Ok(app)
626 })
627 .await?;
628
629 let systemd_units = vec![(
630 plan.systemd.file_name(),
631 systemd::render_unit(&plan.systemd),
632 )];
633
634 let dist_detail = config.dist_root.display().to_string();
635 let layout = DistLayout::new(&config.dist_root);
636 steps
637 .run("Assemble dist", Some(&dist_detail), async {
638 let inputs = dist::DistAssemblyInputs {
639 firmware: &firmware_outputs,
640 firmware_originals: &firmware_originals,
641 application: &application,
642 ta: ta.as_deref(),
643 hmi: config
644 .hmi
645 .as_ref()
646 .zip(hmi_dist.as_deref())
647 .map(|(cfg, dist)| dist::HmiStaging {
648 engine: cfg.engine,
649 app: cfg.app.as_deref(),
650 dist,
651 }),
652 dts_files: &plan.dts_files,
653 kconfig_fragments: &plan.kernel_files,
654 boot_files: &plan.boot_files,
655 systemd_units: &systemd_units,
656 };
657 dist::assemble(&layout, inputs).await?;
658 Ok(())
659 })
660 .await?;
661
662 if let Some((dts_name, _)) = plan.dts_files.first() {
665 let arch = dtc::arch_dir(&plan.application.config.target);
666 let dts_dir = layout.dts_linux_dir();
667 let detail = format!("dtc {dts_name}");
668 steps
669 .run("Compile device tree", Some(&detail), async {
670 let out = dtc::compile_device_tree(
671 &dts_dir,
672 dts_name,
673 &plan.dts_source_closure,
674 &config.linux,
675 arch,
676 )
677 .await?;
678 Ok(out)
679 })
680 .await?;
681 }
682
683 for action in &config.bitbake {
684 let recipe_spec = match action {
688 BitbakeAction::InvokeBitbake {
689 recipe, dry_run, ..
690 } if *dry_run => Some(recipe_spec(
691 recipe,
692 &session.config.profile.deploy_root,
693 &application,
694 &firmware_outputs,
695 ta.as_deref(),
696 hmi_dist.as_deref(),
697 &plan.systemd.file_name(),
698 &plan.dts_files,
699 &plan.boot_files,
700 )?),
701 _ => None,
702 };
703
704 let (label, detail) = match action {
705 BitbakeAction::CopyIntoRecipe { recipe_files_dir } => (
706 "Stage into recipe",
707 format!("copy to {}", recipe_files_dir.display()),
708 ),
709 BitbakeAction::InvokeBitbake {
710 build_dir,
711 recipe,
712 dry_run,
713 } => {
714 let dry = if *dry_run {
715 " (dry-run, recipe only)"
716 } else {
717 ""
718 };
719 (
720 "Run bitbake",
721 format!("{recipe} in {}{dry}", build_dir.display()),
722 )
723 }
724 };
725 steps
726 .run(label, Some(&detail), async {
727 bitbake::run_with_reporter(
728 action,
729 &config.dist_root,
730 recipe_spec.as_ref(),
731 reporter,
732 )
733 .await?;
734 Ok(())
735 })
736 .await?;
737 }
738
739 Ok(config.dist_root)
740}
741
742#[allow(clippy::too_many_arguments)]
746fn recipe_spec(
747 recipe_name: &str,
748 deploy_root: &str,
749 application: &Path,
750 firmware_outputs: &HashMap<VisibleCore, PathBuf>,
751 ta: Option<&Path>,
752 hmi_dist: Option<&Path>,
753 systemd_unit: &str,
754 dts_files: &[(String, String)],
755 boot_files: &[(String, String)],
756) -> Result<RecipeSpec, PipelineError> {
757 let file_name = |path: &Path| -> Result<String, PipelineError> {
758 path.file_name()
759 .and_then(|n| n.to_str())
760 .map(str::to_string)
761 .ok_or_else(|| PipelineError::Codegen(format!("no file name for {}", path.display())))
762 };
763
764 let firmware = firmware_outputs
766 .keys()
767 .map(|core| format!("{core}.elf"))
768 .collect();
769
770 Ok(RecipeSpec {
771 recipe_name: recipe_name.to_string(),
772 deploy_root: deploy_root.to_string(),
773 app_bin: file_name(application)?,
774 firmware,
775 ta: ta.map(file_name).transpose()?,
776 has_www: hmi_dist.is_some(),
777 systemd_unit: systemd_unit.to_string(),
778 dts_files: dts_files.iter().map(|(name, _)| name.clone()).collect(),
779 boot_files: boot_files.iter().map(|(name, _)| name.clone()).collect(),
780 })
781}
782
783struct StepRunner<'a> {
784 reporter: Option<&'a dyn BuildReporter>,
785 next: usize,
786 total: usize,
787}
788
789impl<'a> StepRunner<'a> {
790 fn new(reporter: Option<&'a dyn BuildReporter>, total: usize) -> Self {
791 Self {
792 reporter,
793 next: 0,
794 total,
795 }
796 }
797
798 async fn run<T, F>(
799 &mut self,
800 name: &str,
801 detail: Option<&str>,
802 future: F,
803 ) -> Result<T, PipelineError>
804 where
805 F: std::future::Future<Output = Result<T, PipelineError>>,
806 {
807 self.next += 1;
808 let step = BuildStep {
809 index: self.next,
810 total: self.total,
811 name,
812 detail,
813 };
814 if let Some(reporter) = self.reporter {
815 reporter.step_started(step);
816 }
817
818 let result = future.await;
819 if let Some(reporter) = self.reporter {
820 match &result {
821 Ok(_) => reporter.step_succeeded(step),
822 Err(_) => reporter.step_failed(step),
823 }
824 }
825 result
826 }
827}
828
829pub(crate) fn visible_core_of(cp: &Coprocessor) -> Result<VisibleCore, PipelineError> {
831 cp.name
832 .parse()
833 .map_err(|_| PipelineError::UnknownCore(cp.name.clone()))
834}
835
836pub(crate) fn core_rust_triple(core: VisibleCore) -> String {
838 match core {
839 VisibleCore::Mcore(name) => name.rust_triple(),
840 VisibleCore::Rcore(name) => name.rust_triple(),
841 VisibleCore::Rvcore(name) => name.rust_triple(),
842 VisibleCore::Ap => "aarch64-unknown-linux-gnu".to_string(),
845 }
846}
847
848pub(crate) fn ipc_config_for(ir: &LoweredIr, core: VisibleCore) -> Option<SharedMemConfig> {
854 let transceiver = ir
855 .shuhan
856 .transceivers()
857 .iter()
858 .find(|t| t.channel_primary.0 == core || t.channel_secondary.0 == core)?;
859 let uio = match core {
861 VisibleCore::Ap => transceiver.region.address.ap.uio_id as u32,
862 _ => 0,
863 };
864 Some(SharedMemConfig {
865 length: u64::from(transceiver.region.length),
866 uio,
867 mtu: transceiver.mtu,
868 })
869}
870
871async fn resolve_ty_crate(
877 session: &Session,
878 manifest_dir: &Path,
879) -> Result<syn::Path, PipelineError> {
880 let shared_dir = manifest_dir.join(&session.config.profile.shared);
881 let name = match crate::compile::utils::binary_name(&shared_dir).await {
882 Ok(name) => name,
883 Err(_) => shared_dir
884 .file_name()
885 .and_then(|n| n.to_str())
886 .unwrap_or("shared")
887 .to_string(),
888 };
889 let ident = name.replace('-', "_");
890 syn::parse_str(&format!("::{ident}"))
891 .map_err(|e| PipelineError::Codegen(format!("bad shared crate name '{ident}': {e}")))
892}
893
894async fn systemd_config(
897 session: &Session,
898 app_path: &Path,
899) -> Result<SystemdUnitConfig, PipelineError> {
900 let binary = crate::compile::utils::binary_name(app_path)
901 .await
902 .map_err(PipelineError::Codegen)?;
903 let deploy_root = &session.config.profile.deploy_root;
904 Ok(SystemdUnitConfig {
905 unit_name: "consortium-app".to_string(),
906 exec_start: format!("{deploy_root}/app/{binary}"),
907 working_directory: format!("{deploy_root}/app"),
908 description: None,
909 restart: SystemdRestartPolicy::OnFailure,
910 })
911}
912
913async fn write_generated_module(
918 project_dir: &Path,
919 source: &str,
920) -> Result<PathBuf, PipelineError> {
921 let src_dir = project_dir.join("src");
922 tokio::fs::create_dir_all(&src_dir)
923 .await
924 .map_err(|source| {
925 consortium_log::trace!("failed to create src dir {}", src_dir.display());
926 PipelineError::GeneratedWrite {
927 path: src_dir.display().to_string(),
928 source,
929 }
930 })?;
931 let path = src_dir.join(GENERATED_MODULE);
932 consortium_log::trace!(
933 "writing {} bytes of generated code to {}",
934 source.len(),
935 path.display()
936 );
937 tokio::fs::write(&path, source).await.map_err(|source| {
938 consortium_log::trace!("failed to write generated module {}", path.display());
939 PipelineError::GeneratedWrite {
940 path: path.display().to_string(),
941 source,
942 }
943 })?;
944 Ok(path)
945}
946
947async fn write_generated_memory_x(
949 config: &crate::compile::firmware::FirmwareConfiguration,
950) -> Result<PathBuf, PipelineError> {
951 let layout = config.remoteproc.as_ref().ok_or_else(|| {
952 PipelineError::Codegen(format!(
953 "firmware endpoint {} has no remoteproc memory contract",
954 config.path.display()
955 ))
956 })?;
957 let source = layout
958 .linker_memory()
959 .map_err(|error| PipelineError::Codegen(error.to_string()))?
960 .render();
961 let path = config.path.join(GENERATED_MEMORY_X);
962 tokio::fs::write(&path, source)
963 .await
964 .map_err(|source| PipelineError::GeneratedWrite {
965 path: path.display().to_string(),
966 source,
967 })?;
968 Ok(path)
969}
970
971#[cfg(test)]
972mod tests {
973 use super::*;
974
975 const STM32: &str = include_str!("../../../consortium-cfg/Consortium.example.stm32mp257.toml");
976
977 fn fabricate_project(tag: &str, manifest: &str) -> PathBuf {
981 let root =
982 std::env::temp_dir().join(format!("consortium-pipeline-{tag}-{}", std::process::id()));
983 let _ = std::fs::remove_dir_all(&root);
984 std::fs::create_dir_all(&root).unwrap();
985 std::fs::write(root.join("Consortium.toml"), manifest).unwrap();
986 for (dir, pkg) in [
987 ("app", "melt-pot-app"),
988 ("ta", "melt-pot-ta"),
989 ("firmware", "melt-pot-mcu"),
990 ("shared", "melt-pot-shared"),
991 ] {
992 let crate_dir = root.join(dir);
993 std::fs::create_dir_all(&crate_dir).unwrap();
994 std::fs::write(
995 crate_dir.join("Cargo.toml"),
996 format!("[package]\nname = \"{pkg}\"\nversion = \"0.0.0\"\n"),
997 )
998 .unwrap();
999 }
1000 root
1001 }
1002
1003 fn unified(manifest: &str) -> String {
1004 manifest.replace(
1005 "deploy_root = \"/opt/consortium\"",
1006 "deploy_root = \"/opt/consortium\"\nworkspace = \"unified\"",
1007 )
1008 }
1009
1010 #[test]
1011 fn unified_workspace_requires_a_root_workspace_manifest() {
1012 let manifest = unified(STM32);
1013 let root = fabricate_project("unified-missing", &manifest);
1014 let session = Session::from_toml(&manifest).unwrap();
1015
1016 let err = resolve_workspace(&session, &root).expect_err("no root Cargo.toml");
1017 assert!(matches!(
1018 err,
1019 PipelineError::UnifiedWorkspace(_, "does not exist")
1020 ));
1021
1022 std::fs::write(
1024 root.join("Cargo.toml"),
1025 "[package]\nname = \"x\"\nversion = \"0.0.0\"\n",
1026 )
1027 .unwrap();
1028 let err = resolve_workspace(&session, &root).expect_err("no [workspace]");
1029 assert!(matches!(
1030 err,
1031 PipelineError::UnifiedWorkspace(_, "has no [workspace] table")
1032 ));
1033
1034 let _ = std::fs::remove_dir_all(&root);
1035 }
1036
1037 #[tokio::test]
1038 async fn unified_workspace_builds_into_the_manifest_target() {
1039 let manifest = unified(STM32);
1040 let root = fabricate_project("unified", &manifest);
1041 std::fs::write(
1042 root.join("Cargo.toml"),
1043 "[workspace]\nmembers = [\"app\", \"firmware\"]\n",
1044 )
1045 .unwrap();
1046 let session = Session::from_toml(&manifest).unwrap();
1047
1048 let workspace = resolve_workspace(&session, &root).expect("workspace root present");
1049 assert_eq!(workspace.layout, Workspace::Unified);
1050 let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
1051 let firmware = firmware::plan_firmware_builds(
1052 &session,
1053 &workspace,
1054 CargoOptions::default(),
1055 &ty_crate,
1056 )
1057 .unwrap();
1058 let elf = firmware::firmware_elf_path(&firmware[0].config)
1059 .await
1060 .unwrap();
1061 assert_eq!(
1062 elf,
1063 root.join("target/thumbv8m.main-none-eabihf/debug/melt-pot-mcu")
1064 );
1065
1066 let dbg_elfs = HashMap::from([(
1067 firmware[0].core,
1068 crate::compile::firmware::original_elf_path(&elf),
1069 )]);
1070 let application = application::plan_application_build(
1071 &session,
1072 &workspace,
1073 None,
1074 &dbg_elfs,
1075 CargoOptions::default(),
1076 &ty_crate,
1077 )
1078 .unwrap();
1079 assert!(
1080 application
1081 .generated
1082 .contains("::core::env!(\"CSTI_MANIFEST_PATH\")")
1083 );
1084 assert!(
1085 application
1086 .generated
1087 .contains("\"/target/thumbv8m.main-none-eabihf/debug/melt-pot-mcu_original.elf\"")
1088 );
1089
1090 let _ = std::fs::remove_dir_all(&root);
1091 }
1092
1093 #[tokio::test]
1094 async fn plan_threads_all_stages() {
1095 let manifest = format!("{STM32}\n[dts]\nbase = \"test-board\"\nmode = \"merge\"\n");
1096 let root = fabricate_project("plan", &manifest);
1097 let vendor = root.join("linux/arch/arm64/boot/dts/st");
1098 std::fs::create_dir_all(&vendor).unwrap();
1099 std::fs::write(
1100 vendor.join("test-board.dts"),
1101 r#"/dts-v1/;
1102 / {
1103 model = "Pipeline test board";
1104 m33: remoteproc-m33 {
1105 compatible = "st,stm32mp2-m33";
1106 };
1107 };"#,
1108 )
1109 .unwrap();
1110 let session = Session::from_toml(&manifest).unwrap();
1111 let config = PipelineConfig {
1112 manifest_path: root.join("Consortium.toml"),
1113 dist_root: root.join("dist"),
1114 linux: root.join("linux"),
1115 release: true,
1116 offline: false,
1117 hmi: Some(HmiConfiguration {
1118 engine: HmiEngine::Cog,
1119 app: None,
1120 project_dir: root.join("website"),
1121 dist_dir: root.join("website/dist"),
1122 command: crate::compile::web::HmiBuildCommand::Custom(vec!["true".into()]),
1123 }),
1124 tee_toolchain: Some(TeeToolchain {
1125 ta_dev_kit_dir: PathBuf::from("/opt/optee/export-ta_arm64"),
1126 optee_signer: PathBuf::from("/opt/optee/scripts/sign_encrypt.py"),
1127 linker: None,
1128 }),
1129 sign_tee: true,
1130 strict_remoteproc: true,
1131 bitbake: Vec::new(),
1132 };
1133
1134 let plan = plan(&session, &config).await.expect("plan succeeds");
1135
1136 assert_eq!(
1139 plan.ty_crate.segments.last().unwrap().ident.to_string(),
1140 "melt_pot_shared"
1141 );
1142
1143 assert_eq!(plan.firmware.len(), 1);
1146 let cm33 = VisibleCore::Mcore("cm33".parse().unwrap());
1147 let expected_elf =
1149 root.join("./firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu");
1150 assert_eq!(plan.firmware_outputs[&cm33], expected_elf);
1151
1152 let original_elf =
1155 std::path::absolute(crate::compile::firmware::original_elf_path(&expected_elf))
1156 .unwrap();
1157 assert_eq!(plan.dbg_elfs[&cm33], original_elf);
1158
1159 assert!(plan.application.generated.contains("include_bytes!"));
1166 assert!(
1167 plan.application
1168 .generated
1169 .contains("::core::env!(\"CSTI_MCU_PATH\")")
1170 );
1171 let relative = original_elf.strip_prefix(root.join("firmware")).unwrap();
1172 assert!(
1173 plan.application
1174 .generated
1175 .contains(&format!("\"/{}\"", relative.display()))
1176 );
1177 let workspace = plan.application.config.workspace.as_ref().unwrap();
1178 assert_eq!(
1179 workspace.mcu_dir.as_deref(),
1180 Some(root.join("firmware").as_path())
1181 );
1182 assert_eq!(plan.application.config.target, "aarch64-unknown-linux-gnu");
1183 assert_eq!(
1184 plan.application.config.hmi.as_ref().unwrap(),
1185 &crate::compile::env::HmiConfig::Cog {
1186 dist: "/opt/consortium/www".to_string(),
1187 debug_url: Some("http://127.0.0.1:5173/".to_string()),
1188 }
1189 );
1190
1191 assert!(plan.ta.is_some());
1193
1194 assert_eq!(plan.systemd.exec_start, "/opt/consortium/app/melt-pot-app");
1196
1197 assert_eq!(
1201 plan.dts_files
1202 .iter()
1203 .map(|(n, _)| n.as_str())
1204 .collect::<Vec<_>>(),
1205 vec!["consortium-test-board.dts"]
1206 );
1207 assert!(plan.dts_files[0].1.contains("compatible = \"generic-uio\""));
1209 let kconfig = plan
1210 .kernel_files
1211 .iter()
1212 .find(|(n, _)| n == dts::KCONFIG_NAME)
1213 .expect("consolidated kconfig fragment planned");
1214 assert!(kconfig.1.contains("CONFIG_UIO_PDRV_GENIRQ=y"));
1215 assert!(kconfig.1.contains("CONFIG_UIO_DMEM_GENIRQ=y"));
1216 assert!(kconfig.1.contains("CONFIG_REMOTEPROC=y"));
1217 assert!(kconfig.1.contains("CONFIG_STM32_RPROC=y"));
1218 assert_eq!(
1219 plan.kernel_files
1220 .iter()
1221 .filter(|(name, _)| name.ends_with(".cfg"))
1222 .count(),
1223 1
1224 );
1225 assert!(kconfig.1.contains("CONFIG_OF_OVERLAY=y"));
1226 assert!(kconfig.1.contains("CONFIG_OF_CONFIGFS=y"));
1227 assert!(plan.boot_files.iter().any(|(n, _)| n == "extlinux-uio.cfg"));
1229 assert!(
1231 !plan
1232 .kernel_files
1233 .iter()
1234 .any(|(n, _)| n.ends_with(".bbappend"))
1235 );
1236
1237 let _ = std::fs::remove_dir_all(&root);
1238 }
1239}