WSL images for Guix System

September 26, 2022

During my holidays some rare conditions were met: I had some free time and access to a Windows machine. Time to review this patch from Alex Griffin adding WSL support to the guix system image command.

I'm not exactly sure how WSL works, but it looks like some kind of emulation layer allowing to run GNU/Linux distributions on Windows. To be frank, I hope that I won't have to use this WSL thing in the future. However, some less fortunate people are forced to work in a Windows environment so being able to run GNU Guix could be a lifeline.

Merge the patch

Let's have a short look to the patches proposed by Alex to better understand what adding WSL support means.

First stop is to add tarball support to guix system image.

tarball

It means that if you were to run:

guix system image --image-type=tarball my-image.scm

you would obtain a tarball archive containing the Guix System image declared in my-image.scm. Handy and pretty straightforward.

Then, Alex added some plumbing to declare that a WSL image is basically a tarball image. My only concern here is that generating tar.gz archives compressed at level 9 takes quite some times. I wonder if Windows would accept zstd archives or so. This question is left open for early adopters.

wsl

The last required patch, that I tweaked a little bit adds a (gnu system images wsl2) module. In this module, the wsl-os variable is declared.

(define-public wsl-os
  (operating-system
    (host-name "gnu")
    (timezone "Etc/UTC")
    (bootloader
     (bootloader-configuration
      (bootloader dummy-bootloader)))
    (kernel dummy-kernel)
    (initrd dummy-initrd)
    (initrd-modules '())
    (firmware '())
    (file-systems '())
    ...))

This operating-system is meant to be the foundation for any future WSL image. It is a bit special because several fields are set to dummy packages. That's because the WSL environment already provides the Linux kernel and possibly the initrd, mounts the file-systems and so on.

The strategy of defining a dummy-kernel, dummy-initrd and dummy-bootloader for this specific use-case works but is not perfectly satisfactory to me. Maybe we should not define those dummy packages directly in (gnu system images wsl2) but rather in (gnu system) or so.

Another point worth noticing is how Alex managed to start the Shepherd, the init system used by Guix System. WSL is taking care of starting the Linux kernel but it cannot know how to start the Shepherd because that's the role of the Guix System initrd.

To work around that, a wsl-boot-program script is defined. This script takes care of starting the Shepherd and spawning a shell. It is registered as the login program of the root account to trick WSL into running it.

(users (cons* (user-account
               (name "guest")
               (group "users")
               (supplementary-groups '("wheel")) ; allow use of sudo
               (password "")
               (comment "Guest of GNU"))
              (user-account
               (inherit %root-account)
               (shell (wsl-boot-program "guest")))
              %base-user-accounts))

OK, so this is a good start and all those patches are now merged.

Generate images

Now let's generate some images. We can first try to generate a basic image that does nothing more than instantiating the wsl-os operating-system and produce a WSL bootable image out of it. From a Guix checkout, we can run:

guix system image gnu/system/images/wsl2.scm
/gnu/store/cgbd1yl5ab12pn7a103pixld868lcslf-image.tar.gz

that's it! The resulting image can be imported in Windows using the following commands:

wsl --import Guix ./guix cgbd1yl5ab12pn7a103pixld868lcslf-image.tar.gz 
wsl -d Guix

From within the WSL environment we can stop holding our breath and enjoy a classic Guix System distribution.

pull

That's nice but we can go a step further. Let's say we want to deploy a WSL image which role is to run a particular service, say tailon a web server for browsing some system logs. We can define our operating-system this way

(use-modules (gnu system images wsl2)
             (gnu services web))

(operating-system
  (inherit wsl-os)
  (services
   (cons
    (service tailon-service-type)
    (operating-system-user-services wsl-os))))

run,

guix system image --image-type=wsl2 wsl-tailon.scm

import the image, like the first one, and from a Windows browser we can directly use the tailon service.

tailon

We can also choose a more complex service: the Nginx web server.

(use-modules (gnu system images wsl2)
             (gnu packages web)
             (gnu services web)
             (srfi srfi-1))

(operating-system
  (inherit wsl-os)
  (services
   (cons*
    (service dummy-service-type)
    (simple-service
     'index
     activation-service-type
     (with-imported-modules '((guix build utils))
       #~(begin
           (use-modules (guix build utils))
           (mkdir-p "/srv/http/www.example.com")
           (copy-file
            #$(file-append nginx "/share/nginx/html/index.html")
            "/srv/http/www.example.com/index.html"))))
    (service nginx-service-type
             (nginx-configuration
              (server-blocks
               (list (nginx-server-configuration
                      (server-name '("www.example.com"))
                      (listen '("8080"))
                      (root "/srv/http/www.example.com"))))))
    (operating-system-user-services wsl-os))))

This way, nginx will serve the index.html file at the http://localhost:8080 location. Sadly this won't work because the nginx Shepherd service requires the loopback service. If we add the loopback service however, we will get a run time issue because WSL already took care of creating this interface and Shepherd will fail to start it and refuse to start nginx which depends of the loopback service.

As a quick hack, I defined a dummy-service which is conveniently called loopback and does nothing. The operating-system declaration now looks like:

(use-modules (gnu system images wsl2)
             (guix gexp)
             (gnu packages web)
             (gnu services shepherd)
             (gnu services web)
             (srfi srfi-1))

(define (dummy-service _)
  (list
   (shepherd-service
    (documentation "")
    (provision '(loopback))
    (start #~(const #t))
    (stop #~(const #t))
    (respawn? #f))))

(define dummy-service-type
  (service-type
   (name 'loopback)
   (extensions
    (list (service-extension shepherd-root-service-type
                             dummy-service)))
   (default-value '())
   (description "")))

(operating-system
  (inherit wsl-os)
  (services
   (cons*
    (service dummy-service-type)
    (simple-service
     'index
     activation-service-type
     (with-imported-modules '((guix build utils))
       #~(begin
           (use-modules (guix build utils))
           (mkdir-p "/srv/http/www.example.com")
           (copy-file
            #$(file-append nginx "/share/nginx/html/index.html")
            "/srv/http/www.example.com/index.html"))))
    (service nginx-service-type
             (nginx-configuration
              (server-blocks
               (list (nginx-server-configuration
                      (server-name '("www.example.com"))
                      (listen '("8080"))
                      (root "/srv/http/www.example.com"))))))
    (operating-system-user-services wsl-os))))

And just like before, we can generate an image, import it, and open a Windows web browser to check that is works correctly.

nginx

All in all that's a pretty good start that still needs to be polished a bit by future users. It is also a good demonstration that generating new kind of system images is quite easy with the Guix System image API. Thank you Alex for working on that topic!