Porting a macOS Clojure dev setup to Windows WSL2

EDIT 2020-09-16: this post is now also available as a talk. The slides are available here.

In the last few years I've been developing predominantly on Apple MacBooks. Recently I built myself a new PC with a fast processor and lots of memory. I wanted to move some of my development tasks to that machine. I still like the freedom that laptops give me to work from wherever I want. Be it from the couch in my own home or from a random coffee place in the city where I live, I just like to change environment every few hours and not sit behind my desk all day long. I hadn't used Windows as my primary OS for almost 10 years. But since Windows 10 (still) comes with RDP, which is better than VNC in terms of performance, and with WSL2, which offers a "real" and well integrated linux experience, I was curious if I could create a setup for remotely working on the new machine. It also would give me an environment to debug Windows specific problems on, which should come in handy for my side projects like babashka and clj-kondo. If it didn't work out, I could always go dual boot with Ubuntu as the primary dev OS and use VNC if I needed to do anything graphical. For what it is worth, here is a report of setup process I went through.

The pristine smell and beauty of a new PC contrasted with the chaotic reality of my home. pic.twitter.com/3zLIzYg4XB

— (λ. borkdude) (@borkdude) July 8, 2020

Because I had no other Windows machines in my home, I tried creating a USB Windows installation drive in macOS based on this blog post. No matter what I tried, at the end of the installation process Windows would complain with

Windows installation encountered an unexpected error. Verify that the installation sources are accessible, and restart the installation.

Installing Ubuntu on new PC. The installer is surprisingly smooth, detects my wifi, even nvidia video card apparently. Whereas Windows be like pic.twitter.com/FQNzeJ6V2X

— (λ. borkdude) (@borkdude) July 8, 2020

A good friend offered help and created an installation drive using Rufus on his Windows Home installation, which did work.

Once Windows was installed, I enabled RDP and installed WSL2, Ubuntu 20.04, Windows Terminal and Docker Desktop for WSL2.

To be able to run WSL2, I had to enable a virtualization option in my BIOS first.

I intend to use Ubuntu WSL2 as my primary dev environment on this machine, so it made sense to tweak the Terminal configuration to open Ubuntu by default, instead of PowerShell. I found it annoying that Ubuntu in Terminal starts in the Windows home directory (/mnt/c/Users/borkdude), so I set `"startingDirectory": "//wsl$/Ubuntu-20.04/home/borkdude"` in my Ubuntu profile.

I use zsh as my default shell on macOS and wanted that in WSL2 as well so I installed that using sudo apt update && sudo apt install zsh followed by `chsh -s $(which zsh)`. I also installed ohmyzsh and enabled the only two plugins I use on a daily basis: git and jump.

Then I started porting the dev setup I use for work on my MacBook, mainly consisting of Docker images, a couple of Clojure projects, knitted together with some bash scripts. I use a couple of shell scripts to set all my development environment variables and aliases for easily setting up ssh tunnels. These worked after only making a few tweaks (for example, the ethernet card is called differently in macOS and Ubuntu in WSL2). Of course I needed to install git to clone my work projects. To run Clojure projects I installed OpenJDK version 8 and 11 via apt. I use jenv to manage Java versions on a project basis, which worked perfectly:

sudo apt-get install openjdk-8-jdk
jenv add /usr/lib/jvm/java-1.8.0-openjdk-amd64/

Also I installed boot, a Clojure build tool and the Clojure CLI which are all well supported in linux.

Before running my work projects, I wanted to copy over a PostgreSQL database and other data from my MacBook using rsync. Before I could do that, I needed to enable ssh in Ubuntu. I found out I could do this by running `sudo service ssh start` but I still could not log in from my MacBook. It turns out you also have to forward and open up the port in the Windows Firewall. I found this PowerShell script that lets you do that. As the comment suggests, I also created a scheduled task that runs this script upon Windows login. I did the same thing for running the ssh service using wsl -u root sudo service ssh start. Be sure to select Run using the highest privileges. I'm sure there is a better way to do this via systemd in WSL2, but this works for me. So now I could rsync my the data from macOS to WSL2. I started the Docker containers, the Clojure projects and it worked! We have a fairly I/O intensive process as part of our stack, which seems to perform well in WSL2.

Enabling SSH in WSL also lets me use Emacs from my laptop and edit files on the remote machine using tramp mode. I ran into one issue with this. Since I use a non-standard prompt in zsh, emacs tramp could not parse the output from the remote shell. So I added this hack to my ~/.zshrc:

case "$TERM" in

Emacs sets TERM to "dumb" when opening files using tramp. When this is the case, I just invoke bash instead of continuing with zsh. There may be a better solution, but this works.

Of course I also wanted to be able to run Emacs on the machine itself. I first tried to run Emacs in Windows natively, but I read somewhere that editing WSL2 files directly from Windows is not recommended. Also you might run into the line ending differences between Windows and linux/macOS. I decided to play it safe and install emacs inside WSL2 using apt. If you do decide to use the Windows-native Emacs and have trouble finding where emacs expects configuration files, you can find this using `M-x describe-variable user-init-file`. On my machine that was C:\Users\borkdude\AppData\Roaming.config\emacs\init.el.. To get a graphical UI for the emacs started from WSL2, I needed to install an X-server on Windows. There are plenty of free and open source solutions to choose from, but I chose to buy X410 on sale for $9.99 instead of $49.99. It seems to be actively worked on and has good documentation. To automatically start X410 on Windows startup I followed these instructions. When started, in the tray there is an X icon where you can modify settings for X410. I have enabled Windowed Apps, Allow Public Access, DPI Scaling (High Quality) and Shared Clipboard. To export the right DISPLAY value for GUI applications in WSL2 I have this in my .zshenv:

export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2; exit;}'):0.0

and then run emacs using setsid emacs from a zsh session (I created an alias for this in my .zshenv).

I'm using the exact same emacs config I use on macOS, which is based on @bbatsov's prelude. It looks and feels the same as on macOS, although now I probably have to learn the "real" emacs keybindings for copying (M-w) and pasting (C-y) instead of using the macOS keybindings, which may be in fact a nice side effect of this experiment.

Even connecting to an nREPL server with cider-connect works seemlessly from my laptop, provided that I forward and open up the port in Windows Firewall like described above.

Some closing tips.

There is a known issue with memory usage of WSL2. Since linux uses non-allocated memory for filesystem caching, Windows thinks this memory is really used. As time goes by, Windows could end up allocating all your system's memory to WSL2. To put a limit to this, I use this config in C:\Users\borkdude.wslconfig:



My PC has a whopping 128GB of memory so this still leaves 36GB of memory to Windows.

As I will mainly use this PC for work, I turn it off at night and during the weekends. To get back where I left, without restarting all my development processes from scratch, I use the Hibernate setting.

I use Tailscale to set up a VPN between my laptop and Windows machine so I can connect through RDP, emacs tramp and/or nREPL when I go outside of my home.

For RDP I use Microsoft Remote Desktop.app. Since I have a Retina screen, I enabled the "optimize for Retina displays" setting which gives a better resolution.

As a bonus I can re-use some licenses I already owned for apps I am using on macOS: Beyond Compare and Acronis True Image.

I'm pleasantly surprised with WSL2, the Terminal app and Docker integration. I've only been using this setup for week but so far so good.

Pretty good startup time of #babashka in WSL2 on Windows on new machine (Ryzen 3950X): 4ms on average@graalvm pic.twitter.com/TKPPoeFZlL

— (λ. borkdude) (@borkdude) July 10, 2020

Published: 2020-07-26

Tagged: clojure wsl2