Skip to main content

consortium_builder/compile/
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//! Linux application compilation.
18//!
19//! Builds the A-core executable with `cargo`, usually cross-compiled, and injects the
20//! `CONSORTIUM_*` environment variables the application and its generated module read at
21//! compile time.
22//!
23//! The interesting part is the sysroot. Cross-compiling against a Yocto/OpenSTLinux SDK
24//! needs a target sysroot, a cross linker, and a `PATH` that does not leak host tools
25//! into the build. Resolution order is `[profile] sysroot` first, then the
26//! `SDKTARGETSYSROOT` / `OECORE_TARGET_SYSROOT` environment variables — so a manifest can
27//! pin a sysroot without the developer having sourced an SDK environment script.
28//!
29//! On an arm64 Linux host, `aarch64-unknown-linux-gnu` is a *native* build and
30//! `sysroot = "/"` resolves to the image root, which is why the melt-pot manifests can
31//! be built inside an Apple `container` VM with no SDK at all.
32
33use std::path::{Path, PathBuf};
34use thiserror::Error;
35use tokio::process::Command;
36
37use super::Compiler;
38use crate::compile::env::{DbgConfig, HmiConfig, SharedMemConfig};
39use crate::compile::utils::{
40    clean_path, elf_path, inject_dbg, inject_hmi, inject_shared_mem, resolve_cargo, resolve_rustc,
41    run_command, verify_elf,
42};
43use crate::progress::BuildReporter;
44
45/// Error type for application compilation.
46#[derive(Error, Debug)]
47pub enum ApplicationCompileError {
48    #[error("Failed to resolve sysroot: {0}")]
49    SysrootError(String),
50
51    #[error("Failed to resolve rustup tool: {0}")]
52    RustupError(String),
53
54    #[error("Cargo build failed: {0}")]
55    BuildFailed(String),
56
57    #[error("ELF not found at expected path: {0:?}")]
58    ElfNotFound(PathBuf),
59
60    #[error("{0}")]
61    UtilityError(String),
62}
63
64/// Result of cross-compiler detection, including whether an explicit compiler was found.
65struct CrossCompiler {
66    /// Absolute path to the C cross-compiler, or `None` when falling back to the generic linker.
67    cc: Option<String>,
68    /// Absolute path to the C++ cross-compiler, or `None` when falling back to the generic linker.
69    cxx: Option<String>,
70}
71
72impl CrossCompiler {
73    /// Detect the cross-compiler in priority order:
74    /// 1. `CC` / `CXX` env vars (explicit, platform-agnostic)
75    /// 2. `None` → generic Rust linker
76    fn detect() -> Self {
77        // 1. Explicit CC/CXX: honour whatever the user or SDK set
78        if let Ok(cc) = std::env::var("CC") {
79            let cxx = std::env::var("CXX").unwrap_or_else(|_| cc.clone());
80            return CrossCompiler {
81                cc: Some(cc),
82                cxx: Some(cxx),
83            };
84        }
85
86        // 2. Nothing found
87        CrossCompiler {
88            cc: None,
89            cxx: None,
90        }
91    }
92}
93
94struct SdkEnv {
95    sysroot: String,
96    cross: CrossCompiler,
97    /// Tune flags from `OECORE_TUNE_CCARGS`
98    arch_flags: String,
99    /// Native sysroot used to strip SDK entries from PATH
100    native_sysroot: String,
101}
102
103impl SdkEnv {
104    /// Resolve the SDK environment.
105    ///
106    /// `sysroot_override` is the `[profile] sysroot` from the manifest (already
107    /// resolved to an absolute path by the caller). When present it takes
108    /// priority over the `SDKTARGETSYSROOT` / `OECORE_TARGET_SYSROOT` environment
109    /// variables, so a build does not require a sourced SDK environment.
110    fn resolve(sysroot_override: Option<&Path>) -> Result<Self, ApplicationCompileError> {
111        let sysroot = match sysroot_override {
112            Some(path) => path.to_string_lossy().into_owned(),
113            None => std::env::var("SDKTARGETSYSROOT")
114                .or_else(|_| std::env::var("OECORE_TARGET_SYSROOT"))
115                .map_err(|_| {
116                    ApplicationCompileError::SysrootError(
117                        "Could not determine target sysroot. Set the [profile] sysroot in \
118                     Consortium.toml, export SDKTARGETSYSROOT, or source a Yocto \
119                     environment-setup script."
120                            .to_string(),
121                    )
122                })?,
123        };
124
125        if !PathBuf::from(&sysroot).exists() {
126            return Err(ApplicationCompileError::SysrootError(format!(
127                "Target sysroot does not exist: {sysroot}"
128            )));
129        }
130
131        // OECORE_NATIVE_SYSROOT is optional; only needed for PATH sanitization
132        let native_sysroot = std::env::var("OECORE_NATIVE_SYSROOT").unwrap_or_default();
133
134        let arch_flags = std::env::var("OECORE_TUNE_CCARGS")
135            .unwrap_or_else(|_| "-mcpu=cortex-a35+crc -mbranch-protection=standard".to_string());
136
137        Ok(SdkEnv {
138            sysroot,
139            cross: CrossCompiler::detect(),
140            arch_flags,
141            native_sysroot,
142        })
143    }
144
145    fn rustflags(&self) -> String {
146        let arch_link_args: String = self
147            .arch_flags
148            .split_whitespace()
149            .map(|f| format!(" -C link-arg={f}"))
150            .collect();
151
152        match &self.cross.cc {
153            Some(cc) => format!(
154                "-C linker={cc} -C link-arg=--sysroot={sysroot}{arch_link_args}",
155                sysroot = self.sysroot,
156            ),
157            None => format!(
158                "-C link-arg=--sysroot={sysroot}{arch_link_args}",
159                sysroot = self.sysroot,
160            ),
161        }
162    }
163}
164
165/// Build parameters for a Linux application (typically targeting a Yocto A-core).
166///
167/// # Environment variables (in priority order)
168///
169/// Cross-compiler detection:
170///   `CC` / `CXX`: explicit cross-compiler (any Yocto flavor)
171///   Fallback: generic Rust linker
172///
173/// Sysroot:
174///   `SDKTARGETSYSROOT` > `OECORE_TARGET_SYSROOT`
175///
176/// Rustup overrides:
177///   `CONSORTIUM_RUSTC`, `CONSORTIUM_CARGO`
178///
179/// HMI (when `hmi` is `Some`):
180///   `CONSORTIUM_HMI_DIST`: absolute path to the compiled frontend dist directory
181///   `CONSORTIUM_HMI_DEBUG_URL`: dev-server URL (only injected when present)
182#[derive(Clone)]
183pub struct ApplicationConfiguration {
184    /// Path to the Cargo project root.
185    pub path: PathBuf,
186    /// Rust target triple (e.g. `"aarch64-unknown-linux-gnu"`).
187    pub target: String,
188    /// Build in release mode.
189    pub release: bool,
190    /// Optional HMI config. `dist` must be an absolute path (resolved by the
191    /// caller from the manifest directory before constructing this struct).
192    pub hmi: Option<HmiConfig>,
193    /// IPC shared-memory layout, injected as `CONSORTIUM_IPC_*` env vars.
194    pub ipc: Option<SharedMemConfig>,
195    /// Debug ring layout, injected as `CONSORTIUM_DBG_*` env vars.
196    pub dbg: Option<DbgConfig>,
197    /// Target sysroot from `[profile] sysroot`, resolved to an absolute path.
198    /// Takes priority over the `SDKTARGETSYSROOT` / `OECORE_TARGET_SYSROOT`
199    /// environment variables when present.
200    pub sysroot: Option<PathBuf>,
201}
202
203/// Compiler for Linux applications targeting Yocto A-cores.
204pub struct ApplicationCompiler;
205
206impl ApplicationCompiler {
207    pub async fn build_with_reporter(
208        config: ApplicationConfiguration,
209        reporter: Option<&dyn BuildReporter>,
210    ) -> Result<PathBuf, ApplicationCompileError> {
211        let sdk = SdkEnv::resolve(config.sysroot.as_deref())?;
212
213        let rustc = resolve_rustc()
214            .await
215            .map_err(ApplicationCompileError::RustupError)?;
216        let cargo = resolve_cargo()
217            .await
218            .map_err(ApplicationCompileError::RustupError)?;
219
220        let rustflags = sdk.rustflags();
221        let sanitized_path = clean_path(&sdk.native_sysroot);
222
223        let mut cmd = Command::new(&cargo);
224        cmd.arg("build")
225            .arg("--target")
226            .arg(&config.target)
227            .current_dir(&config.path)
228            .env("PATH", &sanitized_path)
229            .env("RUSTC", &rustc)
230            .env("RUSTFLAGS", &rustflags)
231            .env("SYSROOT", &sdk.sysroot)
232            .env("PKG_CONFIG_SYSROOT_DIR", &sdk.sysroot)
233            .env(
234                "PKG_CONFIG_PATH",
235                format!(
236                    "{s}/usr/lib/pkgconfig:{s}/usr/share/pkgconfig",
237                    s = sdk.sysroot,
238                ),
239            );
240
241        if let Some(ref cc) = sdk.cross.cc {
242            cmd.arg("--config")
243                .arg(format!("{}.linker={cc}", config.target));
244            cmd.env("CC", format!("{cc} --sysroot={}", sdk.sysroot));
245        }
246
247        if let Some(ref cxx) = sdk.cross.cxx {
248            cmd.env("CXX", format!("{cxx} --sysroot={}", sdk.sysroot));
249        }
250
251        inject_hmi(&mut cmd, config.hmi.as_ref());
252
253        inject_shared_mem(&mut cmd, "IPC", config.ipc.as_ref());
254        inject_dbg(&mut cmd, config.dbg.as_ref());
255
256        if config.release {
257            cmd.arg("--release");
258        }
259
260        let status = run_command(&mut cmd, reporter)
261            .await
262            .map_err(|e| ApplicationCompileError::BuildFailed(e.to_string()))?;
263
264        if !status.success() {
265            return Err(ApplicationCompileError::BuildFailed(format!(
266                "cargo build failed with status: {status}"
267            )));
268        }
269
270        let elf = Self::output_path(&config).await?;
271
272        if !elf.exists() {
273            return Err(ApplicationCompileError::ElfNotFound(elf));
274        }
275
276        verify_elf(&elf)
277            .await
278            .map_err(ApplicationCompileError::UtilityError)?;
279
280        Ok(elf)
281    }
282}
283
284impl Compiler for ApplicationCompiler {
285    type Configuration = ApplicationConfiguration;
286    type Error = ApplicationCompileError;
287    type Output = PathBuf;
288
289    async fn build(config: Self::Configuration) -> Result<Self::Output, Self::Error> {
290        Self::build_with_reporter(config, None).await
291    }
292
293    async fn output_path(config: &Self::Configuration) -> Result<PathBuf, Self::Error> {
294        elf_path(&config.path, &config.target, config.release)
295            .await
296            .map_err(ApplicationCompileError::UtilityError)
297    }
298}
299
300/// Backward-compatible builder; delegates to [`ApplicationCompiler`].
301///
302/// Cross-compiler detection and environment handling is identical to
303/// [`ApplicationCompiler`]. See [`ApplicationConfiguration`] for env var docs.
304pub struct ApplicationBuilder {
305    /// Path to the Cargo project root.
306    pub path: PathBuf,
307    pub release: bool,
308    pub target: String,
309    pub hmi: Option<HmiConfig>,
310    pub ipc: Option<SharedMemConfig>,
311    pub dbg: Option<DbgConfig>,
312    pub sysroot: Option<PathBuf>,
313}
314
315impl ApplicationBuilder {
316    /// Builds the Linux application, returning the path to the output ELF.
317    pub async fn build(&self) -> Result<PathBuf, String> {
318        ApplicationCompiler::build(ApplicationConfiguration {
319            path: self.path.clone(),
320            target: self.target.clone(),
321            release: self.release,
322            hmi: self.hmi.clone(),
323            ipc: self.ipc,
324            dbg: self.dbg,
325            sysroot: self.sysroot.clone(),
326        })
327        .await
328        .map_err(|e| e.to_string())
329    }
330
331    /// Returns the expected output ELF path without building.
332    pub async fn elf_path(&self) -> Result<PathBuf, String> {
333        ApplicationCompiler::output_path(&ApplicationConfiguration {
334            path: self.path.clone(),
335            target: self.target.clone(),
336            release: self.release,
337            hmi: self.hmi.clone(),
338            ipc: self.ipc,
339            dbg: self.dbg,
340            sysroot: self.sysroot.clone(),
341        })
342        .await
343        .map_err(|e| e.to_string())
344    }
345}