1use clap::{Args, Parser, Subcommand};
2use const_format::formatcp;
3use nargo::workspace::Workspace;
4use nargo_toml::{
5 ManifestError, NargoToml, PackageConfig, PackageMetadata, PackageSelection,
6 get_package_manifest, resolve_workspace_from_fixed_toml, resolve_workspace_from_toml,
7};
8use noir_artifact_cli::commands::parse_and_normalize_path;
9use noirc_driver::{CrateName, NOIR_ARTIFACT_VERSION_STRING};
10use std::{
11 collections::BTreeMap,
12 fs::File,
13 path::{Path, PathBuf},
14 str::FromStr,
15};
16
17use color_eyre::eyre;
18
19use crate::errors::CliError;
20
21mod check_cmd;
22pub mod compile_cmd;
23mod dap_cmd;
24mod debug_cmd;
25mod doc_cmd;
26mod execute_cmd;
27mod expand_cmd;
28mod export_cmd;
29mod fmt_cmd;
30mod fuzz_cmd;
31mod generate_completion_script_cmd;
32mod info_cmd;
33mod init_cmd;
34mod interpret_cmd;
35mod lsp_cmd;
36mod new_cmd;
37mod test_cmd;
38
39const GIT_HASH: &str = env!("GIT_COMMIT");
40const IS_DIRTY: &str = env!("GIT_DIRTY");
41const NARGO_VERSION: &str = env!("CARGO_PKG_VERSION");
42
43static VERSION_STRING: &str = formatcp!(
44 "version = {}\nnoirc version = {}\n(git version hash: {}, is dirty: {})",
45 NARGO_VERSION,
46 NOIR_ARTIFACT_VERSION_STRING,
47 GIT_HASH,
48 IS_DIRTY
49);
50
51#[derive(Parser, Debug)]
52#[command(name="nargo", author, version=VERSION_STRING, about, long_about = None)]
53struct NargoCli {
54 #[command(subcommand)]
55 command: NargoCommand,
56
57 #[clap(flatten)]
58 config: NargoConfig,
59}
60
61#[non_exhaustive]
62#[derive(Args, Clone, Debug)]
63pub struct NargoConfig {
64 #[arg(long, hide = true, global = true, default_value = "./", value_parser = parse_and_normalize_path)]
66 program_dir: PathBuf,
67
68 #[arg(long, hide = true, global = true, value_parser = parse_and_normalize_path)]
70 target_dir: Option<PathBuf>,
71}
72
73#[derive(Args, Clone, Debug, Default)]
75pub(crate) struct PackageOptions {
76 #[clap(long, conflicts_with = "workspace")]
79 package: Option<CrateName>,
80
81 #[clap(long, conflicts_with = "package")]
83 workspace: bool,
84}
85
86impl PackageOptions {
87 pub(crate) fn package_selection(&self) -> PackageSelection {
92 let default_selection =
93 if self.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll };
94
95 self.package.clone().map_or(default_selection, PackageSelection::Selected)
96 }
97}
98
99#[non_exhaustive]
100#[derive(Subcommand, Clone, Debug)]
101enum NargoCommand {
102 Check(check_cmd::CheckCommand),
103 Fmt(fmt_cmd::FormatCommand),
104 #[command(alias = "build")]
105 Compile(compile_cmd::CompileCommand),
106 #[command(hide = true)]
107 Interpret(interpret_cmd::InterpretCommand),
108 New(new_cmd::NewCommand),
109 Init(init_cmd::InitCommand),
110 Execute(execute_cmd::ExecuteCommand),
111 Export(export_cmd::ExportCommand),
112 Debug(debug_cmd::DebugCommand),
113 Test(test_cmd::TestCommand),
114 Fuzz(fuzz_cmd::FuzzCommand),
115 Info(info_cmd::InfoCommand),
116 Lsp(lsp_cmd::LspCommand),
117 #[command(hide = true)]
118 Dap(dap_cmd::DapCommand),
119 Expand(expand_cmd::ExpandCommand),
120 Doc(doc_cmd::DocCommand),
121 GenerateCompletionScript(generate_completion_script_cmd::GenerateCompletionScriptCommand),
122}
123
124trait WorkspaceCommand {
126 fn package_selection(&self) -> PackageSelection;
128 fn lock_type(&self) -> LockType;
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
134#[allow(dead_code)] enum LockType {
136 Exclusive,
138 Shared,
140 None,
142}
143
144#[cfg(not(feature = "codegen-docs"))]
145#[tracing::instrument(level = "trace")]
146pub(crate) fn start_cli() -> eyre::Result<()> {
147 let NargoCli { command, config } = NargoCli::parse();
148
149 match command {
150 NargoCommand::New(args) => new_cmd::run(args, config),
151 NargoCommand::Init(args) => init_cmd::run(args, config),
152 NargoCommand::Check(args) => with_workspace(args, config, check_cmd::run),
153 NargoCommand::Compile(args) => compile_with_maybe_dummy_workspace(args, config),
154 NargoCommand::Interpret(args) => with_workspace(args, config, interpret_cmd::run),
155 NargoCommand::Debug(args) => with_workspace(args, config, debug_cmd::run),
156 NargoCommand::Execute(args) => with_workspace(args, config, execute_cmd::run),
157 NargoCommand::Export(args) => with_workspace(args, config, export_cmd::run),
158 NargoCommand::Test(args) => with_workspace(args, config, test_cmd::run),
159 NargoCommand::Fuzz(args) => with_workspace(args, config, fuzz_cmd::run),
160 NargoCommand::Info(args) => with_workspace(args, config, info_cmd::run),
161 NargoCommand::Lsp(_) => lsp_cmd::run(),
162 NargoCommand::Dap(args) => dap_cmd::run(args),
163 NargoCommand::Fmt(args) => with_workspace(args, config, fmt_cmd::run),
164 NargoCommand::Expand(args) => with_workspace(args, config, expand_cmd::run),
165 NargoCommand::Doc(args) => with_workspace(args, config, doc_cmd::run),
166 NargoCommand::GenerateCompletionScript(args) => generate_completion_script_cmd::run(args),
167 }?;
168
169 Ok(())
170}
171
172#[cfg(feature = "codegen-docs")]
173pub(crate) fn start_cli() -> eyre::Result<()> {
174 let markdown: String = clap_markdown::help_markdown::<NargoCli>();
175 println!("{markdown}");
176 Ok(())
177}
178
179fn read_workspace(
181 program_dir: &Path,
182 selection: PackageSelection,
183) -> Result<Workspace, ManifestError> {
184 let toml_path = get_package_manifest(program_dir)?;
185
186 let workspace = resolve_workspace_from_toml(
187 &toml_path,
188 selection,
189 Some(NOIR_ARTIFACT_VERSION_STRING.to_owned()),
190 )?;
191
192 Ok(workspace)
193}
194
195#[allow(clippy::field_reassign_with_default)]
197fn compile_with_maybe_dummy_workspace(
198 cmd: compile_cmd::CompileCommand,
199 config: NargoConfig,
200) -> Result<(), CliError> {
201 if cmd.compile_options.debug_compile_stdin {
202 let package_name = "debug_compile_stdin".to_string();
203
204 let root_dir = PathBuf::new();
206 let mut package = PackageMetadata::default();
209 package.name = package_name.clone();
210 package.package_type = Some("bin".into());
211 let dependencies = BTreeMap::new();
212 let package_config = PackageConfig { package, dependencies };
213 let config = nargo_toml::Config::Package { package_config };
214 let nargo_toml = NargoToml { root_dir, config };
215 let package_name =
216 CrateName::from_str(&package_name).expect("package_name to be a valid CrateName");
217 let selection = PackageSelection::Selected(package_name);
218
219 let assume_default_entry = true;
220 let workspace = resolve_workspace_from_fixed_toml(
221 nargo_toml,
222 selection,
223 Some(NOIR_ARTIFACT_VERSION_STRING.to_owned()),
224 assume_default_entry,
225 )?;
226 compile_cmd::run(cmd, workspace)
227 } else {
228 with_workspace(cmd, config, compile_cmd::run)
229 }
230}
231
232fn with_workspace<C, R>(cmd: C, config: NargoConfig, run: R) -> Result<(), CliError>
234where
235 C: WorkspaceCommand,
236 R: FnOnce(C, Workspace) -> Result<(), CliError>,
237{
238 if !config.program_dir.exists() {
239 return Err(CliError::ProgramDirDoesNotExist(config.program_dir));
240 }
241 if !config.program_dir.is_dir() {
242 return Err(CliError::ProgramDirIsNotADirectory(config.program_dir));
243 }
244
245 let workspace_dir = nargo_toml::find_root(&config.program_dir, true)?;
247 let package_dir = nargo_toml::find_root(&config.program_dir, false)?;
248 let selection = match cmd.package_selection() {
251 PackageSelection::DefaultOrAll if workspace_dir != package_dir => {
252 let package = read_workspace(&package_dir, PackageSelection::DefaultOrAll)?;
253 let package = package.into_iter().next().expect("there should be exactly 1 package");
254 PackageSelection::Selected(package.name.clone())
255 }
256 other => other,
257 };
258 let mut workspace = read_workspace(&workspace_dir, selection)?;
260 workspace.target_dir = config.target_dir;
263 let _locks = match cmd.lock_type() {
265 LockType::None => None,
266 typ => Some(lock_workspace(&workspace, typ == LockType::Exclusive)?),
267 };
268 run(cmd, workspace)
269}
270
271fn lock_workspace(
275 workspace: &Workspace,
276 exclusive: bool,
277) -> Result<Vec<impl Drop + use<>>, CliError> {
278 struct LockedFile(File);
279
280 impl Drop for LockedFile {
281 fn drop(&mut self) {
282 let _ = fs2::FileExt::unlock(&self.0);
283 }
284 }
285
286 let mut locks = Vec::new();
287 for pkg in workspace {
288 let toml_path = get_package_manifest(&pkg.root_dir)?;
289 let path_display = toml_path.display();
290
291 let file = File::open(&toml_path)
292 .unwrap_or_else(|e| panic!("Expected {path_display} to exist: {e}"));
293
294 if exclusive {
295 if fs2::FileExt::try_lock_exclusive(&file).is_err() {
296 eprintln!("Waiting for lock on {path_display}...");
297 }
298 fs2::FileExt::lock_exclusive(&file)
299 .unwrap_or_else(|e| panic!("Failed to lock {path_display}: {e}"));
300 } else {
301 if fs2::FileExt::try_lock_shared(&file).is_err() {
302 eprintln!("Waiting for lock on {path_display}...",);
303 }
304 fs2::FileExt::lock_shared(&file)
305 .unwrap_or_else(|e| panic!("Failed to lock {path_display}: {e}"));
306 }
307
308 locks.push(LockedFile(file));
309 }
310 Ok(locks)
311}
312
313#[cfg(test)]
314mod tests {
315 use super::NargoCli;
316 use clap::Parser;
317
318 #[test]
319 fn test_parse_target_dir() {
320 let cmd = "nargo --program-dir . --target-dir ../foo/bar execute";
321 let cli = NargoCli::try_parse_from(cmd.split_ascii_whitespace()).expect("should parse");
322
323 let target_dir = cli.config.target_dir.expect("should parse target dir");
324 assert!(target_dir.is_absolute(), "should be made absolute");
325 assert!(target_dir.ends_with("foo/bar"));
326
327 let cmd = "nargo --program-dir . execute";
328 let cli = NargoCli::try_parse_from(cmd.split_ascii_whitespace()).expect("should parse");
329 assert!(cli.config.target_dir.is_none());
330 }
331}