Skip to main content

airbender_build/
config.rs

1//! Build configuration and artifact packaging flow.
2
3use crate::build::ReproducibleBuild;
4use crate::build::{DistArtifact, DistArtifacts, LocalBuild};
5use crate::constants::DEFAULT_APP_NAME;
6use crate::errors::Result;
7use crate::resolver::ResolvedBuildParams;
8use crate::{ArtifactEntry, BuildMetadata, Manifest, Profile, MANIFEST_VERSION_V1};
9use std::fs;
10use std::path::{Path, PathBuf};
11
12/// Input settings for guest compilation and dist packaging.
13#[derive(Clone, Debug)]
14pub struct BuildConfig {
15    /// Project root containing `Cargo.toml`.
16    pub project_dir: PathBuf,
17    /// Output app folder name inside the dist root.
18    pub app_name: String,
19    /// Override for the binary name.
20    pub bin_name: Option<String>,
21    /// Override for the target triple.
22    pub target: Option<String>,
23    /// Build profile used for artifact extraction.
24    pub profile: Profile,
25    /// Output root directory for `dist/` artifacts.
26    pub dist_dir: Option<PathBuf>,
27    /// Additional arguments forwarded to `cargo build` and `cargo objcopy`.
28    pub cargo_args: Vec<String>,
29    /// When true, compilation runs inside a pinned Docker container for
30    /// bit-for-bit reproducible output across host environments.
31    pub reproducible: bool,
32    /// Overrides the directory bind-mounted as `/src` inside the reproducible
33    /// build container. Only needed when the guest has path dependencies that
34    /// point outside its own cargo workspace root (e.g. in-tree monorepos where
35    /// the guest shares crates with the host via `path = "../../.."`).
36    /// Has no effect unless `reproducible` is also true.
37    pub workspace_root_override: Option<PathBuf>,
38}
39
40impl BuildConfig {
41    /// Creates a config with defaults for app name, profile, and dist root.
42    pub fn new(project_dir: impl Into<PathBuf>) -> Self {
43        Self {
44            project_dir: project_dir.into(),
45            app_name: DEFAULT_APP_NAME.to_string(),
46            bin_name: None,
47            target: None,
48            profile: Profile::Release,
49            dist_dir: None,
50            cargo_args: Vec::new(),
51            reproducible: false,
52            workspace_root_override: None,
53        }
54    }
55
56    /// Builds the guest binary and writes a dist package for this configuration.
57    fn build_dist(&self) -> Result<DistArtifacts> {
58        let cwd = std::env::current_dir()?;
59        let params = ResolvedBuildParams::resolve(self, &cwd)?;
60
61        // Build extra config for `panic_immediate_abort`.
62        // The same --config must be passed to both build and objcopy to prevent
63        // a fall back to a cached artifact built without it.
64        let extra_config = params
65            .panic_immediate_abort
66            .then_some(r#"build.rustflags=["-Zunstable-options","-Cpanic=immediate-abort"]"#);
67
68        fs::create_dir_all(params.dist_app.dir())?;
69
70        if self.reproducible {
71            ReproducibleBuild::new(&params)?.run(self.profile, &self.cargo_args, extra_config)?;
72        } else {
73            LocalBuild::new(&params).run(self.profile, &self.cargo_args, extra_config)?;
74        }
75
76        let artifacts = DistArtifacts {
77            dir: params.dist_app.dir().to_path_buf(),
78            app_bin: DistArtifact::new(params.dist_app.bin().to_path_buf())?,
79            app_elf: DistArtifact::new(params.dist_app.elf().to_path_buf())?,
80            app_text: DistArtifact::new(params.dist_app.text().to_path_buf())?,
81            manifest_path: params.dist_app.manifest().to_path_buf(),
82        };
83
84        fn file_name(p: &Path) -> String {
85            p.file_name()
86                .expect("must be valid")
87                .to_str()
88                .expect("must be valid")
89                .to_string()
90        }
91        let manifest = Manifest {
92            package: params.package_name,
93            bin_name: params.manifest_bin_name,
94            manifest: MANIFEST_VERSION_V1.to_string(),
95            codec: format!("v{}", airbender_codec::AIRBENDER_CODEC_V0),
96            target: params.target,
97            bin: ArtifactEntry {
98                path: file_name(params.dist_app.bin()),
99                sha256: artifacts.app_bin.sha256.clone(),
100            },
101            elf: ArtifactEntry {
102                path: file_name(params.dist_app.elf()),
103                sha256: artifacts.app_elf.sha256.clone(),
104            },
105            text: ArtifactEntry {
106                path: file_name(params.dist_app.text()),
107                sha256: artifacts.app_text.sha256.clone(),
108            },
109            build: BuildMetadata {
110                profile: self.profile,
111                reproducible: self.reproducible,
112                git_branch: params.git.branch,
113                git_commit: params.git.commit,
114                is_dirty: params.git.is_dirty,
115            },
116        };
117        manifest.write_to_file(params.dist_app.manifest())?;
118
119        Ok(artifacts)
120    }
121}
122
123/// Builds and packages guest artifacts using the provided configuration.
124pub fn build_dist(config: &BuildConfig) -> Result<DistArtifacts> {
125    config.build_dist()
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn reproducible_flag_defaults_to_false() {
134        let config = BuildConfig::new(PathBuf::from("."));
135        assert!(!config.reproducible);
136    }
137
138    #[test]
139    fn workspace_root_override_defaults_to_none() {
140        let config = BuildConfig::new(PathBuf::from("."));
141        assert!(config.workspace_root_override.is_none());
142    }
143}