diff --git a/Cargo.lock b/Cargo.lock index 302cddc7..13526b78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -865,7 +865,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -1961,6 +1961,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -2808,6 +2817,7 @@ dependencies = [ "tracing-subscriber", "walkdir", "which", + "whoami", "wildmatch", "winapi", ] @@ -3042,6 +3052,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -3139,6 +3155,17 @@ dependencies = [ "winsafe", ] +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall 0.5.8", + "wasite", + "web-sys", +] + [[package]] name = "widestring" version = "0.4.3" diff --git a/Cargo.toml b/Cargo.toml index 120e39aa..a3844f5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ notify-rust = "~4.11" wildmatch = "2.3.0" rust-i18n = "3.0.1" sys-locale = "0.3.1" +whoami = "1.5.2" [package.metadata.generate-rpm] assets = [{ source = "target/release/topgrade", dest = "/usr/bin/topgrade" }] diff --git a/locales/app.yml b/locales/app.yml index 838a525d..4b9fd3bd 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -793,3 +793,21 @@ _version: 2 es: "No se pueden actualizar las aplicaciones de Microsoft Store, se requiere intervención manual" fr: "Impossible de mettre à jour les applications du Microsoft Store, une intervention manuelle est nécessaire" zh_TW: "無法更新 Microsoft Store 應用,需手動幹預" + +"No flake configurations found for nh": + en: "No flake configurations found for nh" + es: "No se encontraron configuraciones de flake para nh." + fr: "Aucune configuration de flake trouvée pour nh." + zh_TW: "没有为 nh 找到 flake 配置" + +"No username found": + en: "No username found" + es: "No se encontró ningún nombre de usuario." + fr: "Aucun nom d'utilisateur trouvé." + zh_TW: "未找到用户名" + +"No hostname found": + en: "No hostname found" + es: "No se encontró ningún nombre de host." + fr: "Aucun nom d'hôte trouvé." + zh_TW: "未找到主机名" diff --git a/src/config.rs b/src/config.rs index dcbd7f14..c97be0f4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -115,6 +115,7 @@ pub enum Step { Mise, Myrepos, Nix, + NixHelper, Node, Opam, Pacdef, diff --git a/src/main.rs b/src/main.rs index 71c449e2..8b5ba409 100644 --- a/src/main.rs +++ b/src/main.rs @@ -311,6 +311,7 @@ fn run() -> Result<()> { runner.execute(Step::Yadm, "yadm", || unix::run_yadm(&ctx))?; runner.execute(Step::Nix, "nix", || unix::run_nix(&ctx))?; runner.execute(Step::Nix, "nix upgrade-nix", || unix::run_nix_self_upgrade(&ctx))?; + runner.execute(Step::NixHelper, "nh", || unix::run_nix_helper(&ctx))?; runner.execute(Step::Guix, "guix", || unix::run_guix(&ctx))?; runner.execute(Step::HomeManager, "home-manager", || unix::run_home_manager(&ctx))?; runner.execute(Step::Asdf, "asdf", || unix::run_asdf(&ctx))?; diff --git a/src/steps/os/unix.rs b/src/steps/os/unix.rs index ec3f136a..9da1f216 100644 --- a/src/steps/os/unix.rs +++ b/src/steps/os/unix.rs @@ -20,6 +20,8 @@ use regex::Regex; use rust_i18n::t; use semver::Version; use tracing::debug; +use whoami::fallible::hostname; +use whoami::fallible::username; #[cfg(target_os = "linux")] use super::linux::Distribution; @@ -620,8 +622,82 @@ fn nix_profile_dir(nix: &Path) -> Result> { ) } +/// Update NixOS and home-manager through a flake using `nh` +/// +/// See: https://github.com/viperML/nh +pub fn run_nix_helper(ctx: &ExecutionContext) -> Result<()> { + let nix = require("nix")?; + let nix_helper = require("nh")?; + let flake_path: PathBuf = std::env::var_os("FLAKE") + .ok_or_else(|| SkipStep("$FLAKE not set".into()))? + .require()? + .into(); + + flake_path.join("flake.nix").require()?; + + let run_type = ctx.run_type(); + + // we never dry run this because it doesn't perform any changes and because it ensures that the + // behaviour between dry and wet runs is the same i.e. the checks performed are the same + // between dry and wet runs + let run_nix_eval = |attr| -> Result { + Ok(String::from_utf8( + std::process::Command::new(&nix) + .args(nix_args()) + .arg("eval") + .arg(format!("{}#{attr}", flake_path.display())) + .arg("--apply") + .arg("builtins.attrNames") + .output_checked()? + .stdout, + )?) + }; + + let nixos_cfg = run_nix_eval("nixosConfigurations"); + let home_cfg = run_nix_eval("homeConfigurations"); + + if nixos_cfg.as_ref().or(home_cfg.as_ref()).is_err() && !run_type.dry() { + return Err(SkipStep(t!("No flake configurations found for nh").to_string()).into()); + } + + print_separator("nh flake"); + run_type + .execute(&nix) + .args(nix_args()) + .arg("flake") + .arg("update") + .arg("--flake") + .arg(&flake_path) + .status_checked()?; + + let username = username().wrap_err_with(|| t!("No username found").to_string())?; + let hostname = hostname().wrap_err_with(|| t!("No hostname found").to_string())?; + + // Let's not do any JSON parsing and just Keep It Simple + if nixos_cfg.is_ok_and(|x| x.contains(&format!(r#""{hostname}""#))) { + print_separator("nh os"); + + run_type.execute(&nix_helper).arg("os").arg("switch").status_checked()?; + } + + if let Ok(home_cfg) = home_cfg { + let username_at_hostname = format!(r#""{username}@{hostname}""#); + if home_cfg.contains(&format!(r#""{username}""#)) || home_cfg.contains(&username_at_hostname) { + print_separator("nh home"); + + run_type + .execute(&nix_helper) + .arg("home") + .arg("switch") + .status_checked()?; + } + } + + Ok(()) +} + fn nix_args() -> [&'static str; 2] { - ["--extra-experimental-features", "nix-command"] + ["--extra-experimental-features", "nix-command flakes"] } pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> {