Skip to content

ci: add WSL2 CI #1883

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,35 @@ jobs:

windows:
name: "Windows tests"
runs-on: windows-2022
runs-on: windows-2022-8-cores
timeout-minutes: 30
steps:
- name: Enable WSL2
run: |
wsl --set-default-version 2
# Manually install the latest kernel from MSI
Invoke-WebRequest -Uri "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi" -OutFile "wsl_update_x64.msi"
$pwd = (pwd).Path
Start-Process msiexec.exe -Wait -ArgumentList "/I $pwd\wsl_update_x64.msi /quiet"
wsl --update
wsl --status
wsl --list --online
- name: Install WSL2 distro
timeout-minutes: 3
run: |
# FIXME: At least one distro has to be installed here,
# otherwise `wsl --list --verbose` (called from Lima) fails:
# https://github.com/lima-vm/lima/pull/1826#issuecomment-1729993334
# The distro image itself is not consumed by Lima.
# ------------------------------------------------------------------
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d like to see this resolved. Can be another PR though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep looking into this separately. Maybe there's something we can do with conditionally calling wsl --import / wsl --install based on the status returned from wsl --list -v

# Ubuntu-22.04: gets stuck in some infinite loop during adduser
# OracleLinux_9_1: almostly silently fails, and just prints "Usage: adduser [options] LOGIN"
wsl --install -d openSUSE-Leap-15.5
wsl --list --verbose
- name: Set gitconfig
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@v4
with:
fetch-depth: 1
Expand All @@ -86,6 +112,19 @@ jobs:
run: go test -v ./...
- name: Make
run: make
- name: Smoke test
# Make sure the path is set properly and then run limactl
run: |
$env:Path = 'C:\Program Files\Git\usr\bin;' + $env:Path
Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $env:Path
.\_output\bin\limactl.exe start template://experimental/wsl2
# TODO: run the full integration tests
- name: Debug
if: always()
run: type C:\Users\runneradmin\.lima\wsl2\ha.stdout.log
- name: Debug
if: always()
run: type C:\Users\runneradmin\.lima\wsl2\ha.stderr.log

integration:
name: Integration tests
Expand Down
4 changes: 2 additions & 2 deletions examples/experimental/wsl2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ vmType: wsl2

images:
# Source: https://github.com/runfinch/finch-core/blob/main/Dockerfile
- location: "https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1690920103.tar.zst"
- location: "https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1694791577.tar.gz"
arch: "x86_64"
digest: "sha256:53f2e329b8da0f6a25e025d1f6cc262ae228402ba615ad095739b2f0ec6babc9"
digest: "sha256:2d4d2e7386450899c6d0587fd0db21afadb31d974fa744aa9365c883935c5341"

mountType: wsl2

Expand Down
43 changes: 38 additions & 5 deletions pkg/store/instance_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,33 @@ func inspectStatus(instDir string, inst *Instance, y *limayaml.LimaYAML) {
}
}

