cargo_airbender/commands/
build.rs1use 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}