Repository: lima-vm/lima Branch: master Commit: 69751d55f379 Files: 676 Total size: 2.3 MB Directory structure: gitextract_kwe40__b/ ├── .codespellrc ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ ├── actions/ │ │ ├── setup_cache_for_template/ │ │ │ └── action.yml │ │ └── upload_failure_logs_if_exists/ │ │ └── action.yml │ ├── dependabot.yml │ └── workflows/ │ ├── codeql.yaml │ ├── release.yml │ ├── scorecard.yml │ ├── spell.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── .ls-lint.yml ├── .markdownlint.json ├── .protolint.yaml ├── .shellcheckrc ├── .yamllint ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── NOTICE ├── README.md ├── ROADMAP.md ├── cmd/ │ ├── apptainer.lima │ ├── docker.lima │ ├── kubectl.lima │ ├── lima │ ├── lima-driver-krunkit/ │ │ └── main_darwin_arm64.go │ ├── lima-driver-qemu/ │ │ └── main.go │ ├── lima-driver-vz/ │ │ └── main_darwin.go │ ├── lima-driver-wsl2/ │ │ └── main_windows.go │ ├── lima-guestagent/ │ │ ├── daemon_linux.go │ │ ├── fake_cloud_init_darwin.go │ │ ├── install_systemd_linux.go │ │ ├── lima-guestagent.TEMPLATE.service │ │ ├── main.go │ │ ├── root_darwin.go │ │ ├── root_linux.go │ │ └── root_others.go │ ├── lima.bat │ ├── limactl/ │ │ ├── clone.go │ │ ├── completion.go │ │ ├── copy.go │ │ ├── debug.go │ │ ├── delete.go │ │ ├── disk.go │ │ ├── edit.go │ │ ├── editflags/ │ │ │ ├── editflags.go │ │ │ └── editflags_test.go │ │ ├── factory-reset.go │ │ ├── gendoc.go │ │ ├── genschema.go │ │ ├── guest-install.go │ │ ├── hostagent.go │ │ ├── info.go │ │ ├── list.go │ │ ├── main.go │ │ ├── main_darwin.go │ │ ├── main_qemu.go │ │ ├── main_windows.go │ │ ├── network.go │ │ ├── protect.go │ │ ├── prune.go │ │ ├── restart.go │ │ ├── shell.go │ │ ├── shell_test.go │ │ ├── show-ssh.go │ │ ├── snapshot.go │ │ ├── start-at-login.go │ │ ├── start-at-login_unix.go │ │ ├── start-at-login_windows.go │ │ ├── start.go │ │ ├── stop.go │ │ ├── sudoers.go │ │ ├── sudoers_darwin.go │ │ ├── sudoers_nodarwin.go │ │ ├── template.go │ │ ├── tunnel.go │ │ ├── unprotect.go │ │ ├── usernet.go │ │ └── watch.go │ ├── limactl-mcp/ │ │ └── main.go │ ├── limactl-url-fedora-rawhide │ ├── nerdctl.lima │ ├── podman.lima │ └── yq/ │ └── yq.go ├── docs/ │ └── README.md ├── go.mod ├── go.sum ├── hack/ │ ├── allowed-licenses.txt │ ├── ansible-test.yaml │ ├── bats/ │ │ ├── README.md │ │ ├── extras/ │ │ │ ├── README.md │ │ │ ├── colima.bats │ │ │ ├── freebsd.bats │ │ │ ├── k8s.bats │ │ │ └── port-monitor.bats │ │ ├── helpers/ │ │ │ ├── limactl.bash │ │ │ ├── load.bash │ │ │ └── logs.bash │ │ └── tests/ │ │ ├── copy.bats │ │ ├── list.bats │ │ ├── mcp.bats │ │ ├── path.bats │ │ ├── preserve-env.bats │ │ ├── protect.bats │ │ ├── shell-sync.bats │ │ ├── shell.bats │ │ ├── url-github.bats │ │ └── yq.bats │ ├── brew-install-version.sh │ ├── cache-common-inc.sh │ ├── calculate-cache.sh │ ├── codesign/ │ │ └── debugserver │ ├── common.inc.sh │ ├── debug-cache.sh │ ├── gogenerate/ │ │ └── protoc.sh │ ├── inject-cmdline-to-template.sh │ ├── install-qemu.sh │ ├── ltag/ │ │ ├── bash.txt │ │ ├── dockerfile.txt │ │ ├── go.txt │ │ └── makefile.txt │ ├── oss-fuzz-build.sh │ ├── test-mount-home.sh │ ├── test-nonplain-static-port-forward.sh │ ├── test-plain-static-port-forward.sh │ ├── test-port-forwarding.pl │ ├── test-selinux.sh │ ├── test-templates/ │ │ ├── alpine-iso-9p-writable.yaml │ │ ├── net-user-v2.yaml │ │ └── test-misc.yaml │ ├── test-templates.sh │ ├── test-upgrade.sh │ ├── toolexec-for-codesign.sh │ ├── tools/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── pinversion.go │ ├── update-template-almalinux-kitten.sh │ ├── update-template-almalinux.sh │ ├── update-template-alpine.sh │ ├── update-template-archlinux.sh │ ├── update-template-centos-stream.sh │ ├── update-template-debian.sh │ ├── update-template-fedora.sh │ ├── update-template-freebsd.sh │ ├── update-template-macos.sh │ ├── update-template-opensuse.sh │ ├── update-template-oraclelinux.sh │ ├── update-template-rocky.sh │ ├── update-template-ubuntu.sh │ ├── update-template.sh │ └── validate-artifact.sh ├── pkg/ │ ├── apfs/ │ │ ├── chown.go │ │ ├── chown_darwin_test.go │ │ ├── fletcher64.go │ │ ├── fletcher64_test.go │ │ └── types.go │ ├── autostart/ │ │ ├── autostart.go │ │ ├── autostart_test.go │ │ ├── launchd/ │ │ │ ├── io.lima-vm.autostart.INSTANCE.plist │ │ │ ├── launchd.go │ │ │ └── launchd_test.go │ │ ├── managers.go │ │ ├── managers_darwin.go │ │ ├── managers_linux.go │ │ ├── managers_others.go │ │ └── systemd/ │ │ ├── lima-vm@INSTANCE.service │ │ ├── systemd.go │ │ ├── systemd_linux.go │ │ ├── systemd_others.go │ │ └── systemd_test.go │ ├── bicopy/ │ │ └── bicopy.go │ ├── cacheutil/ │ │ └── cacheutil.go │ ├── cidata/ │ │ ├── cidata.TEMPLATE.d/ │ │ │ ├── boot.FreeBSD/ │ │ │ │ └── 05-lima-mounts.sh │ │ │ ├── boot.Linux/ │ │ │ │ ├── 00-alpine-user-group.sh │ │ │ │ ├── 00-check-rtc-and-wait-ntp.sh │ │ │ │ ├── 00-guest-home.sh │ │ │ │ ├── 00-modprobe.sh │ │ │ │ ├── 00-reboot-if-required.sh │ │ │ │ ├── 01-alpine-ash-as-bash.sh │ │ │ │ ├── 04-persistent-data-volume.sh │ │ │ │ ├── 05-lima-disks.sh │ │ │ │ ├── 05-lima-mounts.sh │ │ │ │ ├── 06-enable-mdns-on-systemd.sh │ │ │ │ ├── 06-etc-hosts.sh │ │ │ │ ├── 07-etc-environment.sh │ │ │ │ ├── 08-shell-prompt.sh │ │ │ │ ├── 09-host-dns-setup.sh │ │ │ │ ├── 10-alpine-prep.sh │ │ │ │ ├── 11-colorterm-environment.sh │ │ │ │ ├── 20-rootless-base.sh │ │ │ │ ├── 25-guestagent-base.sh │ │ │ │ ├── 30-install-packages.sh │ │ │ │ ├── 35-setup-packages.sh │ │ │ │ └── 40-install-containerd.sh │ │ │ ├── boot.essential.FreeBSD/ │ │ │ │ └── 00-freebsd-user-group.sh │ │ │ ├── boot.sh │ │ │ ├── etc_environment │ │ │ ├── lima.env │ │ │ ├── meta-data │ │ │ ├── network-config │ │ │ ├── param.env │ │ │ ├── user-data │ │ │ ├── util/ │ │ │ │ └── compare_version.sh │ │ │ └── util.FreeBSD/ │ │ │ └── print_cidata_fstab.lua │ │ ├── cidata.go │ │ ├── cidata_test.go │ │ ├── cloudinittypes/ │ │ │ ├── metadata.go │ │ │ └── userdata.go │ │ ├── fuzz_test.go │ │ ├── template.go │ │ └── template_test.go │ ├── copytool/ │ │ ├── copytool.go │ │ ├── copytool_test.go │ │ ├── rsync.go │ │ └── scp.go │ ├── debugutil/ │ │ └── debugutil.go │ ├── downloader/ │ │ ├── downloader.go │ │ ├── downloader_test.go │ │ ├── fuzz_test.go │ │ └── testdata/ │ │ └── downloader.txt │ ├── driver/ │ │ ├── driver.go │ │ ├── external/ │ │ │ ├── client/ │ │ │ │ ├── client.go │ │ │ │ └── methods.go │ │ │ ├── driver.pb.desc │ │ │ ├── driver.pb.go │ │ │ ├── driver.proto │ │ │ ├── driver_grpc.pb.go │ │ │ ├── gen.go │ │ │ └── server/ │ │ │ ├── methods.go │ │ │ └── server.go │ │ ├── krunkit/ │ │ │ ├── boot.Linux/ │ │ │ │ ├── 00-add-user-to-video-render-group.sh │ │ │ │ └── 01-gpu-device-perms.sh │ │ │ ├── errors_darwin_arm64.go │ │ │ ├── krunkit_darwin_arm64.go │ │ │ └── krunkit_driver_darwin_arm64.go │ │ ├── qemu/ │ │ │ ├── entitlementutil/ │ │ │ │ └── entitlementutil.go │ │ │ ├── qemu.go │ │ │ ├── qemu_driver.go │ │ │ ├── qemu_test.go │ │ │ └── register.go │ │ ├── vz/ │ │ │ ├── boot.Linux/ │ │ │ │ └── 05-rosetta-volume.sh │ │ │ ├── errors_darwin.go │ │ │ ├── network_darwin.go │ │ │ ├── network_darwin_test.go │ │ │ ├── register.go │ │ │ ├── rosetta_directory_share.go │ │ │ ├── rosetta_directory_share_arm64.go │ │ │ ├── vm_darwin.go │ │ │ ├── vm_darwin_amd64.go │ │ │ ├── vm_darwin_arm64.go │ │ │ ├── vsock_forwarder.go │ │ │ └── vz_driver_darwin.go │ │ └── wsl2/ │ │ ├── boot.Linux/ │ │ │ └── 02-no-cloud-init-setup.sh │ │ ├── errors_windows.go │ │ ├── fs.go │ │ ├── lima-init.TEMPLATE │ │ ├── register.go │ │ ├── vm_windows.go │ │ └── wsl_driver_windows.go │ ├── driverutil/ │ │ ├── disk.go │ │ ├── disk_test.go │ │ ├── instance.go │ │ └── vm.go │ ├── editutil/ │ │ ├── editorcmd/ │ │ │ └── editorcmd.go │ │ └── editutil.go │ ├── envutil/ │ │ ├── envutil.go │ │ └── envutil_test.go │ ├── executil/ │ │ ├── command.go │ │ ├── opts_others.go │ │ └── opts_windows.go │ ├── fileutils/ │ │ └── download.go │ ├── freeport/ │ │ ├── freeport.go │ │ ├── freeport_unix.go │ │ └── freeport_windows.go │ ├── fsutil/ │ │ ├── fsutil_linux.go │ │ └── fsutil_others.go │ ├── guestagent/ │ │ ├── api/ │ │ │ ├── client/ │ │ │ │ ├── client.go │ │ │ │ └── credentials.go │ │ │ ├── gen.go │ │ │ ├── guestservice.pb.desc │ │ │ ├── guestservice.pb.go │ │ │ ├── guestservice.proto │ │ │ ├── guestservice_grpc.pb.go │ │ │ ├── ipport.go │ │ │ └── server/ │ │ │ └── server.go │ │ ├── fakecloudinit/ │ │ │ └── fakecloudinit_darwin.go │ │ ├── guestagent.go │ │ ├── guestagent_linux.go │ │ ├── kubernetesservice/ │ │ │ ├── kubernetesservice.go │ │ │ ├── kubernetesservice_test.go │ │ │ └── types.go │ │ ├── serialport/ │ │ │ ├── serialconn_linux.go │ │ │ ├── seriallistener_linux.go │ │ │ └── serialport_linux.go │ │ ├── sockets/ │ │ │ ├── sockets.go │ │ │ ├── sockets_linux.go │ │ │ └── sockets_linux_test.go │ │ ├── ticker/ │ │ │ ├── compound.go │ │ │ ├── ebpf_linux.go │ │ │ ├── simple.go │ │ │ └── ticker.go │ │ └── timesync/ │ │ ├── timesync_linux.go │ │ └── timesync_others.go │ ├── guestpatch/ │ │ └── macos/ │ │ └── macos_darwin.go │ ├── hostagent/ │ │ ├── api/ │ │ │ ├── api.go │ │ │ ├── client/ │ │ │ │ └── client.go │ │ │ └── server/ │ │ │ └── server.go │ │ ├── dns/ │ │ │ ├── dns.go │ │ │ └── dns_test.go │ │ ├── events/ │ │ │ ├── events.go │ │ │ └── watcher.go │ │ ├── hostagent.go │ │ ├── hostagent_unix.go │ │ ├── hostagent_windows.go │ │ ├── inotify.go │ │ ├── inotify_darwin.go │ │ ├── inotify_linux.go │ │ ├── inotify_others.go │ │ ├── inotify_test.go │ │ ├── mount.go │ │ ├── port.go │ │ ├── port_darwin.go │ │ ├── port_others.go │ │ ├── port_windows.go │ │ ├── requirements.go │ │ └── timesync.go │ ├── httpclientutil/ │ │ ├── httpclientutil.go │ │ ├── httpclientutil_others.go │ │ └── httpclientutil_windows.go │ ├── httputil/ │ │ └── httputil.go │ ├── identifiers/ │ │ ├── validate.go │ │ └── validate_test.go │ ├── imgutil/ │ │ ├── manager.go │ │ ├── nativeimgutil/ │ │ │ ├── asifutil/ │ │ │ │ ├── asif_darwin.go │ │ │ │ ├── asif_darwin_test.go │ │ │ │ └── asif_others.go │ │ │ ├── fuzz_test.go │ │ │ ├── nativeimgutil.go │ │ │ └── nativeimgutil_test.go │ │ └── proxyimgutil/ │ │ └── proxyimgutil.go │ ├── instance/ │ │ ├── ansible.go │ │ ├── clone.go │ │ ├── create.go │ │ ├── delete.go │ │ ├── hostname/ │ │ │ ├── hostname.go │ │ │ └── hostname_test.go │ │ ├── restart.go │ │ ├── start.go │ │ ├── start_unix.go │ │ ├── start_windows.go │ │ └── stop.go │ ├── ioutilx/ │ │ └── ioutilx.go │ ├── iso9660util/ │ │ ├── fuzz_test.go │ │ ├── iso9660util.go │ │ └── joliet.go │ ├── jsonschemautil/ │ │ ├── jsonschemautil.go │ │ ├── jsonschemautil_test.go │ │ └── testdata/ │ │ ├── invalid.yaml │ │ ├── schema.json │ │ └── valid.yaml │ ├── limactlutil/ │ │ └── limactlutil.go │ ├── limainfo/ │ │ └── limainfo.go │ ├── limatmpl/ │ │ ├── abs.go │ │ ├── abs_test.go │ │ ├── embed.go │ │ ├── embed_test.go │ │ ├── github.go │ │ ├── locator.go │ │ ├── locator_test.go │ │ └── template.go │ ├── limatype/ │ │ ├── dirnames/ │ │ │ └── dirnames.go │ │ ├── filenames/ │ │ │ └── filenames.go │ │ ├── lima_instance.go │ │ └── lima_yaml.go │ ├── limayaml/ │ │ ├── containerd.yaml │ │ ├── defaults.go │ │ ├── defaults_test.go │ │ ├── defaults_unix.go │ │ ├── defaults_unix_test.go │ │ ├── defaults_windows.go │ │ ├── limayaml_test.go │ │ ├── load.go │ │ ├── load_test.go │ │ ├── marshal.go │ │ ├── marshal_test.go │ │ ├── validate.go │ │ ├── validate_test.go │ │ └── validate_unix_test.go │ ├── localpathutil/ │ │ └── localpathutil.go │ ├── lockutil/ │ │ ├── lockutil_test.go │ │ ├── lockutil_unix.go │ │ └── lockutil_windows.go │ ├── logrusutil/ │ │ ├── logrusutil.go │ │ └── logrusutil_test.go │ ├── mcp/ │ │ ├── msi/ │ │ │ ├── filesystem.go │ │ │ ├── msi.go │ │ │ └── shell.go │ │ └── toolset/ │ │ ├── filesystem.go │ │ ├── shell.go │ │ └── toolset.go │ ├── must/ │ │ └── must.go │ ├── networks/ │ │ ├── commands.go │ │ ├── commands_darwin_test.go │ │ ├── commands_test.go │ │ ├── config.go │ │ ├── config_test.go │ │ ├── const.go │ │ ├── networks.TEMPLATE.yaml │ │ ├── networks.go │ │ ├── reconcile/ │ │ │ └── reconcile.go │ │ ├── sudoers.go │ │ ├── usernet/ │ │ │ ├── client.go │ │ │ ├── config.go │ │ │ ├── config_test.go │ │ │ ├── dnshosts/ │ │ │ │ ├── dnshosts.go │ │ │ │ └── dnshosts_test.go │ │ │ ├── gvproxy.go │ │ │ ├── gvproxy_test.go │ │ │ ├── recoincile.go │ │ │ └── udpfileconn.go │ │ └── validate.go │ ├── osutil/ │ │ ├── dns_darwin.go │ │ ├── dns_others.go │ │ ├── exit.go │ │ ├── file.go │ │ ├── machineid.go │ │ ├── machineid_test.go │ │ ├── mount_darwin.go │ │ ├── osutil_linux.go │ │ ├── osutil_others.go │ │ ├── osutil_unix.go │ │ ├── osutil_windows.go │ │ ├── osversion_darwin.go │ │ ├── osversion_others.go │ │ ├── rosetta_darwin.go │ │ ├── rosetta_others.go │ │ ├── user.go │ │ └── user_test.go │ ├── plist/ │ │ ├── plist.go │ │ └── plist_test.go │ ├── plugins/ │ │ └── plugins.go │ ├── portfwd/ │ │ ├── client.go │ │ ├── control_others.go │ │ ├── control_windows.go │ │ ├── forward.go │ │ ├── listener.go │ │ ├── listener_darwin.go │ │ └── listener_others.go │ ├── portfwdserver/ │ │ └── server.go │ ├── progressbar/ │ │ └── progressbar.go │ ├── ptr/ │ │ ├── ptr.go │ │ └── ptr_test.go │ ├── qemuimgutil/ │ │ ├── qemuimgutil.go │ │ └── qemuimgutil_test.go │ ├── reflectutil/ │ │ └── reflectutil.go │ ├── registry/ │ │ ├── registry.go │ │ └── registry_test.go │ ├── snapshot/ │ │ └── snapshot.go │ ├── sshutil/ │ │ ├── format.go │ │ ├── sshutil.go │ │ └── sshutil_test.go │ ├── store/ │ │ ├── disk.go │ │ ├── disk_test.go │ │ ├── fuzz_test.go │ │ ├── instance.go │ │ ├── instance_test.go │ │ ├── store.go │ │ └── store_test.go │ ├── sysprof/ │ │ ├── network_darwin.go │ │ └── sysprof_darwin.go │ ├── templatestore/ │ │ └── templatestore.go │ ├── textutil/ │ │ ├── textutil.go │ │ └── textutil_test.go │ ├── uiutil/ │ │ └── uiutil.go │ ├── usrlocal/ │ │ └── usrlocal.go │ ├── version/ │ │ ├── version.go │ │ └── versionutil/ │ │ ├── versionutil.go │ │ └── versionutil_test.go │ ├── windows/ │ │ ├── process_windows.go │ │ ├── registry_windows.go │ │ └── wsl_util_windows.go │ └── yqutil/ │ ├── fuzz_test.go │ ├── yqutil.go │ └── yqutil_test.go ├── templates/ │ ├── README.md │ ├── _default/ │ │ └── mounts.yaml │ ├── _images/ │ │ ├── almalinux-10.yaml │ │ ├── almalinux-8.yaml │ │ ├── almalinux-9.yaml │ │ ├── almalinux-kitten-10.yaml │ │ ├── alpine-iso.yaml │ │ ├── alpine.yaml │ │ ├── archlinux.yaml │ │ ├── centos-stream-10.yaml │ │ ├── centos-stream-9.yaml │ │ ├── debian-11.yaml │ │ ├── debian-12.yaml │ │ ├── debian-13.yaml │ │ ├── fedora-41.yaml │ │ ├── fedora-42.yaml │ │ ├── fedora-43.yaml │ │ ├── freebsd-15.yaml │ │ ├── macos-15.yaml │ │ ├── macos-26.yaml │ │ ├── opensuse-leap-15.yaml │ │ ├── opensuse-leap-16.yaml │ │ ├── oraclelinux-10.yaml │ │ ├── oraclelinux-8.yaml │ │ ├── oraclelinux-9.yaml │ │ ├── rocky-10.yaml │ │ ├── rocky-8.yaml │ │ ├── rocky-9.yaml │ │ ├── ubuntu-20.04.yaml │ │ ├── ubuntu-22.04.yaml │ │ ├── ubuntu-24.04.yaml │ │ ├── ubuntu-24.10.yaml │ │ ├── ubuntu-25.04.yaml │ │ └── ubuntu-25.10.yaml │ ├── almalinux-10.yaml │ ├── almalinux-8.yaml │ ├── almalinux-9.yaml │ ├── almalinux-kitten-10.yaml │ ├── alpine-iso.yaml │ ├── alpine.yaml │ ├── apptainer-rootful.yaml │ ├── apptainer.yaml │ ├── archlinux.yaml │ ├── buildkit.yaml │ ├── centos-stream-10.yaml │ ├── centos-stream-9.yaml │ ├── debian-11.yaml │ ├── debian-12.yaml │ ├── debian-13.yaml │ ├── default.yaml │ ├── docker-rootful.yaml │ ├── docker.yaml │ ├── experimental/ │ │ ├── alsa.yaml │ │ ├── debian-sid.yaml │ │ ├── fedora-rawhide.yaml │ │ ├── freebsd-16.yaml │ │ ├── gentoo.yaml │ │ ├── opensuse-tumbleweed.yaml │ │ ├── rke2.yaml │ │ ├── u7s.yaml │ │ ├── ubuntu-26.04.yaml │ │ ├── vnc.yaml │ │ └── wsl2.yaml │ ├── faasd.yaml │ ├── fedora-41.yaml │ ├── fedora-42.yaml │ ├── fedora-43.yaml │ ├── freebsd-15.yaml │ ├── homebrew-macos.yaml │ ├── k0s.yaml │ ├── k3s.yaml │ ├── k8s.yaml │ ├── linuxbrew.yaml │ ├── macos-15.yaml │ ├── macos-26.yaml │ ├── opensuse-leap-15.yaml │ ├── opensuse-leap-16.yaml │ ├── oraclelinux-10.yaml │ ├── oraclelinux-8.yaml │ ├── oraclelinux-9.yaml │ ├── podman-rootful.yaml │ ├── podman.yaml │ ├── rocky-10.yaml │ ├── rocky-8.yaml │ ├── rocky-9.yaml │ ├── ubuntu-20.04.yaml │ ├── ubuntu-22.04.yaml │ ├── ubuntu-24.04.yaml │ ├── ubuntu-24.10.yaml │ ├── ubuntu-25.04.yaml │ └── ubuntu-25.10.yaml ├── vz.entitlements └── website/ ├── .gitignore ├── .nvmrc ├── Makefile ├── README.md ├── assets/ │ └── scss/ │ └── _variables_project.scss ├── config.yaml ├── content/ │ └── en/ │ ├── _index.html │ ├── docs/ │ │ ├── _index.md │ │ ├── community/ │ │ │ ├── _index.md │ │ │ ├── contributing.md │ │ │ ├── governance.md │ │ │ ├── roadmap.md │ │ │ └── subprojects.md │ │ ├── config/ │ │ │ ├── _index.md │ │ │ ├── ai/ │ │ │ │ ├── _index.md │ │ │ │ ├── inside/ │ │ │ │ │ └── _index.md │ │ │ │ └── outside/ │ │ │ │ ├── _index.md │ │ │ │ └── gemini.md │ │ │ ├── disk.md │ │ │ ├── environment-variables.md │ │ │ ├── gpu.md │ │ │ ├── mount.md │ │ │ ├── multi-arch.md │ │ │ ├── network/ │ │ │ │ ├── _index.md │ │ │ │ ├── user-v2.md │ │ │ │ ├── user.md │ │ │ │ └── vmnet.md │ │ │ ├── plugin/ │ │ │ │ ├── _index.md │ │ │ │ ├── cli.md │ │ │ │ ├── url.md │ │ │ │ └── vm.md │ │ │ ├── port.md │ │ │ └── vmtype/ │ │ │ ├── _index.md │ │ │ ├── krunkit.md │ │ │ ├── qemu.md │ │ │ ├── vz.md │ │ │ └── wsl2.md │ │ ├── dev/ │ │ │ ├── _index.md │ │ │ ├── drivers.md │ │ │ ├── git.md │ │ │ ├── internals.md │ │ │ └── testing/ │ │ │ ├── _index.md │ │ │ └── bats-style.md │ │ ├── examples/ │ │ │ ├── _index.md │ │ │ ├── ai.md │ │ │ ├── containers/ │ │ │ │ ├── _index.md │ │ │ │ ├── apptainer/ │ │ │ │ │ └── _index.md │ │ │ │ ├── containerd/ │ │ │ │ │ ├── _index.md │ │ │ │ │ └── advanced/ │ │ │ │ │ ├── _index.md │ │ │ │ │ ├── bypass4netns.md │ │ │ │ │ ├── gomodjail.md │ │ │ │ │ └── stargz.md │ │ │ │ ├── docker/ │ │ │ │ │ └── _index.md │ │ │ │ ├── kubernetes/ │ │ │ │ │ └── _index.md │ │ │ │ └── podman/ │ │ │ │ └── _index.md │ │ │ ├── gha.md │ │ │ └── vscode.md │ │ ├── faq/ │ │ │ ├── _index.md │ │ │ └── colima.md │ │ ├── installation/ │ │ │ ├── _index.md │ │ │ └── source.md │ │ ├── reference/ │ │ │ └── _index.md │ │ ├── releases/ │ │ │ ├── _index.md │ │ │ ├── breaking.md │ │ │ ├── deprecated.md │ │ │ └── experimental.md │ │ ├── security/ │ │ │ └── _index.md │ │ ├── talks/ │ │ │ └── _index.md │ │ ├── templates/ │ │ │ ├── _index.md │ │ │ └── github.md │ │ └── usage/ │ │ ├── _index.md │ │ ├── guests/ │ │ │ ├── _index.md │ │ │ ├── freebsd.md │ │ │ ├── linux.md │ │ │ └── macos.md │ │ └── ssh.md │ └── search.md ├── data/ │ ├── adopters.yaml │ └── helpfullinks.yaml ├── go.mod ├── go.sum ├── hugo.toml ├── layouts/ │ ├── 404.html │ ├── partials/ │ │ ├── footer.html │ │ └── navbar.html │ └── shortcodes/ │ ├── blocks/ │ │ ├── adopters.html │ │ ├── cncf.html │ │ ├── cover.html │ │ └── helpfullinks.html │ ├── fixlinks.html │ └── readtemplates.html ├── netlify.toml ├── package.json └── static/ └── images/ └── internals/ └── lima-sequence-diagram.puml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codespellrc ================================================ # https://github.com/codespell-project/codespell#using-a-config-file [codespell] # Comma separated list of dirs to be skipped. skip = .git,go.mod,go.sum,*.pb.desc,*/node_modules/*,*/public/js/*,*/public/scss/* # Comma separated list of words to be ignored. Words must be lowercased. ignore-words-list = ans,distroname,testof,hda,ststr,archtypes,sme # Check file names as well. check-filenames = true ================================================ FILE: .editorconfig ================================================ root = true [*] trim_trailing_whitespace = true insert_final_newline = true # Protobuf descriptors are binary files [*.pb.desc] insert_final_newline = unset trim_trailing_whitespace = unset ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug report description: Report a potential bug body: - type: textarea attributes: label: Description description: Please make sure to include the version of Lima and the host OS ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Ask a question (GitHub Discussions) url: https://github.com/lima-vm/lima/discussions about: We use GitHub Discussions for questions, GitHub issues for tracking bug reports and feature requests - name: Chat with Lima users and developers url: https://slack.cncf.io/ about: CNCF slack has `#lima` channel ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: Feature request description: Request a feature body: - type: textarea attributes: label: Description ================================================ FILE: .github/actions/setup_cache_for_template/action.yml ================================================ name: 'setup cache for template' description: 'setup cache for template' inputs: arch: description: arch to setup cache for required: false template: description: template yaml file required: true runs: using: "composite" steps: - name: "detect platform for download directory" id: detect-platform run: | if [[ "$(uname)" == "Darwin" ]]; then download_dir=~/Library/Caches/lima/download else download_dir=~/.cache/lima/download fi echo "download-dir=$download_dir" >> "$GITHUB_OUTPUT" shell: bash - name: "create cache parameters from template" if: always() id: cache-params-from-template run: | set -eux source hack/cache-common-inc.sh print_cache_informations_from_template "${{ inputs.template }}" "${{ inputs.arch }}" >> "$GITHUB_OUTPUT" shell: bash - name: "Cache ${{ steps.cache-params-from-template.outputs.image-path }}" if: ${{ steps.cache-params-from-template.outputs.image-key != '' }} # avoid using `~` in path that will be expanded to platform specific home directory uses: actions/cache@v4 with: path: ${{ steps.cache-params-from-template.outputs.image-path }} key: ${{ steps.cache-params-from-template.outputs.image-key }} enableCrossOsArchive: true - name: "Cache ${{ steps.cache-params-from-template.outputs.kernel-path }}" if: ${{ steps.cache-params-from-template.outputs.kernel-key != '' }} # avoid using `~` in path that will be expanded to platform specific home directory uses: actions/cache@v4 with: path: ${{ steps.cache-params-from-template.outputs.kernel-path }} key: ${{ steps.cache-params-from-template.outputs.kernel-key }} enableCrossOsArchive: true - name: "Cache ${{ steps.cache-params-from-template.outputs.initrd-path }}" if: ${{ steps.cache-params-from-template.outputs.initrd-key != '' }} # avoid using `~` in path that will be expanded to platform specific home directory uses: actions/cache@v4 with: path: ${{ steps.cache-params-from-template.outputs.initrd-path }} key: ${{ steps.cache-params-from-template.outputs.initrd-key }} enableCrossOsArchive: true - name: "Cache ${{ steps.cache-params-from-template.outputs.containerd-key }}" if: ${{ steps.cache-params-from-template.outputs.containerd-key != '' }} uses: actions/cache@v4 with: path: ${{ steps.cache-params-from-template.outputs.containerd-path }} key: ${{ steps.cache-params-from-template.outputs.containerd-key }} enableCrossOsArchive: true - name: "Create symbolic link named ${{ steps.detect-platform.outputs.download-dir }} pointing to .download" run: | set -eux [ -d .download ] || mkdir -p .download path_to_cache=${{ steps.detect-platform.outputs.download-dir }} mkdir -p "$(dirname "$path_to_cache")" ln -sfn "$PWD/.download" "$path_to_cache" shell: bash ================================================ FILE: .github/actions/upload_failure_logs_if_exists/action.yml ================================================ name: 'upload failure-logs if exists' description: 'upload failure-logs if exists' inputs: suffix: description: suffix to append to the name of the artifact required: false default: '' runs: using: "composite" steps: - name: "Check if failure-logs exists" if: always() id: check-if-failure-logs-exists run: echo "exists=$([ -d "failure-logs" ] && echo "true" || echo "false")" >> "$GITHUB_OUTPUT" shell: bash - id: normalize-suffix # To avoid using special characters in artifact name, normalize the suffix if: steps.check-if-failure-logs-exists.outputs.exists == 'true' run: | suffix="${{ inputs.suffix }}" suffix="${suffix//[^a-zA-Z0-9_]/_}" suffix="${suffix:+-$suffix}" echo "result=$suffix" >> "$GITHUB_OUTPUT" shell: bash - name: "Upload failure-logs" if: steps.check-if-failure-logs-exists.outputs.exists == 'true' uses: actions/upload-artifact@v4 with: name: failure-logs-${{ github.job }}${{ steps.normalize-suffix.outputs.result }} path: failure-logs/ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: gomod directories: - "/" - "/hack/tools" schedule: interval: daily open-pull-requests-limit: 10 groups: golang-x: patterns: - "golang.org/x/*" k8s: patterns: - "k8s.io/*" - package-ecosystem: github-actions directory: "/" schedule: interval: daily open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/codeql.yaml ================================================ name: "CodeQL Advanced" on: # paths-ignore should be kept in sync with test.yml push: branches: ["master"] paths-ignore: - "docs/**" - "website/**" - "**.md" pull_request: branches: ["master"] paths-ignore: - "docs/**" - "website/**" - "**.md" schedule: - cron: '33 19 * * 5' workflow_dispatch: permissions: contents: read jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: 'ubuntu-latest' permissions: security-events: write # required to fetch internal or private CodeQL packs packages: read strategy: fail-fast: false matrix: include: - language: go build-mode: autobuild steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Initialize CodeQL uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/release.yml ================================================ # Forked from https://github.com/containerd/nerdctl/blob/v0.8.1/.github/workflows/release.yml # Apache License 2.0 name: Release on: # paths-ignore should be kept in sync with test.yml push: branches: - 'master' tags: - 'v*' paths-ignore: - "docs/**" - "website/**" - "**.md" pull_request: branches: - 'master' paths-ignore: - "docs/**" - "website/**" - "**.md" env: GO111MODULE: on GOTOOLCHAIN: local permissions: contents: read jobs: artifacts-darwin: name: Artifacts Darwin # The latest release of macOS is used to enable new features. # https://github.com/lima-vm/lima/issues/2767 # # Apparently, a binary built on a newer version of macOS can still run on # an older release of macOS without an error. # This is quite different from Linux and glibc. runs-on: macos-26 timeout-minutes: 20 steps: - name: "Show xcode and SDK version" run: | # Xcode version xcodebuild -version # macOS SDK version xcrun --show-sdk-version || true - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make darwin artifacts run: make artifacts-darwin - name: "Upload artifacts" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-darwin path: _artifacts/ release: # An old release of Ubuntu is chosen for glibc compatibility runs-on: ubuntu-22.04 needs: artifacts-darwin timeout-minutes: 20 # The maximum access is "read" for PRs from public forked repos # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token permissions: contents: write # for releases id-token: write # for provenances attestations: write # for provenances steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: artifacts-darwin path: _artifacts/ - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Install gcc run: | sudo apt-get update sudo apt-get install -y gcc-x86-64-linux-gnu gcc-aarch64-linux-gnu - name: "Compile binaries" run: make artifacts-linux - name: "Make misc artifacts" run: make artifacts-misc - name: "Validate artifactts" run: ./hack/validate-artifact.sh ./_artifacts/*.tar.gz - name: "SHA256SUMS" run: | ( cd _artifacts; sha256sum *.tar.gz ) | tee /tmp/SHA256SUMS mv /tmp/SHA256SUMS _artifacts/SHA256SUMS - name: "The sha256sum of the SHA256SUMS file" run: (cd _artifacts; sha256sum SHA256SUMS) - name: "Prepare the release note" run: | shasha=$(sha256sum _artifacts/SHA256SUMS | awk '{print $1}') cat <<-EOF | tee /tmp/release-note.txt (Changes to be documented) ## Usage \`\`\`console $ limactl create $ limactl start ... INFO[0029] READY. Run \`lima\` to open the shell. $ lima uname Linux \`\`\` - - - The binaries were built automatically on GitHub Actions. The build log is available for 90 days: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} The sha256sum of the SHA256SUMS file itself is \`${shasha}\` . - - - Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') with: subject-path: _artifacts/* - name: "Create release" if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tag="${GITHUB_REF##*/}" gh release create -F /tmp/release-note.txt --draft --title "${tag}" "${tag}" _artifacts/* ================================================ FILE: .github/workflows/scorecard.yml ================================================ name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '23 13 * * 4' push: branches: - master workflow_dispatch: # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write steps: - name: "Checkout code" uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 with: sarif_file: results.sarif ================================================ FILE: .github/workflows/spell.yml ================================================ # split from test.yml so as to run spell checks for doc-only PRs too name: spell on: push: branches: - master - 'release/**' pull_request: permissions: contents: read jobs: spell: name: "Spell check" runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2 with: check_filenames: true check_hidden: true # by default, codespell uses configuration from the .codespellrc ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: - master - 'release/**' paths-ignore: - "docs/**" - "website/**" - "**.md" pull_request: paths-ignore: - "docs/**" - "website/**" - "**.md" env: LIMACTL_CREATE_ARGS: "" GOTOOLCHAIN: local permissions: read-all jobs: lints: name: "Lints" runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Install protoc and Go plugins run: | sudo apt-get update sudo apt-get install -y protobuf-compiler make install-protoc-tools - name: Verify generated files run: make generate check-generated - name: Run nobin run: >- go tool -modfile=./hack/tools/go.mod nobin --allow-emoji --allow-escape --gitignore --skip-ext pb.desc --skip 'website/static/images/**/*.{gif,png}' --skip 'docs/reports/**/*.pdf' - name: Run editorconfig-checker run: go tool -modfile=./hack/tools/go.mod editorconfig-checker - name: Run yamllint run: yamllint . - name: Install shellcheck run: | sudo apt-get update sudo apt-get install -y shellcheck - name: Run file and directory name linter uses: ls-lint/action@02e380fe8733d499cbfc9e22276de5085508a5bd # v2.3.1 - name: Run shellcheck run: find . -name '*.sh' | xargs shellcheck - name: Run shfmt run: find . -name '*.sh' | xargs go tool -modfile=./hack/tools/go.mod shfmt -s -d - name: Check hyperlinks uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 with: args: >- --exclude https://img.shields.io --exclude http://127.0.0.1:8080 --exclude https://github.com/lima-vm/lima/releases/download --exclude https://xbarapp.com --exclude https://api.github.com README.md token: ${{ secrets.GITHUB_TOKEN }} - name: Install go-licenses # TODO: move to `go tool` after upgrading to v2 run: go install github.com/google/go-licenses@v1.6.0 - name: Check licenses # the allow list corresponds to https://github.com/cncf/foundation/blob/e5db022a0009f4db52b89d9875640cf3137153fe/allowed-third-party-license-policy.md # hashicorp/hcl/v2 is MPL-2.0; covered by the CNCF license exception for hashicorp/hcl # see also https://github.com/cncf/foundation/issues/1242 run: go-licenses check --include_tests --ignore github.com/hashicorp/hcl/v2 ./... --allowed_licenses=$(cat ./hack/allowed-licenses.txt) - name: Check license boilerplates run: go tool -modfile=./hack/tools/go.mod ltag -t ./hack/ltag --check -v - name: Check protobuf files run: go tool -modfile=./hack/tools/go.mod protolint . lint-go: name: "Lint Go" timeout-minutes: 30 strategy: matrix: runs-on: [ubuntu-24.04, macos-26, windows-2025] runs-on: ${{ matrix.runs-on }} steps: - name: Force git to use LF # This step is required on Windows to work around golangci-lint issues with formatters. See https://github.com/golangci/golangci-lint/discussions/5840 # TODO: replace with a checkout option when https://github.com/actions/checkout/issues/226 is implemented if: runner.os == 'Windows' run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - id: golangci-lint-version shell: bash working-directory: hack/tools run: | echo "GOLANGCI_LINT_VERSION=$(go list -m -f '{{.Version}}' github.com/golangci/golangci-lint/v2)" >> $GITHUB_OUTPUT - name: Run golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: version: ${{ steps.golangci-lint-version.outputs.GOLANGCI_LINT_VERSION }} args: --verbose security: name: "Vulncheck" runs-on: ubuntu-24.04 timeout-minutes: 5 steps: - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 unit: name: "Unit tests" runs-on: ubuntu-24.04 timeout-minutes: 30 strategy: fail-fast: false matrix: # For non-Homebrew we have to support an old release of Go go-version: ["oldstable", "stable"] steps: - name: Install test dependencies run: | sudo apt-get update sudo apt-get install -y --no-install-recommends qemu-utils - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: ${{ matrix.go-version }} - name: Unit tests run: go test -v ./... - name: Make run: make - name: Install run: sudo make install - name: Verify templates match `limactl edit` format run: | find templates -name '*.yaml' -exec limactl edit --set 'del(.nothing)' {} \; git diff-index --exit-code HEAD - name: Uninstall run: sudo make uninstall windows: name: "Windows tests (WSL2)" runs-on: windows-2025 timeout-minutes: 30 steps: - name: Enable WSL2 run: | wsl --set-default-version 2 wsl --shutdown wsl --update wsl --status wsl --version wsl --list --online - name: Install WSL2 distro timeout-minutes: 1 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. # Starting with WSL2 version 2.5.7.0 the distro will be rejected # if it doesn't contain /bin/sh and /etc. # ------------------------------------------------------------------ mkdir dummy mkdir dummy\bin mkdir dummy\etc echo "" >dummy\bin\sh tar -cf dummy.tar --format ustar -C dummy . wsl --import dummy $env:TEMP dummy.tar wsl --list --verbose - name: Set gitconfig run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Unit tests run: go test -v ./... - name: Make run: make - name: Integration tests (WSL2, Windows host) run: | $env:PATH = "$pwd\_output\bin;" + 'C:\msys64\usr\bin;' + $env:PATH pacman -Sy --noconfirm openbsd-netcat diffutils socat w3m $env:MSYS2_ENV_CONV_EXCL = 'HOME_HOST;HOME_GUEST;_LIMA_WINDOWS_EXTRA_PATH' $env:HOME_HOST = $(cygpath.exe "$env:USERPROFILE") $env:HOME_GUEST = "/mnt$env:HOME_HOST" $env:LIMACTL_CREATE_ARGS = '--vm-type=wsl2 --mount-type=wsl2 --containerd=system' $env:_LIMA_WINDOWS_EXTRA_PATH = 'C:\Program Files\Git\usr\bin' bash.exe -c "./hack/test-templates.sh templates/experimental/wsl2.yaml" windows-qemu: name: "Windows tests (QEMU)" runs-on: windows-2025 timeout-minutes: 30 steps: - name: Set gitconfig run: | git config --global core.autocrlf false git config --global core.eol lf - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make run: make - name: Install QEMU run: | winget install --silent --accept-source-agreements --accept-package-agreements --disable-interactivity SoftwareFreedomConservancy.QEMU - name: Integration tests (QEMU, Windows host) run: | $env:PATH = "$pwd\_output\bin;" + 'C:\msys64\usr\bin;' + 'C:\Program Files\QEMU;' + $env:PATH pacman -Sy --noconfirm openbsd-netcat diffutils socat w3m $env:MSYS2_ENV_CONV_EXCL = 'HOME_HOST;HOME_GUEST;_LIMA_WINDOWS_EXTRA_PATH' $env:HOME_HOST = $(cygpath.exe "$env:USERPROFILE") $env:HOME_GUEST = "$env:HOME_HOST" $env:LIMACTL_CREATE_ARGS = '--vm-type=qemu' $env:_LIMA_WINDOWS_EXTRA_PATH = 'C:\Program Files\Git\usr\bin' bash.exe -c "./hack/test-templates.sh templates/default.yaml" qemu: name: "Integration tests (QEMU, macOS host)" runs-on: macos-15-large # Intel timeout-minutes: 120 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Unit tests run: go test -v ./... - name: Make run: make - name: "Inject `no_timer_check` to kernel cmdline" # workaround to https://github.com/lima-vm/lima/issues/84 run: | export PATH="$PWD/_output/bin:$PATH" ./hack/inject-cmdline-to-template.sh _output/share/lima/templates/_images/ubuntu.yaml no_timer_check - name: Install run: sudo make install - name: Validate jsonschema run: make schema-limayaml.json - name: Validate templates # Can't validate base templates in `_default` because they have no images run: find -L templates -name '*.yaml' ! -path '*/_default/*' | xargs limactl validate - name: Install test dependencies # QEMU: required by Lima itself # bash: required by test-templates.sh (OS version of bash is too old) # coreutils: required by test-templates.sh for the "timeout" command # w3m : required by test-templates.sh for port forwarding tests # socat: required by test-templates.sh for port forwarding tests run: brew install qemu bash coreutils w3m socat - name: "Adjust LIMACTL_CREATE_ARGS" run: echo "LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --vm-type=qemu" >>$GITHUB_ENV - name: Cache image used by default.yaml uses: ./.github/actions/setup_cache_for_template with: template: templates/default.yaml - name: "Show cache" run: ./hack/debug-cache.sh - name: "Test default.yaml" uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 30 retry_on: error max_attempts: 3 command: ./hack/test-templates.sh templates/default.yaml # GHA macOS is slow and flaky, so we only test default.yaml here. # Other yamls are tested on Linux instances. # - if: always() uses: ./.github/actions/upload_failure_logs_if_exists - name: "Show cache" if: always() run: ./hack/debug-cache.sh # Non-default templates are tested on Linux instances of GHA, # as they seem more stable than macOS instances. qemu-linux: name: "Integration tests (QEMU, Linux host)" runs-on: ubuntu-24.04 timeout-minutes: 120 strategy: fail-fast: false matrix: # Most templates use 9p as the mount type template: - alpine.yaml - debian.yaml # reverse-sshfs - fedora.yaml - archlinux.yaml - opensuse.yaml - docker.yaml - ../hack/test-templates/alpine-iso-9p-writable.yaml # Covers alpine-iso.yaml - ../hack/test-templates/net-user-v2.yaml - ../hack/test-templates/test-misc.yaml # TODO: merge net-user-v2 into test-misc steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make run: make - name: Install run: sudo make install - name: Cache image used by templates/${{ matrix.template }} uses: ./.github/actions/setup_cache_for_template with: template: templates/${{ matrix.template }} - name: Install test dependencies run: | sudo apt-get update sudo ./hack/install-qemu.sh sudo apt-get install -y --no-install-recommends socat w3m - name: Install ansible-playbook run: | sudo apt-get install -y --no-install-recommends ansible if: matrix.template == '../hack/test-templates/test-misc.yaml' - name: "Show cache" run: ./hack/debug-cache.sh - name: "Test" uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 30 retry_on: error max_attempts: 3 command: ./hack/test-templates.sh templates/${{ matrix.template }} - if: always() uses: ./.github/actions/upload_failure_logs_if_exists with: suffix: ${{ matrix.template }} - name: "Show cache" run: ./hack/debug-cache.sh bats: name: "Integration tests (BATS)" runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # BATS tests don't expect lima version warnings like: # msg="treating lima version \"ea336ae\" from \"/Users/runner/.lima-bats/dummy/lima-version\" as very latest release" fetch-depth: 0 submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make run: make - name: Install run: sudo make install - name: Install test dependencies run: | sudo apt-get update sudo ./hack/install-qemu.sh - name: Cache image used by templates/default.yaml uses: ./.github/actions/setup_cache_for_template with: template: templates/default.yaml - name: "Run BATS integration tests" run: make bats env: # The url-github.bats tests makes GitHub API requests GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Cache image used by templates/k8s.yaml uses: ./.github/actions/setup_cache_for_template with: template: templates/k8s.yaml - name: "Run BATS k8s tests" run: ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/extras/k8s.bats env: LIMA_BATS_ALL_TESTS_RETRIES: 3 - name: Cache image used by templates/freebsd.yaml uses: ./.github/actions/setup_cache_for_template with: template: templates/freebsd.yaml - name: "Run BATS freebsd tests" run: | set -eux -o pipefail # xorriso is required for creating Joliet filesystem sudo apt-get install -y xorriso ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/extras/freebsd.bats colima: name: "Colima tests (QEMU, Linux host)" runs-on: ubuntu-24.04 timeout-minutes: 120 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # fetch-depth is set to 0 to let `limactl --version` print semver-ish version # fetch-depth: 0 is required for Colima integration test because Colima # checks Lima's version and requires proper semantic version format fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} submodules: true - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make run: make - name: Install run: sudo make install - name: Install colima id: install-colima run: | git clone https://github.com/abiosoft/colima cd colima latest="$(git tag --sort=-v:refname | head -n1)" git checkout "$latest" make sudo make install echo "version=$latest" >>$GITHUB_OUTPUT - name: Install test dependencies run: | sudo apt-get update sudo ./hack/install-qemu.sh - name: Cache colima uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/.cache/colima key: colima-${{ steps.install-colima.outputs.version }} - name: Run BATS colima tests run: ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/extras/colima.bats vmnet: name: "VMNet tests (QEMU)" runs-on: macos-15-large # Intel timeout-minutes: 120 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make run: make - name: "Inject `no_timer_check` to kernel cmdline" # workaround to https://github.com/lima-vm/lima/issues/84 run: | export PATH="$PWD/_output/bin:$PATH" ./hack/inject-cmdline-to-template.sh _output/share/lima/templates/_images/ubuntu.yaml no_timer_check - name: Install run: sudo make install - name: "Adjust LIMACTL_CREATE_ARGS" run: echo "LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --vm-type=qemu --network=lima:shared" >>$GITHUB_ENV - name: Cache image used by default .yaml uses: ./.github/actions/setup_cache_for_template with: template: templates/default.yaml - name: Install test dependencies run: brew install qemu bash coreutils w3m socat - name: Install socket_vmnet env: SOCKET_VMNET_VERSION: v1.2.2 run: | ( cd ~ git clone https://github.com/lima-vm/socket_vmnet cd socket_vmnet git checkout $SOCKET_VMNET_VERSION sudo git config --global --add safe.directory /Users/runner/socket_vmnet sudo make PREFIX=/opt/socket_vmnet install ) limactl sudoers | sudo tee /etc/sudoers.d/lima - name: Unit test (pkg/networks) with socket_vmnet # Set -count=1 to disable cache run: go test -v -count=1 ./pkg/networks/... - name: Test socket_vmnet uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 30 retry_on: error max_attempts: 3 command: ./hack/test-templates.sh templates/default.yaml - if: always() uses: ./.github/actions/upload_failure_logs_if_exists upgrade: name: "Upgrade tests (QEMU, macOS host)" runs-on: macos-15-large # Intel timeout-minutes: 120 strategy: matrix: oldver: ["v0.15.1"] # The default VM type was always QEMU until Lima v1.0 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Fetch homebrew-core commit messages uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # needed by ./hack/brew-install-version.sh repository: homebrew/homebrew-core path: homebrew-core fetch-depth: 0 filter: tree:0 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Cache image used by ${{ matrix.oldver }}/examples/ubuntu-lts.yaml uses: ./.github/actions/setup_cache_for_template with: template: https://raw.githubusercontent.com/lima-vm/lima/${{ matrix.oldver }}/examples/ubuntu-lts.yaml - name: Install test dependencies run: | brew install bash coreutils # QEMU 9.1.0 seems to break on GitHub runners, both on Monterey and Ventura # We revert back to 8.2.1, which seems to work fine git config --global user.name "GitHub Actions Bot" git config --global user.email "nobody@localhost" ./hack/brew-install-version.sh qemu 8.2.1 - name: Test uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 30 retry_on: error max_attempts: 3 command: ./hack/test-upgrade.sh ${{ matrix.oldver }} ${{ github.sha }} - if: always() uses: ./.github/actions/upload_failure_logs_if_exists vz: name: "Integration tests (vz)" runs-on: macos-15-large # Intel timeout-minutes: 120 strategy: fail-fast: false matrix: template: - default.yaml steps: - name: "Adjust LIMACTL_CREATE_ARGS" # --cpus=1 is needed for running vz on GHA: https://github.com/lima-vm/lima/pull/1511#issuecomment-1574937888 run: echo "LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --cpus 1 --memory 1" >>$GITHUB_ENV - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make run: make - name: Install run: sudo make install - name: Cache image used by templates/${{ matrix.template }} uses: ./.github/actions/setup_cache_for_template with: template: templates/${{ matrix.template }} - name: Install test dependencies run: brew install bash coreutils w3m socat - name: Uninstall qemu run: brew uninstall --ignore-dependencies --force qemu - name: Test run: ./hack/test-templates.sh templates/${{ matrix.template }} - if: failure() uses: ./.github/actions/upload_failure_logs_if_exists with: suffix: ${{ matrix.template }} # gomodjail is a library sandbox for Go # https://github.com/AkihiroSuda/gomodjail # # This is an early experiment. # CI failures that only occurs with gomodjail shall not block merging PRs. gomodjail: name: "gomodjail (experimental; failures shall not block merging PRs)" runs-on: macos-15-large # Intel timeout-minutes: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Install gomodjail run: | set -eux -o pipefail git clone https://github.com/AkihiroSuda/gomodjail cd gomodjail make binaries install - name: Install Lima # gomodjail depends on symbols run: | make KEEP_SYMBOLS=1 binaries sudo make install - name: Cache image used by templates/default.yaml uses: ./.github/actions/setup_cache_for_template with: template: templates/default.yaml - name: Smoke test run: gomodjail run --go-mod=./go.mod -- limactl start --tty=false cross: name: "Cross-compile (NetBSD, DragonFlyBSD)" runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - run: GOOS=netbsd go build ./... - run: GOOS=dragonfly go build ./... qemu-linux-old: name: "Smoke tests (QEMU, old Linux host)" runs-on: ubuntu-22.04 # QEMU 6.2 timeout-minutes: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: stable - name: Make run: make - name: Install run: sudo make install - name: Cache image used by templates/default.yaml uses: ./.github/actions/setup_cache_for_template with: template: templates/default.yaml - name: Install test dependencies run: | sudo apt-get update sudo ./hack/install-qemu.sh - name: Smoke test run: limactl start --tty=false ================================================ FILE: .gitignore ================================================ _output/ _artifacts/ lima.REJECTED.yaml default-template.yaml schema-limayaml.json .config ================================================ FILE: .gitmodules ================================================ [submodule "hack/bats/lib/bats-core"] path = hack/bats/lib/bats-core url = https://github.com/bats-core/bats-core.git branch = master [submodule "hack/bats/lib/bats-file"] path = hack/bats/lib/bats-file url = https://github.com/bats-core/bats-file.git branch = master [submodule "hack/bats/lib/bats-assert"] path = hack/bats/lib/bats-assert url = https://github.com/bats-core/bats-assert.git branch = master [submodule "hack/bats/lib/bats-support"] path = hack/bats/lib/bats-support url = https://github.com/bats-core/bats-support.git branch = master ================================================ FILE: .golangci.yml ================================================ # golangci-lint configuration file. # https://golangci-lint.run/usage/configuration/ version: "2" run: concurrency: 6 linters: default: none enable: - bodyclose - copyloopvar - depguard - dupword - errcheck - errorlint - forbidigo - gocritic - godot - govet - ineffassign - intrange - misspell - modernize - nakedret - noctx - nolintlint - perfsprint - revive - staticcheck - unconvert - unused - usetesting - whitespace settings: depguard: rules: main: deny: - pkg: golang.org/x/net/context desc: use the 'context' package from the standard library - pkg: math/rand$ desc: use the 'math/rand/v2' package errorlint: asserts: false forbidigo: forbid: - pattern: ^print(ln)?$ msg: Do not use builtin print functions. It's for bootstrapping only and may be removed in the future. - pattern: ^fmt\.Print.*$ msg: Do not use fmt.Print statements. - pattern: ^testing.T.Fatal.*$ msg: Use assert functions from the gotest.tools/v3/assert package instead. - pattern: ^sort.*$ msg: Use sorting functions from the slices package instead. analyze-types: true gocritic: disabled-checks: - appendCombine - sloppyReassign - unlabelStmt - rangeValCopy - hugeParam - importShadow - sprintfQuotedString - builtinShadow - filepathJoin # See "Tags" section in https://github.com/go-critic/go-critic#usage enabled-tags: - diagnostic - performance - style - opinionated - experimental settings: ifElseChain: # Min number of if-else blocks that makes the warning trigger. minThreshold: 3 modernize: disable: - omitzero perfsprint: int-conversion: false integer-format: false err-error: false errorf: true sprintf1: false strconcat: false concat-loop: false revive: # Set below 0.8 to enable error-strings confidence: 0.6 # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md rules: - name: bare-return - name: blank-imports - name: context-as-argument - name: context-keys-type - name: deep-exit - name: dot-imports arguments: - allowedPackages: - github.com/lima-vm/lima/v2/pkg/must - name: empty-block - name: error-naming - name: error-return - name: error-strings - name: errorf - name: exported disabled: true - name: increment-decrement - name: indent-error-flow - name: package-comments disabled: true - name: package-directory-mismatch - name: range - name: receiver-naming - name: redefines-builtin-id - name: superfluous-else - name: time-naming - name: unexported-return - name: unnecessary-format - name: unreachable-code - name: unused-parameter - name: use-any - name: var-declaration - name: var-naming staticcheck: # https://staticcheck.dev/docs/configuration/options/#checks checks: - all - -QF1001 # apply De Morgan's law - -QF1008 # remove embedded field from selector - -SA3000 # false positive for Go 1.15+. See https://github.com/golang/go/issues/34129 - -ST1000 - -ST1001 # duplicates revive.dot-imports - -ST1022 usetesting: os-temp-dir: true context-background: true context-todo: true exclusions: presets: - common-false-positives - legacy - std-error-handling rules: # Allow using Uid, Gid in pkg/osutil. - path: pkg/osutil/ text: '(?i)(uid)|(gid)' # Disable some linters for test files. - linters: - godot path: _test\.go # Allow "api" package names in hostagent and guestagent. - linters: - revive path: pkg/(hostagent|guestagent)/api/ text: avoid meaningless package names issues: # Maximum issues count per one linter. max-issues-per-linter: 0 # Maximum count of issues with the same text. max-same-issues: 0 formatters: enable: - gci - gofmt - gofumpt settings: gci: sections: - standard - default - localmodule ================================================ FILE: .ls-lint.yml ================================================ # ls-lint configuration file. # https://ls-lint.org/2.2/configuration/the-basics.html ls: .dir: kebab-case .go: snake_case .lima: snake_case .sh: kebab-case .TEMPLATE.yaml: kebab-case .yaml: kebab-case .yml: kebab-case .github: .yaml: snake_case cmd/limactl: # valid names are `show-ssh.go`, `show-ssh_windows.go` or `show-ssh_test.go` .go: lowercase templates: # _default and _images have leading underscores .dir: lowercase website/content: .dir: lowercase ignore: - .git - .golangci.yml - .ls-lint.yml - '_output' - '**/*.pb\.go' - hack/common.inc.sh - pkg/cidata/cidata.TEMPLATE.d - pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh - website # "ubuntu-24.04" does not follow the kebab-case - '**/ubuntu-*\.yaml' # "boot." does not follow the kebab-case - '**/boot.*' ================================================ FILE: .markdownlint.json ================================================ { "first-line-h1": false, "no-inline-html": false, "blanks-around-headers": false, "blanks-around-lists": false, "blanks-around-fences": false, "commands-show-output": false, "ul-style": false, "line-length": false } ================================================ FILE: .protolint.yaml ================================================ # This is the configuration for protolint. # See https://github.com/yoheimuta/protolint/blob/v0.55.6/_example/config/.protolint.yaml for details. --- lint: rules: # Enable all default rules. no_default: false rules_option: # MAX_LINE_LENGTH rule option. max_line_length: max_chars: 100 ================================================ FILE: .shellcheckrc ================================================ source-path=SCRIPTDIR ================================================ FILE: .yamllint ================================================ --- extends: default ignore: | # this is a yaml template, needs to be executed pkg/cidata/cloud-config.yaml rules: indentation: indent-sequences: false truthy: allowed-values: ['true', 'false', 'on', 'off'] comments-indentation: disable document-start: disable line-length: disable ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ Moved to . ================================================ FILE: Makefile ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # Files are installed under $(DESTDIR)/$(PREFIX) PREFIX ?= /usr/local DEST := $(shell echo "$(DESTDIR)/$(PREFIX)" | sed 's:///*:/:g; s://*$$::') GO ?= go TAR ?= tar ZIP ?= zip PLANTUML ?= plantuml # may also be "java -jar plantuml.jar" if installed elsewhere GOARCH ?= $(shell $(GO) env GOARCH) GOHOSTARCH := $(shell $(GO) env GOHOSTARCH) GOHOSTOS := $(shell $(GO) env GOHOSTOS) GOOS ?= $(shell $(GO) env GOOS) ifeq ($(GOOS),windows) bat = .bat exe = .exe endif ifeq ($(GOOS),darwin) MACOS_SDK_VERSION = $(shell xcrun --show-sdk-version | cut -d . -f 1) # xcrun command seems to fail even when the SDK is available: # > xcrun: error: unable to lookup item 'SDKVersion' in SDK '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk' ifeq ($(MACOS_SDK_VERSION),) MACOS_SDK_VERSION = $(shell readlink /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk | sed -E -e "s/^MacOSX//" -e "s/\.[0-9]+\.sdk//") endif endif DEFAULT_ADDITIONAL_DRIVERS := ifeq ($(GOOS),darwin) ifeq ($(GOARCH),arm64) ifeq ($(shell test $(MACOS_SDK_VERSION) -ge 14; echo $$?),0) # krunkit needs macOS 14 or later: https://github.com/containers/libkrun/blob/main/README.md#macos-efi-variant DEFAULT_ADDITIONAL_DRIVERS += krunkit endif endif endif ADDITIONAL_DRIVERS ?= $(DEFAULT_ADDITIONAL_DRIVERS) GO_BUILDTAGS ?= ifeq ($(GOOS),darwin) ifeq ($(shell test $(MACOS_SDK_VERSION) -lt 13; echo $$?),0) # The "vz" mode needs macOS 13 SDK or later GO_BUILDTAGS += no_vz endif endif ifeq ($(GOHOSTOS),windows) WINVER_MAJOR=$(shell powershell.exe "[System.Environment]::OSVersion.Version.Major") ifeq ($(WINVER_MAJOR),10) WINVER_BUILD=$(shell powershell.exe "[System.Environment]::OSVersion.Version.Build") WINVER_BUILD_HIGH_ENOUGH=$(shell powershell.exe $(WINVER_BUILD) -ge 19041) ifeq ($(WINVER_BUILD_HIGH_ENOUGH),False) GO_BUILDTAGS += no_wsl endif endif endif PACKAGE := github.com/lima-vm/lima/v2 VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags 2>/dev/null) VERSION_TRIMMED := $(VERSION:v%=%) ifeq ($(VERSION),) $(error VERSION could not be determined. Build from a git repo or run: make VERSION=vX.Y.Z) endif # `DEBUG` flag to build binaries with debug information for use by `dlv exec`. # This implies KEEP_DWARF=1 and KEEP_SYMBOLS=1. DEBUG ?= GO_BUILD_GCFLAGS ?= KEEP_DWARF ?= KEEP_SYMBOLS ?= ifeq ($(DEBUG),1) # Disable optimizations and inlining to make debugging easier. GO_BUILD_GCFLAGS = -gcflags="all=-N -l" # Keep the symbol table KEEP_DWARF = 1 # Enable DWARF generation KEEP_SYMBOLS = 1 endif GO_BUILD_LDFLAGS_W := true ifeq ($(KEEP_DWARF),1) GO_BUILD_LDFLAGS_W = false endif GO_BUILD_LDFLAGS_S := true ifeq ($(KEEP_SYMBOLS),1) GO_BUILD_LDFLAGS_S = false endif # `-s`: Strip the symbol table according to the KEEP_SYMBOLS config # `-w`: Disable DWARF generation according to the KEEP_DWARF config # `-X`: Embed version information. GO_BUILD_LDFLAGS := -ldflags="-s=$(GO_BUILD_LDFLAGS_S) -w=$(GO_BUILD_LDFLAGS_W) -X $(PACKAGE)/pkg/version.Version=$(VERSION)" # `go -version -m` returns -tags with comma-separated list, because space-separated list is deprecated in go1.13. # converting to comma-separated list is useful for comparing with the output of `go version -m`. GO_BUILD_FLAG_TAGS := $(addprefix -tags=,$(shell echo "$(GO_BUILDTAGS)"|tr " " "\n"|paste -sd "," -)) GO_BUILD := $(strip $(GO) build $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)) ################################################################################ # Features .NOTPARALLEL: .SECONDEXPANSION: ################################################################################ .PHONY: all all: binaries manpages ################################################################################ # Help .PHONY: help help: @echo ' binaries - Build all binaries' @echo ' manpages - Build manual pages' @echo @echo ' help-variables - Show Makefile variables' @echo ' help-targets - Show additional Makefile targets' .PHONY: help-variables help-variables: @echo '# Variables that can be overridden.' @echo @echo '- PREFIX (directory) : Installation prefix (default: /usr/local)' @echo '- KEEP_DWARF (1 or 0) : Whether to keep DWARF information (default: 0)' @echo '- KEEP_SYMBOLS (1 or 0) : Whether to keep symbols (default: 0)' @echo '- DEBUG (1 or 0) : Whether to build with debug information (default: 0)' .PHONY: help-targets help-targets: @echo '# Targets can be categorized by their location.' @echo @echo 'Targets for files in _output/bin/:' @echo '- limactl : Build limactl, and lima' @echo '- lima : Copy lima, and lima.bat' @echo '- helpers : Copy nerdctl.lima, apptainer.lima, docker.lima, podman.lima, and kubectl.lima' @echo @echo 'Targets for files in _output/libexec/lima/:' @echo '- limactl-plugins : Build limactl-* CLI plugins' @echo @echo 'Targets for files in _output/share/lima/:' @echo '- guestagents : Build guestagents' @echo '- native-guestagent : Build guestagent for native arch' @echo '- additional-guestagents : Build guestagents for archs other than native arch' @echo '- -guestagent : Build guestagent for : $(sort $(LINUX_GUESTAGENT_ARCHS))' @echo @echo 'Targets for files in _output/share/lima/templates/:' @echo '- templates : Copy templates' @echo '- template_experimentals : Copy experimental templates to experimental/' @echo '- default_template : Copy default.yaml template' @echo '- update-templates : Update templates' @echo @echo 'Targets for files in _output/share/doc/lima:' @echo '- documentation : Copy documentation to _output/share/doc/lima' @echo '- create-links-in-doc-dir : Create some symlinks pointing ../../lima/templates' @echo @echo '# e.g. to install limactl, helpers, native guestagent, and templates:' @echo '# make native install' .PHONY: help-artifact help-artifact: @echo '# Targets for building artifacts to _artifacts/' @echo @echo 'Targets to building multiple archs artifacts for GOOS:' @echo '- artifacts : Build artifacts for current OS and supported archs' @echo '- artifacts- : Build artifacts for supported archs and : darwin, linux, or windows' @echo @echo 'Targets to building GOOS and ARCH (GOARCH, or uname -m) specific artifacts:' @echo '- artifact : Build artifacts for current GOOS and GOARCH' @echo '- artifact- : Build artifacts for current GOARCH and : darwin, linux, or windows' @echo '- artifact- : Build artifacts for current GOOS with : amd64, arm64, x86_64, or aarch64' @echo '- artifact-- : Build artifacts for and ' @echo @echo '# GOOS and GOARCH can be specified with make parameters or environment variables.' @echo '# e.g. to build artifact for linux and arm64:' @echo '# make GOOS=linux GOARCH=arm64 artifact' @echo @echo 'Targets for miscellaneous artifacts:' @echo '- artifacts-misc : Build artifacts for go.mod, go.sum, and vendor' ################################################################################ # convenience targets exe: _output/bin/limactl$(exe) .PHONY: minimal native minimal: clean limactl native-guestagent default_template native: clean limactl limactl-plugins helpers native-guestagent templates template_experimentals additional-drivers ################################################################################ # These configs were once customizable but should no longer be changed. CONFIG_GUESTAGENT_OS_LINUX=y CONFIG_GUESTAGENT_OS_DARWIN= ifeq ($(GOOS),darwin) ifeq ($(GOARCH),arm64) CONFIG_GUESTAGENT_OS_DARWIN=y endif endif CONFIG_GUESTAGENT_ARCH_X8664=y CONFIG_GUESTAGENT_ARCH_AARCH64=y CONFIG_GUESTAGENT_ARCH_ARMV7L=y CONFIG_GUESTAGENT_ARCH_PPC64LE=y CONFIG_GUESTAGENT_ARCH_RISCV64=y CONFIG_GUESTAGENT_ARCH_S390X=y CONFIG_GUESTAGENT_COMPRESS=y ################################################################################ .PHONY: binaries binaries: limactl helpers limactl-plugins guestagents \ templates template_experimentals \ documentation create-links-in-doc-dir ################################################################################ # _output/bin .PHONY: limactl lima helpers limactl: _output/bin/limactl$(exe) lima ### Listing Dependencies # returns a list of files expanded from $(1) excluding directories and files ending with '_test.go'. find_files_excluding_dir_and_test = $(shell find $(1) ! -type d ! -name '*_test.go') FILES_IN_PKG = $(call find_files_excluding_dir_and_test, ./pkg) # returns a list of files which are dependencies for the command $(1). dependencies_for_cmd = go.mod $(call find_files_excluding_dir_and_test, ./cmd/$(1)) $(FILES_IN_PKG) ### Force Building Targets # returns GOVERSION, CGO*, GO*, -ldflags, and -tags build variables from the output of `go version -m $(1)`. # When CGO_* variables are not set, they are not included in the output. # Because the CGO_* variables are not set means that those values are default values, # it can be assumed that those values are same if the GOVERSION is same. # $(1): target binary extract_build_vars = $(shell \ ($(GO) version -m $(1) 2>&- || echo $(1):) | \ awk 'FNR==1{print "GOVERSION="$$2}$$2~/^(CGO|GO|-gcflags|-ldflags|-tags).*=.+$$/{sub("^.*"$$2,$$2); print $$0}' \ ) # a list of keys from the GO build variables to be used for calling `go env`. # keys starting with '-' are excluded because `go env` does not support those keys. # $(1): extracted build variables from the binary keys_in_build_vars = $(filter-out -%,$(shell for i in $(1); do echo $${i%%=*}; done)) # a list of GO build variables to build the target binary. # $(1): target binary. expecting ENVS_$(2) is set to use the environment variables for the target binary. # $(2): key of the GO build variable to be used for calling `go env`. go_build_vars = $(shell \ $(ENVS_$(1)) $(GO) env $(2) | \ awk '/ /{print "\""$$0"\""; next}{print}' | \ for k in $(2); do read -r v && echo "$$k=$${v}"; done \ ) $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS) # returns the difference between $(1) and $(2). diff = $(filter-out $(2),$(1))$(filter-out $(1),$(2)) # returns diff between the GO build variables in the binary $(1) and the building variables. # $(1): target binary # $(2): extracted GO build variables from the binary compare_build_vars = $(call diff,$(call go_build_vars,$(1),$(call keys_in_build_vars,$(2))),$(2)) # returns "force" if the GO build variables in the binary $(1) is different from the building variables. # $(1): target binary. expecting ENVS_$(1) is set to use the environment variables for the target binary. force_build = $(if $(call compare_build_vars,$(1),$(call extract_build_vars,$(1))),force,) # returns the file name without .gz extension. It also gunzips the file with .gz extension if exists. # $(1): target file gunzip_if_exists = $(shell f=$(1); f=$${f%.gz}; test -f "$${f}.gz" && (set -x; gunzip -f "$${f}.gz") ; echo "$${f}") # call force_build with passing output of gunzip_if_exists as an argument. # $(1): target file force_build_with_gunzip = $(call force_build,$(call gunzip_if_exists,$(1))) force: # placeholder for force build ################################################################################ # _output/bin/limactl$(exe) # dependencies for limactl LIMACTL_DEPS = $(call dependencies_for_cmd,limactl) ifeq ($(GOOS),darwin) LIMACTL_DEPS += vz.entitlements endif # environment variables for limactl. this variable is used for checking force build. # # The hostagent must be compiled with CGO_ENABLED=1 so that net.LookupIP() in the DNS server # calls the native resolver library and not the simplistic version in the Go library. ENVS__output/bin/limactl$(exe) = CGO_ENABLED=1 GOOS="$(GOOS)" GOARCH="$(GOARCH)" CC="$(CC)" LIMACTL_DRIVER_TAGS := ifneq (,$(findstring vz,$(ADDITIONAL_DRIVERS))) LIMACTL_DRIVER_TAGS += external_vz endif ifneq (,$(findstring qemu,$(ADDITIONAL_DRIVERS))) LIMACTL_DRIVER_TAGS += external_qemu endif ifneq (,$(findstring wsl2,$(ADDITIONAL_DRIVERS))) LIMACTL_DRIVER_TAGS += external_wsl2 endif GO_BUILDTAGS ?= GO_BUILDTAGS_LIMACTL := $(strip $(GO_BUILDTAGS) $(LIMACTL_DRIVER_TAGS)) _output/bin/limactl$(exe): $(LIMACTL_DEPS) $$(call force_build,$$@) ifneq ($(GOOS),windows) # @rm -rf _output/bin/limactl.exe else @rm -rf _output/bin/limactl endif $(ENVS_$@) $(GO_BUILD) -tags '$(GO_BUILDTAGS_LIMACTL)' -o $@ ./cmd/limactl ifeq ($(GOOS),darwin) codesign -f -v --entitlements vz.entitlements -s - $@ endif LIBEXEC_LIMA := _output/libexec/lima limactl-plugins: $(LIBEXEC_LIMA)/limactl-mcp$(exe) $(LIBEXEC_LIMA)/limactl-url-fedora-rawhide $(LIBEXEC_LIMA)/limactl-mcp$(exe): $(call dependencies_for_cmd,limactl-mcp) $$(call force_build,$$@) @mkdir -p $(LIBEXEC_LIMA) $(ENVS_$@) $(GO_BUILD) -o $@ ./cmd/limactl-mcp $(LIBEXEC_LIMA)/limactl-url-fedora-rawhide: cmd/limactl-url-fedora-rawhide cp -aL $< $@ .PHONY: additional-drivers additional-drivers: @mkdir -p $(LIBEXEC_LIMA) @for drv in $(ADDITIONAL_DRIVERS); do \ echo "Building $$drv as external"; \ if [ "$(GOOS)" = "windows" ]; then \ $(GO_BUILD) -o $(LIBEXEC_LIMA)/lima-driver-$$drv.exe ./cmd/lima-driver-$$drv; \ else \ $(GO_BUILD) -o $(LIBEXEC_LIMA)/lima-driver-$$drv ./cmd/lima-driver-$$drv; \ fi; \ if [ "$$drv" = "vz" ] && [ "$(GOOS)" = "darwin" ]; then \ codesign -f -v --entitlements vz.entitlements -s - $(LIBEXEC_LIMA)/lima-driver-vz; \ fi; \ done LIMA_CMDS = $(sort lima lima$(bat)) # $(sort ...) deduplicates the list LIMA_DEPS = $(addprefix _output/bin/,$(LIMA_CMDS)) lima: $(LIMA_DEPS) HELPER_CMDS = nerdctl.lima apptainer.lima docker.lima podman.lima kubectl.lima HELPERS_DEPS = $(addprefix _output/bin/,$(HELPER_CMDS)) helpers: $(HELPERS_DEPS) _output/bin/%: ./cmd/% | _output/bin cp -a $< $@ MKDIR_TARGETS += _output/bin ################################################################################ # _output/share/lima/lima-guestagent LINUX_GUESTAGENT_PATH_COMMON = _output/share/lima/lima-guestagent.Linux- DARWIN_GUESTAGENT_PATH_COMMON = _output/share/lima/lima-guestagent.Darwin- # How to add architecture specific guestagent: # 1. Add the architecture to GUESTAGENT_ARCHS # 2. Add ENVS_$(*_GUESTAGENT_PATH_COMMON) to set GOOS, GOARCH, and other necessary environment variables LINUX_GUESTAGENT_ARCHS = aarch64 armv7l ppc64le riscv64 s390x x86_64 DARWIN_GUESTAGENT_ARCHS = aarch64 ifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y) ALL_GUESTAGENTS_NOT_COMPRESSED += $(addprefix $(LINUX_GUESTAGENT_PATH_COMMON),$(LINUX_GUESTAGENT_ARCHS)) endif ifeq ($(CONFIG_GUESTAGENT_OS_DARWIN),y) ALL_GUESTAGENTS_NOT_COMPRESSED += $(addprefix $(DARWIN_GUESTAGENT_PATH_COMMON),$(DARWIN_GUESTAGENT_ARCHS)) endif ifeq ($(CONFIG_GUESTAGENT_COMPRESS),y) gz=.gz endif ALL_GUESTAGENTS = $(addsuffix $(gz),$(ALL_GUESTAGENTS_NOT_COMPRESSED)) # guestagent path for the given platform. it may has .gz extension if CONFIG_GUESTAGENT_COMPRESS is enabled. # $(1): operating system (os) # $(2): list of architectures guestagent_path = $(foreach arch,$(2),$($(1)_GUESTAGENT_PATH_COMMON)$(arch)$(gz)) ifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y) NATIVE_GUESTAGENT_ARCH = $(shell echo $(GOARCH) | sed -e s/arm64/aarch64/ -e s/arm/armv7l/ -e s/amd64/x86_64/) NATIVE_GUESTAGENT = $(call guestagent_path,LINUX,$(NATIVE_GUESTAGENT_ARCH)) ADDITIONAL_GUESTAGENT_ARCHS = $(filter-out $(NATIVE_GUESTAGENT_ARCH),$(LINUX_GUESTAGENT_ARCHS)) ADDITIONAL_GUESTAGENTS = $(call guestagent_path,LINUX,$(ADDITIONAL_GUESTAGENT_ARCHS)) endif ifeq ($(CONFIG_GUESTAGENT_OS_DARWIN),y) ifeq ($(GOARCH),arm64) NATIVE_GUESTAGENT_ARCH = aarch64 NATIVE_GUESTAGENT += $(call guestagent_path,DARWIN,$(NATIVE_GUESTAGENT_ARCH)) endif endif # No ADDITIONAL_GUESTAGENTS for Darwin, as only one architecture is supported. # config_guestagent_arch returns expanded value of CONFIG_GUESTAGENT_ARCH_ # $(1): architecture # CONFIG_GUESTAGENT_ARCH_ naming convention: uppercase, remove '_' config_guestagent_arch = $(filter y,$(CONFIG_GUESTAGENT_ARCH_$(shell echo $(1)|tr -d _|tr a-z A-Z))) # guestagent_path_enabled_by_config returns the path to the guestagent binary for the given architecture, # or an empty string if the CONFIG_GUESTAGENT_ARCH_ is not set. guestagent_path_enabled_by_config = $(if $(call config_guestagent_arch,$(2)),$(call guestagent_path,$(1),$(2))) ifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y) # apply CONFIG_GUESTAGENT_ARCH_* GUESTAGENTS += $(foreach arch,$(LINUX_GUESTAGENT_ARCHS),$(call guestagent_path_enabled_by_config,LINUX,$(arch))) endif ifeq ($(CONFIG_GUESTAGENT_OS_DARWIN),y) GUESTAGENTS += $(call guestagent_path,DARWIN,aarch64) endif .PHONY: guestagents native-guestagent additional-guestagents guestagents: $(GUESTAGENTS) native-guestagent: $(NATIVE_GUESTAGENT) additional-guestagents: $(ADDITIONAL_GUESTAGENTS) %-guestagent: @[ "$(findstring $(*),$(LINUX_GUESTAGENT_ARCHS))" == "$(*)" ] && make $(call guestagent_path,LINUX,$*) @[ "$(findstring $(*),$(DARWIN_GUESTAGENT_ARCHS))" == "$(*)" ] && make $(call guestagent_path,DARWIN,$*) # environment variables for linux-guestagent. these variable are used for checking force build. ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)aarch64 = CGO_ENABLED=0 GOOS=linux GOARCH=arm64 ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)armv7l = CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)ppc64le = CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)riscv64 = CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)s390x = CGO_ENABLED=0 GOOS=linux GOARCH=s390x ENVS_$(LINUX_GUESTAGENT_PATH_COMMON)x86_64 = CGO_ENABLED=0 GOOS=linux GOARCH=amd64 ENVS_$(DARWIN_GUESTAGENT_PATH_COMMON)aarch64 = CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(ALL_GUESTAGENTS_NOT_COMPRESSED): $(call dependencies_for_cmd,lima-guestagent) $$(call force_build_with_gunzip,$$@) | _output/share/lima $(ENVS_$@) $(GO_BUILD) -o $@ ./cmd/lima-guestagent chmod 644 $@ $(LINUX_GUESTAGENT_PATH_COMMON)%.gz: $(LINUX_GUESTAGENT_PATH_COMMON)% $$(call force_build_with_gunzip,$$@) @set -x; gzip -n $< $(DARWIN_GUESTAGENT_PATH_COMMON)%.gz: $(DARWIN_GUESTAGENT_PATH_COMMON)% $$(call force_build_with_gunzip,$$@) @set -x; gzip -n $< MKDIR_TARGETS += _output/share/lima ################################################################################ # _output/share/lima/templates TEMPLATES = $(addprefix _output/share/lima/templates/,$(filter-out experimental,$(notdir $(wildcard templates/*)))) TEMPLATE_DEFAULTS = ${addprefix _output/share/lima/templates/_default/,$(notdir $(wildcard templates/_default/*))} TEMPLATE_IMAGES = $(addprefix _output/share/lima/templates/_images/,$(notdir $(wildcard templates/_images/*))) TEMPLATE_EXPERIMENTALS = $(addprefix _output/share/lima/templates/experimental/,$(notdir $(wildcard templates/experimental/*))) .PHONY: default_template templates template_experimentals default_template: _output/share/lima/templates/default.yaml templates: $(TEMPLATES) $(TEMPLATE_DEFAULTS) $(TEMPLATE_IMAGES) template_experimentals: $(TEMPLATE_EXPERIMENTALS) $(TEMPLATES): | _output/share/lima/templates $(TEMPLATE_DEFAULTS): | _output/share/lima/templates/_default $(TEMPLATE_IMAGES): | _output/share/lima/templates/_images $(TEMPLATE_EXPERIMENTALS): | _output/share/lima/templates/experimental MKDIR_TARGETS += _output/share/lima/templates _output/share/lima/templates/_default _output/share/lima/templates/_images _output/share/lima/templates/experimental _output/share/lima/templates/%: templates/% cp -aL $< $@ # returns "force" if GOOS==windows, or GOOS!=windows and the file $(1) is not a symlink. # $(1): target file # On Windows, always copy to ensure the target has the same file as the source. force_link = $(if $(filter windows,$(GOOS)),force,$(shell test ! -L $(1) && echo force)) ################################################################################ # templates/_images # fedora-N.yaml should not be updated to refer to Fedora N+1 images TEMPLATES_TO_BE_UPDATED = $(filter-out $(wildcard templates/_images/fedora*.yaml),$(wildcard templates/_images/*.yaml)) .PHONY: update-templates update-templates: $(TEMPLATES_TO_BE_UPDATED) ./hack/update-template.sh $^ ################################################################################ # _output/share/doc/lima DOCUMENTATION = $(addprefix _output/share/doc/lima/,$(wildcard *.md) LICENSE SECURITY.md VERSION) .PHONY: documentation documentation: $(DOCUMENTATION) _output/share/doc/lima/SECURITY.md: | _output/share/doc/lima echo "Moved to https://github.com/lima-vm/.github/blob/main/SECURITY.md" > $@ _output/share/doc/lima/VERSION: | _output/share/doc/lima echo $(VERSION) > $@ _output/share/doc/lima/%: % | _output/share/doc/lima cp -aL $< $@ MKDIR_TARGETS += _output/share/doc/lima .PHONY: create-links-in-doc-dir create-links-in-doc-dir: _output/share/doc/lima/templates _output/share/doc/lima/templates: _output/share/lima/templates $$(call force_link,$$@) # remove the existing directory or symlink rm -rf $@ ifneq ($(GOOS),windows) ln -sf ../../lima/templates $@ else # copy from templates built in build process cp -aL $< $@ endif ################################################################################ # returns difference between GOOS GOARCH and GOHOSTOS GOHOSTARCH. cross_compiling = $(call diff,$(GOOS) $(GOARCH),$(GOHOSTOS) $(GOHOSTARCH)) # returns true if cross_compiling is empty. native_compiling = $(if $(cross_compiling),,true) ################################################################################ # _output/share/man/man1 .PHONY: manpages # Set limactl.1 as explicit dependency. # It's uncertain how many manpages will be generated by `make`, # because `limactl` generates them without corresponding source code for the manpages. manpages: _output/share/man/man1/limactl.1 _output/share/man/man1/limactl.1: _output/bin/limactl$(exe) @mkdir -p _output/share/man/man1 ifeq ($(native_compiling),true) # The manpages are generated by limactl, so the limactl binary must be native. $< generate-doc _output/share/man/man1 \ --output _output --prefix $(PREFIX) endif ################################################################################ .PHONY: docsy # Set limactl.md as explicit dependency. # It's uncertain how many docsy pages will be generated by `make`, # because `limactl` generates them without corresponding source code for the docsy pages. docsy: website/_output/docsy/limactl.md website/_output/docsy-mcp/mcp.md website/_output/docsy/limactl.md: _output/bin/limactl$(exe) @mkdir -p website/_output/docsy ifeq ($(native_compiling),true) # The docs are generated by limactl, so the limactl binary must be native. $< generate-doc --type docsy website/_output/docsy \ --output _output --prefix $(PREFIX) endif website/_output/docsy-mcp/mcp.md: _output/libexec/lima/limactl-mcp$(exe) @mkdir -p website/_output/docsy-mcp ifeq ($(native_compiling),true) $< generate-doc website/_output/docsy-mcp endif ################################################################################ default-template.yaml: _output/bin/limactl$(exe) ifeq ($(native_compiling),true) $< tmpl copy --embed-all templates/default.yaml $@ endif schema-limayaml.json: _output/bin/limactl$(exe) templates/default.yaml default-template.yaml ifeq ($(native_compiling),true) # validate both the original template (with the "base" etc), and the embedded template $< generate-jsonschema --schemafile $@ templates/default.yaml default-template.yaml endif .PHONY: check-jsonschema check-jsonschema: schema-limayaml.json templates/default.yaml default-template.yaml check-jsonschema --schemafile schema-limayaml.json templates/default.yaml default-template.yaml ################################################################################ .PHONY: diagrams diagrams: docs/lima-sequence-diagram.png docs/lima-sequence-diagram.png: docs/images/lima-sequence-diagram.puml $(PLANTUML) ./docs/images/lima-sequence-diagram.puml ################################################################################ .PHONY: install install: uninstall mkdir -p "$(DEST)" # Use tar rather than cp, for better symlink handling ( cd _output && $(TAR) c * | $(TAR) -xv --no-same-owner -C "$(DEST)" ) if [ "$(shell uname -s )" != "Linux" -a ! -e "$(DEST)/bin/nerdctl" ]; then ln -sf nerdctl.lima "$(DEST)/bin/nerdctl"; fi if [ "$(shell uname -s )" != "Linux" -a ! -e "$(DEST)/bin/apptainer" ]; then ln -sf apptainer.lima "$(DEST)/bin/apptainer"; fi .PHONY: uninstall uninstall: @test -f "$(DEST)/bin/lima" || echo "lima not found in $(DEST) prefix" rm -rf \ "$(DEST)/bin/lima" \ "$(DEST)/bin/lima$(bat)" \ "$(DEST)/bin/limactl$(exe)" \ "$(DEST)/bin/nerdctl.lima" \ "$(DEST)/bin/apptainer.lima" \ "$(DEST)/bin/docker.lima" \ "$(DEST)/bin/podman.lima" \ "$(DEST)/bin/kubectl.lima" \ "$(DEST)/share/man/man1/lima.1" \ "$(DEST)/share/man/man1/limactl"*".1" \ "$(DEST)/share/lima" \ "$(DEST)/share/doc/lima" \ "$(DEST)/libexec/lima/limactl-mcp$(exe)" \ "$(DEST)/libexec/lima/limactl-url-fedora-rawhide" \ "$(DEST)/libexec/lima/lima-driver-qemu$(exe)" \ "$(DEST)/libexec/lima/lima-driver-vz$(exe)" \ "$(DEST)/libexec/lima/lima-driver-wsl2$(exe)" \ "$(DEST)/libexec/lima/lima-driver-krunkit$(exe)" if [ "$$(readlink "$(DEST)/bin/nerdctl")" = "nerdctl.lima" ]; then rm "$(DEST)/bin/nerdctl"; fi if [ "$$(readlink "$(DEST)/bin/apptainer")" = "apptainer.lima" ]; then rm "$(DEST)/bin/apptainer"; fi .PHONY: check-generated check-generated: git diff --exit-code || \ (echo "Please run 'make generate' when making changes to proto files and check-in the generated file changes" && false) .PHONY: bats bats: native limactl-plugins PATH=$$PWD/_output/bin:$$PATH ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/tests .PHONY: lint lint: check-generated nobin --allow-emoji --allow-escape --gitignore --skip-ext pb.desc --skip 'website/static/images/**/*.{gif,png}' --skip 'docs/reports/**/*.pdf' editorconfig-checker golangci-lint run ./... yamllint . ls-lint find . -name '*.sh' ! -path "./.git/*" | xargs shellcheck find . -name '*.sh' ! -path "./.git/*" | xargs shfmt -s -d # the allow list corresponds to https://github.com/cncf/foundation/blob/e5db022a0009f4db52b89d9875640cf3137153fe/allowed-third-party-license-policy.md # hashicorp/hcl/v2 is MPL-2.0; covered by the CNCF license exception for hashicorp/hcl # see also https://github.com/cncf/foundation/issues/1242 go-licenses check --include_tests --ignore github.com/hashicorp/hcl/v2 ./... --allowed_licenses=$$(cat ./hack/allowed-licenses.txt) ltag -t ./hack/ltag --check -v protolint . .PHONY: clean clean: rm -rf _output vendor .PHONY: install-protoc-tools install-protoc-tools: go install -modfile=./hack/tools/go.mod google.golang.org/protobuf/cmd/protoc-gen-go go install -modfile=./hack/tools/go.mod google.golang.org/grpc/cmd/protoc-gen-go-grpc .PHONY: generate generate: go generate ./... ################################################################################ # _artifacts/lima-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M) # _artifacts/lima-additional-guestagents-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M) .PHONY: artifact # returns the capitalized string of $(1). capitalize = $(shell echo "$(1)"|awk '{print toupper(substr($$0,1,1)) tolower(substr($$0,2))}') # returns the architecture name converted from GOARCH to GNU coreutils uname -m. to_uname_m = $(foreach arch,$(1),$(shell echo $(arch) | sed 's/amd64/x86_64/' | sed 's/arm64/aarch64/')) ARTIFACT_FILE_EXTENSIONS := .tar.gz ifeq ($(GOOS),darwin) # returns the architecture name converted from GOARCH to macOS's uname -m. to_uname_m = $(foreach arch,$(1),$(shell echo $(arch) | sed 's/amd64/x86_64/')) else ifeq ($(GOOS),linux) # CC is required for cross-compiling on Linux. # On Debian, Ubuntu, and related distributions, compilers are named like x86_64-linux-gnu-gcc # On Fedora, RHEL, and related distributions, the equivalent is x86_64-redhat-linux-gcc # On openSUSE and as a generic fallback, gcc is used CC := $(shell \ if command -v $(call to_uname_m,$(GOARCH))-redhat-linux-gcc >/dev/null 2>&1; then \ echo $(call to_uname_m,$(GOARCH))-redhat-linux-gcc; \ elif command -v $(call to_uname_m,$(GOARCH))-linux-gnu-gcc >/dev/null 2>&1; then \ echo $(call to_uname_m,$(GOARCH))-linux-gnu-gcc; \ else \ echo gcc; \ fi) else ifeq ($(GOOS),windows) # artifact in zip format also provided for Windows. ARTIFACT_FILE_EXTENSIONS += .zip endif # artifacts: artifacts-$(GOOS) ARTIFACT_OS = $(call capitalize,$(GOOS)) ARTIFACT_UNAME_M = $(call to_uname_m,$(GOARCH)) ARTIFACT_PATH_COMMON = _artifacts/lima-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M) ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON = _artifacts/lima-additional-guestagents-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M) artifact: $(addprefix $(ARTIFACT_PATH_COMMON),$(ARTIFACT_FILE_EXTENSIONS)) \ $(addprefix $(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON),$(ARTIFACT_FILE_EXTENSIONS)) ARTIFACT_DES = _output/bin/limactl$(exe) limactl-plugins $(LIMA_DEPS) $(HELPERS_DEPS) \ $(NATIVE_GUESTAGENT) \ $(TEMPLATES) $(TEMPLATE_IMAGES) $(TEMPLATE_DEFAULTS) $(TEMPLATE_EXPERIMENTALS) \ additional-drivers \ $(DOCUMENTATION) _output/share/doc/lima/templates \ _output/share/man/man1/limactl.1 # file targets $(ARTIFACT_PATH_COMMON).tar.gz: $(ARTIFACT_DES) | _artifacts $(TAR) -C _output/ --no-xattrs -czvf $@ ./ $(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON).tar.gz: # FIXME: do not exec make from make make clean additional-guestagents $(TAR) -C _output/ --no-xattrs -czvf $@ ./ $(ARTIFACT_PATH_COMMON).zip: $(ARTIFACT_DES) | _artifacts cd _output && $(ZIP) -r ../$@ * $(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON).zip: make clean additional-guestagents cd _output && $(ZIP) -r ../$@ * # generate manpages using native limactl. manpages-using-native-limactl: GOOS = $(GOHOSTOS) manpages-using-native-limactl: GOARCH = $(GOHOSTARCH) manpages-using-native-limactl: manpages # returns "manpages-using-native-limactl" if $(1) is not equal to $(GOHOSTOS). # $(1): GOOS generate_manpages_if_needed = $(if $(filter $(if $(1),$(1),$(GOOS)),$(GOHOSTOS)),,manpages-using-native-limactl) # build native arch first, then build other archs. artifact_goarchs = arm64 amd64 goarchs_native_and_others = $(GOHOSTARCH) $(filter-out $(GOHOSTARCH),$(artifact_goarchs)) # artifacts is artifact bundles for each OS. # if target GOOS is native, build native arch first, generate manpages, then build other archs. # if target GOOS is not native, build native limactl, generate manpages, then build the target GOOS with archs. .PHONY: artifacts artifacts-darwin artifacts-linux artifacts-windows artifacts: $$(addprefix artifact-$$(GOOS)-,$$(goarchs_native_and_others)) artifacts-darwin: $$(call generate_manpages_if_needed,darwin) $$(addprefix artifact-darwin-,$$(goarchs_native_and_others)) artifacts-linux: $$(call generate_manpages_if_needed,linux) $$(addprefix artifact-linux-,$$(goarchs_native_and_others)) artifacts-windows: $$(call generate_manpages_if_needed,windows) $$(addprefix artifact-windows-,$$(goarchs_native_and_others)) # set variables for artifact variant targets. artifact-darwin% artifact-darwin: GOOS = darwin artifact-linux% artifact-linux: GOOS = linux artifact-windows% artifact-windows: GOOS = windows artifact-%-amd64 artifact-%-x86_64 artifact-amd64 artifact-x86_64: GOARCH = amd64 artifact-%-arm64 artifact-%-aarch64 artifact-arm64 artifact-aarch64: GOARCH = arm64 # build cross arch binaries. artifact-%: $$(call generate_manpages_if_needed) make clean artifact GOOS=$(GOOS) GOARCH=$(GOARCH) .PHONY: artifacts-misc artifacts-misc: | _artifacts go mod vendor $(TAR) --no-xattrs -czf _artifacts/lima-$(VERSION_TRIMMED)-go-mod-vendor.tar.gz go.mod go.sum vendor MKDIR_TARGETS += _artifacts ################################################################################ # This target must be placed after any changes to the `MKDIR_TARGETS` variable. # It seems that variable expansion in Makefile targets is not done recursively. $(MKDIR_TARGETS): mkdir -p $@ ================================================ FILE: NOTICE ================================================ Lima Copyright The Lima Authors. This project contains portions of other projects that are licensed under the terms of Apache License 2.0. The NOTICE files of those projects are replicated here for compliance of the section 4(d) of the license. === https://github.com/containerd/containerd === https://github.com/containerd/containerd/blob/v2.1.1/LICENSE https://github.com/containerd/containerd/blob/v2.1.1/NOTICE > Docker > Copyright 2012-2015 Docker, Inc. > > This product includes software developed at Docker, Inc. (https://www.docker.com). > > The following is courtesy of our legal counsel: > > > Use and transfer of Docker may be subject to certain restrictions by the > United States and other governments. > It is your responsibility to ensure that your use and/or transfer does not > violate applicable laws. > > For more information, please see https://www.bis.doc.gov > > See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. ================================================ FILE: README.md ================================================ [[🌎**Web site**]](https://lima-vm.io/) [[📖**Documentation**]](https://lima-vm.io/docs/) [[👤**Slack (`#lima`)**]](https://slack.cncf.io) Shows a stylized 'Lima' text in bold, modern font # Lima: Linux Machines [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/lima-vm/lima) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/6505/badge)](https://www.bestpractices.dev/projects/6505) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/lima-vm/lima/badge)](https://scorecard.dev/viewer/?uri=github.com/lima-vm/lima) [Lima](https://lima-vm.io/) launches Linux virtual machines with automatic file sharing and port forwarding (similar to WSL2). The original goal of Lima was to promote [containerd](https://containerd.io) including [nerdctl (contaiNERD ctl)](https://github.com/containerd/nerdctl) to Mac users, but Lima can be used for non-container applications as well. Lima also supports other container engines (Docker, Podman, Kubernetes, etc.) and non-macOS hosts (Linux, NetBSD, etc.). ## Getting started Set up (Homebrew): ```bash brew install lima limactl start ``` To run Linux commands: ```bash lima uname -a ``` To run containers with containerd: ```bash lima nerdctl run --rm hello-world ``` To run containers with Docker: ```bash limactl start template:docker export DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock') docker run --rm hello-world ``` To run containers with Kubernetes: ```bash limactl start template:k8s export KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml') kubectl apply -f ... ``` See for the further information. ## Contributing We welcome contributions! Please see our [Contributing Guide](https://lima-vm.io/docs/community/contributing/) for details on: - **Developer Certificate of Origin (DCO)**: All commits must be signed off with `git commit -s` - Code licensing and pull request guidelines - Testing requirements ## Community ### Adopters Container environments: - [Rancher Desktop](https://rancherdesktop.io/): Kubernetes and container management to the desktop - [Colima](https://github.com/abiosoft/colima): Docker (and Kubernetes) on macOS with minimal setup - [Finch](https://github.com/runfinch/finch): Finch is a command line client for local container development - [Podman Desktop](https://podman-desktop.io/): Podman Desktop GUI has a plug-in for Lima virtual machines GUI: - [Lima xbar plugin](https://github.com/unixorn/lima-xbar-plugin): [xbar](https://xbarapp.com/) plugin to start/stop VMs from the menu bar and see their running status. - [lima-gui](https://github.com/afbjorklund/lima-gui): Qt GUI for Lima ### Communication channels - [GitHub Discussions](https://github.com/lima-vm/lima/discussions) - `#lima` channel in the CNCF Slack - New account: - Login: - Zoom meetings (tentatively monthly) - Meeting notes & agenda proposals: https://github.com/lima-vm/lima/discussions/categories/meetings - Calendar: https://zoom-lfx.platform.linuxfoundation.org/meetings/lima ### Social media accounts Follow us for project updates, release announcements, and community news: - https://x.com/@TheLimaProject - https://mastodon.social/@TheLimaProject ### Code of Conduct Lima follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). - - - **We are a [Cloud Native Computing Foundation](https://cncf.io/) incubating project.** The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see [Trademark Usage](https://www.linuxfoundation.org/legal/trademark-usage). ================================================ FILE: ROADMAP.md ================================================ Moved to . ================================================ FILE: cmd/apptainer.lima ================================================ #!/bin/sh set -eu : "${LIMA_INSTANCE:=apptainer}" : "${APPTAINER_BINDPATH:=}" if [ "$(limactl ls -q "$LIMA_INSTANCE" 2>/dev/null)" != "$LIMA_INSTANCE" ]; then echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template:apptainer\` to create a new instance" >&2 exit 1 elif [ "$(limactl ls -f '{{ .Status }}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2 exit 1 fi export LIMA_INSTANCE if [ -n "$APPTAINER_BINDPATH" ]; then APPTAINER_BINDPATH="$APPTAINER_BINDPATH," fi APPTAINER_BINDPATH="$APPTAINER_BINDPATH$HOME" exec lima APPTAINER_BINDPATH="$APPTAINER_BINDPATH" apptainer "$@" ================================================ FILE: cmd/docker.lima ================================================ #!/bin/sh set -eu # Environment Variables # LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is "docker". : "${LIMA_INSTANCE:=docker}" : "${DOCKER:=docker}" if [ "$(limactl ls -q "$LIMA_INSTANCE" 2>/dev/null)" != "$LIMA_INSTANCE" ]; then echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template:docker\` to create a new instance" >&2 exit 1 elif [ "$(limactl ls -f '{{ .Status }}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2 exit 1 fi DOCKER=$(command -v "$DOCKER" || true) if [ -n "$DOCKER" ]; then DOCKER_HOST=$(limactl list "$LIMA_INSTANCE" --format 'unix://{{.Dir}}/sock/docker.sock') export DOCKER_HOST exec "$DOCKER" "$@" else export LIMA_INSTANCE exec lima docker "$@" fi ================================================ FILE: cmd/kubectl.lima ================================================ #!/bin/sh set -eu # Environment Variables # LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is empty. : "${LIMA_INSTANCE:=}" : "${KUBECTL:=kubectl}" if [ -z "$LIMA_INSTANCE" ]; then if [ "$(limactl ls -q k3s 2>/dev/null)" = "k3s" ]; then LIMA_INSTANCE=k3s elif [ "$(limactl ls -q k8s 2>/dev/null)" = "k8s" ]; then LIMA_INSTANCE=k8s else echo "No k3s or k8s existing instances found. Either create one with" >&2 echo "limactl create --name=k3s template:k3s" >&2 echo "limactl create --name=k8s template:k8s" >&2 echo "or set LIMA_INSTANCE to the name of your Kubernetes instance" >&2 exit 1 fi if [ "$(limactl ls -f '{{.Status}}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2 exit 1 fi elif [ "$(limactl ls -q "$LIMA_INSTANCE" 2>/dev/null)" != "$LIMA_INSTANCE" ]; then echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE\` to create a new instance" >&2 exit 1 elif [ "$(limactl ls -f '{{ .Status }}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2 exit 1 fi KUBECTL=$(command -v "$KUBECTL" || true) if [ -n "$KUBECTL" ]; then KUBECONFIG=$(limactl list "$LIMA_INSTANCE" --format '{{.Dir}}/copied-from-guest/kubeconfig.yaml') export KUBECONFIG exec "$KUBECTL" "$@" else export LIMA_INSTANCE exec lima sudo kubectl "$@" fi ================================================ FILE: cmd/lima ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu # Environment Variables # LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is "default". # LIMA_SHELL: Specifies the shell interpreter to use inside the Lima instance. Default is the user's shell configured inside the instance. # LIMA_WORKDIR: Specifies the initial working directory inside the Lima instance. Default is the current directory from the host. # LIMACTL: Specifies the path to the limactl binary. Default is "limactl" in $PATH. : "${LIMA_INSTANCE:=default}" : "${LIMA_SHELL:=}" : "${LIMA_WORKDIR:=}" : "${LIMACTL:=limactl}" if [ "$#" -eq 1 ]; then if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then base="$(basename "$0")" echo "Usage: ${base} [COMMAND...]" echo echo "${base} is an alias for \"${LIMACTL} shell ${LIMA_INSTANCE}\"." echo "The instance name (\"${LIMA_INSTANCE}\") can be changed by specifying \$LIMA_INSTANCE." echo echo "The shell and initial workdir inside the instance can be specified via \$LIMA_SHELL" echo "and \$LIMA_WORKDIR." echo echo "See \`${LIMACTL} shell --help\` for further information." exit 0 elif [ "$1" = "-v" ] || [ "$1" = "--version" ]; then exec "$LIMACTL" "$@" fi fi set - --instance "$LIMA_INSTANCE" "$@" if [ -n "${LIMA_SHELL}" ]; then set - --shell "$LIMA_SHELL" "$@" fi if [ -n "${LIMA_WORKDIR}" ]; then set - --workdir "$LIMA_WORKDIR" "$@" fi # Avoid converting paths with MSYS2 MSYS2_ARG_CONV_EXCL="*" export MSYS2_ARG_CONV_EXCL exec "$LIMACTL" shell "$@" ================================================ FILE: cmd/lima-driver-krunkit/main_darwin_arm64.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "github.com/lima-vm/lima/v2/pkg/driver/external/server" "github.com/lima-vm/lima/v2/pkg/driver/krunkit" ) // To be used as an external driver for Lima. func main() { server.Serve(context.Background(), krunkit.New()) } ================================================ FILE: cmd/lima-driver-qemu/main.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "github.com/lima-vm/lima/v2/pkg/driver/external/server" "github.com/lima-vm/lima/v2/pkg/driver/qemu" ) // To be used as an external driver for Lima. func main() { server.Serve(context.Background(), qemu.New()) } ================================================ FILE: cmd/lima-driver-vz/main_darwin.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "github.com/lima-vm/lima/v2/pkg/driver/external/server" "github.com/lima-vm/lima/v2/pkg/driver/vz" ) // To be used as an external driver for Lima. func main() { server.Serve(context.Background(), vz.New()) } ================================================ FILE: cmd/lima-driver-wsl2/main_windows.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "github.com/lima-vm/lima/v2/pkg/driver/external/server" "github.com/lima-vm/lima/v2/pkg/driver/wsl2" ) // To be used as an external driver for Lima. func main() { server.Serve(context.Background(), wsl2.New()) } ================================================ FILE: cmd/lima-guestagent/daemon_linux.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "net" "os" "os/signal" "syscall" "time" "github.com/mdlayher/vsock" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/guestagent" "github.com/lima-vm/lima/v2/pkg/guestagent/api/server" "github.com/lima-vm/lima/v2/pkg/guestagent/serialport" "github.com/lima-vm/lima/v2/pkg/guestagent/ticker" "github.com/lima-vm/lima/v2/pkg/portfwdserver" ) const hostCID = 2 type cidFilteredListener struct { *vsock.Listener } func (l *cidFilteredListener) Accept() (net.Conn, error) { for { conn, err := l.Listener.Accept() if err != nil { return nil, err } if vsockConn, ok := conn.(*vsock.Conn); ok { if addr, ok := vsockConn.RemoteAddr().(*vsock.Addr); ok { if addr.ContextID != hostCID { logrus.Warnf("rejected vsock connection from unauthorized CID %d", addr.ContextID) conn.Close() continue } } } return conn, nil } } func newDaemonCommand() *cobra.Command { daemonCommand := &cobra.Command{ Use: "daemon", Short: "Run the daemon", RunE: daemonAction, } daemonCommand.Flags().String("runtime-dir", "/run/lima-guestagent", "Directory to store runtime state") daemonCommand.Flags().Duration("tick", 3*time.Second, "Tick for polling events") daemonCommand.Flags().Int("vsock-port", 0, "Use vsock server instead a UNIX socket") daemonCommand.Flags().String("virtio-port", "", "Use virtio server instead a UNIX socket") return daemonCommand } func daemonAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() runtimeDir, err := cmd.Flags().GetString("runtime-dir") if err != nil { return err } if err := os.MkdirAll(runtimeDir, 0o755); err != nil { return err } socket := "/run/lima-guestagent.sock" tick, err := cmd.Flags().GetDuration("tick") if err != nil { return err } vSockPort, err := cmd.Flags().GetInt("vsock-port") if err != nil { return err } virtioPort, err := cmd.Flags().GetString("virtio-port") if err != nil { return err } if tick == 0 { return errors.New("tick must be specified") } if os.Geteuid() != 0 { return errors.New("must run as the root user") } logrus.Infof("event tick: %v", tick) simpleTicker := ticker.NewSimpleTicker(time.NewTicker(tick)) tickerInst := simpleTicker // See /sys/kernel/debug/tracing/available_events for the list of available tracepoints tracepoints := []string{"syscalls:sys_exit_bind"} if ebpfTicker, err := ticker.NewEbpfTicker(tracepoints); err != nil { logrus.WithError(err).Warn("failed to create eBPF ticker, falling back to simple ticker") } else { logrus.Infof("using eBPF ticker with tracepoints: %v", tracepoints) tickerInst = ticker.NewCompoundTicker(simpleTicker, ebpfTicker) } ctx, stop := signal.NotifyContext(ctx, syscall.SIGTERM) defer stop() go func() { <-ctx.Done() logrus.Debug("Received SIGTERM, shutting down the guest agent") }() agent, err := guestagent.New(ctx, tickerInst, runtimeDir) if err != nil { return err } err = os.RemoveAll(socket) if err != nil { return err } var l net.Listener if virtioPort != "" { qemuL, err := serialport.Listen("/dev/virtio-ports/" + virtioPort) if err != nil { return err } l = qemuL logrus.Infof("serving the guest agent on qemu serial file: %s", virtioPort) } else if vSockPort != 0 { vsockL, err := vsock.Listen(uint32(vSockPort), nil) if err != nil { return err } l = &cidFilteredListener{Listener: vsockL} logrus.Infof("serving the guest agent on vsock port: %d (host CID only)", vSockPort) } else { var lc net.ListenConfig socketL, err := lc.Listen(ctx, "unix", socket) if err != nil { return err } if err := os.Chmod(socket, 0o777); err != nil { return err } l = socketL logrus.Infof("serving the guest agent on %q", socket) } defer logrus.Debug("exiting lima-guestagent daemon") return server.StartServer(ctx, l, &server.GuestServer{Agent: agent, TunnelS: portfwdserver.NewTunnelServer()}) } ================================================ FILE: cmd/lima-guestagent/fake_cloud_init_darwin.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/guestagent/fakecloudinit" ) func newFakeCloudInitCommand() *cobra.Command { cmd := &cobra.Command{ Use: "fake-cloud-init", Short: "Run fake cloud-init", RunE: fakeCloudInitAction, } return cmd } func fakeCloudInitAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() return fakecloudinit.Run(ctx) } ================================================ FILE: cmd/lima-guestagent/install_systemd_linux.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "bytes" _ "embed" "errors" "fmt" "os" "os/exec" "path/filepath" "slices" "strings" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/textutil" ) func newInstallSystemdCommand() *cobra.Command { installSystemdCommand := &cobra.Command{ Use: "install-systemd", Short: "Install a systemd unit (user)", RunE: installSystemdAction, } installSystemdCommand.Flags().Bool("guestagent-updated", false, "Indicate that the guest agent has been updated") installSystemdCommand.Flags().Int("vsock-port", 0, "Use vsock server on specified port") installSystemdCommand.Flags().String("virtio-port", "", "Use virtio server instead a UNIX socket") return installSystemdCommand } func installSystemdAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() guestAgentUpdated, err := cmd.Flags().GetBool("guestagent-updated") if err != nil { return err } vsockPort, err := cmd.Flags().GetInt("vsock-port") if err != nil { return err } virtioPort, err := cmd.Flags().GetString("virtio-port") if err != nil { return err } debug, err := cmd.Flags().GetBool("debug") if err != nil { return err } unit, err := generateSystemdUnit(vsockPort, virtioPort, debug) if err != nil { return err } unitPath := "/etc/systemd/system/lima-guestagent.service" unitFileChanged := true if _, err := os.Stat(unitPath); !errors.Is(err, os.ErrNotExist) { if existingUnit, err := os.ReadFile(unitPath); err == nil && bytes.Equal(unit, existingUnit) { logrus.Infof("File %q is up-to-date", unitPath) unitFileChanged = false } else { logrus.Infof("File %q needs update", unitPath) } } else { unitDir := filepath.Dir(unitPath) if err := os.MkdirAll(unitDir, 0o755); err != nil { return err } } if unitFileChanged { if err := os.WriteFile(unitPath, unit, 0o644); err != nil { return err } logrus.Infof("Written file %q", unitPath) } else if !guestAgentUpdated { logrus.Info("lima-guestagent.service already up-to-date") return nil } // unitFileChanged || guestAgentUpdated args := make([][]string, 0, 4) if unitFileChanged { args = append(args, []string{"daemon-reload"}) } args = slices.Concat( args, [][]string{ {"enable", "lima-guestagent.service"}, {"try-restart", "lima-guestagent.service"}, // try-restart: restart if running, otherwise do nothing {"start", "lima-guestagent.service"}, // start: start if not running, otherwise do nothing }, ) for _, args := range args { cmd := exec.CommandContext(ctx, "systemctl", append([]string{"--system"}, args...)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logrus.Infof("Executing: %s", strings.Join(cmd.Args, " ")) if err := cmd.Run(); err != nil { return err } } logrus.Info("Done") return nil } //go:embed lima-guestagent.TEMPLATE.service var systemdUnitTemplate string func generateSystemdUnit(vsockPort int, virtioPort string, debug bool) ([]byte, error) { selfExeAbs, err := os.Executable() if err != nil { return nil, err } var args []string if vsockPort != 0 { args = append(args, fmt.Sprintf("--vsock-port %d", vsockPort)) } if virtioPort != "" { args = append(args, fmt.Sprintf("--virtio-port %s", virtioPort)) } if debug { args = append(args, "--debug") } m := map[string]string{ "Binary": selfExeAbs, "Args": strings.Join(args, " "), } return textutil.ExecuteTemplate(systemdUnitTemplate, m) } ================================================ FILE: cmd/lima-guestagent/lima-guestagent.TEMPLATE.service ================================================ [Unit] Description=lima-guestagent [Service] ExecStart={{.Binary}} daemon {{.Args}} --runtime-dir="%t/%N" Type=simple Restart=on-failure OOMPolicy=continue OOMScoreAdjust=-500 [Install] WantedBy=multi-user.target ================================================ FILE: cmd/lima-guestagent/main.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "strings" "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/osutil" "github.com/lima-vm/lima/v2/pkg/version" ) func main() { yq.MaybeRunYQ() if err := newApp().Execute(); err != nil { osutil.HandleExitError(err) logrus.Fatal(err) } } func newApp() *cobra.Command { rootCmd := &cobra.Command{ Use: "lima-guestagent", Short: "Do not launch manually", Version: strings.TrimPrefix(version.Version, "v"), } rootCmd.PersistentFlags().Bool("debug", false, "Debug mode") rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { debug, _ := cmd.Flags().GetBool("debug") if debug { logrus.SetLevel(logrus.DebugLevel) debugutil.Debug = true } return nil } addRootCommands(rootCmd) return rootCmd } ================================================ FILE: cmd/lima-guestagent/root_darwin.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/spf13/cobra" ) func addRootCommands(rootCmd *cobra.Command) { rootCmd.AddCommand( newFakeCloudInitCommand(), ) } ================================================ FILE: cmd/lima-guestagent/root_linux.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/spf13/cobra" ) func addRootCommands(rootCmd *cobra.Command) { rootCmd.AddCommand( newDaemonCommand(), newInstallSystemdCommand(), ) } ================================================ FILE: cmd/lima-guestagent/root_others.go ================================================ //go:build !linux && !darwin // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/spf13/cobra" ) func addRootCommands(_ *cobra.Command) { // NOP } ================================================ FILE: cmd/lima.bat ================================================ @echo off REM Environment Variables REM LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is "default". REM LIMACTL: Specifies the path to the limactl binary. Default is "limactl" in %PATH%. IF NOT DEFINED LIMACTL (SET LIMACTL=limactl) IF NOT DEFINED LIMA_INSTANCE (SET LIMA_INSTANCE=default) %LIMACTL% shell --instance %LIMA_INSTANCE% %* ================================================ FILE: cmd/limactl/clone.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "path/filepath" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/networks/reconcile" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/yqutil" ) func newCloneCommand() *cobra.Command { cloneCommand := &cobra.Command{ Use: "clone OLDINST NEWINST", Short: "Clone an instance of Lima", Long: `Clone an instance of Lima. Not to be confused with 'limactl copy' ('limactl cp'). `, Args: WrapArgsError(cobra.ExactArgs(2)), RunE: cloneOrRenameAction, ValidArgsFunction: cloneBashComplete, GroupID: advancedCommand, } cloneCommand.Flags().Bool("start", false, "Start the instance after cloning") editflags.RegisterEdit(cloneCommand, "[limactl edit] ") return cloneCommand } func newRenameCommand() *cobra.Command { renameCommand := &cobra.Command{ Use: "rename OLDINST NEWINST", // No "mv" alias, to avoid confusion with a theoretical equivalent of `limactl cp` but s/cp/mv/. Short: "Rename an instance of Lima", Args: WrapArgsError(cobra.ExactArgs(2)), RunE: cloneOrRenameAction, ValidArgsFunction: cloneBashComplete, GroupID: advancedCommand, } renameCommand.Flags().Bool("start", false, "Start the instance after renaming") editflags.RegisterEdit(renameCommand, "[limactl edit] ") return renameCommand } func cloneOrRenameAction(cmd *cobra.Command, args []string) error { rename := cmd.Name() == "rename" ctx := cmd.Context() flags := cmd.Flags() tty, err := flags.GetBool("tty") if err != nil { return err } oldInstName, newInstName := args[0], args[1] oldInst, err := store.Inspect(ctx, oldInstName) if err != nil { if errors.Is(err, os.ErrNotExist) { return fmt.Errorf("instance %q not found", oldInstName) } return err } newInst, err := instance.CloneOrRename(ctx, oldInst, newInstName, rename) if err != nil { return err } yqExprs, err := editflags.YQExpressions(flags, false) if err != nil { return err } if len(yqExprs) > 0 { // TODO: reduce duplicated codes across cloneAction and editAction yq := yqutil.Join(yqExprs) filePath := filepath.Join(newInst.Dir, filenames.LimaYAML) yContent, err := os.ReadFile(filePath) if err != nil { return err } yBytes, err := yqutil.EvaluateExpression(yq, yContent) if err != nil { return err } y, err := limayaml.LoadWithWarnings(ctx, yBytes, filePath) if err != nil { return err } if err := driverutil.ResolveVMType(ctx, y, filePath); err != nil { return fmt.Errorf("failed to resolve vm for %q: %w", filePath, err) } if err := limayaml.Validate(y, true); err != nil { return saveRejectedYAML(yBytes, err) } if err := limayaml.ValidateAgainstLatestConfig(ctx, yBytes, yContent); err != nil { return saveRejectedYAML(yBytes, err) } if err := os.WriteFile(filePath, yBytes, 0o644); err != nil { return err } newInst, err = store.Inspect(ctx, newInst.Name) if err != nil { return err } } start, err := flags.GetBool("start") if err != nil { return err } if tty && !flags.Changed("start") { start, err = askWhetherToStart(cmd) if err != nil { return err } } if !start { return nil } err = reconcile.Reconcile(ctx, newInst.Name) if err != nil { return err } return instance.Start(ctx, newInst, false, false) } func cloneBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/completion.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "maps" "net" "slices" "strings" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/networks" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/templatestore" ) func bashCompleteInstanceNames(_ *cobra.Command) ([]string, cobra.ShellCompDirective) { instances, err := store.Instances() if err != nil { return nil, cobra.ShellCompDirectiveDefault } return instances, cobra.ShellCompDirectiveNoFileComp } func bashCompleteTemplateNames(_ *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) { var comp []string if templates, err := templatestore.Templates(); err == nil { for _, f := range templates { name := "template:" + f.Name if !strings.HasPrefix(name, toComplete) { continue } if len(toComplete) == len(name) { comp = append(comp, name) continue } // Skip private snippets (beginning with '_') from completion. if (name[len(toComplete)-1] == '/') && (name[len(toComplete)] == '_') { continue } comp = append(comp, name) } } return comp, cobra.ShellCompDirectiveDefault } func bashCompleteDiskNames(_ *cobra.Command) ([]string, cobra.ShellCompDirective) { disks, err := store.Disks() if err != nil { return nil, cobra.ShellCompDirectiveDefault } return disks, cobra.ShellCompDirectiveNoFileComp } func bashCompleteNetworkNames(_ *cobra.Command) ([]string, cobra.ShellCompDirective) { config, err := networks.LoadConfig() if err != nil { return nil, cobra.ShellCompDirectiveDefault } networks := slices.Sorted(maps.Keys(config.Networks)) return networks, cobra.ShellCompDirectiveNoFileComp } func bashFlagCompleteNetworkInterfaceNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { intf, err := net.Interfaces() if err != nil { return nil, cobra.ShellCompDirectiveDefault } var intfNames []string for _, f := range intf { intfNames = append(intfNames, f.Name) } return intfNames, cobra.ShellCompDirectiveNoFileComp } ================================================ FILE: cmd/limactl/copy.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/copytool" ) const copyHelp = `Copy files between host and guest Prefix guest filenames with the instance name and a colon. Backends: auto - Automatically selects the best available backend (rsync preferred, falls back to scp) rsync - Uses rsync for faster transfers with resume capability (requires rsync on both host and guest) scp - Uses scp for reliable transfers (always available) Not to be confused with 'limactl clone'. ` const copyExample = ` # Copy file from guest to host (auto backend) limactl copy default:/etc/os-release . # Copy file from host to guest with verbose output limactl copy -v myfile.txt default:/tmp/ # Copy directory recursively using rsync backend limactl copy --backend=rsync -r ./mydir default:/tmp/ # Copy using scp backend specifically limactl copy --backend=scp default:/var/log/app.log ./logs/ # Copy multiple files limactl copy file1.txt file2.txt default:/tmp/ ` func newCopyCommand() *cobra.Command { copyCommand := &cobra.Command{ Use: "copy SOURCE ... TARGET", Aliases: []string{"cp"}, Short: "Copy files between host and guest", Long: copyHelp, Example: copyExample, Args: WrapArgsError(cobra.MinimumNArgs(2)), RunE: copyAction, GroupID: advancedCommand, } copyCommand.Flags().BoolP("recursive", "r", false, "Copy directories recursively") copyCommand.Flags().BoolP("verbose", "v", false, "Enable verbose output") copyCommand.Flags().String("backend", "auto", "Copy backend (scp|rsync|auto)") return copyCommand } func copyAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() recursive, err := cmd.Flags().GetBool("recursive") if err != nil { return err } verbose, err := cmd.Flags().GetBool("verbose") if err != nil { return err } debug, err := cmd.Flags().GetBool("debug") if err != nil { return err } if debug { verbose = true } backend, err := cmd.Flags().GetString("backend") if err != nil { return err } cpTool, err := copytool.New(ctx, backend, args, ©tool.Options{ Recursive: recursive, Verbose: verbose, }) if err != nil { return err } logrus.Debugf("using copy tool %q", cpTool.Name()) copyCmd, err := cpTool.Command(ctx, args, nil) if err != nil { return err } copyCmd.Stdin = cmd.InOrStdin() copyCmd.Stdout = cmd.OutOrStdout() copyCmd.Stderr = cmd.ErrOrStderr() logrus.Debugf("executing %v (may take a long time)", copyCmd) // TODO: use syscall.Exec directly (results in losing tty?) return copyCmd.Run() } ================================================ FILE: cmd/limactl/debug.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "strconv" "time" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/hostagent/dns" ) func newDebugCommand() *cobra.Command { cmd := &cobra.Command{ Use: "debug", Short: "Debug utilities", Long: "DO NOT USE! THE COMMAND SYNTAX IS SUBJECT TO CHANGE!", Hidden: true, } cmd.AddCommand(newDebugDNSCommand()) return cmd } func newDebugDNSCommand() *cobra.Command { cmd := &cobra.Command{ Use: "dns UDPPORT [TCPPORT]", Short: "Debug built-in DNS", Long: "DO NOT USE! THE COMMAND SYNTAX IS SUBJECT TO CHANGE!", Args: WrapArgsError(cobra.RangeArgs(1, 2)), RunE: debugDNSAction, } cmd.Flags().BoolP("ipv6", "6", false, "Lookup IPv6 addresses too") return cmd } func debugDNSAction(cmd *cobra.Command, args []string) error { ipv6, err := cmd.Flags().GetBool("ipv6") if err != nil { return err } udpLocalPort, err := strconv.Atoi(args[0]) if err != nil { return err } tcpLocalPort := 0 if len(args) > 1 { tcpLocalPort, err = strconv.Atoi(args[1]) if err != nil { return err } } srvOpts := dns.ServerOptions{ UDPPort: udpLocalPort, TCPPort: tcpLocalPort, Address: "127.0.0.1", HandlerOptions: dns.HandlerOptions{ IPv6: ipv6, StaticHosts: map[string]string{}, }, } srv, err := dns.Start(srvOpts) if err != nil { return err } logrus.Infof("Started srv %+v (UDP %d, TCP %d)", srv, udpLocalPort, tcpLocalPort) for { time.Sleep(time.Hour) } } ================================================ FILE: cmd/limactl/delete.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/networks/reconcile" "github.com/lima-vm/lima/v2/pkg/store" ) func newDeleteCommand() *cobra.Command { deleteCommand := &cobra.Command{ Use: "delete INSTANCE [INSTANCE, ...]", Aliases: []string{"remove", "rm"}, Short: "Delete an instance of Lima", Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: deleteAction, ValidArgsFunction: deleteBashComplete, GroupID: basicCommand, } deleteCommand.Flags().BoolP("force", "f", false, "Forcibly kill the processes") return deleteCommand } func deleteAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() force, err := cmd.Flags().GetBool("force") if err != nil { return err } for _, instName := range args { inst, err := store.Inspect(ctx, instName) if err != nil { if errors.Is(err, os.ErrNotExist) { logrus.Warnf("Ignoring non-existent instance %q", instName) continue } return err } if err := instance.Delete(cmd.Context(), inst, force); err != nil { return fmt.Errorf("failed to delete instance %q: %w", instName, err) } if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { logrus.WithError(err).Warnf("Failed to check if the autostart entry for instance %q is registered", instName) } else if registered { if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil { logrus.WithError(err).Warnf("Failed to unregister the autostart entry for instance %q", instName) } else { logrus.Infof("The autostart entry for instance %q has been unregistered", instName) } } logrus.Infof("Deleted %q (%q)", instName, inst.Dir) } return reconcile.Reconcile(cmd.Context(), "") } func deleteBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/disk.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "text/tabwriter" contfs "github.com/containerd/continuity/fs" "github.com/docker/go-units" "github.com/lima-vm/go-qcow2reader" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/store" ) func newDiskCommand() *cobra.Command { diskCommand := &cobra.Command{ Use: "disk", Short: "Lima disk management", Example: ` Create a disk: $ limactl disk create DISK --size SIZE [--format qcow2] List existing disks: $ limactl disk ls Delete a disk: $ limactl disk delete DISK Resize a disk: $ limactl disk resize DISK --size SIZE`, SilenceUsage: true, SilenceErrors: true, GroupID: advancedCommand, } diskCommand.AddCommand( newDiskCreateCommand(), newDiskListCommand(), newDiskDeleteCommand(), newDiskUnlockCommand(), newDiskResizeCommand(), newDiskImportCommand(), ) return diskCommand } func newDiskCreateCommand() *cobra.Command { diskCreateCommand := &cobra.Command{ Use: "create DISK", Example: ` To create a new disk: $ limactl disk create DISK --size SIZE [--format qcow2] `, Short: "Create a Lima disk", Args: WrapArgsError(cobra.ExactArgs(1)), RunE: diskCreateAction, } diskCreateCommand.Flags().String("size", "", "Configure the disk size") _ = diskCreateCommand.MarkFlagRequired("size") diskCreateCommand.Flags().String("format", "qcow2", "Specify the disk format") return diskCreateCommand } func diskCreateAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() size, err := cmd.Flags().GetString("size") if err != nil { return err } format, err := cmd.Flags().GetString("format") if err != nil { return err } diskSize, err := units.RAMInBytes(size) if err != nil { return err } switch format { case "qcow2", "raw": default: return fmt.Errorf(`disk format %q not supported, use "qcow2" or "raw" instead`, format) } // only exactly one arg is allowed name := args[0] diskDir, err := store.DiskDir(name) if err != nil { return err } if _, err := os.Stat(diskDir); !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("disk %q already exists (%q)", name, diskDir) } logrus.Infof("Creating %s disk %q with size %s", format, name, units.BytesSize(float64(diskSize))) if err := os.MkdirAll(diskDir, 0o700); err != nil { return err } // qemu may not be available, use it only if needed. dataDisk := filepath.Join(diskDir, filenames.DataDisk) diskUtil := proxyimgutil.NewDiskUtil(ctx) err = diskUtil.CreateDisk(ctx, dataDisk, diskSize) if err != nil { rerr := os.RemoveAll(diskDir) if rerr != nil { err = errors.Join(err, fmt.Errorf("failed to remove a directory %q: %w", diskDir, rerr)) } return fmt.Errorf("failed to create %s disk in %q: %w", format, diskDir, err) } return nil } func newDiskListCommand() *cobra.Command { diskListCommand := &cobra.Command{ Use: "list", Example: ` To list existing disks: $ limactl disk list `, Short: "List existing Lima disks", Aliases: []string{"ls"}, Args: WrapArgsError(cobra.ArbitraryArgs), RunE: diskListAction, } diskListCommand.Flags().Bool("json", false, "JSONify output") return diskListCommand } func nameMatches(nameName string, names []string) []string { matches := []string{} for _, name := range names { if name == nameName { matches = append(matches, name) } } return matches } func diskListAction(cmd *cobra.Command, args []string) error { jsonFormat, err := cmd.Flags().GetBool("json") if err != nil { return err } allDisks, err := store.Disks() if err != nil { return err } disks := []string{} if len(args) > 0 { for _, arg := range args { matches := nameMatches(arg, allDisks) if len(matches) > 0 { disks = append(disks, matches...) } else { logrus.Warnf("No disk matching %v found.", arg) } } } else { disks = allDisks } if jsonFormat { for _, diskName := range disks { disk, err := store.InspectDisk(diskName, nil) if err != nil { logrus.WithError(err).Errorf("disk %q does not exist?", diskName) continue } j, err := json.Marshal(disk) if err != nil { return err } fmt.Fprintln(cmd.OutOrStdout(), string(j)) } return nil } w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) fmt.Fprintln(w, "NAME\tSIZE\tFORMAT\tDIR\tIN-USE-BY") if len(disks) == 0 { logrus.Warn("No disk found. Run `limactl disk create DISK --size SIZE` to create a disk.") } for _, diskName := range disks { disk, err := store.InspectDisk(diskName, nil) if err != nil { logrus.WithError(err).Errorf("disk %q does not exist?", diskName) continue } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", disk.Name, units.BytesSize(float64(disk.Size)), disk.Format, disk.Dir, disk.Instance) } return w.Flush() } func newDiskDeleteCommand() *cobra.Command { diskDeleteCommand := &cobra.Command{ Use: "delete DISK [DISK, ...]", Example: ` To delete a disk: $ limactl disk delete DISK To delete multiple disks: $ limactl disk delete DISK1 DISK2 ... `, Aliases: []string{"remove", "rm"}, Short: "Delete one or more Lima disks", Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: diskDeleteAction, ValidArgsFunction: diskBashComplete, } diskDeleteCommand.Flags().BoolP("force", "f", false, "Force delete") return diskDeleteCommand } func diskDeleteAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() force, err := cmd.Flags().GetBool("force") if err != nil { return err } instNames, err := store.Instances() if err != nil { return err } var instances []*limatype.Instance for _, instName := range instNames { inst, err := store.Inspect(ctx, instName) if err != nil { continue } instances = append(instances, inst) } for _, diskName := range args { disk, err := store.InspectDisk(diskName, nil) if err != nil { if errors.Is(err, fs.ErrNotExist) { logrus.Warnf("Ignoring non-existent disk %q", diskName) continue } return err } if !force { if disk.Instance != "" { return fmt.Errorf("cannot delete disk %q in use by instance %q", disk.Name, disk.Instance) } var refInstances []string for _, inst := range instances { for _, d := range inst.AdditionalDisks { if d.Name == diskName { refInstances = append(refInstances, inst.Name) } } } if len(refInstances) > 0 { logrus.Warnf("Skipping deleting disk %q, disk is referenced by one or more non-running instances: %q", diskName, refInstances) logrus.Warnf("To delete anyway, run %q", forceDeleteCommand(diskName)) continue } } if err := deleteDisk(disk); err != nil { return fmt.Errorf("failed to delete disk %q: %w", diskName, err) } logrus.Infof("Deleted %q (%q)", diskName, disk.Dir) } return nil } func deleteDisk(disk *store.Disk) error { if err := os.RemoveAll(disk.Dir); err != nil { return fmt.Errorf("failed to remove %q: %w", disk.Dir, err) } return nil } func forceDeleteCommand(diskName string) string { return fmt.Sprintf("limactl disk delete --force %v", diskName) } func newDiskUnlockCommand() *cobra.Command { diskUnlockCommand := &cobra.Command{ Use: "unlock DISK [DISK, ...]", Example: ` Emergency recovery! If an instance is force stopped, it may leave a disk locked while not actually using it. To unlock a disk: $ limactl disk unlock DISK To unlock multiple disks: $ limactl disk unlock DISK1 DISK2 ... `, Short: "Unlock one or more Lima disks", Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: diskUnlockAction, ValidArgsFunction: diskBashComplete, } return diskUnlockCommand } func diskUnlockAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() for _, diskName := range args { disk, err := store.InspectDisk(diskName, nil) if err != nil { if errors.Is(err, fs.ErrNotExist) { logrus.Warnf("Ignoring non-existent disk %q", diskName) continue } return err } if disk.Instance == "" { logrus.Warnf("Ignoring unlocked disk %q", diskName) continue } // if store.Inspect throws an error, the instance does not exist, and it is safe to unlock inst, err := store.Inspect(ctx, disk.Instance) if err == nil { if len(inst.Errors) > 0 { logrus.Warnf("Cannot unlock disk %q, attached instance %q has errors: %+v", diskName, disk.Instance, inst.Errors) continue } if inst.Status == limatype.StatusRunning { logrus.Warnf("Cannot unlock disk %q used by running instance %q", diskName, disk.Instance) continue } } if err := disk.Unlock(); err != nil { return fmt.Errorf("failed to unlock disk %q: %w", diskName, err) } logrus.Infof("Unlocked disk %q (%q)", diskName, disk.Dir) } return nil } func newDiskResizeCommand() *cobra.Command { diskResizeCommand := &cobra.Command{ Use: "resize DISK", Example: ` Resize a disk: $ limactl disk resize DISK --size SIZE`, Short: "Resize existing Lima disk", Args: WrapArgsError(cobra.ExactArgs(1)), RunE: diskResizeAction, ValidArgsFunction: diskBashComplete, } diskResizeCommand.Flags().String("size", "", "Disk size") _ = diskResizeCommand.MarkFlagRequired("size") return diskResizeCommand } func diskResizeAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() size, err := cmd.Flags().GetString("size") if err != nil { return err } diskSize, err := units.RAMInBytes(size) if err != nil { return err } diskName := args[0] disk, err := store.InspectDisk(diskName, nil) if err != nil { if errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("disk %q does not exists", diskName) } return err } // Shrinking can cause a disk failure if diskSize < disk.Size { return fmt.Errorf("specified size %q is less than the current disk size %q. Disk shrinking is currently unavailable", units.BytesSize(float64(diskSize)), units.BytesSize(float64(disk.Size))) } if disk.Instance != "" { inst, err := store.Inspect(ctx, disk.Instance) if err == nil { if inst.Status == limatype.StatusRunning { return fmt.Errorf("cannot resize disk %q used by running instance %q. Please stop the VM instance", diskName, disk.Instance) } } } // qemu may not be available, use it only if needed. dataDisk := filepath.Join(disk.Dir, filenames.DataDisk) diskUtil := proxyimgutil.NewDiskUtil(ctx) err = diskUtil.ResizeDisk(ctx, dataDisk, diskSize) if err != nil { return fmt.Errorf("failed to resize disk %q: %w", diskName, err) } logrus.Infof("Resized disk %q (%q)", diskName, disk.Dir) return nil } func diskBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteDiskNames(cmd) } func newDiskImportCommand() *cobra.Command { diskImportCommand := &cobra.Command{ Use: "import DISK FILE", Example: ` Import a disk: $ limactl disk import DISK DISKPATH `, Short: "Import an existing disk to Lima", Args: WrapArgsError(cobra.ExactArgs(2)), RunE: diskImportAction, ValidArgsFunction: diskBashComplete, } return diskImportCommand } func diskImportAction(_ *cobra.Command, args []string) error { diskName := args[0] fName := args[1] diskDir, err := store.DiskDir(diskName) if err != nil { return err } if _, err := os.Stat(diskDir); !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("disk %q already exists (%q)", diskName, diskDir) } f, err := os.Open(fName) if err != nil { return err } defer f.Close() img, err := qcow2reader.Open(f) if err != nil { return err } diskSize := img.Size() format := img.Type() switch format { case "qcow2", "raw": default: return fmt.Errorf(`disk format %q not supported, use "qcow2" or "raw" instead`, format) } if err := os.MkdirAll(diskDir, 0o755); err != nil { return err } if err := contfs.CopyFile(filepath.Join(diskDir, filenames.DataDisk), fName); err != nil { return nil } logrus.Infof("Imported %s with size %s", diskName, units.BytesSize(float64(diskSize))) return nil } ================================================ FILE: cmd/limactl/edit.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "bytes" "errors" "fmt" "os" "path/filepath" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/editutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/networks/reconcile" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/uiutil" "github.com/lima-vm/lima/v2/pkg/yqutil" ) func newEditCommand() *cobra.Command { editCommand := &cobra.Command{ Use: "edit INSTANCE|FILE.yaml", Short: "Edit an instance of Lima or a template", Args: WrapArgsError(cobra.MaximumNArgs(1)), RunE: editAction, ValidArgsFunction: editBashComplete, GroupID: basicCommand, } editCommand.Flags().Bool("start", false, "Start the instance after editing") editflags.RegisterEdit(editCommand, "") return editCommand } func editAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() var arg string if len(args) > 0 { arg = args[0] } var filePath string var err error var inst *limatype.Instance if arg == "" { arg = DefaultInstanceName } if err := dirnames.ValidateInstName(arg); err == nil { inst, err = store.Inspect(ctx, arg) if err != nil { if errors.Is(err, os.ErrNotExist) { return fmt.Errorf("instance %q not found", arg) } return err } if inst.Status == limatype.StatusRunning { return errors.New("cannot edit a running instance") } filePath = filepath.Join(inst.Dir, filenames.LimaYAML) } else { // absolute path is required for `limayaml.Validate` filePath, err = filepath.Abs(arg) if err != nil { return err } } yContent, err := os.ReadFile(filePath) if err != nil { return err } flags := cmd.Flags() tty, err := flags.GetBool("tty") if err != nil { return err } yqExprs, err := editflags.YQExpressions(flags, false) if err != nil { return err } var yBytes []byte if len(yqExprs) > 0 { yq := yqutil.Join(yqExprs) yBytes, err = yqutil.EvaluateExpression(yq, yContent) if err != nil { return err } } else if tty { var hdr string if inst != nil { hdr = fmt.Sprintf("# Please edit the following configuration for Lima instance %q\n", inst.Name) } else { hdr = fmt.Sprintf("# Please edit the following configuration %q\n", filePath) } hdr += "# and an empty file will abort the edit.\n" hdr += "\n" hdr += editutil.GenerateEditorWarningHeader() yBytes, err = editutil.OpenEditor(ctx, yContent, hdr) if err != nil { return err } } if len(yBytes) == 0 { logrus.Info("Aborting, as requested by saving the file with empty content") return nil } if bytes.Equal(yBytes, yContent) { logrus.Info("Aborting, no changes made to the instance") return nil } y, err := limayaml.LoadWithWarnings(ctx, yBytes, filePath) if err != nil { return err } if err := driverutil.ResolveVMType(ctx, y, filePath); err != nil { return fmt.Errorf("failed to resolve vm for %q: %w", filePath, err) } if err := limayaml.Validate(y, true); err != nil { return saveRejectedYAML(yBytes, err) } if err := limayaml.ValidateAgainstLatestConfig(ctx, yBytes, yContent); err != nil { return saveRejectedYAML(yBytes, err) } if err := os.WriteFile(filePath, yBytes, 0o644); err != nil { return err } if inst != nil { logrus.Infof("Instance %q configuration edited", inst.Name) } if inst == nil { // edited a limayaml file directly return nil } start, err := flags.GetBool("start") if err != nil { return err } if tty && !flags.Changed("start") { start, err = askWhetherToStart(cmd) if err != nil { return err } } if !start { return nil } // Network reconciliation will be performed by the process launched by the autostart manager if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) } else if !registered { err = reconcile.Reconcile(ctx, inst.Name) if err != nil { return err } } // store.Inspect() syncs values between inst.YAML and the store. // This call applies the validated template to the store. inst, err = store.Inspect(ctx, inst.Name) if err != nil { return err } return instance.Start(ctx, inst, false, false) } func askWhetherToStart(cmd *cobra.Command) (bool, error) { isTTY := uiutil.InputIsTTY(cmd.InOrStdin()) if isTTY { message := "Do you want to start the instance now? " return uiutil.Confirm(message, true) } return false, nil } func editBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } // saveRejectedYAML writes the rejected config and returns an error. func saveRejectedYAML(y []byte, origErr error) error { rejectedYAML := "lima.REJECTED.yaml" if writeErr := os.WriteFile(rejectedYAML, y, 0o644); writeErr != nil { return fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %w", rejectedYAML, errors.Join(writeErr, origErr)) } // TODO: may need to support editing the rejected YAML return fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, origErr) } ================================================ FILE: cmd/limactl/editflags/editflags.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package editflags import ( "errors" "fmt" "math/bits" "runtime" "slices" "strconv" "strings" "github.com/pbnjay/memory" "github.com/sirupsen/logrus" "github.com/spf13/cobra" flag "github.com/spf13/pflag" "github.com/lima-vm/lima/v2/pkg/localpathutil" "github.com/lima-vm/lima/v2/pkg/registry" ) // RegisterEdit registers flags related to in-place YAML modification, for `limactl edit`. func RegisterEdit(cmd *cobra.Command, commentPrefix string) { flags := cmd.Flags() flags.Int("cpus", 0, commentPrefix+"Number of CPUs") // Similar to colima's --cpu, but the flag name is slightly different (cpu vs cpus) _ = cmd.RegisterFlagCompletionFunc("cpus", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { var res []string for _, f := range completeCPUs(runtime.NumCPU()) { res = append(res, strconv.Itoa(f)) } return res, cobra.ShellCompDirectiveNoFileComp }) flags.IPSlice("dns", nil, commentPrefix+"Specify custom DNS (disable host resolver)") // colima-compatible flags.Float32("memory", 0, commentPrefix+"Memory in GiB") // colima-compatible _ = cmd.RegisterFlagCompletionFunc("memory", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { var res []string for _, f := range completeMemoryGiB(memory.TotalMemory()) { res = append(res, fmt.Sprintf("%.1f", f)) } return res, cobra.ShellCompDirectiveNoFileComp }) flags.StringSlice("mount", nil, commentPrefix+"Directories to mount, suffix ':w' for writable (Do not specify directories that overlap with the existing mounts)") // colima-compatible flags.StringSlice("mount-only", nil, commentPrefix+"Similar to --mount, but overrides the existing mounts") flags.Bool("mount-none", false, commentPrefix+"Remove all mounts") flags.String("mount-type", "", commentPrefix+"Mount type (reverse-sshfs, 9p, virtiofs)") // Similar to colima's --mount-type=(sshfs|9p|virtiofs), but "reverse-sshfs" is Lima is called "sshfs" in colima _ = cmd.RegisterFlagCompletionFunc("mount-type", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return []string{"reverse-sshfs", "9p", "virtiofs"}, cobra.ShellCompDirectiveNoFileComp }) flags.Bool("mount-writable", false, commentPrefix+"Make all mounts writable") flags.Bool("mount-inotify", false, commentPrefix+"Enable inotify for mounts") flags.StringSlice("network", nil, commentPrefix+"Additional networks, e.g., \"vzNAT\" or \"lima:shared\" to assign vmnet IP") _ = cmd.RegisterFlagCompletionFunc("network", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { // TODO: retrieve the lima:* network list from networks.yaml return []string{"lima:shared", "lima:bridged", "lima:host", "lima:user-v2", "vzNAT"}, cobra.ShellCompDirectiveNoFileComp }) flags.Bool("nested-virt", false, commentPrefix+"Enable nested virtualization") flags.Bool("rosetta", false, commentPrefix+"Enable Rosetta (for vz instances)") flags.StringArray("set", []string{}, commentPrefix+"Modify the template inplace, using yq syntax. Can be passed multiple times.") flags.Uint16("ssh-port", 0, commentPrefix+"SSH port (0 for random)") // colima-compatible _ = cmd.RegisterFlagCompletionFunc("ssh-port", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { // Until Lima v2.0, 60022 was the default SSH port for the instance named "default". return []string{"60022"}, cobra.ShellCompDirectiveNoFileComp }) // negative performance impact: https://gitlab.com/qemu-project/qemu/-/issues/334 flags.Bool("video", false, commentPrefix+"Enable video output (has negative performance impact for QEMU)") flags.Float32("disk", 0, commentPrefix+"Disk size in GiB") // colima-compatible _ = cmd.RegisterFlagCompletionFunc("disk", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return []string{"10", "30", "50", "100", "200"}, cobra.ShellCompDirectiveNoFileComp }) flags.String("vm-type", "", commentPrefix+"Virtual machine type") _ = cmd.RegisterFlagCompletionFunc("vm-type", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { var drivers []string for k := range registry.List() { drivers = append(drivers, k) } return drivers, cobra.ShellCompDirectiveNoFileComp }) } // RegisterCreate registers flags related to in-place YAML modification, for `limactl create`. func RegisterCreate(cmd *cobra.Command, commentPrefix string) { RegisterEdit(cmd, commentPrefix) flags := cmd.Flags() flags.String("arch", "", commentPrefix+"Machine architecture (x86_64, aarch64, riscv64, armv7l, s390x, ppc64le)") // colima-compatible _ = cmd.RegisterFlagCompletionFunc("arch", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return []string{"x86_64", "aarch64", "riscv64", "armv7l", "s390x", "ppc64le"}, cobra.ShellCompDirectiveNoFileComp }) flags.String("containerd", "", commentPrefix+"containerd mode (user, system, user+system, none)") _ = cmd.RegisterFlagCompletionFunc("containerd", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return []string{"user", "system", "user+system", "none"}, cobra.ShellCompDirectiveNoFileComp }) flags.Bool("plain", false, commentPrefix+"Plain mode. Disables mounts, port forwarding, containerd, etc.") flags.StringArray("port-forward", nil, commentPrefix+"Port forwards (host:guest), e.g., '8080:80' or '9090:9090,static=true' for static port-forwards") _ = cmd.RegisterFlagCompletionFunc("port-forward", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return []string{"8080:80", "3000:3000", "8080:80,static=true"}, cobra.ShellCompDirectiveNoFileComp }) } func defaultExprFunc(expr string) func(v *flag.Flag) ([]string, error) { return func(v *flag.Flag) ([]string, error) { return []string{fmt.Sprintf(expr, v.Value)}, nil } } func ParsePortForward(spec string) (hostPort, guestPort string, isStatic bool, err error) { parts := strings.Split(spec, ",") if len(parts) > 2 { return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST or HOST:GUEST,static=true", spec) } portParts := strings.Split(strings.TrimSpace(parts[0]), ":") if len(portParts) != 2 { return "", "", false, fmt.Errorf("invalid port forward format %q, expected HOST:GUEST", parts[0]) } hostPort = strings.TrimSpace(portParts[0]) guestPort = strings.TrimSpace(portParts[1]) if len(parts) == 2 { staticPart := strings.TrimSpace(parts[1]) if staticValue, ok := strings.CutPrefix(staticPart, "static="); ok { isStatic, err = strconv.ParseBool(staticValue) if err != nil { return "", "", false, fmt.Errorf("invalid value for static parameter: %q", staticValue) } } else { return "", "", false, fmt.Errorf("invalid parameter %q, expected 'static=' followed by a boolean value", staticPart) } } return hostPort, guestPort, isStatic, nil } func BuildPortForwardExpression(portForwards []string) (string, error) { if len(portForwards) == 0 { return "", nil } ports := make([]string, len(portForwards)) for i, spec := range portForwards { hostPort, guestPort, isStatic, err := ParsePortForward(spec) if err != nil { return "", err } ports[i] = fmt.Sprintf(`{"guestPort": %q, "hostPort": %q, "static": %v}`, guestPort, hostPort, isStatic) } expr := fmt.Sprintf(".portForwards += [%s]", strings.Join(ports, ",")) return expr, nil } func buildMountListExpression(ss []string) (string, error) { mounts := make([]string, len(ss)) for i, s := range ss { writable := strings.HasSuffix(s, ":w") loc, err := localpathutil.Expand(strings.TrimSuffix(s, ":w")) if err != nil { return "", err } mounts[i] = fmt.Sprintf(`{"location": %q, "mountPoint": %q, "writable": %v}`, loc, loc, writable) } expr := fmt.Sprintf("[%s]", strings.Join(mounts, ",")) return expr, nil } // YQExpressions returns YQ expressions. func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { type def struct { flagName string exprFunc func(*flag.Flag) ([]string, error) onlyValidForNewInstances bool experimental bool } d := defaultExprFunc defs := []def{ { "cpus", func(_ *flag.Flag) ([]string, error) { numCpus, err := flags.GetInt("cpus") if err != nil { return nil, err } if numCpus < 0 { return nil, errors.New("invalid value for number of cpus, must be >= 0") } return []string{fmt.Sprintf(".cpus = %d", numCpus)}, nil }, false, false, }, { "dns", func(_ *flag.Flag) ([]string, error) { ipSlice, err := flags.GetIPSlice("dns") if err != nil { return nil, err } ips := make([]string, len(ipSlice)) for i, ip := range ipSlice { ips[i] = `"` + ip.String() + `"` } expr := fmt.Sprintf(".dns += [%s] | .dns |= unique | .hostResolver.enabled=false", strings.Join(ips, ",")) logrus.Warnf("Disabling HostResolver, as custom DNS addresses (%v) are specified", ipSlice) return []string{expr}, nil }, false, false, }, {"memory", d(".memory = \"%sGiB\""), false, false}, { "mount", func(_ *flag.Flag) ([]string, error) { ss, err := flags.GetStringSlice("mount") slices.Reverse(ss) if err != nil { return nil, err } mountListExpr, err := buildMountListExpression(ss) if err != nil { return nil, err } // mount options take precedence over template settings expr := fmt.Sprintf(".mounts = %s + .mounts", mountListExpr) mountOnly, err := flags.GetStringSlice("mount-only") if err != nil { return nil, err } if len(mountOnly) > 0 { return nil, errors.New("flag `--mount` conflicts with `--mount-only`") } return []string{expr}, nil }, false, false, }, { "mount-only", func(_ *flag.Flag) ([]string, error) { ss, err := flags.GetStringSlice("mount-only") if err != nil { return nil, err } mountListExpr, err := buildMountListExpression(ss) if err != nil { return nil, err } expr := `.mounts = ` + mountListExpr return []string{expr}, nil }, false, false, }, { "mount-none", func(_ *flag.Flag) ([]string, error) { incompatibleFlagNames := []string{"mount", "mount-only"} for _, name := range incompatibleFlagNames { ss, err := flags.GetStringSlice(name) if err != nil { return nil, err } if len(ss) > 0 { return nil, errors.New("flag `--mount-none` conflicts with `" + name + "`") } } return []string{".mounts = null"}, nil }, false, false, }, {"mount-type", d(".mountType = %q"), false, false}, {"vm-type", d(".vmType = %q"), false, false}, {"mount-inotify", d(".mountInotify = %s"), false, true}, {"mount-writable", d(".mounts[].writable = %s"), false, false}, { "network", func(_ *flag.Flag) ([]string, error) { ss, err := flags.GetStringSlice("network") if err != nil { return nil, err } networks := make([]string, len(ss)) for i, s := range ss { // CLI syntax is still experimental (YAML syntax is out of experimental) switch { case s == "vzNAT": networks[i] = `{"vzNAT": true}` case strings.HasPrefix(s, "lima:"): network := strings.TrimPrefix(s, "lima:") networks[i] = fmt.Sprintf(`{"lima": %q}`, network) default: return nil, fmt.Errorf(`network name must be "vzNAT" or "lima:*", got %q`, s) } } expr := fmt.Sprintf(`.networks += [%s] | .networks |= unique_by(.lima)`, strings.Join(networks, ",")) return []string{expr}, nil }, false, false, }, {"nested-virt", d(".nestedVirtualization = %s"), false, false}, { "rosetta", func(_ *flag.Flag) ([]string, error) { b, err := flags.GetBool("rosetta") if err != nil { return nil, err } return []string{fmt.Sprintf(".vmOpts.vz.rosetta.enabled = %v | .vmOpts.vz.rosetta.binfmt = %v", b, b)}, nil }, false, false, }, {"set", func(v *flag.Flag) ([]string, error) { return v.Value.(flag.SliceValue).GetSlice(), nil }, false, false}, { "video", func(_ *flag.Flag) ([]string, error) { b, err := flags.GetBool("video") if err != nil { return nil, err } if b { return []string{".video.display = \"default\""}, nil } return []string{".video.display = \"none\""}, nil }, false, false, }, {"ssh-port", d(".ssh.localPort = %s"), false, false}, {"arch", d(".arch = %q"), true, false}, { "containerd", func(_ *flag.Flag) ([]string, error) { s, err := flags.GetString("containerd") if err != nil { return nil, err } switch s { case "user": return []string{`.containerd.user = true | .containerd.system = false`}, nil case "system": return []string{`.containerd.user = false | .containerd.system = true`}, nil case "user+system", "system+user": return []string{`.containerd.user = true | .containerd.system = true`}, nil case "none": return []string{`.containerd.user = false | .containerd.system = false`}, nil default: return nil, fmt.Errorf(`expected one of ["user", "system", "user+system", "none"], got %q`, s) } }, true, false, }, {"disk", d(".disk= \"%sGiB\""), false, false}, {"plain", d(".plain = %s"), true, false}, { "port-forward", func(_ *flag.Flag) ([]string, error) { ss, err := flags.GetStringArray("port-forward") if err != nil { return nil, err } value, err := BuildPortForwardExpression(ss) if err != nil { return nil, err } return []string{value}, nil }, false, false, }, } var exprs []string for _, def := range defs { v := flags.Lookup(def.flagName) if v != nil && v.Changed { if def.experimental { logrus.Warnf("`--%s` is experimental", def.flagName) } if def.onlyValidForNewInstances && !newInstance { logrus.Warnf("`--%s` is not applicable to an existing instance (Hint: create a new instance with `limactl create --%s=%s --name=NAME`)", def.flagName, def.flagName, v.Value.String()) continue } newExprs, err := def.exprFunc(v) if err != nil { return exprs, fmt.Errorf("error while processing flag %q: %w", def.flagName, err) } exprs = append(exprs, newExprs...) } } return exprs, nil } func isPowerOfTwo(x int) bool { return bits.OnesCount(uint(x)) == 1 } func completeCPUs(hostCPUs int) []int { var res []int for i := 1; i <= hostCPUs; i *= 2 { res = append(res, i) } if !isPowerOfTwo(hostCPUs) { res = append(res, hostCPUs) } return res } func completeMemoryGiB(hostMemory uint64) []float32 { hostMemoryHalfGiB := int(hostMemory / 2 / 1024 / 1024 / 1024) var res []float32 if hostMemoryHalfGiB < 1 { res = append(res, 0.5) } for i := 1; i <= hostMemoryHalfGiB; i *= 2 { res = append(res, float32(i)) } if hostMemoryHalfGiB > 1 && !isPowerOfTwo(hostMemoryHalfGiB) { res = append(res, float32(hostMemoryHalfGiB)) } return res } ================================================ FILE: cmd/limactl/editflags/editflags_test.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package editflags import ( "strings" "testing" "github.com/spf13/cobra" "gotest.tools/v3/assert" "github.com/lima-vm/lima/v2/pkg/localpathutil" ) func TestCompleteCPUs(t *testing.T) { assert.DeepEqual(t, []int{1}, completeCPUs(1)) assert.DeepEqual(t, []int{1, 2}, completeCPUs(2)) assert.DeepEqual(t, []int{1, 2, 4, 8}, completeCPUs(8)) assert.DeepEqual(t, []int{1, 2, 4, 8, 16, 20}, completeCPUs(20)) } func TestCompleteMemoryGiB(t *testing.T) { assert.DeepEqual(t, []float32{0.5}, completeMemoryGiB(1<<30)) assert.DeepEqual(t, []float32{1}, completeMemoryGiB(2<<30)) assert.DeepEqual(t, []float32{1, 2}, completeMemoryGiB(4<<30)) assert.DeepEqual(t, []float32{1, 2, 4}, completeMemoryGiB(8<<30)) assert.DeepEqual(t, []float32{1, 2, 4, 8, 10}, completeMemoryGiB(20<<30)) } func TestBuildPortForwardExpression(t *testing.T) { tests := []struct { name string portForwards []string expected string expectError bool }{ { name: "empty port forwards", portForwards: []string{}, expected: "", }, { name: "single dynamic port forward", portForwards: []string{"8080:80"}, expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": false}]`, }, { name: "single static port forward", portForwards: []string{"8080:80,static=true"}, expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": true}]`, }, { name: "multiple mixed port forwards", portForwards: []string{"8080:80", "2222:22,static=true", "3000:3000"}, expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": false},{"guestPort": "22", "hostPort": "2222", "static": true},{"guestPort": "3000", "hostPort": "3000", "static": false}]`, }, { name: "invalid format - missing colon", portForwards: []string{"8080"}, expectError: true, }, { name: "invalid format - too many colons", portForwards: []string{"8080:80:extra"}, expectError: true, }, { name: "invalid static parameter", portForwards: []string{"8080:80,invalid=true"}, expectError: true, }, { name: "too many parameters", portForwards: []string{"8080:80,static=true,extra=value"}, expectError: true, }, { name: "whitespace handling", portForwards: []string{" 8080 : 80 , static=true "}, expected: `.portForwards += [{"guestPort": "80", "hostPort": "8080", "static": true}]`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := BuildPortForwardExpression(tt.portForwards) if tt.expectError { assert.Check(t, err != nil) } else { assert.NilError(t, err) assert.Equal(t, tt.expected, result) } }) } } func TestParsePortForward(t *testing.T) { tests := []struct { name string spec string hostPort string guestPort string isStatic bool expectError bool }{ { name: "dynamic port forward", spec: "8080:80", hostPort: "8080", guestPort: "80", isStatic: false, }, { name: "static port forward", spec: "8080:80,static=true", hostPort: "8080", guestPort: "80", isStatic: true, }, { name: "whitespace handling", spec: " 8080 : 80 , static=true ", hostPort: "8080", guestPort: "80", isStatic: true, }, { name: "invalid format - missing colon", spec: "8080", expectError: true, }, { name: "invalid format - too many colons", spec: "8080:80:extra", expectError: true, }, { name: "invalid parameter", spec: "8080:80,invalid=true", expectError: true, }, { name: "too many parameters", spec: "8080:80,static=true,extra=value", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hostPort, guestPort, isStatic, err := ParsePortForward(tt.spec) if tt.expectError { assert.Check(t, err != nil) } else { assert.NilError(t, err) assert.Equal(t, tt.hostPort, hostPort) assert.Equal(t, tt.guestPort, guestPort) assert.Equal(t, tt.isStatic, isStatic) } }) } } func TestYQExpressions(t *testing.T) { expand := func(s string) string { s, err := localpathutil.Expand(s) assert.NilError(t, err) // `D:\foo` -> `D:\\foo` (appears in YAML) s = strings.ReplaceAll(s, "\\", "\\\\") return s } tests := []struct { name string args []string newInstance bool expected []string expectError string }{ { name: "mount", args: []string{"--mount", "/foo", "--mount", "./bar:w"}, newInstance: false, expected: []string{`.mounts = [{"location": "` + expand("./bar") + `", "mountPoint": "` + expand("./bar") + `", "writable": true},{"location": "` + expand("/foo") + `", "mountPoint": "` + expand("/foo") + `", "writable": false}] + .mounts`}, }, { name: "mount-only", args: []string{"--mount-only", "/foo", "--mount-only", "/bar:w"}, newInstance: false, expected: []string{`.mounts = [{"location": "` + expand("/foo") + `", "mountPoint": "` + expand("/foo") + `", "writable": false},{"location": "` + expand("/bar") + `", "mountPoint": "` + expand("/bar") + `", "writable": true}]`}, }, { name: "mixture of mount and mount-only", args: []string{"--mount", "/foo", "--mount-only", "/bar:w"}, newInstance: false, expectError: "flag `--mount` conflicts with `--mount-only`", }, { name: "dns", args: []string{"--dns", "8.8.8.8", "--dns", "8.8.4.4", "--dns", "1.1.1.1"}, newInstance: false, expected: []string{`.dns += ["8.8.8.8","8.8.4.4","1.1.1.1"] | .dns |= unique | .hostResolver.enabled=false`}, }, { name: "network vzNAT", args: []string{"--network", "vzNAT"}, newInstance: true, expected: []string{`.networks += [{"vzNAT": true}] | .networks |= unique_by(.lima)`}, }, { name: "network lima:shared", args: []string{"--network", "lima:shared"}, newInstance: true, expected: []string{`.networks += [{"lima": "shared"}] | .networks |= unique_by(.lima)`}, }, { name: "multiple networks", args: []string{"--network", "vzNAT", "--network", "lima:shared", "--network", "lima:bridged"}, newInstance: true, expected: []string{`.networks += [{"vzNAT": true},{"lima": "shared"},{"lima": "bridged"}] | .networks |= unique_by(.lima)`}, }, { name: "nested-virt", args: []string{"--nested-virt"}, newInstance: false, expected: []string{`.nestedVirtualization = true`}, }, { name: "invalid network", args: []string{"--network", "invalid"}, newInstance: true, expectError: `network name must be "vzNAT" or "lima:*", got "invalid"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &cobra.Command{} RegisterEdit(cmd, "") assert.NilError(t, cmd.ParseFlags(tt.args)) expr, err := YQExpressions(cmd.Flags(), tt.newInstance) if tt.expectError != "" { assert.ErrorContains(t, err, tt.expectError) } else { assert.NilError(t, err) assert.DeepEqual(t, tt.expected, expr) } }) } } ================================================ FILE: cmd/limactl/factory-reset.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "os" "path/filepath" "strings" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/cidata" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/store" ) func newFactoryResetCommand() *cobra.Command { resetCommand := &cobra.Command{ Use: "factory-reset INSTANCE", Short: "Factory reset an instance of Lima", Args: WrapArgsError(cobra.MaximumNArgs(1)), RunE: factoryResetAction, ValidArgsFunction: factoryResetBashComplete, GroupID: advancedCommand, } return resetCommand } func factoryResetAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() instName := DefaultInstanceName if len(args) > 0 { instName = args[0] } inst, err := store.Inspect(ctx, instName) if err != nil { if errors.Is(err, os.ErrNotExist) { logrus.Infof("Instance %q not found", instName) return nil } return err } if inst.Protected { return errors.New("instance is protected to prohibit accidental factory-reset (Hint: use `limactl unprotect`)") } instance.StopForcibly(inst) fi, err := os.ReadDir(inst.Dir) if err != nil { return err } retain := map[string]struct{}{ filenames.LimaVersion: {}, filenames.Protected: {}, filenames.VzIdentifier: {}, } for _, f := range fi { path := filepath.Join(inst.Dir, f.Name()) if _, ok := retain[f.Name()]; !ok && !strings.HasSuffix(path, ".yaml") && !strings.HasSuffix(path, ".yml") { logrus.Infof("Removing %q", path) if err := os.Remove(path); err != nil { logrus.Error(err) } } } // Regenerate the cloud-config.yaml, to reflect any changes to the global _config if err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil { logrus.Error(err) } logrus.Infof("Instance %q has been factory reset", instName) return nil } func factoryResetBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/gendoc.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "bytes" "fmt" "io/fs" "os" "path/filepath" "strings" "github.com/cpuguy83/go-md2man/v2/md2man" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" ) func newGenDocCommand() *cobra.Command { genmanCommand := &cobra.Command{ Use: "generate-doc DIR", Short: "Generate cli-reference pages", Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: gendocAction, Hidden: true, } genmanCommand.Flags().String("type", "man", "Output type (man, docsy)") genmanCommand.Flags().String("output", "", "Output directory") genmanCommand.Flags().String("prefix", "", "Install prefix") return genmanCommand } func gendocAction(cmd *cobra.Command, args []string) error { output, err := cmd.Flags().GetString("output") if err != nil { return err } output, err = filepath.Abs(output) if err != nil { return err } prefix, err := cmd.Flags().GetString("prefix") if err != nil { return err } outputType, err := cmd.Flags().GetString("type") if err != nil { return err } homeDir, err := os.UserHomeDir() if err != nil { return err } dir := args[0] switch outputType { case "man": if err := genMan(cmd, dir); err != nil { return err } case "docsy": if err := genDocsy(cmd, dir); err != nil { return err } } if output != "" && prefix != "" { if err := replaceAll(dir, output, prefix); err != nil { return err } } return replaceAll(dir, homeDir, "~") } func genMan(cmd *cobra.Command, dir string) error { logrus.Infof("Generating man %q", dir) // lima(1) filePath := filepath.Join(dir, "lima.1") md := "LIMA 1\n======" + ` # NAME lima - ` + cmd.Root().Short + ` # SYNOPSIS **lima** [_COMMAND_...] # DESCRIPTION lima is an alias for "limactl shell default". The instance name ("default") can be changed by specifying $LIMA_INSTANCE. The shell and initial workdir inside the instance can be specified via $LIMA_SHELL and $LIMA_WORKDIR. # SEE ALSO **limactl**(1) ` out := md2man.Render([]byte(md)) if err := os.WriteFile(filePath, out, 0o644); err != nil { return err } // limactl(1) header := &doc.GenManHeader{ Title: "LIMACTL", Section: "1", } return doc.GenManTree(cmd.Root(), header, dir) } func escapeMarkdown(text string) string { lines := strings.Split(text, "\n") for i := range lines { // Need to escape backticks first, before adding more for c := range strings.SplitSeq("\\`*_[]()#+-.|", "") { lines[i] = strings.ReplaceAll(lines[i], c, "\\"+c) } if i < len(lines)-1 { if lines[i] != "" && lines[i+1] != "" { lines[i] += " " // line break } } } return strings.Join(lines, "\n") } func genDocsy(cmd *cobra.Command, dir string) error { for _, c := range cmd.Root().Commands() { c.Long = escapeMarkdown(c.Long) } return doc.GenMarkdownTreeCustom(cmd.Root(), dir, func(s string) string { // Replace limactl_completion_bash to completion bash for docsy title name := filepath.Base(s) name = strings.ReplaceAll(name, "limactl_", "") name = strings.ReplaceAll(name, "_", " ") name = strings.TrimSuffix(name, filepath.Ext(name)) return fmt.Sprintf(`--- title: %s weight: 3 --- `, name) }, func(s string) string { // Use ../ for move one folder up for docsy return "../" + strings.TrimSuffix(s, filepath.Ext(s)) }) } // replaceAll replaces all occurrences of text with replacement, for all files in dir. func replaceAll(dir, text, replacement string) error { logrus.Infof("Replacing %q with %q", text, replacement) return filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } if path == dir { return nil } if info.IsDir() { return filepath.SkipDir } in, err := os.ReadFile(path) if err != nil { return err } out := bytes.ReplaceAll(in, []byte(text), []byte(replacement)) err = os.WriteFile(path, out, 0o644) if err != nil { return err } return nil }) } ================================================ FILE: cmd/limactl/genschema.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "encoding/json" "errors" "fmt" "os" "github.com/invopop/jsonschema" "github.com/sirupsen/logrus" "github.com/spf13/cobra" orderedmap "github.com/wk8/go-ordered-map/v2" "github.com/lima-vm/lima/v2/pkg/jsonschemautil" "github.com/lima-vm/lima/v2/pkg/limatype" ) func newGenSchemaCommand() *cobra.Command { genschemaCommand := &cobra.Command{ Use: "generate-jsonschema", Short: "Generate json-schema document", Args: WrapArgsError(cobra.ArbitraryArgs), RunE: genschemaAction, Hidden: true, } genschemaCommand.Flags().String("schemafile", "", "Output file") return genschemaCommand } func toAny(args []string) []any { result := []any{nil} for _, arg := range args { result = append(result, arg) } return result } func getProp(props *orderedmap.OrderedMap[string, *jsonschema.Schema], key string) *jsonschema.Schema { value, ok := props.Get(key) if !ok { return nil } return value } func genschemaAction(cmd *cobra.Command, args []string) error { file, err := cmd.Flags().GetString("schemafile") if err != nil { return err } schema := jsonschema.Reflect(&limatype.LimaYAML{}) // allow Disk to be either string (name) or object (struct) schema.Definitions["Disk"].Type = "" // was: "object" schema.Definitions["Disk"].OneOf = []*jsonschema.Schema{ {Type: "string"}, {Type: "object"}, } // allow BaseTemplates to be either string (url) or array (array) schema.Definitions["BaseTemplates"].Type = "" // was: "array" schema.Definitions["BaseTemplates"].OneOf = []*jsonschema.Schema{ {Type: "string"}, {Type: "array"}, } // allow LocatorWithDigest to be either string (url) or object (struct) schema.Definitions["LocatorWithDigest"].Type = "" // was: "object" schema.Definitions["LocatorWithDigest"].OneOf = []*jsonschema.Schema{ {Type: "string"}, {Type: "object"}, } properties := schema.Definitions["LimaYAML"].Properties getProp(properties, "os").Enum = toAny(limatype.OSTypes) getProp(properties, "arch").Enum = toAny(limatype.ArchTypes) getProp(properties, "mountType").Enum = toAny(limatype.MountTypes) getProp(properties, "vmType").Enum = toAny(limatype.VMTypes) j, err := json.MarshalIndent(schema, "", " ") if err != nil { return err } if len(args) == 0 { _, err = fmt.Fprintln(cmd.OutOrStdout(), string(j)) return err } if file == "" { return errors.New("need --schemafile to validate") } err = os.WriteFile(file, j, 0o644) if err != nil { return err } for _, f := range args { err = jsonschemautil.Validate(file, f) if err != nil { return fmt.Errorf("%q: %w", f, err) } logrus.Infof("%q: OK", f) } return err } ================================================ FILE: cmd/limactl/guest-install.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "bytes" "compress/gzip" "context" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/cacheutil" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/usrlocal" ) func newGuestInstallCommand() *cobra.Command { guestInstallCommand := &cobra.Command{ Use: "guest-install INSTANCE", Short: "Install guest components", Args: WrapArgsError(cobra.MaximumNArgs(1)), RunE: guestInstallAction, ValidArgsFunction: cobra.NoFileCompletions, Hidden: true, } return guestInstallCommand } func runCmd(ctx context.Context, name string, flags []string, args ...string) error { cmd := exec.CommandContext(ctx, name, append(flags, args...)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logrus.Debugf("executing %v", cmd.Args) return cmd.Run() } func shell(ctx context.Context, name string, flags []string, args ...string) (string, error) { cmd := exec.CommandContext(ctx, name, append(flags, args...)...) out, err := cmd.Output() if err != nil { return "", err } out = bytes.TrimSuffix(out, []byte{'\n'}) return string(out), nil } func guestInstallAction(cmd *cobra.Command, args []string) error { instName := DefaultInstanceName if len(args) > 0 { instName = args[0] } inst, err := store.Inspect(cmd.Context(), instName) if err != nil { return err } if inst.Status == limatype.StatusStopped { return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName) } ctx := cmd.Context() sshExe := "ssh" sshConfig := filepath.Join(inst.Dir, filenames.SSHConfig) sshFlags := []string{"-F", sshConfig} scpExe := "scp" scpFlags := sshFlags hostname := fmt.Sprintf("lima-%s", inst.Name) prefix := *inst.Config.GuestInstallPrefix // lima-guestagent guestAgentBinary, err := usrlocal.GuestAgentBinary(*inst.Config.OS, *inst.Config.Arch) if err != nil { return err } guestAgentFilename := filepath.Base(guestAgentBinary) if filepath.Ext(guestAgentBinary) == ".gz" { compressedGuestAgent, err := os.Open(guestAgentBinary) if err != nil { return err } defer compressedGuestAgent.Close() tmpGuestAgent, err := os.CreateTemp("", "lima-guestagent-") if err != nil { return err } logrus.Debugf("Decompressing %s", guestAgentBinary) guestAgent, err := gzip.NewReader(compressedGuestAgent) if err != nil { return err } defer guestAgent.Close() _, err = io.Copy(tmpGuestAgent, guestAgent) if err != nil { return err } tmpGuestAgent.Close() guestAgentBinary = tmpGuestAgent.Name() defer os.RemoveAll(guestAgentBinary) guestAgentFilename = strings.TrimSuffix(guestAgentFilename, ".gz") } tmpname := "lima-guestagent" tmp, err := shell(ctx, sshExe, sshFlags, hostname, "mktemp", "-t", "lima-guestagent.XXXXXX") if err != nil { return err } bin := prefix + "/bin/lima-guestagent" logrus.Infof("Copying %q to %s:%s", guestAgentFilename, inst.Name, tmpname) scpArgs := []string{guestAgentBinary, hostname + ":" + tmp} if err := runCmd(ctx, scpExe, scpFlags, scpArgs...); err != nil { return nil } logrus.Infof("Installing %s to %s", tmpname, bin) sshArgs := []string{hostname, "sudo", "install", "-m", "755", tmp, bin} if err := runCmd(ctx, sshExe, sshFlags, sshArgs...); err != nil { return nil } _, _ = shell(ctx, sshExe, sshFlags, hostname, "rm", tmp) // nerdctl-full.tgz nerdctlFilename := cacheutil.NerdctlArchive(inst.Config) if nerdctlFilename != "" { nerdctlArchive, err := cacheutil.EnsureNerdctlArchiveCache(cmd.Context(), inst.Config, false) if err != nil { return err } tmpname := "nerdctl-full.tgz" tmp, err := shell(ctx, sshExe, sshFlags, hostname, "mktemp", "-t", "nerdctl-full.XXXXXX.tgz") if err != nil { return err } logrus.Infof("Copying %q to %s:%s", nerdctlFilename, inst.Name, tmpname) scpArgs := []string{nerdctlArchive, hostname + ":" + tmp} if err := runCmd(ctx, scpExe, scpFlags, scpArgs...); err != nil { return nil } logrus.Infof("Installing %s in %s", tmpname, prefix) sshArgs := []string{hostname, "sudo", "tar", "Cxzf", prefix, tmp} if err := runCmd(ctx, sshExe, sshFlags, sshArgs...); err != nil { return nil } _, _ = shell(ctx, sshExe, sshFlags, hostname, "rm", tmp) } return nil } ================================================ FILE: cmd/limactl/hostagent.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "io" "net" "net/http" "os" "os/signal" "runtime" "strconv" "syscall" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/hostagent" "github.com/lima-vm/lima/v2/pkg/hostagent/api/server" "github.com/lima-vm/lima/v2/pkg/store" ) func newHostagentCommand() *cobra.Command { hostagentCommand := &cobra.Command{ Use: "hostagent INSTANCE", Short: "Run hostagent", Args: WrapArgsError(cobra.ExactArgs(1)), RunE: hostagentAction, Hidden: true, } hostagentCommand.Flags().StringP("pidfile", "p", "", "Write PID to file") hostagentCommand.Flags().String("socket", "", "Path of hostagent socket") hostagentCommand.Flags().Bool("run-gui", false, "Run GUI synchronously within hostagent") hostagentCommand.Flags().String("guestagent", "", "Local file path (not URL) of lima-guestagent.OS-ARCH[.gz]") hostagentCommand.Flags().String("nerdctl-archive", "", "Local file path (not URL) of nerdctl-full-VERSION-GOOS-GOARCH.tar.gz") hostagentCommand.Flags().Bool("progress", false, "Show provision script progress by monitoring cloud-init logs") return hostagentCommand } func hostagentAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() pidfile, err := cmd.Flags().GetString("pidfile") if err != nil { return err } if pidfile != "" { if existingPID, err := store.ReadPIDFile(pidfile); existingPID != 0 { return fmt.Errorf("another hostagent may already be running with pid %d (pidfile %q)", existingPID, pidfile) } else if err != nil { return fmt.Errorf("failed to determine if another hostagent is running: %w", err) } if err := os.WriteFile(pidfile, []byte(strconv.Itoa(os.Getpid())+"\n"), 0o644); err != nil { return err } defer os.RemoveAll(pidfile) } socket, err := cmd.Flags().GetString("socket") if err != nil { return err } if socket == "" { return errors.New("socket must be specified (limactl version mismatch?)") } instName := args[0] runGUI, err := cmd.Flags().GetBool("run-gui") if err != nil { return err } if runGUI { // Without this the call to vz.RunGUI fails. Adding it here, as this has to be called before the vz cgo loads. runtime.LockOSThread() defer runtime.UnlockOSThread() } signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) stdout := &syncWriter{w: cmd.OutOrStdout()} stderr := &syncWriter{w: cmd.ErrOrStderr()} initLogrus(stderr) var opts []hostagent.Opt guestagentBinary, err := cmd.Flags().GetString("guestagent") if err != nil { return err } if guestagentBinary != "" { opts = append(opts, hostagent.WithGuestAgentBinary(guestagentBinary)) } nerdctlArchive, err := cmd.Flags().GetString("nerdctl-archive") if err != nil { return err } if nerdctlArchive != "" { opts = append(opts, hostagent.WithNerdctlArchive(nerdctlArchive)) } showProgress, err := cmd.Flags().GetBool("progress") if err != nil { return err } if showProgress { opts = append(opts, hostagent.WithCloudInitProgress(showProgress)) } ha, err := hostagent.New(ctx, instName, stdout, signalCh, opts...) if err != nil { return err } backend := &server.Backend{ Agent: ha, } r := http.NewServeMux() server.AddRoutes(r, backend) srv := &http.Server{Handler: r} err = os.RemoveAll(socket) if err != nil { return err } var lc net.ListenConfig l, err := lc.Listen(ctx, "unix", socket) logrus.Infof("hostagent socket created at %s", socket) if err != nil { return err } go func() { if serveErr := srv.Serve(l); serveErr != http.ErrServerClosed { logrus.WithError(serveErr).Warn("hostagent API server exited with an error") } }() defer srv.Close() return ha.Run(cmd.Context()) } // syncer is implemented by *os.File. type syncer interface { Sync() error } type syncWriter struct { w io.Writer } func (w *syncWriter) Write(p []byte) (int, error) { written, err := w.w.Write(p) if err == nil { if s, ok := w.w.(syncer); ok { _ = s.Sync() } } return written, err } func initLogrus(stderr io.Writer) { logrus.SetOutput(stderr) // JSON logs are parsed in pkg/hostagent/events.Watcher() logrus.SetFormatter(new(logrus.JSONFormatter)) // HostAgent logging is one level more verbose than the start command itself if logrus.GetLevel() == logrus.DebugLevel { logrus.SetLevel(logrus.TraceLevel) } else { logrus.SetLevel(logrus.DebugLevel) } } ================================================ FILE: cmd/limactl/info.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "encoding/json" "fmt" "github.com/mikefarah/yq/v4/pkg/yqlib" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/limainfo" "github.com/lima-vm/lima/v2/pkg/uiutil" "github.com/lima-vm/lima/v2/pkg/yqutil" ) func newInfoCommand() *cobra.Command { infoCommand := &cobra.Command{ Use: "info", Short: "Show diagnostic information", Args: WrapArgsError(cobra.NoArgs), RunE: infoAction, GroupID: advancedCommand, } infoCommand.Flags().String("yq", ".", "Apply yq expression to output") return infoCommand } func infoAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() yq, err := cmd.Flags().GetString("yq") if err != nil { return err } info, err := limainfo.New(ctx) if err != nil { return err } j, err := json.MarshalIndent(info, "", " ") if err != nil { return err } encoderPrefs := yqlib.ConfiguredJSONPreferences.Copy() encoderPrefs.Indent = 4 encoderPrefs.ColorsEnabled = uiutil.OutputIsTTY(cmd.OutOrStdout()) encoder := yqlib.NewJSONEncoder(encoderPrefs) str, err := yqutil.EvaluateExpressionWithEncoder(yq, string(j), encoder) if err == nil { _, err = fmt.Fprint(cmd.OutOrStdout(), str) } return err } ================================================ FILE: cmd/limactl/list.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "reflect" "slices" "strings" "github.com/cheggaaa/pb/v3/termutil" "github.com/mikefarah/yq/v4/pkg/yqlib" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/uiutil" "github.com/lima-vm/lima/v2/pkg/yqutil" ) func fieldNames() []string { names := []string{} t := reflect.TypeFor[store.FormatData]() for i := range t.NumField() { f := t.Field(i) if f.Anonymous { for j := range f.Type.NumField() { if tag := f.Tag.Get("lima"); tag != "deprecated" { names = append(names, f.Type.Field(j).Name) } } } else { if tag := f.Tag.Get("lima"); tag != "deprecated" { names = append(names, t.Field(i).Name) } } } return names } func newListCommand() *cobra.Command { listCommand := &cobra.Command{ Use: "list [flags] [INSTANCE]...", Aliases: []string{"ls"}, Short: "List instances of Lima", Long: `List instances of Lima. The output can be presented in one of several formats, using the --format flag. --format json - Output in JSON format --format yaml - Output in YAML format --format table - Output in table format --format '{{ }}' - If the format begins and ends with '{{ }}', then it is used as a go template. Filtering instances: --filter EXPR - Filter instances using yq expression (this is equivalent to --yq 'select(EXPR)') Can be specified multiple times (combined with AND logic) and it works with all output formats. Examples: --filter '.status == "Running"' --filter '.vmType == "vz"' --filter '.status == "Running"' --filter '.vmType == "vz"' (Same as AND) ` + store.FormatHelp + ` The following legacy flags continue to function: --json - equal to '--format json'`, Args: WrapArgsError(cobra.ArbitraryArgs), RunE: listAction, ValidArgsFunction: listBashComplete, GroupID: basicCommand, } listCommand.Flags().StringP("format", "f", "table", "Output format, one of: json, yaml, table, go-template") listCommand.Flags().Bool("list-fields", false, "List fields available for format") listCommand.Flags().Bool("json", false, "Same as --format=json") listCommand.Flags().BoolP("quiet", "q", false, "Only show names") listCommand.Flags().Bool("all-fields", false, "Show all fields") listCommand.Flags().StringArray("yq", nil, "Apply yq expression to each instance") listCommand.Flags().StringArrayP("filter", "l", nil, "Filter instances using yq expression (equivalent to --yq 'select(EXPR)')") return listCommand } func instanceMatches(arg string, instances []string) []string { matches := []string{} for _, instance := range instances { if instance == arg { matches = append(matches, instance) } } return matches } // unmatchedInstancesError is created when unmatched instance names found. type unmatchedInstancesError struct{} // Error implements error. func (unmatchedInstancesError) Error() string { return "unmatched instances" } // ExitCode implements ExitCoder. func (unmatchedInstancesError) ExitCode() int { return 1 } func listAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() quiet, err := cmd.Flags().GetBool("quiet") if err != nil { return err } format, err := cmd.Flags().GetString("format") if err != nil { return err } listFields, err := cmd.Flags().GetBool("list-fields") if err != nil { return err } jsonFormat, err := cmd.Flags().GetBool("json") if err != nil { return err } yq, err := cmd.Flags().GetStringArray("yq") if err != nil { return err } filter, err := cmd.Flags().GetStringArray("filter") if err != nil { return err } if jsonFormat { format = "json" } // conflicts if jsonFormat && cmd.Flags().Changed("format") { return errors.New("option --json conflicts with option --format") } if listFields && cmd.Flags().Changed("format") { return errors.New("option --list-fields conflicts with option --format") } if len(yq) != 0 { if cmd.Flags().Changed("format") && format != "json" && format != "yaml" { return errors.New("option --yq only works with --format json or yaml") } if listFields { return errors.New("option --list-fields conflicts with option --yq") } } if len(filter) != 0 { if listFields { return errors.New("option --list-fields conflicts with option --filter") } } if quiet && format != "table" { return errors.New("option --quiet can only be used with '--format table'") } if listFields { names := fieldNames() slices.Sort(names) fmt.Fprintln(cmd.OutOrStdout(), strings.Join(names, "\n")) return nil } if err := store.Validate(); err != nil { logrus.Warnf("The directory %q does not look like a valid Lima directory: %v", store.Directory(), err) } allInstances, err := store.Instances() if err != nil { return err } if len(args) == 0 && len(allInstances) == 0 { logrus.Warn("No instance found. Run `limactl create` to create an instance.") return nil } instanceNames := []string{} unmatchedInstances := false if len(args) > 0 { for _, arg := range args { matches := instanceMatches(arg, allInstances) if len(matches) > 0 { instanceNames = append(instanceNames, matches...) } else { logrus.Warnf("No instance matching %v found.", arg) unmatchedInstances = true } } } else { instanceNames = allInstances } if quiet && len(yq) == 0 && len(filter) == 0 { for _, instName := range instanceNames { fmt.Fprintln(cmd.OutOrStdout(), instName) } if unmatchedInstances { return unmatchedInstancesError{} } return nil } // get the state and config for all the requested instances var instances []*limatype.Instance for _, instanceName := range instanceNames { instance, err := store.Inspect(ctx, instanceName) if err != nil { return fmt.Errorf("unable to load instance %s: %w", instanceName, err) } instances = append(instances, instance) } if len(filter) > 0 { var filterExprs []string for _, f := range filter { filterExprs = append(filterExprs, "select("+f+")") } instances, err = filterInstances(instances, filterExprs) if err != nil { return err } } if quiet && len(yq) == 0 { for _, instance := range instances { fmt.Fprintln(cmd.OutOrStdout(), instance.Name) } if unmatchedInstances { return unmatchedInstancesError{} } return nil } for _, instance := range instances { if len(instance.Errors) > 0 { logrus.WithField("errors", instance.Errors).Warnf("instance %q has errors", instance.Name) } } allFields, err := cmd.Flags().GetBool("all-fields") if err != nil { return err } options := store.PrintOptions{AllFields: allFields} isTTY := uiutil.OutputIsTTY(cmd.OutOrStdout()) if isTTY { if w, err := termutil.TerminalWidth(); err == nil { options.TerminalWidth = w } } // --yq implies --format json unless --format has been explicitly specified if len(yq) != 0 && !cmd.Flags().Changed("format") { format = "json" } // Always pipe JSON and YAML through yq to colorize it if isTTY if len(yq) == 0 && (format == "json" || format == "yaml") { yq = append(yq, ".") } if len(yq) == 0 { err = store.PrintInstances(cmd.OutOrStdout(), instances, format, &options) if err == nil && unmatchedInstances { return unmatchedInstancesError{} } return err } if quiet { yq = append(yq, ".name") } yqExpr := strings.Join(yq, " | ") buf := new(bytes.Buffer) err = store.PrintInstances(buf, instances, format, &options) if err != nil { return err } if format == "json" { // The JSON encoder will create empty objects (YAML maps), even when they have the ",omitempty" tag. deleteEmptyObjects := `del(.. | select(tag == "!!map" and length == 0))` yqExpr += " | " + deleteEmptyObjects encoderPrefs := yqlib.ConfiguredJSONPreferences.Copy() encoderPrefs.ColorsEnabled = false encoderPrefs.Indent = 0 plainEncoder := yqlib.NewJSONEncoder(encoderPrefs) // Using non-0 indent means the instance will be printed over multiple lines, // so is no longer in JSON Lines format. This is a compromise for readability. encoderPrefs.Indent = 4 encoderPrefs.ColorsEnabled = true colorEncoder := yqlib.NewJSONEncoder(encoderPrefs) // Each line contains the JSON object for one Lima instance. scanner := bufio.NewScanner(buf) for scanner.Scan() { var str string if str, err = yqutil.EvaluateExpressionWithEncoder(yqExpr, scanner.Text(), plainEncoder); err != nil { return err } // Repeatedly delete empty objects until there are none left. for { length := len(str) if str, err = yqutil.EvaluateExpressionWithEncoder(deleteEmptyObjects, str, plainEncoder); err != nil { return err } if len(str) >= length { break } } if isTTY { // pretty-print and colorize the output if str, err = yqutil.EvaluateExpressionWithEncoder(".", str, colorEncoder); err != nil { return err } } if _, err = fmt.Fprint(cmd.OutOrStdout(), str); err != nil { return err } } err = scanner.Err() if err == nil && unmatchedInstances { return unmatchedInstancesError{} } return err } var str string if isTTY { // This branch is trading the better formatting from yamlfmt for colorizing from yqlib. if str, err = yqutil.EvaluateExpressionPlain(yqExpr, buf.String(), true); err != nil { return err } } else { var res []byte if res, err = yqutil.EvaluateExpression(yqExpr, buf.Bytes()); err != nil { return err } str = string(res) } _, err = fmt.Fprint(cmd.OutOrStdout(), str) if err == nil && unmatchedInstances { return unmatchedInstancesError{} } return err } func listBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } // filterInstances applies yq expressions to instances and returns the filtered results. func filterInstances(instances []*limatype.Instance, yqExprs []string) ([]*limatype.Instance, error) { if len(yqExprs) == 0 { return instances, nil } // the yq expression is evaluated with yqutil.EvaluateExpression, which disables environment variable access // and file operations, mitigating injection attacks like ".name=strenv(SOME_SECRET_ENV)" which could // trick Lima into exposing environment variables. yqExpr := strings.Join(yqExprs, " | ") var filteredInstances []*limatype.Instance for _, instance := range instances { jsonBytes, err := json.Marshal(instance) if err != nil { return nil, fmt.Errorf("failed to marshal instance %q: %w", instance.Name, err) } result, err := yqutil.EvaluateExpression(yqExpr, jsonBytes) if err != nil { return nil, fmt.Errorf("failed to apply filter %q: %w", yqExpr, err) } if len(bytes.TrimSpace(result)) > 0 { filteredInstances = append(filteredInstances, instance) } } return filteredInstances, nil } ================================================ FILE: cmd/limactl/main.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "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/plugins" "github.com/lima-vm/lima/v2/pkg/version" ) const ( DefaultInstanceName = "default" basicCommand = "basic" advancedCommand = "advanced" ) func main() { if os.Geteuid() == 0 && (len(os.Args) < 2 || os.Args[1] != "generate-doc") { fmt.Fprint(os.Stderr, "limactl: must not run as the root user\n") os.Exit(1) } 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") } } } err := newApp().Execute() server.StopAllExternalDrivers() osutil.HandleExitError(err) if err != nil { logrus.Fatal(err) } } func processGlobalFlags(rootCmd *cobra.Command) error { // --log-level will override --debug, but will not reset debugutil.Debug if debug, _ := rootCmd.Flags().GetBool("debug"); debug { logrus.SetLevel(logrus.DebugLevel) debugutil.Debug = true } l, _ := rootCmd.Flags().GetString("log-level") if l != "" { lvl, err := logrus.ParseLevel(l) if err != nil { return err } logrus.SetLevel(lvl) } logFormat, _ := rootCmd.Flags().GetString("log-format") switch logFormat { case "json": formatter := new(logrus.JSONFormatter) logrus.StandardLogger().SetFormatter(formatter) case "text": // logrus use text format by default. if runtime.GOOS == "windows" && isatty.IsCygwinTerminal(os.Stderr.Fd()) { formatter := new(logrus.TextFormatter) // the default setting does not recognize cygwin on windows formatter.ForceColors = true logrus.StandardLogger().SetFormatter(formatter) } default: return fmt.Errorf("unsupported log-format: %q", logFormat) } return nil } 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") // TODO: "survey" does not support using cygwin terminal on windows yet 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 { if err := processGlobalFlags(rootCmd); err != nil { return err } if osutil.IsBeingRosettaTranslated() && cmd.Parent().Name() != "completion" && cmd.Name() != "generate-doc" && cmd.Name() != "validate" { // running under rosetta would provide inappropriate runtime.GOARCH info, see: https://github.com/lima-vm/lima/issues/543 // allow commands that are used for packaging to run under rosetta to allow cross-architecture builds return errors.New("limactl is running under rosetta, please reinstall lima with native arch") } // Make sure either $HOME or $LIMA_HOME is defined, so we don't need // to check for errors later dir, err := dirnames.LimaDir() if err != nil { return err } nfs, err := fsutil.IsNFS(dir) if err != nil { return err } if nfs { logrus.Warn("LIMA_HOME should not be on an NFS mount") } 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") { switch cmd.Name() { case "clone", "edit", "rename": logrus.Warn("--yes flag is deprecated (--tty=false is still supported and works in the same way. Also consider using --start)") } // Sets the value of the yesValue flag by using the yes flag. yesValue, _ := cmd.Flags().GetBool("yes") if yesValue { // Sets to the default value false 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.AddGroup(&cobra.Group{ID: "plugin", Title: "Available Plugins (Experimental):"}) 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(), newRenameCommand(), newWatchCommand(), ) addPluginCommands(rootCmd) return rootCmd } func addPluginCommands(rootCmd *cobra.Command) { // The global options are only processed when rootCmd.Execute() is called. // Let's take a sneak peek here to help debug the plugin discovery code. if len(os.Args) > 1 && os.Args[1] == "--debug" { logrus.SetLevel(logrus.DebugLevel) } allPlugins, err := plugins.Discover() if err != nil { logrus.Warnf("Failed to discover plugins: %v", err) return } for _, plugin := range allPlugins { pluginCmd := &cobra.Command{ Use: plugin.Name, Short: plugin.Description, GroupID: "plugin", DisableFlagParsing: true, SilenceErrors: true, SilenceUsage: true, PreRunE: func(*cobra.Command, []string) error { for i, arg := range os.Args { if arg == plugin.Name { // parse global options but ignore plugin options err := rootCmd.ParseFlags(os.Args[1:i]) if err == nil { err = processGlobalFlags(rootCmd) } return err } } // unreachable return nil }, Run: func(cmd *cobra.Command, _ []string) { for i, arg := range os.Args { if arg == plugin.Name { // ignore global options plugin.Run(cmd.Context(), os.Args[i+1:]) // plugin.Run() never returns because it calls os.Exit() } } // unreachable }, } // Don't show the url scheme helper in the help output. if strings.HasPrefix(plugin.Name, "url-") { pluginCmd.Hidden = true } rootCmd.AddCommand(pluginCmd) } } // WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly. 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, ) } } ================================================ FILE: cmd/limactl/main_darwin.go ================================================ //go:build !external_vz && !no_vz // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main // Import vz driver to register it in the registry on darwin. import _ "github.com/lima-vm/lima/v2/pkg/driver/vz" ================================================ FILE: cmd/limactl/main_qemu.go ================================================ //go:build !external_qemu // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main // Import qemu driver to register it in the registry on all platforms. import _ "github.com/lima-vm/lima/v2/pkg/driver/qemu" ================================================ FILE: cmd/limactl/main_windows.go ================================================ //go:build !external_wsl2 // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main // Import wsl2 driver to register it in the registry on windows. import _ "github.com/lima-vm/lima/v2/pkg/driver/wsl2" ================================================ FILE: cmd/limactl/network.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "encoding/json" "errors" "fmt" "maps" "net" "os" "slices" "strings" "text/tabwriter" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/networks" "github.com/lima-vm/lima/v2/pkg/yqutil" ) const networkExample = ` List all networks: $ limactl network list Create a network: $ limactl network create foo --gateway 192.168.42.1/24 Connect VM instances to the newly created network: $ limactl create --network lima:foo --name vm1 $ limactl create --network lima:foo --name vm2 Delete a network: $ limactl network delete --force foo ` const networkCreateExample = ` Create a network: $ limactl network create foo --gateway 192.168.42.1/24 Connect VM instances to the newly created network: $ limactl create --network lima:foo --name vm1 $ limactl create --network lima:foo --name vm2 ` func newNetworkCommand() *cobra.Command { networkCommand := &cobra.Command{ Use: "network", Short: "Lima network management", Example: networkExample, GroupID: advancedCommand, } networkCommand.AddCommand( newNetworkListCommand(), newNetworkCreateCommand(), newNetworkDeleteCommand(), ) return networkCommand } func newNetworkListCommand() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List networks", Example: ` List all networks: $ limactl network list List networks in JSON format: $ limactl network list --json `, Aliases: []string{"ls"}, Args: WrapArgsError(cobra.ArbitraryArgs), RunE: networkListAction, ValidArgsFunction: networkBashComplete, } flags := cmd.Flags() flags.Bool("json", false, "JSONify output") return cmd } func networkListAction(cmd *cobra.Command, args []string) error { flags := cmd.Flags() jsonFormat, err := flags.GetBool("json") if err != nil { return err } config, err := networks.LoadConfig() if err != nil { return err } allNetworks := slices.Sorted(maps.Keys(config.Networks)) networks := []string{} if len(args) > 0 { for _, arg := range args { matches := nameMatches(arg, allNetworks) if len(matches) > 0 { networks = append(networks, matches...) } else { logrus.Warnf("No network matching %v found.", arg) } } } else { networks = allNetworks } if jsonFormat { w := cmd.OutOrStdout() for _, name := range networks { nw, ok := config.Networks[name] if !ok { logrus.Errorf("network %q does not exist", nw) continue } j, err := json.Marshal(nw) if err != nil { return err } fmt.Fprintln(w, string(j)) } return nil } w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) fmt.Fprintln(w, "NAME\tMODE\tGATEWAY\tINTERFACE") for _, name := range networks { nw, ok := config.Networks[name] if !ok { logrus.Errorf("network %q does not exist", nw) continue } gwStr := "-" if nw.Gateway != nil { gw := net.IPNet{ IP: nw.Gateway, Mask: net.IPMask(nw.NetMask), } gwStr = gw.String() } intfStr := "-" if nw.Interface != "" { intfStr = nw.Interface } fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, nw.Mode, gwStr, intfStr) } return w.Flush() } func newNetworkCreateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "create NETWORK", Short: "Create a Lima network", Example: networkCreateExample, Args: WrapArgsError(cobra.ExactArgs(1)), RunE: networkCreateAction, } flags := cmd.Flags() flags.String("mode", networks.ModeUserV2, "mode") _ = cmd.RegisterFlagCompletionFunc("mode", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return networks.Modes, cobra.ShellCompDirectiveNoFileComp }) flags.String("gateway", "", "gateway, e.g., \"192.168.42.1/24\"") flags.String("interface", "", "interface for bridged mode") _ = cmd.RegisterFlagCompletionFunc("interface", bashFlagCompleteNetworkInterfaceNames) return cmd } func networkCreateAction(cmd *cobra.Command, args []string) error { name := args[0] // LoadConfig ensures existence of networks.yaml config, err := networks.LoadConfig() if err != nil { return err } if _, ok := config.Networks[name]; ok { return fmt.Errorf("network %q already exists", name) } flags := cmd.Flags() mode, err := flags.GetString("mode") if err != nil { return err } gateway, err := flags.GetString("gateway") if err != nil { return err } intf, err := flags.GetString("interface") if err != nil { return err } switch mode { case networks.ModeBridged: if gateway != "" { return fmt.Errorf("network mode %q does not support specifying gateway", mode) } if intf == "" { return fmt.Errorf("network mode %q requires specifying interface", mode) } yq := fmt.Sprintf(`.networks.%q = {"mode":%q,"interface":%q}`, name, mode, intf) return networkApplyYQ(yq) default: if gateway == "" { return fmt.Errorf("network mode %q requires specifying gateway", mode) } if intf != "" { return fmt.Errorf("network mode %q does not support specifying interface", mode) } if !strings.Contains(gateway, "/") { gateway += "/24" } gwIP, gwMask, err := net.ParseCIDR(gateway) if err != nil { return fmt.Errorf("failed to parse CIDR %q: %w", gateway, err) } if gwIP.IsUnspecified() || gwIP.IsLoopback() { return fmt.Errorf("invalid IP address: %v", gwIP) } gwMaskStr := "255.255.255.0" if gwMask != nil { gwMaskStr = net.IP(gwMask.Mask).String() } // TODO: check IP range collision yq := fmt.Sprintf(`.networks.%q = {"mode":%q,"gateway":%q,"netmask":%q,"interface":%q}`, name, mode, gwIP.String(), gwMaskStr, intf) return networkApplyYQ(yq) } } func networkApplyYQ(yq string) error { filePath, err := networks.ConfigFile() if err != nil { return err } yContent, err := os.ReadFile(filePath) if err != nil { return err } yBytes, err := yqutil.EvaluateExpression(yq, yContent) if err != nil { return err } if err := os.WriteFile(filePath, yBytes, 0o644); err != nil { return err } return nil } func newNetworkDeleteCommand() *cobra.Command { cmd := &cobra.Command{ Use: "delete NETWORK [NETWORK, ...]", Short: "Delete one or more Lima networks", Example: ` Delete a network: $ limactl network delete --force foo Delete multiple networks: $ limactl network delete --force foo bar `, Aliases: []string{"remove", "rm"}, Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: networkDeleteAction, ValidArgsFunction: networkBashComplete, } flags := cmd.Flags() flags.BoolP("force", "f", false, "Force delete (currently always required)") return cmd } func networkDeleteAction(cmd *cobra.Command, args []string) error { flags := cmd.Flags() force, err := flags.GetBool("force") if err != nil { return err } if !force { return errors.New("`limactl network delete` currently always requires `--force`") // Because the command currently does not check whether the network being removed is in use } networks := make([]string, len(args)) for i, name := range args { networks[i] = fmt.Sprintf("del(.networks.%q)", name) } yq := strings.Join(networks, " | ") return networkApplyYQ(yq) } func networkBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteNetworkNames(cmd) } ================================================ FILE: cmd/limactl/protect.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/store" ) func newProtectCommand() *cobra.Command { protectCommand := &cobra.Command{ Use: "protect INSTANCE [INSTANCE, ...]", Short: "Protect an instance to prohibit accidental removal", Long: `Protect an instance to prohibit accidental removal via the 'limactl delete' command. The instance is not being protected against removal via '/bin/rm', Finder, etc.`, Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: protectAction, ValidArgsFunction: protectBashComplete, GroupID: advancedCommand, } return protectCommand } func protectAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() var errs []error for _, instName := range args { inst, err := store.Inspect(ctx, instName) if err != nil { errs = append(errs, fmt.Errorf("failed to inspect instance %q: %w", instName, err)) continue } if inst.Protected { logrus.Warnf("Instance %q is already protected. Skipping.", instName) continue } if err := inst.Protect(); err != nil { errs = append(errs, fmt.Errorf("failed to protect instance %q: %w", instName, err)) continue } logrus.Infof("Protected %q", instName) } return errors.Join(errs...) } func protectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/prune.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "maps" "os" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/downloader" "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/limatmpl" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/templatestore" ) func newPruneCommand() *cobra.Command { pruneCommand := &cobra.Command{ Use: "prune", Short: "Prune garbage objects", Args: WrapArgsError(cobra.NoArgs), RunE: pruneAction, ValidArgsFunction: cobra.NoFileCompletions, GroupID: advancedCommand, } pruneCommand.Flags().Bool("keep-referred", false, "Keep objects that are referred by some instances or templates") return pruneCommand } func pruneAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() keepReferred, err := cmd.Flags().GetBool("keep-referred") if err != nil { return err } opt := downloader.WithCache() if !keepReferred { return downloader.RemoveAllCacheDir(opt) } // Prune downloads that are not used by any instances or templates cacheEntries, err := downloader.CacheEntries(opt) if err != nil { return err } knownLocations, err := knownLocations(ctx) if err != nil { return err } for cacheKey, cachePath := range cacheEntries { if file, exists := knownLocations[cacheKey]; exists { logrus.Debugf("Keep %q caching %q", cacheKey, file.Location) } else { logrus.Debug("Deleting ", cacheKey) if err := os.RemoveAll(cachePath); err != nil { logrus.Warnf("Failed to delete %q: %v", cacheKey, err) return err } } } return nil } func knownLocations(ctx context.Context) (map[string]limatype.File, error) { locations := make(map[string]limatype.File) // Collect locations from instances instances, err := store.Instances() if err != nil { return nil, err } for _, instanceName := range instances { instance, err := store.Inspect(ctx, instanceName) if err != nil { return nil, err } if instance.Errors != nil { logrus.Warnf("skipping instance %q because it has errors: %v", instanceName, instance.Errors) continue } maps.Copy(locations, locationsFromLimaYAML(instance.Config)) } // Collect locations from templates templates, err := templatestore.Templates() if err != nil { return nil, err } for _, t := range templates { tmpl, err := limatmpl.Read(ctx, "", t.Location) if err != nil { return nil, err } if err := tmpl.Embed(ctx, true, true); err != nil { return nil, err } y, err := limayaml.Load(ctx, tmpl.Bytes, tmpl.Name) if err != nil { return nil, err } if err := driverutil.ResolveVMType(ctx, y, t.Name); err != nil { logrus.Warnf("failed to resolve vmType for %q: %v", t.Name, err) } maps.Copy(locations, locationsFromLimaYAML(y)) } return locations, nil } func locationsFromLimaYAML(y *limatype.LimaYAML) map[string]limatype.File { locations := make(map[string]limatype.File) for _, f := range y.Images { locations[downloader.CacheKey(f.Location)] = f.File if f.Kernel != nil { locations[downloader.CacheKey(f.Kernel.Location)] = f.Kernel.File } if f.Initrd != nil { locations[downloader.CacheKey(f.Initrd.Location)] = *f.Initrd } } for _, f := range y.Containerd.Archives { locations[downloader.CacheKey(f.Location)] = f } for _, f := range y.Firmware.Images { locations[downloader.CacheKey(f.Location)] = f.File } return locations } ================================================ FILE: cmd/limactl/restart.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/store" ) func newRestartCommand() *cobra.Command { restartCmd := &cobra.Command{ Use: "restart INSTANCE", Short: "Restart a running instance", Args: WrapArgsError(cobra.MaximumNArgs(1)), RunE: restartAction, ValidArgsFunction: restartBashComplete, GroupID: basicCommand, } restartCmd.Flags().BoolP("force", "f", false, "Force stop and restart the instance") restartCmd.Flags().Bool("progress", false, "Show provision script progress by tailing cloud-init logs") return restartCmd } func restartAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() instName := DefaultInstanceName if len(args) > 0 { instName = args[0] } inst, err := store.Inspect(ctx, instName) if err != nil { return err } force, err := cmd.Flags().GetBool("force") if err != nil { return err } progress, err := cmd.Flags().GetBool("progress") if err != nil { return err } if force { return instance.RestartForcibly(ctx, inst, progress) } return instance.Restart(ctx, inst, progress) } func restartBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/shell.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "al.essio.dev/pkg/shellescape" "github.com/coreos/go-semver/semver" "github.com/lima-vm/sshocker/pkg/ssh" "github.com/mattn/go-isatty" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/copytool" "github.com/lima-vm/lima/v2/pkg/envutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/ioutilx" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/networks/reconcile" "github.com/lima-vm/lima/v2/pkg/sshutil" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/uiutil" ) const shellHelp = `Execute shell in Lima lima command is provided as an alias for limactl shell $LIMA_INSTANCE. $LIMA_INSTANCE defaults to "` + DefaultInstanceName + `". By default, the first 'ssh' executable found in the host's PATH is used to connect to the Lima instance. A custom ssh alias can be used instead by setting the $` + sshutil.EnvShellSSH + ` environment variable. Environment Variables: --preserve-env: Propagates host environment variables to the guest instance. Use LIMA_SHELLENV_ALLOW to specify which variables to allow. Use LIMA_SHELLENV_BLOCK to specify which variables to block (extends default blocklist with +). Hint: try --debug to show the detailed logs, if it seems hanging (mostly due to some SSH issue). ` func newShellCommand() *cobra.Command { shellCmd := &cobra.Command{ Use: "shell [flags] INSTANCE [COMMAND...]", SuggestFor: []string{"ssh"}, Short: "Execute shell in Lima", Long: shellHelp, Args: WrapArgsError(cobra.ArbitraryArgs), RunE: shellAction, ValidArgsFunction: shellBashComplete, SilenceErrors: true, GroupID: basicCommand, } shellCmd.Flags().SetInterspersed(false) shellCmd.Flags().String("instance", "", "Instance name (used by the lima wrapper script)") _ = shellCmd.Flags().MarkHidden("instance") shellCmd.Flags().String("shell", "", "Shell interpreter, e.g. /bin/bash") shellCmd.Flags().String("workdir", "", "Working directory") shellCmd.Flags().Bool("reconnect", false, "Reconnect to the SSH session") shellCmd.Flags().Bool("preserve-env", false, "Propagate environment variables to the shell") shellCmd.Flags().Bool("start", false, "Start the instance if it is not already running") shellCmd.Flags().String("sync", "", "Copy a host directory to the guest and vice-versa upon exit") return shellCmd } const ( rsyncMinimumSrcDirDepth = 4 // Depth of "/Users/USER" is 3. colorGray = "\033[0;90m" colorNone = "\033[0m" ) func shellAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() flags := cmd.Flags() tty, err := flags.GetBool("tty") if err != nil { return err } // When --instance is specified, all positional args are treated as COMMAND. // Otherwise, the first positional arg is the instance name (backward compatible). instName, err := flags.GetString("instance") if err != nil { return err } if instName != "" { // All args are COMMAND; prepend a placeholder instance name so the rest of the code works unchanged. args = append([]string{instName}, args...) } else { if len(args) == 0 { return errors.New("requires instance name as first argument") } // simulate the behavior of double dash newArg := []string{} if len(args) >= 2 && args[1] == "--" { newArg = append(newArg, args[:1]...) newArg = append(newArg, args[2:]...) args = newArg } instName = args[0] } if len(args) >= 2 { switch args[1] { case "create", "start", "delete", "shell": // `lima start` (alias of `limactl $LIMA_INSTANCE start`) is probably a typo of `limactl start` logrus.Warnf("Perhaps you meant `limactl %s`?", strings.Join(args[1:], " ")) } } inst, err := store.Inspect(ctx, instName) if err != nil { if errors.Is(err, os.ErrNotExist) { return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName) } return err } if len(inst.Errors) > 0 { logrus.WithError(errors.Join(inst.Errors...)).Errorf("Instance %q has configuration errors", instName) } if inst.Config == nil { return fmt.Errorf("instance %q has no configuration", instName) } if inst.Status == limatype.StatusStopped { startNow, err := flags.GetBool("start") if err != nil { return err } if tty && !flags.Changed("start") { startNow, err = askWhetherToStart(cmd) if err != nil { return err } } if !startNow { return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName) } // Network reconciliation will be performed by the process launched by the autostart manager if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) } else if !registered { err = reconcile.Reconcile(ctx, inst.Name) if err != nil { return err } } err = instance.Start(instance.WithLaunchingShell(ctx), inst, false, false) if err != nil { return err } inst, err = store.Inspect(ctx, instName) if err != nil { return err } } restart, err := cmd.Flags().GetBool("reconnect") if err != nil { return err } if restart && sshutil.IsControlMasterExisting(inst.Dir) { logrus.Infof("Exiting ssh session for the instance %q", instName) sshConfig := &ssh.SSHConfig{ ConfigFile: inst.SSHConfigFile, Persist: false, AdditionalArgs: []string{}, } if err := ssh.ExitMaster(inst.Hostname, inst.SSHLocalPort, sshConfig); err != nil { return err } } syncDirVal, err := flags.GetString("sync") if err != nil { return fmt.Errorf("failed to get sync flag: %w", err) } syncHostWorkdir := syncDirVal != "" if syncHostWorkdir && len(inst.Config.Mounts) > 0 { return errors.New("cannot use `--sync` when the instance has host mounts configured, start the instance with `--mount-none` to disable mounts") } // When workDir is explicitly set, the shell MUST have workDir as the cwd, or exit with an error. // // changeDirCmd := "cd workDir || exit 1" if workDir != "" // := "cd hostCurrentDir || cd hostHomeDir" if workDir == "" var changeDirCmd string var hostCurrentDir string if syncDirVal != "" { hostCurrentDir, err = filepath.Abs(syncDirVal) if err == nil && runtime.GOOS == "windows" { hostCurrentDir, err = mountDirFromWindowsDir(ctx, inst, hostCurrentDir) } } else { hostCurrentDir, err = hostCurrentDirectory(ctx, inst) } if err != nil { changeDirCmd = "false" logrus.WithError(err).Warn("failed to get the current directory") } if syncHostWorkdir { if _, err := exec.LookPath(string(copytool.BackendRsync)); err != nil { return fmt.Errorf("rsync is required for `--sync` but not found: %w", err) } srcWdDepth := len(strings.Split(hostCurrentDir, string(os.PathSeparator))) if srcWdDepth < rsyncMinimumSrcDirDepth { return fmt.Errorf("expected the depth of the host working directory (%q) to be more than %d, only got %d (Hint: %s)", hostCurrentDir, rsyncMinimumSrcDirDepth, srcWdDepth, "cd to a deeper directory") } } var destRsyncDir string workDir, err := cmd.Flags().GetString("workdir") if err != nil { return err } if workDir != "" && syncHostWorkdir { return errors.New("cannot use `--workdir` and `--sync` at the same time") } if syncHostWorkdir { destRsyncDir = *inst.Config.User.Home + hostCurrentDir } switch { case workDir != "": changeDirCmd = fmt.Sprintf("cd %s || exit 1", shellescape.Quote(workDir)) // FIXME: check whether y.Mounts contains the home, not just len > 0 case len(inst.Config.Mounts) > 0 || inst.VMType == limatype.WSL2: changeDirCmd = fmt.Sprintf("cd %s", shellescape.Quote(hostCurrentDir)) hostHomeDir, err := os.UserHomeDir() if err == nil && runtime.GOOS == "windows" { hostHomeDir, err = mountDirFromWindowsDir(ctx, inst, hostHomeDir) } if err == nil { changeDirCmd = fmt.Sprintf("%s || cd %s", changeDirCmd, shellescape.Quote(hostHomeDir)) } else { logrus.WithError(err).Warn("failed to get the home directory") } case syncHostWorkdir: changeDirCmd = fmt.Sprintf("cd %s", shellescape.Quote(destRsyncDir)) default: logrus.Debug("the host home does not seem mounted, so the guest shell will have a different cwd") } if changeDirCmd == "" { changeDirCmd = "false" } logrus.Debugf("changeDirCmd=%q", changeDirCmd) shell, err := cmd.Flags().GetString("shell") if err != nil { return err } if shell == "" { shell = `"$SHELL"` } else { shell = shellescape.Quote(shell) } // Handle environment variable propagation var envPrefix string preserveEnv, err := cmd.Flags().GetBool("preserve-env") if err != nil { return err } if preserveEnv { filteredEnv := envutil.FilterEnvironment() if len(filteredEnv) > 0 { envPrefix = "env " for _, envVar := range filteredEnv { envPrefix += shellescape.Quote(envVar) + " " } } } // -l is known to be available in bash, zsh, and FreeBSD sh. // Note that --login is not available in FreeBSD sh. script := fmt.Sprintf("%s ; exec %s%s -l", changeDirCmd, envPrefix, shell) if len(args) > 1 { quotedArgs := make([]string, len(args[1:])) parsingEnv := true for i, arg := range args[1:] { if parsingEnv && isEnv(arg) { quotedArgs[i] = quoteEnv(arg) } else { parsingEnv = false quotedArgs[i] = shellescape.Quote(arg) } } script += fmt.Sprintf( " -c %s", shellescape.Quote(strings.Join(quotedArgs, " ")), ) } sshExe, err := sshutil.NewSSHExe() if err != nil { return err } sshOpts, err := sshutil.SSHOpts( ctx, sshExe, inst.Dir, *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, *inst.Config.SSH.ForwardX11Trusted) if err != nil { return err } if runtime.GOOS == "windows" { // Remove ControlMaster, ControlPath, and ControlPersist options, // because Cygwin-based SSH clients do not support multiplexing when executing commands. // References: // https://inbox.sourceware.org/cygwin/c98988a5-7e65-4282-b2a1-bb8e350d5fab@acm.org/T/ // https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible // By removing these options: // - Avoids execution failures when the control master is not yet available. // - Prevents error messages such as: // > mux_client_request_session: read from master failed: Connection reset by peer // > ControlSocket ....sock already exists, disabling multiplexing // Only remove these options when writing the SSH config file and executing `limactl shell`, since multiplexing seems to work with port forwarding. sshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts) } sshArgs := append([]string{}, sshExe.Args...) sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...) var ( sshExecForRsync *exec.Cmd rsync copytool.CopyTool ) if syncHostWorkdir { logrus.Infof("Syncing host current directory(%s) to guest instance...", hostCurrentDir) sshExecForRsync = exec.CommandContext(ctx, sshExe.Exe, sshArgs...) // Create the destination directory in the guest instance, // we could have done this by using `--rsync-path` but it's more // complex to quote properly. if err := executeSSHForRsync(ctx, *sshExecForRsync, inst.SSHLocalPort, inst.SSHAddress, fmt.Sprintf("mkdir -p %s", shellescape.Quote(destRsyncDir))); err != nil { return fmt.Errorf("failed to create the synced workdir in guest instance: %w", err) } // The macOS release of rsync (the latest being 2.6.9) does not support shell escaping of destination path but other versions do. rsyncVer, err := rsyncVersion(ctx) if err != nil { return fmt.Errorf("failed to get rsync version: %w", err) } if rsyncVer.LessThan(*semver.New("3.0.0")) { destRsyncDir = shellescape.Quote(destRsyncDir) } paths := []string{ hostCurrentDir, fmt.Sprintf("%s:%s", inst.Name, destRsyncDir), } rsync, err = copytool.New(ctx, string(copytool.BackendRsync), paths, ©tool.Options{ Recursive: true, Verbose: false, AdditionalArgs: []string{ "--delete", }, }) if err != nil { return err } logrus.Debugf("using copy tool %q", rsync.Name()) if err := rsyncDirectory(ctx, cmd, rsync, paths); err != nil { return fmt.Errorf("failed to rsync to the guest %w", err) } logrus.Infof("Successfully synced host current directory to guest(%s) instance.", destRsyncDir) } if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) { // required for showing the shell prompt: https://stackoverflow.com/a/626574 sshArgs = append(sshArgs, "-t") } if _, present := os.LookupEnv("COLORTERM"); present { // SendEnv config is cumulative, with already existing options in ssh_config sshArgs = append(sshArgs, "-o", "SendEnv=COLORTERM") } logLevel := "ERROR" // For versions older than OpenSSH 8.9p, LogLevel=QUIET was needed to // avoid the "Shared connection to 127.0.0.1 closed." message with -t. olderSSH := sshutil.DetectOpenSSHVersion(ctx, sshExe).LessThan(*semver.New("8.9.0")) if olderSSH { logLevel = "QUIET" } sshArgs = append(sshArgs, []string{ "-o", fmt.Sprintf("LogLevel=%s", logLevel), "-p", strconv.Itoa(inst.SSHLocalPort), inst.SSHAddress, "--", script, }...) sshCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...) sshCmd.Stdin = os.Stdin sshCmd.Stdout = os.Stdout sshCmd.Stderr = os.Stderr logrus.Debugf("executing ssh (may take a long)): %+v", sshCmd.Args) // TODO: use syscall.Exec directly (results in losing tty?) if err := sshCmd.Run(); err != nil { return err } // Once the shell command finishes, rsync back the changes from guest workdir // to the host and delete the guest synced workdir only if the user // confirms the changes. if syncHostWorkdir { tty, err := flags.GetBool("tty") if err != nil { return err } return askUserForRsyncBack(ctx, cmd, inst, sshExecForRsync, hostCurrentDir, destRsyncDir, rsync, tty) } return nil } func askUserForRsyncBack(ctx context.Context, cmd *cobra.Command, inst *limatype.Instance, sshCmd *exec.Cmd, hostCurrentDir, destRsyncDir string, rsync copytool.CopyTool, tty bool) error { remoteSource := fmt.Sprintf("%s:%s", inst.Name, destRsyncDir) clean := filepath.Clean(hostCurrentDir) dirForCleanup := shellescape.Quote(filepath.Join(*inst.Config.User.Home, clean)) rsyncBack := func() error { paths := []string{ remoteSource, hostCurrentDir, } if err := rsyncDirectory(ctx, cmd, rsync, paths); err != nil { return fmt.Errorf("failed to sync back the changes from guest instance to host: %w", err) } logrus.Info("Successfully synced back the changes to host.") return nil } defer func() { // Clean up the guest synced workdir if err := executeSSHForRsync(ctx, *sshCmd, inst.SSHLocalPort, inst.SSHAddress, fmt.Sprintf("rm -rf %s", dirForCleanup)); err != nil { logrus.WithError(err).Warn("Failed to clean up guest synced workdir") } }() if !tty { return rsyncBack() } rawOutput, stats, err := getRsyncStats(ctx, remoteSource, filepath.Dir(hostCurrentDir)) if err != nil { logrus.WithError(err).Warn("failed to get rsync stats") } if stats != nil && stats.String() == "" { logrus.Info("No changes detected") return nil } statsMsg := "" if stats != nil { if s := stats.String(); s != "" { statsMsg = fmt.Sprintf(" (%s)", s) } } message := fmt.Sprintf("⚠️ Accept the changes?%s", statsMsg) options := []string{ "Yes", "No", "View the changed contents", } baseDir, err := os.MkdirTemp("", "lima-guest-synced-*") if err != nil { return err } defer func() { if err := os.RemoveAll(baseDir); err != nil { logrus.WithError(err).Warnf("Failed to clean up temporary directory %s", baseDir) } }() hostTmpDest := filepath.Join(baseDir, filepath.Base(hostCurrentDir)) err = os.MkdirAll(hostTmpDest, 0o755) if err != nil { return err } rsyncToTempDir := false for { ans, err := uiutil.Select(message, options) if err != nil { return fmt.Errorf("failed to open TUI: %w", err) } switch ans { case 0: // Yes return rsyncBack() case 1: // No logrus.Info("Skipping syncing back the changes to host.") return nil case 2: // View the changed contents var diffCmd *exec.Cmd if _, err := exec.LookPath("diff"); err != nil { logrus.WithError(err).Warn("`diff` not found; showing rsync dry-run output only") } else { diffCmd = exec.CommandContext(ctx, "diff", "-ruN", "--color=always", hostCurrentDir, hostTmpDest) if !rsyncToTempDir { paths := []string{ remoteSource, hostTmpDest, } if err := rsyncDirectory(ctx, cmd, rsync, paths); err != nil { return fmt.Errorf("failed to sync back the changes from guest instance to host temporary directory: %w", err) } rsyncToTempDir = true } } pager := os.Getenv("PAGER") pager = strings.TrimSpace(pager) if pager == "" { pager = "less" } pagerArgs := strings.Fields(pager) lessCmd := exec.CommandContext(ctx, pagerArgs[0], pagerArgs[1:]...) pipeIn, err := lessCmd.StdinPipe() if err != nil { return fmt.Errorf("failed to create pipe for less: %w", err) } if diffCmd != nil { diffCmd.Stdout = pipeIn } lessCmd.Stdout = cmd.OutOrStdout() lessCmd.Stderr = cmd.OutOrStderr() if err := lessCmd.Start(); err != nil { return fmt.Errorf("failed to start less: %w", err) } // Write rsync dry-run output first if stats != nil { rsyncHead := fmt.Sprintf("%s--- rsync dry-run statistics ---%s", colorGray, colorNone) diffHead := fmt.Sprintf("%s--- detailed diff --- %s", colorGray, colorNone) if diffCmd == nil { diffHead = fmt.Sprintf("%s--- detailed diff unavailable (`diff` not found) --- %s", colorGray, colorNone) } combinedOutput := fmt.Sprintf( "%s\n%s\n\n%s\n\n\n%s\n", rsyncHead, stats.String(), rawOutput, diffHead, ) if _, err := fmt.Fprint(pipeIn, combinedOutput); err != nil { _ = pipeIn.Close() return fmt.Errorf("failed to write rsync stats to pager: %w", err) } } if diffCmd != nil { if err := diffCmd.Run(); err != nil { // Command `diff` returns exit code 1 when files differ. var exitErr *exec.ExitError if errors.As(err, &exitErr) && exitErr.ExitCode() >= 2 { _ = pipeIn.Close() return fmt.Errorf("failed to run diff command: %w", err) } } } _ = pipeIn.Close() if err := lessCmd.Wait(); err != nil { return fmt.Errorf("failed to wait for less command: %w", err) } } } } func executeSSHForRsync(ctx context.Context, sshCmd exec.Cmd, sshLocalPort int, sshAddress, command string) error { sshCmd.Args = append(sshCmd.Args, "-p", strconv.Itoa(sshLocalPort), sshAddress, ) // Skip Args[0] (program name) to avoid duplication sshRmCmd := exec.CommandContext(ctx, sshCmd.Path, append(sshCmd.Args[1:], command)...) if err := sshRmCmd.Run(); err != nil { return err } return nil } func hostCurrentDirectory(ctx context.Context, inst *limatype.Instance) (string, error) { hostCurrentDir, err := os.Getwd() if err == nil && runtime.GOOS == "windows" { hostCurrentDir, err = mountDirFromWindowsDir(ctx, inst, hostCurrentDir) } return hostCurrentDir, err } func rsyncVersion(ctx context.Context) (*semver.Version, error) { out, err := exec.CommandContext(ctx, string(copytool.BackendRsync), "--version").Output() if err != nil { return nil, err } // `rsync version 3.2.7 protocol version 31` re := regexp.MustCompile(`version (\d+\.\d+\.\d+)`) matches := re.FindSubmatch(out) if len(matches) < 2 { return nil, errors.New("failed to parse rsync version") } return semver.NewVersion(string(matches[1])) } // Syncs a directory from host to guest and vice-versa. It creates a directory in the guest's home directory and copies the contents of the host's // current working directory into it. The guest directory paths should be prefixed with `:` followed by the path. func rsyncDirectory(ctx context.Context, cmd *cobra.Command, rsync copytool.CopyTool, paths []string) error { rsyncCmd, err := rsync.Command(ctx, paths, nil) if err != nil { return err } rsyncCmd.Stdout = cmd.OutOrStdout() rsyncCmd.Stderr = cmd.OutOrStderr() logrus.Debugf("executing rsync: %+v", rsyncCmd.Args) return rsyncCmd.Run() } func mountDirFromWindowsDir(ctx context.Context, inst *limatype.Instance, dir string) (string, error) { if inst.VMType == limatype.WSL2 { distroName := "lima-" + inst.Name return ioutilx.WindowsSubsystemPathForLinux(ctx, dir, distroName) } return ioutilx.WindowsSubsystemPath(ctx, dir) } func shellBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } func isEnv(arg string) bool { return len(strings.Split(arg, "=")) > 1 } func quoteEnv(arg string) string { env := strings.SplitN(arg, "=", 2) env[1] = shellescape.Quote(env[1]) return strings.Join(env, "=") } type rsyncStats struct { Added int Deleted int Modified int Metadata int } func (s *rsyncStats) String() string { if s.Added == 0 && s.Deleted == 0 && s.Modified == 0 && s.Metadata == 0 { return "" } return fmt.Sprintf("added: %d, deleted: %d, modified: %d, metadata: %d", s.Added, s.Deleted, s.Modified, s.Metadata) } func getRsyncStats(ctx context.Context, source, destination string) (string, *rsyncStats, error) { paths := []string{source, destination} rsync, err := copytool.New(ctx, string(copytool.BackendRsync), paths, ©tool.Options{ Verbose: true, AdditionalArgs: []string{ "--dry-run", "--itemize-changes", "-ah", "--delete", }, }) if err != nil { return "", nil, err } rsyncCmd, err := rsync.Command(ctx, paths, nil) if err != nil { return "", nil, err } logrus.Debugf("executing rsync for stats: %+v", rsyncCmd.Args) out, err := rsyncCmd.Output() if err != nil { return "", nil, err } output := string(out) return output, parseRsyncStats(output), nil } // parseRsyncStats parses the output of `rsync --itemize-changes` to extract file operation statistics. // // Rsync itemized format: YXcstpoguax path/to/file // Where Y=update type, X=file type, c=checksum status, and positions 3-10 are other attributes. // // Examples: // // >f+++++++++ file.txt → Added (new file received) // >f.st...... file.txt → Modified (existing file updated) // *deleting file.txt → Deleted // // Logic: // // - `*deleting`: Count as Deleted. // - Update type `<` (sent), `>` (received), or `c` (local change): // - If checksum is `+` (created): Count as Added. // - Otherwise: Count as Modified. // // - Update type `.` with non-`.` metadata attributes (positions 3-10): // - Count as Metadata. func parseRsyncStats(output string) *rsyncStats { var s rsyncStats for line := range strings.SplitSeq(output, "\n") { if len(line) < 12 { continue } if strings.HasPrefix(line, "*deleting") { s.Deleted++ continue } updateType := line[0] checksum := line[2] if updateType == '<' || updateType == '>' || updateType == 'c' { if checksum == '+' { s.Added++ } else { s.Modified++ } continue } if updateType == '.' && hasMetadataDelta(line[2:11]) { s.Metadata++ } } return &s } func hasMetadataDelta(attrs string) bool { for _, ch := range attrs { if ch != '.' { return true } } return false } ================================================ FILE: cmd/limactl/shell_test.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "testing" "gotest.tools/v3/assert" ) func TestParseRsyncStats(t *testing.T) { tests := []struct { name string output string expected *rsyncStats }{ { name: "mixed output", output: ` >f+++++++++ new-file.txt cd+++++++++ dir/ cL+++++++++ new-symlink -> target *deleting deleted.txt `, expected: &rsyncStats{ Added: 3, Deleted: 1, Modified: 0, }, }, { name: "metadata-only changes", output: ` .d..t...... ./ .f...p..... existing.txt `, expected: &rsyncStats{ Added: 0, Deleted: 0, Modified: 0, Metadata: 2, }, }, { name: "many changes", output: ` 1 && fields[1] != "TAG" { // make sure that output matches the expected return fmt.Errorf("unknown header: %s", line) } if i == 0 || line == "" { // skip header and empty line after using split continue } tag := fields[1] fmt.Fprintf(cmd.OutOrStdout(), "%s\n", tag) } return nil } fmt.Fprint(cmd.OutOrStdout(), out) return nil } func snapshotBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/start-at-login.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/spf13/cobra" ) func newStartAtLoginCommand() *cobra.Command { startAtLoginCommand := &cobra.Command{ Use: "start-at-login INSTANCE", Short: "Register/Unregister an autostart file for the instance", Args: WrapArgsError(cobra.MaximumNArgs(1)), RunE: startAtLoginAction, ValidArgsFunction: startAtLoginComplete, GroupID: advancedCommand, } startAtLoginCommand.Flags().Bool( "enabled", true, "Automatically start the instance when the user logs in", ) return startAtLoginCommand } func startAtLoginComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/start-at-login_unix.go ================================================ //go:build !windows // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/store" ) func startAtLoginAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() instName := DefaultInstanceName if len(args) > 0 { instName = args[0] } inst, err := store.Inspect(ctx, instName) if err != nil { if errors.Is(err, os.ErrNotExist) { logrus.Infof("Instance %q not found", instName) return nil } return err } flags := cmd.Flags() startAtLogin, err := flags.GetBool("enabled") if err != nil { return err } if registered, err := autostart.IsRegistered(ctx, inst); err != nil { return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) } else if startAtLogin { verb := "create" if registered { verb = "update" } if err := autostart.RegisterToStartAtLogin(ctx, inst); err != nil { return fmt.Errorf("failed to %s the autostart entry for instance %q: %w", verb, inst.Name, err) } logrus.Infof("The autostart entry for instance %q has been %sd", inst.Name, verb) } else { if !registered { logrus.Infof("The autostart entry for instance %q is not registered", inst.Name) } else if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil { return fmt.Errorf("failed to unregister the autostart entry for instance %q: %w", inst.Name, err) } else { logrus.Infof("The autostart entry for instance %q has been unregistered", inst.Name) } } return nil } ================================================ FILE: cmd/limactl/start-at-login_windows.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "github.com/spf13/cobra" ) func startAtLoginAction(_ *cobra.Command, _ []string) error { return errors.New("start-at-login command is only supported on macOS and Linux right now") } ================================================ FILE: cmd/limactl/start.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "errors" "fmt" "os" "path/filepath" "runtime" "strings" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/editutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/limatmpl" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/networks/reconcile" "github.com/lima-vm/lima/v2/pkg/registry" "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/templatestore" "github.com/lima-vm/lima/v2/pkg/uiutil" "github.com/lima-vm/lima/v2/pkg/yqutil" ) func registerCreateFlags(cmd *cobra.Command, commentPrefix string) { flags := cmd.Flags() flags.String("name", "", commentPrefix+"Override the instance name") flags.Bool("list-templates", false, commentPrefix+"List available templates and exit") flags.Bool("list-drivers", false, commentPrefix+"List available drivers and exit") editflags.RegisterCreate(cmd, commentPrefix) } func newCreateCommand() *cobra.Command { createCommand := &cobra.Command{ Use: "create FILE.yaml|URL", Example: ` To create an instance "default" from the default Ubuntu template: $ limactl create To create an instance "default" from a template "docker": $ limactl create --name=default template:docker To create an instance "default" with modified parameters: $ limactl create --cpus=2 --memory=2 To create an instance "default" with yq expressions: $ limactl create --set='.cpus = 2 | .memory = "2GiB"' To see the template list: $ limactl create --list-templates To create an instance "default" from a local file: $ limactl create --name=default /usr/local/share/lima/templates/fedora.yaml To create an instance "default" from a remote URL (use carefully, with a trustable source): $ limactl create --name=default https://raw.githubusercontent.com/lima-vm/lima/master/templates/alpine.yaml To create an instance "local" from a template passed to stdin (--name parameter is required): $ cat template.yaml | limactl create --name=local - `, Short: "Create an instance of Lima", Args: WrapArgsError(cobra.MaximumNArgs(1)), ValidArgsFunction: createBashComplete, RunE: createAction, GroupID: basicCommand, } registerCreateFlags(createCommand, "") return createCommand } func newStartCommand() *cobra.Command { startCommand := &cobra.Command{ Use: "start NAME|FILE.yaml|URL", Example: ` To create an instance "default" (if not created yet) from the default Ubuntu template, and start it: $ limactl start To create an instance "default" from a template "docker", and start it: $ limactl start --name=default template:docker `, Short: "Start an instance of Lima", Args: WrapArgsError(cobra.MaximumNArgs(1)), ValidArgsFunction: startBashComplete, RunE: startAction, GroupID: basicCommand, } registerCreateFlags(startCommand, "[limactl create] ") if runtime.GOOS != "windows" { startCommand.Flags().Bool("foreground", false, "Run the hostagent in the foreground") } startCommand.Flags().Duration("timeout", instance.DefaultWatchHostAgentEventsTimeout, "Duration to wait for the instance to be running before timing out") startCommand.Flags().Bool("progress", false, "Show provision script progress by tailing cloud-init logs") startCommand.SetHelpFunc(func(cmd *cobra.Command, _ []string) { printCommandSummary(cmd) allFlags, createFlags := collectFlags(cmd) printFlags(allFlags, createFlags) printGlobalFlags(cmd) }) return startCommand } func printCommandSummary(cmd *cobra.Command) { fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n", cmd.Short) fmt.Fprintf(cmd.OutOrStdout(), "Usage:\n %s\n\n", cmd.UseLine()) if cmd.Example != "" { fmt.Fprintf(cmd.OutOrStdout(), "Examples:\n%s\n\n", cmd.Example) } } func getFlagType(flag *pflag.Flag) string { switch flag.Value.Type() { case "bool": return "" case "string": return "string" case "int": return "int" case "duration": return "duration" case "stringSlice", "stringArray": return "strings" case "ipSlice": return "ipSlice" case "uint16": return "uint16" case "float32": return "float32" default: return flag.Value.Type() } } func formatFlag(flag *pflag.Flag) (flagName, shorthand string) { flagName = "--" + flag.Name if flag.Shorthand != "" { shorthand = "-" + flag.Shorthand } flagType := getFlagType(flag) if flagType != "" { flagName += " " + flagType } return flagName, shorthand } func collectFlags(cmd *cobra.Command) (allFlags, createFlags []string) { cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) { flagName, shorthand := formatFlag(flag) flagUsage := flag.Usage var formattedFlag string if shorthand != "" { formattedFlag = fmt.Sprintf(" %s, %s", shorthand, flagName) } else { formattedFlag = fmt.Sprintf(" %s", flagName) } if strings.HasPrefix(flagUsage, "[limactl create]") { cleanUsage := strings.TrimPrefix(flagUsage, "[limactl create] ") createFlags = append(createFlags, fmt.Sprintf("%-25s %s", formattedFlag, cleanUsage)) } else { allFlags = append(allFlags, fmt.Sprintf("%-25s %s", formattedFlag, flagUsage)) } }) return allFlags, createFlags } func printFlags(allFlags, createFlags []string) { if len(allFlags) > 0 { fmt.Fprint(os.Stdout, "Flags:\n") for _, flag := range allFlags { fmt.Fprintln(os.Stdout, flag) } fmt.Fprint(os.Stdout, "\n") } if len(createFlags) > 0 { fmt.Fprint(os.Stdout, "Flags inherited from `limactl create`:\n") for _, flag := range createFlags { fmt.Fprintln(os.Stdout, flag) } fmt.Fprint(os.Stdout, "\n") } } func printGlobalFlags(cmd *cobra.Command) { if cmd.HasAvailableInheritedFlags() { fmt.Fprintf(cmd.OutOrStdout(), "Global Flags:\n%s", cmd.InheritedFlags().FlagUsages()) } } func loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*limatype.Instance, error) { ctx := cmd.Context() var arg string // can be empty if len(args) > 0 { arg = args[0] } flags := cmd.Flags() // Create an instance, with menu TUI when TTY is available tty, err := flags.GetBool("tty") if err != nil { return nil, err } name, err := flags.GetString("name") if err != nil { return nil, err } if name != "" { err := dirnames.ValidateInstName(name) if err != nil { return nil, err } } if isTemplateURL, templateName := limatmpl.SeemsTemplateURL(arg); isTemplateURL { switch templateName { case "experimental/vz": logrus.Warn("template:experimental/vz was merged into the default template in Lima v1.0. See also .") case "experimental/riscv64": logrus.Warn("template:experimental/riscv64 was merged into the default template in Lima v1.0. Use `limactl create --arch=riscv64 template:default` instead.") case "experimental/armv7l": logrus.Warn("template:experimental/armv7l was merged into the default template in Lima v1.0. Use `limactl create --arch=armv7l template:default` instead.") case "vmnet": logrus.Warn("template:vmnet was removed in Lima v1.0. Use `limactl create --network=lima:shared template:default` instead. See also .") case "experimental/net-user-v2": logrus.Warn("template:experimental/net-user-v2 was removed in Lima v1.0. Use `limactl create --network=lima:user-v2 template:default` instead. See also .") case "experimental/9p": logrus.Warn("template:experimental/9p was removed in Lima v1.0. Use `limactl create --vm-type=qemu --mount-type=9p template:default` instead. See also .") case "experimental/virtiofs-linux": logrus.Warn("template:experimental/virtiofs-linux was removed in Lima v1.0. Use `limactl create --mount-type=virtiofs template:default` instead. See also .") } } if arg == "-" { if name == "" { return nil, errors.New("must pass instance name with --name when reading template from stdin") } // see if the tty was set explicitly or not ttySet := cmd.Flags().Changed("tty") if ttySet && tty { return nil, errors.New("cannot use --tty=true when reading template from stdin") } tty = false } var tmpl *limatmpl.Template if err := dirnames.ValidateInstName(arg); arg == "" || err == nil { tmpl = &limatmpl.Template{Name: name} if arg == "" { if name == "" { tmpl.Name = DefaultInstanceName } } else { logrus.Debugf("interpreting argument %q as an instance name", arg) if name != "" && name != arg { return nil, fmt.Errorf("instance name %q and CLI flag --name=%q cannot be specified together", arg, tmpl.Name) } tmpl.Name = arg } // store.Inspect() will validate the template name (in case it has been set to arg) inst, err := store.Inspect(ctx, tmpl.Name) if err == nil { if createOnly { return nil, fmt.Errorf("instance %q already exists", tmpl.Name) } logrus.Infof("Using the existing instance %q", tmpl.Name) yqExprs, err := editflags.YQExpressions(flags, false) if err != nil { return nil, err } if len(yqExprs) > 0 { yq := yqutil.Join(yqExprs) inst, err = applyYQExpressionToExistingInstance(ctx, inst, yq) if err != nil { return nil, fmt.Errorf("failed to apply yq expression %q to instance %q: %w", yq, tmpl.Name, err) } } return inst, nil } if !errors.Is(err, os.ErrNotExist) { return nil, err } if arg != "" && arg != DefaultInstanceName { logrus.Infof("Creating an instance %q from template:default (Not from template:%s)", tmpl.Name, tmpl.Name) logrus.Warnf("This form is deprecated. Use `limactl create --name=%s template:default` instead", tmpl.Name) } // Read the default template for creating a new instance tmpl.Bytes, err = templatestore.Read(templatestore.Default) if err != nil { return nil, err } } else { tmpl, err = limatmpl.Read(cmd.Context(), name, arg) if err != nil { return nil, err } if createOnly { // store.Inspect() will also validate the instance name if _, err := store.Inspect(ctx, tmpl.Name); err == nil { return nil, fmt.Errorf("instance %q already exists", tmpl.Name) } } else if err := dirnames.ValidateInstName(tmpl.Name); err != nil { return nil, err } } if err := tmpl.Embed(cmd.Context(), true, true); err != nil { return nil, err } if err := tmpl.Unmarshal(); err != nil { return nil, err } if tmpl.Config != nil && tmpl.Config.OS != nil && *tmpl.Config.OS != limatype.LINUX { logrus.Warn("Support for non-Linux guests is experimental") } yqExprs, err := editflags.YQExpressions(flags, true) if err != nil { return nil, err } yq := yqutil.Join(yqExprs) if tty { var err error tmpl, err = chooseNextCreatorState(cmd.Context(), tmpl, yq) if err != nil { return nil, err } } else { logrus.Info("Terminal is not available, proceeding without opening an editor") if err := modifyInPlace(tmpl, yq); err != nil { return nil, err } } saveBrokenYAML := tty return instance.Create(cmd.Context(), tmpl.Name, tmpl.Bytes, saveBrokenYAML) } func applyYQExpressionToExistingInstance(ctx context.Context, inst *limatype.Instance, yq string) (*limatype.Instance, error) { if strings.TrimSpace(yq) == "" { return inst, nil } filePath := filepath.Join(inst.Dir, filenames.LimaYAML) yContent, err := os.ReadFile(filePath) if err != nil { return nil, err } logrus.Debugf("Applying yq expression %q to an existing instance %q", yq, inst.Name) yBytes, err := yqutil.EvaluateExpression(yq, yContent) if err != nil { return nil, err } y, err := limayaml.Load(ctx, yBytes, filePath) if err != nil { return nil, err } if err := driverutil.ResolveVMType(ctx, y, filePath); err != nil { return nil, fmt.Errorf("failed to resolve vm for %q: %w", filePath, err) } if err := limayaml.Validate(y, true); err != nil { rejectedYAML := "lima.REJECTED.yaml" if writeErr := os.WriteFile(rejectedYAML, yBytes, 0o644); writeErr != nil { return nil, fmt.Errorf("the YAML is invalid, attempted to save the buffer as %q but failed: %w: %w", rejectedYAML, writeErr, err) } // TODO: may need to support editing the rejected YAML return nil, fmt.Errorf("the YAML is invalid, saved the buffer as %q: %w", rejectedYAML, err) } if err := os.WriteFile(filePath, yBytes, 0o644); err != nil { return nil, err } // Reload return store.Inspect(ctx, inst.Name) } func modifyInPlace(st *limatmpl.Template, yq string) error { out, err := yqutil.EvaluateExpression(yq, st.Bytes) if err != nil { return err } st.Bytes = out return nil } // exitSuccessError is an error that indicates a successful exit. type exitSuccessError struct { Msg string } // Error implements error. func (e exitSuccessError) Error() string { return e.Msg } // ExitCode implements ExitCoder. func (exitSuccessError) ExitCode() int { return 0 } func chooseNextCreatorState(ctx context.Context, tmpl *limatmpl.Template, yq string) (*limatmpl.Template, error) { for { if err := modifyInPlace(tmpl, yq); err != nil { logrus.WithError(err).Warn("Failed to evaluate yq expression") return tmpl, err } message := fmt.Sprintf("Creating an instance %q", tmpl.Name) options := []string{ "Proceed with the current configuration", "Open an editor to review or modify the current configuration", "Choose another template (docker, podman, archlinux, fedora, ...)", "Exit", } ans, err := uiutil.Select(message, options) if err != nil { if errors.Is(err, uiutil.InterruptErr) { logrus.Fatal("Interrupted by user") } logrus.WithError(err).Warn("Failed to open TUI") return tmpl, nil } switch ans { case 0: // "Proceed with the current configuration" return tmpl, nil case 1: // "Open an editor ..." hdr := fmt.Sprintf("# Review and modify the following configuration for Lima instance %q.\n", tmpl.Name) if tmpl.Name == DefaultInstanceName { hdr += "# - In most cases, you do not need to modify this file.\n" } hdr += "# - To cancel starting Lima, just save this file as an empty file.\n" hdr += "\n" hdr += editutil.GenerateEditorWarningHeader() var err error tmpl.Bytes, err = editutil.OpenEditor(ctx, tmpl.Bytes, hdr) tmpl.Config = nil if err != nil { return tmpl, err } if len(tmpl.Bytes) == 0 { const msg = "Aborting, as requested by saving the file with empty content" logrus.Info(msg) return nil, exitSuccessError{Msg: msg} } err = tmpl.Embed(ctx, true, true) if err != nil { return nil, err } return tmpl, nil case 2: // "Choose another template..." templates, err := filterHiddenTemplates() if err != nil { return tmpl, err } message := "Choose a template" options := make([]string, len(templates)) for i := range templates { options[i] = templates[i].Name } ansEx, err := uiutil.Select(message, options) if err != nil { return tmpl, err } if ansEx > len(templates)-1 { return tmpl, fmt.Errorf("invalid answer %d for %d entries", ansEx, len(templates)) } yamlPath := templates[ansEx].Location if tmpl.Name == "" { tmpl.Name, err = limatmpl.InstNameFromYAMLPath(yamlPath) if err != nil { return nil, err } } tmpl, err = limatmpl.Read(ctx, tmpl.Name, yamlPath) if err != nil { return nil, err } err = tmpl.Embed(ctx, true, true) if err != nil { return nil, err } continue case 3: // "Exit" return nil, exitSuccessError{Msg: "Choosing to exit"} default: return tmpl, fmt.Errorf("unexpected answer %q", ans) } } } // createStartActionCommon is shared by createAction and startAction. func createStartActionCommon(cmd *cobra.Command, _ []string) (exit bool, err error) { if listTemplates, err := cmd.Flags().GetBool("list-templates"); err != nil { return true, err } else if listTemplates { templates, err := filterHiddenTemplates() if err != nil { return true, err } w := cmd.OutOrStdout() for _, f := range templates { _, _ = fmt.Fprintln(w, f.Name) } return true, nil } else if listDrivers, err := cmd.Flags().GetBool("list-drivers"); err != nil { return true, err } else if listDrivers { w := cmd.OutOrStdout() for k := range registry.List() { _, _ = fmt.Fprintln(w, k) } return true, nil } return false, nil } func filterHiddenTemplates() ([]templatestore.Template, error) { templates, err := templatestore.Templates() if err != nil { return nil, err } var filtered []templatestore.Template for _, f := range templates { // Don't show internal base templates like `_default/*` and `_images/*`. if !strings.HasPrefix(f.Name, "_") { filtered = append(filtered, f) } } return filtered, nil } func createAction(cmd *cobra.Command, args []string) error { if exit, err := createStartActionCommon(cmd, args); err != nil { return err } else if exit { return nil } inst, err := loadOrCreateInstance(cmd, args, true) if err != nil { return err } if len(inst.Errors) > 0 { return fmt.Errorf("errors inspecting instance: %+v", inst.Errors) } if _, err = instance.Prepare(cmd.Context(), inst, ""); err != nil { return err } logrus.Infof("Run `limactl start %s` to start the instance.", inst.Name) return nil } func startAction(cmd *cobra.Command, args []string) error { if exit, err := createStartActionCommon(cmd, args); err != nil { return err } else if exit { return nil } inst, err := loadOrCreateInstance(cmd, args, false) if err != nil { return err } if len(inst.Errors) > 0 { return fmt.Errorf("errors inspecting instance: %+v", inst.Errors) } switch inst.Status { case limatype.StatusRunning: logrus.Infof("The instance %q is already running. Run `%s` to open the shell.", inst.Name, instance.LimactlShellCmd(inst.Name)) // Not an error return nil case limatype.StatusStopped: // NOP default: logrus.Warnf("expected status %q, got %q", limatype.StatusStopped, inst.Status) } ctx := cmd.Context() // Network reconciliation will be performed by the process launched by the autostart manager if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) } else if (registered && autostart.AutoStartedIdentifier() != "") || !registered { err = reconcile.Reconcile(ctx, inst.Name) if err != nil { return err } } launchHostAgentForeground := false if runtime.GOOS != "windows" { foreground, err := cmd.Flags().GetBool("foreground") if err != nil { return err } launchHostAgentForeground = foreground } timeout, err := cmd.Flags().GetDuration("timeout") if err != nil { return err } if timeout > 0 { ctx = instance.WithWatchHostAgentTimeout(ctx, timeout) } progress, err := cmd.Flags().GetBool("progress") if err != nil { return err } return instance.Start(ctx, inst, launchHostAgentForeground, progress) } func createBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { return bashCompleteTemplateNames(cmd, toComplete) } func startBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { compInst, _ := bashCompleteInstanceNames(cmd) compTmpl, _ := bashCompleteTemplateNames(cmd, toComplete) return append(compInst, compTmpl...), cobra.ShellCompDirectiveDefault } ================================================ FILE: cmd/limactl/stop.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/networks/reconcile" "github.com/lima-vm/lima/v2/pkg/store" ) func newStopCommand() *cobra.Command { stopCmd := &cobra.Command{ Use: "stop INSTANCE", Short: "Stop an instance", Args: WrapArgsError(cobra.MaximumNArgs(1)), RunE: stopAction, ValidArgsFunction: stopBashComplete, GroupID: basicCommand, } stopCmd.Flags().BoolP("force", "f", false, "Force stop the instance") return stopCmd } func stopAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() instName := DefaultInstanceName if len(args) > 0 { instName = args[0] } inst, err := store.Inspect(ctx, instName) if err != nil { return err } force, err := cmd.Flags().GetBool("force") if err != nil { return err } if force { instance.StopForcibly(inst) } else { err = instance.StopGracefully(ctx, inst, false) } // TODO: should we also reconcile networks if graceful stop returned an error? if err == nil { err = reconcile.Reconcile(ctx, "") } return err } func stopBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/sudoers.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "fmt" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/networks" ) const socketVMNetURL = "https://lima-vm.io/docs/config/network/vmnet/#socket_vmnet" // newSudoersCommand is specific to macOS, but the help message is // compiled on Linux too, as depended by `make docsy`. // https://github.com/lima-vm/lima/issues/3436 func newSudoersCommand() *cobra.Command { sudoersCommand := &cobra.Command{ Use: "sudoers [--check [SUDOERSFILE-TO-CHECK]]", Example: ` To generate the /etc/sudoers.d/lima file: $ limactl sudoers | sudo tee /etc/sudoers.d/lima To validate the existing /etc/sudoers.d/lima file: $ limactl sudoers --check /etc/sudoers.d/lima `, Short: "Generate the content of the /etc/sudoers.d/lima file", Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support (socket_vmnet) on macOS. The content is written to stdout, NOT to the file. This command must not run as the root user. See %s for the usage.`, socketVMNetURL), Args: WrapArgsError(cobra.MaximumNArgs(1)), RunE: sudoersAction, GroupID: advancedCommand, } cfgFile, _ := networks.ConfigFile() sudoersCommand.Flags().Bool("check", false, fmt.Sprintf("check that the sudoers file is up-to-date with %q", cfgFile)) return sudoersCommand } ================================================ FILE: cmd/limactl/sudoers_darwin.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "errors" "fmt" "io" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/networks" ) func sudoersAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() nwCfg, err := networks.LoadConfig() if err != nil { return err } // Make sure the current network configuration is secure if err := nwCfg.Validate(); err != nil { logrus.Infof("Please check %s for more information.", socketVMNetURL) return err } check, err := cmd.Flags().GetBool("check") if err != nil { return err } if check { return verifySudoAccess(ctx, nwCfg, args, cmd.OutOrStdout()) } switch len(args) { case 0: // NOP case 1: return errors.New("the file argument can be specified only for --check mode") default: return fmt.Errorf("unexpected arguments %v", args) } sudoers, err := networks.Sudoers() if err != nil { return err } fmt.Fprint(cmd.OutOrStdout(), sudoers) return nil } func verifySudoAccess(ctx context.Context, nwCfg networks.Config, args []string, stdout io.Writer) error { var file string switch len(args) { case 0: file = nwCfg.Paths.Sudoers if file == "" { cfgFile, _ := networks.ConfigFile() return fmt.Errorf("no sudoers file defined in %q", cfgFile) } case 1: file = args[0] default: return errors.New("can check only a single sudoers file") } if err := nwCfg.VerifySudoAccess(ctx, file); err != nil { return err } fmt.Fprintf(stdout, "%q is up-to-date (or sudo doesn't require a password)\n", file) return nil } ================================================ FILE: cmd/limactl/sudoers_nodarwin.go ================================================ //go:build !darwin // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "github.com/spf13/cobra" ) func sudoersAction(_ *cobra.Command, _ []string) error { return errors.New("sudoers command is only supported on macOS right now") } ================================================ FILE: cmd/limactl/template.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "errors" "fmt" "os" "path/filepath" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/limatmpl" "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/uiutil" "github.com/lima-vm/lima/v2/pkg/yqutil" ) func newTemplateCommand() *cobra.Command { templateCommand := &cobra.Command{ Use: "template", Aliases: []string{"tmpl"}, Short: "Lima template management (EXPERIMENTAL)", SilenceUsage: true, SilenceErrors: true, GroupID: advancedCommand, PreRun: func(*cobra.Command, []string) { logrus.Warn("`limactl template` is experimental") }, } templateCommand.AddCommand( newTemplateCopyCommand(), newTemplateValidateCommand(), newTemplateYQCommand(), newTemplateURLCommand(), ) return templateCommand } // The validate command exists for backwards compatibility, and because the template command is still hidden. func newValidateCommand() *cobra.Command { validateCommand := newTemplateValidateCommand() validateCommand.GroupID = advancedCommand return validateCommand } var templateCopyExample = ` Template locators are local files, file://, https://, or template: URLs # Copy default template to STDOUT limactl template copy template:default - # Copy template from web location to local file and embed all external references # (this does not embed template: references) limactl template copy --embed https://example.com/lima.yaml mighty-machine.yaml ` func newTemplateCopyCommand() *cobra.Command { templateCopyCommand := &cobra.Command{ Use: "copy [OPTIONS] TEMPLATE [DEST]", Short: "Copy template", Long: "Copy a template via locator to a local file or stdout (default)", Example: templateCopyExample, Args: WrapArgsError(cobra.RangeArgs(1, 2)), RunE: templateCopyAction, } templateCopyCommand.Flags().Bool("embed", false, "Embed external dependencies into template") templateCopyCommand.Flags().Bool("embed-all", false, "Embed all dependencies into template") templateCopyCommand.Flags().Bool("fill", false, "Fill defaults") templateCopyCommand.Flags().Bool("verbatim", false, "Don't make locators absolute") return templateCopyCommand } func fillDefaults(ctx context.Context, tmpl *limatmpl.Template) error { limaDir, err := dirnames.LimaDir() if err != nil { return err } // Load() will merge the template with override.yaml and default.yaml via FillDefaults(). // FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}. filePath := filepath.Join(limaDir, tmpl.Name+".yaml") tmpl.Config, err = limayaml.Load(ctx, tmpl.Bytes, filePath) if err == nil { tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false) } if err := driverutil.ResolveVMType(ctx, tmpl.Config, filePath); err != nil { logrus.Warnf("failed to resolve VM type for %q: %v", filePath, err) return nil } return err } func templateCopyAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() source := args[0] target := "-" if len(args) > 1 { target = args[1] } embed, err := cmd.Flags().GetBool("embed") if err != nil { return err } embedAll, err := cmd.Flags().GetBool("embed-all") if err != nil { return err } fill, err := cmd.Flags().GetBool("fill") if err != nil { return err } verbatim, err := cmd.Flags().GetBool("verbatim") if err != nil { return err } if fill { embedAll = true } if embedAll { embed = true } if embed && verbatim { return errors.New("--verbatim cannot be used with any of --embed, --embed-all, or --fill") } tmpl, err := limatmpl.Read(cmd.Context(), "", source) if err != nil { return err } if len(tmpl.Bytes) == 0 { return fmt.Errorf("don't know how to interpret %q as a template locator", source) } if !verbatim { if embed { // Embed default base.yaml only when fill is true. if err := tmpl.Embed(cmd.Context(), embedAll, fill); err != nil { return err } } else { if err := tmpl.UseAbsLocators(); err != nil { return err } } } if fill { if err := fillDefaults(ctx, tmpl); err != nil { return err } } if target == "-" && uiutil.OutputIsTTY(cmd.OutOrStdout()) { // run the output through YQ to colorize it out, err := yqutil.EvaluateExpressionPlain(".", string(tmpl.Bytes), true) if err == nil { _, err = fmt.Fprint(cmd.OutOrStdout(), out) } return err } writer := cmd.OutOrStdout() if target != "-" { file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } defer file.Close() writer = file } _, err = fmt.Fprint(writer, string(tmpl.Bytes)) return err } const templateYQHelp = `Use the builtin YQ evaluator to extract information from a template. External references are embedded and default values are filled in before the YQ expression is evaluated. Example: limactl template yq template:default '.images[].location' The example command is equivalent to using an external yq command like this: limactl template copy --fill template:default - | yq '.images[].location' ` func newTemplateYQCommand() *cobra.Command { templateYQCommand := &cobra.Command{ Use: "yq TEMPLATE EXPR", Short: "Query template expressions", Long: templateYQHelp, Args: WrapArgsError(cobra.ExactArgs(2)), RunE: templateYQAction, } return templateYQCommand } func templateYQAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() locator := args[0] expr := args[1] tmpl, err := limatmpl.Read(cmd.Context(), "", locator) if err != nil { return err } if len(tmpl.Bytes) == 0 { return fmt.Errorf("don't know how to interpret %q as a template locator", locator) } if err := tmpl.Embed(cmd.Context(), true, true); err != nil { return err } if err := fillDefaults(ctx, tmpl); err != nil { return err } colorsEnabled := uiutil.OutputIsTTY(cmd.OutOrStdout()) out, err := yqutil.EvaluateExpressionPlain(expr, string(tmpl.Bytes), colorsEnabled) if err == nil { _, err = fmt.Fprint(cmd.OutOrStdout(), out) } return err } func newTemplateValidateCommand() *cobra.Command { templateValidateCommand := &cobra.Command{ Use: "validate TEMPLATE [TEMPLATE, ...]", Short: "Validate YAML templates", Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: templateValidateAction, } templateValidateCommand.Flags().Bool("fill", false, "Fill defaults") return templateValidateCommand } func templateValidateAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() fill, err := cmd.Flags().GetBool("fill") if err != nil { return err } limaDir, err := dirnames.LimaDir() if err != nil { return err } for _, arg := range args { tmpl, err := limatmpl.Read(cmd.Context(), "", arg) if err != nil { return err } if len(tmpl.Bytes) == 0 { return fmt.Errorf("don't know how to interpret %q as a template locator", arg) } if tmpl.Name == "" { return fmt.Errorf("can't determine instance name from template locator %q", arg) } // Embed default base.yaml only when fill is true. if err := tmpl.Embed(cmd.Context(), true, fill); err != nil { return err } // Load() will merge the template with override.yaml and default.yaml via FillDefaults(). // FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}. filePath := filepath.Join(limaDir, tmpl.Name+".yaml") y, err := limayaml.Load(ctx, tmpl.Bytes, filePath) if err != nil { return err } if err := driverutil.ResolveVMType(ctx, y, filePath); err != nil { logrus.Warnf("failed to resolve VM type for %q: %v", filePath, err) } if err := limayaml.Validate(y, false); err != nil { return fmt.Errorf("failed to validate YAML file %q: %w", arg, err) } logrus.Infof("%q: OK", arg) if fill { b, err := limayaml.Marshal(y, len(args) > 1) if err != nil { return fmt.Errorf("failed to marshal template %q again after filling defaults: %w", arg, err) } fmt.Fprint(cmd.OutOrStdout(), string(b)) } } return nil } func newTemplateURLCommand() *cobra.Command { templateURLCommand := &cobra.Command{ Use: "url CUSTOM_URL", Short: "Transform custom template URLs to regular file or https URLs", Args: WrapArgsError(cobra.ExactArgs(1)), RunE: templateURLAction, } return templateURLCommand } func templateURLAction(cmd *cobra.Command, args []string) error { url, err := limatmpl.TransformCustomURL(cmd.Context(), args[0]) if err != nil { return err } _, err = fmt.Fprintln(cmd.OutOrStdout(), url) return err } ================================================ FILE: cmd/limactl/tunnel.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "os/exec" "runtime" "strconv" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/freeport" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/sshutil" "github.com/lima-vm/lima/v2/pkg/store" ) const tunnelHelp = `Create a tunnel for Lima Create a SOCKS tunnel so that the host can join the guest network. ` func newTunnelCommand() *cobra.Command { tunnelCmd := &cobra.Command{ Use: "tunnel [flags] INSTANCE", Short: "Create a tunnel for Lima", PersistentPreRun: func(*cobra.Command, []string) { logrus.Warn("`limactl tunnel` is experimental") }, Long: tunnelHelp, Args: WrapArgsError(cobra.ExactArgs(1)), RunE: tunnelAction, ValidArgsFunction: tunnelBashComplete, SilenceErrors: true, GroupID: advancedCommand, } tunnelCmd.Flags().SetInterspersed(false) // TODO: implement l2tp, ikev2, masque, ... tunnelCmd.Flags().String("type", "socks", "Tunnel type, currently only \"socks\" is implemented") tunnelCmd.Flags().Int("socks-port", 0, "SOCKS port, defaults to a random port") return tunnelCmd } func tunnelAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() flags := cmd.Flags() tunnelType, err := flags.GetString("type") if err != nil { return err } if tunnelType != "socks" { return fmt.Errorf("unknown tunnel type: %q", tunnelType) } port, err := flags.GetInt("socks-port") if err != nil { return err } if port != 0 && (port < 1024 || port > 65535) { return fmt.Errorf("invalid socks port %d", port) } stdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr() instName := args[0] inst, err := store.Inspect(ctx, instName) if err != nil { if errors.Is(err, os.ErrNotExist) { return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName) } return err } if inst.Status == limatype.StatusStopped { return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName) } if port == 0 { port, err = freeport.TCP() if err != nil { return err } } sshExe, err := sshutil.NewSSHExe() if err != nil { return err } sshOpts, err := sshutil.SSHOpts( ctx, sshExe, inst.Dir, *inst.Config.User.Name, *inst.Config.SSH.LoadDotSSHPubKeys, *inst.Config.SSH.ForwardAgent, *inst.Config.SSH.ForwardX11, *inst.Config.SSH.ForwardX11Trusted) if err != nil { return err } sshArgs := append([]string{}, sshExe.Args...) sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...) sshArgs = append(sshArgs, []string{ "-q", // quiet "-f", // background "-N", // no command "-D", fmt.Sprintf("127.0.0.1:%d", port), "-p", strconv.Itoa(inst.SSHLocalPort), inst.SSHAddress, }...) sshCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...) sshCmd.Stdout = stderr sshCmd.Stderr = stderr logrus.Debugf("executing ssh (may take a long)): %+v", sshCmd.Args) if err := sshCmd.Run(); err != nil { return err } switch runtime.GOOS { case "darwin": fmt.Fprint(stdout, "Open (or whatever) →
,\n") fmt.Fprint(stdout, "and specify the following configuration:\n") fmt.Fprint(stdout, "- Server: 127.0.0.1\n") fmt.Fprintf(stdout, "- Port: %d\n", port) case "windows": fmt.Fprint(stdout, "Open ,\n") fmt.Fprint(stdout, "and specify the following configuration:\n") fmt.Fprint(stdout, "- Address: socks=127.0.0.1\n") fmt.Fprintf(stdout, "- Port: %d\n", port) default: fmt.Fprintf(stdout, "Set `ALL_PROXY=socks5h://127.0.0.1:%d`, etc.\n", port) } fmt.Fprintf(stdout, "The instance can be connected from the host as via a web browser.\n", inst.Hostname) // TODO: show the port in `limactl list --json` ? // TODO: add `--stop` flag to shut down the tunnel return nil } func tunnelBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/unprotect.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/store" ) func newUnprotectCommand() *cobra.Command { unprotectCommand := &cobra.Command{ Use: "unprotect INSTANCE [INSTANCE, ...]", Short: "Unprotect an instance", Args: WrapArgsError(cobra.MinimumNArgs(1)), RunE: unprotectAction, ValidArgsFunction: unprotectBashComplete, GroupID: advancedCommand, } return unprotectCommand } func unprotectAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() var errs []error for _, instName := range args { inst, err := store.Inspect(ctx, instName) if err != nil { errs = append(errs, fmt.Errorf("failed to inspect instance %q: %w", instName, err)) continue } if !inst.Protected { logrus.Warnf("Instance %q isn't protected. Skipping.", instName) continue } if err := inst.Unprotect(); err != nil { errs = append(errs, fmt.Errorf("failed to unprotect instance %q: %w", instName, err)) continue } logrus.Infof("Unprotected %q", instName) } return errors.Join(errs...) } func unprotectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl/usernet.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "errors" "fmt" "os" "os/signal" "strconv" "syscall" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/networks/usernet" ) func newUsernetCommand() *cobra.Command { hostagentCommand := &cobra.Command{ Use: "usernet", Short: "Run usernet", Args: cobra.ExactArgs(0), RunE: usernetAction, Hidden: true, } hostagentCommand.Flags().StringP("pidfile", "p", "", "Write PID to file") hostagentCommand.Flags().StringP("endpoint", "e", "", "Exposes usernet API(s) on this endpoint") hostagentCommand.Flags().String("listen-qemu", "", "Listen for QMEU connections") hostagentCommand.Flags().String("listen", "", "Listen on a Unix socket and receive Bess-compatible FDs as SCM_RIGHTS messages") hostagentCommand.Flags().String("subnet", "192.168.5.0/24", "Sets subnet value for the usernet network") hostagentCommand.Flags().Int("mtu", 1500, "mtu") hostagentCommand.Flags().StringToString("leases", nil, "Pass default static leases for startup. Eg: '192.168.104.1=52:55:55:b3:bc:d9,192.168.104.2=5a:94:ef:e4:0c:df' ") return hostagentCommand } func usernetAction(cmd *cobra.Command, _ []string) error { pidfile, err := cmd.Flags().GetString("pidfile") if err != nil { return err } if pidfile != "" { if _, err := os.Stat(pidfile); !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("pidfile %q already exists", pidfile) } if err := os.WriteFile(pidfile, []byte(strconv.Itoa(os.Getpid())+"\n"), 0o644); err != nil { return err } defer os.RemoveAll(pidfile) } endpoint, err := cmd.Flags().GetString("endpoint") if err != nil { return err } qemuSocket, err := cmd.Flags().GetString("listen-qemu") if err != nil { return err } fdSocket, err := cmd.Flags().GetString("listen") if err != nil { return err } subnet, err := cmd.Flags().GetString("subnet") if err != nil { return err } leases, err := cmd.Flags().GetStringToString("leases") if err != nil { return err } mtu, err := cmd.Flags().GetInt("mtu") if err != nil { return err } os.RemoveAll(endpoint) os.RemoveAll(qemuSocket) os.RemoveAll(fdSocket) ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) defer cancel() // Environment Variables // LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT: Specifies the timeout duration for resolving IP addresses in minutes. Default is 2 minutes. return usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{ MTU: mtu, Endpoint: endpoint, QemuSocket: qemuSocket, FdSocket: fdSocket, Subnet: subnet, DefaultLeases: leases, }) } ================================================ FILE: cmd/limactl/watch.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "sync" "time" "github.com/rjeczalik/notify" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/hostagent/events" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/store" ) func newWatchCommand() *cobra.Command { watchCommand := &cobra.Command{ Use: "watch [INSTANCE]...", Short: "Watch events from instances", Long: `Watch events from Lima instances. Events include status changes (starting, running, stopping), port forwarding events, and other instance lifecycle events. If no instance is specified, events from all instances are watched, including newly created instances. The command will continue watching until interrupted (Ctrl+C).`, Example: ` # Watch events from all instances: $ limactl watch # Watch events from a specific instance: $ limactl watch default # Include historical events: $ limactl watch --history default # Show verbose output (host agent logs, etc.): $ limactl watch --verbose # Watch events in JSON format (for scripting): $ limactl watch --json default`, Args: WrapArgsError(cobra.ArbitraryArgs), RunE: watchAction, ValidArgsFunction: watchBashComplete, GroupID: advancedCommand, } watchCommand.Flags().Bool("json", false, "Output events as newline-delimited JSON") watchCommand.Flags().Bool("history", false, "Include historical events from before watch started") watchCommand.Flags().Bool("verbose", false, "Show verbose output") return watchCommand } type watchEvent struct { Instance string `json:"instance"` Event events.Event `json:"event"` } type eventWatcher struct { ctx context.Context begin time.Time propagateStderr bool eventCh chan watchEvent watching sync.Map } func (w *eventWatcher) startInstance(instName string) { if _, loaded := w.watching.LoadOrStore(instName, true); loaded { return } inst, err := store.Inspect(w.ctx, instName) if err != nil { logrus.WithError(err).Warnf("Failed to inspect instance %q", instName) w.watching.Delete(instName) return } haStdoutPath := filepath.Join(inst.Dir, filenames.HostAgentStdoutLog) haStderrPath := filepath.Join(inst.Dir, filenames.HostAgentStderrLog) go w.watchInstance(instName, haStdoutPath, haStderrPath) } func (w *eventWatcher) watchInstance(instName, haStdoutPath, haStderrPath string) { err := events.Watch(w.ctx, haStdoutPath, haStderrPath, w.begin, w.propagateStderr, func(ev events.Event) bool { select { case w.eventCh <- watchEvent{Instance: instName, Event: ev}: case <-w.ctx.Done(): return true } return false }) if err != nil && w.ctx.Err() == nil { logrus.WithError(err).Warnf("Watcher for instance %q stopped", instName) } } func watchAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() jsonFormat, err := cmd.Flags().GetBool("json") if err != nil { return err } history, err := cmd.Flags().GetBool("history") if err != nil { return err } verbose, err := cmd.Flags().GetBool("verbose") if err != nil { return err } if !verbose { logrus.SetLevel(logrus.WarnLevel) } var begin time.Time if !history { begin = time.Now() } stdout := cmd.OutOrStdout() stderr := cmd.ErrOrStderr() watchAll := len(args) == 0 var instNames []string if watchAll { instNames, err = store.Instances() if err != nil { return err } if len(instNames) == 0 { printStatus(stderr, "No instances found") } } else { instNames = args } newInstanceCh := make(chan string, 16) w := &eventWatcher{ ctx: ctx, begin: begin, propagateStderr: verbose, eventCh: make(chan watchEvent, 64), } for _, instName := range instNames { w.startInstance(instName) } if watchAll { go watchLimaDir(ctx, newInstanceCh) } printStatus(stderr, "Watching for events...") for { select { case <-ctx.Done(): return nil case instName := <-newInstanceCh: printStatus(stderr, "New instance detected: "+instName) w.startInstance(instName) case ev := <-w.eventCh: if jsonFormat { j, err := json.Marshal(ev) if err != nil { fmt.Fprintf(stderr, "error marshaling event: %v\n", err) continue } fmt.Fprintln(stdout, string(j)) } else { printHumanReadableEvent(stdout, ev.Instance, ev.Event) } } } } func watchLimaDir(ctx context.Context, newInstanceCh chan<- string) { limaDir := store.Directory() if limaDir == "" { logrus.Warn("Could not determine lima directory") return } fsEvents := make(chan notify.EventInfo, 128) if err := notify.Watch(limaDir, fsEvents, notify.Create); err != nil { logrus.WithError(err).Warn("Failed to watch lima directory for new instances") return } defer notify.Stop(fsEvents) for { select { case <-ctx.Done(): return case ev := <-fsEvents: name := filepath.Base(ev.Path()) if !isValidInstanceName(name) { continue } if !isInstanceDir(ev.Path()) { continue } select { case newInstanceCh <- name: case <-ctx.Done(): return } } } } func isValidInstanceName(name string) bool { return !strings.HasPrefix(name, ".") && !strings.HasPrefix(name, "_") } func isInstanceDir(path string) bool { info, err := os.Stat(path) if err != nil || !info.IsDir() { return false } yamlPath := filepath.Join(path, filenames.LimaYAML) _, err = os.Stat(yamlPath) return err == nil } func printStatus(out io.Writer, msg string) { fmt.Fprintf(out, "%s %s\n", time.Now().Format("2006-01-02 15:04:05"), msg) } func printHumanReadableEvent(out io.Writer, instName string, ev events.Event) { timestamp := ev.Time.Format("2006-01-02 15:04:05") printEvent := func(msg string) { fmt.Fprintf(out, "%s %s | %s\n", timestamp, instName, msg) } if ev.Status.Running { if ev.Status.Degraded { printEvent("running (degraded)") } else { printEvent("running") } } if ev.Status.Exiting { printEvent("exiting") } if ev.Status.SSHLocalPort != 0 { printEvent(fmt.Sprintf("ssh available on port %d", ev.Status.SSHLocalPort)) } for _, e := range ev.Status.Errors { printEvent(fmt.Sprintf("error: %s", e)) } if ev.Status.CloudInitProgress != nil { if ev.Status.CloudInitProgress.Completed { printEvent("cloud-init completed") } else if ev.Status.CloudInitProgress.LogLine != "" { printEvent(fmt.Sprintf("cloud-init: %s", ev.Status.CloudInitProgress.LogLine)) } } if ev.Status.PortForward != nil { pf := ev.Status.PortForward switch pf.Type { case events.PortForwardEventForwarding: printEvent(fmt.Sprintf("forwarding %s %s to %s", pf.Protocol, pf.GuestAddr, pf.HostAddr)) case events.PortForwardEventNotForwarding: printEvent(fmt.Sprintf("not forwarding %s %s", pf.Protocol, pf.GuestAddr)) case events.PortForwardEventStopping: printEvent(fmt.Sprintf("stopping forwarding %s %s", pf.Protocol, pf.GuestAddr)) case events.PortForwardEventFailed: printEvent(fmt.Sprintf("failed to forward %s %s: %s", pf.Protocol, pf.GuestAddr, pf.Error)) } } if ev.Status.Vsock != nil { vs := ev.Status.Vsock switch vs.Type { case events.VsockEventStarted: printEvent(fmt.Sprintf("started vsock forwarder: %s -> vsock:%d", vs.HostAddr, vs.VsockPort)) case events.VsockEventSkipped: printEvent(fmt.Sprintf("skipped vsock forwarder: %s", vs.Reason)) case events.VsockEventFailed: printEvent(fmt.Sprintf("failed to start vsock forwarder: %s", vs.Reason)) } } } func watchBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return bashCompleteInstanceNames(cmd) } ================================================ FILE: cmd/limactl-mcp/main.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package main import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "runtime" "strings" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/text/cases" "golang.org/x/text/language" "github.com/lima-vm/lima/v2/pkg/limactlutil" "github.com/lima-vm/lima/v2/pkg/mcp/toolset" "github.com/lima-vm/lima/v2/pkg/version" ) func main() { if err := newApp().Execute(); err != nil { logrus.Fatal(err) } } func newApp() *cobra.Command { cmd := &cobra.Command{ Use: "limactl-mcp", Short: "Model Context Protocol plugin for Lima (EXPERIMENTAL)", Version: strings.TrimPrefix(version.Version, "v"), SilenceUsage: true, SilenceErrors: true, } cmd.AddCommand( newMcpInfoCommand(), newMcpServeCommand(), newMcpGenDocCommand(), // TODO: `limactl-mcp configure gemini` ? ) return cmd } func newServer() *mcp.Server { impl := &mcp.Implementation{ Name: "lima", Title: "Lima VM, for sandboxing local command executions and file I/O operations", Version: version.Version, } serverOpts := &mcp.ServerOptions{ Instructions: `This MCP server provides tools for sandboxing local command executions and file I/O operations, by wrapping them in Lima VM (https://lima-vm.io). Use these tools to avoid accidentally executing malicious codes directly on the host. `, } if runtime.GOOS != "linux" { serverOpts.Instructions += fmt.Sprintf(` NOTE: the guest OS of the VM is Linux, while the host OS is %s. `, cases.Title(language.English).String(runtime.GOOS)) } return mcp.NewServer(impl, serverOpts) } func newMcpInfoCommand() *cobra.Command { cmd := &cobra.Command{ Use: "info", Short: "Show information about the MCP server", Args: cobra.NoArgs, RunE: mcpInfoAction, } return cmd } func mcpInfoAction(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() info, err := inspectInfo(ctx) if err != nil { return err } j, err := json.MarshalIndent(info, "", " ") if err != nil { return err } _, err = fmt.Fprint(cmd.OutOrStdout(), string(j)) return err } func inspectInfo(ctx context.Context) (*Info, error) { ts, err := toolset.New("") if err != nil { return nil, err } server := newServer() if err = ts.RegisterServer(server); err != nil { return nil, err } serverTransport, clientTransport := mcp.NewInMemoryTransports() serverSession, err := server.Connect(ctx, serverTransport, nil) if err != nil { return nil, err } client := mcp.NewClient(&mcp.Implementation{Name: "client"}, nil) clientSession, err := client.Connect(ctx, clientTransport, nil) if err != nil { return nil, err } toolsResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{}) if err != nil { return nil, err } if err = clientSession.Close(); err != nil { return nil, err } if err = serverSession.Wait(); err != nil { return nil, err } info := &Info{ Tools: toolsResult.Tools, } return info, nil } type Info struct { Tools []*mcp.Tool `json:"tools"` } func newMcpServeCommand() *cobra.Command { cmd := &cobra.Command{ Use: "serve INSTANCE", Short: "Serve MCP over stdio", Long: `Serve MCP over stdio. Expected to be executed via an AI agent, not by a human`, Args: cobra.MaximumNArgs(1), RunE: mcpServeAction, } return cmd } func mcpServeAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() instName := "default" if len(args) > 0 { instName = args[0] } limactl, err := limactlutil.Path() if err != nil { return err } // FIXME: We can not use store.Inspect() here because it requires VM drivers to be compiled in. // https://github.com/lima-vm/lima/pull/3744#issuecomment-3289274347 inst, err := limactlutil.Inspect(ctx, limactl, instName) if err != nil { return err } if len(inst.Errors) != 0 { return errors.Join(inst.Errors...) } ts, err := toolset.New(limactl) if err != nil { return err } server := newServer() if err = ts.RegisterServer(server); err != nil { return err } if err = ts.RegisterInstance(ctx, inst); err != nil { return err } transport := &mcp.StdioTransport{} return server.Run(ctx, transport) } func newMcpGenDocCommand() *cobra.Command { cmd := &cobra.Command{ Use: "generate-doc DIR", Short: "Generate documentation pages", Args: cobra.MinimumNArgs(1), RunE: mcpGenDocAction, Hidden: true, } return cmd } func mcpGenDocAction(cmd *cobra.Command, args []string) error { ctx := cmd.Context() dir := args[0] if err := os.MkdirAll(dir, 0o755); err != nil { return err } fName := filepath.Join(dir, "mcp.md") f, err := os.Create(fName) if err != nil { return err } defer f.Close() fmt.Fprint(f, `--- title: MCP tools weight: 99 --- Lima implements the "MCP Sandbox Interface" (tentative name): https://pkg.go.dev/github.com/lima-vm/lima/v2/pkg/mcp/msi MCP Sandbox Interface defines MCP (Model Context Protocol) tools that can be used for reading, writing, and executing local files with an appropriate sandboxing technology, such as Lima. The sandboxing technology can be more secure and/or efficient than the default tools provided by an AI agent. MCP Sandbox Interface was inspired by [Google Gemini CLI's built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools). `) info, err := inspectInfo(ctx) if err != nil { return err } for _, tool := range info.Tools { fmt.Fprintf(f, "## `%s`\n\n", tool.Name) if tool.Title != "" { fmt.Fprintf(f, "### Title\n\n%s\n\n", tool.Title) } if tool.Description != "" { fmt.Fprintf(f, "### Description\n\n%s\n\n", tool.Description) } if tool.InputSchema != nil { fmt.Fprint(f, "### Input Schema\n\n") schema, err := json.MarshalIndent(tool.InputSchema, "", " ") if err != nil { return err } fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema)) } if tool.OutputSchema != nil { fmt.Fprint(f, "### Output Schema\n\n") schema, err := json.MarshalIndent(tool.OutputSchema, "", " ") if err != nil { return err } fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema)) } } return f.Close() } ================================================ FILE: cmd/limactl-url-fedora-rawhide ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu if [ "$#" -ne 1 ]; then echo >&2 "Usage: $0 _images/fedora-rawhide" exit 1 fi if [ "$1" != "_images/fedora-rawhide" ]; then echo >&2 "Expected argument to be '_images/fedora-rawhide', but got '$1'" exit 1 fi CACHE_HOME_DEFAULT="${HOME}/.cache" if [ "$(uname -s)" = "Darwin" ]; then CACHE_HOME_DEFAULT="${HOME}/Library/Caches" fi : "${XDG_CACHE_HOME:=${CACHE_HOME_DEFAULT}}" CACHE_DIR="${XDG_CACHE_HOME}/lima/limactl-url-fedora-rawhide" # COMPOSE_ID is like "Fedora-Rawhide-20260316.n.0" COMPOSE_ID="$(curl -fsSL https://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/COMPOSE_ID)" # COMPOSE_ID_TRIMMED is like "20260316.n.0" COMPOSE_ID_TRIMMED="${COMPOSE_ID#Fedora-Rawhide-}" mkdir -p "${CACHE_DIR}" FILE="${CACHE_DIR}/fedora-rawhide.yaml" cat <"${FILE}" images: - location: "https://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-Rawhide-${COMPOSE_ID_TRIMMED}.x86_64.qcow2" arch: "x86_64" - location: "https://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-Rawhide-${COMPOSE_ID_TRIMMED}.aarch64.qcow2" arch: "aarch64" EOF echo "$FILE" ================================================ FILE: cmd/nerdctl.lima ================================================ #!/bin/sh set -eu # Environment Variables # LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is "default". : "${LIMA_INSTANCE:=default}" # Use --preserve-env to pass through environment variables from host machine into guest instance exec limactl shell --preserve-env "$LIMA_INSTANCE" nerdctl "$@" ================================================ FILE: cmd/podman.lima ================================================ #!/bin/sh set -eu : "${LIMA_INSTANCE:=podman}" : "${PODMAN:=podman}" if [ "$(limactl ls -q "$LIMA_INSTANCE" 2>/dev/null)" != "$LIMA_INSTANCE" ]; then echo "instance \"$LIMA_INSTANCE\" does not exist, run \`limactl create --name=$LIMA_INSTANCE template:podman\` to create a new instance" >&2 exit 1 elif [ "$(limactl ls -f '{{ .Status }}' "$LIMA_INSTANCE" 2>/dev/null)" != "Running" ]; then echo "instance \"$LIMA_INSTANCE\" is not running, run \`limactl start $LIMA_INSTANCE\` to start the existing instance" >&2 exit 1 fi PODMAN=$(command -v "$PODMAN" || true) if [ -n "$PODMAN" ]; then CONTAINER_HOST=$(limactl list "$LIMA_INSTANCE" --format 'unix://{{.Dir}}/sock/podman.sock') export CONTAINER_HOST exec "$PODMAN" --remote "$@" else export LIMA_INSTANCE exec lima podman "$@" fi ================================================ FILE: cmd/yq/yq.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright (c) 2017 Mike Farah // This file has been adapted from https://github.com/mikefarah/yq/blob/v4.47.1/yq.go package yq import ( "os" "path/filepath" "strings" command "github.com/mikefarah/yq/v4/cmd" ) func main() { cmd := command.New() args := os.Args[1:] _, _, err := cmd.Find(args) if err != nil && args[0] != "__complete" { // default command when nothing matches... newArgs := []string{"eval"} cmd.SetArgs(append(newArgs, os.Args[1:]...)) } code := 0 if err := cmd.Execute(); err != nil { code = 1 } os.Exit(code) } // MaybeRunYQ runs as `yq` if the program name or first argument is `yq`. // Only returns to caller if os.Args doesn't contain a `yq` command. func MaybeRunYQ() { progName := filepath.Base(os.Args[0]) // remove all extensions, so we match "yq.lima.exe" progName, _, _ = strings.Cut(progName, ".") if progName == "yq" { main() } if len(os.Args) > 1 && os.Args[1] == "yq" { os.Args = os.Args[1:] main() } } ================================================ FILE: docs/README.md ================================================ Moved to ================================================ FILE: go.mod ================================================ // gomodjail:confined module github.com/lima-vm/lima/v2 go 1.25.7 require ( al.essio.dev/pkg/shellescape v1.6.0 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Code-Hex/vz/v3 v3.7.1 // gomodjail:unconfined github.com/Microsoft/go-winio v0.6.2 // gomodjail:unconfined github.com/apparentlymart/go-cidr v1.1.0 github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e github.com/cheggaaa/pb/v3 v3.1.7 // gomodjail:unconfined github.com/cilium/ebpf v0.21.0 // gomodjail:unconfined github.com/containerd/continuity v0.4.5 github.com/containers/gvisor-tap-vsock v0.8.8 // gomodjail:unconfined github.com/coreos/go-semver v0.3.1 github.com/coreos/go-systemd/v22 v22.7.0 github.com/cpuguy83/go-md2man/v2 v2.0.7 github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 github.com/diskfs/go-diskfs v1.8.0 // gomodjail:unconfined github.com/docker/go-units v0.5.0 github.com/foxcpp/go-mockdns v1.2.0 github.com/goccy/go-yaml v1.19.2 github.com/google/go-cmp v0.7.0 github.com/google/yamlfmt v0.21.0 github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 github.com/invopop/jsonschema v0.13.0 github.com/lima-vm/go-qcow2reader v0.7.1 github.com/lima-vm/sshocker v0.3.9 // gomodjail:unconfined github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-shellwords v1.0.12 github.com/mdlayher/netlink v1.9.0 github.com/mdlayher/vsock v1.2.1 // gomodjail:unconfined github.com/miekg/dns v1.1.72 // gomodjail:unconfined github.com/mikefarah/yq/v4 v4.52.4 github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/nxadm/tail v1.4.11 // gomodjail:unconfined github.com/opencontainers/go-digest v1.0.0 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pkg/sftp v1.13.10 github.com/rjeczalik/notify v0.9.3 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sethvargo/go-password v0.3.1 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 // gomodjail:unconfined github.com/spf13/pflag v1.0.10 github.com/wk8/go-ordered-map/v2 v2.1.8 golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 golang.org/x/sys v0.42.0 // gomodjail:unconfined golang.org/x/text v0.35.0 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 // gomodjail:unconfined gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 gotest.tools/v3 v3.5.2 ) require ( github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/a8m/envsubst v1.4.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/alecthomas/participle/v2 v2.1.4 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect github.com/buger/jsonparser v1.1.2 // indirect github.com/containerd/log v0.1.0 // indirect github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/djherbis/times v1.6.0 // indirect github.com/elliotchance/orderedmap v1.8.0 // indirect github.com/fatih/color v1.18.0 // indirect // gomodjail:unconfined github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect // gomodjail:unconfined github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/fs v0.1.0 // indirect github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mdlayher/socket v0.5.1 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.5.4 // indirect // gomodjail:unconfined github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect github.com/zclconf/go-cty v1.17.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect golang.org/x/crypto v0.49.0 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect // gomodjail:unconfined gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f // indirect ) ================================================ FILE: go.sum ================================================ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1pFAqvnP87Zh0Nw= github.com/Code-Hex/go-infinity-channel v1.0.0/go.mod h1:5yUVg/Fqao9dAjcpzoQ33WwfdMWmISOrQloDRn3bsvY= github.com/Code-Hex/vz/v3 v3.7.1 h1:EN1yNiyrbPq+dl388nne2NySo8I94EnPppvqypA65XM= github.com/Code-Hex/vz/v3 v3.7.1/go.mod h1:1LsW0jqW0r0cQ+IeR4hHbjdqOtSidNCVMWhStMHGho8= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e h1:IdMhFPEfTZQU971tIHx3UhY4l+yCeynprnINrDTSrOc= github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e/go.mod h1:aXGMJsd3XrnUFTuyf/pTGg5jG6CY8JMZ5juywvShjgQ= github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI= github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ= github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containers/gvisor-tap-vsock v0.8.8 h1:5FznbOYMIuaCv8B6zQ7M6wjqP63Lasy0A6GpViEnjTg= github.com/containers/gvisor-tap-vsock v0.8.8/go.mod h1:m/PzhZWAS6T9pCRH1fLkq2OqbEd6QEUZWjm3FS5F+CE= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e h1:SCnqm8SjSa0QqRxXbo5YY//S+OryeJioe17nK+iDZpg= github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e/go.mod h1:o129ljs6alsIQTc8d6eweihqpmmrbxZ2g1jhgjhPykI= github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 h1:3OVJAbR131SnAXao7c9w8bFlAGH0oa29DCwsa88MJGk= github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7/go.mod h1:K4+o74YGNjOb9N6yyG+LPj1NjHtk+Qz0IYQPvirbaLs= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/diskfs/go-diskfs v1.8.0 h1:YynvepHpU7xpl8z2RqiV/x3NPAj3zU0ki0vdjPty0U4= github.com/diskfs/go-diskfs v1.8.0/go.mod h1:rW9+4MPN1tbMpQqRZlcM3YQsh3Ucc+Q1k1iIqzzmZcg= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw= github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY= github.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/yamlfmt v0.21.0 h1:9FKApQkDpMKgBjwLFytBHUCgqnQgxaQnci0uiESfbzs= github.com/google/yamlfmt v0.21.0/go.mod h1:q6FYExB+Ueu7jZDjKECJk+EaeDXJzJ6Ne0dxx69GWfI= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 h1:jaqViOFFlZtkAwqvwZN+id37fosQqR5l3Oki9Dk4hz8= github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI= github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 h1:LZJWucZz7ztCqY6Jsu7N9g124iJ2kt/O62j3+UchZFg= github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lima-vm/go-qcow2reader v0.7.1 h1:fZ5u38uaRX3ukuVA6IpeImh9BfRhzRGvTr87yGqENbY= github.com/lima-vm/go-qcow2reader v0.7.1/go.mod h1:Ai9fcmE2dXF6YFomrSCttveOT1x3+x5eG7AfIUUnSqw= github.com/lima-vm/sshocker v0.3.9 h1:jA1uLM8GS74ZI6lJ9f/SmQhxuNSQbY44TmrHXrNaVR4= github.com/lima-vm/sshocker v0.3.9/go.mod h1:YU7BBIy8iCJm5F68qwA+XCLOFJH5cMj8NsRUppKA33Y= github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y= github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco= github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg= github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mikefarah/yq/v4 v4.52.4 h1:wZlxBMjyKCzzQjL0u6a3zToKuyE7OdJr4OtLBtwph4Q= github.com/mikefarah/yq/v4 v4.52.4/go.mod h1:8QwgSgDsmt4LCbfwvGUAh5oWSukRRuVJ8Gj98zJ/45o= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU= github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k= gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= ================================================ FILE: hack/allowed-licenses.txt ================================================ Apache-2.0,BSD-2-Clause,BSD-2-Clause-FreeBSD,BSD-3-Clause,MIT,ISC,Python-2.0,PostgreSQL,X11,Zlib ================================================ FILE: hack/ansible-test.yaml ================================================ - hosts: all tasks: - name: Create test file file: path: "/tmp/param-{{ lookup('ansible.builtin.env', 'PARAM_ANSIBLE') }}" state: touch ================================================ FILE: hack/bats/README.md ================================================ # BATS Integration Tests See the [Testing](https://lima-vm.io/docs/dev/testing/) page for how to run these tests and the [BATS Style Guide](https://lima-vm.io/docs/dev/testing/bats-style/) for coding conventions. ================================================ FILE: hack/bats/extras/README.md ================================================ # Extra tests The extra tests located in this directory are not automatically executed via `make bats`. Some tests are executed on the CI, some ones are not. Refer to the configuration of the GitHub Actions to see what tests are executed. ================================================ FILE: hack/bats/extras/colima.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" local_setup_file() { colima start } local_teardown_file() { colima stop colima delete -f } @test 'Docker' { docker run -p 8080:80 -d --name nginx "${TEST_CONTAINER_IMAGES[nginx]}" sleep 5 run -0 curl -sSI --retry 5 --retry-all-errors http://localhost:8080 assert_output --partial "200 OK" docker rm -f nginx } ================================================ FILE: hack/bats/extras/freebsd.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" local_setup_file() { limactl start --tty=false template:freebsd } local_teardown_file() { # set -x tail -n 100 "${LIMA_HOME}"/freebsd/*.log limactl shell freebsd -- cat /var/log/messages # limactl stop freebsd limactl rm freebsd } @test 'Smoke test' { run -0 limactl shell freebsd -- uname # FIXME: the shell always shows freebsd-tips (specified in ~/.login) assert_output --partial "FreeBSD" } ================================================ FILE: hack/bats/extras/k8s.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # This test verifies that a Kubernetes cluster can be started and that the single node is ready. load "../helpers/load" : "${TEMPLATE:=k8s}" # Instance names are "${NAME}-0", "${NAME}-1", ... NAME="k8s" get_num_nodes() { local nodes=0 for tag in "${BATS_TEST_TAGS[@]}"; do if [[ $tag =~ ^nodes:([0-9]+)$ ]]; then nodes="${BASH_REMATCH[1]}" fi done if [[ $nodes -eq 0 ]]; then echo >&2 "nodes:N tag is required" exit 1 fi echo "$nodes" } local_setup() { local nodes=$(get_num_nodes) local params="" for ((i=0; i<1; i++)); do limactl delete --force "${NAME}-$i" || : local limactl_start_flags="--tty=false --name "${NAME}-$i"" # Multi-node setup requires user-v2 network for VM-to-VM communication if [[ $nodes -gt 1 ]]; then limactl_start_flags+=" --network lima:user-v2" fi limactl start ${limactl_start_flags} "template:${TEMPLATE}" 3>&- 4>&- & done wait $(jobs -p) # Multi-node setup if [[ $nodes -gt 1 ]]; then for ((i=0; i&- 4>&- & fi done wait $(jobs -p) fi for node in $(k get node -o name); do k wait --timeout=5m --for=condition=ready "${node}" done } local_teardown() { local nodes=$(get_num_nodes) for ((i=0; i&- 4>&- } local_teardown_file() { limactl delete --force "$NAME" } ctrctl() { if [[ $(limactl ls "$NAME" --yq .config.containerd.user) == true ]]; then limactl shell $NAME nerdctl "$@" else limactl shell $NAME docker "$@" fi } nginx_start() { echo "$COUNTER" >"${BATS_TEST_TMPDIR}/index.html" ctrctl run -d --name nginx -p 8080:80 -v "${BATS_TEST_TMPDIR}:/usr/share/nginx/html:ro" nginx } nginx_stop() { ctrctl stop nginx ctrctl rm nginx } verify_port() { run curl --silent http://127.0.0.1:8080 # If nginx is not quite ready and doesn't send any response at all, give it one extra chance if [[ $status -eq 52 ]]; then sleep 0.5 run curl --silent http://127.0.0.1:8080 fi assert_success assert_output "$COUNTER" } @test 'Verify that the container is working' { COUNTER=0 ctrctl pull --quiet nginx nginx_start verify_port } @test 'Stop and restart the container multiple times' { for COUNTER in {1..100}; do nginx_stop nginx_start verify_port done } ================================================ FILE: hack/bats/helpers/limactl.bash ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # Create a dummy Lima instance for testing purposes. It cannot be started because it doesn't have an actual image. # This function intentionally doesn't use create/editflags, but modifies the template with yq instead. create_dummy_instance() { local name=$1 local expr=${2:-} # Template does not validate without an image, and the image must point to a file that exists (for clonefile). local template="{images: [location: /etc/profile]}" if [[ -n $expr ]]; then template="$(limactl yq "$expr" <<<"$template")" fi limactl create --name "$name" - <<<"$template" } # Ensure a Lima instance exists. When LIMA_BATS_REUSE_INSTANCE is set, reuse an # existing running instance. Otherwise delete and recreate it. # The instance configuration is determined by its name; add a case below for new names. # Close file handles 3 and 4 so the host agent doesn't block BATS from exiting. ensure_instance() { local instance=$1 if [[ -n "${LIMA_BATS_REUSE_INSTANCE:-}" ]]; then run limactl list --format '{{.Status}}' "$instance" [[ $status == 0 ]] && [[ $output == "Running" ]] && return fi limactl unprotect "$instance" || : limactl delete --force "$instance" || : case "$instance" in bats) limactl start --yes --name "$instance" template:default 3>&- 4>&- ;; bats-nomount) limactl start --yes --name "$instance" --mount-none template:default 3>&- 4>&- ;; bats-dummy) create_dummy_instance "$instance" '.disk = "1M"' ;; *) echo "ensure_instance: unknown instance name '$instance'" >&2 return 1 ;; esac } # Delete the given Lima instance unless LIMA_BATS_REUSE_INSTANCE is set. delete_instance() { local instance=$1 if [[ -z "${LIMA_BATS_REUSE_INSTANCE:-}" ]]; then limactl unprotect "$instance" || : limactl delete --force "$instance" || : fi } ================================================ FILE: hack/bats/helpers/load.bash ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -o errexit -o nounset -o pipefail # Make sure run() will execute all functions with errexit enabled. # This enables the functionality from https://github.com/lima-vm/bats-core/commit/507da84de75fa78798d53eceb42e68851ef5c48b # The upstream PR https://github.com/bats-core/bats-core/pull/1118 is still open, so our submodule points to the PR commit. export BATS_RUN_ERREXIT=1 # BATS_TEST_RETRIES must be set for the individual test and cannot be imported from the # parent environment because the BATS test runner sets it to 0 before running the test. BATS_TEST_RETRIES=${LIMA_BATS_ALL_TESTS_RETRIES:-0} # Known flaky tests should call `flaky` inside the @test to allow retries up to # LIMA_BATS_FLAKY_TESTS_RETRIES even when the LIMA_BATS_ALL_TESTS_RETRIES is lower. flaky() { BATS_TEST_RETRIES=${LIMA_BATS_FLAKY_TESTS_RETRIES:-$BATS_TEST_RETRIES} } # Don't run the tests in ~/.lima because they may destroy _config, _templates etc. export LIMA_HOME=${LIMA_BATS_LIMA_HOME:-$HOME/.lima-bats} absolute_path() { ( cd "$1" pwd ) } PATH_BATS_HELPERS=$(absolute_path "$(dirname "${BASH_SOURCE[0]}")") PATH_BATS_ROOT=$(absolute_path "$PATH_BATS_HELPERS/..") source "$PATH_BATS_ROOT/lib/bats-support/load.bash" source "$PATH_BATS_ROOT/lib/bats-assert/load.bash" source "$PATH_BATS_ROOT/lib/bats-file/load.bash" source "$PATH_BATS_HELPERS/limactl.bash" source "$PATH_BATS_HELPERS/logs.bash" bats_require_minimum_version 1.5.0 run_e() { run --separate-stderr "$@" } # If called from foo() this function will call local_foo() if it exist. call_local_function() { local func func="local_${FUNCNAME[1]}" if [ "$(type -t "$func")" = "function" ]; then "$func" fi } setup_file() { if [[ ${CI:-} == true ]]; then # Without a terminal the output is using TAP formatting, which does not include the filename local TEST_FILENAME=${BATS_TEST_FILENAME#"$PATH_BATS_ROOT/tests/"} TEST_FILENAME=${TEST_FILENAME%.bats} echo "# ===== ${TEST_FILENAME} =====" >&3 fi if [[ -n "${INSTANCE:-}" ]]; then ensure_instance "$INSTANCE" fi call_local_function } teardown_file() { call_local_function if [[ -n "${INSTANCE:-}" ]]; then delete_instance "$INSTANCE" fi } setup() { call_local_function } teardown() { call_local_function } assert_output_lines_count() { assert_equal "${#lines[@]}" "$1" } # Use GHCR and ECR to avoid hitting Docker Hub rate limit. # NOTE: keep this list in sync with hack/test-templates.sh . declare -A -g TEST_CONTAINER_IMAGES=( ["nginx"]="ghcr.io/stargz-containers/nginx:1.19-alpine-org" ["coredns"]="public.ecr.aws/eks-distro/coredns/coredns:v1.12.2-eks-1-31-latest" ) ================================================ FILE: hack/bats/helpers/logs.bash ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # Format the string the way strconv.Quote() would do. # If the input ends with an ellipsis then no closing quote will be added (and the … will be removed). quote_msg() { local quoted quoted=$(sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/^/"/' <<<"$1") if [[ $quoted == *… ]]; then echo "${quoted%…}" else echo "${quoted}\"" fi } assert_fatal() { assert_stderr_line --partial "level=fatal msg=$(quote_msg "$1")" } assert_error() { assert_stderr_line --partial "level=error msg=$(quote_msg "$1")" } assert_warning() { assert_stderr_line --partial "level=warning msg=$(quote_msg "$1")" } assert_info() { assert_stderr_line --partial "level=info msg=$(quote_msg "$1")" } assert_debug() { assert_stderr_line --partial "level=debug msg=$(quote_msg "$1")" } ================================================ FILE: hack/bats/tests/copy.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" INSTANCE=bats setup() { limactl shell "$INSTANCE" -- mkdir -p /tmp/test_limactl_copy } teardown() { limactl shell "$INSTANCE" -- rm -rf /tmp/test_limactl_copy } test_copy_dir_from_host_to_instance() { backend="$1" mkdir -p "$BATS_TEST_TMPDIR/foo/bar" # SRC DST limactl copy --backend="$backend" -r "$BATS_TEST_TMPDIR/foo" "$INSTANCE":/tmp/test_limactl_copy/foo limactl shell "$INSTANCE" -- test -d /tmp/test_limactl_copy/foo/bar limactl shell "$INSTANCE" -- rm -rf /tmp/test_limactl_copy/foo # SRC/ DST limactl shell "$INSTANCE" -- mkdir -p /tmp/test_limactl_copy/foo_src_with_slash/ limactl copy --backend="$backend" -r "$BATS_TEST_TMPDIR/foo/" "$INSTANCE":/tmp/test_limactl_copy/foo_src_with_slash limactl shell "$INSTANCE" -- test -d /tmp/test_limactl_copy/foo_src_with_slash/foo limactl shell "$INSTANCE" -- rm -rf /tmp/test_limactl_copy/foo_src_with_slash # SRC DST/ limactl shell "$INSTANCE" -- mkdir -p /tmp/test_limactl_copy/foo_dst_with_slash/ limactl copy --backend="$backend" -r "$BATS_TEST_TMPDIR/foo" "$INSTANCE":/tmp/test_limactl_copy/foo_dst_with_slash/ limactl shell "$INSTANCE" -- test -d /tmp/test_limactl_copy/foo_dst_with_slash/foo limactl shell "$INSTANCE" -- rm -rf /tmp/test_limactl_copy/foo_dst_with_slash # SRC/ DST/ limactl shell "$INSTANCE" -- mkdir -p /tmp/test_limactl_copy/foo_src_dst_with_slash/ limactl copy --backend="$backend" -r "$BATS_TEST_TMPDIR/foo/" "$INSTANCE":/tmp/test_limactl_copy/foo_src_dst_with_slash/ limactl shell "$INSTANCE" -- test -d /tmp/test_limactl_copy/foo_src_dst_with_slash/foo limactl shell "$INSTANCE" -- rm -rf /tmp/test_limactl_copy/foo_src_dst_with_slash } @test "copy directory from host to Lima instance (scp)" { test_copy_dir_from_host_to_instance scp } # https://github.com/lima-vm/lima/issues/4468 @test "copy directory from host to Lima instance (rsync)" { test_copy_dir_from_host_to_instance rsync } test_copy_dir_from_instance_to_host() { backend="$1" limactl shell "$INSTANCE" -- mkdir -p /tmp/test_limactl_copy/foo/bar # SRC DST limactl copy --backend="$backend" -r "$INSTANCE":/tmp/test_limactl_copy/foo "$BATS_TEST_TMPDIR/foo" assert_dir_exists "$BATS_TEST_TMPDIR/foo/bar" # SRC/ DST limactl copy --backend="$backend" -r "$INSTANCE":/tmp/test_limactl_copy/foo/ "$BATS_TEST_TMPDIR/foo_src_with_slash" assert_dir_exists "$BATS_TEST_TMPDIR/foo_src_with_slash/bar" # SRC DST/ limactl copy --backend="$backend" -r "$INSTANCE":/tmp/test_limactl_copy/foo "$BATS_TEST_TMPDIR/foo_dst_with_slash/" assert_dir_exists "$BATS_TEST_TMPDIR/foo_dst_with_slash/bar" # SRC/ DST/ limactl copy --backend="$backend" -r "$INSTANCE":/tmp/test_limactl_copy/foo/ "$BATS_TEST_TMPDIR/foo_src_dst_with_slash/" assert_dir_exists "$BATS_TEST_TMPDIR/foo_src_dst_with_slash/bar" } @test "copy directory from Lima instance to host (scp)" { test_copy_dir_from_instance_to_host scp } @test "copy directory from Lima instance to host (rsync)" { test_copy_dir_from_instance_to_host rsync } test_copy_file_from_host_to_instance() { backend="$1" echo "hello" > "$BATS_TEST_TMPDIR/hello.txt" limactl copy --backend="$backend" "$BATS_TEST_TMPDIR/hello.txt" "$INSTANCE":/tmp/test_limactl_copy/hello.txt run -0 limactl shell "$INSTANCE" -- cat /tmp/test_limactl_copy/hello.txt assert_output "hello" limactl shell "$INSTANCE" -- rm -f /tmp/test_limactl_copy/hello.txt } @test "copy file from host to Lima instance (scp)" { test_copy_file_from_host_to_instance scp } @test "copy file from host to Lima instance (rsync)" { test_copy_file_from_host_to_instance rsync } test_copy_file_from_instance_to_host() { backend="$1" limactl shell "$INSTANCE" -- bash -c 'echo "hello" > /tmp/test_limactl_copy/hello.txt' limactl copy --backend="$backend" "$INSTANCE":/tmp/test_limactl_copy/hello.txt "$BATS_TEST_TMPDIR/hello.txt" run -0 cat "$BATS_TEST_TMPDIR/hello.txt" assert_output "hello" limactl shell "$INSTANCE" -- rm -f /tmp/hello.txt } @test "copy file from Lima instance to host (scp)" { test_copy_file_from_instance_to_host scp } @test "copy file from Lima instance to host (rsync)" { test_copy_file_from_instance_to_host rsync } ================================================ FILE: hack/bats/tests/list.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" # Use a separate LIMA_HOME for this test that we can just wipe because it will never have running instances. # We cannot use $BATS_FILE_TMPDIR because `limactl create` will complain about max socket path name length. LOCAL_LIMA_HOME="${LIMA_HOME:?}/_bats" local_setup_file() { export LIMA_HOME="${LOCAL_LIMA_HOME:?}" rm -rf "${LIMA_HOME:?}" run -0 create_dummy_instance "foo" '.disk = "1M"' run -0 create_dummy_instance "bar" '.disk = "2M"' run -0 create_dummy_instance "baz" '.disk = "3M"' } local_setup() { export LIMA_HOME="${LOCAL_LIMA_HOME:?}" } @test 'list with no running instances shows a warning and exits without error' { export LIMA_HOME="$BATS_TEST_TMPDIR" run_e -0 limactl list assert_warning 'No instance found. Run `limactl create` to create an instance.' } @test 'check plain list output' { # also verifies that `ls` is an alias for `list` run -0 limactl ls # instances will be sorted alphabetically assert_line --index 0 --regexp '^NAME +STATUS.+DISK' assert_line --index 1 --regexp '^bar +Stopped.+ 2MiB' assert_line --index 2 --regexp '^baz +Stopped.+ 3MiB' assert_line --index 3 --regexp '^foo +Stopped.+ 1MiB' # there is no other output assert_output_lines_count 4 } @test 'only list selected instances' { run -0 limactl ls foo bar # instances will be sorted in the order they were specified assert_line --index 0 --regexp '^NAME' assert_line --index 1 --regexp '^foo' assert_line --index 2 --regexp '^bar' refute_line --partial baz assert_output_lines_count 3 } @test 'requesting non-existing instance is an error' { run_e -1 limactl ls foo foobar bar assert_warning 'No instance matching foobar found.' assert_fatal 'unmatched instances' # existing instances are still listed assert_line --index 0 --regexp '^NAME' assert_line --index 1 --regexp '^foo' assert_line --index 2 --regexp '^bar' assert_output_lines_count 3 } @test '--quiet option shows only names, no header' { run -0 limactl list --quiet foo bar assert_line --index 0 foo assert_line --index 1 bar assert_output_lines_count 2 } @test '--format json returns JSON output' { run -0 limactl ls --format json foo bar # test may be too strict in expecting "name" to be the first key on the line assert_line --index 0 --regexp '^\{"name":"foo",' assert_line --index 1 --regexp '^\{"name":"bar",' assert_output_lines_count 2 } @test '--json is shorthand for --format json' { run -0 limactl ls foo bar --format json format_json=$output run -0 limactl ls foo bar --json assert_output "$format_json" } @test '--format YAML returns YAML documents' { # save JSON output for comparison run -0 limactl ls foo bar --format json json=$output run -0 limactl ls foo bar --format yaml yaml=$output assert_line --regexp '^name: foo' assert_line --regexp '^name: bar' refute_line --regexp '^name: baz' # verify that the output consists of 2 documents run -0 limactl yq 'true' <<<"$yaml" assert_output_lines_count 2 # convert YAML to JSON run -0 limactl yq --input-format yaml --output-format json --indent 0 "." <<<"$yaml" assert_output_lines_count 2 # verify it matches the JSON output assert_output "$json" } @test 'JSON output to terminal is colorized, but semantically identical' { run -0 limactl ls foo bar --format json json=$output # colorize output even when stdout is not a tty export _LIMA_OUTPUT_IS_TTY=1 run -0 limactl ls foo bar --format json colorized=$output # check if the output contains an ANSI "reset mode" sequence ("ESC[0m") run -0 cat -v <<<"$output" assert_output --partial "^[[0m" # remove all ANSI formatting codes from the output run -0 sed 's/\x1b\[[0-9;]*m//g' <<<"$colorized" # Flatten the pretty-printed JSON to a single line per object with no extra whitespace. # yq processes JSON objects in sequence (when input format is set to json) and # does not require each object to be on a single line. run -0 limactl yq --input-format json --output-format json --indent 0 "." <<<"$output" assert_output_lines_count 2 # compare to the plain (uncolorized) json output assert_output "$json" } @test 'YAML output to terminal is colorized, but semantically identical' { # save uncolorized JSON output run -0 limactl ls foo bar --format json json=$output # colorize output even when stdout is not a tty export _LIMA_OUTPUT_IS_TTY=1 run -0 limactl ls foo bar --format yaml colorized=$output # check if the output contains an ANSI "reset mode" sequence ("ESC[0m") run -0 cat -v <<<"$output" assert_output --partial "^[[0m" # remove all ANSI formatting codes from the output run -0 sed 's/\x1b\[[0-9;]*m//g' <<<"$colorized" yaml=$output # Verify that the output consists of 2 documents run -0 limactl yq 'true' <<<"$yaml" assert_output_lines_count 2 # convert the pretty-printed YAML to JSON Lines format with no whitespace run -0 limactl yq --indent 0 --input-format yaml --output-format json "." <<<"$yaml" assert_output_lines_count 2 # verify it matches the JSON output assert_output "$json" } @test '--all-fields includes all fields in the table' { skip "only works with output to a terminal (#3986)" # TODO provide a way to specify the width, e.g. with `--width 120` # See https://github.com/lima-vm/lima/issues/3986 } @test 'Use field names in Go template format' { run -0 limactl ls foo bar --format '{{.Name}} {{.Disk}}' assert_line --index 0 "foo 1048576" assert_line --index 1 "bar 2097152" } @test '--list-fields list all available fields' { run -0 limactl ls --list-fields assert_line Name assert_line CPUs assert_line Memory # All field names start with an uppercase letter and don't contain any spaces refute_line --regexp '^[^A-Z]' refute_line --partial ' ' } @test '--list-fields does not list deprecated field, but they are still available' { run -0 limactl ls --list-fields # no deprecated fields are listed refute_line "HostArch" refute_line "HostOS" refute_line "IdentityFile" refute_line "LimaHome" # all deprecated fields exist and produce output run -0 limactl ls foo --format '{{.HostArch}}' assert_output run -0 limactl ls foo --format '{{.HostOS}}' assert_output run -0 limactl ls foo --format '{{.IdentityFile}}' assert_output run -0 limactl ls foo --format '{{.LimaHome}}' assert_output "$LIMA_HOME" # verify that a non-existing field throws an error and produces no output # TODO the error message is not really end-user friendly, not sure if we can do something about it run_e -1 limactl ls foo --format '{{.Unknown}}' assert_stderr --regexp "level=fatal.*can't evaluate field Unknown" refute_output } @test '--quiet option can only be used with format --table' { # TODO the error message is incorrect, it can be used with --yq run_e -1 limactl list --quiet --format json assert_fatal "option --quiet can only be used with '--format table'" run_e -1 limactl list --quiet --format yaml assert_fatal "option --quiet can only be used with '--format table'" run_e -1 limactl list --quiet --format '{{.Name}} {{.Disk}}' assert_fatal "option --quiet can only be used with '--format table'" } @test '--yq option implies --format json' { run -0 limactl ls baz --yq '.config' assert_output --regexp '^\{"' run -0 limactl yq '.disk' <<<"$output" assert_output "3M" } @test '--yq option can be used with --format yaml' { run -0 limactl ls baz --yq '.config' --format yaml assert_line "disk: 3M" } @test '--yq option can be specified multiple times' { run -0 limactl ls foo --yq '.config' --yq '.user' --yq '.uid' assert_output "$UID" run -0 limactl ls --yq 'select(.disk > 1024*1024)' --yq 'select(.name | test("z"))' --yq '.name' assert_output "baz" } @test '--yq option is incompatible with --format table or Go templates' { run_e -1 limactl ls --yq '.name' --format table assert_fatal "option --yq only works with --format json or yaml" run_e -1 limactl ls --yq '.name' --format '{{.Name}} {{.Disk}}' assert_fatal "option --yq only works with --format json or yaml" } @test '--quiet option can be used with --yq' { run -0 limactl ls --quiet assert_line --index 0 "bar" assert_output_lines_count 3 run -0 limactl ls --quiet --yq 'select(.name == "foo")' assert_output "foo" } @test '--yq cannot access environment variables' { run_e -1 limactl ls --yq 'env(HOME)' assert_fatal "env operations have been disabled" } @test '--yq cannot load files' { run_e -1 limactl ls --yq "load(\"${BASH_SOURCE[0]}\")" assert_fatal "file operations have been disabled" } @test '--filter option filters instances' { run -0 limactl ls --filter '.name == "foo"' assert_line --index 0 --regexp '^NAME' assert_line --index 1 --regexp '^foo' assert_output_lines_count 2 } @test '--filter option works with all output formats' { run -0 limactl ls --filter '.name == "foo"' assert_line --index 1 --regexp '^foo' run -0 limactl ls --filter '.name == "foo"' --format json assert_line --index 0 --regexp '^\{"name":"foo",' run -0 limactl ls --filter '.name == "foo"' --format '{{.Name}}' assert_output "foo" } @test '--filter option is compatible with --yq' { run -0 limactl ls --filter '.name == "foo"' --yq '.name' assert_output "foo" } @test '--quiet option can be used with --filter' { run -0 limactl ls --quiet --filter '.name == "foo"' assert_output "foo" } @test '--filter option with no matching instances returns empty output' { run -0 limactl ls -q --filter '.name == "kcp"' assert_output "" } @test 'multiple --filter options are combined with AND logic' { run -0 limactl ls -q --filter '.name == "foo"' --filter '.disk == "1M"' assert_output "" run -0 limactl ls --filter '.name == "foo"' --filter '.disk == "1048576"' assert_line --index 1 --regexp '^foo' } ================================================ FILE: hack/bats/tests/mcp.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" INSTANCE=bats # TODO Move helper functions to shared location run_yq() { run -0 --separate-stderr limactl yq "$@" } json_edit() { limactl yq --input-format json --output-format json --indent 0 "$@" } local_setup() { cd "$PATH_BATS_ROOT" coproc MCP { limactl mcp serve "$INSTANCE"; } ID=0 mcp initialize '{"protocolVersion":"2025-06-18"}' # Each mcp request should increment the ID [[ $ID -eq 1 ]] run_yq .serverInfo.name <<<"$output" assert_output "lima" } local_teardown() { kill "${MCP_PID:?}" 2>&1 >/dev/null || : } mcp() { local method=$1 local params=${2:-} local request printf -v request '{"jsonrpc":"2.0","id":%d,"method":"%s"}' "$((++ID))" "$method" if [[ -n $params ]]; then request=$(json_edit ".params=${params}" <<<"$request") fi # send request to MCP server stdin echo "$request" >&"${MCP[1]}" # read response from MCP server stdout with 5s timeout local json while true; do if ! read -t 5 -r json <&"${MCP[0]}"; then break fi # If it has no "method" field, it's a response, not a notification if ! jq -e 'has("method")' <<<"$json" >/dev/null 2>&1; then break fi done # verify that the response matches the request; also validates the output is valid JSON run_yq .id <<<"$json" assert_output "$ID" # there must be no error object in the response run_yq .error <<<"$json" assert_output "null" # set $output to .result run_yq .result <<<"$json" } tools_call() { local name=$1 local args=${2:-} local params printf -v params '{"name":"%s"}' "$name" if [[ -n $args ]]; then params=$(json_edit ".arguments=${args}" <<<"$params") fi mcp tools/call "$params" } @test 'list tools' { mcp tools/list run_yq '.tools[].name' <<<"$output" assert_line glob assert_line list_directory assert_line read_file assert_line run_shell_command assert_line search_file_content assert_line write_file } @test 'verify that tools descriptions include input and output schema' { mcp tools/list run_yq '.tools[] | select(.name == "run_shell_command")' <<<"$output" json=$output run_yq '.inputSchema.required[]' <<<"$json" assert_line command assert_line directory assert_output_lines_count 2 run_yq '.inputSchema.properties | keys[]' <<<"$json" assert_line command assert_line description assert_line directory assert_output_lines_count 3 run_yq '.outputSchema.required[]' <<<"$json" assert_line stdout assert_line stderr assert_output_lines_count 2 run_yq '.outputSchema.properties | keys[]' <<<"$json" assert_line error assert_line exit_code assert_line stdout assert_line stderr assert_output_lines_count 4 } @test 'run shell command returns command output' { run -0 limactl shell "$INSTANCE" cat /etc/os-release assert_output expected=$output tools_call run_shell_command '{"directory":"/etc","command":["cat","os-release"]}' json=$output run_yq '.structuredContent.exit_code' <<<"$json" assert_output 0 run_yq '.structuredContent.stdout' <<<"$json" assert_output "$expected" run_yq '.structuredContent.stderr' <<<"$json" refute_output # The same data is also available as encoded JSON run_yq '.content[0].type' <<<"$json" assert_output "text" run_yq '.content[0].text' <<<"$json" text=$output run_yq '.exit_code' <<<"$text" assert_output 0 run_yq '.stdout' <<<"$text" assert_output "$expected" run_yq '.stderr' <<<"$text" refute_output } @test 'run shell command returns stderr and exit code' { tools_call run_shell_command '{"directory":"/","command":["bash","-c","echo NO>&2; exit 13"]}' json=$output run_yq '.structuredContent.exit_code' <<<"$json" assert_output 13 run_yq '.structuredContent.error' <<<"$json" assert_output "exit status 13" run_yq '.structuredContent.stdout' <<<"$json" refute_output run_yq '.structuredContent.stderr' <<<"$json" assert_output "NO" } @test 'run shell command fails if the directory does not exist' { tools_call run_shell_command '{"directory":"/etcetera","command":["cat","os-release"]}' json=$output run_yq '.structuredContent.exit_code' <<<"$json" assert_output 1 run_yq '.structuredContent.stderr' <<<"$json" assert_output --partial "No such file or directory" } @test 'read_file reads a file' { run -0 limactl shell "$INSTANCE" cat /etc/os-release assert_output expected=$output tools_call read_file '{"path":"/etc/os-release"}' json=$output run_yq '.content[0].text' <<<"$json" run_yq '.content' <<<"$output" assert_output "$expected" run_yq '.structuredContent.content' <<<"$json" assert_output "$expected" } @test 'read_file returns an error when path does not exist' { tools_call read_file '{"path":"/etc/os-release-info"}' json=$output run_yq '.isError' <<<"$json" assert_output "true" run_yq '.content[0].text' <<<"$json" assert_output "file does not exist" } @test 'read_file returns an error when path is not absolute' { tools_call read_file '{"path":"os-release"}' json=$output run_yq '.isError' <<<"$json" assert_output "true" run_yq '.content[0].text' <<<"$json" assert_output --partial "expected an absolute path" } @test 'write_file creates new file and overwrites existing file' { limactl shell "$INSTANCE" rm -f /tmp/mcp.test tools_call write_file '{"path":"/tmp/mcp.test","content":"foo"}' run_yq '.content[0].text' <<<"$output" assert_output "{}" run -0 limactl shell "$INSTANCE" cat /tmp/mcp.test assert_output "foo" tools_call write_file '{"path":"/tmp/mcp.test","content":"bar"}' run_yq '.content[0].text' <<<"$output" assert_output "{}" run -0 limactl shell "$INSTANCE" cat /tmp/mcp.test assert_output "bar" } @test 'write_file creates the directory if it does not yet exist' { # Make sure /tmp/tmp is deletable even if we run the tests multiple times against the same Lima instance limactl shell "$INSTANCE" chmod -R 777 /tmp/tmp || true limactl shell "$INSTANCE" rm -rf /tmp/tmp tools_call write_file '{"path":"/tmp/tmp/tmp","content":"tmp"}' json=$output run_yq '.isError' <<<"$json" assert_output "null" run -0 limactl shell "$INSTANCE" cat /tmp/tmp/tmp assert_output "tmp" } @test 'write_file returns an error when the directory is not writable' { limactl shell "$INSTANCE" mkdir -p /tmp/tmp limactl shell "$INSTANCE" chmod 444 /tmp/tmp tools_call write_file '{"path":"/tmp/tmp/tmp","content":"tmp"}' json=$output run_yq '.isError' <<<"$json" assert_output "true" run_yq '.content[0].text' <<<"$json" assert_output "permission denied" } @test 'write_file returns an error when path is not absolute' { tools_call write_file '{"path":"tmp/mcp.test","content":"baz"}' json=$output run_yq '.isError' <<<"$json" assert_output "true" run_yq '.content[0].text' <<<"$json" assert_output --partial "expected an absolute path" } @test 'glob finds files by wildcard' { tools_call glob '{"pattern":"*/*p.bats"}' run_yq '.structuredContent.matches[]' <<<"$output" assert_line --regexp '/tests/mcp.bats$' } @test 'glob returns an empty list when the pattern does not match' { tools_call glob '{"pattern":"nothing.to.see"}' run_yq '.structuredContent.matches[]' <<<"$output" assert_output_lines_count 0 } @test 'search_file_content finds text inside files' { tools_call search_file_content '{"pattern":"needle in a haystack"}' run_yq '.structuredContent.git_grep_output' <<<"$output" assert_line --regexp '^tests/mcp.bats:[0-9]+: +tools_call' } @test 'search_file_content can find unicode characters above U+FFFF' { # The light bulb emoji 💡 (U+1F4A1) tools_call search_file_content '{"pattern":"💡"}' run_yq '.structuredContent.git_grep_output' <<<"$output" assert_line --regexp '^tests/mcp.bats:[0-9]+: +# The light bulb' assert_line --regexp '^tests/mcp.bats:[0-9]+: +tools_call' } @test 'search_file_content returns an empty string if it cannot find the pattern' { tools_call search_file_content "$(printf '{"pattern":"\U0001f4a1 not found"}')" run_yq '.structuredContent.git_grep_output' <<<"$output" refute_output } ================================================ FILE: hack/bats/tests/path.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" INSTANCE=bats @test "The guest home is accessible via both .guest and .linux paths" { limactl shell "$INSTANCE" -- ls -ld /home/"${USER}.guest/.ssh" limactl shell "$INSTANCE" -- ls -ld /home/"${USER}.linux/.ssh" } ================================================ FILE: hack/bats/tests/preserve-env.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" INSTANCE=bats local_setup_file() { unset LIMA_SHELLENV_ALLOW unset LIMA_SHELLENV_BLOCK } local_setup() { # make sure changes from previous tests are removed limactl shell "$INSTANCE" sh -c '[ ! -f ~/.bash_profile ] || sed -i -E "/^export (FOO|BAR|SSH_)/d" ~/.bash_profile' } @test 'there are no FOO*, BAR*, or SSH_FOO* variables defined in the VM' { # just to confirm because the other tests depend on these being unused run -0 limactl shell "$INSTANCE" printenv refute_line --regexp '^FOO' refute_line --regexp '^BAR' refute_line --regexp '^SSH_FOO' } @test 'environment is not preserved by default' { export FOO=foo run -0 limactl shell "$INSTANCE" printenv refute_line --regexp '^FOO=' } @test 'environment is preserved with --preserve-env' { export FOO=foo run -0 limactl shell --preserve-env "$INSTANCE" printenv assert_line FOO=foo } @test 'profile settings inside the VM take precedence over preserved variables' { limactl shell "$INSTANCE" sh -c 'echo "export FOO=bar" >>~/.bash_profile' export FOO=foo run -0 limactl shell --preserve-env "$INSTANCE" printenv assert_line FOO=bar } @test 'builtin block list is used when LIMA_SHELLENV_BLOCK is not set' { # default block list includes SSH_* export SSH_FOO=ssh_foo run -0 limactl shell --preserve-env "$INSTANCE" printenv refute_line --regexp '^SSH_FOO=' } @test 'custom block list replaces builtin block list' { export LIMA_SHELLENV_BLOCK=FOO export FOO=foo export SSH_FOO=foo run -0 limactl shell --preserve-env "$INSTANCE" printenv refute_line --regexp '^FOO=' assert_line SSH_FOO=foo } @test 'custom block list starting with + appends to builtin block list' { export LIMA_SHELLENV_BLOCK=+FOO export FOO=foo export SSH_FOO=foo run -0 limactl shell --preserve-env "$INSTANCE" printenv refute_line --regexp '^FOO=' refute_line --regexp '^SSH_FOO=' } @test 'block list entries can use * wildcard at the end' { export LIMA_SHELLENV_BLOCK="FOO*" export FOO=foo export FOOBAR=foobar export BAR=bar run -0 limactl shell --preserve-env "$INSTANCE" printenv refute_line --regexp '^FOO' assert_line BAR=bar } @test 'wildcard works at the start of the pattern' { export LIMA_SHELLENV_BLOCK="*FOO" export FOO=foo export BARFOO=barfoo run -0 limactl shell --preserve-env "$INSTANCE" printenv refute_line --regexp '^BARFOO=' refute_line --regexp '^FOO=' } @test 'block list can use a , separated list with whitespace ignored' { export LIMA_SHELLENV_BLOCK="FOO*, , BAR" export FOO=foo export FOOBAR=foobar export BAR=bar export BARBAZ=barbaz run -0 limactl shell --preserve-env "$INSTANCE" printenv refute_line --regexp '^FOO' refute_line --regexp '^BAR=' assert_line BARBAZ=barbaz } @test 'allow list can use a , separated list with whitespace ignored' { export LIMA_SHELLENV_ALLOW="SSH_FOO, , BAR*, LD_UID" export SSH_FOO=ssh_foo export SSH_BAR=ssh_bar export SSH_BLOCK=ssh_block export BAR=bar export BARBAZ=barbaz export LD_UID=randomuid run -0 limactl shell --preserve-env "$INSTANCE" printenv assert_line SSH_FOO=ssh_foo assert_line BAR=bar assert_line BARBAZ=barbaz assert_line LD_UID=randomuid refute_line --regexp '^SSH_BAR=' refute_line --regexp '^SSH_BLOCK=' } @test 'wildcard patterns work in all positions' { export LIMA_SHELLENV_BLOCK="*FOO*BAR*" export FOO=foo export FOOBAR=foobar export FOOXYZBAR=fooxyzbar export FOOBAZ=foobaz export BAZBAR=bazbar export BAR=bar export XFOOYBARZDOTCOM=xfooybarzdotcom export NORMAL_VAR=normal_var export UNRELATED=unrelated run -0 limactl shell --preserve-env "$INSTANCE" printenv refute_line --regexp '^FOOBAR=' refute_line --regexp '^FOOXYZBAR=' refute_line --regexp '^XFOOYBARZDOTCOM=' assert_line FOOBAZ=foobaz assert_line NORMAL_VAR=normal_var assert_line UNRELATED=unrelated assert_line BAZBAR=bazbar assert_line BAR=bar assert_line FOO=foo } @test 'allowlist overrides default blocklist with wildcards' { export LIMA_SHELLENV_ALLOW="SSH_*,CUSTOM*" export LIMA_SHELLENV_BLOCK="+*TOKEN" export SSH_AUTH_SOCK=ssh_auth_sock export SSH_CONNECTION=ssh_connection export CUSTOM_VAR=custom_var export MY_TOKEN=my_token export UNRELATED=unrelated run -0 limactl shell --preserve-env "$INSTANCE" printenv assert_line SSH_AUTH_SOCK=ssh_auth_sock assert_line SSH_CONNECTION=ssh_connection assert_line CUSTOM_VAR=custom_var refute_line --regexp '^MY_TOKEN=' assert_line UNRELATED=unrelated } @test 'invalid characters in patterns cause fatal errors' { export LIMA_SHELLENV_BLOCK="FOO-BAR" run ! limactl shell --preserve-env "$INSTANCE" printenv assert_output --partial "Invalid LIMA_SHELLENV_BLOCK pattern" assert_output --partial "contains invalid character" } @test 'limactl info includes the default block list' { run -0 limactl info run -0 limactl yq '.shellEnvBlock[]' <<<"$output" assert_line PATH assert_line "SSH_*" assert_line USER } ================================================ FILE: hack/bats/tests/protect.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" INSTANCE=bats-dummy CLONE=clone NOTEXIST=notexist local_setup_file() { local inst for inst in "$CLONE" "$NOTEXIST"; do limactl unprotect "$inst" || : limactl delete --force "$inst" || : done } @test 'protecting a non-existing instance fails' { run_e -1 limactl protect "${NOTEXIST}" assert_fatal "failed to inspect instance \"${NOTEXIST}\"…" } @test 'protecting the dummy instance succeeds' { run_e -0 limactl protect "$INSTANCE" assert_info "Protected \"${INSTANCE}\"" assert_file_exists "${LIMA_HOME}/${INSTANCE}/protected" } @test 'protecting it again shows a warning, but succeeds' { run_e -0 limactl protect "$INSTANCE" assert_warning "Instance \"${INSTANCE}\" is already protected. Skipping." assert_file_exists "${LIMA_HOME}/${INSTANCE}/protected" } @test 'cloning a protected instance creates an unprotected clone' { run_e -0 limactl clone --yes "$INSTANCE" "$CLONE" # TODO there is currently no output from the clone command, which feels wrong refute_output assert_file_not_exists "${LIMA_HOME}/${CLONE}/protected" } @test 'deleting the unprotected clone instance succeeds' { run_e -0 limactl delete --force "$CLONE" assert_info "Deleted \"${CLONE}\"…" } @test 'deleting protected dummy instance fails' { run_e -1 limactl delete --force "$INSTANCE" assert_fatal "failed to delete instance \"${INSTANCE}\": instance is protected…" assert_file_exists "$LIMA_HOME/$INSTANCE/protected" } @test 'unprotecting the dummy instance succeeds' { run_e -0 limactl unprotect "$INSTANCE" assert_info "Unprotected \"${INSTANCE}\"" assert_file_not_exists "$LIMA_HOME/$INSTANCE/protected" } @test 'unprotecting it again shows a warning, but succeeds' { run_e -0 limactl unprotect "$INSTANCE" assert_warning "Instance \"${INSTANCE}\" isn't protected. Skipping." assert_file_not_exists "$LIMA_HOME/$INSTANCE/protected" } @test 'deleting unprotected dummy instance succeeds' { run_e -0 limactl delete --force "$INSTANCE" assert_info "Deleted \"${INSTANCE}\"…" } ================================================ FILE: hack/bats/tests/shell-sync.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" INSTANCE=bats-nomount setup() { # Create a temporary test directory and files TEST_SYNC_DIR="$BATS_TEST_TMPDIR/sync-test" mkdir -p "$TEST_SYNC_DIR" touch "$TEST_SYNC_DIR/foo.txt" touch "$TEST_SYNC_DIR/bar.txt" # Create a simple script that makes changes to these files cat > "$TEST_SYNC_DIR/modify.sh" << 'EOF' #!/bin/sh set -eu echo "modified foo" > foo.txt echo "modified bar" > bar.txt EOF chmod +x "$TEST_SYNC_DIR/modify.sh" } teardown() { # Clean up test directory if [[ -d "$TEST_SYNC_DIR" ]]; then rm -rf "$TEST_SYNC_DIR" fi } @test 'shell --sync preserves working directory path from host to guest' { cd "$TEST_SYNC_DIR" # Get path of the TEST_SYNC_DIR for verification local path_test_dir path_test_dir="$PWD" run -0 bash -c "limactl shell --sync . --yes '$INSTANCE' pwd && ./modify.sh" # Verify the guest working directory matches the host path structure assert_output --regexp ".*${path_test_dir#/}" # Verify files were modified run cat "$TEST_SYNC_DIR/foo.txt" assert_output "modified foo" run cat "$TEST_SYNC_DIR/bar.txt" assert_output "modified bar" } @test 'shell --sync with directory path containing spaces and quotes' { # Create directory with spaces and quotes in name and move test directory to it local special_dir="$BATS_TEST_TMPDIR/sync test 'with' \"quotes\"" mkdir -p "$special_dir" mv "$TEST_SYNC_DIR" "$special_dir" cd "$special_dir/sync-test" # Count files before sync local files_before files_before=$(find . -type f | wc -l) run -0 bash -c "limactl shell --sync . --yes '$INSTANCE' ./modify.sh" # Verify files were modified run cat "$special_dir/sync-test/foo.txt" assert_output "modified foo" run cat "$special_dir/sync-test/bar.txt" assert_output "modified bar" # Count files after sync local files_after files_after=$(find "$special_dir/sync-test" -type f | wc -l) [[ $files_after -eq $files_before ]] # Cleanup rm -rf "$special_dir" } @test 'shell --sync reflects file deletion from guest to host' { cd "$TEST_SYNC_DIR" run -0 bash -c "limactl shell --sync . --yes '$INSTANCE' rm -f foo.txt" assert_file_not_exists "$TEST_SYNC_DIR/foo.txt" } @test 'shell --sync reflects new directory and file creation from guest to host' { cd "$TEST_SYNC_DIR" # Create a script that creates a new directory with a file cat > "$TEST_SYNC_DIR/create_new.sh" << 'EOF' #!/bin/sh set -eu mkdir -p new_directory echo "foo bar baz" > new_directory/new_file.txt EOF chmod +x "$TEST_SYNC_DIR/create_new.sh" run -0 bash -c "limactl shell --sync . --yes '$INSTANCE' ./create_new.sh && ./modify.sh" # Verify new directory was created on host assert_dir_exists "$TEST_SYNC_DIR/new_directory" assert_file_exists "$TEST_SYNC_DIR/new_directory/new_file.txt" # Verify file content run cat "$TEST_SYNC_DIR/new_directory/new_file.txt" assert_output "foo bar baz" run cat "$TEST_SYNC_DIR/foo.txt" assert_output "modified foo" run cat "$TEST_SYNC_DIR/bar.txt" assert_output "modified bar" } @test 'shell --sync preserves file permissions' { cd "$TEST_SYNC_DIR" # Create a file with specific permissions touch "$TEST_SYNC_DIR/executable.sh" chmod 755 "$TEST_SYNC_DIR/executable.sh" # Modify the file in guest run -0 bash -c "limactl shell --sync . --yes '$INSTANCE' ./modify.sh" # Verify file is still executable on host if [[ "$OSTYPE" == darwin* ]]; then run stat -f '%A' "$TEST_SYNC_DIR/executable.sh" else run stat -c '%a' "$TEST_SYNC_DIR/executable.sh" fi assert_output "755" # Verify files were modified run cat "$TEST_SYNC_DIR/foo.txt" assert_output "modified foo" run cat "$TEST_SYNC_DIR/bar.txt" assert_output "modified bar" } @test 'shell --sync works without existing ControlMaster socket' { cd "$TEST_SYNC_DIR" # Remove the ControlMaster socket local sock_path="$LIMA_HOME/$INSTANCE/ssh.sock" if [[ -S "$sock_path" ]]; then rm "$sock_path" fi run -0 bash -c "limactl shell --sync . --yes '$INSTANCE' ./modify.sh" # Verify files were modified run cat "$TEST_SYNC_DIR/foo.txt" assert_output "modified foo" run cat "$TEST_SYNC_DIR/bar.txt" assert_output "modified bar" } @test 'shell --sync should preserve top-level subtree after clean up' { local foo_dir="$TEST_SYNC_DIR/foo" mkdir -p "$foo_dir" cd "$foo_dir" run -0 bash -c "limactl shell --sync . --yes '$INSTANCE' sh -c 'pwd > pwd.txt'" assert_file_exists "$foo_dir/pwd.txt" local guest_pwd guest_pwd=$(cat "$foo_dir/pwd.txt") run -0 bash -c "limactl shell '$INSTANCE' test -d ${guest_pwd%/*}" } ================================================ FILE: hack/bats/tests/shell.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" INSTANCE=bats-dummy @test 'lima stopped lima instance' { # check that the "tty" flag is used, also for stdin run_e -1 limactl shell --tty=false "$INSTANCE" true templates/demo.yaml # ├── back # │ └── .lima.yaml -> github:jandubois//loop/ # ├── docs # │ └── .lima.yaml -> ../templates/demo.yaml # ├── example.yaml -> templates/demo.yaml # ├── invalid # │ ├── org # │ │ └── .lima.yaml -> github:lima-vm # │ └── tag # │ └── .lima.yaml -> github:jandubois//@v0.0.0 # ├── loop # │ └── .lima.yaml -> github:jandubois//back/ # ├── redirect # │ └── .lima.yaml -> github:jandubois/lima/templates/default # ├── templates # │ └── demo.yaml "base: template:default" # └── yaml # └── .lima.yaml "{}" # # Both the `main` branch and the `v0.0.0` tag have this layout. # # All these URLs should redirect to the same template URL (either on "main" or at "v0.0.0"): # "https://raw.githubusercontent.com/jandubois/jandubois/${tag}/templates/demo.yaml" # # Additional tests rely on jandubois/lima existing and containing the v1.2.1 tag. URLS=( github:jandubois/jandubois/templates/demo.yaml@main github:jandubois/jandubois/templates/demo.yaml github:jandubois/jandubois/templates/demo github:jandubois/jandubois/.lima.yaml github:jandubois/jandubois/@v0.0.0 github:jandubois/jandubois github:jandubois//templates/demo.yaml@main github:jandubois//templates/demo.yaml github:jandubois//templates/demo github:jandubois//.lima.yaml github:jandubois//@v0.0.0 github:jandubois// github:jandubois/ github:jandubois@v0.0.0 github:jandubois github:jandubois/jandubois/example.yaml github:jandubois/jandubois/example@main github:jandubois//example.yaml@v0.0.0 github:jandubois//example github:jandubois/jandubois/docs/.lima.yaml@main github:jandubois/jandubois/docs/.lima.yaml github:jandubois/jandubois/docs/.lima github:jandubois/jandubois/docs/ github:jandubois//docs/.lima.yaml@v0.0.0 github:jandubois//docs/.lima.yaml github:jandubois//docs/.lima github:jandubois//docs/@v0.0.0 github:jandubois//docs/ ) url() { run_e "$1" limactl --debug template url "$2" } test_jandubois_url() { local url=$1 local tag="main" if [[ $url == *v0.0.0* ]]; then tag="v0.0.0" fi url -0 "$url" assert_output "https://raw.githubusercontent.com/jandubois/jandubois/${tag}/templates/demo.yaml" } # Dynamically register a @test for each URL in the list for url in "${URLS[@]}"; do bats_test_function --description "$url" -- test_jandubois_url "$url" done @test '.lima.yaml is retained when it exits and is not a symlink' { url -0 'github:jandubois//yaml/' assert_output 'https://raw.githubusercontent.com/jandubois/jandubois/main/yaml/.lima.yaml' } @test 'non-existing .lima.yaml returns an error' { url -1 'github:jandubois//missing/' assert_fatal 'file "https://raw.githubusercontent.com/jandubois/jandubois/main/missing/.lima.yaml" not found or inaccessible: status 404' } @test 'hidden files without an extension get a .yaml extension' { url -1 'github:jandubois//test/.hidden' assert_fatal 'file "https://raw.githubusercontent.com/jandubois/jandubois/main/test/.hidden.yaml" not found or inaccessible: status 404' } @test 'files that have an extension do not get a .yaml extension' { # This command doesn't fail because only *.yaml files are checked for redirects/symlinks, and therefore fail right away if they don't exist. url -0 'github:jandubois//test/.script.sh' assert_output 'https://raw.githubusercontent.com/jandubois/jandubois/main/test/.script.sh' } @test 'github: URLs are EXPERIMENTAL' { url -0 'github:jandubois' assert_warning "The github: scheme is still EXPERIMENTAL" } # Invalid URLs @test 'empty github: url returns an error' { url -1 'github:' assert_fatal 'github: URL must contain at least an ORG, got ""' } @test 'missing org returns an error' { url -1 'github:/jandubois' assert_fatal 'github: URL must contain at least an ORG, got ""' } # github: redirects in github:ORG// repos @test 'org redirects can point to different repo and may switch the branch name' { url -0 'github:jandubois//redirect/' # Note that the default branch in jandubois/jandubois is main, but in jandubois/lima it is master assert_debug 'Locator "github:jandubois//redirect/" replaced with "github:jandubois/lima/templates/default"' assert_debug 'Locator "github:jandubois/lima/templates/default" replaced with "https://raw.githubusercontent.com/jandubois/lima/master/templates/default.yaml"' assert_output 'https://raw.githubusercontent.com/jandubois/lima/master/templates/default.yaml' } @test 'org redirects propagate an explicit branch/tag to the other repo' { url -0 'github:jandubois//redirect/@v1.2.1' assert_debug 'Locator "github:jandubois//redirect/@v1.2.1" replaced with "github:jandubois/lima/templates/default@v1.2.1"' assert_debug 'Locator "github:jandubois/lima/templates/default@v1.2.1" replaced with "https://raw.githubusercontent.com/jandubois/lima/v1.2.1/templates/default.yaml"' assert_output 'https://raw.githubusercontent.com/jandubois/lima/v1.2.1/templates/default.yaml' } @test 'org redirects cannot point to another org' { url -1 'github:jandubois//invalid/org/' assert_fatal 'redirect "github:lima-vm" is not a "github:jandubois" URL…' } @test 'org redirects with branch cannot point to another org' { url -1 'github:jandubois//invalid/org/@main' assert_fatal 'redirect "github:lima-vm" is not a "github:jandubois" URL…' } @test 'org redirects cannot include a branch or tag' { url -1 'github:jandubois//invalid/tag/' assert_fatal 'redirect "github:jandubois//@v0.0.0" must not include a branch/tag/sha…' } @test 'org redirects with tag cannot include a branch or tag' { url -1 'github:jandubois//invalid/tag/@v0.0.0' assert_fatal 'redirect "github:jandubois//@v0.0.0" must not include a branch/tag/sha…' } @test 'org redirects must not create circular redirects' { url -1 'github:jandubois//loop/' assert_fatal 'custom locator "github:jandubois//loop/" has a redirect loop' } @test 'org redirects with branch must not create circular redirects' { url -1 'github:jandubois//back/@main' assert_fatal 'custom locator "github:jandubois//back/@main" has a redirect loop' } ================================================ FILE: hack/bats/tests/yq.bats ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 load "../helpers/load" @test 'make sure the yq subcommand exists' { run -0 limactl yq --version assert_output --regexp '^yq .*mikefarah.* version v' } @test 'yq can evaluate yq expressions' { run -0 limactl yq -n .foo=42 assert_output 'foo: 42' } @test 'yq command understand yq options' { run -0 limactl yq -n -o json -I 0 .foo=42 assert_output '{"foo":42}' } @test 'yq errors set non-zero exit code' { run -1 limactl yq -n foo assert_output --partial "invalid input" } @test 'yq works as a multi-call binary' { # multi-call command detection strips all extensions YQ="yq.lima.exe" ln -sf "$(which limactl)" "${BATS_TEST_TMPDIR}/${YQ}" export PATH="$BATS_TEST_TMPDIR:$PATH" run -0 "$YQ" --version assert_output --regexp '^yq .*mikefarah.* version v' run -0 "$YQ" -n -o json -I 0 .foo=42 assert_output '{"foo":42}' } @test 'yq multi-call command has support for env access' { export FOO=bar run -0 limactl yq -n 'env(FOO)' assert_output "bar" } @test 'yq multi-call command has support for --security-disable-env-ops' { export FOO=bar run_e -1 limactl yq -n --security-disable-env-ops 'env(FOO)' assert_stderr "Error: env operations have been disabled" } ================================================ FILE: hack/brew-install-version.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # This script only works for formulas in the homebrew-core. # It assumes the homebrew-core has been checked out into ./homebrew-core. # It only needs commit messages, so the checkout can be filtered with tree:0. set -eu -o pipefail FORMULA=$1 VERSION=$2 export HOMEBREW_NO_AUTO_UPDATE=1 export HOMEBREW_NO_INSTALL_UPGRADE=1 export HOMEBREW_NO_INSTALL_CLEANUP=1 TAP=lima/tap if ! brew tap | grep -q "^${TAP}\$"; then brew tap-new "$TAP" fi # Get the latest commit id for the commit that updated this bottle SHA=$(git -C homebrew-core log --max-count 1 --grep "^${FORMULA}: update ${VERSION} bottle" --format="%H") if [[ -z $SHA ]]; then echo "${FORMULA} ${VERSION} not found" exit 1 fi OUTPUT="$(brew --repo "$TAP")/Formula/${FORMULA}.rb" RAW="https://raw.githubusercontent.com/Homebrew/homebrew-core" curl -s "${RAW}/${SHA}/Formula/${FORMULA::1}/${FORMULA}.rb" -o "$OUTPUT" if brew ls -1 | grep -q "^${FORMULA}\$"; then brew uninstall "$FORMULA" --ignore-dependencies fi brew install "${TAP}/${FORMULA}" ================================================ FILE: hack/cache-common-inc.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # print the error message and exit with status 1 function error_exit() { echo "Error: $*" >&2 exit 1 } # e.g. # ```console # $ download_template_if_needed templates/default.yaml # templates/default.yaml # $ download_template_if_needed https://raw.githubusercontent.com/lima-vm/lima/v0.15.1/examples/ubuntu-lts.yaml # /tmp/tmp.1J9Q6Q/template.yaml # ``` function download_template_if_needed() { local template="$1" tmp_yaml="$(mktemp -d)/template.yaml" # The upgrade test doesn't have limactl installed first. The old version wouldn't support `limactl tmpl` anyways. if command -v limactl >/dev/null; then limactl tmpl copy --embed-all "${template}" "${tmp_yaml}" || return else if [[ $template == https://* ]]; then curl -sSLf "${template}" >"${tmp_yaml}" || return else cp "${template}" "${tmp_yaml}" fi fi echo "${tmp_yaml}" } # e.g. # ```console # $ print_image_locations_for_arch_from_template templates/default.yaml # https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a # https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img null # ``` function print_image_locations_for_arch_from_template() { local template arch template=$(download_template_if_needed "$1") || return local -r template=${template} arch=$(detect_arch "${template}" "${2:-}") || return local -r arch=${arch} # extract digest, location and size by parsing template using arch local -r yq_filter="[.images | map(select(.arch == \"${arch}\")) | .[].location] | .[]" yq eval "${yq_filter}" "${template}" } # e.g. # ```console # $ detect_arch templates/default.yaml # x86_64 # $ detect_arch templates/default.yaml arm64 # aarch64 # ``` function detect_arch() { local template arch template=$(download_template_if_needed "$1") || return local -r template=${template} arch="${2:-$(yq '.arch // ""' "${template}")}" arch="${arch:-$(uname -m)}" # normalize arch. amd64 -> x86_64, arm64 -> aarch64 case "${arch}" in amd64 | x86_64) arch=x86_64 ;; aarch64 | arm64) arch=aarch64 ;; *) ;; esac echo "${arch}" } # e.g. # ```console # $ print_image_locations_for_arch_from_template templates/default.yaml|print_valid_image_index # 0 # ``` function print_valid_image_index() { local index=0 while read -r location; do [[ ${location} != "null" ]] || continue http_code_and_size=$(check_location_with_cache "${location}") read -r http_code _size <<<"${http_code_and_size}" if [[ ${http_code} -eq 200 ]]; then echo "${index}" return fi index=$((index + 1)) done echo "Failed to get the valid image location" >&2 return 1 } # e.g. # ```console # $ size_from_location "https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img" # 585498624 # ``` function size_from_location() { ( set -o pipefail local location=$1 check_location "${location}" | cut -d' ' -f2 ) } # Check the remote location and print the http code and size. # If GITHUB_ACTIONS is true, the result is not cached. # e.g. # ```console # $ check_location "https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img" # 200 585498624 # ``` function check_location() { # shellcheck disable=SC2154 if [[ ${GITHUB_ACTIONS:-false} == true ]]; then check_location_without_cache "$1" else check_location_with_cache "$1" fi } # Check the remote location and print the http code and size. # The result is cached in .check_location-response-cache.yaml # e.g. # ```console # $ check_location_with_cache "https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img" # 200 585498624 # ``` function check_location_with_cache() { local -r location="$1" cache_file=".check_location-response-cache.yaml" # check ${cache_file} for the cache if [[ -f ${cache_file} ]]; then cached=$(yq -e eval ".[\"${location}\"]" "${cache_file}" 2>/dev/null) && echo "${cached}" && return else touch "${cache_file}" fi http_code_and_size=$(check_location_without_cache "${location}") || return yq eval ".[\"${location}\"] = \"${http_code_and_size}\"" -i "${cache_file}" || return echo "${http_code_and_size}" } # e.g. # ```console # $ check_location "https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img" # 200 585498624 # ``` function check_location_without_cache() { local -r location="$1" curl -sIL -w "%{http_code} %header{Content-Length}" "${location}" -o /dev/null } # e.g. # ```console # $ print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index templates/default.yaml 0 # https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img # sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a # null # null # null # null # ``` function print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index() { local template index="${2:-}" arch template=$(download_template_if_needed "$1") || return local -r template=${template} arch=$(detect_arch "${template}" "${3:-}") || return local -r arch=${arch} local -r yq_filter="[(.images[] | select(.arch == \"${arch}\"))].[${index}]|[ .location, .digest, .kernel.location, .kernel.digest, .initrd.location, .initrd.digest ]" yq -o=t eval "${yq_filter}" "${template}" } # e.g. # ```console # $ print_containerd_config_for_arch_from_template templates/default.yaml # true # https://github.com/containerd/nerdctl/releases/download/v1.7.6/nerdctl-full-1.7.6-linux-arm64.tar.gz # sha256:77c747f09853ee3d229d77e8de0dd3c85622537d82be57433dc1fca4493bab95 # ``` function print_containerd_config_for_arch_from_template() { local template arch template=$(download_template_if_needed "$1") || return local -r template=${template} arch=$(detect_arch "${template}" "${2:-}") || return local -r arch=${arch} local -r yq_filter=" [.containerd|[.system or .user], .containerd.archives | map(select(.arch == \"${arch}\")) | [.[0].location, .[0].digest]]|flatten " validated_template="$( limactl validate "${template}" --fill 2>/dev/null || echo "{.containerd: {system: false, user: false, archives: []}}" )" yq -o=t eval "${yq_filter}" <<<"${validated_template}" } # e.g. # ```console # $ location_to_sha256 "https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img" # ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8 # ``` function location_to_sha256() { ( set -o pipefail local -r location="$1" if command -v sha256sum >/dev/null; then sha256="$(echo -n "${location}" | sha256sum | cut -d' ' -f1)" elif command -v shasum >/dev/null; then sha256="$(echo -n "${location}" | shasum -a 256 | cut -d' ' -f1)" else error_exit "sha256sum or shasum not found" fi echo "${sha256}" ) } # e.g. # ```console # $ cache_download_dir # .download # on GitHub Actions # /home/user/.cache/lima/download # on Linux # /Users/user/Library/Caches/lima/download # on macOS # /home/user/.cache/lima/download # on others # ``` function cache_download_dir() { if [[ ${GITHUB_ACTIONS:-false} == true ]]; then echo ".download" else case "$(uname -s)" in Linux) echo "${XDG_CACHE_HOME:-${HOME}/.cache}/lima/download" ;; Darwin) echo "${HOME}/Library/Caches/lima/download" ;; *) echo "${HOME}/.cache/lima/download" ;; esac fi } # e.g. # ```console # $ location_to_cache_path "https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img" # .download/by-url-sha256/ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8 # ``` function location_to_cache_path() { local location=$1 [[ ${location} != "null" ]] || return sha256=$(location_to_sha256 "${location}") && download_dir=$(cache_download_dir) && echo "${download_dir}/by-url-sha256/${sha256}" } # e.g. # ```console # $ cache_key_from_prefix_location_and_digest image "https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img" "sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a" # image:ubuntu-24.04-server-cloudimg-arm64.img-sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a # $ cache_key_from_prefix_location_and_digest image "https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img" "null" # image:ubuntu-24.04-server-cloudimg-arm64.img-url-sha256:ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8 # ``` function cache_key_from_prefix_location_and_digest() { local prefix=$1 location=$2 digest=$3 location_basename [[ ${location} != "null" ]] || return location_basename=$(basename "${location}") if [[ ${digest} != "null" ]]; then echo "${prefix}:${location_basename}-${digest}" else # use sha256 of location as key if digest is not available echo "${prefix}:${location_basename}-url-sha256:$(location_to_sha256 "${location}")" fi } # e.g. # ```console # $ print_path_and_key_for_cache image "https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img" "sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a" # image-path=.download/by-url-sha256/ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8 # image-key=image:ubuntu-24.04-server-cloudimg-arm64.img-sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a # ``` function print_path_and_key_for_cache() { local -r prefix=$1 location=$2 digest=$3 cache_path=$(location_to_cache_path "${location}" || true) cache_key=$(cache_key_from_prefix_location_and_digest "${prefix}" "${location}" "${digest}" || true) echo "${prefix}-path=${cache_path}" echo "${prefix}-key=${cache_key}" } # e.g. # ```console # $ print_cache_informations_from_template templates/default.yaml # image-path=.download/by-url-sha256/ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8 # image-key=image:ubuntu-24.04-server-cloudimg-arm64.img-sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a # kernel-path= # kernel-key= # initrd-path= # initrd-key= # containerd-path=.download/by-url-sha256/21cc8dfa548ea8a678135bd6984c9feb9f8a01901d10b11bb491f6f4e7537158 # containerd-key=containerd:nerdctl-full-1.7.6-linux-arm64.tar.gz-sha256:77c747f09853ee3d229d77e8de0dd3c85622537d82be57433dc1fca4493bab95 # $ print_cache_informations_from_template templates/experimental/riscv64.yaml # image-path=.download/by-url-sha256/760b6ec69c801177bdaea06d7ee25bcd6ab72a331b9d3bf38376578164eb8f01 # image-key=image:ubuntu-24.04-server-cloudimg-riscv64.img-sha256:361d72c5ed9781b097ab2dfb1a489c64e51936be648bbc5badee762ebdc50c31 # kernel-path=.download/by-url-sha256/4568026693dc0f31a551b6741839979c607ee6bb0bf7209c89f3348321c52c61 # kernel-key=kernel:qemu-riscv64_smode_uboot.elf-sha256:d4b3a10c3ef04219641802a586dca905e768805f5a5164fb68520887df54f33c # initrd-path= # initrd-key= # ``` function print_cache_informations_from_template() { ( set -o pipefail local template index image_kernel_initrd_info location digest containerd_info template=$(download_template_if_needed "$1") || return local -r template="${template}" index=$(print_image_locations_for_arch_from_template "${template}" "${@:2}" | print_valid_image_index) || return local -r index="${index}" image_kernel_initrd_info=$(print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index "${template}" "${index}" "${@:2}") || return # shellcheck disable=SC2034 read -r image_location image_digest kernel_location kernel_digest initrd_location initrd_digest <<<"${image_kernel_initrd_info}" for prefix in image kernel initrd; do location=${prefix}_location digest=${prefix}_digest print_path_and_key_for_cache "${prefix}" "${!location}" "${!digest}" done if command -v limactl >/dev/null; then containerd_info=$(print_containerd_config_for_arch_from_template "${template}" "${@:2}") || return read -r containerd_enabled containerd_location containerd_digest <<<"${containerd_info}" if [[ ${containerd_enabled} == "true" ]]; then print_path_and_key_for_cache "containerd" "${containerd_location}" "${containerd_digest}" fi fi ) } # Compatible with hashFile() in GitHub Actions # e.g. # ```console # $ hash_file templates/default.yaml # ceec5ba3dc8872c083b2eb7f44e3e3f295d5dcdeccf0961ee153be6586525e5e # ``` function hash_file() { ( set -o pipefail local hash="" for file in "$@"; do hash="${hash}$(sha256sum "${file}" | cut -d' ' -f1)" || return done echo "${hash}" | xxd -r -p | sha256sum | cut -d' ' -f1 ) } # Download the file to the cache directory and print the path. # e.g. # ```console # $ download_to_cache "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-arm64.img" # .download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on GitHub Actions # /home/user/.cache/lima/download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on Linux # /Users/user/Library/Caches/lima/download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on macOS # /home/user/.cache/lima/download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on others function download_to_cache() { local cache_path use_redirected_location=${2:-YES} cache_path=$(location_to_cache_path "$1") # before checking remote location, check if the data file is already downloaded and the time file is updated within 10 minutes if [[ -f ${cache_path}/data && -n "$(find "${cache_path}/time" -mmin -10 || true)" ]]; then echo "${cache_path}/data" return fi # check the remote location local curl_info_json write_out write_out='{ "json":%{json}, "header":%{header_json} }' curl_info_json=$(curl -sSLI -w "${write_out}" "$1" -o /dev/null) local code time type url code=$(jq -r '.json.http_code' <<<"${curl_info_json}") time=$(jq -r '(.header["last-modified"]|first) // (.header["date"]|first) // empty' <<<"${curl_info_json}") type=$(jq -r '.json.content_type' <<<"${curl_info_json}") if [[ ${use_redirected_location} == "YES" ]]; then url=$(jq -r '.json.url_effective' <<<"${curl_info_json}") else url=$1 fi [[ ${code} == 200 ]] || error_exit "Failed to download $1" cache_path=$(location_to_cache_path "${url}") [[ -d ${cache_path} ]] || mkdir -p "${cache_path}" local needs_download=0 [[ -f ${cache_path}/data ]] || needs_download=1 [[ -f ${cache_path}/time && "$(<"${cache_path}/time")" == "${time}" ]] || needs_download=1 [[ -f ${cache_path}/type && "$(<"${cache_path}/type")" == "${type}" ]] || needs_download=1 if [[ ${needs_download} -eq 1 ]]; then curl_info_json=$( echo "downloading ${url}" >&2 curl -SL -w "${write_out}" --no-clobber -o "${cache_path}/data" "${url}" ) local filename code=$(jq -r '.json.http_code' <<<"${curl_info_json}") time=$(jq -r '(.header["last-modified"]|first) // (.header["date"]|first) // empty' <<<"${curl_info_json}") type=$(jq -r '.json.content_type' <<<"${curl_info_json}") url=$(jq -r '.json.url_effective' <<<"${curl_info_json}") filename=$(jq -r '.json.filename_effective' <<<"${curl_info_json}") [[ ${code} == 200 ]] || error_exit "Failed to download ${url}" [[ "${cache_path}/data" == "${filename}" ]] || mv "${filename}" "${cache_path}/data" # sha256.digest seems existing if expected digest is available. so, not creating it here. # sha256sum "${cache_path}/data" | awk '{print "sha256:"$1}' >"${cache_path}/sha256.digest" echo -n "${time}" >"${cache_path}/time" else touch "${cache_path}/time" fi [[ -f ${cache_path}/type ]] || echo -n "${type}" >"${cache_path}/type" [[ -f ${cache_path}/url ]] || echo -n "${url}" >"${cache_path}/url" echo "${cache_path}/data" } # Download the file to the cache directory without redirect and print the path. function download_to_cache_without_redirect() { download_to_cache "$1" "NO" } ================================================ FILE: hack/calculate-cache.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # This script calculates the expected content size, actual cached size, and cache-keys used in caching method before and after # implementation in https://github.com/lima-vm/lima/pull/2508 # # Answer to the question in https://github.com/lima-vm/lima/pull/2508#discussion_r1699798651 scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./common.inc.sh . "${scriptdir}/cache-common-inc.sh" # usage: [DEBUG=1] ./hack/calculate-cache.sh # DEBUG=1 will save the collected information in .calculate-cache-collected-info-{before,after}.yaml # # This script does: # 1. extracts `runs_on` and `template` from workflow file (.github/workflows/test.yml) # 2. check each template for image, kernel, initrd, and nerdctl # 3. detect size of image, kernel, initrd, and nerdctl (responses from remote are cached for faster iteration) # save the response in .check_location-response-cache.yaml # 4. print content size, actual cache size (if available), by cache key # # The major differences for reducing cache usage are as follows: # - it is now cached `~/.cache/lima/download/by-url-sha256/$sha256` instead of caching `~/.cache/lima/download` # - the cache keys are now based on the image, kernel, initrd, and nerdctl digest instead of the template file's hash # - enables the use of cache regardless of the operating system used to execute CI. # # The script requires the following commands: # - gh: GitHub CLI. # Using to get the cache information # - jq: Command-line JSON processor # Parse the workflow file and print runs-on and template. # Parse output from gh cache list # Calculate the expected content size, actual cached size, and cache-keys used. # - limactl: lima CLI. # Using to validate the template file for getting nerdctl location and digest. # - sha256sum: Print or check SHA256 (256-bit) checksums # - xxd: make a hexdump or do the reverse. # Using to simulate the 'hashFile()' function in the workflow. # - yq: Command-line YAML processor. # Parse the template file for image and nerdctl location, digest, and size. # Parse the cache response file for the cache. # Convert the collected information to JSON. set -u -o pipefail required_commands=(gh jq limactl sha256sum xxd yq) for cmd in "${required_commands[@]}"; do if ! command -v "${cmd}" &>/dev/null; then echo "${cmd} is required. Please install it" >&2 exit 1 fi done # current workflow uses x86_64 only arch=x86_64 LIMA_HOME=$(mktemp -d) export LIMA_HOME # parse the workflow file and print runs-on and template # e.g. # ```console # $ print_runs_on_template_from_workflow .github/workflows/test.yml # macos-12 templates/default.yaml # ubuntu-24.04 templates/alpine.yaml # ubuntu-24.04 templates/debian.yaml # ubuntu-24.04 templates/fedora.yaml # ubuntu-24.04 templates/archlinux.yaml # ubuntu-24.04 templates/opensuse.yaml # ubuntu-24.04 templates/experimental/net-user-v2.yaml # ubuntu-24.04 templates/experimental/9p.yaml # ubuntu-24.04 templates/docker.yaml # ubuntu-24.04 templates/../hack/test-templates/alpine-iso-9p-writable.yaml # ubuntu-24.04 templates/../hack/test-templates/test-misc.yaml # macos-12 templates/vmnet.yaml # macos-12 https://raw.githubusercontent.com/lima-vm/lima/v0.15.1/examples/ubuntu-lts.yaml # macos-13 templates/experimental/vz.yaml # macos-13 templates/fedora.yaml # ``` function print_runs_on_template_from_workflow() { yq -o=j "$1" | jq -r ' "./.github/actions/setup_cache_for_template" as $action | "\\$\\{\\{\\s*(?\\S*)\\s*\\}\\}" as $pattern | .jobs | map_values(select(.steps)| ."runs-on" as $runs_on | { template: .steps | map_values(select(.uses == $action)) | first |.with.template, matrix: .strategy.matrix } | select(.template) | . + { path: .template | (if test($pattern) then sub(".*\($pattern).*";"\(.path)")|split(".") else null end) } | ( .template as $template| if .path then getpath(.path)|map(. as $item|$template|sub($pattern;$item)) else [$template] end ) | map("\($runs_on)\t\(.)") ) | flatten |.[] ' } # returns the OS name from the runner equivalent to the expression `${{ runner.os }}` in the workflow # e.g. # ```console # $ runner_os_from_runner "macos-12" # macOS # $ runner_os_from_runner "ubuntu-24.04" # Linux # ``` function runner_os_from_runner() { # shellcheck disable=SC2249 case "$1" in macos*) echo macOS ;; ubuntu*) echo Linux ;; esac } # format first column to MiB # e.g. # ```console # $ echo 585498624 | size_to_mib # 558.38 MiB # ``` function size_to_mib() { awk ' function mib(size) { return sprintf("%7.2f MiB", size / 1024 / 1024) } int($1)>0{ $1=" "mib($1) } int($2)>0{ $2=mib($2) } int($2)==0 && NF>1{ $2="<>" } { print } ' } # actual_cache_sizes=$(gh cache list --json key,createdAt,sizeInBytes|jq '[.[]|{"key":.key,"value":.sizeInBytes}]|from_entries') # e.g. # ```console # $ echo "${actual_cache_sizes}" # { # "Linux-1c3b2791d52735d916dc44767c745c2319eb7cae74af71bbf45ddb268f42fc1d": 810758533, # "Linux-231c66957fc2cdb18ea10e63f60770049026e29051ecd6598fc390b60d6a4fa6": 633036717, # "Linux-3b906d46fa532e3bc348c35fc8e7ede6c69f0b27032046ee2cbb56d4022d1146": 574242367, # "Linux-69a547b760dbf1650007ed541408474237bc611704077214adcac292de556444": 70310855, # "Linux-7782f8b4ff8cd378377eb79f8d61c9559b94bbd0c11d19eb380ee7bda19af04e": 494141177, # "Linux-8812aedfe81b4456d421645928b493b1f2f88aff04b7f3171207492fd44cd189": 812730766, # "Linux-caa7d8af214d55ad8902e82d5918e61573f3d6795d2b5ad9a35305e26fa0e6a9": 754723892, # "Linux-colima-v0.6.5": 226350335, # "Linux-de83bce0608d787e3c68c7a31c5fab2b6d054320fd7bf633a031845e2ee03414": 810691197, # "Linux-eb88a19dfcf2fb98278e7c7e941c143737c6d7cd8950a88f58e04b4ee7cef1bc": 570625794, # "Linux-f88f0b3b678ff6432386a42bdd27661133c84a36ad29f393da407c871b0143eb": 68490954, # "golangci-lint.cache-Linux-2850-74615231540133417fd618c72e37be92c5d3b3ad": 2434144, # "macOS-231c66957fc2cdb18ea10e63f60770049026e29051ecd6598fc390b60d6a4fa6": 633020464, # "macOS-49aa50a4872ded07ebf657c0eaf9e44ecc0c174d033a97c537ecd270f35b462f": 813179462, # "macOS-8f37f663956af5f743f0f99ab973729b6a02f200ebfac7a3a036eff296550732": 810756770, # "macOS-ef5509b5d4495c8c3590442ee912ad1c9a33f872dc4a29421c524fc1e2103b59": 813179476, # "macOS-upgrade-v0.15.1": 1157814690, # "setup-go-Linux-ubuntu20-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52": 1015518352, # "setup-go-Linux-ubuntu20-go-1.23.0-6bce2eefc6111ace836de8bb322432c072805737d5f3c5a3d47d2207a05f50df": 936433302, # "setup-go-Linux-ubuntu24-go-1.22.6-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52": 1090001859, # "setup-go-Linux-ubuntu24-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52": 526146768, # "setup-go-Windows-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52": 1155374040, # "setup-go-Windows-go-1.23.0-6bce2eefc6111ace836de8bb322432c072805737d5f3c5a3d47d2207a05f50df": 1056433137, # "setup-go-macOS-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52": 1060919942, # "setup-go-macOS-go-1.23.0-6bce2eefc6111ace836de8bb322432c072805737d5f3c5a3d47d2207a05f50df": 982139209 # } actual_cache_sizes=$( gh cache list --json key,createdAt,sizeInBytes \ --jq 'sort_by(.createdAt)|reverse|unique_by(.key)|sort_by(.key)|map({"key":.key,"value":.sizeInBytes})|from_entries' ) workflows=( .github/workflows/test.yml ) # shellcheck disable=SC2016 echo "=> compare expected content size, actual cached size, and cache-keys used before and after the change in https://github.com/lima-vm/lima/pull/2508" # iterate over before and after for cache_method in before after; do echo "==> ${cache_method}" echo "content-size actual-size cache-key" output_yaml=$( for workflow in "${workflows[@]}"; do print_runs_on_template_from_workflow "${workflow}" done | while IFS=$'\t' read -r runner template; do template=$(download_template_if_needed "${template}") || continue arch=$(detect_arch "${template}" "${arch}") || continue index=$(print_image_locations_for_arch_from_template "${template}" "${arch}" | print_valid_image_index) || continue image_kernel_initrd_info=$(print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index "${template}" "${index}" "${arch}") || continue # shellcheck disable=SC2034 # shellcheck does not detect dynamic variables usage read -r image_location image_digest kernel_location kernel_digest initrd_location initrd_digest <<<"${image_kernel_initrd_info}" containerd_info=$(print_containerd_config_for_arch_from_template "${template}" "${@:2}") || continue # shellcheck disable=SC2034 # shellcheck does not detect dynamic variables usage read -r _containerd_enabled containerd_location containerd_digest <<<"${containerd_info}" if [[ ${cache_method} != after ]]; then key=$(runner_os_from_runner "${runner}" || true)-$(hash_file "${template}") else key=$(cache_key_from_prefix_location_and_digest image "${image_location}" "${image_digest}") fi size=$(size_from_location "${image_location}") for prefix in containerd kernel initrd; do location="${prefix}_location" digest="${prefix}_digest" [[ ${!location} != null ]] || continue if [[ ${cache_method} != after ]]; then # previous caching method packages all files in download to a single cache key size=$((size + $(size_from_location "${!location}"))) else # new caching method caches each file separately key_for_prefix=$(cache_key_from_prefix_location_and_digest "${prefix}" "${!location}" "${!digest}") size_for_prefix=$(size_from_location "${!location}") printf -- "- key: %s\n template: %s\n location: %s\n digest: %s\n size: %s\n" \ "${key_for_prefix}" "${template}" "${!location}" "${!digest}" "${size_for_prefix}" fi done printf -- "- key: %s\n template: %s\n location: %s\n digest: %s\n size: %s\n" \ "${key}" "${template}" "${image_location}" "${image_digest}" "${size}" done ) output_json=$(yq -o=j . <<<"${output_yaml}") # print size key jq --argjson actual_size "${actual_cache_sizes}" -r 'unique_by(.key)|sort_by(.key)|.[]|[.size, $actual_size[.key] // 0, .key]|@tsv' <<<"${output_json}" | size_to_mib # total echo "------------" jq '[unique_by(.key)|.[]|.size]|add' <<<"${output_json}" | size_to_mib # save the collected information as yaml if DEBUG is set if [[ -n ${DEBUG:+1} ]]; then cat <<<"${output_yaml}" >".calculate-cache-collected-info-${cache_method}.yaml" echo "Saved the collected information in .calculate-cache-collected-info-${cache_method}.yaml" fi echo "" done ================================================ FILE: hack/codesign/debugserver ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # # `hack/codesign/debugserver` # # This script wraps the LLDB `debugserver` to `codesign` the target executable. # This is needed for [Delve](https://github.com/go-delve/delve) to work properly # on macOS with the virtualization framework. # # ## How to use this script with Delve # # ### Use `DELVE_DEBUGSERVER_PATH` environment variable # # Override `debugserver` by setting the `DELVE_DEBUGSERVER_PATH` environment variable. # Ref: [Environment variables - Using Delve](https://github.com/go-delve/delve/blob/master/Documentation/usage/README.md#environment-variables) # # e.g. in `.vscode/launch.json`: # ```jsonc # { # "version": "0.2.0", # "configurations": [ # { # "name": "hostagent for input instance", # "type": "go", # "request": "launch", # "mode": "debug", # // Use integratedTerminal to stop using ctrl+C # "console": "integratedTerminal", # "program": "${workspaceFolder}/cmd/limactl", # "buildFlags": [ # "-ldflags=-X github.com/lima-vm/lima/pkg/version.Version=2.0.0-alpha.0", # ], # "env": { # "CGO_ENABLED": "1", # "DELVE_DEBUGSERVER_PATH": "${workspaceFolder}/hack/codesign/debugserver", # "LIMA_SSH_PORT_FORWARDER": "true", # }, # "cwd": "${userHome}/.lima/${input:targetInstance}", # "args": [ # "--debug", # "hostagent", # "--pidfile", "ha.pid", # "--socket", "ha.sock", # "--guestagent", "${workspaceFolder}/_output/share/lima/lima-guestagent.Linux-aarch64", # "${input:targetInstance}" # ], # }, # ], # "inputs": [ # { # "id": "targetInstance", # "type": "promptString", # "description": "Input target instance parameter for `limactl` command", # } # ] # } # ``` # # ### Override `debugserver` in the PATH # # You can also override the `debugserver` in the PATH environment variable. # # e.g. in `.vscode/launch.json`: # ```diff # "env": { # "CGO_ENABLED": "1", # - "DELVE_DEBUGSERVER_PATH": "${workspaceFolder}/hack/codesign/debugserver", # + "PATH": "${workspaceFolder}/hack/codesign:${env:PATH}", # }, # ``` set -eu -o pipefail script_dir="$(dirname "$0")" # Codesign the target executable if vz.entitlements exists entitlements="${script_dir}/../../vz.entitlements" if [ -f "${entitlements}" ]; then prev_arg= for arg in "$@"; do # find the target executable in the args next to "--" if [ "${prev_arg}" = "--" ]; then codesign --entitlements "${entitlements}" --force -s - -v "${arg}" || { echo "error: codesign failed" >&2 exit 1 } break fi prev_arg="${arg}" done fi # Simulate how Delve locates debugserver # https://github.com/go-delve/delve/blob/65a6830eb7f0b882e0c52df0ef9585945ed55470/pkg/proc/gdbserial/gdbserver.go#L103-L129 # 1. PATH (in this script, excluding the directory of this script to avoid recursion) candidate_paths="$(echo "${PATH}" | tr ':' '\n' | grep --fixed-strings --invert-match --line-regexp "${script_dir}" | paste -sd: -)" # 2. CommandLineTools LLDB path CLT_path="/Library/Developer/CommandLineTools" candidate_paths="${candidate_paths}:${CLT_path}/Library/PrivateFrameworks/LLDB.framework/Versions/A/Resources/" # 3. Xcode LLDB path (if Xcode is installed) if developer_dir=$(xcode-select --print-path 2>/dev/null) && [ "${developer_dir}" != "${CLT_path}" ]; then # Xcode is installed candidate_paths="${candidate_paths}:${developer_dir}/../SharedFrameworks/LLDB.framework/Versions/A/Resources/" fi # Find debugserver in the candidate paths debugserver=$(PATH="${candidate_paths}" command -v debugserver) || { echo "error: debugserver not found" >&2 exit 1 } # Execute debugserver with the original arguments exec "${debugserver}" "$@" ================================================ FILE: hack/common.inc.sh ================================================ # shellcheck shell=bash cleanup_cmd="" trap 'eval ${cleanup_cmd}' EXIT function defer { [ -n "${cleanup_cmd}" ] && cleanup_cmd="${cleanup_cmd}; " cleanup_cmd="${cleanup_cmd}$1" } function INFO() { echo "TEST| [INFO] $*" } function WARNING() { echo >&2 "TEST| [WARNING] $*" } function ERROR() { echo >&2 "TEST| [ERROR] $*" } if [[ ${BASH_VERSINFO:-0} -lt 4 ]]; then ERROR "Bash version is too old: ${BASH_VERSION}" exit 1 fi : "${LIMA_HOME:=${HOME_HOST:-$HOME}/.lima}" _IPERF3=iperf3 # iperf3-darwin does some magic on macOS to avoid "No route on host" on macOS 15 # https://github.com/lima-vm/socket_vmnet/issues/85 [ "$(uname -s)" = "Darwin" ] && _IPERF3="iperf3-darwin" : "${IPERF3:=$_IPERF3}" # Setup LIMA_TEMPLATES_PATH because the templates are not installed, but reference base templates # via template:_images/* and template:_default/*. templates_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../templates" && pwd)" : "${LIMA_TEMPLATES_PATH:-$templates_dir}" export LIMA_TEMPLATES_PATH ================================================ FILE: hack/debug-cache.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail cache_dir="${HOME}/Library/Caches" if [ "$(uname -s)" != "Darwin" ]; then cache_dir="${HOME}/.cache" fi if [ ! -e "${cache_dir}/lima/download/by-url-sha256" ]; then echo "No cache" exit 0 fi for f in "${cache_dir}/lima/download/by-url-sha256/"*; do echo "$f" ls -l "$f" cat "${f}/url" echo echo --- done ================================================ FILE: hack/gogenerate/protoc.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # Generate Go code from a .proto file. # Expected to be called from `//go:generate` directive. set -eu if [ "$#" -ne 1 ]; then echo >&2 "Usage: $0 FILE" exit 1 fi PROTO="$1" ## a.proto BASE="$(basename "$PROTO" .proto)" ## a PB_DESC="${BASE}.pb.desc" ## a.pb.desc PB_GO="${BASE}.pb.go" ## a.pb.go GRPC_PB_GO="${BASE}_grpc.pb.go" ## a_grpc.pb.go set -x protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative "$PROTO" --descriptor_set_out="$PB_DESC" # -// - protoc v6.32.0 # +// - protoc version [omitted for reproducibility] # # perl is used because `sed -i` is not portable across BSD (macOS) and GNU. perl -pi -E 's@(^//.*protoc.*)[[:blank:]]+v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+@\1 [version omitted for reproducibility]@g' "$PB_GO" "$GRPC_PB_GO" ================================================ FILE: hack/inject-cmdline-to-template.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # # This script does # 1. detect arch from template if not provided # 2. extract location by parsing template using arch # 3. get the image location # 4. check the image location is supported # 5. build the kernel and initrd location, digest, and cmdline # 6. inject the kernel and initrd location, digest, and cmdline to the template # 7. output kernel_location, kernel_digest, cmdline, initrd_location, initrd_digest set -eu -o pipefail template="$1" appending_options="$2" # 1. detect arch from template if not provided arch="${3:-$(yq '.arch // ""' "${template}")}" arch="${arch:-$(uname -m)}" # normalize arch. amd64 -> x86_64, arm64 -> aarch64 case "${arch}" in amd64 | x86_64) arch=x86_64 ;; aarch64 | arm64) arch=aarch64 ;; armv7l | armhf) arch=armv7l ;; ppc64el | ppc64le) arch=ppc64le ;; s390x) arch=s390x ;; riscv64) arch=riscv64 ;; *) echo "Unsupported arch: ${arch}" >&2 exit 1 ;; esac # 2. extract location by parsing template using arch readonly yq_filter=" .images[]|select(.arch == \"${arch}\")|.location " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location function check_location() { local location=$1 http_code http_code=$(curl -sIL -w "%{http_code}" "${location}" -o /dev/null) [[ ${http_code} -eq 200 ]] } while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" readonly locations=("${arr[@]}") for ((i = 0; i < ${#locations[@]}; i++)); do [[ ${locations[i]} != "null" ]] || continue # shellcheck disable=SC2310 if check_location "${locations[i]}"; then location=${locations[i]} index=${i} break fi done # 4. check the image location is supported case "${location:-}" in https://cloud-images.ubuntu.com/minimal/* | https://cloud-images.ubuntu.com/daily/server/minimal/*) readonly default_cmdline="root=/dev/vda1 ro console=tty1 console=ttyAMA0" ;; https://cloud-images.ubuntu.com/*) readonly default_cmdline="root=LABEL=cloudimg-rootfs ro console=tty1 console=ttyAMA0" ;; *) echo "Unsupported image location: ${location}" >&2 exit 1 ;; esac # 5. build the kernel and initrd location, digest, and cmdline location_dirname=$(dirname "${location}")/unpacked sha256sums=$(curl -sSLf "${location_dirname}/SHA256SUMS") location_basename=$(basename "${location}") # cmdline cmdline="${default_cmdline} ${appending_options}" # kernel kernel_basename="${location_basename/.img/-vmlinuz-generic}" kernel_digest=$(awk "/${kernel_basename}/{print \"sha256:\"\$1}" <<<"${sha256sums}") kernel_location="${location_dirname}/${kernel_basename}" # initrd initrd_basename="${location_basename/.img/-initrd-generic}" initrd_digest=$(awk "/${initrd_basename}/{print \"sha256:\"\$1}" <<<"${sha256sums}") initrd_location="${location_dirname}/${initrd_basename}" # 6. inject the kernel and initrd location, digest, and cmdline to the template function inject_to() { # shellcheck disable=SC2034 local template=$1 arch=$2 index=$3 key=$4 location=$5 digest=$6 cmdline=${7:-} fields=() IFS=, # shellcheck disable=SC2310 check_location "${location}" || return 0 for field_name in location digest cmdline; do [[ -z ${!field_name} ]] || fields+=("\"${field_name}\": \"${!field_name}\"") done limactl edit --log-level error --set "setpath([(.images[] | select(.arch == \"${arch}\") | path)].[${index}] + \"${key}\"; { ${fields[*]}})" "${template}" } inject_to "${template}" "${arch}" "${index}" "kernel" "${kernel_location}" "${kernel_digest}" "${cmdline}" inject_to "${template}" "${arch}" "${index}" "initrd" "${initrd_location}" "${initrd_digest}" # 7. output kernel_location, kernel_digest, cmdline, initrd_location, initrd_digest readonly outputs=(kernel_location kernel_digest cmdline initrd_location initrd_digest) for output in "${outputs[@]}"; do echo "${output}=${!output}" done ================================================ FILE: hack/install-qemu.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # Install qemu on GitHub Actions runner. # Not expected to be used outside GitHub Actions. set -eux # apt-get update has to be run beforehand apt-get install -y --no-install-recommends ovmf qemu-system-x86 qemu-utils modprobe kvm # `usermod -aG kvm ${SUDO_USER}` does not take an effect on GHA chown "${SUDO_USER}" /dev/kvm qemu-system-x86_64 --version ================================================ FILE: hack/ltag/bash.txt ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 ================================================ FILE: hack/ltag/dockerfile.txt ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 ================================================ FILE: hack/ltag/go.txt ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 ================================================ FILE: hack/ltag/makefile.txt ================================================ # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 ================================================ FILE: hack/oss-fuzz-build.sh ================================================ #!/bin/bash -eu # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # This script is used by OSS-Fuzz to build and run Limas fuzz tests continuously. # Limas OSS-Fuzz integration can be found here: https://github.com/google/oss-fuzz/tree/master/projects/lima # Modify https://github.com/google/oss-fuzz/blob/master/projects/lima/project.yaml for access management to Limas OSS-Fuzz crashes. printf "package store\nimport _ \"github.com/AdamKorcz/go-118-fuzz-build/testing\"\n" >"$SRC"/lima/pkg/store/register.go go mod tidy compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/store FuzzLoadYAMLByFilePath FuzzLoadYAMLByFilePath compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/store FuzzInspect FuzzInspect compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/nativeimgutil FuzzConvertToRaw FuzzConvertToRaw compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/cidata FuzzSetupEnv FuzzSetupEnv compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/iso9660util FuzzIsISO9660 FuzzIsISO9660 compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/guestagent/procnettcp FuzzParse FuzzParse compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/yqutil FuzzEvaluateExpression FuzzEvaluateExpression compile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/downloader FuzzDownload FuzzDownload ================================================ FILE: hack/test-mount-home.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=common.inc.sh source "${scriptdir}/common.inc.sh" if [ "$#" -ne 1 ]; then ERROR "Usage: $0 NAME" exit 1 fi NAME="$1" hometmp="${HOME_HOST:-$HOME}/lima-test-tmp" hometmpguest="${HOME_GUEST:-$HOME}/lima-test-tmp" INFO "Testing home access (\"$hometmp\")" rm -rf "$hometmp" mkdir -p "$hometmp" defer "rm -rf \"$hometmp\"" echo "random-content-${RANDOM}" >"$hometmp/random" expected="$(cat "$hometmp/random")" got="$(limactl shell "$NAME" cat "$hometmpguest/random")" INFO "$hometmp/random: expected=${expected}, got=${got}" if [ "$got" != "$expected" ]; then ERROR "Home directory is not shared?" exit 1 fi ================================================ FILE: hack/test-nonplain-static-port-forward.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -euxo pipefail INSTANCE=nonplain-static-port-forward TEMPLATE="$(dirname "$0")/test-templates/test-misc.yaml" limactl delete -f "$INSTANCE" || true limactl start --name="$INSTANCE" --tty=false "$TEMPLATE" limactl shell "$INSTANCE" -- bash -c 'until systemctl is-active --quiet nginx; do sleep 1; done' limactl shell "$INSTANCE" -- bash -c 'until systemctl is-active --quiet test-server-9080; do sleep 1; done' limactl shell "$INSTANCE" -- bash -c 'until systemctl is-active --quiet test-server-9070; do sleep 1; done' curl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'Static port forwarding (9090) works in normal mode!' curl -sSf http://127.0.0.1:29080 | grep -i 'Dynamic port 9080' && echo 'Dynamic port forwarding (29080) works in normal mode!' curl -sSf http://127.0.0.1:29070 | grep -i 'Dynamic port 9070' && echo 'Dynamic port forwarding (29070) works in normal mode!' limactl delete -f "$INSTANCE" echo "All tests passed for normal mode - both static and dynamic ports work!" # EOF ================================================ FILE: hack/test-plain-static-port-forward.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -euxo pipefail INSTANCE=plain-static-port-forward TEMPLATE="$(dirname "$0")/test-templates/test-misc.yaml" limactl delete -f "$INSTANCE" || true limactl start --name="$INSTANCE" --plain=true --tty=false "$TEMPLATE" limactl shell "$INSTANCE" -- bash -c 'until systemctl is-active --quiet nginx; do sleep 1; done' if ! curl -sSf http://127.0.0.1:9090 | grep -i 'nginx'; then echo 'ERROR: Static port forwarding (9090) does not work in plain mode!' exit 1 fi echo 'Static port forwarding (9090) works in plain mode!' if curl -sSf http://127.0.0.1:29080 2>/dev/null; then echo 'ERROR: Dynamic port 29080 should not be forwarded in plain mode!' exit 1 else echo 'Dynamic port 29080 is correctly NOT forwarded in plain mode.' fi if curl -sSf http://127.0.0.1:29070 2>/dev/null; then echo 'ERROR: Dynamic port 29070 should not be forwarded in plain mode!' exit 1 else echo 'Dynamic port 29070 is correctly NOT forwarded in plain mode.' fi limactl delete -f "$INSTANCE" echo "All tests passed for plain mode - only static ports work!" # EOF ================================================ FILE: hack/test-port-forwarding.pl ================================================ #!/usr/bin/env perl # This script tests the port forwarding settings of lima. It has to be run # twice: once to update the instance yaml file with the port forwarding # rules (before the instance is started). And once when the instance is # running to perform the tests: # # ./hack/test-port-forwarding.pl templates/default.yaml # limactl --tty=false start templates/default.yaml # git restore templates/default.yaml # ./hack/test-port-forwarding.pl default [nc|socat [nc|socat]] [timeout] # # TODO: support for ipv6 host addresses use strict; use warnings; use Config qw(%Config); use File::Spec::Functions qw(catfile); use IO::Handle qw(); use JSON::PP; use Socket qw(inet_ntoa); use Sys::Hostname qw(hostname); my $connectionTimeout = 1; # seconds my $instance = shift; my $listener; my $writer; while (my $arg = shift) { if ($arg eq "nc" || $arg eq "socat") { $listener = $arg unless defined $listener; $writer = $arg if defined $listener && !defined $writer; } elsif ($arg =~ /^\d+$/) { $connectionTimeout = $arg; } else { die "Usage: $0 [instance|yaml-file] [nc|socat [nc|socat]] [timeout]\n"; } } $listener ||= "nc"; $writer ||= $listener; my $addr = scalar gethostbyname(hostname()); # If hostname address cannot be determines, use localhost to trigger fallback to system_profiler lookup my $ipv4 = length $addr ? inet_ntoa($addr) : "127.0.0.1"; my $ipv6 = ""; # todo # macOS GitHub runners seem to use "localhost" as the hostname if ($ipv4 eq "127.0.0.1" && $Config{osname} eq "darwin") { $ipv4 = qx(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first'); chomp $ipv4; } my $instDir = qx(limactl list "$instance" --yq .dir); chomp $instDir; # platform independent way to add trailing path separator my $sockDir = catfile($instDir, "sock", ""); # If $instance is a filename, add our portForwards to it to enable testing if (-f $instance) { open(my $fh, "+< $instance") or die "Can't open $instance for read/write: $!"; my @yaml; while (<$fh>) { # Remove existing "portForwards:" section from the config file my $seq = /^portForwards:/ ... /^[a-z]/; next if $seq && $seq !~ /E0$/; push @yaml, $_; } seek($fh, 0, 0); truncate($fh, 0); print $fh $_ for @yaml; while () { s/ipv4/$ipv4/g; s/ipv6/$ipv6/g; print $fh $_; } exit; } # Check if netcat is available before running tests my $nc_path = `command -v nc 2>/dev/null`; chomp $nc_path; unless ($nc_path) { die "Error: 'nc' (netcat) is not installed on the host system.\n" . "Please install netcat to run this test script:\n" . " - On macOS: brew install netcat\n" . " - On Ubuntu/Debian: sudo apt-get install netcat\n" . " - On RHEL/CentOS: sudo yum install nmap-ncat\n"; } # Otherwise $instance must be the name of an already running instance that has been # configured with our portForwards settings. my $instanceType = qx(limactl ls --json "$instance" | jq -r '.vmType' | sed s/x/x/); chomp $instanceType; # Get sshLocalPort for lima instance my $sshLocalPort; open(my $ls, "limactl ls --json |") or die; while (<$ls>) { next unless /"name":"$instance"/; ($sshLocalPort) = /"sshLocalPort":(\d+)/ or die; last; } die "Cannot determine sshLocalPort" unless $sshLocalPort; # Extract forwarding tests from the "portForwards" section my @test; while () { chomp; s/^\s+#\s*//; next unless /^(forward|ignore)/; if (/ipv6/ && !$ipv6) { printf "🚧 Not yet: # $_\n"; next; } s/sshLocalPort/$sshLocalPort/g; s/ipv4/$ipv4/g; s/ipv6/$ipv6/g; s/sockDir\//$sockDir/g; # forward: 127.0.0.1 899 → 127.0.0.1 799 # ignore: 127.0.0.2 8888 /^(forward|ignore):\s+([0-9.:]+)\s+(\d+)(?:\s+→)?(?:\s+(?:([0-9.:]+)(?:\s+(\d+))|(\S+))?)?/; die "Cannot parse test '$_'" unless $1; my %test; @test{qw(mode guest_ip guest_port host_ip host_port host_socket)} = ($1, $2, $3, $4, $5, $6); $test{host_ip} ||= "127.0.0.1"; $test{host_port} ||= $test{guest_port}; $test{host_socket} ||= ""; if ($test{mode} eq "forward" && $test{host_socket} eq "" && $test{host_port} < 1024 && $Config{osname} ne "darwin") { printf "🚧 Not supported on $Config{osname}: # $_\n"; next; } if ($test{mode} eq "ignore" && ($test{guest_ip} eq "0.0.0.0" || $test{guest_ip} eq "127.0.0.1") && "$instanceType" eq "wsl2") { printf "🚧 Not supported for $instanceType machines: # $_\n"; next; } if ($test{guest_ip} eq "192.168.5.15" && "$instanceType" eq "wsl2") { printf "🚧 Not supported for $instanceType machines: # $_\n"; next; } if ($test{host_socket} ne "" && $Config{osname} eq "cygwin") { printf "🚧 Not supported on $Config{osname}: # $_\n"; next; } my $remote = JoinHostPort($test{guest_ip},$test{guest_port}); my $local = $test{host_socket} eq "" ? JoinHostPort($test{host_ip},$test{host_port}) : $test{host_socket}; if ($test{mode} eq "ignore") { $test{log_msg} = "Not forwarding TCP $remote"; } else { $test{log_msg} = "Forwarding TCP from $remote to $local"; } push @test, \%test; } open(my $lima, "| limactl shell --workdir / $instance") or die "Can't run lima shell on $instance: $!"; $lima->autoflush; print $lima <<'EOF'; set -e cd $HOME sudo pkill -x nc || true sudo pkill -x socat || true rm -f nc.* socat.* EOF # Give the hostagent some time to remove any port forwards from a previous (crashed?) test run sleep 5; # Record current log size, so we can skip prior output $ENV{HOME_HOST} ||= "$ENV{HOME}"; $ENV{LIMA_HOME} ||= "$ENV{HOME_HOST}/.lima"; my $ha_stdout_log = "$ENV{LIMA_HOME}/$instance/ha.stdout.log"; my $ha_stderr_log = "$ENV{LIMA_HOME}/$instance/ha.stderr.log"; my $ha_stdout_log_size = -s $ha_stdout_log or die; my $ha_stderr_log_size = -s $ha_stderr_log or die; # Setup a netcat listener on the guest for each test foreach my $id (0..@test-1) { my $test = $test[$id]; my $cmd; if ($listener eq "nc") { $cmd = "nc -l $test->{guest_ip} $test->{guest_port}"; if ($instance =~ /^alpine/) { $cmd = "nc -l -s $test->{guest_ip} -p $test->{guest_port}"; } } elsif ($listener eq "socat") { my $proto = $test->{guest_ip} =~ /:/ ? "TCP6" : "TCP"; $cmd = "socat -u $proto-LISTEN:$test->{guest_port},bind=$test->{guest_ip} STDOUT"; } my $sudo = $test->{guest_port} < 1024 ? "sudo " : ""; print $lima "${sudo}${cmd} >$listener.${id} 2>/dev/null &\n"; } # Make sure the guest- and hostagents had enough time to set up the forwards sleep 5; # Try to reach each listener from the host foreach my $test (@test) { next if $test->{host_port} == $sshLocalPort; my $cmd; if ($writer eq "nc") { if ($Config{osname} eq "darwin") { # macOS nc doesn't support -w for connection timeout, so use -G instead $cmd = $test->{host_socket} eq "" ? "nc -G $connectionTimeout $test->{host_ip} $test->{host_port}" : "nc -G $connectionTimeout -U $test->{host_socket}"; } else { $cmd = $test->{host_socket} eq "" ? "nc -w $connectionTimeout $test->{host_ip} $test->{host_port}" : "nc -w $connectionTimeout -U $test->{host_socket}"; } } elsif ($writer eq "socat") { my $tcp_dest = $test->{host_ip} =~ /:/ ? "TCP6:[$test->{host_ip}]:$test->{host_port}" : "TCP:$test->{host_ip}:$test->{host_port}"; $cmd = $test->{host_socket} eq "" ? "socat -u STDIN $tcp_dest,connect-timeout=$connectionTimeout" : "socat -u STDIN UNIX-CONNECT:$test->{host_socket}"; } print "Running: $cmd\n"; open(my $netcat, "| $cmd") or die "Can't run '$cmd': $!"; print $netcat "$test->{log_msg}\n"; # Don't check for errors on close; macOS nc seems to return non-zero exit code even on success close($netcat); } # Extract forwarding log messages from hostagent JSON event log my $json_parser = JSON::PP->new->utf8->relaxed; open(my $log, "< $ha_stdout_log") or die "Can't read $ha_stdout_log: $!"; seek($log, $ha_stdout_log_size, 0) or die "Can't seek $ha_stdout_log to $ha_stdout_log_size: $!"; my %seen; my %failed_to_listen_tcp; while (<$log>) { chomp; next unless /^\s*\{/; # Skip non-JSON lines my $event = eval { $json_parser->decode($_) }; next unless $event; my $pf = $event->{status}{portForward}; next unless $pf && $pf->{type}; my $type = $pf->{type}; my $protocol = uc($pf->{protocol} || "tcp"); my $guest_addr = $pf->{guestAddr} || ""; my $host_addr = $pf->{hostAddr} || ""; my $error = $pf->{error} || ""; if ($type eq "forwarding") { my $msg = "Forwarding $protocol from $guest_addr to $host_addr"; $seen{$msg}++; } elsif ($type eq "not-forwarding") { my $msg = "Not forwarding $protocol $guest_addr"; $seen{$msg}++; } elsif ($type eq "failed" && $error =~ /listen tcp/) { # Extract the address from the error message if ($error =~ /listen tcp (.*?:\d+):/) { my $addr = $1; $failed_to_listen_tcp{$addr} = "failed to listen tcp: $error"; } } } close $log or die; # Also check stderr log for failed_to_listen_tcp messages (these may not be in JSON events) open(my $stderr_log, "< $ha_stderr_log") or die "Can't read $ha_stderr_log: $!"; seek($stderr_log, $ha_stderr_log_size, 0) or die "Can't seek $ha_stderr_log to $ha_stderr_log_size: $!"; while (<$stderr_log>) { $failed_to_listen_tcp{$2}=$1 if /(failed to listen tcp: listen tcp (.*?:\d+):[^"]+)/; } close $stderr_log or die; my $rc = 0; my %expected; foreach my $id (0..@test-1) { my $test = $test[$id]; my $err = ""; $expected{$test->{log_msg}}++; unless ($seen{$test->{log_msg}}) { $err .= "\n Message missing from ha.stdout.log (JSON events)"; } my $log = qx(limactl shell --workdir / $instance sh -c "cd; cat $listener.$id"); chomp $log; if ($test->{mode} eq "forward" && $test->{log_msg} ne $log) { $err .= "\n Guest received: '$log'"; } if ($test->{mode} eq "ignore" && $log) { $err .= "\n Guest received: '$log' (instead of nothing)"; } printf "%s %s%s\n", ($err ? "❌" : "✅"), $test->{log_msg}, $err; $rc = 1 if $err; } foreach (keys %seen) { next if $expected{$_}; # Should this be an error? Really should only happen if something else failed as well. print "😕 Unexpected log message: $_\n"; } if (%failed_to_listen_tcp) { foreach (keys %failed_to_listen_tcp) { print "⚠️ $failed_to_listen_tcp{$_}\n"; } my @tcp_list = keys %failed_to_listen_tcp; if ($Config{osname} eq "darwin") { my @lsof_args = map { "-iTCP\@$_" } @tcp_list; print `lsof -P @lsof_args`; } elsif ($Config{osname} eq "linux") { my @lss_args = map { "src = $_" } @tcp_list; my $ss_expression = join(" or ", @lss_args); print `sudo ss -lnpt "$ss_expression"`; } elsif ($Config{osname} eq "cygwin") { my @awk_args = map { "-e'/$_/'" } @tcp_list; print `netstat -aon | awk -e'/^ +Proto/' @awk_args`; } } # Cleanup remaining netcat instances (and port forwards) print $lima "sudo pkill -x $listener"; exit $rc; sub JoinHostPort { my($host,$port) = @_; $host = "[$host]" if $host =~ /:/; return "$host:$port"; } # This YAML section includes port forwarding `rules` for the guest- and hostagents, # with interleaved `tests` (in comments) that are executed by this script. The strings # "ipv4" and "ipv6" will be replaced by the actual host ipv4 and ipv6 addresses. __DATA__ portForwards: # We can't test that port 22 will be blocked because the guestagent has # been ignoring it since startup, so the log message is in the part of # the log we skipped. # skip: 127.0.0.1 22 → 127.0.0.1 2222 # ignore: 127.0.0.1 sshLocalPort - guestIP: 127.0.0.2 guestPortRange: [3000, 3009] hostPortRange: [2000, 2009] ignore: true - guestIP: 0.0.0.0 guestIPMustBeZero: false guestPortRange: [3010, 3019] hostPortRange: [2010, 2019] ignore: true - guestIP: 0.0.0.0 guestIPMustBeZero: false guestPortRange: [3000, 3029] hostPortRange: [2000, 2029] # The following rule is completely shadowed by the previous one and has no effect - guestIP: 0.0.0.0 guestIPMustBeZero: false guestPortRange: [3020, 3029] hostPortRange: [2020, 2029] ignore: true # ignore: 127.0.0.2 3000 # forward: 127.0.0.3 3001 → 127.0.0.1 2001 # Blocking 127.0.0.2 cannot block forwarding from 0.0.0.0 # forward: 0.0.0.0 3002 → 127.0.0.1 2002 # Blocking 0.0.0.0 will block forwarding from any interface because guestIPMustBeZero is false # ignore: 0.0.0.0 3010 # ignore: 127.0.0.1 3011 # Forwarding from 0.0.0.0 works for any interface (including IPv6) # The "ignore" rule above has no effect because the previous rule already matched. # forward: 127.0.0.2 3020 → 127.0.0.1 2020 # forward: 127.0.0.1 3021 → 127.0.0.1 2021 # forward: 0.0.0.0 3022 → 127.0.0.1 2022 # forward: :: 3023 → 127.0.0.1 2023 # forward: ::1 3024 → 127.0.0.1 2024 - guestPortRange: [3030, 3039] hostPortRange: [2030, 2039] hostIP: ipv4 # forward: 127.0.0.1 3030 → ipv4 2030 # forward: 0.0.0.0 3031 → ipv4 2031 # forward: :: 3032 → ipv4 2032 # forward: ::1 3033 → ipv4 2033 - guestPortRange: [300, 304] # forward: 127.0.0.1 300 → 127.0.0.1 300 # forward: 0.0.0.0 301 → 127.0.0.1 301 # forward: :: 302 → 127.0.0.1 302 # forward: ::1 303 → 127.0.0.1 303 # ignore: 192.168.5.15 304 → 127.0.0.1 304 - guestPortRange: [305, 309] guestIPMustBeZero: false # forward: 127.0.0.1 325 → 127.0.0.1 325 # forward: 0.0.0.0 326 → 127.0.0.1 326 # forward: :: 327 → 127.0.0.1 327 # forward: ::1 328 → 127.0.0.1 328 # ignore: 192.168.5.15 329 → 127.0.0.1 329 - guestPortRange: [310, 314] hostIP: 0.0.0.0 # forward: 127.0.0.1 310 → 0.0.0.0 310 # forward: 0.0.0.0 311 → 0.0.0.0 311 # forward: :: 312 → 0.0.0.0 312 # forward: ::1 313 → 0.0.0.0 313 # ignore: 192.168.5.15 314 → 0.0.0.0 314 - guestPortRange: [315, 319] guestIPMustBeZero: false hostIP: 0.0.0.0 # forward: 127.0.0.1 315 → 0.0.0.0 315 # forward: 0.0.0.0 316 → 0.0.0.0 316 # forward: :: 317 → 0.0.0.0 317 # forward: ::1 318 → 0.0.0.0 318 # ignore: 192.168.5.15 319 → 0.0.0.0 319 # Things we can't test: # - Accessing a forward from a different interface (e.g. connect to ipv4 to connect to 0.0.0.0) # - failed forward to privileged port - guestIP: "192.168.5.15" guestPortRange: [4000, 4009] hostIP: "ipv4" # forward: 192.168.5.15 4000 → ipv4 4000 - guestIP: "::1" guestPortRange: [4010, 4019] hostIP: "::" # forward: ::1 4010 → :: 4010 - guestIP: "::" guestPortRange: [4020, 4029] hostIP: "ipv4" # forward: 127.0.0.1 4020 → ipv4 4020 # forward: 127.0.0.2 4021 → ipv4 4021 # forward: 192.168.5.15 4022 → ipv4 4022 # forward: 0.0.0.0 4023 → ipv4 4023 # forward: :: 4024 → ipv4 4024 # forward: ::1 4025 → ipv4 4025 - guestIP: "0.0.0.0" guestIPMustBeZero: false guestPortRange: [4030, 4039] hostIP: "ipv4" # forward: 127.0.0.1 4030 → ipv4 4030 # forward: 127.0.0.2 4031 → ipv4 4031 # forward: 192.168.5.15 4032 → ipv4 4032 # forward: 0.0.0.0 4033 → ipv4 4033 # forward: :: 4034 → ipv4 4034 # forward: ::1 4035 → ipv4 4035 - guestIPMustBeZero: true guestPortRange: [4040, 4049] - guestIP: "0.0.0.0" guestIPMustBeZero: false guestPortRange: [4040, 4049] ignore: true # forward: 0.0.0.0 4040 → 127.0.0.1 4040 # forward: :: 4041 → 127.0.0.1 4041 # ignore: 127.0.0.1 4043 → 127.0.0.1 4043 # ignore: 192.168.5.15 4044 → 127.0.0.1 4044 # This rule exist to test `nerdctl run` binding to 0.0.0.0 by default, # and making sure it gets forwarded to the external host IP. # The actual test code is in test-example.sh in the "port-forwarding" block. - guestIPMustBeZero: true guestPort: 8888 hostIP: 0.0.0.0 - guestPort: 5000 hostSocket: port5000.sock # forward: 127.0.0.1 5000 → sockDir/port5000.sock - guestPort: 5001 hostSocket: port5001.sock # ignore: 192.168.5.15 5001 → sockDir/port5001.sock - guestPort: 5002 guestIPMustBeZero: false hostSocket: port5002.sock # forward: 127.0.0.1 5002 → sockDir/port5002.sock - guestPort: 5003 guestIPMustBeZero: false hostSocket: port5003.sock # ignore: 192.168.5.15 5003 → sockDir/port5003.sock ================================================ FILE: hack/test-selinux.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=common.inc.sh source "${scriptdir}/common.inc.sh" if [ "$#" -ne 1 ]; then ERROR "Usage: $0 NAME" exit 1 fi NAME="$1" ########################################################################################## ## When using vz & virtiofs, initially container_file_t selinux label ## was considered which works perfectly for container work loads ## but it might break for other work loads if the process is running with ## different label. Also these are the remote mounts from the host machine, ## so keeping the label as nfs_t fits right. Package container-selinux by ## default adds rules for nfs_t context which allows container workloads to work as well. ## https://github.com/lima-vm/lima/pull/1965 ## ## With integration[https://github.com/lima-vm/lima/pull/2474] with systemd-binfmt, ## the existing "nfs_t" selinux label for Rosetta is causing issues while registering it. ## This behaviour needs to be fixed by setting the label as "bin_t" ## https://github.com/lima-vm/lima/pull/2630 ########################################################################################## INFO "Testing secontext is set for rosetta" expected="context=system_u:object_r:bin_t:s0" #Skip Rosetta checks for x86 GHA mac runners if [[ "$(uname)" == "Darwin" && "$(arch)" == "arm64" ]]; then INFO "Testing secontext is set for rosetta mounts" got=$(limactl shell "$NAME" mount | grep "rosetta" | awk '{print $6}') INFO "secontext rosetta: expected=${expected}, got=${got}" if [[ $got != *$expected* ]]; then ERROR "secontext for rosetta mount is not set or Invalid" exit 1 fi fi INFO "Testing secontext is set for bind mounts" expected="context=system_u:object_r:nfs_t:s0" INFO "Checking in mounts" got=$(limactl shell "$NAME" mount | grep "$HOME" | awk '{print $6}') INFO "secontext ${HOME}: expected=${expected}, got=${got}" if [[ $got != *$expected* ]]; then ERROR "secontext for \"$HOME\" dir is not set or Invalid" exit 1 fi got=$(limactl shell "$NAME" mount | grep "/tmp/lima" | awk '{print $6}') INFO "secontext /tmp/lima: expected=${expected}, got=${got}" if [[ $got != *$expected* ]]; then ERROR 'secontext for "/tmp/lima" dir is not set or Invalid' exit 1 fi INFO "Checking in fstab file" expected='context="system_u:object_r:nfs_t:s0"' got=$(limactl shell "$NAME" cat /etc/fstab | grep "$HOME" | awk '{print $4}') INFO "secontext ${HOME}: expected=${expected}, got=${got}" if [[ $got != *$expected* ]]; then ERROR "secontext for \"$HOME\" dir is not set or Invalid" exit 1 fi got=$(limactl shell "$NAME" cat /etc/fstab | grep "/tmp/lima" | awk '{print $4}') INFO "secontext /tmp/lima: expected=${expected}, got=${got}" if [[ $got != *$expected* ]]; then ERROR 'secontext for "/tmp/lima" dir is not set or Invalid' exit 1 fi ================================================ FILE: hack/test-templates/alpine-iso-9p-writable.yaml ================================================ # Background: https://github.com/lima-vm/lima/pull/2234 # Should be tested on a Linux host images: - location: "https://github.com/lima-vm/alpine-lima/releases/download/v0.2.37/alpine-lima-std-3.19.0-x86_64.iso" arch: "x86_64" digest: "sha512:568852df405e6b9858e678171a9894c058f483df0b0570c22cf33fc75f349ba6cc5bb3d50188180d8c31faaf53400fe884ca3e5f949961b03b2bf53e65de88d7" - location: "https://github.com/lima-vm/alpine-lima/releases/download/v0.2.37/alpine-lima-std-3.19.0-aarch64.iso" arch: "aarch64" digest: "sha512:3a4bd5ad0201f503e9bb9f3b812aa0df292e2e099148c0323d23244046ad199a2946ef9e0619fec28726bfdcc528233f43c3b4b036c9e06e92ac730d579f0ca3" mountType: "9p" mounts: - location: "~" writable: true - location: "/tmp/lima test dir with spaces" writable: true - location: "/tmp/lima" writable: true containerd: system: false user: false ================================================ FILE: hack/test-templates/net-user-v2.yaml ================================================ # A template to run lima instance with experimental user-v2 network enabled # This template requires Lima v0.16.0 or later. images: - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" arch: "x86_64" - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" arch: "aarch64" hostResolver: hosts: host.docker.internal: host.lima.internal mounts: - location: "~" - location: "/tmp/lima" writable: true networks: - lima: user-v2 ================================================ FILE: hack/test-templates/test-misc.yaml ================================================ # The test template for testing misc configurations: # - disk # - snapshots # - (More to come) # base: template:ubuntu-22.04 # 9p is not compatible with `limactl snapshot` mountTypesUnsupported: ["9p"] mounts: - location: "~" writable: true - location: "/tmp/lima test dir with spaces" writable: true param: ANSIBLE: ansible BOOT: boot DEPENDENCY: dependency PROBE: probe SYSTEM: system USER: user YQ: yq provision: - mode: ansible playbook: ./hack/ansible-test.yaml - mode: boot script: "touch /tmp/param-$PARAM_BOOT" - mode: dependency script: "touch /tmp/param-$PARAM_DEPENDENCY" - mode: system script: | touch /tmp/param-$PARAM_SYSTEM # port forwarding test setup apt-get update apt-get install -y nginx python3 systemctl enable nginx systemctl start nginx cat > /etc/systemd/system/test-server-9080.service << 'EOF' [Unit] Description=Test Server on Port 9080 After=network.target [Service] Type=simple User=root ExecStart=/usr/bin/python3 -m http.server 9080 --bind 127.0.0.1 Restart=always [Install] WantedBy=multi-user.target EOF cat > /etc/systemd/system/test-server-9070.service << 'EOF' [Unit] Description=Test Server on Port 9070 After=network.target [Service] Type=simple User=root ExecStart=/usr/bin/python3 -m http.server 9070 --bind 127.0.0.1 Restart=always [Install] WantedBy=multi-user.target EOF mkdir -p /var/www/html-9080 mkdir -p /var/www/html-9070 echo '

Dynamic port 9080

' > /var/www/html-9080/index.html echo '

Dynamic port 9070

' > /var/www/html-9070/index.html systemctl daemon-reload systemctl enable test-server-9080 systemctl enable test-server-9070 systemctl start test-server-9080 systemctl start test-server-9070 - mode: user script: "touch /tmp/param-$PARAM_USER" - mode: data path: /etc/sysctl.d/99-inotify.conf content: | fs.inotify.max_user_watches = 524288 fs.inotify.max_user_instances = 512 - mode: yq path: "/tmp/param-{{.Param.YQ}}.json" expression: .YQ = "{{.Param.YQ}}" probes: - mode: readiness script: | #!/bin/sh touch /tmp/param-$PARAM_PROBE # in order to use this example, you must first create the disks. run: # $ limactl disk create data --size 10G # $ limactl disk create swap --size 2G additionalDisks: - "data" - name: "swap" format: true fsType: swap user: name: john comment: John Doe home: "/home/{{.User}}-{{.User}}" uid: 4711 # Ubuntu has identical /bin/bash and /usr/bin/bash shell: /usr/bin/bash portForwards: - guestPort: 80 hostPort: 9090 static: true - guestPort: 9080 hostPort: 29080 static: false - guestPort: 9070 hostPort: 29070 static: false ================================================ FILE: hack/test-templates.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # will prevent msys2 converting Linux path arguments into Windows paths before passing to limactl export MSYS2_ARG_CONV_EXCL='*' scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=common.inc.sh source "${scriptdir}/common.inc.sh" if [ "$#" -ne 1 ]; then ERROR "Usage: $0 FILE.yaml" exit 1 fi # Resolve any ../ fragments in the filename because they are invalid in relative template locators FILE="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" NAME="$(basename -s .yaml "$FILE")" OS_HOST="$(uname -o)" # On Windows $HOME of the bash runner, %USERPROFILE% of the host machine and mounting point in the guest machine # are all different folders. This will handle path differences, when values are explicitly set. HOME_HOST=${HOME_HOST:-$HOME} HOME_GUEST=${HOME_GUEST:-$HOME} FILE_HOST=$FILE if [ "${OS_HOST}" = "Msys" ]; then FILE_HOST="$(cygpath -w "$FILE")" fi INFO "Validating \"$FILE_HOST\"" limactl validate "$FILE_HOST" LIMACTL_CREATE=(limactl --tty=false create) CONTAINER_ENGINE="nerdctl" declare -A CHECKS=( ["proxy-settings"]="1" ["systemd"]="1" ["mount-home"]="1" ["container-engine"]="1" ["restart"]="1" # snapshot tests are too flaky (especially with archlinux) ["snapshot-online"]="" ["snapshot-offline"]="" ["clone"]="" ["port-forwards"]="1" ["vmnet"]="" ["disk"]="" ["user-v2"]="" ["mount-path-with-spaces"]="" ["provision-data"]="" ["provision-yq"]="" ["param-env-variables"]="" ["set-user"]="" ["preserve-env"]="1" ["static-port-forwards"]="" ["ssh-over-vsock"]="" ) case "$NAME" in "default") # CI failure: # "[hostagent] failed to confirm whether /c/Users/runneradmin [remote] is successfully mounted" [ "${OS_HOST}" = "Msys" ] && CHECKS["mount-home"]= [ "${OS_HOST}" = "Darwin" ] && CHECKS["ssh-over-vsock"]="1" ;; "alpine"*) WARNING "Alpine does not support systemd" CHECKS["systemd"]= CHECKS["container-engine"]= [ "$NAME" = "alpine-iso-9p-writable" ] && CHECKS["mount-path-with-spaces"]="1" ;; "k3s") ERROR "File \"$FILE\" is not testable with this script" exit 1 ;; "test-misc") CHECKS["disk"]=1 CHECKS["snapshot-online"]="1" CHECKS["snapshot-offline"]="1" CHECKS["clone"]="1" CHECKS["mount-path-with-spaces"]="1" CHECKS["provision-data"]="1" CHECKS["provision-yq"]="1" CHECKS["param-env-variables"]="1" CHECKS["set-user"]="1" CHECKS["static-port-forwards"]="1" ;; "docker") CONTAINER_ENGINE="docker" ;; "wsl2") # TODO https://github.com/lima-vm/lima/issues/3268 CHECKS["proxy-settings"]= ;; esac if limactl ls -q "$NAME" 2>/dev/null; then ERROR "Instance $NAME already exists" exit 1 fi case "$(limactl tmpl yq "$FILE_HOST" '.networks[].lima')" in "shared") CHECKS["vmnet"]=1 ;; "user-v2") CHECKS["port-forwards"]="" CHECKS["user-v2"]=1 ;; esac if [[ -n ${CHECKS["port-forwards"]} ]]; then tmpconfig="$HOME_HOST/lima-config-tmp" mkdir -p "${tmpconfig}" defer "rm -rf \"$tmpconfig\"" tmpfile="${tmpconfig}/${NAME}.yaml" cp "$FILE" "${tmpfile}" FILE="${tmpfile}" FILE_HOST=$FILE if [ "${OS_HOST}" = "Msys" ]; then FILE_HOST="$(cygpath -w "$FILE")" fi INFO "Setup port forwarding rules for testing in \"${FILE}\"" "${scriptdir}/test-port-forwarding.pl" "${FILE}" INFO "Validating \"$FILE_HOST\"" limactl validate "$FILE_HOST" fi INFO "Make sure template embedding copies \"$FILE_HOST\" exactly" diff -u <(echo -n "base: $FILE_HOST" | limactl tmpl copy --embed - -) "$FILE_HOST" function diagnose() { NAME="$1" set -x +e tail "$HOME_HOST/.lima/${NAME}"/*.log limactl shell "$NAME" systemctl --no-pager status limactl shell "$NAME" systemctl --no-pager mkdir -p failure-logs cp -pf "$HOME_HOST/.lima/${NAME}"/*.log failure-logs/ limactl shell "$NAME" sudo cat /var/log/cloud-init-output.log | tee failure-logs/cloud-init-output.log limactl shell "$NAME" sh -c "command -v journalctl >/dev/null && sudo journalctl --no-pager" >failure-logs/journal.log set +x -e } export ftp_proxy=http://localhost:2121 INFO "Creating \"$NAME\" from \"$FILE_HOST\"" defer "limactl delete -f \"$NAME\"" if [[ -n ${CHECKS["disk"]} ]]; then if [[ -z "$(limactl disk ls data --json 2>/dev/null)" ]]; then defer "limactl disk delete data" limactl disk create data --size 10G fi if ! limactl disk ls | grep -q "^swap\s"; then defer "limactl disk delete swap" limactl disk create swap --size 2G fi fi set -x # shellcheck disable=SC2086 "${LIMACTL_CREATE[@]}" ${LIMACTL_CREATE_ARGS:-} "$FILE_HOST" set +x if [[ -n ${CHECKS["mount-path-with-spaces"]} ]]; then mkdir -p "/tmp/lima test dir with spaces" echo "test file content" >"/tmp/lima test dir with spaces/test file" fi INFO "Starting \"$NAME\"" set -x if ! limactl start "$NAME"; then ERROR "Failed to start \"$NAME\"" diagnose "$NAME" exit 1 fi limactl shell "$NAME" uname -a limactl shell "$NAME" cat /etc/os-release set +x INFO "Testing that host home is not wiped out" [ -e "$HOME_HOST/.lima" ] if [[ -n ${CHECKS["mount-path-with-spaces"]} ]]; then INFO 'Testing that "/tmp/lima test dir with spaces" is not wiped out' [ "$(cat "/tmp/lima test dir with spaces/test file")" = "test file content" ] [ "$(limactl shell "$NAME" cat "/tmp/lima test dir with spaces/test file")" = "test file content" ] fi if [[ -n ${CHECKS["provision-data"]} ]]; then INFO 'Testing that /etc/sysctl.d/99-inotify.conf was created successfully on provision' limactl shell "$NAME" grep -q fs.inotify.max_user_watches /etc/sysctl.d/99-inotify.conf fi if [[ -n ${CHECKS["provision-yq"]} ]]; then INFO 'Testing that /tmp/param-yq.json was created successfully on provision' limactl shell "$NAME" grep -q '"YQ": "yq"' /tmp/param-yq.json fi if [[ -n ${CHECKS["param-env-variables"]} ]]; then INFO 'Testing that PARAM env variables are exported to all types of provisioning scripts and probes' limactl shell "$NAME" test -e /tmp/param-ansible limactl shell "$NAME" test -e /tmp/param-boot limactl shell "$NAME" test -e /tmp/param-dependency limactl shell "$NAME" test -e /tmp/param-probe limactl shell "$NAME" test -e /tmp/param-system limactl shell "$NAME" test -e /tmp/param-user fi if [[ -n ${CHECKS["set-user"]} ]]; then INFO 'Testing that user settings can be provided by lima.yaml' limactl shell "$NAME" grep "^john:x:4711:4711:John Doe:/home/john-john:/usr/bin/bash" /etc/passwd fi if [[ -n ${CHECKS["proxy-settings"]} ]]; then INFO "Testing proxy settings are imported" got=$(limactl shell "$NAME" env | grep FTP_PROXY) # Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced # by the gateway address, and the value is set immediately without a restart gatewayIp=$(limactl shell "$NAME" ip route show 0.0.0.0/0 dev eth0 | cut -d\ -f3) expected="FTP_PROXY=http://${gatewayIp}:2121" INFO "FTP_PROXY: expected=${expected} got=${got}" if [ "$got" != "$expected" ]; then ERROR "proxy environment variable not set to correct value" exit 1 fi fi INFO "Testing limactl copy command" tmpdir="$(mktemp -d "${TMPDIR:-/tmp}"/lima-test-templates.XXXXXX)" defer "rm -rf \"$tmpdir\"" tmpfile="$tmpdir/lima-hostname" rm -f "$tmpfile" tmpfile_host=$tmpfile if [ "${OS_HOST}" = "Msys" ]; then tmpfile_host="$(cygpath -w "$tmpfile")" fi limactl cp "$NAME":/etc/hostname "$tmpfile_host" expected="$(limactl shell "$NAME" cat /etc/hostname)" got="$(cat "$tmpfile")" INFO "/etc/hostname: expected=${expected}, got=${got}" if [ "$got" != "$expected" ]; then ERROR "copy command did not fetch the file" exit 1 fi INFO "Testing limactl copy command with scp backend" tmpfile_scp="$tmpdir/lima-hostname-scp" rm -f "$tmpfile_scp" tmpfile_scp_host=$tmpfile_scp if [ "${OS_HOST}" = "Msys" ]; then tmpfile_scp_host="$(cygpath -w "$tmpfile_scp")" fi limactl cp --backend=scp "$NAME":/etc/hostname "$tmpfile_scp_host" expected="$(limactl shell "$NAME" cat /etc/hostname)" got="$(cat "$tmpfile_scp")" INFO "/etc/hostname (scp): expected=${expected}, got=${got}" if [ "$got" != "$expected" ]; then ERROR "copy command with scp backend did not fetch the file" exit 1 fi if command -v rsync >/dev/null && limactl shell "$NAME" command -v rsync >/dev/null 2>&1; then INFO "Testing limactl copy command with rsync backend" tmpfile_rsync="$tmpdir/lima-hostname-rsync" rm -f "$tmpfile_rsync" tmpfile_rsync_host=$tmpfile_rsync if [ "${OS_HOST}" = "Msys" ]; then tmpfile_rsync_host="$(cygpath -w "$tmpfile_rsync")" fi limactl cp --backend=rsync "$NAME":/etc/hostname "$tmpfile_rsync_host" expected="$(limactl shell "$NAME" cat /etc/hostname)" got="$(cat "$tmpfile_rsync")" INFO "/etc/hostname (rsync): expected=${expected}, got=${got}" if [ "$got" != "$expected" ]; then ERROR "copy command with rsync backend did not fetch the file" exit 1 fi INFO "Testing limactl copy command with rsync backend (verbose, recursive)" testdir="$tmpdir/test-rsync-dir" mkdir -p "$testdir" echo "test content" >"$testdir/testfile.txt" limactl cp --backend=rsync -r -v "$testdir" "$NAME":/tmp/ if ! limactl shell "$NAME" test -f /tmp/test-rsync-dir/testfile.txt; then ERROR "rsync recursive copy failed" exit 1 fi rsync_content="$(limactl shell "$NAME" cat /tmp/test-rsync-dir/testfile.txt)" if [ "$rsync_content" != "test content" ]; then ERROR "rsync file content mismatch" exit 1 fi else INFO "Skipping rsync backend test (rsync not available on host or guest)" fi INFO "Testing limactl command with escaped characters" limactl shell "$NAME" bash -c "$(echo -e '\n\techo foo\n\techo bar')" INFO "Testing limactl command with quotes" limactl shell "$NAME" bash -c "echo 'foo \"bar\"'" if [[ -n ${CHECKS["systemd"]} ]]; then set -x if ! limactl shell "$NAME" systemctl is-system-running --wait; then ERROR '"systemctl is-system-running" failed' diagnose "$NAME" exit 1 fi set +x fi if [[ -n ${CHECKS["mount-home"]} ]]; then "${scriptdir}"/test-mount-home.sh "$NAME" fi if [[ -n ${CHECKS["ssh-over-vsock"]} ]]; then if [[ "$(limactl ls "${NAME}" --yq .vmType)" == "vz" ]]; then INFO "Testing SSH over vsock" set -x log_file="$HOME_HOST/.lima/${NAME}/ha.stdout.log" # Helper function to check vsock events in the log file # $1: event_type to check for check_vsock_event() { local event_type="$1" if jq -e --arg type "$event_type" 'select(.status.vsock.type == $type)' "$log_file" >/dev/null 2>&1; then return 0 fi return 1 } INFO "Testing .ssh.overVsock=true configuration" limactl stop "${NAME}" # Detection of the SSH server on VSOCK may fail; however, a failing log indicates that controlling detection via the environment variable works as expected. limactl start --set '.ssh.overVsock=true' "${NAME}" if ! check_vsock_event "started" && ! check_vsock_event "failed"; then set +x diagnose "${NAME}" ERROR ".ssh.overVsock=true did not enable vsock forwarder" exit 1 fi INFO 'Testing .ssh.overVsock=null configuration' limactl stop "${NAME}" # Detection of the SSH server on VSOCK may fail; however, a failing log indicates that controlling detection via the environment variable works as expected. limactl start --set '.ssh.overVsock=null' "${NAME}" if ! check_vsock_event "started" && ! check_vsock_event "failed"; then set +x diagnose "${NAME}" ERROR ".ssh.overVsock=null did not enable vsock forwarder" exit 1 fi INFO "Testing .ssh.overVsock=false configuration" limactl stop "${NAME}" limactl start --set '.ssh.overVsock=false' "${NAME}" if ! check_vsock_event "skipped"; then set +x diagnose "${NAME}" ERROR ".ssh.overVsock=false did not disable vsock forwarder" exit 1 fi set +x fi fi # Use GHCR and ECR to avoid hitting Docker Hub rate limit nginx_image="ghcr.io/stargz-containers/nginx:1.19-alpine-org" alpine_image="ghcr.io/containerd/alpine:3.14.0" coredns_image="public.ecr.aws/eks-distro/coredns/coredns:v1.12.2-eks-1-31-latest" if [[ -n ${CHECKS["container-engine"]} ]]; then sudo="" # Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future. if [[ "$(limactl ls "${NAME}" --yq .vmType)" == "wsl2" ]]; then sudo="sudo" fi INFO "Run a nginx container with port forwarding 127.0.0.1:8080" set -x if ! limactl shell "$NAME" $sudo $CONTAINER_ENGINE info; then limactl shell "$NAME" cat /var/log/cloud-init-output.log ERROR "\"${CONTAINER_ENGINE} info\" failed" exit 1 fi limactl shell "$NAME" $sudo $CONTAINER_ENGINE pull --quiet ${nginx_image} limactl shell "$NAME" $sudo $CONTAINER_ENGINE run -d --name nginx -p 127.0.0.1:8080:80 ${nginx_image} timeout 3m bash -euxc "until curl -f --retry 30 --retry-connrefused http://127.0.0.1:8080; do sleep 3; done" limactl shell "$NAME" $sudo $CONTAINER_ENGINE rm -f nginx if [ "${OS_HOST}" != "Msys" ]; then # TODO: support UDP on Windows INFO "Run a coredns container with port forwarding 127.0.0.1:10053/udp" limactl shell "$NAME" $sudo $CONTAINER_ENGINE pull --quiet ${coredns_image} limactl shell "$NAME" $sudo $CONTAINER_ENGINE run -d --name coredns -p 127.0.0.1:10053:53/udp ${coredns_image} dig @127.0.0.1 -p 10053 lima-vm.io limactl shell "$NAME" $sudo $CONTAINER_ENGINE rm -f coredns fi set +x if [[ -n ${CHECKS["mount-home"]} ]]; then hometmp="$HOME_HOST/lima-container-engine-test-tmp" hometmpguest="$HOME_GUEST/lima-container-engine-test-tmp" # test for https://github.com/lima-vm/lima/issues/187 INFO "Testing home bind mount (\"$hometmp\")" rm -rf "$hometmp" mkdir -p "$hometmp" defer "rm -rf \"$hometmp\"" set -x limactl shell "$NAME" $sudo $CONTAINER_ENGINE pull --quiet ${alpine_image} echo "random-content-${RANDOM}" >"$hometmp/random" expected="$(cat "$hometmp/random")" got="$(limactl shell "$NAME" $sudo $CONTAINER_ENGINE run --rm -v "$hometmpguest/random":/mnt/foo ${alpine_image} cat /mnt/foo)" INFO "$hometmp/random: expected=${expected}, got=${got}" if [ "$got" != "$expected" ]; then ERROR "Home directory is not shared?" exit 1 fi set +x fi fi if [[ -n ${CHECKS["port-forwards"]} ]]; then PORT_FORWARDING_CONNECTION_TIMEOUT=1 INFO "Testing port forwarding rules using netcat and socat with connection timeout ${PORT_FORWARDING_CONNECTION_TIMEOUT}s" set -x if [[ ${NAME} == "alpine"* ]]; then limactl shell "${NAME}" sudo apk add socat fi if [[ ${NAME} == "archlinux" ]]; then limactl shell "${NAME}" sudo pacman -Syu --noconfirm openbsd-netcat socat fi if [[ ${NAME} == "debian" || ${NAME} == "default" || ${NAME} == "docker" || ${NAME} == "test-misc" ]]; then limactl shell "${NAME}" sudo apt-get install -y netcat-openbsd socat fi if [[ ${NAME} == "fedora" || ${NAME} == "wsl2" ]]; then limactl shell "${NAME}" sudo dnf install -y nc socat fi if [[ ${NAME} == "opensuse" ]]; then limactl shell "${NAME}" sudo zypper in -y netcat-openbsd socat fi if limactl shell "${NAME}" command -v dnf; then limactl shell "${NAME}" sudo dnf install -y nc socat fi if "${scriptdir}/test-port-forwarding.pl" "${NAME}" socat $PORT_FORWARDING_CONNECTION_TIMEOUT; then INFO "Port forwarding rules work" else ERROR "Port forwarding rules do not work with socat" diagnose "$NAME" exit 1 fi if [[ -n ${CHECKS["container-engine"]} || ${NAME} == "alpine"* ]]; then INFO "Testing that \"${CONTAINER_ENGINE} run\" binds to 0.0.0.0 and is forwarded to the host (non-default behavior, configured via test-port-forwarding.pl)" if [ "$(uname)" = "Darwin" ]; then # macOS runners seem to use `localhost` as the hostname, so the perl lookup just returns `127.0.0.1` hostip=$(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first') else hostip=$(perl -MSocket -MSys::Hostname -E 'say inet_ntoa(scalar gethostbyname(hostname()))') fi if [ -n "${hostip}" ]; then sudo="" if [[ ${NAME} == "alpine"* ]]; then arch=$(limactl info | jq -r .defaultTemplate.arch) nerdctl=$(limactl info | jq -r ".defaultTemplate.containerd.archives[] | select(.arch==\"$arch\").location") curl -Lso nerdctl-full.tgz "${nerdctl}" limactl shell "$NAME" sudo apk add containerd limactl shell "$NAME" sudo rc-service containerd start limactl shell "$NAME" sudo tar xzf "${PWD}/nerdctl-full.tgz" -C /usr/local rm nerdctl-full.tgz sudo="sudo" fi # Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future. if [[ "$(limactl ls "${NAME}" --yq .vmType)" == "wsl2" ]]; then sudo="sudo" fi limactl shell "$NAME" $sudo $CONTAINER_ENGINE info limactl shell "$NAME" $sudo $CONTAINER_ENGINE pull --quiet ${nginx_image} limactl shell "$NAME" $sudo $CONTAINER_ENGINE run -d --name nginx -p 8888:80 ${nginx_image} timeout 3m bash -euxc "until curl -f --retry 30 --retry-connrefused http://${hostip}:8888; do sleep 3; done" limactl shell "$NAME" $sudo $CONTAINER_ENGINE rm -f nginx if [ "$(uname)" = "Darwin" ]; then # Only macOS can bind to port 80 without root limactl shell "$NAME" $sudo $CONTAINER_ENGINE run -d --name nginx -p 127.0.0.1:80:80 ${nginx_image} timeout 3m bash -euxc "until curl -f --retry 30 --retry-connrefused http://localhost:80; do sleep 3; done" limactl shell "$NAME" $sudo $CONTAINER_ENGINE rm -f nginx fi fi if [[ ${NAME} != "alpine"* ]] && command -v w3m >/dev/null; then INFO "Testing https://github.com/lima-vm/lima/issues/3685 ([gRPC portfwd] client connection is not closed immediately when server closed the connection)" # Skip the test on Alpine, as systemd-run is missing # Skip the test on WSL2, as port forwarding is half broken https://github.com/lima-vm/lima/pull/3686#issuecomment-3034842616 limactl shell "$NAME" systemd-run --user python3 -m http.server 3685 # curl is not enough to reproduce https://github.com/lima-vm/lima/issues/3685 # `w3m -dump` exits with status code 0 even on "Can't load" error. timeout 30s bash -euxc "until w3m -dump http://localhost:3685 | grep -v \"w3m: Can't load\"; do sleep 3; done" fi fi set +x fi if [[ -n ${CHECKS["vmnet"]} ]]; then INFO "Testing vmnet functionality" guestip="$(limactl shell "$NAME" ip -4 -j addr show dev lima0 | jq -r '.[0].addr_info[0].local')" INFO "Pinging the guest IP ${guestip}" set -x ping -c 3 "$guestip" set +x INFO "Benchmarking with iperf3" set -x limactl shell "$NAME" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y iperf3 limactl shell "$NAME" iperf3 -s -1 -D ${IPERF3} -c "$guestip" set +x # NOTE: we only test the shared interface here, as the bridged interface cannot be used on GHA (and systemd-networkd-wait-online.service will fail) fi if [[ -n ${CHECKS["disk"]} ]]; then INFO "Testing disk is attached" set -x if ! limactl shell "$NAME" lsblk --output NAME,MOUNTPOINT | grep -q "/mnt/lima-data"; then ERROR "Disk is not mounted" exit 1 fi if ! limactl shell "$NAME" lsblk --output NAME,MOUNTPOINT | grep -q "\[SWAP\]"; then ERROR "Disk is not mounted" exit 1 fi set +x fi if [[ -n ${CHECKS["restart"]} ]]; then INFO "Create file in the guest home directory and verify that it still exists after a restart" # shellcheck disable=SC2016 limactl shell "$NAME" sh -c 'touch $HOME/sweet-home' if [[ -n ${CHECKS["disk"]} ]]; then INFO "Create file in disk and verify that it still exists when it is reattached" limactl shell "$NAME" sudo sh -c 'touch /mnt/lima-data/sweet-disk' fi INFO "Stopping \"$NAME\"" limactl stop "$NAME" sleep 3 if [[ -n ${CHECKS["disk"]} ]]; then INFO "Resize disk and verify that partition and fs size are increased" limactl disk resize data --size 11G fi export ftp_proxy=my.proxy:8021 INFO "Restarting \"$NAME\"" if ! limactl start "$NAME"; then ERROR "Failed to start \"$NAME\"" diagnose "$NAME" exit 1 fi INFO "Make sure proxy setting is updated" got=$(limactl shell "$NAME" env | grep FTP_PROXY) expected="FTP_PROXY=my.proxy:8021" INFO "FTP_PROXY: expected=${expected} got=${got}" if [ "$got" != "$expected" ]; then ERROR "proxy environment variable not set to correct value" exit 1 fi # shellcheck disable=SC2016 if ! limactl shell "$NAME" sh -c 'test -f $HOME/sweet-home'; then ERROR "Guest home directory does not persist across restarts" exit 1 fi if [[ -n ${CHECKS["disk"]} ]]; then if ! limactl shell "$NAME" sh -c 'test -f /mnt/lima-data/sweet-disk'; then ERROR "Disk does not persist across restarts" exit 1 fi if ! limactl shell "$NAME" sh -c 'df -h /mnt/lima-data/ --output=size | grep -q 11G'; then ERROR "Disk FS does not resized after restart" exit 1 fi fi fi if [[ -n ${CHECKS["user-v2"]} ]]; then INFO "Testing user-v2 network" secondvm="$NAME-1" "${LIMACTL_CREATE[@]}" --set ".additionalDisks=null" "$FILE_HOST" --name "$secondvm" if ! limactl start "$secondvm"; then ERROR "Failed to start \"$secondvm\"" diagnose "$secondvm" exit 1 fi secondvmDNS="lima-$secondvm.internal" INFO "DNS of $secondvm is $secondvmDNS" set -x if ! limactl shell "$NAME" ping -c 1 "$secondvmDNS"; then ERROR "Failed to do vm->vm communication via user-v2" INFO "Stopping \"$secondvm\"" limactl stop "$secondvm" INFO "Deleting \"$secondvm\"" limactl delete "$secondvm" exit 1 fi INFO "Stopping \"$secondvm\"" limactl stop "$secondvm" INFO "Deleting \"$secondvm\"" limactl delete "$secondvm" set +x fi if [[ -n ${CHECKS["snapshot-online"]} ]]; then INFO "Testing online snapshots" limactl shell "$NAME" sh -c 'echo foo > /tmp/test' limactl snapshot create "$NAME" --tag snap1 got=$(limactl snapshot list "$NAME" --quiet) expected="snap1" INFO "snapshot list: expected=${expected} got=${got}" if [ "$got" != "$expected" ]; then ERROR "snapshot list did not return expected value" exit 1 fi limactl shell "$NAME" sh -c 'echo bar > /tmp/test' limactl snapshot apply "$NAME" --tag snap1 got=$(limactl shell "$NAME" cat /tmp/test) expected="foo" INFO "snapshot apply: expected=${expected} got=${got}" if [ "$got" != "$expected" ]; then ERROR "snapshot apply did not restore snapshot" exit 1 fi limactl snapshot delete "$NAME" --tag snap1 limactl shell "$NAME" rm /tmp/test fi if [[ -n ${CHECKS["snapshot-offline"]} ]]; then INFO "Testing offline snapshots" limactl stop "$NAME" sleep 3 limactl snapshot create "$NAME" --tag snap2 got=$(limactl snapshot list "$NAME" --quiet) expected="snap2" INFO "snapshot list: expected=${expected} got=${got}" if [ "$got" != "$expected" ]; then ERROR "snapshot list did not return expected value" exit 1 fi limactl snapshot apply "$NAME" --tag snap2 limactl snapshot delete "$NAME" --tag snap2 limactl start "$NAME" fi if [[ -n ${CHECKS["clone"]} ]]; then INFO "Testing cloning" limactl stop "$NAME" sleep 3 # [hostagent] could not attach disk \"data\", in use by instance \"test-misc-clone\" limactl clone --set '.additionalDisks = null' "$NAME" "${NAME}-clone" limactl start "${NAME}-clone" [ "$(limactl shell "${NAME}-clone" hostname)" = "lima-${NAME}-clone" ] limactl start "$NAME" fi if [[ $NAME == "fedora" && "$(limactl ls "${NAME}" --yq .vmType)" == "vz" ]]; then "${scriptdir}"/test-selinux.sh "$NAME" fi INFO "Stopping \"$NAME\"" limactl stop "$NAME" sleep 3 INFO "Deleting \"$NAME\"" limactl delete "$NAME" if [[ -n ${CHECKS["mount-path-with-spaces"]} ]]; then rm -rf "/tmp/lima test dir with spaces" fi if [[ -n ${CHECKS["static-port-forwards"]} ]]; then INFO "Testing static port forwarding functionality" "${scriptdir}/test-plain-static-port-forward.sh" "$NAME" "${scriptdir}/test-nonplain-static-port-forward.sh" "$NAME" INFO "All static port forwarding tests passed!" fi ================================================ FILE: hack/test-upgrade.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail scriptdir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=common.inc.sh source "${scriptdir}/common.inc.sh" cd "${scriptdir}/.." if [ "$#" -ne 2 ]; then ERROR "Usage: $0 OLDVER NEWVER" exit 1 fi OLDVER="$1" NEWVER="$2" PREFIX="/usr/local" function install_lima() { ver="$1" git checkout "${ver}" make clean make if [ -w "${PREFIX}/bin" ] && [ -w "${PREFIX}/share" ] && [ -w "${PREFIX}/libexec" ]; then make install else sudo make install fi } function install_lima_binary() { ver="$1" tar="tar" if [ ! -w "${PREFIX}/bin" ] || [ ! -w "${PREFIX}/share" ] || [ ! -w "${PREFIX}/libexec" ]; then tar="sudo ${tar}" fi curl -fsSL "https://github.com/lima-vm/lima/releases/download/${ver}/lima-${ver:1}-$(uname -s)-$(uname -m).tar.gz" | ${tar} Cxzvm "${PREFIX}" } function uninstall_lima() { files="${PREFIX}/bin/lima ${PREFIX}/bin/limactl ${PREFIX}/share/lima ${PREFIX}/share/doc/lima ${PREFIX}/libexec/lima" if [ -w "${PREFIX}/bin" ] && [ -w "${PREFIX}/share" ] && [ -w "${PREFIX}/libexec" ]; then # shellcheck disable=SC2086 rm -rf $files else # shellcheck disable=SC2086 sudo rm -rf $files fi } function show_lima_log() { tail -n 100 ~/.lima/"${LIMA_INSTANCE}"/*.log || true mkdir -p failure-logs cp -pf ~/.lima/"${LIMA_INSTANCE}"/*.log failure-logs/ || true limactl shell "${LIMA_INSTANCE}" sudo cat /var/log/cloud-init-output.log | tee failure-logs/cloud-init-output.log || true } INFO "Uninstalling lima" uninstall_lima INFO "Installing the old Lima ${OLDVER}" install_lima_binary "${OLDVER}" || install_lima "${OLDVER}" export LIMA_INSTANCE="test-upgrade" INFO "Creating an instance \"${LIMA_INSTANCE}\" with the old Lima" defer "show_lima_log;limactl delete -f \"${LIMA_INSTANCE}\"" limactl start --tty=false --name="${LIMA_INSTANCE}" template://ubuntu-lts || ( show_lima_log exit 1 ) lima nerdctl info image_name="lima-test-upgrade-containerd-${RANDOM}" image_context="${HOME}/${image_name}" INFO "Building containerd image \"${image_name}\" from \"${image_context}\"" defer "rm -rf \"${image_context}\"" mkdir -p "${image_context}" cat <"${image_context}"/Dockerfile # Use GHCR to avoid hitting Docker Hub rate limit FROM ghcr.io/containerd/alpine:3.14.0 CMD ["echo", "Built with Lima ${OLDVER}"] EOF lima nerdctl build -t "${image_name}" "${image_context}" lima nerdctl run --rm "${image_name}" INFO "Stopping the instance" limactl stop "${LIMA_INSTANCE}" INFO "==============================================================================" INFO "Installing the new Lima ${NEWVER}" install_lima "${NEWVER}" INFO "Editing the instance to specify vm-type as qemu explicitly" limactl edit --vm-type=qemu "${LIMA_INSTANCE}" INFO "Restarting the instance" limactl start --tty=false --vm-type=qemu "${LIMA_INSTANCE}" || show_lima_log lima nerdctl info INFO "Confirming that the host filesystem is still mounted" "${scriptdir}"/test-mount-home.sh "${LIMA_INSTANCE}" INFO "Confirming that the image \"${image_name}\" still exists" lima nerdctl run --rm "${image_name}" ================================================ FILE: hack/toolexec-for-codesign.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # This script is used to wrap the compiler and linker commands in the build # process. It captures the output of the command and logs it to a file. # The script's primary purpose is codesigning the output of the linker command # with the entitlements file if it exists. # If the OS is macOS, the result of the command is 0, the entitlements file # exists, and codesign is available, sign the output of the linker command with # the entitlements file. # # Usage: # go build -toolexec hack/toolexec-to-codesign.sh repository_root="$(dirname "$(dirname "$0")")" logfile="${repository_root}/.toolexec-to-codesign.log" echo $$: cmd: "$@" >>"${logfile}" output="$("$@")" result=$? echo $$: output: "${output}" >>"${logfile}" entitlements="${repository_root}/vz.entitlements" # If the OS is macOS, the result of the command is 0, the entitlements file # exists, and codesign is available, sign the output of the linker command. if OS=$(uname -s) && [ "${OS}" = "Darwin" ] && [ "${result}" -eq 0 ] && [ -f "${entitlements}" ] && command -v codesign >/dev/null 2>&1; then # Check if the command is a linker command. case "$1" in *link) shift # Find a parameter that is a output file. while [ $# -gt 1 ]; do case "$1" in -o) # If the output file is a executable, sign it with the entitlements file. if [ -x "$2" ]; then codesign_output="$(codesign -v --entitlements "${entitlements}" -s - "$2" 2>&1)" echo "$$: ${codesign_output}" >>"${logfile}" fi break ;; *) shift ;; esac done ;; *) ;; esac fi # Print the output of the command and exit with the result of the command. echo "${output}" exit "${result}" ================================================ FILE: hack/tools/go.mod ================================================ module tools go 1.25.0 // Should be in sync with pinversion.go tool ( github.com/containerd/ltag github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker github.com/golangci/golangci-lint/v2/cmd/golangci-lint github.com/jandubois/nobin github.com/yoheimuta/protolint/cmd/protolint google.golang.org/grpc/cmd/protoc-gen-go-grpc google.golang.org/protobuf/cmd/protoc-gen-go mvdan.cc/sh/v3/cmd/shfmt ) require ( github.com/containerd/ltag v0.3.0 github.com/golangci/golangci-lint/v2 v2.11.3 github.com/jandubois/nobin v0.8.0 github.com/yoheimuta/protolint v0.56.4 google.golang.org/grpc v1.79.3 google.golang.org/protobuf v1.36.11 mvdan.cc/sh/v3 v3.13.0 ) require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect dev.gaijin.team/go/golib v0.6.0 // indirect github.com/4meepo/tagalign v1.4.3 // indirect github.com/Abirdcfly/dupword v0.1.7 // indirect github.com/AdminBenni/iota-mixing v1.0.0 // indirect github.com/AlwxSin/noinlineerr v1.0.5 // indirect github.com/Antonboom/errname v1.1.1 // indirect github.com/Antonboom/nilnil v1.1.1 // indirect github.com/Antonboom/testifylint v1.6.4 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Djarvur/go-err113 v0.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/MirrexOne/unqueryvet v1.5.4 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.1.0 // indirect github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect github.com/ashanbrown/makezero/v2 v2.1.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bombsimon/wsl/v4 v4.7.0 // indirect github.com/bombsimon/wsl/v5 v5.6.0 // indirect github.com/breml/bidichk v0.3.3 // indirect github.com/breml/errchkjson v0.4.1 // indirect github.com/butuzov/ireturn v0.4.0 // indirect github.com/butuzov/mirror v1.3.0 // indirect github.com/catenacyber/perfsprint v0.10.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.11 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/editorconfig-checker/editorconfig-checker/v3 v3.6.1 // indirect github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gertd/go-pluralize v0.2.1 // indirect github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect github.com/go-toolsmith/astfmt v1.1.0 // indirect github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godoc-lint/godoc-lint v0.11.2 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.1 // indirect github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect github.com/golangci/golines v0.15.0 // indirect github.com/golangci/misspell v0.8.0 // indirect github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/renameio/v2 v2.0.2 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect github.com/gostaticanalysis/nilerr v0.1.2 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect github.com/kisielk/errcheck v1.10.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/exptostd v0.4.5 // indirect github.com/ldez/gomoddirectives v0.8.0 // indirect github.com/ldez/grignotin v0.10.1 // indirect github.com/ldez/structtags v0.6.1 // indirect github.com/ldez/tagliatelle v0.7.2 // indirect github.com/ldez/usetesting v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.5.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect github.com/maratori/testpackage v1.1.2 // indirect github.com/matoous/godox v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgechev/revive v1.15.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/oklog/run v1.2.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/raeperd/recvcheck v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sonatard/noctx v0.5.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.12.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/tetafro/godot v1.5.4 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect github.com/uudashr/gocognit v1.2.1 // indirect github.com/uudashr/iface v1.4.1 // indirect github.com/wlynxg/chardet v1.0.4 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect go-simpler.org/sloglint v0.11.1 // indirect go.augendre.info/arangolint v0.4.0 // indirect go.augendre.info/fatcontext v0.9.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.7.0 // indirect mvdan.cc/editorconfig v0.3.0 // indirect mvdan.cc/gofumpt v0.9.2 // indirect mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect ) ================================================ FILE: hack/tools/go.sum ================================================ 4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ= github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M= github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8= github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU= github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/ltag v0.3.0 h1:AbeBQAGLwWxWVkgtLblT5Zd5fFW1+45On3+RvuZO+Go= github.com/containerd/ltag v0.3.0/go.mod h1:VEpXtwQK+FDdhegH7NLRJM5gzdHtHWDztP1YoZxWJlQ= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/editorconfig-checker/editorconfig-checker/v3 v3.6.1 h1:jdfYul8o1YpAb0tjNBcfJsVFwTn1s4qis4ec5Feuhhs= github.com/editorconfig-checker/editorconfig-checker/v3 v3.6.1/go.mod h1:3M/pJVyyr63yWjRWOFpPqKNAzj6JaE/I/+dNDfgN+3I= github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 h1:CHwUbBVVyKWRX9kt5A/OtwhYUJB32DrFp9xzmjR6cac= github.com/editorconfig/editorconfig-core-go/v2 v2.6.4/go.mod h1:JWRVKHdVW+dkv6F8p+xGCa6a+TyMrqsFbFkSs/aQkrQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-snaps v0.5.19 h1:hUJlCQOpTt1M+kSisMwioDWZDWpDtdAvUhvWCx1YGW0= github.com/gkampitakis/go-snaps v0.5.19/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM= github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= github.com/golangci/golangci-lint/v2 v2.11.3 h1:ySX1GtLwlwOEzcLKJifI/aIVesrcHDno+5mrro8rWes= github.com/golangci/golangci-lint/v2 v2.11.3/go.mod h1:HmDEVZuxz77cNLumPfNNHAFyMX/b7IbA0tpmAbwiVfo= github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg= github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw= github.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jandubois/nobin v0.8.0 h1:nJ4UDkh14goFC19EqayuH5DtdqjWI0QugZss262HXus= github.com/jandubois/nobin v0.8.0/go.mod h1:qRcr5FDrIKDLCS6TFd7LzH6j+SDRjElpmmhVQeZLTWM= github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 h1:AoLtJX4WUtZkhhUUMFy3GgecAALp/Mb4S1iyQOA2s0U= github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08/go.mod h1:+XLCJiRE95ga77XInNELh2M6zQP+PdqiT9Zpm0D9Wpk= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= github.com/sonatard/noctx v0.5.0 h1:e/jdaqAsuWVOKQ0P6NWiIdDNHmHT5SwuuSfojFjzwrw= github.com/sonatard/noctx v0.5.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/wlynxg/chardet v1.0.4 h1:hkI71Dx8v3RiAz3XKV5lJEh9QfKo7xXKUmYJQeIMlpo= github.com/wlynxg/chardet v1.0.4/go.mod h1:HLQMNsa0w4MkH2e7waQaFD+Yh85riFFTLhFtP8fsdbQ= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yoheimuta/go-protoparser/v4 v4.14.2 h1:/P/LlX1CF9NaTWEltGcIZVvNlPbhABuAnBtAWpb3+74= github.com/yoheimuta/go-protoparser/v4 v4.14.2/go.mod h1:AHNNnSWnb0UoL4QgHPiOAg2BniQceFscPI5X/BZNHl8= github.com/yoheimuta/protolint v0.56.4 h1:FWvXjVNRaKJWJFxsnilRZhfQ4tc3KS8VVGWecxnLXLo= github.com/yoheimuta/protolint v0.56.4/go.mod h1:XrnOc0O5mckLR1GAOjqMPdb3R3ZEfLkMpLoq5RxxoG0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= mvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ= mvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ= mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= mvdan.cc/sh/v3 v3.13.0 h1:dSfq/MVsY4w0Vsi6Lbs0IcQquMVqLdKLESAOZjuHdLg= mvdan.cc/sh/v3 v3.13.0/go.mod h1:KV1GByGPc/Ho0X1E6Uz9euhsIQEj4hwyKnodLlFLoDM= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: hack/tools/pinversion.go ================================================ //go:build tools // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 // Package tools is used to explicitly pin tool versions. // It's needed to work around @dependabot's lack of upgrading indirect dependencies. package tools import ( _ "github.com/containerd/ltag" _ "github.com/golangci/golangci-lint/v2/pkg/exitcodes" _ "github.com/jandubois/nobin/version" _ "github.com/yoheimuta/protolint/lib" _ "google.golang.org/grpc" _ "google.golang.org/protobuf/proto" _ "mvdan.cc/sh/v3/pattern" ) ================================================ FILE: hack/update-template-almalinux-kitten.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function almalinux_kitten_print_help() { cat <] ... Description: This script updates the AlmaLinux Kitten image location in the specified templates. If the image location in the template contains a minor version and release date in the URL, the script replaces it with the latest available minor version and date. Image location basename format: AlmaLinux-Kitten-GenericCloud--[latest|.]..qcow2 Published AlmaLinux Kitten image information is fetched from the following URLs: https://kitten.repo.almalinux.org/-kitten/cloud//images/ To parsing html, this script requires 'htmlq' or 'pup' command. The downloaded files will be cached in the Lima cache directory. Examples: Update the AlmaLinux Kitten image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the AlmaLinux Kitten image location in ~/.lima/almalinux-kitten/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/almalinux-kitten/lima.yaml $ limactl factory-reset almalinux-kitten Update the AlmaLinux Kitten image location to major version 10 in ~/.lima/almalinux-kitten/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version-major 10 ~/.lima/almalinux-kitten/lima.yaml $ limactl factory-reset almalinux-kitten Flags: --version-major Use the specified version. The version must be 10 or later. -h, --help Print this help message HELP } # print the URL spec for the given location function almalinux_kitten_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture( "^https://kitten\\.repo\\.almalinux\\.org/(?\\d+)-kitten/cloud/(?[^/]+)/images/" + "AlmaLinux-Kitten-(?.*)-(?\\d+)-" + "(latest|(?\\d{8}(\\\\d+)?))\\.(?\\d+)\\.(?[^.]+).(?.*)$" ;"x") ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") jq -e '.path_arch == .arch' <<<"${url_spec}" >/dev/null || error_exit "Validation failed: .path_arch != .arch: ${location}" echo "${url_spec}" } readonly almalinux_kitten_jq_filter_directory='"https://kitten.repo.almalinux.org/\(.path_version)-kitten/cloud/\(.path_arch)/images/"' readonly almalinux_kitten_jq_filter_filename='"AlmaLinux-Kitten-\(.target_vendor)-\(.major_version)-\(if .date then .date + ".0" else "latest" end).\(.arch).\(.file_extension)"' # print the location for the given URL spec function almalinux_kitten_location_from_url_spec() { local -r url_spec=$1 jq -e -r "${almalinux_kitten_jq_filter_directory} + ${almalinux_kitten_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function almalinux_kitten_image_directory_from_url_spec() { local -r url_spec=$1 jq -e -r "${almalinux_kitten_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the image directory for ${url_spec}" } function almalinux_kitten_image_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${almalinux_kitten_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the image filename for ${url_spec}" } # function almalinux_kitten_latest_image_entry_for_url_spec() { local url_spec=$1 arch major_version_url_spec major_version_image_directory downloaded_page links_in_page latest_minor_version_info arch=$(jq -r '.arch' <<<"${url_spec}") # to detect minor version updates, we need to get the major version URL major_version_url_spec=$(jq -e -r '.path_version = .major_version' <<<"${url_spec}") major_version_image_directory=$(almalinux_kitten_image_directory_from_url_spec "${major_version_url_spec}") downloaded_page=$(download_to_cache "${major_version_image_directory}") if command -v htmlq >/dev/null; then links_in_page=$(htmlq 'pre a' --attribute href <"${downloaded_page}") elif command -v pup >/dev/null; then links_in_page=$(pup 'pre a attr{href}' <"${downloaded_page}") else error_exit "Please install 'htmlq' or 'pup' to list images from ${major_version_image_directory}" fi latest_minor_version_info=$(jq -e -Rrs --argjson spec "${url_spec}" ' [ split("\n").[] | capture( "^AlmaLinux-Kitten-\($spec.target_vendor)-" + "(?\($spec.major_version))-" + "(?\\d{8})\\.(?\\d)+\\.\($spec.arch)\\.\($spec.file_extension)$" ;"x" ) | .version_number_array = ([.major_minor_version | scan("\\d+") | tonumber]) ] | sort_by(.version_number_array, .date_and_ci_job_id) | last ' <<<"${links_in_page}") [[ -n ${latest_minor_version_info} ]] || return local newer_url_spec location directory checksum_location downloaded_sha256sum filename digest # prefer the major_minor_version in the path newer_url_spec=$(jq -e -r ". + ${latest_minor_version_info} | .path_version = .major_minor_version" <<<"${url_spec}") location=$(almalinux_kitten_location_from_url_spec "${newer_url_spec}") directory=$(almalinux_kitten_image_directory_from_url_spec "${newer_url_spec}") checksum_location="${directory}CHECKSUM" downloaded_sha256sum=$(download_to_cache "${checksum_location}") filename=$(almalinux_kitten_image_filename_from_url_spec "${newer_url_spec}") digest=$(awk "/${filename}/{print \"sha256:\"\$1}" "${downloaded_sha256sum}") [[ -n ${digest} ]] || error_exit "Failed to get the SHA256 digest for ${filename}" json_vars location arch digest } function almalinux_kitten_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(almalinux_kitten_url_spec_from_location "${location}") jq -r '["almalinux-kitten", .major_minor_version // .major_version, .target_vendor, if .date then "timestamped" else "latest" end, .arch, .file_extension] | join(":")' <<<"${url_spec}" } function almalinux_kitten_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-"{}"} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on AlmaLinux Kitten" >&2 url_spec=$(almalinux_kitten_url_spec_from_location "${location}" | jq -r ". + ${overriding}") if jq -e '.date' <<<"${url_spec}" >/dev/null; then image_entry=$(almalinux_kitten_latest_image_entry_for_url_spec "${url_spec}") else image_entry=$( # shellcheck disable=SC2030 location=$(almalinux_kitten_location_from_url_spec "${url_spec}") location=$(validate_url_without_redirect "${location}") arch=$(jq -r '.path_arch' <<<"${url_spec}") json_vars location arch ) fi # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then error_exit "Please install 'htmlq' or 'pup' to list images from https://kitten.repo.almalinux.org/-kitten/cloud//images/" fi # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then echo "Please install 'htmlq' or 'pup' to list images from https://kitten.repo.almalinux.org/-kitten/cloud//images/" >&2 elif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("almalinux_kitten") else declare -a SUPPORTED_DISTRIBUTIONS=("almalinux_kitten") fi return 0 fi declare -a templates=() declare overriding="{}" while [[ $# -gt 0 ]]; do case "$1" in -h | --help) almalinux_kitten_print_help exit 0 ;; -d | --debug) set -x ;; --version-major) if [[ -n $2 && $2 != -* ]]; then overriding=$( major_version="${2%%.*}" [[ ${major_version} -ge 10 ]] || error_exit "AlmaLinux Kitten major version must be 8 or later" # shellcheck disable=2034 path_version="${major_version}" json_vars path_version major_version <<<"${overriding}" ) shift else error_exit "--version-major requires a value" fi ;; --version-major=*) overriding=$( major_version="${1#*=}" major_version="${major_version%%.*}" [[ ${major_version} -ge 10 ]] || error_exit "AlmaLinux Kitten major version must be 8 or later" # shellcheck disable=2034 path_version="${major_version}" json_vars path_version major_version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then almalinux_kitten_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. almalinux_kitten_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else almalinux_kitten_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-almalinux.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function almalinux_print_help() { cat <] ... Description: This script updates the AlmaLinux image location in the specified templates. If the image location in the template contains a minor version and release date in the URL, the script replaces it with the latest available minor version and date. Image location basename format: AlmaLinux--GenericCloud-[latest|.-]..qcow2 Published AlmaLinux image information is fetched from the following URLs: https://repo.almalinux.org/almalinux//cloud//images/ To parsing html, this script requires 'htmlq' or 'pup' command. The downloaded files will be cached in the Lima cache directory. Examples: Update the AlmaLinux image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the AlmaLinux image location in ~/.lima/almalinux/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/almalinux/lima.yaml $ limactl factory-reset almalinux Update the AlmaLinux image location to major version 9 in ~/.lima/almalinux/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version-major 9 ~/.lima/almalinux/lima.yaml $ limactl factory-reset almalinux Flags: --version-major Use the specified version. The version must be 8 or later. -h, --help Print this help message HELP } # print the URL spec for the given location function almalinux_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture( "^https://repo\\.almalinux\\.org/almalinux/(?\\d+(\\.\\d+)?)/cloud/(?[^/]+)/images/" + "AlmaLinux-(?\\d+)-(?.*)-" + "(latest|(?\\d+\\.\\d+)-(?\\d{8})(?:\\.\\d+)?)\\.(?[^.]+).(?.*)$" ;"x") ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") jq -e '.path_arch == .arch' <<<"${url_spec}" >/dev/null || error_exit "Validation failed: .path_arch != .arch: ${location}" echo "${url_spec}" } readonly almalinux_jq_filter_directory='"https://repo.almalinux.org/almalinux/\(.path_version)/cloud/\(.path_arch)/images/"' readonly almalinux_jq_filter_filename='"AlmaLinux-\(.major_version)-\(.target_vendor)-\(if .date then .major_minor_version + "-" + .date else "latest" end).\(.arch).\(.file_extension)"' # print the location for the given URL spec function almalinux_location_from_url_spec() { local -r url_spec=$1 jq -e -r "${almalinux_jq_filter_directory} + ${almalinux_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function almalinux_image_directory_from_url_spec() { local -r url_spec=$1 jq -e -r "${almalinux_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the image directory for ${url_spec}" } function almalinux_image_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${almalinux_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the image filename for ${url_spec}" } # function almalinux_latest_image_entry_for_url_spec() { local url_spec=$1 arch major_version_url_spec major_version_image_directory downloaded_page links_in_page latest_minor_version_info arch=$(jq -r '.arch' <<<"${url_spec}") # to detect minor version updates, we need to get the major version URL major_version_url_spec=$(jq -e -r '.path_version = .major_version' <<<"${url_spec}") major_version_image_directory=$(almalinux_image_directory_from_url_spec "${major_version_url_spec}") downloaded_page=$(download_to_cache "${major_version_image_directory}") if command -v htmlq >/dev/null; then links_in_page=$(htmlq 'pre a' --attribute href <"${downloaded_page}") elif command -v pup >/dev/null; then links_in_page=$(pup 'pre a attr{href}' <"${downloaded_page}") else error_exit "Please install 'htmlq' or 'pup' to list images from ${major_version_image_directory}" fi latest_minor_version_info=$(jq -e -Rrs --argjson spec "${url_spec}" ' [ split("\n").[] | capture( "^AlmaLinux-\($spec.major_version)-\($spec.target_vendor)-" + "(?\($spec.major_version)\\.\\d+)-" + "(?\\d{8}(?:\\.\\d)?)\\.\($spec.arch)\\.\($spec.file_extension)$" ;"x" ) | .version_number_array = ([.major_minor_version | scan("\\d+") | tonumber]) ] | sort_by(.version_number_array, .date_and_ci_job_id) | last ' <<<"${links_in_page}") [[ -n ${latest_minor_version_info} ]] || return local newer_url_spec location directory checksum_location downloaded_sha256sum filename digest # prefer the major_minor_version in the path newer_url_spec=$(jq -e -r ". + ${latest_minor_version_info} | .path_version = .major_minor_version" <<<"${url_spec}") location=$(almalinux_location_from_url_spec "${newer_url_spec}") directory=$(almalinux_image_directory_from_url_spec "${newer_url_spec}") checksum_location="${directory}CHECKSUM" downloaded_sha256sum=$(download_to_cache "${checksum_location}") filename=$(almalinux_image_filename_from_url_spec "${newer_url_spec}") digest=$(awk "/${filename}/{print \"sha256:\"\$1}" "${downloaded_sha256sum}") [[ -n ${digest} ]] || error_exit "Failed to get the SHA256 digest for ${filename}" json_vars location arch digest } function almalinux_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(almalinux_url_spec_from_location "${location}") jq -r '["almalinux", .major_minor_version // .major_version, .target_vendor, if .date then "timestamped" else "latest" end, .arch, .file_extension] | join(":")' <<<"${url_spec}" } function almalinux_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-"{}"} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on AlmaLinux" >&2 url_spec=$(almalinux_url_spec_from_location "${location}" | jq -r ". + ${overriding}") if jq -e '.date' <<<"${url_spec}" >/dev/null; then image_entry=$(almalinux_latest_image_entry_for_url_spec "${url_spec}") else image_entry=$( # shellcheck disable=SC2030 location=$(almalinux_location_from_url_spec "${url_spec}") location=$(validate_url_without_redirect "${location}") arch=$(jq -r '.path_arch' <<<"${url_spec}") json_vars location arch ) fi # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then error_exit "Please install 'htmlq' or 'pup' to list images from https://repo.almalinux.org/almalinux//cloud//images/" fi # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then echo "Please install 'htmlq' or 'pup' to list images from https://repo.almalinux.org/almalinux//cloud//images/" >&2 elif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("almalinux") else declare -a SUPPORTED_DISTRIBUTIONS=("almalinux") fi return 0 fi declare -a templates=() declare overriding="{}" while [[ $# -gt 0 ]]; do case "$1" in -h | --help) almalinux_print_help exit 0 ;; -d | --debug) set -x ;; --version-major) if [[ -n $2 && $2 != -* ]]; then overriding=$( major_version="${2%%.*}" [[ ${major_version} -ge 8 ]] || error_exit "AlmaLinux major version must be 8 or later" # shellcheck disable=2034 path_version="${major_version}" json_vars path_version major_version <<<"${overriding}" ) shift else error_exit "--version-major requires a value" fi ;; --version-major=*) overriding=$( major_version="${1#*=}" major_version="${major_version%%.*}" [[ ${major_version} -ge 8 ]] || error_exit "AlmaLinux major version must be 8 or later" # shellcheck disable=2034 path_version="${major_version}" json_vars path_version major_version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then almalinux_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. almalinux_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else almalinux_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-alpine.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function alpine_print_help() { cat <.|latest-stable)|--version-major --version-minor ] ... Description: This script updates the Alpine Linux image location in the specified templates. Image location basename format: _alpine----[-]-.qcow2 Published Alpine Linux image information is fetched from the following URLs: latest-stable: https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/cloud .: https://dl-cdn.alpinelinux.org/alpine/v./releases/cloud To parsing html, this script requires 'htmlq' or 'pup' command. The downloaded files will be cached in the Lima cache directory. Examples: Update the Alpine Linux image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Alpine Linux image location to version 3.18 in ~/.lima/alpine/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version-major-minor 3.18 ~/.lima/alpine/lima.yaml $ limactl factory-reset alpine Flags: --version-major-minor (.|latest-stable) Use the specified . version or alias "latest-stable". The . version must be 3.18 or later. --version-major --version-minor Use the specified and version. -h, --help Print this help message HELP } # print the URL spec for the given location function alpine_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture(" ^https://dl-cdn\\.alpinelinux\\.org/alpine/(?v\\d+\\.\\d+|latest-stable)/releases/cloud/ (?[^_]+)_alpine-(?\\d+\\.\\d+\\.\\d+)-(?[^-]+)- (?[^-]+)-(?[^-]+)(-(?metal|vm))?-(?r\\d+)\\.(?.*)$ ";"x") ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") echo "${url_spec}" } readonly alpine_jq_filter_directory='"https://dl-cdn.alpinelinux.org/alpine/\(.path_version)/releases/cloud/"' readonly alpine_jq_filter_filename=' "\(.target_vendor)_alpine-\(.version)-\(.arch)-\(.firmware)-\(.bootstrap)" + "\(if .machine then "-" + .machine else "" end)-\(.image_revision).\(.file_extension)" ' # print the location for the given URL spec function alpine_location_from_url_spec() { local -r url_spec=$1 jq -e -r "${alpine_jq_filter_directory} + ${alpine_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function alpine_image_directory_from_url_spec() { local -r url_spec=$1 jq -e -r "${alpine_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the image directory for ${url_spec}" } function alpine_image_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${alpine_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the image filename for ${url_spec}" } # function alpine_latest_image_entry_for_url_spec() { local url_spec=$1 arch image_directory downloaded_page links_in_page latest_version_info # shellcheck disable=SC2034 arch=$(jq -r '.arch' <<<"${url_spec}") image_directory=$(alpine_image_directory_from_url_spec "${url_spec}") downloaded_page=$(download_to_cache "${image_directory}") if command -v htmlq >/dev/null; then links_in_page=$(htmlq 'pre a' --attribute href <"${downloaded_page}") elif command -v pup >/dev/null; then links_in_page=$(pup 'pre a attr{href}' <"${downloaded_page}") else error_exit "Please install 'htmlq' or 'pup' to list images from ${image_directory}" fi latest_version_info=$(jq -e -Rrs --argjson spec "${url_spec}" ' [ split("\n").[] | capture( "^\($spec.target_vendor)_alpine-(?\\d+\\.\\d+\\.\\d+)-\($spec.arch)-" + "\($spec.firmware)-\($spec.bootstrap)\(if $spec.machine then "-" + $spec.machine else "" end)-" + "(?r\\d+)\\.\($spec.file_extension)" ;"x" ) | .version_number_array = ([.version | scan("\\d+") | tonumber]) ] | sort_by(.version_number_array, .image_revision) | last ' <<<"${links_in_page}") [[ -n ${latest_version_info} ]] || return local newer_url_spec location sha512sum_location downloaded_sha256sum filename digest # prefer the v. in the path newer_url_spec=$(jq -e -r ". + ${latest_version_info} | .path_version = \"v\" + (.version_number_array[:2]|map(tostring)|join(\".\"))" <<<"${url_spec}") location=$(alpine_location_from_url_spec "${newer_url_spec}") location=$(validate_url_without_redirect "${location}") sha512sum_location="${location}.sha512" downloaded_sha256sum=$(download_to_cache "${sha512sum_location}") filename=$(alpine_image_filename_from_url_spec "${newer_url_spec}") digest="sha512:$(<"${downloaded_sha256sum}")" [[ -n ${digest} ]] || error_exit "Failed to get the digest for ${filename}" json_vars location arch digest } function alpine_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(alpine_url_spec_from_location "${location}") jq -r '["alpine", .path_version, .target_vendor, .arch, .file_extension] | join(":")' <<<"${url_spec}" } function alpine_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-'{"path_version":"latest-stable"}'} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on Alpine Linux" >&2 url_spec=$(alpine_url_spec_from_location "${location}" | jq -r ". + ${overriding}") image_entry=$(alpine_latest_image_entry_for_url_spec "${url_spec}") # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then error_exit "Please install 'htmlq' or 'pup' to list images from https://dl-cdn.alpinelinux.org/alpine//releases/cloud/" fi # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then echo "Please install 'htmlq' or 'pup' to list images from https://dl-cdn.alpinelinux.org/alpine//releases/cloud/" >&2 elif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("alpine") else declare -a SUPPORTED_DISTRIBUTIONS=("alpine") fi return 0 fi declare -a templates=() declare overriding='{}' declare version_major='' version_minor='' while [[ $# -gt 0 ]]; do case "$1" in -h | --help) alpine_print_help exit 0 ;; -d | --debug) set -x ;; --version-major-minor) if [[ -n ${2:-} && $2 != -* ]]; then version="$2" shift else error_exit "--version-major-minor requires a value" fi ;& --version-major-minor=*) version=${version:-${1#*=}} overriding=$( version="${version#v}" if [[ ${version} =~ ^v?[0-9]+.[0-9]+ ]]; then version="$(echo "${version}" | cut -d. -f1-2)" [[ ${version%%.*} -gt 3 || (${version%%.*} -eq 3 && ${version#*.} -ge 18) ]] || error_exit "Alpine Linux version must be 3.18 or later" path_version="v${version}" elif [[ ${version} == "latest-stable" ]]; then path_version="latest-stable" else error_exit "--version-major-minor requires a value in the format . or latest-stable" fi json_vars path_version <<<"${overriding}" ) ;; --version-major) if [[ -n ${2:-} && $2 != -* ]]; then version_major="$2" shift else error_exit "--version-major requires a value" fi ;& --version-major=*) version_major=${version_major:-${1#*=}} [[ ${version_major} =~ ^[0-9]+$ ]] || error_exit "Please specify --version-major in numbers" ;; --version-minor) if [[ -n ${2:-} && $2 != -* ]]; then version_minor="$2" shift else error_exit "--version-minor requires a value" fi ;& --version-minor=*) version_minor=${version_minor:-${1#*=}} [[ ${version_minor} =~ ^[0-9]+$ ]] || error_exit "Please specify --version-minor in numbers" ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if ! jq -e '.path_version' <<<"${overriding}" >/dev/null; then # --version-major-minor is not specified if [[ -n ${version_major} && -n ${version_minor} ]]; then [[ ${version_major} -gt 3 || (${version_major} -eq 3 && ${version_minor} -ge 18) ]] || error_exit "Alpine Linux version must be 3.18 or later" # shellcheck disable=2034 path_version="v${version_major}.${version_minor}" overriding=$(json_vars path_version <<<"${overriding}") elif [[ -n ${version_major} ]]; then error_exit "--version-minor is required when --version-major is specified" elif [[ -n ${version_minor} ]]; then error_exit "--version-major is required when --version-minor is specified" fi elif [[ -n ${version_major} || -n ${version_minor} ]]; then # --version-major-minor is specified echo "Ignoring --version-major and --version-minor because --version-major-minor is specified" >&2 fi [[ ${overriding} == "{}" ]] && overriding='{"path_version":"latest-stable"}' if [[ ${#templates[@]} -eq 0 ]]; then alpine_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. alpine_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else alpine_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-archlinux.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function archlinux_print_help() { cat <... Description: This script updates the Arch-Linux image location in the specified templates. If the image location in the template contains a release date in the URL, the script replaces it with the latest available date. Image location basename format: Arch-Linux--cloudimg[-.].qcow2 Published Arch-Linux image information is fetched from the following URLs: x86_64: listing: https://gitlab.archlinux.org/api/v4/projects/archlinux%2Farch-boxes/packages details: https://gitlab.archlinux.org/api/v4/projects/archlinux%2Farch-boxes/packages/:package_id/package_files aarch64: https://github.com/mcginty/arch-boxes-arm/releases/ Using 'gh' CLI tool for fetching the latest release from GitHub. Examples: Update the Arch-Linux image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Arch-Linux image location in ~/.lima/archlinux/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/archlinux/lima.yaml $ limactl factory-reset archlinux Flags: -h, --help Print this help message HELP } # print the URL spec for the given location # shellcheck disable=SC2034 function archlinux_url_spec_from_location() { local location=$1 location_basename arch flavor source date_and_ci_job_id='' file_extension location_basename=$(basename "${location}") arch=$(echo "${location_basename}" | cut -d- -f3) flavor=$(echo "${location_basename}" | cut -d- -f4 | cut -d. -f1) case "${location}" in https://geo.mirror.pkgbuild.com/images/*) source="geo.mirror.pkgbuild.com" local -r date_and_ci_job_id_pattern='[0-9]{8}\.[0-9]+' if [[ ${location} =~ ${date_and_ci_job_id_pattern} ]]; then date_and_ci_job_id="${BASH_REMATCH[0]}" fi if [[ ${location_basename} =~ ${date_and_ci_job_id_pattern} ]]; then file_extension=${location_basename##*"${BASH_REMATCH[0]}".} else file_extension=${location_basename#*.} fi ;; https://github.com/mcginty/arch-boxes-arm/releases/download/*) source="github.com/mcginty/arch-boxes-arm" local -r date_pattern='[0-9]{8}' if [[ ${location} =~ ${date_pattern} ]]; then date_and_ci_job_id="${BASH_REMATCH[0]}" file_extension=${location_basename#*"${date_and_ci_job_id}".*.} else error_exit "Failed to extract date from ${location}" fi ;; *) # Unsupported location return 1 ;; esac json_vars source arch flavor date_and_ci_job_id file_extension } # print the location for the given URL spec function archlinux_location_from_url_spec() { local url_spec=$1 source arch flavor date_and_ci_job_id file_extension location='' source=$(jq -r '.source' <<<"${url_spec}") arch=$(jq -r '.arch' <<<"${url_spec}") flavor=$(jq -r '.flavor' <<<"${url_spec}") date_and_ci_job_id=$(jq -r '.date_and_ci_job_id' <<<"${url_spec}") file_extension=$(jq -r '.file_extension' <<<"${url_spec}") case "${source}" in geo.mirror.pkgbuild.com) location="https://geo.mirror.pkgbuild.com/images/" if [[ -n ${date_and_ci_job_id} ]]; then location+="v${date_and_ci_job_id}/Arch-Linux-${arch}-${flavor}-${date_and_ci_job_id}.${file_extension}" else location+="latest/Arch-Linux-${arch}-${flavor}.${file_extension}" fi ;; github.com/mcginty/arch-boxes-arm) ;; *) error_exit "Unsupported source: ${source}" ;; esac echo "${location}" } # returns the image entry for the latest image in the gitlab mirror function archlinux_image_entry_for_image_kernel_gitlab_mirror() { local location=$1 url_spec=$2 arch flavor date_and_ci_job_id gitlab_package_api_base latest_package_id jq_filter latest_package_file file_name digest updated_url_spec arch=$(jq -r '.arch' <<<"${url_spec}") if ! jq -e '.date_and_ci_job_id' <<<"${url_spec}" >/dev/null; then json_vars location arch return 1 fi flavor=$(jq -r '.flavor' <<<"${url_spec}") date_and_ci_job_id=$(jq -r '.date_and_ci_job_id' <<<"${url_spec}") file_extension=$(jq -r '.file_extension' <<<"${url_spec}") gitlab_package_api_base="https://gitlab.archlinux.org/api/v4/projects/archlinux%2Farch-boxes/packages" latest_package_id=$(curl --silent --show-error "${gitlab_package_api_base}" | jq -r 'last|.id') || error_exit "Failed to fetch latest package_id" jq_filter=" .[]|select(.file_name|test(\"^Arch-Linux-${arch}-${flavor}-.*\\\.${file_extension}\$\")) " latest_package_file=$(curl -s "${gitlab_package_api_base}/${latest_package_id}/package_files" | jq -r "${jq_filter}") || error_exit "Failed to fetch latest package_file" file_name=$(jq -r '.file_name' <<<"${latest_package_file}") digest="sha256:$(jq -r '.file_sha256' <<<"${latest_package_file}")" local -r date_and_ci_job_id_pattern='[0-9]{8}\.[0-9]+' [[ ${file_name} =~ ${date_and_ci_job_id_pattern} ]] || error_exit "Failed to extract date_and_ci_job_id from ${file_name}" date_and_ci_job_id="${BASH_REMATCH[0]}" updated_url_spec=$(json_vars date_and_ci_job_id <<<"${url_spec}") location=$(archlinux_location_from_url_spec "${updated_url_spec}") location=$(validate_url_without_redirect "${location}") json_vars location arch digest } # returns the image entry for the latest image in the GitHub repo function archlinux_image_entry_for_image_kernel_github_com() { local location=$1 url_spec=$2 arch flavor file_extension repo jq_filter latest_location downloaded_img digest arch=$(jq -r '.arch' <<<"${url_spec}") if ! jq -e '.date_and_ci_job_id' <<<"${url_spec}" >/dev/null; then json_vars location arch return 1 fi flavor=$(jq -r '.flavor' <<<"${url_spec}") file_extension=$(jq -r '.file_extension' <<<"${url_spec}") command -v gh >/dev/null || error_exit "gh is required for fetching the latest release from GitHub, but it's not installed" local -r repo_pattern='github.com/(.*)/releases/download/(.*)' if [[ ${location} =~ ${repo_pattern} ]]; then repo="${BASH_REMATCH[1]}" else error_exit "Failed to extract repo and release from ${location}" fi jq_filter=".assets[]|select(.name|test(\"^Arch-Linux-${arch}-${flavor}-.*\\\.${file_extension}\$\"))|.url" latest_location=$(gh release view --repo "${repo}" --json assets --jq "${jq_filter}") [[ -n ${latest_location} ]] || error_exit "Failed to fetch the latest release URL from ${repo}" if [[ ${location} == "${latest_location}" ]]; then json_vars location arch return fi location=${latest_location} downloaded_img=$(download_to_cache_without_redirect "${latest_location}") # shellcheck disable=SC2034 digest="sha512:$(sha512sum "${downloaded_img}" | cut -d' ' -f1)" json_vars location arch digest } function archlinux_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(archlinux_url_spec_from_location "${location}") jq -r '["archlinux", .source, .arch, .date_and_ci_job_id // empty]|join(":")' <<<"${url_spec}" } function archlinux_image_entry_for_image_kernel() { local location=$1 url_spec source image_entry='' url_spec=$(archlinux_url_spec_from_location "${location}") source=$(jq -r '.source' <<<"${url_spec}") case "${source}" in geo.mirror.pkgbuild.com) image_entry=$(archlinux_image_entry_for_image_kernel_gitlab_mirror "${location}" "${url_spec}") ;; github.com/mcginty/arch-boxes-arm) image_entry=$(archlinux_image_entry_for_image_kernel_github_com "${location}" "${url_spec}") ;; *) error_exit "Unsupported source: ${source}" ;; esac if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("archlinux") else declare -a SUPPORTED_DISTRIBUTIONS=("archlinux") fi return 0 fi declare -a templates=() declare overriding="{}" while [[ $# -gt 0 ]]; do case "$1" in -h | --help) archlinux_print_help exit 0 ;; -d | --debug) set -x ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then archlinux_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. archlinux_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else archlinux_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-centos-stream.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function centos_print_help() { cat <] ... Description: This script updates the CentOS Stream image location in the specified templates. If the image location in the template contains a release date in the URL, the script replaces it with the latest available date. Image location basename format: CentOS-Stream-GenericCloud--[latest|.0]..qcow2 Published CentOS Stream image information is fetched from the following URLs: https://cloud.centos.org/centos/-stream//images/ To parsing html, this script requires 'htmlq' or 'pup' command. The downloaded files will be cached in the Lima cache directory. Examples: Update the CentOS Stream image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the CentOS Stream image location in ~/.lima/centos/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/centos/lima.yaml $ limactl factory-reset centos Update the CentOS Stream image location to 9-Stream in ~/.lima/centos/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version 9-stream ~/.lima/centos/lima.yaml $ limactl factory-reset centos Flags: --version Use the specified version. The version must be 8 or later. -h, --help Print this help message HELP } # print the URL spec for the given location function centos_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture( "^https://cloud\\.centos\\.org/centos/(?\\d+)-stream/(?[^/]+)/images/" + "CentOS-Stream-(?.*)-(?\\d+(\\.[.\\d]+)?)-" + "(latest|(?\\d{8}\\.\\d+))\\.(?[^.]+).(?.*)$" ;"x")' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") jq -e '.path_version == .version' <<<"${url_spec}" >/dev/null || error_exit "Validation failed: .path_version != .version: ${location}" jq -e '.path_arch == .arch' <<<"${url_spec}" >/dev/null || error_exit "Validation failed: .path_arch != .arch: ${location}" echo "${url_spec}" } readonly centos_jq_filter_directory='"https://cloud.centos.org/centos/\(.version)-stream/\(.path_arch)/images/"' readonly centos_jq_filter_filename='"CentOS-Stream-\(.target_vendor)-\(.version)-\(.date_and_ci_job_id // "latest").\(.arch).\(.file_extension)"' # print the location for the given URL spec function centos_location_from_url_spec() { local -r url_spec=$1 jq -e -r "${centos_jq_filter_directory} + ${centos_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function centos_image_directory_from_url_spec() { local -r url_spec=$1 jq -e -r "${centos_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the image directory for ${url_spec}" } function centos_image_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${centos_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the image filename for ${url_spec}" } # function centos_latest_image_entry_for_url_spec() { local url_spec=$1 version arch image_directory downloaded_page links_in_page latest_info version=$(jq -r '.version' <<<"${url_spec}") arch=$(jq -r '.arch' <<<"${url_spec}") image_directory=$(centos_image_directory_from_url_spec "${url_spec}") downloaded_page=$(download_to_cache "${image_directory}") if command -v htmlq >/dev/null; then links_in_page=$(htmlq 'td.indexcolname a' --attribute href <"${downloaded_page}") elif command -v pup >/dev/null; then links_in_page=$(pup 'td[class=indexcolname] a attr{href}' <"${downloaded_page}") else error_exit "Please install 'htmlq' or 'pup' to list images from https://cloud.centos.org/centos/${version}/${arch}/images/" fi latest_info=$(jq -e -Rrs --argjson spec "${url_spec}" ' [ split("\n").[] | capture( "^CentOS-Stream-\($spec.target_vendor)-\($spec.version)-(?\\d{8}\\.\\d+)\\.\($spec.arch)\\.\($spec.file_extension)$" ;"x" ) ] | sort_by(.date_and_ci_job_id) | last ' <<<"${links_in_page}") [[ -n ${latest_info} ]] || return local newer_url_spec location sha256sum_location downloaded_sha256sum filename digest newer_url_spec=$(jq -e -r ". + ${latest_info}" <<<"${url_spec}") location=$(centos_location_from_url_spec "${newer_url_spec}") sha256sum_location="${location}.SHA256SUM" downloaded_sha256sum=$(download_to_cache "${sha256sum_location}") filename=$(centos_image_filename_from_url_spec "${newer_url_spec}") digest="sha256:$(awk "/SHA256 \(${filename}\) =/{print \$4}" "${downloaded_sha256sum}")" [[ -n ${digest} ]] || error_exit "Failed to get the SHA256 digest for ${filename}" json_vars location arch digest } function centos_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(centos_url_spec_from_location "${location}") jq -r '["centos", .version, .target_vendor, if .date_and_ci_job_id then "timestamped" else "latest" end, .arch, .file_extension] | join(":")' <<<"${url_spec}" } function centos_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-"{}"} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on CentOS Stream" >&2 url_spec=$(centos_url_spec_from_location "${location}" | jq -r ". + ${overriding}") if jq -e '.date_and_ci_job_id' <<<"${url_spec}" >/dev/null; then image_entry=$(centos_latest_image_entry_for_url_spec "${url_spec}") else image_entry=$( # shellcheck disable=SC2030 location=$(centos_location_from_url_spec "${url_spec}") location=$(validate_url_without_redirect "${location}") arch=$(jq -r '.path_arch' <<<"${url_spec}") json_vars location arch ) fi # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then error_exit "Please install 'htmlq' or 'pup' to list images from https://cloud.centos.org/centos///images/" fi # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then echo "Please install 'htmlq' or 'pup' to list images from https://cloud.centos.org/centos///images/" >&2 elif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("centos") else declare -a SUPPORTED_DISTRIBUTIONS=("centos") fi return 0 fi declare -a templates=() declare overriding="{}" while [[ $# -gt 0 ]]; do case "$1" in -h | --help) centos_print_help exit 0 ;; -d | --debug) set -x ;; --version) if [[ -n $2 && $2 != -* ]]; then overriding=$( version="${2%%-*}" [[ ${version} -ge 8 ]] || error_exit "CentOS Stream version must be 8 or later" json_vars version <<<"${overriding}" ) shift else error_exit "--version requires a value" fi ;; --version=*) overriding=$( version="${1#*=}" version="${version%%-*}" [[ ${version} -ge 8 ]] || error_exit "CentOS Stream version must be 8 or later" json_vars version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then centos_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. centos_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else centos_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-debian.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function debian_print_help() { cat <]] [--daily[=]] [--timestamped[=]] [--version ] ... Description: This script updates the Debian image location in the specified templates. If the image location in the template contains a release date in the URL, the script replaces it with the latest available date. If no flags are specified, the script uses the version from the image location basename in the template. Image location basename format: debian-[-backports]-genericcloud-[-daily][-].qcow2 Published Debian image information is fetched from the following URLs: https://cloud.debian.org/images/cloud/[-backports]/[daily/](latest|)/debian-[-backports]-genericcloud-[-daily][-].json The downloaded JSON file will be cached in the Lima cache directory. Examples: Update the Debian image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Debian image location in ~/.lima/debian/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/debian/lima.yaml $ limactl factory-reset debian Update the Debian image location to debian-13-genericcloud-.qcow2 in ~/.lima/debian/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version trixie ~/.lima/debian/lima.yaml $ limactl factory-reset debian Flags: --backports[=] Use the backports image The boolean value can be true, false, 1, or 0 --daily[=] Use the daily image --timestamped[=] Use the timestamped image --version Use the specified version The version can be a codename, version number, or alias (testing, stable, oldstable) -h, --help Print this help message HELP } readonly debian_base_url=https://cloud.debian.org/images/cloud/ readonly debian_target_vendor=genericcloud readonly -A debian_version_to_codename=( [10]=buster [11]=bullseye [12]=bookworm [13]=trixie [14]=forky ) declare -A debian_codename_to_version function debian_setup_codename_to_version() { local version codename for version in "${!debian_version_to_codename[@]}"; do codename=${debian_version_to_codename[${version}]} debian_codename_to_version[${codename}]="${version}" done readonly -A debian_codename_to_version } debian_setup_codename_to_version readonly -A debian_alias_to_codename=( [testing]=trixie [stable]=bookworm [oldstable]=bullseye ) # debian_downloaded_json downloads the JSON file for the given url_spec(JSON) and caches it # e.g. # ```console # debian_downloaded_json '{"backports":false,"daily":false,"version":12,"arch":"amd64","file_extension":"qcow2"}' # # ``` function debian_downloaded_json() { local url_spec=$1 json_url_spec json_url json_url_spec=$(jq -r '. | del(.timestamp) | .file_extension = "json"' <<<"${url_spec}") || error_exit "Failed to create JSON URL spec" json_url=$(debian_location_from_url_spec "${json_url_spec}") download_to_cache "${json_url}" } function debian_digest_from_upload_entry() { local upload_entry=$1 debian_digest digest debian_digest=$(jq -e -r '.metadata.annotations."cloud.debian.org/digest"' <<<"${upload_entry}") || error_exit "Failed to get the digest from ${upload_entry}" case "${debian_digest%:*}" in sha512) digest=$(echo "${debian_digest#*:}==" | base64 -d | xxd -p -c -) || error_exit "Failed to decode the digest from ${debian_digest}" ;; *) error_exit "Unsupported digest type: ${debian_digest%:*}" ;; esac echo "${debian_digest/:*/:}${digest}" } # debian_image_url_timestamped prints the latest image URL and its digest for the given flavor, version, arch, and path suffix. function debian_image_url_timestamped() { local url_spec=$1 debian_downloaded_json jq_filter upload_entry timestamp timestamped_url_spec location arch digest debian_downloaded_json=$(debian_downloaded_json "${url_spec}") # shellcheck disable=SC2016 jq_filter=' [.items[]|select(.kind == "Upload")| select(.metadata.labels."upload.cloud.debian.org/image-format" == $ARGS.named.url_spec.image_format)]|first ' upload_entry=$(jq -e -r --argjson url_spec "${url_spec}" "${jq_filter}" "${debian_downloaded_json}") || error_exit "Failed to find the upload entry from ${debian_downloaded_json}" timestamp=$(jq -e -r '.metadata.labels."cloud.debian.org/version"' <<<"${upload_entry}") || error_exit "Failed to get the timestamp from ${upload_entry}" timestamped_url_spec=$(json_vars timestamp <<<"${url_spec}") location=$(debian_location_from_url_spec "${timestamped_url_spec}") location=$(validate_url_without_redirect "${location}") arch=$(jq -e -r '.arch' <<<"${url_spec}") || error_exit "missing arch in ${url_spec}" arch=$(limayaml_arch "${arch}") digest=$(debian_digest_from_upload_entry "${upload_entry}") json_vars location arch digest } # debian_image_url_not_timestamped prints the release image URL for the given url_spec(JSON) function debian_image_url_not_timestamped() { local url_spec=$1 location arch location=$(debian_location_from_url_spec "${url_spec}") location=$(validate_url_without_redirect "${location}") arch=$(jq -e -r '.arch' <<<"${url_spec}") || error_exit "missing arch in ${url_spec}" arch=$(limayaml_arch "${arch}") json_vars location arch } # debian_version_resolve_aliases resolves the version aliases. # e.g. # ```console # debian_version_resolve_aliases testing # 13 # debian_version_resolve_aliases stable # 12 # debian_version_resolve_aliases oldstable # 11 # debian_version_resolve_aliases bookworm # 12 # debian_version_resolve_aliases 10 # 10 # debian_version_resolve_aliases '' # # ``` function debian_version_resolve_aliases() { local version=$1 [[ -v debian_alias_to_codename[${version}] ]] && version=${debian_alias_to_codename[${version}]} [[ -v debian_codename_to_version[${version}] ]] && version=${debian_codename_to_version[${version}]} [[ -v debian_version_to_codename[${version}] ]] || error_exit "Unsupported version: ${version}" [[ -z ${version} ]] || echo "${version}" } function debian_arch_from_location_basename() { local location=$1 location_basename arch location_basename=$(basename "${location}") location_basename=${location_basename/-backports/} arch=$(echo "${location_basename}" | cut -d- -f4 | cut -d. -f1) [[ -n ${arch} ]] || error_exit "Failed to get arch from ${location}" echo "${arch}" } function debian_file_extension_from_location_basename() { local location=$1 location_basename file_extension location_basename=$(basename "${location}") file_extension=$(echo "${location_basename}" | cut -d. -f2-) # remove the first field [[ -n ${file_extension} ]] || error_exit "Failed to get file extension from ${location}" echo "${file_extension}" } function debian_image_format_from_file_extension() { local file_extension=$1 case "${file_extension}" in json) echo "json" ;; qcow2) echo "qcow2" ;; raw) echo "raw" ;; tar.xz) echo "internal" ;; *) error_exit "Unsupported file extension: ${file_extension}" ;; esac } # debian_url_spec_from_location returns the URL spec for the given location. # If the location is not supported, it returns 1. # e.g. # ```console # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2 # {"backports":false,"daily":false,"version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/20241004-1890/debian-12-generic-amd64-20241004-1890.qcow2 # {"backports":false,"daily":false,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/daily/latest/debian-12-genericcloud-amd64-daily.qcow2 # {"backports":false,"daily":true,"version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/daily/20241019-1905/debian-12-genericcloud-amd64-daily-20241019-1905.qcow2 # {"backports":false,"daily":true,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm-backports/latest/debian-12-backports-genericcloud-amd64.qcow2 # {"backports":true,"daily":false,"version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm-backports/20241004-1890/debian-12-backports-genericcloud-amd64-20241004-1890.qcow2 # {"backports":true,"daily":false,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm-backports/daily/latest/debian-12-backports-genericcloud-amd64-daily.qcow2 # {"backports":true,"daily":true,"version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm-backports/daily/20241019-1905/debian-12-backports-genericcloud-amd64-daily-20241019-1905.qcow2 # {"backports":true,"daily":true,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2","image_format":"qcow2"} # ``` # shellcheck disable=SC2034 function debian_url_spec_from_location() { local location=$1 backports=false daily=false timestamp='' codename version='' arch file_extension image_format local -r timestamp_pattern='[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]' case "${location}" in ${debian_base_url}*-backports/*) backports=true ;;& ${debian_base_url}*/daily/*) daily=true ;;& ${debian_base_url}*/${timestamp_pattern}/*) [[ ${location} =~ ${timestamp_pattern} ]] && timestamp=${BASH_REMATCH[0]} ;; ${debian_base_url}*/latest/*) timestamp='' ;; *) # echo "Unsupported image location: ${location}" >&2 return 1 ;; esac codename=$(echo "${location#"${debian_base_url}"}" | cut -d/ -f1 | cut -d- -f1) [[ -v debian_codename_to_version[${codename}] ]] || error_exit "Unknown codename: ${codename}" version=${debian_codename_to_version[${codename}]} arch=$(debian_arch_from_location_basename "${location}") file_extension=$(debian_file_extension_from_location_basename "${location}") image_format=$(debian_image_format_from_file_extension "${file_extension}") json_vars backports daily timestamp version arch file_extension image_format } # debian_location_from_url_spec returns the location for the given URL spec. # e.g. # ```console # debian_location_from_url_spec '{"backports":false,"daily":false,"version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2 # debian_location_from_url_spec '{"backports":false,"daily":false,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm/20241019-1905/debian-12-generic-amd64-20241019-1905.qcow2 # debian_location_from_url_spec '{"backports":false,"daily":true,"version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm/daily/latest/debian-12-genericcloud-amd64-daily.qcow2 # debian_location_from_url_spec '{"backports":false,"daily":true,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm/daily/20241019-1905/debian-12-generic-amd64-daily-20241019-1905.qcow2 # debian_location_from_url_spec '{"backports":true,"daily":false,"version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm-backports/latest/debian-12-backports-genericcloud-amd64.qcow2 # debian_location_from_url_spec '{"backports":true,"daily":false,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm-backports/20241019-1905/debian-12-backports-genericcloud-amd64-20241019-1905.qcow2 # debian_location_from_url_spec '{"backports":true,"daily":true,"version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm-backports/daily/latest/debian-12-backports-genericcloud-amd64-daily.qcow2 # debian_location_from_url_spec '{"backports":true,"daily":true,"timestamp":"20241019-1905","version":12,"arch":"amd64","file_extension":"qcow2"}' # https://cloud.debian.org/images/cloud/bookworm-backports/daily/20241019-1905/debian-12-backports-genericcloud-amd64-daily-20241019-1905.qcow2 # ``` function debian_location_from_url_spec() { local url_spec=$1 base_url version backports daily timestamp arch file_extension base_url=${debian_base_url} version=$(jq -e -r '.version' <<<"${url_spec}") [[ -v debian_version_to_codename[${version}] ]] || error_exit "Unsupported version: ${version}" base_url+=${debian_version_to_codename[${version}]} backports=$(jq -r 'if .backports then "-backports" else empty end' <<<"${url_spec}") base_url+=${backports}/ daily=$(jq -r 'if .daily then "daily" else empty end' <<<"${url_spec}") base_url+=${daily:+${daily}/} timestamp=$(jq -r 'if .timestamp then .timestamp else empty end' <<<"${url_spec}") base_url+=${timestamp:-latest}/ arch=$(jq -e -r '.arch' <<<"${url_spec}") file_extension=$(jq -e -r '.file_extension' <<<"${url_spec}") base_url+=debian-${version}${backports}-${debian_target_vendor}-${arch}${daily:+-${daily}}${timestamp:+-${timestamp}}.${file_extension} echo "${base_url}" } # debian_cache_key_for_image_kernel_overriding returns the cache key for the given location, kernel_location, flavor, and version. # If the image location is not supported, it returns 1. # kernel_location is not validated. # e.g. # ```console # debian_cache_key_for_image_kernel_overriding https://cloud-images.debian.com/minimal/releases/24.04/release-20210914/debian-24.04-minimal-cloudimg-amd64.img # debian_latest_24.04-minimal-amd64-release-.img # debian_cache_key_for_image_kernel_overriding https://cloud-images.debian.com/minimal/releases/24.04/release-20210914/debian-24.04-minimal-cloudimg-amd64.img https://... # debian_latest_with_kernel_24.04-minimal-amd64-release-.img # debian_cache_key_for_image_kernel_overriding https://cloud-images.debian.com/releases/24.04/release/debian-24.04-server-cloudimg-amd64.img null # debian_release_24.04-server-amd64-.img # ``` function debian_cache_key_for_image_kernel_overriding() { local location=$1 kernel_location=${2:-null} url_spec with_kernel='' version backports arch daily timestamped file_extension url_spec=$(debian_url_spec_from_location "${location}") [[ ${kernel_location} != "null" ]] && with_kernel=_with_kernel version=$(jq -r '.version|if . then "-\(.)" else empty end' <<<"${url_spec}") backports=$(jq -r 'if .backports then "-backports" else empty end' <<<"${url_spec}") arch=$(jq -e -r '.arch' <<<"${url_spec}") daily=$(jq -r 'if .daily then "-daily" else empty end' <<<"${url_spec}") timestamped=$(jq -r 'if .timestamp then "-timestamped" else empty end' <<<"${url_spec}") file_extension=$(jq -e -r '.file_extension' <<<"${url_spec}") echo "debian${with_kernel}${version}${backports}-${debian_target_vendor}-${arch}${daily}${timestamped}.${file_extension}" } function debian_image_entry_for_image_kernel_overriding() { local location=$1 kernel_location=$2 overriding=${3:-"{}"} url_spec timestamped [[ ${kernel_location} == "null" ]] || error_exit "Updating image with kernel is not supported" url_spec=$(debian_url_spec_from_location "${location}" | jq -r ". + ${overriding}") timestamped=$(jq -r 'if .timestamp then "timestamped" else "not_timestamped" end' <<<"${url_spec}") local image_entry image_entry=$(debian_image_url_"${timestamped}" "${url_spec}") if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("debian") else declare -a SUPPORTED_DISTRIBUTIONS=("debian") fi # required functions for Debian function debian_cache_key_for_image_kernel() { debian_cache_key_for_image_kernel_overriding "$@"; } function debian_image_entry_for_image_kernel() { debian_image_entry_for_image_kernel_overriding "$@"; } return 0 fi declare -a templates=() declare overriding="{}" while [[ $# -gt 0 ]]; do case "$1" in -h | --help) debian_print_help exit 0 ;; -d | --debug) set -x ;; --backports | --daily | --timestamped) overriding=$(json "${1#--}" true <<<"${overriding}") ;; --backports=* | --daily=* | --timestamped=*) overriding=$( key=${1#--} value=$(validate_boolean "${1#*=}") json "${key%%=*}" "${value}" <<<"${overriding}" ) ;; --version) if [[ -n $2 && $2 != -* ]]; then overriding=$( version=$(debian_version_resolve_aliases "$2") json_vars version <<<"${overriding}" ) shift else error_exit "--version requires a value" fi ;; --version=*) overriding=$( version=$(debian_version_resolve_aliases "${1#*=}") json_vars version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then debian_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. debian_cache_key_for_image_kernel_overriding "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else debian_image_entry_for_image_kernel_overriding "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-fedora.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function fedora_print_help() { cat <|release|development[/]|rawhide)] ... Description: This script updates the Fedora Linux image location in the specified templates. Image location basename format: Fedora-Cloud-Base[-]--..qcow2 Fedora-Cloud-Base[-].--.qcow2 Published Fedora Linux image information is fetched from the following URL: ${fedora_image_list_url} The downloaded files will be cached in the Lima cache directory. Examples: Update the Fedora Linux image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Fedora Linux image location to version 41 in ~/.lima/fedora/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version 41 ~/.lima/fedora/lima.yaml $ limactl factory-reset fedora Flags: --version Use the specified version. The version must be , 'release', 'development[/]', or 'rawhide'. -h, --help Print this help message HELP } # print the URL spec for the given location function fedora_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture(" ^https://download\\.fedoraproject\\.org/pub/fedora/linux/(?(releases|development)/(\\d+|rawhide))/Cloud/(?[^/]+)/images/ Fedora-Cloud-Base(?-Generic)?( (-(?\\d+|Rawhide)-(?[^-]+)(?\\.[^.]+))| ((?\\.[^-]+)-(?\\d+|Rawhide)-(?[^-]+)) )\\.(?.*)$ ";"x") | .version = (.version_before_arch // .version_after_arch) | .build_info = (.build_info_before_arch // .build_info_after_arch ) | map_values(. // empty) # remove null values ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") echo "${url_spec}" } readonly fedora_jq_filter_directory='"https://download.fedoraproject.org/pub/fedora/linux/\(.path_version)/Cloud/\(.path_arch)/images/"' readonly fedora_jq_filter_filename=' "Fedora-Cloud-Base\(.target_vendor // "")\(.arch_prefix // "")-\(.version)-\(.build_info)\(.arch_postfix // "").\(.file_extension)" ' readonly fedora_jq_filter_checksum_filename=' "Fedora-Cloud-\( if .path_version|startswith("development/") then "images-\(.version)-\(.path_arch)-\(.build_info)" else "\(.version)-\(.build_info)-\(.path_arch)" end )-CHECKSUM" ' # print the location for the given URL spec function fedora_location_from_url_spec() { local -r url_spec=$1 jq -e -r "${fedora_jq_filter_directory} + ${fedora_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function fedora_image_directory_from_url_spec() { local -r url_spec=$1 jq -e -r "${fedora_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the image directory for ${url_spec}" } function fedora_image_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${fedora_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the image filename for ${url_spec}" } function fedora_image_checksum_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${fedora_jq_filter_checksum_filename}" <<<"${url_spec}" || error_exit "Failed to get the checksum filename for ${url_spec}" } readonly fedora_image_list_url='https://dl.fedoraproject.org/pub/fedora/imagelist-fedora' # function fedora_latest_image_entry_for_url_spec() { local url_spec=$1 arch image_list spec_for_query latest_version_info # shellcheck disable=SC2034 arch=$(jq -r '.path_arch' <<<"${url_spec}") image_list=$(download_to_cache "${fedora_image_list_url}") spec_for_query=$(jq -r '. | {path_version, path_arch, file_extension}' <<<"${url_spec}") latest_version_info=$(jq -e -Rrs --argjson spec "${spec_for_query}" ' [ split("\n").[] | capture(" ^\\./linux/(?\($spec.path_version))/Cloud/\($spec.path_arch)/images/ Fedora-Cloud-Base(?-Generic)?( -(?\\d+|Rawhide)-(?[^-]+)(?\\.\($spec.path_arch))| (?\\.\($spec.path_arch))-(?\\d+|Rawhide)-(?[^-]+) )\\.\($spec.file_extension)$ ";"x") | .version = (.version_before_arch // .version_after_arch) | .build_info = (.build_info_before_arch // .build_info_after_arch) | # do not remove null values. we need them for creating newer_url_spec # map_values(. // empty) | .version_number_array = ([(if (.version|test("\\d+")) then (.version|tonumber) else .version end)] + [.build_info | scan("\\d+") | tonumber]) ] | sort_by(.version_number_array) | last ' <"${image_list}" || error_exit "Failed to get the latest version info for ${spec_for_query}") [[ -n ${latest_version_info} ]] || return local newer_url_spec directory filename location sha512sum_location downloaded_sha256sum digest # prefer the v. in the path newer_url_spec=$(jq -e -r ". + ${latest_version_info}" <<<"${url_spec}") directory=$(fedora_image_directory_from_url_spec "${newer_url_spec}") filename=$(fedora_image_filename_from_url_spec "${newer_url_spec}") location="${directory}${filename}" # validate the location. use original url since the location may be redirected to some mirror location=$(validate_url_without_redirect "${location}") sha512sum_location="${directory}$(fedora_image_checksum_filename_from_url_spec "${newer_url_spec}")" # download the checksum file and get the sha256sum # cache original url since the checksum file may be redirected to some mirror downloaded_sha256sum=$(download_to_cache_without_redirect "${sha512sum_location}") digest="sha256:$(awk "/SHA256 \(${filename}\) =/{print \$4}" "${downloaded_sha256sum}")" [[ -n ${digest} ]] || error_exit "Failed to get the digest for ${filename}" json_vars location arch digest } function fedora_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(fedora_url_spec_from_location "${location}") jq -r '["fedora", .path_version, .target_vendor, .path_arch, .file_extension] | join(":")' <<<"${url_spec}" } function fedora_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-'{}'} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on Fedora Linux" >&2 url_spec=$(fedora_url_spec_from_location "${location}" | jq -r ". + ${overriding}") image_entry=$(fedora_latest_image_entry_for_url_spec "${url_spec}") # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("fedora") else declare -a SUPPORTED_DISTRIBUTIONS=("fedora") fi return 0 fi declare -a templates=() declare overriding='{}' while [[ $# -gt 0 ]]; do case "$1" in -h | --help) fedora_print_help exit 0 ;; -d | --debug) set -x ;; --version) if [[ -n ${2:-} && $2 != -* ]]; then version="$2" shift else error_exit "--version requires a value" fi ;& --version=*) version=${version:-${1#*=}} overriding=$( [[ ${version} =~ ^[0-9]+$ ]] && path_version="releases/${version}" [[ ${version} =~ ^releases?$ ]] && path_version="releases/\d+" [[ ${version} == "development" ]] && path_version="development/\d+" [[ ${version} =~ ^(releases|development)/([0-9]+)$ ]] && path_version="${version}" [[ ${version} =~ ^(development/)?rawhide$ ]] && path_version="development/rawhide" [[ -n ${path_version:-} ]] || error_exit "The version must be , 'release', 'development[/'], or 'rawhide'." json_vars path_version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then fedora_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. fedora_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else fedora_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-freebsd.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function freebsd_print_help() { cat <.] ... Description: This script updates the FreeBSD image location in the specified templates. Image location basename format: FreeBSD--RELEASE-[-BASIC-CLOUDINIT]-..xz Published FreeBSD image information is fetched from the following URL: ${freebsd_archive_url} The downloaded files will be cached in the Lima cache directory. Examples: Update the FreeBSD image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the FreeBSD image location to version 14.3 in ~/.lima/freebsd/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version 14.3 ~/.lima/freebsd/lima.yaml $ limactl factory-reset freebsd Flags: --version . Use the specified . version. -h, --help Print this help message HELP } # ftp-archive.freebsd.org doesn't seem to support HTTPS readonly freebsd_archive_url='http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/' # freebsd_url_spec_from_location prints the URL spec for the given location. # If the location is not supported, it returns 1. # e.g. # ```console # freebsd_url_spec_from_location http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/15.0-RELEASE/amd64/Latest/FreeBSD-15.0-RELEASE-amd64-BASIC-CLOUDINIT-zfs.raw.xz # {"version":"15.0","dir_arch":"amd64","filename_arch":"amd64","cloudinit":true,"fs":"zfs","format":"raw"} # freebsd_url_spec_from_location http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/15.0-RELEASE/aarch64/Latest/FreeBSD-15.0-RELEASE-arm64-aarch64-BASIC-CLOUDINIT-zfs.raw.xz # {"version":"15.0","dir_arch":"aarch64","filename_arch":"arm64-aarch64","cloudinit":true,"fs":"zfs","format":"raw"} # ``` function freebsd_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture(" ^http://ftp-archive\\.freebsd\\.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/ (?\\d+\\.\\d+)-RELEASE/(?[^/]+)/Latest/ FreeBSD-\\d+\\.\\d+-RELEASE-(?arm64-aarch64|riscv-riscv64|amd64) (?-BASIC-CLOUDINIT)?-(?zfs|ufs)\\.(?raw|qcow2)\\.xz$ ";"x") | .cloudinit = (.cloudinit != null) ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") echo "${url_spec}" } readonly freebsd_jq_filter_directory='"http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/\(.version)-RELEASE/\(.dir_arch)/Latest/"' readonly freebsd_jq_filter_filename=' "FreeBSD-\(.version)-RELEASE-\(.filename_arch)\(if .cloudinit then "-BASIC-CLOUDINIT" else "" end)-\(.fs).\(.format).xz" ' # freebsd_location_from_url_spec prints the location for the given URL spec. function freebsd_location_from_url_spec() { local url_spec=$1 jq -e -r "${freebsd_jq_filter_directory} + ${freebsd_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function freebsd_directory_from_url_spec() { local url_spec=$1 jq -e -r "${freebsd_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the directory for ${url_spec}" } function freebsd_filename_from_url_spec() { local url_spec=$1 jq -e -r "${freebsd_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the filename for ${url_spec}" } function freebsd_latest_image_entry_for_url_spec() { local url_spec=$1 releases_page latest_version newer_url_spec location filename checksum_url downloaded_checksum digest arch releases_page=$(download_to_cache "${freebsd_archive_url}") # Find the latest RELEASE version with the same major version as in url_spec latest_version=$(jq -e -Rrs --argjson spec "${url_spec}" ' [ split("\n").[] | select(test("href=\"\\d+\\.\\d+-RELEASE/\"")) | capture("href=\"(?\\d+\\.\\d+)-RELEASE/\"") | .ver | select((split(".")[0] | tonumber) == ($spec.version | split(".")[0] | tonumber)) ] | sort_by(split(".") | map(tonumber)) | last ' <"${releases_page}") || error_exit "No RELEASE found for FreeBSD $(jq -r '.version | split(".")[0]' <<<"${url_spec}").x in ${freebsd_archive_url}" [[ -n ${latest_version} ]] || return newer_url_spec=$(jq -e -r ". + {version: \"${latest_version}\"}" <<<"${url_spec}") filename=$(freebsd_filename_from_url_spec "${newer_url_spec}") checksum_url="$(freebsd_directory_from_url_spec "${newer_url_spec}")CHECKSUM.SHA256" downloaded_checksum=$(download_to_cache "${checksum_url}") digest="sha256:$(awk "/SHA256 \\(${filename}\\) =/{print \$4}" "${downloaded_checksum}")" [[ -n ${digest} ]] || error_exit "Failed to get the digest for ${filename}" location=$(freebsd_location_from_url_spec "${newer_url_spec}") location=$(validate_url "${location}") arch=$(jq -e -r '.dir_arch' <<<"${newer_url_spec}") arch=$(limayaml_arch "${arch}") json_vars location arch digest } function freebsd_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(freebsd_url_spec_from_location "${location}") jq -r '["freebsd", (.version | split(".")[0]), .dir_arch, (if .cloudinit then "cloudinit" else "" end), .fs, .format] | join(":")' <<<"${url_spec}" } function freebsd_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-'{}'} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on FreeBSD" >&2 url_spec=$(freebsd_url_spec_from_location "${location}" | jq -r ". + ${overriding}") image_entry=$(freebsd_latest_image_entry_for_url_spec "${url_spec}") # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("freebsd") else declare -a SUPPORTED_DISTRIBUTIONS=("freebsd") fi return 0 fi declare -a templates=() declare overriding='{}' while [[ $# -gt 0 ]]; do case "$1" in -h | --help) freebsd_print_help exit 0 ;; -d | --debug) set -x ;; --version) if [[ -n ${2:-} && $2 != -* ]]; then version="$2" shift else error_exit "--version requires a value" fi ;& --version=*) version=${version:-${1#*=}} overriding=$( [[ ${version} =~ ^[0-9]+\.[0-9]+$ ]] || error_exit "The version must be in the format ." json_vars version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then freebsd_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. freebsd_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else freebsd_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-macos.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function macos_print_help() { cat <... Description: This script updates the macOS image location in the specified templates. Image location format: https://updates.cdn-apple.com/.../UniversalMac___Restore.ipsw Published macOS image information (URL and SHA256 digest) is fetched from the ipsw.me API: https://api.ipsw.me/v4/device/VirtualMac2,1 The downloaded JSON will be cached in the Lima cache directory. Examples: Update the macOS image location in templates/_images/macos-*.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/_images/macos-*.yaml Flags: -h, --help Print this help message HELP } # URL of the ipsw.me device API endpoint for Apple Virtual Machine 1 (VirtualMac2,1). # This returns all available macOS IPSW firmwares with URL and SHA256 digest. readonly macos_ipsw_me_device_url="https://api.ipsw.me/v4/device/VirtualMac2,1" # macos_url_spec_from_location prints the URL spec for the given location. # If the location is not a macOS IPSW URL from Apple's CDN, it returns 1. # e.g. # ```console # macos_url_spec_from_location https://updates.cdn-apple.com/2025SummerFCS/fullrestores/082-08674/51294E4D-A273-44BE-A280-A69FC347FB87/UniversalMac_15.6_24G84_Restore.ipsw # {"version":"15.6","major_version":"15","build":"24G84"} # macos_url_spec_from_location https://updates.cdn-apple.com/2025SummerFCS/fullrestores/093-10809/CFD6DD38-DAF0-40DA-854F-31AAD1294C6F/UniversalMac_15.6.1_24G90_Restore.ipsw # {"version":"15.6.1","major_version":"15","build":"24G90"} # ``` function macos_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture(" ^https://updates\\.cdn-apple\\.com/[^/]+/fullrestores/[^/]+/[^/]+/ UniversalMac_(?(?\\d+)(?:\\.\\d+)+)_(?[^_]+)_Restore\\.ipsw$ ";"x") ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") echo "${url_spec}" } # macos_latest_image_entry_for_url_spec prints the latest image entry for the given URL spec. # e.g. # ```console # macos_latest_image_entry_for_url_spec '{"major_version":"15"}' # {"location":"https://updates.cdn-apple.com/.../UniversalMac_15.6.1_24G90_Restore.ipsw","arch":"aarch64","digest":"sha256:..."} # ``` function macos_latest_image_entry_for_url_spec() { local url_spec=$1 major_version ipsw_me_file latest_entry location digest arch="aarch64" major_version=$(jq -r '.major_version' <<<"${url_spec}") ipsw_me_file=$(download_to_cache "${macos_ipsw_me_device_url}") latest_entry=$(jq -e -r --arg major "${major_version}" ' .firmwares | [.[] | select(.version | test("^" + $major + "\\."))] | sort_by(.releasedate) | last ' "${ipsw_me_file}") [[ -n ${latest_entry} && ${latest_entry} != "null" ]] || error_exit "Failed to get the latest macOS ${major_version} image from ${macos_ipsw_me_device_url}" location=$(jq -r '.url' <<<"${latest_entry}") digest=$(jq -r '.sha256sum // "" | if . != "" then "sha256:" + . else "" end' <<<"${latest_entry}") json_vars location arch digest } function macos_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(macos_url_spec_from_location "${location}") jq -r '["macos", .major_version] | join(":")' <<<"${url_spec}" } function macos_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on macOS" >&2 url_spec=$(macos_url_spec_from_location "${location}") image_entry=$(macos_latest_image_entry_for_url_spec "${url_spec}") # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("macos") else declare -a SUPPORTED_DISTRIBUTIONS=("macos") fi return 0 fi declare -a templates=() while [[ $# -gt 0 ]]; do case "$1" in -h | --help) macos_print_help exit 0 ;; -d | --debug) set -x ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift done if [[ ${#templates[@]} -eq 0 ]]; then macos_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. macos_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else macos_image_entry_for_image_kernel "${location}" "${kernel_location}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-opensuse.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function opensuse_print_help() { cat <.|current|stable|tumbleweed)|--version-major --version-minor ] ... Description: This script updates the openSUSE Linux image location in the specified templates. Image location basename format: openSUSE-(Leap-|Tumbleweed)-Minimal-VM.-Cloud.qcow2 Published openSUSE Linux image information is fetched from the following URLs: Leap: .: https://download.opensuse.org/distribution/leap/./appliances/?jsontable current: https://download.opensuse.org/distribution/openSUSE-current/appliances/?jsontable stable: https://download.opensuse.org/distribution/openSUSE-stable/appliances/?jsontable Tumbleweed: x86_64: https://download.opensuse.org/tumbleweed/appliances/?jsontable not x86_64: https://download.opensuse.org/ports//tumbleweed/appliances/?jsontable The downloaded files will be cached in the Lima cache directory. Examples: Update the openSUSE Linux image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the openSUSE Linux image location to version 15.6 in ~/.lima/opensuse/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version-major-minor 15.6 ~/.lima/opensuse/lima.yaml $ limactl factory-reset opensuse Update the openSUSE Linux image location to tumbleweed in ~/.lima/opensuse/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version-major-minor tumbleweed ~/.lima/opensuse/lima.yaml $ limactl factory-reset opensuse Flags: --version-major-minor (.|current|stable|tumbleweed) Use the specified . version or aliases "current", "stable", or "tumbleweed". The . version must be 15.0 or later. --version-major --version-minor Use the specified and version. -h, --help Print this help message HELP } # print the URL spec for the given location function opensuse_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture(" ^https://download\\.opensuse\\.org/(?: distribution/(?: leap/(?\\d+\\.\\d+)| openSUSE-(?current|stable) )| (?:ports/aarch64/)?(?tumbleweed) )/appliances/ openSUSE-(?Leap-\\d+\\.\\d+|Tumbleweed)-Minimal-VM \\.(?[^-]+)(?-\\d+\\.\\d+\\.\\d+)?-(?.*)(?-Build\\d+\\.\\d+)?\\.(?.*)$ ";"x") | .path_version = (.path_version_leap // .path_version_leap_alias // .path_version_tumbleweed) ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") echo "${url_spec}" } readonly opensuse_jq_filter_directory='"https://download.opensuse.org/\( if .path_version == "tumbleweed" then if .arch != "x86_64" then "ports/\(.arch)/" else "" end + "tumbleweed" else "distribution/" + if .path_version == "current" or .path_version == "stable" then "openSUSE-\(.path_version)" else "leap/\(.path_version)" end end )/appliances/"' readonly opensuse_jq_filter_filename=' "openSUSE-\( if .version == "tumbleweed" then "Tumbleweed" else "Leap-\(.version)" end )-Minimal-VM.\(.arch)\( if .major_minor_patch then .major_minor_patch else "" end )-\(.target_vendor)\( if .build_info then .build_info else "" end ).\(.file_extension)" ' # print the location for the given URL spec function opensuse_location_from_url_spec() { local -r url_spec=$1 jq -e -r "${opensuse_jq_filter_directory} + ${opensuse_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function opensuse_image_directory_from_url_spec() { local -r url_spec=$1 jq -e -r "${opensuse_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the image directory for ${url_spec}" } function opensuse_image_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${opensuse_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the image filename for ${url_spec}" } function opensuse_json_url_from_url_spec() { local -r url_spec=$1 local json_url json_url="$(opensuse_image_directory_from_url_spec "${url_spec}")?jsontable" echo "${json_url}" } # function opensuse_latest_image_entry_for_url_spec() { local url_spec=$1 arch json_url downloaded_json latest_version_info # shellcheck disable=SC2034 arch=$(jq -r '.arch' <<<"${url_spec}") json_url="$(opensuse_image_directory_from_url_spec "${url_spec}")?jsontable" downloaded_json=$(download_to_cache "${json_url}") latest_version_info=$(jq -e -r --argjson spec "${url_spec}" ' [ .data |sort_by(.mtime)|.[].name| if $spec.major_minor_patch then capture( "^openSUSE-(?:Leap-(?\\d+\\.\\d+)|(?Tumbleweed))-Minimal-VM \\.\($spec.arch)(?-\\d+\\.\\d+\\.\\d+)?-\($spec.target_vendor)(?-Build\\d+\\.\\d+)?\\.\($spec.file_extension)$" ;"x" ) else capture( "^openSUSE-(?:Leap-(?\\d+\\.\\d+)|(?Tumbleweed))-Minimal-VM \\.\($spec.arch)-\($spec.target_vendor)\\.\($spec.file_extension)$" ;"x" ) end | .version = (.version_leap // (.version_tumbleweed|ascii_downcase)) | .version_number_array = ([.version | scan("\\d+") | tonumber]) ] | sort_by(.version_number_array, .image_revision) | last ' <"${downloaded_json}") [[ -n ${latest_version_info} ]] || return local newer_url_spec location # prefer the . in the path newer_url_spec=$(jq -e -r ". + ${latest_version_info} | .path_version = .version" <<<"${url_spec}") location=$(opensuse_location_from_url_spec "${newer_url_spec}") location=$(validate_url_without_redirect "${location}") # Digest is not used here because URLs containing dates are not retained long-term. # Instead, URLs without dates must be used, and their content is often updated without a URL change, # resulting in only the digest being updated. Therefore, recording the digest is not meaningful. # # local sha256sum_location downloaded_sha256sum filename digest # sha256sum_location="${location}.sha256" # downloaded_sha256sum=$(download_to_cache "${sha256sum_location}") # filename=$(opensuse_image_filename_from_url_spec "${newer_url_spec}") # digest="sha256:$(awk '{print $1}' <"${downloaded_sha256sum}")" # [[ -n ${digest} ]] || error_exit "Failed to get the digest for ${filename}" json_vars location arch # digest } function opensuse_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(opensuse_url_spec_from_location "${location}") jq -r '["opensuse", .path_version, .target_vendor, .arch, .file_extension] | join(":")' <<<"${url_spec}" } function opensuse_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 url_spec path_version overriding image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on openSUSE Linux" >&2 url_spec=$(opensuse_url_spec_from_location "${location}") path_version=$(jq -r '.path_version' <<<"${url_spec}") if [[ ${path_version} == "tumbleweed" ]]; then overriding=${3:-'{"path_version":"tumbleweed"}'} else overriding=${3:-'{"path_version":"stable"}'} fi url_spec=$(jq -r '. + '"${overriding}" <<<"${url_spec}") image_entry=$(opensuse_latest_image_entry_for_url_spec "${url_spec}") # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("opensuse") else declare -a SUPPORTED_DISTRIBUTIONS=("opensuse") fi return 0 fi declare -a templates=() declare overriding='{}' declare version_major='' version_minor='' while [[ $# -gt 0 ]]; do case "$1" in -h | --help) opensuse_print_help exit 0 ;; -d | --debug) set -x ;; --version-major-minor) if [[ -n ${2:-} && $2 != -* ]]; then version="$2" shift else error_exit "--version-major-minor requires a value" fi ;& --version-major-minor=*) version=${version:-${1#*=}} overriding=$( version="${version#v}" if [[ ${version} =~ ^v?[0-9]+.[0-9]+ ]]; then version="$(echo "${version}" | cut -d. -f1-2)" [[ ${version%%.*} -ge 15 ]] || error_exit "openSUSE Linux version must be 15.0 or later" path_version="${version}" elif [[ ${version} == "current" || ${version} == "stable" || ${version} == "tumbleweed" ]]; then path_version=${version} else error_exit "--version-major-minor requires a value in the format ., current, stable, or tumbleweed" fi json_vars path_version <<<"${overriding}" ) ;; --version-major) if [[ -n ${2:-} && $2 != -* ]]; then version_major="$2" shift else error_exit "--version-major requires a value" fi ;& --version-major=*) version_major=${version_major:-${1#*=}} [[ ${version_major} =~ ^[0-9]+$ ]] || error_exit "Please specify --version-major in numbers" ;; --version-minor) if [[ -n ${2:-} && $2 != -* ]]; then version_minor="$2" shift else error_exit "--version-minor requires a value" fi ;& --version-minor=*) version_minor=${version_minor:-${1#*=}} [[ ${version_minor} =~ ^[0-9]+$ ]] || error_exit "Please specify --version-minor in numbers" ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if ! jq -e '.path_version' <<<"${overriding}" >/dev/null; then # --version-major-minor is not specified if [[ -n ${version_major} && -n ${version_minor} ]]; then [[ ${version_major} -ge 15 ]] || error_exit "openSUSE Linux version must be 15.0 or later" # shellcheck disable=2034 path_version="${version_major}.${version_minor}" overriding=$(json_vars path_version <<<"${overriding}") elif [[ -n ${version_major} ]]; then error_exit "--version-minor is required when --version-major is specified" elif [[ -n ${version_minor} ]]; then error_exit "--version-major is required when --version-minor is specified" fi elif [[ -n ${version_major} || -n ${version_minor} ]]; then # --version-major-minor is specified echo "Ignoring --version-major and --version-minor because --version-major-minor is specified" >&2 fi [[ ${overriding} == "{}" ]] && overriding='' if [[ ${#templates[@]} -eq 0 ]]; then opensuse_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. opensuse_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else opensuse_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-oraclelinux.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function oraclelinux_print_help() { cat <] ... Description: This script updates the Oracle Linux image location in the specified templates. Image location basename format: OLU_-kvm[-cloud]-b.qcow2 Published Oracle Linux image information is fetched from the following URLs: OL8: x86_64: https://yum.oracle.com/templates/OracleLinux/ol8-template.json aarch64: https://yum.oracle.com/templates/OracleLinux/ol8_aarch64-cloud-template.json OL9: x86_64: https://yum.oracle.com/templates/OracleLinux/ol9-template.json aarch64: https://yum.oracle.com/templates/OracleLinux/ol9_aarch64-cloud-template.json OL10: x86_64: https://yum.oracle.com/templates/OracleLinux/ol10-template.json aarch64: https://yum.oracle.com/templates/OracleLinux/ol10_aarch64-cloud-template.json The downloaded files will be cached in the Lima cache directory. Examples: Update the Oracle Linux image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Oracle Linux image location to major version 9 in ~/.lima/oraclelinux/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version-major 9 ~/.lima/oraclelinux/lima.yaml $ limactl factory-reset oraclelinux Flags: --version-major Use the specified Oracle Linux . The major version must be 7+ for x86_64 or 8+ for aarch64. -h, --help Print this help message HELP } # print the URL spec for the given location function oraclelinux_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture(" ^https://yum\\.oracle\\.com/templates/OracleLinux/OL(?\\d+)/u(?\\d+)/(?[^/]+)/ OL(?\\d+)U(?\\d+)_(?[^-]+)-(?[^-]+)(?-cloud)?-b(?\\d+)\\.(?.*)$ ";"x") ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") echo "${url_spec}" } readonly oraclelinux_jq_filter_json_url=' "https://yum.oracle.com/templates/OracleLinux/" + "ol\(.path_major_version)\(if .path_arch != "x86_64" then "_" + .path_arch else "" end)\(.cloud // "")-template.json" ' function oraclelinux_json_url_from_url_spec() { local -r url_spec=$1 jq -e -r "${oraclelinux_jq_filter_json_url}" <<<"${url_spec}" || error_exit "Failed to get the JSON url for ${url_spec}" } function oraclelinux_latest_image_entry_for_url_spec() { local url_spec=$1 arch json_url downloaded_json latest_version_info # shellcheck disable=SC2034 arch=$(jq -r '.arch' <<<"${url_spec}") json_url=$(oraclelinux_json_url_from_url_spec "${url_spec}") downloaded_json=$(download_to_cache "${json_url}") latest_version_info="$(jq -e -r --argjson spec "${url_spec}" '{ location: ("https://yum.oracle.com" + .base_url + "/" + .[$spec.type].image), sha256: ("sha256:" + .[$spec.type].sha256) }' <"${downloaded_json}")" [[ -n ${latest_version_info} ]] || return local location digest # prefer the v. in the path location=$(jq -e -r '.location' <<<"${latest_version_info}") location=$(validate_url_without_redirect "${location}") # shellcheck disable=SC2034 digest=$(jq -e -r '.sha256' <<<"${latest_version_info}") json_vars location arch digest } function oraclelinux_cache_key_for_image_kernel() { local location=$1 overriding=${3:-"{}"} url_spec url_spec=$(oraclelinux_url_spec_from_location "${location}" | jq -r ". + ${overriding}") jq -r '["oraclelinux", .path_major_version, .type, .cloud // empty, .arch, .file_extension] | join(":")' <<<"${url_spec}" } function oraclelinux_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-"{}"} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on Oracle Linux" >&2 url_spec=$(oraclelinux_url_spec_from_location "${location}" | jq -r ". + ${overriding}") image_entry=$(oraclelinux_latest_image_entry_for_url_spec "${url_spec}") # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("oraclelinux") else declare -a SUPPORTED_DISTRIBUTIONS=("oraclelinux") fi return 0 fi declare -a templates=() declare overriding='{}' while [[ $# -gt 0 ]]; do case "$1" in -h | --help) oraclelinux_print_help exit 0 ;; -d | --debug) set -x ;; --version-major) if [[ -n $2 && $2 != -* ]]; then overriding=$( path_major_version="${2}" [[ ${path_major_version} =~ ^[0-9]+$ ]] || error_exit "Oracle Linux major version must be a number" [[ ${path_major_version} -eq 7 ]] && echo 'Oracle Linux major version 7 exists only for x86_64. It may fail for aarch64.' >&2 [[ ${path_major_version} -gt 7 ]] || error_exit "Oracle Linux major version must be 7+ for x86_64 or 8+ for aarch64" json_vars path_major_version <<<"${overriding}" ) shift else error_exit "--version-major requires a value" fi ;; --version-major=*) overriding=$( path_major_version="${1#*=}" [[ ${path_major_version} =~ ^[0-9]+$ ]] || error_exit "Oracle Linux major version must be a number" [[ ${path_major_version} -eq 7 ]] && echo 'Oracle Linux major version 7 exists only for x86_64. It may fail for aarch64.' >&2 [[ ${path_major_version} -gt 7 ]] || error_exit "Oracle Linux major version must be 7+ for x86_64 or 8+ for aarch64" json_vars path_major_version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then oraclelinux_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. oraclelinux_cache_key_for_image_kernel "${location}" "${kernel_location}" "${overriding}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else oraclelinux_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-rocky.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function rocky_print_help() { cat <] ... Description: This script updates the Rocky Linux image location in the specified templates. If the image location in the template contains a minor version, release date, and job id in the URL, the script replaces it with the latest available minor version, release date, and job id. Image location basename format: Rocky--GenericCloud[.latest|-.latest|-.-.]..qcow2 Published Rocky Linux image information is fetched from the following URLs: https://dl.rockylinux.org/pub/rocky//images// To parsing html, this script requires 'htmlq' or 'pup' command. The downloaded files will be cached in the Lima cache directory. Examples: Update the Rocky Linux image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Rocky Linux image location in ~/.lima/rocky/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/rocky/lima.yaml $ limactl factory-reset rocky Update the Rocky Linux image location to major version 9 in ~/.lima/rocky/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --version-major 9 ~/.lima/rocky/lima.yaml $ limactl factory-reset rocky Flags: --version-major Use the specified version. The version must be 8 or later. -h, --help Print this help message HELP } # print the URL spec for the given location function rocky_url_spec_from_location() { local location=$1 jq_filter url_spec jq_filter='capture(" ^https://dl\\.rockylinux\\.org/pub/rocky/(?\\d+(\\.\\d+)?)/images/(?[^/]+)/ Rocky-(?\\d+)-(?.*) ( -(?.*)-(?\\d+\\.\\d+)-(?\\d{8}\\.\\d+)| -(?.*)\\.latest| \\.latest )\\.(?[^.]+).(?.*)$ ";"x") ' url_spec=$(jq -e -r "${jq_filter}" <<<"\"${location}\"") jq -e '.path_arch == .arch' <<<"${url_spec}" >/dev/null || error_exit "Validation failed: .path_arch != .arch: ${location}" echo "${url_spec}" } readonly rocky_jq_filter_directory='"https://dl.rockylinux.org/pub/rocky/\(.path_version)/images/\(.path_arch)/"' readonly rocky_jq_filter_filename=' "Rocky-\(.major_version)-\(.target_vendor)\( if .date_and_ci_job_id then "-\(.type)-\(.major_minor_version)-\(.date_and_ci_job_id)" else if .type then "-\(.type_latest).latest" else ".latest" end end ).\(.arch).\(.file_extension)" ' # print the location for the given URL spec function rocky_location_from_url_spec() { local -r url_spec=$1 jq -e -r "${rocky_jq_filter_directory} + ${rocky_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the location for ${url_spec}" } function rocky_image_directory_from_url_spec() { local -r url_spec=$1 jq -e -r "${rocky_jq_filter_directory}" <<<"${url_spec}" || error_exit "Failed to get the image directory for ${url_spec}" } function rocky_image_filename_from_url_spec() { local -r url_spec=$1 jq -e -r "${rocky_jq_filter_filename}" <<<"${url_spec}" || error_exit "Failed to get the image filename for ${url_spec}" } # function rocky_latest_image_entry_for_url_spec() { local url_spec=$1 arch major_version_url_spec major_version_image_directory downloaded_page links_in_page latest_minor_version_info arch=$(jq -r '.arch' <<<"${url_spec}") # to detect minor version updates, we need to get the major version URL major_version_url_spec=$(jq -e -r '.path_version = .major_version' <<<"${url_spec}") major_version_image_directory=$(rocky_image_directory_from_url_spec "${major_version_url_spec}") downloaded_page=$(download_to_cache "${major_version_image_directory}") if command -v htmlq >/dev/null; then links_in_page=$(htmlq 'pre a' --attribute href <"${downloaded_page}") elif command -v pup >/dev/null; then links_in_page=$(pup 'pre a attr{href}' <"${downloaded_page}") else error_exit "Please install 'htmlq' or 'pup' to list images from ${major_version_image_directory}" fi latest_minor_version_info=$(jq -e -Rrs --argjson spec "${url_spec}" ' [ split("\n").[] | capture( "^Rocky-\($spec.major_version)-\($spec.target_vendor)-\($spec.type)" + "-(?\($spec.major_version)\\.\\d+)" + "-(?\\d{8}\\.\\d+)\\.\($spec.arch)\\.\($spec.file_extension)$" ;"x" ) | .version_number_array = ([.major_minor_version | scan("\\d+") | tonumber]) ] | sort_by(.version_number_array, .date_and_ci_job_id) | last ' <<<"${links_in_page}") [[ -n ${latest_minor_version_info} ]] || return local newer_url_spec location sha256sum_location downloaded_sha256sum filename digest # prefer the major_minor_version in the path newer_url_spec=$(jq -e -r ". + ${latest_minor_version_info} | .path_version = .major_minor_version" <<<"${url_spec}") location=$(rocky_location_from_url_spec "${newer_url_spec}") sha256sum_location="${location}.CHECKSUM" downloaded_sha256sum=$(download_to_cache "${sha256sum_location}") filename=$(rocky_image_filename_from_url_spec "${newer_url_spec}") digest="sha256:$(awk "/SHA256 \(${filename}\) =/{print \$4}" "${downloaded_sha256sum}")" [[ -n ${digest} ]] || error_exit "Failed to get the SHA256 digest for ${filename}" json_vars location arch digest } function rocky_cache_key_for_image_kernel() { local location=$1 url_spec url_spec=$(rocky_url_spec_from_location "${location}") jq -r '["rocky", .major_minor_version // .major_version, .target_vendor, if .date_and_ci_job_id then "timestamped" else "latest" end, .arch, .file_extension] | join(":")' <<<"${url_spec}" } function rocky_image_entry_for_image_kernel() { local location=$1 kernel_is_not_supported=$2 overriding=${3:-"{}"} url_spec image_entry='' [[ ${kernel_is_not_supported} == "null" ]] || echo "Updating kernel information is not supported on Rocky Linux" >&2 url_spec=$(rocky_url_spec_from_location "${location}" | jq -r ". + ${overriding}") if jq -e '.date_and_ci_job_id' <<<"${url_spec}" >/dev/null; then image_entry=$(rocky_latest_image_entry_for_url_spec "${url_spec}") else image_entry=$( # shellcheck disable=SC2030 location=$(rocky_location_from_url_spec "${url_spec}") location=$(validate_url_without_redirect "${location}") arch=$(jq -r '.path_arch' <<<"${url_spec}") json_vars location arch ) fi # shellcheck disable=SC2031 if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then error_exit "Please install 'htmlq' or 'pup' to list images from https://dl.rockylinux.org/pub/rocky//images//" fi # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then echo "Please install 'htmlq' or 'pup' to list images from https://dl.rockylinux.org/pub/rocky//images//" >&2 elif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("rocky") else declare -a SUPPORTED_DISTRIBUTIONS=("rocky") fi return 0 fi declare -a templates=() declare overriding="{}" while [[ $# -gt 0 ]]; do case "$1" in -h | --help) rocky_print_help exit 0 ;; -d | --debug) set -x ;; --version-major) if [[ -n $2 && $2 != -* ]]; then overriding=$( major_version="${2%%.*}" [[ ${major_version} -ge 8 ]] || error_exit "Rocky Linux major version must be 8 or later" # shellcheck disable=2034 path_version="${major_version}" json_vars path_version major_version <<<"${overriding}" ) shift else error_exit "--version-major requires a value" fi ;; --version-major=*) overriding=$( major_version="${1#*=}" major_version="${major_version%%.*}" [[ ${major_version} -ge 8 ]] || error_exit "Rocky Linux major version must be 8 or later" # shellcheck disable=2034 path_version="${major_version}" json_vars path_version major_version <<<"${overriding}" ) ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift [[ -z ${overriding} ]] && overriding="{}" done if [[ ${#templates[@]} -eq 0 ]]; then rocky_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. rocky_cache_key_for_image_kernel "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else rocky_image_entry_for_image_kernel "${location}" "${kernel_location}" "${overriding}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template-ubuntu.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function ubuntu_print_help() { cat <|--minimal|--server] [--version ] ... Description: This script updates the Ubuntu image location in the specified templates. If the image location in the template contains a release date in the URL, the script replaces it with the latest available date. If no flags are specified, the script uses the flavor and version from the image location basename in the template. Image location basename format: ubuntu---cloudimg-.img Released Ubuntu image information is fetched from the following URLs: Server: https://cloud-images.ubuntu.com/releases/stream/v1/com.ubuntu.cloud:released:download.json Minimal: https://cloud-images.ubuntu.com/minimal/releases/stream/v1/com.ubuntu.cloud:released:download.json The downloaded JSON file will be cached in the Lima cache directory. Examples: Update the Ubuntu image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Ubuntu image location in ~/.lima/ubuntu/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/ubuntu/lima.yaml $ limactl factory-reset ubuntu Update the Ubuntu image location to ubuntu-24.04-minimal-cloudimg-.img in ~/.lima/docker/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") --minimal --version 24.04 ~/.lima/docker/lima.yaml $ limactl factory-reset docker Flags: --flavor Use the specified flavor image --server Shortcut for --flavor server --minimal Shortcut for --flavor minimal --version Use the specified version The version can be an alias: latest, latest_lts, or lts. -h, --help Print this help message HELP } readonly -A ubuntu_base_urls=( ["minimal"]=https://cloud-images.ubuntu.com/minimal/releases/ ["server"]=https://cloud-images.ubuntu.com/releases/ ["daily"]=https://cloud-images.ubuntu.com/daily/ ["daily:minimal"]=https://cloud-images.ubuntu.com/daily/server/minimal/daily/ ) # ubuntu_base_url prints the base URL for the given flavor. # e.g. # ```console # ubuntu_base_url minimal # https://cloud-images.ubuntu.com/minimal/releases/ # ``` function ubuntu_base_url() { [[ -v ubuntu_base_urls[$1] ]] || error_exit "Unsupported flavor: $1" echo "${ubuntu_base_urls[$1]}" } # ubuntu_downloaded_json downloads the JSON file for the given flavor and prints the path. # e.g. # ```console # ubuntu_downloaded_json server # /Users/user/Library/Caches/lima/download/by-url-sha256/255f982f5bbda07f5377369093e21c506d7240f5ba901479bdadfa205ddafb01/data # ``` function ubuntu_downloaded_json() { local flavor=$1 base_url json_url case "${flavor}" in daily*) json_url=$(ubuntu_base_url "${flavor}")streams/v1/com.ubuntu.cloud:daily:download.json ;; *) json_url=$(ubuntu_base_url "${flavor}")streams/v1/com.ubuntu.cloud:released:download.json ;; esac download_to_cache "${json_url}" } # ubuntu_image_url_try_replace_release_with_version tries to replace the release with the version in the URL. # If the URL is valid, it prints the URL with the version. function ubuntu_image_url_try_replace_release_with_version() { local location=$1 release=$2 version=$3 location_using_version set +e # Disable 'set -e' to avoid exiting on error for the next assignment. location_using_version=$( set -e validate_url "${location/\/${release}\//\/${version}\/}" 2>/dev/null ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 if [[ $? -eq 0 ]]; then echo "${location_using_version}" else echo "${location}" fi set -e } # ubuntu_image_url_latest prints the latest image URL and its digest for the given flavor, version, arch, and path suffix. function ubuntu_image_url_latest() { local flavor=$1 version=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json jq_filter location_digest_release base_url=$(ubuntu_base_url "${flavor}") ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}") jq_filter=" [ .products[\"com.ubuntu.cloud:${flavor}:${version}:${arch}\"] | .release as \$release | .versions[]?.items[] | select(.path | endswith(\"${path_suffix}\")) | [\"${base_url}\"+.path, \"sha256:\"+.sha256, \$release] | @tsv ] | last " location_digest_release=$(jq -r "${jq_filter}" "${ubuntu_downloaded_json}") [[ ${location_digest_release} != "null" ]] || error_exit "The URL for ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}." local location digest release location_using_version read -r location digest release <<<"${location_digest_release}" location=$(validate_url "${location}") location=$(ubuntu_image_url_try_replace_release_with_version "${location}" "${release}" "${version}") arch=$(limayaml_arch "${arch}") json_vars location arch digest } # ubuntu_image_url_release prints the release image URL for the given flavor, version, arch, and path suffix. function ubuntu_image_url_release() { local flavor=$1 version=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json base_url=$(ubuntu_base_url "${flavor}") ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}") local jq_filter release location jq_filter=" [ .products | to_entries[] as \$product_entry | \$product_entry.value| select(.version == \"${version}\") | .release ] | first " release=$(jq -r "${jq_filter}" "${ubuntu_downloaded_json}") [[ ${release} != "null" ]] || error_exit "The URL for ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}." location=$(validate_url "${base_url}${release}/release/ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix}") location=$(ubuntu_image_url_try_replace_release_with_version "${location}" "${release}" "${version}") arch=$(limayaml_arch "${arch}") json_vars location arch } # ubuntu_image_url_daily prints the daily image URL and its digest for the given flavor, codename, arch, and path suffix. function ubuntu_image_url_daily() { local flavor=$1 codename=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json products_flavor jq_filter location_digest_version base_url=$(ubuntu_base_url "${flavor}") ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}") case "${flavor}" in daily:minimal) products_flavor="minimal" ;; *) products_flavor="server" ;; esac jq_filter=" [ .products | to_entries[] as \$product_entry | \$product_entry.value| select(.release == \"${codename}\" and .arch == \"${arch}\") | .version as \$version | .versions[]?.items[] | select(.path | endswith(\"${path_suffix}\")) | [\"${base_url}\"+.path, \"sha256:\"+.sha256, \$version] | @tsv ] | last " location_digest_version=$(jq -r "${jq_filter}" "${ubuntu_downloaded_json}") [[ ${location_digest_version} != "null" ]] || error_exit "The URL for ${codename}-${products_flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}." local location digest version location_using_version read -r location digest version <<<"${location_digest_version}" location=$(validate_url "${location}") location=$(ubuntu_image_url_try_replace_release_with_version "${location}" "${codename}" "${version}") arch=$(limayaml_arch "${arch}") json_vars location arch digest } # ubuntu_image_url_current prints the daily current image URL for the given flavor, codename, arch, and path suffix. function ubuntu_image_url_current() { local flavor=$1 codename=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json path_prefix products_flavor base_url=$(ubuntu_base_url "${flavor}") ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}") case "${flavor}" in daily:minimal) path_prefix="" products_flavor="minimal" ;; *) path_prefix="server/" products_flavor="server" ;; esac local jq_filter version location jq_filter=" [ .products | to_entries[] as \$product_entry | \$product_entry.value| select(.release == \"${codename}\" and .arch == \"${arch}\") | .version ] | first " version=$(jq -r "${jq_filter}" "${ubuntu_downloaded_json}") [[ ${version} != "null" ]] || error_exit "The URL for ubuntu-${version}-${products_flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}." location=$(validate_url "${base_url}${path_prefix}${codename}/current/${codename}-${products_flavor}-cloudimg-${arch}${path_suffix}") location=$(ubuntu_image_url_try_replace_release_with_version "${location}" "${codename}" "${version}") arch=$(limayaml_arch "${arch}") json_vars location arch } function ubuntu_file_info() { local location=$1 location_dirname sha256sums location_basename digest location=$(validate_url "${location}") location_dirname=$(dirname "${location}") sha256sums=$(download_to_cache "${location_dirname}/SHA256SUMS") location_basename=$(basename "${location}") # shellcheck disable=SC2034 digest=${location+$(awk "/${location_basename}/{print \"sha256:\"\$1}" "${sha256sums}")} json_vars location digest } # ubuntu_image_entry_with_kernel_info prints image entry with kernel and initrd info. # $1: image_entry # e.g. # ```console # ubuntu_image_entry_with_kernel_info '{"location":"https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img"}' # {"location":"https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img","kernel":{"location":"https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-vmlinuz-generic","digest":"sha256:..."}} # ``` # shellcheck disable=SC2034 function ubuntu_image_entry_with_kernel_info() { local image_entry=$1 location location=$(jq -e -r '.location' <<<"${image_entry}") local location_dirname location_basename location_prefix location_dirname=$(dirname "${location}")/unpacked location_basename=$(basename "${location}") case "${location_basename}" in ubuntu*) location_prefix="${location_dirname}/$(echo "${location_basename}" | cut -d- -f1-5 | cut -d. -f1-2)" ;; *) location_prefix="${location_dirname}/$(echo "${location_basename}" | cut -d- -f1-4 | cut -d. -f1)" ;; esac local kernel initrd set +e # Disable 'set -e' to avoid exiting on error for the next assignment. kernel=$( set -e ubuntu_file_info "${location_prefix}-vmlinuz-generic" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || error_exit "kernel image not found at ${location_prefix}-vmlinuz-generic" initrd=$( set -e ubuntu_file_info "${location_prefix}-initrd-generic" 2>/dev/null ) # may not exist set -e json_vars kernel initrd <<<"${image_entry}" } function ubuntu_flavor_from_location_basename() { local location=$1 location_basename flavor location_basename=$(basename "${location}") case "${location_basename}" in ubuntu*) flavor=$(echo "${location_basename}" | cut -d- -f3) ;; *) flavor=$(echo "${location_basename}" | cut -d- -f2) case "${flavor}" in minimal) flavor="daily:minimal" ;; *) flavor="daily" ;; esac ;; esac [[ -n ${flavor} ]] || error_exit "Failed to get flavor from ${location}" echo "${flavor}" } # ubuntu_version_from_location_basename prints the version from the location basename. # On daily images, it prints the codename. function ubuntu_version_from_location_basename() { local location=$1 location_basename version location_basename=$(basename "${location}") case "${location_basename}" in ubuntu*) version=$(echo "${location_basename}" | cut -d- -f2) ;; *) version=$(echo "${location_basename}" | cut -d- -f1) ;; esac [[ -n ${version} ]] || error_exit "Failed to get version from ${location}" echo "${version}" } # ubuntu_version_latest_lts prints the latest LTS version for the given flavor. # e.g. # ```console # ubuntu_version_latest_lts minimal # 24.04 # ``` function ubuntu_version_latest_lts() { local flavor=${1:-server} ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}") jq -e -r '[.products[]|.version|select(endswith(".04"))]|last // empty' "${ubuntu_downloaded_json}" } # ubuntu_version_latest prints the latest version for the given flavor. # e.g. # ```console # ubuntu_version_latest minimal # 24.10 # ``` function ubuntu_version_latest() { local flavor=${1:-server} ubuntu_downloaded_json=$(ubuntu_downloaded_json "${flavor}") jq -e -r '[.products[]|.version]|last // empty' "${ubuntu_downloaded_json}" } # ubuntu_version_resolve_aliases resolves the version aliases. # e.g. # ```console # ubuntu_version_resolve_aliases https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img minimal latest # 24.10 # ubuntu_version_resolve_aliases https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img minimal latest_lts # 24.04 # ubuntu_version_resolve_aliases https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img # # ``` function ubuntu_version_resolve_aliases() { local location=$1 flavor version flavor=${2:-$(ubuntu_flavor_from_location_basename "${location}")} version=${3:-} case "${version}" in latest_lts | lts) ubuntu_version_latest_lts "${flavor}" ;; latest) ubuntu_version_latest "${flavor}" ;; *) echo "${version}" ;; esac } function ubuntu_arch_from_location_basename() { local location=$1 location_basename arch location_basename=$(basename "${location}") case "${location_basename}" in ubuntu*) arch=$(echo "${location_basename}" | cut -d- -f5 | cut -d. -f1) ;; *) arch=$(echo "${location_basename}" | cut -d- -f4 | cut -d. -f1) ;; esac [[ -n ${arch} ]] || error_exit "Failed to get arch from ${location}" echo "${arch}" } function ubuntu_path_suffix_from_location_basename() { local location=$1 arch path_suffix arch=$(ubuntu_arch_from_location_basename "${location}") path_suffix="${location##*"${arch}"}" [[ -n ${path_suffix} ]] || error_exit "Failed to get path suffix from ${location}" echo "${path_suffix}" } # ubuntu_location_url_spec prints the URL spec for the given location. # If the location is not supported, it returns 1. # e.g. # ```console # ubuntu_location_url_spec https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img # latest # ubuntu_location_url_spec https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img # release # ``` function ubuntu_location_url_spec() { local location=$1 url_spec case "${location}" in https://cloud-images.ubuntu.com/minimal/releases/*/release/*) url_spec=release ;; https://cloud-images.ubuntu.com/minimal/releases/*/release-*/*) url_spec=latest ;; https://cloud-images.ubuntu.com/releases/*/release/*) url_spec=release ;; https://cloud-images.ubuntu.com/releases/*/release-*/*) url_spec=latest ;; https://cloud-images.ubuntu.com/daily/server/minimal/daily/*/current/*) url_spec=current ;; https://cloud-images.ubuntu.com/daily/server/minimal/daily/*/*/*) url_spec=daily ;; https://cloud-images.ubuntu.com/daily/server/*/current/*) url_spec=current ;; https://cloud-images.ubuntu.com/daily/server/*/*/*) url_spec=daily ;; *) # echo "Unsupported image location: ${location}" >&2 return 1 ;; esac echo "${url_spec}" } # ubuntu_cache_key_for_image_kernel_flavor_version prints the cache key for the given location, kernel_location, flavor, and version. # If the image location is not supported, it returns 1. # kernel_location is not validated. # e.g. # ```console # ubuntu_cache_key_for_image_kernel_flavor_version https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img # ubuntu_latest_24.04-minimal-amd64-release-.img # ubuntu_cache_key_for_image_kernel_flavor_version https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img https://... # ubuntu_latest_with_kernel_24.04-minimal-amd64-release-.img # ubuntu_cache_key_for_image_kernel_flavor_version https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img null # ubuntu_release_24.04-server-amd64-.img # ``` function ubuntu_cache_key_for_image_kernel_flavor_version() { local location=$1 kernel_location=${2:-null} url_spec with_kernel='' flavor version arch path_suffix url_spec=$(ubuntu_location_url_spec "${location}") [[ ${kernel_location} != "null" ]] && with_kernel=_with_kernel flavor=$(ubuntu_flavor_from_location_basename "${location}") version=$(ubuntu_version_from_location_basename "${location}") arch=$(ubuntu_arch_from_location_basename "${location}") path_suffix=$(ubuntu_path_suffix_from_location_basename "${location}") echo "ubuntu_${url_spec}${with_kernel}_${version}-${flavor}-${arch}-${path_suffix}" } function ubuntu_image_entry_for_image_kernel_flavor_version() { local location=$1 kernel_location=$2 url_spec url_spec=$(ubuntu_location_url_spec "${location}") local flavor version arch path_suffix flavor=${3:-$(ubuntu_flavor_from_location_basename "${location}")} version=${4:-$(ubuntu_version_from_location_basename "${location}")} arch=$(ubuntu_arch_from_location_basename "${location}") path_suffix=$(ubuntu_path_suffix_from_location_basename "${location}") local image_entry image_entry=$(ubuntu_image_url_"${url_spec}" "${flavor}" "${version}" "${arch}" "${path_suffix}") if [[ -z ${image_entry} ]]; then error_exit "Failed to get the ${url_spec} image location for ${location}" elif jq -e ".location == \"${location}\"" <<<"${image_entry}" >/dev/null; then echo "Image location is up-to-date: ${location}" >&2 elif [[ ${kernel_location} != "null" ]]; then ubuntu_image_entry_with_kernel_info "${image_entry}" else echo "${image_entry}" fi } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # shellcheck source=/dev/null # avoid shellcheck hangs on source looping . "${scriptdir}/update-template.sh" else # this script is sourced if [[ -v SUPPORTED_DISTRIBUTIONS ]]; then SUPPORTED_DISTRIBUTIONS+=("ubuntu") else declare -a SUPPORTED_DISTRIBUTIONS=("ubuntu") fi # required functions for Ubuntu function ubuntu_cache_key_for_image_kernel() { ubuntu_cache_key_for_image_kernel_flavor_version "$@"; } function ubuntu_image_entry_for_image_kernel() { ubuntu_image_entry_for_image_kernel_flavor_version "$@"; } return 0 fi declare -a templates=() declare overriding_flavor= declare overriding_version= while [[ $# -gt 0 ]]; do case "$1" in -h | --help) ubuntu_print_help exit 0 ;; -d | --debug) set -x ;; --flavor) if [[ -n $2 && $2 != -* ]]; then overriding_flavor="$2" shift else error_exit "--flavor requires a value" fi ;; --flavor=*) overriding_flavor="${1#*=}" ;; --minimal) overriding_flavor="minimal" ;; --server) overriding_flavor="server" ;; --version) if [[ -n $2 && $2 != -* ]]; then overriding_version="$2" shift else error_exit "--version requires a value" fi ;; --version=*) overriding_version="${1#*=}" ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift done if [[ ${#templates[@]} -eq 0 ]]; then ubuntu_print_help exit 0 fi declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" set +e # Disable 'set -e' to avoid exiting on error for the next assignment. overriding_version=$( set -e # Enable 'set -e' for the next command. ubuntu_version_resolve_aliases "${location}" "${overriding_flavor}" "${overriding_version}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue cache_key=$( set -e # Enable 'set -e' for the next command. ubuntu_cache_key_for_image_kernel_flavor_version "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else ubuntu_image_entry_for_image_kernel_flavor_version "${location}" "${kernel_location}" "${overriding_flavor}" "${overriding_version}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done ================================================ FILE: hack/update-template.sh ================================================ #!/usr/bin/env bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu -o pipefail # Functions in this script assume error handling with 'set -e'. # To ensure 'set -e' works correctly: # - Use 'set +e' before assignments and '$(set -e; )' to capture output without exiting on errors. # - Avoid calling functions directly in conditions to prevent disabling 'set -e'. # - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'. shopt -s inherit_errexit || error_exit "inherit_errexit not supported. Please use bash 4.4 or later." function print_help() { cat <... Description: This script updates the image location in the specified templates. If the image location in the template contains a release date in the URL, the script replaces it with the latest available date. Examples: Update the Ubuntu image location in templates/**.yaml: $ $(basename "${BASH_SOURCE[0]}") templates/**.yaml Update the Ubuntu image location in ~/.lima/ubuntu/lima.yaml: $ $(basename "${BASH_SOURCE[0]}") ~/.lima/ubuntu/lima.yaml $ limactl factory-reset ubuntu Flags: -h, --help Print this help message HELP } # json prints the JSON object with the given arguments. # json [key value ...] # if the value is empty, both key and value are omitted. # e.g. # ```console # json location https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img arch amd64 digest sha256:... # {"location":"https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img","arch":"amd64","digest":"sha256:..."} # ``` function json() { local args=() pattern='^(\[.*\]|\{.*\}|true|false|[0-9]+)$' value [[ ! -p /dev/stdin ]] && args+=(--null-input) while [[ $# -gt 0 ]]; do value="${2-}" if [[ ${value} =~ ${pattern} ]]; then args+=(--argjson "${1}" "${value}") elif [[ -n ${value} ]]; then args+=(--arg "${1}" "${value}") fi # omit empty values shift shift # shift 2 does not work when $# is 1 done jq -c "${args[@]}" '. + $ARGS.named | if . == {} then empty else . end' } # json_vars prints the JSON object with the given variable names. # e.g. # ```console # location=https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img # arch=amd64 # digest=sha256:... # json_vars location arch digest # {"location":"https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img","arch":"amd64","digest":"sha256:..."} # ``` function json_vars() { local args=() var for var in "$@"; do [[ -v ${var} ]] || error_exit "${var} is not set" args+=("${var}" "${!var}") done json "${args[@]}" } # limayaml_arch prints the arch in the lima.yaml format function limayaml_arch() { local arch=$1 arch=${arch/amd64/x86_64} arch=${arch/arm64/aarch64} arch=${arch/armhf/armv7l} arch=${arch/ppc64el/ppc64le} echo "${arch}" } function validate_boolean() { local value=$1 case "${value}" in '') ;; true | 1) echo true ;; false | 0) echo false ;; *) error_exit "Invalid boolean value: ${value}" ;; esac } # validate_url checks if the URL is valid and prints the location if it is. # If the URL is redirected, it prints the redirected location. # e.g. # ```console # validate_url https://cloud-images.ubuntu.com/server/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img # https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img # ``` function validate_url() { local url=$1 code_location=$(curl -sSL -o /dev/null -I -w "%{http_code}\t%{url_effective}" "${url}") read -r code location <<<"${code_location}" [[ ${code} -eq 200 ]] || error_exit "[${code}]: The image is not available for download from ${location}" echo "${location}" } # validate_url_without_redirect checks if the URL is valid and prints the location if it is. # If the URL is redirected, it prints the URL before the redirection. # e.g. # ```console # validate_url_without_redirect https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-arm64.qcow2 # https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-arm64.qcow2 # ``` # cloud.debian.org may be redirected to other domains(e.g. chuangtzu.ftp.acc.umu.se), but we want to use the original URL. function validate_url_without_redirect() { local url=$1 location location=$(validate_url "${url}") [[ -n ${location} ]] || error_exit "The image is not available for download from ${url}" echo "${url}" } # check if the script is executed or sourced # shellcheck disable=SC1091 if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then scriptdir=$(dirname "${BASH_SOURCE[0]}") # shellcheck source=./cache-common-inc.sh . "${scriptdir}/cache-common-inc.sh" # Scripts for each distribution are expected to: # - Add their identifier to the SUPPORTED_DISTRIBUTIONS array. # - Register the following functions: # - ${distribution}_cache_key_for_image_kernel # - Arguments: location, kernel_location # - Returns: cache_key (string) # - Exits with an error if the image location is not supported. # - ${distribution}_image_entry_for_image_kernel # - Arguments: location, kernel_location # - Returns: image_entry (JSON object) # - Exits with an error if the image location is not supported. declare -a SUPPORTED_DISTRIBUTIONS=() # shellcheck source=./update-template-ubuntu.sh . "${scriptdir}/update-template-ubuntu.sh" # shellcheck source=./update-template-debian.sh . "${scriptdir}/update-template-debian.sh" # shellcheck source=./update-template-archlinux.sh . "${scriptdir}/update-template-archlinux.sh" # shellcheck source=./update-template-centos-stream.sh . "${scriptdir}/update-template-centos-stream.sh" # shellcheck source=./update-template-almalinux.sh . "${scriptdir}/update-template-almalinux.sh" # shellcheck source=./update-template-almalinux-kitten.sh . "${scriptdir}/update-template-almalinux-kitten.sh" # shellcheck source=./update-template-rocky.sh . "${scriptdir}/update-template-rocky.sh" # shellcheck source=./update-template-alpine.sh . "${scriptdir}/update-template-alpine.sh" # shellcheck source=./update-template-oraclelinux.sh . "${scriptdir}/update-template-oraclelinux.sh" # shellcheck source=./update-template-fedora.sh . "${scriptdir}/update-template-fedora.sh" # shellcheck source=./update-template-opensuse.sh . "${scriptdir}/update-template-opensuse.sh" # shellcheck source=./update-template-freebsd.sh . "${scriptdir}/update-template-freebsd.sh" # shellcheck source=./update-template-macos.sh . "${scriptdir}/update-template-macos.sh" else # this script is sourced return 0 fi declare -a templates=() while [[ $# -gt 0 ]]; do case "$1" in -h | --help) print_help exit 0 ;; -d | --debug) set -x ;; *.yaml) templates+=("$1") ;; *) error_exit "Unknown argument: $1" ;; esac shift done if [[ ${#templates[@]} -eq 0 ]]; then print_help exit 0 fi declare -a distributions=() # Check if the distribution has the required functions for distribution in "${SUPPORTED_DISTRIBUTIONS[@]}"; do if declare -f "${distribution}_cache_key_for_image_kernel" >/dev/null && declare -f "${distribution}_image_entry_for_image_kernel" >/dev/null; then distributions+=("${distribution}") fi done [[ ${#distributions[@]} -gt 0 ]] || error_exit "No supported distributions found" declare -A image_entry_cache=() for template in "${templates[@]}"; do echo "Processing ${template}" # 1. extract location by parsing template using arch yq_filter=" .images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv " parsed=$(yq eval "${yq_filter}" "${template}") # 3. get the image location arr=() while IFS= read -r line; do arr+=("${line}"); done <<<"${parsed}" locations=("${arr[@]}") for ((index = 0; index < ${#locations[@]}; index++)); do [[ ${locations[index]} != "null" ]] || continue set -e IFS=$'\t' read -r location kernel_location kernel_cmdline <<<"${locations[index]}" for distribution in "${distributions[@]}"; do set +e # Disable 'set -e' to avoid exiting on error for the next assignment. cache_key=$( set -e # Enable 'set -e' for the next command. "${distribution}_cache_key_for_image_kernel" "${location}" "${kernel_location}" ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue image_entry=$( set -e # Enable 'set -e' for the next command. if [[ -v image_entry_cache[${cache_key}] ]]; then echo "${image_entry_cache[${cache_key}]}" else "${distribution}_image_entry_for_image_kernel" "${location}" "${kernel_location}" fi ) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition. # shellcheck disable=2181 [[ $? -eq 0 ]] || continue set -e image_entry_cache[${cache_key}]="${image_entry}" if [[ -n ${image_entry} ]]; then [[ ${kernel_cmdline} != "null" ]] && jq -e 'has("kernel")' <<<"${image_entry}" >/dev/null && image_entry=$(jq ".kernel.cmdline = \"${kernel_cmdline}\"" <<<"${image_entry}") echo "${image_entry}" | jq limactl edit --log-level error --set " .images[${index}] = ${image_entry}| (.images[${index}] | ..) style = \"double\" " "${template}" fi done done done ================================================ FILE: hack/validate-artifact.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # # This script validates that lima--Darwin-arm64.tar.gz # contains lima-guestagent.Linux-aarch64 # but does not contain share/lima/lima-guestagent.Linux-x86_64 set -eu -o pipefail must_contain() { tmp="$(mktemp)" tar tzf "$1" >"$tmp" if ! grep -q "$2" "$tmp"; then echo >&2 "ERROR: $1 must contain $2" cat "$tmp" rm -f "$tmp" exit 1 fi rm -f "$tmp" } must_not_contain() { tmp="$(mktemp)" tar tzf "$1" >"$tmp" if grep -q "$2" "$tmp"; then echo >&2 "ERROR: $1 must not contain $2" cat "$tmp" rm -f "$tmp" exit 1 fi rm -f "$tmp" } validate_artifact() { FILE="$1" MYARCH="x86_64" OTHERARCH="aarch64" if [[ $FILE == *"aarch64"* || $FILE == *"arm64"* ]]; then MYARCH="aarch64" OTHERARCH="x86_64" fi if [[ $FILE == *"go-mod-vendor.tar.gz" ]]; then : NOP elif [[ $FILE == *"lima-additional-guestagents"*".tar.gz" ]]; then must_not_contain "$FILE" "lima-guestagent.Linux-$MYARCH" must_contain "$FILE" "lima-guestagent.Linux-$OTHERARCH" elif [[ $FILE == *"lima-"*".tar.gz" ]]; then must_not_contain "$FILE" "lima-guestagent.Linux-$OTHERARCH" must_contain "$FILE" "lima-guestagent.Linux-$MYARCH" else echo >&2 "ERROR: Unexpected file: $FILE" exit 1 fi } for FILE in "$@"; do validate_artifact "$FILE" done ================================================ FILE: pkg/apfs/chown.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package apfs import ( "encoding/binary" "errors" "fmt" "hash/crc32" "os" "strings" ) var le = binary.LittleEndian // Chown sets the owner and group of files on an unmounted APFS disk image. // volumeRole selects the target volume (e.g., VolRoleData). // Paths are relative to the volume root (e.g., "Library/LaunchDaemons/foo.plist"). func Chown(diskPath string, volumeRole uint16, uid, gid uint32, paths ...string) error { c, err := openContainer(diskPath) if err != nil { return err } defer c.close() vol, err := c.findVolume(volumeRole) if err != nil { return err } fsRootPhys, err := c.omapLookup(vol.omapTreeAddr, vol.rootTreeOID, vol.latestXID) if err != nil { return fmt.Errorf("resolving filesystem root tree OID %d: %w", vol.rootTreeOID, err) } for _, path := range paths { inodeNum, err := c.resolvePath(fsRootPhys, vol.omapTreeAddr, vol.latestXID, path) if err != nil { return fmt.Errorf("resolving path %q: %w", path, err) } if err := c.chownInode(fsRootPhys, vol.omapTreeAddr, vol.latestXID, inodeNum, uid, gid); err != nil { return fmt.Errorf("chown inode %d (%q): %w", inodeNum, path, err) } } return nil } // container holds the open disk image file, the byte offset where // the APFS container starts (nonzero for GPT-partitioned disks), // and the APFS block size. type container struct { f *os.File baseOffset int64 // byte offset of APFS container within file blockSize uint32 } // volumeInfo holds resolved volume information. type volumeInfo struct { omapTreeAddr uint64 // physical address of volume omap B-tree root rootTreeOID uint64 // virtual OID of filesystem B-tree root latestXID uint64 // transaction ID of the volume superblock } func openContainer(path string) (*container, error) { f, err := os.OpenFile(path, os.O_RDWR, 0) if err != nil { return nil, fmt.Errorf("open disk: %w", err) } c := &container{f: f} hdr := make([]byte, 4096) // minimum APFS block size if _, err := f.ReadAt(hdr, 0); err != nil { f.Close() return nil, fmt.Errorf("read block 0: %w", err) } if le.Uint32(hdr[nxMagicOff:]) == nxMagic { // Raw APFS container (no partition table). c.blockSize = le.Uint32(hdr[nxBlockSizeOff:]) } else { // Look for a GPT partition table and find the APFS partition. offset, err := findAPFSPartitionGPT(f) if err != nil { f.Close() return nil, fmt.Errorf("finding APFS partition: %w", err) } c.baseOffset = offset if _, err := f.ReadAt(hdr, offset); err != nil { f.Close() return nil, fmt.Errorf("read APFS superblock at offset %d: %w", offset, err) } if le.Uint32(hdr[nxMagicOff:]) != nxMagic { f.Close() return nil, fmt.Errorf("APFS partition at offset %d has bad magic", offset) } c.blockSize = le.Uint32(hdr[nxBlockSizeOff:]) } if c.blockSize < 4096 { f.Close() return nil, fmt.Errorf("invalid block size %d", c.blockSize) } return c, nil } // GPT constants. const ( gptHeaderSignature = "EFI PART" gptLBASectorSize = 512 ) // apfsPartTypeGUID is the APFS Container partition type GUID as stored // on disk (mixed-endian encoding per GPT spec). // 7C3457EF-0000-11AA-AA11-00306543ECAC. var apfsPartTypeGUID = [16]byte{ 0xEF, 0x57, 0x34, 0x7C, // time_low (LE) 0x00, 0x00, // time_mid (LE) 0xAA, 0x11, // time_hi_and_version (LE) 0xAA, 0x11, // clock_seq 0x00, 0x30, 0x65, 0x43, 0xEC, 0xAC, // node } // findAPFSPartitionGPT reads a GPT partition table and returns the // byte offset of the first APFS Container partition. func findAPFSPartitionGPT(f *os.File) (int64, error) { // Read GPT header at LBA 1. gptHdr := make([]byte, gptLBASectorSize) if _, err := f.ReadAt(gptHdr, gptLBASectorSize); err != nil { return 0, fmt.Errorf("reading GPT header: %w", err) } if string(gptHdr[0:8]) != gptHeaderSignature { return 0, fmt.Errorf("no GPT header found (expected %q)", gptHeaderSignature) } partEntryLBA := le.Uint64(gptHdr[72:]) numEntries := le.Uint32(gptHdr[80:]) entrySize := le.Uint32(gptHdr[84:]) entryBuf := make([]byte, entrySize) for i := range numEntries { off := int64(partEntryLBA)*gptLBASectorSize + int64(i)*int64(entrySize) if _, err := f.ReadAt(entryBuf, off); err != nil { return 0, fmt.Errorf("reading GPT entry %d: %w", i, err) } var typeGUID [16]byte copy(typeGUID[:], entryBuf[0:16]) if typeGUID == apfsPartTypeGUID { firstLBA := le.Uint64(entryBuf[32:]) return int64(firstLBA) * gptLBASectorSize, nil } // Stop at empty entries. allZero := true for _, b := range typeGUID { if b != 0 { allZero = false break } } if allZero { break } } return 0, errors.New("no APFS partition found in GPT") } func (c *container) close() { c.f.Close() } func (c *container) readBlock(addr uint64) ([]byte, error) { buf := make([]byte, c.blockSize) _, err := c.f.ReadAt(buf, c.baseOffset+int64(addr)*int64(c.blockSize)) if err != nil { return nil, fmt.Errorf("read block %d: %w", addr, err) } return buf, nil } func (c *container) writeBlock(addr uint64, data []byte) error { _, err := c.f.WriteAt(data, c.baseOffset+int64(addr)*int64(c.blockSize)) if err != nil { return fmt.Errorf("write block %d: %w", addr, err) } return nil } // latestSuperblock scans the checkpoint descriptor area for the // superblock with the highest valid transaction ID. func (c *container) latestSuperblock() ([]byte, error) { // Read block 0 at full block size to get checkpoint descriptor area info. block0, err := c.readBlock(0) if err != nil { return nil, err } if err := verifyChecksum(block0); err != nil { return nil, fmt.Errorf("block 0 checksum: %w", err) } descBase := le.Uint64(block0[nxXPDescBaseOff:]) descBlocks := le.Uint32(block0[nxXPDescBlocksOff:]) & nxXPDescBlocksMask var bestBlock []byte var bestXID uint64 for i := range descBlocks { blk, err := c.readBlock(descBase + uint64(i)) if err != nil { continue } if verifyChecksum(blk) != nil { continue } oType := le.Uint32(blk[objTypeOff:]) & objTypeMask if oType != objectTypeNXSuperblock { continue } if le.Uint32(blk[nxMagicOff:]) != nxMagic { continue } xid := le.Uint64(blk[objXIDOff:]) if xid > bestXID { bestXID = xid bestBlock = blk } } if bestBlock == nil { return nil, errors.New("no valid container superblock found in checkpoint area") } return bestBlock, nil } // findVolume locates the volume with the given role. func (c *container) findVolume(role uint16) (*volumeInfo, error) { sb, err := c.latestSuperblock() if err != nil { return nil, fmt.Errorf("reading container superblock: %w", err) } // Read the container omap to resolve volume virtual OIDs. containerOmapAddr := le.Uint64(sb[nxOmapOIDOff:]) containerOmap, err := c.readBlock(containerOmapAddr) if err != nil { return nil, fmt.Errorf("reading container omap at %d: %w", containerOmapAddr, err) } containerOmapTreeAddr := le.Uint64(containerOmap[omapTreeOIDOff:]) containerXID := le.Uint64(sb[objXIDOff:]) for i := range nxMaxFileSystems { off := nxFSOIDOff + i*8 volOID := le.Uint64(sb[off:]) if volOID == 0 { continue } // Resolve virtual OID through container omap. volPhysAddr, err := c.omapLookup(containerOmapTreeAddr, volOID, containerXID) if err != nil { continue // skip volumes we can't resolve } volBlock, err := c.readBlock(volPhysAddr) if err != nil { continue } if verifyChecksum(volBlock) != nil { continue } if le.Uint32(volBlock[apfsMagicOff:]) != apfsMagic { continue } volRole := le.Uint16(volBlock[apfsRoleOff:]) if volRole == role { omapAddr := le.Uint64(volBlock[apfsOmapOIDOff:]) omapBlock, err := c.readBlock(omapAddr) if err != nil { return nil, fmt.Errorf("reading volume omap: %w", err) } return &volumeInfo{ omapTreeAddr: le.Uint64(omapBlock[omapTreeOIDOff:]), rootTreeOID: le.Uint64(volBlock[apfsRootTreeOIDOff:]), latestXID: le.Uint64(volBlock[objXIDOff:]), }, nil } } return nil, fmt.Errorf("no volume with role %#x found", role) } // omapLookup searches the omap B-tree for a virtual OID, returning // the physical address from the entry with the highest xid <= maxXID. func (c *container) omapLookup(omapTreeAddr, oid, maxXID uint64) (uint64, error) { blk, err := c.readBlock(omapTreeAddr) if err != nil { return 0, err } for { if verifyChecksum(blk) != nil { return 0, errors.New("omap node checksum failed") } if err := verifyBTreeNodeType(blk); err != nil { return 0, fmt.Errorf("omap node: %w", err) } flags := le.Uint16(blk[btnFlagsOff:]) nkeys := le.Uint32(blk[btnNKeysOff:]) tspOff := le.Uint16(blk[btnTableSpaceOff:]) tspLen := le.Uint16(blk[btnTableSpaceOff+2:]) tocStart := btnDataOff + uint32(tspOff) keyAreaStart := tocStart + uint32(tspLen) isLeaf := flags&btnodeLeaf != 0 isFixedKV := flags&btnodeFixedKVSize != 0 isRoot := flags&btnodeRoot != 0 valueAreaEnd := c.blockSize if isRoot { valueAreaEnd -= btreeInfoSize } if isLeaf { // Search for matching oid with highest xid <= maxXID. var bestPhysAddr uint64 var bestXID uint64 found := false for i := range nkeys { kOff, vOff := c.readTocEntry(blk, tocStart, i, isFixedKV) keyStart := keyAreaStart + kOff entryOID := le.Uint64(blk[keyStart:]) entryXID := le.Uint64(blk[keyStart+8:]) if entryOID == oid && entryXID <= maxXID { // Value: ov_flags(4) + ov_size(4) + ov_paddr(8). valStart := valueAreaEnd - vOff physAddr := le.Uint64(blk[valStart+8:]) if !found || entryXID > bestXID { bestPhysAddr = physAddr bestXID = entryXID found = true } } } if !found { return 0, fmt.Errorf("omap entry for OID %d not found", oid) } return bestPhysAddr, nil } // Internal node: find the last key <= (oid, maxXID) and descend. childIdx := uint32(0) for i := range nkeys { kOff, _ := c.readTocEntry(blk, tocStart, i, isFixedKV) keyStart := keyAreaStart + kOff entryOID := le.Uint64(blk[keyStart:]) entryXID := le.Uint64(blk[keyStart+8:]) cmp := compareOmapKey(entryOID, entryXID, oid, maxXID) if cmp <= 0 { childIdx = i } else { break } } // Read child pointer (physical address, 8 bytes). _, vOff := c.readTocEntry(blk, tocStart, childIdx, isFixedKV) childAddr := le.Uint64(blk[valueAreaEnd-vOff:]) blk, err = c.readBlock(childAddr) if err != nil { return 0, fmt.Errorf("reading omap child node: %w", err) } } } // readTocEntry reads the key and value offsets from a ToC entry. // For fixed-KV nodes (kvoff_t): 4 bytes per entry (k_off u16, v_off u16). // For variable-KV nodes (kvloc_t): 8 bytes per entry (k.off u16, k.len u16, v.off u16, v.len u16). // Returns keyOffset and valueOffset (both relative to their respective areas). func (c *container) readTocEntry(blk []byte, tocStart, index uint32, fixedKV bool) (keyOff, valOff uint32) { if fixedKV { off := tocStart + index*4 return uint32(le.Uint16(blk[off:])), uint32(le.Uint16(blk[off+2:])) } off := tocStart + index*8 return uint32(le.Uint16(blk[off:])), uint32(le.Uint16(blk[off+4:])) } // verifyBTreeNodeType checks that a block's object type indicates a // B-tree node: OBJECT_TYPE_BTREE (0x02, root) or OBJECT_TYPE_BTREE_NODE // (0x03, non-root). func verifyBTreeNodeType(blk []byte) error { oType := le.Uint32(blk[objTypeOff:]) & objTypeMask if oType != 0x02 && oType != 0x03 { return fmt.Errorf("expected B-tree node type (2 or 3), got %#x", oType) } return nil } func compareOmapKey(oid1, xid1, oid2, xid2 uint64) int { if oid1 < oid2 { return -1 } if oid1 > oid2 { return 1 } if xid1 < xid2 { return -1 } if xid1 > xid2 { return 1 } return 0 } // compareFSKeyHeader compares two filesystem B-tree key headers. // APFS sorts filesystem keys by (obj_id, type), not by the raw uint64. func compareFSKeyHeader(a, b uint64) int { aID := a & objIDMask bID := b & objIDMask if aID < bID { return -1 } if aID > bID { return 1 } aType := a >> objTypeShift bType := b >> objTypeShift if aType < bType { return -1 } if aType > bType { return 1 } return 0 } var crc32cTable = crc32.MakeTable(crc32.Castagnoli) // drecNameHash computes the 22-bit directory record name hash as stored // in the upper bits of j_drec_hashed_key_t.name_len_and_hash. // // Per the Apple APFS Reference: NFD-normalize the name, encode as UTF-32LE // (without null terminator), compute CRC-32C, complement, keep low 22 bits. // The complement cancels with the CRC's standard finalization XOR, so we // use ^crc32.Checksum to get the raw CRC. func drecNameHash(name string) uint32 { runes := []rune(strings.ToLower(name)) buf := make([]byte, len(runes)*4) for i, r := range runes { le.PutUint32(buf[i*4:], uint32(r)) } return ^crc32.Checksum(buf, crc32cTable) & 0x3FFFFF } // compareDrecKey compares a directory record key from a B-tree node // against a target (parentCNID, name). Returns <0, 0, or >0. func (c *container) compareDrecKey(blk []byte, keyStart uint32, targetKeyHeader uint64, targetName string, targetHash uint32) int { keyHeader := le.Uint64(blk[keyStart:]) cmp := compareFSKeyHeader(keyHeader, targetKeyHeader) if cmp != 0 { return cmp } // Headers match (same parent CNID and type DIR_REC). // Compare name hash for hashed keys, or name directly for non-hashed. val32 := le.Uint32(blk[keyStart+8:]) if val32&0xFFFFFC00 != 0 { // Hashed key: compare hash values (upper 22 bits). entryHash := (val32 >> 10) & 0x3FFFFF if entryHash < targetHash { return -1 } if entryHash > targetHash { return 1 } // Hash collision: compare actual names. entryName := c.readDrecName(blk, keyStart+8) return strings.Compare(strings.ToLower(entryName), strings.ToLower(targetName)) } // Non-hashed key: compare names directly. entryName := c.readDrecName(blk, keyStart+8) return strings.Compare(entryName, targetName) } // resolvePath walks path components from the volume root, returning // the inode number of the final path element. func (c *container) resolvePath(fsRootPhys, omapTreeAddr, maxXID uint64, path string) (uint64, error) { parts := strings.Split(strings.Trim(path, "/"), "/") cnid := uint64(rootDirInodeNum) for _, name := range parts { if name == "" { continue } fileID, err := c.lookupDirEntry(fsRootPhys, omapTreeAddr, maxXID, cnid, name) if err != nil { return 0, fmt.Errorf("looking up %q in directory (cnid %d): %w", name, cnid, err) } cnid = fileID } return cnid, nil } // lookupDirEntry searches the filesystem B-tree for a directory record // matching parentCNID and name, returning the file_id from j_drec_val_t. func (c *container) lookupDirEntry(fsRootPhys, omapTreeAddr, maxXID, parentCNID uint64, name string) (uint64, error) { targetKeyHeader := (uint64(apfsTypeDirRec) << objTypeShift) | (parentCNID & objIDMask) targetHash := drecNameHash(name) blk, err := c.readBlock(fsRootPhys) if err != nil { return 0, err } for { if verifyChecksum(blk) != nil { return 0, errors.New("filesystem B-tree node checksum failed") } if err := verifyBTreeNodeType(blk); err != nil { return 0, fmt.Errorf("filesystem B-tree node: %w", err) } flags := le.Uint16(blk[btnFlagsOff:]) nkeys := le.Uint32(blk[btnNKeysOff:]) tspOff := le.Uint16(blk[btnTableSpaceOff:]) tspLen := le.Uint16(blk[btnTableSpaceOff+2:]) tocStart := btnDataOff + uint32(tspOff) keyAreaStart := tocStart + uint32(tspLen) isLeaf := flags&btnodeLeaf != 0 isFixedKV := flags&btnodeFixedKVSize != 0 isRoot := flags&btnodeRoot != 0 valueAreaEnd := c.blockSize if isRoot { valueAreaEnd -= btreeInfoSize } if isLeaf { for i := range nkeys { kOff, vOff := c.readTocEntry(blk, tocStart, i, isFixedKV) keyStart := keyAreaStart + kOff keyHeader := le.Uint64(blk[keyStart:]) if keyHeader != targetKeyHeader { continue } // Parse the directory record key name. entryName := c.readDrecName(blk, keyStart+8) if entryName != name { continue } // Read file_id from j_drec_val_t (first 8 bytes of value). valStart := valueAreaEnd - vOff return le.Uint64(blk[valStart:]), nil } return 0, fmt.Errorf("directory entry %q not found", name) } // Internal node: find the child to descend into. // The last key <= our target determines the child. // For DIR_REC keys with matching headers, we also compare the // name hash to find the correct subtree. childIdx := uint32(0) for i := range nkeys { kOff, _ := c.readTocEntry(blk, tocStart, i, isFixedKV) keyStart := keyAreaStart + kOff cmp := c.compareDrecKey(blk, keyStart, targetKeyHeader, name, targetHash) if cmp <= 0 { childIdx = i } else { break } } // Read child OID (virtual) and resolve through omap. _, vOff := c.readTocEntry(blk, tocStart, childIdx, isFixedKV) childOID := le.Uint64(blk[valueAreaEnd-vOff:]) childPhys, err := c.omapLookup(omapTreeAddr, childOID, maxXID) if err != nil { return 0, fmt.Errorf("resolving child OID %d: %w", childOID, err) } blk, err = c.readBlock(childPhys) if err != nil { return 0, err } } } // readDrecName reads a directory record name from the key data // starting at nameFieldOff. Handles both hashed (j_drec_hashed_key_t, // 4-byte name_len_and_hash) and non-hashed (j_drec_key_t, 2-byte // name_len) formats. func (c *container) readDrecName(blk []byte, nameFieldOff uint32) string { // Heuristic: if the 4-byte field at nameFieldOff has its upper // bits set (hash), it's a hashed key. For non-hashed keys, the // 2-byte name_len is followed by the name bytes. // // In j_drec_hashed_key_t: name_len_and_hash (uint32) where // lower 10 bits = name length including null terminator. // In j_drec_key_t: name_len (uint16) = name length including // null terminator. // // We distinguish them by checking: if the upper 22 bits of the // first uint32 are nonzero, it's hashed. val32 := le.Uint32(blk[nameFieldOff:]) if val32&0xFFFFFC00 != 0 { // Hashed: lower 10 bits = length (including null terminator). nameLen := int(val32 & 0x3FF) nameStart := nameFieldOff + 4 if nameLen > 1 { return string(blk[nameStart : nameStart+uint32(nameLen-1)]) } return "" } // Non-hashed: 2-byte name_len. nameLen := int(le.Uint16(blk[nameFieldOff:])) nameStart := nameFieldOff + 2 if nameLen > 1 { return string(blk[nameStart : nameStart+uint32(nameLen-1)]) } return "" } // chownInode finds an inode in the filesystem B-tree and modifies // its owner and group fields. func (c *container) chownInode(fsRootPhys, omapTreeAddr, maxXID, inodeNum uint64, uid, gid uint32) error { targetKeyHeader := (uint64(apfsTypeInode) << objTypeShift) | (inodeNum & objIDMask) blkAddr := fsRootPhys blk, err := c.readBlock(blkAddr) if err != nil { return err } // Walk down to the leaf containing the inode. for { if verifyChecksum(blk) != nil { return errors.New("filesystem B-tree node checksum failed") } if err := verifyBTreeNodeType(blk); err != nil { return fmt.Errorf("filesystem B-tree node: %w", err) } flags := le.Uint16(blk[btnFlagsOff:]) nkeys := le.Uint32(blk[btnNKeysOff:]) tspOff := le.Uint16(blk[btnTableSpaceOff:]) tspLen := le.Uint16(blk[btnTableSpaceOff+2:]) tocStart := btnDataOff + uint32(tspOff) keyAreaStart := tocStart + uint32(tspLen) isLeaf := flags&btnodeLeaf != 0 isFixedKV := flags&btnodeFixedKVSize != 0 isRoot := flags&btnodeRoot != 0 valueAreaEnd := c.blockSize if isRoot { valueAreaEnd -= btreeInfoSize } if isLeaf { for i := range nkeys { kOff, vOff := c.readTocEntry(blk, tocStart, i, isFixedKV) keyStart := keyAreaStart + kOff keyHeader := le.Uint64(blk[keyStart:]) if keyHeader != targetKeyHeader { continue } // Found the inode. Validate before writing. valStart := valueAreaEnd - vOff if valStart+inodeGroupOff+4 > c.blockSize { return fmt.Errorf("inode %d value exceeds block boundary", inodeNum) } if privateID := le.Uint64(blk[valStart+inodePrivateIDOff:]); privateID != inodeNum { return fmt.Errorf("inode %d has mismatched private_id %d", inodeNum, privateID) } currentUID := le.Uint32(blk[valStart+inodeOwnerOff:]) currentGID := le.Uint32(blk[valStart+inodeGroupOff:]) if currentUID != noownersPlaceholderID || currentGID != noownersPlaceholderID { return fmt.Errorf("inode %d has ownership %d:%d, expected %d:%d from noowners mount", inodeNum, currentUID, currentGID, noownersPlaceholderID, noownersPlaceholderID) } le.PutUint32(blk[valStart+inodeOwnerOff:], uid) le.PutUint32(blk[valStart+inodeGroupOff:], gid) updateChecksum(blk) return c.writeBlock(blkAddr, blk) } return fmt.Errorf("inode %d not found in filesystem B-tree", inodeNum) } // Internal node: descend. childIdx := uint32(0) for i := range nkeys { kOff, _ := c.readTocEntry(blk, tocStart, i, isFixedKV) keyStart := keyAreaStart + kOff keyHeader := le.Uint64(blk[keyStart:]) if compareFSKeyHeader(keyHeader, targetKeyHeader) <= 0 { childIdx = i } else { break } } _, vOff := c.readTocEntry(blk, tocStart, childIdx, isFixedKV) childOID := le.Uint64(blk[valueAreaEnd-vOff:]) childPhys, err := c.omapLookup(omapTreeAddr, childOID, maxXID) if err != nil { return fmt.Errorf("resolving child OID %d: %w", childOID, err) } blkAddr = childPhys blk, err = c.readBlock(blkAddr) if err != nil { return err } } } ================================================ FILE: pkg/apfs/chown_darwin_test.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package apfs import ( "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "gotest.tools/v3/assert" ) // TestChownIntegration creates a real APFS disk image with hdiutil, // writes a test file, runs Chown on the raw image, and verifies // ownership via os.Stat after remounting with ownership enabled. // No root required. func TestChownIntegration(t *testing.T) { tmpDir := t.TempDir() imgPath := filepath.Join(tmpDir, "test.dmg") // Create a 64MB APFS disk image (GPT + APFS container). ctx := t.Context() cmd := exec.CommandContext(ctx, "hdiutil", "create", "-size", "64m", "-fs", "APFS", "-volname", "TestVol", imgPath, ) out, err := cmd.CombinedOutput() assert.NilError(t, err, "hdiutil create failed: %s", out) // Attach, write a test file, and detach. Default mount uses // noowners, so the on-disk UID will be 99 (nobody). mntDir := filepath.Join(tmpDir, "mnt") cmd = exec.CommandContext(ctx, "hdiutil", "attach", imgPath, "-mountpoint", mntDir) out, err = cmd.CombinedOutput() assert.NilError(t, err, "hdiutil attach failed: %s", out) assert.NilError(t, os.WriteFile(filepath.Join(mntDir, "testfile.txt"), []byte("hello"), 0o644)) cmd = exec.CommandContext(ctx, "hdiutil", "detach", mntDir) out, err = cmd.CombinedOutput() assert.NilError(t, err, "hdiutil detach failed: %s", out) // Change ownership to root:wheel via raw APFS modification. // hdiutil creates a single volume with role=0 (APFS_VOL_ROLE_NONE). assert.NilError(t, Chown(imgPath, 0, 0, 0, "testfile.txt")) // Verify filesystem integrity after raw block modification. cmd = exec.CommandContext(ctx, "hdiutil", "attach", imgPath, "-nomount") out, err = cmd.CombinedOutput() assert.NilError(t, err, "hdiutil attach -nomount failed: %s", out) dev := strings.Fields(string(out))[0] cmd = exec.CommandContext(ctx, "fsck_apfs", "-n", dev) out, err = cmd.CombinedOutput() assert.NilError(t, err, "fsck_apfs failed: %s", out) cmd = exec.CommandContext(ctx, "hdiutil", "detach", dev) out, err = cmd.CombinedOutput() assert.NilError(t, err, "hdiutil detach failed: %s", out) // Re-attach with ownership enabled so os.Stat reflects on-disk UIDs. cmd = exec.CommandContext(ctx, "hdiutil", "attach", imgPath, "-mountpoint", mntDir, "-owners", "on") out, err = cmd.CombinedOutput() assert.NilError(t, err, "hdiutil re-attach failed: %s", out) defer func() { _ = exec.CommandContext(ctx, "hdiutil", "detach", mntDir, "-force").Run() }() fi, err := os.Stat(filepath.Join(mntDir, "testfile.txt")) assert.NilError(t, err) stat := fi.Sys().(*syscall.Stat_t) assert.Equal(t, stat.Uid, uint32(0), "expected uid=0, got uid=%d", stat.Uid) assert.Equal(t, stat.Gid, uint32(0), "expected gid=0, got gid=%d", stat.Gid) } ================================================ FILE: pkg/apfs/fletcher64.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package apfs import ( "encoding/binary" "fmt" ) const fletcher64Mod = 0xFFFFFFFF // fletcher64 computes the APFS Fletcher-64 checksum over block[8:]. func fletcher64(block []byte) uint64 { var sum1, sum2 uint64 for i := 8; i+3 < len(block); i += 4 { sum1 = (sum1 + uint64(binary.LittleEndian.Uint32(block[i:]))) % fletcher64Mod sum2 = (sum2 + sum1) % fletcher64Mod } ckLow := fletcher64Mod - ((sum1 + sum2) % fletcher64Mod) ckHigh := fletcher64Mod - ((sum1 + ckLow) % fletcher64Mod) return (ckHigh << 32) | ckLow } // verifyChecksum returns an error if the stored checksum does not match. func verifyChecksum(block []byte) error { stored := binary.LittleEndian.Uint64(block[objChecksumOff:]) computed := fletcher64(block) if stored != computed { return fmt.Errorf("checksum mismatch: stored %#x, computed %#x", stored, computed) } return nil } // updateChecksum recomputes and stores the Fletcher-64 checksum. func updateChecksum(block []byte) { binary.LittleEndian.PutUint64(block[objChecksumOff:], fletcher64(block)) } ================================================ FILE: pkg/apfs/fletcher64_test.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package apfs import ( "encoding/binary" "testing" "gotest.tools/v3/assert" ) func TestFletcher64(t *testing.T) { // Construct a minimal 4096-byte block and verify the checksum // round-trips: compute -> store -> verify. block := make([]byte, 4096) for i := 8; i < len(block); i++ { block[i] = byte(i % 251) } updateChecksum(block) assert.NilError(t, verifyChecksum(block)) // Corrupt one byte and verify detection. block[100]++ assert.Assert(t, verifyChecksum(block) != nil, "verifyChecksum should have failed after corruption") } func TestFletcher64KnownVector(t *testing.T) { // A block of all zeros (except checksum) should produce a // deterministic checksum. Verify consistency. block := make([]byte, 4096) ck := fletcher64(block) updateChecksum(block) stored := binary.LittleEndian.Uint64(block[0:]) assert.Equal(t, stored, ck) assert.NilError(t, verifyChecksum(block)) } ================================================ FILE: pkg/apfs/types.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 // Package apfs provides minimal APFS disk image manipulation. // It reads on-disk structures at known byte offsets per the Apple APFS // Reference and modifies inode UID/GID fields directly, bypassing // the kernel VFS. package apfs // Magic numbers. const ( nxMagic = 0x4253584E // "NXSB" apfsMagic = 0x42535041 // "APSB" ) // Object types (lower 16 bits of o_type). const ( objectTypeNXSuperblock = 0x01 objectTypeCheckpointMap = 0x0C objectTypeOmap = 0x0B objectTypeBTreeNode = 0x02 objectTypeFS = 0x0D objectTypeFSTree = 0x0E objectTypeBlockRefTree = 0x0F objectTypeOmapSnapshot = 0x10 objectTypeNXReaperFS = 0x12 objectTypeNXReaperListKey = 0x13 ) // Object storage classes (upper 16 bits of o_type, shifted). const ( objPhysical = 0x00000000 objEphemeral = 0x80000000 objVirtual = 0x00000000 objTypeMask = 0x0000FFFF objFlagsMask = 0xFFFF0000 ) // B-tree node flags. const ( btnodeRoot = 0x0001 btnodeLeaf = 0x0002 btnodeFixedKVSize = 0x0004 ) // Filesystem object types (upper 4 bits of j_key_t.obj_id_and_type). const ( apfsTypeInode = 3 apfsTypeDirRec = 9 ) // Filesystem key masks. const ( objIDMask = 0x0FFFFFFFFFFFFFFF objTypeShift = 60 ) // Well-known inode numbers. const ( rootDirInodeNum = 2 ) // noownersPlaceholderID is the on-disk UID and GID that APFS stores // for files on volumes mounted with the noowners option. const noownersPlaceholderID = 99 // VolRoleData is the APFS volume role for "Data" volumes. const VolRoleData = 0x0040 // APFS_VOL_ROLE_DATA (shifted representation: (1 << 2) << 4) // Container superblock (nx_superblock_t) field offsets from block start. const ( nxMagicOff = 32 // uint32 nxBlockSizeOff = 36 // uint32 nxXPDescBlocksOff = 104 // uint32 (mask 0x7FFFFFFF for count) nxXPDescBaseOff = 112 // uint64 (physical block address) nxXPDescIndexOff = 136 // uint32 nxXPDescLenOff = 140 // uint32 nxOmapOIDOff = 160 // uint64 (physical) nxFSOIDOff = 184 // uint64[100] (virtual OIDs) nxMaxFileSystems = 100 nxXPDescBlocksMask = 0x7FFFFFFF ) // Object header (obj_phys_t) field offsets. const ( objChecksumOff = 0 // uint64 objOIDOff = 8 // uint64 objXIDOff = 16 // uint64 objTypeOff = 24 // uint32 objSubtypeOff = 28 // uint32 objHeaderSize = 32 ) // Volume superblock (apfs_superblock_t) field offsets from block start. const ( apfsMagicOff = 32 // uint32 apfsOmapOIDOff = 128 // uint64 (physical) apfsRootTreeOIDOff = 136 // uint64 (virtual) apfsVolNameOff = 704 // 256 bytes UTF-8 apfsRoleOff = 964 // uint16 ) // Object map (omap_phys_t) field offsets from block start. const ( omapTreeOIDOff = 48 // uint64 (physical) ) // B-tree node (btree_node_phys_t) field offsets from block start. const ( btnFlagsOff = 32 // uint16 btnLevelOff = 34 // uint16 btnNKeysOff = 36 // uint32 btnTableSpaceOff = 40 // nloc_t: off uint16 + len uint16 btnDataOff = 56 // start of btn_data[] ) // btree_info_t size (sits at end of root node block). const btreeInfoSize = 40 // j_inode_val_t field offsets (from start of value data). const ( inodeParentIDOff = 0 // uint64 inodePrivateIDOff = 8 // uint64 inodeOwnerOff = 72 // uint32 inodeGroupOff = 76 // uint32 ) ================================================ FILE: pkg/autostart/autostart.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 // Package autostart manage start at login unit files for darwin/linux package autostart import ( "context" "sync" "github.com/lima-vm/lima/v2/pkg/limatype" ) // IsRegistered checks if the instance is registered to start at login. func IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error) { return manager().IsRegistered(ctx, inst) } // RegisterToStartAtLogin creates a start-at-login entry for the instance. func RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error { return manager().RegisterToStartAtLogin(ctx, inst) } // UnregisterFromStartAtLogin deletes the start-at-login entry for the instance. func UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error { return manager().UnregisterFromStartAtLogin(ctx, inst) } // AutoStartedIdentifier returns the identifier if the current process was started by the autostart manager. func AutoStartedIdentifier() string { return manager().AutoStartedIdentifier() } // RequestStart requests to start the instance by identifier. func RequestStart(ctx context.Context, inst *limatype.Instance) error { return manager().RequestStart(ctx, inst) } // RequestStop requests to stop the instance by identifier. func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { return manager().RequestStop(ctx, inst) } type autoStartManager interface { // Registration IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error) RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error // Status AutoStartedIdentifier() string // Operation // RequestStart requests to start the instance by identifier. RequestStart(ctx context.Context, inst *limatype.Instance) error // RequestStop requests to stop the instance by identifier. RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) } var manager = sync.OnceValue(Manager) ================================================ FILE: pkg/autostart/autostart_test.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package autostart import ( "runtime" "testing" "gotest.tools/v3/assert" "github.com/lima-vm/lima/v2/pkg/autostart/launchd" "github.com/lima-vm/lima/v2/pkg/autostart/systemd" ) var ( Launchd = &TemplateFileBasedManager{ filePath: launchd.GetPlistPath, template: launchd.Template, enabler: launchd.EnableDisableService, autoStartedIdentifier: launchd.AutoStartedServiceName, requestStart: launchd.RequestStart, requestStop: launchd.RequestStop, } Systemd = &TemplateFileBasedManager{ filePath: systemd.GetUnitPath, template: systemd.Template, enabler: systemd.EnableDisableUnit, autoStartedIdentifier: systemd.AutoStartedUnitName, requestStart: systemd.RequestStart, requestStop: systemd.RequestStop, } ) func TestRenderTemplate(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("skipping testing on windows host") } tests := []struct { Manager *TemplateFileBasedManager Name string InstanceName string Expected string WorkDir string GetExecutable func() (string, error) }{ { Manager: Launchd, Name: "render darwin launchd plist", InstanceName: "default", Expected: ` Label io.lima-vm.autostart.default ProgramArguments /limactl start default --foreground RunAtLoad StandardErrorPath launchd.stderr.log StandardOutPath launchd.stdout.log WorkingDirectory /some/path ProcessType Background `, GetExecutable: func() (string, error) { return "/limactl", nil }, WorkDir: "/some/path", }, { Manager: Systemd, Name: "render linux systemd service", InstanceName: "default", Expected: `[Unit] Description=Lima - Linux virtual machines, with a focus on running containers. Documentation=man:lima(1) [Service] ExecStart=/limactl start %i --foreground WorkingDirectory=%h Type=simple TimeoutSec=10 Restart=on-failure [Install] WantedBy=default.target `, GetExecutable: func() (string, error) { return "/limactl", nil }, WorkDir: "/some/path", }, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { tmpl, err := tt.Manager.renderTemplate(tt.InstanceName, tt.WorkDir, tt.GetExecutable) assert.NilError(t, err) assert.Equal(t, string(tmpl), tt.Expected) }) } } ================================================ FILE: pkg/autostart/launchd/io.lima-vm.autostart.INSTANCE.plist ================================================ Label io.lima-vm.autostart.{{ .Instance }} ProgramArguments {{ .Binary }} start {{ .Instance }} --foreground RunAtLoad StandardErrorPath launchd.stderr.log StandardOutPath launchd.stdout.log WorkingDirectory {{ .WorkDir }} ProcessType Background ================================================ FILE: pkg/autostart/launchd/launchd.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package launchd import ( "context" _ "embed" "fmt" "os" "os/exec" "sync" "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/limatype" ) //go:embed io.lima-vm.autostart.INSTANCE.plist var Template string // GetPlistPath returns the path to the launchd plist file for the given instance name. func GetPlistPath(instName string) string { return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", os.Getenv("HOME"), ServiceNameFrom(instName)) } // ServiceNameFrom returns the launchd service name for the given instance name. func ServiceNameFrom(instName string) string { return fmt.Sprintf("io.lima-vm.autostart.%s", instName) } // EnableDisableService enables or disables the launchd service for the given instance name. func EnableDisableService(ctx context.Context, enable bool, instName string) error { action := "enable" if !enable { action = "disable" } return launchctl(ctx, action, serviceTarget(instName)) } func launchctl(ctx context.Context, args ...string) error { cmd := exec.CommandContext(ctx, "launchctl", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logrus.Debugf("running command: %v", cmd.Args) return cmd.Run() } func launchctlWithoutOutput(ctx context.Context, args ...string) error { cmd := exec.CommandContext(ctx, "launchctl", args...) logrus.Debugf("running command without output: %v", cmd.Args) return cmd.Run() } // AutoStartedServiceName returns the launchd service name if the instance is started by launchd. func AutoStartedServiceName() string { // Assume the instance is started by launchd if XPC_SERVICE_NAME is set and not "0". // To confirm it is actually started by launchd, it needs to use `launch_activate_socket`. // But that requires actual socket activation setup in the plist file. // So we just check XPC_SERVICE_NAME here. if xpcServiceName := os.Getenv("XPC_SERVICE_NAME"); xpcServiceName != "0" { return xpcServiceName } return "" } var domainTarget = sync.OnceValue(func() string { return fmt.Sprintf("gui/%d", os.Getuid()) }) func serviceTarget(instName string) string { return fmt.Sprintf("%s/%s", domainTarget(), ServiceNameFrom(instName)) } func RequestStart(ctx context.Context, inst *limatype.Instance) error { // Call `launchctl bootout` first, because instance may be stopped without unloading the plist file. // If the plist file is not unloaded, `launchctl bootstrap` will fail. _ = launchctlWithoutOutput(ctx, "bootout", serviceTarget(inst.Name)) // If disabled, `launchctl bootstrap` will fail. _ = EnableDisableService(ctx, true, inst.Name) if err := launchctl(ctx, "bootstrap", domainTarget(), GetPlistPath(inst.Name)); err != nil { return fmt.Errorf("failed to start the instance %q via launchctl: %w", inst.Name, err) } return nil } func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { logrus.Debugf("AutoStartedIdentifier=%q, ServiceNameFrom=%q", inst.AutoStartedIdentifier, ServiceNameFrom(inst.Name)) if inst.AutoStartedIdentifier == ServiceNameFrom(inst.Name) { logrus.Infof("Stopping the instance %q started by launchd", inst.Name) if err := launchctl(ctx, "bootout", serviceTarget(inst.Name)); err != nil { return false, fmt.Errorf("failed to stop the instance %q via launchctl: %w", inst.Name, err) } return true, nil } return false, nil } ================================================ FILE: pkg/autostart/launchd/launchd_test.go ================================================ //go:build !windows // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package launchd import ( "strings" "testing" "gotest.tools/v3/assert" ) func TestGetPlistPath(t *testing.T) { tests := []struct { Name string InstanceName string Expected string }{ { Name: "darwin with docker instance name", InstanceName: "docker", Expected: "Library/LaunchAgents/io.lima-vm.autostart.docker.plist", }, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { assert.Check(t, strings.HasSuffix(GetPlistPath(tt.InstanceName), tt.Expected)) }) } } ================================================ FILE: pkg/autostart/managers.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 // Package autostart manage start at login unit files for darwin/linux package autostart import ( "context" "errors" "fmt" "os" "path/filepath" "runtime" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/textutil" ) type notSupportedManager struct{} var _ autoStartManager = (*notSupportedManager)(nil) var ErrNotSupported = fmt.Errorf("autostart is not supported on %s", runtime.GOOS) func (*notSupportedManager) IsRegistered(_ context.Context, _ *limatype.Instance) (bool, error) { return false, ErrNotSupported } func (*notSupportedManager) RegisterToStartAtLogin(_ context.Context, _ *limatype.Instance) error { return ErrNotSupported } func (*notSupportedManager) UnregisterFromStartAtLogin(_ context.Context, _ *limatype.Instance) error { return ErrNotSupported } func (*notSupportedManager) AutoStartedIdentifier() string { return "" } func (*notSupportedManager) RequestStart(_ context.Context, _ *limatype.Instance) error { return ErrNotSupported } func (*notSupportedManager) RequestStop(_ context.Context, _ *limatype.Instance) (bool, error) { return false, ErrNotSupported } // TemplateFileBasedManager is an autostart manager that uses a template file to create the autostart entry. type TemplateFileBasedManager struct { enabler func(ctx context.Context, enable bool, instName string) error filePath func(instName string) string template string autoStartedIdentifier func() string requestStart func(ctx context.Context, inst *limatype.Instance) error requestStop func(ctx context.Context, inst *limatype.Instance) (bool, error) } var _ autoStartManager = (*TemplateFileBasedManager)(nil) func (t *TemplateFileBasedManager) IsRegistered(_ context.Context, inst *limatype.Instance) (bool, error) { if t.filePath == nil { return false, errors.New("no filePath function available") } autostartFilePath := t.filePath(inst.Name) if _, err := os.Stat(autostartFilePath); err != nil { if os.IsNotExist(err) { return false, nil } return false, err } return true, nil } func (t *TemplateFileBasedManager) RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error { if _, err := t.IsRegistered(ctx, inst); err != nil { return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) } content, err := t.renderTemplate(inst.Name, inst.Dir, os.Executable) if err != nil { return fmt.Errorf("failed to render the autostart entry for instance %q: %w", inst.Name, err) } entryFilePath := t.filePath(inst.Name) if err := os.MkdirAll(filepath.Dir(entryFilePath), os.ModePerm); err != nil { return fmt.Errorf("failed to create the directory for the autostart entry for instance %q: %w", inst.Name, err) } if err := os.WriteFile(entryFilePath, content, 0o644); err != nil { return fmt.Errorf("failed to write the autostart entry for instance %q: %w", inst.Name, err) } if t.enabler != nil { return t.enabler(ctx, true, inst.Name) } return nil } func (t *TemplateFileBasedManager) UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error { if registered, err := t.IsRegistered(ctx, inst); err != nil { return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) } else if !registered { return nil } if t.enabler != nil { if err := t.enabler(ctx, false, inst.Name); err != nil { return fmt.Errorf("failed to disable the autostart entry for instance %q: %w", inst.Name, err) } } if err := os.Remove(t.filePath(inst.Name)); err != nil { return fmt.Errorf("failed to remove the autostart entry for instance %q: %w", inst.Name, err) } return nil } func (t *TemplateFileBasedManager) renderTemplate(instName, workDir string, getExecutable func() (string, error)) ([]byte, error) { if t.template == "" { return nil, errors.New("no template available") } selfExeAbs, err := getExecutable() if err != nil { return nil, err } return textutil.ExecuteTemplate( t.template, map[string]string{ "Binary": selfExeAbs, "Instance": instName, "WorkDir": workDir, }) } func (t *TemplateFileBasedManager) AutoStartedIdentifier() string { if t.autoStartedIdentifier != nil { return t.autoStartedIdentifier() } return "" } func (t *TemplateFileBasedManager) RequestStart(ctx context.Context, inst *limatype.Instance) error { if t.requestStart == nil { return errors.New("no RequestStart function available") } return t.requestStart(ctx, inst) } func (t *TemplateFileBasedManager) RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { if t.requestStop == nil { return false, errors.New("no RequestStop function available") } return t.requestStop(ctx, inst) } ================================================ FILE: pkg/autostart/managers_darwin.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package autostart import "github.com/lima-vm/lima/v2/pkg/autostart/launchd" // Manager returns the autostart manager for Darwin. func Manager() autoStartManager { return &TemplateFileBasedManager{ filePath: launchd.GetPlistPath, template: launchd.Template, enabler: launchd.EnableDisableService, autoStartedIdentifier: launchd.AutoStartedServiceName, requestStart: launchd.RequestStart, requestStop: launchd.RequestStop, } } ================================================ FILE: pkg/autostart/managers_linux.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package autostart import "github.com/lima-vm/lima/v2/pkg/autostart/systemd" // Manager returns the autostart manager for Linux. func Manager() autoStartManager { if systemd.IsRunningSystemd() { return &TemplateFileBasedManager{ filePath: systemd.GetUnitPath, template: systemd.Template, enabler: systemd.EnableDisableUnit, autoStartedIdentifier: systemd.AutoStartedUnitName, requestStart: systemd.RequestStart, requestStop: systemd.RequestStop, } } // TODO: add support for non-systemd Linux distros return ¬SupportedManager{} } ================================================ FILE: pkg/autostart/managers_others.go ================================================ //go:build !darwin && !linux // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package autostart // Manager returns a notSupportedManager for unsupported OSes. func Manager() autoStartManager { return ¬SupportedManager{} } ================================================ FILE: pkg/autostart/systemd/lima-vm@INSTANCE.service ================================================ [Unit] Description=Lima - Linux virtual machines, with a focus on running containers. Documentation=man:lima(1) [Service] ExecStart={{.Binary}} start %i --foreground WorkingDirectory=%h Type=simple TimeoutSec=10 Restart=on-failure [Install] WantedBy=default.target ================================================ FILE: pkg/autostart/systemd/systemd.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package systemd import ( "context" _ "embed" "fmt" "os" "os/exec" "path/filepath" "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/limatype" ) //go:embed lima-vm@INSTANCE.service var Template string // GetUnitPath returns the path to the systemd unit file for the given instance name. func GetUnitPath(instName string) string { // Use instance name as argument to systemd service // Instance name available in unit file as %i xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") if xdgConfigHome == "" { xdgConfigHome = filepath.Join(os.Getenv("HOME"), ".config") } return fmt.Sprintf("%s/systemd/user/%s", xdgConfigHome, UnitNameFrom(instName)) } // UnitNameFrom returns the systemd service name for the given instance name. func UnitNameFrom(instName string) string { return fmt.Sprintf("lima-vm@%s.service", instName) } // EnableDisableUnit enables or disables the systemd service for the given instance name. func EnableDisableUnit(ctx context.Context, enable bool, instName string) error { action := "enable" if !enable { action = "disable" } return systemctl(ctx, "--user", action, UnitNameFrom(instName)) } func systemctl(ctx context.Context, args ...string) error { cmd := exec.CommandContext(ctx, "systemctl", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logrus.Debugf("running command: %v", cmd.Args) return cmd.Run() } // AutoStartedUnitName returns the systemd service name if the instance is started by systemd. func AutoStartedUnitName() string { return CurrentUnitName() } func RequestStart(ctx context.Context, inst *limatype.Instance) error { if err := systemctl(ctx, "--user", "start", UnitNameFrom(inst.Name)); err != nil { return fmt.Errorf("failed to start the instance %q via systemctl: %w", inst.Name, err) } return nil } func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { if inst.AutoStartedIdentifier == UnitNameFrom(inst.Name) { logrus.Infof("Stopping the instance %q started by systemd", inst.Name) if err := systemctl(ctx, "--user", "stop", inst.AutoStartedIdentifier); err != nil { return false, fmt.Errorf("failed to stop the instance %q via systemctl: %w", inst.Name, err) } return true, nil } return false, nil } ================================================ FILE: pkg/autostart/systemd/systemd_linux.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package systemd import ( "github.com/coreos/go-systemd/v22/util" "github.com/sirupsen/logrus" ) func CurrentUnitName() string { unit, err := util.CurrentUnitName() if err != nil { logrus.WithError(err).Debug("cannot determine current systemd unit name") } return unit } func IsRunningSystemd() bool { return util.IsRunningSystemd() } ================================================ FILE: pkg/autostart/systemd/systemd_others.go ================================================ //go:build !linux // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package systemd func CurrentUnitName() string { return "" } func IsRunningSystemd() bool { return false } ================================================ FILE: pkg/autostart/systemd/systemd_test.go ================================================ //go:build !windows // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package systemd import ( "strings" "testing" "gotest.tools/v3/assert" ) func TestGetUnitPath(t *testing.T) { tests := []struct { Name string InstanceName string Expected string }{ { Name: "linux with docker instance name", InstanceName: "docker", Expected: ".config/systemd/user/lima-vm@docker.service", }, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { assert.Check(t, strings.HasSuffix(GetUnitPath(tt.InstanceName), tt.Expected)) }) } } ================================================ FILE: pkg/bicopy/bicopy.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 // From https://raw.githubusercontent.com/norouter/norouter/v0.6.5/pkg/agent/bicopy/bicopy.go /* Copyright (C) NoRouter authors. Copyright (C) libnetwork authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package bicopy import ( "io" "sync" "github.com/sirupsen/logrus" ) // Bicopy is from https://github.com/rootless-containers/rootlesskit/blob/v0.10.1/pkg/port/builtin/parent/tcp/tcp.go#L73-L104 // (originally from libnetwork, Apache License 2.0). func Bicopy(x, y io.ReadWriter, quit <-chan struct{}) { type closeReader interface { CloseRead() error } type closeWriter interface { CloseWrite() error } var wg sync.WaitGroup broker := func(to, from io.ReadWriter) { if _, err := io.Copy(to, from); err != nil { logrus.WithError(err).Debug("failed to call io.Copy") } if fromCR, ok := from.(closeReader); ok { if err := fromCR.CloseRead(); err != nil { logrus.WithError(err).Debug("failed to call CloseRead") } } if toCW, ok := to.(closeWriter); ok { if err := toCW.CloseWrite(); err != nil { logrus.WithError(err).Debug("failed to call CloseWrite") } } wg.Done() } wg.Add(2) go broker(x, y) go broker(y, x) finish := make(chan struct{}) go func() { wg.Wait() close(finish) }() select { case <-quit: case <-finish: } if xCloser, ok := x.(io.Closer); ok { if err := xCloser.Close(); err != nil { logrus.WithError(err).Debug("failed to call xCloser.Close") } } if yCloser, ok := y.(io.Closer); ok { if err := yCloser.Close(); err != nil { logrus.WithError(err).Debug("failed to call yCloser.Close") } } <-finish // TODO: return copied bytes } ================================================ FILE: pkg/cacheutil/cacheutil.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package cacheutil import ( "context" "fmt" "path" "github.com/lima-vm/lima/v2/pkg/downloader" "github.com/lima-vm/lima/v2/pkg/fileutils" "github.com/lima-vm/lima/v2/pkg/limatype" ) // NerdctlArchive returns the basename of the archive. func NerdctlArchive(y *limatype.LimaYAML) string { if *y.Containerd.System || *y.Containerd.User { for _, f := range y.Containerd.Archives { if f.Arch == *y.Arch { return path.Base(f.Location) } } } return "" } // EnsureNerdctlArchiveCache prefetches the nerdctl-full-VERSION-GOOS-GOARCH.tar.gz archive // into the cache before launching the hostagent process, so that we can show the progress in tty. // https://github.com/lima-vm/lima/issues/326 func EnsureNerdctlArchiveCache(ctx context.Context, y *limatype.LimaYAML, created bool) (string, error) { if !*y.Containerd.System && !*y.Containerd.User { // nerdctl archive is not needed return "", nil } errs := make([]error, len(y.Containerd.Archives)) for i, f := range y.Containerd.Archives { // Skip downloading again if the file is already in the cache if created && f.Arch == *y.Arch && !downloader.IsLocal(f.Location) { path, err := fileutils.CachedFile(f) if err == nil { return path, nil } } path, err := fileutils.DownloadFile(ctx, "", f, false, "the nerdctl archive", *y.Arch) if err != nil { errs[i] = err continue } if path == "" { if downloader.IsLocal(f.Location) { return f.Location, nil } return "", fmt.Errorf("cache did not contain %q", f.Location) } return path, nil } return "", fileutils.Errors(errs) } ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.FreeBSD/05-lima-mounts.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # Populate mounts from the cidata. # This script is a workaround until nuageinit supports mounts. set -eux if ! grep -q '#LIMA-START' /etc/fstab; then "${LIMA_CIDATA_MNT}"/util.FreeBSD/print_cidata_fstab.lua >/etc/fstab.lima.tmp if [ -s /etc/fstab.lima.tmp ]; then { echo "#LIMA-START" cat /etc/fstab.lima.tmp echo "#LIMA-END" } >>/etc/fstab fi rm -f /etc/fstab.lima.tmp fi # Run mkdir on every boot, as the mount points may be on tmpfs awk '/^[^#]/ && $2 != "none" {print $2}' /etc/fstab | while IFS= read -r line; do mkdir -p "${line}" done mount -a ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-alpine-user-group.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 test -f /etc/alpine-release || exit 0 # Make sure that root is in the sudoers file. # This is needed to run the user provisioning scripts. SUDOERS=/etc/sudoers.d/00-root-user if [ ! -f $SUDOERS ]; then echo "root ALL=(ALL) NOPASSWD:ALL" >$SUDOERS chmod 660 $SUDOERS fi # Remove the user embedded in the image, # and use cloud-init for users and groups. if [ "$LIMA_CIDATA_USER" != "alpine" ]; then if [ "$(id -u alpine 2>&1)" = "1000" ]; then userdel alpine rmdir /home/alpine cloud-init clean --logs reboot fi fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-check-rtc-and-wait-ntp.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu # In vz, the VM lacks an RTC when booting with a kernel image (see: https://developer.apple.com/forums/thread/760344). # This causes incorrect system time until NTP synchronizes it, leading to TLS errors. # To avoid TLS errors, this script waits for NTP synchronization if RTC is unavailable. test ! -c /dev/rtc0 || exit 0 # This script is intended for services running with systemd. command -v systemctl >/dev/null 2>&1 || exit 0 echo_with_time_usec() { time_usec=$(timedatectl show --property=TimeUSec) echo "${time_usec}, ${1}" } # For the first boot, where the above setting is not yet active, wait for NTP synchronization here. max_retry=60 retry=0 until ntp_synchronized=$(timedatectl show --property=NTPSynchronized --value) && [ "${ntp_synchronized}" = "yes" ] || [ "${retry}" -gt "${max_retry}" ]; do if [ "${retry}" -eq 0 ]; then # If /dev/rtc is not available, the system time set during the Linux kernel build is used. # The larger the difference between this system time and the NTP server time, the longer the NTP synchronization will take. # By setting the system time to the modification time of the reference file, which is likely to be closer to the actual time, reference_file="${LIMA_CIDATA_MNT:-/mnt/lima-cidata}/user-data" [ -f "${reference_file}" ] || reference_file="${0}" # the NTP synchronization time can be shortened. echo_with_time_usec "Setting the system time to the modification time of ${reference_file}." # To set the time to a specified time, it is necessary to stop systemd-timesyncd. systemctl stop systemd-timesyncd # Since `timedatectl set-time` fails if systemd-timesyncd is not stopped, # ensure that it is completely stopped before proceeding. until pid_of_timesyncd=$(systemctl show systemd-timesyncd --property=MainPID --value) && [ "${pid_of_timesyncd}" -eq 0 ]; do echo_with_time_usec "Waiting for systemd-timesyncd to stop..." sleep 1 done # Set the system time to the modification time of the reference file. modification_time=$(stat -c %y "${reference_file}") echo_with_time_usec "Setting the system time to ${modification_time}." timedatectl set-time "${modification_time}" # Restart systemd-timesyncd systemctl start systemd-timesyncd else echo_with_time_usec "Waiting for NTP synchronization..." fi retry=$((retry + 1)) sleep 1 done # Print the result of NTP synchronization ntp_message=$(timedatectl show-timesync --property=NTPMessage) if [ "${ntp_synchronized}" = "yes" ]; then echo_with_time_usec "NTP synchronization complete." echo "${ntp_message}" else echo_with_time_usec "NTP synchronization timed out." echo "${ntp_message}" exit 1 fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-guest-home.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu # The default guest home directory is "/home/${LIMA_CIDATA_USER}.guest" since Lima 2.1.0. # The path was previously "/home/${LIMA_CIDATA_USER}.linux". if [ -d "/home/${LIMA_CIDATA_USER}.guest" ] && [ ! -e "/home/${LIMA_CIDATA_USER}.linux" ]; then ln -s "${LIMA_CIDATA_USER}.guest" "/home/${LIMA_CIDATA_USER}.linux" fi if [ -d "/home/${LIMA_CIDATA_USER}.linux" ] && [ ! -e "/home/${LIMA_CIDATA_USER}.guest" ]; then ln -s "${LIMA_CIDATA_USER}.linux" "/home/${LIMA_CIDATA_USER}.guest" fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-modprobe.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # Load modules as soon as the cloud-init starts up. # Because Arch Linux removes kernel module files when the kernel package was updated during running cloud-init :( set -eu command -v modprobe >/dev/null 2>&1 || exit 0 for f in \ fuse \ tun tap \ bridge veth \ ip_tables ip6_tables iptable_nat ip6table_nat iptable_filter ip6table_filter \ nf_tables \ x_tables xt_MASQUERADE xt_addrtype xt_comment xt_conntrack xt_mark xt_multiport xt_nat xt_tcpudp \ overlay; do echo "Loading kernel module \"$f\"" if ! modprobe "$f"; then echo >&2 "Failed to load \"$f\" (negligible if it is built-in the kernel)" fi done ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-reboot-if-required.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux [ "$LIMA_CIDATA_UPGRADE_PACKAGES" = "1" ] || exit 0 # Check if cloud-init forgot to reboot_if_required # (only implemented for apt at the moment, not dnf) if command -v dnf >/dev/null 2>&1; then # dnf-utils needs to be installed, for needs-restarting if dnf -h needs-restarting >/dev/null 2>&1; then # check-update returns "false" (100) if updates (!) set +e dnf check-update >/dev/null if [ "$?" != "1" ]; then # needs-restarting messages are translated _() export LC_ALL=C.UTF-8 logfile=$(mktemp) # needs-restarting returns "false" if needed (!) set -o pipefail dnf needs-restarting -r | tee "$logfile" if [ "$?" = "1" ]; then if grep -q "Reboot is required" "$logfile"; then systemctl reboot fi fi rm "$logfile" fi fi fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/01-alpine-ash-as-bash.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # This script pretends that /bin/ash can be used as /bin/bash, so all following # cloud-init scripts can use `#!/bin/bash` and `set -o pipefail`. test -f /etc/alpine-release || exit 0 # If bash already exists, do nothing. test -x /bin/bash && exit 0 # Redirect bash to ash (built with CONFIG_ASH_BASH_COMPAT) and hope for the best :) # It does support `set -o pipefail`, but not `[[`. # /bin/bash can't be a symlink because /bin/ash is a symlink to /bin/busybox cat >/bin/bash <<'EOF' #!/bin/sh exec /bin/ash "$@" EOF chmod +x /bin/bash ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/04-persistent-data-volume.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # bash is used for enabling `set -o pipefail`. # NOTE: On Alpine, /bin/bash is ash with ASH_BASH_COMPAT, not GNU bash set -eux -o pipefail # Restrict the rest of this script to Alpine until it has been tested with other distros test -f /etc/alpine-release || exit 0 # Nothing to do unless we are running from a ramdisk [ "$(awk '$2 == "/" {print $3}' /proc/mounts)" != "tmpfs" ] && exit 0 # Data directories that should be persisted across reboots DATADIRS="/etc /home /root /tmp /usr/local /var/lib" # Prepare mnt.sh (used for restoring mounts later) echo "#!/bin/sh" >/mnt.sh echo "set -eux" >>/mnt.sh for DIR in ${DATADIRS}; do while IFS= read -r LINE; do [ -z "$LINE" ] && continue MNTDEV="$(echo "${LINE}" | awk '{print $1}')" # unmangle " \t\n\\#" # https://github.com/torvalds/linux/blob/v6.6/fs/proc_namespace.c#L89 MNTPNT="$(echo "${LINE}" | awk '{print $2}' | sed -e 's/\\040/ /g; s/\\011/\t/g; s/\\012/\n/g; s/\\134/\\/g; s/\\043/#/g')" # Ignore if MNTPNT is neither DIR nor a parent directory of DIR. # It is not a parent if MNTPNT doesn't start with DIR, or the first # character after DIR isn't a slash. WITHOUT_DIR="${MNTPNT#"$DIR"}" # shellcheck disable=SC2166 [ "$MNTPNT" != "$DIR" ] && [ "$MNTPNT" == "$WITHOUT_DIR" -o "${WITHOUT_DIR::1}" != "/" ] && continue MNTTYPE="$(echo "${LINE}" | awk '{print $3}')" [ "${MNTTYPE}" = "ext4" ] && continue [ "${MNTTYPE}" = "tmpfs" ] && continue MNTOPTS="$(echo "${LINE}" | awk '{print $4}')" if [ "${MNTTYPE}" = "9p" ]; then # https://github.com/torvalds/linux/blob/v6.6/fs/9p/v9fs.h#L61 MNTOPTS="$(echo "${MNTOPTS}" | sed -e 's/cache=8f,/cache=fscache,/; s/cache=f,/cache=loose,/; s/cache=5,/cache=mmap,/; s/cache=1,/cache=readahead,/; s/cache=0,/cache=none,/')" fi # Before mv, unmount filesystems (virtiofs, 9p, etc.) below "${DIR}", otherwise host mounts will be wiped out # https://github.com/rancher-sandbox/rancher-desktop/issues/6582 umount "${MNTPNT}" || exit 1 MNTPNT=${MNTPNT//\\/\\\\} MNTPNT=${MNTPNT//\"/\\\"} echo "mount -t \"${MNTTYPE}\" -o \"${MNTOPTS}\" \"${MNTDEV}\" \"${MNTPNT}\"" >>/mnt.sh done /dev/null 2>&1 && command -v resize2fs >/dev/null 2>&1; then # Automatically expand the data volume filesystem growpart "$DATA_DISK" 1 || true # Only resize when filesystem is in a healthy state if e2fsck -f -p /dev/disk/by-label/data-volume; then resize2fs /dev/disk/by-label/data-volume || true fi fi # Mount data volume mount -t ext4 /dev/disk/by-label/data-volume /mnt/data # Update /etc files that might have changed during this boot cp /etc/network/interfaces /mnt/data/etc/network/ cp /etc/resolv.conf /mnt/data/etc/ if [ -f /etc/localtime ]; then # Preserve symlink cp -d /etc/localtime /mnt/data/etc/ # setup-timezone copies the single zoneinfo file into /etc/zoneinfo and targets the symlink there if [ -d /etc/zoneinfo ]; then rm -rf /mnt/data/etc/zoneinfo cp -r /etc/zoneinfo /mnt/data/etc fi fi if [ -f /etc/timezone ]; then cp /etc/timezone /mnt/data/etc/ fi # TODO there are probably others that should be updated as well else # Find an unpartitioned disk and create data-volume DISKS=$(lsblk --list --noheadings --output name,type | awk '$2 == "disk" {print $1}') for DISK in ${DISKS}; do IN_USE=false # Looking for a disk that is not mounted or partitioned # shellcheck disable=SC2013 for PART in $(awk '/^\/dev\// {gsub("/dev/", ""); print $1}' /proc/mounts); do if [ "${DISK}" == "${PART}" ] || [ -e /sys/block/"${DISK}"/"${PART}" ]; then IN_USE=true break fi done if [ "${IN_USE}" == "false" ]; then echo 'type=83' | sfdisk --label dos /dev/"${DISK}" PART=$(lsblk --list /dev/"${DISK}" --noheadings --output name,type | awk '$2 == "part" {print $1}') mkfs.ext4 -L data-volume /dev/"${PART}" mount -t ext4 /dev/disk/by-label/data-volume /mnt/data # setup apk package cache mkdir -p /mnt/data/apk/cache mkdir -p /etc/apk ln -s /mnt/data/apk/cache /etc/apk/cache # Move all persisted directories to the data volume for DIR in ${DATADIRS}; do DEST="/mnt/data$(dirname "${DIR}")" mkdir -p "${DIR}" "${DEST}" mv "${DIR}" "${DEST}" done # Make sure all data moved to the persistent volume has been committed to disk sync break fi done fi for DIR in ${DATADIRS}; do if [ -d /mnt/data"${DIR}" ]; then mkdir -p "${DIR}" mount --bind /mnt/data"${DIR}" "${DIR}" fi done # Remount submounts on top of the new ${DIR} /mnt.sh # Reinstall packages from /mnt/data/apk/cache into the RAM disk apk fix --no-network ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-disks.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux -o pipefail test "$LIMA_CIDATA_DISKS" -gt 0 || exit 0 get_disk_var() { diskvarname="LIMA_CIDATA_DISK_${1}_${2}" eval echo \$"$diskvarname" } for i in $(seq 0 $((LIMA_CIDATA_DISKS - 1))); do DISK_NAME="$(get_disk_var "$i" "NAME")" DEVICE_NAME="$(get_disk_var "$i" "DEVICE")" FORMAT_DISK="$(get_disk_var "$i" "FORMAT")" FORMAT_FSTYPE="$(get_disk_var "$i" "FSTYPE")" FORMAT_FSARGS="$(get_disk_var "$i" "FSARGS")" test -n "$FORMAT_DISK" || FORMAT_DISK=true test -n "$FORMAT_FSTYPE" || FORMAT_FSTYPE=ext4 # first time setup if [[ ! -b "/dev/disk/by-label/lima-${DISK_NAME}" ]]; then if $FORMAT_DISK; then if [ "$FORMAT_FSTYPE" == "swap" ]; then echo 'type=swap' | sfdisk --label gpt "/dev/${DEVICE_NAME}" # shellcheck disable=SC2086 mkswap $FORMAT_FSARGS -L "lima-${DISK_NAME}" "/dev/${DEVICE_NAME}1" else echo 'type=linux' | sfdisk --label gpt "/dev/${DEVICE_NAME}" # shellcheck disable=SC2086 mkfs.$FORMAT_FSTYPE $FORMAT_FSARGS -L "lima-${DISK_NAME}" "/dev/${DEVICE_NAME}1" fi fi fi if [ "$FORMAT_FSTYPE" == "swap" ]; then swapon "/dev/${DEVICE_NAME}1" else mkdir -p "/mnt/lima-${DISK_NAME}" mount -t "$FORMAT_FSTYPE" "/dev/${DEVICE_NAME}1" "/mnt/lima-${DISK_NAME}" fi if command -v growpart >/dev/null 2>&1 && command -v resize2fs >/dev/null 2>&1; then growpart "/dev/${DEVICE_NAME}" 1 || true # Only resize when filesystem is in a healthy state if command -v "fsck.$FORMAT_FSTYPE" -f -p "/dev/disk/by-label/lima-${DISK_NAME}"; then if [[ $FORMAT_FSTYPE == "ext2" || $FORMAT_FSTYPE == "ext3" || $FORMAT_FSTYPE == "ext4" ]]; then resize2fs "/dev/disk/by-label/lima-${DISK_NAME}" || true elif [ "$FORMAT_FSTYPE" == "xfs" ]; then xfs_growfs "/dev/disk/by-label/lima-${DISK_NAME}" || true else echo >&2 "WARNING: unknown fs '$FORMAT_FSTYPE'. FS will not be grew up automatically" fi fi fi done ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-mounts.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux -o pipefail # Check if mount type is virtiofs and vm type as vz if ! [[ ${LIMA_CIDATA_VMTYPE} == "vz" && ${LIMA_CIDATA_MOUNTTYPE} == "virtiofs" ]]; then exit 0 fi # Update fstab entries and unmount/remount the volumes with secontext options # when selinux is enabled in kernel if [ -d /sys/fs/selinux ]; then LABEL_BIN="system_u:object_r:bin_t:s0" LABEL_NFS="system_u:object_r:nfs_t:s0" # shellcheck disable=SC2013 for line in $(grep -n virtiofs /proc/sys/fs/binfmt_misc/rosetta fi umount "${TAG}" mount -t virtiofs "${TAG}" "${MOUNT_POINT}" -o "${OPTIONS}" fi fi done fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/06-enable-mdns-on-systemd.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux -o pipefail # Do nothing on OpenRC systems (e.g., Alpine Linux) if [[ -f /sbin/openrc-run ]]; then exit 0 fi # It depends on systemd-resolved command -v systemctl >/dev/null 2>&1 || exit 0 command -v resolvectl >/dev/null 2>&1 || exit 0 # Configure systemd-resolved to enable mDNS resolution globally enable_mdns_conf_path=/etc/systemd/resolved.conf.d/00-lima-enable-mdns.conf enable_mdns_conf_content="[Resolve] MulticastDNS=yes " # Create /etc/systemd/resolved.conf.d/00-lima-enable-mdns.conf if its content is different if [ "$(echo "${enable_mdns_conf_content}" | sha256sum)" != "$(sha256sum <"${enable_mdns_conf_path}" 2>/dev/null)" ]; then mkdir -p "$(dirname "${enable_mdns_conf_path}")" echo "${enable_mdns_conf_content}" >"${enable_mdns_conf_path}" systemctl daemon-reload systemctl restart systemd-resolved.service fi # On Ubuntu, systemd.network's configuration won't work. # See: https://unix.stackexchange.com/a/652582 # So we need to enable mDNS per-link using resolvectl. for iface in $(resolvectl status | sed -n -E 's/^Link +[0-9]+ \(([^)]+)\)/\1/p'); do # This setting is volatile and will be lost on reboot, so we need to set it every time resolvectl mdns "${iface}" yes done ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/06-etc-hosts.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux -o pipefail # Define host.lima.internal in case the hostResolver is disabled. When using # the hostResolver, the name is provided by the lima resolver itself because # it doesn't have access to /etc/hosts inside the VM. sed -i '/host.lima.internal/d' /etc/hosts echo -e "${LIMA_CIDATA_SLIRP_GATEWAY}\thost.lima.internal" >>/etc/hosts ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/07-etc-environment.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux # /etc/environment must be written after 04-persistent-data-volume.sh has run to # make sure the changes on a restart are applied to the persisted version. orig=$(test ! -f /etc/environment || cat /etc/environment) if [ -e /etc/environment ]; then sed -i '/#LIMA-START/,/#LIMA-END/d' /etc/environment fi cat "${LIMA_CIDATA_MNT}/etc_environment" >>/etc/environment # It is possible that a requirements script has started an ssh session before # /etc/environment was updated, so we need to kill it to make sure it will # restart with the updated environment before "linger" is being enabled. if command -v loginctl >/dev/null 2>&1 && [ "${orig}" != "$(cat /etc/environment)" ]; then loginctl terminate-user "${LIMA_CIDATA_USER}" || true fi # Signal that provisioning is done. The instance ID changes on every boot, # so any value from a previous boot cycle will be different. echo "${LIMA_CIDATA_IID}" >/run/lima-ssh-ready ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/08-shell-prompt.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux # This script is only intended for the default.yaml image, which is based on Ubuntu LTS if [ "${LIMA_CIDATA_NAME}" = "default" ] && command -v patch >/dev/null 2>&1 && grep -q color_prompt "${LIMA_CIDATA_HOME}/.bashrc"; then ! grep -q "^# Lima PS1" "${LIMA_CIDATA_HOME}/.bashrc" || exit 0 # Change the default shell prompt from "green" to "lime green" (#32CD32) patch --forward -r - "${LIMA_CIDATA_HOME}/.bashrc" <<'EOF' @@ -37,7 +37,11 @@ # set a fancy prompt (non-color, unless we know we "want" color) case "$TERM" in - xterm-color|*-256color) color_prompt=yes;; + xterm-color) color_prompt=yes;; + *-256color) color_prompt=256;; +esac +case "$COLORTERM" in + truecolor) color_prompt=true;; esac # uncomment for a colored prompt, if the terminal has the capability; turned @@ -56,7 +60,12 @@ fi fi -if [ "$color_prompt" = yes ]; then +# Lima PS1: set color to lime green +if [ "$color_prompt" = true ]; then + PS1='${debian_chroot:+($debian_chroot)}\[\033[38;2;50;205;50m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' +elif [ "$color_prompt" = 256 ]; then + PS1='${debian_chroot:+($debian_chroot)}\[\033[38;5;77m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' +elif [ "$color_prompt" = yes ]; then PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' else PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' EOF fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/09-host-dns-setup.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux readonly chain=LIMADNS chain_exists() { iptables --table nat -n --list "${chain}" >/dev/null 2>&1 } # Wait until iptables has been installed; 35-configure-packages.sh will call this script again if command -v iptables >/dev/null 2>&1; then if ! chain_exists; then iptables --table nat --new-chain ${chain} iptables --table nat --insert PREROUTING 1 --jump "${chain}" iptables --table nat --insert OUTPUT 1 --jump "${chain}" fi # Remove old rules iptables --table nat --flush ${chain} # Add rules for the existing ip:port if [ -n "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" -ne 0 ]; then iptables --table nat --append "${chain}" --destination "${LIMA_CIDATA_SLIRP_DNS}" --protocol udp --dport 53 --jump DNAT \ --to-destination "${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" fi if [ -n "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" -ne 0 ]; then iptables --table nat --append "${chain}" --destination "${LIMA_CIDATA_SLIRP_DNS}" --protocol tcp --dport 53 --jump DNAT \ --to-destination "${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" fi fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/10-alpine-prep.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux # This script prepares Alpine for lima; there is nothing in here for other distros test -f /etc/alpine-release || exit 0 # Configure apk repos BRANCH=edge VERSION_ID=$(awk -F= '$1=="VERSION_ID" {print $2}' /etc/os-release) case ${VERSION_ID} in *_alpha* | *_beta*) BRANCH=edge ;; *.*.*) BRANCH=v${VERSION_ID%.*} ;; esac for REPO in main community; do URL="https://dl-cdn.alpinelinux.org/alpine/${BRANCH}/${REPO}" if ! grep -q "^${URL}$" /etc/apk/repositories; then echo "${URL}" >>/etc/apk/repositories fi done # Alpine comes with doas instead of sudo if ! command -v sudo >/dev/null 2>&1; then apk add sudo fi # Alpine doesn't use PAM so we need to explicitly allow public key auth usermod -p '*' "${LIMA_CIDATA_USER}" # Alpine disables TCP forwarding, which is needed by the lima-guestagent sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config # Enable PAM so as to load /etc/environment via pam_env sed -i 's/#UsePAM no/UsePAM yes/g' /etc/ssh/sshd_config rc-service --ifstarted sshd reload # mount /sys/fs/cgroup rc-service cgroups start # `limactl stop` tells acpid to powerdown rc-update add acpid rc-service acpid start ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/11-colorterm-environment.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux if [ -d /etc/ssh/sshd_config.d ]; then if [ -e /etc/ssh/sshd_config.d/10-acceptenv-colorterm.conf ]; then exit 0 fi # accept any incoming COLORTERM environment variable echo "AcceptEnv COLORTERM" >/etc/ssh/sshd_config.d/10-acceptenv-colorterm.conf elif [ -e /etc/ssh/sshd_config ]; then if grep -q "COLORTERM" /etc/ssh/sshd_config; then exit 0 fi # accept any incoming COLORTERM environment variable sed -i 's/^AcceptEnv LANG LC_\*$/AcceptEnv COLORTERM LANG LC_*/' /etc/ssh/sshd_config else exit 0 fi if [ -f /sbin/openrc-run ]; then if [ -f etc/init.d/ssh ]; then rc-service --ifstarted ssh reload elif [ -f etc/init.d/sshd ]; then rc-service --ifstarted sshd reload fi elif command -v systemctl >/dev/null 2>&1; then if systemctl -q is-active ssh; then systemctl reload ssh elif systemctl -q is-active sshd; then systemctl reload sshd fi fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/20-rootless-base.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux # This script does not work unless systemd is available command -v systemctl >/dev/null 2>&1 || exit 0 if [ -O "${LIMA_CIDATA_HOME}" ]; then # Fix ownership of the user home directory when created by root. # In cases where mount points exist in the user's home directory, the home directory and # the mount points are created by root before the user is created. This leads to the home # directory being owned by root. # Following commands fix the ownership of the home directory and its contents (on the same filesystem) # is updated to the correct user. # shellcheck disable=SC2046 # it fails if find results are quoted. chown "${LIMA_CIDATA_USER}" $(find "${LIMA_CIDATA_HOME}" -xdev) || true # Ignore errors because changing owner of the mount points may fail but it is not critical. fi # Set up env for f in .profile .bashrc .zshrc; do if ! grep -q "# Lima BEGIN" "${LIMA_CIDATA_HOME}/$f"; then cat >>"${LIMA_CIDATA_HOME}/$f" <>"${LIMA_CIDATA_HOME}/$f" <>"${LIMA_CIDATA_HOME}/$f" <"/etc/systemd/system/user@.service.d/lima.conf" <>"${sysctl_conf}" fi echo "net.ipv4.ping_group_range = 0 2147483647" >>"${sysctl_conf}" echo "net.ipv4.ip_unprivileged_port_start=0" >>"${sysctl_conf}" sysctl --system fi # Set up subuid for f in /etc/subuid /etc/subgid; do # systemd-homed expects the subuid range to be within 524288-1878982656 (0x80000-0x6fff0000). # See userdbctl. # 1073741824 (1G) is just an arbitrary number. # 1073741825-1878982656 is left blank for additional accounts. subuid_begin=524288 # https://github.com/moby/moby/issues/49810#issuecomment-2808108191 [ "${LIMA_CIDATA_UID}" -ge "${subuid_begin}" ] && subuid_begin="$((LIMA_CIDATA_UID + 1))" grep -qw "${LIMA_CIDATA_USER}" $f || echo "${LIMA_CIDATA_USER}:${subuid_begin}:1073741824" >>$f done # Start systemd session systemctl start systemd-logind.service loginctl enable-linger "${LIMA_CIDATA_USER}" ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/25-guestagent-base.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then # Create mount points # NOTE: Busybox sh does not support `for ((i=0;i<$N;i++))` form for f in $(seq 0 $((LIMA_CIDATA_MOUNTS - 1))); do mountpointvar="LIMA_CIDATA_MOUNTS_${f}_MOUNTPOINT" mountpoint="$(eval echo \$"$mountpointvar")" mkdir -p "${mountpoint}" gid=$(id -g "${LIMA_CIDATA_USER}") chown "${LIMA_CIDATA_UID}:${gid}" "${mountpoint}" done fi # Install or update the guestagent binary mkdir -p "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin guestagent_updated=false if [ "$(sha256sum <"${LIMA_CIDATA_MNT}"/lima-guestagent)" = "$(sha256sum <"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent 2>/dev/null)" ]; then echo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent is up-to-date" else install -m 755 "${LIMA_CIDATA_MNT}"/lima-guestagent "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent guestagent_updated=true fi # Launch the guestagent service if [ -f /sbin/openrc-run ]; then print_config() { # Convert .env to conf.d by wrapping values in double quotes. # Split the variable and value at the first "=" to handle cases where the value contains additional "=" characters. sed -E 's/^([^=]+)=(.*)/\1="\2"/' "${LIMA_CIDATA_MNT}/lima.env" } print_script() { # the openrc lima-guestagent service script cat <<-'EOF' #!/sbin/openrc-run supervisor=supervise-daemon log_file="${log_file:-/var/log/${RC_SVCNAME}.log}" err_file="${err_file:-${log_file}}" log_mode="${log_mode:-0644}" log_owner="${log_owner:-root:root}" supervise_daemon_args="${supervise_daemon_opts:---stderr \"${err_file}\" --stdout \"${log_file}\"}" name="lima-guestagent" description="Forward ports to the lima-hostagent" command=${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent command_args="daemon --debug=${LIMA_CIDATA_DEBUG} --vsock-port \"${LIMA_CIDATA_VSOCK_PORT}\" --virtio-port \"${LIMA_CIDATA_VIRTIO_PORT}\"" command_background=true pidfile="/run/lima-guestagent.pid" EOF } if [ "${guestagent_updated}" = "false" ] && [ "$(print_config | sha256sum)" = "$(sha256sum /dev/null)" ] && [ "$(print_script | sha256sum)" = "$(sha256sum /dev/null)" ]; then echo "lima-guestagent service already up-to-date" exit 0 fi print_config >/etc/conf.d/lima-guestagent print_script >/etc/init.d/lima-guestagent chmod 755 /etc/init.d/lima-guestagent rc-update add lima-guestagent default rc-service --ifstarted lima-guestagent restart # restart if running, otherwise do nothing rc-service --ifstopped lima-guestagent start # start if not running, otherwise do nothing else # Remove legacy systemd service rm -f "${LIMA_CIDATA_HOME}/.config/systemd/user/lima-guestagent.service" if [ "${LIMA_CIDATA_VSOCK_PORT}" != "0" ]; then sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --debug="${LIMA_CIDATA_DEBUG}" --guestagent-updated="${guestagent_updated}" --vsock-port "${LIMA_CIDATA_VSOCK_PORT}" elif [ "${LIMA_CIDATA_VIRTIO_PORT}" != "" ]; then sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --debug="${LIMA_CIDATA_DEBUG}" --guestagent-updated="${guestagent_updated}" --virtio-port "${LIMA_CIDATA_VIRTIO_PORT}" else sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --debug="${LIMA_CIDATA_DEBUG}" --guestagent-updated="${guestagent_updated}" fi fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/30-install-packages.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux INSTALL_IPTABLES=0 if [ "${LIMA_CIDATA_CONTAINERD_SYSTEM}" = 1 ] || [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ]; then INSTALL_IPTABLES=1 fi if [ "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" -ne 0 ] || [ "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" -ne 0 ]; then INSTALL_IPTABLES=1 fi # Install minimum dependencies # Run any user provided dependency scripts first if [ -d "${LIMA_CIDATA_MNT}"/provision.dependency ]; then echo "Detected dependency provisioning scripts, running before default dependency installation" CODE=0 for f in "${LIMA_CIDATA_MNT}"/provision.dependency/*; do if ! "$f"; then CODE=1 fi done if [ $CODE != 0 ]; then exit "$CODE" fi fi # apt-get detected through the first bytes of apt-get binary to ensure we're # matching to an actual binary and not a wrapper script. This case is an issue # on OpenSuse which wraps its own package manager in to a script named apt-get # to mimic certain options but doesn't offer full parameters compatibility # See : https://github.com/lima-vm/lima/pull/1014 if [ "${LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION}" = 1 ]; then echo "LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION is set, skipping regular dependency installation" exit 0 fi REMOUNT_VIRTIOFS=0 if head -c 4 "$(command -v apt-get)" | grep -qP '\x7fELF' >/dev/null 2>&1; then pkgs="" if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then if [ "${LIMA_CIDATA_MOUNTS}" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then pkgs="${pkgs} sshfs" fi fi if [ "${INSTALL_IPTABLES}" = 1 ] && [ ! -e /usr/sbin/iptables ]; then pkgs="${pkgs} iptables" fi if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ] && ! command -v newuidmap >/dev/null 2>&1; then pkgs="${pkgs} uidmap fuse3 dbus-user-session" fi if ! command -v rsync >/dev/null 2>&1; then pkgs="${pkgs} rsync" fi if [ -n "${pkgs}" ]; then DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND apt-get update # shellcheck disable=SC2086 apt-get install -y --no-upgrade --no-install-recommends -q ${pkgs} fi elif command -v dnf >/dev/null 2>&1; then pkgs="" extrapkgs="" if ! command -v tar >/dev/null 2>&1; then pkgs="${pkgs} tar" fi if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then if [ "${LIMA_CIDATA_MOUNTS}" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then # fuse-sshfs is not included in EL extrapkgs="${extrapkgs} fuse-sshfs" fi fi if [ "${INSTALL_IPTABLES}" = 1 ] && [ ! -e /usr/sbin/iptables ]; then pkgs="${pkgs} iptables" fi if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ]; then if ! command -v newuidmap >/dev/null 2>&1; then pkgs="${pkgs} shadow-utils" fi if ! command -v mount.fuse3 >/dev/null 2>&1; then pkgs="${pkgs} fuse3" fi fi if ! command -v rsync >/dev/null 2>&1; then pkgs="${pkgs} rsync" fi if [ -n "${pkgs}" ] || [ -n "${extrapkgs}" ]; then dnf_install_flags="-y --setopt=install_weak_deps=False" epel_install_flags="" if grep -q "Oracle Linux Server release 8" /etc/system-release; then # repo flag instead of enable repo to reduce metadata syncing on slow Oracle repos dnf_install_flags="${dnf_install_flags} --repo ol8_baseos_latest --repo ol8_codeready_builder" elif grep -q "release 8" /etc/system-release; then dnf_install_flags="${dnf_install_flags} --enablerepo powertools" elif grep -q "Oracle Linux Server release 9" /etc/system-release; then # shellcheck disable=SC2086 dnf install ${dnf_install_flags} oracle-epel-release-el9 dnf config-manager --disable ol9_developer_EPEL >/dev/null 2>&1 epel_install_flags="${epel_install_flags} --enablerepo ol9_developer_EPEL" elif grep -q "Oracle Linux Server release 10" /etc/system-release; then oraclelinux_version="$(awk '{print $5}' /etc/system-release)" oraclelinux_version_major=$(echo "$oraclelinux_version" | cut -d. -f1) oraclelinux_version_minor=$(echo "$oraclelinux_version" | cut -d. -f2) oraclelinux_epel_repo="ol${oraclelinux_version_major}_u${oraclelinux_version_minor}_developer_EPEL" # shellcheck disable=SC2086 dnf install ${dnf_install_flags} oracle-epel-release-el${oraclelinux_version_major} dnf config-manager --disable "$oraclelinux_epel_repo" >/dev/null 2>&1 || true epel_install_flags="${epel_install_flags} --enablerepo $oraclelinux_epel_repo" elif grep -q -E "release (9|10)" /etc/system-release; then # shellcheck disable=SC2086 dnf install ${dnf_install_flags} epel-release # Disable the OpenH264 repository as well, by default dnf config-manager --disable epel\* >/dev/null 2>&1 epel_install_flags="${epel_install_flags} --enablerepo epel" fi if grep -q "Oracle Linux Server" /etc/system-release && [ "${LIMA_CIDATA_MOUNTTYPE}" = "virtiofs" ]; then # Enable repositories like "ol9_UEKR7" for repo in $(dnf -q repolist | awk '/UEK/NR>1{print $1}'); do epel_install_flags="${epel_install_flags} --enablerepo ${repo}" done extrapkgs="${extrapkgs} kernel-uek-modules-$(uname -r)" REMOUNT_VIRTIOFS=1 fi if [ -n "${pkgs}" ]; then # shellcheck disable=SC2086 dnf install ${dnf_install_flags} ${pkgs} fi if [ -n "${extrapkgs}" ]; then # shellcheck disable=SC2086 dnf install ${dnf_install_flags} ${epel_install_flags} ${extrapkgs} fi fi if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ] && [ ! -e /usr/bin/fusermount ]; then # Workaround for https://github.com/containerd/stargz-snapshotter/issues/340 ln -s fusermount3 /usr/bin/fusermount fi elif command -v yum >/dev/null 2>&1; then echo "DEPRECATED: CentOS7 and others RHEL-like version 7 are unsupported and might be removed or stop to work in future lima releases" pkgs="" yum_install_flags="-y" if ! rpm -ql epel-release >/dev/null 2>&1; then yum install ${yum_install_flags} epel-release fi if ! command -v tar >/dev/null 2>&1; then pkgs="${pkgs} tar" fi if [ "${LIMA_CIDATA_MOUNTS}" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then pkgs="${pkgs} fuse-sshfs" fi if [ "${INSTALL_IPTABLES}" = 1 ] && [ ! -e /usr/sbin/iptables ]; then pkgs="${pkgs} iptables" fi if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ]; then if ! command -v newuidmap >/dev/null 2>&1; then pkgs="${pkgs} shadow-utils" fi if ! command -v mount.fuse3 >/dev/null 2>&1; then pkgs="${pkgs} fuse3" fi fi if ! command -v rsync >/dev/null 2>&1; then pkgs="${pkgs} rsync" fi if [ -n "${pkgs}" ]; then # shellcheck disable=SC2086 yum install ${yum_install_flags} ${pkgs} yum-config-manager --disable epel >/dev/null 2>&1 fi elif command -v pacman >/dev/null 2>&1; then pkgs="" if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then if [ "${LIMA_CIDATA_MOUNTS}" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then pkgs="${pkgs} sshfs" fi fi if ! command -v rsync >/dev/null 2>&1; then pkgs="${pkgs} rsync" fi # other dependencies are preinstalled on Arch Linux if [ -n "${pkgs}" ]; then # shellcheck disable=SC2086 pacman -Sy --noconfirm ${pkgs} fi elif command -v zypper >/dev/null 2>&1; then pkgs="" if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then if [ "${LIMA_CIDATA_MOUNTS}" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then pkgs="${pkgs} sshfs" fi fi if [ "${INSTALL_IPTABLES}" = 1 ] && [ ! -e /usr/sbin/iptables ]; then pkgs="${pkgs} iptables" fi if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ] && ! command -v mount.fuse3 >/dev/null 2>&1; then pkgs="${pkgs} fuse3" fi if ! command -v rsync >/dev/null 2>&1; then pkgs="${pkgs} rsync" fi if [ -n "${pkgs}" ]; then # shellcheck disable=SC2086 zypper --non-interactive install -y --no-recommends ${pkgs} fi elif command -v apk >/dev/null 2>&1; then pkgs="" if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then if [ "${LIMA_CIDATA_MOUNTS}" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then pkgs="${pkgs} sshfs" fi fi if [ "${INSTALL_IPTABLES}" = 1 ] && ! command -v iptables >/dev/null 2>&1; then pkgs="${pkgs} iptables" fi if ! command -v rsync >/dev/null 2>&1; then pkgs="${pkgs} rsync" fi if [ -n "${pkgs}" ]; then apk update # shellcheck disable=SC2086 apk add ${pkgs} fi fi if [ "${REMOUNT_VIRTIOFS}" = 1 ]; then mount -t virtiofs -a fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/35-setup-packages.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux update_fuse_conf() { # Modify /etc/fuse.conf (/etc/fuse3.conf) to allow "-o allow_root" if [ "${LIMA_CIDATA_MOUNTS}" -gt 0 ]; then fuse_conf="/etc/fuse.conf" if [ -e /etc/fuse3.conf ]; then fuse_conf="/etc/fuse3.conf" fi if ! grep -q "^user_allow_other" "${fuse_conf}"; then echo "user_allow_other" >>"${fuse_conf}" fi fi } # update_fuse_conf has to be called after installing all the packages, # otherwise apt-get fails with conflict if [ "${LIMA_CIDATA_MOUNTTYPE}" = "reverse-sshfs" ]; then update_fuse_conf fi SETUP_DNS=0 if [ -n "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}" -ne 0 ]; then SETUP_DNS=1 fi if [ -n "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" ] && [ "${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}" -ne 0 ]; then SETUP_DNS=1 fi if [ "${SETUP_DNS}" = 1 ]; then # Try to setup iptables rule again, in case we just installed iptables "${LIMA_CIDATA_MNT}/boot.Linux/09-host-dns-setup.sh" fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.Linux/40-install-containerd.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eux : "${CONTAINERD_NAMESPACE:=default}" # Overridable in .bashrc : "${CONTAINERD_SNAPSHOTTER:=overlayfs}" if [ "${LIMA_CIDATA_CONTAINERD_SYSTEM}" != 1 ] && [ "${LIMA_CIDATA_CONTAINERD_USER}" != 1 ]; then exit 0 fi # This script does not work unless systemd is available command -v systemctl >/dev/null 2>&1 || exit 0 # Extract bin/nerdctl and compare whether it is newer than the current /usr/local/bin/nerdctl (if already exists). # Takes 4-5 seconds. (FIXME: optimize) tmp_extract_nerdctl="$(mktemp -d)" tar Cxaf "${tmp_extract_nerdctl}" "${LIMA_CIDATA_MNT}"/"${LIMA_CIDATA_CONTAINERD_ARCHIVE}" bin/nerdctl if [ ! -f "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/nerdctl ] || [[ "${tmp_extract_nerdctl}"/bin/nerdctl -nt "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/nerdctl ]]; then if [ -f "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/nerdctl ]; then ( set +e echo "Upgrading existing nerdctl" echo "- Old: $("${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/nerdctl --version)" echo "- New: $("${tmp_extract_nerdctl}"/bin/nerdctl --version)" systemctl disable --now containerd default-buildkit stargz-snapshotter sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "PATH=${PATH}" "CONTAINERD_NAMESPACE=${CONTAINERD_NAMESPACE}" containerd-rootless-setuptool.sh uninstall-buildkit-containerd sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "PATH=${PATH}" containerd-rootless-setuptool.sh uninstall ) fi tar Cxaf "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}" "${LIMA_CIDATA_MNT}"/"${LIMA_CIDATA_CONTAINERD_ARCHIVE}" mkdir -p /etc/bash_completion.d nerdctl completion bash >/etc/bash_completion.d/nerdctl # TODO: enable zsh completion too fi rm -rf "${tmp_extract_nerdctl}" if [ "${LIMA_CIDATA_CONTAINERD_SYSTEM}" = 1 ]; then if [ ! -e /etc/containerd/config.toml ]; then mkdir -p /etc/containerd cat >"/etc/containerd/config.toml" <"/etc/buildkit/buildkitd.toml" <"${LIMA_CIDATA_HOME}/.config/containerd/config.toml" </dev/null 2>&1 && selinuxenabled; then selinux=1 fi if [ ! -e "/etc/apparmor.d/usr.local.bin.rootlesskit" ] && [ -e "/etc/apparmor.d/abi/4.0" ] && [ -e "/proc/sys/kernel/apparmor_restrict_unprivileged_userns" ]; then cat >"/etc/apparmor.d/usr.local.bin.rootlesskit" <, include /usr/local/bin/rootlesskit flags=(unconfined) { userns, # Site-specific additions and overrides. See local/README for details. include if exists } EOF systemctl restart apparmor.service fi if [ ! -e "${LIMA_CIDATA_HOME}/.config/systemd/user/containerd.service" ]; then until [ -e "/run/user/${LIMA_CIDATA_UID}/systemd/private" ]; do sleep 3; done if [ -n "$selinux" ]; then echo "Temporarily disabling SELinux, during installing containerd units" setenforce 0 fi if [ "$(sudo -iu "${LIMA_CIDATA_USER}" sh -ec 'systemctl --user show --property=RefuseManualStart --value dbus')" != "yes" ]; then sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" systemctl --user enable --now dbus fi sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "PATH=${PATH}" containerd-rootless-setuptool.sh install sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "PATH=${PATH}" \ "CONTAINERD_NAMESPACE=${CONTAINERD_NAMESPACE}" "CONTAINERD_SNAPSHOTTER=${CONTAINERD_SNAPSHOTTER}" \ containerd-rootless-setuptool.sh install-buildkit-containerd # $CONTAINERD_SNAPSHOTTER is configured in 20-rootless-base.sh, when the guest kernel is < 5.13, or the instance was created with Lima < 0.9.0. if [ "$(sudo -iu "${LIMA_CIDATA_USER}" sh -ec 'echo $CONTAINERD_SNAPSHOTTER')" = "fuse-overlayfs" ]; then sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "PATH=${PATH}" containerd-rootless-setuptool.sh install-fuse-overlayfs fi if compare_version.sh "$(uname -r)" -ge "5.13"; then sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" "PATH=${PATH}" containerd-rootless-setuptool.sh install-stargz else echo >&2 "WARNING: the guest kernel seems older than 5.13. Skipping installing rootless stargz." fi if [ -n "$selinux" ]; then echo "Restoring SELinux" setenforce 1 fi fi fi ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.essential.FreeBSD/00-freebsd-user-group.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 # This script ensures that the user is created with the expected UID. # This script is needed because nuageinit does not create the user with the expected UID. set -eu [ "$(stat -f %u "${LIMA_CIDATA_HOME}")" = "${LIMA_CIDATA_UID}" ] && exit 0 pw usermod -n "${LIMA_CIDATA_USER}" -u "${LIMA_CIDATA_UID}" gid="$(id -g "${LIMA_CIDATA_USER}")" chown -R "${LIMA_CIDATA_UID}:${gid}" "${LIMA_CIDATA_HOME}" ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/boot.sh ================================================ #!/bin/sh # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu INFO() { echo "LIMA $(date -Iseconds)| $*" } WARNING() { echo "LIMA $(date -Iseconds)| WARNING: $*" } UNAME="$(uname -s)" RUN="/run" if [ "${UNAME}" != "Linux" ]; then RUN="/var/run" fi rm -f "${RUN}/lima-boot-done" # shellcheck disable=SC2163 while read -r line; do [ -n "$line" ] && export "$line"; done <"${LIMA_CIDATA_MNT}"/lima.env # shellcheck disable=SC2163 while read -r line; do [ -n "$line" ] && export "$line"; done <"${LIMA_CIDATA_MNT}"/param.env # shellcheck disable=SC2163 while read -r line; do # pam_env implementation: # - '#' is treated the same as newline; terminates value # - skip leading tabs and spaces # - skip leading "export " prefix (only single space) # - skip leading quote ('\'' or '"') on the value side # - skip trailing quote only if leading quote has been skipped; # quotes don't need to match; trailing quote may be omitted line="$(echo "$line" | sed -E "s/^[ \\t]*(export )?//; s/#.*//; s/(^[^=]+=)[\"'](.*[^\"'])?[\"']?$/\1\2/")" [ -n "$line" ] && export "$line" done <"${LIMA_CIDATA_MNT}"/etc_environment PATH="${LIMA_CIDATA_MNT}"/util:"${PATH}" export PATH CODE=0 # Don't make any changes to /etc or /var/lib until boot.Linux/04-persistent-data-volume.sh # has run because it might move the directories to /mnt/data on first boot. In that # case changes made on restart would be lost. run_boot_scripts() { boot="$1" if [ -e "${boot}" ]; then for f in "${boot}"/*.sh; do INFO "Executing $f" if ! "$f"; then WARNING "Failed to execute $f" CODE=1 fi done fi } # The boot.essential.${UNAME} scripts are executed in plain mode too. run_boot_scripts "${LIMA_CIDATA_MNT}/boot.essential.${UNAME}" if [ "$LIMA_CIDATA_PLAIN" = "1" ]; then INFO "Plain mode. Skipping to run non-essential boot scripts. Provisioning scripts will be still executed. Guest agent will not be running." else run_boot_scripts "${LIMA_CIDATA_MNT}/boot.${UNAME}" fi # indirect variable lookup, like ${!var} in bash deref() { eval echo \$"$1" } if [ -d "${LIMA_CIDATA_MNT}"/provision.data ]; then for f in "${LIMA_CIDATA_MNT}"/provision.data/*; do filename=$(basename "$f") overwrite=$(deref "LIMA_CIDATA_DATAFILE_${filename}_OVERWRITE") owner=$(deref "LIMA_CIDATA_DATAFILE_${filename}_OWNER") path=$(deref "LIMA_CIDATA_DATAFILE_${filename}_PATH") permissions=$(deref "LIMA_CIDATA_DATAFILE_${filename}_PERMISSIONS") user="${owner%%:*}" if [ -e "$path" ] && [ "$overwrite" = "false" ]; then INFO "Not overwriting $path" else INFO "Copying $f to $path" if ! sudo -iu "${user}" mkdir -p "$(dirname "$path")"; then WARNING "Failed to create directory for ${path} (as user ${user})" WARNING "Falling back to creating directory as root to maintain compatibility" mkdir -p "$(dirname "$path")" fi cp "$f" "$path" chown "$owner" "$path" chmod "$permissions" "$path" fi done fi if [ -d "${LIMA_CIDATA_MNT}"/provision.yq ]; then yq="${LIMA_CIDATA_MNT}/lima-guestagent yq" for f in "${LIMA_CIDATA_MNT}"/provision.yq/*; do filename=$(basename "${f}") format=$(deref "LIMA_CIDATA_YQ_PROVISION_${filename}_FORMAT") owner=$(deref "LIMA_CIDATA_YQ_PROVISION_${filename}_OWNER") path=$(deref "LIMA_CIDATA_YQ_PROVISION_${filename}_PATH") permissions=$(deref "LIMA_CIDATA_YQ_PROVISION_${filename}_PERMISSIONS") user="${owner%%:*}" # Creating intermediate directories may fail if the user does not have permission. # TODO: Create intermediate directories with the specified group ownership. if ! sudo -iu "${user}" mkdir -p "$(dirname "${path}")"; then WARNING "Failed to create directory for ${path} (as user ${user})" CODE=1 continue fi # Since CIDATA is mounted with dmode=700,fmode=700, # `lima-guestagent yq` cannot be executed by non-root users, # and provision.yq/* files cannot be read by non-root users. if [ -f "${path}" ]; then INFO "Updating ${path}" # If the user does not have write permission, it should fail. # This avoids changes being made by the wrong user. if ! sudo -iu "${user}" test -w "${path}"; then WARNING "File ${path} is not writable by user ${user}" CODE=1 continue fi # Relies on the fact that yq does not change the owner of the existing file. if ! ${yq} --inplace --from-file "${f}" --input-format "${format}" --output-format "${format}" "${path}"; then WARNING "Failed to update ${path} (as user ${user})" CODE=1 continue fi else if [ "${format}" = "auto" ]; then # yq can't determine the output format from non-existing files case "${path}" in *.csv) format=csv ;; *.ini) format=ini ;; *.json) format=json ;; *.properties) format=properties ;; *.toml) format=toml ;; *.tsv) format=tsv ;; *.xml) format=xml ;; *.yaml | *.yml) format=yaml ;; *) format=yaml WARNING "Cannot determine file type for ${path}, using yaml format" ;; esac fi INFO "Creating ${path}" if ! ${yq} --null-input --from-file "${f}" --output-format "${format}" | sudo -iu "${user}" tee "${path}"; then WARNING "Failed to create ${path} (as user ${user})" CODE=1 continue fi fi if ! sudo -iu "${user}" chown "${owner}" "${path}"; then WARNING "Failed to set owner for ${path} (as user ${user})" CODE=1 fi if ! sudo -iu "${user}" chmod "${permissions}" "${path}"; then WARNING "Failed to set permissions for ${path} (as user ${user})" CODE=1 fi done fi if [ -d "${LIMA_CIDATA_MNT}"/provision.system ]; then for f in "${LIMA_CIDATA_MNT}"/provision.system/*; do INFO "Executing $f" if ! "$f"; then WARNING "Failed to execute $f" CODE=1 fi done fi USER_SCRIPT="${LIMA_CIDATA_HOME}/.lima-user-script" if [ -d "${LIMA_CIDATA_MNT}"/provision.user ]; then if [ "$UNAME" = "Linux" ] && [ ! -f /sbin/openrc-run ]; then until [ -e "/run/user/${LIMA_CIDATA_UID}/systemd/private" ]; do sleep 3; done fi params=$(grep -o '^PARAM_[^=]*' "${LIMA_CIDATA_MNT}"/param.env | paste -sd , -) for f in "${LIMA_CIDATA_MNT}"/provision.user/*; do INFO "Executing $f (as user ${LIMA_CIDATA_USER})" cp "$f" "${USER_SCRIPT}" chown "${LIMA_CIDATA_USER}" "${USER_SCRIPT}" chmod 755 "${USER_SCRIPT}" XDG_RUNTIME_DIR_ENV= if [ "${UNAME}" = "Linux" ]; then XDG_RUNTIME_DIR_ENV="XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" fi # shellcheck disable=SC2086 if ! sudo -iu "${LIMA_CIDATA_USER}" "--preserve-env=${params}" $XDG_RUNTIME_DIR_ENV "${USER_SCRIPT}"; then WARNING "Failed to execute $f (as user ${LIMA_CIDATA_USER})" CODE=1 fi rm "${USER_SCRIPT}" done fi # Signal that provisioning is done. The instance ID changes on every boot, # so any value from a previous boot cycle will be different. echo "${LIMA_CIDATA_IID}" >"${RUN}/lima-boot-done" INFO "Exiting with code $CODE" exit "$CODE" ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/etc_environment ================================================ #LIMA-START {{- range $key, $val := .Env}} {{$key}}={{$val}} {{- end}} #LIMA-END ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/lima.env ================================================ LIMA_CIDATA_DEBUG={{ .Debug }} LIMA_CIDATA_IID={{ .IID }} LIMA_CIDATA_NAME={{ .Name }} LIMA_CIDATA_USER={{ .User }} LIMA_CIDATA_UID={{ .UID }} LIMA_CIDATA_COMMENT={{ .Comment }} LIMA_CIDATA_HOME={{ .Home}} LIMA_CIDATA_SHELL={{ .Shell }} LIMA_CIDATA_HOSTHOME_MOUNTPOINT={{ .HostHomeMountPoint }} LIMA_CIDATA_MOUNTS={{ len .Mounts }} {{- range $i, $val := .Mounts}} LIMA_CIDATA_MOUNTS_{{$i}}_MOUNTPOINT={{$val.MountPoint}} {{- end}} LIMA_CIDATA_MOUNTTYPE={{ .MountType }} LIMA_CIDATA_DISKS={{ len .Disks }} {{- range $i, $disk := .Disks}} LIMA_CIDATA_DISK_{{$i}}_NAME={{$disk.Name}} LIMA_CIDATA_DISK_{{$i}}_DEVICE={{$disk.Device}} LIMA_CIDATA_DISK_{{$i}}_FORMAT={{$disk.Format}} LIMA_CIDATA_DISK_{{$i}}_FSTYPE={{$disk.FSType}} LIMA_CIDATA_DISK_{{$i}}_FSARGS={{range $j, $arg := $disk.FSArgs}}{{if $j}} {{end}}{{$arg}}{{end}} {{- end}} {{- range $dataFile := .DataFiles}} LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_OVERWRITE={{$dataFile.Overwrite}} LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_OWNER={{$dataFile.Owner}} LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_PATH={{$dataFile.Path}} LIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_PERMISSIONS={{$dataFile.Permissions}} {{- end}} {{- range $yqProvision := .YQProvisions}} LIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_FORMAT={{$yqProvision.Format}} LIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_OWNER={{$yqProvision.Owner}} LIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_PATH={{$yqProvision.Path}} LIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_PERMISSIONS={{$yqProvision.Permissions}} {{- end}} LIMA_CIDATA_GUEST_INSTALL_PREFIX={{ .GuestInstallPrefix }} {{- if .UpgradePackages}} LIMA_CIDATA_UPGRADE_PACKAGES=1 {{- else}} LIMA_CIDATA_UPGRADE_PACKAGES= {{- end}} {{- if .Containerd.User}} LIMA_CIDATA_CONTAINERD_USER=1 {{- else}} LIMA_CIDATA_CONTAINERD_USER= {{- end}} {{- if .Containerd.System}} LIMA_CIDATA_CONTAINERD_SYSTEM=1 {{- else}} LIMA_CIDATA_CONTAINERD_SYSTEM= {{- end}} {{- if or .Containerd.User .Containerd.System}} LIMA_CIDATA_CONTAINERD_ARCHIVE={{.Containerd.Archive}} {{- end}} LIMA_CIDATA_SLIRP_DNS={{.SlirpDNS}} LIMA_CIDATA_SLIRP_GATEWAY={{.SlirpGateway}} LIMA_CIDATA_SLIRP_IP_ADDRESS={{.SlirpIPAddress}} LIMA_CIDATA_UDP_DNS_LOCAL_PORT={{.UDPDNSLocalPort}} LIMA_CIDATA_TCP_DNS_LOCAL_PORT={{.TCPDNSLocalPort}} LIMA_CIDATA_ROSETTA_ENABLED={{.RosettaEnabled}} LIMA_CIDATA_ROSETTA_BINFMT={{.RosettaBinFmt}} {{- if .SkipDefaultDependencyResolution}} LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION=1 {{- else}} LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION= {{- end}} LIMA_CIDATA_VMTYPE={{ .VMType }} LIMA_CIDATA_VSOCK_PORT={{ .VSockPort }} LIMA_CIDATA_VIRTIO_PORT={{ .VirtioPort}} {{- if .Plain}} LIMA_CIDATA_PLAIN=1 {{- else}} LIMA_CIDATA_PLAIN= {{- end}} {{- if .NoCloudInit}} LIMA_CIDATA_NO_CLOUD_INIT=1 {{- else}} LIMA_CIDATA_NO_CLOUD_INIT= {{- end}} ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/meta-data ================================================ instance-id: {{.IID}} local-hostname: {{.Hostname}} ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/network-config ================================================ version: 2 ethernets: {{- range $nw := .Networks}} {{$nw.Interface}}: match: macaddress: '{{$nw.MACAddress}}' dhcp4: true set-name: {{$nw.Interface}} dhcp4-overrides: route-metric: {{$nw.Metric}} dhcp-identifier: mac {{- if and (eq $nw.Interface $.SlirpNICName) (gt (len $.DNSAddresses) 0) }} nameservers: addresses: {{- range $ns := $.DNSAddresses }} - {{$ns}} {{- end }} {{- end }} {{- end }} ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/param.env ================================================ {{range $key, $val := .Param -}} PARAM_{{ $key }}={{ $val }} {{end -}} ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/user-data ================================================ #cloud-config # vim:syntax=yaml growpart: mode: auto devices: ['/'] {{- if eq .OS "FreeBSD" }} packages: # boot.sh depends on sudo. # TODO: consider replacing sudo with doas. # FIXME: The hostagent script depends on sudo too. # https://github.com/lima-vm/lima/issues/4594 - sudo {{- end }} {{- if .UpgradePackages }} package_update: true package_upgrade: true package_reboot_if_required: true {{- end }} {{- if or .RosettaEnabled (and .Mounts (or (eq .MountType "9p") (eq .MountType "virtiofs"))) }} mounts: {{- if .RosettaEnabled }}{{/* Mount the rosetta volume before systemd-binfmt.service(8) starts */}} - [vz-rosetta, /mnt/lima-rosetta, virtiofs, defaults, "0", "0"] {{- end }} {{- if and .Mounts (or (eq .MountType "9p") (eq .MountType "virtiofs")) }} {{- range $m := $.Mounts}} - [{{$m.Tag}}, {{$m.MountPoint}}, {{$m.Type}}, "{{$m.Options}}", "0", "0"] {{- end }} {{- end }} {{- end }} {{- if .TimeZone }} timezone: {{.TimeZone}} {{- end }} users: - name: "{{.User}}" {{- if ne .OS "FreeBSD" }} # nuageinit does not support specifying the UID. # The UID is fixed up in boot.essential.FreeBSD/00-freebsd-user-group.sh uid: "{{.UID}}" {{- end }} {{- if .Comment }} gecos: {{ printf "%q" .Comment }} {{- end }} homedir: "{{.Home}}" shell: {{.Shell}} {{- if eq .OS "Darwin" }} {{/* On macOS, the password is not locked so as to allow GUI login. */}} {{/* Since the user can run sudo with their own password, basically we don't need to set up passwordless sudo. */}} {{/* However, it is still configured to allow `/sbin/shutdown -h now` without password, as it is invoked by `limactl stop` for graceful shutdown. */}} {{/* (Why doesn't macOS VM support graceful shutdown?) */}} sudo: ALL=(ALL) NOPASSWD:/sbin/shutdown -h now {{- else }} sudo: ALL=(ALL) NOPASSWD:ALL {{- if eq .OS "FreeBSD" }} groups: - wheel doas: permit nopass :wheel {{- end}} lock_passwd: true {{- end }} {{- if eq .OS "FreeBSD" }} ssh_authorized_keys: {{- else }} ssh-authorized-keys: {{- end }} {{- range $val := .SSHPubKeys }} - {{ printf "%q" $val }} {{- end }} {{- if .BootScripts }} write_files: - content: | #!/bin/sh set -eux LIMA_CIDATA_MNT="/mnt/lima-cidata" UNAME="$(uname -s)" if [ "${UNAME}" = "Darwin" ]; then LIMA_CIDATA_MNT="/Volumes/cidata" # Should have been mounted automatically elif [ "${UNAME}" = "FreeBSD" ]; then LIMA_CIDATA_DEV="/dev/iso9660/cidata" if [ ! -e "${LIMA_CIDATA_DEV}" ]; then # When the iso is created with `hdiutil` on macOS, # apparently the volume name becomes "CIDATA" not "cidata" LIMA_CIDATA_DEV="/dev/iso9660/CIDATA" fi mkdir -p -m 700 "${LIMA_CIDATA_MNT}" mount_cd9660 -G wheel -U root -m 0700 -o ro,exec "${LIMA_CIDATA_DEV}" "${LIMA_CIDATA_MNT}" elif [ "${UNAME}" = "Linux" ]; then LIMA_CIDATA_DEV="/dev/disk/by-label/cidata" mkdir -p -m 700 "${LIMA_CIDATA_MNT}" mount -o ro,mode=0700,dmode=0700,overriderockperm,exec,uid=0 "${LIMA_CIDATA_DEV}" "${LIMA_CIDATA_MNT}" else echo "Unsupported OS: ${UNAME}" >&2 exit 1 fi export LIMA_CIDATA_MNT exec "${LIMA_CIDATA_MNT}"/boot.sh {{- if or (eq .OS "Darwin") (eq .OS "FreeBSD") }} owner: root:wheel {{- else }} owner: root:root {{- end }} {{- if eq .OS "FreeBSD" }} # nuageinit requires the path to be under an existing directory path: /usr/sbin/lima-freebsd-init.sh {{- else }} path: /var/lib/cloud/scripts/per-boot/00-lima.boot.sh {{- end }} permissions: '0755' {{- if eq .OS "FreeBSD" }} # nuageinit does not run /var/lib/cloud/scripts/per-boot/* scripts - content: | #!/bin/sh # PROVIDE: lima_freebsd_init # REQUIRE: DAEMON # BEFORE: LOGIN . /etc/rc.subr name="lima_freebsd_init" rcvar="lima_freebsd_init_enable" command="/usr/sbin/lima-freebsd-init.sh" load_rc_config "$name" run_rc_command "$1" owner: root:wheel path: /etc/rc.d/lima_freebsd_init permissions: '0755' - content: | lima_freebsd_init_enable="YES" owner: root:wheel path: /etc/rc.conf.d/lima_freebsd_init permissions: '0644' {{- end }} {{- end }} {{- if .DNSAddresses }} # This has no effect on systems using systemd-resolved, but is used # on e.g. Alpine to set up /etc/resolv.conf on first boot. manage_resolv_conf: true resolv_conf: nameservers: {{- range $ns := $.DNSAddresses }} - {{$ns}} {{- end }} {{- end }} {{- if or .CACerts.RemoveDefaults .CACerts.Trusted }} {{ with .CACerts }} ca_certs: {{- if .RemoveDefaults }} remove_defaults: {{ .RemoveDefaults }} {{- end }} {{- if .Trusted}} trusted: {{- range $cert := .Trusted }} - | {{- range $line := $cert.Lines }} {{ $line }} {{- end }} {{- end }} {{- end }} {{- end }} {{- end }} {{- if .BootCmds }} bootcmd: {{- range $cmd := $.BootCmds }} - | # We need to embed the params.env as a here-doc because /mnt/lima-cidata is not yet mounted while read -r line; do [ -n "$line" ] && export "$line"; done <<'EOF' {{- range $key, $val := $.Param }} PARAM_{{ $key }}={{ $val }} {{- end }} EOF {{- range $line := $cmd.Lines }} {{ $line }} {{- end }} {{- end }} {{- end }} ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh ================================================ #!/bin/bash # SPDX-FileCopyrightText: Copyright The Lima Authors # SPDX-License-Identifier: Apache-2.0 set -eu : "${SELFTEST:=}" if [ -n "$SELFTEST" ]; then unset SELFTEST echo >&2 "=== Running positive tests ===" ( set -x "$0" 0.1.2 -eq 0.1.2 "$0" 0.1.2 -ne 0.1.3 "$0" 0.1.2 -ge 0.1.1 "$0" 0.1.2 -ge 0.1.2 "$0" 0.1.10 -ge 0.1.9 "$0" 0.1.2 -gt 0.1.1 "$0" 0.1.10 -gt 0.1.9 "$0" 0.1.2 -le 0.1.2 "$0" 0.1.2 -le 0.1.3 "$0" 0.1.2 -le 0.1.10 "$0" 0.1.2 -lt 0.1.3 "$0" 0.1.2 -lt 0.1.10 ) echo >&2 "=== Running negative tests ===" ( set -x "$0" 0.1.2 -eq 0.1.1 && false "$0" 0.1.2 -ne 0.1.2 && false "$0" 0.1.2 -ge 0.1.3 && false "$0" 0.1.2 -gt 0.1.2 && false "$0" 0.1.2 -le 0.1.1 && false "$0" 0.1.2 -lt 0.1.2 && false true ) exit 0 fi if [ "$#" -ne 3 ]; then echo >&2 "Usage: $0 VERSION-A OP VERSION-B" echo >&2 "Implemented operators: -eq, -ne, -ge, -gt, -le, -lt" echo >&2 "" echo >&2 "Example: $0 1.2.10 -ge 1.2.9" exit 1 fi version_a="$1" op="$2" version_b="$3" sorted="$(echo -ne "${version_a}\n${version_b}\n" | sort -V -r | head -n1)" case "${op}" in -eq) [ "${version_a}" = "${version_b}" ] ;; -ne) [ "${version_a}" != "${version_b}" ] ;; -ge) [ "${version_a}" = "${sorted}" ] ;; -gt) [ "${version_a}" = "${sorted}" ] && [ "${version_a}" != "${version_b}" ] ;; -le) [ "${version_b}" = "${sorted}" ] ;; -lt) [ "${version_b}" = "${sorted}" ] && [ "${version_a}" != "${version_b}" ] ;; *) echo "Unknown operator \"$op\"" exit 1 ;; esac ================================================ FILE: pkg/cidata/cidata.TEMPLATE.d/util.FreeBSD/print_cidata_fstab.lua ================================================ #!/usr/libexec/flua local yaml = require("lyaml") local fpath = "/mnt/lima-cidata/user-data" local f = io.open(fpath, "r") if not f then error("Could not open " .. fpath) end local content = f:read("*a") f:close() local config = yaml.load(content) for i, row in ipairs(config.mounts) do for j, value in ipairs(row) do io.write(value, "\t") end io.write("\n") end ================================================ FILE: pkg/cidata/cidata.go ================================================ // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 package cidata import ( "compress/gzip" "context" "errors" "fmt" "io" "maps" "net" "net/url" "os" "path" "path/filepath" "slices" "strconv" "strings" "time" "unicode" "github.com/docker/go-units" "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/debugutil" "github.com/lima-vm/lima/v2/pkg/driver" "github.com/lima-vm/lima/v2/pkg/instance/hostname" "github.com/lima-vm/lima/v2/pkg/iso9660util" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/localpathutil" "github.com/lima-vm/lima/v2/pkg/networks" "github.com/lima-vm/lima/v2/pkg/networks/usernet" "github.com/lima-vm/lima/v2/pkg/osutil" "github.com/lima-vm/lima/v2/pkg/sshutil" ) var netLookupIP = func(host string) []net.IP { ips, err := net.LookupIP(host) if err != nil { logrus.Debugf("net.LookupIP %s: %s", host, err) return nil } return ips } func setupEnv(instConfigEnv map[string]string, propagateProxyEnv bool, slirpGateway string) (map[string]string, error) { // Start with the proxy variables from the system settings. env, err := osutil.ProxySettings() if err != nil { return env, err } // env.* settings from lima.yaml override system settings without giving a warning maps.Copy(env, instConfigEnv) // Current process environment setting override both system settings and env.* lowerVars := []string{"ftp_proxy", "http_proxy", "https_proxy", "no_proxy"} upperVars := make([]string, len(lowerVars)) for i, name := range lowerVars { upperVars[i] = strings.ToUpper(name) } if propagateProxyEnv { for _, name := range append(lowerVars, upperVars...) { if value, ok := os.LookupEnv(name); ok { if _, ok := env[name]; ok && value != env[name] { logrus.Infof("Overriding %q value %q with %q from limactl process environment", name, env[name], value) } env[name] = value } } } // Replace IP that IsLoopback in proxy settings with the gateway address // Delete settings with empty values, so the user can choose to ignore system settings. for _, name := range append(lowerVars, upperVars...) { value, ok := env[name] if ok && value == "" { delete(env, name) } else if ok && !strings.EqualFold(name, "no_proxy") { u, err := url.Parse(value) if err != nil { logrus.Warnf("Ignoring invalid proxy %q=%v: %s", name, value, err) continue } for _, ip := range netLookupIP(u.Hostname()) { if ip.IsLoopback() { newHost := slirpGateway if u.Port() != "" { newHost = net.JoinHostPort(newHost, u.Port()) } u.Host = newHost value = u.String() } } if value != env[name] { logrus.Infof("Replacing %q value %q with %q", name, env[name], value) env[name] = value } } } // Make sure uppercase variants have the same value as lowercase ones. // If both are set, the lowercase variant value takes precedence. for _, lowerName := range lowerVars { upperName := strings.ToUpper(lowerName) if _, ok := env[lowerName]; ok { if _, ok := env[upperName]; ok && env[lowerName] != env[upperName] { logrus.Warnf("Changing %q value from %q to %q to match %q", upperName, env[upperName], env[lowerName], lowerName) } env[upperName] = env[lowerName] } else if _, ok := env[upperName]; ok { env[lowerName] = env[upperName] } } return env, nil } func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string, noCloudInit, rosettaEnabled, rosettaBinFmt bool) (*TemplateArgs, error) { if err := limayaml.Validate(instConfig, false); err != nil { return nil, err } archive := "nerdctl-full.tgz" args := TemplateArgs{ Debug: debugutil.Debug, OS: *instConfig.OS, BootScripts: bootScripts, Name: name, Hostname: hostname.FromInstName(name), // TODO: support customization User: *instConfig.User.Name, Comment: removeControlChars(*instConfig.User.Comment), Home: *instConfig.User.Home, Shell: *instConfig.User.Shell, UID: *instConfig.User.UID, GuestInstallPrefix: *instConfig.GuestInstallPrefix, UpgradePackages: *instConfig.UpgradePackages, Containerd: Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User, Archive: archive}, SlirpNICName: networks.SlirpNICName, VMType: *instConfig.VMType, VSockPort: vsockPort, VirtioPort: virtioPort, RosettaEnabled: rosettaEnabled, RosettaBinFmt: rosettaBinFmt, Plain: *instConfig.Plain, TimeZone: *instConfig.TimeZone, NoCloudInit: noCloudInit, Param: instConfig.Param, } firstUsernetIndex := limayaml.FirstUsernetIndex(instConfig) var subnet net.IP var err error if firstUsernetIndex != -1 { usernetName := instConfig.Networks[firstUsernetIndex].Lima subnet, err = usernet.Subnet(usernetName) if err != nil { return nil, err } args.SlirpGateway = usernet.GatewayIP(subnet) args.SlirpDNS = usernet.GatewayIP(subnet) } else { subnet, _, err = net.ParseCIDR(networks.SlirpNetwork) if err != nil { return nil, err } args.SlirpGateway = usernet.GatewayIP(subnet) if *instConfig.VMType == limatype.VZ { args.SlirpDNS = usernet.GatewayIP(subnet) } else { args.SlirpDNS = usernet.DNSIP(subnet) } args.SlirpIPAddress = networks.SlirpIPAddress } // change instance id on every boot so network config will be processed again args.IID = fmt.Sprintf("iid-%d", time.Now().Unix()) pubKeys, err := sshutil.DefaultPubKeys(ctx, *instConfig.SSH.LoadDotSSHPubKeys) if err != nil { return nil, err } if len(pubKeys) == 0 { return nil, errors.New("no SSH key was found, run `ssh-keygen`") } for _, f := range pubKeys { args.SSHPubKeys = append(args.SSHPubKeys, f.Content) } var fstype string switch *instConfig.MountType { case limatype.REVSSHFS: fstype = "sshfs" case limatype.NINEP: fstype = "9p" if *instConfig.OS == limatype.FREEBSD { fstype = "p9fs" } case limatype.VIRTIOFS: fstype = "virtiofs" } hostHome, err := localpathutil.Expand("~") if err != nil { return nil, err } for _, f := range instConfig.Mounts { tag := limayaml.MountTag(f.Location, *f.MountPoint) options := "rw" if *instConfig.OS == limatype.LINUX { options = "defaults" } switch fstype { case "9p", "p9fs", "virtiofs": options = "ro" if *f.Writable { options = "rw" } if fstype == "9p" { options += ",trans=virtio" options += fmt.Sprintf(",version=%s", *f.NineP.ProtocolVersion) msize, err := units.RAMInBytes(*f.NineP.Msize) if err != nil { return nil, fmt.Errorf("failed to parse msize for %q: %w", f.Location, err) } options += fmt.Sprintf(",msize=%d", msize) options += fmt.Sprintf(",cache=%s", *f.NineP.Cache) } // don't fail the boot, if virtfs is not available switch *instConfig.OS { case limatype.LINUX: options += ",nofail" case limatype.FREEBSD: options += ",failok" } } args.Mounts = append(args.Mounts, Mount{Tag: tag, MountPoint: *f.MountPoint, Type: fstype, Options: options}) if f.Location == hostHome { args.HostHomeMountPoint = *f.MountPoint } } switch *instConfig.MountType { case limatype.REVSSHFS: args.MountType = "reverse-sshfs" case limatype.NINEP: args.MountType = "9p" case limatype.VIRTIOFS: args.MountType = "virtiofs" } for i, d := range instConfig.AdditionalDisks { format := true if d.Format != nil { format = *d.Format } fstype := "" if d.FSType != nil { fstype = *d.FSType } args.Disks = append(args.Disks, Disk{ Name: d.Name, Device: diskDeviceNameFromOrder(i), Format: format, FSType: fstype, FSArgs: d.FSArgs, }) } args.Networks = append(args.Networks, Network{MACAddress: limayaml.MACAddress(instDir), Interface: networks.SlirpNICName, Metric: 200}) for i, nw := range instConfig.Networks { if i == firstUsernetIndex { continue } args.Networks = append(args.Networks, Network{MACAddress: nw.MACAddress, Interface: nw.Interface, Metric: *nw.Metric}) } args.Env, err = setupEnv(instConfig.Env, *instConfig.PropagateProxyEnv, args.SlirpGateway) if err != nil { return nil, err } switch { case len(instConfig.DNS) > 0: for _, addr := range instConfig.DNS { args.DNSAddresses = append(args.DNSAddresses, addr.String()) } case firstUsernetIndex != -1 || *instConfig.VMType == limatype.VZ: args.DNSAddresses = append(args.DNSAddresses, args.SlirpDNS) case *instConfig.HostResolver.Enabled: args.UDPDNSLocalPort = udpDNSLocalPort args.TCPDNSLocalPort = tcpDNSLocalPort args.DNSAddresses = append(args.DNSAddresses, args.SlirpDNS) default: args.DNSAddresses, err = osutil.DNSAddresses() if err != nil { return nil, err } } args.CACerts.RemoveDefaults = instConfig.CACertificates.RemoveDefaults for _, path := range instConfig.CACertificates.Files { expanded, err := localpathutil.Expand(path) if err != nil { return nil, err } content, err := os.ReadFile(expanded) if err != nil { return nil, err } cert := getCert(string(content)) args.CACerts.Trusted = append(args.CACerts.Trusted, cert) } for _, content := range instConfig.CACertificates.Certs { cert := getCert(content) args.CACerts.Trusted = append(args.CACerts.Trusted, cert) } // Remove empty caCerts (default values) from configuration yaml if !*args.CACerts.RemoveDefaults && len(args.CACerts.Trusted) == 0 { args.CACerts.RemoveDefaults = nil args.CACerts.Trusted = nil } args.BootCmds = getBootCmds(instConfig.Provision) for i, f := range instConfig.Provision { if f.Mode == limatype.ProvisionModeDependency && *f.SkipDefaultDependencyResolution { args.SkipDefaultDependencyResolution = true } if f.Mode == limatype.ProvisionModeData { args.DataFiles = append(args.DataFiles, DataFile{ FileName: fmt.Sprintf("%08d", i), Overwrite: strconv.FormatBool(*f.Overwrite), Owner: *f.Owner, Path: *f.Path, Permissions: *f.Permissions, }) } if f.Mode == limatype.ProvisionModeYQ { args.YQProvisions = append(args.YQProvisions, YQProvision{ FileName: fmt.Sprintf("%08d", i), Format: *f.Format, Owner: *f.Owner, Path: *f.Path, Permissions: *f.Permissions, }) } } return &args, nil } func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *limatype.LimaYAML) error { args, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, "", false, false, false) if err != nil { return err } // mounts are not included here args.Mounts = nil // resolv_conf is not included here args.DNSAddresses = nil if err := ValidateTemplateArgs(args); err != nil { return err } config, err := ExecuteTemplateCloudConfig(args) if err != nil { return err } os.RemoveAll(filepath.Join(instDir, filenames.CloudConfig)) // delete existing return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444) } // GenerateISO9660 generates the cidata ISO9660 image (or directory, for noCloudInit) // in instDir. It returns the instance ID, which changes on every boot. func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string, noCloudInit, rosettaEnabled, rosettaBinFmt bool) (string, error) { args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort, noCloudInit, rosettaEnabled, rosettaBinFmt) if err != nil { return "", err } if err := ValidateTemplateArgs(args); err != nil { return "", err } layout, err := ExecuteTemplateCIDataISO(args) if err != nil { return "", err } driverScripts, err := drv.BootScripts() if err != nil { return "", fmt.Errorf("failed to get boot scripts: %w", err) } for filename, content := range driverScripts { layoutPath := path.Join("boot.Linux", filename) if strings.Contains(filename, "/") { // When the filename contains a slash, it must be in the format of "boot./