Skip to content

darwin: initial remote deployment support#523

Open
NotAShelf wants to merge 2 commits intomasterfrom
notashelf/push-zpmuorlvnsuz
Open

darwin: initial remote deployment support#523
NotAShelf wants to merge 2 commits intomasterfrom
notashelf/push-zpmuorlvnsuz

Conversation

@NotAShelf
Copy link
Copy Markdown
Member

@NotAShelf NotAShelf commented Jan 7, 2026

Part of #522

This implements an initial, very basic remote activation method for Darwin. The foundation is in place, so it was as simple as designing the remote activation system. The real question remains whether we want to keep calling darwin-rebuild, but this should work as an initial draft.

Change-Id: I0ad5b8e4c75008622fa4fd2bea58f6846a6a6964

Summary by CodeRabbit

  • New Features

    • SSH-based remote build-and-activate workflow allowing build on one host and activation on another.
    • Darwin platform support for remote activation.
  • Enhancements

    • Pre-activation system closure validation to ensure required binaries are present.
    • New CLI options to specify build and target hosts and to skip validation when desired.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 7, 2026

Walkthrough

Adds SSH-based remote build and activation support for Darwin by refactoring darwin flow to a build-and-activate pathway, adding pre-activation validations, host-targeting fields, and remote activation logic in the remote module.

Changes

Cohort / File(s) Summary
SSH Deployment Interface
src/interface.rs
Added build_host: Option<String>, target_host: Option<String>, and no_validate: bool to rebuild/activate argument structs to support SSH build/target selection and optional validation bypass.
Darwin Build & Activation
src/darwin.rs
Reworked control flow to rebuild_and_activate with helpers: setup_build_context, determine_output_path, resolve_installable_and_toplevel, execute_build, activate_rebuilt_config, handle_dix_diff, validate_system_closure/_remote, toplevel_for tweaks, get_nh_darwin_flake_env, has_elevation_status, and ESSENTIAL_FILES validation. Introduces local/remote build and activation paths and extra logging/error contexts.
Remote Activation Platform
src/remote.rs
Added Platform::Darwin and dispatched Darwin to a new private activate_darwin_remote implementing SSH-based remote darwin-rebuild activation, elevation handling, and log/exit handling analogous to NixOS remote activation.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI/Interface
    participant Darwin as darwin.rs
    participant Remote as remote.rs
    participant SSH as SSH Layer
    participant Host as Remote Host
    participant Rebuild as darwin-rebuild

    CLI->>Darwin: rebuild_and_activate(args)
    Darwin->>Darwin: setup_build_context()
    alt build on remote host
        Darwin->>Remote: execute_build (remote)
        Remote->>SSH: connect to build_host
        SSH->>Host: transfer build inputs / run nix build
        Host->>Remote: return store path
    else build locally
        Darwin->>Rebuild: nix build locally
        Rebuild-->>Darwin: local store path
    end
    Darwin->>Darwin: validate_system_closure(local/remote)
    Darwin->>Remote: if target_host -> activate_rebuilt_config triggers activate_darwin_remote
    Remote->>SSH: connect to target_host
    SSH->>Host: copy profile, run darwin-rebuild activate (with elevation if needed)
    Host-->>Remote: activation result
    Remote-->>Darwin: final status
    Darwin-->>CLI: report result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • faukah
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'darwin: initial remote deployment support' directly describes the main changes in the pull request, which adds remote activation support for Darwin platform and establishes the foundation for remote deployment.
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

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.

@NotAShelf NotAShelf force-pushed the notashelf/push-zpmuorlvnsuz branch 2 times, most recently from abbf3be to 6668337 Compare January 7, 2026 09:24
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I0ad5b8e4c75008622fa4fd2bea58f6846a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-zpmuorlvnsuz branch from 6668337 to 5ab78b5 Compare January 9, 2026 05:03
@NotAShelf NotAShelf marked this pull request as ready for review January 9, 2026 10:30
Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In @src/interface.rs:
- Around line 270-278: OsRebuildActivateArgs duplicates CLI fields already
defined in OsRebuildArgs (build_host and target_host) which causes clap to
panic; remove the duplicate build_host and target_host fields from
OsRebuildActivateArgs (the struct that has #[command(flatten)] for
OsRebuildArgs) and update any usage to read the values via the embedded field
(e.g., self.rebuild.build_host and self.rebuild.target_host) so the flattened
OsRebuildArgs provides the arguments.

