Skip to main content

consortium_builder/pipeline/
application.rs

1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Stage 5: compile the Linux application.
18//!
19//! The AP's generated init module embeds each debug ring's firmware ELF via
20//! `include_bytes!`, so this stage substitutes Stage 1's resolved ELF paths into
21//! the debug IR before generating that module. Each path is emitted relative to
22//! the `CSTI_*` directory the application build is given (see
23//! [`embed_locations`](super::elf_map::embed_locations)), so the generated
24//! module carries no build-host path.
25
26use std::collections::HashMap;
27use std::path::{Path, PathBuf};
28
29use consortium_cfg::Session;
30use consortium_cfg_common::config::{AcoreName, Party, VisibleCore};
31
32use super::{CargoOptions, PipelineError, elf_map, ipc_config_for};
33use crate::compile::application::ApplicationConfiguration;
34use crate::compile::env::{CstiWorkspace, HmiConfig};
35
36/// A resolved application build plus its generated AP init module.
37pub struct ApplicationBuildPlan {
38    /// `cargo build` parameters for the Linux application.
39    pub config: ApplicationConfiguration,
40    /// The generated AP init module, rendered to Rust source, to be written into
41    /// the application crate before building. Its `DbgRingSpec`s already embed the
42    /// resolved firmware ELF paths.
43    pub generated: String,
44}
45
46/// Plans the Linux application build.
47///
48/// `hmi_dist` is `Some` when Stage 3 built the frontend; it only gates whether an
49/// [`HmiConfig`] is injected — the *on-target* serve path is derived from
50/// `deploy_root`, not the build-host dist. `resolved_dbg_elfs` maps each debug
51/// core to the absolute firmware ELF the AP embeds.
52pub fn plan_application_build(
53    session: &Session,
54    workspace: &CstiWorkspace,
55    hmi_dist: Option<&Path>,
56    resolved_dbg_elfs: &HashMap<VisibleCore, PathBuf>,
57    cargo: CargoOptions,
58    ty_crate: &syn::Path,
59) -> Result<ApplicationBuildPlan, PipelineError> {
60    let root = session
61        .config
62        .endpoints
63        .get(&Party::AcoreNonSecure)
64        .ok_or_else(|| PipelineError::MissingEndpoint(Party::AcoreNonSecure.to_string()))?;
65    let manifest_dir = &workspace.manifest_dir;
66    let path = manifest_dir.join(root);
67
68    let target = acore_linux_triple(&session.database.ap)?;
69
70    // The HMI carrier points the deployed app at its on-target install path
71    // under `deploy_root` (Cog: `www/`; PocketJS: `hmi/<app>.{js,pak}`), which a
72    // packaging recipe populates from the matching `dist/` subtree.
73    let hmi = match (&session.config.hmi, hmi_dist) {
74        (Some(hmi), Some(_)) => Some(HmiConfig::from_hmi(
75            hmi,
76            &session.config.profile.deploy_root,
77        )),
78        _ => None,
79    };
80
81    let ipc = ipc_config_for(&session.lower()?, VisibleCore::Ap);
82
83    // A relative `[profile] sysroot` is anchored at the manifest directory; an
84    // absolute one is used verbatim.
85    let sysroot = session
86        .config
87        .profile
88        .sysroot
89        .as_ref()
90        .map(|s| manifest_dir.join(s));
91
92    // Substitute the resolved firmware ELF paths into the debug IR, then generate
93    // the AP init module that `include_bytes!`es them.
94    let mut ir = session.lower()?;
95    let embedded = elf_map::embed_locations(resolved_dbg_elfs, workspace);
96    ir.dbg = std::mem::take(&mut ir.dbg).with_resolved_elf_paths(&embedded);
97    consortium_log::trace!("rendering AP init module for target {}", target);
98    let generated =
99        consortium_cfg::render(ir.initialize(VisibleCore::Ap, ty_crate)).map_err(|e| {
100            consortium_log::trace!("AP init codegen render failed: {}", e);
101            PipelineError::Codegen(e.to_string())
102        })?;
103
104    Ok(ApplicationBuildPlan {
105        config: ApplicationConfiguration {
106            path,
107            target,
108            release: cargo.release,
109            offline: cargo.offline,
110            hmi,
111            ipc,
112            // The AP is the debug *host*: it reads rings through the generated UIO
113            // code and the embedded ELF, not via `CONSORTIUM_DBG_*` (those
114            // configure a ring *writer*), so no debug layout is injected here.
115            dbg: None,
116            sysroot,
117            workspace: Some(workspace.clone()),
118        },
119        generated,
120    })
121}
122
123/// The Linux Rust target triple for the AP core.
124///
125/// Derived from the AP core's bitness rather than hardcoded, so a 32-bit A-core
126/// (e.g. Cortex-A7) targets armv7 instead of aarch64. This is the Linux/`gnu`
127/// ABI, distinct from [`AcoreName::as_rust_triple`]'s bare-metal `-none` triples.
128pub(crate) fn acore_linux_triple(ap: &str) -> Result<String, PipelineError> {
129    let name: AcoreName = ap
130        .parse()
131        .map_err(|_| PipelineError::UnknownCore(ap.to_string()))?;
132    Ok(if name.is_64_bit() {
133        "aarch64-unknown-linux-gnu".to_string()
134    } else {
135        "armv7-unknown-linux-gnueabihf".to_string()
136    })
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::pipeline::resolve_workspace;
143
144    const STM32: &str = include_str!("../../../consortium-cfg/Consortium.example.stm32mp257.toml");
145
146    fn cm33() -> VisibleCore {
147        VisibleCore::Mcore("cm33".parse().unwrap())
148    }
149
150    #[test]
151    fn acore_triple_tracks_bitness() {
152        assert_eq!(
153            acore_linux_triple("ca35").unwrap(),
154            "aarch64-unknown-linux-gnu"
155        );
156        assert_eq!(
157            acore_linux_triple("ca7").unwrap(),
158            "armv7-unknown-linux-gnueabihf"
159        );
160        assert!(acore_linux_triple("bogus").is_err());
161    }
162
163    #[test]
164    fn plans_application_with_hmi_and_embedded_dbg() {
165        let session = Session::from_toml(STM32).expect("manifest loads");
166        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
167        let resolved = HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]);
168
169        let plan = plan_application_build(
170            &session,
171            &resolve_workspace(&session, Path::new("/proj")).unwrap(),
172            Some(Path::new("/build/website/dist")),
173            &resolved,
174            CargoOptions {
175                release: true,
176                offline: true,
177            },
178            &ty_crate,
179        )
180        .expect("plan succeeds");
181
182        assert_eq!(plan.config.path, Path::new("/proj/app"));
183        assert_eq!(plan.config.target, "aarch64-unknown-linux-gnu");
184        assert!(plan.config.release);
185        // The shared cargo flags reach the application plan too.
186        assert!(plan.config.offline);
187
188        // The HMI serve path resolves under the deploy root, not the build dist.
189        let hmi = plan.config.hmi.expect("hmi injected");
190        assert_eq!(
191            hmi,
192            HmiConfig::Cog {
193                dist: "/opt/consortium/www".to_string(),
194                debug_url: Some("http://127.0.0.1:5173/".to_string()),
195            }
196        );
197
198        // The generated AP module embeds the resolved firmware ELF.
199        assert!(
200            plan.generated
201                .contains("include_bytes!(\"/build/cm33.elf\")")
202        );
203        // The AP is the host, so no debug writer layout is injected.
204        assert!(plan.config.dbg.is_none());
205
206        // The stm32 example does not declare a sysroot, so none is planned.
207        assert!(plan.config.sysroot.is_none());
208    }
209
210    #[test]
211    fn embeds_debug_elf_relative_to_mcu_path() {
212        // The usual shape: the ELF sits under the nested firmware crate's
213        // `target/`, so the module names it through `CSTI_MCU_PATH` and the build
214        // is given that variable.
215        let session = Session::from_toml(STM32).expect("manifest loads");
216        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
217        let workspace = resolve_workspace(&session, Path::new("/proj")).unwrap();
218        let elf = "/proj/firmware/target/thumbv8m.main-none-eabihf/debug/mcu_original.elf";
219
220        let plan = plan_application_build(
221            &session,
222            &workspace,
223            None,
224            &HashMap::from([(cm33(), PathBuf::from(elf))]),
225            CargoOptions::default(),
226            &ty_crate,
227        )
228        .expect("plan succeeds");
229
230        assert!(!plan.generated.contains("/proj"), "{}", plan.generated);
231        assert!(
232            plan.generated.contains("::core::env!(\"CSTI_MCU_PATH\")"),
233            "{}",
234            plan.generated
235        );
236        assert!(
237            plan.generated
238                .contains("\"/target/thumbv8m.main-none-eabihf/debug/mcu_original.elf\"")
239        );
240        assert_eq!(plan.config.workspace, Some(workspace));
241    }
242
243    #[test]
244    fn relative_profile_sysroot_anchors_at_manifest_dir() {
245        let toml = STM32.replace(
246            "deploy_root = \"/opt/consortium\"",
247            "deploy_root = \"/opt/consortium\"\nsysroot     = \"sdk/sysroot\"",
248        );
249        let session = Session::from_toml(&toml).expect("manifest loads");
250        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
251
252        let plan = plan_application_build(
253            &session,
254            &resolve_workspace(&session, Path::new("/proj")).unwrap(),
255            None,
256            &HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]),
257            CargoOptions::default(),
258            &ty_crate,
259        )
260        .expect("plan succeeds");
261
262        assert_eq!(
263            plan.config.sysroot.as_deref(),
264            Some(Path::new("/proj/sdk/sysroot"))
265        );
266    }
267
268    #[test]
269    fn absolute_profile_sysroot_used_verbatim() {
270        let toml = STM32.replace(
271            "deploy_root = \"/opt/consortium\"",
272            "deploy_root = \"/opt/consortium\"\nsysroot     = \"/sdk/target/sysroot\"",
273        );
274        let session = Session::from_toml(&toml).expect("manifest loads");
275        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
276
277        let plan = plan_application_build(
278            &session,
279            &resolve_workspace(&session, Path::new("/proj")).unwrap(),
280            None,
281            &HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]),
282            CargoOptions::default(),
283            &ty_crate,
284        )
285        .expect("plan succeeds");
286
287        assert_eq!(
288            plan.config.sysroot.as_deref(),
289            Some(Path::new("/sdk/target/sysroot"))
290        );
291    }
292
293    #[test]
294    fn omits_hmi_when_frontend_not_built() {
295        let session = Session::from_toml(STM32).expect("manifest loads");
296        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
297        let plan = plan_application_build(
298            &session,
299            &resolve_workspace(&session, Path::new("/proj")).unwrap(),
300            None,
301            &HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]),
302            CargoOptions::default(),
303            &ty_crate,
304        )
305        .expect("plan succeeds");
306        assert!(plan.config.hmi.is_none());
307    }
308}