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.
22
23use std::collections::HashMap;
24use std::path::{Path, PathBuf};
25
26use consortium_cfg::Session;
27use consortium_cfg_common::config::{AcoreName, Party, VisibleCore};
28
29use super::{PipelineError, ipc_config_for};
30use crate::compile::application::ApplicationConfiguration;
31use crate::compile::env::HmiConfig;
32
33/// A resolved application build plus its generated AP init module.
34pub struct ApplicationBuildPlan {
35    /// `cargo build` parameters for the Linux application.
36    pub config: ApplicationConfiguration,
37    /// The generated AP init module, rendered to Rust source, to be written into
38    /// the application crate before building. Its `DbgRingSpec`s already embed the
39    /// resolved firmware ELF paths.
40    pub generated: String,
41}
42
43/// Plans the Linux application build.
44///
45/// `hmi_dist` is `Some` when Stage 3 built the frontend; it only gates whether an
46/// [`HmiConfig`] is injected — the *on-target* serve path is derived from
47/// `deploy_root`, not the build-host dist. `resolved_dbg_elfs` maps each debug
48/// core to the firmware ELF the AP embeds.
49pub fn plan_application_build(
50    session: &Session,
51    manifest_dir: &Path,
52    hmi_dist: Option<&Path>,
53    resolved_dbg_elfs: &HashMap<VisibleCore, PathBuf>,
54    release: bool,
55    ty_crate: &syn::Path,
56) -> Result<ApplicationBuildPlan, PipelineError> {
57    let root = session
58        .config
59        .endpoints
60        .get(&Party::AcoreNonSecure)
61        .ok_or_else(|| PipelineError::MissingEndpoint(Party::AcoreNonSecure.to_string()))?;
62    let path = manifest_dir.join(root);
63
64    let target = acore_linux_triple(&session.database.ap)?;
65
66    // The HMI carrier points the deployed app at its on-target install path
67    // under `deploy_root` (Cog: `www/`; PocketJS: `hmi/<app>.{js,pak}`), which a
68    // packaging recipe populates from the matching `dist/` subtree.
69    let hmi = match (&session.config.hmi, hmi_dist) {
70        (Some(hmi), Some(_)) => Some(HmiConfig::from_hmi(
71            hmi,
72            &session.config.profile.deploy_root,
73        )),
74        _ => None,
75    };
76
77    let ipc = ipc_config_for(&session.lower()?, VisibleCore::Ap);
78
79    // A relative `[profile] sysroot` is anchored at the manifest directory; an
80    // absolute one is used verbatim.
81    let sysroot = session
82        .config
83        .profile
84        .sysroot
85        .as_ref()
86        .map(|s| manifest_dir.join(s));
87
88    // Substitute the resolved firmware ELF paths into the debug IR, then generate
89    // the AP init module that `include_bytes!`es them.
90    let mut ir = session.lower()?;
91    ir.dbg = std::mem::take(&mut ir.dbg).with_resolved_elf_paths(resolved_dbg_elfs);
92    consortium_log::trace!("rendering AP init module for target {}", target);
93    let generated =
94        consortium_cfg::render(ir.initialize(VisibleCore::Ap, ty_crate)).map_err(|e| {
95            consortium_log::trace!("AP init codegen render failed: {}", e);
96            PipelineError::Codegen(e.to_string())
97        })?;
98
99    Ok(ApplicationBuildPlan {
100        config: ApplicationConfiguration {
101            path,
102            target,
103            release,
104            hmi,
105            ipc,
106            // The AP is the debug *host*: it reads rings through the generated UIO
107            // code and the embedded ELF, not via `CONSORTIUM_DBG_*` (those
108            // configure a ring *writer*), so no debug layout is injected here.
109            dbg: None,
110            sysroot,
111        },
112        generated,
113    })
114}
115
116/// The Linux Rust target triple for the AP core.
117///
118/// Derived from the AP core's bitness rather than hardcoded, so a 32-bit A-core
119/// (e.g. Cortex-A7) targets armv7 instead of aarch64. This is the Linux/`gnu`
120/// ABI, distinct from [`AcoreName::as_rust_triple`]'s bare-metal `-none` triples.
121fn acore_linux_triple(ap: &str) -> Result<String, PipelineError> {
122    let name: AcoreName = ap
123        .parse()
124        .map_err(|_| PipelineError::UnknownCore(ap.to_string()))?;
125    Ok(if name.is_64_bit() {
126        "aarch64-unknown-linux-gnu".to_string()
127    } else {
128        "armv7-unknown-linux-gnueabihf".to_string()
129    })
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    const STM32: &str = include_str!("../../../consortium-cfg/Consortium.example.stm32mp257.toml");
137
138    fn cm33() -> VisibleCore {
139        VisibleCore::Mcore("cm33".parse().unwrap())
140    }
141
142    #[test]
143    fn acore_triple_tracks_bitness() {
144        assert_eq!(
145            acore_linux_triple("ca35").unwrap(),
146            "aarch64-unknown-linux-gnu"
147        );
148        assert_eq!(
149            acore_linux_triple("ca7").unwrap(),
150            "armv7-unknown-linux-gnueabihf"
151        );
152        assert!(acore_linux_triple("bogus").is_err());
153    }
154
155    #[test]
156    fn plans_application_with_hmi_and_embedded_dbg() {
157        let session = Session::from_toml(STM32).expect("manifest loads");
158        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
159        let resolved = HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]);
160
161        let plan = plan_application_build(
162            &session,
163            Path::new("/proj"),
164            Some(Path::new("/build/website/dist")),
165            &resolved,
166            true,
167            &ty_crate,
168        )
169        .expect("plan succeeds");
170
171        assert_eq!(plan.config.path, Path::new("/proj/app"));
172        assert_eq!(plan.config.target, "aarch64-unknown-linux-gnu");
173        assert!(plan.config.release);
174
175        // The HMI serve path resolves under the deploy root, not the build dist.
176        let hmi = plan.config.hmi.expect("hmi injected");
177        assert_eq!(
178            hmi,
179            HmiConfig::Cog {
180                dist: "/opt/consortium/www".to_string(),
181                debug_url: Some("http://127.0.0.1:5173/".to_string()),
182            }
183        );
184
185        // The generated AP module embeds the resolved firmware ELF.
186        assert!(
187            plan.generated
188                .contains("include_bytes!(\"/build/cm33.elf\")")
189        );
190        // The AP is the host, so no debug writer layout is injected.
191        assert!(plan.config.dbg.is_none());
192
193        // The stm32 example does not declare a sysroot, so none is planned.
194        assert!(plan.config.sysroot.is_none());
195    }
196
197    #[test]
198    fn relative_profile_sysroot_anchors_at_manifest_dir() {
199        let toml = STM32.replace(
200            "deploy_root = \"/opt/consortium\"",
201            "deploy_root = \"/opt/consortium\"\nsysroot     = \"sdk/sysroot\"",
202        );
203        let session = Session::from_toml(&toml).expect("manifest loads");
204        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
205
206        let plan = plan_application_build(
207            &session,
208            Path::new("/proj"),
209            None,
210            &HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]),
211            false,
212            &ty_crate,
213        )
214        .expect("plan succeeds");
215
216        assert_eq!(
217            plan.config.sysroot.as_deref(),
218            Some(Path::new("/proj/sdk/sysroot"))
219        );
220    }
221
222    #[test]
223    fn absolute_profile_sysroot_used_verbatim() {
224        let toml = STM32.replace(
225            "deploy_root = \"/opt/consortium\"",
226            "deploy_root = \"/opt/consortium\"\nsysroot     = \"/sdk/target/sysroot\"",
227        );
228        let session = Session::from_toml(&toml).expect("manifest loads");
229        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
230
231        let plan = plan_application_build(
232            &session,
233            Path::new("/proj"),
234            None,
235            &HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]),
236            false,
237            &ty_crate,
238        )
239        .expect("plan succeeds");
240
241        assert_eq!(
242            plan.config.sysroot.as_deref(),
243            Some(Path::new("/sdk/target/sysroot"))
244        );
245    }
246
247    #[test]
248    fn omits_hmi_when_frontend_not_built() {
249        let session = Session::from_toml(STM32).expect("manifest loads");
250        let ty_crate: syn::Path = syn::parse_str("::shared").unwrap();
251        let plan = plan_application_build(
252            &session,
253            Path::new("/proj"),
254            None,
255            &HashMap::from([(cm33(), PathBuf::from("/build/cm33.elf"))]),
256            false,
257            &ty_crate,
258        )
259        .expect("plan succeeds");
260        assert!(plan.config.hmi.is_none());
261    }
262}