Skip to main content

cargo_airbender/commands/new/
mod.rs

1mod 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}