├── .envrc ├── .gitignore ├── LICENSE.md ├── README.md ├── bin ├── analyze_path.nix ├── analyze_path.py ├── iterate.sh └── xdg-runtime-dir.sh ├── distros ├── alpine │ ├── default.nix │ ├── etc │ │ └── profile │ ├── generated.json │ ├── generated.nix │ └── update.sh ├── ubuntu │ ├── default.nix │ ├── generated.json │ ├── generated.nix │ └── update.sh └── void │ ├── default.nix │ ├── generated.json │ ├── generated.nix │ └── update.sh ├── flake.lock ├── flake.nix ├── home.nix └── module.nix /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | result* 2 | *.tar* 3 | .direnv 4 | .vscode 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Fernando Ayats (ayatsfer@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ❄ home-manager-wsl 2 | 3 | [Home-manager](https://github.com/nix-community/home-manager) module, that lets you build a [WSL](https://docs.microsoft.com/en-us/windows/wsl/about) distribution tarball, which contains: 4 | 5 | - Alpine/Ubuntu/Void as a base image 6 | - Nix single-user installation 7 | - Your HM config pre-installed and ready to go 🚀 8 | 9 | Home-manager is a project that lets you build reproducible user environments using the [Nix package manager](https://nixos.org/). 10 | 11 | ## ✏️ Installation 12 | 13 | The installation is as simple as possible. You will need a flake-based home-manager config. 14 | 15 | 1. Import the module into your config 16 | ```nix 17 | { 18 | inputs = { 19 | # ... 20 | home-manager-wsl.url = "github:viperML/home-manager-wsl"; 21 | }; 22 | outputs = { 23 | # ... 24 | home-manager-wsl, 25 | }: { 26 | homeConfigurations."USERNAME" = home-manager.lib.homeManagerConfiguration { 27 | modules = [ 28 | # ... 29 | home-manager-wsl.homeModules.default 30 | ]; 31 | }; 32 | }; 33 | } 34 | ``` 35 | 36 | 2. Build the tarball 37 | 38 | ```console 39 | nix build /path/to/your/flake#homeConfigurations.USERNAME.config.wsl.tarball 40 | ``` 41 | 42 | 3. Install 43 | 44 | Copy the resulting tarball under `./result/.tar.gz` into your Windows Host with WSL2 enabled. 45 | 46 | Then, import it with `wsl --import ` 47 | 48 | 49 | ## 💨 Quick start 50 | 51 | If you want to give it a spin without setting up a new flake, you can build the sample config of this repo. 52 | 53 | ```console 54 | nix build github:viperML/home-manager-wsl 55 | ``` 56 | 57 | And follow the installation guide from step 3. 58 | 59 | 60 | 61 | ## 📐 Design considerations 62 | 63 | ### NixOS-WSL already exists 64 | 65 | The project [NixOS-WSL](https://github.com/nix-community/NixOS-WSL) already provides a fully NixOS based WSL image. It is fantastic, but the main problem that I had with it is that it runs `systemd`. This brings some problems, because now any commands run from windows with the form `wsl.exe -d NixOS ls ~` will be run under the root user. In my experience, not running systemd means faster boot times. 66 | 67 | Moreover, using an FHS distro with a runtime dynamic linker, simplifies the integration with the WSL ecosystem, where many tools will download dynamically-linked binaries (VSCode for example). 68 | 69 | 70 | ### Base distros 71 | 72 | You can choose between some Linux Distributions to use a base. The full list is just the contents of [./distros](./distros/), and the configuration option is `wsl.baseDistro = ""`. 73 | 74 | The default is Alpine Linux, as it provides a clean environment without much binaries in `PATH`, which makes the development experience similar to NixOS. Void should also provide a minimal base, but using `gnu libc` instead of `musl libc`. 75 | 76 | 77 | ## 📄 Disclaimer 78 | 79 | This project is not directly affiliated with home-manager. 80 | -------------------------------------------------------------------------------- /bin/analyze_path.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenvNoCC, 3 | python3Minimal, 4 | }: 5 | stdenvNoCC.mkDerivation rec { 6 | name = "analyze_path"; 7 | propagatedBuildInputs = [ 8 | python3Minimal 9 | ]; 10 | dontBuild = true; 11 | dontUnpack = true; 12 | installPhase = '' 13 | install -Dm555 ${./. + "/${name}.py"} $out/bin/${name} 14 | ''; 15 | } 16 | -------------------------------------------------------------------------------- /bin/analyze_path.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | from pathlib import Path 5 | 6 | """ 7 | Small script that compares the binaries provided by the base distribution, 8 | to the binaries provided by the home-manager profile. 9 | """ 10 | 11 | FHS_PATH = [ 12 | "/bin", 13 | "/usr/bin", 14 | "/sbin", 15 | "/usr/sbin", 16 | ] 17 | 18 | FHS_PATH = [Path(x) if Path(x).exists() else None for x in FHS_PATH] 19 | 20 | fhs_programs = set() 21 | for path in FHS_PATH: 22 | fhs_programs = fhs_programs | {b.name for b in path.glob("*")} 23 | 24 | PROFILE_PATH = Path(f"/nix/var/nix/profiles/per-user/{os.environ['USER']}/profile/bin") 25 | 26 | nix_programs = {p.name for p in PROFILE_PATH.glob("*")} 27 | 28 | only_in_fhs = sorted(fhs_programs - nix_programs) 29 | 30 | print("Binaries not in nix profile:", file=sys.stderr) 31 | for command in only_in_fhs: 32 | print(command) 33 | -------------------------------------------------------------------------------- /bin/iterate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | CONFIG="${1:-sample}" 5 | 6 | NAME=nix 7 | 8 | DIR="$(cd $(dirname $BASH_SOURCE)/..; pwd)" 9 | 10 | nix build "$DIR#homeConfigurations.$CONFIG.config.wsl.tarball" -L 11 | cp -fvL result/*.tar* ~/Desktop/wsl.tar.gz 12 | 13 | set +e 14 | wsl.exe --unregister $NAME || : 15 | set -e 16 | 17 | wsl.exe --import $NAME "C:\WSL\\$NAME" "C:\Users\\$USER\Desktop\\wsl.tar.gz" 18 | wsl.exe -d nix 19 | -------------------------------------------------------------------------------- /bin/xdg-runtime-dir.sh: -------------------------------------------------------------------------------- 1 | if [ -n "$HOME" ] && [ -n "$USER" ] && [ -z "$XDG_RUNTIME_DIR" ]; then 2 | # We can't use /run, only root has write access 3 | export XDG_RUNTIME_DIR="/tmp/user-$(id -u)-xdg-runtime" 4 | mkdir -p -m 700 "$XDG_RUNTIME_DIR" 5 | fi 6 | -------------------------------------------------------------------------------- /distros/alpine/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | # 4 | runCommand, 5 | callPackage, 6 | lib, 7 | bubblewrap, 8 | }: 9 | runCommand "alpine-rootfs" { 10 | src = (callPackage ./generated.nix {}).rootfs.src; 11 | } '' 12 | set -xe 13 | trap "set +x" ERR 14 | 15 | gzip -d $src -c > $out 16 | 17 | tar -xpf $out 18 | 19 | ${lib.concatMapStringsSep "\n" (c: '' 20 | ${bubblewrap}/bin/bwrap \ 21 | --bind $PWD / \ 22 | --uid 0 \ 23 | --gid 0 \ 24 | --setenv PATH /bin:/sbin:/usr/bin:/usr/sbin \ 25 | -- ${c} 26 | '') [ 27 | "adduser --help" 28 | "adduser -h ${config.home.homeDirectory} -s /bin/sh -G users -D ${config.home.username}" 29 | "addgroup ${config.home.username} wheel" 30 | ]} 31 | 32 | cp -vfL ${./etc/profile} etc/profile 33 | 34 | 35 | ${lib.concatMapStringsSep "\n" (c: '' 36 | tar \ 37 | -rvf $out \ 38 | --numeric-owner \ 39 | --hard-dereference \ 40 | --mtime='@1' \ 41 | ${c} 42 | '') [ 43 | "--owner=0 --group=0 ./etc/passwd" 44 | "--owner=0 --group=42 ./etc/shadow" 45 | "--owner=0 --group=0 ./etc/group" 46 | "--owner=0 --group=0 ./etc/profile" 47 | ]} 48 | 49 | set +x 50 | '' 51 | -------------------------------------------------------------------------------- /distros/alpine/etc/profile: -------------------------------------------------------------------------------- 1 | export CHARSET=UTF-8 2 | export LANG=C.UTF-8 3 | # export PAGER=less 4 | umask 022 5 | 6 | if [ -d /etc/profile.d ]; then 7 | for i in /etc/profile.d/*.sh; do 8 | if [ -r "$i" ]; then 9 | # shellcheck source=/dev/null 10 | . "$i" 11 | fi 12 | done 13 | unset i 14 | fi 15 | -------------------------------------------------------------------------------- /distros/alpine/generated.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootfs": { 3 | "cargoLocks": null, 4 | "extract": null, 5 | "name": "rootfs", 6 | "passthru": null, 7 | "pinned": false, 8 | "src": { 9 | "name": null, 10 | "sha256": "sha256-vqYIya4BkAlvFnHfvvnWhocBaB44cO7K1ljd4DLFiFw=", 11 | "type": "url", 12 | "url": "https://dl-cdn.alpinelinux.org/alpine/v3.16/releases/x86_64/alpine-minirootfs-3.16.2-x86_64.tar.gz" 13 | }, 14 | "version": "3.16.2" 15 | } 16 | } -------------------------------------------------------------------------------- /distros/alpine/generated.nix: -------------------------------------------------------------------------------- 1 | # This file was generated by nvfetcher, please do not modify it manually. 2 | { fetchgit, fetchurl, fetchFromGitHub }: 3 | { 4 | rootfs = { 5 | pname = "rootfs"; 6 | version = "3.16.2"; 7 | src = fetchurl { 8 | url = "https://dl-cdn.alpinelinux.org/alpine/v3.16/releases/x86_64/alpine-minirootfs-3.16.2-x86_64.tar.gz"; 9 | sha256 = "sha256-vqYIya4BkAlvFnHfvvnWhocBaB44cO7K1ljd4DLFiFw="; 10 | }; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /distros/alpine/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TEMP="$(mktemp -d)" 4 | DIR="$(cd $(dirname $BASH_SOURCE); pwd)" 5 | 6 | 7 | curl -o "$TEMP/latest-releases.yaml" -Ls "https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/latest-releases.yaml" 8 | 9 | RELEASE="$(yq -r '.[] | select( .flavor | contains("alpine-minirootfs") ) | .version' $TEMP/latest-releases.yaml)" 10 | IFS=$'\n' read -d "" -ra arr <<< "${RELEASE//./$'\n'}" 11 | MAJOR_RELEASE="$(printf ${arr[0]}.${arr[1]})" 12 | 13 | tee "$TEMP/nvfetcher.toml" < $out 16 | 17 | set +e 18 | tar -xpf $out 19 | set -e 20 | 21 | 22 | ${lib.concatMapStringsSep "\n" (c: '' 23 | ${bubblewrap}/bin/bwrap \ 24 | --bind $PWD / \ 25 | --uid 0 \ 26 | --gid 0 \ 27 | --setenv PATH /bin:/sbin:/usr/bin:/usr/sbin \ 28 | -- ${c} 29 | '') [ 30 | "useradd -m -N -g 100 -d ${config.home.homeDirectory} ${config.home.username}" 31 | ]} 32 | 33 | tee etc/sudoers.d/${config.home.username} < $out 16 | 17 | set +e 18 | tar -xpf $out 19 | set -e 20 | 21 | 22 | ${lib.concatMapStringsSep "\n" (c: '' 23 | ${bubblewrap}/bin/bwrap \ 24 | --bind $PWD / \ 25 | --uid 0 \ 26 | --gid 0 \ 27 | --setenv PATH /bin:/sbin:/usr/bin:/usr/sbin \ 28 | -- ${c} 29 | '') [ 30 | # useradd fails with bad permissions otherwise 31 | "chmod u+w -R /etc" 32 | "useradd -m -N -g 100 -d ${config.home.homeDirectory} ${config.home.username}" 33 | ]} 34 | 35 | tee etc/sudoers.d/${config.home.username} < ${outFile} 241 | '' 242 | else "cp -v result.tar ${outFile}" 243 | } 244 | 245 | set +x 246 | ''; 247 | }; 248 | }; 249 | } 250 | --------------------------------------------------------------------------------