use super::{Cmd, COMMANDS}; use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_complete::{generate_to, shells}; use clap_complete_nushell::Nushell; use clap_mangen::Man; use std::{ error::Error, fs, io::{self, ErrorKind}, os::unix::fs::symlink, path::{Path, PathBuf}, }; #[derive(Debug)] pub struct Bootstrap { name: &'static str, path: Option, } impl Default for Bootstrap { fn default() -> Self { Self { name: "bootstrap", path: None, } } } impl Bootstrap { fn all() -> clap::Command { Command::new("all").about("Install everything").args([ Arg::new("soft") .help("Install soft links instead of hardlinks") .short('s') .long("soft") .action(ArgAction::SetTrue), Arg::new("all") .help("Install completions for all supported shells") .short('a') .long("all") .conflicts_with_all(["bash", "fish", "nu", "pwsh", "zsh"]) .action(ArgAction::SetTrue), Arg::new("bash") .help("Bash shell completions") .short('b') .long("bash") .action(ArgAction::SetTrue), Arg::new("fish") .help("Fish shell completions") .short('f') .long("fish") .action(ArgAction::SetTrue), Arg::new("nu") .help("Nushell completions") .short('n') .long("nu") .action(ArgAction::SetTrue), Arg::new("pwsh") .help("PowerShell completions") .short('p') .long("pwsh") .action(ArgAction::SetTrue), Arg::new("zsh") .help("Zshell completions") .short('z') .long("zsh") .action(ArgAction::SetTrue), ]) } fn links() -> clap::Command { Command::new("links") .about("Install links for each applet") .arg( Arg::new("soft") .help("Install soft links instead of hardlinks") .short('s') .long("soft") .action(ArgAction::SetTrue), ) } fn manpages() -> clap::Command { Command::new("manpages") .about("Install Unix man pages") .alias("man") } fn completions() -> clap::Command { Command::new("completions") .about("Install shell completions") .alias("comp") .args([ Arg::new("all") .help("Install completions for all supported shells") .short('a') .long("all") .exclusive(true) .action(ArgAction::SetTrue), Arg::new("bash") .help("Bash shell completions") .short('b') .long("bash") .action(ArgAction::SetTrue), Arg::new("fish") .help("Fish shell completions") .short('f') .long("fish") .action(ArgAction::SetTrue), Arg::new("nu") .help("Nushell completions") .short('n') .long("nu") .action(ArgAction::SetTrue), Arg::new("pwsh") .help("PowerShell completions") .short('p') .long("pwsh") .action(ArgAction::SetTrue), Arg::new("zsh") .help("Zshell completions") .short('z') .long("zsh") .action(ArgAction::SetTrue), ]) } } impl Cmd for Bootstrap { fn name(&self) -> &str { self.name } fn cli(&self) -> clap::Command { Command::new(self.name) .version(env!("CARGO_PKG_VERSION")) .author("Nathan Fisher") .about("Install shitbox into the filesystem") .long_about("Install symlinks, manpages and shell completions") .args([ Arg::new("prefix") .help("The directory path under which to install") .short('p') .long("prefix") .num_args(1) .default_value("/") .required(false), Arg::new("usr") .help("Use /usr") .long_help( "Split the installation so that some applets go into /bin | /sbin\n\ while others are placed into /usr/bin | /usr/sbin", ) .short('u') .long("usr") .action(ArgAction::SetTrue), Arg::new("soft") .help("Install soft links instead of hardlinks") .short('s') .long("soft") .action(ArgAction::SetTrue), ]) .subcommands([ Self::all(), Self::links(), Self::manpages(), Self::completions(), ]) } fn run(&self, matches: Option<&ArgMatches>) -> Result<(), Box> { let Some(matches) = matches else { return Err(io::Error::new(ErrorKind::Other, "No input").into()); }; if let Some(prefix) = matches.get_one::("prefix") { let usr = matches.get_flag("usr"); if let Some(progpath) = crate::progpath() { let mut outpath = PathBuf::from(prefix); outpath.push("bin"); println!("Installing binary:"); if !outpath.exists() { fs::create_dir_all(&outpath)?; println!(" mkdir: {}", outpath.display()); } outpath.push(env!("CARGO_PKG_NAME")); fs::copy(&progpath, &outpath)?; println!( " install: {} -> {}", progpath.display(), outpath.display() ); } match matches.subcommand() { Some(("links", matches)) => { links(prefix, usr, matches)?; } Some(("manpages", _matches)) => { manpages(prefix)?; } Some(("completions", matches)) => { completions(prefix, matches)?; } Some(("all", matches)) => { links(prefix, usr, matches)?; manpages(prefix)?; completions(prefix, matches)?; } _ => {} } } Ok(()) } fn path(&self) -> Option { self.path } } pub trait BootstrapCmd { fn completion(&self, outdir: &Path, gen: &str) -> Result<(), io::Error>; fn linkpath(&self, prefix: &str, usr: bool) -> Option; fn link(&self, prefix: &str, usr: bool, soft: bool) -> Result<(), Box>; fn manpage(&self, prefix: &str) -> Result<(), io::Error>; } impl BootstrapCmd for dyn Cmd { fn completion(&self, outdir: &Path, gen: &str) -> Result<(), io::Error> { let name = self.name(); let mut cmd = self.cli(); if !outdir.exists() { fs::create_dir_all(outdir)?; } let path = match gen { "bash" => generate_to(shells::Bash, &mut cmd, name, outdir)?, "fish" => generate_to(shells::Fish, &mut cmd, name, outdir)?, "nu" => generate_to(Nushell, &mut cmd, name, outdir)?, "pwsh" => generate_to(shells::PowerShell, &mut cmd, name, outdir)?, "zsh" => generate_to(shells::Zsh, &mut cmd, name, outdir)?, _ => unimplemented!(), }; println!(" {}", path.display()); Ok(()) } fn linkpath(&self, prefix: &str, usr: bool) -> Option { let mut path = PathBuf::from(prefix); let binpath = self.path(); match binpath { Some(crate::Path::Bin) => path.push("bin"), Some(crate::Path::Sbin) => path.push("sbin"), Some(crate::Path::UsrBin) => { if usr { path.push("usr"); } path.push("bin"); } Some(crate::Path::UsrSbin) => { if usr { path.push("usr"); } path.push("sbin"); } None => return None, } path.push(self.name()); Some(path) } fn link(&self, prefix: &str, usr: bool, soft: bool) -> Result<(), Box> { if let Some(linkpath) = self.linkpath(prefix, usr) { if soft { let binpath = match self.path().unwrap() { crate::Path::Bin => "shitbox", crate::Path::Sbin => "../bin/shitbox", crate::Path::UsrBin => { if usr { "../../bin/shitbox" } else { "shitbox" } } crate::Path::UsrSbin => { if usr { "../../bin/shitbox" } else { "../bin/shitbox" } } }; symlink(binpath, &linkpath)?; println!(" symlink: {binpath} -> {}", linkpath.display()); } else { let mut binpath = PathBuf::from(prefix); binpath.push("bin"); binpath.push(env!("CARGO_PKG_NAME")); fs::hard_link(&binpath, &linkpath)?; println!(" link: {} -> {}", binpath.display(), linkpath.display()); } } Ok(()) } fn manpage(&self, prefix: &str) -> Result<(), io::Error> { let command = self.cli(); let fname = match self.name() { "bootstrap" => "shitbox-bootstrap.1".to_string(), s => format!("{s}.1"), }; let outdir: PathBuf = [prefix, "usr", "share", "man", "man1"].iter().collect(); if !outdir.exists() { fs::create_dir_all(&outdir)?; } let mut outfile = outdir; outfile.push(fname); let man = Man::new(command); let mut buffer: Vec = vec![]; man.render(&mut buffer)?; fs::write(&outfile, buffer)?; println!(" {}", outfile.display()); Ok(()) } } fn manpages(prefix: &str) -> Result<(), io::Error> { println!("Generating Unix man pages:"); COMMANDS .iter() .try_for_each(|cmd| crate::cmd::get(cmd).unwrap().manpage(prefix))?; Ok(()) } fn completions(prefix: &str, matches: &ArgMatches) -> Result<(), io::Error> { println!("Generating completions:"); if matches.get_flag("bash") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "bash-completion", "completion"] .iter() .collect(); COMMANDS.iter().try_for_each(|cmd| { let cmd = crate::cmd::get(cmd).unwrap(); cmd.completion(&outdir, "bash") })?; } if matches.get_flag("fish") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "fish", "completions"].iter().collect(); COMMANDS.iter().try_for_each(|cmd| { let cmd = crate::cmd::get(cmd).unwrap(); cmd.completion(&outdir, "fish") })?; } if matches.get_flag("nu") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "nu", "completions"].iter().collect(); COMMANDS.iter().try_for_each(|cmd| { let cmd = crate::cmd::get(cmd).unwrap(); cmd.completion(&outdir, "nu") })?; } if matches.get_flag("pwsh") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "pwsh", "completions"].iter().collect(); COMMANDS.iter().try_for_each(|cmd| { let cmd = crate::cmd::get(cmd).unwrap(); cmd.completion(&outdir, "pwsh") })?; } if matches.get_flag("zsh") || matches.get_flag("all") { let outdir: PathBuf = [prefix, "share", "zsh", "site-functions"].iter().collect(); COMMANDS.iter().try_for_each(|cmd| { let cmd = crate::cmd::get(cmd).unwrap(); cmd.completion(&outdir, "zsh") })?; } Ok(()) } fn links(prefix: &str, usr: bool, cmd: &ArgMatches) -> Result<(), Box> { println!("Generating links:"); let mut binpath = PathBuf::from(prefix); binpath.push("bin"); if !binpath.exists() { fs::create_dir_all(&binpath)?; println!(" mkdir: {}", binpath.display()); } binpath.pop(); binpath.push("sbin"); if !binpath.exists() { fs::create_dir_all(&binpath)?; println!(" mkdir: {}", binpath.display()); } if usr { binpath.pop(); binpath.push("usr"); binpath.push("bin"); if !binpath.exists() { fs::create_dir_all(&binpath)?; println!(" mkdir: {}", binpath.display()); } binpath.pop(); binpath.push("sbin"); if !binpath.exists() { fs::create_dir_all(&binpath)?; println!(" mkdir: {}", binpath.display()); } } let soft = cmd.get_flag("soft"); COMMANDS.iter().try_for_each(|c| { let cmd = crate::cmd::get(c).unwrap(); cmd.link(prefix, usr, soft) })?; Ok(()) }