My Windows Subsystem for Linux setup

My trusty 2011 Macbook Air had started to show its age – a couple of missing keys, the fan being on pretty much all the time – so I looked for a replacement. After some tedious investigation, I settled upon a Dell XPS 9370 from their Outlet store. Windows 10 can run Linux now, via the Windows Subsystem for Linux (WSL), so it seemed like an interesting thing to spend some time with, and if it all went sour I could always fully install Linux instead.

I’m sure some people have a reason to prefer one OS over another, but I have never really understood it, at least not since my last RISC OS machine :) They all have awful file management (as do I), and they’re all much of a muchness in every way I seem to use them; I can install most things I want to use on anything, and most of the rest is in a web browser.

I’ve had it about a month now, and I thought it worth writing up some of the things I’ve done to aid shared development between Windows and WSL.

Applications

So firstly, let us rattle off the list of apps I installed. Many were identical to what it would be on a Mac: Firefox (with Containers), Thunderbird, Slack, Steam, VSCode, Licecap, Spotify, Git, KeePassXC, Chrome, and OpenVPN. As replacements for some small Mac programs, I have installed HexChat (IRC), KeyFerret (special character input), Ditto (clipboard memory), and NVDA (screen reader).

I also newly installed LDraw because my four year old has made her own Lego dinosaur and wanted to generate some instructions :)

Setting up WSL

To install WSL and Ubuntu, I followed Microsoft’s instructions (except in more stereotypical Windows behaviour, I activated WSL by going to Control Panel → Programs and Features → Turn Windows features on or off, rather than using the command line). Ubuntu was indeed a one click install and initial set up worked fine. I’m not sure this is something I’d ever have predicted years ago!

Shell and terminal

fish is my shell of choice (saves so many typos). If you start WSL with wsl.exe rather than bash.exe, then it uses your default shell. I tried a few terminals and settled on Hyper with the Fira Code font, though it is somewhat annoying (copy and paste use Ctrl-Shift, new tabs don’t keep the same directory, etc). Thankfully I have always loved autojump, which alleviates the latter annoyance, making project navigation trivial (j f takes me directly to my main FixMyStreet git checkout).

File permissions

In order for WSL to maintain Linux file permissions, you need to enable metadata on the mounted C drive. wsl-config documentation tells you how, but not why, which can be found at this blog entry. My wsl.conf looks like this:

[automount]
enabled = true
options = "metadata,umask=002"
mountFsTab = true

Database server

postgres installed and ran fine in WSL, but I’d like it to start on computer start. I found the instructions at https://dev.to/ironfroggy/wsl-tips-starting-linux-background-services-on-windows-login-3o98 helpful, though I made a few changes (e.g. I added a file to /etc/sudoers.d).

Sharing ssh keys between Windows and Linux

So now I was at a point where I had a working Linux installation inside my Windows laptop. But I didn’t want to keep it totally separate, because that would make development harder. I wanted them to work together where that would be useful.

For example, KeePassXC lets me store my SSH key passphrase within it, and automatically add/remove it from the SSH agent in Windows while unlocked. I didn’t want to have to enter my passphrase within WSL when running SSH, in order to add the key to the Linux SSH agent.

Firstly, I made the /home/matthew/.ssh directory in WSL a symlink to /mnt/C/Users/Matthew/.ssh so my SSH keys could be shared. I made the SSH agent run automatically on Windows (it’s not by default), and then installed ssh-agent-wsl which can pipe the Windows SSH agent information through to WSL.

Everyone appears to have slightly different SSH agent script to run on login; here’s mine:

~ $ cat .config/fish/functions/fish_ssh_agent.fish 
set SSH_ENV $HOME/.ssh/environment
set SSH_AGENT /mnt/c/Users/Matthew/bin/ssh-agent-wsl -c -b

function __ssh_agent_needs_starting -d "check if ssh agent is already started"
    if test -f $SSH_ENV
        source $SSH_ENV > /dev/null
    end

    ssh-add -l >/dev/null 2>&1
    if test $status -eq 2
        return 0
    end
    return 1
end

function __ssh_agent_start -d "start a new ssh agent"
    umask 077
    eval $SSH_AGENT > $SSH_ENV
    source $SSH_ENV > /dev/null
end

function fish_ssh_agent
    if __ssh_agent_needs_starting
        __ssh_agent_start
    end
end

As you can see I run ssh-agent-wsl with -b, as without that it started using runaway CPU after the last terminal window was closed.

