Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@NotAShelf
Copy link
Member

@NotAShelf NotAShelf commented Dec 4, 2025

Fixes #428, Fixes #434

This is a large architectural change to NH, which lead to me extracting the remote build logic to its own file so that we may implement it for Darwin and Home-Manager as well. The --builders flag was dropped from nh::commands, and it was replaced with the new and shiny logic that hopefully avoids previous pitfalls.

The new nh::remote module handles remote builds, including:

  • Parsing remote host specifications.
  • Copying derivations to remote hosts using nix-copy-closure.
  • Building derivations on remote hosts via nix build.
  • Copying results back to localhost or directly to a target host.

As planned, this implements similar remote build semantics to add addsupport for remote builds via SSH for Home Manager and Darwin configurations.

Change-Id: I236eb1e35dd645f2169462d207bc82e76a6a6964

Summary by CodeRabbit

  • New Features

    • SSH-based remote builds and remote activation via --build-host (os, home, darwin); multi-host build/activate workflows; SSH connection multiplexing/cleanup
  • Options

    • --elevation-strategy (alias --elevation-program) with none/passwordless/program modes
    • --no-validate (env NH_NO_VALIDATE)
    • --install-bootloader for switch/boot
    • NH_REMOTE_CLEANUP opt-in
  • Bug Fixes

    • Unified password caching across remote ops; empty passwords rejected
    • Improved shell argument parsing and quoting
  • Documentation

    • New remote-build guide, expanded README, updated manpage ENVIRONMENT/FILES and changelog entries

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 4, 2025

Walkthrough

Adds a public SSH-based remote build and activation subsystem, threads elevation strategy and password-caching through build/activation flows, replaces ad-hoc tokenization with shlex, introduces --build-host and --no-validate flags, and wires SSH control sockets plus interrupt cleanup into remote operations.

Changes

Cohort / File(s) Summary
Remote Module
src/remote.rs, src/lib.rs
New public remote module: RemoteHost, RemoteBuildConfig, build_remote/eval_drv_path, remote activation (activate_remote/activate_nixos_remote), init_ssh_control/SshControlGuard, SSH opts/quoting, copy/build helpers, interrupt handling, and tests.
NixOS / Build & Activation
src/nixos.rs, src/home.rs, src/darwin.rs
Thread ElevationStrategy and remote context through build/activation; execute_build now returns Option<PathBuf> for actual store paths; add remote-aware validation, copying, and activation branches; parse build_host and init SSH control where used; update signatures to accept &ElevationStrategy.
CLI / Arguments
src/interface.rs, src/main.rs
Replace elevation_program with elevation_strategy (ElevationStrategyArg), add --build-host to OS/Home/Darwin args, add --no-validate / NH_NO_VALIDATE, and add backward-compatible NH_ELEVATION_PROGRAM fallback.
Commands & Elevation API
src/commands.rs
Add ElevationStrategyArg; expand ElevationStrategy variants and add resolve(); add get_cached_password/cache_password returning Result; remove old cmdline-quoting helper and some builder/setter methods.
SSH Control & Interrupts
src/remote.rs
SSH control socket lifecycle and automatic cleanup (SshControlGuard); global SIGINT handling with opt-in remote cleanup (NH_REMOTE_CLEANUP).
Tokenization & Dependencies
Cargo.toml
Replace ad-hoc tokenization with shlex; add dependencies: shlex, signal-hook, urlencoding.
Utilities Refactor
src/util/platform.rs
Remove activate_nixos_configuration (activation moved into nixos/remote flows).
Docs & Manpage
docs/remote-build.md, docs/README.md, CHANGELOG.md, xtask/src/man.rs
Add remote-build docs, README updates, changelog entries (build-host, no-validate, NH_REMOTE_CLEANUP), and manpage ENVIRONMENT/FILES sections.
Packaging & Tests
package.nix, tests/...
Add sudo input and Darwin test skips; tests for RemoteHost parsing, quoting, SSH control, interrupt handling, and related utilities.
Small UX/API tweaks
src/home.rs, src/main.rs, others
Improved UTF‑8 handling for spec_location, added unreachable guard attribute, extended docstrings/error contexts, and empty-password validation to avoid caching invalid credentials.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as nh (local)
    participant Build as Build Host
    participant Target as Target Host

    User->>CLI: nh os switch --build-host build.example
    activate CLI

    rect `#EAF5FF`
      CLI->>CLI: evaluate drvPath (local nix eval)
    end

    rect `#E8FFE6`
      CLI->>Build: init SSH control + nix-copy-closure (ssh)
      Build->>Build: remote nix build (nom optional)
      Build-->>CLI: copy result back (nix-copy-closure / nix copy)
    end

    rect `#FFF6E6`
      alt target_host specified
        CLI->>Target: copy result -> target_host
        Target->>Target: validate & activate (switch/test/boot)
      else local activation
        CLI->>CLI: validate & activate using out_path (local)
      end
    end

    CLI-->>User: completed
    deactivate CLI
Loading
sequenceDiagram
    participant CLI as nh
    participant SSHCtrl as SSH Control
    participant Signal as SIGINT handler
    participant RemoteOp as Remote Operation

    CLI->>SSHCtrl: init_ssh_control() (create control path)
    CLI->>Signal: register SIGINT handler
    SSHCtrl->>RemoteOp: reuse control socket for remote commands
    RemoteOp->>RemoteOp: execute remote nix build / copy

    alt user presses Ctrl+C
        Signal->>RemoteOp: set interrupt flag
        alt NH_REMOTE_CLEANUP enabled
            RemoteOp->>RemoteOp: terminate remote Nix processes via SSH
        end
    end

    SSHCtrl->>SSHCtrl: SshControlGuard::drop() cleans control path
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly summarizes the main change: refactoring --build-host to use remote build semantics, which is the primary objective addressed across multiple files in the changeset.
Linked Issues check ✅ Passed The PR successfully implements the core requirements from issue #428: reimplement --build-host to open SSH connections as the invoking user in interactive mode, transfer closures via nix-copy-closure, and perform remote builds. Remote build semantics are applied to NixOS, Home Manager, and Darwin configurations through the new remote module.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the --build-host refactoring objectives. New infrastructure includes remote build module, SSH control management, elevation strategy updates, and password caching—all supporting the remote build semantics implementation. Documentation and build system updates are appropriately scoped to the feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom Pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b8f6f2 and ed1e21a.

📒 Files selected for processing (3)
  • package.nix
  • src/commands.rs
  • src/nixos.rs
🧰 Additional context used
🧬 Code graph analysis (2)
src/nixos.rs (1)
src/remote.rs (3)
  • activate_remote (1151-1163)
  • build_remote (1494-1594)
  • validate_closure_remote (827-924)
src/commands.rs (1)
src/checks.rs (1)
  • new (348-357)
⏰ 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: treewide-checks
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
🔇 Additional comments (9)
package.nix (1)

82-106: LGTM - Test skipping configuration is correct.

The previous review comment about --skip incompatibility with nextest was incorrect. As confirmed by the nextest documentation, nextest does support --skip and --exact flags with the same semantics as cargo test. The current implementation correctly uses --skip flags to exclude sudo-dependent tests on Darwin where the sudo package is unavailable.

The conditional test configuration is well-structured:

  • nativeCheckInputs includes sudo only on non-Darwin platforms
  • checkFlags skips the appropriate tests on Darwin
  • This aligns with the useNextest = true setting
src/commands.rs (3)

25-73: Well-designed password caching for remote operations.

The password caching implementation is thread-safe and follows best practices:

  • Uses OnceLock<Mutex<HashMap>> for safe concurrent access
  • Stores passwords as SecretString for secure memory handling
  • Provides clear error messages for lock poisoning
  • Documentation clearly explains the caching lifetime and usage

628-652: Password prompting logic is correct.

The empty password check at line 643-645 is appropriate here. For remote deployments requiring password input, an empty password would fail authentication. For passwordless sudo configurations (NOPASSWD in sudoers), users should use --elevation-strategy=passwordless, which bypasses this prompt entirely as noted in the comment on line 629-630.


850-894: Critical fix: Correctly handles nix build failures when using nom.

The nom pipeline always succeeds (exit status 0) even when the underlying nix build command fails. The fix at lines 865-881 correctly addresses this by:

  1. Using popen() instead of join() to access individual process exit statuses
  2. Checking the first process (nix build) exit status explicitly
  3. Failing the entire operation if nix build fails

This ensures build failures are properly propagated to the user instead of being silently masked by nom's success status.

src/nixos.rs (5)

167-175: Well-placed SSH control socket initialization.

Initializing the SSH control socket early and keeping it alive with _ssh_guard for the duration of both build and activation operations is the right approach. This enables connection multiplexing and avoids repeated SSH handshakes, which is especially important for high-latency connections or when multiple operations (build, copy, validate, activate) are performed against the same remote host.


232-243: Copy operation handles remote build fallback correctly.

This copy operation is not redundant. It handles the fallback scenario where remote::build_remote() attempts direct remote-to-remote copying (build_host → target_host) but fails, falling back to copying through localhost (build_host → localhost). In that fallback case, the result exists locally (out_path.exists() is true), and this code ensures it gets copied from localhost to target_host.

When direct remote-to-remote copying succeeds or when build_host == target_host, out_path.exists() is false and this copy is correctly skipped.


246-287: Validation logic correctly handles multiple scenarios.

The validation logic properly distinguishes between:

  1. Remote builds with actual_store_path captured from remote::build_remote()
  2. Remote builds where result wasn't copied locally (target_host == build_host)
  3. Local builds requiring canonicalization

The fallback chain at lines 250-263 ensures the correct store path is used for validation in each scenario. The distinction between local validation (validate_system_closure) and remote validation (validate_system_closure_remote) is appropriate, with remote validation properly delegating to SSH-based checks via remote::validate_closure_remote().


489-552: Remote build integration is well-designed.

