mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-08-25 17:55:21 +00:00
linux-builder-vz: init modules and package
This commit is contained in:
87
nixos/modules/profiles/nix-builder-vz-vm.nix
Normal file
87
nixos/modules/profiles/nix-builder-vz-vm.nix
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
The Linux remote-builder profile, on Apple's Virtualization.framework.
|
||||
|
||||
The vzvm counterpart to `./nix-builder-vm.nix`:
|
||||
|
||||
- Backend-neutral half lives in `./nix-builder.nix`.
|
||||
- everything below is what differs from the QEMU backend.
|
||||
|
||||
Main differences:
|
||||
|
||||
- `networking.nameservers = [ "8.8.8.8" ]` is not needed.
|
||||
vz uses the host's NAT, which provides working DNS.
|
||||
- `virtualisation.graphics = false`: no display support in vz
|
||||
- `virtualisation.useNixStoreImage`: vz only supports nix store image
|
||||
*/
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.virtualisation.darwin-builder;
|
||||
|
||||
keysDirectory = "/var/keys";
|
||||
keysMountUnit = "var-keys.mount";
|
||||
in
|
||||
|
||||
{
|
||||
imports = [
|
||||
./nix-builder.nix
|
||||
../virtualisation/vz-vm.nix
|
||||
];
|
||||
|
||||
config = {
|
||||
virtualisation.vz.forwardPorts = [
|
||||
{
|
||||
host.address = "127.0.0.1";
|
||||
host.port = cfg.hostPort;
|
||||
# Must track the port the guest serves SSH on, or the host forwards nowhere.
|
||||
guest.port = config.virtualisation.vz.vsockSSH.port;
|
||||
}
|
||||
];
|
||||
|
||||
# The guest console belongs where a Mac user looks for service logs. Switch to `file`
|
||||
# for a complete record: the unified log drops messages under bursts.
|
||||
virtualisation.vz.console = lib.mkDefault "log";
|
||||
virtualisation.vz.consoleLog = lib.mkDefault "./console.log";
|
||||
|
||||
# Our raw image must keep the `.qcow2` name nix-darwin's `ephemeral` wipes; vzvm checks
|
||||
# the magic bytes and refuses a genuine leftover qcow2 rather than misreading it.
|
||||
virtualisation.vz.diskImage = "./${config.networking.hostName}.qcow2";
|
||||
|
||||
# authorized keys come via virtiofs. fix uid and mode for sshd's ownership checks.
|
||||
systemd.services.copy-builder-keys = {
|
||||
description = "Stage builder SSH keys where sshd will accept them";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
requires = [ keysMountUnit ];
|
||||
after = [ keysMountUnit ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
script = ''
|
||||
install -d -m 0755 -o root -g root /run/builder-keys
|
||||
for key in ${keysDirectory}/*.pub; do
|
||||
[ -e "$key" ] || continue
|
||||
install -m 0444 -o root -g root "$key" /run/builder-keys/
|
||||
done
|
||||
'';
|
||||
};
|
||||
|
||||
# The dependency belongs on the per-connection service, not on the socket.
|
||||
systemd.services."vzvm-ssh@" = {
|
||||
requires = [ "copy-builder-keys.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
after = [
|
||||
"copy-builder-keys.service"
|
||||
"network-online.target"
|
||||
];
|
||||
};
|
||||
|
||||
services.openssh.authorizedKeysFiles = lib.mkForce [
|
||||
"/run/builder-keys/%u_ed25519.pub"
|
||||
];
|
||||
};
|
||||
}
|
||||
204
nixos/modules/virtualisation/vm-base.nix
Normal file
204
nixos/modules/virtualisation/vm-base.nix
Normal file
@@ -0,0 +1,204 @@
|
||||
# Backend-neutral pieces of running a NixOS guest in a VM.
|
||||
#
|
||||
# `qemu-vm.nix` still carries its own copies of these options: the two modules are
|
||||
# never imported together because a configuration has exactly one VM backend, so
|
||||
# the duplicated declarations cannot collide.
|
||||
# Deduping `qemu-vm.nix` onto this module is a refactor left for later.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.virtualisation;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
|
||||
virtualisation.memorySize = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 1024;
|
||||
description = ''
|
||||
The memory size in megabytes of the virtual machine.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.cores = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 1;
|
||||
description = ''
|
||||
Specify the number of cores the guest is permitted to use.
|
||||
The number can be higher than the available cores on the
|
||||
host system.
|
||||
'';
|
||||
};
|
||||
|
||||
# `virtualisation.diskSize` comes from `disk-size-option.nix` in the default module list.
|
||||
|
||||
virtualisation.additionalPaths = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = [ ];
|
||||
description = ''
|
||||
A list of paths whose closure should be made available to the VM.
|
||||
|
||||
The closure is copied into the VM's Nix store image and registered in
|
||||
the guest's Nix database.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.writableStore = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
If enabled, the Nix store in the VM is made writable by layering an
|
||||
overlay filesystem on top of the (read-only) store image.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.writableStoreUseTmpfs = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Use a tmpfs for the writable store instead of writing to a disk image.
|
||||
|
||||
Turning this off makes store writes survive a reboot, at the cost of
|
||||
needing a disk to put them on.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.useHostCerts = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
If enabled, when `NIX_SSL_CERT_FILE` is set on the host,
|
||||
pass the CA certificates from the host to the VM.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.sharedDirectories = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options.source = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "The path of the directory to share, can be a shell variable";
|
||||
};
|
||||
options.target = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "The mount point of the directory inside the virtual machine";
|
||||
};
|
||||
options.tag = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
description = ''
|
||||
The tag the guest mounts this share by. Defaults to the attribute
|
||||
name. Backends impose their own length limits on tags.
|
||||
'';
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
default = { };
|
||||
example = {
|
||||
my-share = {
|
||||
source = "/path/to/be/shared";
|
||||
target = "/mnt/shared";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
An attribute set of directories that will be shared with the virtual
|
||||
machine. The attribute name is used as the mount tag.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.host.pkgs = lib.mkOption {
|
||||
type = lib.types.pkgs;
|
||||
default = pkgs;
|
||||
defaultText = lib.literalExpression "pkgs";
|
||||
example = lib.literalExpression ''
|
||||
import pkgs.path { system = "aarch64-darwin"; }
|
||||
'';
|
||||
description = ''
|
||||
Package set to use for the host-side tooling that launches the VM.
|
||||
|
||||
This is not the guest's package set: the host may well be a different
|
||||
platform than the guest, which is the entire point of running a VM.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
|
||||
# Passed on the kernel command line: a direct reference would make the closure self-referential.
|
||||
systemd.services.register-nix-paths = lib.mkIf config.nix.enable {
|
||||
# Runs early so the store DB is populated first; `--load-db` needs no daemon.
|
||||
unitConfig.DefaultDependencies = false;
|
||||
wantedBy = [ "sysinit.target" ];
|
||||
before = [
|
||||
"sysinit.target"
|
||||
"shutdown.target"
|
||||
"nix-daemon.socket"
|
||||
"nix-daemon.service"
|
||||
];
|
||||
after = [ "local-fs.target" ];
|
||||
conflicts = [ "shutdown.target" ];
|
||||
restartIfChanged = false;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
User = lib.mkIf (config.nix.daemonUser != "root") config.nix.daemonUser;
|
||||
Group = lib.mkIf (config.nix.daemonGroup != "root") config.nix.daemonGroup;
|
||||
};
|
||||
script = ''
|
||||
if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
|
||||
${lib.getExe' config.nix.package.out "nix-store"} --load-db < "''${BASH_REMATCH[1]}"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.additionalPaths = [ config.system.build.toplevel ];
|
||||
|
||||
# Read-only erofs store, overlaid when writable. Override per entry: `mkVMOverride` on
|
||||
# the whole set would drop other modules' filesystems, including the Rosetta share.
|
||||
fileSystems = {
|
||||
"/nix/.ro-store" = lib.mkVMOverride {
|
||||
device = "/dev/disk/by-label/nix-store";
|
||||
fsType = "erofs";
|
||||
neededForBoot = true;
|
||||
options = [ "ro" ];
|
||||
};
|
||||
"/nix/store" = lib.mkVMOverride (
|
||||
if cfg.writableStore then
|
||||
{
|
||||
overlay = {
|
||||
lowerdir = [ "/nix/.ro-store" ];
|
||||
upperdir = "/nix/.rw-store/upper";
|
||||
workdir = "/nix/.rw-store/work";
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
device = "/nix/.ro-store";
|
||||
fsType = "none";
|
||||
options = [ "bind" ];
|
||||
}
|
||||
);
|
||||
"/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) (
|
||||
lib.mkVMOverride {
|
||||
fsType = "tmpfs";
|
||||
options = [ "mode=0755" ];
|
||||
neededForBoot = true;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
swapDevices = lib.mkVMOverride [ ];
|
||||
boot.initrd.luks.devices = lib.mkVMOverride { };
|
||||
|
||||
# The host keeps time for us.
|
||||
services.timesyncd.enable = false;
|
||||
};
|
||||
}
|
||||
452
nixos/modules/virtualisation/vz-vm.nix
Normal file
452
nixos/modules/virtualisation/vz-vm.nix
Normal file
@@ -0,0 +1,452 @@
|
||||
# Runs a NixOS guest on Apple's Virtualization.framework with `vzvm`: Rosetta, and a
|
||||
# lighter hypervisor than QEMU.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.virtualisation;
|
||||
vzCfg = cfg.vz;
|
||||
|
||||
hostPkgs = cfg.host.pkgs;
|
||||
|
||||
regInfo = hostPkgs.closureInfo { rootPaths = cfg.additionalPaths; };
|
||||
|
||||
# Host-built erofs store image, named by closure hash so only a changed guest rebuilds it.
|
||||
# A derivation is not an option: it would need the Linux builder this VM provides.
|
||||
storeClosureInfo = hostPkgs.closureInfo {
|
||||
rootPaths = [
|
||||
config.system.build.toplevel
|
||||
regInfo
|
||||
];
|
||||
};
|
||||
storeImageName = "store-${lib.head (lib.splitString "-" (baseNameOf (toString storeClosureInfo)))}.img";
|
||||
|
||||
toplevel = config.system.build.toplevel;
|
||||
|
||||
kernelParams = [
|
||||
# Virtualization.framework offers a virtio console
|
||||
"console=hvc0"
|
||||
"init=${toplevel}/init"
|
||||
"regInfo=${regInfo}/registration"
|
||||
]
|
||||
++ config.boot.kernelParams;
|
||||
|
||||
forwards = map (forward: {
|
||||
listen = "${forward.host.address}:${toString forward.host.port}";
|
||||
vsockPort = forward.guest.port;
|
||||
}) vzCfg.forwardPorts;
|
||||
|
||||
# The builder profile passes the keys directory as `"$KEYS"`; the runner resolves it.
|
||||
shareFragment = lib.concatMapStrings (share: ''
|
||||
shares=$(${lib.getExe hostPkgs.jq} -n --argjson shares "$shares" \
|
||||
--arg tag ${lib.escapeShellArg share.tag} --arg path ${share.source} \
|
||||
'$shares + [{tag: $tag, path: $path}]')
|
||||
'') (lib.attrValues cfg.sharedDirectories);
|
||||
|
||||
staticConfig = {
|
||||
cpuCount = cfg.cores;
|
||||
memorySizeMiB = cfg.memorySize;
|
||||
kernel = "${toplevel}/kernel";
|
||||
initrd = "${toplevel}/initrd";
|
||||
cmdline = toString kernelParams;
|
||||
vsock.forwards = forwards;
|
||||
rosetta = vzCfg.rosetta.enable;
|
||||
inherit (vzCfg) nestedVirtualization;
|
||||
# Only `file` carries a path; dispatch rather than fall through, so a new mode cannot
|
||||
# silently be emitted as `file`.
|
||||
console =
|
||||
if vzCfg.console == "file" then
|
||||
{
|
||||
mode = "file";
|
||||
path = vzCfg.consoleLog;
|
||||
}
|
||||
else
|
||||
{ mode = vzCfg.console; };
|
||||
}
|
||||
// vzCfg.extraConfig;
|
||||
|
||||
staticConfigFile = hostPkgs.writeText "vzvm-config.json" (builtins.toJSON staticConfig);
|
||||
in
|
||||
{
|
||||
imports = [ ./vm-base.nix ];
|
||||
|
||||
options = {
|
||||
|
||||
virtualisation.vz.package = lib.mkPackageOption hostPkgs "vzvm" { };
|
||||
|
||||
virtualisation.vz.rosetta = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Expose Rosetta to the guest, so that it can execute x86_64 binaries.
|
||||
|
||||
Rosetta must be installed on the host; the VM refuses to start
|
||||
otherwise rather than quietly losing the ability to build for
|
||||
x86_64-linux. Install it with:
|
||||
|
||||
```
|
||||
softwareupdate --install-rosetta --agree-to-license
|
||||
```
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
virtualisation.vz.nestedVirtualization = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Boot the guest at EL2 so it gets a working `/dev/kvm`, which the Nix
|
||||
daemon inside then advertises as the `kvm` system feature — required
|
||||
for running NixOS integration tests on the builder.
|
||||
|
||||
Needs macOS 15+ and an M3 or newer chip; the VM refuses to start
|
||||
otherwise rather than quietly advertising a `kvm` it does not have.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.vz.forwardPorts = lib.mkOption {
|
||||
type = lib.types.listOf (
|
||||
lib.types.submodule {
|
||||
options.host.address = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Host address to listen on.";
|
||||
};
|
||||
options.host.port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
description = "Host port to listen on.";
|
||||
};
|
||||
options.guest.port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
description = "Guest *vsock* port that connections are forwarded to.";
|
||||
};
|
||||
}
|
||||
);
|
||||
default = [ ];
|
||||
example = lib.literalExpression ''
|
||||
[ { host.port = 2222; guest.port = 22; } ]
|
||||
'';
|
||||
description = ''
|
||||
Forward host TCP ports into guest over vsock.
|
||||
|
||||
Going via vsock relieves us from having to find a stable inbound address
|
||||
in the NAT network setup.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.vz.console = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"stdio"
|
||||
"file"
|
||||
"log"
|
||||
];
|
||||
default = "stdio";
|
||||
description = ''
|
||||
Where the guest console goes.
|
||||
|
||||
- `stdio`: standard output, which is interactive but goes nowhere under launchd.
|
||||
- `file`: the path in {option}`virtualisation.vz.consoleLog`. The only complete
|
||||
record: the unified log rate-limits chatty sources and drops under bursts.
|
||||
- `log`: macOS unified logging, alongside vzvm's own diagnostics. Read it with
|
||||
`log show --predicate 'subsystem == "systems.applicative.vzvm"'`, and narrow to
|
||||
the guest with `AND category == "guest"`.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.vz.consoleLog = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "./console.log";
|
||||
description = ''
|
||||
Console log file, used when {option}`virtualisation.vz.console` is `file`.
|
||||
Relative paths are resolved against working directory.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.vz.diskImage = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = "./${config.system.name}.img";
|
||||
defaultText = lib.literalExpression ''"./''${config.system.name}.img"'';
|
||||
description = ''
|
||||
Path to the raw data disk backing the writable store, created on first
|
||||
start. Set to `null` to run without one, which requires
|
||||
{option}`virtualisation.writableStoreUseTmpfs`.
|
||||
'';
|
||||
};
|
||||
|
||||
virtualisation.vz.vsockSSH = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.services.openssh.enable;
|
||||
defaultText = lib.literalExpression "config.services.openssh.enable";
|
||||
description = ''
|
||||
Serve SSH on a vsock port in addition to any TCP listeners.
|
||||
|
||||
Inbound connections arrive over vsock rather than TCP because NAT
|
||||
networking gives the guest no stable inbound address. Each connection
|
||||
is handed to its own `sshd -i`, same way a socket-activated `sshd`
|
||||
works over TCP.
|
||||
'';
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 22;
|
||||
description = "vsock port that SSH is served on.";
|
||||
};
|
||||
};
|
||||
|
||||
virtualisation.vz.extraConfig = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
default = { };
|
||||
description = ''
|
||||
Additional attributes merged into the generated `vzvm` JSON configuration.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = hostPkgs.stdenv.hostPlatform.system == "aarch64-darwin";
|
||||
message = ''
|
||||
virtualisation.vz only runs on aarch64-darwin hosts, but
|
||||
`virtualisation.host.pkgs` is ${hostPkgs.stdenv.hostPlatform.system}.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = pkgs.stdenv.hostPlatform.isAarch64;
|
||||
message = ''
|
||||
virtualisation.vz cannot emulate a foreign architecture: the guest
|
||||
(${pkgs.stdenv.hostPlatform.system}) must be aarch64. x86_64 guest
|
||||
binaries run through Rosetta instead, not through emulation.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = vzCfg.diskImage != null || cfg.writableStoreUseTmpfs;
|
||||
message = ''
|
||||
virtualisation.vz.diskImage is null, so there is nowhere to put the
|
||||
writable store. Enable virtualisation.writableStoreUseTmpfs.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
boot.loader.grub.enable = false;
|
||||
|
||||
boot.initrd.availableKernelModules = [
|
||||
"virtio_pci"
|
||||
"virtio_blk"
|
||||
"virtio_console"
|
||||
"virtiofs"
|
||||
"erofs"
|
||||
"overlay"
|
||||
];
|
||||
|
||||
# All inbound connections go via vsock
|
||||
boot.kernelModules = [ "vmw_vsock_virtio_transport" ];
|
||||
|
||||
boot.initrd.systemd.enable = lib.mkDefault true;
|
||||
|
||||
system.requiredKernelConfig = with config.lib.kernelConfig; [
|
||||
(isEnabled "VIRTIO_BLK")
|
||||
(isEnabled "VIRTIO_PCI")
|
||||
(isEnabled "VIRTIO_CONSOLE")
|
||||
(isEnabled "VIRTIO_NET")
|
||||
(isYes "BLK_DEV_INITRD")
|
||||
(isEnabled "FUSE_FS")
|
||||
(isEnabled "VIRTIO_FS")
|
||||
(isEnabled "EROFS_FS")
|
||||
(isEnabled "OVERLAY_FS")
|
||||
(isEnabled "VSOCKETS")
|
||||
(isEnabled "VIRTIO_VSOCKETS")
|
||||
];
|
||||
|
||||
# Override per entry and not as a whole: see note in vm-base.nix.
|
||||
fileSystems = {
|
||||
"/" = lib.mkVMOverride {
|
||||
device = "tmpfs";
|
||||
fsType = "tmpfs";
|
||||
neededForBoot = true;
|
||||
options = [ "mode=0755" ];
|
||||
};
|
||||
}
|
||||
// lib.optionalAttrs (!cfg.writableStoreUseTmpfs && vzCfg.diskImage != null) {
|
||||
# Second disk, hence /dev/vdb: store image is always first.
|
||||
"/nix/.rw-store" = lib.mkVMOverride {
|
||||
device = "/dev/vdb";
|
||||
fsType = "ext4";
|
||||
autoFormat = true;
|
||||
neededForBoot = true;
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
_: share:
|
||||
lib.nameValuePair share.target (
|
||||
lib.mkVMOverride {
|
||||
device = share.tag;
|
||||
fsType = "virtiofs";
|
||||
}
|
||||
)
|
||||
) cfg.sharedDirectories;
|
||||
|
||||
# DHCP and DNS come from host NAT
|
||||
networking.useDHCP = lib.mkDefault true;
|
||||
|
||||
virtualisation.rosetta.enable = lib.mkIf vzCfg.rosetta.enable true;
|
||||
# Must agree with the tag vzvm shares Rosetta under, which is fixed.
|
||||
virtualisation.rosetta.mountTag = lib.mkIf vzCfg.rosetta.enable "rosetta";
|
||||
|
||||
system.build.vm =
|
||||
hostPkgs.runCommand "nixos-vm"
|
||||
{
|
||||
preferLocalBuild = true;
|
||||
meta.mainProgram = "run-${config.system.name}-vm";
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin
|
||||
ln -s ${config.system.build.toplevel} $out/system
|
||||
ln -s ${hostPkgs.writeShellScript "run-${config.system.name}-vm" ''
|
||||
set -eu
|
||||
|
||||
# nix-darwin runs this with a working directory it owns
|
||||
imageDir="''${VZVM_STATE_DIR:-$PWD}"
|
||||
mkdir -p "$imageDir"
|
||||
|
||||
if [ -z "''${TMPDIR:-}" ]; then
|
||||
# GNU mktemp needs the XXXXXX placeholder; only reachable when TMPDIR is unset.
|
||||
TMPDIR=$(${hostPkgs.coreutils}/bin/mktemp -d -t vzvm.XXXXXX)
|
||||
fi
|
||||
export TMPDIR
|
||||
|
||||
${lib.optionalString cfg.useHostCerts ''
|
||||
mkdir -p "$TMPDIR/certs"
|
||||
if [ -e "''${NIX_SSL_CERT_FILE:-}" ]; then
|
||||
${hostPkgs.coreutils}/bin/install -m 0644 \
|
||||
"$NIX_SSL_CERT_FILE" "$TMPDIR/certs/ca-certificates.crt"
|
||||
else
|
||||
echo "vzvm: NIX_SSL_CERT_FILE is unset or missing; guest gets no host CA certificates" >&2
|
||||
: > "$TMPDIR/certs/ca-certificates.crt"
|
||||
fi
|
||||
''}
|
||||
|
||||
storeImage="$imageDir/${storeImageName}"
|
||||
|
||||
if [ ! -f "$storeImage" ]; then
|
||||
echo "Building Nix store image, this can take a minute on first boot..." >&2
|
||||
|
||||
# Build into a temp file so an interrupted run leaves no truncated image behind.
|
||||
storeImageTmp="$storeImage.tmp.$$"
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -f '$storeImageTmp'" EXIT
|
||||
|
||||
${hostPkgs.gnutar}/bin/tar --create \
|
||||
--absolute-names \
|
||||
--verbatim-files-from \
|
||||
--transform 'flags=rSh;s|/nix/store/||' \
|
||||
--transform 'flags=rSh;s|~nix~case~hack~[[:digit:]]\+||g' \
|
||||
--files-from ${storeClosureInfo}/store-paths \
|
||||
| ${hostPkgs.erofs-utils}/bin/mkfs.erofs \
|
||||
--quiet \
|
||||
--force-uid=0 \
|
||||
--force-gid=0 \
|
||||
-L nix-store \
|
||||
-U eb176051-bd15-49b7-9e6b-462e0b467019 \
|
||||
-T 0 \
|
||||
--hard-dereference \
|
||||
--tar=f \
|
||||
"$storeImageTmp"
|
||||
|
||||
mv "$storeImageTmp" "$storeImage"
|
||||
trap - EXIT
|
||||
|
||||
# Images from previous generations are dead weight and cheap to rebuild.
|
||||
find "$imageDir" -maxdepth 1 -name 'store-*.img' ! -name ${lib.escapeShellArg storeImageName} -delete
|
||||
fi
|
||||
|
||||
disks=$(${lib.getExe hostPkgs.jq} -n --arg store "$storeImage" \
|
||||
'[{path: $store, readOnly: true}]')
|
||||
|
||||
shares='[]'
|
||||
${shareFragment}
|
||||
|
||||
${lib.optionalString (vzCfg.diskImage != null) ''
|
||||
dataDisk="${vzCfg.diskImage}"
|
||||
case "$dataDisk" in
|
||||
/*) ;;
|
||||
*) dataDisk="$imageDir/''${dataDisk#./}" ;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$dataDisk" ]; then
|
||||
echo "Creating ${toString cfg.diskSize}M data disk at $dataDisk..." >&2
|
||||
${hostPkgs.coreutils}/bin/dd if=/dev/zero of="$dataDisk" bs=1M count=0 \
|
||||
seek=${toString cfg.diskSize}
|
||||
fi
|
||||
|
||||
disks=$(${lib.getExe hostPkgs.jq} -n --argjson disks "$disks" --arg data "$dataDisk" \
|
||||
'$disks + [{path: $data, readOnly: false}]')
|
||||
''}
|
||||
|
||||
config="$imageDir/vzvm.json"
|
||||
${lib.getExe hostPkgs.jq} --argjson disks "$disks" --argjson shares "$shares" \
|
||||
'.disks = $disks | .shares = $shares' ${staticConfigFile} > "$config"
|
||||
|
||||
exec ${lib.getExe vzCfg.package} "$config"
|
||||
''} $out/bin/run-${config.system.name}-vm
|
||||
'';
|
||||
}
|
||||
|
||||
(lib.mkIf vzCfg.vsockSSH.enable {
|
||||
# sshd has no vsock listener, so systemd owns the socket and hands it each connection.
|
||||
systemd.sockets.vzvm-ssh = {
|
||||
description = "SSH vsock socket";
|
||||
wantedBy = [ "sockets.target" ];
|
||||
socketConfig = {
|
||||
ListenStream = "vsock::${toString vzCfg.vsockSSH.port}";
|
||||
Accept = true;
|
||||
|
||||
# The default 64 suits interactive logins, not `ssh-ng://`; past the limit systemd
|
||||
# stops accepting and connections hang without a banner or a log line anywhere.
|
||||
MaxConnections = 512;
|
||||
|
||||
# stop the socket unit outright once exceeded
|
||||
TriggerLimitIntervalSec = 0;
|
||||
};
|
||||
};
|
||||
|
||||
# vsock delivers no RST, so without keepalives a session whose peer vanished never dies.
|
||||
services.openssh.settings = {
|
||||
ClientAliveInterval = lib.mkDefault 60;
|
||||
ClientAliveCountMax = lib.mkDefault 3;
|
||||
};
|
||||
|
||||
systemd.services."vzvm-ssh@" = {
|
||||
description = "SSH per-connection daemon (vsock)";
|
||||
after = [ "sshd-keygen.service" ];
|
||||
serviceConfig = {
|
||||
ExecStart = "-${lib.getExe' config.services.openssh.package "sshd"} -i -f /etc/ssh/sshd_config";
|
||||
StandardInput = "socket";
|
||||
StandardOutput = "socket";
|
||||
StandardError = "journal";
|
||||
|
||||
# Not `KillMode = "process"`: sshd's `nix-daemon` child would outlive the connection
|
||||
# and leak instances against MaxConnections until the guest stops accepting SSH.
|
||||
TimeoutStopSec = 10;
|
||||
};
|
||||
};
|
||||
})
|
||||
|
||||
(lib.mkIf cfg.useHostCerts {
|
||||
virtualisation.sharedDirectories.certs = {
|
||||
source = "$TMPDIR/certs";
|
||||
target = "/etc/ssl/certs";
|
||||
};
|
||||
security.pki.installCACerts = false;
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -177,6 +177,31 @@ makeScopeWithSplicing' {
|
||||
linux-builder-x86_64 = self.linux-builder.override {
|
||||
modules = [ { nixpkgs.hostPlatform = "x86_64-linux"; } ];
|
||||
};
|
||||
|
||||
# Like `linux-builder`, but runs the guest on Apple's Virtualization.framework
|
||||
# via `vzvm`, translating x86_64-linux builds with Rosetta instead of emulating
|
||||
# them. See doc/packages/darwin-builder.section.md
|
||||
linux-builder-vz = lib.makeOverridable (
|
||||
{ modules }:
|
||||
let
|
||||
nixos = import ../../nixos {
|
||||
configuration = {
|
||||
imports = [
|
||||
../../nixos/modules/profiles/nix-builder-vz-vm.nix
|
||||
]
|
||||
++ modules;
|
||||
|
||||
virtualisation.host = { inherit pkgs; };
|
||||
|
||||
# aarch64-darwin is the only supported host, so the guest is fixed too.
|
||||
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
|
||||
};
|
||||
|
||||
system = null;
|
||||
};
|
||||
in
|
||||
nixos.config.system.build.macos-builder-installer
|
||||
) { modules = [ ]; };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user