Skip to content

Conversation

NotAShelf
Copy link
Member

@NotAShelf NotAShelf commented Sep 10, 2025

Allows specifying a --profile for Nix commands in NixOS, Darwin, and Home-Manager subcommands (os, darwin, and home respectively). This fills in the gap of --profile-path from nixos-install and similar CLIs being missing in NH.

Summary by CodeRabbit

  • New Features

    • Add --profile option to OS, Darwin, Home rebuild and OS rollback to target a custom profile path.
  • Improvements

    • Clear, early error when a provided profile path does not exist; CLI validates profile as a symlink.
    • Use the provided profile path for builds, rollbacks, diffs, and activation flows.
    • Refined sudo prompt wording and improved environment handling for elevated commands.
  • Chores

    • More resilient password-cache locking behavior (no public API changes).
  • Documentation

    • Add changelog entry describing the new --profile option.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I6a6a6964ededbfb54041c1c3e7c98467d08b5fd3
Copy link

coderabbitai bot commented Sep 10, 2025

Walkthrough

Adds a new optional --profile/-P option to OS, Home, and Darwin rebuild/rollback commands with CLI-time symlink validation, threads the provided profile through build/rollback/diff logic, adjusts mutex poison handling and sudo/env formatting in command helpers, and updates nix invocations to propagate env and wrap errors.

Changes

Cohort / File(s) Summary of changes
CLI: add profile flag & validator
src/interface.rs
Adds pub profile: Option<std::path::PathBuf> to OsRebuildArgs, OsRollbackArgs, HomeRebuildArgs, DarwinRebuildArgs; introduces symlink_path_validator(s: &str) -> Result<PathBuf,String> and wires it as value_parser for --profile (-P) with FilePath hint.
NixOS: unify/derive profile path
src/nixos.rs
Thread an optionally provided profile through rebuilds and rollbacks: derive system_profile_path from --profile or fallback to SYSTEM_PROFILE, use it for --profile in nix builds, diffs, symlink targets, generation resolution, rollback restoration, and activation flows.
Darwin: helpers & nix invocation
src/darwin.rs
Add get_system_profile and get_current_profile_pathbuf helpers; use computed profile path for nix --profile arg; update nix invocation to call .with_required_env(), .run(), and wrap errors with context.
Home: profile override for prev_generation
src/home.rs
Add USER_PROFILE_PATH and HOME_PROFILE_PATH constants; if --profile provided and exists, use it as prev_generation (clone selected path); otherwise preserve existing discovery between user and home profiles; log chosen prev_generation.
Command helpers: mutex, sudo prompt, env formatting
src/commands.rs
Recover poisoned mutex guards using PoisonError::into_inner() in password cache access; change sudo prompt to use a named {host} placeholder; format elevated command environment pairs as {key}={value} in both Set and Preserve branches.
Docs
CHANGELOG.md
Add Unreleased entry documenting the new --profile flag for os, home, and darwin subcommands and its behavior.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as CLI Parser
  participant Cmd as Rebuild/Rollback Logic
  participant Validator as Symlink Validator
  participant Nix as Nix Runner

  User->>CLI: Invoke command [--profile /path]?
  CLI->>Validator: validate --profile (symlink exists)
  alt valid profile provided
    Validator-->>CLI: Ok(profile PathBuf)
  else invalid/missing provided profile
    Validator-->>CLI: Err -> CLI returns error
  end
  CLI->>Cmd: pass args (profile: Option<PathBuf>)
  Cmd->>Cmd: derive system_profile_path (provided or default)
  Cmd->>Nix: build/run with ["build","--no-link","--profile", <system_profile_path>]
  Note right of Nix: env formatted as key=value\n.with_required_env() → .run()\nerrors wrapped with context
  Nix-->>Cmd: result
  Cmd-->>User: outcome / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Pre-merge checks (2 passed, 1 inconclusive)

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The current title captures the core feature—adding profile specification to multiple commands—but prefixes it with the vague term “various,” which adds noise without clarifying which commands are affected, making the title less precise and harder to scan at a glance. Please remove the generic “various” prefix and rename the PR to succinctly reflect the main change, for example: “Allow specifying --profile for os, home, and darwin commands.”
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch notashelf/push-lmlmokouvzvy

Comment @coderabbitai help to get the list of available commands and usage tips.

@NotAShelf NotAShelf force-pushed the notashelf/push-lmlmokouvzvy branch from 0202446 to 814d921 Compare September 10, 2025 09:55
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/commands.rs (1)

