Got my NixOS flake to run under WSL!

This commit is contained in:
Emmet 2023-08-20 17:55:05 -05:00
parent e877fd63bc
commit 0a71232a56
18 changed files with 1063 additions and 10 deletions

View file

@ -0,0 +1,97 @@
{ config, pkgs, lib, ... }:
with builtins; with lib;
let
pkgs2storeContents = l: map (x: { object = x; symlink = "none"; }) l;
nixpkgs = lib.cleanSource pkgs.path;
channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}"
{ preferLocalBuild = true; }
''
mkdir -p $out
cp -prd ${nixpkgs.outPath} $out/nixos
chmod -R u+w $out/nixos
if [ ! -e $out/nixos/nixpkgs ]; then
ln -s . $out/nixos/nixpkgs
fi
echo -n ${toString config.system.nixos.revision} > $out/nixos/.git-revision
echo -n ${toString config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
echo ${toString config.system.nixos.versionSuffix} | sed -e s/pre// > $out/nixos/svn-revision
'';
preparer = pkgs.writeShellScriptBin "wsl-prepare" ''
set -e
mkdir -m 0755 ./bin ./etc
mkdir -m 1777 ./tmp
# WSL requires a /bin/sh - only temporary, NixOS's activate will overwrite
ln -s ${config.users.users.root.shell} ./bin/sh
# WSL also requires a /bin/mount, otherwise the host fs isn't accessible
ln -s /nix/var/nix/profiles/system/sw/bin/mount ./bin/mount
# Set system profile
system=${config.system.build.toplevel}
./$system/sw/bin/nix-store --store `pwd` --load-db < ./nix-path-registration
rm ./nix-path-registration
./$system/sw/bin/nix-env --store `pwd` -p ./nix/var/nix/profiles/system --set $system
# Set channel
mkdir -p ./nix/var/nix/profiles/per-user/root
./$system/sw/bin/nix-env --store `pwd` -p ./nix/var/nix/profiles/per-user/root/channels --set ${channelSources}
mkdir -m 0700 -p ./root/.nix-defexpr
ln -s /nix/var/nix/profiles/per-user/root/channels ./root/.nix-defexpr/channels
# It's now a NixOS!
touch ./etc/NIXOS
# Write wsl.conf so that it is present when NixOS is started for the first time
cp ${config.environment.etc."wsl.conf".source} ./etc/wsl.conf
${lib.optionalString config.wsl.tarball.includeConfig ''
# Copy the system configuration
mkdir -p ./etc/nixos/nixos-wsl
cp -R ${lib.cleanSource ../.}/. ./etc/nixos/nixos-wsl
mv ./etc/nixos/nixos-wsl/configuration.nix ./etc/nixos/configuration.nix
# Patch the import path to avoid having a flake.nix in /etc/nixos
sed -i 's|import \./default\.nix|import \./nixos-wsl|' ./etc/nixos/configuration.nix
''}
'';
in
{
options.wsl.tarball = {
includeConfig = mkOption {
type = types.bool;
default = true;
description = "Whether or not to copy the system configuration into the tarball";
};
};
config = mkIf config.wsl.enable {
# These options make no sense without the wsl-distro module anyway
system.build.tarball = pkgs.callPackage "${nixpkgs}/nixos/lib/make-system-tarball.nix" {
# No contents, structure will be added by prepare script
contents = [ ];
fileName = "nixos-wsl-${pkgs.hostPlatform.system}";
storeContents = pkgs2storeContents [
config.system.build.toplevel
channelSources
preparer
];
extraCommands = "${preparer}/bin/wsl-prepare";
# Use gzip
compressCommand = "gzip";
compressionExtension = ".gz";
};
};
}

View file

@ -0,0 +1,41 @@
{ config, lib, pkgs, ... }:
with builtins; with lib; {
imports = [
(mkRenamedOptionModule [ "wsl" "docker" ] [ "wsl" "docker-desktop" ])
];
options.wsl.docker-desktop = with types; {
enable = mkEnableOption "Docker Desktop integration";
};
config =
let
cfg = config.wsl.docker-desktop;
in
mkIf (config.wsl.enable && cfg.enable) {
environment.systemPackages = with pkgs; [
docker
docker-compose
];
systemd.services.docker-desktop-proxy = {
description = "Docker Desktop proxy";
script = ''
${config.wsl.automountPath}/wsl/docker-desktop/docker-desktop-user-distro proxy --docker-desktop-root ${config.wsl.automountPath}/wsl/docker-desktop
'';
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Restart = "on-failure";
RestartSec = "30s";
};
};
users.groups.docker.members = [
config.wsl.defaultUser
];
};
}

View file

@ -0,0 +1,40 @@
{ config, lib, pkgs, ... }:
with builtins; with lib; {
options.wsl.docker-native = with types; {
enable = mkEnableOption "Native Docker integration in NixOS.";
addToDockerGroup = mkOption {
type = bool;
default = config.security.sudo.wheelNeedsPassword;
description = ''
Wether to add the default user to the docker group.
This is not recommended, if you have a password, because it essentially permits unauthenticated root access.
'';
};
};
config =
let
cfg = config.wsl.docker-native;
in
mkIf (config.wsl.enable && cfg.enable) {
nixpkgs.overlays = [
(self: super: {
docker = super.docker.override { iptables = pkgs.iptables-legacy; };
})
];
environment.systemPackages = with pkgs; [
docker
docker-compose
];
virtualisation.docker.enable = true;
users.groups.docker.members = lib.mkIf cfg.addToDockerGroup [
config.wsl.defaultUser
];
};
}

View file

@ -0,0 +1,73 @@
{ config, lib, pkgs, ... }:
with builtins; with lib; {
config = mkIf config.wsl.enable (
let
mkTarball = pkgs.callPackage "${lib.cleanSource pkgs.path}/nixos/lib/make-system-tarball.nix";
pkgs2storeContents = map (x: { object = x; symlink = "none"; });
rootfs = let tarball = config.system.build.tarball; in "${tarball}/tarball/${tarball.fileName}.tar${tarball.extension}";
installer = pkgs.writeScript "installer.sh" ''
#!${pkgs.busybox}/bin/sh
BASEPATH=$PATH
export PATH=$BASEPATH:${pkgs.busybox}/bin # Add busybox to path
set -e
cd /
echo "Unpacking root file system..."
${pkgs.pv}/bin/pv ${rootfs} | tar xz
echo "Activating nix configuration..."
/nix/var/nix/profiles/system/activate
PATH=$BASEPATH:/run/current-system/sw/bin # Use packages from target system
echo "Cleaning up installer files..."
nix-collect-garbage
rm /nix-path-registration
echo "Optimizing store..."
nix-store --optimize
# Don't package the shell here, it's contained in the rootfs
exec ${builtins.unsafeDiscardStringContext config.users.users.root.shell} "$@"
'';
# Set installer.sh as the root shell
passwd = pkgs.writeText "passwd" ''
root:x:0:0:System administrator:/root:${installer}
'';
in
{
system.build.installer = mkTarball {
fileName = "nixos-wsl-installer";
compressCommand = "gzip";
compressionExtension = ".gz";
extraArgs = "--hard-dereference";
storeContents = with pkgs; pkgs2storeContents [
installer
];
contents = [
{ source = config.environment.etc."wsl.conf".source; target = "/etc/wsl.conf"; }
{ source = config.environment.etc."fstab".source; target = "/etc/fstab"; }
{ source = passwd; target = "/etc/passwd"; }
{ source = "${pkgs.busybox}/bin/busybox"; target = "/bin/sh"; }
{ source = "${pkgs.busybox}/bin/busybox"; target = "/bin/mount"; }
];
extraCommands = pkgs.writeShellScript "prepare" ''
export PATH=$PATH:${pkgs.coreutils}/bin
mkdir -p bin
ln -s /init bin/wslpath
'';
};
}
);
}

View file

@ -0,0 +1,88 @@
{ lib, pkgs, config, ... }:
with builtins; with lib;
{
imports = [
(mkRenamedOptionModule [ "wsl" "compatibility" "interopPreserveArgvZero" ] [ "wsl" "interop" "preserveArgvZero" ])
];
options.wsl.interop = with types; {
register = mkOption {
type = bool;
default = false; # Use the existing registration by default
description = "Explicitly register the binfmt_misc handler for Windows executables";
};
includePath = mkOption {
type = bool;
default = true;
description = "Include Windows PATH in WSL PATH";
};
preserveArgvZero = mkOption {
type = nullOr bool;
default = null;
description = ''
Register binfmt interpreter for Windows executables with 'preserves argv[0]' flag.
Default (null): autodetect, at some performance cost.
To avoid the performance cost, set this to true for WSL Preview 0.58 and up,
or to false for any older versions, including pre-Microsoft Store and Windows 10.
'';
};
};
config =
let
cfg = config.wsl.interop;
in
mkIf config.wsl.enable {
boot.binfmt.registrations = mkIf cfg.register {
WSLInterop =
let
compat = cfg.preserveArgvZero;
# WSL Preview 0.58 and up registers the /init binfmt interp for Windows executable
# with the "preserve argv[0]" flag, so if you run `./foo.exe`, the interp gets invoked
# as `/init foo.exe ./foo.exe`.
# argv[0] --^ ^-- actual path
#
# Older versions expect to be called without the argv[0] bit, simply as `/init ./foo.exe`.
#
# We detect that by running `/init /known-not-existing-path.exe` and checking the exit code:
# the new style interp expects at least two arguments, so exits with exit code 1,
# presumably meaning "parsing error"; the old style interp attempts to actually run
# the executable, fails to find it, and exits with 255.
compatWrapper = pkgs.writeShellScript "nixos-wsl-binfmt-hack" ''
/init /nixos-wsl-does-not-exist.exe
[ $? -eq 255 ] && shift
exec /init "$@"
'';
# use the autodetect hack if unset, otherwise call /init directly
interpreter = if compat == null then compatWrapper else "/init";
# enable for the wrapper and autodetect hack
preserveArgvZero = if compat == false then false else true;
in
{
magicOrExtension = "MZ";
fixBinary = true;
wrapInterpreterInShell = false;
inherit interpreter preserveArgvZero;
};
};
# Include Windows %PATH% in Linux $PATH.
environment.extraInit = mkIf cfg.includePath ''PATH="$PATH:$WSLPATH"'';
warnings =
let
registrations = config.boot.binfmt.registrations;
in
optional (!(registrations ? WSLInterop) && (length (attrNames config.boot.binfmt.registrations)) != 0) "Having any binfmt registrations without re-registering WSLInterop (wsl.interop.register) will break running .exe files from WSL2";
};
}