The execute_build() method cleanly separates remote and local build paths:

Remote builds (lines 501-537):

  • Evaluates derivation locally
  • Copies to build_host via user-initiated SSH
  • Builds on remote host
  • Handles copying to target_host or localhost as appropriate
  • Returns the actual store path for use in validation and activation

Local builds (lines 538-551):

  • Uses existing commands::Build infrastructure
  • Returns None since local builds use symlinks rather than explicit store paths

The remote build configuration properly aggregates all necessary context (build_host, target_host, extra_args, substitutes, nom) for the remote build subsystem.


961-1015: Pre-activation validation improves reliability.

The validation functions provide a safety check that catches incomplete or corrupted system closures before attempting activation. This is especially valuable for remote operations where activation failures are more difficult to recover from.

The essential files checked (switch-to-configuration, nixos-version, init, sw/bin) align with what nixos-rebuild-ng validates. The error messages provide clear guidance on common causes and remediation steps.

The remote validation function (validate_system_closure_remote) properly delegates to remote::validate_closure_remote(), which batches checks for efficiency and provides detailed context in error messages (including build_host information when available).


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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: 5

🧹 Nitpick comments (2)
src/remote.rs (2)

18-37: Consider cleanup of SSH control directory on exit.

The SSH control socket directory (nh-ssh-{pid}) persists after the program terminates. While the ControlPersist=60 setting ensures connections close after 60 seconds of inactivity, the empty directory remains. This is a minor resource leak.

You could implement cleanup using a Drop guard or atexit handler, though this is low priority since the impact is minimal (just empty directories in /tmp or XDG_RUNTIME_DIR).


368-409: Consider extracting common attribute-appending logic.

The pattern of cloning the installable and appending "drvPath" to the attribute is repeated for Flake, File, and Expression variants. This could be simplified with a helper method on Installable.

This is a minor refactor opportunity for future cleanup - the current implementation is correct and readable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8be4df and ec92308.

📒 Files selected for processing (7)
  • src/commands.rs (1 hunks)
  • src/home.rs (1 hunks)
  • src/interface.rs (2 hunks)
  • src/lib.rs (1 hunks)
  • src/main.rs (1 hunks)
  • src/nixos.rs (2 hunks)
  • src/remote.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/interface.rs (1)
src/commands.rs (1)
  • arg (248-251)
src/remote.rs (1)
src/util.rs (1)
  • get_nix_variant (56-85)
src/nixos.rs (1)
src/remote.rs (2)
  • parse (67-107)
  • build_remote (474-517)
⏰ 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: Build NH on Linux
  • GitHub Check: profile
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
🔇 Additional comments (10)
src/main.rs (1)

12-12: LGTM!

Module declaration is correctly placed and follows the existing naming conventions.

src/lib.rs (1)

13-13: LGTM!

Public module declaration is consistent with other modules in this file. The internal-use comment at line 1 applies to the entire library.

src/nixos.rs (2)

25-25: LGTM!

Import of remote module types is correctly placed and all imported items are used in the execute_build_command method.


354-407: Well-structured remote build implementation matching nixos-rebuild semantics.

The implementation correctly:

  1. Parses host specifications with proper error context
  2. Constructs the RemoteBuildConfig with all necessary parameters
  3. Preserves the local build path as a fallback
  4. Passes extra args from both explicit args and passthrough options

The inline documentation clearly explains the workflow, which helps maintainability.

src/commands.rs (1)

654-752: LGTM! Clean removal of builder field from Build struct.

The Build struct and its tests have been correctly updated to remove the builder field and related method. The test at line 1200 properly removes the .builder("user@host") call, and the struct now cleanly delegates remote build concerns to the new remote module.

src/remote.rs (5)

67-107: LGTM! Robust host specification parsing with helpful error messages.

The RemoteHost::parse function handles various input formats correctly and provides actionable error messages (e.g., suggesting NIX_SSHOPTS for port configuration). The validation of edge cases like empty usernames, hostnames, and invalid characters is comprehensive.


206-254: LGTM! Secure remote command execution.

The function properly:

  • Shell-quotes all arguments to prevent injection
  • Uses -- separator to prevent argument injection to SSH
  • Includes stderr in error messages for debugging
  • Wraps errors with context

256-362: LGTM! Well-structured closure copying functions.

The three copy functions handle the different scenarios correctly:

  • copy_closure_to and copy_closure_from use nix-copy-closure for localhost ↔ remote transfers
  • copy_closure_between_remotes uses nix copy with SSH URIs for remote-to-remote transfers
  • Substitutes flags are correctly mapped (--use-substitutes vs --substitute-on-destination)

577-653: LGTM! Clever approach for nom integration.

The double-invocation pattern (build with nom consuming output, then query for output path) is a reasonable workaround since nom consumes the JSON output. The second build is a no-op due to Nix caching, so the overhead is minimal.


655-762: LGTM! Comprehensive unit tests for parsing and quoting logic.

The tests provide good coverage for:

  • Various RemoteHost input formats and error cases
  • Shell quoting edge cases including the critical drv^* syntax
  • SSH options default values

The unsafe block at lines 753-755 for environment variable manipulation is acceptable in test code.

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)

106-124: Consider extracting shared remote build configuration logic.

The RemoteBuildConfig construction here is nearly identical to the one in src/darwin.rs (lines 119-137). A shared helper function could reduce duplication:

// In a shared location (e.g., remote.rs or a new util)
fn build_config_from_args(
    build_host: RemoteHost,
    no_nom: bool,
    use_substitutes: bool,
    extra_args: &[String],
    passthrough: &Passthrough,
) -> RemoteBuildConfig { ... }

This is a minor refactoring opportunity that can be deferred.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec92308 and 1463ab0.

📒 Files selected for processing (2)
  • src/darwin.rs (2 hunks)
  • src/home.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/home.rs (1)
src/remote.rs (2)
  • parse (67-107)
  • build_remote (474-517)
src/darwin.rs (1)
src/remote.rs (2)
  • parse (67-107)
  • build_remote (474-517)
⏰ 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). (11)
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: treewide-checks
  • GitHub Check: profile
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: treewide-checks
🔇 Additional comments (4)
src/darwin.rs (2)

18-18: LGTM!

The new imports for the remote build subsystem are correctly added and all three symbols (remote, RemoteBuildConfig, RemoteHost) are used in the remote build logic below.


112-151: Well-structured remote build integration.

The conditional remote build path correctly:

  • Parses the host specification with proper error context
  • Constructs the RemoteBuildConfig with appropriate field mappings
  • Passes out_path as the out-link so build_remote creates the symlink there
  • Falls back to the local build flow when no build_host is specified

The subsequent code (line 153 onwards) correctly uses out_path which will be a valid symlink after either build path completes.

src/home.rs (2)

14-14: Imports are now properly used.

The previously flagged unused imports (remote, RemoteBuildConfig, RemoteHost) are now correctly used in the remote build logic at lines 103, 106, and 126 respectively.


99-138: Remote build integration looks correct.

The conditional remote build path properly:

  • Parses the host specification with error context
  • Builds RemoteBuildConfig with the correct field mappings
  • Invokes build_remote with the out-link for symlink creation
  • Falls back to local build when no build_host is provided

The post-build steps (activation, diffs, etc.) work correctly since out_path will be a valid symlink after either path.

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

♻️ Duplicate comments (1)
src/remote.rs (1)

604-612: Non-UTF-8 extra arguments are silently dropped.

As flagged in a previous review, filter_map(|s| s.to_str().map(String::from)) silently discards any OsString arguments that aren't valid UTF-8. Consider logging a warning when this occurs:

   let extra_args_strings: Vec<String> = config
     .extra_args
     .iter()
-    .filter_map(|s| s.to_str().map(String::from))
+    .filter_map(|s| {
+      match s.to_str() {
+        Some(str) => Some(str.to_string()),
+        None => {
+          tracing::warn!("Dropping non-UTF-8 argument: {:?}", s);
+          None
+        }
+      }
+    })
     .collect();

The same pattern exists at lines 649-656.

🧹 Nitpick comments (5)
src/commands.rs (1)

44-46: Silent failure on malformed input may hide user errors.

Using unwrap_or_default() silently returns an empty vector when shlex::split fails (e.g., unclosed quotes). While the test at line 1421-1424 documents this behavior, returning an empty vector could mask user input errors in contexts like self_elevate_cmd (line 444), where an empty parse leads to a bail at line 447.

Consider logging a warning when parsing fails to aid debugging:

 fn parse_cmdline_with_quotes(cmdline: &str) -> Vec<String> {
-  shlex::split(cmdline).unwrap_or_default()
+  shlex::split(cmdline).unwrap_or_else(|| {
+    tracing::warn!("Failed to parse command line (unclosed quote?): {cmdline}");
+    Vec::new()
+  })
 }
src/darwin.rs (1)

112-140: Consider adding SSH reachability check for consistency with NixOS module.