In @src/remote.rs:
- Around line 1397-1401: The constructed command in variable base_cmd is using
an invalid `darwin-rebuild profile=<path> activate` syntax; change the remote
Darwin activation to match the local pattern in src/darwin.rs by splitting into
two commands: first run the nix build step that sets the profile (use the same
approach as the local implementation, e.g., use `nix build`/`--profile`
invocation with rebuild_path_str and system_profile), then run `darwin-rebuild
activate` as a separate command; update the code that builds base_cmd (and any
callers relying on it) to produce these two steps using the existing variables
rebuild_path_str and system_profile instead of injecting `profile=` into
darwin-rebuild.
🧹 Nitpick comments (1)
src/remote.rs (1)

1356-1382: Consider extracting password handling to reduce duplication.

This password handling block is nearly identical to the one in activate_nixos_remote (lines 1179-1207). Extracting this into a shared helper function would improve maintainability.

♻️ Suggested helper function
/// Prompts for and caches sudo password if elevation is required.
/// Returns None if no password is needed (None/Passwordless strategies).
fn get_elevation_password(
    host: &RemoteHost,
    elevation: Option<&ElevationStrategy>,
) -> Result<Option<SecretString>> {
    let Some(strategy) = elevation else {
        return Ok(None);
    };
    
    if matches!(strategy, ElevationStrategy::None | ElevationStrategy::Passwordless) {
        return Ok(None);
    }
    
    let host_str = host.ssh_host();
    if let Some(cached) = get_cached_password(&host_str)? {
        return Ok(Some(cached));
    }
    
    let password = inquire::Password::new(&format!("[sudo] password for {host_str}:"))
        .without_confirmation()
        .prompt()
        .context("Failed to read sudo password")?;
        
    if password.is_empty() {
        bail!("Password cannot be empty");
    }
    
    let secret = SecretString::new(password.into());
    cache_password(&host_str, secret.clone())?;
    Ok(Some(secret))
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec31d40 and e84d151.

📒 Files selected for processing (3)
  • src/darwin.rs
  • src/interface.rs
  • src/remote.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/interface.rs (1)
src/commands.rs (1)
  • arg (315-318)
src/remote.rs (1)
src/commands.rs (5)
  • get_cached_password (40-46)
  • new (279-290)
  • new (788-795)
  • new (925-934)
  • cache_password (64-73)
src/darwin.rs (2)
src/util.rs (2)
  • ensure_ssh_key_login (201-219)
  • get_hostname (237-266)
src/remote.rs (3)
  • init_ssh_control (259-262)
  • copy_to_remote (993-1023)
  • validate_closure_remote (827-924)
🔇 Additional comments (13)
src/interface.rs (2)

576-583: LGTM!

The new target_host and no_validate fields are correctly added to HomeRebuildArgs, following the same pattern as other rebuild argument structs.


694-701: LGTM!

The new target_host and no_validate fields are correctly added to DarwinRebuildArgs, enabling remote deployment and validation bypass for Darwin configurations.

src/remote.rs (1)

1103-1104: LGTM!

Clean extension of the Platform enum to support Darwin, with proper dispatch to the new activate_darwin_remote function.

Also applies to: 1151-1151

src/darwin.rs (10)

29-36: LGTM!

Good defensive programming to validate the Darwin system closure before activation. The essential files list covers the critical activation components.


82-134: LGTM!

Well-structured orchestration flow that cleanly separates build context setup, output path determination, build execution, and activation. The SSH control guard initialization is correctly scoped to cover both build and activation phases.


215-261: LGTM!

The activation logic correctly separates remote and local paths. The local activation properly:

  1. Sets the system profile via nix build --profile
  2. Checks the activate-user file for elevation requirements
  3. Runs darwin-rebuild activate with appropriate elevation

289-309: LGTM!

Clean helper function that properly handles SSH key verification for remote operations and elevation status determination.


349-412: LGTM!

The build execution correctly separates remote and local paths, returning Some(PathBuf) for remote builds to track the actual store path, and None for local builds where the symlink can be followed.


488-525: LGTM!

Thorough validation with clear, actionable error messages that guide users on how to resolve issues with incomplete system closures.


599-620: LGTM!

Clean elevation status check that properly handles the bypass flag, None strategy, and root user detection with a clear error message.


556-582: LGTM!

Clean implementation that properly parses the NH_DARWIN_FLAKE environment variable into a flake installable, with appropriate handling for missing attributes.


157-170: Condition logic for remote copy is correct.

The check if out_path.exists() properly handles all scenarios. When build_host != target_host and the output must relay through localhost (due to failed direct copy between remotes), build_remote() creates the out-link symlink at out_path locally via copy_closure_from(), so the condition evaluates true and the copy to target proceeds. When build_host == target_host, the result is already on the target host and the out-link is not created locally, so the condition evaluates false and the redundant copy is correctly skipped.


672-672: The Store installable handling is intentional and correct as-is.

The Installable::Store case appears as a no-op because different callers have different validation strategies:

  • In darwin.rs and nixos.rs: Store installables are explicitly rejected with a bail error at lines 461 and 1245 respectively, before calling toplevel_for, making the empty block at 672 unreachable for those paths.
  • In home.rs: There is no Store validation before calling toplevel_for (lines 105, 483), so the empty block serves as a defensive pass-through.

This pattern is mirrored in home.rs (line 441) and nixos.rs (line 1217), confirming it's an intentional design pattern: explicit validation in execute-style functions paired with defensive handling in the utility function.

Comment thread src/interface.rs
Comment on lines +270 to 278

/// Build the configuration on a different host over SSH
#[arg(long)]
pub build_host: Option<String>,

/// Deploy the built configuration to a different host over SSH
#[arg(long)]
pub target_host: Option<String>,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Duplicate CLI arguments will cause runtime panic.

OsRebuildActivateArgs already embeds OsRebuildArgs via #[command(flatten)] at line 264-265, and OsRebuildArgs already defines build_host (line 254-255) and target_host (line 250-251). Adding these fields again here creates duplicate argument definitions, which will cause clap to panic at runtime.

🐛 Proposed fix: Remove duplicate fields
 pub struct OsRebuildActivateArgs {
   #[command(flatten)]
   pub rebuild: OsRebuildArgs,

   /// Show activation logs
   #[arg(long, env = "NH_SHOW_ACTIVATION_LOGS")]
   pub show_activation_logs: bool,
-
-  /// Build the configuration on a different host over SSH
-  #[arg(long)]
-  pub build_host: Option<String>,
-
-  /// Deploy the built configuration to a different host over SSH
-  #[arg(long)]
-  pub target_host: Option<String>,
 }

Access these fields via self.rebuild.build_host and self.rebuild.target_host instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Build the configuration on a different host over SSH
#[arg(long)]
pub build_host: Option<String>,
/// Deploy the built configuration to a different host over SSH
#[arg(long)]
pub target_host: Option<String>,
}
pub struct OsRebuildActivateArgs {
#[command(flatten)]
pub rebuild: OsRebuildArgs,
/// Show activation logs
#[arg(long, env = "NH_SHOW_ACTIVATION_LOGS")]
pub show_activation_logs: bool,
}
🤖 Prompt for AI Agents
In @src/interface.rs around lines 270 - 278, OsRebuildActivateArgs duplicates
CLI fields already defined in OsRebuildArgs (build_host and target_host) which
causes clap to panic; remove the duplicate build_host and target_host fields
from OsRebuildActivateArgs (the struct that has #[command(flatten)] for
OsRebuildArgs) and update any usage to read the values via the embedded field
(e.g., self.rebuild.build_host and self.rebuild.target_host) so the flattened
OsRebuildArgs provides the arguments.

