diff --git a/Cargo.lock b/Cargo.lock index 42128b6f5..5af92f4b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,6 +303,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clap_mangen" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +dependencies = [ + "clap", + "roff", +] + [[package]] name = "cmdline_words_parser" version = "0.2.1" @@ -1193,6 +1203,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "roff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" + [[package]] name = "rustix" version = "0.38.37" @@ -1610,6 +1626,7 @@ dependencies = [ "ci_info", "clap", "clap_complete", + "clap_mangen", "dirs", "envoy", "hamcrest2", diff --git a/Cargo.toml b/Cargo.toml index 0f54ce3cf..0a12e1153 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "volta" version = "2.0.1" -authors = ["David Herman <david.herman@gmail.com>", "Charles Pierce <cpierce.grad@gmail.com>"] +authors = [ + "David Herman <david.herman@gmail.com>", + "Charles Pierce <cpierce.grad@gmail.com>", +] license = "BSD-2-Clause" repository = "https://github.com/volta-cli/volta" edition = "2021" @@ -38,6 +41,7 @@ textwrap = "0.16.1" which = "6.0.3" dirs = "5.0.1" volta-migrate = { path = "crates/volta-migrate" } +clap_mangen = "0.2.23" [target.'cfg(windows)'.dependencies] winreg = "0.52.0" diff --git a/crates/volta-core/src/error/kind.rs b/crates/volta-core/src/error/kind.rs index 6add88efa..b17840634 100644 --- a/crates/volta-core/src/error/kind.rs +++ b/crates/volta-core/src/error/kind.rs @@ -530,6 +530,11 @@ pub enum ErrorKind { YarnVersionNotFound { matching: String, }, + + /// Thrown when there is an error writing the man pages to a file + ManPagesOutFileError { + path: PathBuf, + }, } impl fmt::Display for ErrorKind { @@ -1452,6 +1457,14 @@ Please verify your internet connection.", Please verify that the version is correct."#, matching ), + ErrorKind::ManPagesOutFileError { path } => write!( + f, + "Could not write man pages to {} + +{}", + path.display(), + PERMISSIONS_CTA + ), } } } @@ -1577,6 +1590,7 @@ impl ErrorKind { ErrorKind::Yarn2NotSupported => ExitCode::NoVersionMatch, ErrorKind::YarnLatestFetchError { .. } => ExitCode::NetworkError, ErrorKind::YarnVersionNotFound { .. } => ExitCode::NoVersionMatch, + ErrorKind::ManPagesOutFileError { .. } => ExitCode::FileSystemError, } } } diff --git a/crates/volta-core/src/session.rs b/crates/volta-core/src/session.rs index 001dddfaf..9536b1290 100644 --- a/crates/volta-core/src/session.rs +++ b/crates/volta-core/src/session.rs @@ -38,6 +38,7 @@ pub enum ActivityKind { Setup, Run, Args, + ManPages, } impl Display for ActivityKind { @@ -66,6 +67,7 @@ impl Display for ActivityKind { ActivityKind::Which => "which", ActivityKind::Run => "run", ActivityKind::Args => "args", + ActivityKind::ManPages => "man-pages", }; f.write_str(s) } diff --git a/dev/unix/volta-install.sh b/dev/unix/volta-install.sh index fddaaa616..718c266a6 100755 --- a/dev/unix/volta-install.sh +++ b/dev/unix/volta-install.sh @@ -191,6 +191,22 @@ create_tree() { fi } +generate_man_page() { + local install_dir="$1" + local man_dir="$install_dir/share/man/man1" + + info 'Generating' "man page" + + mkdir -p "$man_dir" + + if ! "$install_dir/bin/volta" man > "$man_dir/volta.1" 2>/dev/null; then + warning "Failed to generate man page. Man pages may not be available." + return 1 + fi + + info 'Generated' "man page at $man_dir/volta.1" +} + install_version() { local version_to_install="$1" local install_dir="$2" @@ -223,6 +239,7 @@ install_version() { if [ "$?" == 0 ] then + generate_man_page "$install_dir" if [ "$should_run_setup" == "true" ]; then info 'Finished' "installation. Updating user profile settings." "$install_dir"/bin/volta setup diff --git a/src/cli.rs b/src/cli.rs index 56453cde2..b844f19ad 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -101,6 +101,9 @@ pub(crate) enum Subcommand { /// Run a command with custom Node, npm, pnpm, and/or Yarn versions Run(command::Run), + + /// Man pages + Man(command::ManPages), } impl Subcommand { @@ -116,6 +119,7 @@ impl Subcommand { Subcommand::Use(r#use) => r#use.run(session), Subcommand::Setup(setup) => setup.run(session), Subcommand::Run(run) => run.run(session), + Subcommand::Man(man) => man.run(session), } } } diff --git a/src/command/man_pages.rs b/src/command/man_pages.rs new file mode 100644 index 000000000..8cae157fb --- /dev/null +++ b/src/command/man_pages.rs @@ -0,0 +1,84 @@ +use std::path::PathBuf; + +use clap::CommandFactory; +use clap_mangen::Man; +use log::info; + +use volta_core::{ + error::{Context, ErrorKind, ExitCode, Fallible}, + session::{ActivityKind, Session}, + style::{note_prefix, success_prefix}, +}; + +use crate::command::Command; + +#[derive(Debug, clap::Args)] +pub(crate) struct ManPages { + /// File to write generated man pages to + #[arg(short, long = "output")] + out_file: Option<PathBuf>, + + /// Write over an existing file, if any. + #[arg(short, long)] + force: bool, +} + +impl Command for ManPages { + fn run(self, session: &mut Session) -> Fallible<ExitCode> { + session.add_event_start(ActivityKind::ManPages); + + let app = crate::cli::Volta::command(); + let man = Man::new(app); + + match self.out_file { + Some(path) => { + if path.is_file() && !self.force { + return Err(ErrorKind::ManPagesOutFileError { path }.into()); + } + + // Create parent directory if it doesn't exist + if let Some(parent) = path.parent() { + if !parent.is_dir() { + info!( + "{} {} does not exist, creating it", + note_prefix(), + parent.display() + ); + std::fs::create_dir_all(parent).with_context(|| { + ErrorKind::CreateDirError { + dir: parent.to_path_buf(), + } + })?; + } + } + + let mut file = std::fs::File::create(&path).with_context(|| { + ErrorKind::ManPagesOutFileError { + path: path.to_path_buf(), + } + })?; + + man.render(&mut file) + .map_err(|_e| ErrorKind::ManPagesOutFileError { + path: path.to_path_buf(), + })?; + + info!( + "{} generated man pages to {}", + success_prefix(), + path.display() + ); + } + None => { + man.render(&mut std::io::stdout()).map_err(|_e| { + ErrorKind::ManPagesOutFileError { + path: PathBuf::from("stdout"), + } + })?; + } + }; + + session.add_event_end(ActivityKind::ManPages, ExitCode::Success); + Ok(ExitCode::Success) + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs index f219fb307..9ff7c1912 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod completions; pub(crate) mod fetch; pub(crate) mod install; pub(crate) mod list; +pub(crate) mod man_pages; pub(crate) mod pin; pub(crate) mod run; pub(crate) mod setup; @@ -14,6 +15,7 @@ pub(crate) use completions::Completions; pub(crate) use fetch::Fetch; pub(crate) use install::Install; pub(crate) use list::List; +pub(crate) use man_pages::ManPages; pub(crate) use pin::Pin; pub(crate) use r#use::Use; pub(crate) use run::Run;