diff --git a/pkg/lima/files/config.yml b/pkg/lima/files/config.yml index fb91e759..0a6a4f0e 100644 --- a/pkg/lima/files/config.yml +++ b/pkg/lima/files/config.yml @@ -1,6 +1,16 @@ +{{- if eq .VMType "vz" -}} vmType: "vz" rosetta: enabled: false +mountType: "virtiofs" +networks: +- vzNAT: true +{{- else -}} +vmType: "qemu" +mountType: "9p" +networks: +- lima: user-v2 +{{- end }} images: {{ range $image := .Config.Images -}} - location: {{ $image.Location }} @@ -12,12 +22,9 @@ mounts: mountPoint: /srv/www/{{ $siteName }}/current writable: true {{ end }} -mountType: "virtiofs" ssh: forwardAgent: true loadDotSSHPubKeys: true -networks: -- vzNAT: true {{ if .Config.PortForwards }} portForwards: {{ range $port := .Config.PortForwards -}} diff --git a/pkg/lima/instance.go b/pkg/lima/instance.go index f84eacc2..92a3c69b 100644 --- a/pkg/lima/instance.go +++ b/pkg/lima/instance.go @@ -54,6 +54,7 @@ type Instance struct { SshLocalPort int `json:"sshLocalPort,omitempty"` Config Config `json:"config"` Username string `json:"username,omitempty"` + VMType string `json:"vmType,omitempty"` // "vz" for macOS, "qemu" for Linux } func (i *Instance) ConfigFile() string { @@ -111,15 +112,30 @@ Gets the IP address of the instance using the output of `ip route`: default via 192.168.64.1 proto dhcp src 192.168.64.2 metric 100 192.168.64.0/24 proto kernel scope link src 192.168.64.2 192.168.64.1 proto dhcp scope link src 192.168.64.2 metric 100 + +On Linux with QEMU, the interface name varies (eth0, enp0s1, etc.) so we use +the default route without specifying a device. */ func (i *Instance) IP() (ip string, err error) { - output, err := command.Cmd( - "limactl", - []string{"shell", "--workdir", "/", i.Name, "ip", "route", "show", "dev", "lima0"}, - ).CombinedOutput() + // Try lima0 first (macOS vz), then fall back to default route (Linux QEMU) + args := []string{"shell", "--workdir", "/", i.Name, "ip", "route", "show", "dev", "lima0"} + output, err := command.Cmd("limactl", args).CombinedOutput() if err != nil { - return "", fmt.Errorf("%w: %v\n%s", IpErr, err, string(output)) + // lima0 doesn't exist (likely Linux QEMU), try getting default route + args = []string{"shell", "--workdir", "/", i.Name, "ip", "route", "get", "1.1.1.1"} + output, err = command.Cmd("limactl", args).CombinedOutput() + if err != nil { + return "", fmt.Errorf("%w: %v\n%s", IpErr, err, string(output)) + } + + // Parse output like: "1.1.1.1 via 10.0.2.2 dev eth0 src 10.0.2.15 uid 1000" + re := regexp.MustCompile(`src ([0-9\.]+)`) + matches := re.FindStringSubmatch(string(output)) + if len(matches) < 2 { + return "", fmt.Errorf("%w: no IP address could be matched in the ip route output\n%s", IpErr, string(output)) + } + return matches[1], nil } re := regexp.MustCompile(`default via .* src ([0-9\.]+)`) diff --git a/pkg/lima/instance_test.go b/pkg/lima/instance_test.go index 1449955a..9070083c 100644 --- a/pkg/lima/instance_test.go +++ b/pkg/lima/instance_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "testing" "github.com/roots/trellis-cli/command" @@ -19,8 +20,15 @@ func TestGenerateConfig(t *testing.T) { dir := t.TempDir() + // Set VMType based on OS, as newInstance() does + vmType := "qemu" + if runtime.GOOS == "darwin" { + vmType = "vz" + } + instance := &Instance{ - Dir: dir, + Dir: dir, + VMType: vmType, Config: Config{ Images: []Image{ { @@ -45,9 +53,14 @@ func TestGenerateConfig(t *testing.T) { absSitePath := filepath.Join(trellis.Path, "../site") - expected := fmt.Sprintf(`vmType: "vz" + var expected string + if runtime.GOOS == "darwin" { + expected = fmt.Sprintf(`vmType: "vz" rosetta: enabled: false +mountType: "virtiofs" +networks: +- vzNAT: true images: - location: http://ubuntu.com/focal arch: aarch64 @@ -57,12 +70,39 @@ mounts: mountPoint: /srv/www/example.com/current writable: true -mountType: "virtiofs" ssh: forwardAgent: true loadDotSSHPubKeys: true + +portForwards: +- guestPort: 80 + hostPort: 1234 + +containerd: + user: false +provision: +- mode: system + script: | + #!/bin/bash + echo "127.0.0.1 $(hostname)" >> /etc/hosts +`, absSitePath) + } else { + expected = fmt.Sprintf(`vmType: "qemu" +mountType: "9p" networks: -- vzNAT: true +- lima: user-v2 +images: +- location: http://ubuntu.com/focal + arch: aarch64 + +mounts: +- location: %s + mountPoint: /srv/www/example.com/current + writable: true + +ssh: + forwardAgent: true + loadDotSSHPubKeys: true portForwards: - guestPort: 80 @@ -76,6 +116,7 @@ provision: #!/bin/bash echo "127.0.0.1 $(hostname)" >> /etc/hosts `, absSitePath) + } if content.String() != expected { t.Errorf("expected %s\ngot %s", expected, content.String()) @@ -91,8 +132,15 @@ func TestUpdateConfig(t *testing.T) { dir := t.TempDir() + // Set VMType based on OS, as newInstance() does + vmType := "qemu" + if runtime.GOOS == "darwin" { + vmType = "vz" + } + instance := &Instance{ - Dir: dir, + Dir: dir, + VMType: vmType, Config: Config{ Images: []Image{ { @@ -123,9 +171,14 @@ func TestUpdateConfig(t *testing.T) { absSitePath := filepath.Join(trellis.Path, "../site") - expected := fmt.Sprintf(`vmType: "vz" + var expected string + if runtime.GOOS == "darwin" { + expected = fmt.Sprintf(`vmType: "vz" rosetta: enabled: false +mountType: "virtiofs" +networks: +- vzNAT: true images: - location: http://ubuntu.com/focal arch: aarch64 @@ -135,12 +188,39 @@ mounts: mountPoint: /srv/www/example.com/current writable: true -mountType: "virtiofs" ssh: forwardAgent: true loadDotSSHPubKeys: true + +portForwards: +- guestPort: 80 + hostPort: 1234 + +containerd: + user: false +provision: +- mode: system + script: | + #!/bin/bash + echo "127.0.0.1 $(hostname)" >> /etc/hosts +`, absSitePath) + } else { + expected = fmt.Sprintf(`vmType: "qemu" +mountType: "9p" networks: -- vzNAT: true +- lima: user-v2 +images: +- location: http://ubuntu.com/focal + arch: aarch64 + +mounts: +- location: %s + mountPoint: /srv/www/example.com/current + writable: true + +ssh: + forwardAgent: true + loadDotSSHPubKeys: true portForwards: - guestPort: 80 @@ -154,6 +234,7 @@ provision: #!/bin/bash echo "127.0.0.1 $(hostname)" >> /etc/hosts `, absSitePath) + } if string(content) != expected { t.Errorf("expected %s\ngot %s", expected, string(content)) diff --git a/pkg/lima/lima.go b/pkg/lima/lima.go index 88ebc335..cacbf5b4 100644 --- a/pkg/lima/lima.go +++ b/pkg/lima/lima.go @@ -20,11 +20,18 @@ func Installed() error { output, err := command.Cmd("limactl", []string{"-v"}).Output() if err != nil { - return fmt.Errorf("Could get determine the version of Lima.") + return fmt.Errorf("Could not determine the version of Lima.") } - re := regexp.MustCompile(`.*([0-9]+\.[0-9]+\.[0-9]+(-alpha|beta)?)`) + re := regexp.MustCompile(`([0-9]+\.[0-9]+\.[0-9]+(-alpha|-beta)?)`) v := re.FindStringSubmatch(string(output)) + + // If no semantic version found (e.g., git hash on Linux distro packages), + // assume it's a recent enough version since distro packages are typically up-to-date + if len(v) < 2 { + return nil + } + constraint := version.NewConstrainGroupFromString(VersionRequired) matched := constraint.Match(v[1]) diff --git a/pkg/lima/manager.go b/pkg/lima/manager.go index e457090f..5554e662 100644 --- a/pkg/lima/manager.go +++ b/pkg/lima/manager.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "github.com/fatih/color" @@ -26,7 +27,7 @@ const ( var ( ErrConfigPath = errors.New("could not create config directory") - ErrUnsupportedOS = errors.New("unsupported OS or macOS version. The macOS Virtualization Framework requires macOS 13.0 (Ventura) or later.") + ErrUnsupportedOS = errors.New("unsupported OS version. Lima on macOS requires macOS 13.0 (Ventura) or later.") ) type PortFinder interface { @@ -299,6 +300,14 @@ func (m *Manager) initInstance(instance *Instance) { func (m *Manager) newInstance(name string) (Instance, error) { instance := Instance{Name: name} + + // Set VMType based on OS: "vz" for macOS (Virtualization.framework), "qemu" for Linux + if runtime.GOOS == "darwin" { + instance.VMType = "vz" + } else { + instance.VMType = "qemu" + } + m.initInstance(&instance) images := []Image{} @@ -390,6 +399,16 @@ func getMacOSVersion() (string, error) { } func ensureRequirements() error { + if runtime.GOOS == "linux" { + // Linux doesn't have macOS version requirements + // Just check that Lima is installed + if err := Installed(); err != nil { + return fmt.Errorf("%s\nInstall or upgrade Lima to continue.\n\nSee https://lima-vm.io/docs/installation/ for installation options.", err.Error()) + } + return nil + } + + // macOS requirements macOSVersion, err := getMacOSVersion() if err != nil { return ErrUnsupportedOS diff --git a/pkg/lima/manager_test.go b/pkg/lima/manager_test.go index e8ca27a7..418fc5f7 100644 --- a/pkg/lima/manager_test.go +++ b/pkg/lima/manager_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "testing" "github.com/hashicorp/cli" @@ -34,17 +35,28 @@ func TestNewManager(t *testing.T) { path := os.Getenv("PATH") t.Setenv("PATH", fmt.Sprintf("PATH=%s:%s", path, tmp)) - commands := []command.MockCommand{ - { - Command: "sw_vers", - Args: []string{"-productVersion"}, - Output: `13.0.1`, - }, - { - Command: "limactl", - Args: []string{"-v"}, - Output: `limactl version 0.15.0`, - }, + var commands []command.MockCommand + if runtime.GOOS == "darwin" { + commands = []command.MockCommand{ + { + Command: "sw_vers", + Args: []string{"-productVersion"}, + Output: `13.0.1`, + }, + { + Command: "limactl", + Args: []string{"-v"}, + Output: `limactl version 0.15.0`, + }, + } + } else { + commands = []command.MockCommand{ + { + Command: "limactl", + Args: []string{"-v"}, + Output: `limactl version 0.15.0`, + }, + } } defer command.MockExecCommands(t, commands)() @@ -55,6 +67,11 @@ func TestNewManager(t *testing.T) { } func TestNewManagerUnsupportedOS(t *testing.T) { + // This test is macOS-specific (tests for old macOS version rejection) + if runtime.GOOS != "darwin" { + t.Skip("Skipping macOS-specific test on non-macOS platform") + } + defer trellis.LoadFixtureProject(t)() trellis := trellis.NewTrellis() if err := trellis.LoadProject(); err != nil { @@ -81,7 +98,7 @@ func TestNewManagerUnsupportedOS(t *testing.T) { t.Fatal(err) } - expected := "unsupported OS or macOS version. The macOS Virtualization Framework requires macOS 13.0 (Ventura) or later." + expected := "unsupported OS version. Lima on macOS requires macOS 13.0 (Ventura) or later." if err.Error() != expected { t.Errorf("expected error to be %q, got %q", expected, err.Error()) diff --git a/trellis/trellis.go b/trellis/trellis.go index 3591a46b..7e4c7c1a 100644 --- a/trellis/trellis.go +++ b/trellis/trellis.go @@ -387,7 +387,7 @@ func (t *Trellis) WriteYamlFile(s interface{}, path string, header string) error func (t *Trellis) VmManagerType() string { switch t.CliConfig.Vm.Manager { case "auto": - if runtime.GOOS == "darwin" { + if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { return "lima" } return ""