Skip to main content

cargo_airbender/commands/
build.rs

1use crate::cli::{BuildArgs, BuildProfile};
2use crate::error::{CliError, Result};
3use crate::ui;
4use airbender_build::{
5    build_dist, BuildConfig, Profile, DEFAULT_GUEST_TARGET, DEFAULT_GUEST_TOOLCHAIN,
6};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub fn run(args: BuildArgs) -> Result<()> {
11    let BuildArgs {
12        app_name,
13        bin,
14        target,
15        dist,
16        project,
17        profile,
18        debug,
19        release,
20        cargo_args,
21        reproducible,
22        workspace_root,
23    } = args;
24
25    let project_dir = match project {
26        Some(path) => path,
27        None => {
28            let invocation_cwd = std::env::current_dir().map_err(|err| {
29                CliError::with_source("failed to resolve current working directory", err)
30            })?;
31            discover_project_dir_from(&invocation_cwd)?
32        }
33    };
34
35    let manifest_path = project_dir.join("Cargo.toml");
36    if !manifest_path.is_file() {
37        return Err(missing_manifest_error(&project_dir));
38    }
39
40    let mut config = BuildConfig::new(project_dir);
41    config.app_name = app_name;
42    config.bin_name = bin;
43    config.target = target;
44    config.dist_dir = dist;
45    config.profile = resolve_profile(profile, debug, release);
46    config.cargo_args = cargo_args;
47    config.reproducible = reproducible;
48    config.workspace_root_override = workspace_root;
49
50    let artifacts = build_dist(&config).map_err(|err| {
51        CliError::with_source("failed to build guest artifacts", err)
52            .with_hint("set `RUST_LOG=info` if you need backend diagnostic logs")
53    })?;
54
55    ui::success("built guest artifacts");
56    if reproducible {
57        ui::info("reproducible build (Docker)");
58        ui::field("toolchain", DEFAULT_GUEST_TOOLCHAIN);
59    }
60    ui::field("dist", artifacts.dir.display());
61    ui::field("app.bin", artifacts.app_bin.path.display());
62    ui::field("app.elf", artifacts.app_elf.path.display());
63    ui::field("app.text", artifacts.app_text.path.display());
64    ui::field("manifest", artifacts.manifest_path.display());
65    ui::blank_line();
66    ui::info("next step");
67    ui::command(format!(
68        "cargo airbender run \"{}\" --input <input.hex>",
69        artifacts.app_bin.path.display()
70    ));
71
72    Ok(())
73}
74
75fn resolve_profile(profile: Option<BuildProfile>, debug: bool, release: bool) -> Profile {
76    if debug {
77        return Profile::Debug;
78    }
79    if release {
80        return Profile::Release;
81    }
82    match profile {
83        Some(BuildProfile::Debug) => Profile::Debug,
84        Some(BuildProfile::Release) => Profile::Release,
85        None => Profile::Release,
86    }
87}
88
89fn discover_project_dir_from(invocation_cwd: &Path) -> Result<PathBuf> {
90    for candidate in invocation_cwd.ancestors() {
91        if !candidate.join("Cargo.toml").is_file() {
92            continue;
93        }
94
95        let is_guest = is_guest_project_dir(candidate).map_err(|err| {
96            CliError::with_source(
97                format!("failed to inspect guest project `{}`", candidate.display()),
98                err,
99            )
100        })?;
101        if is_guest {
102            return Ok(candidate.to_path_buf());
103        }
104    }
105
106    Err(missing_manifest_error(invocation_cwd))
107}
108
109fn is_guest_project_dir(project_dir: &Path) -> std::io::Result<bool> {
110    let cargo_config = project_dir.join(".cargo/config.toml");
111    if !cargo_config.is_file() {
112        return Ok(false);
113    }
114
115    let cargo_config = fs::read_to_string(cargo_config)?;
116    cargo_config_targets_guest(&cargo_config)
117}
118
119fn cargo_config_targets_guest(cargo_config: &str) -> std::io::Result<bool> {
120    let cargo_config = cargo_config.parse::<toml::Table>().map_err(|err| {
121        std::io::Error::new(
122            std::io::ErrorKind::InvalidData,
123            format!("failed to parse .cargo/config.toml: {err}"),
124        )
125    })?;
126
127    Ok(cargo_config
128        .get("build")
129        .and_then(toml::Value::as_table)
130        .and_then(|build| build.get("target"))
131        .and_then(toml::Value::as_str)
132        == Some(DEFAULT_GUEST_TARGET))
133}
134
135fn missing_manifest_error(project_dir: &Path) -> CliError {
136    CliError::new(format!(
137        "guest project `{}` does not contain a Cargo.toml",
138        project_dir.display()
139    ))
140    .with_hint("use --project <path-to-guest-crate>")
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use std::fs;
147
148    #[test]
149    fn discovers_project_dir_from_parent_manifest_when_project_is_omitted() {
150        let temp_dir = tempfile::tempdir().expect("create temp directory");
151        let project_dir = temp_dir.path().join("guest");
152        let nested_dir = project_dir.join("src").join("nested");
153
154        write_guest_project(&project_dir);
155        fs::create_dir_all(&nested_dir).expect("create nested guest directory");
156
157        let resolved = discover_project_dir_from(&nested_dir).expect("resolve project dir");
158
159        assert_eq!(resolved, project_dir);
160    }
161
162    #[test]
163    fn returns_hint_when_only_non_guest_manifests_exist_in_ancestors() {
164        let temp_dir = tempfile::tempdir().expect("create temp directory");
165        let project_dir = temp_dir.path().join("helper");
166        let nested_dir = project_dir.join("src");
167
168        write_package_manifest(
169            &project_dir,
170            "helper",
171            "\n[dependencies]\nairbender-sdk = \"0.1\"\n",
172        );
173        fs::create_dir_all(&nested_dir).expect("create nested host directory");
174
175        let err = discover_project_dir_from(&nested_dir).expect_err("missing guest manifest");
176
177        assert_eq!(
178            err.to_string(),
179            format!(
180                "guest project `{}` does not contain a Cargo.toml",
181                nested_dir.display()
182            )
183        );
184        assert_eq!(err.hint(), Some("use --project <path-to-guest-crate>"));
185    }
186
187    #[test]
188    fn skips_workspace_manifests_when_project_is_omitted() {
189        let temp_dir = tempfile::tempdir().expect("create temp directory");
190        let project_dir = temp_dir.path().join("host");
191        let nested_dir = project_dir.join("src");
192
193        write_file(
194            &temp_dir.path().join("Cargo.toml"),
195            "[workspace]\nmembers = [\"host\"]\n",
196        );
197        write_package_manifest(&project_dir, "host", "");
198        fs::create_dir_all(&nested_dir).expect("create nested host directory");
199
200        let err = discover_project_dir_from(&nested_dir).expect_err("missing guest manifest");
201
202        assert_eq!(
203            err.to_string(),
204            format!(
205                "guest project `{}` does not contain a Cargo.toml",
206                nested_dir.display()
207            )
208        );
209        assert_eq!(err.hint(), Some("use --project <path-to-guest-crate>"));
210    }
211
212    #[test]
213    fn returns_hint_when_no_manifest_exists_in_ancestors() {
214        let temp_dir = tempfile::tempdir().expect("create temp directory");
215        let nested_dir = temp_dir.path().join("guest").join("src");
216        fs::create_dir_all(&nested_dir).expect("create nested guest directory");
217
218        let err = discover_project_dir_from(&nested_dir).expect_err("missing manifest");
219
220        assert_eq!(
221            err.to_string(),
222            format!(
223                "guest project `{}` does not contain a Cargo.toml",
224                nested_dir.display()
225            )
226        );
227        assert_eq!(err.hint(), Some("use --project <path-to-guest-crate>"));
228    }
229
230    #[test]
231    fn detects_guest_target_from_cargo_config_toml() {
232        let cargo_config = format!(
233            "[build]\nrustflags = [\"-C\", \"link-arg=-Tmemory.x\"]\ntarget = \"{DEFAULT_GUEST_TARGET}\"\n"
234        );
235
236        let targets_guest =
237            cargo_config_targets_guest(&cargo_config).expect("parse guest cargo config");
238
239        assert!(targets_guest);
240    }
241
242    fn write_package_manifest(project_dir: &Path, name: &str, suffix: &str) {
243        write_file(
244            &project_dir.join("Cargo.toml"),
245            &format!(
246                "[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n{suffix}"
247            ),
248        );
249        write_file(&project_dir.join("src/main.rs"), "fn main() {}\n");
250    }
251
252    fn write_guest_project(project_dir: &Path) {
253        write_package_manifest(project_dir, "guest", "");
254        write_file(
255            &project_dir.join(".cargo/config.toml"),
256            &format!("[build]\ntarget = \"{DEFAULT_GUEST_TARGET}\"\n"),
257        );
258    }
259
260    fn write_file(path: &Path, contents: &str) {
261        let parent = path
262            .parent()
263            .expect("test file should have a parent directory");
264        fs::create_dir_all(parent).expect("create test directory");
265        fs::write(path, contents).expect("write test file");
266    }
267}