Skip to main content

noi_build/
lib.rs

1use std::{
2    env,
3    path::{Path, PathBuf},
4    process::Command,
5};
6
7use anyhow::{anyhow, Context, Result};
8use fs_err as fs;
9use walkdir::WalkDir;
10
11const EXPORT_SUBDIR: &str = "noi_exports";
12
13#[derive(Debug, Clone)]
14pub struct BuildOptions {
15    pub program_dir: PathBuf,
16    pub out_dir: PathBuf,
17}
18
19#[derive(Debug, Clone)]
20pub struct ExportSummary {
21    pub staged_dir: PathBuf,
22    pub artifacts: Vec<PathBuf>,
23}
24
25pub fn export(opts: &BuildOptions) -> Result<ExportSummary> {
26    ensure_program_dir(&opts.program_dir)?;
27    let stage_dir = opts.out_dir.join(EXPORT_SUBDIR);
28    if stage_dir.exists() {
29        fs::remove_dir_all(&stage_dir).with_context(|| {
30            format!(
31                "failed to clear previous exports at `{}`",
32                stage_dir.display()
33            )
34        })?;
35    }
36    fs::create_dir_all(&stage_dir)?;
37
38    let run_result = run_nargo(&opts.program_dir);
39    emit_cargo_metadata(&opts.program_dir, &stage_dir);
40
41    let staged = match stage_exports(&opts.program_dir, &stage_dir) {
42        Ok(result) => {
43            if let StageSource::Fallback(ref path) = result.source {
44                println!(
45                    "cargo:warning=Using pre-exported Noir artifacts from {}",
46                    path.display()
47                );
48                if let Err(err) = &run_result {
49                    verbose(&format!("nargo export skipped: {err}"));
50                }
51            }
52            result
53        }
54        Err(stage_err) => {
55            if let Err(run_err) = run_result {
56                return Err(stage_err.context(run_err));
57            }
58            return Err(stage_err);
59        }
60    };
61
62    Ok(ExportSummary {
63        staged_dir: stage_dir,
64        artifacts: staged.artifacts,
65    })
66}
67
68fn ensure_program_dir(path: &Path) -> Result<()> {
69    if !path.exists() {
70        return Err(anyhow!(
71            "program directory `{}` does not exist",
72            path.display()
73        ));
74    }
75    if !path.join("Nargo.toml").exists() {
76        return Err(anyhow!(
77            "`{}` is missing Nargo.toml; point BuildOptions::program_dir at a Noir workspace",
78            path.display()
79        ));
80    }
81    Ok(())
82}
83
84fn run_nargo(program_dir: &Path) -> Result<()> {
85    let bin = env::var("NOI_NARGO_BIN").unwrap_or_else(|_| "nargo".into());
86    verbose(&format!(
87        "running `{bin} export` in {}",
88        program_dir.display()
89    ));
90
91    let status = Command::new(&bin)
92        .arg("export")
93        .current_dir(program_dir)
94        .status()
95        .with_context(|| format!("failed to start `{bin}`"))?;
96
97    if !status.success() {
98        return Err(anyhow!("`{bin} export` failed with status {status}"));
99    }
100
101    Ok(())
102}
103
104fn stage_exports(program_dir: &Path, dest_dir: &Path) -> Result<StageResult> {
105    let target_root = program_dir.join("target");
106    if target_root.exists() {
107        let staged = copy_artifacts(&target_root, dest_dir)?;
108        if !staged.is_empty() {
109            verbose(&format!(
110                "staged {} export(s) into {}",
111                staged.len(),
112                dest_dir.display()
113            ));
114            return Ok(StageResult {
115                artifacts: staged,
116                source: StageSource::Target,
117            });
118        }
119    }
120
121    let fallback = program_dir.join("exports");
122    if fallback.exists() {
123        let staged = copy_artifacts(&fallback, dest_dir)?;
124        if !staged.is_empty() {
125            verbose(&format!(
126                "staged {} fallback export(s) into {}",
127                staged.len(),
128                dest_dir.display()
129            ));
130            return Ok(StageResult {
131                artifacts: staged,
132                source: StageSource::Fallback(fallback),
133            });
134        }
135    }
136
137    Err(anyhow!(
138        "no JSON artifacts found in `{}` (expected either `target/` or `exports/`)",
139        program_dir.display()
140    ))
141}
142
143fn copy_artifacts(root: &Path, dest_dir: &Path) -> Result<Vec<PathBuf>> {
144    let mut staged = Vec::new();
145    for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
146        if !entry.file_type().is_file() {
147            continue;
148        }
149        if entry
150            .path()
151            .extension()
152            .and_then(|ext| ext.to_str())
153            .map(|ext| ext != "json")
154            .unwrap_or(true)
155        {
156            continue;
157        }
158
159        let rel = entry
160            .path()
161            .strip_prefix(root)
162            .unwrap_or_else(|_| entry.path());
163        let dest = dest_dir.join(rel);
164        if let Some(parent) = dest.parent() {
165            fs::create_dir_all(parent)?;
166        }
167        fs::copy(entry.path(), &dest).with_context(|| {
168            format!(
169                "failed to copy `{}` to `{}`",
170                entry.path().display(),
171                dest.display()
172            )
173        })?;
174        staged.push(dest);
175    }
176
177    Ok(staged)
178}
179
180struct StageResult {
181    artifacts: Vec<PathBuf>,
182    source: StageSource,
183}
184
185enum StageSource {
186    Target,
187    Fallback(PathBuf),
188}
189
190fn emit_cargo_metadata(program_dir: &Path, stage_dir: &Path) {
191    println!("cargo:rustc-env=NOI_EXPORT_DIR={}", stage_dir.display());
192    println!(
193        "cargo:rerun-if-changed={}",
194        program_dir.join("Nargo.toml").display()
195    );
196
197    let src_dir = program_dir.join("src");
198    if src_dir.exists() {
199        for entry in WalkDir::new(&src_dir).into_iter().filter_map(|e| e.ok()) {
200            if entry.file_type().is_file() {
201                println!("cargo:rerun-if-changed={}", entry.path().display());
202            }
203        }
204    }
205}
206
207fn verbose(msg: &str) {
208    if env::var("NOI_VERBOSE").as_deref() == Ok("1") {
209        eprintln!("[noi-build] {msg}");
210    }
211}