pub mod application;
pub mod bitbake;
pub mod check;
pub mod dist;
pub mod dtc;
pub mod dts;
pub mod elf_map;
pub mod fetch;
pub mod firmware;
pub mod tee;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use consortium_cfg::Session;
use consortium_cfg_common::config::{HmiEngine, VisibleCore, Workspace, XcoreName};
use consortium_cfg_common::database::Coprocessor;
use thiserror::Error;
use crate::compile::application::{ApplicationCompileError, ApplicationCompiler};
use crate::compile::deploy::systemd::{self, SystemdRestartPolicy, SystemdUnitConfig};
use crate::compile::env::CstiWorkspace;
use crate::compile::firmware::{FirmwareCompileError, FirmwareCompiler};
use crate::compile::optee::{OpteeCompileError, OpteeCompiler, OpteeConfiguration};
use crate::compile::web::{HmiCompileError, HmiCompiler, HmiConfiguration};
use crate::progress::{BuildReporter, BuildStep};
use application::ApplicationBuildPlan;
use bitbake::{BitbakeAction, BitbakeError, RecipeSpec};
use dist::{DistAssemblyError, DistLayout};
use firmware::FirmwareBuildPlan;
use tee::TeeToolchain;
const GENERATED_MODULE: &str = "consortium.gen.rs";
const GENERATED_MEMORY_X: &str = "memory.x";
#[derive(Error, Debug)]
pub enum PipelineError {
#[error("reading manifest {path}: {source}")]
ManifestRead {
path: String,
source: std::io::Error,
},
#[error(
"[profile] workspace = \"unified\" requires a workspace root Cargo.toml beside the manifest, but {0} {1}"
)]
UnifiedWorkspace(String, &'static str),
#[error(transparent)]
Cfg(#[from] Box<consortium_cfg::CfgError>),
#[error(transparent)]
Firmware(#[from] FirmwareCompileError),
#[error(transparent)]
Application(#[from] ApplicationCompileError),
#[error(transparent)]
Optee(#[from] OpteeCompileError),
#[error(transparent)]
Hmi(#[from] HmiCompileError),
#[error(transparent)]
Dist(#[from] DistAssemblyError),
#[error(transparent)]
Dtc(#[from] dtc::DtcError),
#[error(transparent)]
Bitbake(#[from] BitbakeError),
#[error("writing generated module to {path}: {source}")]
GeneratedWrite {
path: String,
source: std::io::Error,
},
#[error(
"debug core '{0}' has no compiled firmware to embed: `[dbg.{0}]` names a \
core that is not a remote firmware target"
)]
DbgCoreNotBuilt(String),
#[error("manifest declares no endpoint for the required party '{0}'")]
MissingEndpoint(String),
#[error(
"manifest has a [tee] section but no OP-TEE toolchain was provided \
(set TA_DEV_KIT_DIR or pass a toolchain)"
)]
MissingTeeToolchain,
#[error("unknown core '{0}'")]
UnknownCore(String),
#[error("codegen error: {0}")]
Codegen(String),
#[error("fetching dependencies for {label} ({target}): {message}")]
Fetch {
label: String,
target: String,
message: String,
},
#[error("installing rust target(s) {targets}: {message}")]
RustTarget {
targets: String,
message: String,
},
#[error("installing HMI dependencies in {dir}: {message}")]
HmiFetch {
dir: String,
message: String,
},
}
impl From<consortium_cfg::CfgError> for PipelineError {
fn from(error: consortium_cfg::CfgError) -> Self {
Self::Cfg(Box::new(error))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CargoOptions {
pub release: bool,
pub offline: bool,
}
pub struct PipelineConfig {
pub manifest_path: PathBuf,
pub dist_root: PathBuf,
pub linux: PathBuf,
pub release: bool,
pub offline: bool,
pub hmi: Option<HmiConfiguration>,
pub tee_toolchain: Option<TeeToolchain>,
pub sign_tee: bool,
pub strict_remoteproc: bool,
pub bitbake: Vec<BitbakeAction>,
}
impl PipelineConfig {
pub fn cargo(&self) -> CargoOptions {
CargoOptions {
release: self.release,
offline: self.offline,
}
}
}
pub struct PipelinePlan {
pub manifest_dir: PathBuf,
pub ty_crate: syn::Path,
pub firmware: Vec<FirmwareBuildPlan>,
pub firmware_outputs: HashMap<VisibleCore, PathBuf>,
pub dbg_elfs: HashMap<VisibleCore, PathBuf>,
pub hmi: Option<HmiConfiguration>,
pub hmi_dist: Option<PathBuf>,
pub ta: Option<OpteeConfiguration>,
pub application: ApplicationBuildPlan,
pub dts_files: Vec<(String, String)>,
pub dts_source_closure: Vec<(String, String)>,
pub kernel_files: Vec<(String, String)>,
pub boot_files: Vec<(String, String)>,
pub systemd: SystemdUnitConfig,
}
pub async fn load_session(manifest_path: &Path) -> Result<Session, PipelineError> {
consortium_log::trace!("loading manifest {}", manifest_path.display());
let toml = tokio::fs::read_to_string(manifest_path)
.await
.map_err(|source| {
consortium_log::trace!("manifest read failed: {}", manifest_path.display());
PipelineError::ManifestRead {
path: manifest_path.display().to_string(),
source,
}
})?;
Ok(Session::from_toml(&toml)?)
}
pub fn resolve_workspace(
session: &Session,
manifest_dir: &Path,
) -> Result<CstiWorkspace, PipelineError> {
let absolute = |p: &Path| std::path::absolute(p).unwrap_or_else(|_| p.to_path_buf());
let manifest_dir = absolute(manifest_dir);
let layout = session.config.profile.workspace;
if layout == Workspace::Unified {
let cargo_toml = manifest_dir.join("Cargo.toml");
let shown = cargo_toml.display().to_string();
let contents = std::fs::read_to_string(&cargo_toml)
.map_err(|_| PipelineError::UnifiedWorkspace(shown.clone(), "does not exist"))?;
let is_workspace = contents
.parse::<toml::Table>()
.is_ok_and(|table| table.contains_key("workspace"));
if !is_workspace {
return Err(PipelineError::UnifiedWorkspace(
shown,
"has no [workspace] table",
));
}
}
let firmware = firmware::firmware_endpoints(session)?;
let mcu_dir = match firmware.as_slice() {
[(_, _, root)] => Some(absolute(&manifest_dir.join(root))),
_ => None,
};
Ok(CstiWorkspace {
layout,
manifest_dir,
mcu_dir,
})
}
pub fn derive_hmi_configuration(
session: &Session,
manifest_dir: &Path,
) -> Result<Option<HmiConfiguration>, PipelineError> {
let Some(hmi) = &session.config.hmi else {
return Ok(None);
};
hmi.validate().map_err(PipelineError::Codegen)?;
let project_dir = manifest_dir.join(&hmi.source);
let dist_dir = manifest_dir.join(&hmi.build);
let command = match hmi.engine {
HmiEngine::Cog => {
crate::compile::utils::resolve_hmi_build_command().map_err(PipelineError::Codegen)?
}
HmiEngine::Pocket => {
let app = hmi.app.as_deref().unwrap_or_default();
crate::compile::utils::resolve_pocket_build_command(app, &dist_dir)
.map_err(PipelineError::Codegen)?
}
};
Ok(Some(HmiConfiguration {
engine: hmi.engine,
app: hmi.app.clone(),
project_dir,
dist_dir,
command,
}))
}
pub async fn plan(
session: &Session,
config: &PipelineConfig,
) -> Result<PipelinePlan, PipelineError> {
let manifest_dir = config
.manifest_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let ty_crate = resolve_ty_crate(session, &manifest_dir).await?;
let cargo = config.cargo();
let workspace = resolve_workspace(session, &manifest_dir)?;
let mut firmware = firmware::plan_firmware_builds(session, &workspace, cargo, &ty_crate)?;
for plan in &mut firmware {
plan.config.strict_remoteproc = config.strict_remoteproc;
}
let mut firmware_outputs = HashMap::new();
for plan in &firmware {
let elf = firmware::firmware_elf_path(&plan.config).await?;
firmware_outputs.insert(plan.core, elf);
}
let ir = session.lower()?;
let dbg_sources: HashMap<VisibleCore, PathBuf> = firmware_outputs
.iter()
.map(|(core, raw)| (*core, crate::compile::firmware::original_elf_path(raw)))
.collect();
let dbg_elfs = elf_map::resolve_dbg_elf_paths(&dbg_sources, ir.dbg.rings().keys().copied())?;
let hmi_dist = config.hmi.as_ref().map(|h| h.dist_dir.clone());
let ta = tee::plan_ta_build(
session,
&workspace,
config.tee_toolchain.as_ref(),
cargo,
config.sign_tee,
)?;
let application = application::plan_application_build(
session,
&workspace,
hmi_dist.as_deref(),
&dbg_elfs,
cargo,
&ty_crate,
)?;
let profile = &session.config.profile;
let dts_cfg = &session.config.dts;
let target_cores: Vec<String> = firmware.iter().map(|p| p.core.to_string()).collect();
let (dts_files, dts_source_closure) = match dts::render_linux_dts(
&profile.chip,
profile.devkit.as_deref(),
dts_cfg.base.as_deref(),
dts_cfg.mode,
&ir,
&target_cores,
&config.linux,
config.strict_remoteproc,
)? {
Some(rendered) => {
consortium_log::trace!(
"rendered linux dts {} with {} warnings",
rendered.file_name,
rendered.warnings.len()
);
for warning in &rendered.warnings {
consortium_log::trace!("dts warning: {}", warning);
eprintln!("warning: {warning}");
}
(
vec![(rendered.file_name, rendered.contents)],
rendered.source_closure,
)
}
None => {
consortium_log::trace!("no UIO or remoteproc devices; no linux dts rendered");
(Vec::new(), Vec::new())
}
};
let mut kernel_files = dts::render_kernel_kconfig(&profile.chip, &ir)
.map(|contents| vec![(dts::KCONFIG_NAME.to_string(), contents)])
.unwrap_or_default();
if let Some(kernel_recipe) = &session.config.profile.kernel_recipe {
let fragment_names: Vec<String> = kernel_files.iter().map(|(n, _)| n.clone()).collect();
if let Some(bbappend) = dts::render_kernel_bbappend(&fragment_names) {
kernel_files.push((dts::kernel_bbappend_name(kernel_recipe), bbappend));
}
}
let boot_files = dts::render_boot(&profile.chip, &ir);
let systemd = systemd_config(session, &application.config.path).await?;
Ok(PipelinePlan {
manifest_dir,
ty_crate,
firmware,
firmware_outputs,
dbg_elfs,
hmi: config.hmi.clone(),
hmi_dist,
ta,
application,
dts_files,
dts_source_closure,
kernel_files,
boot_files,
systemd,
})
}
pub async fn run(config: PipelineConfig) -> Result<PathBuf, PipelineError> {
run_inner(config, None).await
}
pub async fn run_with_reporter(
config: PipelineConfig,
reporter: &dyn BuildReporter,
) -> Result<PathBuf, PipelineError> {
run_inner(config, Some(reporter)).await
}
async fn run_inner(
config: PipelineConfig,
reporter: Option<&dyn BuildReporter>,
) -> Result<PathBuf, PipelineError> {
consortium_log::trace!("pipeline run: manifest {}", config.manifest_path.display());
let session = load_session(&config.manifest_path).await?;
let plan = plan(&session, &config).await?;
consortium_log::trace!(
"pipeline plan: {} firmware, ta={}, hmi={}, {} bitbake",
plan.firmware.len(),
plan.ta.is_some(),
config.hmi.is_some(),
config.bitbake.len()
);
let total_steps = plan.firmware.len()
+ usize::from(config.hmi.is_some())
+ usize::from(plan.ta.is_some())
+ 2
+ config.bitbake.len();
let mut steps = StepRunner::new(reporter, total_steps);
let mut firmware_outputs = HashMap::new();
let mut firmware_originals = HashMap::new();
for fw in &plan.firmware {
let detail = format!("{} ({})", fw.core, fw.config.target);
let (deploy, original) = steps
.run("Build firmware", Some(&detail), async {
write_generated_module(&fw.config.path, &fw.generated).await?;
write_generated_memory_x(&fw.config).await?;
let artifacts =
FirmwareCompiler::build_with_reporter(fw.config.clone(), reporter).await?;
Ok((artifacts.deploy, artifacts.original))
})
.await?;
firmware_outputs.insert(fw.core, deploy);
firmware_originals.insert(fw.core, original);
}
let hmi_dist = match &config.hmi {
Some(hmi) => {
let detail = hmi.project_dir.display().to_string();
Some(
steps
.run("Build HMI", Some(&detail), async {
let dist = HmiCompiler::build_with_reporter(hmi.clone(), reporter).await?;
Ok(dist)
})
.await?,
)
}
None => None,
};
let ta = match plan.ta {
Some(ta) => {
let detail = if ta.sign {
"signed trusted application"
} else {
"unsigned trusted application"
};
Some(
steps
.run("Build TEE", Some(detail), async {
let ta = OpteeCompiler::build_with_reporter(ta, reporter).await?;
Ok(ta)
})
.await?,
)
}
None => None,
};
let app_detail = plan.application.config.target.clone();
let application = steps
.run("Build application", Some(&app_detail), async {
write_generated_module(&plan.application.config.path, &plan.application.generated)
.await?;
let app =
ApplicationCompiler::build_with_reporter(plan.application.config.clone(), reporter)
.await?;
Ok(app)
})
.await?;
let systemd_units = vec![(
plan.systemd.file_name(),
systemd::render_unit(&plan.systemd),
)];
let dist_detail = config.dist_root.display().to_string();
let layout = DistLayout::new(&config.dist_root);
steps
.run("Assemble dist", Some(&dist_detail), async {
let inputs = dist::DistAssemblyInputs {
firmware: &firmware_outputs,
firmware_originals: &firmware_originals,
application: &application,
ta: ta.as_deref(),
hmi: config
.hmi
.as_ref()
.zip(hmi_dist.as_deref())
.map(|(cfg, dist)| dist::HmiStaging {
engine: cfg.engine,
app: cfg.app.as_deref(),
dist,
}),
dts_files: &plan.dts_files,
kconfig_fragments: &plan.kernel_files,
boot_files: &plan.boot_files,
systemd_units: &systemd_units,
};
dist::assemble(&layout, inputs).await?;
Ok(())
})
.await?;
if let Some((dts_name, _)) = plan.dts_files.first() {
let arch = dtc::arch_dir(&plan.application.config.target);
let dts_dir = layout.dts_linux_dir();
let detail = format!("dtc {dts_name}");
steps
.run("Compile device tree", Some(&detail), async {
let out = dtc::compile_device_tree(
&dts_dir,
dts_name,
&plan.dts_source_closure,
&config.linux,
arch,
)
.await?;
Ok(out)
})
.await?;
}
for action in &config.bitbake {
let recipe_spec = match action {
BitbakeAction::InvokeBitbake {
recipe, dry_run, ..
} if *dry_run => Some(recipe_spec(
recipe,
&session.config.profile.deploy_root,
&application,
&firmware_outputs,
ta.as_deref(),
hmi_dist.as_deref(),
&plan.systemd.file_name(),
&plan.dts_files,
&plan.boot_files,
)?),
_ => None,
};
let (label, detail) = match action {
BitbakeAction::CopyIntoRecipe { recipe_files_dir } => (
"Stage into recipe",
format!("copy to {}", recipe_files_dir.display()),
),
BitbakeAction::InvokeBitbake {
build_dir,
recipe,
dry_run,
} => {
let dry = if *dry_run {
" (dry-run, recipe only)"
} else {
""
};
(
"Run bitbake",
format!("{recipe} in {}{dry}", build_dir.display()),
)
}
};
steps
.run(label, Some(&detail), async {
bitbake::run_with_reporter(
action,
&config.dist_root,
recipe_spec.as_ref(),
reporter,
)
.await?;
Ok(())
})
.await?;
}
Ok(config.dist_root)
}
#[allow(clippy::too_many_arguments)]
fn recipe_spec(
recipe_name: &str,
deploy_root: &str,
application: &Path,
firmware_outputs: &HashMap<VisibleCore, PathBuf>,
ta: Option<&Path>,
hmi_dist: Option<&Path>,
systemd_unit: &str,
dts_files: &[(String, String)],
boot_files: &[(String, String)],
) -> Result<RecipeSpec, PipelineError> {
let file_name = |path: &Path| -> Result<String, PipelineError> {
path.file_name()
.and_then(|n| n.to_str())
.map(str::to_string)
.ok_or_else(|| PipelineError::Codegen(format!("no file name for {}", path.display())))
};
let firmware = firmware_outputs
.keys()
.map(|core| format!("{core}.elf"))
.collect();
Ok(RecipeSpec {
recipe_name: recipe_name.to_string(),
deploy_root: deploy_root.to_string(),
app_bin: file_name(application)?,
firmware,
ta: ta.map(file_name).transpose()?,
has_www: hmi_dist.is_some(),
systemd_unit: systemd_unit.to_string(),
dts_files: dts_files.iter().map(|(name, _)| name.clone()).collect(),
boot_files: boot_files.iter().map(|(name, _)| name.clone()).collect(),
})
}
struct StepRunner<'a> {
reporter: Option<&'a dyn BuildReporter>,
next: usize,
total: usize,
}
impl<'a> StepRunner<'a> {
fn new(reporter: Option<&'a dyn BuildReporter>, total: usize) -> Self {
Self {
reporter,
next: 0,
total,
}
}
async fn run<T, F>(
&mut self,
name: &str,
detail: Option<&str>,
future: F,
) -> Result<T, PipelineError>
where
F: std::future::Future<Output = Result<T, PipelineError>>,
{
self.next += 1;
let step = BuildStep {
index: self.next,
total: self.total,
name,
detail,
};
if let Some(reporter) = self.reporter {
reporter.step_started(step);
}
let result = future.await;
if let Some(reporter) = self.reporter {
match &result {
Ok(_) => reporter.step_succeeded(step),
Err(_) => reporter.step_failed(step),
}
}
result
}
}
pub(crate) fn visible_core_of(cp: &Coprocessor) -> Result<VisibleCore, PipelineError> {
cp.name
.parse()
.map_err(|_| PipelineError::UnknownCore(cp.name.clone()))
}
pub(crate) fn core_rust_triple(core: VisibleCore) -> String {
match core {
VisibleCore::Mcore(name) => name.rust_triple(),
VisibleCore::Rcore(name) => name.rust_triple(),
VisibleCore::Rvcore(name) => name.rust_triple(),
VisibleCore::Ap => "aarch64-unknown-linux-gnu".to_string(),
}
}
async fn resolve_ty_crate(
session: &Session,
manifest_dir: &Path,
) -> Result<syn::Path, PipelineError> {
let shared_dir = manifest_dir.join(&session.config.profile.shared);
let name = match crate::compile::utils::binary_name(&shared_dir).await {
Ok(name) => name,
Err(_) => shared_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("shared")
.to_string(),
};
let ident = name.replace('-', "_");
syn::parse_str(&format!("::{ident}"))
.map_err(|e| PipelineError::Codegen(format!("bad shared crate name '{ident}': {e}")))
}
async fn systemd_config(
session: &Session,
app_path: &Path,
) -> Result<SystemdUnitConfig, PipelineError> {
let binary = crate::compile::utils::binary_name(app_path)
.await
.map_err(PipelineError::Codegen)?;
let deploy_root = &session.config.profile.deploy_root;
Ok(SystemdUnitConfig {
unit_name: "consortium-app".to_string(),
exec_start: format!("{deploy_root}/app/{binary}"),
working_directory: format!("{deploy_root}/app"),
description: None,
restart: SystemdRestartPolicy::OnFailure,
})
}
async fn write_generated_module(
project_dir: &Path,
source: &str,
) -> Result<PathBuf, PipelineError> {
let src_dir = project_dir.join("src");
tokio::fs::create_dir_all(&src_dir)
.await
.map_err(|source| {
consortium_log::trace!("failed to create src dir {}", src_dir.display());
PipelineError::GeneratedWrite {
path: src_dir.display().to_string(),
source,
}
})?;
let path = src_dir.join(GENERATED_MODULE);
consortium_log::trace!(
"writing {} bytes of generated code to {}",
source.len(),
path.display()
);
tokio::fs::write(&path, source).await.map_err(|source| {
consortium_log::trace!("failed to write generated module {}", path.display());
PipelineError::GeneratedWrite {
path: path.display().to_string(),
source,
}
})?;
Ok(path)
}
async fn write_generated_memory_x(
config: &crate::compile::firmware::FirmwareConfiguration,
) -> Result<PathBuf, PipelineError> {
let layout = config.remoteproc.as_ref().ok_or_else(|| {
PipelineError::Codegen(format!(
"firmware endpoint {} has no remoteproc memory contract",
config.path.display()
))
})?;
let source = layout
.linker_memory()
.map_err(|error| PipelineError::Codegen(error.to_string()))?
.render();
let path = config.path.join(GENERATED_MEMORY_X);
tokio::fs::write(&path, source)
.await
.map_err(|source| PipelineError::GeneratedWrite {
path: path.display().to_string(),
source,
})?;
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
const STM32: &str = include_str!("../../../consortium-cfg/Consortium.example.stm32mp257.toml");
fn fabricate_project(tag: &str, manifest: &str) -> PathBuf {
let root =
std::env::temp_dir().join(format!("consortium-pipeline-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("Consortium.toml"), manifest).unwrap();
for (dir, pkg) in [
("app", "melt-pot-app"),
("ta", "melt-pot-ta"),
("firmware", "melt-pot-mcu"),
("shared", "melt-pot-shared"),
] {
let crate_dir = root.join(dir);
std::fs::create_dir_all(&crate_dir).unwrap();
std::fs::write(
crate_dir.join("Cargo.toml"),
format!("[package]\nname = \"{pkg}\"\nversion = \"0.0.0\"\n"),
)
.unwrap();
}
root
}
fn unified(manifest: &str) -> String {
manifest.replace(
"deploy_root = \"/opt/consortium\"",
"deploy_root = \"/opt/consortium\"\nworkspace = \"unified\"",
)
}
#[test]
fn unified_workspace_requires_a_root_workspace_manifest() {
let manifest = unified(STM32);
let root = fabricate_project("unified-missing", &manifest);
let session = Session::from_toml(&manifest).unwrap();
let err = resolve_workspace(&session, &root).expect_err("no root Cargo.toml");
assert!(matches!(
err,
PipelineError::UnifiedWorkspace(_, "does not exist")
));
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"x\"\nversion = \"0.0.0\"\n",
)
.unwrap();
let err = resolve_workspace(&session, &root).expect_err("no [workspace]");
assert!(matches!(
err,
PipelineError::UnifiedWorkspace(_, "has no [workspace] table")
));
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn unified_workspace_builds_into_the_manifest_target() {
let manifest = unified(STM32);
let root = fabricate_project("unified", &manifest);
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"app\", \"firmware\"]\n",
)
.unwrap();
let session = Session::from_toml(&manifest).unwrap();
let workspace = resolve_workspace(&session, &root).expect("workspace root present");
assert_eq!(workspace.layout, Workspace::Unified);
let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
let firmware = firmware::plan_firmware_builds(
&session,
&workspace,
CargoOptions::default(),
&ty_crate,
)
.unwrap();
let elf = firmware::firmware_elf_path(&firmware[0].config)
.await
.unwrap();
assert_eq!(
elf,
root.join("target/thumbv8m.main-none-eabihf/debug/melt-pot-mcu")
);
let dbg_elfs = HashMap::from([(
firmware[0].core,
crate::compile::firmware::original_elf_path(&elf),
)]);
let application = application::plan_application_build(
&session,
&workspace,
None,
&dbg_elfs,
CargoOptions::default(),
&ty_crate,
)
.unwrap();
assert!(
application
.generated
.contains("::core::env!(\"CSTI_MANIFEST_PATH\")")
);
assert!(
application
.generated
.contains("\"/target/thumbv8m.main-none-eabihf/debug/melt-pot-mcu_original.elf\"")
);
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn plan_threads_all_stages() {
let manifest = format!("{STM32}\n[dts]\nbase = \"test-board\"\nmode = \"merge\"\n");
let root = fabricate_project("plan", &manifest);
let vendor = root.join("linux/arch/arm64/boot/dts/st");
std::fs::create_dir_all(&vendor).unwrap();
std::fs::write(
vendor.join("test-board.dts"),
r#"/dts-v1/;
/ {
model = "Pipeline test board";
m33: remoteproc-m33 {
compatible = "st,stm32mp2-m33";
};
};"#,
)
.unwrap();
let session = Session::from_toml(&manifest).unwrap();
let config = PipelineConfig {
manifest_path: root.join("Consortium.toml"),
dist_root: root.join("dist"),
linux: root.join("linux"),
release: true,
offline: false,
hmi: Some(HmiConfiguration {
engine: HmiEngine::Cog,
app: None,
project_dir: root.join("website"),
dist_dir: root.join("website/dist"),
command: crate::compile::web::HmiBuildCommand::Custom(vec!["true".into()]),
}),
tee_toolchain: Some(TeeToolchain {
ta_dev_kit_dir: PathBuf::from("/opt/optee/export-ta_arm64"),
optee_signer: PathBuf::from("/opt/optee/scripts/sign_encrypt.py"),
linker: None,
}),
sign_tee: true,
strict_remoteproc: true,
bitbake: Vec::new(),
};
let plan = plan(&session, &config).await.expect("plan succeeds");
assert_eq!(
plan.ty_crate.segments.last().unwrap().ident.to_string(),
"melt_pot_shared"
);
assert_eq!(plan.firmware.len(), 1);
let cm33 = VisibleCore::Mcore("cm33".parse().unwrap());
let expected_elf =
root.join("./firmware/target/thumbv8m.main-none-eabihf/release/melt-pot-mcu");
assert_eq!(plan.firmware_outputs[&cm33], expected_elf);
let original_elf =
std::path::absolute(crate::compile::firmware::original_elf_path(&expected_elf))
.unwrap();
assert_eq!(plan.dbg_elfs[&cm33], original_elf);
assert!(plan.application.generated.contains("include_bytes!"));
assert!(
plan.application
.generated
.contains("::core::env!(\"CSTI_MCU_PATH\")")
);
let relative = original_elf.strip_prefix(root.join("firmware")).unwrap();
assert!(
plan.application
.generated
.contains(&format!("\"/{}\"", relative.display()))
);
let workspace = plan.application.config.workspace.as_ref().unwrap();
assert_eq!(
workspace.mcu_dir.as_deref(),
Some(root.join("firmware").as_path())
);
assert_eq!(plan.application.config.target, "aarch64-unknown-linux-gnu");
assert_eq!(
plan.application.config.hmi.as_ref().unwrap(),
&crate::compile::env::HmiConfig::Cog {
dist: "/opt/consortium/www".to_string(),
debug_url: Some("http://127.0.0.1:5173/".to_string()),
}
);
assert!(plan.ta.is_some());
assert_eq!(plan.systemd.exec_start, "/opt/consortium/app/melt-pot-app");
assert_eq!(
plan.dts_files
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>(),
vec!["consortium-test-board.dts"]
);
assert!(plan.dts_files[0].1.contains("compatible = \"generic-uio\""));
let kconfig = plan
.kernel_files
.iter()
.find(|(n, _)| n == dts::KCONFIG_NAME)
.expect("consolidated kconfig fragment planned");
assert!(kconfig.1.contains("CONFIG_UIO_PDRV_GENIRQ=y"));
assert!(kconfig.1.contains("CONFIG_UIO_DMEM_GENIRQ=y"));
assert!(kconfig.1.contains("CONFIG_REMOTEPROC=y"));
assert!(kconfig.1.contains("CONFIG_STM32_RPROC=y"));
assert_eq!(
plan.kernel_files
.iter()
.filter(|(name, _)| name.ends_with(".cfg"))
.count(),
1
);
assert!(kconfig.1.contains("CONFIG_OF_OVERLAY=y"));
assert!(kconfig.1.contains("CONFIG_OF_CONFIGFS=y"));
assert!(plan.boot_files.iter().any(|(n, _)| n == "extlinux-uio.cfg"));
assert!(
!plan
.kernel_files
.iter()
.any(|(n, _)| n.ends_with(".bbappend"))
);
let _ = std::fs::remove_dir_all(&root);
}
}