Creating WSL environment for CI

A quick guide to testing your software in WSL-like environment.

Ubuntu 20.04 WSL distribution running inside a disposable container.

Introduction

That being said, not everything was smooth sailing for WSL. This is because WSL1 fundamentally worked by translating Linux syscalls into Windows API. This meant that some behaviour either differed from native Linux or was not implemented at all. ptrace was the syscall that developers arguably missed the most, as it’s commonly used for debugging. Additionally, filesystem performance was occasionally a blocker — especially for workflows that stressed it with thousands of small files (looking at you, Node.JS).

WSL2 comes to the rescue

  • 🐧 Native Linux kernel — this means that all syscalls are now supported. Developers are now free to take full advantage of the kernel, which, among other things, led to a much better Docker experience on Windows.
  • 📁 Native Linux filesystem — this means performance for Linux paths (e.g., /home/eric) is much better. Developers can now benefit from this by simply moving their projects from /mnt/c to /home.

As a result of above, WSL2 is a much more viable development environment. These days, if your create tools for developers, you may now want to test your software against common distributions available in WSL2. This raises the question of how to get the WSL2 configured in a server/CI environment.

WSL2 on Windows Server

  • As of writing (April 2021), long-term support versions of Windows Server ship version 1809 of Windows. Whilst WSL2 did get backported to earlier versions of Windows, you still need at least version 1903 of Windows. This (technically) means that only WSL1 would be available.
  • Even if you got WSL1 installed on version 1809, you’ll find that the experience is rather different. wsl CLI got a number of useful subcommands in recent updates for creating (e.g., -import) and managing distributions (-u for choosing the user and -l for listing distributions). Without these, the process of creating short-lived distributions is much harder and you have to rely on third-party tools like LxRunOffline.
  • Alternatively if you tried to get the 2022 preview release of Windows Server, you will find that WSL2 is actually broken and cannot be used (see microsoft/WSL#6301).
  • Finally, based on reactions of Microsoft employees (see Microsoft forum and this Twitter interaction), it appears that Windows Server isn’t meant to officially support WSL since they don’t want developers to “run WSL in production”. This calls into question the fact that WSL1 works on Windows Server at all and whether it’s an oversight rather than a feature.

Yet there’s no reason to despair. There’s another technology that works very similarly to short-lived WSL distributions… 🐳

Take two… for WSL2

With virtualisation method tackled, the next challenge is reproducing the desired WSL distribution. Whilst we could take a corresponding image off Docker Hub, it wouldn’t have quite the same setup. Typically, WSL integrations are distributed as Microsoft Store apps. Of course, this is less than ideal for us if we’re planning to run our CI on a non-Windows host. Thankfully, Microsoft provides alternative downloads links for circumstances like ours.

Let’s download and inspect a distribution to see if we can create a container image out of it. From my previous experience, I know that .appx files are just ZIP archives:

$ unzip -l Ubuntu_2004.2020.424.0_x64.appx
Archive: Ubuntu_2004.2020.424.0_x64.appx
Length Date Time Name
--------- ---------- ----- ----
4740 2020-04-23 16:01 Assets/SmallTile.scale-200.png
22702 2020-04-23 16:01 Assets/SplashScreen.scale-200.png
10515 2020-04-23 16:01 Assets/Square150x150Logo.scale-200.png
8262 2020-04-23 16:01 Assets/LargeTile.scale-200.png
755 2020-04-23 16:01 Assets/Square44x44Logo.altform-unplated_targetsize-16.png
13788 2020-04-23 16:01 Assets/Square44x44Logo.altform-unplated_targetsize-256.png
1551 2020-04-23 16:01 Assets/Square44x44Logo.altform-unplated_targetsize-32.png
2452 2020-04-23 16:01 Assets/Square44x44Logo.altform-unplated_targetsize-48.png
2844 2020-04-23 16:01 Assets/Square44x44Logo.scale-200.png
404 2020-04-23 16:01 Assets/Square44x44Logo.targetsize-16.png
655 2020-04-23 16:01 Assets/Square44x44Logo.targetsize-24.png
1148 2020-04-23 16:01 Assets/Square44x44Logo.targetsize-24_altform-unplated.png
8974 2020-04-23 16:01 Assets/Square44x44Logo.targetsize-256.png
896 2020-04-23 16:01 Assets/Square44x44Logo.targetsize-32.png
1379 2020-04-23 16:01 Assets/Square44x44Logo.targetsize-48.png
5092 2020-04-23 16:01 Assets/StoreLogo.scale-200.png
7425 2020-04-23 16:01 Assets/Wide310x150Logo.scale-200.png
3544 2020-04-23 16:01 resources.pri
452534052 2020-04-23 16:01 install.tar.gz
468480 2020-04-23 16:01 ubuntu2004.exe
3709 2020-04-23 16:01 AppxManifest.xml
418038 2020-04-23 16:01 AppxBlockMap.xml
744 2020-04-23 16:01 [Content_Types].xml
11003 2020-04-23 16:01 AppxMetadata/CodeIntegrity.cat
10951 2020-04-23 16:04 AppxSignature.p7x
--------- -------
453544103 25 files

The interesting bit here is the install.tar.gz file, so let’s extract and inspect it:

$ unzip Ubuntu_2004.2020.424.0_x64.appx install.tar.gz
Archive: Ubuntu_2004.2020.424.0_x64.appx
extracting: install.tar.gz
$ tar -tzvf install.tar.gz | head -10
lrwxrwxrwx root/root 0 2020-04-23 07:40 bin -> usr/bin
drwxr-xr-x root/root 0 2020-04-23 07:49 boot/
drwxr-xr-x root/root 0 2020-04-23 07:43 dev/
crw-rw-rw- root/root 5,1 2020-04-23 07:40 dev/console
lrwxrwxrwx root/root 0 2020-04-23 07:40 dev/fd -> /proc/self/fd
crw-rw-rw- root/root 1,7 2020-04-23 07:40 dev/full
drwxr-xr-x root/root 0 2020-04-23 07:42 dev/mapper/
crw------- root/root 10,236 2020-04-23 07:42 dev/mapper/control
crw-rw-rw- root/root 1,3 2020-04-23 07:40 dev/null
crw-rw-rw- root/root 5,2 2020-04-23 07:40 dev/ptmx

As we were hoping, this appears to be the root filesystem for the distribution. Seems like this could be a viable option 🎉

Automate away

FROM ubuntu AS build
RUN apt-get update && \
apt-get install -y \
curl \
unzip \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
ARG DISTRO_URL
RUN curl -L $DISTRO_URL -o distro.appx
RUN unzip distro.appx install.tar.gz && \
mkdir rootfs && \
tar -zxvf install.tar.gz -C rootfs
FROM scratch
COPY --from=build rootfs/ /

We’re using ubuntu image to download and extract the root filesystem of a distribution specified by DISTRO_URL build argument. We then create an empty image and populate it with the downloaded root filesystem. Let’s now try to test our creation:

$ docker build --build-arg DISTRO_URL=https://aka.ms/wslubuntu2004 -t test .
...
=> => naming to docker.io/library/test
$ docker run --rm -it test
docker: Error response from daemon: No command specified.
See 'docker run --help'.
$ docker run --rm -it test bash
root@721db7d0cf38:/# ls
bin dev home lib32 libx32 mnt proc run snap sys usr
boot etc lib lib64 media opt root sbin srv tmp var

We’re somewhat successful! Since our image was created from scratch, it doesn’t have the necessary metadata just yet; we can still see that it’s functioning as expected though.

To populate the necessary metadata, we need to understand how this root filesystem is turned into a development environment. Luckily, we can learn from Fedora Remix WSL integration, as its source code is available on GitHub. If we look at InstallDistribution and DistributionInfo::CreateUser routines, we can replicate the installation process. Let’s amend our Dockerfile with these changes:

FROM ubuntu AS build
RUN apt-get update && \
apt-get install -y \
curl \
unzip \
&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
ARG DISTRO_URL
RUN curl -L $DISTRO_URL -o distro.appx
RUN unzip distro.appx install.tar.gz && \
mkdir rootfs && \
tar -zxvf install.tar.gz -C rootfs
FROM scratch
COPY --from=build rootfs/ /
ARG USERNAME=developer
RUN useradd -m $USERNAME
USER $USERNAME
CMD ["bash", "-l"]

We’re now adding an unprivileged user to the environment and setting it as the default. Additionally, we’re setting the default command to bash -l since we want to execute the login shell. Let’s now try the updated version:

$ docker build --build-arg DISTRO_URL=https://aka.ms/wslubuntu2004 -t test .
...
=> => naming to docker.io/library/test
$ docker run --rm -it test
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.72-microsoft-standard-WSL2 x86_64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Sun Apr 18 11:40:49 UTC 2021System load: 0.0 Processes: 5
Usage of /home: unknown Users logged in: 0
Memory usage: 18% IPv4 address for eth0: 172.17.0.2
Swap usage: 0%
0 updates can be installed immediately.
0 of these updates are security updates.
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
This message is shown once once a day. To disable it please create the
/home/developer/.hushlogin file.
developer@401dc25d6633:/$

Of course, we can also run commands as root (similar to wsl -u root) with docker run -u root. I have tested this method with all the downloads provided on Manually download Windows Subsystem for Linux distro packages and it appears to work fine.

Success! This means that we’ve effectively solved the problem of creating WSL-like environment and can go ahead and run automated tests there to ensure that everything is working as expected. 🤖

P.S: Going multi-platform

$ docker buildx build --platform linux/arm64  --build-arg DISTRO_URL=https://aka.ms/wslubuntu2004arm -t test .
$ docker run --rm -it --platform linux/arm64 test
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.72-microsoft-standard-WSL2 aarch64)
* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage
System information as of Sun Apr 18 12:22:37 UTC 2021System load: 0.02 Memory usage: 27% Processes: 5
Usage of /home: unknown Swap usage: 1% Users logged in: 0
=> There were exceptions while processing one or more plugins. See
/home/developer/.landscape/sysinfo.log for more information.
0 updates can be installed immediately.
0 of these updates are security updates.
The list of available updates is more than a week old.
To check for new updates run: sudo apt update
This message is shown once once a day. To disable it please create the
/home/developer/.hushlogin file.
developer@75395fa2c091:/$

This means that we can now run our automated tests on arm64 and amd64 side-by-side if our tooling targets both architectures. 🙌

Passionate about all things containers. Here, I document my adventures from coding for open source and pet projects.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store