@nix-community nix-community deleted a comment from coderabbitai bot Jan 9, 2026
Finalizes the work done in my previous commit to complete refactoring
the Darwin module to bring it up to speed with NixOS remote build logic.
Namely it:

- Splits `rebuild()` into `rebuild_and_activate()` + `activate_rebuilt_config()`
- Extracts helper methods for build context, output path, installables
- Adds closure validation with ESSENTIAL_FILES
- Adds remote build support with proper elevation handling

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6472eec06c6b6bcddf1cc6bacf35684b6a6a6964
@NotAShelf NotAShelf force-pushed the notashelf/push-zpmuorlvnsuz branch from e84d151 to 9ef934d Compare January 9, 2026 15:29
Copy link
Copy Markdown

@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: 3

🤖 Fix all issues with AI agents
In @src/interface.rs:
- Around line 270-277: The duplicate Option<String> fields build_host and
target_host on OsRebuildActivateArgs create ambiguity with the nested
OsRebuildArgs (accessible via OsRebuildActivateArgs.rebuild); remove the
build_host and target_host fields from OsRebuildActivateArgs, update any
references to use args.rebuild.build_host and args.rebuild.target_host, and
ensure command-line parsing / argument annotations are only defined on the
OsRebuildArgs struct to avoid duplicate CLI flags.