View file

@ -0,0 +1,139 @@
{ lib, pkgs, config, ... }:
with builtins; with lib;
{
options.wsl = with types;
let
coercedToStr = coercedTo (oneOf [ bool path int ]) (toString) str;
in
{
enable = mkEnableOption "support for running NixOS as a WSL distribution";
automountPath = mkOption {
type = str;
default = "/mnt";
description = "The path where windows drives are mounted (e.g. /mnt/c)";
};
automountOptions = mkOption {
type = str;
default = "metadata,uid=1000,gid=100";
description = "Options to use when mounting windows drives";
};
defaultUser = mkOption {
type = str;
default = "nixos";
description = "The name of the default user";
};
startMenuLaunchers = mkEnableOption "shortcuts for GUI applications in the windows start menu";
wslConf = mkOption {
type = attrsOf (attrsOf (oneOf [ str int bool ]));
description = "Entries that are added to /etc/wsl.conf";
};
};
config =
let
cfg = config.wsl;
syschdemd = import ../syschdemd.nix { inherit lib pkgs config; inherit (cfg) automountPath defaultUser; defaultUserHome = config.users.users.${cfg.defaultUser}.home; };
in
mkIf cfg.enable {
wsl.wslConf = {
automount = {
enabled = true;
mountFsTab = true;
root = "${cfg.automountPath}/";
options = cfg.automountOptions;
};
network = {
generateResolvConf = mkDefault true;
generateHosts = mkDefault true;
};
};
# WSL is closer to a container than anything else
boot.isContainer = true;
environment.noXlibs = lib.mkForce false; # override xlibs not being installed (due to isContainer) to enable the use of GUI apps
hardware.opengl.enable = true; # Enable GPU acceleration
environment = {
etc = {
"wsl.conf".text = generators.toINI { } cfg.wslConf;
# DNS settings are managed by WSL
hosts.enable = !config.wsl.wslConf.network.generateHosts;
"resolv.conf".enable = !config.wsl.wslConf.network.generateResolvConf;
};
systemPackages = [
(pkgs.runCommand "wslpath" { } ''
mkdir -p $out/bin
ln -s /init $out/bin/wslpath
'')
];
};
networking.dhcpcd.enable = false;
users.users.${cfg.defaultUser} = {
isNormalUser = true;
uid = 1000;
extraGroups = [ "wheel" ]; # Allow the default user to use sudo
};
users.users.root = {
shell = "${syschdemd}/bin/syschdemd";
# Otherwise WSL fails to login as root with "initgroups failed 5"
extraGroups = [ "root" ];
};
security.sudo = {
extraConfig = ''
Defaults env_keep+=INSIDE_NAMESPACE
'';
wheelNeedsPassword = mkDefault false; # The default user will not have a password by default
};
system.activationScripts = {
copy-launchers = mkIf cfg.startMenuLaunchers (
stringAfter [ ] ''
for x in applications icons; do
echo "Copying /usr/share/$x"
mkdir -p /usr/share/$x
${pkgs.rsync}/bin/rsync -ar --delete $systemConfig/sw/share/$x/. /usr/share/$x
done
''
);
populateBin = stringAfter [ ] ''
echo "setting up /bin..."
ln -sf /init /bin/wslpath
ln -sf ${pkgs.bashInteractive}/bin/bash /bin/sh
ln -sf ${pkgs.util-linux}/bin/mount /bin/mount
'';
};
systemd = {
# Disable systemd units that don't make sense on WSL
services = {
"serial-getty@ttyS0".enable = false;
"serial-getty@hvc0".enable = false;
"getty@tty1".enable = false;
"autovt@".enable = false;
firewall.enable = false;
systemd-resolved.enable = false;
systemd-udevd.enable = false;
};
tmpfiles.rules = [
# Don't remove the X11 socket
"d /tmp/.X11-unix 1777 root root"
];
# Don't allow emergency mode, because we don't have a console.
enableEmergencyMode = false;
};
warnings = (optional (config.systemd.services.systemd-resolved.enable && config.wsl.wslConf.network.generateResolvConf) "systemd-resolved is enabled, but resolv.conf is managed by WSL");
};
}