Skip to content
Open
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
18 changes: 17 additions & 1 deletion pkg/lima/files/config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
vmType: "vz"
vmType: "{{ .VmType }}"
rosetta:
enabled: false
images:
Expand All @@ -17,7 +17,11 @@ ssh:
forwardAgent: true
loadDotSSHPubKeys: true
networks:
{{- if eq .VmType "qemu" }}
- lima: user-v2
{{- else }}
- vzNAT: true
{{- end }}
{{ if .Config.PortForwards }}
portForwards:
{{ range $port := .Config.PortForwards -}}
Expand All @@ -32,3 +36,15 @@ provision:
script: |
#!/bin/bash
echo "127.0.0.1 $(hostname)" >> /etc/hosts
{{- if eq .VmType "qemu" }}
# Additional network setup for QEMU on Linux
# 1. Find interface by MAC (matches the one set in your Go wrapper)
TAP_IFACE=$(ip -o link show | grep "52:54:00:12:34:56" | awk -F': ' '{print $2}' || echo "")

# 2. If found, configure it manually
if [ -n "$TAP_IFACE" ]; then
ip link set "$TAP_IFACE" up
# "|| true" prevents errors if the IP is already assigned from a previous run
ip addr add 192.168.56.5/24 dev "$TAP_IFACE" || true
fi
{{- end }}
30 changes: 26 additions & 4 deletions pkg/lima/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"text/template"

"github.com/roots/trellis-cli/command"
Expand Down Expand Up @@ -54,6 +55,8 @@ type Instance struct {
SshLocalPort int `json:"sshLocalPort,omitempty"`
Config Config `json:"config"`
Username string `json:"username,omitempty"`
GOOS string `json:"goos,omitempty"`
VmType string `json:"vmType,omitempty"`
}

