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