In @src/remote.rs:
- Around line 1349-1467: activate_darwin_remote ignores config.activation_type
and always runs "activate"; update activate_darwin_remote to mirror
activate_nixos_remote by matching on config.activation_type (e.g., Test, Switch,
Boot) and set the darwin-rebuild subcommand accordingly instead of hardcoding
"activate" — change the base_cmd construction for the darwin_rebuild invocation
(where base_cmd is currently format!("{} activate", ...)) to select "test",
"switch", or "boot" based on config.activation_type and pass that into
build_remote_command, ensuring stdin password handling and show_logs behavior
remain unchanged.
🧹 Nitpick comments (3)
src/remote.rs (1)

1473-1475: Clarify why Darwin and NixOS share the same profile path.

DARWIN_SYSTEM_PROFILE and NIXOS_SYSTEM_PROFILE (line 1471) both point to /nix/var/nix/profiles/system. If Darwin and NixOS truly use the same system profile path, consider:

  1. Using a single SYSTEM_PROFILE constant, or
  2. Adding a comment explaining why separate constants exist despite identical values

This helps avoid confusion about whether the duplication is intentional or an oversight.

src/darwin.rs (2)

175-189: Path resolution logic may have edge case issues.

The path resolution for validation (lines 175-189) has a fallback condition that's unclear:

} else if is_remote_build && !out_path.exists() {
  // Remote build with no local result and no store path captured
  // (shouldn't happen, but fallback)
  target_profile.to_path_buf()

If actual_store_path is None AND the output path doesn't exist locally, using target_profile (which is just a symlink path) for validation may not work correctly, especially for remote validation where the symlink won't exist on the remote host.

Consider either:

  1. Making this an error condition rather than a silent fallback, since the comment says "shouldn't happen"
  2. Adding more detailed logging to help diagnose if this case ever occurs
Alternative approach
   } else if is_remote_build && !out_path.exists() {
-    // Remote build with no local result and no store path captured
-    // (shouldn't happen, but fallback)
-    target_profile.to_path_buf()
+    // This shouldn't happen - remote builds should always return a store path
+    bail!(
+      "Internal error: Remote build completed but no store path was captured \
+       and output path doesn't exist locally. This is a bug."
+    );
   } else {

29-36: Validation checks sw/bin as a file, but it's a directory.

Line 35 includes ("sw/bin", "system path") in ESSENTIAL_FILES, but sw/bin is a directory, not a file. The validation logic at line 504 uses path.exists(), which returns true for both files and directories, so this works.

However, for clarity and robustness, consider:

  1. Renaming the constant to ESSENTIAL_PATHS to reflect that it checks both files and directories, or
  2. Adding a separate validation for directories vs files with more specific error messages
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e84d151 and 9ef934d.

📒 Files selected for processing (3)
  • src/darwin.rs
  • src/interface.rs
  • src/remote.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/remote.rs (1)
src/commands.rs (2)
  • get_cached_password (40-46)
  • cache_password (64-73)
⏰ 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). (7)
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Linux
  • GitHub Check: Test NH on Darwin
  • GitHub Check: Test NH on Linux
  • GitHub Check: treewide-checks
  • GitHub Check: Build NH on Linux
🔇 Additional comments (6)
src/remote.rs (1)

1103-1104: LGTM! Clean platform extension.

The addition of the Darwin variant to the Platform enum follows the existing pattern and is properly integrated with the dispatch logic in activate_remote (line 1151).

src/darwin.rs (5)

500-554: Well-structured validation with helpful error messages.

The validate_system_closure and validate_system_closure_remote functions provide good pre-activation validation with:

  • Clear error messages explaining what's missing
  • Helpful guidance on how to fix the issues
  • Proper context for remote vs local validation

This defensive approach should prevent cryptic activation failures.


561-582: LGTM! Clean environment variable parsing.

The get_nh_darwin_flake_env function properly parses the NH_DARWIN_FLAKE environment variable into an Installable::Flake, with appropriate error handling and debug logging.


599-620: Clear elevation status determination.

The has_elevation_status function properly handles all elevation scenarios:

  • Respects ElevationStrategy::None
  • Handles bypass_root_check flag
  • Prevents direct root execution with helpful error message

The logic is sound and the error guidance is clear.


642-657: Improved error messaging and debugging.

The additions to toplevel_for enhance user experience:

  • Line 642-645: Helpful info log when inferring hostname
  • Lines 651-657: Clear error message with actionable suggestions when attribute path is too specific

These changes make the function more user-friendly without changing its core behavior.


82-134: Excellent refactoring for maintainability.

The refactored rebuild_and_activate function successfully breaks down a complex operation into well-named helper functions:

  • setup_build_context - Initial validation and setup
  • determine_output_path - Path resolution
  • resolve_installable_and_toplevel - Installable preparation
  • execute_build - Build execution (local or remote)
  • handle_dix_diff - Diff display
  • activate_rebuilt_config - Activation logic

This separation of concerns significantly improves readability and maintainability. The SSH control guard initialization (lines 103-108) is properly scoped to cover both build and activation phases.

Comment thread src/interface.rs
Comment on lines +270 to +277

/// Build the configuration on a different host over SSH
#[arg(long)]
pub build_host: Option<String>,

/// Deploy the built configuration to a different host over SSH
#[arg(long)]
pub target_host: Option<String>,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Duplicate field definitions create ambiguity.

The build_host and target_host fields added here duplicate fields that already exist in the nested OsRebuildArgs structure (accessible via self.rebuild.build_host and self.rebuild.target_host at lines 254-255). This creates two ways to access the same configuration, which can lead to:

  1. Inconsistent behavior if both are set
  2. Confusion about which field takes precedence
  3. Maintenance burden keeping both in sync

Since OsRebuildActivateArgs already contains pub rebuild: OsRebuildArgs (line 265), the nested fields should be sufficient.

🔧 Proposed fix

Remove the duplicate field definitions:

   /// Show activation logs
   #[arg(long, env = "NH_SHOW_ACTIVATION_LOGS")]
   pub show_activation_logs: bool,
-
-  /// Build the configuration on a different host over SSH
-  #[arg(long)]
-  pub build_host: Option<String>,
-
-  /// Deploy the built configuration to a different host over SSH
-  #[arg(long)]
-  pub target_host: Option<String>,
 }

Then access these fields via args.rebuild.build_host and args.rebuild.target_host in the implementation code.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @src/interface.rs around lines 270 - 277, The duplicate Option<String> fields
build_host and target_host on OsRebuildActivateArgs create ambiguity with the
nested OsRebuildArgs (accessible via OsRebuildActivateArgs.rebuild); remove the
build_host and target_host fields from OsRebuildActivateArgs, update any
references to use args.rebuild.build_host and args.rebuild.target_host, and
ensure command-line parsing / argument annotations are only defined on the
OsRebuildArgs struct to avoid duplicate CLI flags.

Comment thread src/remote.rs
Comment on lines +1349 to +1467
fn activate_darwin_remote(
host: &RemoteHost,
system_profile: &Path,
config: &ActivateRemoteConfig,
) -> Result<()> {
let ssh_opts = get_ssh_opts();

let sudo_password = if let Some(ref strategy) = config.elevation {
if matches!(
strategy,
ElevationStrategy::None | ElevationStrategy::Passwordless
) {
None
} else {
let host_str = host.ssh_host();
if let Some(cached_password) = get_cached_password(&host_str)? {
Some(cached_password)
} else {
let password =
inquire::Password::new(&format!("[sudo] password for {host_str}:"))
.without_confirmation()
.prompt()
.context("Failed to read sudo password")?;
if password.is_empty() {
bail!("Password cannot be empty");
}
let secret_password = SecretString::new(password.into());
cache_password(&host_str, secret_password.clone())?;
Some(secret_password)
}
}
} else {
None
};

// Step 1: Set the system profile using nix build
let mut profile_ssh_cmd = Exec::cmd("ssh");
for opt in &ssh_opts {
profile_ssh_cmd = profile_ssh_cmd.arg(opt);
}
// Add -T flag to disable pseudo-terminal allocation (needed for stdin)
profile_ssh_cmd = profile_ssh_cmd.arg("-T");
profile_ssh_cmd = profile_ssh_cmd.arg(host.ssh_host());

// Build the remote command using helper function
let base_cmd = format!(
"nix build --no-link --profile {} {}",
DARWIN_SYSTEM_PROFILE,
shell_quote(&system_profile.to_string_lossy())
);
let profile_remote_cmd =
build_remote_command(config.elevation.as_ref(), &base_cmd)?;

profile_ssh_cmd = profile_ssh_cmd.arg(profile_remote_cmd);

// Pass password via stdin if elevation is needed
if let Some(ref password) = sudo_password {
profile_ssh_cmd =
profile_ssh_cmd.stdin(format!("{}\n", password.expose_secret()).as_str());
}

debug!(?profile_ssh_cmd, "Setting Darwin profile");

let profile_capture = profile_ssh_cmd
.capture()
.wrap_err("Failed to set Darwin profile")?;

if !profile_capture.exit_status.success() {
bail!(
"Failed to set system profile on '{}':\n{}",
host,
profile_capture.stderr_str()
);
}

// Step 2: Run darwin-rebuild activate
let darwin_rebuild = system_profile.join("sw/bin/darwin-rebuild");

let rebuild_path_str = darwin_rebuild
.to_str()
.ok_or_else(|| eyre!("darwin-rebuild path contains invalid UTF-8"))?;

let mut ssh_cmd = Exec::cmd("ssh");
for opt in &ssh_opts {
ssh_cmd = ssh_cmd.arg(opt);
}
// Add -T flag to disable pseudo-terminal allocation (needed for stdin)
ssh_cmd = ssh_cmd.arg("-T");
ssh_cmd = ssh_cmd.arg(host.ssh_host());

// Build the remote command using helper function
let base_cmd = format!("{} activate", shell_quote(rebuild_path_str));
let remote_cmd = build_remote_command(config.elevation.as_ref(), &base_cmd)?;

ssh_cmd = ssh_cmd.arg(remote_cmd);

// Pass password via stdin if elevation is needed
if let Some(ref password) = sudo_password {
ssh_cmd = ssh_cmd.stdin(format!("{}\n", password.expose_secret()).as_str());
}

if config.show_logs {
ssh_cmd = ssh_cmd
.stdout(Redirection::Merge)
.stderr(Redirection::Merge);
}

debug!(?ssh_cmd, "Activating Darwin configuration");

let capture = ssh_cmd
.capture()
.wrap_err("Failed to activate Darwin configuration")?;

if !capture.exit_status.success() {
bail!("Activation failed on '{}':\n{}", host, capture.stderr_str());
}

Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Significant code duplication with activate_nixos_remote.

The activate_darwin_remote function duplicates substantial logic from activate_nixos_remote (lines 1169-1347):

  • Password prompting and caching (lines 1356-1382 vs 1179-1207) - nearly identical
  • SSH command construction pattern
  • Error handling and command execution flow

Consider extracting common logic into helper functions to reduce duplication and improve maintainability:

  • get_elevation_password(host, strategy, config) for password handling
  • execute_remote_command(host, cmd, password, show_logs) for SSH execution

This would make the platform-specific activation functions focus only on their unique command sequences.


⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the activate_darwin_remote function to see if it uses activation_type
rg -A 150 "fn activate_darwin_remote" src/remote.rs | head -200

Repository: nix-community/nh

Length of output: 4701


🏁 Script executed:

# Check the ActivateRemoteConfig structure
rg -B 5 -A 10 "struct ActivateRemoteConfig" src/remote.rs

Repository: nix-community/nh

Length of output: 466


🏁 Script executed:

# Check the ActivationType enum
rg -B 2 -A 10 "enum ActivationType" src/remote.rs

Repository: nix-community/nh

Length of output: 366


🏁 Script executed:

# Check if activate_nixos_remote uses activation_type
rg -A 100 "fn activate_nixos_remote" src/remote.rs | grep -E "(activation_type|Test|Switch|Boot|::)" | head -30

Repository: nix-community/nh

Length of output: 606


🌐 Web query:

darwin-rebuild activation modes test switch boot

💡 Result:

Short answer: darwin-rebuild uses the same activation-mode semantics as nixos-rebuild.

  • switch — build the configuration, activate it now and make it the default for future boots. [1][2]
  • test — build and activate the configuration for the current session but do NOT create a persistent boot entry (useful for testing). [2]
  • boot — build the configuration and install it as the boot/default generation but do not switch the running system to it immediately. [2]

Sources: nix-darwin README (darwin-rebuild usage). [1] NixOS/nixos-rebuild manpage (defines switch/test/boot semantics used by darwin-rebuild). [2]

References
[1] nix-darwin README (darwin-rebuild usage).
[2] nixos-rebuild / manpage (switch/test/boot descriptions).


Darwin activation ignores the activation_type parameter.

The activate_darwin_remote function receives config.activation_type but never uses it. The function hardcodes "activate" as the command (line 1424) and always executes the same two-step process regardless of the requested activation type.

In contrast, activate_nixos_remote properly handles different activation types (Test, Switch, Boot) by matching on config.activation_type.

Since darwin-rebuild supports the same activation modes as nixos-rebuild (test, switch, boot), the Darwin implementation should handle these modes explicitly rather than defaulting to "activate".

🤖 Prompt for AI Agents
In @src/remote.rs around lines 1349 - 1467, activate_darwin_remote ignores
config.activation_type and always runs "activate"; update activate_darwin_remote
to mirror activate_nixos_remote by matching on config.activation_type (e.g.,
Test, Switch, Boot) and set the darwin-rebuild subcommand accordingly instead of
hardcoding "activate" — change the base_cmd construction for the darwin_rebuild
invocation (where base_cmd is currently format!("{} activate", ...)) to select
"test", "switch", or "boot" based on config.activation_type and pass that into
build_remote_command, ensuring stdin password handling and show_logs behavior
remain unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant