How to run `nixos-rebuild --target-host` from Darwin?

I want to trigger nixos-rebuild switch remotely, using a local file from a Darwin machine. For the purpose of tinkering with an existing NixOS machine on the network, NixOps is overkill. If I were on Linux, I’d just run

nixos-rebuild switch --target-host=<target> configuration.nix

but Darwin naturally does not have nixos-rebuild. Did anybody try something like this already? Will nix complain about the platform even if the target is Linux?

More generally, how do I get an executable from a package in a NixOS module to run on Darwin (assuming all dependencies are compatible)? Do I really have to trick the NixOS tools module into outputting a package that I can capture for my purposes by supplying arguments that are convincing enough?

2 Likes

I haven’t tried this yet, but it’s on my TODO list, but I think you can just copy nixos-rebuild from say here: https://github.com/Kloenk/nix/blob/beb7c9abd050d62babb02cce5eeac53b8cf249ec/nixos-rebuild

and go to town. If you do try it, please update here how it worked out!

Finally got around to doing this somewhat properly. This was exhaustingly complicated, because there are so many moving parts in nixos-rebuild. Here is a run-down.

The basic idea is to evaluate the target configuration on the local Darwin machine, and then push the derivations to be built on the target machine. For this we pass --argstr system x86_64-linux to all the local runs of nix-instantiate. The rest was figuring out details, such as how to inject that argument into nixos-rebuild in the first place, how not let it go into commands which don’t understand it, how to manage which version of nix is used where, since there are multiple possibilities.

As a first step, the actual package is just a reproduction of what we find in <nixpkgs/nixos>/modules/installer/tools/tools.nix, but obviously with the nix package set to pkgs.nix instead of config.system.nix.package.

The fun starts when nixos-rebuild re-executes the target system’s version of itself. Originally this amounts to whatever is defined by the configuration, but that will be run on Darwin now and we need to tell it to evaluate expressions for Linux explicitly.

nixos-rebuild.nix:

{ pkgs, lib, ... }:
let
  path = pkgs.path + /nixos/modules/installer/tools;
  fallback = import (path + /nix-fallback-paths.nix);
in
(pkgs.substituteAll {
  dir = "bin";
  isExecutable = true;
  name = "nixos-rebuild";
  src =  path + /nixos-rebuild.sh;
  inherit (pkgs) runtimeShell;
  # here is the only difference to the original definition
  nix = pkgs.nix.out;
  nix_x86_64_linux = fallback.x86_64-linux;
  nix_i686_linux = fallback.i686-linux;
  path = lib.makeBinPath [ pkgs.jq ];
}).overrideAttrs (old: {
  postInstall = ''
    # use a patched version of the target configuration's `nixos-rebuild`
    # that will work from Darwin
    substituteInPlace $out/bin/nixos-rebuild \
      --replace "--expr 'with import <nixpkgs/nixos> {}; config.system.build.nixos-rebuild'" "${./nixos-rebuild-patch.nix}"
  '';
})

Most importantly we just inject the required arguments to nix right into the extraBuildFlags variable, as we’d need to patch both the initial as well as the target version if we wanted them to support command line parameters. Unfortunately this has consequences:

  1. We can’t continue using the Linux nix built for building the target configuration on Darwin, so we have to omit it being added to $PATH.
  2. We have to filter out the newly added arguments again, because for remote building they are passed verbatim to nix-store, which does not take --argstr. While there is already a filtering step happening, it lets everything through, as opposed to the top-level command line parsing, which throws an error on unknown parameters.

nixos-rebuild-patch.nix:

with import <nixpkgs/nixos> {};
config.system.build.nixos-rebuild.overrideAttrs (old: {
  postInstall = ''
    # 1. evaluate for Linux to avoid being stopped by assertions.
    #    see: <https://github.com/NixOS/nixops/issues/1033>
    # 2. do not put the newly built Linux `nix` into `$PATH`, since we're on
    #    Darwin and are already using the target configuration's version.
    substituteInPlace $out/bin/nixos-rebuild \
      --replace "extraBuildFlags=()" "extraBuildFlags=(--argstr system x86_64-linux)" \
      --replace 'PATH="$tmpDir/nix/bin:$PATH"' ""
    # filter `--argstr` parameters back out before passing to remote `nix-store`
    pattern="# We don't want this in buildArgs"
    # WTF `sed`?! line break is mandatory for `i` command to work
    sed -i "0,/$pattern/{/$pattern/i --argstr) shift 2;;
    }" $out/bin/nixos-rebuild
  '';
}

If you add the package to your environment, you can now run from Darwin:

$ nixos-rebuild --target-host <target> switch

Don’t forget to specify <nixpkgs> and <nixos-config> with environment variables or -I to have predictable results!

Note that for this to Just Work™ on targets with NixOS >=18.09 you need root access to your target host over ssh. There is also an option to run as sudo, which was introduced in 20.03.

Edit: It works. But that won’t work either, because remote $PATH is set to local $PATH, such that essentially only nix will be available, but definitely not sudo. This may be an actual bug, and I submitted a pull request to change the offending line to env PATH="$remoteNix:"'$PATH'.

Hope this helps!

8 Likes

Thank you so much for investigating and posting how to do this! FWIW the PR fixing $PATH has been merged so --use-remote-sudo may work now. It also looks like the actual implementation of nixos-rebuild has moved to nixpkgs/pkgs/os-specific/linux/nixos-rebuild/default.nix .

1 Like