494-505: Honor NH_PRESERVE_ENV in remote-elevation env propagation (parity with local).

Remote sudo path currently preserves envs unconditionally, diverging from build_sudo_cmd. Align behavior to avoid leaking envs when NH_PRESERVE_ENV=0.

-      // Add env command to handle environment variables
+      // Add env command to handle environment variables
       elev_cmd = elev_cmd.arg("env");
+      let preserve_env = std::env::var("NH_PRESERVE_ENV")
+        .as_deref()
+        .map(|x| !matches!(x, "0"))
+        .unwrap_or(true);
       for (key, action) in &self.env_vars {
         match action {
           EnvAction::Set(value) => {
             elev_cmd = elev_cmd.arg(format!("{key}={value}"));
           },
-          EnvAction::Preserve => {
-            if let Ok(value) = std::env::var(key) {
-              elev_cmd = elev_cmd.arg(format!("{key}={value}"));
-            }
-          },
+          EnvAction::Preserve if preserve_env => {
+            if let Ok(value) = std::env::var(key) {
+              elev_cmd = elev_cmd.arg(format!("{key}={value}"));
+            }
+          }
           _ => {},
         }
       }
src/nixos.rs (2)

399-418: Rollback ignores custom --profile when selecting generations.

find_previous_generation/find_generation_by_number still scan the default profiles dir, then you later write to a possibly different profile_dir. This can select a non-existent generation for the chosen profile, risking a broken system profile symlink.

Minimal fix: derive system_profile_path/profile_dir first, then pass profile_dir into the generation lookups.

-    // Find previous generation or specific generation
-    let target_generation = if let Some(gen_number) = self.to {
-      find_generation_by_number(gen_number)?
-    } else {
-      find_previous_generation()?
-    };
+    // Resolve profile_dir from --profile (or default) first
+    let system_profile_path = self
+      .profile
+      .as_deref()
+      .unwrap_or_else(|| Path::new(SYSTEM_PROFILE));
+    let profile_dir = system_profile_path.parent().unwrap_or_else(|| {
+      tracing::warn!("system profile has no parent, defaulting to /nix/var/nix/profiles");
+      Path::new("/nix/var/nix/profiles")
+    });
+    // Find previous generation or specific generation within that directory
+    let target_generation = if let Some(gen_number) = self.to {
+      find_generation_by_number(gen_number, profile_dir)?
+    } else {
+      find_previous_generation(profile_dir)?
+    };
-
-    // Construct path to the generation
-    let system_profile_path = self
-      .profile
-      .as_deref()
-      .unwrap_or_else(|| Path::new(SYSTEM_PROFILE));
-    let profile_dir = system_profile_path.parent().unwrap_or_else(|| {
-      tracing::warn!(
-        "SYSTEM_PROFILE has no parent, defaulting to /nix/var/nix/profiles"
-      );
-      Path::new("/nix/var/nix/profiles")
-    });
+    // Construct path to the generation

And update current-gen lookup later:

-    let current_gen_number = match get_current_generation_number() {
+    let current_gen_number = match get_current_generation_number(profile_dir) {

Additional changes required outside this hunk are provided below.


574-619: Refactor helpers to accept profile_dir (supporting custom --profile).

Apply this change outside the modified hunks to keep behavior consistent:

// Replace these signatures and bodies:

fn find_previous_generation() -> Result<generations::GenerationInfo> { /* ... */ }
fn find_generation_by_number(number: u64) -> Result<generations::GenerationInfo> { /* ... */ }
fn get_current_generation_number() -> Result<u64> { /* ... */ }

// With:

fn find_previous_generation(profile_dir: &Path) -> Result<generations::GenerationInfo> {
  let mut generations: Vec<_> = std::fs::read_dir(profile_dir)?
    .filter_map(|e| {
      e.ok().and_then(|e| {
        let path = e.path();
        path.file_name()
          .and_then(|n| n.to_str())
          .filter(|n| n.starts_with("system-") && n.ends_with("-link"))
          .and_then(|_| generations::describe(&path))
      })
    })
    .collect();
  if generations.is_empty() {
    bail!("No generations found in {}", profile_dir.display());
  }
  generations.sort_by(|a, b| a.number.parse::<u64>().unwrap_or(0).cmp(&b.number.parse::<u64>().unwrap_or(0)));
  let current_idx = generations.iter().position(|g| g.current).ok_or_else(|| eyre!("Current generation not found"))?;
  if current_idx == 0 {
    bail!("No generation older than the current one exists");
  }
  Ok(generations[current_idx - 1].clone())
}

fn find_generation_by_number(number: u64, profile_dir: &Path) -> Result<generations::GenerationInfo> {
  let generations: Vec<_> = std::fs::read_dir(profile_dir)?
    .filter_map(|e| {
      e.ok().and_then(|e| {
        let path = e.path();
        path.file_name()
          .and_then(|n| n.to_str())
          .filter(|n| n.starts_with("system-") && n.ends_with("-link"))
          .and_then(|_| generations::describe(&path))
      })
    })
    .filter(|g| g.number == number.to_string())
    .collect();
  if generations.is_empty() {
    bail!("Generation {} not found in {}", number, profile_dir.display());
  }
  Ok(generations[0].clone())
}

fn get_current_generation_number(profile_dir: &Path) -> Result<u64> {
  let generations: Vec<_> = std::fs::read_dir(profile_dir)?
    .filter_map(|e| e.ok().and_then(|e| generations::describe(&e.path())))
    .collect();
  let current = generations.iter().find(|g| g.current).ok_or_else(|| eyre!("Current generation not found"))?;
  current.number.parse::<u64>().wrap_err("Invalid generation number")
}
🧹 Nitpick comments (4)
src/interface.rs (1)

232-235: Add path value hints to improve CLI UX and shell completion.

Attach a file-path hint to each new --profile flag so clap-driven shells offer correct completion.

-  #[arg(long, short = 'P')]
+  #[arg(long, short = 'P', value_hint = clap::ValueHint::FilePath)]
   pub profile: Option<std::path::PathBuf>,

Apply to OsRebuildArgs, OsRollbackArgs, HomeRebuildArgs, and DarwinRebuildArgs.

Also applies to: 293-296, 526-529, 639-642

src/commands.rs (1)

299-313: Mask sensitive env values in debug logs.

Debug logs currently print full values (incl. NH_*). Safer to redact likely secrets.

-    debug!(
-      "Configured envs: {}",
-      self
-        .env_vars
-        .iter()
-        .map(|(key, action)| {
-          match action {
-            EnvAction::Set(value) => format!("{key}={value}"),
-            EnvAction::Preserve => format!("{key}=<preserved>"),
-            EnvAction::Remove => format!("{key}=<removed>"),
-          }
-        })
-        .collect::<Vec<_>>()
-        .join(", ")
-    );
+    let redact = |k: &str, v: &str| {
+      static SUSPICIOUS: &[&str] = &["TOKEN", "PASSWORD", "PASS", "SECRET", "KEY", "CREDENTIAL"];
+      if SUSPICIOUS.iter().any(|s| k.contains(s)) { "<redacted>" } else { v }
+    };
+    debug!(
+      "Configured envs: {}",
+      self.env_vars.iter().map(|(key, action)| match action {
+        EnvAction::Set(value) => format!("{key}={}", redact(key, value)),
+        EnvAction::Preserve => format!("{key}=<preserved>"),
+        EnvAction::Remove => format!("{key}=<removed>"),
+      }).collect::<Vec<_>>().join(", ")
+    );
src/darwin.rs (1)

158-165: Create parent directory for custom --profile before nix writes it.

If a user-provided profile path’s parent doesn’t exist, nix build will fail. Ensure (or error with context) before invoking nix.

-      let profile_path = self.profile.as_ref().map_or_else(
-        || std::ffi::OsStr::new(SYSTEM_PROFILE),
-        |p| p.as_os_str(),
-      );
+      let profile_path: std::path::PathBuf = self
+        .profile
+        .clone()
+        .unwrap_or_else(|| std::path::PathBuf::from(SYSTEM_PROFILE));
+      if let Some(parent) = profile_path.parent() {
+        if !parent.exists() {
+          std::fs::create_dir_all(parent)
+            .wrap_err("Failed to create parent directory for --profile")?;
+        }
+      }
       Command::new("nix")
-        .args(["build", "--no-link", "--profile"])
-        .arg(profile_path)
+        .args(["build", "--no-link", "--profile"])
+        .arg(&profile_path)
src/home.rs (1)

102-123: Don’t silently disable diff when --profile doesn’t exist; warn and fall back.

Current behavior sets prev_generation=None if the provided path is missing, skipping diffs. Prefer warning and using default discovery, optionally validating it’s a symlink.

-    let prev_generation: Option<PathBuf> = if let Some(ref profile) =
-      self.profile
-    {
-      if profile.exists() {
-        Some(profile.clone())
-      } else {
-        None
-      }
-    } else {
-      [
-        PathBuf::from("/nix/var/nix/profiles/per-user")
-          .join(env::var("USER").map_err(|_| eyre!("Couldn't get username"))?)
-          .join("home-manager"),
-        PathBuf::from(
-          env::var("HOME").map_err(|_| eyre!("Couldn't get home directory"))?,
-        )
-        .join(".local/state/nix/profiles/home-manager"),
-      ]
-      .into_iter()
-      .find(|next| next.exists())
-    };
+    let candidates = || -> color_eyre::Result<[PathBuf; 2]> {
+      Ok([
+        PathBuf::from("/nix/var/nix/profiles/per-user")
+          .join(env::var("USER").map_err(|_| eyre!("Couldn't get username"))?)
+          .join("home-manager"),
+        PathBuf::from(
+          env::var("HOME").map_err(|_| eyre!("Couldn't get home directory"))?,
+        )
+        .join(".local/state/nix/profiles/home-manager"),
+      ])
+    };
+    let prev_generation: Option<PathBuf> = if let Some(ref profile) = self.profile {
+      if profile.exists() {
+        Some(profile.clone())
+      } else {
+        warn!("--profile '{}' not found; falling back to default detection", profile.display());
+        candidates()?.into_iter().find(|p| p.exists())
+      }
+    } else {
+      candidates()?.into_iter().find(|p| p.exists())
+    };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 440d3a6 and 814d921.

📒 Files selected for processing (5)
  • src/commands.rs (3 hunks)
  • src/darwin.rs (1 hunks)
  • src/home.rs (1 hunks)
  • src/interface.rs (4 hunks)
  • src/nixos.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/darwin.rs (1)
src/commands.rs (3)
  • new (160-171)
  • new (601-609)
  • new (723-732)
src/nixos.rs (1)
src/commands.rs (4)
  • new (160-171)
  • new (601-609)
  • new (723-732)
  • elevate (175-178)
src/commands.rs (1)
src/checks.rs (1)
  • new (338-347)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
🔇 Additional comments (4)
src/commands.rs (2)

25-27: Good: resilient recovery from poisoned password-cache mutex.

Switching to PoisonError::into_inner keeps the cache usable after a panic.

Also applies to: 33-35


456-457: Prompt tweak LGTM.

More natural sudo prompt with host interpolation.

src/nixos.rs (2)

333-341: Nice: thread --profile path through build/switch.

Passing the path as a separate arg is correct and avoids accidental concatenation bugs.


480-489: Follow-ups for the same bug: use the resolved system_profile_path consistently.

After parameterizing generation discovery by profile_dir, these call sites are correct for writing the link, but the discovery functions must also use profile_dir (see below).

Would you like me to push a patch PR branch with the refactor wired through all helpers and tests?

Also applies to: 550-558

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I6a6a69644cca0780985012b18715ffaec0eb76d5
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I6a6a69640fea6789de7fd14d88a130fa0c488933
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/home.rs (1)

3-5: Document intent and XDG nuance for profile path constants

Add brief docs so future readers know HOME_PROFILE_PATH is a HOME-based fallback and that XDG_STATE_HOME may take precedence elsewhere.

-const USER_PROFILE_PATH: &str = "/nix/var/nix/profiles/per-user";
-const HOME_PROFILE_PATH: &str = ".local/state/nix/profiles/home-manager";
+/// Base dir for per-user profiles (multi-user Nix).
+const USER_PROFILE_PATH: &str = "/nix/var/nix/profiles/per-user";
+/// HOME-based fallback path for HM profile; prefer XDG_STATE_HOME when set.
+const HOME_PROFILE_PATH: &str = ".local/state/nix/profiles/home-manager";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 814d921 and e44479a.

📒 Files selected for processing (4)
  • src/darwin.rs (2 hunks)
  • src/home.rs (2 hunks)
  • src/interface.rs (4 hunks)
  • src/nixos.rs (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/interface.rs
  • src/nixos.rs
  • src/darwin.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: treewide-checks
🔇 Additional comments (1)
src/home.rs (1)

105-131: Drop XDG_STATE_HOME/nix/profiles/home-manager fallback
Home Manager’s profile link remains ~/.nix-profile by default and, when Nix’s use-xdg-base-directories is enabled, becomes $XDG_STATE_HOME/nix/profile; it does not use $XDG_STATE_HOME/nix/profiles/home-manager.

Likely an incorrect or invalid review comment.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I6a6a69646f8083bc0869647ef1e8130b9497f545
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I6a6a6964c952f3e7c68a3d5933de3f6616b0c031
@NotAShelf NotAShelf force-pushed the notashelf/push-lmlmokouvzvy branch from ff4cc37 to ea51c44 Compare September 10, 2025 11:53
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/nixos.rs (2)

604-705: Parameterize generation discovery by profile path; current impl ignores --profile

find_previous_generation, find_generation_by_number, and get_current_generation_number hard-code SYSTEM_PROFILE and look for entries starting with system-. With --profile, rollback will search the wrong directory/prefix.

Proposed implementation (outside the changed hunk) to make these helpers profile-aware:

// Change signatures
fn find_previous_generation(profile: &Path) -> Result<generations::GenerationInfo> { /* ... */ }
fn find_generation_by_number(profile: &Path, number: u64) -> Result<generations::GenerationInfo> { /* ... */ }
fn get_current_generation_number(profile: &Path) -> Result<u64> { /* ... */ }

Core logic sketch:

fn profile_dir_and_prefix(profile: &Path) -> (PathBuf, String) {
  let dir = profile.parent().unwrap_or_else(|| Path::new("/nix/var/nix/profiles"));
  let prefix = profile.file_name()
    .and_then(|s| s.to_str())
    .unwrap_or("system")
    .to_owned();
  (dir.to_path_buf(), prefix)
}

Use starts_with(format!("{prefix}-")) when scanning, and pass profile from rollback:

let profile_path = self.profile.as_deref().unwrap_or(Path::new(SYSTEM_PROFILE));
let target_generation = if let Some(gen) = self.to {
  find_generation_by_number(profile_path, gen)?
} else {
  find_previous_generation(profile_path)?
};
let current_gen_number = get_current_generation_number(profile_path).unwrap_or(0);

I can provide a complete patch if helpful.

Also applies to: 650-681, 683-703


419-425: find_previous_generation and find_generation_by_number ignore custom profile
Both functions in src/nixos.rs (starting around lines 603 and 651) unconditionally use the hard-coded SYSTEM_PROFILE constant and never take the user’s profile into account. This will break rollbacks when a custom profile is specified. Refactor these functions to accept the configured profile path and use it instead of SYSTEM_PROFILE.

🧹 Nitpick comments (4)
src/nixos.rs (4)

241-247: Avoid cloning PathBuf for diff ‘lhs’ and simplify to &Path

Use as_deref() to pass an &Path directly and remove duplication at both call sites.

Apply this diff to both places:

-        let _ = print_dix_diff(
-          &self
-            .profile
-            .as_ref()
-            .map_or_else(|| PathBuf::from(CURRENT_PROFILE), PathBuf::from),
-          &target_profile,
-        );
+        let _ = print_dix_diff(
+          self.profile
+            .as_deref()
+            .unwrap_or_else(|| Path::new(CURRENT_PROFILE)),
+          &target_profile,
+        );

And similarly below:

-          let _ = print_dix_diff(
-            &self
-              .profile
-              .as_ref()
-              .map_or_else(|| PathBuf::from(CURRENT_PROFILE), PathBuf::from),
-            &target_profile,
-          );
+          let _ = print_dix_diff(
+            self.profile
+              .as_deref()
+              .unwrap_or_else(|| Path::new(CURRENT_PROFILE)),
+            &target_profile,
+          );

Also applies to: 261-267


239-275: Consider suppressing local diffs when acting on a remote host

Even with --diff always, comparing against a local CURRENT_PROFILE or local --profile is misleading for --target-host. Either skip diffs or print a clear note when targeting remote hosts.


429-435: Minor: warning text mentions SYSTEM_PROFILE constant

The message should be generic (it’s not necessarily SYSTEM_PROFILE anymore).

Already fixed in the earlier diff; calling it out so it isn’t missed.


352-361: Optional: name for OsStr binding

system_profile_path is an &OsStr here; consider renaming to system_profile_osstr (or use &Path) for clarity and type consistency with other uses.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff4cc37 and eda1b74.

📒 Files selected for processing (4)
  • CHANGELOG.md (1 hunks)
  • src/darwin.rs (4 hunks)
  • src/home.rs (2 hunks)
  • src/nixos.rs (8 hunks)
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/home.rs
  • src/darwin.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/nixos.rs (3)
src/commands.rs (4)
  • new (160-171)
  • new (601-609)
  • new (723-732)
  • elevate (175-178)
src/util.rs (1)
  • print_dix_diff (308-338)
src/interface.rs (3)
  • OsGenerationsArgs (344-348)
  • OsRebuildArgs (196-231)
  • CommonRebuildArgs (291-317)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
🔇 Additional comments (3)
src/nixos.rs (3)

352-361: LGTM: correct use of nix --profile path for Boot/Switch

Using --profile with the derived path is a clean way to set the system profile, and keeping --no-link avoids the extra result symlink.


508-513: LGTM: switching to elevated ln -sfn with the derived profile path

Using ln -sfn via the command wrapper (with elevation) is robust and avoids partial state when retargeting the profile symlink.


578-583: LGTM: correct restoration path on activation failure

Restoring the previous profile with the same derived --profile path keeps rollback consistent with custom profiles.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I6a6a696402d1085e6699ec5eed6ecf0084df54b6
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I6a6a6964db21018c13af7ffc0cee613e01ea24b4
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/nixos.rs (1)

410-417: find_previous_generation/find_generation_by_number ignore --profile

Both helpers scan only “system-*-link” under the default directory. With --profile, rollback may select the wrong generation or fail.

Proposed approach (new helpers; call them when self.profile is Some):

// New helpers (add near existing find_* fns)
fn find_previous_generation_for_profile(profile: &Path) -> Result<generations::GenerationInfo> {
  let profile_dir = profile.parent().unwrap_or(Path::new("/nix/var/nix/profiles"));
  let base = profile.file_name().and_then(|s| s.to_str()).unwrap_or("system");

  let mut gens: Vec<_> = fs::read_dir(profile_dir)?
    .filter_map(|e| e.ok())
    .map(|e| e.path())
    .filter(|p| {
      p.file_name()
        .and_then(|n| n.to_str())
        .map(|n| n.starts_with(&format!("{base}-")) && n.ends_with("-link"))
        .unwrap_or(false)
    })
    .filter_map(|p| generations::describe(&p))
    .collect();

  if gens.is_empty() { bail!("No generations found"); }
  gens.sort_by_key(|g| g.number.parse::<u64>().unwrap_or(0));

  let current_idx = gens.iter().position(|g| g.current)
    .ok_or_else(|| eyre!("Current generation not found"))?;
  if current_idx == 0 { bail!("No generation older than the current one exists"); }
  Ok(gens[current_idx - 1].clone())
}

fn find_generation_by_number_for_profile(profile: &Path, number: u64) -> Result<generations::GenerationInfo> {
  let profile_dir = profile.parent().unwrap_or(Path::new("/nix/var/nix/profiles"));
  let base = profile.file_name().and_then(|s| s.to_str()).unwrap_or("system");

  let gens: Vec<_> = fs::read_dir(profile_dir)?
    .filter_map(|e| e.ok())
    .map(|e| e.path())
    .filter(|p| {
      p.file_name()
        .and_then(|n| n.to_str())
        .map(|n| n.starts_with(&format!("{base}-")) && n.ends_with("-link"))
        .unwrap_or(false)
    })
    .filter_map(|p| generations::describe(&p))
    .filter(|g| g.number == number.to_string())
    .collect();

  if gens.is_empty() { bail!("Generation {} not found", number); }
  Ok(gens[0].clone())
}

And in rollback():

let target_generation = if let Some(gen_number) = self.to {
  if let Some(profile) = self.profile.as_ref() {
    find_generation_by_number_for_profile(profile, gen_number)?
  } else {
    find_generation_by_number(gen_number)?
  }
} else {
  if let Some(profile) = self.profile.as_ref() {
    find_previous_generation_for_profile(profile)?
  } else {
    find_previous_generation()?
  }
};

I can prepare a full patch if you want this wired in now.

♻️ Duplicate comments (2)
src/nixos.rs (2)

419-433: Rollback uses hard-coded “system-” prefix; must derive from the selected profile basename

If --profile is /nix/var/nix/profiles/foo, generation symlinks are foo--link. Current code will point to non-existent “system--link”.

Apply:

-    let profile_dir = system_profile_path.parent().unwrap_or_else(|| {
-      tracing::warn!(
-        "SYSTEM_PROFILE has no parent, defaulting to /nix/var/nix/profiles"
-      );
+    let profile_dir = system_profile_path.parent().unwrap_or_else(|| {
+      tracing::warn!(
+        "Profile path has no parent, defaulting to /nix/var/nix/profiles"
+      );
       Path::new("/nix/var/nix/profiles")
     });
-    let generation_link =
-      profile_dir.join(format!("system-{}-link", target_generation.number));
+    let profile_base = system_profile_path
+      .file_name()
+      .and_then(|s| s.to_str())
+      .unwrap_or("system");
+    let generation_link =
+      profile_dir.join(format!("{profile_base}-{}-link", target_generation.number));

Follow-up: the “restore previous” path below must use the same profile_base; see further comment.


569-574: Restore-on-failure must also use dynamic basename

Use the same profile_base computed above to build current_gen_link.

Apply:

-          let current_gen_link =
-            profile_dir.join(format!("system-{current_gen_number}-link"));
+          let current_gen_link =
+            profile_dir.join(format!("{profile_base}-{current_gen_number}-link"));

Also applies to: 578-579

🧹 Nitpick comments (11)
src/home.rs (2)

105-121: Profile resolution: handle missing USER more gracefully

If $USER is unset, this errors even though we could fall back to the $HOME-based profile. Prefer optional username + existence check, then fall back.

Apply:

-  let user_profile = PathBuf::from(USER_PROFILE_PATH)
-    .join(env::var("USER").map_err(|_| eyre!("Couldn't get username"))?)
-    .join("home-manager");
-  let home_profile = PathBuf::from(
+  let home_profile = PathBuf::from(
     env::var("HOME").map_err(|_| eyre!("Couldn't get home directory"))?,
   )
   .join(HOME_PROFILE_PATH);
-  if user_profile.exists() {
-    user_profile
-  } else {
-    home_profile
-  }
+  let user_profile = env::var("USER").ok().map(|u| {
+    PathBuf::from(USER_PROFILE_PATH).join(u).join("home-manager")
+  });
+  if let Some(up) = user_profile.as_ref().filter(|p| p.exists()) {
+    up.clone()
+  } else {
+    home_profile
+  }

122-127: Warn when an explicit --profile doesn’t exist (skip diff visibly)

Currently we silently skip diff if the override path is missing.

Apply:

-let prev_generation: Option<PathBuf> = if profile_path.exists() {
-  Some(profile_path.clone())
-} else {
-  None
-};
+let prev_generation: Option<PathBuf> = profile_path.exists().then(|| profile_path.clone());
+if prev_generation.is_none() && self.profile.is_some() {
+  warn!(
+    "--profile path provided but does not exist: {}. Skipping diff.",
+    profile_path.display()
+  );
+}
src/darwin.rs (1)

162-166: Skip diff if the “current” profile path doesn’t exist

On Darwin, the default CURRENT_PROFILE may not exist. Guard to avoid noisy errors.

Apply:

-      let _ = print_dix_diff(
-        &get_current_profile_pathbuf(&self.profile),
-        &target_profile,
-      );
+      let current = get_current_profile_pathbuf(&self.profile);
+      if current.exists() {
+        let _ = print_dix_diff(&current, &target_profile);
+      } else {
+        debug!("Skipping diff: current profile not found at {}", current.display());
+      }
src/nixos.rs (2)

232-238: Nit: avoid PathBuf::from on a PathBuf

Use clone/unwrap_or_else to prevent redundant construction.

Apply:

-        let _ = print_dix_diff(
-          &self
-            .profile
-            .as_ref()
-            .map_or_else(|| PathBuf::from(CURRENT_PROFILE), PathBuf::from),
-          &target_profile,
-        );
+        let lhs = self.profile.clone().unwrap_or_else(|| PathBuf::from(CURRENT_PROFILE));
+        let _ = print_dix_diff(&lhs, &target_profile);

252-258: Nit: same simplification here

Apply:

-          let _ = print_dix_diff(
-            &self
-              .profile
-              .as_ref()
-              .map_or_else(|| PathBuf::from(CURRENT_PROFILE), PathBuf::from),
-            &target_profile,
-          );
+          let lhs = self.profile.clone().unwrap_or_else(|| PathBuf::from(CURRENT_PROFILE));
+          let _ = print_dix_diff(&lhs, &target_profile);
src/interface.rs (6)

9-10: Drop eyre import for value parser errors.

These errors are plain strings; eyre adds no value here.

Apply:

- use color_eyre::eyre::eyre;

11-29: Doc/behavior mismatch: do not claim to return a canonicalized path.

The function returns the provided path, not a canonicalized PathBuf.

Apply:

-/// - `Ok(PathBuf)`: If the path exists and is a symlink, returns the
-///   canonicalized `PathBuf`.
+/// - `Ok(PathBuf)`: If the path exists and is a symlink, returns the provided
+///   `PathBuf` unchanged.
@@
-/// Returns an error if the path does not exist or is not a symbolic link.
+/// Returns an error if the path does not exist (including broken symlinks) or is not a symbolic link.

29-49: Tighten diagnostics and avoid eyre in the validator.

Simplify to format!-based strings and distinguish broken symlinks.

Apply:

 fn symlink_path_validator(s: &str) -> Result<PathBuf, String> {
   let path = Path::new(s);

-  // `bail!` is for early returns in functions that return `Result<T, E>`, i.e.,
-  // it immediately returns from the function with an error. Since this is a
-  // value parser and we need to return `Err(String)` `eyre!` is more
-  // appropriate.
+  // Clap value parsers must return Result<T, String>; build plain strings so clap can render them.
-  if !path.exists() {
-    return Err(
-      eyre!("--profile path provided but does not exist: {}", s).to_string(),
-    );
-  }
-
-  if !path.is_symlink() {
-    return Err(
-      eyre!("--profile path exists but is not a symlink: {}", s).to_string(),
-    );
-  }
+  if path.is_symlink() && !path.exists() {
+    return Err(format!("--profile symlink is broken: {s}"));
+  }
+  if !path.exists() {
+    return Err(format!("--profile path does not exist: {s}"));
+  }
+  if !path.is_symlink() {
+    return Err(format!("--profile path exists but is not a symlink: {s}"));
+  }

   Ok(path.to_path_buf())
 }

Optional: If you want to accept ~ expansion for Home Manager paths, I can follow up with a minimal tilde-expansion (adds a tiny dependency) or a home-dir based helper.


276-279: Nit: possessive “Nix's”.

Minor grammar fix in help text.

-  /// Path to Nix' system profile
+  /// Path to Nix's system profile

Optional: For consistency with OsGenerationsArgs, consider aligning on PathBuf + validator there too.


337-340: Nit: possessive “Nix's”.

-  /// Path to Nix' system profile for rollback
+  /// Path to Nix's system profile for rollback

570-573: Nit: project name styling.

Prefer “Home Manager” (project name) over “Home-Manager”.

-  /// Path to Home-Manager profile
+  /// Path to Home Manager profile
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eda1b74 and 206dcd1.

📒 Files selected for processing (4)
  • src/darwin.rs (3 hunks)
  • src/home.rs (3 hunks)
  • src/interface.rs (5 hunks)
  • src/nixos.rs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/nixos.rs (2)
src/util.rs (1)
  • print_dix_diff (308-338)
src/commands.rs (4)
  • new (160-171)
  • new (601-609)
  • new (723-732)
  • elevate (175-178)
src/darwin.rs (1)
src/util.rs (1)
  • print_dix_diff (308-338)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Darwin
🔇 Additional comments (10)
src/home.rs (2)

3-5: Constants for default Home-Manager profiles: LGTM

Paths look correct for per-user and XDG state locations.


154-164: Diff gate on prev_generation: LGTM

No diff on fresh installs or non-existent profile is sensible.

src/darwin.rs (3)

26-33: Helper get_system_profile(): LGTM

Returns &OsStr without allocations; fits the nix invocation usage.


34-43: Helper get_current_profile_pathbuf(): LGTM (naming matches return type)

Comment clarifies Darwin semantics; good.


179-183: Using derived profile in nix build: LGTM

Passing --profile from user or default aligns with Nix expectations.

src/nixos.rs (3)

343-347: Using derived profile for nix --profile: LGTM

OsStr handling avoids allocations and works locally/over SSH.

Also applies to: 350-352


456-462: Diff path for rollback: LGTM

Left-hand uses provided profile or CURRENT_PROFILE; good.


499-503: Setting system profile via ln with derived path: LGTM

Consistent with earlier profile selection.

src/interface.rs (2)

1-4: Imports expansion looks good.

Path is needed for the new validator; no issues.


683-686: Darwin profile flag wiring looks good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant