Skip to content

Instantly share code, notes, and snippets.

Last active May 31, 2024 06:31
Show Gist options
  • Save giuliano108/49ec5bd0a9339db98535bc793ceb5ab4 to your computer and use it in GitHub Desktop.
Save giuliano108/49ec5bd0a9339db98535bc793ceb5ab4 to your computer and use it in GitHub Desktop.

Guix on WSL2

(updated versions of this document, plus more, live here)

This will show you how to get Guix running on WSL2.
We're going to go as "minimal" as possible, without starting off one of the readily available WSL2 distros.
Parts of this guide should help with understanding how to set up any custom distro on WSL, not just Guix.

Disclaimer: I'm a Guix nOOb! (hence going through the trouble of installing it on WSL2)

About WSL

WSL2 distros are, in fact, more like OS containers:

  • A single instance of the Microsoft-shipped Linux Kernel is running at any given time (under Hyper-V).
  • Running, from a command prompt, wsl -d distroname instructs the Kernel to create one such container, the root filesystem coming an ext4.vhdx disk image.
  • The Kernel executes an /init binary which, again, is supplied by Microsoft and cannot be changed/customized. Together with the Kernel itself, init can be found in %systemroot%\System32\lxss\tools.
  • /init executes a shell or the command supplied to wsl.exe
  • /init also takes care of mounting all the file systems, setting up /dev, ...
  • A very useful side-effect of the "single Kernel for all distros" thing is that if, say, you've got a working Ubuntu WSL distro, and a Guix WSL distro that doesn't boot, you can run dmesg under Ubuntu and see what's wrong with Guix. The Kernel is shared, the Kernel ring buffer is also shared.

In this guide we'll use the wsl --import command. Here's what that does:

  • Takes a .tar or .tar.gz rootfs archive.
  • Creates an Hyper-V disk image from it: the ext4.vhdx file we mentioned before.
  • Extracts the contents of the archive into the disk image.
  • Adds a bunch of files/directories to the image, including /init.
  • Stopping the LxssManager service allows you to "mount" ext4.vhdx in Windows. The disk image contents can be inspected with f.e. DiskInternals Linux Reader.
  • Not going to go into the details here, but docker save generates an archive which can then be fed to wsl --import. That's a great way to turn a Docker container into a WSL distro.


I'll prefix each command with the "place" it's been run from.

  • C:\ for a Windows command prompt.
  • (ubuntu) $ for things that are run on an Ubuntu WSL distro. This is used only to create the rootfs. Doesn't have to be Ubuntu, any distro will do. You can also get creative and do everything from Windows.
  • If no "place" is specified, assume the command is run from the Guix WSL distro.

Minimal rootfs archive

Let's start by adding busybox to the archive and nothing else.

C:\Users\Giuliano\Documents\WSL>mkdir guix
(ubuntu) /mnt/c/Users/Giuliano/Documents/WSL/guix $ mkdir rootfs
(ubuntu) /mnt/c/Users/Giuliano/Documents/WSL/guix $ cd rootfs
(ubuntu) /mnt/c/Users/Giuliano/Documents/WSL/guix/rootfs $ curl -LO
(ubuntu) /mnt/c/Users/Giuliano/Documents/WSL/guix/rootfs $ cd ..
(ubuntu) /mnt/c/Users/Giuliano/Documents/WSL/guix $ tar -C rootfs -cvf rootfs.tar .

Now we can try to wsl --import.

Our main working folder contains:

05/01/2020  05:54 PM    <DIR>          .
05/01/2020  05:51 PM    <DIR>          ..
05/01/2020  05:53 PM    <DIR>          rootfs
05/01/2020  05:54 PM           983,040 rootfs.tar

And we only have one Ubuntu distro available:

C:\Users\Giuliano\Documents\WSL\guix>wsl -l
Windows Subsystem for Linux Distributions:

--import command causes ext4.vhdx to be created:

C:\Users\Giuliano\Documents\WSL\guix>wsl --import guix . rootfs.tar
05/01/2020  05:58 PM    <DIR>          .
05/01/2020  05:51 PM    <DIR>          ..
05/01/2020  05:58 PM        66,060,288 ext4.vhdx
05/01/2020  05:53 PM    <DIR>          rootfs
05/01/2020  05:54 PM           983,040 rootfs.tar

...and registers the guix distro:

C:\Users\Giuliano\Documents\WSL\guix>wsl -l
Windows Subsystem for Linux Distributions:

Can we actually run the guix distro? Remember there's nothing but busybox in there so we're going to try busybox's own sh implementation:

C:\Users\Giuliano\Documents\WSL\guix>wsl -d guix /busybox sh


Doesn't work, we are immediately taken back to the Windows prompt. :( Let's try the dmesg-from-Ubuntu trick explained above:

(ubuntu) $ dmesg
[29242.265446] EXT4-fs (sde): mounted filesystem with ordered data mode. Opts: discard,errors=remount-ro,data=ordered
[29242.270345] init: (1) ERROR: ConfigUpdateInformation:2623: creat /etc/hostname failed: 2
[29242.270349] init: (1) ERROR: ConfigUpdateInformation:2657: creat /etc/hosts failed 2
[29242.270494] init: (2) ERROR: UtilCreateProcessAndWait:635: /bin/mount failed with 2
[29242.270583] init: (1) ERROR: UtilCreateProcessAndWait:655: /bin/mount failed with status 0x
[29242.270585] ff00
[29242.270589] init: (1) ERROR: ConfigMountFsTab:2110: Processing fstab with mount -a failed.
[29242.271405] init: (3) ERROR: UtilCreateProcessAndWait:635: /bin/mount failed with 2
[29242.271506] init: (1) ERROR: UtilCreateProcessAndWait:655: /bin/mount failed with status 0x
[29242.271508] ff00

/etc/hostname and /etc/hosts don't exist. /bin/mount seems to "fail".

According to wsl --help, the "default shell" is used well, by default. Our rootfs doesn't have an /etc/passwd yet, Microsoft's /init can't check it to know what the root user shell should be. --exec promises to run a command as is.

C:\Users\Giuliano>wsl --help
Usage: wsl.exe [Argument] [Options...] [CommandLine]

Arguments for running Linux binaries:

    If no command line is provided, wsl.exe launches the default shell.

    --exec, -e <CommandLine>
        Execute the specified command without using the default Linux shell.

Does --exec work?

C:\Users\Giuliano\Documents\WSL\guix>wsl -d guix --exec /busybox sh
/ #

It does! We've got shell access to the hopefully-soon-to-be guix distro.

Now that we're in, we can check what WSL (--import) added our bare rootfs.tar. Notably, /root, /etc, /tmp and /var are missing...

/ # /busybox find / | /busybox grep -v '^.proc\|^.dev\|^.sys'

Despite the mount errors in dmesg, things look OK:

/ # /busybox mount
/dev/sde on / type ext4 (rw,relatime,discard,errors=remount-ro,data=ordered)
tmpfs on /mnt/wsl type tmpfs (rw,relatime)
tools on /init type 9p (ro,dirsync,relatime,aname=tools;fmask=022,loose,access=client,trans=fd,rfd=6,wfd=6)
none on /dev type devtmpfs (rw,nosuid,relatime,size=3194532k,nr_inodes=798633,mode=755)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,noatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,noatime)
devpts on /dev/pts type devpts (rw,nosuid,noexec,noatime,gid=5,mode=620,ptmxmode=000)
none on /run type tmpfs (rw,nosuid,noexec,noatime,mode=755)
none on /run/lock type tmpfs (rw,nosuid,nodev,noexec,noatime)
none on /run/shm type tmpfs (rw,nosuid,nodev,noatime)
none on /run/user type tmpfs (rw,nosuid,nodev,noexec,noatime,mode=755)

Installing Guix

Guix refuses to perform certain operations as root so let's add the users/group it needs. groupadd/useradd are not available yet, so we're going to do it manually:

### Make sure you're running these from the Guix WSL distro!
# /busybox mkdir -p /root /etc /tmp /var/run /run /home
# /busybox chmod 1777 /tmp

# /busybox cat <<EOM >> /etc/passwd
guixbuilder01:x:999:999:Guix build user 01:/var/empty:/usr/sbin/nologin
guixbuilder02:x:998:999:Guix build user 02:/var/empty:/usr/sbin/nologin
guixbuilder03:x:997:999:Guix build user 03:/var/empty:/usr/sbin/nologin
guixbuilder04:x:996:999:Guix build user 04:/var/empty:/usr/sbin/nologin
guixbuilder05:x:995:999:Guix build user 05:/var/empty:/usr/sbin/nologin
guixbuilder06:x:994:999:Guix build user 06:/var/empty:/usr/sbin/nologin
guixbuilder07:x:993:999:Guix build user 07:/var/empty:/usr/sbin/nologin
guixbuilder08:x:992:999:Guix build user 08:/var/empty:/usr/sbin/nologin
guixbuilder09:x:991:999:Guix build user 09:/var/empty:/usr/sbin/nologin
guixbuilder10:x:990:999:Guix build user 10:/var/empty:/usr/sbin/nologin

# /busybox cat <<EOM >> /etc/group

At this point /etc contains:

/ # /busybox find /etc

WSL distros usually have the host C:\ drive automagically mounted on /mnt/c (by the usual suspect: /init), but we don't:

/tmp # /busybox mount | /busybox grep mnt
tmpfs on /mnt/wsl type tmpfs (rw,relatime)
/tmp #

Copy /busybox to /bin/mount:

/tmp # /busybox cp /busybox /bin/mount

Logout of the Guix WSL distro, terminate it, make sure it's stopped:

C:\Users\Giuliano>wsl -l -v
  NAME            STATE           VERSION
  Ubuntu-18.04    Running         2
  guix            Running         2
C:\Users\Giuliano>wsl -t guix
C:\Users\Giuliano>wsl -l -v
  NAME            STATE           VERSION
  Ubuntu-18.04    Running         2
  guix            Stopped         2

Log back in and notice how WSL (now that /etc exists) created resolv.conf, hosts and hostname for us:

C:\Users\Giuliano>wsl -d guix --exec /busybox sh
/ # /busybox find /etc

The /busybox -> /bin/mount trick caused /mnt/c to appear:

/ #  /busybox mount | /busybox grep mnt
tmpfs on /mnt/wsl type tmpfs (rw,relatime)
C:\134 on /mnt/c type 9p (rw,dirsync,noatime,aname=drvfs;path=C:\;uid=0;gid=0;symlinkroot=/mnt/,mmap,access=client,msize=65536,trans=fd,rfd=10,wfd=10)
/ #

Networking works now. If /busybox wget properly supported HTTPS, we could use it to download the Guix binary tarball.

/tmp # /busybox wget
Connecting to (
wget: note: TLS certificate validation not implemented
wget: TLS error from peer (alert code 40): handshake failure
wget: error getting response: Connection reset by peer
/tmp #

Except that it doesn't. Download the tarball with Windows and extract it from /mnt/c instead.

/ # /busybox tar -C / -xvJf /mnt/c/Users/Giuliano/Downloads/guix-binary-1.1.0.x86_64-linux.tar.xz

/gnu and /var/guix are in place:

/ # /busybox ls -ld /gnu /var/guix
drwxr-xr-x    3 root     root          4096 May  1 21:37 /gnu
drwxr-xr-x    5 root     root          4096 May  1 21:37 /var/guix
/ #

As per the Binary Installation instructions, activate the profile included in the tarball:

/ # /busybox mkdir -p ~root/.config/guix
/ # /busybox ln -sf /var/guix/profiles/per-user/root/current-guix ~root/.config/guix/current
/ # GUIX_PROFILE="`echo ~root`/.config/guix/current"
/ # source $GUIX_PROFILE/etc/profile

And finally start the daemon:

/ # guix-daemon --build-users-group=guixbuild &

Great, in theory we can now start using Guix to install stuff!

Make sure to enable subtitutes, unless you want Guix to buid everything from source:

/ # guix archive --authorize < /var/guix/profiles/per-user/root/current-guix/share/guix/

But guix pull doesn't work... :(

/ # guix pull
accepted connection from pid 23, user root
substitute: guix substitute: warning: host not found: Servname not supported for ai_socktype
build of /gnu/store/dd5mga544azygz2hhs3ksp1d8bgmi38j-isrgrootx1.pem.drv failed
View build log at '/var/log/guix/drvs/dd/5mga544azygz2hhs3ksp1d8bgmi38j-isrgrootx1.pem.drv.bz2'.
cannot build derivation `/gnu/store/j814i78jg0832akx856ha6za526rdvy6-le-certs-0.drv': 1 dependencies couldn't be built
guix pull: error: build of `/gnu/store/j814i78jg0832akx856ha6za526rdvy6-le-certs-0.drv' failed

The build log also mentions those Servname not supported for ai_socktype errors. Googling tells us they are caused by a broken/missing /etc/services which indeed does not exist. Let's populate it with the basics:

/ # /busybox cat <<EOM >> /etc/services
ftp-data        20/tcp
ftp             21/tcp
ssh             22/tcp                          # SSH Remote Login Protocol
domain          53/tcp                          # Domain Name Server
domain          53/udp
http            80/tcp          www             # WorldWideWeb HTTP
https           443/tcp                         # http protocol over TLS/SSL
ftps-data       989/tcp                         # FTP over SSL (data)
ftps            990/tcp
http-alt        8080/tcp        webcache        # WWW caching service
http-alt        8080/udp

guix pull now works...

Let Guix take over

Now it'd be great if we could get our Guix WSL distro to be more like GuixSD. F.e., Guix itself should be able to make a "more proper" /etc/passwd than the one we manually edited, fix /etc/services, ...

To achieve that, we're going to try and put together a Guix System Configuration file. A WSL distro, unlike a VM or a bare metal server, doesn't need a lot of the stuff that Guix's operating-system declaration expects. In my configuration file I tried to convince Guix that it's fine to not care about the kernel/bootloader, deal with disks, ...

This is all very hacky/uneducated, but here goes the wsl-config.scm I'm using:

;(use-modules (oop goops))
;(add-to-load-path "/root/.guix-profile/share/guile/site/3.0/")
;(use-modules (ice-9 readline))

  (guix profiles)
  (guix packages)
  (srfi srfi-1))
(use-service-modules networking ssh)
(use-package-modules screen vim)

(define os
  (host-name "scarpa")
  (timezone "Europe/London")
  (locale "en_US.utf8")

  (kernel hello)  ; dummy package
  (initrd (lambda* (. rest) (plain-file "dummyinitrd" "dummyinitrd")))
  (initrd-modules '())
  (firmware '())

          (name 'dummybootloader)
          (package hello)
          (configuration-file "/dev/null")
	  (configuration-file-generator (lambda* (. rest) (computed-file "dummybootloader" #~(mkdir #$output))))
          (installer #~(const #t))))))

  (file-systems (list (file-system
                        (device "/dev/sdb")
                        (mount-point "/")
                        (type "ext4")
                        (mount? #t))))  ; saying #f here doesn't work :(

  (users (cons (user-account
                (name "giuliano")
                (group "users")
                (supplementary-groups '("wheel")))

  (packages (append (list screen  ; global packages to add
                  (lambda (x)
                    (member (package-name x)
                            (list "zile"  ; global packages to not add

      (lambda (x)
        (member (service-type-name (service-kind x))
                (list 'firmware 'linux-bare-metal)))
      (operating-system-default-essential-services this-operating-system)))

  (services (list (service guix-service-type)
                  (service nscd-service-type)))))

; Hackish way to avoid building/including linux-module-database in the system,

(define hooks-modifier
  (eval '(record-modifier <profile> 'hooks)
    (resolve-module '(guix profiles))))

(define my-essential-services (operating-system-essential-services os))

(define system-service (car my-essential-services))
(unless (eq? 'system (service-type-name (service-kind system-service)))
  (raise-exception "The first essential service is not 'system'"))

(define kernel-profile (car (cdr (car (service-value system-service)))))
(unless (string=? "hello" (manifest-entry-name (car (manifest-entries (profile-content kernel-profile)))))
  (raise-exception "I was expecting 'hello' as the (dummy) kernel"))

(hooks-modifier kernel-profile '())

(define os
  (inherit os)
  (essential-services my-essential-services)))


Passing the file to guix system reconfigure...

~ # guix system reconfigure --no-bootloader wsl-config.scm

...creates a new "instance" of the entire OS and switches to it (that's the beauty of GuixSD and NixOS!).

building /gnu/store/68wyiashnhc05wspcn26p3fw0vg2wn2l-switch-to-system.scm.drv...
making '/gnu/store/b544x90ncmxm95344m0c4cygz98w2azr-system' the current system...
setting up setuid programs in '/run/setuid-programs'...
populating /etc from /gnu/store/5s856h5r91xslm90ymm3gn179q6z4hmi-etc...
substitute: updating substitutes from ''... 100.0%
0.0 MB will be downloaded:
downloading from ...
 module-import-compiled  24KiB                                                1.3MiB/s 00:00 [##################] 100.0%

guix system: warning: while talking to shepherd: No such file or directory
~ #

Booting the Guix WSL distro as if it were a GuixSD system

After the previous step the system is functional. But, when the Guix WSL distro starts afresh (because it was stopped with wsl -t guix, Windows was restarted or other), you'll notice that f.e. /run/setuid-programs disappeared (sudo stops working). Some important Guix things live in /run, which is mounted by WSL on a tmpfs (as per the FHS). Because WSL distros don't boot like normal VMs/servers, Guix doesn't have a chance to populate /run at boot time.

The source and this thread help with understanding how to "boot GuixSD by hand":

export GUIX_NEW_SYSTEM=$(/busybox readlink -f /var/guix/profiles/system)
# $GUIX_NEW_SYSTEM/boot needs this to exist even though /run is expected to be empty.
# I installed GuixSD in a proper VM and /run is not on tmpfs, so I'm not sure.
/busybox ln -s none /run/current-system
/var/guix/profiles/system/profile/bin/guile --no-auto-compile $GUIX_NEW_SYSTEM/boot &

Running the above populates /run and starts shepherd.

~ # . /etc/profile
~ # pstree
     │                       └─shepherd─┬─guix-daemon
     │                                  ├─nscd───7*[{nscd}]
     │                                  └─4*[{shepherd}]
~ #


Can only run one "wsl -d guix" terminal at a time

I don't know why but, after shepherd is started with the recipe above, you can't open another "terminal" (I mean: open cmd.exe, run wsl -d guix --exec /busybox sh . The wsl command returns immediately back to prompt. dmesg contains:

[ 3541.851862] init: (48) ERROR: CreateProcessEntryCommon:600: initgroups failed 29
[ 3541.851865] init: (48) ERROR: CreateProcessEntryCommon:645: Create process not expected to return

This isn't really a problem for me, one terminal is enough and I use tmux anyway.
I start Guix with wsl -d guix --exec /busybox sh /root/, where contains:

#!/busybox sh
export GUIX_NEW_SYSTEM=$(/busybox readlink -f /var/guix/profiles/system)
# $GUIX_NEW_SYSTEM/boot needs this to exist even though /run is expected to be empty.
# I installed GuixSD in a proper VM and /run is not on tmpfs, so I'm not sure.
/busybox ln -s none /run/current-system
/var/guix/profiles/system/profile/bin/guile --no-auto-compile $GUIX_NEW_SYSTEM/boot &

/busybox sleep 3
source /etc/profile

# why are these permissions not there in the first place?
for f in ping su sudo; do
        chmod 4755 $(readlink -f $(which $f))

su -l giuliano -c tmux

How much disk space does the Guix WSL distro take?

Freshly installed:

bash-5.0# df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb        251G  2.7G  236G   2% /
Copy link

yugawara commented Sep 30, 2020

I also face the same inability to open the second window in my environment. It would be nice if we could fix this!

PS C:\Users\yasu> gc .\guix.ps1
wsl -t guix ; wsl -d guix --exec /busybox sh /root/
PS C:\Users\yasu> wsl -d guix --exec /busybox sh /root/
PS C:\Users\yasu> # nothing happens

Copy link

Copy link

I also face the same inability to open the second window in my environment. It would be nice if we could fix this!

PS C:\Users\yasu> gc .\guix.ps1
wsl -t guix ; wsl -d guix --exec /busybox sh /root/
PS C:\Users\yasu> wsl -d guix --exec /busybox sh /root/
PS C:\Users\yasu> # nothing happens

@yugawara, until I update the systems definitions and fix this issue properly, a workaround is mentioned in giuliano108/guix-packages#4 . Stopping nscd (with herd stop nscd) allows multiple windows to be opened.

Copy link

I have just tried it and it worked!!! Thank you so much!!!

Copy link

This is very cool and also taught me a little more about WSL and Guix - thanks for posting.

One thing - without using tmux some apps don't seem to display in the console - any ideas who to fix this? For example:

$ emacs
emacs: Could not open file: /dev/tty

Copy link

mrjbj commented Mar 21, 2021

This is an outstanding article that explains every nuanced detail interactively in a way that shows you what happens when you miss a step (and therefore why each step is required). Gives you a great context for what's going on and why. I'm a total noob for guix and was able to follow along and learn a lot in a very shot time. Nice work! thank you.

Also, btw, I couldn't get "git pull" to work with HTTPS. it complained about SSL certificate error. Workaround was the following:
guix pull --url=

Copy link

nullscm commented Jun 6, 2021

Maybe try my gist :) i used the prebuilt rootfs from 0xbadfca11/miniwsl, also look the init script maybe.. with setsid the guile script doesn't get killed after exiting the wsl session and the other terminals are still alive :) this way also tmux scripts or daemons are accessible from other sessions too :)

Copy link

emixa-d commented Apr 8, 2022

FWIW, the name GuixSD is now outdated, it has been called Guix System for some years. From (guix)Introduction:

(2) We used to refer to Guix System as “Guix System Distribution” or
“GuixSD”.  We now consider it makes more sense to group everything under
the “Guix” banner since, after all, Guix System is readily available
through the ‘guix system’ command, even if you’re using a different
distro underneath!

Copy link

qubitz commented Sep 28, 2023

Was really excited to give this a go. The article is really detailed! However I soon ran into inconsistencies from what is being explained. Things of note:

  1. Make sure wsl is up to date with wsl --update. I was on a version that didn't produce "ex4.vhdx"s.
  2. As an alternative, you may find it easier to use guix as a package manager on a normal wsl distro to create a full guix system image made for wsl and then have wsl import that system image. The concept is explained here

Copy link

ok now do it on the better version WSL1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment