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}