diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bdabad76..2dc5c35c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -18,8 +18,10 @@ assignees: '' option to skip this step? ## I want to suggest some general feature + Topgrade should... ## More information + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f793acea..f5992ad7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,5 @@ ## What does this PR do - ## Standards checklist - [ ] The PR title is descriptive diff --git a/.github/workflows/check_config_creation_if_not_exists.yml b/.github/workflows/check_config_creation_if_not_exists.yml index 6e33b8ef..0a896b7d 100644 --- a/.github/workflows/check_config_creation_if_not_exists.yml +++ b/.github/workflows/check_config_creation_if_not_exists.yml @@ -15,8 +15,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5.0.0 - - run: | + - uses: actions/checkout@v5.0.0 + - run: | CONFIG_PATH=~/.config/topgrade.toml; if [ -f "$CONFIG_PATH" ]; then rm $CONFIG_PATH; fi cargo build; diff --git a/BREAKINGCHANGES_dev.md b/BREAKINGCHANGES_dev.md index 2da49f26..63c2b66b 100644 --- a/BREAKINGCHANGES_dev.md +++ b/BREAKINGCHANGES_dev.md @@ -1,3 +1,3 @@ 1. The `jet_brains_toolbox` step was renamed to `jetbrains_toolbox`. If you're using the old name in your configuration file in the `disable` or `only` - fields, simply change it to `jetbrains_toolbox`. + fields, simply change it to `jetbrains_toolbox`. \ No newline at end of file diff --git a/locales/app.yml b/locales/app.yml index 6c50b6f7..5f6b4618 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -16,6 +16,22 @@ _version: 2 zh_CN: "正在模拟 %{program_name} %{arguments} 的运行过程" zh_TW: "正在模擬 %{program_name} %{arguments} 的執行過程" de: "Testlauf: %{program_name} %{arguments}" +"Executing: {program_name} {arguments}": + en: "Executing: %{program_name} %{arguments}" + lt: "Vykdymas: %{program_name} %{arguments}" + es: "Ejecutando: %{program_name} %{arguments}" + fr: "Exécution: %{program_name} %{arguments}" + zh_CN: "执行: %{program_name} %{arguments}" + zh_TW: "执行: %{program_name} %{arguments}" + de: "Ausführung: %{program_name} %{arguments}" +"with env: {env}": + en: "with env: %{env}" + lt: "su env: %{env}" + es: "con env: %{env}" + fr: "avec env: %{env}" + zh_CN: "与env: %{env}" + zh_TW: "与env: %{env}" + de: "mit env: %{env}" "in {directory}": en: "in %{directory}" lt: "kataloge %{directory}" @@ -1298,30 +1314,14 @@ _version: 2 zh_CN: "Windows 更新" zh_TW: "Windows 更新" de: "Windows-Update" -"Would check if OpenBSD is -current": - en: "Would check if OpenBSD is -current" - lt: "Patikrintų, ar OpenBSD yra -current" - es: "Comprobaría si OpenBSD está en -current" - fr: "Vérifierait si OpenBSD est à -curent" - zh_CN: "将检查 OpenBSD 是否为 -current" - zh_TW: "會檢查 OpenBSD 是否為 -current" - de: "Würde überprüfen, ob OpenBSD -current ist" -"Would upgrade the OpenBSD system": - en: "Would upgrade the OpenBSD system" - lt: "Atnaujintų OpenBSD sistemą" - es: "Actualizaría el sistema OpenBSD" - fr: "Mettrait à jour le système OpenBSD" - zh_CN: "将升级 OpenBSD 系统" - zh_TW: "會升級 OpenBSD 系統" - de: "Würde das OpenBSD-System aktualisieren" -"Would upgrade OpenBSD packages": - en: "Would upgrade OpenBSD packages" - lt: "Atnaujintų OpenBSD paketus" - es: "Actualizaría los paquetes de OpenBSD" - fr: "Mettrait à jour les paquets OpenBSD" - zh_CN: "将升级 OpenBSD 软件包" - zh_TW: "會升級 OpenBSD 套件" - de: "Würde OpenBSD-Pakete aktualisieren" +"Checking if /etc/motd contains -current or -beta": + en: "Checking if /etc/motd contains -current or -beta" + lt: "Tikrinimas, jei /etc/motd yra -current arba -beta" + es: "Comprobación de si /etc/motd contiene -current o -beta" + fr: "Vérification si /etc/motd contient -current ou -beta" + zh_CN: "检查 /etc/motd 是否包含 -current 或 -beta" + zh_TW: "检查 /etc/motd 是否包含 -current 或 -beta" + de: "Überprüfen, ob /etc/motd -current oder -beta enthält" "Microsoft Store": en: "Microsoft Store" lt: "Microsoft parduotuvė" diff --git a/src/config.rs b/src/config.rs index a47f9e71..91826749 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,14 +18,15 @@ use regex_split::RegexSplit; use rust_i18n::t; use serde::Deserialize; use strum::IntoEnumIterator; +use tracing::{debug, error}; use which_crate::which; use super::utils::editor; use crate::command::CommandExt; +use crate::execution_context::RunType; use crate::step::Step; use crate::sudo::SudoKind; use crate::utils::string_prepend_str; -use tracing::{debug, error}; // TODO: Add i18n to this. Tracking issue: https://github.com/topgrade-rs/topgrade/issues/859 pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml"); @@ -708,9 +709,15 @@ pub struct CommandLineArgs { cleanup: bool, /// Print what would be done + /// + /// Alias for --run-type dry #[arg(short = 'n', long = "dry-run")] dry_run: bool, + /// Pick between just running commands, running and logging commands, and just logging commands + #[arg(short = 'r', long = "run-type", value_enum, default_value_t)] + run_type: RunType, + /// Do not ask to retry failed steps #[arg(long = "no-retry")] no_retry: bool, @@ -1001,9 +1008,13 @@ impl Config { .unwrap_or(false) } - /// Tell whether we are dry-running. - pub fn dry_run(&self) -> bool { - self.opt.dry_run + /// Get the [RunType] for the current execution + pub fn run_type(&self) -> RunType { + if self.opt.dry_run { + RunType::Dry + } else { + self.opt.run_type + } } /// Tell whether we should not attempt to retry anything. diff --git a/src/execution_context.rs b/src/execution_context.rs index 808a9976..683eb817 100644 --- a/src/execution_context.rs +++ b/src/execution_context.rs @@ -1,11 +1,15 @@ #![allow(dead_code)] -use color_eyre::eyre::Result; -use rust_i18n::t; use std::env::var; use std::ffi::OsStr; use std::process::Command; use std::sync::{LazyLock, Mutex}; +use clap::ValueEnum; +use color_eyre::eyre::Result; +use rust_i18n::t; +use serde::Deserialize; +use strum::EnumString; + use crate::config::Config; use crate::error::MissingSudo; use crate::executor::{DryCommand, Executor}; @@ -16,30 +20,26 @@ use crate::sudo::Sudo; use crate::utils::require_option; /// An enum telling whether Topgrade should perform dry runs or actually perform the steps. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Deserialize, Default, EnumString, ValueEnum)] pub enum RunType { /// Executing commands will just print the command with its argument. Dry, /// Executing commands will perform actual execution. + #[default] Wet, + + /// Executing commands will print the command and perform actual execution. + Damp, } impl RunType { - /// Create a new instance from a boolean telling whether to dry run. - pub fn new(dry_run: bool) -> Self { - if dry_run { - RunType::Dry - } else { - RunType::Wet - } - } - /// Tells whether we're performing a dry run. pub fn dry(self) -> bool { match self { RunType::Dry => true, RunType::Wet => false, + RunType::Damp => false, } } } @@ -84,6 +84,7 @@ impl<'a> ExecutionContext<'a> { match self.run_type { RunType::Dry => Executor::Dry(DryCommand::new(program)), RunType::Wet => Executor::Wet(Command::new(program)), + RunType::Damp => Executor::Damp(Command::new(program)), } } diff --git a/src/executor.rs b/src/executor.rs index ffa7bee1..267b68e1 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,11 +1,13 @@ //! Utilities for command execution use std::ffi::{OsStr, OsString}; +use std::fmt::Debug; +use std::iter; use std::path::Path; use std::process::{Child, Command, ExitStatus, Output}; use color_eyre::eyre::Result; use rust_i18n::t; -use tracing::debug; +use tracing::{debug, enabled, Level}; use crate::command::CommandExt; use crate::error::DryRun; @@ -15,6 +17,7 @@ use crate::error::DryRun; /// If the enum is set to `Dry`, execution will just print the command with its arguments. pub enum Executor { Wet(Command), + Damp(Command), Dry(DryCommand), } @@ -24,7 +27,7 @@ impl Executor { /// Will give weird results for non-UTF-8 programs; see `to_string_lossy()`. pub fn get_program(&self) -> String { match self { - Executor::Wet(c) => c.get_program().to_string_lossy().into_owned(), + Executor::Wet(c) | Executor::Damp(c) => c.get_program().to_string_lossy().into_owned(), Executor::Dry(c) => c.program.to_string_lossy().into_owned(), } } @@ -32,7 +35,7 @@ impl Executor { /// See `std::process::Command::arg` pub fn arg>(&mut self, arg: S) -> &mut Executor { match self { - Executor::Wet(c) => { + Executor::Wet(c) | Executor::Damp(c) => { c.arg(arg); } Executor::Dry(c) => { @@ -50,7 +53,7 @@ impl Executor { S: AsRef, { match self { - Executor::Wet(c) => { + Executor::Wet(c) | Executor::Damp(c) => { c.args(args); } Executor::Dry(c) => { @@ -65,7 +68,7 @@ impl Executor { /// See `std::process::Command::current_dir` pub fn current_dir>(&mut self, dir: P) -> &mut Executor { match self { - Executor::Wet(c) => { + Executor::Wet(c) | Executor::Damp(c) => { c.current_dir(dir); } Executor::Dry(c) => c.directory = Some(dir.as_ref().into()), @@ -81,7 +84,7 @@ impl Executor { K: AsRef, { match self { - Executor::Wet(c) => { + Executor::Wet(c) | Executor::Damp(c) => { c.env_remove(key); } Executor::Dry(_) => (), @@ -98,7 +101,7 @@ impl Executor { V: AsRef, { match self { - Executor::Wet(c) => { + Executor::Wet(c) | Executor::Damp(c) => { c.env(key, val); } Executor::Dry(_) => (), @@ -109,18 +112,16 @@ impl Executor { /// See `std::process::Command::spawn` pub fn spawn(&mut self) -> Result { + self.log_command(); let result = match self { - Executor::Wet(c) => { + Executor::Wet(c) | Executor::Damp(c) => { debug!("Running {:?}", c); // We should use `spawn()` here rather than `spawn_checked()` since // their semantics and behaviors are different. #[allow(clippy::disallowed_methods)] c.spawn().map(ExecutorChild::Wet)? } - Executor::Dry(c) => { - c.dry_run(); - ExecutorChild::Dry - } + Executor::Dry(_) => ExecutorChild::Dry, }; Ok(result) @@ -128,17 +129,15 @@ impl Executor { /// See `std::process::Command::output` pub fn output(&mut self) -> Result { + self.log_command(); match self { - Executor::Wet(c) => { + Executor::Wet(c) | Executor::Damp(c) => { // We should use `output()` here rather than `output_checked()` since // their semantics and behaviors are different. #[allow(clippy::disallowed_methods)] Ok(ExecutorOutput::Wet(c.output()?)) } - Executor::Dry(c) => { - c.dry_run(); - Ok(ExecutorOutput::Dry) - } + Executor::Dry(_) => Ok(ExecutorOutput::Dry), } } @@ -146,18 +145,38 @@ impl Executor { /// that can indicate success of a script #[allow(dead_code)] pub fn status_checked_with_codes(&mut self, codes: &[i32]) -> Result<()> { + self.log_command(); match self { - Executor::Wet(c) => c.status_checked_with(|status| { + Executor::Wet(c) | Executor::Damp(c) => c.status_checked_with(|status| { if status.success() || status.code().as_ref().is_some_and(|c| codes.contains(c)) { Ok(()) } else { Err(()) } }), - Executor::Dry(c) => { - c.dry_run(); - Ok(()) + Executor::Dry(_) => Ok(()), + } + } + + fn log_command(&self) { + match self { + Executor::Wet(_) => (), + Executor::Damp(c) => { + log_command( + "Executing: {program_name} {arguments}", + c.get_program(), + c.get_args(), + c.get_envs(), + c.get_current_dir(), + ); } + Executor::Dry(c) => log_command( + "Dry running: {program_name} {arguments}", + &c.program, + &c.args, + iter::empty(), + c.directory.as_ref(), + ), } } } @@ -182,30 +201,11 @@ impl DryCommand { directory: None, } } - - fn dry_run(&self) { - print!( - "{}", - t!( - "Dry running: {program_name} {arguments}", - program_name = self.program.to_string_lossy(), - arguments = shell_words::join( - self.args - .iter() - .map(|a| String::from(a.to_string_lossy())) - .collect::>() - ) - ) - ); - match &self.directory { - Some(dir) => println!(" {}", t!("in {directory}", directory = dir.to_string_lossy())), - None => println!(), - }; - } } /// The Result of spawn. Contains an actual `std::process::Child` if executed by a wet command. pub enum ExecutorChild { + // Both RunType::Wet and RunType::Damp use this variant #[allow(unused)] // this type has not been used Wet(Child), Dry, @@ -218,22 +218,18 @@ impl CommandExt for Executor { // variant for wet/dry runs. fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> Result { + self.log_command(); match self { - Executor::Wet(c) => c.output_checked_with(succeeded), - Executor::Dry(c) => { - c.dry_run(); - Err(DryRun().into()) - } + Executor::Wet(c) | Executor::Damp(c) => c.output_checked_with(succeeded), + Executor::Dry(_) => Err(DryRun().into()), } } fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> Result<()> { + self.log_command(); match self { - Executor::Wet(c) => c.status_checked_with(succeeded), - Executor::Dry(c) => { - c.dry_run(); - Ok(()) - } + Executor::Wet(c) | Executor::Damp(c) => c.status_checked_with(succeeded), + Executor::Dry(_) => Ok(()), } } @@ -241,3 +237,42 @@ impl CommandExt for Executor { self.spawn() } } + +fn log_command< + 'a, + I: ExactSizeIterator)>, +>( + prefix: &str, + exec: &OsStr, + args: impl IntoIterator + ?Sized + 'a)>, + env: impl IntoIterator), IntoIter = I>, + dir: Option<&'a (impl AsRef + ?Sized)>, +) { + println!( + "{}", + t!( + prefix, + program_name = exec.to_string_lossy(), + arguments = shell_words::join(args.into_iter().map(|s| s.as_ref().to_string_lossy())) + ) + ); + + let env_iter = env.into_iter(); + if env_iter.len() != 0 && enabled!(Level::DEBUG) { + println!( + " {}", + t!( + "with env: {env}", + env = env_iter + .filter(|(_, val)| val.is_some()) + .map(|(key, val)| format!("{:?}={:?}", key, val.unwrap())) + .collect::>() + .join(" ") + ) + ) + } + + if let Some(d) = dir { + println!(" {}", t!("in {directory}", directory = d.as_ref().display())); + } +} diff --git a/src/main.rs b/src/main.rs index 210cdfa6..81d4ceed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -124,7 +124,7 @@ fn run() -> Result<()> { debug!("Version: {}", crate_version!()); debug!("OS: {}", env!("TARGET")); debug!("{:?}", env::args()); - debug!("Binary path: {:?}", std::env::current_exe()); + debug!("Binary path: {:?}", env::current_exe()); debug!("self-update Feature Enabled: {:?}", cfg!(feature = "self-update")); debug!("Configuration: {:?}", config); @@ -163,7 +163,7 @@ fn run() -> Result<()> { #[cfg(target_os = "linux")] let distribution = linux::Distribution::detect(); - let run_type = execution_context::RunType::new(config.dry_run()); + let run_type = config.run_type(); let ctx = execution_context::ExecutionContext::new( run_type, sudo, diff --git a/src/steps/os/macos.rs b/src/steps/os/macos.rs index 6afce4a7..eabe19a5 100644 --- a/src/steps/os/macos.rs +++ b/src/steps/os/macos.rs @@ -38,7 +38,7 @@ pub fn run_mas(ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> { print_separator(t!("macOS system update")); - let should_ask = !(ctx.config().yes(Step::System) || ctx.config().dry_run()); + let should_ask = !(ctx.config().yes(Step::System) || ctx.run_type().dry()); if should_ask { println!("{}", t!("Finding available software")); if system_update_available()? { @@ -95,7 +95,7 @@ pub fn update_xcodes(ctx: &ExecutionContext) -> Result<()> { let xcodes = require("xcodes")?; print_separator("Xcodes"); - let should_ask = !(ctx.config().yes(Step::Xcodes) || ctx.config().dry_run()); + let should_ask = !(ctx.config().yes(Step::Xcodes) || ctx.run_type().dry()); let releases = ctx.execute(&xcodes).args(["update"]).output_checked_utf8()?.stdout; diff --git a/src/steps/os/openbsd.rs b/src/steps/os/openbsd.rs index d04b7125..791956cf 100644 --- a/src/steps/os/openbsd.rs +++ b/src/steps/os/openbsd.rs @@ -1,5 +1,6 @@ use crate::command::CommandExt; use crate::execution_context::ExecutionContext; +use crate::executor::RunType; use crate::terminal::print_separator; use color_eyre::eyre::Result; use rust_i18n::t; @@ -8,12 +9,13 @@ use std::fs; fn is_openbsd_current(ctx: &ExecutionContext) -> Result { let motd_content = fs::read_to_string("/etc/motd")?; let is_current = ["-current", "-beta"].iter().any(|&s| motd_content.contains(s)); - if ctx.config().dry_run() { - println!("{}", t!("Would check if OpenBSD is -current")); - Ok(is_current) - } else { - Ok(is_current) + match ctx.config.run_type() { + RunType::Dry | RunType::Damp => { + println!("{}", t!("Checking if /etc/motd contains -current or -beta")); + } + RunType::Wet => {} } + Ok(is_current) } pub fn upgrade_openbsd(ctx: &ExecutionContext) -> Result<()> { @@ -42,11 +44,6 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { let is_current = is_openbsd_current(ctx)?; - if ctx.config().dry_run() { - println!("{}", t!("Would upgrade OpenBSD packages")); - return Ok(()); - } - if ctx.config().cleanup() { sudo.execute(ctx, "/usr/sbin/pkg_delete")?.arg("-ac").status_checked()?; } diff --git a/translate.sh b/translate.sh new file mode 100755 index 00000000..90b68035 --- /dev/null +++ b/translate.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +## Translate the given string into $langs using translate-shell, outputting to the yaml structure expected for locales/app.yml + +langs="en lt es fr zh_CN zh_TW de" + +printf "\"%s\":\n" "$@" +for lang in $langs; do + result=$(trans -brief -no-auto -s en -t "${lang/_/-/}" "$@") + printf " %s: \"%s\"\n" "$lang" "$result" +done