diff --git a/build_windows.md b/build_windows.md index 557ecdeeb6..fbd44bf43f 100644 --- a/build_windows.md +++ b/build_windows.md @@ -115,7 +115,7 @@ the following command, from a PowerShell or Windows Command Prompt terminal in wsl --install ``` -For more information refer to +Podman requires WSL v2.5.1 or later. For more information refer to [the official documentation](https://learn.microsoft.com/en-us/windows/wsl/). #### Hyper-V diff --git a/docs/tutorials/podman-for-windows.md b/docs/tutorials/podman-for-windows.md index ab825d3f86..1b9c14c7c3 100644 --- a/docs/tutorials/podman-for-windows.md +++ b/docs/tutorials/podman-for-windows.md @@ -53,6 +53,8 @@ later. Internally, WSL and Hyper-V use virtualization, so your system must support and have hardware virtualization enabled. If you are running Windows on a VM, you must have a VM that supports nested virtualization. +WSL v2.5.1 or later is required. + Hyper-V is only available on Windows Enterprise, Pro, or Education editions (not Home). The command to initialize the first Hyper-V Podman machine, and the command to remove the last one, both require administrator privileges because they create diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 39c4f2b32a..b33249c6e2 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -119,6 +119,22 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err return dist, nil } +func verifyWSLVersion(rootful bool) error { + // WSL v2.5.1 or later is required. Earlier versions will fail to + // start Podman machines because they supported cgroupv1. + if !wutil.WSLVersionAtLeast(2, 5, 1) { + return fmt.Errorf("WSL version 2.5.1 or later is required to run Podman. Please upgrade WSL with: wsl --update") + } + + // WSL v2.7.5 or later is required in rootful mode. Earlier versions + // don't support nftables: + // https://github.com/podman-container-tools/podman/issues/25201 + if rootful && !wutil.WSLVersionAtLeast(2, 7, 5) { + return fmt.Errorf("WSL version 2.7.5 or later is required to run Podman in rootful mode. Please use rootless mode or upgrade WSL with: wsl --update") + } + return nil +} + func createKeys(mc *vmconfigs.MachineConfig, dist string) error { user := mc.SSH.RemoteUsername diff --git a/pkg/machine/wsl/stubber.go b/pkg/machine/wsl/stubber.go index d5b523cb8a..cab4e5a62c 100644 --- a/pkg/machine/wsl/stubber.go +++ b/pkg/machine/wsl/stubber.go @@ -35,6 +35,10 @@ func (w WSLStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConf _ = setupWslProxyEnv() + if err = verifyWSLVersion(mc.HostUser.Rootful); err != nil { + return err + } + if opts.UserModeNetworking { if err = verifyWSLUserModeCompat(); err != nil { return err @@ -134,6 +138,9 @@ func (w WSLStubber) SetProviderAttrs(mc *vmconfigs.MachineConfig, opts define.Se } if opts.Rootful != nil && mc.HostUser.Rootful != *opts.Rootful { + if err = verifyWSLVersion(*opts.Rootful); err != nil { + return err + } if err := mc.SetRootful(*opts.Rootful); err != nil { return err } diff --git a/pkg/machine/wsl/wutil/wutil.go b/pkg/machine/wsl/wutil/wutil.go index 6ed0840b95..5caeda166f 100644 --- a/pkg/machine/wsl/wutil/wutil.go +++ b/pkg/machine/wsl/wutil/wutil.go @@ -9,6 +9,8 @@ import ( "io" "os" "os/exec" + "regexp" + "strconv" "strings" "sync" "syscall" @@ -19,6 +21,8 @@ import ( var ( onceStatus sync.Once status wslStatus + onceVersion sync.Once + version wslVersion wslNotInstalledMessages = []string{"kernel file is not found", "The Windows Subsystem for Linux is not installed"} vmpDisabledMessages = []string{"enable the Virtual Machine Platform Windows feature", "Enable \"Virtual Machine Platform\""} wslDisabledMessages = []string{"enable the \"Windows Subsystem for Linux\" optional component"} @@ -30,6 +34,12 @@ type wslStatus struct { wslFeatureEnabled bool } +type wslVersion struct { + val [3]int + cmdErr error + parseErr error +} + func NewWSLCommand(arg ...string) *exec.Cmd { cmd := exec.Command("wsl", arg...) cmd.Env = append(os.Environ(), "WSL_UTF8=1") @@ -88,14 +98,69 @@ func IsWSLInstalled() bool { } func IsWSLStoreVersionInstalled() bool { - cmd := SilentExecCmd("--version") - cmd.Stdout = nil - cmd.Stderr = nil - if err := cmd.Run(); err != nil { + // The Windows built-in version of WSL returns an error when running + // `wsl --version`. Recent versions of WSL instead, are distributed + // via the Microsoft store and supports `wsl --version`. + return parseWSLVersion().cmdErr == nil +} + +// WSLVersionAtLeast returns true if the installed WSL version is >= major.minor.patch +// or if it fails to parse the wsl --version output. It returns false otherwise and if +// the `wsl --version` command has failed. +func WSLVersionAtLeast(major, minor, patch int) bool { + v := parseWSLVersion() + // When the `wsl --version` command fails we return false + // because it's older WSL versions behavior. + if v.cmdErr != nil { + logrus.Debugf("command error while checking the WSL version: %v", v.cmdErr) return false } + // When there is a parsing error we assume that's a WSL future version + // where the output format has been changed. + if v.parseErr != nil { + logrus.Debugf("parsing error while checking the WSL version: %v", v.parseErr) + return true + } + return v.val[0] > major || + (v.val[0] == major && v.val[1] > minor) || + (v.val[0] == major && v.val[1] == minor && v.val[2] >= patch) +} - return true +// parseWSLVersion returns the WSL version as [major, minor, patch] in the +// returned wslVersion struct. +// The returned `wslVersion.cmdErr` is non nil if the `wsl --version` command fails. +// The returned `wslVersion.parseErr` is non nil if the major.minor.patch string +// cannot found in the returned output. +func parseWSLVersion() wslVersion { + onceVersion.Do(func() { + cmd := SilentExecCmd("--version") + out, err := cmd.Output() + if err != nil { + version = wslVersion{cmdErr: fmt.Errorf("running wsl --version failed: %w", err)} + return + } + version = versionFromLocalizedOutput(string(out)) + }) + return version +} + +// versionFromLocalizedOutput returns the WSL version from the output of "wsl --version". +// The output labels are localized, so the version is the first version number +// in the output, that is the WSL package version in all languages. +func versionFromLocalizedOutput(out string) wslVersion { + v := wslVersion{} + versionRegexp := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`) + m := versionRegexp.FindStringSubmatch(out) + if m == nil { + return wslVersion{parseErr: fmt.Errorf("could not parse WSL version from %q", out)} + } + for i := range v.val { + var err error + if v.val[i], err = strconv.Atoi(m[i+1]); err != nil { + return wslVersion{parseErr: fmt.Errorf("could not parse the WSL version digit %q", m[i+1])} + } + } + return v } func matchOutputLine(output io.ReadCloser) (wslStatus, error) { diff --git a/pkg/machine/wsl/wutil/wutil_test.go b/pkg/machine/wsl/wutil/wutil_test.go index 582368ad4d..e557077021 100644 --- a/pkg/machine/wsl/wutil/wutil_test.go +++ b/pkg/machine/wsl/wutil/wutil_test.go @@ -150,3 +150,56 @@ func TestNewWSLCommand(t *testing.T) { assert.Equal(t, []string{"--status"}, cmd.Args[1:]) assert.Contains(t, cmd.Env, "WSL_UTF8=1") } + +func TestVersionFromLocalizedOutput(t *testing.T) { + tests := []struct { + name string + output string + wantVersionVal [3]int + wantParseErr bool + }{ + { + "English", + "WSL version: 2.6.1.0\nKernel version: 6.6.87.2-1\nWSLg version: 1.0.66\nWindows version: 10.0.26100.4652\n", + [3]int{2, 6, 1}, + false, + }, + { + "German", + "WSL-Version: 2.5.10.0\nKernelversion: 6.6.87.2-1\nWSLg-Version: 1.0.66\nWindows-Version: 10.0.26100.4652\n", + [3]int{2, 5, 10}, + false, + }, + { + "Japanese", + "WSL バージョン: 3.0.3.0\nカーネル バージョン: 6.6.87.2-1\nWSLg バージョン: 1.0.66\nWindows バージョン: 10.0.26100.4652\n", + [3]int{3, 0, 3}, + false, + }, + { + "CRLF line endings", + "WSL version: 2.5.1.0\r\nKernel version: 6.6.87.2-1\r\n", + [3]int{2, 5, 1}, + false, + }, + { + "No version", + "Invalid command line option: --version\n", + [3]int{}, + true, + }, + { + "Empty", + "", + [3]int{}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := versionFromLocalizedOutput(tt.output) + assert.Equal(t, tt.wantVersionVal, v.val) + assert.Equal(t, tt.wantParseErr, v.parseErr != nil) + }) + } +}