The NixOS implementation (src/nixos.rs lines 374-385) performs an SSH reachability check before starting expensive evaluation. This provides early feedback if the build host is unreachable. The Darwin implementation omits this check.

     if let Some(ref build_host_str) = self.build_host {
       info!("Building Darwin configuration");
 
       let build_host = RemoteHost::parse(build_host_str)
         .wrap_err("Invalid build host specification")?;
 
+      // Check SSH connectivity before expensive evaluation
+      info!("Checking SSH connectivity to build host...");
+      remote::check_ssh_reachability(&build_host)
+        .wrap_err(format!("Build host ({build_host}) is not reachable"))?;
+
       let config = RemoteBuildConfig {
src/home.rs (1)

99-127: Consider adding SSH reachability check for consistency.

Similar to the Darwin module, the Home-Manager implementation omits the SSH reachability pre-check that exists in the NixOS module (src/nixos.rs lines 374-385). For consistency and better user experience, consider adding the check:

     if let Some(ref build_host_str) = self.build_host {
       info!("Building Home-Manager configuration");
 
       let build_host = RemoteHost::parse(build_host_str)
         .wrap_err("Invalid build host specification")?;
 
+      info!("Checking SSH connectivity to build host...");
+      remote::check_ssh_reachability(&build_host)
+        .wrap_err(format!("Build host ({build_host}) is not reachable"))?;
+
       let config = RemoteBuildConfig {
src/nixos.rs (1)

286-291: Minor: Documentation formatting could be improved.

The Returns section has inconsistent line breaks that affect readability:

   /// # Returns
-  ///
-  /// `Result` containing a tuple:
-  ///
+  /// 
+  /// Returns a `Result` containing a tuple:
+  /// 
   /// - `bool`: `true` if elevation is required, `false` otherwise.
   /// - `String`: The resolved target hostname.
src/remote.rs (1)

18-48: SSH control directory is not cleaned up on exit.

The SSH control directory created at get_ssh_control_dir() persists after the program exits. While the directory itself is small, the SSH control sockets inside may linger. Consider documenting this behavior or implementing cleanup:

+// Note: The SSH control directory and sockets are intentionally not cleaned up
+// on exit to allow connection multiplexing across multiple nh invocations.
+// SSH's ControlPersist=60 handles socket cleanup after idle timeout.
 fn get_ssh_control_dir() -> &'static PathBuf {

Alternatively, you could register an atexit handler to clean up the directory if this is undesirable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e10b61 and 1f912df.

📒 Files selected for processing (5)
  • src/commands.rs (4 hunks)
  • src/darwin.rs (3 hunks)
  • src/home.rs (3 hunks)
  • src/nixos.rs (6 hunks)
  • src/remote.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/home.rs (1)
src/remote.rs (2)
  • parse (78-118)
  • build_remote (522-570)
src/nixos.rs (1)
src/remote.rs (3)
  • check_ssh_reachability (143-173)
  • parse (78-118)
  • build_remote (522-570)
src/darwin.rs (1)
src/remote.rs (2)
  • parse (78-118)
  • build_remote (522-570)
src/remote.rs (1)
src/util.rs (1)
  • get_nix_variant (56-85)
⏰ 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: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
🔇 Additional comments (9)
src/commands.rs (2)

690-733: LGTM! Correct handling of pipeline exit status.

Using popen() to access individual processes and checking the first process's (nix build) exit status is the right approach. This ensures build failures are properly detected even when nom always succeeds.


1377-1442: LGTM! Comprehensive test coverage for shell parsing.

The new tests thoroughly cover edge cases including escaped quotes, backslash escapes, nix store paths, environment variables in quotes, unclosed quotes, and complex sudo commands. Good coverage of the shlex delegation.

src/darwin.rs (1)

141-151: LGTM! Clean conditional branching between remote and local builds.

The remote/local build branching is well-structured. The local build path correctly preserves the existing behavior with --out-link, extra_args, passthrough, and nom support.

src/home.rs (1)

128-138: LGTM! Local build fallback is correctly preserved.

The existing local build path is properly maintained as a fallback when no build_host is specified.

src/nixos.rs (1)

349-421: LGTM! Well-structured remote build implementation.

The execute_build method properly implements the remote build workflow:

  • Validates both build_host and target_host specifications
  • Performs SSH reachability checks before expensive evaluation
  • Constructs RemoteBuildConfig with all necessary fields
  • Falls back to local build when no build_host is specified

The early SSH connectivity check provides good user feedback before starting the evaluation.

src/remote.rs (4)

191-197: LGTM! Shell quoting uses shlex with appropriate fallback.

The implementation correctly uses shlex::try_quote with a sensible fallback for edge cases (NUL bytes). The fallback uses proper single-quote escaping.


704-722: Re-running nix build to get output path is a reasonable workaround.

Since nom consumes the build output, re-querying with --print-out-paths is a pragmatic solution. The second invocation is effectively a no-op since the derivation is already built.


522-569: LGTM! Well-structured remote build workflow.

The build_remote function correctly implements the four-step workflow:

  1. Evaluate drvPath locally
  2. Copy derivation to build host
  3. Build on remote
  4. Copy result to destination (target_host or localhost)

The logic correctly handles the case where both out_link is requested and target_host is set, ensuring the closure is available locally for specialisation resolution.


725-1036: LGTM! Comprehensive test coverage.

The test suite thoroughly covers:

  • Host parsing (bare, user@host, URI schemes, error cases)
  • Shell quoting (simple strings, special characters, roundtrip verification)
  • SSH options (default, with NIX_SSHOPTS, quoted values)
  • Control directory creation

The roundtrip tests at lines 894-926 are particularly valuable for ensuring shell quoting correctness.

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: 1

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

540-610: LGTM: Correct remote build orchestration with comprehensive transfer logic.

The workflow correctly implements the nixos-rebuild-ng pattern (evaluate locally → copy derivation → build remotely → transfer results). The conditional logic for copying to localhost (lines 586-598) handles all edge cases correctly, though it's complex.

Optional: Extract helper for copy-to-localhost decision

The conditional at lines 586-598 is correct but complex. Consider extracting to a helper:

fn should_copy_to_localhost(
  out_link: Option<&std::path::Path>,
  target_host: Option<&RemoteHost>,
  build_host: &RemoteHost,
) -> bool {
  out_link.is_some()
    || target_host.is_none()
    || target_host
      .as_ref()
      .is_some_and(|th| th.hostname() != build_host.hostname())
}

Then use: if should_copy_to_localhost(out_link, config.target_host.as_ref(), build_host) { ... }

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4438ec5 and a440326.

📒 Files selected for processing (1)
  • src/remote.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/remote.rs (1)
src/util.rs (1)
  • get_nix_variant (56-85)
⏰ 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: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Darwin
  • GitHub Check: treewide-checks
🔇 Additional comments (11)
src/remote.rs (11)

1-49: LGTM: Robust SSH control directory initialization.

The fallback chain (XDG_RUNTIME_DIR → /tmp/nh-ssh- → /tmp) ensures graceful degradation, and the process-specific suffix prevents socket conflicts between concurrent invocations.


51-139: LGTM: Comprehensive host parsing with helpful validation.

The parsing handles multiple URI formats correctly, strips schemes appropriately, and provides clear error messages that guide users toward proper configuration (e.g., directing port specification to NIX_SSHOPTS).


141-181: LGTM: Effective pre-flight connectivity check.

The 10-second timeout and BatchMode ensure this check fails fast on unreachable hosts without blocking on interactive prompts. The detailed error message helps users diagnose connection issues.


207-243: LGTM: Proper SSH options integration.

The distinction between get_ssh_opts (parsed with shlex for direct SSH invocation) and get_nix_sshopts_env (whitespace-joined for nix-copy-closure) correctly handles the different parsing behaviors of these tools. The comment on Line 227-230 appropriately documents the nix-copy-closure limitation.


245-310: LGTM: Correct flake flag handling and remote execution.

The Nix variant detection properly accounts for Determinate Nix's stable feature set. The remote command execution correctly quotes arguments for shell interpretation on the remote host.


312-418: LGTM: Comprehensive closure copying with correct tool selection.

The implementation appropriately uses nix-copy-closure for localhost ↔ remote transfers and nix copy for remote-to-remote transfers. Substitute flags are correctly mapped to each tool's syntax.


420-495: LGTM: Correct derivation path evaluation.

The installable type handling properly appends .drvPath for Flake, File, and Expression types, while correctly rejecting Store paths (which are already built) with a clear error message.


497-538: LGTM: Well-structured configuration and pre-flight checks.

The RemoteBuildConfig structure clearly encapsulates all remote build parameters, and check_remote_connectivity provides valuable early feedback before initiating expensive operations.


612-671: LGTM with improved UTF-8 error handling.

The build execution correctly differentiates between simple and nom-based paths. The UTF-8 conversion (lines 645-656) now fails explicitly rather than silently dropping non-UTF-8 arguments, which is a clear improvement over the previous version.

The error message at line 651 could be slightly more helpful by guiding users:

-        .ok_or_else(|| eyre!("Extra argument is not valid UTF-8: {:?}", s))
+        .ok_or_else(|| eyre!("Extra argument is not valid UTF-8: {:?}. Remote builds require UTF-8 arguments.", s))

774-1125: LGTM: Comprehensive test coverage with property-based testing.

The test suite combines property-based tests (for hostname extraction invariants), unit tests (for parsing and options), and roundtrip tests (for shell quoting). The use of #[serial] correctly prevents test interference when manipulating environment variables.


183-205: Use shlex::try_quote for shell quoting, fallback is insufficient.

The code correctly uses shlex::try_quote, but the error handling should be reconsidered. The only error that can be returned is QuoteError::Nul, which occurs when the input contains nul bytes. The fallback using basic quote escaping doesn't address nul bytes and will still produce unsafe output if they occur. Since SSH arguments rarely contain nul bytes, either handle the error explicitly (e.g., reject nul-containing inputs) or document why the fallback is acceptable for this context.

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: 1

♻️ Duplicate comments (1)
src/remote.rs (1)

636-642: Add nom availability check before using nix-output-monitor.

When use_nom is enabled, the code directly calls build_on_remote_with_nom without verifying that nom is installed. If nom is missing, the error "Remote build with nom failed" masks the actual cause (tool not found).

The codebase already uses the which crate for tool availability checks (see src/commands.rs:16). Apply the same pattern here to provide clear diagnostics.

Suggested fix:

Add a pre-flight check before the conditional at line 636:

fn build_on_remote(
  host: &RemoteHost,
  drv_path: &str,
  config: &RemoteBuildConfig,
) -> Result<String> {
  let drv_with_outputs = format!("{drv_path}^*");

  if config.use_nom {
+   // Check that nom is available before attempting to use it
+   which::which("nom")
+     .wrap_err("nom (nix-output-monitor) is required but not found in PATH")?;
    // With nom: pipe through nix-output-monitor
    build_on_remote_with_nom(host, &drv_with_outputs, config)
  } else {
    // Without nom: simple remote execution
    build_on_remote_simple(host, &drv_with_outputs, config)
  }
}

Also add the import: use which::which; at the top of src/remote.rs.

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

614-621: Consider logging when out-link removal fails.

Line 618 silently ignores errors when removing an existing out-link with let _ = std::fs::remove_file(link). While this is often acceptable (the path might not exist, or might be a dangling symlink), it could mask permission issues or cases where the path is a directory.

💡 Optional improvement

Consider adding a debug log when removal fails for non-ENOENT errors:

   // Create local out-link if requested
   if let Some(link) = out_link {
     debug!("Creating out-link: {} -> {}", link.display(), out_path);
     // Remove existing symlink/file if present
-    let _ = std::fs::remove_file(link);
+    if let Err(e) = std::fs::remove_file(link) {
+      if e.kind() != std::io::ErrorKind::NotFound {
+        debug!("Failed to remove existing out-link: {}", e);
+      }
+    }
     std::os::unix::fs::symlink(&out_path, link)
       .wrap_err("Failed to create out-link")?;
   }

This provides diagnostics without changing behavior for the common case (link doesn't exist).

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a440326 and 71ff533.

📒 Files selected for processing (1)
  • src/remote.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/remote.rs (1)
src/util.rs (1)
  • get_nix_variant (56-85)
⏰ 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: treewide-checks
  • 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: treewide-checks
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin

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

♻️ Duplicate comments (2)
src/home.rs (1)

106-123: Extract duplicated RemoteBuildConfig construction logic.

The extra_args chaining pattern (lines 111-123) is duplicated identically in src/darwin.rs (lines 124-136) and src/nixos.rs (lines 383-395). Consider extracting this to a helper function or builder pattern in the remote module as suggested in the darwin.rs review.

src/nixos.rs (1)

378-396: Extract duplicated RemoteBuildConfig construction logic.

The extra_args chaining pattern (lines 383-395) is duplicated identically in src/darwin.rs (lines 124-136) and src/home.rs (lines 111-123). Consider extracting this to a helper function or builder pattern in the remote module as suggested in the darwin.rs review.

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

119-136: Extract duplicated RemoteBuildConfig construction logic.

The extra_args chaining pattern (lines 124-136) is duplicated identically in src/home.rs (lines 111-123) and src/nixos.rs (lines 383-395). This code constructs a RemoteBuildConfig by merging extra_args and passthrough-derived arguments.

🔎 Proposed refactor

Consider extracting this logic to a helper function in the remote module or a builder pattern:

// In src/remote.rs
impl RemoteBuildConfig {
  pub fn new(
    build_host: RemoteHost,
    target_host: Option<RemoteHost>,
    use_nom: bool,
    use_substitutes: bool,
  ) -> Self {
    Self {
      build_host,
      target_host,
      use_nom,
      use_substitutes,
      extra_args: Vec::new(),
    }
  }

  pub fn with_extra_args<I>(mut self, args: I) -> Self
  where
    I: IntoIterator,
    I::Item: Into<OsString>,
  {
    self.extra_args.extend(args.into_iter().map(Into::into));
    self
  }
}

Then in darwin.rs, home.rs, and nixos.rs:

let config = RemoteBuildConfig::new(
  build_host,
  None, // or target_host for nixos
  !self.common.no_nom,
  self.common.passthrough.use_substitutes,
)
.with_extra_args(&self.extra_args)
.with_extra_args(self.common.passthrough.generate_passthrough_args());
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71ff533 and e3cafc3.

📒 Files selected for processing (4)
  • src/darwin.rs
  • src/home.rs
  • src/nixos.rs
  • src/remote.rs
🧰 Additional context used
🧬 Code graph analysis (4)
src/darwin.rs (1)
src/remote.rs (5)
  • parse (174-214)
  • config (682-690)
  • config (731-739)
  • init_ssh_control (95-98)
  • build_remote (576-647)
src/home.rs (1)
src/remote.rs (5)
  • parse (174-214)
  • config (682-690)
  • config (731-739)
  • init_ssh_control (95-98)
  • build_remote (576-647)
src/nixos.rs (1)
src/remote.rs (5)
  • parse (174-214)
  • config (682-690)
  • config (731-739)
  • init_ssh_control (95-98)
  • build_remote (576-647)
src/remote.rs (1)
src/util.rs (1)
  • get_nix_variant (56-85)
⏰ 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 Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Darwin
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: treewide-checks
🔇 Additional comments (6)
src/nixos.rs (2)

188-207: LGTM: Conditional copy prevents errors with remote builds.

The guard if out_path.exists() (line 191) appropriately prevents attempting to copy to the target host when the output is still remote (hasn't been materialized locally yet). This optimization avoids spurious copy operations and potential errors in the remote build flow.


440-456: LGTM: Conditional existence check handles remote builds correctly.

The conditional check if out_path.exists() (line 442) appropriately skips local existence verification when the output is on a remote host. This prevents false negatives in the remote build flow and includes helpful debug logging.

src/remote.rs (4)

12-89: LGTM: SSH control cleanup properly implemented with RAII.

The SshControlGuard and cleanup logic correctly use the RAII pattern to ensure SSH ControlMaster sockets are cleaned up when remote operations complete. The error handling with debug logging is appropriate, and the use of ssh -O exit matches standard SSH ControlMaster cleanup practices.


174-214: LGTM: RemoteHost parsing includes comprehensive validation.

The parse method provides thorough validation with clear, actionable error messages. The checks for empty usernames, invalid characters, and helpful guidance (e.g., suggesting NIX_SSHOPTS for port configuration) ensure users receive good diagnostics for malformed host specifications.


245-251: LGTM: Shell quoting now uses the shlex crate.

The shell_quote function correctly uses shlex::try_quote with an appropriate fallback for edge cases. This addresses the previous FIXME and uses a well-tested library for shell quoting.


576-646: LGTM: build_remote orchestration includes smart copy optimizations.

The build_remote function implements sophisticated copy-path optimizations to avoid unnecessary transfers (lines 596-635). The hostname comparison logic correctly identifies when build_host and target_host are the same machine, and the three-way conditional logic (copy to target, copy to local, or skip) appropriately minimizes data movement while ensuring the closure is available where needed.

@NotAShelf NotAShelf force-pushed the notashelf/push-xwtloylwummt branch from b274b50 to 2e15d6a Compare December 29, 2025 09:05
@NotAShelf NotAShelf mentioned this pull request Dec 29, 2025
13 tasks
Fixes #428

This is a large architectural change to NH, which lead to me extracting
the remote build logic to its own file so that we may implement it for
Darwin and Home-Manager as well. The `--builders` flag was dropped from
`nh::commands`, and it was replaced with the new and shiny logic that 
hopefully avoids previous pitfalls.

The new `nh::remote` module handles remote builds, including:

- Parsing remote host specifications.
- Copying derivations to remote hosts using `nix-copy-closure`.
- Building derivations on remote hosts via `nix build`.
- Copying results back to localhost or directly to a target host.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I236eb1e35dd645f2169462d207bc82e76a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I946d8e54261e9136c83f6dfe38b046106a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I0b82e84223c3df61cfa23464bd3d4bcc6a6a6964
Fixes a minor issue in how commands that are invalid or improperly
handled are forwarded to the Nix command. Replaces `join()` with
`popen()` to access individual processes in the pipeline. This way we
can better check the exist status of the `nix build` process and
properly propagate them.

Also improves naming a little bit because why not?

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I8a44abf924f9c9a1c06d102e5a3f40aa6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: Ia8506cad3352243001a281e99b8162c26a6a6964
Tiny improvement to how remote connections are made. We now check BEFORE
the connection is made, so that we can avoid all that expensive eval if
it's not reachable. This is not infallible, but it is better. To fix
some target-host quirks, we also have to deal with local symlinks so we
enforce it locally either way.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I65fd7258828459ea82fe6739383567556a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I2366fac6ca7a72fc73eecfc0b07bd2d76a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: If5d2072431348a8468150abf15a7a2a06a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I9eb3904e832e58e0f4ac306d537f7dee6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: Idf201a23f71795e0caea9813280084036a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I73a94927d1270b4a499bb22b8220a1326a6a6964
Remove the redundant and poor connectivity checks that added overhead
without any tangible benefit, and implement SSH ControlMaster cleanup on
program exit. This reduces the number of SSH connections made during
remote operations and makes sure SSH control processes are properly
terminated

Signed-off-by: NotAShelf <[email protected]>
Change-Id: Ideb1825cb7e8302316d7d25b64e7859b6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: Ia782562b87e2614a390d8f435114142b6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I296043d85fd74fc68013dc9f1f3761ea6a6a6964
Here's to you, Dami.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I49e84a3efab65791800348c92b1fc5da6a6a6964
I don't like NixOS' remote builds.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: Iea646b3b47926536a1bb1a70e3d776fa6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: Iffe2d63b55ee4a9bab41bb6184184add6a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-xwtloylwummt branch from b573386 to ce1abee Compare January 4, 2026 21:11
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)
CHANGELOG.md (1)

61-62: Remove duplicate content.

Lines 61-62 duplicate the information already provided on lines 47-48 about nh os info hiding empty fields by default. One of these should be removed to avoid redundancy.

♻️ Duplicate comments (1)
CHANGELOG.md (1)

39-39: Grammar: sentence missing subject.

This issue was already flagged in previous reviews. The sentence "Can also be set via the NH_NO_VALIDATE environment variable." is missing a subject.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b573386 and d637bda.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/commands.rs
  • src/interface.rs
  • src/main.rs
  • src/nixos.rs
  • src/remote.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/interface.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/main.rs (1)
src/commands.rs (3)
  • args (310-319)
  • from_str (129-135)
  • arg (303-306)
src/nixos.rs (2)
src/remote.rs (5)
  • parse (386-451)
  • copy_to_remote (993-1023)
  • build_remote (1494-1594)
  • essential_files (834-846)
  • validate_closure_remote (827-924)
src/util.rs (1)
  • ensure_ssh_key_login (201-219)
src/remote.rs (2)
src/commands.rs (2)
  • cache_password (64-73)
  • get_cached_password (40-46)
src/util.rs (1)
  • get_nix_variant (56-85)
🪛 LanguageTool
CHANGELOG.md

[style] ~39-~39: To form a complete sentence, be sure to include a subject.
Context: ...ctivation system validation checks. Can also be set via the NH_NO_VALIDATE en...

(MISSING_IT_THERE)

⏰ 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). (5)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
🔇 Additional comments (9)
src/main.rs (1)

33-48: Good backward compatibility implementation.

The fallback to NH_ELEVATION_PROGRAM with a deprecation warning provides a smooth migration path. The use of .ok() on line 46 is safe since ElevationStrategyArg::from_str returns Infallible (line 127 in src/commands.rs).

src/nixos.rs (1)

229-243: Verify remote build detection logic.

Line 229 sets is_remote_build = self.rebuild.target_host.is_some(), but a remote build can also be triggered when only build_host is set (without target_host). This may lead to incorrect path resolution logic.

Consider checking both:

let is_remote_build = self.rebuild.build_host.is_some() || self.rebuild.target_host.is_some();

Or if the intent is to detect remote activation specifically, the current logic may be correct but the variable name is misleading.

src/commands.rs (2)

630-632: Good security improvement: empty password validation.

Adding validation to reject empty passwords prevents invalid credential caching and potential authentication bypasses. This is especially important for remote operations where cached passwords are reused.


837-881: Proper handling of nom pipeline exit status.

The code correctly uses popen() to access individual process exit codes and checks the first process (nix build) rather than the last (nom). This ensures build failures are properly detected even when nom always exits successfully.

src/remote.rs (5)

157-174: Safe interrupt handler registration with idempotency.

The use of OnceLock to ensure single registration and signal_hook::flag::register (which is async-signal-safe) is the correct approach. The benign race condition comment on lines 169-170 correctly notes that duplicate registration is harmless since both handlers would set the same atomic flag.


630-683: Remote cleanup implementation with appropriate safeguards.

The opt-in nature (NH_REMOTE_CLEANUP), 5-second timeout, and graceful error handling (lines 632-682) make this fragile operation safe to attempt. The stderr parsing for common error patterns (lines 661-679) provides good diagnostics while avoiding false negatives.


827-924: Efficient batched validation with good error messages.

The batched SSH checks (lines 834-861) using test -e joined with && minimize SSH round-trips. When validation fails, individual checks (lines 872-894) identify exactly which files are missing. The context-aware error messages (lines 900-920) provide actionable guidance.


1678-1696: Proper interrupt handling during remote execution.

The polling loop with 100ms intervals (lines 1678-1696) allows timely interrupt detection while waiting for SSH completion. Killing the local SSH process (line 1686) and attempting remote cleanup (line 1690) before bailing provides good cleanup behavior.


1783-1823: Careful interrupt handling in nom pipeline.

The nested polling loop (lines 1791-1822) checks the interrupt flag before waiting on each process, ensuring responsive Ctrl+C handling. The cleanup sequence (kill all processes, wait to reap zombies, attempt remote cleanup) on lines 1797-1803 prevents resource leaks.

This is a massive change, and it primarily concerns
`--elevation-*program*` not making any sense for remote operations. The
change to *strategy* allows the flag to be more descriptive in terms of
how it operates, and allows us to add new strategies to handle.

Fixes #434

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I9e51838007feca7d2d914402d7f11dbc6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: I8837714ede1884ef22c9c0f0c10016746a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-xwtloylwummt branch from d637bda to 1c44bc4 Compare January 4, 2026 21:42
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

♻️ Duplicate comments (1)
CHANGELOG.md (1)

21-49: Tighten grammar and remove duplicated nh os info entry.

Two minor doc nits here:

  • Line 39 is a sentence fragment: “Can also be set via the NH_NO_VALIDATE environment variable.” It should have an explicit subject, e.g. “This flag can also be set via the NH_NO_VALIDATE environment variable.”
  • The bullets at 45–48 already describe that nh os info hides empty columns by default. Lines 61–62 later repeat the same idea (“now hides empty fields by default…”), so that second mention can be dropped to avoid duplication.

Suggested edits:

- A new `--no-validate` flag skips pre-activation system validation checks.
- This flag can also be set via the `NH_NO_VALIDATE` environment variable.
…
- `nh os info` now supports `--fields` to select which field(s) to display
  ([#375](...)).
  - Empty columns are now hidden by default to avoid visual clutter.
  - A new, per-generation "Closure Size" column has been added
…
- `nh os info` now hides empty fields by default, they can be explicitly shown
  via the `--fields` flag.
  [DELETE this duplicated bullet]
🧹 Nitpick comments (3)
src/remote.rs (1)

1596-1865: Robust interrupt handling and optional remote cleanup; consider documenting NH_REMOTE_CLEANUP’s trade-offs.

The build_on_remote_simple / build_on_remote_with_nom paths plus register_interrupt_handler and attempt_remote_cleanup give you:

  • Periodic wait_timeout polling with a shared AtomicBool set by SIGINT, so local SSH processes are killed promptly on Ctrl+C.
  • Optional best-effort remote cleanup via NH_REMOTE_CLEANUP, sending pkill -INT --full '<remote_cmd>' with a short timeout and diagnostic logging.

This is a solid approach for mitigating “stuck waiting for lock” situations on the remote side, without risking hangs in the client. I’d suggest:

  • Clearly documenting in user-facing docs (remote-build guide / manpage) that NH_REMOTE_CLEANUP is opt‑in, what it does, and in which situations it might or might not succeed.
  • Encouraging users who reported lock hangs to try with NH_REMOTE_CLEANUP=true and see if the remote nix process is reliably terminated.

No functional issues spotted here; just a note that better documentation will help users understand the new behavior and knobs.

src/nixos.rs (2)

211-224: Avoid redundant nix copy when using remote builds with --build-host and --target-host.

In activate_rebuilt_config, the block:

if let Some(target_host) = &self.rebuild.target_host {
    if out_path.exists() {
        let target = RemoteHost::parse(target_host)?;
        remote::copy_to_remote(&target, target_profile, self.rebuild.common.passthrough.use_substitutes)?;
    }
}

runs regardless of whether the build was local or remote. For remote builds (self.build_host.is_some()), remote::build_remote already:

  • Copies the result to target_host directly (copy_closure_between_remotes) or
  • Falls back to build_host -> localhost -> target_host.

Running an additional nix copy from localhost to the same target_host is redundant in the success case (it should be a no-op but still costs a round-trip).

Consider restricting this copy to the “local build + remote activation” case, e.g.:

if self.rebuild.build_host.is_none() {
    // local build → remote target: we must push the closure now
    if out_path.exists() {
        let target = RemoteHost::parse(target_host)?;
        remote::copy_to_remote(&target, target_profile, self.rebuild.common.passthrough.use_substitutes)?;
    }
}

This keeps semantics the same while avoiding unnecessary extra copies for remote builds.


1070-1091: has_elevation_status behavior with ElevationStrategy::None is intentional but worth noting.

The updated has_elevation_status:

  • Immediately returns Ok(false) when elevation is ElevationStrategy::None, bypassing both the “don’t run as root” guard and any attempt to escalate.
  • Otherwise, retains the previous behavior: if bypass_root_check is false and the effective UID is root, it errors out; if bypass_root_check is true, it logs a warning and proceeds without elevation.

This means --elevation-strategy none fully opts the user out of both privilege escalation and the “don’t run as root” safety check. That’s consistent with an explicit “no elevation” choice, but you may want to document it clearly in the help text / manpage so users understand that none disables this guard as well.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d637bda and ac0ee87.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • src/commands.rs
  • src/interface.rs
  • src/main.rs
  • src/nixos.rs
  • src/remote.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/interface.rs (1)
src/commands.rs (1)
  • arg (309-312)
src/nixos.rs (1)
src/remote.rs (5)
  • copy_to_remote (993-1023)
  • activate_remote (1151-1163)
  • build_remote (1494-1594)
  • essential_files (834-846)
  • validate_closure_remote (827-924)
src/remote.rs (2)
src/commands.rs (5)
  • cache_password (64-73)
  • get_cached_password (40-46)
  • new (273-284)
  • new (781-788)
  • new (918-927)
src/util.rs (1)
  • get_nix_variant (56-85)
🪛 LanguageTool
CHANGELOG.md

[style] ~39-~39: To form a complete sentence, be sure to include a subject.
Context: ...ctivation system validation checks. Can also be set via the NH_NO_VALIDATE en...

(MISSING_IT_THERE)

⏰ 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). (5)
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Linux
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
🔇 Additional comments (5)
src/remote.rs (1)

1440-1594: Remote build semantics for build_host/target_host look correct; verify they resolve the reported issues.

The RemoteBuildConfig + build_remote flow now:

  • Evaluates the drvPath locally, copies it to build_host, builds on build_host, and computes out_path (<drv>^*).
  • Uses hostname() comparison so when build_host and target_host refer to the same machine, it skips the copy_closure_between_remotes hop and also skips copying the result back to localhost (via need_local_copy = false), which directly addresses the earlier “same host but still copied locally” complaint.
  • Honors use_substitutes consistently across:
    • copy_closure_to / copy_closure_from (nix-copy-closure --use-substitutes),
    • copy_to_remote / copy_closure_between_remotes (nix copy --substitute-on-destination),
    • plus the --use-substitutes flag passed into the remote nix build via extra_args.

Given the subtleties involved, I recommend manually validating in real environments that:

  • --use-substitutes behaves as expected for:
    • local build + remote activation,
    • remote build with --build-host only,
    • remote build with both --build-host and --target-host.
  • When build_host and target_host are identical, the build result is not unnecessarily copied back to the local machine (you should only see SSH traffic for the drv copy + the remote build itself).

If you’d like, I can sketch a small matrix of build_host/target_host/out_link combinations and the expected copy graph to use as a test checklist.

src/interface.rs (2)

54-70: CLI elevation strategy wiring looks consistent with the new model.

The elevation_strategy option is correctly exposed as a global clap flag, wired to NH_ELEVATION_STRATEGY with an elevation-program alias, and typed as ElevationStrategyArg, which main.rs maps into the runtime ElevationStrategy. Aside from the program:<path> parsing detail called out in other comments, this surface looks coherent and compatible with the new elevation API.


249-260: Remote-related rebuild flags (build_host, target_host, no_validate) are well-scoped.

The new target_host, build_host, and no_validate fields on OsRebuildArgs are cleanly introduced:

  • build_host / target_host are documented as SSH-based remote build/activation knobs.
  • no_validate is global for OS rebuilds and bound to NH_NO_VALIDATE, matching the behavior in nixos.rs where pre-activation closure validation can be skipped.

The same build_host flag on HomeRebuildArgs and DarwinRebuildArgs keeps the CLI consistent across subcommands. No structural issues spotted here.

src/nixos.rs (2)

226-283: Closure validation and switch-to-configuration path resolution handle local vs remote correctly.

The new logic around resolved_profile and switch_to_configuration:

  • Uses actual_store_path from execute_build when available (remote builds), avoiding any local canonicalize() on paths that may not exist, which fixes the earlier remote-only canonicalization failure.
  • Falls back to canonicalizing target_profile only for local builds, giving a stable store path for validation and activation.
  • Gates validation on no_validate and dispatches correctly:
    • validate_system_closure_remote for remote activations, passing the store path and both target/build host info for good error messages.
    • validate_system_closure for purely local activations.
  • Resolves switch-to-configuration directly under the resolved store path and only canonicalizes it when we actually expect a local file, skipping canonicalization entirely for remote-only builds where out_path does not exist.

This wiring looks sound and should prevent the remote-activation regressions seen previously, while preserving the stricter local validation path.


931-1013: Local and remote system-closure validation helpers are clear and minimal.

validate_system_closure and validate_system_closure_remote define a small, well-scoped set of essential files and:

  • For local builds, check existence of bin/switch-to-configuration, nixos-version, init, and sw/bin under the resolved system path, returning a detailed, action-oriented error if any are missing.
  • For remote builds, delegate to remote::validate_closure_remote, which batches SSH checks using the shared SSH options and provides context about where the system was built (same host vs different build_host).

The split between “simple local check” and “remote delegation” keeps nixos.rs focused on OS semantics while centralizing SSH details in remote.rs. No issues spotted here.

Copy link
Contributor

@faukah faukah left a comment

Choose a reason for hiding this comment

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

I've been using it a fair bit the last few days, it seems to just work now. Great job on this!

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/commands.rs (1)

520-521: Optional: Simplify map_or pattern

The pattern .map_or(None, |value| Some(...)) can be simplified to .ok().map(|value| ...) for better readability:

-          std::env::var(key)
-            .map_or(None, |value| Some(format!("{key}={value}")))
+          std::env::var(key)
+            .ok()
+            .map(|value| format!("{key}={value}"))

Also applies to: 564-565

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bab129e and f6a35a7.

📒 Files selected for processing (1)
  • src/commands.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/commands.rs (1)
src/checks.rs (1)
  • new (348-357)
⏰ 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). (9)
  • GitHub Check: Build NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Linux
🔇 Additional comments (9)
src/commands.rs (9)

25-73: LGTM: Password cache API improvements

The refactoring to Result-based error handling for poisoned locks is correct, and the comprehensive documentation enhances clarity.


131-148: LGTM: ElevationStrategyArg parsing correctly handles prefix

The FromStr implementation properly strips the program: prefix and provides sensible fallback behavior for raw paths.


180-221: LGTM: Elevation strategy resolution is well-implemented

The resolve() method handles all variants appropriately with clear error messages. The shared resolution logic for Auto and Passwordless is correct since both need to find an elevation program.


411-417: LGTM: macOS HOME handling aligns with Nix

Setting HOME to an empty string for elevated commands on macOS correctly matches Nix's behavior per the referenced source.


477-504: LGTM: Passwordless variant correctly skips askpass

The logic appropriately excludes the askpass mechanism for Passwordless elevation, enabling NOPASSWD sudo configurations as intended.


627-651: LGTM: Password handling improved with empty check

The addition of empty password validation and proper Result-based error propagation enhances robustness. The comment appropriately notes sudo-specific stdin password support.


849-896: LGTM: nom pipeline exit status handling corrected

The refactoring properly checks the nix build exit status rather than nom's, addressing the issue from previous reviews. Using popen() to access individual processes is the correct approach.


1237-1272: LGTM: Comprehensive test coverage for new elevation features

The new tests effectively cover Passwordless resolution, program: prefix parsing, and sudo command construction behavior.


1578-1644: LGTM: Thorough shlex parsing test coverage

The extensive test suite covers edge cases including escaped quotes, nested quotes, backslashes, environment variables, and error conditions. Using shlex is a solid choice over custom parsing.

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)
package.nix (1)

82-96: The Darwin test exclusion logic is sound; consider consolidating skip flags using substring matching.

The conditional logic correctly excludes sudo from Darwin builds and skips the relevant tests. The eight individual --skip entries could be consolidated since Cargo's --skip flag performs substring matching.

Consolidate skip flags for better maintainability

All skipped tests share the substring test_build_sudo_cmd, so you can replace the eight individual skip flags with a single one:

-  checkFlags = lib.optionals stdenv.hostPlatform.isDarwin [
-    # Tests that require sudo in PATH (not available on Darwin)
-    "--skip=commands::tests::test_build_sudo_cmd_basic"
-    "--skip=commands::tests::test_build_sudo_cmd_with_preserve_vars"
-    "--skip=commands::tests::test_build_sudo_cmd_with_preserve_vars_disabled"
-    "--skip=commands::tests::test_build_sudo_cmd_with_set_vars"
-    "--skip=commands::tests::test_build_sudo_cmd_passwordless_no_stdin"
-    "--skip=commands::tests::test_build_sudo_cmd_with_remove_vars"
-    "--skip=commands::tests::test_build_sudo_cmd_with_askpass"
-    "--skip=commands::tests::test_build_sudo_cmd_env_added_once"
-  ];
+  checkFlags = lib.optionals stdenv.hostPlatform.isDarwin [
+    # Tests that require sudo in PATH (not available on Darwin)
+    "--skip=test_build_sudo_cmd"
+  ];

This approach is more maintainable and will automatically cover any new sudo-related tests that follow the same naming convention.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6a35a7 and fee8fc0.

📒 Files selected for processing (1)
  • package.nix
⏰ 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). (5)
  • 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: treewide-checks

@NotAShelf NotAShelf force-pushed the notashelf/push-xwtloylwummt branch from fee8fc0 to d747315 Compare January 6, 2026 13:23
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: 1

🤖 Fix all issues with AI Agents
In @src/commands.rs:
- Around line 1262-1273: The test named
test_build_sudo_cmd_passwordless_no_stdin is misleading because it constructs
Command::new("test").elevate(Some(ElevationStrategy::Force("sudo"))) but claims
to test Passwordless; update the test to either rename it (e.g.,
test_build_sudo_cmd_force_no_stdin) to reflect that it uses
ElevationStrategy::Force, or change the elevation variant to
ElevationStrategy::Passwordless("sudo") so the test actually validates the
Passwordless behavior; update any inline comment to match the chosen change and
keep the assertion using cmd.build_sudo_cmd() and sudo_exec.to_cmdline_lossy()
as-is.
🧹 Nitpick comments (2)
package.nix (1)

84-97: Platform-specific test handling looks correct, but note the FIXME.

The conditional inclusion of sudo in nativeCheckInputs for non-Darwin platforms and the corresponding test skips for Darwin are appropriate given sudo's unavailability on that platform.

However, the FIXME comment on line 86 highlights a maintainability concern: the -- separator in checkFlags is nextest-specific, but useNextest is currently hardcoded to true (line 102). If someone later makes useNextest configurable or changes its default, they must remember to make checkFlags conditional as well, or tests will fail on Darwin.

Optional: Make the nextest separator conditional

If you'd like to make the code more maintainable, consider making the -- separator conditional:

-    checkFlags = lib.optionals stdenv.hostPlatform.isDarwin [
-      "--" # required for nextest. FIXME: make optional based on useNextest
+    checkFlags = 
+      lib.optionals stdenv.hostPlatform.isDarwin (
+        lib.optional finalAttrs.useNextest "--"
+        ++ [
+          # Tests that require sudo in PATH (not available on Darwin)
+          "--skip=commands::tests::test_build_sudo_cmd_basic"
+          # ... rest of skips
+        ]
+      );

This would eliminate the coupling between useNextest and checkFlags.

src/commands.rs (1)

564-566: Consider simplifying the environment variable mapping.

The pattern .map_or(None, |value| Some(...)) is equivalent to .ok().map(...) and could be simplified for clarity:

🔎 Proposed simplification
         EnvAction::Preserve if preserve_env => {
-          std::env::var(key)
-            .map_or(None, |value| Some(format!("{key}={value}")))
+          std::env::var(key)
+            .ok()
+            .map(|value| format!("{key}={value}"))
         },
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fee8fc0 and d747315.

📒 Files selected for processing (2)
  • package.nix
  • src/commands.rs
🔇 Additional comments (12)
package.nix (2)

8-8: LGTM: sudo parameter addition

The sudo parameter is properly declared and correctly wired into the derivation's test infrastructure.


13-80: Formatting improvements enhance readability.

The formatting adjustments throughout the file (indentation, alignment, comment clarity) improve maintainability without changing semantics.

Also applies to: 98-118

src/commands.rs (10)

40-73: LGTM - password caching with proper error handling.

The Result-based API correctly handles lock poisoning and uses SecretString for secure memory handling. The lifetime behavior (passwords persist for program duration) is clearly documented.


131-148: LGTM - FromStr implementation correctly handles program: prefix.

The parsing logic properly strips the program: prefix and falls back to treating unknown strings as program paths. This matches the documented behavior and addresses the previous review feedback.


199-221: LGTM - elevation strategy resolution logic is clear.

The resolution logic appropriately handles each variant with helpful error messages. The Passwordless variant correctly resolves to an elevation program (identical to Auto) since the difference is in password handling during execution, not program selection.


370-450: LGTM - comprehensive environment variable handling.

The method properly handles platform-specific requirements (macOS elevated HOME), distinguishes between elevated and non-elevated contexts, and preserves necessary Nix and NH environment variables. The debug logging aids troubleshooting.


477-531: LGTM - elevation command construction handles Passwordless variant correctly.

The method correctly skips askpass configuration for the Passwordless variant (line 499), which is appropriate for NOPASSWD sudoers configurations. Environment handling respects the NH_PRESERVE_ENV setting.


627-750: LGTM - password prompting and caching logic is sound.

The method correctly:

  • Checks for cached passwords before prompting
  • Validates that passwords are non-empty (preventing silent failures)
  • Handles Result returns from password cache operations
  • Distinguishes between local and remote elevation contexts

850-894: LGTM - nom pipeline correctly checks Nix's exit status.

The reworked implementation properly uses popen() to access individual process handles and checks the first process (nix build) exit status rather than the last (nom). This ensures build failures are detected even when nom succeeds. The comments clearly explain the reasoning.


1579-1645: LGTM - comprehensive test coverage for shlex integration.

The new tests thoroughly validate the switch from custom parsing to the shlex library, covering edge cases including escaped quotes, nested quotes, backslash escapes, unclosed quotes, and realistic command scenarios. This provides confidence in the parsing behavior.


1163-1174: LGTM - platform-agnostic test assertion.

The updated test correctly handles platform differences where sudo might be a full path or just the program name. The comment clearly explains the reasoning for the flexible assertion.


906-911: LGTM - appropriate lint exceptions for test code.

The clippy allow attributes are properly scoped to the test module with clear reasoning. Using expect_used, unwrap_used, and unreachable in tests is standard practice.

The program prefix was advertised as valid, but it actually wasn't
because I regressed it during a different refactor. Strip the "program:"
prefix when present to correctly parse elevation paths, and add a
regression test so that I don't mess it up again.

The deprecation warning advertised "program:<path>" as a valid value but the
parser treated it as a literal path. Strip the "program:" prefix when present
to correctly parse elevation program paths. Add unit test for this case.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: I33de2df17654f95a656a4ee649cf7c9b6a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-xwtloylwummt branch from d747315 to 393d808 Compare January 6, 2026 13:43
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/commands.rs (1)

850-897: Verify empty pipeline handling.

The nom pipeline implementation correctly checks the first process's exit status rather than nom's. However, consider what happens if processes.first() returns None (empty pipeline):

if let Some(nix_proc) = processes.first() {
  // checks exit status
}
// Falls through without error if processes is empty

If the pipeline is empty, the function returns Ok(()) without building anything. While this is unlikely in practice, you may want to add an explicit check.

🔎 Suggested defensive check
 let mut processes = pipeline.popen()?;

+if processes.is_empty() {
+  bail!("Empty pipeline - no processes to execute");
+}
+
 // Wait for all processes to finish
 for proc in &mut processes {
   proc.wait()?;
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d747315 and 393d808.

📒 Files selected for processing (2)
  • package.nix
  • src/commands.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.nix
⏰ 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: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: treewide-checks
🔇 Additional comments (9)
src/commands.rs (9)

25-73: LGTM!

The password cache functions now properly return Result types to handle potential lock poisoning. The error handling is clear and the documentation is comprehensive.


109-148: LGTM!

The ElevationStrategyArg enum and its FromStr implementation correctly handle the program: prefix syntax as addressed in previous review feedback. The fallback to treating bare strings as program paths provides good backward compatibility.


150-221: LGTM!

The ElevationStrategy enum updates and resolve() method are well-structured. The Passwordless variant correctly resolves to an elevation program (same as Auto) while deferring password-prompting logic to the command building phase.


478-531: LGTM!

The build_sudo_cmd method correctly handles the Passwordless elevation variant by skipping the askpass configuration (line 499). This properly supports remote hosts with NOPASSWD sudo configurations as intended by the PR objectives.


628-652: Good addition of empty password validation.

The password prompting logic correctly handles the new Result-returning cache functions. The empty password check at lines 643-645 is a good defensive measure that prevents caching invalid passwords.


1238-1272: LGTM!

The new tests provide good coverage for:

  • Passwordless variant resolution (lines 1238-1247)
  • program: prefix parsing (lines 1250-1259)
  • Force elevation behavior (lines 1262-1272)

These tests validate the key functionality added in this PR.


1429-1644: Excellent shlex test coverage.

The cmdline parsing tests have been properly updated to use shlex::split and comprehensive edge cases have been added:

  • Escaped quotes (line 1579)
  • Nested quotes (line 1587)
  • Backslash escapes (line 1594)
  • Nix store paths (line 1601)
  • Environment variables in quotes (line 1615)
  • Unclosed quotes (line 1623)
  • Complex sudo commands (line 1630)

This provides robust validation of the shlex-based parsing behavior.


416-416: Minor style improvement.

Using String::new() instead of "".to_string() is slightly more idiomatic and explicit about creating an empty string.


520-523: Safer env var handling pattern.

The change from direct std::env::var(key) access to using .ok().map() provides better error handling for environment variable lookups. This prevents potential issues if std::env::var returns an error for reasons other than the variable not being set.

Also applies to: 565-567

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/nixos.rs (1)

479-545: Consider extending the SSH control guard's lifetime.

The _ssh_guard on line 525 is dropped when execute_build returns, which means subsequent SSH operations (validation, activation) won't benefit from connection multiplexing. For better performance, especially with high-latency connections, consider:

  1. Creating the guard in rebuild_and_activate or setup_build_context and keeping it alive for the entire operation sequence, or
  2. Returning the guard from execute_build so the caller can manage its lifetime

This would allow validation and activation SSH calls to reuse the same control connection.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 393d808 and 1b7b1cc.

📒 Files selected for processing (1)
  • src/nixos.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: treewide-checks
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
🔇 Additional comments (12)
src/nixos.rs (12)

1-29: LGTM!

The added imports are appropriate for the remote build functionality and the type conversions in execute_build.


36-44: LGTM!

The essential files list is appropriate and aligns with nixos-rebuild-ng's validation approach. The descriptions will be useful for error messages.


142-198: LGTM!

The refactored flow correctly distinguishes between remote and local builds by capturing the actual store path from remote builds via execute_build, and passes it through to activation.


221-277: Path resolution and validation logic is well-structured.

The three-way branching correctly handles:

  1. Remote builds with actual store path captured
  2. Remote builds without local result (defensive fallback)
  3. Local builds with canonicalized symlinks

The validation dispatches appropriately to remote or local checks based on the target host.


279-293: LGTM!

The conditional canonicalization correctly addresses the issue where remote-only builds would fail on canonicalize(). The fallback to uncanonicalized paths for SSH-executed commands is appropriate.


299-348: LGTM!

The activation logic cleanly separates remote and local paths, with proper activation type mapping and comprehensive debug logging.


350-400: LGTM!

Bootloader activation correctly handles both remote and local cases, with proper environment variable handling for NIXOS_INSTALL_BOOTLOADER.


404-439: LGTM!

The refactored setup_build_context correctly returns Result and threads elevation through to has_elevation_status. The SSH key login check is appropriately conditional on remote host usage.


547-588: LGTM!

The conditional existence check correctly handles remote-only builds by skipping local validation when the output path doesn't exist locally.


941-979: LGTM!

The validation function provides comprehensive error messages with actionable troubleshooting steps, and correctly checks all essential files before reporting failures.


981-1008: LGTM!

The remote validation function provides useful context in error messages by indicating whether the target is also the build host. Clean delegation to the remote module maintains good separation of concerns.


1065-1086: LGTM!

The refactored elevation check correctly respects ElevationStrategy::None and maintains the existing root-user guard logic.

@NotAShelf NotAShelf force-pushed the notashelf/push-xwtloylwummt branch from 1b7b1cc to a2cbed9 Compare January 6, 2026 14:32
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: 1

🤖 Fix all issues with AI Agents
In @package.nix:
- Around line 86-104: The checkFlags currently injects repeated "--skip" entries
which work for cargo test but not for nextest; update the checkFlags assignment
used when stdenv.hostPlatform.isDarwin to use nextest filter syntax instead of
"--skip": replace the array of "--skip" + test names with a two-element array
["-E", "not (test(test_build_sudo_cmd_basic) or
test(test_build_sudo_cmd_with_preserve_vars) or
test(test_build_sudo_cmd_with_preserve_vars_disabled) or
test(test_build_sudo_cmd_with_set_vars) or
test(test_build_sudo_cmd_force_no_stdin) or
test(test_build_sudo_cmd_with_remove_vars) or
test(test_build_sudo_cmd_with_askpass) or
test(test_build_sudo_cmd_env_added_once))"] so nextest interprets the excluded
tests correctly; keep the lib.optionals stdenv.hostPlatform.isDarwin wrapper and
ensure this branch is used when useNextest = true.
🧹 Nitpick comments (3)
src/commands.rs (2)

643-646: Reconsider empty password rejection for remote sudo scenarios.

Line 643-646 rejects empty passwords, but some remote systems may have passwordless sudo configured (NOPASSWD in sudoers). In such cases, users should use --elevation-strategy=passwordless instead, but the error message doesn't guide them to this solution.

Consider adding a hint to the error message:

         if password.is_empty() {
-          bail!("Password cannot be empty");
+          bail!(
+            "Password cannot be empty. If the remote host has passwordless \
+             sudo configured, use --elevation-strategy=passwordless instead."
+          );
         }

520-523: Simplify the environment variable preservation pattern.

Lines 520-523 use a verbose pattern that can be simplified for readability:

         EnvAction::Preserve if preserve_env => {
-          std::env::var(key)
-            .ok()
-            .map(|value| format!("{key}={value}"))
+          std::env::var(key).ok().map(|value| format!("{key}={value}"))
         },

The multiline chain adds visual noise without improving clarity.

src/nixos.rs (1)

240-253: Path resolution logic is comprehensive.

The resolved_profile calculation properly handles three cases:

  1. Remote build with captured store path → use store path directly
  2. Remote build without local result and no store path → use target_profile as-is
  3. Local build → canonicalize to get store path

However, case 2 (lines 244-247) should be unreachable if execute_build always returns Some for remote builds. Consider adding an assertion or warning:

     } else if is_remote_build && !out_path.exists() {
-      // Remote build with no local result and no store path captured
-      // (shouldn't happen, but fallback)
+      warn!("Remote build completed but no store path captured; using target_profile as fallback");
       target_profile.to_path_buf()
     } else {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b7b1cc and a2cbed9.

📒 Files selected for processing (3)
  • package.nix
  • src/commands.rs
  • src/nixos.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/nixos.rs (2)
src/remote.rs (6)
  • parse (386-451)
  • copy_to_remote (993-1023)
  • activate_remote (1151-1163)
  • init_ssh_control (259-262)
  • build_remote (1494-1594)
  • validate_closure_remote (827-924)
src/util.rs (1)
  • ensure_ssh_key_login (201-219)
⏰ 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: treewide-checks
  • 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 Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
🔇 Additional comments (13)
src/commands.rs (5)

131-148: LGTM! Proper program: prefix parsing implemented.

The FromStr implementation correctly handles the documented program:<path> syntax and falls back to treating bare strings as program paths. This addresses the previous review feedback.


199-221: LGTM! Elevation strategy resolution handles all variants correctly.

The resolve() method properly handles all ElevationStrategy variants:

  • Auto and Passwordless both resolve to an available elevation program
  • Prefer falls back to auto-detection if the preferred program isn't found
  • Force strictly requires the specified program
  • None appropriately fails with a clear error message

850-897: LGTM! Correct handling of nom pipeline exit status.

The implementation properly checks the exit status of the nix build command (first process) rather than nom (last process), which is critical since nom always succeeds even when nix fails. The comments clearly explain the issue and solution.

Both the nom and non-nom paths correctly propagate exit status through ExitError.


1238-1272: LGTM! Tests cover new elevation strategy functionality.

The new tests properly verify:

  1. Passwordless strategy resolves to an elevation program
  2. program: prefix parsing extracts the path correctly
  3. Force strategy behavior with sudo

These tests address the previous review feedback about test naming and coverage.


1578-1644: LGTM! Comprehensive shlex parsing test coverage.

The new tests thoroughly exercise shlex parsing with realistic edge cases:

  • Escaped and nested quotes
  • Backslash escaping
  • Nix store paths (common in the codebase)
  • Environment variable syntax preservation
  • Unclosed quote error handling
  • Complex sudo command scenarios

This provides good confidence in the shlex integration.

src/nixos.rs (6)

36-44: LGTM! Well-defined essential files for validation.

The ESSENTIAL_FILES constant provides a clear set of critical system files to validate, with helpful descriptions for error messages. This aligns with nixos-rebuild-ng's validation approach.


479-545: LGTM! Remote build integration is well-structured.

The execute_build method properly distinguishes between remote and local builds:

  • Remote builds return the actual store path for later use
  • Local builds return None (store path is resolved via symlink)
  • SSH control guard ensures connection cleanup
  • Error messages provide clear context

The implementation aligns with the PR's goal of implementing proper remote-build semantics.


569-585: LGTM! Proper handling of remote-only build paths.

The updated logic correctly handles remote-only builds by:

  1. Checking if out_path exists locally before validating target_profile
  2. Skipping local existence checks for remote builds
  3. Providing clear debug logging for troubleshooting

This addresses the previous review feedback about canonicalize() failures for remote paths.


954-1008: LGTM! Validation functions provide clear, actionable error messages.

Both validation functions:

  1. Check essential system files systematically
  2. Provide detailed error messages listing missing files
  3. Explain common causes and solutions
  4. Handle local and remote scenarios appropriately

The remote validation delegates to remote::validate_closure_remote, which properly uses SSH connection multiplexing (per the relevant code snippet).


299-391: LGTM! Remote and local activation paths are properly separated.

The activation logic cleanly separates remote and local scenarios:

Remote activation:

  • Delegates to remote::activate_remote with platform, activation type, and elevation
  • Properly handles Test, Switch, and Boot operations
  • Threads elevation strategy through to remote execution

Local activation:

  • Uses existing Command::new pattern with elevation
  • Preserves required environment variables
  • Maintains backward compatibility

The dual paths ensure proper SSH-based remote activation while keeping local activation unchanged.


221-234: No action needed. The copy operation at lines 221-234 is already protected by the if out_path.exists() check, which correctly prevents redundant copying when build_host and target_host are the same. The remote::build_remote function skips copying the result back to localhost in that case, ensuring out_path won't exist locally and thus the copy block won't execute.

package.nix (2)

8-8: LGTM: sudo added as test dependency.

Adding sudo as a derivation input is appropriate for the new sudo-dependent tests introduced in this PR.


82-85: LGTM: Conditional test dependency handling.

The conditional addition of sudo to nativeCheckInputs only on non-Darwin platforms is correct and well-commented. This approach properly handles platform differences in package availability.

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 (4)
src/nixos.rs (4)

71-83: Consider consistent elevation parameter passing.

Lines 71-78 pass elevation by value to rebuild_and_activate, while line 83 passes &elevation by reference to build_only. This inconsistency in the API surface could be confusing for maintainers.

Consider updating rebuild_and_activate to accept &ElevationStrategy for consistency, or document why the difference is necessary.


249-249: Misleading variable name: is_remote_build only checks target host.

Line 249 defines is_remote_build = self.rebuild.target_host.is_some(), but this only checks if there's a target host, not if the build itself was remote. A configuration with build_host set but no target_host would have is_remote_build = false, even though the build happened remotely.

Consider renaming to has_target_host or checking both build_host.is_some() || target_host.is_some() if the intent is to detect any remote operation.


254-257: Verify if fallback case should be an error.

Lines 254-257 handle "Remote build with no local result and no store path captured" as a fallback by using target_profile.to_path_buf(). The comment indicates "shouldn't happen, but fallback."

If this case genuinely shouldn't occur, consider making it an error with a clear message rather than silently falling back. This would help catch bugs earlier during development and testing.


289-307: Potential inconsistency in path canonicalization.

Lines 295-302 conditionally canonicalize switch_to_configuration_path based on whether the output exists locally. However, line 305 assigns this to canonical_out_path and treats it as canonical in subsequent code.

When is_remote_build && !out_path.exists() is true (lines 295-297), the path is NOT canonicalized, creating an inconsistency where canonical_out_path may not actually be canonical.

Consider renaming canonical_out_path to switch_to_configuration or activation_script_path to avoid implying it's always canonical.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2cbed9 and 9b8f6f2.

📒 Files selected for processing (1)
  • src/nixos.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/nixos.rs (1)
src/remote.rs (3)
  • copy_to_remote (993-1023)
  • build_remote (1494-1594)
  • validate_closure_remote (827-924)
⏰ 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: Build NH on Darwin
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Linux
  • GitHub Check: Build NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: Test NH on Darwin
🔇 Additional comments (5)
src/nixos.rs (5)

36-44: LGTM! Well-documented essential files constant.

The ESSENTIAL_FILES constant is clearly documented and will help with consistent validation across local and remote builds.


169-176: Good practice: SSH control guard initialization.

Initializing the SSH control guard early ensures connection multiplexing is available for both build and activation phases, improving performance for remote operations.


489-552: LGTM! Clean separation of remote and local build paths.

The refactored execute_build method clearly separates remote and local build logic. Remote builds properly return the actual store path, while local builds return None, allowing callers to distinguish between the two cases.


948-1015: LGTM! Validation functions are well-implemented.

Both local and remote validation functions provide clear error messages with actionable guidance. The remote validation properly delegates to remote::validate_closure_remote, which handles SSH connection multiplexing and batched checks (as seen in the relevant code snippets).

The context-building logic (lines 999-1006) helps users understand which host failed validation.


1072-1093: LGTM! Elevation status check properly updated.

The signature change to accept &ElevationStrategy is consistent with the broader refactoring. The logic correctly handles the ElevationStrategy::None case before checking root status.

Signed-off-by: NotAShelf <[email protected]>
Change-Id: Id2ae10eae0c6aa66034c5bf725c9447f6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: If55c758a3ef8022ac2a9798be31a0ffa6a6a6964
Signed-off-by: NotAShelf <[email protected]>
Change-Id: Ib42fe0268a9852055e283deeac4606f66a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-xwtloylwummt branch from 9b8f6f2 to ed1e21a Compare January 7, 2026 07:26
@NotAShelf
Copy link
Member Author

Alright, let's ball.

@NotAShelf NotAShelf merged commit d47475d into master Jan 7, 2026
13 of 14 checks passed
@NotAShelf NotAShelf deleted the notashelf/push-xwtloylwummt branch January 7, 2026 07:38
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.

Add --elevation-program=none option for passwordless sudo on remote target-host --build-host option in nh is misleading compared to nixos-rebuild

3 participants