diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..c4f5ea4b --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +use_flake() { + watch_file flake.nix + watch_file flake.lock + eval "$(nix print-dev-env)" +} + +use flake diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index acfd0ea7..f5e552c4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,10 +28,21 @@ jobs: run: | nix build '.#nixosConfigurations.mysystem.config.system.build.tarball' - - uses: actions/upload-artifact@v2 + - name: Upload tarball + uses: actions/upload-artifact@v2 with: - name: install.tar.gz - path: result/tarball/nixos-system-x86_64-linux.tar.gz + name: rootfs + path: result/tarball/nixos-wsl-x86_64-linux.tar.gz + + - name: Build installer + run: | + nix build '.#nixosConfigurations.mysystem.config.system.build.installer' + + - name: Upload installer + uses: actions/upload-artifact@v2 + with: + name: installer + path: result/tarball/nixos-wsl-installer.tar.gz release: if: startsWith(github.ref, 'refs/tags/') @@ -43,7 +54,11 @@ jobs: - uses: actions/download-artifact@v2 with: - name: install.tar.gz + name: rootfs + + - uses: actions/download-artifact@v2 + with: + name: installer - name: Create Release id: create_release @@ -59,7 +74,17 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} # - asset_name: nixos-system-x86_64-linux.tar.gz - asset_path: nixos-system-x86_64-linux.tar.gz + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_name: nixos-wsl-x86_64-linux.tar.gz + asset_path: result/tarball/nixos-wsl-x86_64-linux.tar.gz + asset_content_type: application/gzip + + - name: Upload installer as release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_name: nixos-wsl-installer.tar.gz + asset_path: result/tarball/nixos-wsl-installer.tar.gz asset_content_type: application/gzip diff --git a/README.rst b/README.rst index a1cb8b26..8fceda69 100644 --- a/README.rst +++ b/README.rst @@ -15,10 +15,10 @@ First, `download the latest release's system tarball Then open up a Terminal, PowerShell or Command Prompt and run:: - wsl --import NixOS .\NixOS\ nixos-system-x86_64-linux.tar.gz --version 2 + wsl --import NixOS .\NixOS\ nixos-wsl-x86_64-linux.tar.gz --version 2 This sets up a new WSL distribution ``NixOS`` that is installed under -``.\NixOS``. ``nixos-system-x86_64-linux.tar.gz`` is the path to the file you +``.\NixOS``. ``nixos-wsl-x86_64-linux.tar.gz`` is the path to the file you downloaded earlier. You might need to change this path or change to the download directory first. @@ -72,10 +72,10 @@ Or, if you want to build with local changes, run inside your checkout:: Without a flakes-enabled Nix, you can build a tarball using:: - nix-build -A system -A config.system.build.tarball ./nixos.nix + nix-build -A nixosConfigurations.mysystem.config.system.build.tarball The resulting mini rootfs can then be found under -``./result-2/tarball/nixos-system-x86_64-linux.tar.gz``. +``./result/tarball/nixos-wsl-x86_64-linux.tar.gz``. License diff --git a/configuration.nix b/configuration.nix index 6d19de0d..89b700be 100644 --- a/configuration.nix +++ b/configuration.nix @@ -2,45 +2,29 @@ with lib; let - defaultUser = "nixos"; syschdemd = import ./syschdemd.nix { inherit lib pkgs config defaultUser; }; + nixos-wsl = import ./default.nix; in { imports = [ "${modulesPath}/profiles/minimal.nix" - ]; - - # WSL is closer to a container than anything else - boot.isContainer = true; - environment.etc.hosts.enable = false; - environment.etc."resolv.conf".enable = false; - - networking.dhcpcd.enable = false; + nixos-wsl.nixosModules.wsl + ]; - users.users.${defaultUser} = { - isNormalUser = true; - extraGroups = [ "wheel" ]; - }; + wsl = { + enable = true; + automountPath = "/mnt"; + defaultUser = "nixos"; + startMenuLaunchers = true; - users.users.root = { - shell = "${syschdemd}/bin/syschdemd"; - # Otherwise WSL fails to login as root with "initgroups failed 5" - extraGroups = [ "root" ]; + # Enable integration with Docker Desktop (needs to be installed) + # docker.enable = true; }; - security.sudo.wheelNeedsPassword = false; - - # Disable systemd units that don't make sense on WSL - systemd.services."serial-getty@ttyS0".enable = false; - systemd.services."serial-getty@hvc0".enable = false; - systemd.services."getty@tty1".enable = false; - systemd.services."autovt@".enable = false; - - systemd.services.firewall.enable = false; - systemd.services.systemd-resolved.enable = false; - systemd.services.systemd-udevd.enable = false; - - # Don't allow emergency mode, because we don't have a console. - systemd.enableEmergencyMode = false; + # Enable nix flakes + nix.package = pkgs.nixFlakes; + nix.extraOptions = '' + experimental-features = nix-command flakes + ''; } diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..873ece49 --- /dev/null +++ b/default.nix @@ -0,0 +1,13 @@ +(import + ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { + src = ./.; + }).defaultNix diff --git a/flake.lock b/flake.lock index 22ca9ed2..48993f77 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,28 @@ { "nodes": { + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1641205782, + "narHash": "sha256-4jY7RCWUoZ9cKD8co0/4tFARpWB+57+r1bLLvXNJliY=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "b7547d3eed6f32d06102ead8991ec52ab0a4f1a7", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "flake-utils": { "locked": { - "lastModified": 1617631617, - "narHash": "sha256-PARRCz55qN3gy07VJZIlFeOX420d0nGF0RzGI/9hVlw=", + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", "owner": "numtide", "repo": "flake-utils", - "rev": "b2c27d1a81b0dc266270fa8aeecebbd1807fc610", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", "type": "github" }, "original": { @@ -17,11 +33,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1618064689, - "narHash": "sha256-MGnHpM3bSd5JClA+1ad+jvcfNr0HrYoGmqCc2s7thkM=", + "lastModified": 1625876282, + "narHash": "sha256-z/Gv+11Uzdv3HcQCY+xGi1+iuc3rmCaPzxxDSMnuRAI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ad7604ddbd9b299701fb5e15bc39cff80deffce2", + "rev": "068984c00e0d4e54b6684d98f6ac47c92dcb642e", "type": "github" }, "original": { @@ -32,6 +48,7 @@ }, "root": { "inputs": { + "flake-compat": "flake-compat", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } diff --git a/flake.nix b/flake.nix index 9e44244b..0c19ef4b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,19 +1,34 @@ { description = "NixOS WSL"; - inputs.nixpkgs.url = "nixpkgs/nixos-20.09"; - inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs = { + nixpkgs.url = "nixpkgs/nixos-20.09"; + flake-utils.url = "github:numtide/flake-utils"; + + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; + }; outputs = { self, nixpkgs, flake-utils, ... }: { + nixosModules.wsl = { + imports = [ + ./modules/build-tarball.nix + ./modules/wsl-distro.nix + ./modules/docker-desktop.nix + ./modules/installer.nix + ]; + }; + nixosConfigurations.mysystem = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ - (import ./configuration.nix) - (import ./build-tarball.nix) + ./configuration.nix ]; - specialArgs = { inherit nixpkgs; }; }; + } // flake-utils.lib.eachDefaultSystem (system: let diff --git a/build-tarball.nix b/modules/build-tarball.nix similarity index 66% rename from build-tarball.nix rename to modules/build-tarball.nix index cbeb1dd1..03d6a609 100644 --- a/build-tarball.nix +++ b/modules/build-tarball.nix @@ -1,6 +1,5 @@ -{ config, pkgs, lib, nixpkgs ? , ... }: - -with lib; +{ config, pkgs, lib, ... }: +with builtins; with lib; let pkgs2storeContents = l: map (x: { object = x; symlink = "none"; }) l; @@ -15,9 +14,9 @@ let if [ ! -e $out/nixos/nixpkgs ]; then ln -s . $out/nixos/nixpkgs fi - echo -n ${config.system.nixos.revision} > $out/nixos/.git-revision - echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix - echo ${config.system.nixos.versionSuffix} | sed -e s/pre// > $out/nixos/svn-revision + 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" '' @@ -47,18 +46,27 @@ let # 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 + # Copy the system configuration - mkdir -p ./etc/nixos - cp ${./configuration.nix} ./etc/nixos/configuration.nix - cp ${./syschdemd.nix} ./etc/nixos/syschdemd.nix - cp ${./syschdemd.sh} ./etc/nixos/syschdemd.sh + 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 havin a flake.nix in /etc/nixos + sed -i 's|import \./default\.nix|import \./nixos-wsl|' ./etc/nixos/configuration.nix ''; + in -{ +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 @@ -71,4 +79,5 @@ in compressCommand = "gzip"; compressionExtension = ".gz"; }; + } diff --git a/modules/docker-desktop.nix b/modules/docker-desktop.nix new file mode 100644 index 00000000..3e0d200f --- /dev/null +++ b/modules/docker-desktop.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, ... }: +with builtins; with lib; { + + options.wsl.docker = with types; { + enable = mkEnableOption "Docker Desktop integration"; + }; + + config = + let + cfg = config.wsl.docker; + 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-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 + ]; + + }; + +} diff --git a/modules/installer.nix b/modules/installer.nix new file mode 100644 index 00000000..65754001 --- /dev/null +++ b/modules/installer.nix @@ -0,0 +1,66 @@ +{ 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 + + + echo "Starting systemd..." + # 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 = passwd; target = "/etc/passwd"; } + ]; + }; + + } + ); + +} diff --git a/modules/wsl-distro.nix b/modules/wsl-distro.nix new file mode 100644 index 00000000..c2eb550f --- /dev/null +++ b/modules/wsl-distro.nix @@ -0,0 +1,109 @@ +{ 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 coercedToStr); + description = "Entries that are added to /etc/wsl.conf"; + }; + }; + + config = + let + cfg = config.wsl; + syschdemd = import ../syschdemd.nix { inherit lib pkgs config; defaultUser = cfg.defaultUser; }; + in + mkIf cfg.enable { + + wsl.wslConf = { + automount = { + enabled = true; + mountFsTab = true; + root = "${cfg.automountPath}/"; + options = cfg.automountOptions; + }; + }; + + # 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 + + environment = { + # Include Windows %PATH% in Linux $PATH. + extraInit = ''PATH="$PATH:$WSLPATH"''; + + etc = { + "wsl.conf".text = generators.toINI { } cfg.wslConf; + + # DNS settings are managed by WSL + hosts.enable = false; + "resolv.conf".enable = false; + }; + }; + + networking.dhcpcd.enable = false; + + users.users.${cfg.defaultUser} = { + isNormalUser = true; + 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 + '' + ); + + # Disable systemd units that don't make sense on WSL + systemd.services."serial-getty@ttyS0".enable = false; + systemd.services."serial-getty@hvc0".enable = false; + systemd.services."getty@tty1".enable = false; + systemd.services."autovt@".enable = false; + + systemd.services.firewall.enable = false; + systemd.services.systemd-resolved.enable = false; + systemd.services.systemd-udevd.enable = false; + + # Don't allow emergency mode, because we don't have a console. + systemd.enableEmergencyMode = false; + }; +} diff --git a/nixos.nix b/nixos.nix deleted file mode 100644 index 3a32460c..00000000 --- a/nixos.nix +++ /dev/null @@ -1,13 +0,0 @@ -# Build with -# nix-build -A system -A config.system.build.tarball ./nixos.nix - -import { - configuration = { - imports = [ - ./configuration.nix - ./build-tarball.nix - ]; - }; - - system = "x86_64-linux"; -} diff --git a/syschdemd.sh b/syschdemd.sh index 7548c949..621ebbb4 100644 --- a/syschdemd.sh +++ b/syschdemd.sh @@ -3,7 +3,7 @@ set -e sw="/nix/var/nix/profiles/system/sw/bin" -systemPath=`${sw}/readlink -f /nix/var/nix/profiles/system` +systemPath=$(${sw}/readlink -f /nix/var/nix/profiles/system) # Needs root to work if [[ $EUID -ne 0 ]]; then @@ -19,16 +19,16 @@ if [ ! -e "/run/systemd.pid" ]; then PATH=/run/current-system/systemd/lib/systemd:@fsPackagesPath@ \ LOCALE_ARCHIVE=/run/current-system/sw/lib/locale/locale-archive \ @daemonize@/bin/daemonize /run/current-system/sw/bin/unshare -fp --mount-proc systemd - /run/current-system/sw/bin/pgrep -xf systemd > /run/systemd.pid + /run/current-system/sw/bin/pgrep -xf systemd >/run/systemd.pid # Wait for systemd to start status=1 while [[ $status -gt 0 ]]; do $sw/sleep 1 status=0 - $sw/nsenter -t $(< /run/systemd.pid) -p -m -- \ - $sw/systemctl is-system-running -q --wait 2>/dev/null \ - || status=$? + $sw/nsenter -t $(/dev/null || + status=$? done fi @@ -40,4 +40,14 @@ if [[ $# -gt 0 ]]; then else cmd="$userShell" fi -exec $sw/nsenter -t $(< /run/systemd.pid) -p -m -- $sw/machinectl -q --uid=@defaultUser@ shell .host /bin/sh -c "cd \"$PWD\"; exec $cmd" + +# Pass external environment but filter variables specific to root user. +exportCmd="$(export -p | $sw/grep -vE ' (HOME|LOGNAME|SHELL|USER)='); export WSLPATH=\"$PATH\"; export INSIDE_NAMESPACE=true" + +if [ -z "${INSIDE_NAMESPACE:-}" ]; then + exec $sw/nsenter -t $(