diff --git a/Cargo.lock b/Cargo.lock index bc50438..fad8ce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,7 +424,7 @@ dependencies = [ [[package]] name = "pupdate" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "directories", diff --git a/Cargo.toml b/Cargo.toml index 2683e10..3df694b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ license = "WTFPL" name = "pupdate" readme = "README.md" repository = "https://github.com/zyllian/pupdate" -version = "0.1.0" +version = "0.2.0" [dependencies] clap = {version = "4", features = ["derive"]} diff --git a/README.md b/README.md index 19e9120..dd7c7c0 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ simple helper utility to update remote systems alongside the local system easily ## usage +you can install easily using `cargo install pupdate`. + run `pupdate -h` for help with arguments. with no arguments, pupdate will update the local system and any remotes configured in the config file (default ~/.pupdate). ## config diff --git a/src/main.rs b/src/main.rs index a36c9bf..1948ce4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,41 @@ struct Config { /// the directory to log to, no logs if missing #[serde(default)] log_dir: Option, + /// the package managers to pupdate the local system with + #[serde(default)] + package_managers: Vec, +} + +/// enum for supported package managers +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +enum PackageManager { + AptGet, + Homebrew, + Pacman, + Yay, +} + +impl PackageManager { + /// gets the packaage manager's name + pub fn get_name(&self) -> &'static str { + match self { + Self::AptGet => "apt-get", + Self::Homebrew => "homebrew", + Self::Pacman => "pacman", + Self::Yay => "yay", + } + } + + /// pupdates the local system + pub async fn pupdate(&self, log_dir: Option) -> eyre::Result { + match self { + Self::AptGet => pupdate_apt(log_dir).await, + Self::Homebrew => pupdate_homebrew(log_dir).await, + Self::Pacman => pupdate_pacman(log_dir).await, + Self::Yay => pupdate_yay(log_dir).await, + } + } } /// pupdates a remote target through ssh @@ -77,40 +112,95 @@ async fn pupdate_remote( Ok((remote, success)) } -/// pupdates the local system using apt-get -async fn pupdate_apt(log_dir: Option) -> eyre::Result { - async fn log(outputs: &[std::process::Output], log_dir: Option) -> eyre::Result { - if let Some(log_dir) = log_dir { - let mut stdout = File::create(log_dir.join("local.stdout.log")).await?; - let mut stderr = File::create(log_dir.join("local.stderr.log")).await?; - for output in outputs { - stdout.write_all(&output.stdout).await?; - stderr.write_all(&output.stderr).await?; - } - } +/// helper for writing log files from multiple processes +async fn log_helper( + outputs: &[std::process::Output], + log_dir: Option, +) -> eyre::Result { + if let Some(log_dir) = log_dir { + let mut stdout = File::create(log_dir.join("local.stdout.log")).await?; + let mut stderr = File::create(log_dir.join("local.stderr.log")).await?; for output in outputs { - if !output.status.success() { - return Ok(false); - } + stdout.write_all(&output.stdout).await?; + stderr.write_all(&output.stderr).await?; } - Ok(true) } + for output in outputs { + if !output.status.success() { + return Ok(false); + } + } + Ok(true) +} - let update_output = Command::new("sudo") - .arg("apt-get") +/// helper to prefix a command with sudo if requested +fn command_maybe_with_sudo(command: &str, with_sudo: bool) -> Command { + if with_sudo { + let mut cmd = Command::new("sudo"); + cmd.arg(command); + cmd + } else { + Command::new(command) + } +} + +/// helper for pupdates which both follow the "X update && X upgrade" set of commands (e.g. apt-get and brew) +async fn update_and_upgrade( + log_dir: Option, + command: &str, + with_sudo: bool, + yes: bool, +) -> eyre::Result { + let update_output = command_maybe_with_sudo(command, with_sudo) .arg("update") .output() .await?; if !update_output.status.success() { - return log(&[update_output], log_dir).await; + return log_helper(&[update_output], log_dir).await; } - let upgrade_output = Command::new("sudo") - .arg("apt-get") - .arg("upgrade") - .arg("-y") - .output() - .await?; - log(&[update_output, upgrade_output], log_dir).await + let mut upgrade_command = command_maybe_with_sudo(command, with_sudo); + upgrade_command.arg("upgrade"); + if yes { + upgrade_command.arg("-y"); + } + let upgrade_output = upgrade_command.output().await?; + log_helper(&[update_output, upgrade_output], log_dir).await +} + +/// pupdates the local system using apt-get +async fn pupdate_apt(log_dir: Option) -> eyre::Result { + update_and_upgrade(log_dir, "apt-get", true, true).await +} + +/// pupdates the local system using brew, untested +async fn pupdate_homebrew(log_dir: Option) -> eyre::Result { + update_and_upgrade(log_dir, "brew", false, false).await +} + +/// helper for pupdates which follow the pacman-style command format +async fn syu( + log_dir: Option, + command: &str, + with_sudo: bool, + yes: bool, +) -> eyre::Result { + let mut command = command_maybe_with_sudo(command, with_sudo); + command.arg("-Syu"); + if yes { + command.arg("--noconfirm"); + } + let output = command.output().await?; + log_helper(&[output], log_dir).await +} + +/// pupdates the local system using pacman +async fn pupdate_pacman(log_dir: Option) -> eyre::Result { + syu(log_dir, "pacman", true, true).await +} + +/// pupdates the local system using yay +async fn pupdate_yay(log_dir: Option) -> eyre::Result { + syu(log_dir, "yay", false, true).await } #[tokio::main] @@ -123,7 +213,11 @@ async fn main() -> eyre::Result<()> { }; let config_path = args.config.or(base_config_path); let config: Config = if let Some(config) = config_path { - serde_json::from_str(&std::fs::read_to_string(config)?)? + if config.exists() { + serde_json::from_str(&std::fs::read_to_string(config)?)? + } else { + Config::default() + } } else { Config::default() }; @@ -199,8 +293,15 @@ async fn main() -> eyre::Result<()> { if !args.skip_local { println!("running local pupdates, you may be pawmpted for your password"); - let start = OffsetDateTime::now_utc(); - if pupdate_apt(log_dir).await? { + + if config.package_managers.is_empty() { + eprintln!("no package managers defined in config, skipping local pupdates"); + } + + for package_manager in config.package_managers { + println!("pupdating with {}", package_manager.get_name()); + let start = OffsetDateTime::now_utc(); + package_manager.pupdate(log_dir.clone()).await?; let end = OffsetDateTime::now_utc(); let duration = end - start; @@ -208,8 +309,6 @@ async fn main() -> eyre::Result<()> { "successfully pupdated the local system in {} seconds", duration.whole_seconds() ); - } else { - println!("failed to pupdate the local system"); } } diff --git a/todo.md b/todo.md index 869cc85..7564e83 100644 --- a/todo.md +++ b/todo.md @@ -4,4 +4,4 @@ some things that may or may not get done at some point: - pupdate daemon to remotely pupdate without using ssh (use ssh keys for authentication though maybe?) - pupdate docker compose containers (interactively?) -- support other pupdate methods than just apt +- support other pupdate methods than just apt and homebrew