func (i *Instance) ConfigFile() string {
Expand All @@ -62,6 +65,12 @@ func (i *Instance) ConfigFile() string {

func (i *Instance) GenerateConfig() (*bytes.Buffer, error) {
var contents bytes.Buffer

i.VmType = "vz"

if runtime.GOOS == "linux" {
i.VmType = "qemu"
}

tpl := template.Must(template.New("lima").Parse(ConfigTemplate))

Expand Down Expand Up @@ -113,15 +122,28 @@ Gets the IP address of the instance using the output of `ip route`:
192.168.64.1 proto dhcp scope link src 192.168.64.2 metric 100
*/
func (i *Instance) IP() (ip string, err error) {
output, err := command.Cmd(
"limactl",
[]string{"shell", "--workdir", "/", i.Name, "ip", "route", "show", "dev", "lima0"},
).CombinedOutput()
args := []string{"shell", "--workdir", "/", i.Name, "ip", "route", "show"}

// Keep existing macOS logic
if runtime.GOOS == "darwin" {
args = append(args, "dev", "lima0")
}

output, err := command.Cmd("limactl", args).CombinedOutput()
if err != nil {
return "", fmt.Errorf("%w: %v\n%s", IpErr, err, string(output))
}

// Prioritize the custom TAP interface IP (192.168.56.x)
if runtime.GOOS == "linux" {
reCustom := regexp.MustCompile(`src (192\.168\.56\.[0-9]+)`)
matchesCustom := reCustom.FindStringSubmatch(string(output))

if len(matchesCustom) >= 2 {
return matchesCustom[1], nil
}
}

re := regexp.MustCompile(`default via .* src ([0-9\.]+)`)
matches := re.FindStringSubmatch(string(output))
if len(matches) < 2 {
Expand Down
117 changes: 110 additions & 7 deletions pkg/lima/manager.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package lima

import (
"bufio"
"bytes"
"encoding/json"
"errors"
Expand All @@ -9,6 +10,7 @@
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/fatih/color"
Expand All @@ -22,6 +24,7 @@
const (
configDir = "lima"
RequiredMacOSVersion = "13.0.0"
RequiredLinuxVersion = "20.04"
)

var (
Expand Down Expand Up @@ -220,6 +223,61 @@
return err
}

if runtime.GOOS == "linux" {
// 1. Ensure tap0 exists (unchanged)
if _, err := exec.Command("ip", "link", "show", "tap0").Output(); err != nil {
m.ui.Info(color.YellowString("🔧 tap0 missing. Creating it (requires sudo)..."))
// Added 'set tap0 up' to ensure the link is active on host
cmd := exec.Command("sudo", "sh", "-c", "ip tuntap add dev tap0 mode tap user $(whoami) && ip addr add 192.168.56.1/24 dev tap0 && ip link set tap0 up")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("Failed to create tap0: %v", err)
}
}

m.ui.Info(color.YellowString("🛠️ Configuring QEMU Network Wrapper..."))

// 2. Create a dedicated directory for the wrapper
wrapperDir := filepath.Join(os.TempDir(), "lima-qemu-wrapper")
if err := os.MkdirAll(wrapperDir, 0755); err != nil {
return fmt.Errorf("failed to create wrapper dir: %v", err)
}

// 3. Name the script exactly 'qemu-system-x86_64' so Lima picks it up
wrapperPath := filepath.Join(wrapperDir, "qemu-system-x86_64")

wrapperScript := `#!/bin/bash
# Pass-through for help/version commands to prevent crashing during capability checks
if [[ "$@" == *"-netdev help"* ]] || \
[[ "$@" == *"-version"* ]] || \
[[ "$@" == *"-accel help"* ]] || \
[[ "$@" == *"-machine help"* ]] || \
[[ "$@" == *"-cpu help"* ]]; then
exec /usr/bin/qemu-system-x86_64 "$@"
fi

# Inject the TAP interface args BEFORE other args ($@) to ensure precedence
exec /usr/bin/qemu-system-x86_64 \
-netdev tap,id=mynet0,ifname=tap0,script=no,downscript=no \
-device virtio-net-pci,netdev=mynet0,mac=52:54:00:12:34:56 \
"$@"
`
if err := os.WriteFile(wrapperPath, []byte(wrapperScript), 0755); err != nil {
m.ui.Warn(fmt.Sprintf("Failed to create QEMU wrapper: %v", err))
}

// 4. CRITICAL: Prepend the wrapper directory to PATH
// This forces Lima to find your script before the real QEMU
currentPath := os.Getenv("PATH")
newPath := wrapperDir + string(os.PathListSeparator) + currentPath
os.Setenv("PATH", newPath)
}




err := command.WithOptions(
command.WithTermOutput(),
command.WithLogging(m.ui),
Expand Down Expand Up @@ -389,14 +447,59 @@
return version, nil
}

func ensureRequirements() error {
macOSVersion, err := getMacOSVersion()
if err != nil {
return ErrUnsupportedOS
}
func getLinuxVersion() (string, error) {
// /etc/os-release is the standard on almost all modern Linux distros
f, err := os.Open("/etc/os-release")
if err != nil {
return "", err
}
defer f.Close()

Check failure on line 456 in pkg/lima/manager.go

View workflow job for this annotation

GitHub Actions / Go Linting

Error return value of `f.Close` is not checked (errcheck)

scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
// Look for the line starting with VERSION_ID=
if strings.HasPrefix(line, "VERSION_ID=") {
// Remove the prefix
val := strings.TrimPrefix(line, "VERSION_ID=")
// Remove double or single quotes if present (e.g. "22.04" -> 22.04)
val = strings.Trim(val, `"'`)

// Normalize using your existing version package
verNormalized := version.Normalize(val)
return verNormalized, nil
}
}

if err := scanner.Err(); err != nil {
return "", err
}

return "", fmt.Errorf("VERSION_ID not found in /etc/os-release")
}

if version.Compare(macOSVersion, RequiredMacOSVersion, "<") {
return fmt.Errorf("%w", ErrUnsupportedOS)
func ensureRequirements() error {
var currentVersion string
var requiredVersion string
var err error

switch runtime.GOOS {
case "darwin":
currentVersion, err = getMacOSVersion()
requiredVersion = RequiredMacOSVersion
case "linux":
currentVersion, err = getLinuxVersion()
requiredVersion = RequiredLinuxVersion
default:
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
if err != nil {
return fmt.Errorf("failed to detect OS version: %w", err)
}

// Now compare the current version against the specific requirement for that OS
if version.Compare(currentVersion, requiredVersion, "<") {
return fmt.Errorf("OS version %s is too old; %s or newer is required", currentVersion, requiredVersion)
}

if err = Installed(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion trellis/trellis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
Loading