// GetWslStatus runs `wsl --list --verbose` and parses its output
// GetWslStatus runs `wsl --list --verbose` and parses its output.
// There are several possible outputs, all listed with their whitespace preserved output below.
//
// Expected output (whitespace preserved):
// (1) Expected output if at least one distro is installed:
// PS > wsl --list --verbose
//
// NAME STATE VERSION
//
// * Ubuntu Stopped 2
//
// (2) Expected output when no distros are installed, but WSL is configured properly:
// PS > wsl --list --verbose
// Windows Subsystem for Linux has no installed distributions.
//
// Use 'wsl.exe --list --online' to list available distributions
// and 'wsl.exe --install <Distro>' to install.
//
// Distributions can also be installed by visiting the Microsoft Store:
// https://aka.ms/wslstore
// Error code: Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND
//
// (3) Expected output when no distros are installed, and WSL2 has no kernel installed:
//
// PS > wsl --list --verbose
// Windows Subsystem for Linux has no installed distributions.
// Distributions can be installed by visiting the Microsoft Store:
// https://aka.ms/wslstore
func GetWslStatus(instName string) (string, error) {
distroName := "lima-" + instName
out, err := executil.RunUTF16leCommand([]string{
Expand All @@ -51,11 +70,25 @@ func GetWslStatus(instName string) (string, error) {
"--verbose",
})
if err != nil {
return "", fmt.Errorf("failed to run `wsl --list --verbose`, err: %w", err)
return "", fmt.Errorf("failed to run `wsl --list --verbose`, err: %w (out=%q)", err, string(out))
}

if len(out) == 0 {
return StatusBroken, fmt.Errorf("failed to read instance state for instance %s, try running `wsl --list --verbose` to debug, err: %w", instName, err)
return StatusBroken, fmt.Errorf("failed to read instance state for instance %q, try running `wsl --list --verbose` to debug, err: %w", instName, err)
}

// Check for edge cases first
outString := string(out)
if strings.Contains(outString, "Windows Subsystem for Linux has no installed distributions.") {
if strings.Contains(outString, "Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND") {
return StatusBroken, fmt.Errorf(
"failed to read instance state for instance %q because no distro is installed,"+
"try running `wsl --install -d Ubuntu` and then re-running Lima", instName)
}
return StatusBroken, fmt.Errorf(
"failed to read instance state for instance %q because there is no WSL kernel installed,"+
"this usually happens when WSL was installed for another user, but never for your user."+
"Try running `wsl --install -d Ubuntu` and `wsl --update`, and then re-running Lima", instName)
}

var instState string
Expand Down Expand Up @@ -94,7 +127,7 @@ func getWslSSHAddress(instName string) (string, error) {
cmd := exec.Command("wsl.exe", "-d", distroName, "bash", "-c", `hostname -I | cut -d ' ' -f1`)
out, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to get hostname for instance %s, err: %w", instName, err)
return "", fmt.Errorf("failed to get hostname for instance %q, err: %w (out=%q)", instName, err, string(out))
}

return strings.TrimSpace(string(out)), nil
Expand Down
11 changes: 6 additions & 5 deletions pkg/wsl2/lima-init.TEMPLATE
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
set -eu; \
export LOG_FILE=/var/log/lima-init.log; \
exec > >(tee \$LOG_FILE) 2>&1; \
export LIMA_CIDATA_MNT="$(/usr/bin/wslpath '{{.CIDataPath}}')"; \
exec "\$LIMA_CIDATA_MNT/boot.sh";
#!/bin/bash
set -eu
export LOG_FILE=/var/log/lima-init.log
exec > >(tee $LOG_FILE) 2>&1
export LIMA_CIDATA_MNT="$(/usr/bin/wslpath '{{.CIDataPath}}')"
exec "$LIMA_CIDATA_MNT/boot.sh"
66 changes: 51 additions & 15 deletions pkg/wsl2/vm_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"context"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/lima-vm/lima/pkg/executil"
Expand All @@ -20,13 +22,14 @@ import (

// startVM calls WSL to start a VM.
func startVM(ctx context.Context, distroName string) error {
_, err := executil.RunUTF16leCommand([]string{
out, err := executil.RunUTF16leCommand([]string{
"wsl.exe",
"--distribution",
distroName,
}, executil.WithContext(&ctx))
if err != nil {
return err
return fmt.Errorf("failed to run `wsl.exe --distribution %s`: %w (out=%q)",
distroName, err, string(out))
}
return nil
}
Expand All @@ -35,28 +38,30 @@ func startVM(ctx context.Context, distroName string) error {
func initVM(ctx context.Context, instanceDir, distroName string) error {
baseDisk := filepath.Join(instanceDir, filenames.BaseDisk)
logrus.Infof("Importing distro from %q to %q", baseDisk, instanceDir)
_, err := executil.RunUTF16leCommand([]string{
out, err := executil.RunUTF16leCommand([]string{
"wsl.exe",
"--import",
distroName,
instanceDir,
baseDisk,
}, executil.WithContext(&ctx))
if err != nil {
return err
return fmt.Errorf("failed to run `wsl.exe --import %s %s %s`: %w (out=%q)",
distroName, instanceDir, baseDisk, err, string(out))
}
return nil
}

// stopVM calls WSL to stop a running VM.
func stopVM(ctx context.Context, distroName string) error {
_, err := executil.RunUTF16leCommand([]string{
out, err := executil.RunUTF16leCommand([]string{
"wsl.exe",
"--terminate",
distroName,
}, executil.WithContext(&ctx))
if err != nil {
return err
return fmt.Errorf("failed to run `wsl.exe --terminate %s`: %w (out=%q)",
distroName, err, string(out))
}
return nil
}
Expand All @@ -70,12 +75,39 @@ func provisionVM(ctx context.Context, instanceDir, instanceName, distroName stri
m := map[string]string{
"CIDataPath": ciDataPath,
}
out, err := textutil.ExecuteTemplate(limaBoot, m)
limaBootB, err := textutil.ExecuteTemplate(limaBoot, m)
if err != nil {
return fmt.Errorf("failed to construct wsl boot.sh script: %w", err)
}
outString := strings.Replace(string(out), `\r\n`, `\n`, -1)

limaBootFile, err := os.CreateTemp("", "lima-wsl2-boot-*.sh")
if err != nil {
return err
}
if _, err = limaBootFile.Write(limaBootB); err != nil {
return err
}
limaBootFileWinPath := limaBootFile.Name()
if err = limaBootFile.Close(); err != nil {
return err
}
// path should be quoted and use \\ as separator
bootFileWSLPath := strconv.Quote(limaBootFileWinPath)
limaBootFilePathOnLinuxB, err := exec.Command(
"wsl.exe",
"-d",
distroName,
"bash",
"-c",
fmt.Sprintf("wslpath -u %s", bootFileWSLPath),
bootFileWSLPath,
).Output()
if err != nil {
os.RemoveAll(limaBootFileWinPath)
// this can return an error with an exit code, which causes it not to be logged
// because main.handleExitCoder() traps it, so wrap the error
return fmt.Errorf("failed to run wslpath command: %w", err)
}
limaBootFileLinuxPath := strings.TrimSpace(string(limaBootFilePathOnLinuxB))
go func() {
cmd := exec.CommandContext(
ctx,
Expand All @@ -84,12 +116,15 @@ func provisionVM(ctx context.Context, instanceDir, instanceName, distroName stri
distroName,
"bash",
"-c",
outString,
limaBootFileLinuxPath,
)
if _, err := cmd.CombinedOutput(); err != nil {
out, err := cmd.CombinedOutput()
os.RemoveAll(limaBootFileWinPath)
logrus.Debugf("%v: %q", cmd.Args, string(out))
if err != nil {
*errCh <- fmt.Errorf(
"error running wslCommand that executes boot.sh: %w, "+
"check /var/log/lima-init.log for more details", err)
"error running wslCommand that executes boot.sh (%v): %w, "+
"check /var/log/lima-init.log for more details (out=%q)", cmd.Args, err, string(out))
}

for {
Expand Down Expand Up @@ -130,13 +165,14 @@ func keepAlive(ctx context.Context, distroName string, errCh *chan error) {
// unregisterVM calls WSL to unregister a VM.
func unregisterVM(ctx context.Context, distroName string) error {
logrus.Info("Unregistering WSL2 VM")
_, err := executil.RunUTF16leCommand([]string{
out, err := executil.RunUTF16leCommand([]string{
"wsl.exe",
"--unregister",
distroName,
}, executil.WithContext(&ctx))
if err != nil {
return err
return fmt.Errorf("failed to run `wsl.exe --unregister %s`: %w (out=%q)",
distroName, err, string(out))
}
return nil
}