package main
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/mattn/go-isatty"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/lima-vm/lima/v2/cmd/yq"
"github.com/lima-vm/lima/v2/pkg/debugutil"
"github.com/lima-vm/lima/v2/pkg/driver/external/server"
"github.com/lima-vm/lima/v2/pkg/fsutil"
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
"github.com/lima-vm/lima/v2/pkg/osutil"
"github.com/lima-vm/lima/v2/pkg/version"
)
const (
DefaultInstanceName = "default"
basicCommand = "basic"
advancedCommand = "advanced"
)
func main() {
yq.MaybeRunYQ()
if runtime.GOOS == "windows" {
extras, hasExtra := os.LookupEnv("_LIMA_WINDOWS_EXTRA_PATH")
if hasExtra && strings.TrimSpace(extras) != "" {
p := os.Getenv("PATH")
err := os.Setenv("PATH", strings.TrimSpace(extras)+string(filepath.ListSeparator)+p)
if err != nil {
logrus.Warning("Can't add extras to PATH, relying entirely on system PATH")
}
}
}
rootCmd := newApp()
if err := executeWithPluginSupport(rootCmd, os.Args[1:]); err != nil {
server.StopAllExternalDrivers()
handleExitError(err)
logrus.Fatal(err)
}
server.StopAllExternalDrivers()
}
func newApp() *cobra.Command {
templatesDir := "$PREFIX/share/lima/templates"
if exe, err := os.Executable(); err == nil {
binDir := filepath.Dir(exe)
prefixDir := filepath.Dir(binDir)
templatesDir = filepath.Join(prefixDir, "share/lima/templates")
}
rootCmd := &cobra.Command{
Use: "limactl",
Short: "Lima: Linux virtual machines",
Version: strings.TrimPrefix(version.Version, "v"),
Example: fmt.Sprintf(` Start the default instance:
$ limactl start
Open a shell:
$ lima
Run a container:
$ lima nerdctl run -d --name nginx -p 8080:80 nginx:alpine
Stop the default instance:
$ limactl stop
See also template YAMLs: %s`, templatesDir),
SilenceUsage: true,
SilenceErrors: true,
DisableAutoGenTag: true,
}
rootCmd.PersistentFlags().String("log-level", "", "Set the logging level [trace, debug, info, warn, error]")
rootCmd.PersistentFlags().String("log-format", "text", "Set the logging format [text, json]")
rootCmd.PersistentFlags().Bool("debug", false, "Debug mode")
rootCmd.PersistentFlags().Bool("tty", isatty.IsTerminal(os.Stdout.Fd()), "Enable TUI interactions such as opening an editor. Defaults to true when stdout is a terminal. Set to false for automation.")
rootCmd.PersistentFlags().BoolP("yes", "y", false, "Alias of --tty=false")
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
l, _ := cmd.Flags().GetString("log-level")
if l != "" {
lvl, err := logrus.ParseLevel(l)
if err != nil {
return err
}
logrus.SetLevel(lvl)
}
logFormat, _ := cmd.Flags().GetString("log-format")
switch logFormat {
case "json":
formatter := new(logrus.JSONFormatter)
logrus.StandardLogger().SetFormatter(formatter)
case "text":
if runtime.GOOS == "windows" && isatty.IsCygwinTerminal(os.Stderr.Fd()) {
formatter := new(logrus.TextFormatter)
formatter.ForceColors = true
logrus.StandardLogger().SetFormatter(formatter)
}
default:
return fmt.Errorf("unsupported log-format: %q", logFormat)
}
debug, _ := cmd.Flags().GetBool("debug")
if debug {
logrus.SetLevel(logrus.DebugLevel)
debugutil.Debug = true
}
if osutil.IsBeingRosettaTranslated() && cmd.Parent().Name() != "completion" && cmd.Name() != "generate-doc" && cmd.Name() != "validate" {
return errors.New("limactl is running under rosetta, please reinstall lima with native arch")
}
if os.Geteuid() == 0 && cmd.Name() != "generate-doc" {
return errors.New("must not run as the root user")
}
dir, err := dirnames.LimaDir()
if err != nil {
return err
}
nfs, err := fsutil.IsNFS(dir)
if err != nil {
return err
}
if nfs {
return errors.New("must not run on NFS dir")
}
if cmd.Flags().Changed("yes") && cmd.Flags().Changed("tty") {
return errors.New("cannot use both --tty and --yes flags at the same time")
}
if cmd.Flags().Changed("yes") {
yesValue, _ := cmd.Flags().GetBool("yes")
if yesValue {
err := cmd.Flags().Set("tty", "false")
if err != nil {
return err
}
}
}
return nil
}
rootCmd.AddGroup(&cobra.Group{ID: "basic", Title: "Basic Commands:"})
rootCmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced Commands:"})
rootCmd.AddCommand(
newCreateCommand(),
newStartCommand(),
newStopCommand(),
newShellCommand(),
newCopyCommand(),
newListCommand(),
newDeleteCommand(),
newValidateCommand(),
newPruneCommand(),
newHostagentCommand(),
newGuestInstallCommand(),
newInfoCommand(),
newShowSSHCommand(),
newDebugCommand(),
newEditCommand(),
newFactoryResetCommand(),
newDiskCommand(),
newUsernetCommand(),
newGenDocCommand(),
newGenSchemaCommand(),
newSnapshotCommand(),
newProtectCommand(),
newUnprotectCommand(),
newTunnelCommand(),
newTemplateCommand(),
newRestartCommand(),
newSudoersCommand(),
newStartAtLoginCommand(),
newNetworkCommand(),
newCloneCommand(),
)
return rootCmd
}
func handleExitError(err error) {
if err == nil {
return
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.ExitCode())
return
}
}
func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error {
if len(args) > 0 {
cmd, _, err := rootCmd.Find(args)
if err != nil || cmd == rootCmd {
runExternalPlugin(rootCmd.Context(), args[0], args[1:])
}
}
rootCmd.SetArgs(args)
return rootCmd.Execute()
}
func runExternalPlugin(ctx context.Context, name string, args []string) {
if ctx == nil {
ctx = context.Background()
}
if err := updatePathEnv(); err != nil {
logrus.Warnf("failed to update PATH environment: %v", err)
}
externalCmd := "limactl-" + name
execPath, err := exec.LookPath(externalCmd)
if err != nil {
return
}
cmd := exec.CommandContext(ctx, execPath, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
err = cmd.Run()
handleExitError(err)
if err == nil {
os.Exit(0)
}
logrus.Fatalf("external command %q failed: %v", execPath, err)
}
func updatePathEnv() error {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
binDir := filepath.Dir(exe)
currentPath := os.Getenv("PATH")
newPath := binDir + string(filepath.ListSeparator) + currentPath
if err := os.Setenv("PATH", newPath); err != nil {
return fmt.Errorf("failed to set PATH environment: %w", err)
}
logrus.Debugf("updated PATH to prioritize %s", binDir)
return nil
}
func WrapArgsError(argFn cobra.PositionalArgs) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
err := argFn(cmd, args)
if err == nil {
return nil
}
return fmt.Errorf("%q %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
cmd.CommandPath(), err.Error(),
cmd.CommandPath(),
cmd.UseLine(), cmd.Short,
)
}
}