rune/cli/
mod.rs

1//! Helper to build customized commandline interfaces using custom rune
2//! contexts.
3//!
4//! This can be used to:
5//! * Generate documentation using types only available in your context.
6//! * Build a language server, which is aware of things only available in your
7//!   context.
8
9mod ace;
10mod benches;
11mod check;
12mod doc;
13mod format;
14mod languageserver;
15mod loader;
16mod naming;
17mod out;
18mod run;
19mod tests;
20mod visitor;
21
22use std::fmt;
23use std::io::{self, IsTerminal, Write};
24use std::path::{Path, PathBuf};
25
26use rust_alloc::string::String;
27use rust_alloc::vec::Vec;
28
29use crate as rune;
30use crate::alloc;
31use crate::alloc::prelude::*;
32use crate::workspace::{self, FoundKind, WorkspaceFilter};
33
34use anyhow::{bail, Context as _, Error, Result};
35use clap::{Parser, Subcommand, ValueEnum};
36use tracing_subscriber::filter::EnvFilter;
37
38use crate::compile::ParseOptionError;
39use crate::modules::capture_io::CaptureIo;
40use crate::termcolor::{ColorChoice, StandardStream};
41use crate::{Context, ContextError, Hash, ItemBuf, Options};
42
43use self::out::{Color, Io, Stream};
44
45/// Default about splash.
46const DEFAULT_ABOUT: &str = "The Rune Language Interpreter";
47
48/// Options for building context.
49#[non_exhaustive]
50pub struct ContextOptions<'a> {
51    /// If we need to capture I/O this is set to the capture instance you should
52    /// be using to do so.
53    pub capture: Option<&'a CaptureIo>,
54    /// If we're running in a test context.
55    pub test: bool,
56}
57
58/// Type used to build a context.
59pub type ContextBuilder = dyn FnMut(ContextOptions<'_>) -> Result<Context, ContextError>;
60
61/// A rune-based entrypoint used for custom applications.
62///
63/// This can be used to construct your own rune-based environment, with a custom
64/// configuration such as your own modules.
65#[derive(Default)]
66pub struct Entry<'a> {
67    about: Option<alloc::String>,
68    context: Option<&'a mut ContextBuilder>,
69}
70
71impl<'a> Entry<'a> {
72    /// Entry point.
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Set about string used in cli output.
78    ///
79    /// For example, this is the first row outputted when the command prints its
80    /// help text.
81    ///
82    /// # Examples
83    ///
84    /// ```no_run
85    /// rune::cli::Entry::new()
86    ///     .about("My own interpreter")
87    ///     .run();
88    ///```
89    pub fn about(mut self, about: impl fmt::Display) -> Self {
90        self.about = Some(
91            about
92                .try_to_string()
93                .expect("Failed to format about string"),
94        );
95        self
96    }
97
98    /// Configure context to use using a builder.
99    ///
100    /// # Examples
101    ///
102    /// ```no_run
103    /// use rune::{Context, ContextError, Module};
104    ///
105    /// fn my_module() -> Result<Module, ContextError> {
106    ///     let module = Module::default();
107    ///     /* install things into module */
108    ///     Ok(module)
109    /// }
110    ///
111    /// rune::cli::Entry::new()
112    ///     .about("My own interpreter")
113    ///     .context(&mut |opts| {
114    ///         let mut c = Context::with_config(opts.capture.is_none())?;
115    ///         c.install(my_module()?);
116    ///         Ok(c)
117    ///     })
118    ///     .run();
119    ///```
120    pub fn context(mut self, context: &'a mut ContextBuilder) -> Self {
121        self.context = Some(context);
122        self
123    }
124
125    /// Run the configured application.
126    ///
127    /// This will take over stdout and stdin.
128    pub fn run(self) -> ! {
129        let runtime = tokio::runtime::Builder::new_current_thread()
130            .enable_all()
131            .build()
132            .expect("Failed to build runtime");
133
134        match runtime.block_on(self.inner()) {
135            Ok(exit_code) => {
136                std::process::exit(exit_code as i32);
137            }
138            Err(error) => {
139                let o = std::io::stderr();
140                // ignore error because stdout / stderr might've been closed.
141                let _ = format_errors(&mut o.lock(), &error);
142                std::process::exit(ExitCode::Failure as i32);
143            }
144        }
145    }
146
147    /// Run the configured application without starting a new tokio runtime.
148    ///
149    /// This will take over stdout and stdin.
150    pub async fn run_async(self) -> ! {
151        match self.inner().await {
152            Ok(exit_code) => {
153                std::process::exit(exit_code as i32);
154            }
155            Err(error) => {
156                let o = std::io::stderr();
157                // ignore error because stdout / stderr might've been closed.
158                let _ = format_errors(&mut o.lock(), &error);
159                std::process::exit(ExitCode::Failure as i32);
160            }
161        }
162    }
163
164    async fn inner(mut self) -> Result<ExitCode> {
165        let args = match Args::try_parse() {
166            Ok(args) => args,
167            Err(e) => {
168                let about = self.about.as_deref().unwrap_or(DEFAULT_ABOUT);
169
170                let code = if e.use_stderr() {
171                    let o = std::io::stderr();
172                    let mut o = o.lock();
173                    o.write_all(about.as_bytes())?;
174                    writeln!(o)?;
175                    writeln!(o)?;
176                    writeln!(o, "{e}")?;
177                    o.flush()?;
178                    ExitCode::Failure
179                } else {
180                    let o = std::io::stdout();
181                    let mut o = o.lock();
182                    o.write_all(about.as_bytes())?;
183                    writeln!(o)?;
184                    writeln!(o)?;
185                    writeln!(o, "{e}")?;
186                    o.flush()?;
187                    ExitCode::Success
188                };
189
190                return Ok(code);
191            }
192        };
193
194        if args.version {
195            let o = std::io::stdout();
196            let mut o = o.lock();
197            let about = self.about.as_deref().unwrap_or(DEFAULT_ABOUT);
198            o.write_all(about.as_bytes())?;
199            o.flush()?;
200            return Ok(ExitCode::Success);
201        }
202
203        let choice = match args.color {
204            ColorArgument::Always => ColorChoice::Always,
205            ColorArgument::Ansi => ColorChoice::AlwaysAnsi,
206            ColorArgument::Auto => {
207                if std::io::stdin().is_terminal() {
208                    ColorChoice::Auto
209                } else {
210                    ColorChoice::Never
211                }
212            }
213            ColorArgument::Never => ColorChoice::Never,
214        };
215
216        let mut stdout = StandardStream::stdout(choice);
217        let mut stderr = StandardStream::stderr(choice);
218
219        let mut io = Io::new(&mut stdout, &mut stderr);
220
221        tracing_subscriber::fmt()
222            .with_env_filter(EnvFilter::from_default_env())
223            .init();
224
225        match main_with_out(&mut io, &mut self, args).await {
226            Ok(code) => Ok(code),
227            Err(error) => {
228                let o = io.with_color(Stream::Stdout, Color::Error)?;
229                format_errors(o, &error)?;
230                o.close()?;
231                Ok(ExitCode::Failure)
232            }
233        }
234    }
235}
236
237/// A single entrypoint that can be built or processed.
238#[derive(TryClone)]
239pub(crate) enum EntryPoint<'a> {
240    /// A plain path entrypoint.
241    Path(PathBuf, bool),
242    /// A package entrypoint.
243    Package(workspace::FoundPackage<'a>),
244}
245
246impl EntryPoint<'_> {
247    /// Path to entrypoint.
248    pub(crate) fn path(&self) -> &Path {
249        match self {
250            EntryPoint::Path(path, _) => path,
251            EntryPoint::Package(p) => &p.found.path,
252        }
253    }
254
255    /// If a path is an additional argument.
256    pub(crate) fn is_argument(&self) -> bool {
257        match self {
258            EntryPoint::Path(_, explicit) => *explicit,
259            EntryPoint::Package(..) => false,
260        }
261    }
262}
263
264impl fmt::Display for EntryPoint<'_> {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        match self {
267            EntryPoint::Path(path, false) => {
268                write!(f, "path in {}", path.display())
269            }
270            EntryPoint::Path(path, true) => {
271                write!(f, "path in {} (argument)", path.display())
272            }
273            EntryPoint::Package(package) => {
274                write!(
275                    f,
276                    "package `{}` in {} ({})",
277                    package.package.name,
278                    package.found.path.display(),
279                    package.found.kind
280                )
281            }
282        }
283    }
284}
285
286#[derive(Parser, Debug, Clone)]
287#[command(rename_all = "kebab-case")]
288struct CommandShared<T>
289where
290    T: clap::Args,
291{
292    #[command(flatten)]
293    shared: SharedFlags,
294    #[command(flatten)]
295    command: T,
296}
297
298impl<T> CommandShared<T>
299where
300    T: CommandBase + clap::Args,
301{
302    /// Construct compiler options from arguments.
303    fn options(&self) -> Result<Options, ParseOptionError> {
304        let mut options = Options::from_default_env()?;
305
306        // Command-specific override defaults.
307        if self.command.is_debug() {
308            options.debug_info(true);
309            options.test(true);
310            options.bytecode(false);
311        }
312
313        for option in &self.shared.compiler_option {
314            options.parse_option(option)?;
315        }
316
317        Ok(options)
318    }
319}
320
321#[derive(Clone, Copy)]
322struct CommandSharedRef<'a> {
323    shared: &'a SharedFlags,
324    command: &'a dyn CommandBase,
325}
326
327impl<'a> CommandSharedRef<'a> {
328    fn find(
329        &self,
330        all_targets: bool,
331        kind: AssetKind,
332        name: Option<&'a str>,
333    ) -> Option<WorkspaceFilter<'a>> {
334        if !all_targets && !self.command.is_workspace(kind) {
335            return None;
336        }
337
338        if let Some(name) = name {
339            return Some(WorkspaceFilter::Name(name));
340        }
341
342        self.shared.is_unfiltered().then_some(WorkspaceFilter::All)
343    }
344
345    #[inline]
346    fn find_bins(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
347        self.find(all_targets, AssetKind::Bin, self.shared.bin.as_deref())
348    }
349
350    #[inline]
351    fn find_libs(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
352        self.find(all_targets, AssetKind::Lib, self.shared.lib.as_deref())
353    }
354
355    #[inline]
356    fn find_tests(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
357        self.find(all_targets, AssetKind::Test, self.shared.test.as_deref())
358    }
359
360    #[inline]
361    fn find_examples(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
362        self.find(all_targets, AssetKind::Bin, self.shared.example.as_deref())
363    }
364
365    #[inline]
366    fn find_benches(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
367        self.find(all_targets, AssetKind::Bench, self.shared.bench.as_deref())
368    }
369}
370
371#[derive(Parser, Debug)]
372#[command(rename_all = "kebab-case")]
373struct HashFlags {
374    /// Generate a random hash.
375    #[arg(long)]
376    random: bool,
377    /// When generating a random hash, generate the given number of hashes.
378    #[arg(long)]
379    count: Option<usize>,
380    /// Items to generate hashes for.
381    item: Vec<String>,
382}
383
384enum AssetKind {
385    Bin,
386    Lib,
387    Test,
388    Bench,
389}
390
391trait CommandBase {
392    /// Test if the command should perform a debug build by default.
393    #[inline]
394    fn is_debug(&self) -> bool {
395        false
396    }
397
398    /// Test if the command should acquire workspace assets for the given asset kind.
399    #[inline]
400    fn is_workspace(&self, _: AssetKind) -> bool {
401        false
402    }
403
404    /// Describe the current command.
405    #[inline]
406    fn describe(&self) -> &str {
407        "Running"
408    }
409
410    /// Propagate related flags from command and config.
411    #[inline]
412    fn propagate(&mut self, _: &mut Config, _: &mut SharedFlags) {}
413
414    /// Extra paths to run.
415    #[inline]
416    fn paths(&self) -> &[PathBuf] {
417        &[]
418    }
419}
420
421#[derive(Subcommand, Debug)]
422enum Command {
423    /// Run checks but do not execute
424    Check(CommandShared<check::Flags>),
425    /// Build documentation.
426    Doc(CommandShared<doc::Flags>),
427    /// Build ace autocompletion.
428    Ace(CommandShared<ace::Flags>),
429    /// Run all tests but do not execute
430    Test(CommandShared<tests::Flags>),
431    /// Run the given program as a benchmark
432    Bench(CommandShared<benches::Flags>),
433    /// Run the designated script
434    Run(CommandShared<run::Flags>),
435    /// Format the provided file
436    Fmt(CommandShared<format::Flags>),
437    /// Run a language server.
438    LanguageServer(SharedFlags),
439    /// Helper command to generate type hashes.
440    Hash(HashFlags),
441}
442
443impl Command {
444    const ALL: [&'static str; 9] = [
445        "check",
446        "doc",
447        "ace",
448        "test",
449        "bench",
450        "run",
451        "fmt",
452        "languageserver",
453        "hash",
454    ];
455
456    fn as_command_base_mut(&mut self) -> Option<(&mut SharedFlags, &mut dyn CommandBase)> {
457        let (shared, command): (_, &mut dyn CommandBase) = match self {
458            Command::Check(shared) => (&mut shared.shared, &mut shared.command),
459            Command::Doc(shared) => (&mut shared.shared, &mut shared.command),
460            Command::Ace(shared) => (&mut shared.shared, &mut shared.command),
461            Command::Test(shared) => (&mut shared.shared, &mut shared.command),
462            Command::Bench(shared) => (&mut shared.shared, &mut shared.command),
463            Command::Run(shared) => (&mut shared.shared, &mut shared.command),
464            Command::Fmt(shared) => (&mut shared.shared, &mut shared.command),
465            Command::LanguageServer(..) => return None,
466            Command::Hash(..) => return None,
467        };
468
469        Some((shared, command))
470    }
471
472    fn as_command_shared_ref(&self) -> Option<CommandSharedRef<'_>> {
473        let (shared, command): (_, &dyn CommandBase) = match self {
474            Command::Check(shared) => (&shared.shared, &shared.command),
475            Command::Doc(shared) => (&shared.shared, &shared.command),
476            Command::Ace(shared) => (&shared.shared, &shared.command),
477            Command::Test(shared) => (&shared.shared, &shared.command),
478            Command::Bench(shared) => (&shared.shared, &shared.command),
479            Command::Run(shared) => (&shared.shared, &shared.command),
480            Command::Fmt(shared) => (&shared.shared, &shared.command),
481            Command::LanguageServer(..) => return None,
482            Command::Hash(..) => return None,
483        };
484
485        Some(CommandSharedRef { shared, command })
486    }
487}
488
489enum BuildPath<'a> {
490    /// A plain path entry.
491    Path(&'a Path, bool),
492    /// An entry from the specified package.
493    Package(workspace::FoundPackage<'a>),
494}
495
496#[derive(Default)]
497struct Config {
498    /// Whether the touput has been filtered at all.
499    filtered: bool,
500    /// Whether or not the test module should be included.
501    test: bool,
502    /// Whether or not to use verbose output.
503    verbose: bool,
504    /// Always include all targets.
505    all_targets: bool,
506    /// Manifest root directory.
507    manifest_root: Option<PathBuf>,
508}
509
510#[derive(Default)]
511struct Inputs {
512    /// Loaded build manifest.
513    manifest: workspace::Manifest,
514    /// Immediate found paths.
515    found_paths: alloc::Vec<(PathBuf, bool)>,
516}
517
518impl Inputs {
519    /// Construct build paths from configuration.
520    fn build_paths<'m>(
521        &'m self,
522        cmd: CommandSharedRef<'_>,
523        c: &mut Config,
524    ) -> Result<alloc::Vec<BuildPath<'m>>> {
525        let mut build_paths = alloc::Vec::new();
526
527        if !self.found_paths.is_empty() {
528            build_paths.try_extend(self.found_paths.iter().map(|(p, e)| BuildPath::Path(p, *e)))?;
529
530            if !cmd.shared.workspace {
531                return Ok(build_paths);
532            }
533        }
534
535        if let Some(filter) = cmd.find_bins(c.all_targets) {
536            c.filtered |= !matches!(filter, WorkspaceFilter::All);
537
538            for p in self.manifest.find_by_kind(filter, FoundKind::Binary)? {
539                build_paths.try_push(BuildPath::Package(p))?;
540            }
541        }
542
543        if let Some(filter) = cmd.find_libs(c.all_targets) {
544            c.filtered |= !matches!(filter, WorkspaceFilter::All);
545
546            for p in self.manifest.find_by_kind(filter, FoundKind::Library)? {
547                build_paths.try_push(BuildPath::Package(p))?;
548            }
549        }
550
551        if let Some(filter) = cmd.find_tests(c.all_targets) {
552            c.filtered |= !matches!(filter, WorkspaceFilter::All);
553
554            for p in self.manifest.find_by_kind(filter, FoundKind::Test)? {
555                build_paths.try_push(BuildPath::Package(p))?;
556            }
557        }
558
559        if let Some(filter) = cmd.find_examples(c.all_targets) {
560            c.filtered |= !matches!(filter, WorkspaceFilter::All);
561
562            for p in self.manifest.find_by_kind(filter, FoundKind::Example)? {
563                build_paths.try_push(BuildPath::Package(p))?;
564            }
565        }
566
567        if let Some(filter) = cmd.find_benches(c.all_targets) {
568            c.filtered |= !matches!(filter, WorkspaceFilter::All);
569
570            for p in self.manifest.find_by_kind(filter, FoundKind::Bench)? {
571                build_paths.try_push(BuildPath::Package(p))?;
572            }
573        }
574
575        if let Some(package) = &cmd.shared.package {
576            build_paths.retain(|path| match path {
577                BuildPath::Path(_, _) => true,
578                BuildPath::Package(found_package) => found_package.package.name.as_str() == package,
579            })
580        }
581
582        Ok(build_paths)
583    }
584}
585
586impl SharedFlags {
587    /// Setup build context.
588    fn context(
589        &self,
590        entry: &mut Entry<'_>,
591        c: &Config,
592        capture: Option<&CaptureIo>,
593    ) -> Result<Context> {
594        let opts = ContextOptions {
595            capture,
596            test: c.test,
597        };
598
599        let mut context =
600            entry
601                .context
602                .as_mut()
603                .context("Context builder not configured with Entry::context")?(opts)?;
604
605        if let Some(capture) = capture {
606            context.install(crate::modules::capture_io::module(capture)?)?;
607        }
608
609        Ok(context)
610    }
611}
612
613#[derive(Default, Debug, Clone, Copy, ValueEnum)]
614enum ColorArgument {
615    #[default]
616    /// Automatically enable coloring if the output is a terminal.
617    Auto,
618    /// Force ANSI coloring.
619    Ansi,
620    /// Always color using the platform-specific coloring implementation.
621    Always,
622    /// Never color output.
623    Never,
624}
625
626#[derive(Parser, Debug)]
627#[command(name = "rune", about = None)]
628struct Args {
629    /// Print the version of the command.
630    #[arg(long)]
631    version: bool,
632
633    /// Control if output is colored or not.
634    #[arg(short = 'C', long, default_value = "auto")]
635    color: ColorArgument,
636
637    /// The command to execute
638    #[command(subcommand)]
639    cmd: Option<Command>,
640}
641
642#[derive(Parser, Debug, Clone)]
643#[command(rename_all = "kebab-case")]
644struct SharedFlags {
645    /// Recursively load all files if a specified build `<path>` is a directory.
646    #[arg(long, short = 'R')]
647    recursive: bool,
648
649    /// Display warnings.
650    #[arg(long)]
651    warnings: bool,
652
653    /// Display verbose output.
654    #[arg(long)]
655    verbose: bool,
656
657    /// Collect sources to operate over from the workspace.
658    ///
659    /// This is what happens by default, but is disabled in case any `<paths>`
660    /// are specified.
661    #[arg(long)]
662    workspace: bool,
663
664    /// Set the given compiler option (see `--list-options` for available options).
665    #[arg(short = 'O', num_args = 1)]
666    compiler_option: Vec<String>,
667
668    /// List available compiler options.
669    #[arg(long)]
670    list_options: bool,
671
672    /// Run with the following binary from a loaded manifest. This requires a
673    /// `Rune.toml` manifest.
674    #[arg(long)]
675    bin: Option<String>,
676
677    /// Run with the following library from a loaded manifest. This requires a
678    /// `Rune.toml` manifest.
679    #[arg(long)]
680    lib: Option<String>,
681
682    /// Run with the following test from a loaded manifest. This requires a
683    /// `Rune.toml` manifest.
684    #[arg(long)]
685    test: Option<String>,
686
687    /// Run with the following example from a loaded manifest. This requires a
688    /// `Rune.toml` manifest.
689    #[arg(long)]
690    example: Option<String>,
691
692    /// Run with the following benchmark by name from a loaded manifest. This
693    /// requires a `Rune.toml` manifest.
694    #[arg(long)]
695    bench: Option<String>,
696
697    /// Only include targets from the given package.
698    /// This requires a `Rune.toml` workspace manifest.
699    #[arg(long, short = 'p')]
700    package: Option<String>,
701
702    /// Include all targets, and not just the ones which are default for the
703    /// current command.
704    #[arg(long)]
705    all_targets: bool,
706
707    /// Build paths to include in the command.
708    ///
709    /// By default, the tool searches for:
710    ///
711    /// * A `Rune.toml` file in a parent directory, in which case this treated
712    ///   as a workspace.
713    ///
714    /// * In order: `main.rn`, `lib.rn`, `src/main.rn`, `src/lib.rn`,
715    ///   `script/main.rn`, and `script/lib.rn`.
716    #[arg(long)]
717    path: Vec<PathBuf>,
718}
719
720impl SharedFlags {
721    fn is_unfiltered(&self) -> bool {
722        self.bin.is_none()
723            && self.test.is_none()
724            && self.example.is_none()
725            && self.bench.is_none()
726            && self.path.is_empty()
727    }
728}
729
730const SPECIAL_FILES: &[&str] = &[
731    "main.rn",
732    "lib.rn",
733    "src/main.rn",
734    "src/lib.rn",
735    "script/main.rn",
736    "script/lib.rn",
737];
738
739// Our own private ExitCode since std::process::ExitCode is nightly only. Note
740// that these numbers are actually meaningful on Windows, but we don't care.
741#[repr(i32)]
742enum ExitCode {
743    Success = 0,
744    Failure = 1,
745    VmError = 2,
746}
747
748/// Format the given error.
749fn format_errors<O>(o: &mut O, error: &Error) -> io::Result<()>
750where
751    O: ?Sized + io::Write,
752{
753    writeln!(o, "Error: {error}")?;
754
755    for error in error.chain().skip(1) {
756        writeln!(o, "Caused by: {error}")?;
757    }
758
759    Ok(())
760}
761
762fn find_manifest() -> Option<(PathBuf, PathBuf)> {
763    let mut path = PathBuf::new();
764
765    loop {
766        let manifest_path = path.join(workspace::MANIFEST_FILE);
767
768        if manifest_path.is_file() {
769            return Some((path, manifest_path));
770        }
771
772        path.push("..");
773
774        if !path.is_dir() {
775            return None;
776        }
777    }
778}
779
780fn populate_config(
781    io: &mut Io<'_>,
782    c: &mut Config,
783    inputs: &mut Inputs,
784    cmd: CommandSharedRef<'_>,
785) -> Result<()> {
786    c.all_targets = cmd.shared.all_targets;
787
788    inputs
789        .found_paths
790        .try_extend(cmd.shared.path.iter().map(|p| (p.clone(), false)))?;
791
792    inputs
793        .found_paths
794        .try_extend(cmd.command.paths().iter().map(|p| (p.clone(), true)))?;
795
796    if !inputs.found_paths.is_empty() && !cmd.shared.workspace {
797        return Ok(());
798    }
799
800    let Some((manifest_root, manifest_path)) = find_manifest() else {
801        for file in SPECIAL_FILES {
802            let path = Path::new(file);
803
804            if path.is_file() {
805                inputs.found_paths.try_push((path.try_to_owned()?, false))?;
806                return Ok(());
807            }
808        }
809
810        let special = SPECIAL_FILES.join(", ");
811
812        bail!(
813            "Could not find `{}` in this or parent directories nor any of the special files: {special}",
814            workspace::MANIFEST_FILE
815        )
816    };
817
818    // When building or running a workspace we need to be more verbose so that
819    // users understand what exactly happens.
820    c.verbose = true;
821    c.manifest_root = Some(manifest_root);
822
823    let mut sources = crate::Sources::new();
824    sources.insert(crate::Source::from_path(manifest_path)?)?;
825
826    let mut diagnostics = workspace::Diagnostics::new();
827
828    let result = workspace::prepare(&mut sources)
829        .with_diagnostics(&mut diagnostics)
830        .build();
831
832    diagnostics.emit(io.stdout, &sources)?;
833    inputs.manifest = result?;
834    Ok(())
835}
836
837async fn main_with_out(io: &mut Io<'_>, entry: &mut Entry<'_>, mut args: Args) -> Result<ExitCode> {
838    let mut c = Config::default();
839    let mut inputs = Inputs::default();
840
841    if let Some((shared, base)) = args.cmd.as_mut().and_then(|c| c.as_command_base_mut()) {
842        base.propagate(&mut c, shared);
843    }
844
845    let Some(cmd) = &args.cmd else {
846        let commands: alloc::String = Command::ALL.into_iter().try_join(", ")?;
847        writeln!(io.stdout, "Expected a subcommand: {commands}")?;
848        return Ok(ExitCode::Failure);
849    };
850
851    let mut entries = alloc::Vec::new();
852
853    if let Some(cmd) = cmd.as_command_shared_ref() {
854        if cmd.shared.list_options {
855            writeln!(
856                io.stdout,
857                "Available compiler options (set with -O <option>=<value>):"
858            )?;
859            writeln!(io.stdout)?;
860
861            for (i, option) in Options::available().iter().enumerate() {
862                if i > 0 {
863                    writeln!(io.stdout)?;
864                }
865
866                io.write(
867                    format_args!("{}", option.key),
868                    Stream::Stdout,
869                    Color::Highlight,
870                )?;
871
872                write!(io.stdout, "={}", option.default)?;
873
874                if option.unstable {
875                    io.write(" (unstable)", Stream::Stdout, Color::Error)?;
876                }
877
878                writeln!(io.stdout, ":")?;
879                writeln!(io.stdout, "    Options: {}", option.options)?;
880                writeln!(io.stdout)?;
881
882                for &line in option.doc {
883                    let line = line.strip_prefix(' ').unwrap_or(line);
884                    writeln!(io.stdout, "    {line}")?;
885                }
886            }
887
888            return Ok(ExitCode::Success);
889        }
890
891        populate_config(io, &mut c, &mut inputs, cmd)?;
892
893        let build_paths = inputs.build_paths(cmd, &mut c)?;
894
895        let what = cmd.command.describe();
896        let verbose = c.verbose;
897        let recursive = cmd.shared.recursive;
898
899        for build_path in build_paths {
900            match build_path {
901                BuildPath::Path(path, explicit) => {
902                    for path in loader::recurse_paths(recursive, path.try_to_owned()?) {
903                        entries.try_push(EntryPoint::Path(path?, explicit))?;
904                    }
905                }
906                BuildPath::Package(p) => {
907                    if verbose {
908                        let mut section = io.section(what, Stream::Stderr, Color::Highlight)?;
909
910                        section.append(format_args!(
911                            " {} `{}` (from {})",
912                            p.found.kind,
913                            p.found.path.display(),
914                            p.package.name
915                        ))?;
916
917                        section.close()?;
918                    }
919
920                    entries.try_push(EntryPoint::Package(p))?;
921                }
922            }
923        }
924    }
925
926    match run_path(io, &c, cmd, entry, entries).await? {
927        ExitCode::Success => (),
928        other => {
929            return Ok(other);
930        }
931    }
932
933    Ok(ExitCode::Success)
934}
935
936/// Run a single path.
937async fn run_path<'p, I>(
938    io: &mut Io<'_>,
939    c: &Config,
940    cmd: &Command,
941    entry: &mut Entry<'_>,
942    entries: I,
943) -> Result<ExitCode>
944where
945    I: IntoIterator<Item = EntryPoint<'p>>,
946{
947    match cmd {
948        Command::Check(f) => {
949            let options = f.options()?;
950
951            for e in entries {
952                let mut options = options.clone();
953
954                if e.is_argument() {
955                    options.script = true;
956                }
957
958                match check::run(io, entry, c, &f.command, &f.shared, &options, e.path())? {
959                    ExitCode::Success => (),
960                    other => return Ok(other),
961                }
962            }
963        }
964        Command::Doc(f) => {
965            let options = f.options()?;
966            return doc::run(io, entry, c, &f.command, &f.shared, &options, entries);
967        }
968        Command::Ace(f) => {
969            let options = f.options()?;
970            return ace::run(io, entry, c, &f.command, &f.shared, &options, entries);
971        }
972        Command::Fmt(f) => {
973            let options = f.options()?;
974            return format::run(io, entry, c, entries, &f.command, &f.shared, &options);
975        }
976        Command::Test(f) => {
977            let options = f.options()?;
978
979            match tests::run(io, c, &f.command, &f.shared, &options, entry, entries).await? {
980                ExitCode::Success => (),
981                other => return Ok(other),
982            }
983        }
984        Command::Bench(f) => {
985            let options = f.options()?;
986
987            for e in entries {
988                let mut options = options.clone();
989
990                if e.is_argument() {
991                    options.script = true;
992                }
993
994                let capture_io = crate::modules::capture_io::CaptureIo::new();
995                let context = f.shared.context(entry, c, Some(&capture_io))?;
996
997                let load = loader::load(
998                    io,
999                    &context,
1000                    &f.shared,
1001                    &options,
1002                    e.path(),
1003                    visitor::Attribute::Bench,
1004                )?;
1005
1006                match benches::run(
1007                    io,
1008                    &f.command,
1009                    &context,
1010                    Some(&capture_io),
1011                    load.unit,
1012                    &load.sources,
1013                    &load.functions,
1014                )
1015                .await?
1016                {
1017                    ExitCode::Success => (),
1018                    other => return Ok(other),
1019                }
1020            }
1021        }
1022        Command::Run(f) => {
1023            let options = f.options()?;
1024            let context = f.shared.context(entry, c, None)?;
1025
1026            for e in entries {
1027                let mut options = options.clone();
1028
1029                if e.is_argument() {
1030                    options.script = true;
1031                }
1032
1033                let load = loader::load(
1034                    io,
1035                    &context,
1036                    &f.shared,
1037                    &options,
1038                    e.path(),
1039                    visitor::Attribute::None,
1040                )?;
1041
1042                let entry = if e.is_argument() {
1043                    Hash::EMPTY
1044                } else {
1045                    Hash::type_hash(["main"])
1046                };
1047
1048                match run::run(io, c, &f.command, &context, load.unit, &load.sources, entry).await?
1049                {
1050                    ExitCode::Success => (),
1051                    other => return Ok(other),
1052                }
1053            }
1054        }
1055        Command::LanguageServer(shared) => {
1056            let context = shared.context(entry, c, None)?;
1057            languageserver::run(context).await?;
1058        }
1059        Command::Hash(args) => {
1060            use rand::prelude::*;
1061
1062            if args.random {
1063                let mut rand = rand::rng();
1064
1065                for _ in 0..args.count.unwrap_or(1) {
1066                    writeln!(io.stdout, "{}", Hash::new(rand.random::<u64>()))?;
1067                }
1068            }
1069
1070            for item in &args.item {
1071                let item: ItemBuf = item.parse()?;
1072                let hash = Hash::type_hash(&item);
1073                writeln!(io.stdout, "{item} => {hash}")?;
1074            }
1075        }
1076    }
1077
1078    Ok(ExitCode::Success)
1079}