Update August 2020: I have upgraded to WSL2, and the above is now a bit simpler. I installed npiperelay in ~/winhome/.wsl/ (winhome a symlink to my Windows homedir), installed socat in Ubuntu, then changed the above function to:

set -x SSH_AUTH_SOCK $HOME/.ssh/agent.sock

function fish_ssh_agent
    ss -a | grep -q $SSH_AUTH_SOCK
    if [ $status -ne 0 ]
        rm -f $SSH_AUTH_SOCK
        begin
            setsid socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"$HOME/winhome/.wsl/npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork &
        end >/dev/null 2>&1
    end
end

Sharing git repos between Windows and Linux

One very frequent thing I do is work with git repositories in both Windows (using VSCode) and WSL, and so want to run git commands in both. So I needed to make sure these interoperated okay.

In Windows git, I set core.autocrlf=false, as I don’t want git to do any cleverness – this repo will be accessible by two different OSs at once! I set core.filemode to false as otherwise it gets confused about executables

It took a while to track this down, but Windows git includes its own SSH commands, which meant I was getting asked for my passphrase even though I had my key loaded into the Windows SSH agent. Thankfully, recent git lets you set core.sshCommand to the SSH you wish to use, so I set it to C:\Windows\System32\OpenSSH\ssh.exe (and ssh.variant to ssh) and this was resolved.

The only annoyance left was symlinks. I enabled symlink creation by turning on Developer Mode and adding my user to the correct policy group, as explained at https://github.com/git-for-windows/git/wiki/Symbolic-Links, which meant I could set core.symlinks to true. Symlinks then worked fine in both Windows and WSL, but sadly git status would say that the symlinks were modified in one OS or the other (ie. if I git reset them in Windows, WSL would say they were modified, and vice-versa). After asking on StackOverflow, it seems this is one thing I have to live with, and as mentioned there have marked them as assume-unchanged, as there are only a small number and they very rarely change.

‘open’ script

On a Mac, you can type "open [file]" to open that file with the default program for it. To recreate the same thing, I created a tiny script in my bin directory:

#!/usr/bin/env fish

cmd.exe /C start "" $argv
For slightly more advanced behaviour, I have just discovered wsl-open which I should probably install.

Cypress

FixMyStreet has a lot of tests in its test suite – most of them are “back end” tests (so run on a server testing various parts of the server-side code do what they should, pretending to be a user fetching pages and submitting forms etc), but some are “front end” tests, which run in an actual browser (this can be done “headlessly” so no actual browser window opens up when they’re run), connecting to a running test server, and check that parts of the front end (mostly JavaScript) do what they should.

For this, we use a piece of software called Cypress which helps automate it all and aid development, and wrote a script to do the set up/tear down parts of creating a test database, running a test server for Cypress to connect to, and so on. Our test suite runs on Travis with each commit, and our front end tests running there push video recordings of the running output to a service called Cypress Dashboard, so you can see the output of the latest run. For developing locally, you can do the same as Travis and run the front end tests headlessly, only seeing the output text and videos, or you can run Cypress as a GUI and see the browser running the tests, inspect the DOM at any point of a test, easily rerun a particular test, and so on.

As Linux on Windows 10 is basically ‘native’, I can call Windows programs from inside Linux. I have Cypress installed natively on Windows (installed node and npm install cypress), and so I run the test script in WSL with a special argument (unsurprisingly, --wsl), which at the point it wants to run Cypress, calls out to it in Windows to run there. (I did have the option of installing Cypress in Linux, setting up a Linux window manager and running it entirely inside, but the GUI support for Linux on Windows isn’t their core focus and I thought this way would probably be easier.) This was a little tricky to find the precise combination of commands that worked (by default, running Windows Cypress from Linux either assumed you were in Linux and died because there was no Linux window manager, or assumed you were in Windows and couldn’t find the command to run because it tried to use Windows path names in Linux), but now that it’s done, it’s all working smoothly.

Update August 2020: With WSL2, I still have it working, but I have to have the code checked out under /mnt/c rather than in Linux, otherwise it is incredibly slow.

Conclusion

I always used MacPorts on my Mac, which worked nicely; however, it is also nice to have Linux directly installed. I’d also have been fine with a full Linux installation, but don’t really have the time at present to play about with that, so the presence of WSL means I am happy to use Windows until such a time as more free time becomes available(!)