Skip to main content

nargo_cli/cli/
mod.rs

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    // REMINDER: Also change this flag in the LSP test lens if renamed
65    #[arg(long, hide = true, global = true, default_value = "./", value_parser = parse_and_normalize_path)]
66    program_dir: PathBuf,
67
68    /// Override the default target directory.
69    #[arg(long, hide = true, global = true, value_parser = parse_and_normalize_path)]
70    target_dir: Option<PathBuf>,
71}
72
73/// Options for commands that work on either workspace or package scope.
74#[derive(Args, Clone, Debug, Default)]
75pub(crate) struct PackageOptions {
76    /// The name of the package to run the command on.
77    /// By default run on the first one found moving up along the ancestors of the current directory.
78    #[clap(long, conflicts_with = "workspace")]
79    package: Option<CrateName>,
80
81    /// Run on all packages in the workspace
82    #[clap(long, conflicts_with = "package")]
83    workspace: bool,
84}
85
86impl PackageOptions {
87    /// Decide which package to run the command on:
88    /// * `package` if non-empty
89    /// * all packages if `workspace` is `true`
90    /// * otherwise the default package
91    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
124/// Commands that can execute on the workspace level, or be limited to a selected package.
125trait WorkspaceCommand {
126    /// Indicate which package the command will be applied to.
127    fn package_selection(&self) -> PackageSelection;
128    /// The kind of lock the command needs to take out on the selected packages.
129    fn lock_type(&self) -> LockType;
130}
131
132/// What kind of lock to take out on the (selected) workspace members.
133#[derive(Clone, Debug, PartialEq, Eq)]
134#[allow(dead_code)] // Not using `Shared` at the moment, e.g. while we `debug` we can `compile` a different version.
135enum LockType {
136    /// For commands that write artifacts.
137    Exclusive,
138    /// For commands that read artifacts, but never write them.
139    Shared,
140    /// For commands that cannot interfere with others.
141    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
179/// Read a given program directory into a workspace.
180fn 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/// "with_workspace", but use a dummy workspace when 'debug_compile_stdin' is enabled
196#[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        // dummy root dir
205        let root_dir = PathBuf::new();
206        // This `PackageMetadata::default()` is leading to a clippy error but the suggested solution
207        // is invalid because the fields are private
208        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
232/// Find the root directory, parse the workspace, lock the packages, then execute the command.
233fn 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    // All commands need to run on the workspace level, because that's where the `target` directory is.
246    let workspace_dir = nargo_toml::find_root(&config.program_dir, true)?;
247    let package_dir = nargo_toml::find_root(&config.program_dir, false)?;
248    // Check if we're running inside the directory of a package, without having selected the entire workspace
249    // or a specific package; if that's the case then parse the package name to select it in the workspace.
250    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    // Parse the top level workspace with the member selected.
259    let mut workspace = read_workspace(&workspace_dir, selection)?;
260    // Optionally override the target directory. It's only done here because most commands like the LSP and DAP
261    // don't read or write artifacts, so they don't use the target directory.
262    workspace.target_dir = config.target_dir;
263    // Lock manifests if the command needs it.
264    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
271/// Lock the (selected) packages in the workspace.
272/// The lock taken can be shared for commands that only read the artifacts,
273/// or exclusive for the ones that (might) write artifacts as well.
274fn 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}