From 40f190cad227e996d90c6170bd830f3f215ce9b8 Mon Sep 17 00:00:00 2001 From: K900 Date: Fri, 23 Sep 2022 22:06:07 +0300 Subject: [PATCH] feat: native systemd support --- .gitignore | 1 + .vscode/settings.json | 5 + configuration.nix | 3 +- flake.nix | 13 +- modules/installer.nix | 4 +- modules/interop.nix | 3 - modules/wsl-distro.nix | 270 ++++++++++++++---------- scripts/native-systemd-shim/Cargo.lock | 166 +++++++++++++++ scripts/native-systemd-shim/Cargo.toml | 10 + scripts/native-systemd-shim/shim.nix | 8 + scripts/native-systemd-shim/src/main.rs | 75 +++++++ 11 files changed, 431 insertions(+), 127 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 scripts/native-systemd-shim/Cargo.lock create mode 100644 scripts/native-systemd-shim/Cargo.toml create mode 100644 scripts/native-systemd-shim/shim.nix create mode 100644 scripts/native-systemd-shim/src/main.rs diff --git a/.gitignore b/.gitignore index 750baebf..0792b54e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ result result-* +scripts/*/target diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..946ae790 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + "scripts/native-systemd-shim/Cargo.toml" + ] +} \ No newline at end of file diff --git a/configuration.nix b/configuration.nix index 16542b48..c76d59ea 100644 --- a/configuration.nix +++ b/configuration.nix @@ -1,6 +1,5 @@ -{ lib, pkgs, config, modulesPath, ... }: +{ pkgs, config, modulesPath, ... }: -with lib; let nixos-wsl = import ./default.nix; in diff --git a/flake.nix b/flake.nix index d03007fe..e344c372 100644 --- a/flake.nix +++ b/flake.nix @@ -34,7 +34,7 @@ } // flake-utils.lib.eachSystem - (with flake-utils.lib.system; [ "x86_64-linux" "aarch64-linux" ]) + [ "x86_64-linux" "aarch64-linux" ] (system: let pkgs = import nixpkgs { inherit system; }; @@ -49,7 +49,16 @@ }; devShell = pkgs.mkShell { - nativeBuildInputs = with pkgs; [ nixpkgs-fmt shfmt ]; + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + + nativeBuildInputs = with pkgs; [ + nixpkgs-fmt + shfmt + rustc + cargo + rustfmt + clippy + ]; }; } ); diff --git a/modules/installer.nix b/modules/installer.nix index 45d191ac..3fd404f2 100644 --- a/modules/installer.nix +++ b/modules/installer.nix @@ -48,9 +48,7 @@ with builtins; with lib; { compressionExtension = ".gz"; extraArgs = "--hard-dereference"; - storeContents = with pkgs; pkgs2storeContents [ - installer - ]; + storeContents = pkgs2storeContents [ installer ]; contents = [ { source = config.environment.etc."wsl.conf".source; target = "/etc/wsl.conf"; } diff --git a/modules/interop.nix b/modules/interop.nix index b7babfb8..d055d4ba 100644 --- a/modules/interop.nix +++ b/modules/interop.nix @@ -74,9 +74,6 @@ with builtins; with lib; }; }; - # Include Windows %PATH% in Linux $PATH. - environment.extraInit = mkIf cfg.includePath ''PATH="$PATH:$WSLPATH"''; - warnings = let registrations = config.boot.binfmt.registrations; diff --git a/modules/wsl-distro.nix b/modules/wsl-distro.nix index 9b187d1c..ded84a3b 100644 --- a/modules/wsl-distro.nix +++ b/modules/wsl-distro.nix @@ -2,144 +2,180 @@ 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 [ string int bool ])); - description = "Entries that are added to /etc/wsl.conf"; - }; + options.wsl = with types; { + enable = mkEnableOption "support for running NixOS as a WSL distribution"; + nativeSystemd = mkOption { + type = bool; + default = false; + description = "Use native WSL systemd support"; + }; + 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 [ string int bool ])); + description = "Entries that are added to /etc/wsl.conf"; }; + }; config = let cfg = config.wsl; syschdemd = pkgs.callPackage ../scripts/syschdemd.nix { inherit (cfg) automountPath defaultUser; }; + shim = pkgs.callPackage ../scripts/native-systemd-shim/shim.nix { }; + + bashWrapper = pkgs.runCommand "nixos-wsl-bash-wrapper" { nativeBuildInputs = [ pkgs.makeWrapper ]; } '' + makeWrapper ${pkgs.bashInteractive}/bin/sh $out/bin/sh --prefix PATH ':' ${lib.makeBinPath [pkgs.systemd pkgs.gnugrep]} + ''; + + bash = if cfg.nativeSystemd then bashWrapper else pkgs.bashInteractive; in - mkIf cfg.enable { - - wsl.wslConf = { - automount = { - enabled = true; - mountFsTab = true; - root = "${cfg.automountPath}/"; - options = cfg.automountOptions; + mkMerge [ + (mkIf cfg.enable { + wsl.wslConf = { + automount = { + enabled = true; + mountFsTab = true; + root = "${cfg.automountPath}/"; + options = cfg.automountOptions; + }; + network = { + generateResolvConf = mkDefault true; + generateHosts = mkDefault true; + }; }; - network = { - generateResolvConf = mkDefault true; - generateHosts = mkDefault true; - }; - }; - - # We don't need a boot loader - boot.loader.grub.enable = false; - system.build.installBootLoader = "${pkgs.coreutils}/bin/true"; - boot.initrd.enable = false; - system.build.initialRamdisk = pkgs.runCommand "fake-initrd" { } '' - mkdir $out - touch $out/${config.system.boot.loader.initrdFile} - ''; - system.build.initialRamdiskSecretAppender = pkgs.writeShellScriptBin "append-initrd-secrets" ""; - hardware.opengl.enable = true; # Enable GPU acceleration + # We don't need a boot loader + boot.loader.grub.enable = false; + system.build.installBootLoader = "${pkgs.coreutils}/bin/true"; + boot.initrd.enable = false; + system.build.initialRamdisk = pkgs.runCommand "fake-initrd" { } '' + mkdir $out + touch $out/${config.system.boot.loader.initrdFile} + ''; + system.build.initialRamdiskSecretAppender = pkgs.writeShellScriptBin "append-initrd-secrets" ""; + + hardware.opengl.enable = true; # Enable GPU acceleration - environment = { + environment = { - etc = { - "wsl.conf".text = generators.toINI { } cfg.wslConf; + 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; - }; + # 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 - '') - ]; - }; + systemPackages = [ + (pkgs.runCommand "wslpath" { } '' + mkdir -p $out/bin + ln -s /init $out/bin/wslpath + '') + ]; + }; - networking.dhcpcd.enable = false; + networking.dhcpcd.enable = false; - users.users.${cfg.defaultUser} = { - isNormalUser = true; - uid = 1000; - extraGroups = [ "wheel" ]; # Allow the default user to use sudo - }; + 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" ]; - }; + users.users.root.extraGroups = [ "root" ]; + + security.sudo.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 ${bash}/bin/sh /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; + }; - security.sudo = { - extraConfig = '' + warnings = (optional (config.systemd.services.systemd-resolved.enable && config.wsl.wslConf.network.generateResolvConf) "systemd-resolved is enabled, but resolv.conf is managed by WSL"); + }) + (mkIf (!cfg.nativeSystemd) { + users.users.root.shell = "${syschdemd}/bin/syschdemd"; + 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; + wsl.wslConf.users.default = "root"; + + # Include Windows %PATH% in Linux $PATH. + environment.extraInit = mkIf cfg.interop.includePath ''PATH="$PATH:$WSLPATH"''; + }) + (mkIf cfg.nativeSystemd { + wsl.wslConf = { + user.default = cfg.defaultUser; + boot.systemd = true; }; - 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; - }; + system.activationScripts = { + shimSystemd = stringAfter [ ] '' + echo "setting up /sbin/init shim..." + mkdir -p /sbin + ln -sf ${shim}/bin/nixos-wsl-native-systemd-shim /sbin/init + ''; + }; - warnings = (optional (config.systemd.services.systemd-resolved.enable && config.wsl.wslConf.network.generateResolvConf) "systemd-resolved is enabled, but resolv.conf is managed by WSL"); - }; + environment = { + # preserve $PATH from parent + variables.PATH = [ "$PATH" ]; + extraInit = '' + export WSLPATH=$(echo "$PATH" | tr ':' '\n' | grep -E "^${cfg.automountPath}" | tr '\n' ':') + ${if cfg.interop.includePath then "" else '' + export PATH=$(echo "$PATH" | tr ':' '\n' | grep -vE "^${cfg.automountPath}" | tr '\n' ':') + ''} + ''; + }; + }) + ]; } diff --git a/scripts/native-systemd-shim/Cargo.lock b/scripts/native-systemd-shim/Cargo.lock new file mode 100644 index 00000000..435582fd --- /dev/null +++ b/scripts/native-systemd-shim/Cargo.lock @@ -0,0 +1,166 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +dependencies = [ + "backtrace", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + +[[package]] +name = "kernlog" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dd79b6ea06a97c93bc5587fba4c2ed6d723b939d35d5e3fb3c6870d12e6d17" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "libc" +version = "0.2.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "nix" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nixos-wsl-native-systemd-shim" +version = "0.1.0" +dependencies = [ + "anyhow", + "kernlog", + "log", + "nix", +] + +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" diff --git a/scripts/native-systemd-shim/Cargo.toml b/scripts/native-systemd-shim/Cargo.toml new file mode 100644 index 00000000..033c3d09 --- /dev/null +++ b/scripts/native-systemd-shim/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nixos-wsl-native-systemd-shim" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { version = "*", features = ["backtrace"] } +nix = { version = "*", features = ["process"] } +log = "*" +kernlog = "*" \ No newline at end of file diff --git a/scripts/native-systemd-shim/shim.nix b/scripts/native-systemd-shim/shim.nix new file mode 100644 index 00000000..d6c2b9ed --- /dev/null +++ b/scripts/native-systemd-shim/shim.nix @@ -0,0 +1,8 @@ +{ rustPlatform }: +rustPlatform.buildRustPackage { + name = "nixos-wsl-native-systemd-shim"; + version = "1.0.0"; + + src = ./.; + cargoLock.lockFile = ./Cargo.lock; +} diff --git a/scripts/native-systemd-shim/src/main.rs b/scripts/native-systemd-shim/src/main.rs new file mode 100644 index 00000000..31867240 --- /dev/null +++ b/scripts/native-systemd-shim/src/main.rs @@ -0,0 +1,75 @@ +use anyhow::Context; +use nix::errno::Errno; +use nix::mount::{mount, MsFlags}; +use nix::sys::wait::{waitid, Id, WaitPidFlag}; +use nix::unistd::Pid; +use std::env; +use std::fs::OpenOptions; +use std::os::unix::io::{FromRawFd, IntoRawFd}; +use std::os::unix::process::CommandExt; +use std::process::{Command, Stdio}; + +fn real_main() -> anyhow::Result<()> { + log::trace!("Remounting /nix/store read-only..."); + + mount( + Some("/nix/store"), + "/nix/store", + None::<&str>, + MsFlags::MS_BIND, + None::<&str>, + ) + .context("When bind mounting /nix/store")?; + + mount( + Some("/nix/store"), + "/nix/store", + None::<&str>, + MsFlags::MS_BIND | MsFlags::MS_REMOUNT | MsFlags::MS_RDONLY, + None::<&str>, + ) + .context("When remounting /nix/store read-only")?; + + log::trace!("Running activation script..."); + + let kmsg_fd = OpenOptions::new() + .write(true) + .open("/dev/kmsg") + .context("When opening /dev/kmsg")? + .into_raw_fd(); + + let child = Command::new("/nix/var/nix/profiles/system/activate") + .env("LANG", "C.UTF-8") + // SAFETY: we just opened this + .stdout(unsafe { Stdio::from_raw_fd(kmsg_fd) }) + .stderr(unsafe { Stdio::from_raw_fd(kmsg_fd) }) + .spawn() + .context("When activating")?; + + let pid = Pid::from_raw(child.id() as i32); + + // If the child catches SIGCHLD, `waitid` will wait for it to exit, then return ECHILD. + // Why? Because POSIX is terrible. + let result = waitid(Id::Pid(pid), WaitPidFlag::WEXITED); + match result { + Ok(_) | Err(Errno::ECHILD) => {} + Err(e) => return Err(e).context("When waiting"), + }; + + log::trace!("Spawning real systemd..."); + + // if things go right, we will never return from here + Err( + Command::new("/nix/var/nix/profiles/system/systemd/lib/systemd/systemd") + .args(env::args_os()) + .exec() + .into(), + ) +} + +fn main() { + env::set_var("RUST_BACKTRACE", "1"); + kernlog::init().expect("Failed to set up logger..."); + let result = real_main(); + log::error!("Error: {:?}", result); +}