cargo_airbender/commands/new/
mod.rs1mod args;
2mod deps;
3mod profiles;
4mod template;
5
6use crate::cli::NewArgs;
7use crate::error::{CliError, Result};
8use crate::ui;
9use args::resolve_new_args;
10use deps::resolve_crate_dependency;
11use profiles::prover_backend_profile;
12use std::fs;
13use std::path::Path;
14use template::{write_templates, TemplateContext};
15
16pub fn run(args: NewArgs) -> Result<()> {
17 let args = resolve_new_args(args)?;
18 let profile = prover_backend_profile(args.prover_backend);
19
20 create_directory(&args.path, "destination")?;
21
22 let guest_destination_dir = args.path.join("guest");
23 create_directory(&guest_destination_dir, "guest")?;
24
25 let host_destination_dir = args.path.join("host");
26 create_directory(&host_destination_dir, "host")?;
27
28 let sdk_dependency = resolve_crate_dependency(
29 &guest_destination_dir,
30 args.sdk_path.as_deref(),
31 args.sdk_version.as_deref(),
32 "airbender-sdk",
33 )?;
34 let host_dependency = resolve_crate_dependency(
35 &host_destination_dir,
36 args.sdk_path.as_deref(),
37 args.sdk_version.as_deref(),
38 "airbender-host",
39 )?;
40
41 let template_context = TemplateContext::new(
42 &args.project_name,
43 &sdk_dependency,
44 &host_dependency,
45 args.enable_std,
46 args.allocator,
47 profile.host_dependency_features,
48 profile.readme_prover_backend_doc,
49 );
50
51 write_templates(&args.path, template_context, profile)?;
52
53 ui::success(format!("created Airbender project `{}`", args.project_name));
54 ui::field("path", args.path.display());
55 ui::field("guest", guest_destination_dir.display());
56 ui::field("host", host_destination_dir.display());
57 ui::blank_line();
58 ui::info("next steps");
59 ui::command(format!("cd \"{}\"", args.path.display()));
60 ui::command("cd guest && cargo airbender build");
61 ui::command(profile.host_run_command);
62
63 Ok(())
64}
65
66fn create_directory(path: &Path, description: &str) -> Result<()> {
67 fs::create_dir_all(path).map_err(|err| {
68 CliError::with_source(
69 format!(
70 "failed to create {description} directory `{}`",
71 path.display()
72 ),
73 err,
74 )
75 })
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::cli::{NewAllocatorArg, NewProverBackendArg};
82 use std::path::PathBuf;
83 use std::time::{SystemTime, UNIX_EPOCH};
84
85 const ALL_SCAFFOLD_FILES: &[&str] = &[
86 ".gitignore",
87 "README.md",
88 "guest/.cargo/config.toml",
89 "guest/Cargo.toml",
90 "guest/rust-toolchain.toml",
91 "guest/src/main.rs",
92 "host/Cargo.toml",
93 "host/rust-toolchain.toml",
94 "host/src/main.rs",
95 ];
96
97 #[test]
98 fn defaults_to_sdk_git_repository() {
99 let dependency =
100 deps::resolve_crate_dependency(Path::new("."), None, None, "airbender-sdk")
101 .expect("resolve default SDK dependency");
102 assert_eq!(
103 dependency,
104 "git = \"https://github.com/matter-labs/airbender-platform\", branch = \"main\""
105 );
106 }
107
108 #[test]
109 fn prefers_explicit_sdk_version() {
110 let dependency =
111 deps::resolve_crate_dependency(Path::new("."), None, Some("0.1.0"), "airbender-sdk")
112 .expect("resolve version SDK dependency");
113 assert_eq!(dependency, "version = \"0.1.0\"");
114 }
115
116 #[test]
117 fn rejects_empty_sdk_version() {
118 let err = deps::resolve_crate_dependency(Path::new("."), None, Some(""), "airbender-sdk")
119 .expect_err("empty version should fail");
120 assert!(err.to_string().contains("--sdk-version"));
121 }
122
123 #[test]
124 fn resolves_dependency_from_workspace_root() {
125 let root = test_workspace_dir("sdk-workspace-root");
126 let destination = root.join("destination").join("guest");
127 let sdk_workspace = root.join("sdk-workspace");
128 let sdk = sdk_workspace.join("crates").join("airbender-sdk");
129 let host = sdk_workspace.join("crates").join("airbender-host");
130
131 fs::create_dir_all(&destination).expect("create destination dir");
132 fs::create_dir_all(&sdk).expect("create sdk dir");
133 fs::create_dir_all(&host).expect("create host dir");
134 fs::write(
135 sdk.join("Cargo.toml"),
136 "[package]\nname = \"airbender-sdk\"\n",
137 )
138 .expect("write sdk Cargo.toml");
139 fs::write(
140 host.join("Cargo.toml"),
141 "[package]\nname = \"airbender-host\"\n",
142 )
143 .expect("write host Cargo.toml");
144
145 let dependency = deps::resolve_crate_dependency(
146 &destination,
147 Some(sdk_workspace.as_path()),
148 None,
149 "airbender-sdk",
150 )
151 .expect("resolve path SDK dependency");
152 assert_eq!(
153 dependency,
154 "path = \"../../sdk-workspace/crates/airbender-sdk\""
155 );
156
157 fs::remove_dir_all(&root).expect("remove test directories");
158 }
159
160 #[test]
161 fn resolves_host_from_sibling_sdk_path() {
162 let root = test_workspace_dir("sdk-sibling-host");
163 let destination = root.join("destination").join("host");
164 let crates_dir = root.join("sdk-workspace").join("crates");
165 let sdk = crates_dir.join("airbender-sdk");
166 let host = crates_dir.join("airbender-host");
167
168 fs::create_dir_all(&destination).expect("create destination dir");
169 fs::create_dir_all(&sdk).expect("create sdk dir");
170 fs::create_dir_all(&host).expect("create host dir");
171 fs::write(
172 sdk.join("Cargo.toml"),
173 "[package]\nname = \"airbender-sdk\"\n",
174 )
175 .expect("write sdk Cargo.toml");
176 fs::write(
177 host.join("Cargo.toml"),
178 "[package]\nname = \"airbender-host\"\n",
179 )
180 .expect("write host Cargo.toml");
181
182 let dependency = deps::resolve_crate_dependency(
183 &destination,
184 Some(sdk.as_path()),
185 None,
186 "airbender-host",
187 )
188 .expect("resolve host dependency from sibling path");
189 assert_eq!(
190 dependency,
191 "path = \"../../sdk-workspace/crates/airbender-host\""
192 );
193
194 fs::remove_dir_all(&root).expect("remove test directories");
195 }
196
197 #[test]
198 fn new_scaffolds_host_and_guest() {
199 let root = test_workspace_dir("scaffold-host-guest");
200 let destination = root.join("hello-airbender");
201
202 run(NewArgs {
203 path: Some(destination.clone()),
204 name: Some("hello-airbender".to_string()),
205 enable_std: false,
206 allocator: NewAllocatorArg::Talc,
207 prover_backend: NewProverBackendArg::Dev,
208 yes: true,
209 sdk_path: None,
210 sdk_version: Some("0.1.0".to_string()),
211 })
212 .expect("create scaffold");
213
214 assert_rendered_files_snapshot(
215 "new_scaffolds_host_and_guest",
216 &destination,
217 ALL_SCAFFOLD_FILES,
218 );
219
220 fs::remove_dir_all(&root).expect("remove test directories");
221 }
222
223 #[test]
224 fn new_enable_std_updates_guest_template() {
225 let root = test_workspace_dir("scaffold-enable-std");
226 let destination = root.join("hello-airbender");
227
228 run(NewArgs {
229 path: Some(destination.clone()),
230 name: Some("hello-airbender".to_string()),
231 enable_std: true,
232 allocator: NewAllocatorArg::Talc,
233 prover_backend: NewProverBackendArg::Dev,
234 yes: true,
235 sdk_path: None,
236 sdk_version: Some("0.1.0".to_string()),
237 })
238 .expect("create std scaffold");
239
240 assert_rendered_files_snapshot(
241 "new_enable_std_updates_guest_template",
242 &destination,
243 &["guest/Cargo.toml", "guest/src/main.rs"],
244 );
245
246 fs::remove_dir_all(&root).expect("remove test directories");
247 }
248
249 #[test]
250 fn new_bump_allocator_disables_sdk_default_features() {
251 let root = test_workspace_dir("scaffold-bump-allocator");
252 let destination = root.join("hello-airbender");
253
254 run(NewArgs {
255 path: Some(destination.clone()),
256 name: Some("hello-airbender".to_string()),
257 enable_std: false,
258 allocator: NewAllocatorArg::Bump,
259 prover_backend: NewProverBackendArg::Dev,
260 yes: true,
261 sdk_path: None,
262 sdk_version: Some("0.1.0".to_string()),
263 })
264 .expect("create bump allocator scaffold");
265
266 assert_rendered_files_snapshot(
267 "new_bump_allocator_disables_sdk_default_features",
268 &destination,
269 &["guest/Cargo.toml"],
270 );
271
272 fs::remove_dir_all(&root).expect("remove test directories");
273 }
274
275 #[test]
276 fn new_custom_allocator_adds_allocator_hook() {
277 let root = test_workspace_dir("scaffold-custom-allocator");
278 let destination = root.join("hello-airbender");
279
280 run(NewArgs {
281 path: Some(destination.clone()),
282 name: Some("hello-airbender".to_string()),
283 enable_std: false,
284 allocator: NewAllocatorArg::Custom,
285 prover_backend: NewProverBackendArg::Dev,
286 yes: true,
287 sdk_path: None,
288 sdk_version: Some("0.1.0".to_string()),
289 })
290 .expect("create custom allocator scaffold");
291
292 assert_rendered_files_snapshot(
293 "new_custom_allocator_adds_allocator_hook",
294 &destination,
295 &["guest/Cargo.toml", "guest/src/main.rs"],
296 );
297
298 fs::remove_dir_all(&root).expect("remove test directories");
299 }
300
301 #[test]
302 fn new_gpu_backend_generates_real_prover_setup() {
303 let root = test_workspace_dir("scaffold-gpu-prover");
304 let destination = root.join("hello-airbender");
305
306 run(NewArgs {
307 path: Some(destination.clone()),
308 name: Some("hello-airbender".to_string()),
309 enable_std: false,
310 allocator: NewAllocatorArg::Talc,
311 prover_backend: NewProverBackendArg::Gpu,
312 yes: true,
313 sdk_path: None,
314 sdk_version: Some("0.1.0".to_string()),
315 })
316 .expect("create gpu scaffold");
317
318 assert_rendered_files_snapshot(
319 "new_gpu_backend_generates_real_prover_setup",
320 &destination,
321 &["README.md", "host/Cargo.toml", "host/src/main.rs"],
322 );
323
324 fs::remove_dir_all(&root).expect("remove test directories");
325 }
326
327 fn assert_rendered_files_snapshot(snapshot_name: &str, root: &Path, relative_paths: &[&str]) {
328 let mut rendered = String::new();
329
330 for relative_path in relative_paths {
331 let contents = fs::read_to_string(root.join(relative_path))
332 .unwrap_or_else(|err| panic!("read rendered file `{relative_path}`: {err}"));
333 rendered.push_str(&format!("=== {relative_path} ===\n"));
334 rendered.push_str(&contents);
335 if !contents.ends_with('\n') {
336 rendered.push('\n');
337 }
338 }
339
340 insta::assert_snapshot!(snapshot_name, rendered);
341 }
342
343 fn test_workspace_dir(suffix: &str) -> PathBuf {
344 let timestamp = SystemTime::now()
345 .duration_since(UNIX_EPOCH)
346 .expect("system clock should be after unix epoch")
347 .as_nanos();
348 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
349 .join("..")
350 .join("..")
351 .join("tmp")
352 .join(format!(
353 "cargo-airbender-new-tests-{suffix}-{timestamp}-{}",
354 std::process::id()
355 ))
356 }
357}