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