[
  {
    "path": ".codespellrc",
    "content": "# https://github.com/codespell-project/codespell#using-a-config-file\n\n[codespell]\n\n# Comma separated list of dirs to be skipped.\nskip = .git,go.mod,go.sum,*.pb.desc,*/node_modules/*,*/public/js/*,*/public/scss/*\n\n# Comma separated list of words to be ignored. Words must be lowercased.\nignore-words-list = ans,distroname,testof,hda,ststr,archtypes,sme\n\n# Check file names as well.\ncheck-filenames = true\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n# Protobuf descriptors are binary files\n[*.pb.desc]\ninsert_final_newline = unset\ntrim_trailing_whitespace = unset\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: Bug report\ndescription: Report a potential bug\nbody:\n- type: textarea\n  attributes:\n    label: Description\n    description: Please make sure to include the version of Lima and the host OS\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n- name: Ask a question (GitHub Discussions)\n  url: https://github.com/lima-vm/lima/discussions\n  about: We use GitHub Discussions for questions, GitHub issues for tracking bug reports and feature requests\n- name: Chat with Lima users and developers\n  url: https://slack.cncf.io/\n  about: CNCF slack has `#lima` channel\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "name: Feature request\ndescription: Request a feature\nbody:\n- type: textarea\n  attributes:\n    label: Description\n"
  },
  {
    "path": ".github/actions/setup_cache_for_template/action.yml",
    "content": "name: 'setup cache for template'\ndescription: 'setup cache for template'\ninputs:\n  arch:\n    description: arch to setup cache for\n    required: false\n  template:\n    description: template yaml file\n    required: true\nruns:\n  using: \"composite\"\n  steps:\n  - name: \"detect platform for download directory\"\n    id: detect-platform\n    run: |\n      if [[ \"$(uname)\" == \"Darwin\" ]]; then\n        download_dir=~/Library/Caches/lima/download\n      else\n        download_dir=~/.cache/lima/download\n      fi\n      echo \"download-dir=$download_dir\" >> \"$GITHUB_OUTPUT\"\n    shell: bash\n  - name: \"create cache parameters from template\"\n    if: always()\n    id: cache-params-from-template\n    run: |\n      set -eux\n      source hack/cache-common-inc.sh\n      print_cache_informations_from_template \"${{ inputs.template }}\" \"${{ inputs.arch }}\" >> \"$GITHUB_OUTPUT\"\n    shell: bash\n\n  - name: \"Cache ${{ steps.cache-params-from-template.outputs.image-path }}\"\n    if: ${{ steps.cache-params-from-template.outputs.image-key != '' }}\n    # avoid using `~` in path that will be expanded to platform specific home directory\n    uses: actions/cache@v4\n    with:\n      path: ${{ steps.cache-params-from-template.outputs.image-path }}\n      key: ${{ steps.cache-params-from-template.outputs.image-key }}\n      enableCrossOsArchive: true\n\n  - name: \"Cache ${{ steps.cache-params-from-template.outputs.kernel-path }}\"\n    if: ${{ steps.cache-params-from-template.outputs.kernel-key != '' }}\n    # avoid using `~` in path that will be expanded to platform specific home directory\n    uses: actions/cache@v4\n    with:\n      path: ${{ steps.cache-params-from-template.outputs.kernel-path }}\n      key: ${{ steps.cache-params-from-template.outputs.kernel-key }}\n      enableCrossOsArchive: true\n\n  - name: \"Cache ${{ steps.cache-params-from-template.outputs.initrd-path }}\"\n    if: ${{ steps.cache-params-from-template.outputs.initrd-key != '' }}\n    # avoid using `~` in path that will be expanded to platform specific home directory\n    uses: actions/cache@v4\n    with:\n      path: ${{ steps.cache-params-from-template.outputs.initrd-path }}\n      key: ${{ steps.cache-params-from-template.outputs.initrd-key }}\n      enableCrossOsArchive: true\n\n  - name: \"Cache ${{ steps.cache-params-from-template.outputs.containerd-key }}\"\n    if: ${{ steps.cache-params-from-template.outputs.containerd-key != '' }}\n    uses: actions/cache@v4\n    with:\n      path: ${{ steps.cache-params-from-template.outputs.containerd-path }}\n      key: ${{ steps.cache-params-from-template.outputs.containerd-key }}\n      enableCrossOsArchive: true\n\n  - name: \"Create symbolic link named ${{ steps.detect-platform.outputs.download-dir }} pointing to .download\"\n    run: |\n      set -eux\n      [ -d .download ] || mkdir -p .download\n      path_to_cache=${{ steps.detect-platform.outputs.download-dir }}\n      mkdir -p \"$(dirname \"$path_to_cache\")\"\n      ln -sfn \"$PWD/.download\" \"$path_to_cache\"\n    shell: bash\n"
  },
  {
    "path": ".github/actions/upload_failure_logs_if_exists/action.yml",
    "content": "name: 'upload failure-logs if exists'\ndescription: 'upload failure-logs if exists'\ninputs:\n  suffix:\n    description: suffix to append to the name of the artifact\n    required: false\n    default: ''\nruns:\n  using: \"composite\"\n  steps:\n  - name: \"Check if failure-logs exists\"\n    if: always()\n    id: check-if-failure-logs-exists\n    run: echo \"exists=$([ -d \"failure-logs\" ] && echo \"true\" || echo \"false\")\" >> \"$GITHUB_OUTPUT\"\n    shell: bash\n  - id: normalize-suffix\n    # To avoid using special characters in artifact name, normalize the suffix\n    if: steps.check-if-failure-logs-exists.outputs.exists == 'true'\n    run: |\n      suffix=\"${{ inputs.suffix }}\"\n      suffix=\"${suffix//[^a-zA-Z0-9_]/_}\"\n      suffix=\"${suffix:+-$suffix}\"\n      echo \"result=$suffix\" >> \"$GITHUB_OUTPUT\"\n    shell: bash\n  - name: \"Upload failure-logs\"\n    if: steps.check-if-failure-logs-exists.outputs.exists == 'true'\n    uses: actions/upload-artifact@v4\n    with:\n      name: failure-logs-${{ github.job }}${{ steps.normalize-suffix.outputs.result }}\n      path: failure-logs/\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: gomod\n  directories:\n  - \"/\"\n  - \"/hack/tools\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n  groups:\n    golang-x:\n      patterns:\n      - \"golang.org/x/*\"\n    k8s:\n      patterns:\n      - \"k8s.io/*\"\n- package-ecosystem: github-actions\n  directory: \"/\"\n  schedule:\n    interval: daily\n  open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/workflows/codeql.yaml",
    "content": "name: \"CodeQL Advanced\"\n\non:\n  # paths-ignore should be kept in sync with test.yml\n  push:\n    branches: [\"master\"]\n    paths-ignore:\n    - \"docs/**\"\n    - \"website/**\"\n    - \"**.md\"\n  pull_request:\n    branches: [\"master\"]\n    paths-ignore:\n    - \"docs/**\"\n    - \"website/**\"\n    - \"**.md\"\n  schedule:\n  - cron: '33 19 * * 5'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  analyze:\n    name: Analyze (${{ matrix.language }})\n    runs-on: 'ubuntu-latest'\n    permissions:\n      security-events: write\n      # required to fetch internal or private CodeQL packs\n      packages: read\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - language: go\n          build-mode: autobuild\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8  # v4.33.0\n      with:\n        languages: ${{ matrix.language }}\n        build-mode: ${{ matrix.build-mode }}\n\n    - if: matrix.build-mode == 'manual'\n      shell: bash\n      run: |\n        echo 'If you are using a \"manual\" build mode for one or more of the' \\\n          'languages you are analyzing, replace this with the commands to build' \\\n          'your code, for example:'\n        echo '  make bootstrap'\n        echo '  make release'\n        exit 1\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8  # v4.33.0\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# Forked from https://github.com/containerd/nerdctl/blob/v0.8.1/.github/workflows/release.yml\n# Apache License 2.0\n\nname: Release\non:\n  # paths-ignore should be kept in sync with test.yml\n  push:\n    branches:\n    - 'master'\n    tags:\n    - 'v*'\n    paths-ignore:\n    - \"docs/**\"\n    - \"website/**\"\n    - \"**.md\"\n  pull_request:\n    branches:\n    - 'master'\n    paths-ignore:\n    - \"docs/**\"\n    - \"website/**\"\n    - \"**.md\"\nenv:\n  GO111MODULE: on\n  GOTOOLCHAIN: local\npermissions:\n  contents: read\n\njobs:\n  artifacts-darwin:\n    name: Artifacts Darwin\n    # The latest release of macOS is used to enable new features.\n    # https://github.com/lima-vm/lima/issues/2767\n    #\n    # Apparently, a binary built on a newer version of macOS can still run on\n    # an older release of macOS without an error.\n    # This is quite different from Linux and glibc.\n    runs-on: macos-26\n    timeout-minutes: 20\n    steps:\n    - name: \"Show xcode and SDK version\"\n      run: |\n        # Xcode version\n        xcodebuild -version\n        # macOS SDK version\n        xcrun --show-sdk-version || true\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make darwin artifacts\n      run: make artifacts-darwin\n    - name: \"Upload artifacts\"\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0\n      with:\n        name: artifacts-darwin\n        path: _artifacts/\n  release:\n    # An old release of Ubuntu is chosen for glibc compatibility\n    runs-on: ubuntu-22.04\n    needs: artifacts-darwin\n    timeout-minutes: 20\n    # The maximum access is \"read\" for PRs from public forked repos\n    # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token\n    permissions:\n      contents: write  # for releases\n      id-token: write  # for provenances\n      attestations: write  # for provenances\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1\n      with:\n        name: artifacts-darwin\n        path: _artifacts/\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Install gcc\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y gcc-x86-64-linux-gnu gcc-aarch64-linux-gnu\n    - name: \"Compile binaries\"\n      run: make artifacts-linux\n    - name: \"Make misc artifacts\"\n      run: make artifacts-misc\n    - name: \"Validate artifactts\"\n      run: ./hack/validate-artifact.sh ./_artifacts/*.tar.gz\n    - name: \"SHA256SUMS\"\n      run: |\n        ( cd _artifacts; sha256sum *.tar.gz ) | tee /tmp/SHA256SUMS\n        mv /tmp/SHA256SUMS _artifacts/SHA256SUMS\n    - name: \"The sha256sum of the SHA256SUMS file\"\n      run: (cd _artifacts; sha256sum SHA256SUMS)\n    - name: \"Prepare the release note\"\n      run: |\n        shasha=$(sha256sum _artifacts/SHA256SUMS | awk '{print $1}')\n        cat <<-EOF | tee /tmp/release-note.txt\n        (Changes to be documented)\n\n        ## Usage\n        \\`\\`\\`console\n        $ limactl create\n        $ limactl start\n        ...\n        INFO[0029] READY. Run \\`lima\\` to open the shell.\n\n        $ lima uname\n        Linux\n        \\`\\`\\`\n\n        - - -\n        The binaries were built automatically on GitHub Actions.\n        The build log is available for 90 days: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n        The sha256sum of the SHA256SUMS file itself is \\`${shasha}\\` .\n        - - -\n        Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE])\n        EOF\n    - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32  # v4.1.0\n      if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')\n      with:\n        subject-path: _artifacts/*\n    - name: \"Create release\"\n      if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      run: |\n        tag=\"${GITHUB_REF##*/}\"\n        gh release create -F /tmp/release-note.txt --draft --title \"${tag}\" \"${tag}\" _artifacts/*\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "name: Scorecard supply-chain security\non:\n  # For Branch-Protection check. Only the default branch is supported. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection\n  branch_protection_rule:\n  # To guarantee Maintained check is occasionally updated. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained\n  schedule:\n  - cron: '23 13 * * 4'\n  push:\n    branches:\n    - master\n  workflow_dispatch:\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n\n    steps:\n    - name: \"Checkout code\"\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n      with:\n        persist-credentials: false\n\n    - name: \"Run analysis\"\n      uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a  # v2.4.3\n      with:\n        results_file: results.sarif\n        results_format: sarif\n        # (Optional) \"write\" PAT token. Uncomment the `repo_token` line below if:\n        # - you want to enable the Branch-Protection check on a *public* repository, or\n        # - you are installing Scorecard on a *private* repository\n        # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.\n        # repo_token: ${{ secrets.SCORECARD_TOKEN }}\n\n        # Public repositories:\n        #   - Publish results to OpenSSF REST API for easy access by consumers\n        #   - Allows the repository to include the Scorecard badge.\n        #   - See https://github.com/ossf/scorecard-action#publishing-results.\n        publish_results: true\n\n    # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n    # format to the repository Actions tab.\n    - name: \"Upload artifact\"\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0\n      with:\n        name: SARIF file\n        path: results.sarif\n        retention-days: 5\n\n    # Upload the results to GitHub's code scanning dashboard (optional).\n    # Commenting out will disable upload of results to your repo's Code Scanning dashboard\n    - name: \"Upload to code-scanning\"\n      uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8  # v4.33.0\n      with:\n        sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/spell.yml",
    "content": "# split from test.yml so as to run spell checks for doc-only PRs too\nname: spell\n\non:\n  push:\n    branches:\n    - master\n    - 'release/**'\n  pull_request:\n\npermissions:\n  contents: read\n\njobs:\n  spell:\n    name: \"Spell check\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 5\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579  # v2.2\n      with:\n        check_filenames: true\n        check_hidden: true\n        # by default, codespell uses configuration from the .codespellrc\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  push:\n    branches:\n    - master\n    - 'release/**'\n    paths-ignore:\n    - \"docs/**\"\n    - \"website/**\"\n    - \"**.md\"\n  pull_request:\n    paths-ignore:\n    - \"docs/**\"\n    - \"website/**\"\n    - \"**.md\"\nenv:\n  LIMACTL_CREATE_ARGS: \"\"\n  GOTOOLCHAIN: local\n\npermissions: read-all\n\njobs:\n  lints:\n    name: \"Lints\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Install protoc and Go plugins\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y protobuf-compiler\n        make install-protoc-tools\n    - name: Verify generated files\n      run: make generate check-generated\n    - name: Run nobin\n      run: >-\n        go tool -modfile=./hack/tools/go.mod nobin\n        --allow-emoji\n        --allow-escape\n        --gitignore\n        --skip-ext pb.desc\n        --skip 'website/static/images/**/*.{gif,png}'\n        --skip 'docs/reports/**/*.pdf'\n    - name: Run editorconfig-checker\n      run: go tool -modfile=./hack/tools/go.mod editorconfig-checker\n    - name: Run yamllint\n      run: yamllint .\n    - name: Install shellcheck\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y shellcheck\n    - name: Run file and directory name linter\n      uses: ls-lint/action@02e380fe8733d499cbfc9e22276de5085508a5bd  # v2.3.1\n    - name: Run shellcheck\n      run: find . -name '*.sh' | xargs shellcheck\n    - name: Run shfmt\n      run: find . -name '*.sh' | xargs go tool -modfile=./hack/tools/go.mod shfmt -s -d\n    - name: Check hyperlinks\n      uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411  # v2.8.0\n      with:\n        args: >-\n          --exclude https://img.shields.io\n          --exclude http://127.0.0.1:8080\n          --exclude https://github.com/lima-vm/lima/releases/download\n          --exclude https://xbarapp.com\n          --exclude https://api.github.com\n          README.md\n        token: ${{ secrets.GITHUB_TOKEN }}\n    - name: Install go-licenses\n      # TODO: move to `go tool` after upgrading to v2\n      run: go install github.com/google/go-licenses@v1.6.0\n    - name: Check licenses\n      # the allow list corresponds to https://github.com/cncf/foundation/blob/e5db022a0009f4db52b89d9875640cf3137153fe/allowed-third-party-license-policy.md\n      # hashicorp/hcl/v2 is MPL-2.0; covered by the CNCF license exception for hashicorp/hcl\n      # see also https://github.com/cncf/foundation/issues/1242\n      run: go-licenses check --include_tests --ignore github.com/hashicorp/hcl/v2 ./... --allowed_licenses=$(cat ./hack/allowed-licenses.txt)\n    - name: Check license boilerplates\n      run: go tool -modfile=./hack/tools/go.mod ltag -t ./hack/ltag --check -v\n    - name: Check protobuf files\n      run: go tool -modfile=./hack/tools/go.mod protolint .\n\n  lint-go:\n    name: \"Lint Go\"\n    timeout-minutes: 30\n    strategy:\n      matrix:\n        runs-on: [ubuntu-24.04, macos-26, windows-2025]\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n    - name: Force git to use LF\n      # This step is required on Windows to work around golangci-lint issues with formatters. See https://github.com/golangci/golangci-lint/discussions/5840\n      # TODO: replace with a checkout option when https://github.com/actions/checkout/issues/226 is implemented\n      if: runner.os == 'Windows'\n      run: |\n        git config --global core.autocrlf false\n        git config --global core.eol lf\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - id: golangci-lint-version\n      shell: bash\n      working-directory: hack/tools\n      run: |\n        echo \"GOLANGCI_LINT_VERSION=$(go list -m -f '{{.Version}}' github.com/golangci/golangci-lint/v2)\" >> $GITHUB_OUTPUT\n    - name: Run golangci-lint\n      uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20  # v9.2.0\n      with:\n        version: ${{ steps.golangci-lint-version.outputs.GOLANGCI_LINT_VERSION }}\n        args: --verbose\n\n  security:\n    name: \"Vulncheck\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 5\n    steps:\n    - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee  # v1.0.4\n\n  unit:\n    name: \"Unit tests\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n    strategy:\n      fail-fast: false\n      matrix:\n        # For non-Homebrew we have to support an old release of Go\n        go-version: [\"oldstable\", \"stable\"]\n    steps:\n    - name: Install test dependencies\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y --no-install-recommends qemu-utils\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: ${{ matrix.go-version }}\n    - name: Unit tests\n      run: go test -v ./...\n    - name: Make\n      run: make\n    - name: Install\n      run: sudo make install\n    - name: Verify templates match `limactl edit` format\n      run: |\n        find templates -name '*.yaml' -exec limactl edit --set 'del(.nothing)' {} \\;\n        git diff-index --exit-code HEAD\n    - name: Uninstall\n      run: sudo make uninstall\n\n  windows:\n    name: \"Windows tests (WSL2)\"\n    runs-on: windows-2025\n    timeout-minutes: 30\n    steps:\n    - name: Enable WSL2\n      run: |\n        wsl --set-default-version 2\n        wsl --shutdown\n        wsl --update\n        wsl --status\n        wsl --version\n        wsl --list --online\n    - name: Install WSL2 distro\n      timeout-minutes: 1\n      run: |\n        # FIXME: At least one distro has to be installed here,\n        # otherwise `wsl --list --verbose` (called from Lima) fails:\n        # https://github.com/lima-vm/lima/pull/1826#issuecomment-1729993334\n        # The distro image itself is not consumed by Lima.\n        # Starting with WSL2 version 2.5.7.0 the distro will be rejected\n        # if it doesn't contain /bin/sh and /etc.\n        # ------------------------------------------------------------------\n        mkdir dummy\n        mkdir dummy\\bin\n        mkdir dummy\\etc\n        echo \"\" >dummy\\bin\\sh\n        tar -cf dummy.tar --format ustar -C dummy .\n        wsl --import dummy $env:TEMP dummy.tar\n        wsl --list --verbose\n    - name: Set gitconfig\n      run: |\n        git config --global core.autocrlf false\n        git config --global core.eol lf\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Unit tests\n      run: go test -v ./...\n    - name: Make\n      run: make\n    - name: Integration tests (WSL2, Windows host)\n      run: |\n        $env:PATH = \"$pwd\\_output\\bin;\" + 'C:\\msys64\\usr\\bin;' + $env:PATH\n        pacman -Sy --noconfirm openbsd-netcat diffutils socat w3m\n        $env:MSYS2_ENV_CONV_EXCL = 'HOME_HOST;HOME_GUEST;_LIMA_WINDOWS_EXTRA_PATH'\n        $env:HOME_HOST = $(cygpath.exe \"$env:USERPROFILE\")\n        $env:HOME_GUEST = \"/mnt$env:HOME_HOST\"\n        $env:LIMACTL_CREATE_ARGS = '--vm-type=wsl2 --mount-type=wsl2 --containerd=system'\n        $env:_LIMA_WINDOWS_EXTRA_PATH = 'C:\\Program Files\\Git\\usr\\bin'\n        bash.exe -c \"./hack/test-templates.sh templates/experimental/wsl2.yaml\"\n\n  windows-qemu:\n    name: \"Windows tests (QEMU)\"\n    runs-on: windows-2025\n    timeout-minutes: 30\n    steps:\n    - name: Set gitconfig\n      run: |\n        git config --global core.autocrlf false\n        git config --global core.eol lf\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make\n      run: make\n    - name: Install QEMU\n      run: |\n        winget install --silent --accept-source-agreements --accept-package-agreements --disable-interactivity SoftwareFreedomConservancy.QEMU\n    - name: Integration tests (QEMU, Windows host)\n      run: |\n        $env:PATH = \"$pwd\\_output\\bin;\" + 'C:\\msys64\\usr\\bin;' + 'C:\\Program Files\\QEMU;' + $env:PATH\n        pacman -Sy --noconfirm openbsd-netcat diffutils socat w3m\n        $env:MSYS2_ENV_CONV_EXCL = 'HOME_HOST;HOME_GUEST;_LIMA_WINDOWS_EXTRA_PATH'\n        $env:HOME_HOST = $(cygpath.exe \"$env:USERPROFILE\")\n        $env:HOME_GUEST = \"$env:HOME_HOST\"\n        $env:LIMACTL_CREATE_ARGS = '--vm-type=qemu'\n        $env:_LIMA_WINDOWS_EXTRA_PATH = 'C:\\Program Files\\Git\\usr\\bin'\n        bash.exe -c \"./hack/test-templates.sh templates/default.yaml\"\n\n  qemu:\n    name: \"Integration tests (QEMU, macOS host)\"\n    runs-on: macos-15-large  # Intel\n    timeout-minutes: 120\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Unit tests\n      run: go test -v ./...\n    - name: Make\n      run: make\n    - name: \"Inject `no_timer_check` to kernel cmdline\"\n      # workaround to https://github.com/lima-vm/lima/issues/84\n      run: |\n        export PATH=\"$PWD/_output/bin:$PATH\"\n        ./hack/inject-cmdline-to-template.sh _output/share/lima/templates/_images/ubuntu.yaml no_timer_check\n    - name: Install\n      run: sudo make install\n    - name: Validate jsonschema\n      run: make schema-limayaml.json\n    - name: Validate templates\n      # Can't validate base templates in `_default` because they have no images\n      run: find -L templates -name '*.yaml' ! -path '*/_default/*' | xargs limactl validate\n    - name: Install test dependencies\n      # QEMU:      required by Lima itself\n      # bash:      required by test-templates.sh (OS version of bash is too old)\n      # coreutils: required by test-templates.sh for the \"timeout\" command\n      # w3m :      required by test-templates.sh for port forwarding tests\n      # socat:     required by test-templates.sh for port forwarding tests\n      run: brew install qemu bash coreutils w3m socat\n    - name: \"Adjust LIMACTL_CREATE_ARGS\"\n      run: echo \"LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --vm-type=qemu\" >>$GITHUB_ENV\n    - name: Cache image used by default.yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/default.yaml\n    - name: \"Show cache\"\n      run: ./hack/debug-cache.sh\n    - name: \"Test default.yaml\"\n      uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08  # v3.0.2\n      with:\n        timeout_minutes: 30\n        retry_on: error\n        max_attempts: 3\n        command: ./hack/test-templates.sh templates/default.yaml\n    # GHA macOS is slow and flaky, so we only test default.yaml here.\n    # Other yamls are tested on Linux instances.\n    #\n    - if: always()\n      uses: ./.github/actions/upload_failure_logs_if_exists\n    - name: \"Show cache\"\n      if: always()\n      run: ./hack/debug-cache.sh\n\n  # Non-default templates are tested on Linux instances of GHA,\n  # as they seem more stable than macOS instances.\n  qemu-linux:\n    name: \"Integration tests (QEMU, Linux host)\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 120\n    strategy:\n      fail-fast: false\n      matrix:\n        # Most templates use 9p as the mount type\n        template:\n        - alpine.yaml\n        - debian.yaml  # reverse-sshfs\n        - fedora.yaml\n        - archlinux.yaml\n        - opensuse.yaml\n        - docker.yaml\n        - ../hack/test-templates/alpine-iso-9p-writable.yaml  # Covers alpine-iso.yaml\n        - ../hack/test-templates/net-user-v2.yaml\n        - ../hack/test-templates/test-misc.yaml  # TODO: merge net-user-v2 into test-misc\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make\n      run: make\n    - name: Install\n      run: sudo make install\n    - name: Cache image used by templates/${{ matrix.template }}\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/${{ matrix.template }}\n    - name: Install test dependencies\n      run: |\n        sudo apt-get update\n        sudo ./hack/install-qemu.sh\n        sudo apt-get install -y --no-install-recommends socat w3m\n    - name: Install ansible-playbook\n      run: |\n        sudo apt-get install -y --no-install-recommends ansible\n      if: matrix.template == '../hack/test-templates/test-misc.yaml'\n    - name: \"Show cache\"\n      run: ./hack/debug-cache.sh\n    - name: \"Test\"\n      uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08  # v3.0.2\n      with:\n        timeout_minutes: 30\n        retry_on: error\n        max_attempts: 3\n        command: ./hack/test-templates.sh templates/${{ matrix.template }}\n    - if: always()\n      uses: ./.github/actions/upload_failure_logs_if_exists\n      with:\n        suffix: ${{ matrix.template }}\n    - name: \"Show cache\"\n      run: ./hack/debug-cache.sh\n\n  bats:\n    name: \"Integration tests (BATS)\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n      with:\n        # BATS tests don't expect lima version warnings like:\n        # msg=\"treating lima version \\\"ea336ae\\\" from \\\"/Users/runner/.lima-bats/dummy/lima-version\\\" as very latest release\"\n        fetch-depth: 0\n        submodules: true\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make\n      run: make\n    - name: Install\n      run: sudo make install\n    - name: Install test dependencies\n      run: |\n        sudo apt-get update\n        sudo ./hack/install-qemu.sh\n    - name: Cache image used by templates/default.yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/default.yaml\n    - name: \"Run BATS integration tests\"\n      run: make bats\n      env:\n        # The url-github.bats tests makes GitHub API requests\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n    - name: Cache image used by templates/k8s.yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/k8s.yaml\n    - name: \"Run BATS k8s tests\"\n      run: ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/extras/k8s.bats\n      env:\n        LIMA_BATS_ALL_TESTS_RETRIES: 3\n    - name: Cache image used by templates/freebsd.yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/freebsd.yaml\n    - name: \"Run BATS freebsd tests\"\n      run: |\n        set -eux -o pipefail\n        # xorriso is required for creating Joliet filesystem\n        sudo apt-get install -y xorriso\n        ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/extras/freebsd.bats\n\n  colima:\n    name: \"Colima tests (QEMU, Linux host)\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 120\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n      with:\n        # fetch-depth is set to 0 to let `limactl --version` print semver-ish version\n        # fetch-depth: 0 is required for Colima integration test because Colima\n        # checks Lima's version and requires proper semantic version format\n        fetch-depth: 0\n        ref: ${{ github.event.pull_request.head.sha }}\n        submodules: true\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make\n      run: make\n    - name: Install\n      run: sudo make install\n    - name: Install colima\n      id: install-colima\n      run: |\n        git clone https://github.com/abiosoft/colima\n        cd colima\n        latest=\"$(git tag --sort=-v:refname | head -n1)\"\n        git checkout \"$latest\"\n        make\n        sudo make install\n        echo \"version=$latest\" >>$GITHUB_OUTPUT\n    - name: Install test dependencies\n      run: |\n        sudo apt-get update\n        sudo ./hack/install-qemu.sh\n    - name: Cache colima\n      uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7  # v5.0.4\n      with:\n        path: ~/.cache/colima\n        key: colima-${{ steps.install-colima.outputs.version }}\n    - name: Run BATS colima tests\n      run: ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/extras/colima.bats\n\n  vmnet:\n    name: \"VMNet tests (QEMU)\"\n    runs-on: macos-15-large  # Intel\n    timeout-minutes: 120\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make\n      run: make\n    - name: \"Inject `no_timer_check` to kernel cmdline\"\n      # workaround to https://github.com/lima-vm/lima/issues/84\n      run: |\n        export PATH=\"$PWD/_output/bin:$PATH\"\n        ./hack/inject-cmdline-to-template.sh _output/share/lima/templates/_images/ubuntu.yaml no_timer_check\n    - name: Install\n      run: sudo make install\n    - name: \"Adjust LIMACTL_CREATE_ARGS\"\n      run: echo \"LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --vm-type=qemu --network=lima:shared\" >>$GITHUB_ENV\n    - name: Cache image used by default .yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/default.yaml\n    - name: Install test dependencies\n      run: brew install qemu bash coreutils w3m socat\n    - name: Install socket_vmnet\n      env:\n        SOCKET_VMNET_VERSION: v1.2.2\n      run: |\n        (\n          cd ~\n          git clone https://github.com/lima-vm/socket_vmnet\n          cd socket_vmnet\n          git checkout $SOCKET_VMNET_VERSION\n          sudo git config --global --add safe.directory /Users/runner/socket_vmnet\n          sudo make PREFIX=/opt/socket_vmnet install\n        )\n        limactl sudoers | sudo tee /etc/sudoers.d/lima\n    - name: Unit test (pkg/networks) with socket_vmnet\n      # Set -count=1 to disable cache\n      run: go test -v -count=1 ./pkg/networks/...\n    - name: Test socket_vmnet\n      uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08  # v3.0.2\n      with:\n        timeout_minutes: 30\n        retry_on: error\n        max_attempts: 3\n        command: ./hack/test-templates.sh templates/default.yaml\n    - if: always()\n      uses: ./.github/actions/upload_failure_logs_if_exists\n\n  upgrade:\n    name: \"Upgrade tests (QEMU, macOS host)\"\n    runs-on: macos-15-large  # Intel\n    timeout-minutes: 120\n    strategy:\n      matrix:\n        oldver: [\"v0.15.1\"]  # The default VM type was always QEMU until Lima v1.0\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - name: Fetch homebrew-core commit messages\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n      with:\n        # needed by ./hack/brew-install-version.sh\n        repository: homebrew/homebrew-core\n        path: homebrew-core\n        fetch-depth: 0\n        filter: tree:0\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Cache image used by ${{ matrix.oldver }}/examples/ubuntu-lts.yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: https://raw.githubusercontent.com/lima-vm/lima/${{ matrix.oldver }}/examples/ubuntu-lts.yaml\n    - name: Install test dependencies\n      run: |\n        brew install bash coreutils\n        # QEMU 9.1.0 seems to break on GitHub runners, both on Monterey and Ventura\n        # We revert back to 8.2.1, which seems to work fine\n        git config --global user.name \"GitHub Actions Bot\"\n        git config --global user.email \"nobody@localhost\"\n        ./hack/brew-install-version.sh qemu 8.2.1\n    - name: Test\n      uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08  # v3.0.2\n      with:\n        timeout_minutes: 30\n        retry_on: error\n        max_attempts: 3\n        command: ./hack/test-upgrade.sh ${{ matrix.oldver }} ${{ github.sha }}\n    - if: always()\n      uses: ./.github/actions/upload_failure_logs_if_exists\n\n  vz:\n    name: \"Integration tests (vz)\"\n    runs-on: macos-15-large  # Intel\n    timeout-minutes: 120\n    strategy:\n      fail-fast: false\n      matrix:\n        template:\n        - default.yaml\n    steps:\n    - name: \"Adjust LIMACTL_CREATE_ARGS\"\n      # --cpus=1 is needed for running vz on GHA: https://github.com/lima-vm/lima/pull/1511#issuecomment-1574937888\n      run: echo \"LIMACTL_CREATE_ARGS=${LIMACTL_CREATE_ARGS} --cpus 1 --memory 1\" >>$GITHUB_ENV\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make\n      run: make\n    - name: Install\n      run: sudo make install\n    - name: Cache image used by templates/${{ matrix.template }}\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/${{ matrix.template }}\n    - name: Install test dependencies\n      run: brew install bash coreutils w3m socat\n    - name: Uninstall qemu\n      run: brew uninstall --ignore-dependencies --force qemu\n    - name: Test\n      run: ./hack/test-templates.sh templates/${{ matrix.template }}\n    - if: failure()\n      uses: ./.github/actions/upload_failure_logs_if_exists\n      with:\n        suffix: ${{ matrix.template }}\n\n  # gomodjail is a library sandbox for Go\n  # https://github.com/AkihiroSuda/gomodjail\n  #\n  # This is an early experiment.\n  # CI failures that only occurs with gomodjail shall not block merging PRs.\n  gomodjail:\n    name: \"gomodjail (experimental; failures shall not block merging PRs)\"\n    runs-on: macos-15-large  # Intel\n    timeout-minutes: 30\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Install gomodjail\n      run: |\n        set -eux -o pipefail\n        git clone https://github.com/AkihiroSuda/gomodjail\n        cd gomodjail\n        make binaries install\n    - name: Install Lima\n      # gomodjail depends on symbols\n      run: |\n        make KEEP_SYMBOLS=1 binaries\n        sudo make install\n    - name: Cache image used by templates/default.yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/default.yaml\n    - name: Smoke test\n      run: gomodjail run --go-mod=./go.mod -- limactl start --tty=false\n\n  cross:\n    name: \"Cross-compile (NetBSD, DragonFlyBSD)\"\n    runs-on: ubuntu-24.04\n    timeout-minutes: 30\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - run: GOOS=netbsd go build ./...\n    - run: GOOS=dragonfly go build ./...\n\n  qemu-linux-old:\n    name: \"Smoke tests (QEMU, old Linux host)\"\n    runs-on: ubuntu-22.04  # QEMU 6.2\n    timeout-minutes: 30\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417  # v6.3.0\n      with:\n        go-version: stable\n    - name: Make\n      run: make\n    - name: Install\n      run: sudo make install\n    - name: Cache image used by templates/default.yaml\n      uses: ./.github/actions/setup_cache_for_template\n      with:\n        template: templates/default.yaml\n    - name: Install test dependencies\n      run: |\n        sudo apt-get update\n        sudo ./hack/install-qemu.sh\n    - name: Smoke test\n      run: limactl start --tty=false\n"
  },
  {
    "path": ".gitignore",
    "content": "_output/\n_artifacts/\nlima.REJECTED.yaml\ndefault-template.yaml\nschema-limayaml.json\n.config\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"hack/bats/lib/bats-core\"]\n\tpath = hack/bats/lib/bats-core\n\turl = https://github.com/bats-core/bats-core.git\n\tbranch = master\n[submodule \"hack/bats/lib/bats-file\"]\n\tpath = hack/bats/lib/bats-file\n\turl = https://github.com/bats-core/bats-file.git\n\tbranch = master\n[submodule \"hack/bats/lib/bats-assert\"]\n\tpath = hack/bats/lib/bats-assert\n\turl = https://github.com/bats-core/bats-assert.git\n\tbranch = master\n[submodule \"hack/bats/lib/bats-support\"]\n\tpath = hack/bats/lib/bats-support\n\turl = https://github.com/bats-core/bats-support.git\n\tbranch = master\n"
  },
  {
    "path": ".golangci.yml",
    "content": "# golangci-lint configuration file.\n# https://golangci-lint.run/usage/configuration/\nversion: \"2\"\nrun:\n  concurrency: 6\nlinters:\n  default: none\n  enable:\n  - bodyclose\n  - copyloopvar\n  - depguard\n  - dupword\n  - errcheck\n  - errorlint\n  - forbidigo\n  - gocritic\n  - godot\n  - govet\n  - ineffassign\n  - intrange\n  - misspell\n  - modernize\n  - nakedret\n  - noctx\n  - nolintlint\n  - perfsprint\n  - revive\n  - staticcheck\n  - unconvert\n  - unused\n  - usetesting\n  - whitespace\n  settings:\n    depguard:\n      rules:\n        main:\n          deny:\n          - pkg: golang.org/x/net/context\n            desc: use the 'context' package from the standard library\n          - pkg: math/rand$\n            desc: use the 'math/rand/v2' package\n    errorlint:\n      asserts: false\n    forbidigo:\n      forbid:\n      - pattern: ^print(ln)?$\n        msg: Do not use builtin print functions. It's for bootstrapping only and may be removed in the future.\n      - pattern: ^fmt\\.Print.*$\n        msg: Do not use fmt.Print statements.\n      - pattern: ^testing.T.Fatal.*$\n        msg: Use assert functions from the gotest.tools/v3/assert package instead.\n      - pattern: ^sort.*$\n        msg: Use sorting functions from the slices package instead.\n      analyze-types: true\n    gocritic:\n      disabled-checks:\n      - appendCombine\n      - sloppyReassign\n      - unlabelStmt\n      - rangeValCopy\n      - hugeParam\n      - importShadow\n      - sprintfQuotedString\n      - builtinShadow\n      - filepathJoin\n      # See \"Tags\" section in https://github.com/go-critic/go-critic#usage\n      enabled-tags:\n      - diagnostic\n      - performance\n      - style\n      - opinionated\n      - experimental\n      settings:\n        ifElseChain:\n          # Min number of if-else blocks that makes the warning trigger.\n          minThreshold: 3\n    modernize:\n      disable:\n      - omitzero\n    perfsprint:\n      int-conversion: false\n      integer-format: false\n      err-error: false\n      errorf: true\n      sprintf1: false\n      strconcat: false\n      concat-loop: false\n    revive:\n      # Set below 0.8 to enable error-strings\n      confidence: 0.6\n      # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md\n      rules:\n      - name: bare-return\n      - name: blank-imports\n      - name: context-as-argument\n      - name: context-keys-type\n      - name: deep-exit\n      - name: dot-imports\n        arguments:\n        - allowedPackages:\n          - github.com/lima-vm/lima/v2/pkg/must\n      - name: empty-block\n      - name: error-naming\n      - name: error-return\n      - name: error-strings\n      - name: errorf\n      - name: exported\n        disabled: true\n      - name: increment-decrement\n      - name: indent-error-flow\n      - name: package-comments\n        disabled: true\n      - name: package-directory-mismatch\n      - name: range\n      - name: receiver-naming\n      - name: redefines-builtin-id\n      - name: superfluous-else\n      - name: time-naming\n      - name: unexported-return\n      - name: unnecessary-format\n      - name: unreachable-code\n      - name: unused-parameter\n      - name: use-any\n      - name: var-declaration\n      - name: var-naming\n    staticcheck:\n      # https://staticcheck.dev/docs/configuration/options/#checks\n      checks:\n      - all\n      - -QF1001  # apply De Morgan's law\n      - -QF1008  # remove embedded field from selector\n      - -SA3000  # false positive for Go 1.15+. See https://github.com/golang/go/issues/34129\n      - -ST1000\n      - -ST1001  # duplicates revive.dot-imports\n      - -ST1022\n    usetesting:\n      os-temp-dir: true\n      context-background: true\n      context-todo: true\n  exclusions:\n    presets:\n    - common-false-positives\n    - legacy\n    - std-error-handling\n    rules:\n    # Allow using Uid, Gid in pkg/osutil.\n    - path: pkg/osutil/\n      text: '(?i)(uid)|(gid)'\n    # Disable some linters for test files.\n    - linters:\n      - godot\n      path: _test\\.go\n    # Allow \"api\" package names in hostagent and guestagent.\n    - linters:\n      - revive\n      path: pkg/(hostagent|guestagent)/api/\n      text: avoid meaningless package names\nissues:\n  # Maximum issues count per one linter.\n  max-issues-per-linter: 0\n  # Maximum count of issues with the same text.\n  max-same-issues: 0\nformatters:\n  enable:\n  - gci\n  - gofmt\n  - gofumpt\n  settings:\n    gci:\n      sections:\n      - standard\n      - default\n      - localmodule\n"
  },
  {
    "path": ".ls-lint.yml",
    "content": "# ls-lint configuration file.\n# https://ls-lint.org/2.2/configuration/the-basics.html\nls:\n  .dir: kebab-case\n  .go: snake_case\n  .lima: snake_case\n  .sh: kebab-case\n  .TEMPLATE.yaml: kebab-case\n  .yaml: kebab-case\n  .yml: kebab-case\n\n  .github:\n    .yaml: snake_case\n\n  cmd/limactl:\n    # valid names are `show-ssh.go`, `show-ssh_windows.go` or `show-ssh_test.go`\n    .go: lowercase\n\n  templates:\n    # _default and _images have leading underscores\n    .dir: lowercase\n\n  website/content:\n    .dir: lowercase\n\nignore:\n- .git\n- .golangci.yml\n- .ls-lint.yml\n- '_output'\n- '**/*.pb\\.go'\n- hack/common.inc.sh\n- pkg/cidata/cidata.TEMPLATE.d\n- pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh\n- website\n# \"ubuntu-24.04\" does not follow the kebab-case\n- '**/ubuntu-*\\.yaml'\n# \"boot.<OS>\" does not follow the kebab-case\n- '**/boot.*'\n"
  },
  {
    "path": ".markdownlint.json",
    "content": "{\n  \"first-line-h1\": false,\n  \"no-inline-html\": false,\n  \"blanks-around-headers\": false,\n  \"blanks-around-lists\": false,\n  \"blanks-around-fences\": false,\n  \"commands-show-output\": false,\n  \"ul-style\": false,\n  \"line-length\": false\n}\n"
  },
  {
    "path": ".protolint.yaml",
    "content": "# This is the configuration for protolint.\n# See https://github.com/yoheimuta/protolint/blob/v0.55.6/_example/config/.protolint.yaml for details.\n---\nlint:\n  rules:\n    # Enable all default rules.\n    no_default: false\n  rules_option:\n    # MAX_LINE_LENGTH rule option.\n    max_line_length:\n      max_chars: 100\n"
  },
  {
    "path": ".shellcheckrc",
    "content": "source-path=SCRIPTDIR\n"
  },
  {
    "path": ".yamllint",
    "content": "---\n\nextends: default\n\nignore: |\n  # this is a yaml template, needs to be executed\n  pkg/cidata/cloud-config.yaml\n\nrules:\n  indentation:\n    indent-sequences: false\n  truthy:\n    allowed-values: ['true', 'false', 'on', 'off']\n  comments-indentation: disable\n  document-start: disable\n  line-length: disable\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MAINTAINERS.md",
    "content": "Moved to <https://lima-vm.io/docs/community/governance/>.\n"
  },
  {
    "path": "Makefile",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n# Files are installed under $(DESTDIR)/$(PREFIX)\nPREFIX ?= /usr/local\nDEST := $(shell echo \"$(DESTDIR)/$(PREFIX)\" | sed 's:///*:/:g; s://*$$::')\n\nGO ?= go\nTAR ?= tar\nZIP ?= zip\nPLANTUML ?= plantuml # may also be \"java -jar plantuml.jar\" if installed elsewhere\n\nGOARCH ?= $(shell $(GO) env GOARCH)\nGOHOSTARCH := $(shell $(GO) env GOHOSTARCH)\nGOHOSTOS := $(shell $(GO) env GOHOSTOS)\nGOOS ?= $(shell $(GO) env GOOS)\nifeq ($(GOOS),windows)\nbat = .bat\nexe = .exe\nendif\n\nifeq ($(GOOS),darwin)\nMACOS_SDK_VERSION = $(shell xcrun --show-sdk-version | cut -d . -f 1)\n# xcrun command seems to fail even when the SDK is available:\n# > xcrun: error: unable to lookup item 'SDKVersion' in SDK '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk'\nifeq ($(MACOS_SDK_VERSION),)\nMACOS_SDK_VERSION = $(shell readlink /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk | sed -E -e \"s/^MacOSX//\" -e \"s/\\.[0-9]+\\.sdk//\")\nendif\nendif\n\nDEFAULT_ADDITIONAL_DRIVERS :=\nifeq ($(GOOS),darwin)\nifeq ($(GOARCH),arm64)\nifeq ($(shell test $(MACOS_SDK_VERSION) -ge 14; echo $$?),0)\n# krunkit needs macOS 14 or later: https://github.com/containers/libkrun/blob/main/README.md#macos-efi-variant\nDEFAULT_ADDITIONAL_DRIVERS += krunkit\nendif\nendif\nendif\nADDITIONAL_DRIVERS ?= $(DEFAULT_ADDITIONAL_DRIVERS)\n\nGO_BUILDTAGS ?=\nifeq ($(GOOS),darwin)\nifeq ($(shell test $(MACOS_SDK_VERSION) -lt 13; echo $$?),0)\n# The \"vz\" mode needs macOS 13 SDK or later\nGO_BUILDTAGS += no_vz\nendif\nendif\n\nifeq ($(GOHOSTOS),windows)\nWINVER_MAJOR=$(shell powershell.exe \"[System.Environment]::OSVersion.Version.Major\")\nifeq ($(WINVER_MAJOR),10)\nWINVER_BUILD=$(shell powershell.exe \"[System.Environment]::OSVersion.Version.Build\")\nWINVER_BUILD_HIGH_ENOUGH=$(shell powershell.exe $(WINVER_BUILD) -ge 19041)\nifeq ($(WINVER_BUILD_HIGH_ENOUGH),False)\nGO_BUILDTAGS += no_wsl\nendif\nendif\nendif\n\nPACKAGE := github.com/lima-vm/lima/v2\n\nVERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags 2>/dev/null)\nVERSION_TRIMMED := $(VERSION:v%=%)\n\nifeq ($(VERSION),)\n$(error VERSION could not be determined. Build from a git repo or run: make VERSION=vX.Y.Z)\nendif\n\n# `DEBUG` flag to build binaries with debug information for use by `dlv exec`.\n# This implies KEEP_DWARF=1 and KEEP_SYMBOLS=1.\nDEBUG ?=\nGO_BUILD_GCFLAGS ?=\nKEEP_DWARF ?=\nKEEP_SYMBOLS ?=\nifeq ($(DEBUG),1)\n\t# Disable optimizations and inlining to make debugging easier.\n\tGO_BUILD_GCFLAGS = -gcflags=\"all=-N -l\"\n\t# Keep the symbol table\n\tKEEP_DWARF = 1\n\t# Enable DWARF generation\n\tKEEP_SYMBOLS = 1\nendif\n\nGO_BUILD_LDFLAGS_W := true\nifeq ($(KEEP_DWARF),1)\n\tGO_BUILD_LDFLAGS_W = false\nendif\n\nGO_BUILD_LDFLAGS_S := true\nifeq ($(KEEP_SYMBOLS),1)\n\tGO_BUILD_LDFLAGS_S = false\nendif\n# `-s`: Strip the symbol table according to the KEEP_SYMBOLS config\n# `-w`: Disable DWARF generation according to the KEEP_DWARF config\n# `-X`: Embed version information.\nGO_BUILD_LDFLAGS := -ldflags=\"-s=$(GO_BUILD_LDFLAGS_S) -w=$(GO_BUILD_LDFLAGS_W) -X $(PACKAGE)/pkg/version.Version=$(VERSION)\"\n# `go -version -m` returns -tags with comma-separated list, because space-separated list is deprecated in go1.13.\n# converting to comma-separated list is useful for comparing with the output of `go version -m`.\nGO_BUILD_FLAG_TAGS := $(addprefix -tags=,$(shell echo \"$(GO_BUILDTAGS)\"|tr \" \" \"\\n\"|paste -sd \",\" -))\nGO_BUILD := $(strip $(GO) build $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS))\n\n################################################################################\n# Features\n.NOTPARALLEL:\n.SECONDEXPANSION:\n\n################################################################################\n.PHONY: all\nall: binaries manpages\n\n################################################################################\n# Help\n.PHONY: help\nhelp:\n\t@echo  '  binaries        - Build all binaries'\n\t@echo  '  manpages        - Build manual pages'\n\t@echo\n\t@echo  '  help-variables  - Show Makefile variables'\n\t@echo  '  help-targets    - Show additional Makefile targets'\n\n.PHONY: help-variables\nhelp-variables:\n\t@echo  '# Variables that can be overridden.'\n\t@echo\n\t@echo  '- PREFIX       (directory)  : Installation prefix (default: /usr/local)'\n\t@echo  '- KEEP_DWARF   (1 or 0)     : Whether to keep DWARF information (default: 0)'\n\t@echo  '- KEEP_SYMBOLS (1 or 0)     : Whether to keep symbols (default: 0)'\n\t@echo  '- DEBUG        (1 or 0)     : Whether to build with debug information (default: 0)'\n\n.PHONY: help-targets\nhelp-targets:\n\t@echo  '# Targets can be categorized by their location.'\n\t@echo\n\t@echo  'Targets for files in _output/bin/:'\n\t@echo  '- limactl                   : Build limactl, and lima'\n\t@echo  '- lima                      : Copy lima, and lima.bat'\n\t@echo  '- helpers                   : Copy nerdctl.lima, apptainer.lima, docker.lima, podman.lima, and kubectl.lima'\n\t@echo\n\t@echo  'Targets for files in _output/libexec/lima/:'\n\t@echo  '- limactl-plugins           : Build limactl-* CLI plugins'\n\t@echo\n\t@echo  'Targets for files in _output/share/lima/:'\n\t@echo  '- guestagents               : Build guestagents'\n\t@echo  '- native-guestagent         : Build guestagent for native arch'\n\t@echo  '- additional-guestagents    : Build guestagents for archs other than native arch'\n\t@echo  '- <arch>-guestagent         : Build guestagent for <arch>: $(sort $(LINUX_GUESTAGENT_ARCHS))'\n\t@echo\n\t@echo  'Targets for files in _output/share/lima/templates/:'\n\t@echo  '- templates                 : Copy templates'\n\t@echo  '- template_experimentals    : Copy experimental templates to experimental/'\n\t@echo  '- default_template          : Copy default.yaml template'\n\t@echo  '- update-templates          : Update templates'\n\t@echo\n\t@echo  'Targets for files in _output/share/doc/lima:'\n\t@echo  '- documentation             : Copy documentation to _output/share/doc/lima'\n\t@echo  '- create-links-in-doc-dir   : Create some symlinks pointing ../../lima/templates'\n\t@echo\n\t@echo  '# e.g. to install limactl, helpers, native guestagent, and templates:'\n\t@echo  '#   make native install'\n\n.PHONY: help-artifact\nhelp-artifact:\n\t@echo  '# Targets for building artifacts to _artifacts/'\n\t@echo\n\t@echo  'Targets to building multiple archs artifacts for GOOS:'\n\t@echo  '- artifacts                 : Build artifacts for current OS and supported archs'\n\t@echo  '- artifacts-<GOOS>          : Build artifacts for supported archs and <GOOS>: darwin, linux, or windows'\n\t@echo\n\t@echo  'Targets to building GOOS and ARCH (GOARCH, or uname -m) specific artifacts:'\n\t@echo  '- artifact                  : Build artifacts for current GOOS and GOARCH'\n\t@echo  '- artifact-<GOOS>           : Build artifacts for current GOARCH and <GOOS>: darwin, linux, or windows'\n\t@echo  '- artifact-<ARCH>           : Build artifacts for current GOOS with <ARCH>: amd64, arm64, x86_64, or aarch64'\n\t@echo  '- artifact-<GOOS>-<ARCH>    : Build artifacts for <GOOS> and <ARCH>'\n\t@echo\n\t@echo  '# GOOS and GOARCH can be specified with make parameters or environment variables.'\n\t@echo  '# e.g. to build artifact for linux and arm64:'\n\t@echo  '#   make GOOS=linux GOARCH=arm64 artifact'\n\t@echo\n\t@echo  'Targets for miscellaneous artifacts:'\n\t@echo  '- artifacts-misc            : Build artifacts for go.mod, go.sum, and vendor'\n\n################################################################################\n# convenience targets\nexe: _output/bin/limactl$(exe)\n\n.PHONY: minimal native\nminimal: clean limactl native-guestagent default_template\nnative: clean limactl limactl-plugins helpers native-guestagent templates template_experimentals additional-drivers\n\n################################################################################\n# These configs were once customizable but should no longer be changed.\nCONFIG_GUESTAGENT_OS_LINUX=y\nCONFIG_GUESTAGENT_OS_DARWIN=\nifeq ($(GOOS),darwin)\nifeq ($(GOARCH),arm64)\nCONFIG_GUESTAGENT_OS_DARWIN=y\nendif\nendif\nCONFIG_GUESTAGENT_ARCH_X8664=y\nCONFIG_GUESTAGENT_ARCH_AARCH64=y\nCONFIG_GUESTAGENT_ARCH_ARMV7L=y\nCONFIG_GUESTAGENT_ARCH_PPC64LE=y\nCONFIG_GUESTAGENT_ARCH_RISCV64=y\nCONFIG_GUESTAGENT_ARCH_S390X=y\nCONFIG_GUESTAGENT_COMPRESS=y\n\n################################################################################\n.PHONY: binaries\nbinaries: limactl helpers limactl-plugins guestagents \\\n\ttemplates template_experimentals \\\n\tdocumentation create-links-in-doc-dir\n\n################################################################################\n# _output/bin\n.PHONY: limactl lima helpers\nlimactl: _output/bin/limactl$(exe) lima\n\n### Listing Dependencies\n\n# returns a list of files expanded from $(1) excluding directories and files ending with '_test.go'.\nfind_files_excluding_dir_and_test = $(shell find $(1) ! -type d ! -name '*_test.go')\nFILES_IN_PKG = $(call find_files_excluding_dir_and_test, ./pkg)\n\n# returns a list of files which are dependencies for the command $(1).\ndependencies_for_cmd = go.mod $(call find_files_excluding_dir_and_test, ./cmd/$(1)) $(FILES_IN_PKG)\n\n### Force Building Targets\n\n# returns GOVERSION, CGO*, GO*, -ldflags, and -tags build variables from the output of `go version -m $(1)`.\n# When CGO_* variables are not set, they are not included in the output.\n# Because the CGO_* variables are not set means that those values are default values,\n# it can be assumed that those values are same if the GOVERSION is same.\n# $(1): target binary\nextract_build_vars = $(shell \\\n\t($(GO) version -m $(1) 2>&- || echo $(1):) | \\\n\tawk 'FNR==1{print \"GOVERSION=\"$$2}$$2~/^(CGO|GO|-gcflags|-ldflags|-tags).*=.+$$/{sub(\"^.*\"$$2,$$2); print $$0}' \\\n)\n\n# a list of keys from the GO build variables to be used for calling `go env`.\n# keys starting with '-' are excluded because `go env` does not support those keys.\n# $(1): extracted build variables from the binary\nkeys_in_build_vars = $(filter-out -%,$(shell for i in $(1); do echo $${i%%=*}; done))\n\n# a list of GO build variables to build the target binary.\n# $(1): target binary. expecting ENVS_$(2) is set to use the environment variables for the target binary.\n# $(2): key of the GO build variable to be used for calling `go env`.\ngo_build_vars = $(shell \\\n\t$(ENVS_$(1)) $(GO) env $(2) | \\\n\tawk '/ /{print \"\\\"\"$$0\"\\\"\"; next}{print}' | \\\n\tfor k in $(2); do read -r v && echo \"$$k=$${v}\"; done \\\n) $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)\n\n# returns the difference between $(1) and $(2).\ndiff = $(filter-out $(2),$(1))$(filter-out $(1),$(2))\n\n# returns diff between the GO build variables in the binary $(1) and the building variables.\n# $(1): target binary\n# $(2): extracted GO build variables from the binary\ncompare_build_vars = $(call diff,$(call go_build_vars,$(1),$(call keys_in_build_vars,$(2))),$(2))\n\n# returns \"force\" if the GO build variables in the binary $(1) is different from the building variables.\n# $(1): target binary. expecting ENVS_$(1) is set to use the environment variables for the target binary.\nforce_build = $(if $(call compare_build_vars,$(1),$(call extract_build_vars,$(1))),force,)\n\n# returns the file name without .gz extension. It also gunzips the file with .gz extension if exists.\n# $(1): target file\ngunzip_if_exists = $(shell f=$(1); f=$${f%.gz}; test -f \"$${f}.gz\" && (set -x; gunzip -f \"$${f}.gz\") ; echo \"$${f}\")\n\n# call force_build with passing output of gunzip_if_exists as an argument.\n# $(1): target file\nforce_build_with_gunzip = $(call force_build,$(call gunzip_if_exists,$(1)))\n\nforce: # placeholder for force build\n\n################################################################################\n# _output/bin/limactl$(exe)\n\n# dependencies for limactl\nLIMACTL_DEPS = $(call dependencies_for_cmd,limactl)\nifeq ($(GOOS),darwin)\nLIMACTL_DEPS += vz.entitlements\nendif\n\n# environment variables for limactl. this variable is used for checking force build.\n#\n# The hostagent must be compiled with CGO_ENABLED=1 so that net.LookupIP() in the DNS server\n# calls the native resolver library and not the simplistic version in the Go library.\nENVS__output/bin/limactl$(exe) = CGO_ENABLED=1 GOOS=\"$(GOOS)\" GOARCH=\"$(GOARCH)\" CC=\"$(CC)\"\n\nLIMACTL_DRIVER_TAGS :=\nifneq (,$(findstring vz,$(ADDITIONAL_DRIVERS)))\nLIMACTL_DRIVER_TAGS += external_vz\nendif\nifneq (,$(findstring qemu,$(ADDITIONAL_DRIVERS)))\nLIMACTL_DRIVER_TAGS += external_qemu\nendif\nifneq (,$(findstring wsl2,$(ADDITIONAL_DRIVERS)))\nLIMACTL_DRIVER_TAGS += external_wsl2\nendif\n\nGO_BUILDTAGS ?=\nGO_BUILDTAGS_LIMACTL := $(strip $(GO_BUILDTAGS) $(LIMACTL_DRIVER_TAGS))\n\n_output/bin/limactl$(exe): $(LIMACTL_DEPS) $$(call force_build,$$@)\nifneq ($(GOOS),windows) #\n\t@rm -rf _output/bin/limactl.exe\nelse\n\t@rm -rf _output/bin/limactl\nendif\n\t$(ENVS_$@) $(GO_BUILD) -tags '$(GO_BUILDTAGS_LIMACTL)' -o $@ ./cmd/limactl\nifeq ($(GOOS),darwin)\n\tcodesign -f -v --entitlements vz.entitlements -s - $@\nendif\n\nLIBEXEC_LIMA := _output/libexec/lima\n\nlimactl-plugins: $(LIBEXEC_LIMA)/limactl-mcp$(exe) $(LIBEXEC_LIMA)/limactl-url-fedora-rawhide\n\n$(LIBEXEC_LIMA)/limactl-mcp$(exe): $(call dependencies_for_cmd,limactl-mcp) $$(call force_build,$$@)\n\t@mkdir -p $(LIBEXEC_LIMA)\n\t$(ENVS_$@) $(GO_BUILD) -o $@ ./cmd/limactl-mcp\n\n$(LIBEXEC_LIMA)/limactl-url-fedora-rawhide: cmd/limactl-url-fedora-rawhide\n\tcp -aL $< $@\n\n.PHONY: additional-drivers\nadditional-drivers:\n\t@mkdir -p $(LIBEXEC_LIMA)\n\t@for drv in $(ADDITIONAL_DRIVERS); do \\\n\t\techo \"Building $$drv as external\"; \\\n\t\tif [ \"$(GOOS)\" = \"windows\" ]; then \\\n\t\t\t$(GO_BUILD) -o $(LIBEXEC_LIMA)/lima-driver-$$drv.exe ./cmd/lima-driver-$$drv; \\\n\t\telse \\\n\t\t\t$(GO_BUILD) -o $(LIBEXEC_LIMA)/lima-driver-$$drv ./cmd/lima-driver-$$drv; \\\n\t\t\tfi; \\\n\t\tif [ \"$$drv\" = \"vz\" ] && [ \"$(GOOS)\" = \"darwin\" ]; then \\\n\t\t\tcodesign -f -v --entitlements vz.entitlements -s - $(LIBEXEC_LIMA)/lima-driver-vz; \\\n\t\tfi; \\\n\tdone\n\nLIMA_CMDS = $(sort lima lima$(bat)) # $(sort ...) deduplicates the list\nLIMA_DEPS = $(addprefix _output/bin/,$(LIMA_CMDS))\nlima: $(LIMA_DEPS)\n\nHELPER_CMDS = nerdctl.lima apptainer.lima docker.lima podman.lima kubectl.lima\nHELPERS_DEPS = $(addprefix _output/bin/,$(HELPER_CMDS))\nhelpers: $(HELPERS_DEPS)\n\n_output/bin/%: ./cmd/% | _output/bin\n\tcp -a $< $@\n\nMKDIR_TARGETS += _output/bin\n\n################################################################################\n# _output/share/lima/lima-guestagent\nLINUX_GUESTAGENT_PATH_COMMON = _output/share/lima/lima-guestagent.Linux-\nDARWIN_GUESTAGENT_PATH_COMMON = _output/share/lima/lima-guestagent.Darwin-\n\n# How to add architecture specific guestagent:\n# 1. Add the architecture to GUESTAGENT_ARCHS\n# 2. Add ENVS_$(*_GUESTAGENT_PATH_COMMON)<arch> to set GOOS, GOARCH, and other necessary environment variables\nLINUX_GUESTAGENT_ARCHS = aarch64 armv7l ppc64le riscv64 s390x x86_64\nDARWIN_GUESTAGENT_ARCHS = aarch64\n\nifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y)\nALL_GUESTAGENTS_NOT_COMPRESSED += $(addprefix $(LINUX_GUESTAGENT_PATH_COMMON),$(LINUX_GUESTAGENT_ARCHS))\nendif\nifeq ($(CONFIG_GUESTAGENT_OS_DARWIN),y)\nALL_GUESTAGENTS_NOT_COMPRESSED += $(addprefix $(DARWIN_GUESTAGENT_PATH_COMMON),$(DARWIN_GUESTAGENT_ARCHS))\nendif\nifeq ($(CONFIG_GUESTAGENT_COMPRESS),y)\ngz=.gz\nendif\n\nALL_GUESTAGENTS = $(addsuffix $(gz),$(ALL_GUESTAGENTS_NOT_COMPRESSED))\n\n# guestagent path for the given platform. it may has .gz extension if CONFIG_GUESTAGENT_COMPRESS is enabled.\n# $(1): operating system (os)\n# $(2): list of architectures\nguestagent_path = $(foreach arch,$(2),$($(1)_GUESTAGENT_PATH_COMMON)$(arch)$(gz))\n\nifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y)\nNATIVE_GUESTAGENT_ARCH = $(shell echo $(GOARCH) | sed -e s/arm64/aarch64/ -e s/arm/armv7l/ -e s/amd64/x86_64/)\nNATIVE_GUESTAGENT = $(call guestagent_path,LINUX,$(NATIVE_GUESTAGENT_ARCH))\nADDITIONAL_GUESTAGENT_ARCHS = $(filter-out $(NATIVE_GUESTAGENT_ARCH),$(LINUX_GUESTAGENT_ARCHS))\nADDITIONAL_GUESTAGENTS = $(call guestagent_path,LINUX,$(ADDITIONAL_GUESTAGENT_ARCHS))\nendif\nifeq ($(CONFIG_GUESTAGENT_OS_DARWIN),y)\nifeq ($(GOARCH),arm64)\nNATIVE_GUESTAGENT_ARCH = aarch64\nNATIVE_GUESTAGENT += $(call guestagent_path,DARWIN,$(NATIVE_GUESTAGENT_ARCH))\nendif\nendif\n# No ADDITIONAL_GUESTAGENTS for Darwin, as only one architecture is supported.\n\n# config_guestagent_arch returns expanded value of CONFIG_GUESTAGENT_ARCH_<arch>\n# $(1): architecture\n# CONFIG_GUESTAGENT_ARCH_<arch> naming convention: uppercase, remove '_'\nconfig_guestagent_arch = $(filter y,$(CONFIG_GUESTAGENT_ARCH_$(shell echo $(1)|tr -d _|tr a-z A-Z)))\n\n# guestagent_path_enabled_by_config returns the path to the guestagent binary for the given architecture,\n# or an empty string if the CONFIG_GUESTAGENT_ARCH_<arch> is not set.\nguestagent_path_enabled_by_config = $(if $(call config_guestagent_arch,$(2)),$(call guestagent_path,$(1),$(2)))\n\nifeq ($(CONFIG_GUESTAGENT_OS_LINUX),y)\n# apply CONFIG_GUESTAGENT_ARCH_*\nGUESTAGENTS += $(foreach arch,$(LINUX_GUESTAGENT_ARCHS),$(call guestagent_path_enabled_by_config,LINUX,$(arch)))\nendif\nifeq ($(CONFIG_GUESTAGENT_OS_DARWIN),y)\nGUESTAGENTS += $(call guestagent_path,DARWIN,aarch64)\nendif\n\n.PHONY: guestagents native-guestagent additional-guestagents\nguestagents: $(GUESTAGENTS)\nnative-guestagent: $(NATIVE_GUESTAGENT)\nadditional-guestagents: $(ADDITIONAL_GUESTAGENTS)\n%-guestagent:\n\t@[ \"$(findstring $(*),$(LINUX_GUESTAGENT_ARCHS))\" == \"$(*)\" ] && make $(call guestagent_path,LINUX,$*)\n\t@[ \"$(findstring $(*),$(DARWIN_GUESTAGENT_ARCHS))\" == \"$(*)\" ] && make $(call guestagent_path,DARWIN,$*)\n\n# environment variables for linux-guestagent. these variable are used for checking force build.\nENVS_$(LINUX_GUESTAGENT_PATH_COMMON)aarch64 = CGO_ENABLED=0 GOOS=linux GOARCH=arm64\nENVS_$(LINUX_GUESTAGENT_PATH_COMMON)armv7l = CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7\nENVS_$(LINUX_GUESTAGENT_PATH_COMMON)ppc64le = CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le\nENVS_$(LINUX_GUESTAGENT_PATH_COMMON)riscv64 = CGO_ENABLED=0 GOOS=linux GOARCH=riscv64\nENVS_$(LINUX_GUESTAGENT_PATH_COMMON)s390x = CGO_ENABLED=0 GOOS=linux GOARCH=s390x\nENVS_$(LINUX_GUESTAGENT_PATH_COMMON)x86_64 = CGO_ENABLED=0 GOOS=linux GOARCH=amd64\nENVS_$(DARWIN_GUESTAGENT_PATH_COMMON)aarch64 = CGO_ENABLED=0 GOOS=darwin GOARCH=arm64\n$(ALL_GUESTAGENTS_NOT_COMPRESSED): $(call dependencies_for_cmd,lima-guestagent) $$(call force_build_with_gunzip,$$@) | _output/share/lima\n\t$(ENVS_$@) $(GO_BUILD) -o $@ ./cmd/lima-guestagent\n\tchmod 644 $@\n$(LINUX_GUESTAGENT_PATH_COMMON)%.gz: $(LINUX_GUESTAGENT_PATH_COMMON)% $$(call force_build_with_gunzip,$$@)\n\t@set -x; gzip -n $<\n$(DARWIN_GUESTAGENT_PATH_COMMON)%.gz: $(DARWIN_GUESTAGENT_PATH_COMMON)% $$(call force_build_with_gunzip,$$@)\n\t@set -x; gzip -n $<\n\nMKDIR_TARGETS += _output/share/lima\n\n################################################################################\n# _output/share/lima/templates\nTEMPLATES = $(addprefix _output/share/lima/templates/,$(filter-out experimental,$(notdir $(wildcard templates/*))))\nTEMPLATE_DEFAULTS = ${addprefix _output/share/lima/templates/_default/,$(notdir $(wildcard templates/_default/*))}\nTEMPLATE_IMAGES = $(addprefix _output/share/lima/templates/_images/,$(notdir $(wildcard templates/_images/*)))\nTEMPLATE_EXPERIMENTALS = $(addprefix _output/share/lima/templates/experimental/,$(notdir $(wildcard templates/experimental/*)))\n\n.PHONY: default_template templates template_experimentals\ndefault_template: _output/share/lima/templates/default.yaml\ntemplates: $(TEMPLATES) $(TEMPLATE_DEFAULTS) $(TEMPLATE_IMAGES)\ntemplate_experimentals: $(TEMPLATE_EXPERIMENTALS)\n\n$(TEMPLATES): | _output/share/lima/templates\n$(TEMPLATE_DEFAULTS): | _output/share/lima/templates/_default\n$(TEMPLATE_IMAGES): | _output/share/lima/templates/_images\n$(TEMPLATE_EXPERIMENTALS): | _output/share/lima/templates/experimental\nMKDIR_TARGETS += _output/share/lima/templates _output/share/lima/templates/_default _output/share/lima/templates/_images _output/share/lima/templates/experimental\n\n_output/share/lima/templates/%: templates/%\n\tcp -aL $< $@\n\n# returns \"force\" if GOOS==windows, or GOOS!=windows and the file $(1) is not a symlink.\n# $(1): target file\n# On Windows, always copy to ensure the target has the same file as the source.\nforce_link = $(if $(filter windows,$(GOOS)),force,$(shell test ! -L $(1) && echo force))\n\n################################################################################\n# templates/_images\n\n# fedora-N.yaml should not be updated to refer to Fedora N+1 images\nTEMPLATES_TO_BE_UPDATED = $(filter-out $(wildcard templates/_images/fedora*.yaml),$(wildcard templates/_images/*.yaml))\n\n.PHONY: update-templates\nupdate-templates: $(TEMPLATES_TO_BE_UPDATED)\n\t./hack/update-template.sh $^\n\n################################################################################\n# _output/share/doc/lima\nDOCUMENTATION = $(addprefix _output/share/doc/lima/,$(wildcard *.md) LICENSE SECURITY.md VERSION)\n\n.PHONY: documentation\ndocumentation: $(DOCUMENTATION)\n\n_output/share/doc/lima/SECURITY.md: | _output/share/doc/lima\n\techo \"Moved to https://github.com/lima-vm/.github/blob/main/SECURITY.md\" > $@\n\n_output/share/doc/lima/VERSION: | _output/share/doc/lima\n\techo $(VERSION) > $@\n\n_output/share/doc/lima/%: % | _output/share/doc/lima\n\tcp -aL $< $@\n\nMKDIR_TARGETS += _output/share/doc/lima\n\n.PHONY: create-links-in-doc-dir\ncreate-links-in-doc-dir: _output/share/doc/lima/templates\n_output/share/doc/lima/templates: _output/share/lima/templates $$(call force_link,$$@)\n# remove the existing directory or symlink\n\trm -rf $@\nifneq ($(GOOS),windows)\n\tln -sf ../../lima/templates $@\nelse\n# copy from templates built in build process\n\tcp -aL $< $@\nendif\n\n################################################################################\n# returns difference between GOOS GOARCH and GOHOSTOS GOHOSTARCH.\ncross_compiling = $(call diff,$(GOOS) $(GOARCH),$(GOHOSTOS) $(GOHOSTARCH))\n# returns true if cross_compiling is empty.\nnative_compiling = $(if $(cross_compiling),,true)\n\n################################################################################\n# _output/share/man/man1\n.PHONY: manpages\n# Set limactl.1 as explicit dependency.\n# It's uncertain how many manpages will be generated by `make`,\n# because `limactl` generates them without corresponding source code for the manpages.\nmanpages: _output/share/man/man1/limactl.1\n_output/share/man/man1/limactl.1: _output/bin/limactl$(exe)\n\t@mkdir -p _output/share/man/man1\nifeq ($(native_compiling),true)\n# The manpages are generated by limactl, so the limactl binary must be native.\n\t$< generate-doc _output/share/man/man1 \\\n\t\t--output _output --prefix $(PREFIX)\nendif\n\n################################################################################\n.PHONY: docsy\n# Set limactl.md as explicit dependency.\n# It's uncertain how many docsy pages will be generated by `make`,\n# because `limactl` generates them without corresponding source code for the docsy pages.\ndocsy: website/_output/docsy/limactl.md website/_output/docsy-mcp/mcp.md\nwebsite/_output/docsy/limactl.md: _output/bin/limactl$(exe)\n\t@mkdir -p website/_output/docsy\nifeq ($(native_compiling),true)\n# The docs are generated by limactl, so the limactl binary must be native.\n\t$< generate-doc --type docsy website/_output/docsy \\\n\t\t--output _output --prefix $(PREFIX)\nendif\nwebsite/_output/docsy-mcp/mcp.md: _output/libexec/lima/limactl-mcp$(exe)\n\t@mkdir -p website/_output/docsy-mcp\nifeq ($(native_compiling),true)\n\t$< generate-doc website/_output/docsy-mcp\nendif\n\n################################################################################\ndefault-template.yaml: _output/bin/limactl$(exe)\nifeq ($(native_compiling),true)\n\t$< tmpl copy --embed-all templates/default.yaml $@\nendif\n\nschema-limayaml.json: _output/bin/limactl$(exe) templates/default.yaml default-template.yaml\nifeq ($(native_compiling),true)\n\t# validate both the original template (with the \"base\" etc), and the embedded template\n\t$< generate-jsonschema --schemafile $@ templates/default.yaml default-template.yaml\nendif\n\n.PHONY: check-jsonschema\ncheck-jsonschema: schema-limayaml.json templates/default.yaml default-template.yaml\n\tcheck-jsonschema --schemafile schema-limayaml.json templates/default.yaml default-template.yaml\n\n################################################################################\n.PHONY: diagrams\ndiagrams: docs/lima-sequence-diagram.png\ndocs/lima-sequence-diagram.png: docs/images/lima-sequence-diagram.puml\n\t$(PLANTUML) ./docs/images/lima-sequence-diagram.puml\n\n################################################################################\n.PHONY: install\ninstall: uninstall\n\tmkdir -p \"$(DEST)\"\n\t# Use tar rather than cp, for better symlink handling\n\t( cd _output && $(TAR) c * | $(TAR) -xv --no-same-owner -C \"$(DEST)\" )\n\tif [ \"$(shell uname -s )\" != \"Linux\" -a ! -e \"$(DEST)/bin/nerdctl\" ]; then ln -sf nerdctl.lima \"$(DEST)/bin/nerdctl\"; fi\n\tif [ \"$(shell uname -s )\" != \"Linux\" -a ! -e \"$(DEST)/bin/apptainer\" ]; then ln -sf apptainer.lima \"$(DEST)/bin/apptainer\"; fi\n\n.PHONY: uninstall\nuninstall:\n\t@test -f \"$(DEST)/bin/lima\" || echo \"lima not found in $(DEST) prefix\"\n\trm -rf \\\n\t\t\"$(DEST)/bin/lima\" \\\n\t\t\"$(DEST)/bin/lima$(bat)\" \\\n\t\t\"$(DEST)/bin/limactl$(exe)\" \\\n\t\t\"$(DEST)/bin/nerdctl.lima\" \\\n\t\t\"$(DEST)/bin/apptainer.lima\" \\\n\t\t\"$(DEST)/bin/docker.lima\" \\\n\t\t\"$(DEST)/bin/podman.lima\" \\\n\t\t\"$(DEST)/bin/kubectl.lima\" \\\n\t\t\"$(DEST)/share/man/man1/lima.1\" \\\n\t\t\"$(DEST)/share/man/man1/limactl\"*\".1\" \\\n\t\t\"$(DEST)/share/lima\" \\\n\t\t\"$(DEST)/share/doc/lima\" \\\n\t\t\"$(DEST)/libexec/lima/limactl-mcp$(exe)\" \\\n\t\t\"$(DEST)/libexec/lima/limactl-url-fedora-rawhide\" \\\n\t\t\"$(DEST)/libexec/lima/lima-driver-qemu$(exe)\" \\\n\t\t\"$(DEST)/libexec/lima/lima-driver-vz$(exe)\" \\\n\t\t\"$(DEST)/libexec/lima/lima-driver-wsl2$(exe)\" \\\n\t\t\"$(DEST)/libexec/lima/lima-driver-krunkit$(exe)\"\n\tif [ \"$$(readlink \"$(DEST)/bin/nerdctl\")\" = \"nerdctl.lima\" ]; then rm \"$(DEST)/bin/nerdctl\"; fi\n\tif [ \"$$(readlink \"$(DEST)/bin/apptainer\")\" = \"apptainer.lima\" ]; then rm \"$(DEST)/bin/apptainer\"; fi\n\n.PHONY: check-generated\ncheck-generated:\n\tgit diff --exit-code || \\\n\t\t(echo \"Please run 'make generate' when making changes to proto files and check-in the generated file changes\" && false)\n\n.PHONY: bats\nbats: native limactl-plugins\n\tPATH=$$PWD/_output/bin:$$PATH ./hack/bats/lib/bats-core/bin/bats --timing ./hack/bats/tests\n\n.PHONY: lint\nlint: check-generated\n\tnobin --allow-emoji --allow-escape --gitignore --skip-ext pb.desc --skip 'website/static/images/**/*.{gif,png}' --skip 'docs/reports/**/*.pdf'\n\teditorconfig-checker\n\tgolangci-lint run ./...\n\tyamllint .\n\tls-lint\n\tfind . -name '*.sh' ! -path \"./.git/*\" | xargs shellcheck\n\tfind . -name '*.sh' ! -path \"./.git/*\" | xargs shfmt -s -d\n\t# the allow list corresponds to https://github.com/cncf/foundation/blob/e5db022a0009f4db52b89d9875640cf3137153fe/allowed-third-party-license-policy.md\n\t# hashicorp/hcl/v2 is MPL-2.0; covered by the CNCF license exception for hashicorp/hcl\n\t# see also https://github.com/cncf/foundation/issues/1242\n\tgo-licenses check --include_tests --ignore github.com/hashicorp/hcl/v2 ./... --allowed_licenses=$$(cat ./hack/allowed-licenses.txt)\n\tltag -t ./hack/ltag --check -v\n\tprotolint .\n\n.PHONY: clean\nclean:\n\trm -rf _output vendor\n\n.PHONY: install-protoc-tools\ninstall-protoc-tools:\n\tgo install -modfile=./hack/tools/go.mod google.golang.org/protobuf/cmd/protoc-gen-go\n\tgo install -modfile=./hack/tools/go.mod google.golang.org/grpc/cmd/protoc-gen-go-grpc\n\n.PHONY: generate\ngenerate:\n\tgo generate ./...\n\n################################################################################\n# _artifacts/lima-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)\n# _artifacts/lima-additional-guestagents-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)\n.PHONY: artifact\n\n# returns the capitalized string of $(1).\ncapitalize = $(shell echo \"$(1)\"|awk '{print toupper(substr($$0,1,1)) tolower(substr($$0,2))}')\n\n# returns the architecture name converted from GOARCH to GNU coreutils uname -m.\nto_uname_m = $(foreach arch,$(1),$(shell echo $(arch) | sed 's/amd64/x86_64/' | sed 's/arm64/aarch64/'))\n\nARTIFACT_FILE_EXTENSIONS := .tar.gz\n\nifeq ($(GOOS),darwin)\n# returns the architecture name converted from GOARCH to macOS's uname -m.\nto_uname_m = $(foreach arch,$(1),$(shell echo $(arch) | sed 's/amd64/x86_64/'))\nelse ifeq ($(GOOS),linux)\n# CC is required for cross-compiling on Linux.\n# On Debian, Ubuntu, and related distributions, compilers are named like x86_64-linux-gnu-gcc\n# On Fedora, RHEL, and related distributions, the equivalent is x86_64-redhat-linux-gcc\n# On openSUSE and as a generic fallback, gcc is used\nCC := $(shell \\\n\tif command -v $(call to_uname_m,$(GOARCH))-redhat-linux-gcc >/dev/null 2>&1; then \\\n\t\techo $(call to_uname_m,$(GOARCH))-redhat-linux-gcc; \\\n\telif command -v $(call to_uname_m,$(GOARCH))-linux-gnu-gcc >/dev/null 2>&1; then \\\n\t\techo $(call to_uname_m,$(GOARCH))-linux-gnu-gcc; \\\n\telse \\\n\t\techo gcc; \\\n\tfi)\nelse ifeq ($(GOOS),windows)\n# artifact in zip format also provided for Windows.\nARTIFACT_FILE_EXTENSIONS += .zip\nendif\n\n# artifacts: artifacts-$(GOOS)\nARTIFACT_OS = $(call capitalize,$(GOOS))\nARTIFACT_UNAME_M = $(call to_uname_m,$(GOARCH))\nARTIFACT_PATH_COMMON = _artifacts/lima-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)\nARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON = _artifacts/lima-additional-guestagents-$(VERSION_TRIMMED)-$(ARTIFACT_OS)-$(ARTIFACT_UNAME_M)\n\nartifact: $(addprefix $(ARTIFACT_PATH_COMMON),$(ARTIFACT_FILE_EXTENSIONS)) \\\n\t$(addprefix $(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON),$(ARTIFACT_FILE_EXTENSIONS))\n\nARTIFACT_DES =  _output/bin/limactl$(exe) limactl-plugins $(LIMA_DEPS) $(HELPERS_DEPS) \\\n\t$(NATIVE_GUESTAGENT) \\\n\t$(TEMPLATES) $(TEMPLATE_IMAGES) $(TEMPLATE_DEFAULTS) $(TEMPLATE_EXPERIMENTALS) \\\n\tadditional-drivers \\\n\t$(DOCUMENTATION) _output/share/doc/lima/templates \\\n\t_output/share/man/man1/limactl.1\n\n# file targets\n$(ARTIFACT_PATH_COMMON).tar.gz: $(ARTIFACT_DES) | _artifacts\n\t$(TAR) -C _output/ --no-xattrs -czvf $@ ./\n\n$(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON).tar.gz:\n\t# FIXME: do not exec make from make\n\tmake clean additional-guestagents\n\t$(TAR) -C _output/ --no-xattrs -czvf $@ ./\n\n$(ARTIFACT_PATH_COMMON).zip: $(ARTIFACT_DES) | _artifacts\n\tcd _output && $(ZIP) -r ../$@ *\n\n$(ARTIFACT_ADDITIONAL_GUESTAGENTS_PATH_COMMON).zip:\n\tmake clean additional-guestagents\n\tcd _output && $(ZIP) -r ../$@ *\n\n# generate manpages using native limactl.\nmanpages-using-native-limactl: GOOS = $(GOHOSTOS)\nmanpages-using-native-limactl: GOARCH = $(GOHOSTARCH)\nmanpages-using-native-limactl: manpages\n\n# returns \"manpages-using-native-limactl\" if $(1) is not equal to $(GOHOSTOS).\n# $(1): GOOS\ngenerate_manpages_if_needed = $(if $(filter $(if $(1),$(1),$(GOOS)),$(GOHOSTOS)),,manpages-using-native-limactl)\n\n# build native arch first, then build other archs.\nartifact_goarchs = arm64 amd64\ngoarchs_native_and_others = $(GOHOSTARCH) $(filter-out $(GOHOSTARCH),$(artifact_goarchs))\n\n# artifacts is artifact bundles for each OS.\n# if target GOOS is native, build native arch first, generate manpages, then build other archs.\n# if target GOOS is not native, build native limactl, generate manpages, then build the target GOOS with archs.\n.PHONY: artifacts artifacts-darwin artifacts-linux artifacts-windows\nartifacts: $$(addprefix artifact-$$(GOOS)-,$$(goarchs_native_and_others))\nartifacts-darwin: $$(call generate_manpages_if_needed,darwin) $$(addprefix artifact-darwin-,$$(goarchs_native_and_others))\nartifacts-linux: $$(call generate_manpages_if_needed,linux) $$(addprefix artifact-linux-,$$(goarchs_native_and_others))\nartifacts-windows: $$(call generate_manpages_if_needed,windows) $$(addprefix artifact-windows-,$$(goarchs_native_and_others))\n\n# set variables for artifact variant targets.\nartifact-darwin% artifact-darwin: GOOS = darwin\nartifact-linux% artifact-linux: GOOS = linux\nartifact-windows% artifact-windows: GOOS = windows\nartifact-%-amd64 artifact-%-x86_64 artifact-amd64 artifact-x86_64: GOARCH = amd64\nartifact-%-arm64 artifact-%-aarch64 artifact-arm64 artifact-aarch64: GOARCH = arm64\n\n# build cross arch binaries.\nartifact-%: $$(call generate_manpages_if_needed)\n\tmake clean artifact GOOS=$(GOOS) GOARCH=$(GOARCH)\n\n.PHONY: artifacts-misc\nartifacts-misc: | _artifacts\n\tgo mod vendor\n\t$(TAR) --no-xattrs -czf _artifacts/lima-$(VERSION_TRIMMED)-go-mod-vendor.tar.gz go.mod go.sum vendor\n\nMKDIR_TARGETS += _artifacts\n\n################################################################################\n# This target must be placed after any changes to the `MKDIR_TARGETS` variable.\n# It seems that variable expansion in Makefile targets is not done recursively.\n$(MKDIR_TARGETS):\n\tmkdir -p $@\n"
  },
  {
    "path": "NOTICE",
    "content": "Lima\nCopyright The Lima Authors.\n\nThis project contains portions of other projects that are licensed under the terms of Apache License 2.0.\nThe NOTICE files of those projects are replicated here for compliance of the section 4(d) of the license.\n\n=== https://github.com/containerd/containerd ===\nhttps://github.com/containerd/containerd/blob/v2.1.1/LICENSE\nhttps://github.com/containerd/containerd/blob/v2.1.1/NOTICE\n\n> Docker\n> Copyright 2012-2015 Docker, Inc.\n>\n> This product includes software developed at Docker, Inc. (https://www.docker.com).\n>\n> The following is courtesy of our legal counsel:\n>\n>\n> Use and transfer of Docker may be subject to certain restrictions by the\n> United States and other governments.\n> It is your responsibility to ensure that your use and/or transfer does not\n> violate applicable laws.\n>\n> For more information, please see https://www.bis.doc.gov\n>\n> See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.\n"
  },
  {
    "path": "README.md",
    "content": "[[🌎**Web site**]](https://lima-vm.io/)\n[[📖**Documentation**]](https://lima-vm.io/docs/)\n[[👤**Slack (`#lima`)**]](https://slack.cncf.io)\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"website/static/images/logo-dark.svg\">\n  <img alt=\"Shows a stylized 'Lima' text in bold, modern font\" src=\"website/static/images/logo.svg\" width=400 />\n</picture>\n\n# Lima: Linux Machines\n\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/lima-vm/lima)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/6505/badge)](https://www.bestpractices.dev/projects/6505)\n[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/lima-vm/lima/badge)](https://scorecard.dev/viewer/?uri=github.com/lima-vm/lima)\n\n[Lima](https://lima-vm.io/) launches Linux virtual machines with automatic file sharing and port forwarding (similar to WSL2).\n\nThe original goal of Lima was to promote [containerd](https://containerd.io) including [nerdctl (contaiNERD ctl)](https://github.com/containerd/nerdctl)\nto Mac users, but Lima can be used for non-container applications as well.\n\nLima also supports other container engines (Docker, Podman, Kubernetes, etc.) and non-macOS hosts (Linux, NetBSD, etc.).\n\n## Getting started\nSet up (Homebrew):\n```bash\nbrew install lima\nlimactl start\n```\n\nTo run Linux commands:\n```bash\nlima uname -a\n```\n\nTo run containers with containerd:\n```bash\nlima nerdctl run --rm hello-world\n```\n\nTo run containers with Docker:\n```bash\nlimactl start template:docker\nexport DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')\ndocker run --rm hello-world\n```\n\nTo run containers with Kubernetes:\n```bash\nlimactl start template:k8s\nexport KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\nkubectl apply -f ...\n```\n\nSee <https://lima-vm.io/docs/> for the further information.\n\n## Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](https://lima-vm.io/docs/community/contributing/) for details on:\n\n- **Developer Certificate of Origin (DCO)**: All commits must be signed off with `git commit -s`\n- Code licensing and pull request guidelines\n- Testing requirements\n\n## Community\n### Adopters\n\nContainer environments:\n- [Rancher Desktop](https://rancherdesktop.io/): Kubernetes and container management to the desktop\n- [Colima](https://github.com/abiosoft/colima): Docker (and Kubernetes) on macOS with minimal setup\n- [Finch](https://github.com/runfinch/finch): Finch is a command line client for local container development\n- [Podman Desktop](https://podman-desktop.io/): Podman Desktop GUI has a plug-in for Lima virtual machines\n\nGUI:\n- [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.\n- [lima-gui](https://github.com/afbjorklund/lima-gui): Qt GUI for Lima\n\n### Communication channels\n<!-- Duplicated from https://lima-vm.io/docs/community/ -->\n- [GitHub Discussions](https://github.com/lima-vm/lima/discussions)\n- `#lima` channel in the CNCF Slack\n  - New account: <https://slack.cncf.io/>\n  - Login: <https://cloud-native.slack.com/>\n- Zoom meetings (tentatively monthly)\n  - Meeting notes & agenda proposals: https://github.com/lima-vm/lima/discussions/categories/meetings\n  - Calendar: https://zoom-lfx.platform.linuxfoundation.org/meetings/lima\n\n### Social media accounts\n\nFollow us for project updates, release announcements, and community news:\n\n- https://x.com/@TheLimaProject\n- https://mastodon.social/@TheLimaProject\n\n### Code of Conduct\nLima follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).\n\n- - -\n**We are a [Cloud Native Computing Foundation](https://cncf.io/) incubating project.**\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://www.cncf.io/wp-content/uploads/2022/07/cncf-white-logo.svg\">\n  <img src=\"https://www.cncf.io/wp-content/uploads/2022/07/cncf-color-bg.svg\" width=300 />\n</picture>\n\nThe 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).\n"
  },
  {
    "path": "ROADMAP.md",
    "content": "Moved to <https://lima-vm.io/docs/community/roadmap/>.\n"
  },
  {
    "path": "cmd/apptainer.lima",
    "content": "#!/bin/sh\nset -eu\n: \"${LIMA_INSTANCE:=apptainer}\"\n: \"${APPTAINER_BINDPATH:=}\"\n\nif [ \"$(limactl ls -q \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"$LIMA_INSTANCE\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" does not exist, run \\`limactl create --name=$LIMA_INSTANCE template:apptainer\\` to create a new instance\" >&2\n  exit 1\nelif [ \"$(limactl ls -f '{{ .Status }}' \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"Running\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" is not running, run \\`limactl start $LIMA_INSTANCE\\` to start the existing instance\" >&2\n  exit 1\nfi\nexport LIMA_INSTANCE\nif [ -n \"$APPTAINER_BINDPATH\" ]; then\n  APPTAINER_BINDPATH=\"$APPTAINER_BINDPATH,\"\nfi\nAPPTAINER_BINDPATH=\"$APPTAINER_BINDPATH$HOME\"\nexec lima APPTAINER_BINDPATH=\"$APPTAINER_BINDPATH\" apptainer \"$@\"\n"
  },
  {
    "path": "cmd/docker.lima",
    "content": "#!/bin/sh\nset -eu\n\n# Environment Variables\n# LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is \"docker\".\n\n: \"${LIMA_INSTANCE:=docker}\"\n: \"${DOCKER:=docker}\"\n\nif [ \"$(limactl ls -q \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"$LIMA_INSTANCE\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" does not exist, run \\`limactl create --name=$LIMA_INSTANCE template:docker\\` to create a new instance\" >&2\n  exit 1\nelif [ \"$(limactl ls -f '{{ .Status }}' \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"Running\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" is not running, run \\`limactl start $LIMA_INSTANCE\\` to start the existing instance\" >&2\n  exit 1\nfi\nDOCKER=$(command -v \"$DOCKER\" || true)\nif [ -n \"$DOCKER\" ]; then\n  DOCKER_HOST=$(limactl list \"$LIMA_INSTANCE\" --format 'unix://{{.Dir}}/sock/docker.sock')\n  export DOCKER_HOST\n  exec \"$DOCKER\" \"$@\"\nelse\n  export LIMA_INSTANCE\n  exec lima docker \"$@\"\nfi\n"
  },
  {
    "path": "cmd/kubectl.lima",
    "content": "#!/bin/sh\nset -eu\n\n# Environment Variables\n# LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is empty.\n\n: \"${LIMA_INSTANCE:=}\"\n: \"${KUBECTL:=kubectl}\"\n\nif [ -z \"$LIMA_INSTANCE\" ]; then\n  if [ \"$(limactl ls -q k3s 2>/dev/null)\" = \"k3s\" ]; then\n    LIMA_INSTANCE=k3s\n  elif [ \"$(limactl ls -q k8s 2>/dev/null)\" = \"k8s\" ]; then\n    LIMA_INSTANCE=k8s\n  else\n    echo \"No k3s or k8s existing instances found. Either create one with\" >&2\n    echo \"limactl create --name=k3s template:k3s\" >&2\n    echo \"limactl create --name=k8s template:k8s\" >&2\n    echo \"or set LIMA_INSTANCE to the name of your Kubernetes instance\" >&2\n    exit 1\n  fi\n  if [ \"$(limactl ls -f '{{.Status}}' \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"Running\" ]; then\n    echo \"instance \\\"$LIMA_INSTANCE\\\" is not running, run \\`limactl start $LIMA_INSTANCE\\` to start the existing instance\" >&2\n    exit 1\n  fi\nelif [ \"$(limactl ls -q \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"$LIMA_INSTANCE\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" does not exist, run \\`limactl create --name=$LIMA_INSTANCE\\` to create a new instance\" >&2\n  exit 1\nelif [ \"$(limactl ls -f '{{ .Status }}' \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"Running\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" is not running, run \\`limactl start $LIMA_INSTANCE\\` to start the existing instance\" >&2\n  exit 1\nfi\nKUBECTL=$(command -v \"$KUBECTL\" || true)\nif [ -n \"$KUBECTL\" ]; then\n  KUBECONFIG=$(limactl list \"$LIMA_INSTANCE\" --format '{{.Dir}}/copied-from-guest/kubeconfig.yaml')\n  export KUBECONFIG\n  exec \"$KUBECTL\" \"$@\"\nelse\n  export LIMA_INSTANCE\n  exec lima sudo kubectl \"$@\"\nfi\n"
  },
  {
    "path": "cmd/lima",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu\n\n# Environment Variables\n# LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is \"default\".\n# LIMA_SHELL: Specifies the shell interpreter to use inside the Lima instance. Default is the user's shell configured inside the instance.\n# LIMA_WORKDIR: Specifies the initial working directory inside the Lima instance. Default is the current directory from the host.\n# LIMACTL: Specifies the path to the limactl binary. Default is \"limactl\" in $PATH.\n\n: \"${LIMA_INSTANCE:=default}\"\n: \"${LIMA_SHELL:=}\"\n: \"${LIMA_WORKDIR:=}\"\n: \"${LIMACTL:=limactl}\"\n\nif [ \"$#\" -eq 1 ]; then\n  if [ \"$1\" = \"-h\" ] || [ \"$1\" = \"--help\" ]; then\n    base=\"$(basename \"$0\")\"\n    echo \"Usage: ${base} [COMMAND...]\"\n    echo\n    echo \"${base} is an alias for \\\"${LIMACTL} shell ${LIMA_INSTANCE}\\\".\"\n    echo \"The instance name (\\\"${LIMA_INSTANCE}\\\") can be changed by specifying \\$LIMA_INSTANCE.\"\n    echo\n    echo \"The shell and initial workdir inside the instance can be specified via \\$LIMA_SHELL\"\n    echo \"and \\$LIMA_WORKDIR.\"\n    echo\n    echo \"See \\`${LIMACTL} shell --help\\` for further information.\"\n    exit 0\n  elif [ \"$1\" = \"-v\" ] || [ \"$1\" = \"--version\" ]; then\n    exec \"$LIMACTL\" \"$@\"\n  fi\nfi\n\nset - --instance \"$LIMA_INSTANCE\" \"$@\"\nif [ -n \"${LIMA_SHELL}\" ]; then\n  set - --shell \"$LIMA_SHELL\" \"$@\"\nfi\nif [ -n \"${LIMA_WORKDIR}\" ]; then\n  set - --workdir \"$LIMA_WORKDIR\" \"$@\"\nfi\n# Avoid converting paths with MSYS2\nMSYS2_ARG_CONV_EXCL=\"*\"\nexport MSYS2_ARG_CONV_EXCL\nexec \"$LIMACTL\" shell \"$@\"\n"
  },
  {
    "path": "cmd/lima-driver-krunkit/main_darwin_arm64.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/krunkit\"\n)\n\n// To be used as an external driver for Lima.\nfunc main() {\n\tserver.Serve(context.Background(), krunkit.New())\n}\n"
  },
  {
    "path": "cmd/lima-driver-qemu/main.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/qemu\"\n)\n\n// To be used as an external driver for Lima.\nfunc main() {\n\tserver.Serve(context.Background(), qemu.New())\n}\n"
  },
  {
    "path": "cmd/lima-driver-vz/main_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/vz\"\n)\n\n// To be used as an external driver for Lima.\nfunc main() {\n\tserver.Serve(context.Background(), vz.New())\n}\n"
  },
  {
    "path": "cmd/lima-driver-wsl2/main_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/wsl2\"\n)\n\n// To be used as an external driver for Lima.\nfunc main() {\n\tserver.Serve(context.Background(), wsl2.New())\n}\n"
  },
  {
    "path": "cmd/lima-guestagent/daemon_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/mdlayher/vsock\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/serialport\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/ticker\"\n\t\"github.com/lima-vm/lima/v2/pkg/portfwdserver\"\n)\n\nconst hostCID = 2\n\ntype cidFilteredListener struct {\n\t*vsock.Listener\n}\n\nfunc (l *cidFilteredListener) Accept() (net.Conn, error) {\n\tfor {\n\t\tconn, err := l.Listener.Accept()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif vsockConn, ok := conn.(*vsock.Conn); ok {\n\t\t\tif addr, ok := vsockConn.RemoteAddr().(*vsock.Addr); ok {\n\t\t\t\tif addr.ContextID != hostCID {\n\t\t\t\t\tlogrus.Warnf(\"rejected vsock connection from unauthorized CID %d\", addr.ContextID)\n\t\t\t\t\tconn.Close()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn conn, nil\n\t}\n}\n\nfunc newDaemonCommand() *cobra.Command {\n\tdaemonCommand := &cobra.Command{\n\t\tUse:   \"daemon\",\n\t\tShort: \"Run the daemon\",\n\t\tRunE:  daemonAction,\n\t}\n\tdaemonCommand.Flags().String(\"runtime-dir\", \"/run/lima-guestagent\", \"Directory to store runtime state\")\n\tdaemonCommand.Flags().Duration(\"tick\", 3*time.Second, \"Tick for polling events\")\n\tdaemonCommand.Flags().Int(\"vsock-port\", 0, \"Use vsock server instead a UNIX socket\")\n\tdaemonCommand.Flags().String(\"virtio-port\", \"\", \"Use virtio server instead a UNIX socket\")\n\treturn daemonCommand\n}\n\nfunc daemonAction(cmd *cobra.Command, _ []string) error {\n\tctx := cmd.Context()\n\truntimeDir, err := cmd.Flags().GetString(\"runtime-dir\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := os.MkdirAll(runtimeDir, 0o755); err != nil {\n\t\treturn err\n\t}\n\tsocket := \"/run/lima-guestagent.sock\"\n\ttick, err := cmd.Flags().GetDuration(\"tick\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tvSockPort, err := cmd.Flags().GetInt(\"vsock-port\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tvirtioPort, err := cmd.Flags().GetString(\"virtio-port\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif tick == 0 {\n\t\treturn errors.New(\"tick must be specified\")\n\t}\n\tif os.Geteuid() != 0 {\n\t\treturn errors.New(\"must run as the root user\")\n\t}\n\n\tlogrus.Infof(\"event tick: %v\", tick)\n\tsimpleTicker := ticker.NewSimpleTicker(time.NewTicker(tick))\n\ttickerInst := simpleTicker\n\t// See /sys/kernel/debug/tracing/available_events for the list of available tracepoints\n\ttracepoints := []string{\"syscalls:sys_exit_bind\"}\n\tif ebpfTicker, err := ticker.NewEbpfTicker(tracepoints); err != nil {\n\t\tlogrus.WithError(err).Warn(\"failed to create eBPF ticker, falling back to simple ticker\")\n\t} else {\n\t\tlogrus.Infof(\"using eBPF ticker with tracepoints: %v\", tracepoints)\n\t\ttickerInst = ticker.NewCompoundTicker(simpleTicker, ebpfTicker)\n\t}\n\n\tctx, stop := signal.NotifyContext(ctx, syscall.SIGTERM)\n\tdefer stop()\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tlogrus.Debug(\"Received SIGTERM, shutting down the guest agent\")\n\t}()\n\tagent, err := guestagent.New(ctx, tickerInst, runtimeDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = os.RemoveAll(socket)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar l net.Listener\n\tif virtioPort != \"\" {\n\t\tqemuL, err := serialport.Listen(\"/dev/virtio-ports/\" + virtioPort)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tl = qemuL\n\t\tlogrus.Infof(\"serving the guest agent on qemu serial file: %s\", virtioPort)\n\t} else if vSockPort != 0 {\n\t\tvsockL, err := vsock.Listen(uint32(vSockPort), nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tl = &cidFilteredListener{Listener: vsockL}\n\t\tlogrus.Infof(\"serving the guest agent on vsock port: %d (host CID only)\", vSockPort)\n\t} else {\n\t\tvar lc net.ListenConfig\n\t\tsocketL, err := lc.Listen(ctx, \"unix\", socket)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := os.Chmod(socket, 0o777); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tl = socketL\n\t\tlogrus.Infof(\"serving the guest agent on %q\", socket)\n\t}\n\tdefer logrus.Debug(\"exiting lima-guestagent daemon\")\n\treturn server.StartServer(ctx, l, &server.GuestServer{Agent: agent, TunnelS: portfwdserver.NewTunnelServer()})\n}\n"
  },
  {
    "path": "cmd/lima-guestagent/fake_cloud_init_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/fakecloudinit\"\n)\n\nfunc newFakeCloudInitCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"fake-cloud-init\",\n\t\tShort: \"Run fake cloud-init\",\n\t\tRunE:  fakeCloudInitAction,\n\t}\n\treturn cmd\n}\n\nfunc fakeCloudInitAction(cmd *cobra.Command, _ []string) error {\n\tctx := cmd.Context()\n\treturn fakecloudinit.Run(ctx)\n}\n"
  },
  {
    "path": "cmd/lima-guestagent/install_systemd_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"bytes\"\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/textutil\"\n)\n\nfunc newInstallSystemdCommand() *cobra.Command {\n\tinstallSystemdCommand := &cobra.Command{\n\t\tUse:   \"install-systemd\",\n\t\tShort: \"Install a systemd unit (user)\",\n\t\tRunE:  installSystemdAction,\n\t}\n\tinstallSystemdCommand.Flags().Bool(\"guestagent-updated\", false, \"Indicate that the guest agent has been updated\")\n\tinstallSystemdCommand.Flags().Int(\"vsock-port\", 0, \"Use vsock server on specified port\")\n\tinstallSystemdCommand.Flags().String(\"virtio-port\", \"\", \"Use virtio server instead a UNIX socket\")\n\treturn installSystemdCommand\n}\n\nfunc installSystemdAction(cmd *cobra.Command, _ []string) error {\n\tctx := cmd.Context()\n\tguestAgentUpdated, err := cmd.Flags().GetBool(\"guestagent-updated\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tvsockPort, err := cmd.Flags().GetInt(\"vsock-port\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tvirtioPort, err := cmd.Flags().GetString(\"virtio-port\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdebug, err := cmd.Flags().GetBool(\"debug\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tunit, err := generateSystemdUnit(vsockPort, virtioPort, debug)\n\tif err != nil {\n\t\treturn err\n\t}\n\tunitPath := \"/etc/systemd/system/lima-guestagent.service\"\n\tunitFileChanged := true\n\tif _, err := os.Stat(unitPath); !errors.Is(err, os.ErrNotExist) {\n\t\tif existingUnit, err := os.ReadFile(unitPath); err == nil && bytes.Equal(unit, existingUnit) {\n\t\t\tlogrus.Infof(\"File %q is up-to-date\", unitPath)\n\t\t\tunitFileChanged = false\n\t\t} else {\n\t\t\tlogrus.Infof(\"File %q needs update\", unitPath)\n\t\t}\n\t} else {\n\t\tunitDir := filepath.Dir(unitPath)\n\t\tif err := os.MkdirAll(unitDir, 0o755); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif unitFileChanged {\n\t\tif err := os.WriteFile(unitPath, unit, 0o644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlogrus.Infof(\"Written file %q\", unitPath)\n\t} else if !guestAgentUpdated {\n\t\tlogrus.Info(\"lima-guestagent.service already up-to-date\")\n\t\treturn nil\n\t}\n\t// unitFileChanged || guestAgentUpdated\n\targs := make([][]string, 0, 4)\n\tif unitFileChanged {\n\t\targs = append(args, []string{\"daemon-reload\"})\n\t}\n\targs = slices.Concat(\n\t\targs,\n\t\t[][]string{\n\t\t\t{\"enable\", \"lima-guestagent.service\"},\n\t\t\t{\"try-restart\", \"lima-guestagent.service\"}, // try-restart: restart if running, otherwise do nothing\n\t\t\t{\"start\", \"lima-guestagent.service\"},       // start: start if not running, otherwise do nothing\n\t\t},\n\t)\n\tfor _, args := range args {\n\t\tcmd := exec.CommandContext(ctx, \"systemctl\", append([]string{\"--system\"}, args...)...)\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tlogrus.Infof(\"Executing: %s\", strings.Join(cmd.Args, \" \"))\n\t\tif err := cmd.Run(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tlogrus.Info(\"Done\")\n\treturn nil\n}\n\n//go:embed lima-guestagent.TEMPLATE.service\nvar systemdUnitTemplate string\n\nfunc generateSystemdUnit(vsockPort int, virtioPort string, debug bool) ([]byte, error) {\n\tselfExeAbs, err := os.Executable()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar args []string\n\tif vsockPort != 0 {\n\t\targs = append(args, fmt.Sprintf(\"--vsock-port %d\", vsockPort))\n\t}\n\tif virtioPort != \"\" {\n\t\targs = append(args, fmt.Sprintf(\"--virtio-port %s\", virtioPort))\n\t}\n\tif debug {\n\t\targs = append(args, \"--debug\")\n\t}\n\n\tm := map[string]string{\n\t\t\"Binary\": selfExeAbs,\n\t\t\"Args\":   strings.Join(args, \" \"),\n\t}\n\treturn textutil.ExecuteTemplate(systemdUnitTemplate, m)\n}\n"
  },
  {
    "path": "cmd/lima-guestagent/lima-guestagent.TEMPLATE.service",
    "content": "[Unit]\nDescription=lima-guestagent\n\n[Service]\nExecStart={{.Binary}} daemon {{.Args}} --runtime-dir=\"%t/%N\"\nType=simple\nRestart=on-failure\nOOMPolicy=continue\nOOMScoreAdjust=-500\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "cmd/lima-guestagent/main.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/cmd/yq\"\n\t\"github.com/lima-vm/lima/v2/pkg/debugutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n)\n\nfunc main() {\n\tyq.MaybeRunYQ()\n\tif err := newApp().Execute(); err != nil {\n\t\tosutil.HandleExitError(err)\n\t\tlogrus.Fatal(err)\n\t}\n}\n\nfunc newApp() *cobra.Command {\n\trootCmd := &cobra.Command{\n\t\tUse:     \"lima-guestagent\",\n\t\tShort:   \"Do not launch manually\",\n\t\tVersion: strings.TrimPrefix(version.Version, \"v\"),\n\t}\n\trootCmd.PersistentFlags().Bool(\"debug\", false, \"Debug mode\")\n\trootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {\n\t\tdebug, _ := cmd.Flags().GetBool(\"debug\")\n\t\tif debug {\n\t\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t\t\tdebugutil.Debug = true\n\t\t}\n\t\treturn nil\n\t}\n\taddRootCommands(rootCmd)\n\treturn rootCmd\n}\n"
  },
  {
    "path": "cmd/lima-guestagent/root_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc addRootCommands(rootCmd *cobra.Command) {\n\trootCmd.AddCommand(\n\t\tnewFakeCloudInitCommand(),\n\t)\n}\n"
  },
  {
    "path": "cmd/lima-guestagent/root_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc addRootCommands(rootCmd *cobra.Command) {\n\trootCmd.AddCommand(\n\t\tnewDaemonCommand(),\n\t\tnewInstallSystemdCommand(),\n\t)\n}\n"
  },
  {
    "path": "cmd/lima-guestagent/root_others.go",
    "content": "//go:build !linux && !darwin\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc addRootCommands(_ *cobra.Command) {\n\t// NOP\n}\n"
  },
  {
    "path": "cmd/lima.bat",
    "content": "@echo off\nREM Environment Variables\nREM LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is \"default\".\nREM LIMACTL: Specifies the path to the limactl binary. Default is \"limactl\" in %PATH%.\n\nIF NOT DEFINED LIMACTL (SET LIMACTL=limactl)\nIF NOT DEFINED LIMA_INSTANCE (SET LIMA_INSTANCE=default)\n%LIMACTL% shell --instance %LIMA_INSTANCE% %*\n"
  },
  {
    "path": "cmd/limactl/clone.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/cmd/limactl/editflags\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/reconcile\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nfunc newCloneCommand() *cobra.Command {\n\tcloneCommand := &cobra.Command{\n\t\tUse:   \"clone OLDINST NEWINST\",\n\t\tShort: \"Clone an instance of Lima\",\n\t\tLong: `Clone an instance of Lima.\n\nNot to be confused with 'limactl copy' ('limactl cp').\n`,\n\t\tArgs:              WrapArgsError(cobra.ExactArgs(2)),\n\t\tRunE:              cloneOrRenameAction,\n\t\tValidArgsFunction: cloneBashComplete,\n\t\tGroupID:           advancedCommand,\n\t}\n\tcloneCommand.Flags().Bool(\"start\", false, \"Start the instance after cloning\")\n\teditflags.RegisterEdit(cloneCommand, \"[limactl edit] \")\n\treturn cloneCommand\n}\n\nfunc newRenameCommand() *cobra.Command {\n\trenameCommand := &cobra.Command{\n\t\tUse: \"rename OLDINST NEWINST\",\n\t\t// No \"mv\" alias, to avoid confusion with a theoretical equivalent of `limactl cp` but s/cp/mv/.\n\t\tShort:             \"Rename an instance of Lima\",\n\t\tArgs:              WrapArgsError(cobra.ExactArgs(2)),\n\t\tRunE:              cloneOrRenameAction,\n\t\tValidArgsFunction: cloneBashComplete,\n\t\tGroupID:           advancedCommand,\n\t}\n\trenameCommand.Flags().Bool(\"start\", false, \"Start the instance after renaming\")\n\teditflags.RegisterEdit(renameCommand, \"[limactl edit] \")\n\treturn renameCommand\n}\n\nfunc cloneOrRenameAction(cmd *cobra.Command, args []string) error {\n\trename := cmd.Name() == \"rename\"\n\tctx := cmd.Context()\n\tflags := cmd.Flags()\n\ttty, err := flags.GetBool(\"tty\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\toldInstName, newInstName := args[0], args[1]\n\toldInst, err := store.Inspect(ctx, oldInstName)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"instance %q not found\", oldInstName)\n\t\t}\n\t\treturn err\n\t}\n\n\tnewInst, err := instance.CloneOrRename(ctx, oldInst, newInstName, rename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tyqExprs, err := editflags.YQExpressions(flags, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(yqExprs) > 0 {\n\t\t// TODO: reduce duplicated codes across cloneAction and editAction\n\t\tyq := yqutil.Join(yqExprs)\n\t\tfilePath := filepath.Join(newInst.Dir, filenames.LimaYAML)\n\t\tyContent, err := os.ReadFile(filePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tyBytes, err := yqutil.EvaluateExpression(yq, yContent)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ty, err := limayaml.LoadWithWarnings(ctx, yBytes, filePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := driverutil.ResolveVMType(ctx, y, filePath); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to resolve vm for %q: %w\", filePath, err)\n\t\t}\n\t\tif err := limayaml.Validate(y, true); err != nil {\n\t\t\treturn saveRejectedYAML(yBytes, err)\n\t\t}\n\t\tif err := limayaml.ValidateAgainstLatestConfig(ctx, yBytes, yContent); err != nil {\n\t\t\treturn saveRejectedYAML(yBytes, err)\n\t\t}\n\t\tif err := os.WriteFile(filePath, yBytes, 0o644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnewInst, err = store.Inspect(ctx, newInst.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tstart, err := flags.GetBool(\"start\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif tty && !flags.Changed(\"start\") {\n\t\tstart, err = askWhetherToStart(cmd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif !start {\n\t\treturn nil\n\t}\n\terr = reconcile.Reconcile(ctx, newInst.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn instance.Start(ctx, newInst, false, false)\n}\n\nfunc cloneBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/completion.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"maps\"\n\t\"net\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/templatestore\"\n)\n\nfunc bashCompleteInstanceNames(_ *cobra.Command) ([]string, cobra.ShellCompDirective) {\n\tinstances, err := store.Instances()\n\tif err != nil {\n\t\treturn nil, cobra.ShellCompDirectiveDefault\n\t}\n\treturn instances, cobra.ShellCompDirectiveNoFileComp\n}\n\nfunc bashCompleteTemplateNames(_ *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) {\n\tvar comp []string\n\tif templates, err := templatestore.Templates(); err == nil {\n\t\tfor _, f := range templates {\n\t\t\tname := \"template:\" + f.Name\n\t\t\tif !strings.HasPrefix(name, toComplete) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(toComplete) == len(name) {\n\t\t\t\tcomp = append(comp, name)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Skip private snippets (beginning with '_') from completion.\n\t\t\tif (name[len(toComplete)-1] == '/') && (name[len(toComplete)] == '_') {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcomp = append(comp, name)\n\t\t}\n\t}\n\treturn comp, cobra.ShellCompDirectiveDefault\n}\n\nfunc bashCompleteDiskNames(_ *cobra.Command) ([]string, cobra.ShellCompDirective) {\n\tdisks, err := store.Disks()\n\tif err != nil {\n\t\treturn nil, cobra.ShellCompDirectiveDefault\n\t}\n\treturn disks, cobra.ShellCompDirectiveNoFileComp\n}\n\nfunc bashCompleteNetworkNames(_ *cobra.Command) ([]string, cobra.ShellCompDirective) {\n\tconfig, err := networks.LoadConfig()\n\tif err != nil {\n\t\treturn nil, cobra.ShellCompDirectiveDefault\n\t}\n\tnetworks := slices.Sorted(maps.Keys(config.Networks))\n\treturn networks, cobra.ShellCompDirectiveNoFileComp\n}\n\nfunc bashFlagCompleteNetworkInterfaceNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\tintf, err := net.Interfaces()\n\tif err != nil {\n\t\treturn nil, cobra.ShellCompDirectiveDefault\n\t}\n\tvar intfNames []string\n\tfor _, f := range intf {\n\t\tintfNames = append(intfNames, f.Name)\n\t}\n\treturn intfNames, cobra.ShellCompDirectiveNoFileComp\n}\n"
  },
  {
    "path": "cmd/limactl/copy.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/copytool\"\n)\n\nconst copyHelp = `Copy files between host and guest\n\nPrefix guest filenames with the instance name and a colon.\n\nBackends:\n  auto   - Automatically selects the best available backend (rsync preferred, falls back to scp)\n  rsync  - Uses rsync for faster transfers with resume capability (requires rsync on both host and guest)\n  scp    - Uses scp for reliable transfers (always available)\n\nNot to be confused with 'limactl clone'.\n`\n\nconst copyExample = `\n  # Copy file from guest to host (auto backend)\n  limactl copy default:/etc/os-release .\n\n  # Copy file from host to guest with verbose output\n  limactl copy -v myfile.txt default:/tmp/\n\n  # Copy directory recursively using rsync backend\n  limactl copy --backend=rsync -r ./mydir default:/tmp/\n\n  # Copy using scp backend specifically\n  limactl copy --backend=scp default:/var/log/app.log ./logs/\n\n  # Copy multiple files\n  limactl copy file1.txt file2.txt default:/tmp/\n`\n\nfunc newCopyCommand() *cobra.Command {\n\tcopyCommand := &cobra.Command{\n\t\tUse:     \"copy SOURCE ... TARGET\",\n\t\tAliases: []string{\"cp\"},\n\t\tShort:   \"Copy files between host and guest\",\n\t\tLong:    copyHelp,\n\t\tExample: copyExample,\n\t\tArgs:    WrapArgsError(cobra.MinimumNArgs(2)),\n\t\tRunE:    copyAction,\n\t\tGroupID: advancedCommand,\n\t}\n\n\tcopyCommand.Flags().BoolP(\"recursive\", \"r\", false, \"Copy directories recursively\")\n\tcopyCommand.Flags().BoolP(\"verbose\", \"v\", false, \"Enable verbose output\")\n\tcopyCommand.Flags().String(\"backend\", \"auto\", \"Copy backend (scp|rsync|auto)\")\n\n\treturn copyCommand\n}\n\nfunc copyAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\trecursive, err := cmd.Flags().GetBool(\"recursive\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tverbose, err := cmd.Flags().GetBool(\"verbose\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdebug, err := cmd.Flags().GetBool(\"debug\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif debug {\n\t\tverbose = true\n\t}\n\n\tbackend, err := cmd.Flags().GetString(\"backend\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcpTool, err := copytool.New(ctx, backend, args, &copytool.Options{\n\t\tRecursive: recursive,\n\t\tVerbose:   verbose,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.Debugf(\"using copy tool %q\", cpTool.Name())\n\n\tcopyCmd, err := cpTool.Command(ctx, args, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcopyCmd.Stdin = cmd.InOrStdin()\n\tcopyCmd.Stdout = cmd.OutOrStdout()\n\tcopyCmd.Stderr = cmd.ErrOrStderr()\n\tlogrus.Debugf(\"executing %v (may take a long time)\", copyCmd)\n\n\t// TODO: use syscall.Exec directly (results in losing tty?)\n\treturn copyCmd.Run()\n}\n"
  },
  {
    "path": "cmd/limactl/debug.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/dns\"\n)\n\nfunc newDebugCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:    \"debug\",\n\t\tShort:  \"Debug utilities\",\n\t\tLong:   \"DO NOT USE! THE COMMAND SYNTAX IS SUBJECT TO CHANGE!\",\n\t\tHidden: true,\n\t}\n\tcmd.AddCommand(newDebugDNSCommand())\n\treturn cmd\n}\n\nfunc newDebugDNSCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"dns UDPPORT [TCPPORT]\",\n\t\tShort: \"Debug built-in DNS\",\n\t\tLong:  \"DO NOT USE! THE COMMAND SYNTAX IS SUBJECT TO CHANGE!\",\n\t\tArgs:  WrapArgsError(cobra.RangeArgs(1, 2)),\n\t\tRunE:  debugDNSAction,\n\t}\n\tcmd.Flags().BoolP(\"ipv6\", \"6\", false, \"Lookup IPv6 addresses too\")\n\treturn cmd\n}\n\nfunc debugDNSAction(cmd *cobra.Command, args []string) error {\n\tipv6, err := cmd.Flags().GetBool(\"ipv6\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tudpLocalPort, err := strconv.Atoi(args[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\ttcpLocalPort := 0\n\tif len(args) > 1 {\n\t\ttcpLocalPort, err = strconv.Atoi(args[1])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tsrvOpts := dns.ServerOptions{\n\t\tUDPPort: udpLocalPort,\n\t\tTCPPort: tcpLocalPort,\n\t\tAddress: \"127.0.0.1\",\n\t\tHandlerOptions: dns.HandlerOptions{\n\t\t\tIPv6:        ipv6,\n\t\t\tStaticHosts: map[string]string{},\n\t\t},\n\t}\n\tsrv, err := dns.Start(srvOpts)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.Infof(\"Started srv %+v (UDP %d, TCP %d)\", srv, udpLocalPort, tcpLocalPort)\n\tfor {\n\t\ttime.Sleep(time.Hour)\n\t}\n}\n"
  },
  {
    "path": "cmd/limactl/delete.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/reconcile\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newDeleteCommand() *cobra.Command {\n\tdeleteCommand := &cobra.Command{\n\t\tUse:               \"delete INSTANCE [INSTANCE, ...]\",\n\t\tAliases:           []string{\"remove\", \"rm\"},\n\t\tShort:             \"Delete an instance of Lima\",\n\t\tArgs:              WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:              deleteAction,\n\t\tValidArgsFunction: deleteBashComplete,\n\t\tGroupID:           basicCommand,\n\t}\n\tdeleteCommand.Flags().BoolP(\"force\", \"f\", false, \"Forcibly kill the processes\")\n\treturn deleteCommand\n}\n\nfunc deleteAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tforce, err := cmd.Flags().GetBool(\"force\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, instName := range args {\n\t\tinst, err := store.Inspect(ctx, instName)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\tlogrus.Warnf(\"Ignoring non-existent instance %q\", instName)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tif err := instance.Delete(cmd.Context(), inst, force); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to delete instance %q: %w\", instName, err)\n\t\t}\n\t\tif registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\t\tlogrus.WithError(err).Warnf(\"Failed to check if the autostart entry for instance %q is registered\", instName)\n\t\t} else if registered {\n\t\t\tif err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil {\n\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to unregister the autostart entry for instance %q\", instName)\n\t\t\t} else {\n\t\t\t\tlogrus.Infof(\"The autostart entry for instance %q has been unregistered\", instName)\n\t\t\t}\n\t\t}\n\t\tlogrus.Infof(\"Deleted %q (%q)\", instName, inst.Dir)\n\t}\n\treturn reconcile.Reconcile(cmd.Context(), \"\")\n}\n\nfunc deleteBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/disk.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"text/tabwriter\"\n\n\tcontfs \"github.com/containerd/continuity/fs\"\n\t\"github.com/docker/go-units\"\n\t\"github.com/lima-vm/go-qcow2reader\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newDiskCommand() *cobra.Command {\n\tdiskCommand := &cobra.Command{\n\t\tUse:   \"disk\",\n\t\tShort: \"Lima disk management\",\n\t\tExample: `  Create a disk:\n  $ limactl disk create DISK --size SIZE [--format qcow2]\n\n  List existing disks:\n  $ limactl disk ls\n\n  Delete a disk:\n  $ limactl disk delete DISK\n\n  Resize a disk:\n  $ limactl disk resize DISK --size SIZE`,\n\t\tSilenceUsage:  true,\n\t\tSilenceErrors: true,\n\t\tGroupID:       advancedCommand,\n\t}\n\tdiskCommand.AddCommand(\n\t\tnewDiskCreateCommand(),\n\t\tnewDiskListCommand(),\n\t\tnewDiskDeleteCommand(),\n\t\tnewDiskUnlockCommand(),\n\t\tnewDiskResizeCommand(),\n\t\tnewDiskImportCommand(),\n\t)\n\treturn diskCommand\n}\n\nfunc newDiskCreateCommand() *cobra.Command {\n\tdiskCreateCommand := &cobra.Command{\n\t\tUse: \"create DISK\",\n\t\tExample: `\nTo create a new disk:\n$ limactl disk create DISK --size SIZE [--format qcow2]\n`,\n\t\tShort: \"Create a Lima disk\",\n\t\tArgs:  WrapArgsError(cobra.ExactArgs(1)),\n\t\tRunE:  diskCreateAction,\n\t}\n\tdiskCreateCommand.Flags().String(\"size\", \"\", \"Configure the disk size\")\n\t_ = diskCreateCommand.MarkFlagRequired(\"size\")\n\tdiskCreateCommand.Flags().String(\"format\", \"qcow2\", \"Specify the disk format\")\n\treturn diskCreateCommand\n}\n\nfunc diskCreateAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tsize, err := cmd.Flags().GetString(\"size\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tformat, err := cmd.Flags().GetString(\"format\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdiskSize, err := units.RAMInBytes(size)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch format {\n\tcase \"qcow2\", \"raw\":\n\tdefault:\n\t\treturn fmt.Errorf(`disk format %q not supported, use \"qcow2\" or \"raw\" instead`, format)\n\t}\n\n\t// only exactly one arg is allowed\n\tname := args[0]\n\n\tdiskDir, err := store.DiskDir(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := os.Stat(diskDir); !errors.Is(err, fs.ErrNotExist) {\n\t\treturn fmt.Errorf(\"disk %q already exists (%q)\", name, diskDir)\n\t}\n\n\tlogrus.Infof(\"Creating %s disk %q with size %s\", format, name, units.BytesSize(float64(diskSize)))\n\n\tif err := os.MkdirAll(diskDir, 0o700); err != nil {\n\t\treturn err\n\t}\n\n\t// qemu may not be available, use it only if needed.\n\tdataDisk := filepath.Join(diskDir, filenames.DataDisk)\n\tdiskUtil := proxyimgutil.NewDiskUtil(ctx)\n\terr = diskUtil.CreateDisk(ctx, dataDisk, diskSize)\n\tif err != nil {\n\t\trerr := os.RemoveAll(diskDir)\n\t\tif rerr != nil {\n\t\t\terr = errors.Join(err, fmt.Errorf(\"failed to remove a directory %q: %w\", diskDir, rerr))\n\t\t}\n\t\treturn fmt.Errorf(\"failed to create %s disk in %q: %w\", format, diskDir, err)\n\t}\n\n\treturn nil\n}\n\nfunc newDiskListCommand() *cobra.Command {\n\tdiskListCommand := &cobra.Command{\n\t\tUse: \"list\",\n\t\tExample: `\nTo list existing disks:\n$ limactl disk list\n`,\n\t\tShort:   \"List existing Lima disks\",\n\t\tAliases: []string{\"ls\"},\n\t\tArgs:    WrapArgsError(cobra.ArbitraryArgs),\n\t\tRunE:    diskListAction,\n\t}\n\tdiskListCommand.Flags().Bool(\"json\", false, \"JSONify output\")\n\treturn diskListCommand\n}\n\nfunc nameMatches(nameName string, names []string) []string {\n\tmatches := []string{}\n\tfor _, name := range names {\n\t\tif name == nameName {\n\t\t\tmatches = append(matches, name)\n\t\t}\n\t}\n\treturn matches\n}\n\nfunc diskListAction(cmd *cobra.Command, args []string) error {\n\tjsonFormat, err := cmd.Flags().GetBool(\"json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tallDisks, err := store.Disks()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdisks := []string{}\n\tif len(args) > 0 {\n\t\tfor _, arg := range args {\n\t\t\tmatches := nameMatches(arg, allDisks)\n\t\t\tif len(matches) > 0 {\n\t\t\t\tdisks = append(disks, matches...)\n\t\t\t} else {\n\t\t\t\tlogrus.Warnf(\"No disk matching %v found.\", arg)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tdisks = allDisks\n\t}\n\n\tif jsonFormat {\n\t\tfor _, diskName := range disks {\n\t\t\tdisk, err := store.InspectDisk(diskName, nil)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Errorf(\"disk %q does not exist?\", diskName)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tj, err := json.Marshal(disk)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Fprintln(cmd.OutOrStdout(), string(j))\n\t\t}\n\t\treturn nil\n\t}\n\n\tw := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0)\n\tfmt.Fprintln(w, \"NAME\\tSIZE\\tFORMAT\\tDIR\\tIN-USE-BY\")\n\n\tif len(disks) == 0 {\n\t\tlogrus.Warn(\"No disk found. Run `limactl disk create DISK --size SIZE` to create a disk.\")\n\t}\n\n\tfor _, diskName := range disks {\n\t\tdisk, err := store.InspectDisk(diskName, nil)\n\t\tif err != nil {\n\t\t\tlogrus.WithError(err).Errorf(\"disk %q does not exist?\", diskName)\n\t\t\tcontinue\n\t\t}\n\t\tfmt.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)\n\t}\n\n\treturn w.Flush()\n}\n\nfunc newDiskDeleteCommand() *cobra.Command {\n\tdiskDeleteCommand := &cobra.Command{\n\t\tUse: \"delete DISK [DISK, ...]\",\n\t\tExample: `\nTo delete a disk:\n$ limactl disk delete DISK\n\nTo delete multiple disks:\n$ limactl disk delete DISK1 DISK2 ...\n`,\n\t\tAliases:           []string{\"remove\", \"rm\"},\n\t\tShort:             \"Delete one or more Lima disks\",\n\t\tArgs:              WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:              diskDeleteAction,\n\t\tValidArgsFunction: diskBashComplete,\n\t}\n\tdiskDeleteCommand.Flags().BoolP(\"force\", \"f\", false, \"Force delete\")\n\treturn diskDeleteCommand\n}\n\nfunc diskDeleteAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tforce, err := cmd.Flags().GetBool(\"force\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinstNames, err := store.Instances()\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar instances []*limatype.Instance\n\tfor _, instName := range instNames {\n\t\tinst, err := store.Inspect(ctx, instName)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tinstances = append(instances, inst)\n\t}\n\n\tfor _, diskName := range args {\n\t\tdisk, err := store.InspectDisk(diskName, nil)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tlogrus.Warnf(\"Ignoring non-existent disk %q\", diskName)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tif !force {\n\t\t\tif disk.Instance != \"\" {\n\t\t\t\treturn fmt.Errorf(\"cannot delete disk %q in use by instance %q\", disk.Name, disk.Instance)\n\t\t\t}\n\t\t\tvar refInstances []string\n\t\t\tfor _, inst := range instances {\n\t\t\t\tfor _, d := range inst.AdditionalDisks {\n\t\t\t\t\tif d.Name == diskName {\n\t\t\t\t\t\trefInstances = append(refInstances, inst.Name)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(refInstances) > 0 {\n\t\t\t\tlogrus.Warnf(\"Skipping deleting disk %q, disk is referenced by one or more non-running instances: %q\",\n\t\t\t\t\tdiskName, refInstances)\n\t\t\t\tlogrus.Warnf(\"To delete anyway, run %q\", forceDeleteCommand(diskName))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif err := deleteDisk(disk); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to delete disk %q: %w\", diskName, err)\n\t\t}\n\t\tlogrus.Infof(\"Deleted %q (%q)\", diskName, disk.Dir)\n\t}\n\treturn nil\n}\n\nfunc deleteDisk(disk *store.Disk) error {\n\tif err := os.RemoveAll(disk.Dir); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove %q: %w\", disk.Dir, err)\n\t}\n\treturn nil\n}\n\nfunc forceDeleteCommand(diskName string) string {\n\treturn fmt.Sprintf(\"limactl disk delete --force %v\", diskName)\n}\n\nfunc newDiskUnlockCommand() *cobra.Command {\n\tdiskUnlockCommand := &cobra.Command{\n\t\tUse: \"unlock DISK [DISK, ...]\",\n\t\tExample: `\nEmergency recovery! If an instance is force stopped, it may leave a disk locked while not actually using it.\n\nTo unlock a disk:\n$ limactl disk unlock DISK\n\nTo unlock multiple disks:\n$ limactl disk unlock DISK1 DISK2 ...\n`,\n\t\tShort:             \"Unlock one or more Lima disks\",\n\t\tArgs:              WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:              diskUnlockAction,\n\t\tValidArgsFunction: diskBashComplete,\n\t}\n\treturn diskUnlockCommand\n}\n\nfunc diskUnlockAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tfor _, diskName := range args {\n\t\tdisk, err := store.InspectDisk(diskName, nil)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tlogrus.Warnf(\"Ignoring non-existent disk %q\", diskName)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tif disk.Instance == \"\" {\n\t\t\tlogrus.Warnf(\"Ignoring unlocked disk %q\", diskName)\n\t\t\tcontinue\n\t\t}\n\t\t// if store.Inspect throws an error, the instance does not exist, and it is safe to unlock\n\t\tinst, err := store.Inspect(ctx, disk.Instance)\n\t\tif err == nil {\n\t\t\tif len(inst.Errors) > 0 {\n\t\t\t\tlogrus.Warnf(\"Cannot unlock disk %q, attached instance %q has errors: %+v\",\n\t\t\t\t\tdiskName, disk.Instance, inst.Errors)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif inst.Status == limatype.StatusRunning {\n\t\t\t\tlogrus.Warnf(\"Cannot unlock disk %q used by running instance %q\", diskName, disk.Instance)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tif err := disk.Unlock(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unlock disk %q: %w\", diskName, err)\n\t\t}\n\t\tlogrus.Infof(\"Unlocked disk %q (%q)\", diskName, disk.Dir)\n\t}\n\treturn nil\n}\n\nfunc newDiskResizeCommand() *cobra.Command {\n\tdiskResizeCommand := &cobra.Command{\n\t\tUse: \"resize DISK\",\n\t\tExample: `\nResize a disk:\n$ limactl disk resize DISK --size SIZE`,\n\t\tShort:             \"Resize existing Lima disk\",\n\t\tArgs:              WrapArgsError(cobra.ExactArgs(1)),\n\t\tRunE:              diskResizeAction,\n\t\tValidArgsFunction: diskBashComplete,\n\t}\n\tdiskResizeCommand.Flags().String(\"size\", \"\", \"Disk size\")\n\t_ = diskResizeCommand.MarkFlagRequired(\"size\")\n\treturn diskResizeCommand\n}\n\nfunc diskResizeAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tsize, err := cmd.Flags().GetString(\"size\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdiskSize, err := units.RAMInBytes(size)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdiskName := args[0]\n\tdisk, err := store.InspectDisk(diskName, nil)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"disk %q does not exists\", diskName)\n\t\t}\n\t\treturn err\n\t}\n\n\t// Shrinking can cause a disk failure\n\tif diskSize < disk.Size {\n\t\treturn 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)))\n\t}\n\n\tif disk.Instance != \"\" {\n\t\tinst, err := store.Inspect(ctx, disk.Instance)\n\t\tif err == nil {\n\t\t\tif inst.Status == limatype.StatusRunning {\n\t\t\t\treturn fmt.Errorf(\"cannot resize disk %q used by running instance %q. Please stop the VM instance\", diskName, disk.Instance)\n\t\t\t}\n\t\t}\n\t}\n\n\t// qemu may not be available, use it only if needed.\n\tdataDisk := filepath.Join(disk.Dir, filenames.DataDisk)\n\tdiskUtil := proxyimgutil.NewDiskUtil(ctx)\n\terr = diskUtil.ResizeDisk(ctx, dataDisk, diskSize)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to resize disk %q: %w\", diskName, err)\n\t}\n\n\tlogrus.Infof(\"Resized disk %q (%q)\", diskName, disk.Dir)\n\treturn nil\n}\n\nfunc diskBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteDiskNames(cmd)\n}\n\nfunc newDiskImportCommand() *cobra.Command {\n\tdiskImportCommand := &cobra.Command{\n\t\tUse: \"import DISK FILE\",\n\t\tExample: `\nImport a disk:\n$ limactl disk import DISK DISKPATH\n`,\n\t\tShort:             \"Import an existing disk to Lima\",\n\t\tArgs:              WrapArgsError(cobra.ExactArgs(2)),\n\t\tRunE:              diskImportAction,\n\t\tValidArgsFunction: diskBashComplete,\n\t}\n\treturn diskImportCommand\n}\n\nfunc diskImportAction(_ *cobra.Command, args []string) error {\n\tdiskName := args[0]\n\tfName := args[1]\n\n\tdiskDir, err := store.DiskDir(diskName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _, err := os.Stat(diskDir); !errors.Is(err, fs.ErrNotExist) {\n\t\treturn fmt.Errorf(\"disk %q already exists (%q)\", diskName, diskDir)\n\t}\n\n\tf, err := os.Open(fName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\timg, err := qcow2reader.Open(f)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdiskSize := img.Size()\n\tformat := img.Type()\n\n\tswitch format {\n\tcase \"qcow2\", \"raw\":\n\tdefault:\n\t\treturn fmt.Errorf(`disk format %q not supported, use \"qcow2\" or \"raw\" instead`, format)\n\t}\n\n\tif err := os.MkdirAll(diskDir, 0o755); err != nil {\n\t\treturn err\n\t}\n\n\tif err := contfs.CopyFile(filepath.Join(diskDir, filenames.DataDisk), fName); err != nil {\n\t\treturn nil\n\t}\n\n\tlogrus.Infof(\"Imported %s with size %s\", diskName, units.BytesSize(float64(diskSize)))\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/limactl/edit.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/cmd/limactl/editflags\"\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/editutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/reconcile\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/uiutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nfunc newEditCommand() *cobra.Command {\n\teditCommand := &cobra.Command{\n\t\tUse:               \"edit INSTANCE|FILE.yaml\",\n\t\tShort:             \"Edit an instance of Lima or a template\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tRunE:              editAction,\n\t\tValidArgsFunction: editBashComplete,\n\t\tGroupID:           basicCommand,\n\t}\n\teditCommand.Flags().Bool(\"start\", false, \"Start the instance after editing\")\n\teditflags.RegisterEdit(editCommand, \"\")\n\treturn editCommand\n}\n\nfunc editAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tvar arg string\n\tif len(args) > 0 {\n\t\targ = args[0]\n\t}\n\n\tvar filePath string\n\tvar err error\n\tvar inst *limatype.Instance\n\n\tif arg == \"\" {\n\t\targ = DefaultInstanceName\n\t}\n\tif err := dirnames.ValidateInstName(arg); err == nil {\n\t\tinst, err = store.Inspect(ctx, arg)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\treturn fmt.Errorf(\"instance %q not found\", arg)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tif inst.Status == limatype.StatusRunning {\n\t\t\treturn errors.New(\"cannot edit a running instance\")\n\t\t}\n\t\tfilePath = filepath.Join(inst.Dir, filenames.LimaYAML)\n\t} else {\n\t\t// absolute path is required for `limayaml.Validate`\n\t\tfilePath, err = filepath.Abs(arg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tyContent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tflags := cmd.Flags()\n\ttty, err := flags.GetBool(\"tty\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tyqExprs, err := editflags.YQExpressions(flags, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar yBytes []byte\n\tif len(yqExprs) > 0 {\n\t\tyq := yqutil.Join(yqExprs)\n\t\tyBytes, err = yqutil.EvaluateExpression(yq, yContent)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if tty {\n\t\tvar hdr string\n\t\tif inst != nil {\n\t\t\thdr = fmt.Sprintf(\"# Please edit the following configuration for Lima instance %q\\n\", inst.Name)\n\t\t} else {\n\t\t\thdr = fmt.Sprintf(\"# Please edit the following configuration %q\\n\", filePath)\n\t\t}\n\t\thdr += \"# and an empty file will abort the edit.\\n\"\n\t\thdr += \"\\n\"\n\t\thdr += editutil.GenerateEditorWarningHeader()\n\t\tyBytes, err = editutil.OpenEditor(ctx, yContent, hdr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif len(yBytes) == 0 {\n\t\tlogrus.Info(\"Aborting, as requested by saving the file with empty content\")\n\t\treturn nil\n\t}\n\tif bytes.Equal(yBytes, yContent) {\n\t\tlogrus.Info(\"Aborting, no changes made to the instance\")\n\t\treturn nil\n\t}\n\ty, err := limayaml.LoadWithWarnings(ctx, yBytes, filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := driverutil.ResolveVMType(ctx, y, filePath); err != nil {\n\t\treturn fmt.Errorf(\"failed to resolve vm for %q: %w\", filePath, err)\n\t}\n\tif err := limayaml.Validate(y, true); err != nil {\n\t\treturn saveRejectedYAML(yBytes, err)\n\t}\n\n\tif err := limayaml.ValidateAgainstLatestConfig(ctx, yBytes, yContent); err != nil {\n\t\treturn saveRejectedYAML(yBytes, err)\n\t}\n\n\tif err := os.WriteFile(filePath, yBytes, 0o644); err != nil {\n\t\treturn err\n\t}\n\n\tif inst != nil {\n\t\tlogrus.Infof(\"Instance %q configuration edited\", inst.Name)\n\t}\n\n\tif inst == nil {\n\t\t// edited a limayaml file directly\n\t\treturn nil\n\t}\n\n\tstart, err := flags.GetBool(\"start\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif tty && !flags.Changed(\"start\") {\n\t\tstart, err = askWhetherToStart(cmd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif !start {\n\t\treturn nil\n\t}\n\t// Network reconciliation will be performed by the process launched by the autostart manager\n\tif registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t} else if !registered {\n\t\terr = reconcile.Reconcile(ctx, inst.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// store.Inspect() syncs values between inst.YAML and the store.\n\t// This call applies the validated template to the store.\n\tinst, err = store.Inspect(ctx, inst.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn instance.Start(ctx, inst, false, false)\n}\n\nfunc askWhetherToStart(cmd *cobra.Command) (bool, error) {\n\tisTTY := uiutil.InputIsTTY(cmd.InOrStdin())\n\tif isTTY {\n\t\tmessage := \"Do you want to start the instance now? \"\n\t\treturn uiutil.Confirm(message, true)\n\t}\n\treturn false, nil\n}\n\nfunc editBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n\n// saveRejectedYAML writes the rejected config and returns an error.\nfunc saveRejectedYAML(y []byte, origErr error) error {\n\trejectedYAML := \"lima.REJECTED.yaml\"\n\tif writeErr := os.WriteFile(rejectedYAML, y, 0o644); writeErr != nil {\n\t\treturn fmt.Errorf(\"the YAML is invalid, attempted to save the buffer as %q but failed: %w\", rejectedYAML, errors.Join(writeErr, origErr))\n\t}\n\t// TODO: may need to support editing the rejected YAML\n\treturn fmt.Errorf(\"the YAML is invalid, saved the buffer as %q: %w\", rejectedYAML, origErr)\n}\n"
  },
  {
    "path": "cmd/limactl/editflags/editflags.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage editflags\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"math/bits\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pbnjay/memory\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\tflag \"github.com/spf13/pflag\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/localpathutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/registry\"\n)\n\n// RegisterEdit registers flags related to in-place YAML modification, for `limactl edit`.\nfunc RegisterEdit(cmd *cobra.Command, commentPrefix string) {\n\tflags := cmd.Flags()\n\n\tflags.Int(\"cpus\", 0, commentPrefix+\"Number of CPUs\") // Similar to colima's --cpu, but the flag name is slightly different (cpu vs cpus)\n\t_ = cmd.RegisterFlagCompletionFunc(\"cpus\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\tvar res []string\n\t\tfor _, f := range completeCPUs(runtime.NumCPU()) {\n\t\t\tres = append(res, strconv.Itoa(f))\n\t\t}\n\t\treturn res, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\tflags.IPSlice(\"dns\", nil, commentPrefix+\"Specify custom DNS (disable host resolver)\") // colima-compatible\n\n\tflags.Float32(\"memory\", 0, commentPrefix+\"Memory in GiB\") // colima-compatible\n\t_ = cmd.RegisterFlagCompletionFunc(\"memory\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\tvar res []string\n\t\tfor _, f := range completeMemoryGiB(memory.TotalMemory()) {\n\t\t\tres = append(res, fmt.Sprintf(\"%.1f\", f))\n\t\t}\n\t\treturn res, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\tflags.StringSlice(\"mount\", nil, commentPrefix+\"Directories to mount, suffix ':w' for writable (Do not specify directories that overlap with the existing mounts)\") // colima-compatible\n\tflags.StringSlice(\"mount-only\", nil, commentPrefix+\"Similar to --mount, but overrides the existing mounts\")\n\n\tflags.Bool(\"mount-none\", false, commentPrefix+\"Remove all mounts\")\n\n\tflags.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\n\t_ = cmd.RegisterFlagCompletionFunc(\"mount-type\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\treturn []string{\"reverse-sshfs\", \"9p\", \"virtiofs\"}, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\tflags.Bool(\"mount-writable\", false, commentPrefix+\"Make all mounts writable\")\n\tflags.Bool(\"mount-inotify\", false, commentPrefix+\"Enable inotify for mounts\")\n\n\tflags.StringSlice(\"network\", nil, commentPrefix+\"Additional networks, e.g., \\\"vzNAT\\\" or \\\"lima:shared\\\" to assign vmnet IP\")\n\t_ = cmd.RegisterFlagCompletionFunc(\"network\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\t// TODO: retrieve the lima:* network list from networks.yaml\n\t\treturn []string{\"lima:shared\", \"lima:bridged\", \"lima:host\", \"lima:user-v2\", \"vzNAT\"}, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\tflags.Bool(\"nested-virt\", false, commentPrefix+\"Enable nested virtualization\")\n\n\tflags.Bool(\"rosetta\", false, commentPrefix+\"Enable Rosetta (for vz instances)\")\n\n\tflags.StringArray(\"set\", []string{}, commentPrefix+\"Modify the template inplace, using yq syntax. Can be passed multiple times.\")\n\n\tflags.Uint16(\"ssh-port\", 0, commentPrefix+\"SSH port (0 for random)\") // colima-compatible\n\t_ = cmd.RegisterFlagCompletionFunc(\"ssh-port\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\t// Until Lima v2.0, 60022 was the default SSH port for the instance named \"default\".\n\t\treturn []string{\"60022\"}, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\t// negative performance impact: https://gitlab.com/qemu-project/qemu/-/issues/334\n\tflags.Bool(\"video\", false, commentPrefix+\"Enable video output (has negative performance impact for QEMU)\")\n\n\tflags.Float32(\"disk\", 0, commentPrefix+\"Disk size in GiB\") // colima-compatible\n\t_ = cmd.RegisterFlagCompletionFunc(\"disk\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\treturn []string{\"10\", \"30\", \"50\", \"100\", \"200\"}, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\tflags.String(\"vm-type\", \"\", commentPrefix+\"Virtual machine type\")\n\t_ = cmd.RegisterFlagCompletionFunc(\"vm-type\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\tvar drivers []string\n\t\tfor k := range registry.List() {\n\t\t\tdrivers = append(drivers, k)\n\t\t}\n\t\treturn drivers, cobra.ShellCompDirectiveNoFileComp\n\t})\n}\n\n// RegisterCreate registers flags related to in-place YAML modification, for `limactl create`.\nfunc RegisterCreate(cmd *cobra.Command, commentPrefix string) {\n\tRegisterEdit(cmd, commentPrefix)\n\tflags := cmd.Flags()\n\n\tflags.String(\"arch\", \"\", commentPrefix+\"Machine architecture (x86_64, aarch64, riscv64, armv7l, s390x, ppc64le)\") // colima-compatible\n\t_ = cmd.RegisterFlagCompletionFunc(\"arch\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\treturn []string{\"x86_64\", \"aarch64\", \"riscv64\", \"armv7l\", \"s390x\", \"ppc64le\"}, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\tflags.String(\"containerd\", \"\", commentPrefix+\"containerd mode (user, system, user+system, none)\")\n\t_ = cmd.RegisterFlagCompletionFunc(\"containerd\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\treturn []string{\"user\", \"system\", \"user+system\", \"none\"}, cobra.ShellCompDirectiveNoFileComp\n\t})\n\n\tflags.Bool(\"plain\", false, commentPrefix+\"Plain mode. Disables mounts, port forwarding, containerd, etc.\")\n\n\tflags.StringArray(\"port-forward\", nil, commentPrefix+\"Port forwards (host:guest), e.g., '8080:80' or '9090:9090,static=true' for static port-forwards\")\n\t_ = cmd.RegisterFlagCompletionFunc(\"port-forward\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\treturn []string{\"8080:80\", \"3000:3000\", \"8080:80,static=true\"}, cobra.ShellCompDirectiveNoFileComp\n\t})\n}\n\nfunc defaultExprFunc(expr string) func(v *flag.Flag) ([]string, error) {\n\treturn func(v *flag.Flag) ([]string, error) {\n\t\treturn []string{fmt.Sprintf(expr, v.Value)}, nil\n\t}\n}\n\nfunc ParsePortForward(spec string) (hostPort, guestPort string, isStatic bool, err error) {\n\tparts := strings.Split(spec, \",\")\n\tif len(parts) > 2 {\n\t\treturn \"\", \"\", false, fmt.Errorf(\"invalid port forward format %q, expected HOST:GUEST or HOST:GUEST,static=true\", spec)\n\t}\n\n\tportParts := strings.Split(strings.TrimSpace(parts[0]), \":\")\n\tif len(portParts) != 2 {\n\t\treturn \"\", \"\", false, fmt.Errorf(\"invalid port forward format %q, expected HOST:GUEST\", parts[0])\n\t}\n\n\thostPort = strings.TrimSpace(portParts[0])\n\tguestPort = strings.TrimSpace(portParts[1])\n\n\tif len(parts) == 2 {\n\t\tstaticPart := strings.TrimSpace(parts[1])\n\t\tif staticValue, ok := strings.CutPrefix(staticPart, \"static=\"); ok {\n\t\t\tisStatic, err = strconv.ParseBool(staticValue)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", \"\", false, fmt.Errorf(\"invalid value for static parameter: %q\", staticValue)\n\t\t\t}\n\t\t} else {\n\t\t\treturn \"\", \"\", false, fmt.Errorf(\"invalid parameter %q, expected 'static=' followed by a boolean value\", staticPart)\n\t\t}\n\t}\n\n\treturn hostPort, guestPort, isStatic, nil\n}\n\nfunc BuildPortForwardExpression(portForwards []string) (string, error) {\n\tif len(portForwards) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\tports := make([]string, len(portForwards))\n\tfor i, spec := range portForwards {\n\t\thostPort, guestPort, isStatic, err := ParsePortForward(spec)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tports[i] = fmt.Sprintf(`{\"guestPort\": %q, \"hostPort\": %q, \"static\": %v}`, guestPort, hostPort, isStatic)\n\t}\n\texpr := fmt.Sprintf(\".portForwards += [%s]\", strings.Join(ports, \",\"))\n\treturn expr, nil\n}\n\nfunc buildMountListExpression(ss []string) (string, error) {\n\tmounts := make([]string, len(ss))\n\tfor i, s := range ss {\n\t\twritable := strings.HasSuffix(s, \":w\")\n\t\tloc, err := localpathutil.Expand(strings.TrimSuffix(s, \":w\"))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tmounts[i] = fmt.Sprintf(`{\"location\": %q, \"mountPoint\": %q, \"writable\": %v}`, loc, loc, writable)\n\t}\n\texpr := fmt.Sprintf(\"[%s]\", strings.Join(mounts, \",\"))\n\treturn expr, nil\n}\n\n// YQExpressions returns YQ expressions.\nfunc YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {\n\ttype def struct {\n\t\tflagName                 string\n\t\texprFunc                 func(*flag.Flag) ([]string, error)\n\t\tonlyValidForNewInstances bool\n\t\texperimental             bool\n\t}\n\td := defaultExprFunc\n\tdefs := []def{\n\t\t{\n\t\t\t\"cpus\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tnumCpus, err := flags.GetInt(\"cpus\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif numCpus < 0 {\n\t\t\t\t\treturn nil, errors.New(\"invalid value for number of cpus, must be >= 0\")\n\t\t\t\t}\n\t\t\t\treturn []string{fmt.Sprintf(\".cpus = %d\", numCpus)}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"dns\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tipSlice, err := flags.GetIPSlice(\"dns\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tips := make([]string, len(ipSlice))\n\t\t\t\tfor i, ip := range ipSlice {\n\t\t\t\t\tips[i] = `\"` + ip.String() + `\"`\n\t\t\t\t}\n\t\t\t\texpr := fmt.Sprintf(\".dns += [%s] | .dns |= unique | .hostResolver.enabled=false\", strings.Join(ips, \",\"))\n\t\t\t\tlogrus.Warnf(\"Disabling HostResolver, as custom DNS addresses (%v) are specified\", ipSlice)\n\t\t\t\treturn []string{expr}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t\t{\"memory\", d(\".memory = \\\"%sGiB\\\"\"), false, false},\n\t\t{\n\t\t\t\"mount\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tss, err := flags.GetStringSlice(\"mount\")\n\t\t\t\tslices.Reverse(ss)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tmountListExpr, err := buildMountListExpression(ss)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\t// mount options take precedence over template settings\n\t\t\t\texpr := fmt.Sprintf(\".mounts = %s + .mounts\", mountListExpr)\n\t\t\t\tmountOnly, err := flags.GetStringSlice(\"mount-only\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif len(mountOnly) > 0 {\n\t\t\t\t\treturn nil, errors.New(\"flag `--mount` conflicts with `--mount-only`\")\n\t\t\t\t}\n\t\t\t\treturn []string{expr}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"mount-only\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tss, err := flags.GetStringSlice(\"mount-only\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tmountListExpr, err := buildMountListExpression(ss)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\texpr := `.mounts = ` + mountListExpr\n\t\t\t\treturn []string{expr}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"mount-none\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tincompatibleFlagNames := []string{\"mount\", \"mount-only\"}\n\t\t\t\tfor _, name := range incompatibleFlagNames {\n\t\t\t\t\tss, err := flags.GetStringSlice(name)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tif len(ss) > 0 {\n\t\t\t\t\t\treturn nil, errors.New(\"flag `--mount-none` conflicts with `\" + name + \"`\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn []string{\".mounts = null\"}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t\t{\"mount-type\", d(\".mountType = %q\"), false, false},\n\t\t{\"vm-type\", d(\".vmType = %q\"), false, false},\n\t\t{\"mount-inotify\", d(\".mountInotify = %s\"), false, true},\n\t\t{\"mount-writable\", d(\".mounts[].writable = %s\"), false, false},\n\t\t{\n\t\t\t\"network\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tss, err := flags.GetStringSlice(\"network\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tnetworks := make([]string, len(ss))\n\t\t\t\tfor i, s := range ss {\n\t\t\t\t\t// CLI syntax is still experimental (YAML syntax is out of experimental)\n\t\t\t\t\tswitch {\n\t\t\t\t\tcase s == \"vzNAT\":\n\t\t\t\t\t\tnetworks[i] = `{\"vzNAT\": true}`\n\t\t\t\t\tcase strings.HasPrefix(s, \"lima:\"):\n\t\t\t\t\t\tnetwork := strings.TrimPrefix(s, \"lima:\")\n\t\t\t\t\t\tnetworks[i] = fmt.Sprintf(`{\"lima\": %q}`, network)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn nil, fmt.Errorf(`network name must be \"vzNAT\" or \"lima:*\", got %q`, s)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\texpr := fmt.Sprintf(`.networks += [%s] | .networks |= unique_by(.lima)`, strings.Join(networks, \",\"))\n\t\t\t\treturn []string{expr}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\n\t\t{\"nested-virt\", d(\".nestedVirtualization = %s\"), false, false},\n\t\t{\n\t\t\t\"rosetta\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tb, err := flags.GetBool(\"rosetta\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn []string{fmt.Sprintf(\".vmOpts.vz.rosetta.enabled = %v | .vmOpts.vz.rosetta.binfmt = %v\", b, b)}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t\t{\"set\", func(v *flag.Flag) ([]string, error) {\n\t\t\treturn v.Value.(flag.SliceValue).GetSlice(), nil\n\t\t}, false, false},\n\t\t{\n\t\t\t\"video\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tb, err := flags.GetBool(\"video\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif b {\n\t\t\t\t\treturn []string{\".video.display = \\\"default\\\"\"}, nil\n\t\t\t\t}\n\t\t\t\treturn []string{\".video.display = \\\"none\\\"\"}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t\t{\"ssh-port\", d(\".ssh.localPort = %s\"), false, false},\n\t\t{\"arch\", d(\".arch = %q\"), true, false},\n\t\t{\n\t\t\t\"containerd\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\ts, err := flags.GetString(\"containerd\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tswitch s {\n\t\t\t\tcase \"user\":\n\t\t\t\t\treturn []string{`.containerd.user = true | .containerd.system = false`}, nil\n\t\t\t\tcase \"system\":\n\t\t\t\t\treturn []string{`.containerd.user = false | .containerd.system = true`}, nil\n\t\t\t\tcase \"user+system\", \"system+user\":\n\t\t\t\t\treturn []string{`.containerd.user = true | .containerd.system = true`}, nil\n\t\t\t\tcase \"none\":\n\t\t\t\t\treturn []string{`.containerd.user = false | .containerd.system = false`}, nil\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, fmt.Errorf(`expected one of [\"user\", \"system\", \"user+system\", \"none\"], got %q`, s)\n\t\t\t\t}\n\t\t\t},\n\t\t\ttrue,\n\t\t\tfalse,\n\t\t},\n\t\t{\"disk\", d(\".disk= \\\"%sGiB\\\"\"), false, false},\n\t\t{\"plain\", d(\".plain = %s\"), true, false},\n\t\t{\n\t\t\t\"port-forward\",\n\t\t\tfunc(_ *flag.Flag) ([]string, error) {\n\t\t\t\tss, err := flags.GetStringArray(\"port-forward\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tvalue, err := BuildPortForwardExpression(ss)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturn []string{value}, nil\n\t\t\t},\n\t\t\tfalse,\n\t\t\tfalse,\n\t\t},\n\t}\n\tvar exprs []string\n\tfor _, def := range defs {\n\t\tv := flags.Lookup(def.flagName)\n\t\tif v != nil && v.Changed {\n\t\t\tif def.experimental {\n\t\t\t\tlogrus.Warnf(\"`--%s` is experimental\", def.flagName)\n\t\t\t}\n\t\t\tif def.onlyValidForNewInstances && !newInstance {\n\t\t\t\tlogrus.Warnf(\"`--%s` is not applicable to an existing instance (Hint: create a new instance with `limactl create --%s=%s --name=NAME`)\",\n\t\t\t\t\tdef.flagName, def.flagName, v.Value.String())\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnewExprs, err := def.exprFunc(v)\n\t\t\tif err != nil {\n\t\t\t\treturn exprs, fmt.Errorf(\"error while processing flag %q: %w\", def.flagName, err)\n\t\t\t}\n\t\t\texprs = append(exprs, newExprs...)\n\t\t}\n\t}\n\treturn exprs, nil\n}\n\nfunc isPowerOfTwo(x int) bool {\n\treturn bits.OnesCount(uint(x)) == 1\n}\n\nfunc completeCPUs(hostCPUs int) []int {\n\tvar res []int\n\tfor i := 1; i <= hostCPUs; i *= 2 {\n\t\tres = append(res, i)\n\t}\n\tif !isPowerOfTwo(hostCPUs) {\n\t\tres = append(res, hostCPUs)\n\t}\n\treturn res\n}\n\nfunc completeMemoryGiB(hostMemory uint64) []float32 {\n\thostMemoryHalfGiB := int(hostMemory / 2 / 1024 / 1024 / 1024)\n\tvar res []float32\n\tif hostMemoryHalfGiB < 1 {\n\t\tres = append(res, 0.5)\n\t}\n\tfor i := 1; i <= hostMemoryHalfGiB; i *= 2 {\n\t\tres = append(res, float32(i))\n\t}\n\tif hostMemoryHalfGiB > 1 && !isPowerOfTwo(hostMemoryHalfGiB) {\n\t\tres = append(res, float32(hostMemoryHalfGiB))\n\t}\n\treturn res\n}\n"
  },
  {
    "path": "cmd/limactl/editflags/editflags_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage editflags\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/spf13/cobra\"\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/localpathutil\"\n)\n\nfunc TestCompleteCPUs(t *testing.T) {\n\tassert.DeepEqual(t, []int{1}, completeCPUs(1))\n\tassert.DeepEqual(t, []int{1, 2}, completeCPUs(2))\n\tassert.DeepEqual(t, []int{1, 2, 4, 8}, completeCPUs(8))\n\tassert.DeepEqual(t, []int{1, 2, 4, 8, 16, 20}, completeCPUs(20))\n}\n\nfunc TestCompleteMemoryGiB(t *testing.T) {\n\tassert.DeepEqual(t, []float32{0.5}, completeMemoryGiB(1<<30))\n\tassert.DeepEqual(t, []float32{1}, completeMemoryGiB(2<<30))\n\tassert.DeepEqual(t, []float32{1, 2}, completeMemoryGiB(4<<30))\n\tassert.DeepEqual(t, []float32{1, 2, 4}, completeMemoryGiB(8<<30))\n\tassert.DeepEqual(t, []float32{1, 2, 4, 8, 10}, completeMemoryGiB(20<<30))\n}\n\nfunc TestBuildPortForwardExpression(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tportForwards []string\n\t\texpected     string\n\t\texpectError  bool\n\t}{\n\t\t{\n\t\t\tname:         \"empty port forwards\",\n\t\t\tportForwards: []string{},\n\t\t\texpected:     \"\",\n\t\t},\n\t\t{\n\t\t\tname:         \"single dynamic port forward\",\n\t\t\tportForwards: []string{\"8080:80\"},\n\t\t\texpected:     `.portForwards += [{\"guestPort\": \"80\", \"hostPort\": \"8080\", \"static\": false}]`,\n\t\t},\n\t\t{\n\t\t\tname:         \"single static port forward\",\n\t\t\tportForwards: []string{\"8080:80,static=true\"},\n\t\t\texpected:     `.portForwards += [{\"guestPort\": \"80\", \"hostPort\": \"8080\", \"static\": true}]`,\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple mixed port forwards\",\n\t\t\tportForwards: []string{\"8080:80\", \"2222:22,static=true\", \"3000:3000\"},\n\t\t\texpected:     `.portForwards += [{\"guestPort\": \"80\", \"hostPort\": \"8080\", \"static\": false},{\"guestPort\": \"22\", \"hostPort\": \"2222\", \"static\": true},{\"guestPort\": \"3000\", \"hostPort\": \"3000\", \"static\": false}]`,\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid format - missing colon\",\n\t\t\tportForwards: []string{\"8080\"},\n\t\t\texpectError:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid format - too many colons\",\n\t\t\tportForwards: []string{\"8080:80:extra\"},\n\t\t\texpectError:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid static parameter\",\n\t\t\tportForwards: []string{\"8080:80,invalid=true\"},\n\t\t\texpectError:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"too many parameters\",\n\t\t\tportForwards: []string{\"8080:80,static=true,extra=value\"},\n\t\t\texpectError:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"whitespace handling\",\n\t\t\tportForwards: []string{\" 8080 : 80 , static=true \"},\n\t\t\texpected:     `.portForwards += [{\"guestPort\": \"80\", \"hostPort\": \"8080\", \"static\": true}]`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, err := BuildPortForwardExpression(tt.portForwards)\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Check(t, err != nil)\n\t\t\t} else {\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParsePortForward(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tspec        string\n\t\thostPort    string\n\t\tguestPort   string\n\t\tisStatic    bool\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:      \"dynamic port forward\",\n\t\t\tspec:      \"8080:80\",\n\t\t\thostPort:  \"8080\",\n\t\t\tguestPort: \"80\",\n\t\t\tisStatic:  false,\n\t\t},\n\t\t{\n\t\t\tname:      \"static port forward\",\n\t\t\tspec:      \"8080:80,static=true\",\n\t\t\thostPort:  \"8080\",\n\t\t\tguestPort: \"80\",\n\t\t\tisStatic:  true,\n\t\t},\n\t\t{\n\t\t\tname:      \"whitespace handling\",\n\t\t\tspec:      \" 8080 : 80 , static=true \",\n\t\t\thostPort:  \"8080\",\n\t\t\tguestPort: \"80\",\n\t\t\tisStatic:  true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid format - missing colon\",\n\t\t\tspec:        \"8080\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid format - too many colons\",\n\t\t\tspec:        \"8080:80:extra\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid parameter\",\n\t\t\tspec:        \"8080:80,invalid=true\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"too many parameters\",\n\t\t\tspec:        \"8080:80,static=true,extra=value\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\thostPort, guestPort, isStatic, err := ParsePortForward(tt.spec)\n\t\t\tif tt.expectError {\n\t\t\t\tassert.Check(t, err != nil)\n\t\t\t} else {\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.Equal(t, tt.hostPort, hostPort)\n\t\t\t\tassert.Equal(t, tt.guestPort, guestPort)\n\t\t\t\tassert.Equal(t, tt.isStatic, isStatic)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestYQExpressions(t *testing.T) {\n\texpand := func(s string) string {\n\t\ts, err := localpathutil.Expand(s)\n\t\tassert.NilError(t, err)\n\t\t// `D:\\foo` -> `D:\\\\foo` (appears in YAML)\n\t\ts = strings.ReplaceAll(s, \"\\\\\", \"\\\\\\\\\")\n\t\treturn s\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\targs        []string\n\t\tnewInstance bool\n\t\texpected    []string\n\t\texpectError string\n\t}{\n\t\t{\n\t\t\tname:        \"mount\",\n\t\t\targs:        []string{\"--mount\", \"/foo\", \"--mount\", \"./bar:w\"},\n\t\t\tnewInstance: false,\n\t\t\texpected:    []string{`.mounts = [{\"location\": \"` + expand(\"./bar\") + `\", \"mountPoint\": \"` + expand(\"./bar\") + `\", \"writable\": true},{\"location\": \"` + expand(\"/foo\") + `\", \"mountPoint\": \"` + expand(\"/foo\") + `\", \"writable\": false}] + .mounts`},\n\t\t},\n\t\t{\n\t\t\tname:        \"mount-only\",\n\t\t\targs:        []string{\"--mount-only\", \"/foo\", \"--mount-only\", \"/bar:w\"},\n\t\t\tnewInstance: false,\n\t\t\texpected:    []string{`.mounts = [{\"location\": \"` + expand(\"/foo\") + `\", \"mountPoint\": \"` + expand(\"/foo\") + `\", \"writable\": false},{\"location\": \"` + expand(\"/bar\") + `\", \"mountPoint\": \"` + expand(\"/bar\") + `\", \"writable\": true}]`},\n\t\t},\n\t\t{\n\t\t\tname:        \"mixture of mount and mount-only\",\n\t\t\targs:        []string{\"--mount\", \"/foo\", \"--mount-only\", \"/bar:w\"},\n\t\t\tnewInstance: false,\n\t\t\texpectError: \"flag `--mount` conflicts with `--mount-only`\",\n\t\t},\n\t\t{\n\t\t\tname:        \"dns\",\n\t\t\targs:        []string{\"--dns\", \"8.8.8.8\", \"--dns\", \"8.8.4.4\", \"--dns\", \"1.1.1.1\"},\n\t\t\tnewInstance: false,\n\t\t\texpected:    []string{`.dns += [\"8.8.8.8\",\"8.8.4.4\",\"1.1.1.1\"] | .dns |= unique | .hostResolver.enabled=false`},\n\t\t},\n\t\t{\n\t\t\tname:        \"network vzNAT\",\n\t\t\targs:        []string{\"--network\", \"vzNAT\"},\n\t\t\tnewInstance: true,\n\t\t\texpected:    []string{`.networks += [{\"vzNAT\": true}] | .networks |= unique_by(.lima)`},\n\t\t},\n\t\t{\n\t\t\tname:        \"network lima:shared\",\n\t\t\targs:        []string{\"--network\", \"lima:shared\"},\n\t\t\tnewInstance: true,\n\t\t\texpected:    []string{`.networks += [{\"lima\": \"shared\"}] | .networks |= unique_by(.lima)`},\n\t\t},\n\t\t{\n\t\t\tname:        \"multiple networks\",\n\t\t\targs:        []string{\"--network\", \"vzNAT\", \"--network\", \"lima:shared\", \"--network\", \"lima:bridged\"},\n\t\t\tnewInstance: true,\n\t\t\texpected:    []string{`.networks += [{\"vzNAT\": true},{\"lima\": \"shared\"},{\"lima\": \"bridged\"}] | .networks |= unique_by(.lima)`},\n\t\t},\n\t\t{\n\t\t\tname:        \"nested-virt\",\n\t\t\targs:        []string{\"--nested-virt\"},\n\t\t\tnewInstance: false,\n\t\t\texpected:    []string{`.nestedVirtualization = true`},\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid network\",\n\t\t\targs:        []string{\"--network\", \"invalid\"},\n\t\t\tnewInstance: true,\n\t\t\texpectError: `network name must be \"vzNAT\" or \"lima:*\", got \"invalid\"`,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcmd := &cobra.Command{}\n\t\t\tRegisterEdit(cmd, \"\")\n\t\t\tassert.NilError(t, cmd.ParseFlags(tt.args))\n\t\t\texpr, err := YQExpressions(cmd.Flags(), tt.newInstance)\n\t\t\tif tt.expectError != \"\" {\n\t\t\t\tassert.ErrorContains(t, err, tt.expectError)\n\t\t\t} else {\n\t\t\t\tassert.NilError(t, err)\n\t\t\t\tassert.DeepEqual(t, tt.expected, expr)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/limactl/factory-reset.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/cidata\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newFactoryResetCommand() *cobra.Command {\n\tresetCommand := &cobra.Command{\n\t\tUse:               \"factory-reset INSTANCE\",\n\t\tShort:             \"Factory reset an instance of Lima\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tRunE:              factoryResetAction,\n\t\tValidArgsFunction: factoryResetBashComplete,\n\t\tGroupID:           advancedCommand,\n\t}\n\treturn resetCommand\n}\n\nfunc factoryResetAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := DefaultInstanceName\n\tif len(args) > 0 {\n\t\tinstName = args[0]\n\t}\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tlogrus.Infof(\"Instance %q not found\", instName)\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\tif inst.Protected {\n\t\treturn errors.New(\"instance is protected to prohibit accidental factory-reset (Hint: use `limactl unprotect`)\")\n\t}\n\n\tinstance.StopForcibly(inst)\n\n\tfi, err := os.ReadDir(inst.Dir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tretain := map[string]struct{}{\n\t\tfilenames.LimaVersion:  {},\n\t\tfilenames.Protected:    {},\n\t\tfilenames.VzIdentifier: {},\n\t}\n\tfor _, f := range fi {\n\t\tpath := filepath.Join(inst.Dir, f.Name())\n\t\tif _, ok := retain[f.Name()]; !ok && !strings.HasSuffix(path, \".yaml\") && !strings.HasSuffix(path, \".yml\") {\n\t\t\tlogrus.Infof(\"Removing %q\", path)\n\t\t\tif err := os.Remove(path); err != nil {\n\t\t\t\tlogrus.Error(err)\n\t\t\t}\n\t\t}\n\t}\n\t// Regenerate the cloud-config.yaml, to reflect any changes to the global _config\n\tif err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil {\n\t\tlogrus.Error(err)\n\t}\n\n\tlogrus.Infof(\"Instance %q has been factory reset\", instName)\n\treturn nil\n}\n\nfunc factoryResetBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/gendoc.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/cpuguy83/go-md2man/v2/md2man\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/cobra/doc\"\n)\n\nfunc newGenDocCommand() *cobra.Command {\n\tgenmanCommand := &cobra.Command{\n\t\tUse:    \"generate-doc DIR\",\n\t\tShort:  \"Generate cli-reference pages\",\n\t\tArgs:   WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:   gendocAction,\n\t\tHidden: true,\n\t}\n\tgenmanCommand.Flags().String(\"type\", \"man\", \"Output type  (man, docsy)\")\n\tgenmanCommand.Flags().String(\"output\", \"\", \"Output directory\")\n\tgenmanCommand.Flags().String(\"prefix\", \"\", \"Install prefix\")\n\treturn genmanCommand\n}\n\nfunc gendocAction(cmd *cobra.Command, args []string) error {\n\toutput, err := cmd.Flags().GetString(\"output\")\n\tif err != nil {\n\t\treturn err\n\t}\n\toutput, err = filepath.Abs(output)\n\tif err != nil {\n\t\treturn err\n\t}\n\tprefix, err := cmd.Flags().GetString(\"prefix\")\n\tif err != nil {\n\t\treturn err\n\t}\n\toutputType, err := cmd.Flags().GetString(\"type\")\n\tif err != nil {\n\t\treturn err\n\t}\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdir := args[0]\n\tswitch outputType {\n\tcase \"man\":\n\t\tif err := genMan(cmd, dir); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"docsy\":\n\t\tif err := genDocsy(cmd, dir); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif output != \"\" && prefix != \"\" {\n\t\tif err := replaceAll(dir, output, prefix); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn replaceAll(dir, homeDir, \"~\")\n}\n\nfunc genMan(cmd *cobra.Command, dir string) error {\n\tlogrus.Infof(\"Generating man %q\", dir)\n\t// lima(1)\n\tfilePath := filepath.Join(dir, \"lima.1\")\n\tmd := \"LIMA 1\\n======\" + `\n# NAME\nlima - ` + cmd.Root().Short + `\n# SYNOPSIS\n**lima** [_COMMAND_...]\n# DESCRIPTION\nlima is an alias for \"limactl shell default\".\nThe instance name (\"default\") can be changed by specifying $LIMA_INSTANCE.\n\nThe shell and initial workdir inside the instance can be specified via $LIMA_SHELL\nand $LIMA_WORKDIR.\n# SEE ALSO\n**limactl**(1)\n`\n\tout := md2man.Render([]byte(md))\n\tif err := os.WriteFile(filePath, out, 0o644); err != nil {\n\t\treturn err\n\t}\n\t// limactl(1)\n\theader := &doc.GenManHeader{\n\t\tTitle:   \"LIMACTL\",\n\t\tSection: \"1\",\n\t}\n\treturn doc.GenManTree(cmd.Root(), header, dir)\n}\n\nfunc escapeMarkdown(text string) string {\n\tlines := strings.Split(text, \"\\n\")\n\tfor i := range lines {\n\t\t// Need to escape backticks first, before adding more\n\t\tfor c := range strings.SplitSeq(\"\\\\`*_[]()#+-.|\", \"\") {\n\t\t\tlines[i] = strings.ReplaceAll(lines[i], c, \"\\\\\"+c)\n\t\t}\n\t\tif i < len(lines)-1 {\n\t\t\tif lines[i] != \"\" && lines[i+1] != \"\" {\n\t\t\t\tlines[i] += \"  \" // line break\n\t\t\t}\n\t\t}\n\t}\n\treturn strings.Join(lines, \"\\n\")\n}\n\nfunc genDocsy(cmd *cobra.Command, dir string) error {\n\tfor _, c := range cmd.Root().Commands() {\n\t\tc.Long = escapeMarkdown(c.Long)\n\t}\n\treturn doc.GenMarkdownTreeCustom(cmd.Root(), dir, func(s string) string {\n\t\t// Replace limactl_completion_bash to completion bash for docsy title\n\t\tname := filepath.Base(s)\n\t\tname = strings.ReplaceAll(name, \"limactl_\", \"\")\n\t\tname = strings.ReplaceAll(name, \"_\", \" \")\n\t\tname = strings.TrimSuffix(name, filepath.Ext(name))\n\t\treturn fmt.Sprintf(`---\ntitle: %s\nweight: 3\n---\n`, name)\n\t}, func(s string) string {\n\t\t// Use ../ for move one folder up for docsy\n\t\treturn \"../\" + strings.TrimSuffix(s, filepath.Ext(s))\n\t})\n}\n\n// replaceAll replaces all occurrences of text with replacement, for all files in dir.\nfunc replaceAll(dir, text, replacement string) error {\n\tlogrus.Infof(\"Replacing %q with %q\", text, replacement)\n\treturn filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif path == dir {\n\t\t\treturn nil\n\t\t}\n\t\tif info.IsDir() {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t\tin, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tout := bytes.ReplaceAll(in, []byte(text), []byte(replacement))\n\t\terr = os.WriteFile(path, out, 0o644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "cmd/limactl/genschema.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/invopop/jsonschema\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\torderedmap \"github.com/wk8/go-ordered-map/v2\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/jsonschemautil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nfunc newGenSchemaCommand() *cobra.Command {\n\tgenschemaCommand := &cobra.Command{\n\t\tUse:    \"generate-jsonschema\",\n\t\tShort:  \"Generate json-schema document\",\n\t\tArgs:   WrapArgsError(cobra.ArbitraryArgs),\n\t\tRunE:   genschemaAction,\n\t\tHidden: true,\n\t}\n\tgenschemaCommand.Flags().String(\"schemafile\", \"\", \"Output file\")\n\treturn genschemaCommand\n}\n\nfunc toAny(args []string) []any {\n\tresult := []any{nil}\n\tfor _, arg := range args {\n\t\tresult = append(result, arg)\n\t}\n\treturn result\n}\n\nfunc getProp(props *orderedmap.OrderedMap[string, *jsonschema.Schema], key string) *jsonschema.Schema {\n\tvalue, ok := props.Get(key)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn value\n}\n\nfunc genschemaAction(cmd *cobra.Command, args []string) error {\n\tfile, err := cmd.Flags().GetString(\"schemafile\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tschema := jsonschema.Reflect(&limatype.LimaYAML{})\n\t// allow Disk to be either string (name) or object (struct)\n\tschema.Definitions[\"Disk\"].Type = \"\" // was: \"object\"\n\tschema.Definitions[\"Disk\"].OneOf = []*jsonschema.Schema{\n\t\t{Type: \"string\"},\n\t\t{Type: \"object\"},\n\t}\n\t// allow BaseTemplates to be either string (url) or array (array)\n\tschema.Definitions[\"BaseTemplates\"].Type = \"\" // was: \"array\"\n\tschema.Definitions[\"BaseTemplates\"].OneOf = []*jsonschema.Schema{\n\t\t{Type: \"string\"},\n\t\t{Type: \"array\"},\n\t}\n\t// allow LocatorWithDigest to be either string (url) or object (struct)\n\tschema.Definitions[\"LocatorWithDigest\"].Type = \"\" // was: \"object\"\n\tschema.Definitions[\"LocatorWithDigest\"].OneOf = []*jsonschema.Schema{\n\t\t{Type: \"string\"},\n\t\t{Type: \"object\"},\n\t}\n\tproperties := schema.Definitions[\"LimaYAML\"].Properties\n\tgetProp(properties, \"os\").Enum = toAny(limatype.OSTypes)\n\tgetProp(properties, \"arch\").Enum = toAny(limatype.ArchTypes)\n\tgetProp(properties, \"mountType\").Enum = toAny(limatype.MountTypes)\n\tgetProp(properties, \"vmType\").Enum = toAny(limatype.VMTypes)\n\tj, err := json.MarshalIndent(schema, \"\", \"    \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(args) == 0 {\n\t\t_, err = fmt.Fprintln(cmd.OutOrStdout(), string(j))\n\t\treturn err\n\t}\n\n\tif file == \"\" {\n\t\treturn errors.New(\"need --schemafile to validate\")\n\t}\n\terr = os.WriteFile(file, j, 0o644)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, f := range args {\n\t\terr = jsonschemautil.Validate(file, f)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"%q: %w\", f, err)\n\t\t}\n\t\tlogrus.Infof(\"%q: OK\", f)\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "cmd/limactl/guest-install.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/cacheutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/usrlocal\"\n)\n\nfunc newGuestInstallCommand() *cobra.Command {\n\tguestInstallCommand := &cobra.Command{\n\t\tUse:               \"guest-install INSTANCE\",\n\t\tShort:             \"Install guest components\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tRunE:              guestInstallAction,\n\t\tValidArgsFunction: cobra.NoFileCompletions,\n\t\tHidden:            true,\n\t}\n\treturn guestInstallCommand\n}\n\nfunc runCmd(ctx context.Context, name string, flags []string, args ...string) error {\n\tcmd := exec.CommandContext(ctx, name, append(flags, args...)...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tlogrus.Debugf(\"executing %v\", cmd.Args)\n\treturn cmd.Run()\n}\n\nfunc shell(ctx context.Context, name string, flags []string, args ...string) (string, error) {\n\tcmd := exec.CommandContext(ctx, name, append(flags, args...)...)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tout = bytes.TrimSuffix(out, []byte{'\\n'})\n\treturn string(out), nil\n}\n\nfunc guestInstallAction(cmd *cobra.Command, args []string) error {\n\tinstName := DefaultInstanceName\n\tif len(args) > 0 {\n\t\tinstName = args[0]\n\t}\n\n\tinst, err := store.Inspect(cmd.Context(), instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif inst.Status == limatype.StatusStopped {\n\t\treturn fmt.Errorf(\"instance %q is stopped, run `limactl start %s` to start the instance\", instName, instName)\n\t}\n\n\tctx := cmd.Context()\n\n\tsshExe := \"ssh\"\n\tsshConfig := filepath.Join(inst.Dir, filenames.SSHConfig)\n\tsshFlags := []string{\"-F\", sshConfig}\n\n\tscpExe := \"scp\"\n\tscpFlags := sshFlags\n\n\thostname := fmt.Sprintf(\"lima-%s\", inst.Name)\n\tprefix := *inst.Config.GuestInstallPrefix\n\n\t// lima-guestagent\n\tguestAgentBinary, err := usrlocal.GuestAgentBinary(*inst.Config.OS, *inst.Config.Arch)\n\tif err != nil {\n\t\treturn err\n\t}\n\tguestAgentFilename := filepath.Base(guestAgentBinary)\n\tif filepath.Ext(guestAgentBinary) == \".gz\" {\n\t\tcompressedGuestAgent, err := os.Open(guestAgentBinary)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer compressedGuestAgent.Close()\n\t\ttmpGuestAgent, err := os.CreateTemp(\"\", \"lima-guestagent-\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlogrus.Debugf(\"Decompressing %s\", guestAgentBinary)\n\t\tguestAgent, err := gzip.NewReader(compressedGuestAgent)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer guestAgent.Close()\n\t\t_, err = io.Copy(tmpGuestAgent, guestAgent)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttmpGuestAgent.Close()\n\t\tguestAgentBinary = tmpGuestAgent.Name()\n\t\tdefer os.RemoveAll(guestAgentBinary)\n\t\tguestAgentFilename = strings.TrimSuffix(guestAgentFilename, \".gz\")\n\t}\n\ttmpname := \"lima-guestagent\"\n\ttmp, err := shell(ctx, sshExe, sshFlags, hostname, \"mktemp\", \"-t\", \"lima-guestagent.XXXXXX\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tbin := prefix + \"/bin/lima-guestagent\"\n\tlogrus.Infof(\"Copying %q to %s:%s\", guestAgentFilename, inst.Name, tmpname)\n\tscpArgs := []string{guestAgentBinary, hostname + \":\" + tmp}\n\tif err := runCmd(ctx, scpExe, scpFlags, scpArgs...); err != nil {\n\t\treturn nil\n\t}\n\tlogrus.Infof(\"Installing %s to %s\", tmpname, bin)\n\tsshArgs := []string{hostname, \"sudo\", \"install\", \"-m\", \"755\", tmp, bin}\n\tif err := runCmd(ctx, sshExe, sshFlags, sshArgs...); err != nil {\n\t\treturn nil\n\t}\n\t_, _ = shell(ctx, sshExe, sshFlags, hostname, \"rm\", tmp)\n\n\t// nerdctl-full.tgz\n\tnerdctlFilename := cacheutil.NerdctlArchive(inst.Config)\n\tif nerdctlFilename != \"\" {\n\t\tnerdctlArchive, err := cacheutil.EnsureNerdctlArchiveCache(cmd.Context(), inst.Config, false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttmpname := \"nerdctl-full.tgz\"\n\t\ttmp, err := shell(ctx, sshExe, sshFlags, hostname, \"mktemp\", \"-t\", \"nerdctl-full.XXXXXX.tgz\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlogrus.Infof(\"Copying %q to %s:%s\", nerdctlFilename, inst.Name, tmpname)\n\t\tscpArgs := []string{nerdctlArchive, hostname + \":\" + tmp}\n\t\tif err := runCmd(ctx, scpExe, scpFlags, scpArgs...); err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tlogrus.Infof(\"Installing %s in %s\", tmpname, prefix)\n\t\tsshArgs := []string{hostname, \"sudo\", \"tar\", \"Cxzf\", prefix, tmp}\n\t\tif err := runCmd(ctx, sshExe, sshFlags, sshArgs...); err != nil {\n\t\t\treturn nil\n\t\t}\n\t\t_, _ = shell(ctx, sshExe, sshFlags, hostname, \"rm\", tmp)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/limactl/hostagent.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"syscall\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent\"\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/api/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newHostagentCommand() *cobra.Command {\n\thostagentCommand := &cobra.Command{\n\t\tUse:    \"hostagent INSTANCE\",\n\t\tShort:  \"Run hostagent\",\n\t\tArgs:   WrapArgsError(cobra.ExactArgs(1)),\n\t\tRunE:   hostagentAction,\n\t\tHidden: true,\n\t}\n\thostagentCommand.Flags().StringP(\"pidfile\", \"p\", \"\", \"Write PID to file\")\n\thostagentCommand.Flags().String(\"socket\", \"\", \"Path of hostagent socket\")\n\thostagentCommand.Flags().Bool(\"run-gui\", false, \"Run GUI synchronously within hostagent\")\n\thostagentCommand.Flags().String(\"guestagent\", \"\", \"Local file path (not URL) of lima-guestagent.OS-ARCH[.gz]\")\n\thostagentCommand.Flags().String(\"nerdctl-archive\", \"\", \"Local file path (not URL) of nerdctl-full-VERSION-GOOS-GOARCH.tar.gz\")\n\thostagentCommand.Flags().Bool(\"progress\", false, \"Show provision script progress by monitoring cloud-init logs\")\n\treturn hostagentCommand\n}\n\nfunc hostagentAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tpidfile, err := cmd.Flags().GetString(\"pidfile\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif pidfile != \"\" {\n\t\tif existingPID, err := store.ReadPIDFile(pidfile); existingPID != 0 {\n\t\t\treturn fmt.Errorf(\"another hostagent may already be running with pid %d (pidfile %q)\", existingPID, pidfile)\n\t\t} else if err != nil {\n\t\t\treturn fmt.Errorf(\"failed to determine if another hostagent is running: %w\", err)\n\t\t}\n\t\tif err := os.WriteFile(pidfile, []byte(strconv.Itoa(os.Getpid())+\"\\n\"), 0o644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer os.RemoveAll(pidfile)\n\t}\n\tsocket, err := cmd.Flags().GetString(\"socket\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif socket == \"\" {\n\t\treturn errors.New(\"socket must be specified (limactl version mismatch?)\")\n\t}\n\n\tinstName := args[0]\n\n\trunGUI, err := cmd.Flags().GetBool(\"run-gui\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif runGUI {\n\t\t// Without this the call to vz.RunGUI fails. Adding it here, as this has to be called before the vz cgo loads.\n\t\truntime.LockOSThread()\n\t\tdefer runtime.UnlockOSThread()\n\t}\n\n\tsignalCh := make(chan os.Signal, 1)\n\tsignal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)\n\n\tstdout := &syncWriter{w: cmd.OutOrStdout()}\n\tstderr := &syncWriter{w: cmd.ErrOrStderr()}\n\n\tinitLogrus(stderr)\n\tvar opts []hostagent.Opt\n\tguestagentBinary, err := cmd.Flags().GetString(\"guestagent\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif guestagentBinary != \"\" {\n\t\topts = append(opts, hostagent.WithGuestAgentBinary(guestagentBinary))\n\t}\n\tnerdctlArchive, err := cmd.Flags().GetString(\"nerdctl-archive\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif nerdctlArchive != \"\" {\n\t\topts = append(opts, hostagent.WithNerdctlArchive(nerdctlArchive))\n\t}\n\tshowProgress, err := cmd.Flags().GetBool(\"progress\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif showProgress {\n\t\topts = append(opts, hostagent.WithCloudInitProgress(showProgress))\n\t}\n\tha, err := hostagent.New(ctx, instName, stdout, signalCh, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbackend := &server.Backend{\n\t\tAgent: ha,\n\t}\n\tr := http.NewServeMux()\n\tserver.AddRoutes(r, backend)\n\tsrv := &http.Server{Handler: r}\n\terr = os.RemoveAll(socket)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar lc net.ListenConfig\n\tl, err := lc.Listen(ctx, \"unix\", socket)\n\tlogrus.Infof(\"hostagent socket created at %s\", socket)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo func() {\n\t\tif serveErr := srv.Serve(l); serveErr != http.ErrServerClosed {\n\t\t\tlogrus.WithError(serveErr).Warn(\"hostagent API server exited with an error\")\n\t\t}\n\t}()\n\tdefer srv.Close()\n\treturn ha.Run(cmd.Context())\n}\n\n// syncer is implemented by *os.File.\ntype syncer interface {\n\tSync() error\n}\n\ntype syncWriter struct {\n\tw io.Writer\n}\n\nfunc (w *syncWriter) Write(p []byte) (int, error) {\n\twritten, err := w.w.Write(p)\n\tif err == nil {\n\t\tif s, ok := w.w.(syncer); ok {\n\t\t\t_ = s.Sync()\n\t\t}\n\t}\n\treturn written, err\n}\n\nfunc initLogrus(stderr io.Writer) {\n\tlogrus.SetOutput(stderr)\n\t// JSON logs are parsed in pkg/hostagent/events.Watcher()\n\tlogrus.SetFormatter(new(logrus.JSONFormatter))\n\t// HostAgent logging is one level more verbose than the start command itself\n\tif logrus.GetLevel() == logrus.DebugLevel {\n\t\tlogrus.SetLevel(logrus.TraceLevel)\n\t} else {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t}\n}\n"
  },
  {
    "path": "cmd/limactl/info.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/mikefarah/yq/v4/pkg/yqlib\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limainfo\"\n\t\"github.com/lima-vm/lima/v2/pkg/uiutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nfunc newInfoCommand() *cobra.Command {\n\tinfoCommand := &cobra.Command{\n\t\tUse:     \"info\",\n\t\tShort:   \"Show diagnostic information\",\n\t\tArgs:    WrapArgsError(cobra.NoArgs),\n\t\tRunE:    infoAction,\n\t\tGroupID: advancedCommand,\n\t}\n\tinfoCommand.Flags().String(\"yq\", \".\", \"Apply yq expression to output\")\n\n\treturn infoCommand\n}\n\nfunc infoAction(cmd *cobra.Command, _ []string) error {\n\tctx := cmd.Context()\n\n\tyq, err := cmd.Flags().GetString(\"yq\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinfo, err := limainfo.New(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tj, err := json.MarshalIndent(info, \"\", \"    \")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tencoderPrefs := yqlib.ConfiguredJSONPreferences.Copy()\n\tencoderPrefs.Indent = 4\n\tencoderPrefs.ColorsEnabled = uiutil.OutputIsTTY(cmd.OutOrStdout())\n\tencoder := yqlib.NewJSONEncoder(encoderPrefs)\n\tstr, err := yqutil.EvaluateExpressionWithEncoder(yq, string(j), encoder)\n\tif err == nil {\n\t\t_, err = fmt.Fprint(cmd.OutOrStdout(), str)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "cmd/limactl/list.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/cheggaaa/pb/v3/termutil\"\n\t\"github.com/mikefarah/yq/v4/pkg/yqlib\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/uiutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nfunc fieldNames() []string {\n\tnames := []string{}\n\tt := reflect.TypeFor[store.FormatData]()\n\tfor i := range t.NumField() {\n\t\tf := t.Field(i)\n\t\tif f.Anonymous {\n\t\t\tfor j := range f.Type.NumField() {\n\t\t\t\tif tag := f.Tag.Get(\"lima\"); tag != \"deprecated\" {\n\t\t\t\t\tnames = append(names, f.Type.Field(j).Name)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif tag := f.Tag.Get(\"lima\"); tag != \"deprecated\" {\n\t\t\t\tnames = append(names, t.Field(i).Name)\n\t\t\t}\n\t\t}\n\t}\n\treturn names\n}\n\nfunc newListCommand() *cobra.Command {\n\tlistCommand := &cobra.Command{\n\t\tUse:     \"list [flags] [INSTANCE]...\",\n\t\tAliases: []string{\"ls\"},\n\t\tShort:   \"List instances of Lima\",\n\t\tLong: `List instances of Lima.\n\nThe output can be presented in one of several formats, using the --format <format> flag.\n\n  --format json  - Output in JSON format\n  --format yaml  - Output in YAML format\n  --format table - Output in table format\n  --format '{{ <go template> }}' - If the format begins and ends with '{{ }}', then it is used as a go template.\n\nFiltering instances:\n  --filter EXPR  - Filter instances using yq expression (this is equivalent to --yq 'select(EXPR)')\n                   Can be specified multiple times (combined with AND logic) and it works with all output formats.\n                   Examples:\n                     --filter '.status == \"Running\"'\n                     --filter '.vmType == \"vz\"'\n                     --filter '.status == \"Running\"' --filter '.vmType == \"vz\"' (Same as AND)\n` + store.FormatHelp + `\nThe following legacy flags continue to function:\n  --json - equal to '--format json'`,\n\t\tArgs:              WrapArgsError(cobra.ArbitraryArgs),\n\t\tRunE:              listAction,\n\t\tValidArgsFunction: listBashComplete,\n\t\tGroupID:           basicCommand,\n\t}\n\n\tlistCommand.Flags().StringP(\"format\", \"f\", \"table\", \"Output format, one of: json, yaml, table, go-template\")\n\tlistCommand.Flags().Bool(\"list-fields\", false, \"List fields available for format\")\n\tlistCommand.Flags().Bool(\"json\", false, \"Same as --format=json\")\n\tlistCommand.Flags().BoolP(\"quiet\", \"q\", false, \"Only show names\")\n\tlistCommand.Flags().Bool(\"all-fields\", false, \"Show all fields\")\n\tlistCommand.Flags().StringArray(\"yq\", nil, \"Apply yq expression to each instance\")\n\tlistCommand.Flags().StringArrayP(\"filter\", \"l\", nil, \"Filter instances using yq expression (equivalent to --yq 'select(EXPR)')\")\n\n\treturn listCommand\n}\n\nfunc instanceMatches(arg string, instances []string) []string {\n\tmatches := []string{}\n\tfor _, instance := range instances {\n\t\tif instance == arg {\n\t\t\tmatches = append(matches, instance)\n\t\t}\n\t}\n\treturn matches\n}\n\n// unmatchedInstancesError is created when unmatched instance names found.\ntype unmatchedInstancesError struct{}\n\n// Error implements error.\nfunc (unmatchedInstancesError) Error() string {\n\treturn \"unmatched instances\"\n}\n\n// ExitCode implements ExitCoder.\nfunc (unmatchedInstancesError) ExitCode() int {\n\treturn 1\n}\n\nfunc listAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tquiet, err := cmd.Flags().GetBool(\"quiet\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tformat, err := cmd.Flags().GetString(\"format\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tlistFields, err := cmd.Flags().GetBool(\"list-fields\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tjsonFormat, err := cmd.Flags().GetBool(\"json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tyq, err := cmd.Flags().GetStringArray(\"yq\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tfilter, err := cmd.Flags().GetStringArray(\"filter\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif jsonFormat {\n\t\tformat = \"json\"\n\t}\n\n\t// conflicts\n\tif jsonFormat && cmd.Flags().Changed(\"format\") {\n\t\treturn errors.New(\"option --json conflicts with option --format\")\n\t}\n\tif listFields && cmd.Flags().Changed(\"format\") {\n\t\treturn errors.New(\"option --list-fields conflicts with option --format\")\n\t}\n\tif len(yq) != 0 {\n\t\tif cmd.Flags().Changed(\"format\") && format != \"json\" && format != \"yaml\" {\n\t\t\treturn errors.New(\"option --yq only works with --format json or yaml\")\n\t\t}\n\t\tif listFields {\n\t\t\treturn errors.New(\"option --list-fields conflicts with option --yq\")\n\t\t}\n\t}\n\tif len(filter) != 0 {\n\t\tif listFields {\n\t\t\treturn errors.New(\"option --list-fields conflicts with option --filter\")\n\t\t}\n\t}\n\n\tif quiet && format != \"table\" {\n\t\treturn errors.New(\"option --quiet can only be used with '--format table'\")\n\t}\n\n\tif listFields {\n\t\tnames := fieldNames()\n\t\tslices.Sort(names)\n\t\tfmt.Fprintln(cmd.OutOrStdout(), strings.Join(names, \"\\n\"))\n\t\treturn nil\n\t}\n\n\tif err := store.Validate(); err != nil {\n\t\tlogrus.Warnf(\"The directory %q does not look like a valid Lima directory: %v\", store.Directory(), err)\n\t}\n\n\tallInstances, err := store.Instances()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(args) == 0 && len(allInstances) == 0 {\n\t\tlogrus.Warn(\"No instance found. Run `limactl create` to create an instance.\")\n\t\treturn nil\n\t}\n\n\tinstanceNames := []string{}\n\tunmatchedInstances := false\n\tif len(args) > 0 {\n\t\tfor _, arg := range args {\n\t\t\tmatches := instanceMatches(arg, allInstances)\n\t\t\tif len(matches) > 0 {\n\t\t\t\tinstanceNames = append(instanceNames, matches...)\n\t\t\t} else {\n\t\t\t\tlogrus.Warnf(\"No instance matching %v found.\", arg)\n\t\t\t\tunmatchedInstances = true\n\t\t\t}\n\t\t}\n\t} else {\n\t\tinstanceNames = allInstances\n\t}\n\n\tif quiet && len(yq) == 0 && len(filter) == 0 {\n\t\tfor _, instName := range instanceNames {\n\t\t\tfmt.Fprintln(cmd.OutOrStdout(), instName)\n\t\t}\n\t\tif unmatchedInstances {\n\t\t\treturn unmatchedInstancesError{}\n\t\t}\n\t\treturn nil\n\t}\n\n\t// get the state and config for all the requested instances\n\tvar instances []*limatype.Instance\n\tfor _, instanceName := range instanceNames {\n\t\tinstance, err := store.Inspect(ctx, instanceName)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to load instance %s: %w\", instanceName, err)\n\t\t}\n\t\tinstances = append(instances, instance)\n\t}\n\n\tif len(filter) > 0 {\n\t\tvar filterExprs []string\n\t\tfor _, f := range filter {\n\t\t\tfilterExprs = append(filterExprs, \"select(\"+f+\")\")\n\t\t}\n\t\tinstances, err = filterInstances(instances, filterExprs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif quiet && len(yq) == 0 {\n\t\tfor _, instance := range instances {\n\t\t\tfmt.Fprintln(cmd.OutOrStdout(), instance.Name)\n\t\t}\n\t\tif unmatchedInstances {\n\t\t\treturn unmatchedInstancesError{}\n\t\t}\n\t\treturn nil\n\t}\n\n\tfor _, instance := range instances {\n\t\tif len(instance.Errors) > 0 {\n\t\t\tlogrus.WithField(\"errors\", instance.Errors).Warnf(\"instance %q has errors\", instance.Name)\n\t\t}\n\t}\n\n\tallFields, err := cmd.Flags().GetBool(\"all-fields\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\toptions := store.PrintOptions{AllFields: allFields}\n\tisTTY := uiutil.OutputIsTTY(cmd.OutOrStdout())\n\tif isTTY {\n\t\tif w, err := termutil.TerminalWidth(); err == nil {\n\t\t\toptions.TerminalWidth = w\n\t\t}\n\t}\n\n\t// --yq implies --format json unless --format has been explicitly specified\n\tif len(yq) != 0 && !cmd.Flags().Changed(\"format\") {\n\t\tformat = \"json\"\n\t}\n\n\t// Always pipe JSON and YAML through yq to colorize it if isTTY\n\tif len(yq) == 0 && (format == \"json\" || format == \"yaml\") {\n\t\tyq = append(yq, \".\")\n\t}\n\n\tif len(yq) == 0 {\n\t\terr = store.PrintInstances(cmd.OutOrStdout(), instances, format, &options)\n\t\tif err == nil && unmatchedInstances {\n\t\t\treturn unmatchedInstancesError{}\n\t\t}\n\t\treturn err\n\t}\n\n\tif quiet {\n\t\tyq = append(yq, \".name\")\n\t}\n\tyqExpr := strings.Join(yq, \" | \")\n\n\tbuf := new(bytes.Buffer)\n\terr = store.PrintInstances(buf, instances, format, &options)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif format == \"json\" {\n\t\t// The JSON encoder will create empty objects (YAML maps), even when they have the \",omitempty\" tag.\n\t\tdeleteEmptyObjects := `del(.. | select(tag == \"!!map\" and length == 0))`\n\t\tyqExpr += \" | \" + deleteEmptyObjects\n\n\t\tencoderPrefs := yqlib.ConfiguredJSONPreferences.Copy()\n\t\tencoderPrefs.ColorsEnabled = false\n\t\tencoderPrefs.Indent = 0\n\t\tplainEncoder := yqlib.NewJSONEncoder(encoderPrefs)\n\t\t// Using non-0 indent means the instance will be printed over multiple lines,\n\t\t// so is no longer in JSON Lines format. This is a compromise for readability.\n\t\tencoderPrefs.Indent = 4\n\t\tencoderPrefs.ColorsEnabled = true\n\t\tcolorEncoder := yqlib.NewJSONEncoder(encoderPrefs)\n\n\t\t// Each line contains the JSON object for one Lima instance.\n\t\tscanner := bufio.NewScanner(buf)\n\t\tfor scanner.Scan() {\n\t\t\tvar str string\n\t\t\tif str, err = yqutil.EvaluateExpressionWithEncoder(yqExpr, scanner.Text(), plainEncoder); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// Repeatedly delete empty objects until there are none left.\n\t\t\tfor {\n\t\t\t\tlength := len(str)\n\t\t\t\tif str, err = yqutil.EvaluateExpressionWithEncoder(deleteEmptyObjects, str, plainEncoder); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif len(str) >= length {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif isTTY {\n\t\t\t\t// pretty-print and colorize the output\n\t\t\t\tif str, err = yqutil.EvaluateExpressionWithEncoder(\".\", str, colorEncoder); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tif _, err = fmt.Fprint(cmd.OutOrStdout(), str); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\terr = scanner.Err()\n\t\tif err == nil && unmatchedInstances {\n\t\t\treturn unmatchedInstancesError{}\n\t\t}\n\t\treturn err\n\t}\n\n\tvar str string\n\tif isTTY {\n\t\t// This branch is trading the better formatting from yamlfmt for colorizing from yqlib.\n\t\tif str, err = yqutil.EvaluateExpressionPlain(yqExpr, buf.String(), true); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tvar res []byte\n\t\tif res, err = yqutil.EvaluateExpression(yqExpr, buf.Bytes()); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tstr = string(res)\n\t}\n\t_, err = fmt.Fprint(cmd.OutOrStdout(), str)\n\tif err == nil && unmatchedInstances {\n\t\treturn unmatchedInstancesError{}\n\t}\n\treturn err\n}\n\nfunc listBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n\n// filterInstances applies yq expressions to instances and returns the filtered results.\nfunc filterInstances(instances []*limatype.Instance, yqExprs []string) ([]*limatype.Instance, error) {\n\tif len(yqExprs) == 0 {\n\t\treturn instances, nil\n\t}\n\n\t// the yq expression is evaluated with yqutil.EvaluateExpression, which disables environment variable access\n\t// and file operations, mitigating injection attacks like \".name=strenv(SOME_SECRET_ENV)\" which could\n\t// trick Lima into exposing environment variables.\n\tyqExpr := strings.Join(yqExprs, \" | \")\n\n\tvar filteredInstances []*limatype.Instance\n\tfor _, instance := range instances {\n\t\tjsonBytes, err := json.Marshal(instance)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to marshal instance %q: %w\", instance.Name, err)\n\t\t}\n\n\t\tresult, err := yqutil.EvaluateExpression(yqExpr, jsonBytes)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to apply filter %q: %w\", yqExpr, err)\n\t\t}\n\n\t\tif len(bytes.TrimSpace(result)) > 0 {\n\t\t\tfilteredInstances = append(filteredInstances, instance)\n\t\t}\n\t}\n\n\treturn filteredInstances, nil\n}\n"
  },
  {
    "path": "cmd/limactl/main.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/cmd/yq\"\n\t\"github.com/lima-vm/lima/v2/pkg/debugutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/fsutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/plugins\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n)\n\nconst (\n\tDefaultInstanceName = \"default\"\n\tbasicCommand        = \"basic\"\n\tadvancedCommand     = \"advanced\"\n)\n\nfunc main() {\n\tif os.Geteuid() == 0 && (len(os.Args) < 2 || os.Args[1] != \"generate-doc\") {\n\t\tfmt.Fprint(os.Stderr, \"limactl: must not run as the root user\\n\")\n\t\tos.Exit(1)\n\t}\n\n\tyq.MaybeRunYQ()\n\tif runtime.GOOS == \"windows\" {\n\t\textras, hasExtra := os.LookupEnv(\"_LIMA_WINDOWS_EXTRA_PATH\")\n\t\tif hasExtra && strings.TrimSpace(extras) != \"\" {\n\t\t\tp := os.Getenv(\"PATH\")\n\t\t\terr := os.Setenv(\"PATH\", strings.TrimSpace(extras)+string(filepath.ListSeparator)+p)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Warning(\"Can't add extras to PATH, relying entirely on system PATH\")\n\t\t\t}\n\t\t}\n\t}\n\terr := newApp().Execute()\n\tserver.StopAllExternalDrivers()\n\tosutil.HandleExitError(err)\n\tif err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n}\n\nfunc processGlobalFlags(rootCmd *cobra.Command) error {\n\t// --log-level will override --debug, but will not reset debugutil.Debug\n\tif debug, _ := rootCmd.Flags().GetBool(\"debug\"); debug {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t\tdebugutil.Debug = true\n\t}\n\n\tl, _ := rootCmd.Flags().GetString(\"log-level\")\n\tif l != \"\" {\n\t\tlvl, err := logrus.ParseLevel(l)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlogrus.SetLevel(lvl)\n\t}\n\n\tlogFormat, _ := rootCmd.Flags().GetString(\"log-format\")\n\tswitch logFormat {\n\tcase \"json\":\n\t\tformatter := new(logrus.JSONFormatter)\n\t\tlogrus.StandardLogger().SetFormatter(formatter)\n\tcase \"text\":\n\t\t// logrus use text format by default.\n\t\tif runtime.GOOS == \"windows\" && isatty.IsCygwinTerminal(os.Stderr.Fd()) {\n\t\t\tformatter := new(logrus.TextFormatter)\n\t\t\t// the default setting does not recognize cygwin on windows\n\t\t\tformatter.ForceColors = true\n\t\t\tlogrus.StandardLogger().SetFormatter(formatter)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported log-format: %q\", logFormat)\n\t}\n\treturn nil\n}\n\nfunc newApp() *cobra.Command {\n\ttemplatesDir := \"$PREFIX/share/lima/templates\"\n\tif exe, err := os.Executable(); err == nil {\n\t\tbinDir := filepath.Dir(exe)\n\t\tprefixDir := filepath.Dir(binDir)\n\t\ttemplatesDir = filepath.Join(prefixDir, \"share/lima/templates\")\n\t}\n\n\trootCmd := &cobra.Command{\n\t\tUse:     \"limactl\",\n\t\tShort:   \"Lima: Linux virtual machines\",\n\t\tVersion: strings.TrimPrefix(version.Version, \"v\"),\n\t\tExample: fmt.Sprintf(`  Start the default instance:\n  $ limactl start\n\n  Open a shell:\n  $ lima\n\n  Run a container:\n  $ lima nerdctl run -d --name nginx -p 8080:80 nginx:alpine\n\n  Stop the default instance:\n  $ limactl stop\n\n  See also template YAMLs: %s`, templatesDir),\n\t\tSilenceUsage:      true,\n\t\tSilenceErrors:     true,\n\t\tDisableAutoGenTag: true,\n\t}\n\trootCmd.PersistentFlags().String(\"log-level\", \"\", \"Set the logging level [trace, debug, info, warn, error]\")\n\trootCmd.PersistentFlags().String(\"log-format\", \"text\", \"Set the logging format [text, json]\")\n\trootCmd.PersistentFlags().Bool(\"debug\", false, \"Debug mode\")\n\t// TODO: \"survey\" does not support using cygwin terminal on windows yet\n\trootCmd.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.\")\n\trootCmd.PersistentFlags().BoolP(\"yes\", \"y\", false, \"Alias of --tty=false\")\n\trootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {\n\t\tif err := processGlobalFlags(rootCmd); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif osutil.IsBeingRosettaTranslated() && cmd.Parent().Name() != \"completion\" && cmd.Name() != \"generate-doc\" && cmd.Name() != \"validate\" {\n\t\t\t// running under rosetta would provide inappropriate runtime.GOARCH info, see: https://github.com/lima-vm/lima/issues/543\n\t\t\t// allow commands that are used for packaging to run under rosetta to allow cross-architecture builds\n\t\t\treturn errors.New(\"limactl is running under rosetta, please reinstall lima with native arch\")\n\t\t}\n\n\t\t// Make sure either $HOME or $LIMA_HOME is defined, so we don't need\n\t\t// to check for errors later\n\t\tdir, err := dirnames.LimaDir()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnfs, err := fsutil.IsNFS(dir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif nfs {\n\t\t\tlogrus.Warn(\"LIMA_HOME should not be on an NFS mount\")\n\t\t}\n\n\t\tif cmd.Flags().Changed(\"yes\") && cmd.Flags().Changed(\"tty\") {\n\t\t\treturn errors.New(\"cannot use both --tty and --yes flags at the same time\")\n\t\t}\n\n\t\tif cmd.Flags().Changed(\"yes\") {\n\t\t\tswitch cmd.Name() {\n\t\t\tcase \"clone\", \"edit\", \"rename\":\n\t\t\t\tlogrus.Warn(\"--yes flag is deprecated (--tty=false is still supported and works in the same way. Also consider using --start)\")\n\t\t\t}\n\n\t\t\t// Sets the value of the yesValue flag by using the yes flag.\n\t\t\tyesValue, _ := cmd.Flags().GetBool(\"yes\")\n\t\t\tif yesValue {\n\t\t\t\t// Sets to the default value false\n\t\t\t\terr := cmd.Flags().Set(\"tty\", \"false\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\trootCmd.AddGroup(&cobra.Group{ID: \"basic\", Title: \"Basic Commands:\"})\n\trootCmd.AddGroup(&cobra.Group{ID: \"advanced\", Title: \"Advanced Commands:\"})\n\trootCmd.AddGroup(&cobra.Group{ID: \"plugin\", Title: \"Available Plugins (Experimental):\"})\n\n\trootCmd.AddCommand(\n\t\tnewCreateCommand(),\n\t\tnewStartCommand(),\n\t\tnewStopCommand(),\n\t\tnewShellCommand(),\n\t\tnewCopyCommand(),\n\t\tnewListCommand(),\n\t\tnewDeleteCommand(),\n\t\tnewValidateCommand(),\n\t\tnewPruneCommand(),\n\t\tnewHostagentCommand(),\n\t\tnewGuestInstallCommand(),\n\t\tnewInfoCommand(),\n\t\tnewShowSSHCommand(),\n\t\tnewDebugCommand(),\n\t\tnewEditCommand(),\n\t\tnewFactoryResetCommand(),\n\t\tnewDiskCommand(),\n\t\tnewUsernetCommand(),\n\t\tnewGenDocCommand(),\n\t\tnewGenSchemaCommand(),\n\t\tnewSnapshotCommand(),\n\t\tnewProtectCommand(),\n\t\tnewUnprotectCommand(),\n\t\tnewTunnelCommand(),\n\t\tnewTemplateCommand(),\n\t\tnewRestartCommand(),\n\t\tnewSudoersCommand(),\n\t\tnewStartAtLoginCommand(),\n\t\tnewNetworkCommand(),\n\t\tnewCloneCommand(),\n\t\tnewRenameCommand(),\n\t\tnewWatchCommand(),\n\t)\n\taddPluginCommands(rootCmd)\n\n\treturn rootCmd\n}\n\nfunc addPluginCommands(rootCmd *cobra.Command) {\n\t// The global options are only processed when rootCmd.Execute() is called.\n\t// Let's take a sneak peek here to help debug the plugin discovery code.\n\tif len(os.Args) > 1 && os.Args[1] == \"--debug\" {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t}\n\n\tallPlugins, err := plugins.Discover()\n\tif err != nil {\n\t\tlogrus.Warnf(\"Failed to discover plugins: %v\", err)\n\t\treturn\n\t}\n\n\tfor _, plugin := range allPlugins {\n\t\tpluginCmd := &cobra.Command{\n\t\t\tUse:                plugin.Name,\n\t\t\tShort:              plugin.Description,\n\t\t\tGroupID:            \"plugin\",\n\t\t\tDisableFlagParsing: true,\n\t\t\tSilenceErrors:      true,\n\t\t\tSilenceUsage:       true,\n\t\t\tPreRunE: func(*cobra.Command, []string) error {\n\t\t\t\tfor i, arg := range os.Args {\n\t\t\t\t\tif arg == plugin.Name {\n\t\t\t\t\t\t// parse global options but ignore plugin options\n\t\t\t\t\t\terr := rootCmd.ParseFlags(os.Args[1:i])\n\t\t\t\t\t\tif err == nil {\n\t\t\t\t\t\t\terr = processGlobalFlags(rootCmd)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// unreachable\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tRun: func(cmd *cobra.Command, _ []string) {\n\t\t\t\tfor i, arg := range os.Args {\n\t\t\t\t\tif arg == plugin.Name {\n\t\t\t\t\t\t// ignore global options\n\t\t\t\t\t\tplugin.Run(cmd.Context(), os.Args[i+1:])\n\t\t\t\t\t\t// plugin.Run() never returns because it calls os.Exit()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// unreachable\n\t\t\t},\n\t\t}\n\t\t// Don't show the url scheme helper in the help output.\n\t\tif strings.HasPrefix(plugin.Name, \"url-\") {\n\t\t\tpluginCmd.Hidden = true\n\t\t}\n\t\trootCmd.AddCommand(pluginCmd)\n\t}\n}\n\n// WrapArgsError annotates cobra args error with some context, so the error message is more user-friendly.\nfunc WrapArgsError(argFn cobra.PositionalArgs) cobra.PositionalArgs {\n\treturn func(cmd *cobra.Command, args []string) error {\n\t\terr := argFn(cmd, args)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn fmt.Errorf(\"%q %s.\\nSee '%s --help'.\\n\\nUsage:  %s\\n\\n%s\",\n\t\t\tcmd.CommandPath(), err.Error(),\n\t\t\tcmd.CommandPath(),\n\t\t\tcmd.UseLine(), cmd.Short,\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "cmd/limactl/main_darwin.go",
    "content": "//go:build !external_vz && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\n// Import vz driver to register it in the registry on darwin.\nimport _ \"github.com/lima-vm/lima/v2/pkg/driver/vz\"\n"
  },
  {
    "path": "cmd/limactl/main_qemu.go",
    "content": "//go:build !external_qemu\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\n// Import qemu driver to register it in the registry on all platforms.\nimport _ \"github.com/lima-vm/lima/v2/pkg/driver/qemu\"\n"
  },
  {
    "path": "cmd/limactl/main_windows.go",
    "content": "//go:build !external_wsl2\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\n// Import wsl2 driver to register it in the registry on windows.\nimport _ \"github.com/lima-vm/lima/v2/pkg/driver/wsl2\"\n"
  },
  {
    "path": "cmd/limactl/network.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nconst networkExample = `  List all networks:\n  $ limactl network list\n\n  Create a network:\n  $ limactl network create foo --gateway 192.168.42.1/24\n\n  Connect VM instances to the newly created network:\n  $ limactl create --network lima:foo --name vm1\n  $ limactl create --network lima:foo --name vm2\n\n  Delete a network:\n  $ limactl network delete --force foo\n`\n\nconst networkCreateExample = `  Create a network:\n  $ limactl network create foo --gateway 192.168.42.1/24\n\n  Connect VM instances to the newly created network:\n  $ limactl create --network lima:foo --name vm1\n  $ limactl create --network lima:foo --name vm2\n`\n\nfunc newNetworkCommand() *cobra.Command {\n\tnetworkCommand := &cobra.Command{\n\t\tUse:     \"network\",\n\t\tShort:   \"Lima network management\",\n\t\tExample: networkExample,\n\t\tGroupID: advancedCommand,\n\t}\n\tnetworkCommand.AddCommand(\n\t\tnewNetworkListCommand(),\n\t\tnewNetworkCreateCommand(),\n\t\tnewNetworkDeleteCommand(),\n\t)\n\treturn networkCommand\n}\n\nfunc newNetworkListCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"list\",\n\t\tShort: \"List networks\",\n\t\tExample: `  List all networks:\n  $ limactl network list\n\n  List networks in JSON format:\n  $ limactl network list --json\n`,\n\t\tAliases:           []string{\"ls\"},\n\t\tArgs:              WrapArgsError(cobra.ArbitraryArgs),\n\t\tRunE:              networkListAction,\n\t\tValidArgsFunction: networkBashComplete,\n\t}\n\tflags := cmd.Flags()\n\tflags.Bool(\"json\", false, \"JSONify output\")\n\treturn cmd\n}\n\nfunc networkListAction(cmd *cobra.Command, args []string) error {\n\tflags := cmd.Flags()\n\tjsonFormat, err := flags.GetBool(\"json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfig, err := networks.LoadConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tallNetworks := slices.Sorted(maps.Keys(config.Networks))\n\n\tnetworks := []string{}\n\tif len(args) > 0 {\n\t\tfor _, arg := range args {\n\t\t\tmatches := nameMatches(arg, allNetworks)\n\t\t\tif len(matches) > 0 {\n\t\t\t\tnetworks = append(networks, matches...)\n\t\t\t} else {\n\t\t\t\tlogrus.Warnf(\"No network matching %v found.\", arg)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tnetworks = allNetworks\n\t}\n\n\tif jsonFormat {\n\t\tw := cmd.OutOrStdout()\n\t\tfor _, name := range networks {\n\t\t\tnw, ok := config.Networks[name]\n\t\t\tif !ok {\n\t\t\t\tlogrus.Errorf(\"network %q does not exist\", nw)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tj, err := json.Marshal(nw)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Fprintln(w, string(j))\n\t\t}\n\t\treturn nil\n\t}\n\n\tw := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0)\n\tfmt.Fprintln(w, \"NAME\\tMODE\\tGATEWAY\\tINTERFACE\")\n\tfor _, name := range networks {\n\t\tnw, ok := config.Networks[name]\n\t\tif !ok {\n\t\t\tlogrus.Errorf(\"network %q does not exist\", nw)\n\t\t\tcontinue\n\t\t}\n\t\tgwStr := \"-\"\n\t\tif nw.Gateway != nil {\n\t\t\tgw := net.IPNet{\n\t\t\t\tIP:   nw.Gateway,\n\t\t\t\tMask: net.IPMask(nw.NetMask),\n\t\t\t}\n\t\t\tgwStr = gw.String()\n\t\t}\n\t\tintfStr := \"-\"\n\t\tif nw.Interface != \"\" {\n\t\t\tintfStr = nw.Interface\n\t\t}\n\t\tfmt.Fprintf(w, \"%s\\t%s\\t%s\\t%s\\n\", name, nw.Mode, gwStr, intfStr)\n\t}\n\treturn w.Flush()\n}\n\nfunc newNetworkCreateCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:     \"create NETWORK\",\n\t\tShort:   \"Create a Lima network\",\n\t\tExample: networkCreateExample,\n\t\tArgs:    WrapArgsError(cobra.ExactArgs(1)),\n\t\tRunE:    networkCreateAction,\n\t}\n\tflags := cmd.Flags()\n\tflags.String(\"mode\", networks.ModeUserV2, \"mode\")\n\t_ = cmd.RegisterFlagCompletionFunc(\"mode\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\treturn networks.Modes, cobra.ShellCompDirectiveNoFileComp\n\t})\n\tflags.String(\"gateway\", \"\", \"gateway, e.g., \\\"192.168.42.1/24\\\"\")\n\tflags.String(\"interface\", \"\", \"interface for bridged mode\")\n\t_ = cmd.RegisterFlagCompletionFunc(\"interface\", bashFlagCompleteNetworkInterfaceNames)\n\treturn cmd\n}\n\nfunc networkCreateAction(cmd *cobra.Command, args []string) error {\n\tname := args[0]\n\t// LoadConfig ensures existence of networks.yaml\n\tconfig, err := networks.LoadConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, ok := config.Networks[name]; ok {\n\t\treturn fmt.Errorf(\"network %q already exists\", name)\n\t}\n\n\tflags := cmd.Flags()\n\tmode, err := flags.GetString(\"mode\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgateway, err := flags.GetString(\"gateway\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tintf, err := flags.GetString(\"interface\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch mode {\n\tcase networks.ModeBridged:\n\t\tif gateway != \"\" {\n\t\t\treturn fmt.Errorf(\"network mode %q does not support specifying gateway\", mode)\n\t\t}\n\t\tif intf == \"\" {\n\t\t\treturn fmt.Errorf(\"network mode %q requires specifying interface\", mode)\n\t\t}\n\t\tyq := fmt.Sprintf(`.networks.%q = {\"mode\":%q,\"interface\":%q}`, name, mode, intf)\n\t\treturn networkApplyYQ(yq)\n\tdefault:\n\t\tif gateway == \"\" {\n\t\t\treturn fmt.Errorf(\"network mode %q requires specifying gateway\", mode)\n\t\t}\n\t\tif intf != \"\" {\n\t\t\treturn fmt.Errorf(\"network mode %q does not support specifying interface\", mode)\n\t\t}\n\t\tif !strings.Contains(gateway, \"/\") {\n\t\t\tgateway += \"/24\"\n\t\t}\n\t\tgwIP, gwMask, err := net.ParseCIDR(gateway)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse CIDR %q: %w\", gateway, err)\n\t\t}\n\t\tif gwIP.IsUnspecified() || gwIP.IsLoopback() {\n\t\t\treturn fmt.Errorf(\"invalid IP address: %v\", gwIP)\n\t\t}\n\t\tgwMaskStr := \"255.255.255.0\"\n\t\tif gwMask != nil {\n\t\t\tgwMaskStr = net.IP(gwMask.Mask).String()\n\t\t}\n\t\t// TODO: check IP range collision\n\n\t\tyq := fmt.Sprintf(`.networks.%q = {\"mode\":%q,\"gateway\":%q,\"netmask\":%q,\"interface\":%q}`, name, mode, gwIP.String(), gwMaskStr, intf)\n\t\treturn networkApplyYQ(yq)\n\t}\n}\n\nfunc networkApplyYQ(yq string) error {\n\tfilePath, err := networks.ConfigFile()\n\tif err != nil {\n\t\treturn err\n\t}\n\tyContent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tyBytes, err := yqutil.EvaluateExpression(yq, yContent)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := os.WriteFile(filePath, yBytes, 0o644); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc newNetworkDeleteCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"delete NETWORK [NETWORK, ...]\",\n\t\tShort: \"Delete one or more Lima networks\",\n\t\tExample: `  Delete a network:\n  $ limactl network delete --force foo\n\n  Delete multiple networks:\n  $ limactl network delete --force foo bar\n`,\n\t\tAliases:           []string{\"remove\", \"rm\"},\n\t\tArgs:              WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:              networkDeleteAction,\n\t\tValidArgsFunction: networkBashComplete,\n\t}\n\tflags := cmd.Flags()\n\tflags.BoolP(\"force\", \"f\", false, \"Force delete (currently always required)\")\n\treturn cmd\n}\n\nfunc networkDeleteAction(cmd *cobra.Command, args []string) error {\n\tflags := cmd.Flags()\n\tforce, err := flags.GetBool(\"force\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !force {\n\t\treturn errors.New(\"`limactl network delete` currently always requires `--force`\")\n\t\t// Because the command currently does not check whether the network being removed is in use\n\t}\n\n\tnetworks := make([]string, len(args))\n\tfor i, name := range args {\n\t\tnetworks[i] = fmt.Sprintf(\"del(.networks.%q)\", name)\n\t}\n\tyq := strings.Join(networks, \" | \")\n\treturn networkApplyYQ(yq)\n}\n\nfunc networkBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteNetworkNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/protect.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newProtectCommand() *cobra.Command {\n\tprotectCommand := &cobra.Command{\n\t\tUse:   \"protect INSTANCE [INSTANCE, ...]\",\n\t\tShort: \"Protect an instance to prohibit accidental removal\",\n\t\tLong: `Protect an instance to prohibit accidental removal via the 'limactl delete' command.\nThe instance is not being protected against removal via '/bin/rm', Finder, etc.`,\n\t\tArgs:              WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:              protectAction,\n\t\tValidArgsFunction: protectBashComplete,\n\t\tGroupID:           advancedCommand,\n\t}\n\treturn protectCommand\n}\n\nfunc protectAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tvar errs []error\n\tfor _, instName := range args {\n\t\tinst, err := store.Inspect(ctx, instName)\n\t\tif err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to inspect instance %q: %w\", instName, err))\n\t\t\tcontinue\n\t\t}\n\t\tif inst.Protected {\n\t\t\tlogrus.Warnf(\"Instance %q is already protected. Skipping.\", instName)\n\t\t\tcontinue\n\t\t}\n\t\tif err := inst.Protect(); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to protect instance %q: %w\", instName, err))\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Infof(\"Protected %q\", instName)\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc protectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/prune.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"maps\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/downloader\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatmpl\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/templatestore\"\n)\n\nfunc newPruneCommand() *cobra.Command {\n\tpruneCommand := &cobra.Command{\n\t\tUse:               \"prune\",\n\t\tShort:             \"Prune garbage objects\",\n\t\tArgs:              WrapArgsError(cobra.NoArgs),\n\t\tRunE:              pruneAction,\n\t\tValidArgsFunction: cobra.NoFileCompletions,\n\t\tGroupID:           advancedCommand,\n\t}\n\tpruneCommand.Flags().Bool(\"keep-referred\", false, \"Keep objects that are referred by some instances or templates\")\n\treturn pruneCommand\n}\n\nfunc pruneAction(cmd *cobra.Command, _ []string) error {\n\tctx := cmd.Context()\n\tkeepReferred, err := cmd.Flags().GetBool(\"keep-referred\")\n\tif err != nil {\n\t\treturn err\n\t}\n\topt := downloader.WithCache()\n\tif !keepReferred {\n\t\treturn downloader.RemoveAllCacheDir(opt)\n\t}\n\n\t// Prune downloads that are not used by any instances or templates\n\tcacheEntries, err := downloader.CacheEntries(opt)\n\tif err != nil {\n\t\treturn err\n\t}\n\tknownLocations, err := knownLocations(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor cacheKey, cachePath := range cacheEntries {\n\t\tif file, exists := knownLocations[cacheKey]; exists {\n\t\t\tlogrus.Debugf(\"Keep %q caching %q\", cacheKey, file.Location)\n\t\t} else {\n\t\t\tlogrus.Debug(\"Deleting \", cacheKey)\n\t\t\tif err := os.RemoveAll(cachePath); err != nil {\n\t\t\t\tlogrus.Warnf(\"Failed to delete %q: %v\", cacheKey, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc knownLocations(ctx context.Context) (map[string]limatype.File, error) {\n\tlocations := make(map[string]limatype.File)\n\n\t// Collect locations from instances\n\tinstances, err := store.Instances()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, instanceName := range instances {\n\t\tinstance, err := store.Inspect(ctx, instanceName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif instance.Errors != nil {\n\t\t\tlogrus.Warnf(\"skipping instance %q because it has errors: %v\", instanceName, instance.Errors)\n\t\t\tcontinue\n\t\t}\n\t\tmaps.Copy(locations, locationsFromLimaYAML(instance.Config))\n\t}\n\n\t// Collect locations from templates\n\ttemplates, err := templatestore.Templates()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, t := range templates {\n\t\ttmpl, err := limatmpl.Read(ctx, \"\", t.Location)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := tmpl.Embed(ctx, true, true); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ty, err := limayaml.Load(ctx, tmpl.Bytes, tmpl.Name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := driverutil.ResolveVMType(ctx, y, t.Name); err != nil {\n\t\t\tlogrus.Warnf(\"failed to resolve vmType for %q: %v\", t.Name, err)\n\t\t}\n\t\tmaps.Copy(locations, locationsFromLimaYAML(y))\n\t}\n\treturn locations, nil\n}\n\nfunc locationsFromLimaYAML(y *limatype.LimaYAML) map[string]limatype.File {\n\tlocations := make(map[string]limatype.File)\n\tfor _, f := range y.Images {\n\t\tlocations[downloader.CacheKey(f.Location)] = f.File\n\t\tif f.Kernel != nil {\n\t\t\tlocations[downloader.CacheKey(f.Kernel.Location)] = f.Kernel.File\n\t\t}\n\t\tif f.Initrd != nil {\n\t\t\tlocations[downloader.CacheKey(f.Initrd.Location)] = *f.Initrd\n\t\t}\n\t}\n\tfor _, f := range y.Containerd.Archives {\n\t\tlocations[downloader.CacheKey(f.Location)] = f\n\t}\n\tfor _, f := range y.Firmware.Images {\n\t\tlocations[downloader.CacheKey(f.Location)] = f.File\n\t}\n\treturn locations\n}\n"
  },
  {
    "path": "cmd/limactl/restart.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newRestartCommand() *cobra.Command {\n\trestartCmd := &cobra.Command{\n\t\tUse:               \"restart INSTANCE\",\n\t\tShort:             \"Restart a running instance\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tRunE:              restartAction,\n\t\tValidArgsFunction: restartBashComplete,\n\t\tGroupID:           basicCommand,\n\t}\n\n\trestartCmd.Flags().BoolP(\"force\", \"f\", false, \"Force stop and restart the instance\")\n\trestartCmd.Flags().Bool(\"progress\", false, \"Show provision script progress by tailing cloud-init logs\")\n\treturn restartCmd\n}\n\nfunc restartAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := DefaultInstanceName\n\tif len(args) > 0 {\n\t\tinstName = args[0]\n\t}\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tforce, err := cmd.Flags().GetBool(\"force\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tprogress, err := cmd.Flags().GetBool(\"progress\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif force {\n\t\treturn instance.RestartForcibly(ctx, inst, progress)\n\t}\n\n\treturn instance.Restart(ctx, inst, progress)\n}\n\nfunc restartBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/shell.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"al.essio.dev/pkg/shellescape\"\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/lima-vm/sshocker/pkg/ssh\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/copytool\"\n\t\"github.com/lima-vm/lima/v2/pkg/envutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/reconcile\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/uiutil\"\n)\n\nconst shellHelp = `Execute shell in Lima\n\nlima command is provided as an alias for limactl shell $LIMA_INSTANCE. $LIMA_INSTANCE defaults to \"` + DefaultInstanceName + `\".\n\nBy default, the first 'ssh' executable found in the host's PATH is used to connect to the Lima instance.\nA custom ssh alias can be used instead by setting the $` + sshutil.EnvShellSSH + ` environment variable.\n\nEnvironment Variables:\n  --preserve-env: Propagates host environment variables to the guest instance.\n                  Use LIMA_SHELLENV_ALLOW to specify which variables to allow.\n                  Use LIMA_SHELLENV_BLOCK to specify which variables to block (extends default blocklist with +).\n\nHint: try --debug to show the detailed logs, if it seems hanging (mostly due to some SSH issue).\n`\n\nfunc newShellCommand() *cobra.Command {\n\tshellCmd := &cobra.Command{\n\t\tUse:               \"shell [flags] INSTANCE [COMMAND...]\",\n\t\tSuggestFor:        []string{\"ssh\"},\n\t\tShort:             \"Execute shell in Lima\",\n\t\tLong:              shellHelp,\n\t\tArgs:              WrapArgsError(cobra.ArbitraryArgs),\n\t\tRunE:              shellAction,\n\t\tValidArgsFunction: shellBashComplete,\n\t\tSilenceErrors:     true,\n\t\tGroupID:           basicCommand,\n\t}\n\n\tshellCmd.Flags().SetInterspersed(false)\n\n\tshellCmd.Flags().String(\"instance\", \"\", \"Instance name (used by the lima wrapper script)\")\n\t_ = shellCmd.Flags().MarkHidden(\"instance\")\n\tshellCmd.Flags().String(\"shell\", \"\", \"Shell interpreter, e.g. /bin/bash\")\n\tshellCmd.Flags().String(\"workdir\", \"\", \"Working directory\")\n\tshellCmd.Flags().Bool(\"reconnect\", false, \"Reconnect to the SSH session\")\n\tshellCmd.Flags().Bool(\"preserve-env\", false, \"Propagate environment variables to the shell\")\n\tshellCmd.Flags().Bool(\"start\", false, \"Start the instance if it is not already running\")\n\tshellCmd.Flags().String(\"sync\", \"\", \"Copy a host directory to the guest and vice-versa upon exit\")\n\n\treturn shellCmd\n}\n\nconst (\n\trsyncMinimumSrcDirDepth = 4 // Depth of \"/Users/USER\" is 3.\n\tcolorGray               = \"\\033[0;90m\"\n\tcolorNone               = \"\\033[0m\"\n)\n\nfunc shellAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tflags := cmd.Flags()\n\ttty, err := flags.GetBool(\"tty\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// When --instance is specified, all positional args are treated as COMMAND.\n\t// Otherwise, the first positional arg is the instance name (backward compatible).\n\tinstName, err := flags.GetString(\"instance\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif instName != \"\" {\n\t\t// All args are COMMAND; prepend a placeholder instance name so the rest of the code works unchanged.\n\t\targs = append([]string{instName}, args...)\n\t} else {\n\t\tif len(args) == 0 {\n\t\t\treturn errors.New(\"requires instance name as first argument\")\n\t\t}\n\t\t// simulate the behavior of double dash\n\t\tnewArg := []string{}\n\t\tif len(args) >= 2 && args[1] == \"--\" {\n\t\t\tnewArg = append(newArg, args[:1]...)\n\t\t\tnewArg = append(newArg, args[2:]...)\n\t\t\targs = newArg\n\t\t}\n\t\tinstName = args[0]\n\t}\n\n\tif len(args) >= 2 {\n\t\tswitch args[1] {\n\t\tcase \"create\", \"start\", \"delete\", \"shell\":\n\t\t\t// `lima start` (alias of `limactl $LIMA_INSTANCE start`) is probably a typo of `limactl start`\n\t\t\tlogrus.Warnf(\"Perhaps you meant `limactl %s`?\", strings.Join(args[1:], \" \"))\n\t\t}\n\t}\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"instance %q does not exist, run `limactl create %s` to create a new instance\", instName, instName)\n\t\t}\n\t\treturn err\n\t}\n\tif len(inst.Errors) > 0 {\n\t\tlogrus.WithError(errors.Join(inst.Errors...)).Errorf(\"Instance %q has configuration errors\", instName)\n\t}\n\tif inst.Config == nil {\n\t\treturn fmt.Errorf(\"instance %q has no configuration\", instName)\n\t}\n\tif inst.Status == limatype.StatusStopped {\n\t\tstartNow, err := flags.GetBool(\"start\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif tty && !flags.Changed(\"start\") {\n\t\t\tstartNow, err = askWhetherToStart(cmd)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif !startNow {\n\t\t\treturn fmt.Errorf(\"instance %q is stopped, run `limactl start %s` to start the instance\", instName, instName)\n\t\t}\n\n\t\t// Network reconciliation will be performed by the process launched by the autostart manager\n\t\tif registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t\t} else if !registered {\n\t\t\terr = reconcile.Reconcile(ctx, inst.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\terr = instance.Start(instance.WithLaunchingShell(ctx), inst, false, false)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tinst, err = store.Inspect(ctx, instName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trestart, err := cmd.Flags().GetBool(\"reconnect\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif restart && sshutil.IsControlMasterExisting(inst.Dir) {\n\t\tlogrus.Infof(\"Exiting ssh session for the instance %q\", instName)\n\n\t\tsshConfig := &ssh.SSHConfig{\n\t\t\tConfigFile:     inst.SSHConfigFile,\n\t\t\tPersist:        false,\n\t\t\tAdditionalArgs: []string{},\n\t\t}\n\n\t\tif err := ssh.ExitMaster(inst.Hostname, inst.SSHLocalPort, sshConfig); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tsyncDirVal, err := flags.GetString(\"sync\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get sync flag: %w\", err)\n\t}\n\tsyncHostWorkdir := syncDirVal != \"\"\n\tif syncHostWorkdir && len(inst.Config.Mounts) > 0 {\n\t\treturn errors.New(\"cannot use `--sync` when the instance has host mounts configured, start the instance with `--mount-none` to disable mounts\")\n\t}\n\n\t// When workDir is explicitly set, the shell MUST have workDir as the cwd, or exit with an error.\n\t//\n\t// changeDirCmd := \"cd workDir || exit 1\"                  if workDir != \"\"\n\t//              := \"cd hostCurrentDir || cd hostHomeDir\"   if workDir == \"\"\n\tvar changeDirCmd string\n\tvar hostCurrentDir string\n\tif syncDirVal != \"\" {\n\t\thostCurrentDir, err = filepath.Abs(syncDirVal)\n\t\tif err == nil && runtime.GOOS == \"windows\" {\n\t\t\thostCurrentDir, err = mountDirFromWindowsDir(ctx, inst, hostCurrentDir)\n\t\t}\n\t} else {\n\t\thostCurrentDir, err = hostCurrentDirectory(ctx, inst)\n\t}\n\n\tif err != nil {\n\t\tchangeDirCmd = \"false\"\n\t\tlogrus.WithError(err).Warn(\"failed to get the current directory\")\n\t}\n\tif syncHostWorkdir {\n\t\tif _, err := exec.LookPath(string(copytool.BackendRsync)); err != nil {\n\t\t\treturn fmt.Errorf(\"rsync is required for `--sync` but not found: %w\", err)\n\t\t}\n\n\t\tsrcWdDepth := len(strings.Split(hostCurrentDir, string(os.PathSeparator)))\n\t\tif srcWdDepth < rsyncMinimumSrcDirDepth {\n\t\t\treturn fmt.Errorf(\"expected the depth of the host working directory (%q) to be more than %d, only got %d (Hint: %s)\",\n\t\t\t\thostCurrentDir, rsyncMinimumSrcDirDepth, srcWdDepth, \"cd to a deeper directory\")\n\t\t}\n\t}\n\n\tvar destRsyncDir string\n\tworkDir, err := cmd.Flags().GetString(\"workdir\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif workDir != \"\" && syncHostWorkdir {\n\t\treturn errors.New(\"cannot use `--workdir` and `--sync` at the same time\")\n\t}\n\tif syncHostWorkdir {\n\t\tdestRsyncDir = *inst.Config.User.Home + hostCurrentDir\n\t}\n\tswitch {\n\tcase workDir != \"\":\n\t\tchangeDirCmd = fmt.Sprintf(\"cd %s || exit 1\", shellescape.Quote(workDir))\n\t\t// FIXME: check whether y.Mounts contains the home, not just len > 0\n\tcase len(inst.Config.Mounts) > 0 || inst.VMType == limatype.WSL2:\n\t\tchangeDirCmd = fmt.Sprintf(\"cd %s\", shellescape.Quote(hostCurrentDir))\n\t\thostHomeDir, err := os.UserHomeDir()\n\t\tif err == nil && runtime.GOOS == \"windows\" {\n\t\t\thostHomeDir, err = mountDirFromWindowsDir(ctx, inst, hostHomeDir)\n\t\t}\n\t\tif err == nil {\n\t\t\tchangeDirCmd = fmt.Sprintf(\"%s || cd %s\", changeDirCmd, shellescape.Quote(hostHomeDir))\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warn(\"failed to get the home directory\")\n\t\t}\n\tcase syncHostWorkdir:\n\t\tchangeDirCmd = fmt.Sprintf(\"cd %s\", shellescape.Quote(destRsyncDir))\n\tdefault:\n\t\tlogrus.Debug(\"the host home does not seem mounted, so the guest shell will have a different cwd\")\n\t}\n\n\tif changeDirCmd == \"\" {\n\t\tchangeDirCmd = \"false\"\n\t}\n\tlogrus.Debugf(\"changeDirCmd=%q\", changeDirCmd)\n\n\tshell, err := cmd.Flags().GetString(\"shell\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif shell == \"\" {\n\t\tshell = `\"$SHELL\"`\n\t} else {\n\t\tshell = shellescape.Quote(shell)\n\t}\n\t// Handle environment variable propagation\n\tvar envPrefix string\n\tpreserveEnv, err := cmd.Flags().GetBool(\"preserve-env\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif preserveEnv {\n\t\tfilteredEnv := envutil.FilterEnvironment()\n\t\tif len(filteredEnv) > 0 {\n\t\t\tenvPrefix = \"env \"\n\t\t\tfor _, envVar := range filteredEnv {\n\t\t\t\tenvPrefix += shellescape.Quote(envVar) + \" \"\n\t\t\t}\n\t\t}\n\t}\n\n\t// -l is known to be available in bash, zsh, and FreeBSD sh.\n\t// Note that --login is not available in FreeBSD sh.\n\tscript := fmt.Sprintf(\"%s ; exec %s%s -l\", changeDirCmd, envPrefix, shell)\n\tif len(args) > 1 {\n\t\tquotedArgs := make([]string, len(args[1:]))\n\t\tparsingEnv := true\n\t\tfor i, arg := range args[1:] {\n\t\t\tif parsingEnv && isEnv(arg) {\n\t\t\t\tquotedArgs[i] = quoteEnv(arg)\n\t\t\t} else {\n\t\t\t\tparsingEnv = false\n\t\t\t\tquotedArgs[i] = shellescape.Quote(arg)\n\t\t\t}\n\t\t}\n\t\tscript += fmt.Sprintf(\n\t\t\t\" -c %s\",\n\t\t\tshellescape.Quote(strings.Join(quotedArgs, \" \")),\n\t\t)\n\t}\n\n\tsshExe, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsshOpts, err := sshutil.SSHOpts(\n\t\tctx,\n\t\tsshExe,\n\t\tinst.Dir,\n\t\t*inst.Config.User.Name,\n\t\t*inst.Config.SSH.LoadDotSSHPubKeys,\n\t\t*inst.Config.SSH.ForwardAgent,\n\t\t*inst.Config.SSH.ForwardX11,\n\t\t*inst.Config.SSH.ForwardX11Trusted)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\t// Remove ControlMaster, ControlPath, and ControlPersist options,\n\t\t// because Cygwin-based SSH clients do not support multiplexing when executing commands.\n\t\t// References:\n\t\t//   https://inbox.sourceware.org/cygwin/c98988a5-7e65-4282-b2a1-bb8e350d5fab@acm.org/T/\n\t\t//   https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible\n\t\t// By removing these options:\n\t\t//   - Avoids execution failures when the control master is not yet available.\n\t\t//   - Prevents error messages such as:\n\t\t//     > mux_client_request_session: read from master failed: Connection reset by peer\n\t\t//     > ControlSocket ....sock already exists, disabling multiplexing\n\t\t// Only remove these options when writing the SSH config file and executing `limactl shell`, since multiplexing seems to work with port forwarding.\n\t\tsshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts)\n\t}\n\tsshArgs := append([]string{}, sshExe.Args...)\n\tsshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)\n\n\tvar (\n\t\tsshExecForRsync *exec.Cmd\n\t\trsync           copytool.CopyTool\n\t)\n\tif syncHostWorkdir {\n\t\tlogrus.Infof(\"Syncing host current directory(%s) to guest instance...\", hostCurrentDir)\n\t\tsshExecForRsync = exec.CommandContext(ctx, sshExe.Exe, sshArgs...)\n\n\t\t// Create the destination directory in the guest instance,\n\t\t// we could have done this by using `--rsync-path` but it's more\n\t\t// complex to quote properly.\n\t\tif err := executeSSHForRsync(ctx, *sshExecForRsync, inst.SSHLocalPort, inst.SSHAddress, fmt.Sprintf(\"mkdir -p %s\", shellescape.Quote(destRsyncDir))); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create the synced workdir in guest instance: %w\", err)\n\t\t}\n\n\t\t// The macOS release of rsync (the latest being 2.6.9) does not support shell escaping of destination path but other versions do.\n\t\trsyncVer, err := rsyncVersion(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get rsync version: %w\", err)\n\t\t}\n\t\tif rsyncVer.LessThan(*semver.New(\"3.0.0\")) {\n\t\t\tdestRsyncDir = shellescape.Quote(destRsyncDir)\n\t\t}\n\n\t\tpaths := []string{\n\t\t\thostCurrentDir,\n\t\t\tfmt.Sprintf(\"%s:%s\", inst.Name, destRsyncDir),\n\t\t}\n\t\trsync, err = copytool.New(ctx, string(copytool.BackendRsync), paths, &copytool.Options{\n\t\t\tRecursive: true,\n\t\t\tVerbose:   false,\n\t\t\tAdditionalArgs: []string{\n\t\t\t\t\"--delete\",\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlogrus.Debugf(\"using copy tool %q\", rsync.Name())\n\n\t\tif err := rsyncDirectory(ctx, cmd, rsync, paths); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to rsync to the guest %w\", err)\n\t\t}\n\t\tlogrus.Infof(\"Successfully synced host current directory to guest(%s) instance.\", destRsyncDir)\n\t}\n\n\tif isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {\n\t\t// required for showing the shell prompt: https://stackoverflow.com/a/626574\n\t\tsshArgs = append(sshArgs, \"-t\")\n\t}\n\tif _, present := os.LookupEnv(\"COLORTERM\"); present {\n\t\t// SendEnv config is cumulative, with already existing options in ssh_config\n\t\tsshArgs = append(sshArgs, \"-o\", \"SendEnv=COLORTERM\")\n\t}\n\tlogLevel := \"ERROR\"\n\t// For versions older than OpenSSH 8.9p, LogLevel=QUIET was needed to\n\t// avoid the \"Shared connection to 127.0.0.1 closed.\" message with -t.\n\tolderSSH := sshutil.DetectOpenSSHVersion(ctx, sshExe).LessThan(*semver.New(\"8.9.0\"))\n\tif olderSSH {\n\t\tlogLevel = \"QUIET\"\n\t}\n\tsshArgs = append(sshArgs, []string{\n\t\t\"-o\", fmt.Sprintf(\"LogLevel=%s\", logLevel),\n\t\t\"-p\", strconv.Itoa(inst.SSHLocalPort),\n\t\tinst.SSHAddress,\n\t\t\"--\",\n\t\tscript,\n\t}...)\n\tsshCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...)\n\tsshCmd.Stdin = os.Stdin\n\tsshCmd.Stdout = os.Stdout\n\tsshCmd.Stderr = os.Stderr\n\tlogrus.Debugf(\"executing ssh (may take a long)): %+v\", sshCmd.Args)\n\n\t// TODO: use syscall.Exec directly (results in losing tty?)\n\tif err := sshCmd.Run(); err != nil {\n\t\treturn err\n\t}\n\n\t// Once the shell command finishes, rsync back the changes from guest workdir\n\t// to the host and delete the guest synced workdir only if the user\n\t// confirms the changes.\n\tif syncHostWorkdir {\n\t\ttty, err := flags.GetBool(\"tty\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn askUserForRsyncBack(ctx, cmd, inst, sshExecForRsync, hostCurrentDir, destRsyncDir, rsync, tty)\n\t}\n\treturn nil\n}\n\nfunc askUserForRsyncBack(ctx context.Context, cmd *cobra.Command, inst *limatype.Instance, sshCmd *exec.Cmd, hostCurrentDir, destRsyncDir string, rsync copytool.CopyTool, tty bool) error {\n\tremoteSource := fmt.Sprintf(\"%s:%s\", inst.Name, destRsyncDir)\n\tclean := filepath.Clean(hostCurrentDir)\n\tdirForCleanup := shellescape.Quote(filepath.Join(*inst.Config.User.Home, clean))\n\n\trsyncBack := func() error {\n\t\tpaths := []string{\n\t\t\tremoteSource,\n\t\t\thostCurrentDir,\n\t\t}\n\n\t\tif err := rsyncDirectory(ctx, cmd, rsync, paths); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to sync back the changes from guest instance to host: %w\", err)\n\t\t}\n\t\tlogrus.Info(\"Successfully synced back the changes to host.\")\n\t\treturn nil\n\t}\n\n\tdefer func() {\n\t\t// Clean up the guest synced workdir\n\t\tif err := executeSSHForRsync(ctx, *sshCmd, inst.SSHLocalPort, inst.SSHAddress, fmt.Sprintf(\"rm -rf %s\", dirForCleanup)); err != nil {\n\t\t\tlogrus.WithError(err).Warn(\"Failed to clean up guest synced workdir\")\n\t\t}\n\t}()\n\n\tif !tty {\n\t\treturn rsyncBack()\n\t}\n\n\trawOutput, stats, err := getRsyncStats(ctx, remoteSource, filepath.Dir(hostCurrentDir))\n\tif err != nil {\n\t\tlogrus.WithError(err).Warn(\"failed to get rsync stats\")\n\t}\n\tif stats != nil && stats.String() == \"\" {\n\t\tlogrus.Info(\"No changes detected\")\n\t\treturn nil\n\t}\n\tstatsMsg := \"\"\n\tif stats != nil {\n\t\tif s := stats.String(); s != \"\" {\n\t\t\tstatsMsg = fmt.Sprintf(\" (%s)\", s)\n\t\t}\n\t}\n\n\tmessage := fmt.Sprintf(\"⚠️ Accept the changes?%s\", statsMsg)\n\toptions := []string{\n\t\t\"Yes\",\n\t\t\"No\",\n\t\t\"View the changed contents\",\n\t}\n\n\tbaseDir, err := os.MkdirTemp(\"\", \"lima-guest-synced-*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err := os.RemoveAll(baseDir); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"Failed to clean up temporary directory %s\", baseDir)\n\t\t}\n\t}()\n\thostTmpDest := filepath.Join(baseDir, filepath.Base(hostCurrentDir))\n\terr = os.MkdirAll(hostTmpDest, 0o755)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trsyncToTempDir := false\n\n\tfor {\n\t\tans, err := uiutil.Select(message, options)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to open TUI: %w\", err)\n\t\t}\n\n\t\tswitch ans {\n\t\tcase 0: // Yes\n\t\t\treturn rsyncBack()\n\t\tcase 1: // No\n\t\t\tlogrus.Info(\"Skipping syncing back the changes to host.\")\n\t\t\treturn nil\n\t\tcase 2: // View the changed contents\n\t\t\tvar diffCmd *exec.Cmd\n\t\t\tif _, err := exec.LookPath(\"diff\"); err != nil {\n\t\t\t\tlogrus.WithError(err).Warn(\"`diff` not found; showing rsync dry-run output only\")\n\t\t\t} else {\n\t\t\t\tdiffCmd = exec.CommandContext(ctx, \"diff\", \"-ruN\", \"--color=always\", hostCurrentDir, hostTmpDest)\n\t\t\t\tif !rsyncToTempDir {\n\t\t\t\t\tpaths := []string{\n\t\t\t\t\t\tremoteSource,\n\t\t\t\t\t\thostTmpDest,\n\t\t\t\t\t}\n\n\t\t\t\t\tif err := rsyncDirectory(ctx, cmd, rsync, paths); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to sync back the changes from guest instance to host temporary directory: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t\trsyncToTempDir = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tpager := os.Getenv(\"PAGER\")\n\t\t\tpager = strings.TrimSpace(pager)\n\t\t\tif pager == \"\" {\n\t\t\t\tpager = \"less\"\n\t\t\t}\n\t\t\tpagerArgs := strings.Fields(pager)\n\t\t\tlessCmd := exec.CommandContext(ctx, pagerArgs[0], pagerArgs[1:]...)\n\n\t\t\tpipeIn, err := lessCmd.StdinPipe()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create pipe for less: %w\", err)\n\t\t\t}\n\t\t\tif diffCmd != nil {\n\t\t\t\tdiffCmd.Stdout = pipeIn\n\t\t\t}\n\t\t\tlessCmd.Stdout = cmd.OutOrStdout()\n\t\t\tlessCmd.Stderr = cmd.OutOrStderr()\n\n\t\t\tif err := lessCmd.Start(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to start less: %w\", err)\n\t\t\t}\n\n\t\t\t// Write rsync dry-run output first\n\t\t\tif stats != nil {\n\t\t\t\trsyncHead := fmt.Sprintf(\"%s--- rsync dry-run statistics ---%s\", colorGray, colorNone)\n\t\t\t\tdiffHead := fmt.Sprintf(\"%s--- detailed diff --- %s\", colorGray, colorNone)\n\t\t\t\tif diffCmd == nil {\n\t\t\t\t\tdiffHead = fmt.Sprintf(\"%s--- detailed diff unavailable (`diff` not found) --- %s\", colorGray, colorNone)\n\t\t\t\t}\n\t\t\t\tcombinedOutput := fmt.Sprintf(\n\t\t\t\t\t\"%s\\n%s\\n\\n%s\\n\\n\\n%s\\n\",\n\t\t\t\t\trsyncHead,\n\t\t\t\t\tstats.String(),\n\t\t\t\t\trawOutput,\n\t\t\t\t\tdiffHead,\n\t\t\t\t)\n\n\t\t\t\tif _, err := fmt.Fprint(pipeIn, combinedOutput); err != nil {\n\t\t\t\t\t_ = pipeIn.Close()\n\t\t\t\t\treturn fmt.Errorf(\"failed to write rsync stats to pager: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif diffCmd != nil {\n\t\t\t\tif err := diffCmd.Run(); err != nil {\n\t\t\t\t\t// Command `diff` returns exit code 1 when files differ.\n\t\t\t\t\tvar exitErr *exec.ExitError\n\t\t\t\t\tif errors.As(err, &exitErr) && exitErr.ExitCode() >= 2 {\n\t\t\t\t\t\t_ = pipeIn.Close()\n\t\t\t\t\t\treturn fmt.Errorf(\"failed to run diff command: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t_ = pipeIn.Close()\n\n\t\t\tif err := lessCmd.Wait(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to wait for less command: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc executeSSHForRsync(ctx context.Context, sshCmd exec.Cmd, sshLocalPort int, sshAddress, command string) error {\n\tsshCmd.Args = append(sshCmd.Args,\n\t\t\"-p\", strconv.Itoa(sshLocalPort),\n\t\tsshAddress,\n\t)\n\n\t// Skip Args[0] (program name) to avoid duplication\n\tsshRmCmd := exec.CommandContext(ctx, sshCmd.Path, append(sshCmd.Args[1:], command)...)\n\tif err := sshRmCmd.Run(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc hostCurrentDirectory(ctx context.Context, inst *limatype.Instance) (string, error) {\n\thostCurrentDir, err := os.Getwd()\n\tif err == nil && runtime.GOOS == \"windows\" {\n\t\thostCurrentDir, err = mountDirFromWindowsDir(ctx, inst, hostCurrentDir)\n\t}\n\treturn hostCurrentDir, err\n}\n\nfunc rsyncVersion(ctx context.Context) (*semver.Version, error) {\n\tout, err := exec.CommandContext(ctx, string(copytool.BackendRsync), \"--version\").Output()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// `rsync  version 3.2.7  protocol version 31`\n\tre := regexp.MustCompile(`version (\\d+\\.\\d+\\.\\d+)`)\n\tmatches := re.FindSubmatch(out)\n\tif len(matches) < 2 {\n\t\treturn nil, errors.New(\"failed to parse rsync version\")\n\t}\n\treturn semver.NewVersion(string(matches[1]))\n}\n\n// 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\n// current working directory into it. The guest directory paths should be prefixed with `<InstanceName>:` followed by the path.\nfunc rsyncDirectory(ctx context.Context, cmd *cobra.Command, rsync copytool.CopyTool, paths []string) error {\n\trsyncCmd, err := rsync.Command(ctx, paths, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\trsyncCmd.Stdout = cmd.OutOrStdout()\n\trsyncCmd.Stderr = cmd.OutOrStderr()\n\tlogrus.Debugf(\"executing rsync: %+v\", rsyncCmd.Args)\n\treturn rsyncCmd.Run()\n}\n\nfunc mountDirFromWindowsDir(ctx context.Context, inst *limatype.Instance, dir string) (string, error) {\n\tif inst.VMType == limatype.WSL2 {\n\t\tdistroName := \"lima-\" + inst.Name\n\t\treturn ioutilx.WindowsSubsystemPathForLinux(ctx, dir, distroName)\n\t}\n\treturn ioutilx.WindowsSubsystemPath(ctx, dir)\n}\n\nfunc shellBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n\nfunc isEnv(arg string) bool {\n\treturn len(strings.Split(arg, \"=\")) > 1\n}\n\nfunc quoteEnv(arg string) string {\n\tenv := strings.SplitN(arg, \"=\", 2)\n\tenv[1] = shellescape.Quote(env[1])\n\treturn strings.Join(env, \"=\")\n}\n\ntype rsyncStats struct {\n\tAdded    int\n\tDeleted  int\n\tModified int\n\tMetadata int\n}\n\nfunc (s *rsyncStats) String() string {\n\tif s.Added == 0 && s.Deleted == 0 && s.Modified == 0 && s.Metadata == 0 {\n\t\treturn \"\"\n\t}\n\treturn fmt.Sprintf(\"added: %d, deleted: %d, modified: %d, metadata: %d\", s.Added, s.Deleted, s.Modified, s.Metadata)\n}\n\nfunc getRsyncStats(ctx context.Context, source, destination string) (string, *rsyncStats, error) {\n\tpaths := []string{source, destination}\n\trsync, err := copytool.New(ctx, string(copytool.BackendRsync), paths, &copytool.Options{\n\t\tVerbose: true,\n\t\tAdditionalArgs: []string{\n\t\t\t\"--dry-run\",\n\t\t\t\"--itemize-changes\",\n\t\t\t\"-ah\",\n\t\t\t\"--delete\",\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\trsyncCmd, err := rsync.Command(ctx, paths, nil)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tlogrus.Debugf(\"executing rsync for stats: %+v\", rsyncCmd.Args)\n\n\tout, err := rsyncCmd.Output()\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\toutput := string(out)\n\treturn output, parseRsyncStats(output), nil\n}\n\n// parseRsyncStats parses the output of `rsync --itemize-changes` to extract file operation statistics.\n//\n// Rsync itemized format: YXcstpoguax  path/to/file\n// Where Y=update type, X=file type, c=checksum status, and positions 3-10 are other attributes.\n//\n// Examples:\n//\n//\t>f+++++++++ file.txt    → Added (new file received)\n//\t>f.st...... file.txt    → Modified (existing file updated)\n//\t*deleting   file.txt    → Deleted\n//\n// Logic:\n//\n// - `*deleting`: Count as Deleted.\n// - Update type `<` (sent), `>` (received), or `c` (local change):\n//   - If checksum is `+` (created): Count as Added.\n//   - Otherwise: Count as Modified.\n//\n// - Update type `.` with non-`.` metadata attributes (positions 3-10):\n//   - Count as Metadata.\nfunc parseRsyncStats(output string) *rsyncStats {\n\tvar s rsyncStats\n\tfor line := range strings.SplitSeq(output, \"\\n\") {\n\t\tif len(line) < 12 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"*deleting\") {\n\t\t\ts.Deleted++\n\t\t\tcontinue\n\t\t}\n\n\t\tupdateType := line[0]\n\t\tchecksum := line[2]\n\n\t\tif updateType == '<' || updateType == '>' || updateType == 'c' {\n\t\t\tif checksum == '+' {\n\t\t\t\ts.Added++\n\t\t\t} else {\n\t\t\t\ts.Modified++\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif updateType == '.' && hasMetadataDelta(line[2:11]) {\n\t\t\ts.Metadata++\n\t\t}\n\t}\n\treturn &s\n}\n\nfunc hasMetadataDelta(attrs string) bool {\n\tfor _, ch := range attrs {\n\t\tif ch != '.' {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "cmd/limactl/shell_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestParseRsyncStats(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\toutput   string\n\t\texpected *rsyncStats\n\t}{\n\t\t{\n\t\t\tname: \"mixed output\",\n\t\t\toutput: `\n>f+++++++++ new-file.txt\ncd+++++++++ dir/\ncL+++++++++ new-symlink -> target\n*deleting deleted.txt\n`,\n\t\t\texpected: &rsyncStats{\n\t\t\t\tAdded:    3,\n\t\t\t\tDeleted:  1,\n\t\t\t\tModified: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"metadata-only changes\",\n\t\t\toutput: `\n.d..t...... ./\n.f...p..... existing.txt\n`,\n\t\t\texpected: &rsyncStats{\n\t\t\t\tAdded:    0,\n\t\t\t\tDeleted:  0,\n\t\t\t\tModified: 0,\n\t\t\t\tMetadata: 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"many changes\",\n\t\t\toutput: `\n<f+++++++++ file1\n<f+++++++++ file2\n*deleting file3\n*deleting file4\n*deleting file5\n<f..T...... file6\n`,\n\t\t\texpected: &rsyncStats{\n\t\t\t\tAdded:    2,\n\t\t\t\tDeleted:  3,\n\t\t\t\tModified: 1,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := parseRsyncStats(tt.output)\n\t\t\tassert.DeepEqual(t, got, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestRsyncStatsString(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tstats    *rsyncStats\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"empty\",\n\t\t\tstats:    &rsyncStats{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"all stats\",\n\t\t\tstats: &rsyncStats{\n\t\t\t\tAdded:    1,\n\t\t\t\tDeleted:  2,\n\t\t\t\tModified: 3,\n\t\t\t\tMetadata: 4,\n\t\t\t},\n\t\t\texpected: \"added: 1, deleted: 2, modified: 3, metadata: 4\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.Equal(t, tt.stats.String(), tt.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/limactl/show-ssh.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nconst showSSHExample = `\n  \"cmd\" format (default): Full SSH command line\n    $ limactl show-ssh --format=cmd default\n    ssh -o IdentityFile=\"/Users/example/.lima/_config/user\" -o User=example -o Hostname=127.0.0.1 -o Port=60022 lima-default\n\n  \"args\" format: Similar to the cmd format but omits \"ssh\" and the destination address\n    $ limactl show-ssh --format=args default\n    -o IdentityFile=\"/Users/example/.lima/_config/user\" -o User=example -o Hostname=127.0.0.1 -o Port=60022\n\n  \"options\" format: SSH option key value pairs\n    $ limactl show-ssh --format=options default\n    IdentityFile=\"/Users/example/.lima/_config/user\"\n    User=example\n    Hostname=127.0.0.1\n    Port=60022\n\n  \"config\" format: ~/.ssh/config format\n    $ limactl show-ssh --format=config default\n    Host lima-default\n      IdentityFile \"/Users/example/.lima/_config/user \"\n      User example\n      Hostname 127.0.0.1\n      Port 60022\n\n  To show the config file path:\n    $ limactl ls --format='{{.SSHConfigFile}}' default\n    /Users/example/.lima/default/ssh.config\n`\n\nfunc newShowSSHCommand() *cobra.Command {\n\tlimaHome := \"~/\" + dirnames.DotLima\n\tif s, err := dirnames.LimaDir(); err == nil {\n\t\tlimaHome = s\n\t}\n\tshellCmd := &cobra.Command{\n\t\tUse:   \"show-ssh [flags] INSTANCE\",\n\t\tShort: \"Show the SSH command line (DEPRECATED; use `ssh -F` instead)\",\n\t\tLong: fmt.Sprintf(`Show the SSH command line (DEPRECATED)\n\nWARNING: 'limactl show-ssh' is deprecated.\nInstead, use 'ssh -F %s/default/ssh.config lima-default' .\n`, limaHome),\n\t\tExample:           showSSHExample,\n\t\tArgs:              WrapArgsError(cobra.ExactArgs(1)),\n\t\tRunE:              showSSHAction,\n\t\tValidArgsFunction: showSSHBashComplete,\n\t\tSilenceErrors:     true,\n\t\tGroupID:           advancedCommand,\n\t}\n\n\tshellCmd.Flags().StringP(\"format\", \"f\", sshutil.FormatCmd, \"Format: \"+strings.Join(sshutil.Formats, \", \"))\n\t_ = shellCmd.RegisterFlagCompletionFunc(\"format\", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {\n\t\treturn sshutil.Formats, cobra.ShellCompDirectiveNoFileComp\n\t})\n\treturn shellCmd\n}\n\nfunc showSSHAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tformat, err := cmd.Flags().GetString(\"format\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tinstName := args[0]\n\tw := cmd.OutOrStdout()\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"instance %q does not exist, run `limactl create %s` to create a new instance\", instName, instName)\n\t\t}\n\t\treturn err\n\t}\n\tlogrus.Warnf(\"`limactl show-ssh` is deprecated. Instead, use `ssh -F %s %s`.\",\n\t\tfilepath.Join(inst.Dir, filenames.SSHConfig), inst.Hostname)\n\tsshExe, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\treturn err\n\t}\n\topts, err := sshutil.SSHOpts(\n\t\tctx,\n\t\tsshExe,\n\t\tinst.Dir,\n\t\t*inst.Config.User.Name,\n\t\t*inst.Config.SSH.LoadDotSSHPubKeys,\n\t\t*inst.Config.SSH.ForwardAgent,\n\t\t*inst.Config.SSH.ForwardX11,\n\t\t*inst.Config.SSH.ForwardX11Trusted)\n\tif err != nil {\n\t\treturn err\n\t}\n\topts = append(opts, \"Hostname=127.0.0.1\")\n\topts = append(opts, fmt.Sprintf(\"Port=%d\", inst.SSHLocalPort))\n\treturn sshutil.Format(w, \"ssh\", instName, format, opts)\n}\n\nfunc showSSHBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/snapshot.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/snapshot\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newSnapshotCommand() *cobra.Command {\n\tsnapshotCmd := &cobra.Command{\n\t\tUse:   \"snapshot\",\n\t\tShort: \"Manage instance snapshots\",\n\t\tExample: `  List all snapshots of an instance:\n  $ limactl snapshot list default\n\n  Create a snapshot:\n  $ limactl snapshot create default --tag snap1\n\n  Apply (restore) a snapshot:\n  $ limactl snapshot apply default --tag snap1\n\n  Delete a snapshot:\n  $ limactl snapshot delete default --tag snap1\n`,\n\t\tPersistentPreRun: func(*cobra.Command, []string) {\n\t\t\tlogrus.Warn(\"`limactl snapshot` is experimental\")\n\t\t},\n\t\tGroupID: advancedCommand,\n\t}\n\tsnapshotCmd.AddCommand(newSnapshotApplyCommand())\n\tsnapshotCmd.AddCommand(newSnapshotCreateCommand())\n\tsnapshotCmd.AddCommand(newSnapshotDeleteCommand())\n\tsnapshotCmd.AddCommand(newSnapshotListCommand())\n\n\treturn snapshotCmd\n}\n\nfunc newSnapshotCreateCommand() *cobra.Command {\n\tcreateCmd := &cobra.Command{\n\t\tUse:     \"create INSTANCE\",\n\t\tAliases: []string{\"save\"},\n\t\tShort:   \"Create (save) a snapshot\",\n\t\tExample: `  Create a snapshot of an instance:\n  $ limactl snapshot create default --tag snap1\n`,\n\t\tArgs:              cobra.MinimumNArgs(1),\n\t\tRunE:              snapshotCreateAction,\n\t\tValidArgsFunction: snapshotBashComplete,\n\t}\n\tcreateCmd.Flags().String(\"tag\", \"\", \"Name of the snapshot\")\n\n\treturn createCmd\n}\n\nfunc snapshotCreateAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := args[0]\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttag, err := cmd.Flags().GetString(\"tag\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif tag == \"\" {\n\t\treturn errors.New(\"expected tag\")\n\t}\n\n\treturn snapshot.Save(ctx, inst, tag)\n}\n\nfunc newSnapshotDeleteCommand() *cobra.Command {\n\tdeleteCmd := &cobra.Command{\n\t\tUse:     \"delete INSTANCE\",\n\t\tAliases: []string{\"del\"},\n\t\tShort:   \"Delete (del) a snapshot\",\n\t\tExample: `  Delete a snapshot:\n  $ limactl snapshot delete default --tag snap1\n`,\n\t\tArgs:              cobra.MinimumNArgs(1),\n\t\tRunE:              snapshotDeleteAction,\n\t\tValidArgsFunction: snapshotBashComplete,\n\t}\n\tdeleteCmd.Flags().String(\"tag\", \"\", \"Name of the snapshot\")\n\n\treturn deleteCmd\n}\n\nfunc snapshotDeleteAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := args[0]\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttag, err := cmd.Flags().GetString(\"tag\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif tag == \"\" {\n\t\treturn errors.New(\"expected tag\")\n\t}\n\n\treturn snapshot.Del(ctx, inst, tag)\n}\n\nfunc newSnapshotApplyCommand() *cobra.Command {\n\tapplyCmd := &cobra.Command{\n\t\tUse:     \"apply INSTANCE\",\n\t\tAliases: []string{\"load\"},\n\t\tShort:   \"Apply (load) a snapshot\",\n\t\tExample: `  Apply (restore) a snapshot:\n  $ limactl snapshot apply default --tag snap1\n`,\n\t\tArgs:              cobra.MinimumNArgs(1),\n\t\tRunE:              snapshotApplyAction,\n\t\tValidArgsFunction: snapshotBashComplete,\n\t}\n\tapplyCmd.Flags().String(\"tag\", \"\", \"Name of the snapshot\")\n\n\treturn applyCmd\n}\n\nfunc snapshotApplyAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := args[0]\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttag, err := cmd.Flags().GetString(\"tag\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif tag == \"\" {\n\t\treturn errors.New(\"expected tag\")\n\t}\n\n\treturn snapshot.Load(ctx, inst, tag)\n}\n\nfunc newSnapshotListCommand() *cobra.Command {\n\tlistCmd := &cobra.Command{\n\t\tUse:     \"list INSTANCE\",\n\t\tAliases: []string{\"ls\"},\n\t\tShort:   \"List existing snapshots\",\n\t\tExample: `  List all snapshots of an instance:\n  $ limactl snapshot list default\n\n  List only snapshot tags:\n  $ limactl snapshot list default --quiet\n`,\n\t\tArgs:              cobra.MinimumNArgs(1),\n\t\tRunE:              snapshotListAction,\n\t\tValidArgsFunction: snapshotBashComplete,\n\t}\n\tlistCmd.Flags().BoolP(\"quiet\", \"q\", false, \"Only show tags\")\n\n\treturn listCmd\n}\n\nfunc snapshotListAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := args[0]\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tquiet, err := cmd.Flags().GetBool(\"quiet\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tout, err := snapshot.List(ctx, inst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif quiet {\n\t\tfor i, line := range strings.Split(out, \"\\n\") {\n\t\t\t// \"ID\", \"TAG\", \"VM SIZE\", \"DATE\", \"VM CLOCK\", \"ICOUNT\"\n\t\t\tfields := strings.Fields(line)\n\t\t\tif i == 0 && len(fields) > 1 && fields[1] != \"TAG\" {\n\t\t\t\t// make sure that output matches the expected\n\t\t\t\treturn fmt.Errorf(\"unknown header: %s\", line)\n\t\t\t}\n\t\t\tif i == 0 || line == \"\" {\n\t\t\t\t// skip header and empty line after using split\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttag := fields[1]\n\t\t\tfmt.Fprintf(cmd.OutOrStdout(), \"%s\\n\", tag)\n\t\t}\n\t\treturn nil\n\t}\n\tfmt.Fprint(cmd.OutOrStdout(), out)\n\treturn nil\n}\n\nfunc snapshotBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/start-at-login.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nfunc newStartAtLoginCommand() *cobra.Command {\n\tstartAtLoginCommand := &cobra.Command{\n\t\tUse:               \"start-at-login INSTANCE\",\n\t\tShort:             \"Register/Unregister an autostart file for the instance\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tRunE:              startAtLoginAction,\n\t\tValidArgsFunction: startAtLoginComplete,\n\t\tGroupID:           advancedCommand,\n\t}\n\n\tstartAtLoginCommand.Flags().Bool(\n\t\t\"enabled\", true,\n\t\t\"Automatically start the instance when the user logs in\",\n\t)\n\n\treturn startAtLoginCommand\n}\n\nfunc startAtLoginComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/start-at-login_unix.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc startAtLoginAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := DefaultInstanceName\n\tif len(args) > 0 {\n\t\tinstName = args[0]\n\t}\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tlogrus.Infof(\"Instance %q not found\", instName)\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\n\tflags := cmd.Flags()\n\tstartAtLogin, err := flags.GetBool(\"enabled\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif registered, err := autostart.IsRegistered(ctx, inst); err != nil {\n\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t} else if startAtLogin {\n\t\tverb := \"create\"\n\t\tif registered {\n\t\t\tverb = \"update\"\n\t\t}\n\t\tif err := autostart.RegisterToStartAtLogin(ctx, inst); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to %s the autostart entry for instance %q: %w\", verb, inst.Name, err)\n\t\t}\n\t\tlogrus.Infof(\"The autostart entry for instance %q has been %sd\", inst.Name, verb)\n\t} else {\n\t\tif !registered {\n\t\t\tlogrus.Infof(\"The autostart entry for instance %q is not registered\", inst.Name)\n\t\t} else if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unregister the autostart entry for instance %q: %w\", inst.Name, err)\n\t\t} else {\n\t\t\tlogrus.Infof(\"The autostart entry for instance %q has been unregistered\", inst.Name)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/limactl/start-at-login_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc startAtLoginAction(_ *cobra.Command, _ []string) error {\n\treturn errors.New(\"start-at-login command is only supported on macOS and Linux right now\")\n}\n"
  },
  {
    "path": "cmd/limactl/start.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n\n\t\"github.com/lima-vm/lima/v2/cmd/limactl/editflags\"\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/editutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatmpl\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/reconcile\"\n\t\"github.com/lima-vm/lima/v2/pkg/registry\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/templatestore\"\n\t\"github.com/lima-vm/lima/v2/pkg/uiutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nfunc registerCreateFlags(cmd *cobra.Command, commentPrefix string) {\n\tflags := cmd.Flags()\n\tflags.String(\"name\", \"\", commentPrefix+\"Override the instance name\")\n\tflags.Bool(\"list-templates\", false, commentPrefix+\"List available templates and exit\")\n\tflags.Bool(\"list-drivers\", false, commentPrefix+\"List available drivers and exit\")\n\teditflags.RegisterCreate(cmd, commentPrefix)\n}\n\nfunc newCreateCommand() *cobra.Command {\n\tcreateCommand := &cobra.Command{\n\t\tUse: \"create FILE.yaml|URL\",\n\t\tExample: `\n  To create an instance \"default\" from the default Ubuntu template:\n  $ limactl create\n\n  To create an instance \"default\" from a template \"docker\":\n  $ limactl create --name=default template:docker\n\n  To create an instance \"default\" with modified parameters:\n  $ limactl create --cpus=2 --memory=2\n\n  To create an instance \"default\" with yq expressions:\n  $ limactl create --set='.cpus = 2 | .memory = \"2GiB\"'\n\n  To see the template list:\n  $ limactl create --list-templates\n\n  To create an instance \"default\" from a local file:\n  $ limactl create --name=default /usr/local/share/lima/templates/fedora.yaml\n\n  To create an instance \"default\" from a remote URL (use carefully, with a trustable source):\n  $ limactl create --name=default https://raw.githubusercontent.com/lima-vm/lima/master/templates/alpine.yaml\n\n  To create an instance \"local\" from a template passed to stdin (--name parameter is required):\n  $ cat template.yaml | limactl create --name=local -\n`,\n\t\tShort:             \"Create an instance of Lima\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tValidArgsFunction: createBashComplete,\n\t\tRunE:              createAction,\n\t\tGroupID:           basicCommand,\n\t}\n\tregisterCreateFlags(createCommand, \"\")\n\treturn createCommand\n}\n\nfunc newStartCommand() *cobra.Command {\n\tstartCommand := &cobra.Command{\n\t\tUse: \"start NAME|FILE.yaml|URL\",\n\t\tExample: `\n  To create an instance \"default\" (if not created yet) from the default Ubuntu template, and start it:\n  $ limactl start\n\n  To create an instance \"default\" from a template \"docker\", and start it:\n  $ limactl start --name=default template:docker\n`,\n\t\tShort:             \"Start an instance of Lima\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tValidArgsFunction: startBashComplete,\n\t\tRunE:              startAction,\n\t\tGroupID:           basicCommand,\n\t}\n\tregisterCreateFlags(startCommand, \"[limactl create] \")\n\tif runtime.GOOS != \"windows\" {\n\t\tstartCommand.Flags().Bool(\"foreground\", false, \"Run the hostagent in the foreground\")\n\t}\n\tstartCommand.Flags().Duration(\"timeout\", instance.DefaultWatchHostAgentEventsTimeout, \"Duration to wait for the instance to be running before timing out\")\n\tstartCommand.Flags().Bool(\"progress\", false, \"Show provision script progress by tailing cloud-init logs\")\n\tstartCommand.SetHelpFunc(func(cmd *cobra.Command, _ []string) {\n\t\tprintCommandSummary(cmd)\n\n\t\tallFlags, createFlags := collectFlags(cmd)\n\t\tprintFlags(allFlags, createFlags)\n\n\t\tprintGlobalFlags(cmd)\n\t})\n\n\treturn startCommand\n}\n\nfunc printCommandSummary(cmd *cobra.Command) {\n\tfmt.Fprintf(cmd.OutOrStdout(), \"%s\\n\\n\", cmd.Short)\n\tfmt.Fprintf(cmd.OutOrStdout(), \"Usage:\\n  %s\\n\\n\", cmd.UseLine())\n\n\tif cmd.Example != \"\" {\n\t\tfmt.Fprintf(cmd.OutOrStdout(), \"Examples:\\n%s\\n\\n\", cmd.Example)\n\t}\n}\n\nfunc getFlagType(flag *pflag.Flag) string {\n\tswitch flag.Value.Type() {\n\tcase \"bool\":\n\t\treturn \"\"\n\tcase \"string\":\n\t\treturn \"string\"\n\tcase \"int\":\n\t\treturn \"int\"\n\tcase \"duration\":\n\t\treturn \"duration\"\n\tcase \"stringSlice\", \"stringArray\":\n\t\treturn \"strings\"\n\tcase \"ipSlice\":\n\t\treturn \"ipSlice\"\n\tcase \"uint16\":\n\t\treturn \"uint16\"\n\tcase \"float32\":\n\t\treturn \"float32\"\n\tdefault:\n\t\treturn flag.Value.Type()\n\t}\n}\n\nfunc formatFlag(flag *pflag.Flag) (flagName, shorthand string) {\n\tflagName = \"--\" + flag.Name\n\n\tif flag.Shorthand != \"\" {\n\t\tshorthand = \"-\" + flag.Shorthand\n\t}\n\n\tflagType := getFlagType(flag)\n\tif flagType != \"\" {\n\t\tflagName += \" \" + flagType\n\t}\n\n\treturn flagName, shorthand\n}\n\nfunc collectFlags(cmd *cobra.Command) (allFlags, createFlags []string) {\n\tcmd.LocalFlags().VisitAll(func(flag *pflag.Flag) {\n\t\tflagName, shorthand := formatFlag(flag)\n\t\tflagUsage := flag.Usage\n\n\t\tvar formattedFlag string\n\t\tif shorthand != \"\" {\n\t\t\tformattedFlag = fmt.Sprintf(\"  %s, %s\", shorthand, flagName)\n\t\t} else {\n\t\t\tformattedFlag = fmt.Sprintf(\"      %s\", flagName)\n\t\t}\n\n\t\tif strings.HasPrefix(flagUsage, \"[limactl create]\") {\n\t\t\tcleanUsage := strings.TrimPrefix(flagUsage, \"[limactl create] \")\n\t\t\tcreateFlags = append(createFlags, fmt.Sprintf(\"%-25s %s\", formattedFlag, cleanUsage))\n\t\t} else {\n\t\t\tallFlags = append(allFlags, fmt.Sprintf(\"%-25s %s\", formattedFlag, flagUsage))\n\t\t}\n\t})\n\treturn allFlags, createFlags\n}\n\nfunc printFlags(allFlags, createFlags []string) {\n\tif len(allFlags) > 0 {\n\t\tfmt.Fprint(os.Stdout, \"Flags:\\n\")\n\t\tfor _, flag := range allFlags {\n\t\t\tfmt.Fprintln(os.Stdout, flag)\n\t\t}\n\t\tfmt.Fprint(os.Stdout, \"\\n\")\n\t}\n\n\tif len(createFlags) > 0 {\n\t\tfmt.Fprint(os.Stdout, \"Flags inherited from `limactl create`:\\n\")\n\t\tfor _, flag := range createFlags {\n\t\t\tfmt.Fprintln(os.Stdout, flag)\n\t\t}\n\t\tfmt.Fprint(os.Stdout, \"\\n\")\n\t}\n}\n\nfunc printGlobalFlags(cmd *cobra.Command) {\n\tif cmd.HasAvailableInheritedFlags() {\n\t\tfmt.Fprintf(cmd.OutOrStdout(), \"Global Flags:\\n%s\", cmd.InheritedFlags().FlagUsages())\n\t}\n}\n\nfunc loadOrCreateInstance(cmd *cobra.Command, args []string, createOnly bool) (*limatype.Instance, error) {\n\tctx := cmd.Context()\n\tvar arg string // can be empty\n\tif len(args) > 0 {\n\t\targ = args[0]\n\t}\n\n\tflags := cmd.Flags()\n\n\t// Create an instance, with menu TUI when TTY is available\n\ttty, err := flags.GetBool(\"tty\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tname, err := flags.GetString(\"name\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif name != \"\" {\n\t\terr := dirnames.ValidateInstName(name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif isTemplateURL, templateName := limatmpl.SeemsTemplateURL(arg); isTemplateURL {\n\t\tswitch templateName {\n\t\tcase \"experimental/vz\":\n\t\t\tlogrus.Warn(\"template:experimental/vz was merged into the default template in Lima v1.0. See also <https://lima-vm.io/docs/config/vmtype/>.\")\n\t\tcase \"experimental/riscv64\":\n\t\t\tlogrus.Warn(\"template:experimental/riscv64 was merged into the default template in Lima v1.0. Use `limactl create --arch=riscv64 template:default` instead.\")\n\t\tcase \"experimental/armv7l\":\n\t\t\tlogrus.Warn(\"template:experimental/armv7l was merged into the default template in Lima v1.0. Use `limactl create --arch=armv7l template:default` instead.\")\n\t\tcase \"vmnet\":\n\t\t\tlogrus.Warn(\"template:vmnet was removed in Lima v1.0. Use `limactl create --network=lima:shared template:default` instead. See also <https://lima-vm.io/docs/config/network/>.\")\n\t\tcase \"experimental/net-user-v2\":\n\t\t\tlogrus.Warn(\"template:experimental/net-user-v2 was removed in Lima v1.0. Use `limactl create --network=lima:user-v2 template:default` instead. See also <https://lima-vm.io/docs/config/network/>.\")\n\t\tcase \"experimental/9p\":\n\t\t\tlogrus.Warn(\"template:experimental/9p was removed in Lima v1.0. Use `limactl create --vm-type=qemu --mount-type=9p template:default` instead. See also <https://lima-vm.io/docs/config/mount/>.\")\n\t\tcase \"experimental/virtiofs-linux\":\n\t\t\tlogrus.Warn(\"template:experimental/virtiofs-linux was removed in Lima v1.0. Use `limactl create --mount-type=virtiofs template:default` instead. See also <https://lima-vm.io/docs/config/mount/>.\")\n\t\t}\n\t}\n\tif arg == \"-\" {\n\t\tif name == \"\" {\n\t\t\treturn nil, errors.New(\"must pass instance name with --name when reading template from stdin\")\n\t\t}\n\t\t// see if the tty was set explicitly or not\n\t\tttySet := cmd.Flags().Changed(\"tty\")\n\t\tif ttySet && tty {\n\t\t\treturn nil, errors.New(\"cannot use --tty=true when reading template from stdin\")\n\t\t}\n\t\ttty = false\n\t}\n\tvar tmpl *limatmpl.Template\n\tif err := dirnames.ValidateInstName(arg); arg == \"\" || err == nil {\n\t\ttmpl = &limatmpl.Template{Name: name}\n\t\tif arg == \"\" {\n\t\t\tif name == \"\" {\n\t\t\t\ttmpl.Name = DefaultInstanceName\n\t\t\t}\n\t\t} else {\n\t\t\tlogrus.Debugf(\"interpreting argument %q as an instance name\", arg)\n\t\t\tif name != \"\" && name != arg {\n\t\t\t\treturn nil, fmt.Errorf(\"instance name %q and CLI flag --name=%q cannot be specified together\", arg, tmpl.Name)\n\t\t\t}\n\t\t\ttmpl.Name = arg\n\t\t}\n\t\t// store.Inspect() will validate the template name (in case it has been set to arg)\n\t\tinst, err := store.Inspect(ctx, tmpl.Name)\n\t\tif err == nil {\n\t\t\tif createOnly {\n\t\t\t\treturn nil, fmt.Errorf(\"instance %q already exists\", tmpl.Name)\n\t\t\t}\n\t\t\tlogrus.Infof(\"Using the existing instance %q\", tmpl.Name)\n\t\t\tyqExprs, err := editflags.YQExpressions(flags, false)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif len(yqExprs) > 0 {\n\t\t\t\tyq := yqutil.Join(yqExprs)\n\t\t\t\tinst, err = applyYQExpressionToExistingInstance(ctx, inst, yq)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to apply yq expression %q to instance %q: %w\", yq, tmpl.Name, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn inst, nil\n\t\t}\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\t\tif arg != \"\" && arg != DefaultInstanceName {\n\t\t\tlogrus.Infof(\"Creating an instance %q from template:default (Not from template:%s)\", tmpl.Name, tmpl.Name)\n\t\t\tlogrus.Warnf(\"This form is deprecated. Use `limactl create --name=%s template:default` instead\", tmpl.Name)\n\t\t}\n\t\t// Read the default template for creating a new instance\n\t\ttmpl.Bytes, err = templatestore.Read(templatestore.Default)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\ttmpl, err = limatmpl.Read(cmd.Context(), name, arg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif createOnly {\n\t\t\t// store.Inspect() will also validate the instance name\n\t\t\tif _, err := store.Inspect(ctx, tmpl.Name); err == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"instance %q already exists\", tmpl.Name)\n\t\t\t}\n\t\t} else if err := dirnames.ValidateInstName(tmpl.Name); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif err := tmpl.Embed(cmd.Context(), true, true); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := tmpl.Unmarshal(); err != nil {\n\t\treturn nil, err\n\t}\n\tif tmpl.Config != nil && tmpl.Config.OS != nil && *tmpl.Config.OS != limatype.LINUX {\n\t\tlogrus.Warn(\"Support for non-Linux guests is experimental\")\n\t}\n\tyqExprs, err := editflags.YQExpressions(flags, true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tyq := yqutil.Join(yqExprs)\n\tif tty {\n\t\tvar err error\n\t\ttmpl, err = chooseNextCreatorState(cmd.Context(), tmpl, yq)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tlogrus.Info(\"Terminal is not available, proceeding without opening an editor\")\n\t\tif err := modifyInPlace(tmpl, yq); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tsaveBrokenYAML := tty\n\treturn instance.Create(cmd.Context(), tmpl.Name, tmpl.Bytes, saveBrokenYAML)\n}\n\nfunc applyYQExpressionToExistingInstance(ctx context.Context, inst *limatype.Instance, yq string) (*limatype.Instance, error) {\n\tif strings.TrimSpace(yq) == \"\" {\n\t\treturn inst, nil\n\t}\n\tfilePath := filepath.Join(inst.Dir, filenames.LimaYAML)\n\tyContent, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlogrus.Debugf(\"Applying yq expression %q to an existing instance %q\", yq, inst.Name)\n\tyBytes, err := yqutil.EvaluateExpression(yq, yContent)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ty, err := limayaml.Load(ctx, yBytes, filePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := driverutil.ResolveVMType(ctx, y, filePath); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to resolve vm for %q: %w\", filePath, err)\n\t}\n\tif err := limayaml.Validate(y, true); err != nil {\n\t\trejectedYAML := \"lima.REJECTED.yaml\"\n\t\tif writeErr := os.WriteFile(rejectedYAML, yBytes, 0o644); writeErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"the YAML is invalid, attempted to save the buffer as %q but failed: %w: %w\", rejectedYAML, writeErr, err)\n\t\t}\n\t\t// TODO: may need to support editing the rejected YAML\n\t\treturn nil, fmt.Errorf(\"the YAML is invalid, saved the buffer as %q: %w\", rejectedYAML, err)\n\t}\n\tif err := os.WriteFile(filePath, yBytes, 0o644); err != nil {\n\t\treturn nil, err\n\t}\n\t// Reload\n\treturn store.Inspect(ctx, inst.Name)\n}\n\nfunc modifyInPlace(st *limatmpl.Template, yq string) error {\n\tout, err := yqutil.EvaluateExpression(yq, st.Bytes)\n\tif err != nil {\n\t\treturn err\n\t}\n\tst.Bytes = out\n\treturn nil\n}\n\n// exitSuccessError is an error that indicates a successful exit.\ntype exitSuccessError struct {\n\tMsg string\n}\n\n// Error implements error.\nfunc (e exitSuccessError) Error() string {\n\treturn e.Msg\n}\n\n// ExitCode implements ExitCoder.\nfunc (exitSuccessError) ExitCode() int {\n\treturn 0\n}\n\nfunc chooseNextCreatorState(ctx context.Context, tmpl *limatmpl.Template, yq string) (*limatmpl.Template, error) {\n\tfor {\n\t\tif err := modifyInPlace(tmpl, yq); err != nil {\n\t\t\tlogrus.WithError(err).Warn(\"Failed to evaluate yq expression\")\n\t\t\treturn tmpl, err\n\t\t}\n\t\tmessage := fmt.Sprintf(\"Creating an instance %q\", tmpl.Name)\n\t\toptions := []string{\n\t\t\t\"Proceed with the current configuration\",\n\t\t\t\"Open an editor to review or modify the current configuration\",\n\t\t\t\"Choose another template (docker, podman, archlinux, fedora, ...)\",\n\t\t\t\"Exit\",\n\t\t}\n\t\tans, err := uiutil.Select(message, options)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, uiutil.InterruptErr) {\n\t\t\t\tlogrus.Fatal(\"Interrupted by user\")\n\t\t\t}\n\t\t\tlogrus.WithError(err).Warn(\"Failed to open TUI\")\n\t\t\treturn tmpl, nil\n\t\t}\n\t\tswitch ans {\n\t\tcase 0: // \"Proceed with the current configuration\"\n\t\t\treturn tmpl, nil\n\t\tcase 1: // \"Open an editor ...\"\n\t\t\thdr := fmt.Sprintf(\"# Review and modify the following configuration for Lima instance %q.\\n\", tmpl.Name)\n\t\t\tif tmpl.Name == DefaultInstanceName {\n\t\t\t\thdr += \"# - In most cases, you do not need to modify this file.\\n\"\n\t\t\t}\n\t\t\thdr += \"# - To cancel starting Lima, just save this file as an empty file.\\n\"\n\t\t\thdr += \"\\n\"\n\t\t\thdr += editutil.GenerateEditorWarningHeader()\n\t\t\tvar err error\n\t\t\ttmpl.Bytes, err = editutil.OpenEditor(ctx, tmpl.Bytes, hdr)\n\t\t\ttmpl.Config = nil\n\t\t\tif err != nil {\n\t\t\t\treturn tmpl, err\n\t\t\t}\n\t\t\tif len(tmpl.Bytes) == 0 {\n\t\t\t\tconst msg = \"Aborting, as requested by saving the file with empty content\"\n\t\t\t\tlogrus.Info(msg)\n\t\t\t\treturn nil, exitSuccessError{Msg: msg}\n\t\t\t}\n\t\t\terr = tmpl.Embed(ctx, true, true)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn tmpl, nil\n\t\tcase 2: // \"Choose another template...\"\n\t\t\ttemplates, err := filterHiddenTemplates()\n\t\t\tif err != nil {\n\t\t\t\treturn tmpl, err\n\t\t\t}\n\t\t\tmessage := \"Choose a template\"\n\t\t\toptions := make([]string, len(templates))\n\t\t\tfor i := range templates {\n\t\t\t\toptions[i] = templates[i].Name\n\t\t\t}\n\t\t\tansEx, err := uiutil.Select(message, options)\n\t\t\tif err != nil {\n\t\t\t\treturn tmpl, err\n\t\t\t}\n\t\t\tif ansEx > len(templates)-1 {\n\t\t\t\treturn tmpl, fmt.Errorf(\"invalid answer %d for %d entries\", ansEx, len(templates))\n\t\t\t}\n\t\t\tyamlPath := templates[ansEx].Location\n\t\t\tif tmpl.Name == \"\" {\n\t\t\t\ttmpl.Name, err = limatmpl.InstNameFromYAMLPath(yamlPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\ttmpl, err = limatmpl.Read(ctx, tmpl.Name, yamlPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\terr = tmpl.Embed(ctx, true, true)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcontinue\n\t\tcase 3: // \"Exit\"\n\t\t\treturn nil, exitSuccessError{Msg: \"Choosing to exit\"}\n\t\tdefault:\n\t\t\treturn tmpl, fmt.Errorf(\"unexpected answer %q\", ans)\n\t\t}\n\t}\n}\n\n// createStartActionCommon is shared by createAction and startAction.\nfunc createStartActionCommon(cmd *cobra.Command, _ []string) (exit bool, err error) {\n\tif listTemplates, err := cmd.Flags().GetBool(\"list-templates\"); err != nil {\n\t\treturn true, err\n\t} else if listTemplates {\n\t\ttemplates, err := filterHiddenTemplates()\n\t\tif err != nil {\n\t\t\treturn true, err\n\t\t}\n\t\tw := cmd.OutOrStdout()\n\t\tfor _, f := range templates {\n\t\t\t_, _ = fmt.Fprintln(w, f.Name)\n\t\t}\n\t\treturn true, nil\n\t} else if listDrivers, err := cmd.Flags().GetBool(\"list-drivers\"); err != nil {\n\t\treturn true, err\n\t} else if listDrivers {\n\t\tw := cmd.OutOrStdout()\n\t\tfor k := range registry.List() {\n\t\t\t_, _ = fmt.Fprintln(w, k)\n\t\t}\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n\nfunc filterHiddenTemplates() ([]templatestore.Template, error) {\n\ttemplates, err := templatestore.Templates()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar filtered []templatestore.Template\n\tfor _, f := range templates {\n\t\t// Don't show internal base templates like `_default/*` and `_images/*`.\n\t\tif !strings.HasPrefix(f.Name, \"_\") {\n\t\t\tfiltered = append(filtered, f)\n\t\t}\n\t}\n\treturn filtered, nil\n}\n\nfunc createAction(cmd *cobra.Command, args []string) error {\n\tif exit, err := createStartActionCommon(cmd, args); err != nil {\n\t\treturn err\n\t} else if exit {\n\t\treturn nil\n\t}\n\tinst, err := loadOrCreateInstance(cmd, args, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(inst.Errors) > 0 {\n\t\treturn fmt.Errorf(\"errors inspecting instance: %+v\", inst.Errors)\n\t}\n\tif _, err = instance.Prepare(cmd.Context(), inst, \"\"); err != nil {\n\t\treturn err\n\t}\n\tlogrus.Infof(\"Run `limactl start %s` to start the instance.\", inst.Name)\n\treturn nil\n}\n\nfunc startAction(cmd *cobra.Command, args []string) error {\n\tif exit, err := createStartActionCommon(cmd, args); err != nil {\n\t\treturn err\n\t} else if exit {\n\t\treturn nil\n\t}\n\tinst, err := loadOrCreateInstance(cmd, args, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(inst.Errors) > 0 {\n\t\treturn fmt.Errorf(\"errors inspecting instance: %+v\", inst.Errors)\n\t}\n\tswitch inst.Status {\n\tcase limatype.StatusRunning:\n\t\tlogrus.Infof(\"The instance %q is already running. Run `%s` to open the shell.\",\n\t\t\tinst.Name, instance.LimactlShellCmd(inst.Name))\n\t\t// Not an error\n\t\treturn nil\n\tcase limatype.StatusStopped:\n\t\t// NOP\n\tdefault:\n\t\tlogrus.Warnf(\"expected status %q, got %q\", limatype.StatusStopped, inst.Status)\n\t}\n\tctx := cmd.Context()\n\t// Network reconciliation will be performed by the process launched by the autostart manager\n\tif registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t} else if (registered && autostart.AutoStartedIdentifier() != \"\") || !registered {\n\t\terr = reconcile.Reconcile(ctx, inst.Name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tlaunchHostAgentForeground := false\n\tif runtime.GOOS != \"windows\" {\n\t\tforeground, err := cmd.Flags().GetBool(\"foreground\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlaunchHostAgentForeground = foreground\n\t}\n\ttimeout, err := cmd.Flags().GetDuration(\"timeout\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif timeout > 0 {\n\t\tctx = instance.WithWatchHostAgentTimeout(ctx, timeout)\n\t}\n\n\tprogress, err := cmd.Flags().GetBool(\"progress\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn instance.Start(ctx, inst, launchHostAgentForeground, progress)\n}\n\nfunc createBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteTemplateNames(cmd, toComplete)\n}\n\nfunc startBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\tcompInst, _ := bashCompleteInstanceNames(cmd)\n\tcompTmpl, _ := bashCompleteTemplateNames(cmd, toComplete)\n\treturn append(compInst, compTmpl...), cobra.ShellCompDirectiveDefault\n}\n"
  },
  {
    "path": "cmd/limactl/stop.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/instance\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/reconcile\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newStopCommand() *cobra.Command {\n\tstopCmd := &cobra.Command{\n\t\tUse:               \"stop INSTANCE\",\n\t\tShort:             \"Stop an instance\",\n\t\tArgs:              WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tRunE:              stopAction,\n\t\tValidArgsFunction: stopBashComplete,\n\t\tGroupID:           basicCommand,\n\t}\n\n\tstopCmd.Flags().BoolP(\"force\", \"f\", false, \"Force stop the instance\")\n\treturn stopCmd\n}\n\nfunc stopAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := DefaultInstanceName\n\tif len(args) > 0 {\n\t\tinstName = args[0]\n\t}\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tforce, err := cmd.Flags().GetBool(\"force\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif force {\n\t\tinstance.StopForcibly(inst)\n\t} else {\n\t\terr = instance.StopGracefully(ctx, inst, false)\n\t}\n\t// TODO: should we also reconcile networks if graceful stop returned an error?\n\tif err == nil {\n\t\terr = reconcile.Reconcile(ctx, \"\")\n\t}\n\treturn err\n}\n\nfunc stopBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/sudoers.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n)\n\nconst socketVMNetURL = \"https://lima-vm.io/docs/config/network/vmnet/#socket_vmnet\"\n\n// newSudoersCommand is specific to macOS, but the help message is\n// compiled on Linux too, as depended by `make docsy`.\n// https://github.com/lima-vm/lima/issues/3436\nfunc newSudoersCommand() *cobra.Command {\n\tsudoersCommand := &cobra.Command{\n\t\tUse: \"sudoers [--check [SUDOERSFILE-TO-CHECK]]\",\n\t\tExample: `\nTo generate the /etc/sudoers.d/lima file:\n$ limactl sudoers | sudo tee /etc/sudoers.d/lima\n\nTo validate the existing /etc/sudoers.d/lima file:\n$ limactl sudoers --check /etc/sudoers.d/lima\n`,\n\t\tShort: \"Generate the content of the /etc/sudoers.d/lima file\",\n\t\tLong: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support (socket_vmnet) on macOS.\nThe content is written to stdout, NOT to the file.\nThis command must not run as the root user.\nSee %s for the usage.`, socketVMNetURL),\n\t\tArgs:    WrapArgsError(cobra.MaximumNArgs(1)),\n\t\tRunE:    sudoersAction,\n\t\tGroupID: advancedCommand,\n\t}\n\tcfgFile, _ := networks.ConfigFile()\n\tsudoersCommand.Flags().Bool(\"check\", false,\n\t\tfmt.Sprintf(\"check that the sudoers file is up-to-date with %q\", cfgFile))\n\treturn sudoersCommand\n}\n"
  },
  {
    "path": "cmd/limactl/sudoers_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n)\n\nfunc sudoersAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tnwCfg, err := networks.LoadConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Make sure the current network configuration is secure\n\tif err := nwCfg.Validate(); err != nil {\n\t\tlogrus.Infof(\"Please check %s for more information.\", socketVMNetURL)\n\t\treturn err\n\t}\n\tcheck, err := cmd.Flags().GetBool(\"check\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif check {\n\t\treturn verifySudoAccess(ctx, nwCfg, args, cmd.OutOrStdout())\n\t}\n\tswitch len(args) {\n\tcase 0:\n\t\t// NOP\n\tcase 1:\n\t\treturn errors.New(\"the file argument can be specified only for --check mode\")\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected arguments %v\", args)\n\t}\n\tsudoers, err := networks.Sudoers()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfmt.Fprint(cmd.OutOrStdout(), sudoers)\n\treturn nil\n}\n\nfunc verifySudoAccess(ctx context.Context, nwCfg networks.Config, args []string, stdout io.Writer) error {\n\tvar file string\n\tswitch len(args) {\n\tcase 0:\n\t\tfile = nwCfg.Paths.Sudoers\n\t\tif file == \"\" {\n\t\t\tcfgFile, _ := networks.ConfigFile()\n\t\t\treturn fmt.Errorf(\"no sudoers file defined in %q\", cfgFile)\n\t\t}\n\tcase 1:\n\t\tfile = args[0]\n\tdefault:\n\t\treturn errors.New(\"can check only a single sudoers file\")\n\t}\n\tif err := nwCfg.VerifySudoAccess(ctx, file); err != nil {\n\t\treturn err\n\t}\n\tfmt.Fprintf(stdout, \"%q is up-to-date (or sudo doesn't require a password)\\n\", file)\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/limactl/sudoers_nodarwin.go",
    "content": "//go:build !darwin\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nfunc sudoersAction(_ *cobra.Command, _ []string) error {\n\treturn errors.New(\"sudoers command is only supported on macOS right now\")\n}\n"
  },
  {
    "path": "cmd/limactl/template.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatmpl\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/uiutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nfunc newTemplateCommand() *cobra.Command {\n\ttemplateCommand := &cobra.Command{\n\t\tUse:           \"template\",\n\t\tAliases:       []string{\"tmpl\"},\n\t\tShort:         \"Lima template management (EXPERIMENTAL)\",\n\t\tSilenceUsage:  true,\n\t\tSilenceErrors: true,\n\t\tGroupID:       advancedCommand,\n\t\tPreRun: func(*cobra.Command, []string) {\n\t\t\tlogrus.Warn(\"`limactl template` is experimental\")\n\t\t},\n\t}\n\ttemplateCommand.AddCommand(\n\t\tnewTemplateCopyCommand(),\n\t\tnewTemplateValidateCommand(),\n\t\tnewTemplateYQCommand(),\n\t\tnewTemplateURLCommand(),\n\t)\n\treturn templateCommand\n}\n\n// The validate command exists for backwards compatibility, and because the template command is still hidden.\nfunc newValidateCommand() *cobra.Command {\n\tvalidateCommand := newTemplateValidateCommand()\n\tvalidateCommand.GroupID = advancedCommand\n\treturn validateCommand\n}\n\nvar templateCopyExample = `  Template locators are local files, file://, https://, or template: URLs\n\n  # Copy default template to STDOUT\n  limactl template copy template:default -\n\n  # Copy template from web location to local file and embed all external references\n  # (this does not embed template: references)\n  limactl template copy --embed https://example.com/lima.yaml mighty-machine.yaml\n`\n\nfunc newTemplateCopyCommand() *cobra.Command {\n\ttemplateCopyCommand := &cobra.Command{\n\t\tUse:     \"copy [OPTIONS] TEMPLATE [DEST]\",\n\t\tShort:   \"Copy template\",\n\t\tLong:    \"Copy a template via locator to a local file or stdout (default)\",\n\t\tExample: templateCopyExample,\n\t\tArgs:    WrapArgsError(cobra.RangeArgs(1, 2)),\n\t\tRunE:    templateCopyAction,\n\t}\n\ttemplateCopyCommand.Flags().Bool(\"embed\", false, \"Embed external dependencies into template\")\n\ttemplateCopyCommand.Flags().Bool(\"embed-all\", false, \"Embed all dependencies into template\")\n\ttemplateCopyCommand.Flags().Bool(\"fill\", false, \"Fill defaults\")\n\ttemplateCopyCommand.Flags().Bool(\"verbatim\", false, \"Don't make locators absolute\")\n\treturn templateCopyCommand\n}\n\nfunc fillDefaults(ctx context.Context, tmpl *limatmpl.Template) error {\n\tlimaDir, err := dirnames.LimaDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Load() will merge the template with override.yaml and default.yaml via FillDefaults().\n\t// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.\n\tfilePath := filepath.Join(limaDir, tmpl.Name+\".yaml\")\n\ttmpl.Config, err = limayaml.Load(ctx, tmpl.Bytes, filePath)\n\tif err == nil {\n\t\ttmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false)\n\t}\n\tif err := driverutil.ResolveVMType(ctx, tmpl.Config, filePath); err != nil {\n\t\tlogrus.Warnf(\"failed to resolve VM type for %q: %v\", filePath, err)\n\t\treturn nil\n\t}\n\treturn err\n}\n\nfunc templateCopyAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tsource := args[0]\n\ttarget := \"-\"\n\tif len(args) > 1 {\n\t\ttarget = args[1]\n\t}\n\tembed, err := cmd.Flags().GetBool(\"embed\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tembedAll, err := cmd.Flags().GetBool(\"embed-all\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tfill, err := cmd.Flags().GetBool(\"fill\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tverbatim, err := cmd.Flags().GetBool(\"verbatim\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif fill {\n\t\tembedAll = true\n\t}\n\tif embedAll {\n\t\tembed = true\n\t}\n\tif embed && verbatim {\n\t\treturn errors.New(\"--verbatim cannot be used with any of --embed, --embed-all, or --fill\")\n\t}\n\ttmpl, err := limatmpl.Read(cmd.Context(), \"\", source)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(tmpl.Bytes) == 0 {\n\t\treturn fmt.Errorf(\"don't know how to interpret %q as a template locator\", source)\n\t}\n\tif !verbatim {\n\t\tif embed {\n\t\t\t// Embed default base.yaml only when fill is true.\n\t\t\tif err := tmpl.Embed(cmd.Context(), embedAll, fill); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif err := tmpl.UseAbsLocators(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tif fill {\n\t\tif err := fillDefaults(ctx, tmpl); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif target == \"-\" && uiutil.OutputIsTTY(cmd.OutOrStdout()) {\n\t\t// run the output through YQ to colorize it\n\t\tout, err := yqutil.EvaluateExpressionPlain(\".\", string(tmpl.Bytes), true)\n\t\tif err == nil {\n\t\t\t_, err = fmt.Fprint(cmd.OutOrStdout(), out)\n\t\t}\n\t\treturn err\n\t}\n\n\twriter := cmd.OutOrStdout()\n\tif target != \"-\" {\n\t\tfile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer file.Close()\n\t\twriter = file\n\t}\n\t_, err = fmt.Fprint(writer, string(tmpl.Bytes))\n\treturn err\n}\n\nconst templateYQHelp = `Use the builtin YQ evaluator to extract information from a template.\nExternal references are embedded and default values are filled in\nbefore the YQ expression is evaluated.\n\nExample:\n  limactl template yq template:default '.images[].location'\n\nThe example command is equivalent to using an external yq command like this:\n  limactl template copy --fill template:default - | yq '.images[].location'\n`\n\nfunc newTemplateYQCommand() *cobra.Command {\n\ttemplateYQCommand := &cobra.Command{\n\t\tUse:   \"yq TEMPLATE EXPR\",\n\t\tShort: \"Query template expressions\",\n\t\tLong:  templateYQHelp,\n\t\tArgs:  WrapArgsError(cobra.ExactArgs(2)),\n\t\tRunE:  templateYQAction,\n\t}\n\treturn templateYQCommand\n}\n\nfunc templateYQAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tlocator := args[0]\n\texpr := args[1]\n\ttmpl, err := limatmpl.Read(cmd.Context(), \"\", locator)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(tmpl.Bytes) == 0 {\n\t\treturn fmt.Errorf(\"don't know how to interpret %q as a template locator\", locator)\n\t}\n\tif err := tmpl.Embed(cmd.Context(), true, true); err != nil {\n\t\treturn err\n\t}\n\tif err := fillDefaults(ctx, tmpl); err != nil {\n\t\treturn err\n\t}\n\tcolorsEnabled := uiutil.OutputIsTTY(cmd.OutOrStdout())\n\tout, err := yqutil.EvaluateExpressionPlain(expr, string(tmpl.Bytes), colorsEnabled)\n\tif err == nil {\n\t\t_, err = fmt.Fprint(cmd.OutOrStdout(), out)\n\t}\n\treturn err\n}\n\nfunc newTemplateValidateCommand() *cobra.Command {\n\ttemplateValidateCommand := &cobra.Command{\n\t\tUse:   \"validate TEMPLATE [TEMPLATE, ...]\",\n\t\tShort: \"Validate YAML templates\",\n\t\tArgs:  WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:  templateValidateAction,\n\t}\n\ttemplateValidateCommand.Flags().Bool(\"fill\", false, \"Fill defaults\")\n\treturn templateValidateCommand\n}\n\nfunc templateValidateAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tfill, err := cmd.Flags().GetBool(\"fill\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tlimaDir, err := dirnames.LimaDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, arg := range args {\n\t\ttmpl, err := limatmpl.Read(cmd.Context(), \"\", arg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(tmpl.Bytes) == 0 {\n\t\t\treturn fmt.Errorf(\"don't know how to interpret %q as a template locator\", arg)\n\t\t}\n\t\tif tmpl.Name == \"\" {\n\t\t\treturn fmt.Errorf(\"can't determine instance name from template locator %q\", arg)\n\t\t}\n\t\t// Embed default base.yaml only when fill is true.\n\t\tif err := tmpl.Embed(cmd.Context(), true, fill); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Load() will merge the template with override.yaml and default.yaml via FillDefaults().\n\t\t// FillDefaults() needs the potential instance directory to validate host templates using {{.Dir}}.\n\t\tfilePath := filepath.Join(limaDir, tmpl.Name+\".yaml\")\n\t\ty, err := limayaml.Load(ctx, tmpl.Bytes, filePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := driverutil.ResolveVMType(ctx, y, filePath); err != nil {\n\t\t\tlogrus.Warnf(\"failed to resolve VM type for %q: %v\", filePath, err)\n\t\t}\n\t\tif err := limayaml.Validate(y, false); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to validate YAML file %q: %w\", arg, err)\n\t\t}\n\t\tlogrus.Infof(\"%q: OK\", arg)\n\t\tif fill {\n\t\t\tb, err := limayaml.Marshal(y, len(args) > 1)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to marshal template %q again after filling defaults: %w\", arg, err)\n\t\t\t}\n\t\t\tfmt.Fprint(cmd.OutOrStdout(), string(b))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc newTemplateURLCommand() *cobra.Command {\n\ttemplateURLCommand := &cobra.Command{\n\t\tUse:   \"url CUSTOM_URL\",\n\t\tShort: \"Transform custom template URLs to regular file or https URLs\",\n\t\tArgs:  WrapArgsError(cobra.ExactArgs(1)),\n\t\tRunE:  templateURLAction,\n\t}\n\treturn templateURLCommand\n}\n\nfunc templateURLAction(cmd *cobra.Command, args []string) error {\n\turl, err := limatmpl.TransformCustomURL(cmd.Context(), args[0])\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = fmt.Fprintln(cmd.OutOrStdout(), url)\n\treturn err\n}\n"
  },
  {
    "path": "cmd/limactl/tunnel.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strconv\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/freeport\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nconst tunnelHelp = `Create a tunnel for Lima\n\nCreate a SOCKS tunnel so that the host can join the guest network.\n`\n\nfunc newTunnelCommand() *cobra.Command {\n\ttunnelCmd := &cobra.Command{\n\t\tUse:   \"tunnel [flags] INSTANCE\",\n\t\tShort: \"Create a tunnel for Lima\",\n\t\tPersistentPreRun: func(*cobra.Command, []string) {\n\t\t\tlogrus.Warn(\"`limactl tunnel` is experimental\")\n\t\t},\n\t\tLong:              tunnelHelp,\n\t\tArgs:              WrapArgsError(cobra.ExactArgs(1)),\n\t\tRunE:              tunnelAction,\n\t\tValidArgsFunction: tunnelBashComplete,\n\t\tSilenceErrors:     true,\n\t\tGroupID:           advancedCommand,\n\t}\n\n\ttunnelCmd.Flags().SetInterspersed(false)\n\t// TODO: implement l2tp, ikev2, masque, ...\n\ttunnelCmd.Flags().String(\"type\", \"socks\", \"Tunnel type, currently only \\\"socks\\\" is implemented\")\n\ttunnelCmd.Flags().Int(\"socks-port\", 0, \"SOCKS port, defaults to a random port\")\n\treturn tunnelCmd\n}\n\nfunc tunnelAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tflags := cmd.Flags()\n\ttunnelType, err := flags.GetString(\"type\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif tunnelType != \"socks\" {\n\t\treturn fmt.Errorf(\"unknown tunnel type: %q\", tunnelType)\n\t}\n\tport, err := flags.GetInt(\"socks-port\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif port != 0 && (port < 1024 || port > 65535) {\n\t\treturn fmt.Errorf(\"invalid socks port %d\", port)\n\t}\n\tstdout, stderr := cmd.OutOrStdout(), cmd.ErrOrStderr()\n\tinstName := args[0]\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"instance %q does not exist, run `limactl create %s` to create a new instance\", instName, instName)\n\t\t}\n\t\treturn err\n\t}\n\tif inst.Status == limatype.StatusStopped {\n\t\treturn fmt.Errorf(\"instance %q is stopped, run `limactl start %s` to start the instance\", instName, instName)\n\t}\n\n\tif port == 0 {\n\t\tport, err = freeport.TCP()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tsshExe, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsshOpts, err := sshutil.SSHOpts(\n\t\tctx,\n\t\tsshExe,\n\t\tinst.Dir,\n\t\t*inst.Config.User.Name,\n\t\t*inst.Config.SSH.LoadDotSSHPubKeys,\n\t\t*inst.Config.SSH.ForwardAgent,\n\t\t*inst.Config.SSH.ForwardX11,\n\t\t*inst.Config.SSH.ForwardX11Trusted)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsshArgs := append([]string{}, sshExe.Args...)\n\tsshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)\n\tsshArgs = append(sshArgs, []string{\n\t\t\"-q\", // quiet\n\t\t\"-f\", // background\n\t\t\"-N\", // no command\n\t\t\"-D\", fmt.Sprintf(\"127.0.0.1:%d\", port),\n\t\t\"-p\", strconv.Itoa(inst.SSHLocalPort),\n\t\tinst.SSHAddress,\n\t}...)\n\tsshCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...)\n\tsshCmd.Stdout = stderr\n\tsshCmd.Stderr = stderr\n\tlogrus.Debugf(\"executing ssh (may take a long)): %+v\", sshCmd.Args)\n\n\tif err := sshCmd.Run(); err != nil {\n\t\treturn err\n\t}\n\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tfmt.Fprint(stdout, \"Open <System Settings> → <Network> → <Wi-Fi> (or whatever) → <Details> → <Proxies> → <SOCKS proxy>,\\n\")\n\t\tfmt.Fprint(stdout, \"and specify the following configuration:\\n\")\n\t\tfmt.Fprint(stdout, \"- Server: 127.0.0.1\\n\")\n\t\tfmt.Fprintf(stdout, \"- Port: %d\\n\", port)\n\tcase \"windows\":\n\t\tfmt.Fprint(stdout, \"Open <Settings> → <Network & Internet> → <Proxy>,\\n\")\n\t\tfmt.Fprint(stdout, \"and specify the following configuration:\\n\")\n\t\tfmt.Fprint(stdout, \"- Address: socks=127.0.0.1\\n\")\n\t\tfmt.Fprintf(stdout, \"- Port: %d\\n\", port)\n\tdefault:\n\t\tfmt.Fprintf(stdout, \"Set `ALL_PROXY=socks5h://127.0.0.1:%d`, etc.\\n\", port)\n\t}\n\tfmt.Fprintf(stdout, \"The instance can be connected from the host as <http://%s.internal> via a web browser.\\n\", inst.Hostname)\n\n\t// TODO: show the port in `limactl list --json` ?\n\t// TODO: add `--stop` flag to shut down the tunnel\n\treturn nil\n}\n\nfunc tunnelBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/unprotect.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newUnprotectCommand() *cobra.Command {\n\tunprotectCommand := &cobra.Command{\n\t\tUse:               \"unprotect INSTANCE [INSTANCE, ...]\",\n\t\tShort:             \"Unprotect an instance\",\n\t\tArgs:              WrapArgsError(cobra.MinimumNArgs(1)),\n\t\tRunE:              unprotectAction,\n\t\tValidArgsFunction: unprotectBashComplete,\n\t\tGroupID:           advancedCommand,\n\t}\n\treturn unprotectCommand\n}\n\nfunc unprotectAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tvar errs []error\n\tfor _, instName := range args {\n\t\tinst, err := store.Inspect(ctx, instName)\n\t\tif err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to inspect instance %q: %w\", instName, err))\n\t\t\tcontinue\n\t\t}\n\t\tif !inst.Protected {\n\t\t\tlogrus.Warnf(\"Instance %q isn't protected. Skipping.\", instName)\n\t\t\tcontinue\n\t\t}\n\t\tif err := inst.Unprotect(); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to unprotect instance %q: %w\", instName, err))\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Infof(\"Unprotected %q\", instName)\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc unprotectBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl/usernet.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"syscall\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n)\n\nfunc newUsernetCommand() *cobra.Command {\n\thostagentCommand := &cobra.Command{\n\t\tUse:    \"usernet\",\n\t\tShort:  \"Run usernet\",\n\t\tArgs:   cobra.ExactArgs(0),\n\t\tRunE:   usernetAction,\n\t\tHidden: true,\n\t}\n\thostagentCommand.Flags().StringP(\"pidfile\", \"p\", \"\", \"Write PID to file\")\n\thostagentCommand.Flags().StringP(\"endpoint\", \"e\", \"\", \"Exposes usernet API(s) on this endpoint\")\n\thostagentCommand.Flags().String(\"listen-qemu\", \"\", \"Listen for QMEU connections\")\n\thostagentCommand.Flags().String(\"listen\", \"\", \"Listen on a Unix socket and receive Bess-compatible FDs as SCM_RIGHTS messages\")\n\thostagentCommand.Flags().String(\"subnet\", \"192.168.5.0/24\", \"Sets subnet value for the usernet network\")\n\thostagentCommand.Flags().Int(\"mtu\", 1500, \"mtu\")\n\thostagentCommand.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' \")\n\treturn hostagentCommand\n}\n\nfunc usernetAction(cmd *cobra.Command, _ []string) error {\n\tpidfile, err := cmd.Flags().GetString(\"pidfile\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif pidfile != \"\" {\n\t\tif _, err := os.Stat(pidfile); !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"pidfile %q already exists\", pidfile)\n\t\t}\n\t\tif err := os.WriteFile(pidfile, []byte(strconv.Itoa(os.Getpid())+\"\\n\"), 0o644); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer os.RemoveAll(pidfile)\n\t}\n\tendpoint, err := cmd.Flags().GetString(\"endpoint\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tqemuSocket, err := cmd.Flags().GetString(\"listen-qemu\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tfdSocket, err := cmd.Flags().GetString(\"listen\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tsubnet, err := cmd.Flags().GetString(\"subnet\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tleases, err := cmd.Flags().GetStringToString(\"leases\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmtu, err := cmd.Flags().GetInt(\"mtu\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tos.RemoveAll(endpoint)\n\tos.RemoveAll(qemuSocket)\n\tos.RemoveAll(fdSocket)\n\n\tctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)\n\tdefer cancel()\n\n\t// Environment Variables\n\t// LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT: Specifies the timeout duration for resolving IP addresses in minutes. Default is 2 minutes.\n\n\treturn usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{\n\t\tMTU:           mtu,\n\t\tEndpoint:      endpoint,\n\t\tQemuSocket:    qemuSocket,\n\t\tFdSocket:      fdSocket,\n\t\tSubnet:        subnet,\n\t\tDefaultLeases: leases,\n\t})\n}\n"
  },
  {
    "path": "cmd/limactl/watch.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/rjeczalik/notify\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc newWatchCommand() *cobra.Command {\n\twatchCommand := &cobra.Command{\n\t\tUse:   \"watch [INSTANCE]...\",\n\t\tShort: \"Watch events from instances\",\n\t\tLong: `Watch events from Lima instances.\n\nEvents include status changes (starting, running, stopping), port forwarding\nevents, and other instance lifecycle events.\n\nIf no instance is specified, events from all instances are watched,\nincluding newly created instances.\n\nThe command will continue watching until interrupted (Ctrl+C).`,\n\t\tExample: `  # Watch events from all instances:\n  $ limactl watch\n\n  # Watch events from a specific instance:\n  $ limactl watch default\n\n  # Include historical events:\n  $ limactl watch --history default\n\n  # Show verbose output (host agent logs, etc.):\n  $ limactl watch --verbose\n\n  # Watch events in JSON format (for scripting):\n  $ limactl watch --json default`,\n\t\tArgs:              WrapArgsError(cobra.ArbitraryArgs),\n\t\tRunE:              watchAction,\n\t\tValidArgsFunction: watchBashComplete,\n\t\tGroupID:           advancedCommand,\n\t}\n\twatchCommand.Flags().Bool(\"json\", false, \"Output events as newline-delimited JSON\")\n\twatchCommand.Flags().Bool(\"history\", false, \"Include historical events from before watch started\")\n\twatchCommand.Flags().Bool(\"verbose\", false, \"Show verbose output\")\n\treturn watchCommand\n}\n\ntype watchEvent struct {\n\tInstance string       `json:\"instance\"`\n\tEvent    events.Event `json:\"event\"`\n}\n\ntype eventWatcher struct {\n\tctx             context.Context\n\tbegin           time.Time\n\tpropagateStderr bool\n\teventCh         chan watchEvent\n\twatching        sync.Map\n}\n\nfunc (w *eventWatcher) startInstance(instName string) {\n\tif _, loaded := w.watching.LoadOrStore(instName, true); loaded {\n\t\treturn\n\t}\n\n\tinst, err := store.Inspect(w.ctx, instName)\n\tif err != nil {\n\t\tlogrus.WithError(err).Warnf(\"Failed to inspect instance %q\", instName)\n\t\tw.watching.Delete(instName)\n\t\treturn\n\t}\n\n\thaStdoutPath := filepath.Join(inst.Dir, filenames.HostAgentStdoutLog)\n\thaStderrPath := filepath.Join(inst.Dir, filenames.HostAgentStderrLog)\n\n\tgo w.watchInstance(instName, haStdoutPath, haStderrPath)\n}\n\nfunc (w *eventWatcher) watchInstance(instName, haStdoutPath, haStderrPath string) {\n\terr := events.Watch(w.ctx, haStdoutPath, haStderrPath, w.begin, w.propagateStderr, func(ev events.Event) bool {\n\t\tselect {\n\t\tcase w.eventCh <- watchEvent{Instance: instName, Event: ev}:\n\t\tcase <-w.ctx.Done():\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\tif err != nil && w.ctx.Err() == nil {\n\t\tlogrus.WithError(err).Warnf(\"Watcher for instance %q stopped\", instName)\n\t}\n}\n\nfunc watchAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\n\tjsonFormat, err := cmd.Flags().GetBool(\"json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\thistory, err := cmd.Flags().GetBool(\"history\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tverbose, err := cmd.Flags().GetBool(\"verbose\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !verbose {\n\t\tlogrus.SetLevel(logrus.WarnLevel)\n\t}\n\n\tvar begin time.Time\n\tif !history {\n\t\tbegin = time.Now()\n\t}\n\n\tstdout := cmd.OutOrStdout()\n\tstderr := cmd.ErrOrStderr()\n\twatchAll := len(args) == 0\n\n\tvar instNames []string\n\tif watchAll {\n\t\tinstNames, err = store.Instances()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(instNames) == 0 {\n\t\t\tprintStatus(stderr, \"No instances found\")\n\t\t}\n\t} else {\n\t\tinstNames = args\n\t}\n\n\tnewInstanceCh := make(chan string, 16)\n\tw := &eventWatcher{\n\t\tctx:             ctx,\n\t\tbegin:           begin,\n\t\tpropagateStderr: verbose,\n\t\teventCh:         make(chan watchEvent, 64),\n\t}\n\n\tfor _, instName := range instNames {\n\t\tw.startInstance(instName)\n\t}\n\n\tif watchAll {\n\t\tgo watchLimaDir(ctx, newInstanceCh)\n\t}\n\n\tprintStatus(stderr, \"Watching for events...\")\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tcase instName := <-newInstanceCh:\n\t\t\tprintStatus(stderr, \"New instance detected: \"+instName)\n\t\t\tw.startInstance(instName)\n\t\tcase ev := <-w.eventCh:\n\t\t\tif jsonFormat {\n\t\t\t\tj, err := json.Marshal(ev)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Fprintf(stderr, \"error marshaling event: %v\\n\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfmt.Fprintln(stdout, string(j))\n\t\t\t} else {\n\t\t\t\tprintHumanReadableEvent(stdout, ev.Instance, ev.Event)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc watchLimaDir(ctx context.Context, newInstanceCh chan<- string) {\n\tlimaDir := store.Directory()\n\tif limaDir == \"\" {\n\t\tlogrus.Warn(\"Could not determine lima directory\")\n\t\treturn\n\t}\n\n\tfsEvents := make(chan notify.EventInfo, 128)\n\tif err := notify.Watch(limaDir, fsEvents, notify.Create); err != nil {\n\t\tlogrus.WithError(err).Warn(\"Failed to watch lima directory for new instances\")\n\t\treturn\n\t}\n\tdefer notify.Stop(fsEvents)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase ev := <-fsEvents:\n\t\t\tname := filepath.Base(ev.Path())\n\t\t\tif !isValidInstanceName(name) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !isInstanceDir(ev.Path()) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase newInstanceCh <- name:\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc isValidInstanceName(name string) bool {\n\treturn !strings.HasPrefix(name, \".\") && !strings.HasPrefix(name, \"_\")\n}\n\nfunc isInstanceDir(path string) bool {\n\tinfo, err := os.Stat(path)\n\tif err != nil || !info.IsDir() {\n\t\treturn false\n\t}\n\tyamlPath := filepath.Join(path, filenames.LimaYAML)\n\t_, err = os.Stat(yamlPath)\n\treturn err == nil\n}\n\nfunc printStatus(out io.Writer, msg string) {\n\tfmt.Fprintf(out, \"%s %s\\n\", time.Now().Format(\"2006-01-02 15:04:05\"), msg)\n}\n\nfunc printHumanReadableEvent(out io.Writer, instName string, ev events.Event) {\n\ttimestamp := ev.Time.Format(\"2006-01-02 15:04:05\")\n\n\tprintEvent := func(msg string) {\n\t\tfmt.Fprintf(out, \"%s %s | %s\\n\", timestamp, instName, msg)\n\t}\n\n\tif ev.Status.Running {\n\t\tif ev.Status.Degraded {\n\t\t\tprintEvent(\"running (degraded)\")\n\t\t} else {\n\t\t\tprintEvent(\"running\")\n\t\t}\n\t}\n\tif ev.Status.Exiting {\n\t\tprintEvent(\"exiting\")\n\t}\n\tif ev.Status.SSHLocalPort != 0 {\n\t\tprintEvent(fmt.Sprintf(\"ssh available on port %d\", ev.Status.SSHLocalPort))\n\t}\n\tfor _, e := range ev.Status.Errors {\n\t\tprintEvent(fmt.Sprintf(\"error: %s\", e))\n\t}\n\tif ev.Status.CloudInitProgress != nil {\n\t\tif ev.Status.CloudInitProgress.Completed {\n\t\t\tprintEvent(\"cloud-init completed\")\n\t\t} else if ev.Status.CloudInitProgress.LogLine != \"\" {\n\t\t\tprintEvent(fmt.Sprintf(\"cloud-init: %s\", ev.Status.CloudInitProgress.LogLine))\n\t\t}\n\t}\n\tif ev.Status.PortForward != nil {\n\t\tpf := ev.Status.PortForward\n\t\tswitch pf.Type {\n\t\tcase events.PortForwardEventForwarding:\n\t\t\tprintEvent(fmt.Sprintf(\"forwarding %s %s to %s\", pf.Protocol, pf.GuestAddr, pf.HostAddr))\n\t\tcase events.PortForwardEventNotForwarding:\n\t\t\tprintEvent(fmt.Sprintf(\"not forwarding %s %s\", pf.Protocol, pf.GuestAddr))\n\t\tcase events.PortForwardEventStopping:\n\t\t\tprintEvent(fmt.Sprintf(\"stopping forwarding %s %s\", pf.Protocol, pf.GuestAddr))\n\t\tcase events.PortForwardEventFailed:\n\t\t\tprintEvent(fmt.Sprintf(\"failed to forward %s %s: %s\", pf.Protocol, pf.GuestAddr, pf.Error))\n\t\t}\n\t}\n\tif ev.Status.Vsock != nil {\n\t\tvs := ev.Status.Vsock\n\t\tswitch vs.Type {\n\t\tcase events.VsockEventStarted:\n\t\t\tprintEvent(fmt.Sprintf(\"started vsock forwarder: %s -> vsock:%d\", vs.HostAddr, vs.VsockPort))\n\t\tcase events.VsockEventSkipped:\n\t\t\tprintEvent(fmt.Sprintf(\"skipped vsock forwarder: %s\", vs.Reason))\n\t\tcase events.VsockEventFailed:\n\t\t\tprintEvent(fmt.Sprintf(\"failed to start vsock forwarder: %s\", vs.Reason))\n\t\t}\n\t}\n}\n\nfunc watchBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {\n\treturn bashCompleteInstanceNames(cmd)\n}\n"
  },
  {
    "path": "cmd/limactl-mcp/main.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/modelcontextprotocol/go-sdk/mcp\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limactlutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/mcp/toolset\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n)\n\nfunc main() {\n\tif err := newApp().Execute(); err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n}\n\nfunc newApp() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:           \"limactl-mcp\",\n\t\tShort:         \"Model Context Protocol plugin for Lima (EXPERIMENTAL)\",\n\t\tVersion:       strings.TrimPrefix(version.Version, \"v\"),\n\t\tSilenceUsage:  true,\n\t\tSilenceErrors: true,\n\t}\n\tcmd.AddCommand(\n\t\tnewMcpInfoCommand(),\n\t\tnewMcpServeCommand(),\n\t\tnewMcpGenDocCommand(),\n\t\t// TODO: `limactl-mcp configure gemini` ?\n\t)\n\treturn cmd\n}\n\nfunc newServer() *mcp.Server {\n\timpl := &mcp.Implementation{\n\t\tName:    \"lima\",\n\t\tTitle:   \"Lima VM, for sandboxing local command executions and file I/O operations\",\n\t\tVersion: version.Version,\n\t}\n\tserverOpts := &mcp.ServerOptions{\n\t\tInstructions: `This MCP server provides tools for sandboxing local command executions and file I/O operations,\nby wrapping them in Lima VM (https://lima-vm.io).\n\nUse these tools to avoid accidentally executing malicious codes directly on the host.\n`,\n\t}\n\tif runtime.GOOS != \"linux\" {\n\t\tserverOpts.Instructions += fmt.Sprintf(`\n\nNOTE: the guest OS of the VM is Linux, while the host OS is %s.\n`, cases.Title(language.English).String(runtime.GOOS))\n\t}\n\treturn mcp.NewServer(impl, serverOpts)\n}\n\nfunc newMcpInfoCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"info\",\n\t\tShort: \"Show information about the MCP server\",\n\t\tArgs:  cobra.NoArgs,\n\t\tRunE:  mcpInfoAction,\n\t}\n\treturn cmd\n}\n\nfunc mcpInfoAction(cmd *cobra.Command, _ []string) error {\n\tctx := cmd.Context()\n\tinfo, err := inspectInfo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tj, err := json.MarshalIndent(info, \"\", \"    \")\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = fmt.Fprint(cmd.OutOrStdout(), string(j))\n\treturn err\n}\n\nfunc inspectInfo(ctx context.Context) (*Info, error) {\n\tts, err := toolset.New(\"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tserver := newServer()\n\tif err = ts.RegisterServer(server); err != nil {\n\t\treturn nil, err\n\t}\n\tserverTransport, clientTransport := mcp.NewInMemoryTransports()\n\tserverSession, err := server.Connect(ctx, serverTransport, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := mcp.NewClient(&mcp.Implementation{Name: \"client\"}, nil)\n\tclientSession, err := client.Connect(ctx, clientTransport, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttoolsResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = clientSession.Close(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err = serverSession.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\tinfo := &Info{\n\t\tTools: toolsResult.Tools,\n\t}\n\treturn info, nil\n}\n\ntype Info struct {\n\tTools []*mcp.Tool `json:\"tools\"`\n}\n\nfunc newMcpServeCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"serve INSTANCE\",\n\t\tShort: \"Serve MCP over stdio\",\n\t\tLong: `Serve MCP over stdio.\n\nExpected to be executed via an AI agent, not by a human`,\n\t\tArgs: cobra.MaximumNArgs(1),\n\t\tRunE: mcpServeAction,\n\t}\n\treturn cmd\n}\n\nfunc mcpServeAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tinstName := \"default\"\n\tif len(args) > 0 {\n\t\tinstName = args[0]\n\t}\n\tlimactl, err := limactlutil.Path()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// FIXME: We can not use store.Inspect() here because it requires VM drivers to be compiled in.\n\t// https://github.com/lima-vm/lima/pull/3744#issuecomment-3289274347\n\tinst, err := limactlutil.Inspect(ctx, limactl, instName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(inst.Errors) != 0 {\n\t\treturn errors.Join(inst.Errors...)\n\t}\n\tts, err := toolset.New(limactl)\n\tif err != nil {\n\t\treturn err\n\t}\n\tserver := newServer()\n\tif err = ts.RegisterServer(server); err != nil {\n\t\treturn err\n\t}\n\tif err = ts.RegisterInstance(ctx, inst); err != nil {\n\t\treturn err\n\t}\n\ttransport := &mcp.StdioTransport{}\n\treturn server.Run(ctx, transport)\n}\n\nfunc newMcpGenDocCommand() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:    \"generate-doc DIR\",\n\t\tShort:  \"Generate documentation pages\",\n\t\tArgs:   cobra.MinimumNArgs(1),\n\t\tRunE:   mcpGenDocAction,\n\t\tHidden: true,\n\t}\n\treturn cmd\n}\n\nfunc mcpGenDocAction(cmd *cobra.Command, args []string) error {\n\tctx := cmd.Context()\n\tdir := args[0]\n\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\treturn err\n\t}\n\tfName := filepath.Join(dir, \"mcp.md\")\n\tf, err := os.Create(fName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\tfmt.Fprint(f, `---\ntitle: MCP tools\nweight: 99\n---\nLima implements the \"MCP Sandbox Interface\" (tentative name):\nhttps://pkg.go.dev/github.com/lima-vm/lima/v2/pkg/mcp/msi\n\nMCP Sandbox Interface defines MCP (Model Context Protocol) tools\nthat can be used for reading, writing, and executing local files\nwith an appropriate sandboxing technology, such as Lima.\n\nThe sandboxing technology can be more secure and/or efficient than\nthe default tools provided by an AI agent.\n\nMCP Sandbox Interface was inspired by\n[Google Gemini CLI's built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools).\n\n`)\n\tinfo, err := inspectInfo(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, tool := range info.Tools {\n\t\tfmt.Fprintf(f, \"## `%s`\\n\\n\", tool.Name)\n\t\tif tool.Title != \"\" {\n\t\t\tfmt.Fprintf(f, \"### Title\\n\\n%s\\n\\n\", tool.Title)\n\t\t}\n\t\tif tool.Description != \"\" {\n\t\t\tfmt.Fprintf(f, \"### Description\\n\\n%s\\n\\n\", tool.Description)\n\t\t}\n\t\tif tool.InputSchema != nil {\n\t\t\tfmt.Fprint(f, \"### Input Schema\\n\\n\")\n\t\t\tschema, err := json.MarshalIndent(tool.InputSchema, \"\", \"    \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Fprintf(f, \"```json\\n%s\\n```\\n\\n\", string(schema))\n\t\t}\n\t\tif tool.OutputSchema != nil {\n\t\t\tfmt.Fprint(f, \"### Output Schema\\n\\n\")\n\t\t\tschema, err := json.MarshalIndent(tool.OutputSchema, \"\", \"    \")\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Fprintf(f, \"```json\\n%s\\n```\\n\\n\", string(schema))\n\t\t}\n\t}\n\treturn f.Close()\n}\n"
  },
  {
    "path": "cmd/limactl-url-fedora-rawhide",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu\n\nif [ \"$#\" -ne 1 ]; then\n\techo >&2 \"Usage: $0 _images/fedora-rawhide\"\n\texit 1\nfi\nif [ \"$1\" != \"_images/fedora-rawhide\" ]; then\n\techo >&2 \"Expected argument to be '_images/fedora-rawhide', but got '$1'\"\n\texit 1\nfi\n\nCACHE_HOME_DEFAULT=\"${HOME}/.cache\"\nif [ \"$(uname -s)\" = \"Darwin\" ]; then\n\tCACHE_HOME_DEFAULT=\"${HOME}/Library/Caches\"\nfi\n: \"${XDG_CACHE_HOME:=${CACHE_HOME_DEFAULT}}\"\nCACHE_DIR=\"${XDG_CACHE_HOME}/lima/limactl-url-fedora-rawhide\"\n\n# COMPOSE_ID is like \"Fedora-Rawhide-20260316.n.0\"\nCOMPOSE_ID=\"$(curl -fsSL https://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/COMPOSE_ID)\"\n# COMPOSE_ID_TRIMMED is like \"20260316.n.0\"\nCOMPOSE_ID_TRIMMED=\"${COMPOSE_ID#Fedora-Rawhide-}\"\n\nmkdir -p \"${CACHE_DIR}\"\nFILE=\"${CACHE_DIR}/fedora-rawhide.yaml\"\n\ncat <<EOF >\"${FILE}\"\nimages:\n- 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\"\n  arch: \"x86_64\"\n- location: \"https://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-Rawhide-${COMPOSE_ID_TRIMMED}.aarch64.qcow2\"\n  arch: \"aarch64\"\nEOF\n\necho \"$FILE\"\n"
  },
  {
    "path": "cmd/nerdctl.lima",
    "content": "#!/bin/sh\nset -eu\n\n# Environment Variables\n# LIMA_INSTANCE: Specifies the name of the Lima instance to use. Default is \"default\".\n\n: \"${LIMA_INSTANCE:=default}\"\n\n# Use --preserve-env to pass through environment variables from host machine into guest instance\nexec limactl shell --preserve-env \"$LIMA_INSTANCE\" nerdctl \"$@\"\n"
  },
  {
    "path": "cmd/podman.lima",
    "content": "#!/bin/sh\nset -eu\n: \"${LIMA_INSTANCE:=podman}\"\n: \"${PODMAN:=podman}\"\n\nif [ \"$(limactl ls -q \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"$LIMA_INSTANCE\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" does not exist, run \\`limactl create --name=$LIMA_INSTANCE template:podman\\` to create a new instance\" >&2\n  exit 1\nelif [ \"$(limactl ls -f '{{ .Status }}' \"$LIMA_INSTANCE\" 2>/dev/null)\" != \"Running\" ]; then\n  echo \"instance \\\"$LIMA_INSTANCE\\\" is not running, run \\`limactl start $LIMA_INSTANCE\\` to start the existing instance\" >&2\n  exit 1\nfi\nPODMAN=$(command -v \"$PODMAN\" || true)\nif [ -n \"$PODMAN\" ]; then\n  CONTAINER_HOST=$(limactl list \"$LIMA_INSTANCE\" --format 'unix://{{.Dir}}/sock/podman.sock')\n  export CONTAINER_HOST\n  exec \"$PODMAN\" --remote \"$@\"\nelse\n  export LIMA_INSTANCE\n  exec lima podman \"$@\"\nfi\n"
  },
  {
    "path": "cmd/yq/yq.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// SPDX-FileCopyrightText: Copyright (c) 2017 Mike Farah\n\n// This file has been adapted from https://github.com/mikefarah/yq/blob/v4.47.1/yq.go\n\npackage yq\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tcommand \"github.com/mikefarah/yq/v4/cmd\"\n)\n\nfunc main() {\n\tcmd := command.New()\n\targs := os.Args[1:]\n\t_, _, err := cmd.Find(args)\n\tif err != nil && args[0] != \"__complete\" {\n\t\t// default command when nothing matches...\n\t\tnewArgs := []string{\"eval\"}\n\t\tcmd.SetArgs(append(newArgs, os.Args[1:]...))\n\t}\n\tcode := 0\n\tif err := cmd.Execute(); err != nil {\n\t\tcode = 1\n\t}\n\tos.Exit(code)\n}\n\n// MaybeRunYQ runs as `yq` if the program name or first argument is `yq`.\n// Only returns to caller if os.Args doesn't contain a `yq` command.\nfunc MaybeRunYQ() {\n\tprogName := filepath.Base(os.Args[0])\n\t// remove all extensions, so we match \"yq.lima.exe\"\n\tprogName, _, _ = strings.Cut(progName, \".\")\n\tif progName == \"yq\" {\n\t\tmain()\n\t}\n\tif len(os.Args) > 1 && os.Args[1] == \"yq\" {\n\t\tos.Args = os.Args[1:]\n\t\tmain()\n\t}\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "Moved to <https://lima-vm.io/docs/>\n"
  },
  {
    "path": "go.mod",
    "content": "// gomodjail:confined\nmodule github.com/lima-vm/lima/v2\n\ngo 1.25.7\n\nrequire (\n\tal.essio.dev/pkg/shellescape v1.6.0\n\tgithub.com/AlecAivazis/survey/v2 v2.3.7\n\tgithub.com/Code-Hex/vz/v3 v3.7.1 // gomodjail:unconfined\n\tgithub.com/Microsoft/go-winio v0.6.2 // gomodjail:unconfined\n\tgithub.com/apparentlymart/go-cidr v1.1.0\n\tgithub.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e\n\tgithub.com/cheggaaa/pb/v3 v3.1.7 // gomodjail:unconfined\n\tgithub.com/cilium/ebpf v0.21.0 // gomodjail:unconfined\n\tgithub.com/containerd/continuity v0.4.5\n\tgithub.com/containers/gvisor-tap-vsock v0.8.8 // gomodjail:unconfined\n\tgithub.com/coreos/go-semver v0.3.1\n\tgithub.com/coreos/go-systemd/v22 v22.7.0\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7\n\tgithub.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7\n\tgithub.com/diskfs/go-diskfs v1.8.0 // gomodjail:unconfined\n\tgithub.com/docker/go-units v0.5.0\n\tgithub.com/foxcpp/go-mockdns v1.2.0\n\tgithub.com/goccy/go-yaml v1.19.2\n\tgithub.com/google/go-cmp v0.7.0\n\tgithub.com/google/yamlfmt v0.21.0\n\tgithub.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048\n\tgithub.com/invopop/jsonschema v0.13.0\n\tgithub.com/lima-vm/go-qcow2reader v0.7.1\n\tgithub.com/lima-vm/sshocker v0.3.9 // gomodjail:unconfined\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/mattn/go-shellwords v1.0.12\n\tgithub.com/mdlayher/netlink v1.9.0\n\tgithub.com/mdlayher/vsock v1.2.1 // gomodjail:unconfined\n\tgithub.com/miekg/dns v1.1.72 // gomodjail:unconfined\n\tgithub.com/mikefarah/yq/v4 v4.52.4\n\tgithub.com/modelcontextprotocol/go-sdk v1.4.1\n\tgithub.com/nxadm/tail v1.4.11 // gomodjail:unconfined\n\tgithub.com/opencontainers/go-digest v1.0.0\n\tgithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58\n\tgithub.com/pkg/sftp v1.13.10\n\tgithub.com/rjeczalik/notify v0.9.3\n\tgithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2\n\tgithub.com/sethvargo/go-password v0.3.1\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/spf13/cobra v1.10.2 // gomodjail:unconfined\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8\n\tgolang.org/x/net v0.52.0\n\tgolang.org/x/sync v0.20.0\n\tgolang.org/x/sys v0.42.0 // gomodjail:unconfined\n\tgolang.org/x/text v0.35.0\n\tgoogle.golang.org/grpc v1.79.3\n\tgoogle.golang.org/protobuf v1.36.11 // gomodjail:unconfined\n\tgopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473\n\tgotest.tools/v3 v3.5.2\n)\n\nrequire (\n\tgithub.com/Code-Hex/go-infinity-channel v1.0.0 // indirect\n\tgithub.com/VividCortex/ewma v1.2.0 // indirect\n\tgithub.com/a8m/envsubst v1.4.3 // indirect\n\tgithub.com/agext/levenshtein v1.2.1 // indirect\n\tgithub.com/alecthomas/participle/v2 v2.1.4 // indirect\n\tgithub.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/bmatcuk/doublestar/v4 v4.7.1 // indirect\n\tgithub.com/buger/jsonparser v1.1.2 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect\n\tgithub.com/dimchansky/utfbom v1.1.1 // indirect\n\tgithub.com/djherbis/times v1.6.0 // indirect\n\tgithub.com/elliotchance/orderedmap v1.8.0 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\t// gomodjail:unconfined\n\tgithub.com/fsnotify/fsnotify v1.8.0 // indirect\n\tgithub.com/go-ini/ini v1.67.0 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/google/gopacket v1.1.19 // indirect\n\tgithub.com/google/jsonschema-go v0.4.2 // indirect\n\tgithub.com/hashicorp/hcl/v2 v2.24.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\t// gomodjail:unconfined\n\tgithub.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 // indirect\n\tgithub.com/jinzhu/copier v0.4.0 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/kr/fs v0.1.0 // indirect\n\tgithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/mdlayher/socket v0.5.1 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect\n\tgithub.com/mitchellh/go-wordwrap v1.0.1 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect\n\tgithub.com/segmentio/asm v1.1.3 // indirect\n\tgithub.com/segmentio/encoding v0.5.4 // indirect\n\t// gomodjail:unconfined\n\tgithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect\n\tgithub.com/yosida95/uritemplate/v3 v3.0.2 // indirect\n\tgithub.com/yuin/gopher-lua v1.1.1 // indirect\n\tgithub.com/zclconf/go-cty v1.17.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgo.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect\n\tgolang.org/x/crypto v0.49.0 // indirect\n\tgolang.org/x/mod v0.33.0 // indirect\n\tgolang.org/x/oauth2 v0.34.0 // indirect\n\tgolang.org/x/term v0.41.0 // indirect\n\tgolang.org/x/time v0.9.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\t// gomodjail:unconfined\n\tgvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=\nal.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=\ngithub.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=\ngithub.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=\ngithub.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1pFAqvnP87Zh0Nw=\ngithub.com/Code-Hex/go-infinity-channel v1.0.0/go.mod h1:5yUVg/Fqao9dAjcpzoQ33WwfdMWmISOrQloDRn3bsvY=\ngithub.com/Code-Hex/vz/v3 v3.7.1 h1:EN1yNiyrbPq+dl388nne2NySo8I94EnPppvqypA65XM=\ngithub.com/Code-Hex/vz/v3 v3.7.1/go.mod h1:1LsW0jqW0r0cQ+IeR4hHbjdqOtSidNCVMWhStMHGho8=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=\ngithub.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=\ngithub.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=\ngithub.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY=\ngithub.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU=\ngithub.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=\ngithub.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=\ngithub.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=\ngithub.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=\ngithub.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=\ngithub.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=\ngithub.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=\ngithub.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=\ngithub.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e h1:IdMhFPEfTZQU971tIHx3UhY4l+yCeynprnINrDTSrOc=\ngithub.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e/go.mod h1:aXGMJsd3XrnUFTuyf/pTGg5jG6CY8JMZ5juywvShjgQ=\ngithub.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=\ngithub.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=\ngithub.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=\ngithub.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=\ngithub.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k=\ngithub.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ=\ngithub.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=\ngithub.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containers/gvisor-tap-vsock v0.8.8 h1:5FznbOYMIuaCv8B6zQ7M6wjqP63Lasy0A6GpViEnjTg=\ngithub.com/containers/gvisor-tap-vsock v0.8.8/go.mod h1:m/PzhZWAS6T9pCRH1fLkq2OqbEd6QEUZWjm3FS5F+CE=\ngithub.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=\ngithub.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=\ngithub.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=\ngithub.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e h1:SCnqm8SjSa0QqRxXbo5YY//S+OryeJioe17nK+iDZpg=\ngithub.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e/go.mod h1:o129ljs6alsIQTc8d6eweihqpmmrbxZ2g1jhgjhPykI=\ngithub.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 h1:3OVJAbR131SnAXao7c9w8bFlAGH0oa29DCwsa88MJGk=\ngithub.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7/go.mod h1:K4+o74YGNjOb9N6yyG+LPj1NjHtk+Qz0IYQPvirbaLs=\ngithub.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=\ngithub.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=\ngithub.com/diskfs/go-diskfs v1.8.0 h1:YynvepHpU7xpl8z2RqiV/x3NPAj3zU0ki0vdjPty0U4=\ngithub.com/diskfs/go-diskfs v1.8.0/go.mod h1:rW9+4MPN1tbMpQqRZlcM3YQsh3Ucc+Q1k1iIqzzmZcg=\ngithub.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=\ngithub.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=\ngithub.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=\ngithub.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw=\ngithub.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=\ngithub.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=\ngithub.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=\ngithub.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=\ngithub.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=\ngithub.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=\ngithub.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=\ngithub.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=\ngithub.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=\ngithub.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=\ngithub.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/yamlfmt v0.21.0 h1:9FKApQkDpMKgBjwLFytBHUCgqnQgxaQnci0uiESfbzs=\ngithub.com/google/yamlfmt v0.21.0/go.mod h1:q6FYExB+Ueu7jZDjKECJk+EaeDXJzJ6Ne0dxx69GWfI=\ngithub.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=\ngithub.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 h1:jaqViOFFlZtkAwqvwZN+id37fosQqR5l3Oki9Dk4hz8=\ngithub.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI=\ngithub.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 h1:LZJWucZz7ztCqY6Jsu7N9g124iJ2kt/O62j3+UchZFg=\ngithub.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=\ngithub.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=\ngithub.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=\ngithub.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA=\ngithub.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=\ngithub.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=\ngithub.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=\ngithub.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lima-vm/go-qcow2reader v0.7.1 h1:fZ5u38uaRX3ukuVA6IpeImh9BfRhzRGvTr87yGqENbY=\ngithub.com/lima-vm/go-qcow2reader v0.7.1/go.mod h1:Ai9fcmE2dXF6YFomrSCttveOT1x3+x5eG7AfIUUnSqw=\ngithub.com/lima-vm/sshocker v0.3.9 h1:jA1uLM8GS74ZI6lJ9f/SmQhxuNSQbY44TmrHXrNaVR4=\ngithub.com/lima-vm/sshocker v0.3.9/go.mod h1:YU7BBIy8iCJm5F68qwA+XCLOFJH5cMj8NsRUppKA33Y=\ngithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y=\ngithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=\ngithub.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=\ngithub.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=\ngithub.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=\ngithub.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=\ngithub.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=\ngithub.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=\ngithub.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=\ngithub.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=\ngithub.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/mikefarah/yq/v4 v4.52.4 h1:wZlxBMjyKCzzQjL0u6a3zToKuyE7OdJr4OtLBtwph4Q=\ngithub.com/mikefarah/yq/v4 v4.52.4/go.mod h1:8QwgSgDsmt4LCbfwvGUAh5oWSukRRuVJ8Gj98zJ/45o=\ngithub.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=\ngithub.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=\ngithub.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=\ngithub.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=\ngithub.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=\ngithub.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=\ngithub.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=\ngithub.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=\ngithub.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=\ngithub.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=\ngithub.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=\ngithub.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=\ngithub.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=\ngithub.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=\ngithub.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=\ngithub.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=\ngithub.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=\ngithub.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=\ngithub.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\ngithub.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=\ngithub.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=\ngithub.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=\ngithub.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=\ngithub.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0=\ngithub.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngo.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=\ngo.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=\ngolang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=\ngolang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=\ngolang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE=\ngopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\ngvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k=\ngvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=\n"
  },
  {
    "path": "hack/allowed-licenses.txt",
    "content": "Apache-2.0,BSD-2-Clause,BSD-2-Clause-FreeBSD,BSD-3-Clause,MIT,ISC,Python-2.0,PostgreSQL,X11,Zlib\n"
  },
  {
    "path": "hack/ansible-test.yaml",
    "content": "- hosts: all\n  tasks:\n  - name: Create test file\n    file:\n      path: \"/tmp/param-{{ lookup('ansible.builtin.env', 'PARAM_ANSIBLE') }}\"\n      state: touch\n"
  },
  {
    "path": "hack/bats/README.md",
    "content": "# BATS Integration Tests\n\nSee the [Testing](https://lima-vm.io/docs/dev/testing/) page for how to run these tests\nand the [BATS Style Guide](https://lima-vm.io/docs/dev/testing/bats-style/) for coding conventions.\n"
  },
  {
    "path": "hack/bats/extras/README.md",
    "content": "# Extra tests\n\nThe extra tests located in this directory are not automatically executed via `make bats`.\n\nSome tests are executed on the CI, some ones are not.\nRefer to the configuration of the GitHub Actions to see what tests are executed.\n"
  },
  {
    "path": "hack/bats/extras/colima.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nlocal_setup_file() {\n    colima start\n}\n\nlocal_teardown_file() {\n    colima stop\n    colima delete -f\n}\n\n@test 'Docker' {\n    docker run -p 8080:80 -d --name nginx \"${TEST_CONTAINER_IMAGES[nginx]}\"\n    sleep 5\n    run -0 curl -sSI --retry 5 --retry-all-errors http://localhost:8080\n    assert_output --partial \"200 OK\"\n    docker rm -f nginx\n}\n"
  },
  {
    "path": "hack/bats/extras/freebsd.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nlocal_setup_file() {\n    limactl start --tty=false template:freebsd\n}\n\nlocal_teardown_file() {\n    # <DEBUG>\n    set -x\n    tail -n 100 \"${LIMA_HOME}\"/freebsd/*.log\n    limactl shell freebsd -- cat /var/log/messages\n    # </DEBUG>\n    limactl stop freebsd\n    limactl rm freebsd\n}\n\n@test 'Smoke test' {\n    run -0 limactl shell freebsd -- uname\n    # FIXME: the shell always shows freebsd-tips (specified in ~/.login)\n    assert_output --partial \"FreeBSD\"\n}\n"
  },
  {
    "path": "hack/bats/extras/k8s.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This test verifies that a Kubernetes cluster can be started and that the single node is ready.\n\nload \"../helpers/load\"\n\n: \"${TEMPLATE:=k8s}\"\n\n# Instance names are \"${NAME}-0\", \"${NAME}-1\", ...\nNAME=\"k8s\"\n\nget_num_nodes() {\n    local nodes=0\n    for tag in \"${BATS_TEST_TAGS[@]}\"; do\n        if [[ $tag =~ ^nodes:([0-9]+)$ ]]; then\n            nodes=\"${BASH_REMATCH[1]}\"\n        fi\n    done\n    if [[ $nodes -eq 0 ]]; then\n        echo >&2 \"nodes:N tag is required\"\n        exit 1\n    fi\n    echo \"$nodes\"\n}\n\nlocal_setup() {\n    local nodes=$(get_num_nodes)\n    local params=\"\"\n    for ((i=0; i<1; i++)); do\n        limactl delete --force \"${NAME}-$i\" || :\n        local limactl_start_flags=\"--tty=false --name \"${NAME}-$i\"\"\n        # Multi-node setup requires user-v2 network for VM-to-VM communication\n        if [[ $nodes -gt 1 ]]; then\n            limactl_start_flags+=\" --network lima:user-v2\"\n        fi\n        limactl start ${limactl_start_flags} \"template:${TEMPLATE}\" 3>&- 4>&- &\n    done\n    wait $(jobs -p)\n    # Multi-node setup\n    if [[ $nodes -gt 1 ]]; then\n        for ((i=0; i<nodes; i++)); do\n            if [[ $i -eq 0 && \"${TEMPLATE}\" == \"k8s\" ]]; then\n                # Get the join command from the first node\n                join_command=$(limactl shell \"${NAME}-0\" sudo kubeadm token create --print-join-command)\n                # kubeadm join ADDRESS --token TOKEN --discovery-token-ca-cert-hash DISCOVERY_TOKEN_CA_CERT_HASH\n                read -ra words <<< \"$join_command\"\n                assert_equal \"${words[1]} ${words[3]} ${words[5]}\" \"join --token --discovery-token-ca-cert-hash\"\n                params=\".param.url=\\\"https://${words[2]}\\\"|.param.token=\\\"${words[4]}\\\"|.param.discoveryTokenCaCertHash=\\\"${words[6]}\\\"\"\n            elif [[ $i -eq 0 && \"${TEMPLATE}\" == \"k3s\" ]]; then\n                url=$(printf \"https://lima-%s.internal:6443\\n\" \"${NAME}-0\")\n                token=$(limactl shell \"${NAME}-0\" sudo cat /var/lib/rancher/k3s/server/node-token)\n                params=\".param.url=\\\"${url}\\\"|.param.token=\\\"${token}\\\"\"\n            else\n                # Execute the join command on worker nodes\n                limactl delete --force \"${NAME}-$i\" || :\n                local limactl_start_flags=\"--tty=false --name \"${NAME}-$i\"\"\n                limactl_start_flags+=\" --network lima:user-v2 --set $params\"\n                limactl start ${limactl_start_flags} \"template:${TEMPLATE}\" 3>&- 4>&- &\n            fi\n        done\n        wait $(jobs -p)\n    fi\n    for node in $(k get node -o name); do\n\t    k wait --timeout=5m --for=condition=ready \"${node}\"\n    done\n}\n\nlocal_teardown() {\n    local nodes=$(get_num_nodes)\n    for ((i=0; i<nodes; i++)); do\n        limactl delete --force \"${NAME}-$i\" || :\n    done\n}\n\nk() {\n    # The host home directory is not mounted in the case of k8s.\n    limactl shell --workdir=/ \"${NAME}-0\" -- kubectl \"$@\"\n}\n\n# bats test_tags=nodes:1\n@test 'Single-node' {\n    # Deploy test services\n    services=(nginx coredns)\n    for svc in \"${services[@]}\"; do\n        k create deployment \"$svc\" --image=\"${TEST_CONTAINER_IMAGES[\"$svc\"]}\"\n    done\n    for svc in \"${services[@]}\"; do\n        k rollout status deployment \"$svc\" --timeout 60s\n    done\n\n    # Test TCP port forwarding\n    k create service nodeport nginx --node-port=31080 --tcp=80:80\n    run curl --fail --silent --show-error --retry 30 --retry-all-errors http://localhost:31080\n    assert_success\n    assert_output --partial \"Welcome to nginx\"\n\n    # Test UDP port forwarding\n    #\n    # `kubectl create service nodeport` does not support UDP, so use `kubectl expose` instead.\n    # https://github.com/kubernetes/kubernetes/issues/134732\n    k expose deployment coredns --port=53 --type=NodePort \\\n        --overrides='{\"spec\":{\"ports\":[{\"port\":53,\"protocol\":\"UDP\",\"targetPort\":53,\"nodePort\":32053}]}}'\n    run dig @127.0.0.1 -p 32053 lima-vm.io\n    assert_success\n\n    # Cleanup\n    for svc in \"${services[@]}\"; do\n        k delete service \"$svc\"\n        k delete deployment \"$svc\"\n    done\n}\n\n# bats test_tags=nodes:3\n@test 'Multi-node' {\n    # Based on https://github.com/rootless-containers/usernetes/blob/gen2-v20250828.0/hack/test-smoke.sh\n    k apply -f - <<EOF\napiVersion: v1\nkind: Service\nmetadata:\n  name: dnstest\n  labels:\n    run: dnstest\nspec:\n  type: ClusterIP\n  clusterIP: None\n  ports:\n  - name: http\n    protocol: TCP\n    port: 80\n    targetPort: 80\n  selector:\n    run: dnstest\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: dnstest\nspec:\n  serviceName: dnstest\n  selector:\n    matchLabels:\n      run: dnstest\n  replicas: 3\n  template:\n    metadata:\n      labels:\n        run: dnstest\n    spec:\n      containers:\n      - name: dnstest\n        image: ${TEST_CONTAINER_IMAGES[nginx]}\n        ports:\n        - containerPort: 80\nEOF\n    k rollout status --timeout=5m statefulset/dnstest || {\n        k describe pods -l run=dnstest\n        false\n    }\n    # --rm requires -i\n    k run -i --rm --image=${TEST_CONTAINER_IMAGES[nginx]} --restart=Never dnstest-shell -- sh -exc 'for f in $(seq 0 2); do wget -O- http://dnstest-${f}.dnstest.default.svc.cluster.local; done'\n    k delete service dnstest\n    k delete statefulset dnstest\n}\n"
  },
  {
    "path": "hack/bats/extras/port-monitor.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This test verifies that when a container is destroyed, its ports can be reused\n# immediately and are not subject to being freed by a polling loop. See #4066.\n\n# This test should not be run in CI as it is not totally reliable: there is always a chance that the server will\n# take longer to actually respond to requests after opening the port. The test works around it by retrying once\n# on curl exit code 52, but have been observed at least once to fail by refusing to connect.\n\nload \"../helpers/load\"\n\n: \"${TEMPLATE:=default}\" # Alternative: \"docker\"\n\nNAME=nginx\n\nlocal_setup_file() {\n    limactl delete --force \"$NAME\" || :\n    limactl start --yes --name \"$NAME\" --mount \"$BATS_TMPDIR\" \"template:${TEMPLATE}\" 3>&- 4>&-\n}\n\nlocal_teardown_file() {\n    limactl delete --force \"$NAME\"\n}\n\nctrctl() {\n    if [[ $(limactl ls \"$NAME\" --yq .config.containerd.user) == true ]]; then\n        limactl shell $NAME nerdctl \"$@\"\n    else\n        limactl shell $NAME docker \"$@\"\n    fi\n}\n\nnginx_start() {\n    echo \"$COUNTER\" >\"${BATS_TEST_TMPDIR}/index.html\"\n    ctrctl run -d --name nginx -p 8080:80 -v \"${BATS_TEST_TMPDIR}:/usr/share/nginx/html:ro\" nginx\n}\n\nnginx_stop() {\n    ctrctl stop nginx\n    ctrctl rm nginx\n}\n\nverify_port() {\n    run curl --silent http://127.0.0.1:8080\n    # If nginx is not quite ready and doesn't send any response at all, give it one extra chance\n    if [[ $status -eq 52 ]]; then\n        sleep 0.5\n        run curl --silent http://127.0.0.1:8080\n    fi\n    assert_success\n    assert_output \"$COUNTER\"\n}\n\n@test 'Verify that the container is working' {\n    COUNTER=0\n    ctrctl pull --quiet nginx\n    nginx_start\n    verify_port\n}\n\n@test 'Stop and restart the container multiple times' {\n    for COUNTER in {1..100}; do\n        nginx_stop\n        nginx_start\n        verify_port\n    done\n}\n"
  },
  {
    "path": "hack/bats/helpers/limactl.bash",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# Create a dummy Lima instance for testing purposes. It cannot be started because it doesn't have an actual image.\n# This function intentionally doesn't use create/editflags, but modifies the template with yq instead.\ncreate_dummy_instance() {\n    local name=$1\n    local expr=${2:-}\n\n    # Template does not validate without an image, and the image must point to a file that exists (for clonefile).\n    local template=\"{images: [location: /etc/profile]}\"\n    if [[ -n $expr ]]; then\n        template=\"$(limactl yq \"$expr\" <<<\"$template\")\"\n    fi\n    limactl create --name \"$name\" - <<<\"$template\"\n}\n\n# Ensure a Lima instance exists. When LIMA_BATS_REUSE_INSTANCE is set, reuse an\n# existing running instance. Otherwise delete and recreate it.\n# The instance configuration is determined by its name; add a case below for new names.\n# Close file handles 3 and 4 so the host agent doesn't block BATS from exiting.\nensure_instance() {\n    local instance=$1\n    if [[ -n \"${LIMA_BATS_REUSE_INSTANCE:-}\" ]]; then\n        run limactl list --format '{{.Status}}' \"$instance\"\n        [[ $status == 0 ]] && [[ $output == \"Running\" ]] && return\n    fi\n    limactl unprotect \"$instance\" || :\n    limactl delete --force \"$instance\" || :\n    case \"$instance\" in\n        bats)          limactl start --yes --name \"$instance\" template:default 3>&- 4>&- ;;\n        bats-nomount)  limactl start --yes --name \"$instance\" --mount-none template:default 3>&- 4>&- ;;\n        bats-dummy)    create_dummy_instance \"$instance\" '.disk = \"1M\"' ;;\n        *)\n            echo \"ensure_instance: unknown instance name '$instance'\" >&2\n            return 1\n            ;;\n    esac\n}\n\n# Delete the given Lima instance unless LIMA_BATS_REUSE_INSTANCE is set.\ndelete_instance() {\n    local instance=$1\n    if [[ -z \"${LIMA_BATS_REUSE_INSTANCE:-}\" ]]; then\n        limactl unprotect \"$instance\" || :\n        limactl delete --force \"$instance\" || :\n    fi\n}\n"
  },
  {
    "path": "hack/bats/helpers/load.bash",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -o errexit -o nounset -o pipefail\n\n# Make sure run() will execute all functions with errexit enabled.\n# This enables the functionality from https://github.com/lima-vm/bats-core/commit/507da84de75fa78798d53eceb42e68851ef5c48b\n# The upstream PR https://github.com/bats-core/bats-core/pull/1118 is still open, so our submodule points to the PR commit.\nexport BATS_RUN_ERREXIT=1\n\n# BATS_TEST_RETRIES must be set for the individual test and cannot be imported from the\n# parent environment because the BATS test runner sets it to 0 before running the test.\nBATS_TEST_RETRIES=${LIMA_BATS_ALL_TESTS_RETRIES:-0}\n\n# Known flaky tests should call `flaky` inside the @test to allow retries up to\n# LIMA_BATS_FLAKY_TESTS_RETRIES even when the LIMA_BATS_ALL_TESTS_RETRIES is lower.\nflaky() {\n    BATS_TEST_RETRIES=${LIMA_BATS_FLAKY_TESTS_RETRIES:-$BATS_TEST_RETRIES}\n}\n\n# Don't run the tests in ~/.lima because they may destroy _config, _templates etc.\nexport LIMA_HOME=${LIMA_BATS_LIMA_HOME:-$HOME/.lima-bats}\n\nabsolute_path() {\n    (\n        cd \"$1\"\n        pwd\n    )\n}\n\nPATH_BATS_HELPERS=$(absolute_path \"$(dirname \"${BASH_SOURCE[0]}\")\")\nPATH_BATS_ROOT=$(absolute_path \"$PATH_BATS_HELPERS/..\")\n\nsource \"$PATH_BATS_ROOT/lib/bats-support/load.bash\"\nsource \"$PATH_BATS_ROOT/lib/bats-assert/load.bash\"\nsource \"$PATH_BATS_ROOT/lib/bats-file/load.bash\"\n\nsource \"$PATH_BATS_HELPERS/limactl.bash\"\nsource \"$PATH_BATS_HELPERS/logs.bash\"\n\nbats_require_minimum_version 1.5.0\n\nrun_e() {\n    run --separate-stderr \"$@\"\n}\n\n# If called from foo() this function will call local_foo() if it exist.\ncall_local_function() {\n    local func\n    func=\"local_${FUNCNAME[1]}\"\n    if [ \"$(type -t \"$func\")\" = \"function\" ]; then\n        \"$func\"\n    fi\n}\n\nsetup_file() {\n    if [[ ${CI:-} == true ]]; then\n        # Without a terminal the output is using TAP formatting, which does not include the filename\n        local TEST_FILENAME=${BATS_TEST_FILENAME#\"$PATH_BATS_ROOT/tests/\"}\n        TEST_FILENAME=${TEST_FILENAME%.bats}\n        echo \"# ===== ${TEST_FILENAME} =====\" >&3\n    fi\n    if [[ -n \"${INSTANCE:-}\" ]]; then\n        ensure_instance \"$INSTANCE\"\n    fi\n    call_local_function\n}\nteardown_file() {\n    call_local_function\n    if [[ -n \"${INSTANCE:-}\" ]]; then\n        delete_instance \"$INSTANCE\"\n    fi\n}\nsetup() {\n    call_local_function\n}\nteardown() {\n    call_local_function\n}\n\nassert_output_lines_count() {\n    assert_equal \"${#lines[@]}\" \"$1\"\n}\n\n# Use GHCR and ECR to avoid hitting Docker Hub rate limit.\n# NOTE: keep this list in sync with hack/test-templates.sh .\ndeclare -A -g TEST_CONTAINER_IMAGES=(\n    [\"nginx\"]=\"ghcr.io/stargz-containers/nginx:1.19-alpine-org\"\n    [\"coredns\"]=\"public.ecr.aws/eks-distro/coredns/coredns:v1.12.2-eks-1-31-latest\"\n)\n"
  },
  {
    "path": "hack/bats/helpers/logs.bash",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# Format the string the way strconv.Quote() would do.\n# If the input ends with an ellipsis then no closing quote will be added (and the … will be removed).\nquote_msg() {\n    local quoted\n    quoted=$(sed -e 's/\\\\/\\\\\\\\/g' -e 's/\"/\\\\\"/g' -e 's/^/\"/' <<<\"$1\")\n    if [[ $quoted == *… ]]; then\n        echo \"${quoted%…}\"\n    else\n        echo \"${quoted}\\\"\"\n  fi\n}\n\nassert_fatal() {\n    assert_stderr_line --partial \"level=fatal msg=$(quote_msg \"$1\")\"\n}\nassert_error() {\n    assert_stderr_line --partial \"level=error msg=$(quote_msg \"$1\")\"\n}\nassert_warning() {\n    assert_stderr_line --partial \"level=warning msg=$(quote_msg \"$1\")\"\n}\nassert_info() {\n    assert_stderr_line --partial \"level=info msg=$(quote_msg \"$1\")\"\n}\nassert_debug() {\n    assert_stderr_line --partial \"level=debug msg=$(quote_msg \"$1\")\"\n}\n"
  },
  {
    "path": "hack/bats/tests/copy.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nINSTANCE=bats\n\nsetup() {\n    limactl shell \"$INSTANCE\" -- mkdir -p /tmp/test_limactl_copy\n}\n\nteardown() {\n    limactl shell \"$INSTANCE\" -- rm -rf /tmp/test_limactl_copy\n}\n\ntest_copy_dir_from_host_to_instance() {\n    backend=\"$1\"\n    mkdir -p \"$BATS_TEST_TMPDIR/foo/bar\"\n\n    # SRC DST\n    limactl copy --backend=\"$backend\" -r \"$BATS_TEST_TMPDIR/foo\" \"$INSTANCE\":/tmp/test_limactl_copy/foo\n    limactl shell \"$INSTANCE\" -- test -d /tmp/test_limactl_copy/foo/bar\n    limactl shell \"$INSTANCE\" -- rm -rf /tmp/test_limactl_copy/foo\n\n    # SRC/ DST\n    limactl shell \"$INSTANCE\" -- mkdir -p /tmp/test_limactl_copy/foo_src_with_slash/\n    limactl copy --backend=\"$backend\" -r \"$BATS_TEST_TMPDIR/foo/\" \"$INSTANCE\":/tmp/test_limactl_copy/foo_src_with_slash\n    limactl shell \"$INSTANCE\" -- test -d /tmp/test_limactl_copy/foo_src_with_slash/foo\n    limactl shell \"$INSTANCE\" -- rm -rf /tmp/test_limactl_copy/foo_src_with_slash\n\n    # SRC DST/\n    limactl shell \"$INSTANCE\" -- mkdir -p /tmp/test_limactl_copy/foo_dst_with_slash/\n    limactl copy --backend=\"$backend\" -r \"$BATS_TEST_TMPDIR/foo\" \"$INSTANCE\":/tmp/test_limactl_copy/foo_dst_with_slash/\n    limactl shell \"$INSTANCE\" -- test -d /tmp/test_limactl_copy/foo_dst_with_slash/foo\n    limactl shell \"$INSTANCE\" -- rm -rf /tmp/test_limactl_copy/foo_dst_with_slash\n\n    # SRC/ DST/\n    limactl shell \"$INSTANCE\" -- mkdir -p /tmp/test_limactl_copy/foo_src_dst_with_slash/\n    limactl copy --backend=\"$backend\" -r \"$BATS_TEST_TMPDIR/foo/\" \"$INSTANCE\":/tmp/test_limactl_copy/foo_src_dst_with_slash/\n    limactl shell \"$INSTANCE\" -- test -d /tmp/test_limactl_copy/foo_src_dst_with_slash/foo\n    limactl shell \"$INSTANCE\" -- rm -rf /tmp/test_limactl_copy/foo_src_dst_with_slash\n}\n\n@test \"copy directory from host to Lima instance (scp)\" {\n    test_copy_dir_from_host_to_instance scp\n}\n\n# https://github.com/lima-vm/lima/issues/4468\n@test \"copy directory from host to Lima instance (rsync)\" {\n    test_copy_dir_from_host_to_instance rsync\n}\n\ntest_copy_dir_from_instance_to_host() {\n    backend=\"$1\"\n\n    limactl shell \"$INSTANCE\" -- mkdir -p /tmp/test_limactl_copy/foo/bar\n\n    # SRC DST\n    limactl copy --backend=\"$backend\" -r \"$INSTANCE\":/tmp/test_limactl_copy/foo \"$BATS_TEST_TMPDIR/foo\"\n    assert_dir_exists \"$BATS_TEST_TMPDIR/foo/bar\"\n\n    # SRC/ DST\n    limactl copy --backend=\"$backend\" -r \"$INSTANCE\":/tmp/test_limactl_copy/foo/ \"$BATS_TEST_TMPDIR/foo_src_with_slash\"\n    assert_dir_exists \"$BATS_TEST_TMPDIR/foo_src_with_slash/bar\"\n\n    # SRC DST/\n    limactl copy --backend=\"$backend\" -r \"$INSTANCE\":/tmp/test_limactl_copy/foo \"$BATS_TEST_TMPDIR/foo_dst_with_slash/\"\n    assert_dir_exists \"$BATS_TEST_TMPDIR/foo_dst_with_slash/bar\"\n\n    # SRC/ DST/\n    limactl copy --backend=\"$backend\" -r \"$INSTANCE\":/tmp/test_limactl_copy/foo/ \"$BATS_TEST_TMPDIR/foo_src_dst_with_slash/\"\n    assert_dir_exists \"$BATS_TEST_TMPDIR/foo_src_dst_with_slash/bar\"\n}\n\n@test \"copy directory from Lima instance to host (scp)\" {\n    test_copy_dir_from_instance_to_host scp\n}\n\n@test \"copy directory from Lima instance to host (rsync)\" {\n    test_copy_dir_from_instance_to_host rsync\n}\n\ntest_copy_file_from_host_to_instance() {\n    backend=\"$1\"\n    echo \"hello\" > \"$BATS_TEST_TMPDIR/hello.txt\"\n\n    limactl copy --backend=\"$backend\" \"$BATS_TEST_TMPDIR/hello.txt\" \"$INSTANCE\":/tmp/test_limactl_copy/hello.txt\n\n    run -0 limactl shell \"$INSTANCE\" -- cat /tmp/test_limactl_copy/hello.txt\n    assert_output \"hello\"\n\n    limactl shell \"$INSTANCE\" -- rm -f /tmp/test_limactl_copy/hello.txt\n}\n\n@test \"copy file from host to Lima instance (scp)\" {\n    test_copy_file_from_host_to_instance scp\n}\n\n@test \"copy file from host to Lima instance (rsync)\" {\n    test_copy_file_from_host_to_instance rsync\n}\n\ntest_copy_file_from_instance_to_host() {\n    backend=\"$1\"\n    limactl shell \"$INSTANCE\" -- bash -c 'echo \"hello\" > /tmp/test_limactl_copy/hello.txt'\n\n    limactl copy --backend=\"$backend\" \"$INSTANCE\":/tmp/test_limactl_copy/hello.txt \"$BATS_TEST_TMPDIR/hello.txt\"\n\n    run -0 cat \"$BATS_TEST_TMPDIR/hello.txt\"\n    assert_output \"hello\"\n\n    limactl shell \"$INSTANCE\" -- rm -f /tmp/hello.txt\n}\n\n@test \"copy file from Lima instance to host (scp)\" {\n    test_copy_file_from_instance_to_host scp\n}\n\n@test \"copy file from Lima instance to host (rsync)\" {\n    test_copy_file_from_instance_to_host rsync\n}\n"
  },
  {
    "path": "hack/bats/tests/list.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\n# Use a separate LIMA_HOME for this test that we can just wipe because it will never have running instances.\n# We cannot use $BATS_FILE_TMPDIR because `limactl create` will complain about max socket path name length.\nLOCAL_LIMA_HOME=\"${LIMA_HOME:?}/_bats\"\n\nlocal_setup_file() {\n    export LIMA_HOME=\"${LOCAL_LIMA_HOME:?}\"\n    rm -rf \"${LIMA_HOME:?}\"\n\n    run -0 create_dummy_instance \"foo\" '.disk = \"1M\"'\n    run -0 create_dummy_instance \"bar\" '.disk = \"2M\"'\n    run -0 create_dummy_instance \"baz\" '.disk = \"3M\"'\n}\n\nlocal_setup() {\n    export LIMA_HOME=\"${LOCAL_LIMA_HOME:?}\"\n}\n\n@test 'list with no running instances shows a warning and exits without error' {\n    export LIMA_HOME=\"$BATS_TEST_TMPDIR\"\n    run_e -0 limactl list\n    assert_warning 'No instance found. Run `limactl create` to create an instance.'\n}\n\n@test 'check plain list output' {\n    # also verifies that `ls` is an alias for `list`\n    run -0 limactl ls\n\n    # instances will be sorted alphabetically\n    assert_line --index 0 --regexp '^NAME +STATUS.+DISK'\n    assert_line --index 1 --regexp '^bar +Stopped.+ 2MiB'\n    assert_line --index 2 --regexp '^baz +Stopped.+ 3MiB'\n    assert_line --index 3 --regexp '^foo +Stopped.+ 1MiB'\n    # there is no other output\n    assert_output_lines_count 4\n}\n\n@test 'only list selected instances' {\n    run -0 limactl ls foo bar\n\n    # instances will be sorted in the order they were specified\n    assert_line --index 0 --regexp '^NAME'\n    assert_line --index 1 --regexp '^foo'\n    assert_line --index 2 --regexp '^bar'\n    refute_line --partial baz\n    assert_output_lines_count 3\n}\n\n@test 'requesting non-existing instance is an error' {\n    run_e -1 limactl ls foo foobar bar\n    assert_warning 'No instance matching foobar found.'\n    assert_fatal 'unmatched instances'\n\n    # existing instances are still listed\n    assert_line --index 0 --regexp '^NAME'\n    assert_line --index 1 --regexp '^foo'\n    assert_line --index 2 --regexp '^bar'\n    assert_output_lines_count 3\n}\n\n@test '--quiet option shows only names, no header' {\n    run -0 limactl list --quiet foo bar\n    assert_line --index 0 foo\n    assert_line --index 1 bar\n    assert_output_lines_count 2\n}\n\n@test '--format json returns JSON output' {\n    run -0 limactl ls --format json foo bar\n\n    # test may be too strict in expecting \"name\" to be the first key on the line\n    assert_line --index 0 --regexp '^\\{\"name\":\"foo\",'\n    assert_line --index 1 --regexp '^\\{\"name\":\"bar\",'\n    assert_output_lines_count 2\n}\n\n@test '--json is shorthand for --format json' {\n    run -0 limactl ls foo bar --format json\n    format_json=$output\n\n    run -0 limactl ls foo bar --json\n    assert_output \"$format_json\"\n}\n\n@test '--format YAML returns YAML documents' {\n    # save JSON output for comparison\n    run -0 limactl ls foo bar --format json\n    json=$output\n\n    run -0 limactl ls foo bar --format yaml\n    yaml=$output\n\n    assert_line --regexp '^name: foo'\n    assert_line --regexp '^name: bar'\n    refute_line --regexp '^name: baz'\n\n    # verify that the output consists of 2 documents\n    run -0 limactl yq 'true' <<<\"$yaml\"\n    assert_output_lines_count 2\n\n    # convert YAML to JSON\n    run -0 limactl yq --input-format yaml --output-format json  --indent 0 \".\" <<<\"$yaml\"\n    assert_output_lines_count 2\n\n    # verify it matches the JSON output\n    assert_output \"$json\"\n\n}\n\n@test 'JSON output to terminal is colorized, but semantically identical' {\n    run -0 limactl ls foo bar --format json\n    json=$output\n\n    # colorize output even when stdout is not a tty\n    export _LIMA_OUTPUT_IS_TTY=1\n    run -0 limactl ls foo bar --format json\n    colorized=$output\n\n    # check if the output contains an ANSI \"reset mode\" sequence (\"ESC[0m\")\n    run -0 cat -v <<<\"$output\"\n    assert_output --partial \"^[[0m\"\n\n    # remove all ANSI formatting codes from the output\n    run -0 sed 's/\\x1b\\[[0-9;]*m//g' <<<\"$colorized\"\n\n    # Flatten the pretty-printed JSON to a single line per object with no extra whitespace.\n    # yq processes JSON objects in sequence (when input format is set to json) and\n    # does not require each object to be on a single line.\n    run -0 limactl yq --input-format json --output-format json  --indent 0 \".\" <<<\"$output\"\n    assert_output_lines_count 2\n\n    # compare to the plain (uncolorized) json output\n    assert_output \"$json\"\n}\n\n@test 'YAML output to terminal is colorized, but semantically identical' {\n    # save uncolorized JSON output\n    run -0 limactl ls foo bar --format json\n    json=$output\n\n    # colorize output even when stdout is not a tty\n    export _LIMA_OUTPUT_IS_TTY=1\n    run -0 limactl ls foo bar --format yaml\n    colorized=$output\n\n    # check if the output contains an ANSI \"reset mode\" sequence (\"ESC[0m\")\n    run -0 cat -v <<<\"$output\"\n    assert_output --partial \"^[[0m\"\n\n    # remove all ANSI formatting codes from the output\n    run -0 sed 's/\\x1b\\[[0-9;]*m//g' <<<\"$colorized\"\n    yaml=$output\n\n    # Verify that the output consists of 2 documents\n    run -0 limactl yq 'true' <<<\"$yaml\"\n    assert_output_lines_count 2\n\n    # convert the pretty-printed YAML to JSON Lines format with no whitespace\n    run -0 limactl yq --indent 0 --input-format yaml --output-format json \".\" <<<\"$yaml\"\n    assert_output_lines_count 2\n\n    # verify it matches the JSON output\n    assert_output \"$json\"\n}\n\n@test '--all-fields includes all fields in the table' {\n    skip \"only works with output to a terminal (#3986)\"\n    # TODO provide a way to specify the width, e.g. with `--width 120`\n    # See https://github.com/lima-vm/lima/issues/3986\n}\n\n@test 'Use field names in Go template format' {\n    run -0 limactl ls foo bar --format '{{.Name}} {{.Disk}}'\n    assert_line --index 0 \"foo 1048576\"\n    assert_line --index 1 \"bar 2097152\"\n}\n\n@test '--list-fields list all available fields' {\n    run -0 limactl ls --list-fields\n    assert_line Name\n    assert_line CPUs\n    assert_line Memory\n    # All field names start with an uppercase letter and don't contain any spaces\n    refute_line --regexp '^[^A-Z]'\n    refute_line --partial ' '\n }\n\n @test '--list-fields does not list deprecated field, but they are still available' {\n    run -0 limactl ls --list-fields\n\n    # no deprecated fields are listed\n    refute_line \"HostArch\"\n    refute_line \"HostOS\"\n    refute_line \"IdentityFile\"\n    refute_line \"LimaHome\"\n\n    # all deprecated fields exist and produce output\n    run -0 limactl ls foo --format '{{.HostArch}}'\n    assert_output\n    run -0 limactl ls foo --format '{{.HostOS}}'\n    assert_output\n    run -0 limactl ls foo --format '{{.IdentityFile}}'\n    assert_output\n    run -0 limactl ls foo --format '{{.LimaHome}}'\n    assert_output \"$LIMA_HOME\"\n\n    # verify that a non-existing field throws an error and produces no output\n    # TODO the error message is not really end-user friendly, not sure if we can do something about it\n    run_e -1 limactl ls foo --format '{{.Unknown}}'\n    assert_stderr --regexp \"level=fatal.*can't evaluate field Unknown\"\n    refute_output\n}\n\n@test '--quiet option can only be used with format --table' {\n    # TODO the error message is incorrect, it can be used with --yq\n    run_e -1 limactl list --quiet --format json\n    assert_fatal \"option --quiet can only be used with '--format table'\"\n\n    run_e -1 limactl list --quiet --format yaml\n    assert_fatal \"option --quiet can only be used with '--format table'\"\n\n    run_e -1 limactl list --quiet --format '{{.Name}} {{.Disk}}'\n    assert_fatal \"option --quiet can only be used with '--format table'\"\n}\n\n@test '--yq option implies --format json' {\n    run -0 limactl ls baz --yq '.config'\n    assert_output --regexp '^\\{\"'\n\n    run -0 limactl yq '.disk' <<<\"$output\"\n    assert_output \"3M\"\n}\n\n@test '--yq option can be used with --format yaml' {\n    run -0 limactl ls baz --yq '.config' --format yaml\n    assert_line \"disk: 3M\"\n}\n\n@test '--yq option can be specified multiple times' {\n    run -0 limactl ls foo --yq '.config' --yq '.user' --yq '.uid'\n    assert_output \"$UID\"\n\n    run -0 limactl ls --yq 'select(.disk > 1024*1024)' --yq 'select(.name | test(\"z\"))' --yq '.name'\n    assert_output \"baz\"\n}\n\n@test '--yq option is incompatible with --format table or Go templates' {\n    run_e -1 limactl ls --yq '.name' --format table\n    assert_fatal \"option --yq only works with --format json or yaml\"\n\n    run_e -1 limactl ls --yq '.name' --format '{{.Name}} {{.Disk}}'\n    assert_fatal \"option --yq only works with --format json or yaml\"\n}\n\n@test '--quiet option can be used with --yq' {\n    run -0 limactl ls --quiet\n    assert_line --index 0 \"bar\"\n    assert_output_lines_count 3\n\n    run -0 limactl ls --quiet --yq 'select(.name == \"foo\")'\n    assert_output \"foo\"\n}\n\n@test '--yq cannot access environment variables' {\n    run_e -1 limactl ls --yq 'env(HOME)'\n    assert_fatal \"env operations have been disabled\"\n}\n\n@test '--yq cannot load files' {\n    run_e -1 limactl ls --yq \"load(\\\"${BASH_SOURCE[0]}\\\")\"\n    assert_fatal \"file operations have been disabled\"\n}\n\n@test '--filter option filters instances' {\n    run -0 limactl ls --filter '.name == \"foo\"'\n    assert_line --index 0 --regexp '^NAME'\n    assert_line --index 1 --regexp '^foo'\n    assert_output_lines_count 2\n}\n\n@test '--filter option works with all output formats' {\n    run -0 limactl ls --filter '.name == \"foo\"'\n    assert_line --index 1 --regexp '^foo'\n\n    run -0 limactl ls --filter '.name == \"foo\"' --format json\n    assert_line --index 0 --regexp '^\\{\"name\":\"foo\",'\n\n    run -0 limactl ls --filter '.name == \"foo\"' --format '{{.Name}}'\n    assert_output \"foo\"\n}\n\n@test '--filter option is compatible with --yq' {\n    run -0 limactl ls --filter '.name == \"foo\"' --yq '.name'\n    assert_output \"foo\"\n}\n\n@test '--quiet option can be used with --filter' {\n    run -0 limactl ls --quiet --filter '.name == \"foo\"'\n    assert_output \"foo\"\n}\n\n@test '--filter option with no matching instances returns empty output' {\n    run -0 limactl ls -q --filter '.name == \"kcp\"'\n    assert_output \"\"\n}\n\n@test 'multiple --filter options are combined with AND logic' {\n    run -0 limactl ls -q --filter '.name == \"foo\"' --filter '.disk == \"1M\"'\n    assert_output \"\"\n\n    run -0 limactl ls --filter '.name == \"foo\"' --filter '.disk == \"1048576\"'\n    assert_line --index 1 --regexp '^foo'\n}\n"
  },
  {
    "path": "hack/bats/tests/mcp.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nINSTANCE=bats\n\n# TODO Move helper functions to shared location\nrun_yq() {\n    run -0 --separate-stderr limactl yq \"$@\"\n}\n\njson_edit() {\n    limactl yq --input-format json --output-format json --indent 0 \"$@\"\n}\n\nlocal_setup() {\n    cd \"$PATH_BATS_ROOT\"\n    coproc MCP { limactl mcp serve \"$INSTANCE\"; }\n\n    ID=0\n    mcp initialize '{\"protocolVersion\":\"2025-06-18\"}'\n\n    # Each mcp request should increment the ID\n    [[ $ID -eq 1 ]]\n\n    run_yq .serverInfo.name <<<\"$output\"\n    assert_output \"lima\"\n}\n\nlocal_teardown() {\n    kill \"${MCP_PID:?}\" 2>&1 >/dev/null || :\n}\n\nmcp() {\n    local method=$1\n    local params=${2:-}\n\n    local request\n    printf -v request '{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"%s\"}' \"$((++ID))\" \"$method\"\n    if [[ -n $params ]]; then\n        request=$(json_edit \".params=${params}\" <<<\"$request\")\n    fi\n\n    # send request to MCP server stdin\n    echo \"$request\" >&\"${MCP[1]}\"\n\n    # read response from MCP server stdout with 5s timeout\n    local json\n    while true; do\n        if ! read -t 5 -r json <&\"${MCP[0]}\"; then\n            break\n        fi\n        # If it has no \"method\" field, it's a response, not a notification\n        if ! jq -e 'has(\"method\")' <<<\"$json\" >/dev/null 2>&1; then\n            break\n        fi\n    done\n\n    # verify that the response matches the request; also validates the output is valid JSON\n    run_yq .id <<<\"$json\"\n    assert_output \"$ID\"\n\n    # there must be no error object in the response\n    run_yq .error <<<\"$json\"\n    assert_output \"null\"\n\n    # set $output to .result\n    run_yq .result <<<\"$json\"\n}\n\ntools_call() {\n    local name=$1\n    local args=${2:-}\n\n    local params\n    printf -v params '{\"name\":\"%s\"}' \"$name\"\n    if [[ -n $args ]]; then\n        params=$(json_edit \".arguments=${args}\" <<<\"$params\")\n    fi\n    mcp tools/call \"$params\"\n}\n\n@test 'list tools' {\n    mcp tools/list\n    run_yq '.tools[].name' <<<\"$output\"\n    assert_line glob\n    assert_line list_directory\n    assert_line read_file\n    assert_line run_shell_command\n    assert_line search_file_content\n    assert_line write_file\n}\n\n@test 'verify that tools descriptions include input and output schema' {\n    mcp tools/list\n    run_yq '.tools[] | select(.name == \"run_shell_command\")' <<<\"$output\"\n    json=$output\n\n    run_yq '.inputSchema.required[]' <<<\"$json\"\n    assert_line command\n    assert_line directory\n    assert_output_lines_count 2\n\n    run_yq '.inputSchema.properties | keys[]' <<<\"$json\"\n    assert_line command\n    assert_line description\n    assert_line directory\n    assert_output_lines_count 3\n\n    run_yq '.outputSchema.required[]' <<<\"$json\"\n    assert_line stdout\n    assert_line stderr\n    assert_output_lines_count 2\n\n    run_yq '.outputSchema.properties | keys[]' <<<\"$json\"\n    assert_line error\n    assert_line exit_code\n    assert_line stdout\n    assert_line stderr\n    assert_output_lines_count 4\n}\n\n@test 'run shell command returns command output' {\n    run -0 limactl shell \"$INSTANCE\" cat /etc/os-release\n    assert_output\n    expected=$output\n\n    tools_call run_shell_command '{\"directory\":\"/etc\",\"command\":[\"cat\",\"os-release\"]}'\n    json=$output\n\n    run_yq '.structuredContent.exit_code' <<<\"$json\"\n    assert_output 0\n\n    run_yq '.structuredContent.stdout' <<<\"$json\"\n    assert_output \"$expected\"\n\n    run_yq '.structuredContent.stderr' <<<\"$json\"\n    refute_output\n\n    # The same data is also available as encoded JSON\n    run_yq '.content[0].type' <<<\"$json\"\n    assert_output \"text\"\n\n    run_yq '.content[0].text' <<<\"$json\"\n    text=$output\n\n    run_yq '.exit_code' <<<\"$text\"\n    assert_output 0\n\n    run_yq '.stdout' <<<\"$text\"\n    assert_output \"$expected\"\n\n    run_yq '.stderr' <<<\"$text\"\n    refute_output\n}\n\n@test 'run shell command returns stderr and exit code' {\n    tools_call run_shell_command '{\"directory\":\"/\",\"command\":[\"bash\",\"-c\",\"echo NO>&2; exit 13\"]}'\n    json=$output\n\n    run_yq '.structuredContent.exit_code' <<<\"$json\"\n    assert_output 13\n\n    run_yq '.structuredContent.error' <<<\"$json\"\n    assert_output \"exit status 13\"\n\n    run_yq '.structuredContent.stdout' <<<\"$json\"\n    refute_output\n\n    run_yq '.structuredContent.stderr' <<<\"$json\"\n    assert_output \"NO\"\n}\n\n@test 'run shell command fails if the directory does not exist' {\n    tools_call run_shell_command '{\"directory\":\"/etcetera\",\"command\":[\"cat\",\"os-release\"]}'\n    json=$output\n\n    run_yq '.structuredContent.exit_code' <<<\"$json\"\n    assert_output 1\n\n    run_yq '.structuredContent.stderr' <<<\"$json\"\n    assert_output --partial \"No such file or directory\"\n}\n\n@test 'read_file reads a file' {\n    run -0 limactl shell \"$INSTANCE\" cat /etc/os-release\n    assert_output\n    expected=$output\n\n    tools_call read_file '{\"path\":\"/etc/os-release\"}'\n    json=$output\n\n    run_yq '.content[0].text' <<<\"$json\"\n    run_yq '.content' <<<\"$output\"\n    assert_output \"$expected\"\n\n    run_yq '.structuredContent.content' <<<\"$json\"\n    assert_output \"$expected\"\n}\n\n@test 'read_file returns an error when path does not exist' {\n    tools_call read_file '{\"path\":\"/etc/os-release-info\"}'\n    json=$output\n\n    run_yq '.isError' <<<\"$json\"\n    assert_output \"true\"\n\n    run_yq '.content[0].text' <<<\"$json\"\n    assert_output \"file does not exist\"\n}\n\n@test 'read_file returns an error when path is not absolute' {\n    tools_call read_file '{\"path\":\"os-release\"}'\n    json=$output\n\n    run_yq '.isError' <<<\"$json\"\n    assert_output \"true\"\n\n    run_yq '.content[0].text' <<<\"$json\"\n    assert_output --partial \"expected an absolute path\"\n}\n\n@test 'write_file creates new file and overwrites existing file' {\n    limactl shell \"$INSTANCE\" rm -f /tmp/mcp.test\n    tools_call write_file '{\"path\":\"/tmp/mcp.test\",\"content\":\"foo\"}'\n\n    run_yq '.content[0].text' <<<\"$output\"\n    assert_output \"{}\"\n\n    run -0 limactl shell \"$INSTANCE\" cat /tmp/mcp.test\n    assert_output \"foo\"\n\n    tools_call write_file '{\"path\":\"/tmp/mcp.test\",\"content\":\"bar\"}'\n\n    run_yq '.content[0].text' <<<\"$output\"\n    assert_output \"{}\"\n\n    run -0 limactl shell \"$INSTANCE\" cat /tmp/mcp.test\n    assert_output \"bar\"\n}\n\n@test 'write_file creates the directory if it does not yet exist' {\n    # Make sure /tmp/tmp is deletable even if we run the tests multiple times against the same Lima instance\n    limactl shell \"$INSTANCE\" chmod -R 777 /tmp/tmp || true\n    limactl shell \"$INSTANCE\" rm -rf /tmp/tmp\n    tools_call write_file '{\"path\":\"/tmp/tmp/tmp\",\"content\":\"tmp\"}'\n    json=$output\n\n    run_yq '.isError' <<<\"$json\"\n    assert_output \"null\"\n\n    run -0 limactl shell \"$INSTANCE\" cat /tmp/tmp/tmp\n    assert_output \"tmp\"\n}\n\n@test 'write_file returns an error when the directory is not writable' {\n    limactl shell \"$INSTANCE\" mkdir -p /tmp/tmp\n    limactl shell \"$INSTANCE\" chmod 444 /tmp/tmp\n    tools_call write_file '{\"path\":\"/tmp/tmp/tmp\",\"content\":\"tmp\"}'\n    json=$output\n\n    run_yq '.isError' <<<\"$json\"\n    assert_output \"true\"\n\n    run_yq '.content[0].text' <<<\"$json\"\n    assert_output \"permission denied\"\n}\n\n@test 'write_file returns an error when path is not absolute' {\n    tools_call write_file '{\"path\":\"tmp/mcp.test\",\"content\":\"baz\"}'\n    json=$output\n\n    run_yq '.isError' <<<\"$json\"\n    assert_output \"true\"\n\n    run_yq '.content[0].text' <<<\"$json\"\n    assert_output --partial \"expected an absolute path\"\n}\n\n@test 'glob finds files by wildcard' {\n    tools_call glob '{\"pattern\":\"*/*p.bats\"}'\n\n    run_yq '.structuredContent.matches[]' <<<\"$output\"\n    assert_line --regexp '/tests/mcp.bats$'\n}\n\n@test 'glob returns an empty list when the pattern does not match' {\n\n    tools_call glob '{\"pattern\":\"nothing.to.see\"}'\n\n    run_yq '.structuredContent.matches[]' <<<\"$output\"\n    assert_output_lines_count 0\n}\n\n@test 'search_file_content finds text inside files' {\n    tools_call search_file_content '{\"pattern\":\"needle in a haystack\"}'\n\n    run_yq '.structuredContent.git_grep_output' <<<\"$output\"\n    assert_line --regexp '^tests/mcp.bats:[0-9]+: +tools_call'\n}\n\n@test 'search_file_content can find unicode characters above U+FFFF' {\n    # The light bulb emoji 💡 (U+1F4A1)\n    tools_call search_file_content '{\"pattern\":\"💡\"}'\n\n    run_yq '.structuredContent.git_grep_output' <<<\"$output\"\n    assert_line --regexp '^tests/mcp.bats:[0-9]+: +# The light bulb'\n    assert_line --regexp '^tests/mcp.bats:[0-9]+: +tools_call'\n}\n\n@test 'search_file_content returns an empty string if it cannot find the pattern' {\n    tools_call search_file_content \"$(printf '{\"pattern\":\"\\U0001f4a1 not found\"}')\"\n\n    run_yq '.structuredContent.git_grep_output' <<<\"$output\"\n    refute_output\n}\n\n"
  },
  {
    "path": "hack/bats/tests/path.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nINSTANCE=bats\n\n@test \"The guest home is accessible via both .guest and .linux paths\" {\n    limactl shell \"$INSTANCE\" -- ls -ld /home/\"${USER}.guest/.ssh\"\n    limactl shell \"$INSTANCE\" -- ls -ld /home/\"${USER}.linux/.ssh\"\n}\n"
  },
  {
    "path": "hack/bats/tests/preserve-env.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nINSTANCE=bats\n\nlocal_setup_file() {\n    unset LIMA_SHELLENV_ALLOW\n    unset LIMA_SHELLENV_BLOCK\n}\n\nlocal_setup() {\n    # make sure changes from previous tests are removed\n    limactl shell \"$INSTANCE\" sh -c '[ ! -f ~/.bash_profile ] || sed -i -E \"/^export (FOO|BAR|SSH_)/d\" ~/.bash_profile'\n}\n\n@test 'there are no FOO*, BAR*, or SSH_FOO* variables defined in the VM' {\n    # just to confirm because the other tests depend on these being unused\n    run -0 limactl shell \"$INSTANCE\" printenv\n    refute_line --regexp '^FOO'\n    refute_line --regexp '^BAR'\n    refute_line --regexp '^SSH_FOO'\n}\n\n@test 'environment is not preserved by default' {\n    export FOO=foo\n    run -0 limactl shell \"$INSTANCE\" printenv\n    refute_line --regexp '^FOO='\n}\n\n@test 'environment is preserved with --preserve-env' {\n    export FOO=foo\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    assert_line FOO=foo\n}\n\n@test 'profile settings inside the VM take precedence over preserved variables' {\n    limactl shell \"$INSTANCE\" sh -c 'echo \"export FOO=bar\" >>~/.bash_profile'\n    export FOO=foo\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    assert_line FOO=bar\n}\n\n@test 'builtin block list is used when LIMA_SHELLENV_BLOCK is not set' {\n    # default block list includes SSH_*\n    export SSH_FOO=ssh_foo\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    refute_line --regexp '^SSH_FOO='\n}\n\n@test 'custom block list replaces builtin block list' {\n    export LIMA_SHELLENV_BLOCK=FOO\n    export FOO=foo\n    export SSH_FOO=foo\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    refute_line --regexp '^FOO='\n    assert_line SSH_FOO=foo\n}\n\n@test 'custom block list starting with + appends to builtin block list' {\n    export LIMA_SHELLENV_BLOCK=+FOO\n    export FOO=foo\n    export SSH_FOO=foo\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    refute_line --regexp '^FOO='\n    refute_line --regexp '^SSH_FOO='\n}\n\n@test 'block list entries can use * wildcard at the end' {\n    export LIMA_SHELLENV_BLOCK=\"FOO*\"\n    export FOO=foo\n    export FOOBAR=foobar\n    export BAR=bar\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    refute_line --regexp '^FOO'\n    assert_line BAR=bar\n}\n\n@test 'wildcard works at the start of the pattern' {\n    export LIMA_SHELLENV_BLOCK=\"*FOO\"\n    export FOO=foo\n    export BARFOO=barfoo\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    refute_line --regexp '^BARFOO='\n    refute_line --regexp '^FOO='\n}\n\n@test 'block list can use a , separated list with whitespace ignored' {\n    export LIMA_SHELLENV_BLOCK=\"FOO*, , BAR\"\n    export FOO=foo\n    export FOOBAR=foobar\n    export BAR=bar\n    export BARBAZ=barbaz\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n    refute_line --regexp '^FOO'\n    refute_line --regexp '^BAR='\n    assert_line BARBAZ=barbaz\n}\n\n@test 'allow list can use a , separated list with whitespace ignored' {\n    export LIMA_SHELLENV_ALLOW=\"SSH_FOO, , BAR*, LD_UID\"\n    export SSH_FOO=ssh_foo\n    export SSH_BAR=ssh_bar\n    export SSH_BLOCK=ssh_block\n    export BAR=bar\n    export BARBAZ=barbaz\n    export LD_UID=randomuid\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n\n    assert_line SSH_FOO=ssh_foo\n    assert_line BAR=bar\n    assert_line BARBAZ=barbaz\n    assert_line LD_UID=randomuid\n\n    refute_line --regexp '^SSH_BAR='\n    refute_line --regexp '^SSH_BLOCK='\n}\n\n@test 'wildcard patterns work in all positions' {\n    export LIMA_SHELLENV_BLOCK=\"*FOO*BAR*\"\n    export FOO=foo\n    export FOOBAR=foobar\n    export FOOXYZBAR=fooxyzbar\n    export FOOBAZ=foobaz\n    export BAZBAR=bazbar\n    export BAR=bar\n    export XFOOYBARZDOTCOM=xfooybarzdotcom\n    export NORMAL_VAR=normal_var\n    export UNRELATED=unrelated\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n\n    refute_line --regexp '^FOOBAR='\n    refute_line --regexp '^FOOXYZBAR='\n    refute_line --regexp '^XFOOYBARZDOTCOM='\n\n    assert_line FOOBAZ=foobaz\n    assert_line NORMAL_VAR=normal_var\n    assert_line UNRELATED=unrelated\n    assert_line BAZBAR=bazbar\n    assert_line BAR=bar\n    assert_line FOO=foo\n}\n\n@test 'allowlist overrides default blocklist with wildcards' {\n    export LIMA_SHELLENV_ALLOW=\"SSH_*,CUSTOM*\"\n    export LIMA_SHELLENV_BLOCK=\"+*TOKEN\"\n    export SSH_AUTH_SOCK=ssh_auth_sock\n    export SSH_CONNECTION=ssh_connection\n    export CUSTOM_VAR=custom_var\n    export MY_TOKEN=my_token\n    export UNRELATED=unrelated\n    run -0 limactl shell --preserve-env \"$INSTANCE\" printenv\n\n    assert_line SSH_AUTH_SOCK=ssh_auth_sock\n    assert_line SSH_CONNECTION=ssh_connection\n    assert_line CUSTOM_VAR=custom_var\n    refute_line --regexp '^MY_TOKEN='\n    assert_line UNRELATED=unrelated\n}\n\n@test 'invalid characters in patterns cause fatal errors' {\n    export LIMA_SHELLENV_BLOCK=\"FOO-BAR\"\n    run ! limactl shell --preserve-env \"$INSTANCE\" printenv\n    assert_output --partial \"Invalid LIMA_SHELLENV_BLOCK pattern\"\n    assert_output --partial \"contains invalid character\"\n}\n\n@test 'limactl info includes the default block list' {\n    run -0 limactl info\n    run -0 limactl yq '.shellEnvBlock[]' <<<\"$output\"\n    assert_line PATH\n    assert_line \"SSH_*\"\n    assert_line USER\n}\n"
  },
  {
    "path": "hack/bats/tests/protect.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nINSTANCE=bats-dummy\nCLONE=clone\nNOTEXIST=notexist\n\nlocal_setup_file() {\n    local inst\n    for inst in \"$CLONE\" \"$NOTEXIST\"; do\n        limactl unprotect \"$inst\" || :\n        limactl delete --force \"$inst\" || :\n    done\n}\n\n@test 'protecting a non-existing instance fails' {\n    run_e -1 limactl protect \"${NOTEXIST}\"\n    assert_fatal \"failed to inspect instance \\\"${NOTEXIST}\\\"…\"\n}\n\n@test 'protecting the dummy instance succeeds' {\n    run_e -0 limactl protect \"$INSTANCE\"\n    assert_info \"Protected \\\"${INSTANCE}\\\"\"\n    assert_file_exists \"${LIMA_HOME}/${INSTANCE}/protected\"\n}\n\n@test 'protecting it again shows a warning, but succeeds' {\n    run_e -0 limactl protect \"$INSTANCE\"\n    assert_warning \"Instance \\\"${INSTANCE}\\\" is already protected. Skipping.\"\n    assert_file_exists \"${LIMA_HOME}/${INSTANCE}/protected\"\n}\n\n@test 'cloning a protected instance creates an unprotected clone' {\n    run_e -0 limactl clone --yes \"$INSTANCE\" \"$CLONE\"\n    # TODO there is currently no output from the clone command, which feels wrong\n    refute_output\n    assert_file_not_exists \"${LIMA_HOME}/${CLONE}/protected\"\n}\n\n@test 'deleting the unprotected clone instance succeeds' {\n    run_e -0 limactl delete --force \"$CLONE\"\n    assert_info \"Deleted \\\"${CLONE}\\\"…\"\n}\n\n@test 'deleting protected dummy instance fails' {\n    run_e -1 limactl delete --force \"$INSTANCE\"\n    assert_fatal \"failed to delete instance \\\"${INSTANCE}\\\": instance is protected…\"\n    assert_file_exists \"$LIMA_HOME/$INSTANCE/protected\"\n}\n\n@test 'unprotecting the dummy instance succeeds' {\n    run_e -0 limactl unprotect \"$INSTANCE\"\n    assert_info \"Unprotected \\\"${INSTANCE}\\\"\"\n    assert_file_not_exists \"$LIMA_HOME/$INSTANCE/protected\"\n}\n\n@test 'unprotecting it again shows a warning, but succeeds' {\n    run_e -0 limactl unprotect \"$INSTANCE\"\n    assert_warning \"Instance \\\"${INSTANCE}\\\" isn't protected. Skipping.\"\n    assert_file_not_exists \"$LIMA_HOME/$INSTANCE/protected\"\n}\n\n@test 'deleting unprotected dummy instance succeeds' {\n    run_e -0 limactl delete --force \"$INSTANCE\"\n    assert_info \"Deleted \\\"${INSTANCE}\\\"…\"\n}\n"
  },
  {
    "path": "hack/bats/tests/shell-sync.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nINSTANCE=bats-nomount\n\nsetup() {\n    # Create a temporary test directory and files\n    TEST_SYNC_DIR=\"$BATS_TEST_TMPDIR/sync-test\"\n    mkdir -p \"$TEST_SYNC_DIR\"\n    touch \"$TEST_SYNC_DIR/foo.txt\"\n    touch \"$TEST_SYNC_DIR/bar.txt\"\n\n    # Create a simple script that makes changes to these files\n    cat > \"$TEST_SYNC_DIR/modify.sh\" << 'EOF'\n#!/bin/sh\nset -eu\necho \"modified foo\" > foo.txt\necho \"modified bar\" > bar.txt\nEOF\n    chmod +x \"$TEST_SYNC_DIR/modify.sh\"\n}\n\nteardown() {\n    # Clean up test directory\n    if [[ -d \"$TEST_SYNC_DIR\" ]]; then\n        rm -rf \"$TEST_SYNC_DIR\"\n    fi\n}\n\n@test 'shell --sync preserves working directory path from host to guest' {\n    cd \"$TEST_SYNC_DIR\"\n\n    # Get path of the TEST_SYNC_DIR for verification\n    local path_test_dir\n    path_test_dir=\"$PWD\"\n\n    run -0 bash -c \"limactl shell --sync . --yes '$INSTANCE' pwd && ./modify.sh\"\n\n    # Verify the guest working directory matches the host path structure\n    assert_output --regexp \".*${path_test_dir#/}\"\n\n    # Verify files were modified\n    run cat \"$TEST_SYNC_DIR/foo.txt\"\n    assert_output \"modified foo\"\n    run cat \"$TEST_SYNC_DIR/bar.txt\"\n    assert_output \"modified bar\"\n}\n\n@test 'shell --sync with directory path containing spaces and quotes' {\n    # Create directory with spaces and quotes in name and move test directory to it\n    local special_dir=\"$BATS_TEST_TMPDIR/sync test 'with' \\\"quotes\\\"\"\n    mkdir -p \"$special_dir\"\n    mv \"$TEST_SYNC_DIR\" \"$special_dir\"\n    cd \"$special_dir/sync-test\"\n\n    # Count files before sync\n    local files_before\n    files_before=$(find . -type f | wc -l)\n\n    run -0 bash -c \"limactl shell --sync . --yes '$INSTANCE' ./modify.sh\"\n\n    # Verify files were modified\n    run cat \"$special_dir/sync-test/foo.txt\"\n    assert_output \"modified foo\"\n    run cat \"$special_dir/sync-test/bar.txt\"\n    assert_output \"modified bar\"\n\n    # Count files after sync\n    local files_after\n    files_after=$(find \"$special_dir/sync-test\" -type f | wc -l)\n    [[ $files_after -eq $files_before ]]\n\n    # Cleanup\n    rm -rf \"$special_dir\"\n}\n\n@test 'shell --sync reflects file deletion from guest to host' {\n    cd \"$TEST_SYNC_DIR\"\n\n    run -0 bash -c \"limactl shell --sync . --yes '$INSTANCE' rm -f foo.txt\"\n\n    assert_file_not_exists \"$TEST_SYNC_DIR/foo.txt\"\n}\n\n@test 'shell --sync reflects new directory and file creation from guest to host' {\n    cd \"$TEST_SYNC_DIR\"\n\n    # Create a script that creates a new directory with a file\n    cat > \"$TEST_SYNC_DIR/create_new.sh\" << 'EOF'\n#!/bin/sh\nset -eu\nmkdir -p new_directory\necho \"foo bar baz\" > new_directory/new_file.txt\nEOF\n    chmod +x \"$TEST_SYNC_DIR/create_new.sh\"\n\n    run -0 bash -c \"limactl shell --sync . --yes '$INSTANCE' ./create_new.sh && ./modify.sh\"\n\n    # Verify new directory was created on host\n    assert_dir_exists \"$TEST_SYNC_DIR/new_directory\"\n    assert_file_exists \"$TEST_SYNC_DIR/new_directory/new_file.txt\"\n\n    # Verify file content\n    run cat \"$TEST_SYNC_DIR/new_directory/new_file.txt\"\n    assert_output \"foo bar baz\"\n    run cat \"$TEST_SYNC_DIR/foo.txt\"\n    assert_output \"modified foo\"\n    run cat \"$TEST_SYNC_DIR/bar.txt\"\n    assert_output \"modified bar\"\n}\n\n@test 'shell --sync preserves file permissions' {\n    cd \"$TEST_SYNC_DIR\"\n\n    # Create a file with specific permissions\n    touch \"$TEST_SYNC_DIR/executable.sh\"\n    chmod 755 \"$TEST_SYNC_DIR/executable.sh\"\n\n    # Modify the file in guest\n    run -0 bash -c \"limactl shell --sync . --yes '$INSTANCE' ./modify.sh\"\n\n    # Verify file is still executable on host\n    if [[ \"$OSTYPE\" == darwin* ]]; then\n        run stat -f '%A' \"$TEST_SYNC_DIR/executable.sh\"\n    else\n        run stat -c '%a' \"$TEST_SYNC_DIR/executable.sh\"\n    fi\n    assert_output \"755\"\n\n    # Verify files were modified\n    run cat \"$TEST_SYNC_DIR/foo.txt\"\n    assert_output \"modified foo\"\n    run cat \"$TEST_SYNC_DIR/bar.txt\"\n    assert_output \"modified bar\"\n}\n\n@test 'shell --sync works without existing ControlMaster socket' {\n    cd \"$TEST_SYNC_DIR\"\n\n    # Remove the ControlMaster socket\n    local sock_path=\"$LIMA_HOME/$INSTANCE/ssh.sock\"\n    if [[ -S \"$sock_path\" ]]; then\n        rm \"$sock_path\"\n    fi\n\n    run -0 bash -c \"limactl shell --sync . --yes '$INSTANCE' ./modify.sh\"\n\n    # Verify files were modified\n    run cat \"$TEST_SYNC_DIR/foo.txt\"\n    assert_output \"modified foo\"\n    run cat \"$TEST_SYNC_DIR/bar.txt\"\n    assert_output \"modified bar\"\n}\n\n@test 'shell --sync should preserve top-level subtree after clean up' {\n    local foo_dir=\"$TEST_SYNC_DIR/foo\"\n    mkdir -p \"$foo_dir\"\n\n    cd \"$foo_dir\"\n    run -0 bash -c \"limactl shell --sync . --yes '$INSTANCE' sh -c 'pwd > pwd.txt'\"\n    assert_file_exists \"$foo_dir/pwd.txt\"\n\n    local guest_pwd\n    guest_pwd=$(cat \"$foo_dir/pwd.txt\")\n\n    run -0 bash -c \"limactl shell '$INSTANCE' test -d ${guest_pwd%/*}\"\n}\n"
  },
  {
    "path": "hack/bats/tests/shell.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\nINSTANCE=bats-dummy\n\n@test 'lima stopped lima instance' {\n    # check that the \"tty\" flag is used, also for stdin\n    run_e -1 limactl shell --tty=false \"$INSTANCE\" true </dev/null\n    assert_stderr --partial \"instance \\\\\\\"$INSTANCE\\\\\\\" is stopped\"\n}\n\n@test 'yes | stopped lima instance' {\n    # check that stdin is verified and not just crashing\n    run_e -1 bash -c \"yes | limactl shell --tty=true $INSTANCE true\"\n    assert_stderr --partial \"instance \\\\\\\"$INSTANCE\\\\\\\" is stopped\"\n}\n\n@test 'limactl shell without --instance requires instance name as first argument' {\n    run_e -1 limactl shell --tty=false\n    assert_stderr --partial \"requires instance name as first argument\"\n}\n\n@test 'limactl shell nonexistent instance' {\n    run_e -1 limactl shell --tty=false nonexistent\n    assert_stderr --partial 'does not exist'\n}\n\n@test 'limactl shell --instance nonexistent instance' {\n    run_e -1 limactl shell --tty=false --instance nonexistent\n    assert_stderr --partial 'does not exist'\n}\n\n@test 'limactl shell --instance resolves instance name' {\n    # --instance should resolve the instance name the same way as the positional arg\n    run_e -1 limactl shell --tty=false --instance nonexistent\n    inst_output=$stderr\n\n    run_e -1 limactl shell --tty=false nonexistent\n    assert_stderr \"$inst_output\"\n}\n\n@test 'limactl shell --instance with unknown flag errors' {\n    # Without --, an unknown flag after --instance should be rejected by cobra\n    run_e -1 limactl shell --tty=false --instance \"$INSTANCE\" --nonexistent-flag\n    assert_stderr --partial \"unknown flag\"\n}\n\n@test 'limactl shell --instance with double dash stops flag parsing' {\n    # With --, \"--nonexistent-flag\" must be treated as a command, not a flag.\n    # The key assertion is that we do NOT get \"unknown flag\" (contrast with the test above).\n    run_e limactl shell --tty=false --instance \"$INSTANCE\" -- --nonexistent-flag </dev/null\n    refute_stderr --partial \"unknown flag\"\n}\n\n@test 'limactl shell positional instance with double dash' {\n    # Same double-dash behavior with positional instance name\n    run_e limactl shell --tty=false \"$INSTANCE\" -- --nonexistent-flag </dev/null\n    refute_stderr --partial \"unknown flag\"\n}\n\n@test 'lima wrapper forwards unknown limactl shell flags' {\n    # The lima wrapper should forward flags to limactl shell.\n    # --start=false is a valid limactl shell flag; if the wrapper didn't forward it,\n    # it would be treated as a command name and fail with \"unknown flag\".\n    run_e env LIMA_INSTANCE=\"$INSTANCE\" lima --tty=false --start=false </dev/null\n    refute_stderr --partial \"unknown flag\"\n}\n\n@test 'lima wrapper rejects unknown flags' {\n    # Flags not recognized by limactl shell should cause an error\n    run_e -1 env LIMA_INSTANCE=\"$INSTANCE\" lima --tty=false --nonexistent-flag\n    assert_stderr --partial \"unknown flag\"\n}\n"
  },
  {
    "path": "hack/bats/tests/url-github.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\n# The jandubois/jandubois GitHub repo has been especially constructed to test\n# various features of the github URL scheme:\n#\n#   * repo defaults to org when not specified\n#   * filename defaults to .lima.yaml when only a path is specified\n#   * .yaml default extension\n#   * .lima.yaml files may be treated as symlinks\n#   * default branch lookup when not specified\n#   * github:ORG// repos can redirect to another github:ORG URL in the same ORG\n#\n# The jandubois/jandubois repo files are:\n#\n#   ├── .lima.yaml         -> templates/demo.yaml\n#   ├── back\n#   │   └── .lima.yaml     -> github:jandubois//loop/\n#   ├── docs\n#   │   └── .lima.yaml     -> ../templates/demo.yaml\n#   ├── example.yaml       -> templates/demo.yaml\n#   ├── invalid\n#   │   ├── org\n#   │   │   └── .lima.yaml -> github:lima-vm\n#   │   └── tag\n#   │       └── .lima.yaml -> github:jandubois//@v0.0.0\n#   ├── loop\n#   │   └── .lima.yaml     -> github:jandubois//back/\n#   ├── redirect\n#   │   └── .lima.yaml     -> github:jandubois/lima/templates/default\n#   ├── templates\n#   │   └── demo.yaml      \"base: template:default\"\n#   └── yaml\n#       └── .lima.yaml     \"{}\"\n#\n# Both the `main` branch and the `v0.0.0` tag have this layout.\n#\n# All these URLs should redirect to the same template URL (either on \"main\" or at \"v0.0.0\"):\n# \"https://raw.githubusercontent.com/jandubois/jandubois/${tag}/templates/demo.yaml\"\n#\n# Additional tests rely on jandubois/lima existing and containing the v1.2.1 tag.\n\nURLS=(\n\tgithub:jandubois/jandubois/templates/demo.yaml@main\n\tgithub:jandubois/jandubois/templates/demo.yaml\n\tgithub:jandubois/jandubois/templates/demo\n\tgithub:jandubois/jandubois/.lima.yaml\n\tgithub:jandubois/jandubois/@v0.0.0\n\tgithub:jandubois/jandubois\n\tgithub:jandubois//templates/demo.yaml@main\n\tgithub:jandubois//templates/demo.yaml\n\tgithub:jandubois//templates/demo\n\tgithub:jandubois//.lima.yaml\n\tgithub:jandubois//@v0.0.0\n\tgithub:jandubois//\n\tgithub:jandubois/\n\tgithub:jandubois@v0.0.0\n\tgithub:jandubois\n\tgithub:jandubois/jandubois/example.yaml\n\tgithub:jandubois/jandubois/example@main\n\tgithub:jandubois//example.yaml@v0.0.0\n\tgithub:jandubois//example\n\tgithub:jandubois/jandubois/docs/.lima.yaml@main\n\tgithub:jandubois/jandubois/docs/.lima.yaml\n\tgithub:jandubois/jandubois/docs/.lima\n\tgithub:jandubois/jandubois/docs/\n\tgithub:jandubois//docs/.lima.yaml@v0.0.0\n\tgithub:jandubois//docs/.lima.yaml\n\tgithub:jandubois//docs/.lima\n\tgithub:jandubois//docs/@v0.0.0\n\tgithub:jandubois//docs/\n)\n\nurl() {\n\trun_e \"$1\" limactl --debug template url \"$2\"\n}\n\ntest_jandubois_url() {\n\tlocal url=$1\n\tlocal tag=\"main\"\n\tif [[ $url == *v0.0.0* ]]; then\n\t\ttag=\"v0.0.0\"\n\tfi\n\n\turl -0 \"$url\"\n\tassert_output \"https://raw.githubusercontent.com/jandubois/jandubois/${tag}/templates/demo.yaml\"\n}\n\n# Dynamically register a @test for each URL in the list\nfor url in \"${URLS[@]}\"; do\n\tbats_test_function --description \"$url\" -- test_jandubois_url \"$url\"\ndone\n\n@test '.lima.yaml is retained when it exits and is not a symlink' {\n\turl -0 'github:jandubois//yaml/'\n\tassert_output 'https://raw.githubusercontent.com/jandubois/jandubois/main/yaml/.lima.yaml'\n}\n\n@test 'non-existing .lima.yaml returns an error' {\n\turl -1 'github:jandubois//missing/'\n\tassert_fatal 'file \"https://raw.githubusercontent.com/jandubois/jandubois/main/missing/.lima.yaml\" not found or inaccessible: status 404'\n}\n\n@test 'hidden files without an extension get a .yaml extension' {\n\turl -1 'github:jandubois//test/.hidden'\n\tassert_fatal 'file \"https://raw.githubusercontent.com/jandubois/jandubois/main/test/.hidden.yaml\" not found or inaccessible: status 404'\n}\n\n@test 'files that have an extension do not get a .yaml extension' {\n\t# This command doesn't fail because only *.yaml files are checked for redirects/symlinks, and therefore fail right away if they don't exist.\n\turl -0 'github:jandubois//test/.script.sh'\n\tassert_output 'https://raw.githubusercontent.com/jandubois/jandubois/main/test/.script.sh'\n}\n\n@test 'github: URLs are EXPERIMENTAL' {\n\turl -0 'github:jandubois'\n\tassert_warning \"The github: scheme is still EXPERIMENTAL\"\n}\n\n# Invalid URLs\n@test 'empty github: url returns an error' {\n\turl -1 'github:'\n\tassert_fatal 'github: URL must contain at least an ORG, got \"\"'\n}\n\n@test 'missing org returns an error' {\n\turl -1 'github:/jandubois'\n\tassert_fatal 'github: URL must contain at least an ORG, got \"\"'\n}\n\n# github: redirects in github:ORG// repos\n@test 'org redirects can point to different repo and may switch the branch name' {\n\turl -0 'github:jandubois//redirect/'\n\t# Note that the default branch in jandubois/jandubois is main, but in jandubois/lima it is master\n\tassert_debug 'Locator \"github:jandubois//redirect/\" replaced with \"github:jandubois/lima/templates/default\"'\n\tassert_debug 'Locator \"github:jandubois/lima/templates/default\" replaced with \"https://raw.githubusercontent.com/jandubois/lima/master/templates/default.yaml\"'\n\tassert_output 'https://raw.githubusercontent.com/jandubois/lima/master/templates/default.yaml'\n}\n\n@test 'org redirects propagate an explicit branch/tag to the other repo' {\n\turl -0 'github:jandubois//redirect/@v1.2.1'\n\tassert_debug 'Locator \"github:jandubois//redirect/@v1.2.1\" replaced with \"github:jandubois/lima/templates/default@v1.2.1\"'\n\tassert_debug 'Locator \"github:jandubois/lima/templates/default@v1.2.1\" replaced with \"https://raw.githubusercontent.com/jandubois/lima/v1.2.1/templates/default.yaml\"'\n\tassert_output 'https://raw.githubusercontent.com/jandubois/lima/v1.2.1/templates/default.yaml'\n}\n\n@test 'org redirects cannot point to another org' {\n\turl -1 'github:jandubois//invalid/org/'\n\tassert_fatal 'redirect \"github:lima-vm\" is not a \"github:jandubois\" URL…'\n}\n\n@test 'org redirects with branch cannot point to another org' {\n\turl -1 'github:jandubois//invalid/org/@main'\n\tassert_fatal 'redirect \"github:lima-vm\" is not a \"github:jandubois\" URL…'\n}\n\n@test 'org redirects cannot include a branch or tag' {\n\turl -1 'github:jandubois//invalid/tag/'\n\tassert_fatal 'redirect \"github:jandubois//@v0.0.0\" must not include a branch/tag/sha…'\n}\n\n@test 'org redirects with tag cannot include a branch or tag' {\n\turl -1 'github:jandubois//invalid/tag/@v0.0.0'\n\tassert_fatal 'redirect \"github:jandubois//@v0.0.0\" must not include a branch/tag/sha…'\n}\n\n@test 'org redirects must not create circular redirects' {\n\turl -1 'github:jandubois//loop/'\n\tassert_fatal 'custom locator \"github:jandubois//loop/\" has a redirect loop'\n}\n\n@test 'org redirects with branch must not create circular redirects' {\n\turl -1 'github:jandubois//back/@main'\n\tassert_fatal 'custom locator \"github:jandubois//back/@main\" has a redirect loop'\n}\n"
  },
  {
    "path": "hack/bats/tests/yq.bats",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nload \"../helpers/load\"\n\n@test 'make sure the yq subcommand exists' {\n    run -0 limactl yq --version\n    assert_output --regexp '^yq .*mikefarah.* version v'\n}\n\n@test 'yq can evaluate yq expressions' {\n    run -0 limactl yq -n .foo=42\n    assert_output 'foo: 42'\n}\n\n@test 'yq command understand yq options' {\n    run -0 limactl yq -n -o json -I 0 .foo=42\n    assert_output '{\"foo\":42}'\n}\n\n@test 'yq errors set non-zero exit code' {\n    run -1 limactl yq -n foo\n    assert_output --partial \"invalid input\"\n}\n\n@test 'yq works as a multi-call binary' {\n    # multi-call command detection strips all extensions\n    YQ=\"yq.lima.exe\"\n    ln -sf \"$(which limactl)\" \"${BATS_TEST_TMPDIR}/${YQ}\"\n    export PATH=\"$BATS_TEST_TMPDIR:$PATH\"\n\n    run -0 \"$YQ\" --version\n    assert_output --regexp '^yq .*mikefarah.* version v'\n\n    run -0 \"$YQ\" -n -o json -I 0 .foo=42\n    assert_output '{\"foo\":42}'\n}\n\n@test 'yq multi-call command has support for env access' {\n    export FOO=bar\n    run -0 limactl yq -n 'env(FOO)'\n    assert_output \"bar\"\n}\n\n@test 'yq multi-call command has support for --security-disable-env-ops' {\n    export FOO=bar\n    run_e -1 limactl yq -n --security-disable-env-ops 'env(FOO)'\n    assert_stderr \"Error: env operations have been disabled\"\n}\n"
  },
  {
    "path": "hack/brew-install-version.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This script only works for formulas in the homebrew-core.\n# It assumes the homebrew-core has been checked out into ./homebrew-core.\n# It only needs commit messages, so the checkout can be filtered with tree:0.\n\nset -eu -o pipefail\n\nFORMULA=$1\nVERSION=$2\n\nexport HOMEBREW_NO_AUTO_UPDATE=1\nexport HOMEBREW_NO_INSTALL_UPGRADE=1\nexport HOMEBREW_NO_INSTALL_CLEANUP=1\n\nTAP=lima/tap\nif ! brew tap | grep -q \"^${TAP}\\$\"; then\n\tbrew tap-new \"$TAP\"\nfi\n\n# Get the latest commit id for the commit that updated this bottle\nSHA=$(git -C homebrew-core log --max-count 1 --grep \"^${FORMULA}: update ${VERSION} bottle\" --format=\"%H\")\nif [[ -z $SHA ]]; then\n\techo \"${FORMULA} ${VERSION} not found\"\n\texit 1\nfi\n\nOUTPUT=\"$(brew --repo \"$TAP\")/Formula/${FORMULA}.rb\"\nRAW=\"https://raw.githubusercontent.com/Homebrew/homebrew-core\"\ncurl -s \"${RAW}/${SHA}/Formula/${FORMULA::1}/${FORMULA}.rb\" -o \"$OUTPUT\"\n\nif brew ls -1 | grep -q \"^${FORMULA}\\$\"; then\n\tbrew uninstall \"$FORMULA\" --ignore-dependencies\nfi\nbrew install \"${TAP}/${FORMULA}\"\n"
  },
  {
    "path": "hack/cache-common-inc.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# print the error message and exit with status 1\nfunction error_exit() {\n\techo \"Error: $*\" >&2\n\texit 1\n}\n\n# e.g.\n# ```console\n# $ download_template_if_needed templates/default.yaml\n# templates/default.yaml\n# $ download_template_if_needed https://raw.githubusercontent.com/lima-vm/lima/v0.15.1/examples/ubuntu-lts.yaml\n# /tmp/tmp.1J9Q6Q/template.yaml\n# ```\nfunction download_template_if_needed() {\n\tlocal template=\"$1\"\n\ttmp_yaml=\"$(mktemp -d)/template.yaml\"\n\t# The upgrade test doesn't have limactl installed first. The old version wouldn't support `limactl tmpl` anyways.\n\tif command -v limactl >/dev/null; then\n\t\tlimactl tmpl copy --embed-all \"${template}\" \"${tmp_yaml}\" || return\n\telse\n\t\tif [[ $template == https://* ]]; then\n\t\t\tcurl -sSLf \"${template}\" >\"${tmp_yaml}\" || return\n\t\telse\n\t\t\tcp \"${template}\" \"${tmp_yaml}\"\n\t\tfi\n\tfi\n\techo \"${tmp_yaml}\"\n}\n\n# e.g.\n# ```console\n# $ print_image_locations_for_arch_from_template templates/default.yaml\n# https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a\n# https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img null\n# ```\nfunction print_image_locations_for_arch_from_template() {\n\tlocal template arch\n\ttemplate=$(download_template_if_needed \"$1\") || return\n\tlocal -r template=${template}\n\tarch=$(detect_arch \"${template}\" \"${2:-}\") || return\n\tlocal -r arch=${arch}\n\n\t# extract digest, location and size by parsing template using arch\n\tlocal -r yq_filter=\"[.images | map(select(.arch == \\\"${arch}\\\")) | .[].location] | .[]\"\n\tyq eval \"${yq_filter}\" \"${template}\"\n}\n\n# e.g.\n# ```console\n# $ detect_arch templates/default.yaml\n# x86_64\n# $ detect_arch templates/default.yaml arm64\n# aarch64\n# ```\nfunction detect_arch() {\n\tlocal template arch\n\ttemplate=$(download_template_if_needed \"$1\") || return\n\tlocal -r template=${template}\n\n\tarch=\"${2:-$(yq '.arch // \"\"' \"${template}\")}\"\n\tarch=\"${arch:-$(uname -m)}\"\n\t# normalize arch. amd64 -> x86_64, arm64 -> aarch64\n\tcase \"${arch}\" in\n\tamd64 | x86_64) arch=x86_64 ;;\n\taarch64 | arm64) arch=aarch64 ;;\n\t*) ;;\n\tesac\n\techo \"${arch}\"\n}\n\n# e.g.\n# ```console\n# $ print_image_locations_for_arch_from_template templates/default.yaml|print_valid_image_index\n# 0\n# ```\nfunction print_valid_image_index() {\n\tlocal index=0\n\twhile read -r location; do\n\t\t[[ ${location} != \"null\" ]] || continue\n\t\thttp_code_and_size=$(check_location_with_cache \"${location}\")\n\t\tread -r http_code _size <<<\"${http_code_and_size}\"\n\t\tif [[ ${http_code} -eq 200 ]]; then\n\t\t\techo \"${index}\"\n\t\t\treturn\n\t\tfi\n\t\tindex=$((index + 1))\n\tdone\n\techo \"Failed to get the valid image location\" >&2\n\treturn 1\n}\n\n# e.g.\n# ```console\n# $ size_from_location \"https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img\"\n# 585498624\n# ```\nfunction size_from_location() {\n\t(\n\t\tset -o pipefail\n\t\tlocal location=$1\n\t\tcheck_location \"${location}\" | cut -d' ' -f2\n\t)\n}\n\n# Check the remote location and print the http code and size.\n# If GITHUB_ACTIONS is true, the result is not cached.\n# e.g.\n# ```console\n# $ check_location \"https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img\"\n# 200 585498624\n# ```\nfunction check_location() {\n\t# shellcheck disable=SC2154\n\tif [[ ${GITHUB_ACTIONS:-false} == true ]]; then\n\t\tcheck_location_without_cache \"$1\"\n\telse\n\t\tcheck_location_with_cache \"$1\"\n\tfi\n}\n\n# Check the remote location and print the http code and size.\n# The result is cached in .check_location-response-cache.yaml\n# e.g.\n# ```console\n# $ check_location_with_cache \"https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img\"\n# 200 585498624\n# ```\nfunction check_location_with_cache() {\n\tlocal -r location=\"$1\" cache_file=\".check_location-response-cache.yaml\"\n\t# check ${cache_file} for the cache\n\tif [[ -f ${cache_file} ]]; then\n\t\tcached=$(yq -e eval \".[\\\"${location}\\\"]\" \"${cache_file}\" 2>/dev/null) && echo \"${cached}\" && return\n\telse\n\t\ttouch \"${cache_file}\"\n\tfi\n\thttp_code_and_size=$(check_location_without_cache \"${location}\") || return\n\tyq eval \".[\\\"${location}\\\"] = \\\"${http_code_and_size}\\\"\" -i \"${cache_file}\" || return\n\techo \"${http_code_and_size}\"\n}\n\n# e.g.\n# ```console\n# $ check_location \"https://cloud-images.ubuntu.com/releases/24.04/release-20240725/ubuntu-24.04-server-cloudimg-amd64.img\"\n# 200 585498624\n# ```\nfunction check_location_without_cache() {\n\tlocal -r location=\"$1\"\n\tcurl -sIL -w \"%{http_code} %header{Content-Length}\" \"${location}\" -o /dev/null\n}\n\n# e.g.\n# ```console\n# $ print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index templates/default.yaml 0\n# https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img\n# sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a\n# null\n# null\n# null\n# null\n# ```\nfunction print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index() {\n\tlocal template index=\"${2:-}\" arch\n\ttemplate=$(download_template_if_needed \"$1\") || return\n\tlocal -r template=${template}\n\tarch=$(detect_arch \"${template}\" \"${3:-}\") || return\n\tlocal -r arch=${arch}\n\n\tlocal -r yq_filter=\"[(.images[] | select(.arch == \\\"${arch}\\\"))].[${index}]|[\n\t\t.location,\n\t\t.digest,\n\t\t.kernel.location,\n\t\t.kernel.digest,\n\t\t.initrd.location,\n\t\t.initrd.digest\n\t]\"\n\tyq -o=t eval \"${yq_filter}\" \"${template}\"\n}\n\n# e.g.\n# ```console\n# $ print_containerd_config_for_arch_from_template templates/default.yaml\n# true\n# https://github.com/containerd/nerdctl/releases/download/v1.7.6/nerdctl-full-1.7.6-linux-arm64.tar.gz\n# sha256:77c747f09853ee3d229d77e8de0dd3c85622537d82be57433dc1fca4493bab95\n# ```\nfunction print_containerd_config_for_arch_from_template() {\n\tlocal template arch\n\ttemplate=$(download_template_if_needed \"$1\") || return\n\tlocal -r template=${template}\n\tarch=$(detect_arch \"${template}\" \"${2:-}\") || return\n\tlocal -r arch=${arch}\n\n\tlocal -r yq_filter=\"\n\t\t[.containerd|[.system or .user],\n\t\t.containerd.archives | map(select(.arch == \\\"${arch}\\\")) | [.[0].location, .[0].digest]]|flatten\n\t\"\n\tvalidated_template=\"$(\n\t\tlimactl validate \"${template}\" --fill 2>/dev/null || echo \"{.containerd: {system: false, user: false, archives: []}}\"\n\t)\"\n\tyq -o=t eval \"${yq_filter}\" <<<\"${validated_template}\"\n}\n\n# e.g.\n# ```console\n# $ location_to_sha256 \"https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img\"\n# ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8\n# ```\nfunction location_to_sha256() {\n\t(\n\t\tset -o pipefail\n\t\tlocal -r location=\"$1\"\n\t\tif command -v sha256sum >/dev/null; then\n\t\t\tsha256=\"$(echo -n \"${location}\" | sha256sum | cut -d' ' -f1)\"\n\t\telif command -v shasum >/dev/null; then\n\t\t\tsha256=\"$(echo -n \"${location}\" | shasum -a 256 | cut -d' ' -f1)\"\n\t\telse\n\t\t\terror_exit \"sha256sum or shasum not found\"\n\t\tfi\n\t\techo \"${sha256}\"\n\t)\n}\n\n# e.g.\n# ```console\n# $ cache_download_dir\n# .download # on GitHub Actions\n# /home/user/.cache/lima/download # on Linux\n# /Users/user/Library/Caches/lima/download # on macOS\n# /home/user/.cache/lima/download # on others\n# ```\nfunction cache_download_dir() {\n\tif [[ ${GITHUB_ACTIONS:-false} == true ]]; then\n\t\techo \".download\"\n\telse\n\t\tcase \"$(uname -s)\" in\n\t\tLinux) echo \"${XDG_CACHE_HOME:-${HOME}/.cache}/lima/download\" ;;\n\t\tDarwin) echo \"${HOME}/Library/Caches/lima/download\" ;;\n\t\t*) echo \"${HOME}/.cache/lima/download\" ;;\n\t\tesac\n\tfi\n}\n\n# e.g.\n# ```console\n# $ location_to_cache_path \"https://cloud-images.ubuntu.com/releases/24.04/release-20240809/ubuntu-24.04-server-cloudimg-arm64.img\"\n# .download/by-url-sha256/ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8\n# ```\nfunction location_to_cache_path() {\n\tlocal location=$1\n\t[[ ${location} != \"null\" ]] || return\n\tsha256=$(location_to_sha256 \"${location}\") && download_dir=$(cache_download_dir) && echo \"${download_dir}/by-url-sha256/${sha256}\"\n}\n\n# e.g.\n# ```console\n# $ 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\"\n# image:ubuntu-24.04-server-cloudimg-arm64.img-sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a\n# $ 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\"\n# image:ubuntu-24.04-server-cloudimg-arm64.img-url-sha256:ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8\n# ```\nfunction cache_key_from_prefix_location_and_digest() {\n\tlocal prefix=$1 location=$2 digest=$3 location_basename\n\t[[ ${location} != \"null\" ]] || return\n\tlocation_basename=$(basename \"${location}\")\n\tif [[ ${digest} != \"null\" ]]; then\n\t\techo \"${prefix}:${location_basename}-${digest}\"\n\telse\n\t\t# use sha256 of location as key if digest is not available\n\t\techo \"${prefix}:${location_basename}-url-sha256:$(location_to_sha256 \"${location}\")\"\n\tfi\n}\n\n# e.g.\n# ```console\n# $ 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\"\n# image-path=.download/by-url-sha256/ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8\n# image-key=image:ubuntu-24.04-server-cloudimg-arm64.img-sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a\n# ```\nfunction print_path_and_key_for_cache() {\n\tlocal -r prefix=$1 location=$2 digest=$3\n\tcache_path=$(location_to_cache_path \"${location}\" || true)\n\tcache_key=$(cache_key_from_prefix_location_and_digest \"${prefix}\" \"${location}\" \"${digest}\" || true)\n\techo \"${prefix}-path=${cache_path}\"\n\techo \"${prefix}-key=${cache_key}\"\n}\n\n# e.g.\n# ```console\n# $ print_cache_informations_from_template templates/default.yaml\n# image-path=.download/by-url-sha256/ae988d797c6de06b9c8a81a2b814904151135ccfd4616c22948057f6280477e8\n# image-key=image:ubuntu-24.04-server-cloudimg-arm64.img-sha256:2e0c90562af1970ffff220a5073a7830f4acc2aad55b31593003e8c363381e7a\n# kernel-path=\n# kernel-key=\n# initrd-path=\n# initrd-key=\n# containerd-path=.download/by-url-sha256/21cc8dfa548ea8a678135bd6984c9feb9f8a01901d10b11bb491f6f4e7537158\n# containerd-key=containerd:nerdctl-full-1.7.6-linux-arm64.tar.gz-sha256:77c747f09853ee3d229d77e8de0dd3c85622537d82be57433dc1fca4493bab95\n# $ print_cache_informations_from_template templates/experimental/riscv64.yaml\n# image-path=.download/by-url-sha256/760b6ec69c801177bdaea06d7ee25bcd6ab72a331b9d3bf38376578164eb8f01\n# image-key=image:ubuntu-24.04-server-cloudimg-riscv64.img-sha256:361d72c5ed9781b097ab2dfb1a489c64e51936be648bbc5badee762ebdc50c31\n# kernel-path=.download/by-url-sha256/4568026693dc0f31a551b6741839979c607ee6bb0bf7209c89f3348321c52c61\n# kernel-key=kernel:qemu-riscv64_smode_uboot.elf-sha256:d4b3a10c3ef04219641802a586dca905e768805f5a5164fb68520887df54f33c\n# initrd-path=\n# initrd-key=\n# ```\nfunction print_cache_informations_from_template() {\n\t(\n\t\tset -o pipefail\n\t\tlocal template index image_kernel_initrd_info location digest containerd_info\n\t\ttemplate=$(download_template_if_needed \"$1\") || return\n\t\tlocal -r template=\"${template}\"\n\t\tindex=$(print_image_locations_for_arch_from_template \"${template}\" \"${@:2}\" | print_valid_image_index) || return\n\t\tlocal -r index=\"${index}\"\n\t\timage_kernel_initrd_info=$(print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index \"${template}\" \"${index}\" \"${@:2}\") || return\n\t\t# shellcheck disable=SC2034\n\t\tread -r image_location image_digest kernel_location kernel_digest initrd_location initrd_digest <<<\"${image_kernel_initrd_info}\"\n\t\tfor prefix in image kernel initrd; do\n\t\t\tlocation=${prefix}_location\n\t\t\tdigest=${prefix}_digest\n\t\t\tprint_path_and_key_for_cache \"${prefix}\" \"${!location}\" \"${!digest}\"\n\t\tdone\n\t\tif command -v limactl >/dev/null; then\n\t\t\tcontainerd_info=$(print_containerd_config_for_arch_from_template \"${template}\" \"${@:2}\") || return\n\t\t\tread -r containerd_enabled containerd_location containerd_digest <<<\"${containerd_info}\"\n\t\t\tif [[ ${containerd_enabled} == \"true\" ]]; then\n\t\t\t\tprint_path_and_key_for_cache \"containerd\" \"${containerd_location}\" \"${containerd_digest}\"\n\t\t\tfi\n\t\tfi\n\t)\n}\n\n# Compatible with hashFile() in GitHub Actions\n# e.g.\n# ```console\n# $ hash_file templates/default.yaml\n# ceec5ba3dc8872c083b2eb7f44e3e3f295d5dcdeccf0961ee153be6586525e5e\n# ```\nfunction hash_file() {\n\t(\n\t\tset -o pipefail\n\t\tlocal hash=\"\"\n\t\tfor file in \"$@\"; do\n\t\t\thash=\"${hash}$(sha256sum \"${file}\" | cut -d' ' -f1)\" || return\n\t\tdone\n\t\techo \"${hash}\" | xxd -r -p | sha256sum | cut -d' ' -f1\n\t)\n}\n\n# Download the file to the cache directory and print the path.\n# e.g.\n# ```console\n# $ download_to_cache \"https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-arm64.img\"\n# .download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on GitHub Actions\n# /home/user/.cache/lima/download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on Linux\n# /Users/user/Library/Caches/lima/download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on macOS\n# /home/user/.cache/lima/download/by-url-sha256/346ee1ff9e381b78ba08e2a29445960b5cd31c51f896fc346b82e26e345a5b9a/data # on others\nfunction download_to_cache() {\n\tlocal cache_path use_redirected_location=${2:-YES}\n\tcache_path=$(location_to_cache_path \"$1\")\n\t# before checking remote location, check if the data file is already downloaded and the time file is updated within 10 minutes\n\tif [[ -f ${cache_path}/data && -n \"$(find \"${cache_path}/time\" -mmin -10 || true)\" ]]; then\n\t\techo \"${cache_path}/data\"\n\t\treturn\n\tfi\n\n\t# check the remote location\n\tlocal curl_info_json write_out\n\twrite_out='{\n\t\t\"json\":%{json},\n\t\t\"header\":%{header_json}\n\t}'\n\tcurl_info_json=$(curl -sSLI -w \"${write_out}\" \"$1\" -o /dev/null)\n\n\tlocal code time type url\n\tcode=$(jq -r '.json.http_code' <<<\"${curl_info_json}\")\n\ttime=$(jq -r '(.header[\"last-modified\"]|first) // (.header[\"date\"]|first) // empty' <<<\"${curl_info_json}\")\n\ttype=$(jq -r '.json.content_type' <<<\"${curl_info_json}\")\n\tif [[ ${use_redirected_location} == \"YES\" ]]; then\n\t\turl=$(jq -r '.json.url_effective' <<<\"${curl_info_json}\")\n\telse\n\t\turl=$1\n\tfi\n\t[[ ${code} == 200 ]] || error_exit \"Failed to download $1\"\n\n\tcache_path=$(location_to_cache_path \"${url}\")\n\t[[ -d ${cache_path} ]] || mkdir -p \"${cache_path}\"\n\n\tlocal needs_download=0\n\t[[ -f ${cache_path}/data ]] || needs_download=1\n\t[[ -f ${cache_path}/time && \"$(<\"${cache_path}/time\")\" == \"${time}\" ]] || needs_download=1\n\t[[ -f ${cache_path}/type && \"$(<\"${cache_path}/type\")\" == \"${type}\" ]] || needs_download=1\n\tif [[ ${needs_download} -eq 1 ]]; then\n\t\tcurl_info_json=$(\n\t\t\techo \"downloading ${url}\" >&2\n\t\t\tcurl -SL -w \"${write_out}\" --no-clobber -o \"${cache_path}/data\" \"${url}\"\n\t\t)\n\t\tlocal filename\n\t\tcode=$(jq -r '.json.http_code' <<<\"${curl_info_json}\")\n\t\ttime=$(jq -r '(.header[\"last-modified\"]|first) // (.header[\"date\"]|first) // empty' <<<\"${curl_info_json}\")\n\t\ttype=$(jq -r '.json.content_type' <<<\"${curl_info_json}\")\n\t\turl=$(jq -r '.json.url_effective' <<<\"${curl_info_json}\")\n\t\tfilename=$(jq -r '.json.filename_effective' <<<\"${curl_info_json}\")\n\t\t[[ ${code} == 200 ]] || error_exit \"Failed to download ${url}\"\n\t\t[[ \"${cache_path}/data\" == \"${filename}\" ]] || mv \"${filename}\" \"${cache_path}/data\"\n\t\t# sha256.digest seems existing if expected digest is available. so, not creating it here.\n\t\t# sha256sum \"${cache_path}/data\" | awk '{print \"sha256:\"$1}' >\"${cache_path}/sha256.digest\"\n\t\techo -n \"${time}\" >\"${cache_path}/time\"\n\telse\n\t\ttouch \"${cache_path}/time\"\n\tfi\n\t[[ -f ${cache_path}/type ]] || echo -n \"${type}\" >\"${cache_path}/type\"\n\t[[ -f ${cache_path}/url ]] || echo -n \"${url}\" >\"${cache_path}/url\"\n\techo \"${cache_path}/data\"\n}\n\n# Download the file to the cache directory without redirect and print the path.\nfunction download_to_cache_without_redirect() {\n\tdownload_to_cache \"$1\" \"NO\"\n}\n"
  },
  {
    "path": "hack/calculate-cache.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This script calculates the expected content size, actual cached size, and cache-keys used in caching method before and after\n# implementation in https://github.com/lima-vm/lima/pull/2508\n#\n# Answer to the question in https://github.com/lima-vm/lima/pull/2508#discussion_r1699798651\n\nscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n# shellcheck source=./common.inc.sh\n. \"${scriptdir}/cache-common-inc.sh\"\n\n# usage: [DEBUG=1] ./hack/calculate-cache.sh\n# DEBUG=1 will save the collected information in .calculate-cache-collected-info-{before,after}.yaml\n#\n# This script does:\n# 1. extracts `runs_on` and `template` from workflow file (.github/workflows/test.yml)\n# 2. check each template for image, kernel, initrd, and nerdctl\n# 3. detect size of image, kernel, initrd, and nerdctl (responses from remote are cached for faster iteration)\n#    save the response in .check_location-response-cache.yaml\n# 4. print content size, actual cache size (if available), by cache key\n#\n# The major differences for reducing cache usage are as follows:\n# - it is now cached `~/.cache/lima/download/by-url-sha256/$sha256` instead of caching `~/.cache/lima/download`\n# - the cache keys are now based on the image, kernel, initrd, and nerdctl digest instead of the template file's hash\n# - enables the use of cache regardless of the operating system used to execute CI.\n#\n# The script requires the following commands:\n# - gh: GitHub CLI.\n#   Using to get the cache information\n# - jq: Command-line JSON processor\n#   Parse the workflow file and print runs-on and template.\n#   Parse output from gh cache list\n#   Calculate the expected content size, actual cached size, and cache-keys used.\n# - limactl: lima CLI.\n#   Using to validate the template file for getting nerdctl location and digest.\n# - sha256sum: Print or check SHA256 (256-bit) checksums\n# - xxd: make a hexdump or do the reverse.\n#   Using to simulate the 'hashFile()' function in the workflow.\n# - yq: Command-line YAML processor.\n#   Parse the template file for image and nerdctl location, digest, and size.\n#   Parse the cache response file for the cache.\n#   Convert the collected information to JSON.\n\nset -u -o pipefail\n\nrequired_commands=(gh jq limactl sha256sum xxd yq)\nfor cmd in \"${required_commands[@]}\"; do\n\tif ! command -v \"${cmd}\" &>/dev/null; then\n\t\techo \"${cmd} is required. Please install it\" >&2\n\t\texit 1\n\tfi\ndone\n\n# current workflow uses x86_64 only\narch=x86_64\n\nLIMA_HOME=$(mktemp -d)\nexport LIMA_HOME\n\n# parse the workflow file and print runs-on and template\n# e.g.\n# ```console\n# $ print_runs_on_template_from_workflow .github/workflows/test.yml\n# macos-12        templates/default.yaml\n# ubuntu-24.04    templates/alpine.yaml\n# ubuntu-24.04    templates/debian.yaml\n# ubuntu-24.04    templates/fedora.yaml\n# ubuntu-24.04    templates/archlinux.yaml\n# ubuntu-24.04    templates/opensuse.yaml\n# ubuntu-24.04    templates/experimental/net-user-v2.yaml\n# ubuntu-24.04    templates/experimental/9p.yaml\n# ubuntu-24.04    templates/docker.yaml\n# ubuntu-24.04    templates/../hack/test-templates/alpine-iso-9p-writable.yaml\n# ubuntu-24.04    templates/../hack/test-templates/test-misc.yaml\n# macos-12        templates/vmnet.yaml\n# macos-12        https://raw.githubusercontent.com/lima-vm/lima/v0.15.1/examples/ubuntu-lts.yaml\n# macos-13        templates/experimental/vz.yaml\n# macos-13        templates/fedora.yaml\n# ```\nfunction print_runs_on_template_from_workflow() {\n\tyq -o=j \"$1\" | jq -r '\n\t\t\"./.github/actions/setup_cache_for_template\" as $action |\n\t\t\"\\\\$\\\\{\\\\{\\\\s*(?<path>\\\\S*)\\\\s*\\\\}\\\\}\" as $pattern |\n\t\t.jobs | map_values(select(.steps)|\n\t\t\t.\"runs-on\" as $runs_on |\n\t\t\t{\n\t\t\t\ttemplate: .steps | map_values(select(.uses == $action)) | first |.with.template,\n\t\t\t\tmatrix: .strategy.matrix\n\t\t\t} | select(.template) |\n\t\t\t. + { path: .template | (if test($pattern) then sub(\".*\\($pattern).*\";\"\\(.path)\")|split(\".\") else null end) } |\n\t\t\t(\n\t\t\t\t.template as $template|\n\t\t\t\tif .path then\n\t\t\t\t\tgetpath(.path)|map(. as $item|$template|sub($pattern;$item))\n\t\t\t\telse\n\t\t\t\t\t[$template]\n\t\t\t\tend\n\t\t\t) | map(\"\\($runs_on)\\t\\(.)\")\n\n\t\t) | flatten |.[]\n\t'\n}\n\n# returns the OS name from the runner equivalent to the expression `${{ runner.os }}` in the workflow\n# e.g.\n# ```console\n# $ runner_os_from_runner \"macos-12\"\n# macOS\n# $ runner_os_from_runner \"ubuntu-24.04\"\n# Linux\n# ```\nfunction runner_os_from_runner() {\n\t# shellcheck disable=SC2249\n\tcase \"$1\" in\n\tmacos*)\n\t\techo macOS\n\t\t;;\n\tubuntu*)\n\t\techo Linux\n\t\t;;\n\tesac\n}\n\n# format first column to MiB\n# e.g.\n# ```console\n# $ echo 585498624 | size_to_mib\n#   558.38 MiB\n# ```\nfunction size_to_mib() {\n\tawk '\n\t\tfunction mib(size) { return sprintf(\"%7.2f MiB\", size / 1024 / 1024) }\n\t\tint($1)>0{ $1=\" \"mib($1) }\n\t\tint($2)>0{ $2=mib($2) }\n\t\tint($2)==0 && NF>1{ $2=\"<<missing>>\" }\n\t\t{ print }\n\t'\n}\n\n# actual_cache_sizes=$(gh cache list --json key,createdAt,sizeInBytes|jq '[.[]|{\"key\":.key,\"value\":.sizeInBytes}]|from_entries')\n# e.g.\n# ```console\n# $ echo \"${actual_cache_sizes}\"\n# {\n#   \"Linux-1c3b2791d52735d916dc44767c745c2319eb7cae74af71bbf45ddb268f42fc1d\": 810758533,\n#   \"Linux-231c66957fc2cdb18ea10e63f60770049026e29051ecd6598fc390b60d6a4fa6\": 633036717,\n#   \"Linux-3b906d46fa532e3bc348c35fc8e7ede6c69f0b27032046ee2cbb56d4022d1146\": 574242367,\n#   \"Linux-69a547b760dbf1650007ed541408474237bc611704077214adcac292de556444\": 70310855,\n#   \"Linux-7782f8b4ff8cd378377eb79f8d61c9559b94bbd0c11d19eb380ee7bda19af04e\": 494141177,\n#   \"Linux-8812aedfe81b4456d421645928b493b1f2f88aff04b7f3171207492fd44cd189\": 812730766,\n#   \"Linux-caa7d8af214d55ad8902e82d5918e61573f3d6795d2b5ad9a35305e26fa0e6a9\": 754723892,\n#   \"Linux-colima-v0.6.5\": 226350335,\n#   \"Linux-de83bce0608d787e3c68c7a31c5fab2b6d054320fd7bf633a031845e2ee03414\": 810691197,\n#   \"Linux-eb88a19dfcf2fb98278e7c7e941c143737c6d7cd8950a88f58e04b4ee7cef1bc\": 570625794,\n#   \"Linux-f88f0b3b678ff6432386a42bdd27661133c84a36ad29f393da407c871b0143eb\": 68490954,\n#   \"golangci-lint.cache-Linux-2850-74615231540133417fd618c72e37be92c5d3b3ad\": 2434144,\n#   \"macOS-231c66957fc2cdb18ea10e63f60770049026e29051ecd6598fc390b60d6a4fa6\": 633020464,\n#   \"macOS-49aa50a4872ded07ebf657c0eaf9e44ecc0c174d033a97c537ecd270f35b462f\": 813179462,\n#   \"macOS-8f37f663956af5f743f0f99ab973729b6a02f200ebfac7a3a036eff296550732\": 810756770,\n#   \"macOS-ef5509b5d4495c8c3590442ee912ad1c9a33f872dc4a29421c524fc1e2103b59\": 813179476,\n#   \"macOS-upgrade-v0.15.1\": 1157814690,\n#   \"setup-go-Linux-ubuntu20-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52\": 1015518352,\n#   \"setup-go-Linux-ubuntu20-go-1.23.0-6bce2eefc6111ace836de8bb322432c072805737d5f3c5a3d47d2207a05f50df\": 936433302,\n#   \"setup-go-Linux-ubuntu24-go-1.22.6-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52\": 1090001859,\n#   \"setup-go-Linux-ubuntu24-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52\": 526146768,\n#   \"setup-go-Windows-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52\": 1155374040,\n#   \"setup-go-Windows-go-1.23.0-6bce2eefc6111ace836de8bb322432c072805737d5f3c5a3d47d2207a05f50df\": 1056433137,\n#   \"setup-go-macOS-go-1.23.0-02756877dbcc9669bb904e42e894c63aa9801138db94426a90a2d554f2705c52\": 1060919942,\n#   \"setup-go-macOS-go-1.23.0-6bce2eefc6111ace836de8bb322432c072805737d5f3c5a3d47d2207a05f50df\": 982139209\n# }\nactual_cache_sizes=$(\n\tgh cache list --json key,createdAt,sizeInBytes \\\n\t\t--jq 'sort_by(.createdAt)|reverse|unique_by(.key)|sort_by(.key)|map({\"key\":.key,\"value\":.sizeInBytes})|from_entries'\n)\n\nworkflows=(\n\t.github/workflows/test.yml\n)\n\n# shellcheck disable=SC2016\necho \"=> 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\"\n# iterate over before and after\nfor cache_method in before after; do\n\techo \"==> ${cache_method}\"\n\techo \"content-size actual-size cache-key\"\n\toutput_yaml=$(\n\t\tfor workflow in \"${workflows[@]}\"; do\n\t\t\tprint_runs_on_template_from_workflow \"${workflow}\"\n\t\tdone | while IFS=$'\\t' read -r runner template; do\n\t\t\ttemplate=$(download_template_if_needed \"${template}\") || continue\n\t\t\tarch=$(detect_arch \"${template}\" \"${arch}\") || continue\n\t\t\tindex=$(print_image_locations_for_arch_from_template \"${template}\" \"${arch}\" | print_valid_image_index) || continue\n\t\t\timage_kernel_initrd_info=$(print_image_kernel_initrd_locations_with_digest_for_arch_from_template_at_index \"${template}\" \"${index}\" \"${arch}\") || continue\n\t\t\t# shellcheck disable=SC2034 # shellcheck does not detect dynamic variables usage\n\t\t\tread -r image_location image_digest kernel_location kernel_digest initrd_location initrd_digest <<<\"${image_kernel_initrd_info}\"\n\t\t\tcontainerd_info=$(print_containerd_config_for_arch_from_template \"${template}\" \"${@:2}\") || continue\n\t\t\t# shellcheck disable=SC2034 # shellcheck does not detect dynamic variables usage\n\t\t\tread -r _containerd_enabled containerd_location containerd_digest <<<\"${containerd_info}\"\n\n\t\t\tif [[ ${cache_method} != after ]]; then\n\t\t\t\tkey=$(runner_os_from_runner \"${runner}\" || true)-$(hash_file \"${template}\")\n\t\t\telse\n\t\t\t\tkey=$(cache_key_from_prefix_location_and_digest image \"${image_location}\" \"${image_digest}\")\n\t\t\tfi\n\t\t\tsize=$(size_from_location \"${image_location}\")\n\t\t\tfor prefix in containerd kernel initrd; do\n\t\t\t\tlocation=\"${prefix}_location\"\n\t\t\t\tdigest=\"${prefix}_digest\"\n\t\t\t\t[[ ${!location} != null ]] || continue\n\t\t\t\tif [[ ${cache_method} != after ]]; then\n\t\t\t\t\t# previous caching method packages all files in download to a single cache key\n\t\t\t\t\tsize=$((size + $(size_from_location \"${!location}\")))\n\t\t\t\telse\n\t\t\t\t\t# new caching method caches each file separately\n\t\t\t\t\tkey_for_prefix=$(cache_key_from_prefix_location_and_digest \"${prefix}\" \"${!location}\" \"${!digest}\")\n\t\t\t\t\tsize_for_prefix=$(size_from_location \"${!location}\")\n\t\t\t\t\tprintf -- \"- key: %s\\n  template: %s\\n  location: %s\\n  digest: %s\\n  size: %s\\n\" \\\n\t\t\t\t\t\t\"${key_for_prefix}\" \"${template}\" \"${!location}\" \"${!digest}\" \"${size_for_prefix}\"\n\t\t\t\tfi\n\t\t\tdone\n\t\t\tprintf -- \"- key: %s\\n  template: %s\\n  location: %s\\n  digest: %s\\n  size: %s\\n\" \\\n\t\t\t\t\"${key}\" \"${template}\" \"${image_location}\" \"${image_digest}\" \"${size}\"\n\t\tdone\n\t)\n\toutput_json=$(yq -o=j . <<<\"${output_yaml}\")\n\n\t# print size key\n\tjq --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\n\t# total\n\techo \"------------\"\n\tjq '[unique_by(.key)|.[]|.size]|add' <<<\"${output_json}\" | size_to_mib\n\t# save the collected information as yaml if DEBUG is set\n\tif [[ -n ${DEBUG:+1} ]]; then\n\t\tcat <<<\"${output_yaml}\" >\".calculate-cache-collected-info-${cache_method}.yaml\"\n\t\techo \"Saved the collected information in .calculate-cache-collected-info-${cache_method}.yaml\"\n\tfi\n\techo \"\"\ndone\n"
  },
  {
    "path": "hack/codesign/debugserver",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# # `hack/codesign/debugserver`\n#\n# This script wraps the LLDB `debugserver` to `codesign` the target executable.\n# This is needed for [Delve](https://github.com/go-delve/delve) to work properly\n# on macOS with the virtualization framework.\n#\n# ## How to use this script with Delve\n#\n# ### Use `DELVE_DEBUGSERVER_PATH` environment variable\n#\n# Override `debugserver` by setting the `DELVE_DEBUGSERVER_PATH` environment variable.\n# Ref: [Environment variables - Using Delve](https://github.com/go-delve/delve/blob/master/Documentation/usage/README.md#environment-variables)\n#\n# e.g. in `.vscode/launch.json`:\n# ```jsonc\n# {\n#     \"version\": \"0.2.0\",\n#     \"configurations\": [\n#         {\n#             \"name\": \"hostagent for input instance\",\n#             \"type\": \"go\",\n#             \"request\": \"launch\",\n#             \"mode\": \"debug\",\n#             // Use integratedTerminal to stop using ctrl+C\n#             \"console\": \"integratedTerminal\",\n#             \"program\": \"${workspaceFolder}/cmd/limactl\",\n#             \"buildFlags\": [\n#                 \"-ldflags=-X github.com/lima-vm/lima/pkg/version.Version=2.0.0-alpha.0\",\n#             ],\n#             \"env\": {\n#                 \"CGO_ENABLED\": \"1\",\n#                 \"DELVE_DEBUGSERVER_PATH\": \"${workspaceFolder}/hack/codesign/debugserver\",\n#                 \"LIMA_SSH_PORT_FORWARDER\": \"true\",\n#             },\n#             \"cwd\": \"${userHome}/.lima/${input:targetInstance}\",\n#             \"args\": [\n#                 \"--debug\",\n#                 \"hostagent\",\n#                 \"--pidfile\", \"ha.pid\",\n#                 \"--socket\", \"ha.sock\",\n#                 \"--guestagent\", \"${workspaceFolder}/_output/share/lima/lima-guestagent.Linux-aarch64\",\n#                 \"${input:targetInstance}\"\n#             ],\n# \t\t},\n#     ],\n#     \"inputs\": [\n#         {\n#             \"id\": \"targetInstance\",\n#             \"type\": \"promptString\",\n#             \"description\": \"Input target instance parameter for `limactl` command\",\n#         }\n#     ]\n# }\n# ```\n#\n# ### Override `debugserver` in the PATH\n#\n# You can also override the `debugserver` in the PATH environment variable.\n#\n# e.g. in `.vscode/launch.json`:\n# ```diff\n#              \"env\": {\n#                  \"CGO_ENABLED\": \"1\",\n# -                \"DELVE_DEBUGSERVER_PATH\": \"${workspaceFolder}/hack/codesign/debugserver\",\n# +                \"PATH\": \"${workspaceFolder}/hack/codesign:${env:PATH}\",\n#              },\n# ```\n\nset -eu -o pipefail\n\nscript_dir=\"$(dirname \"$0\")\"\n\n# Codesign the target executable if vz.entitlements exists\nentitlements=\"${script_dir}/../../vz.entitlements\"\nif [ -f \"${entitlements}\" ]; then\n\tprev_arg=\n\tfor arg in \"$@\"; do\n\t\t# find the target executable in the args next to \"--\"\n\t\tif [ \"${prev_arg}\" = \"--\" ]; then\n\t\t\tcodesign --entitlements \"${entitlements}\" --force -s - -v \"${arg}\" || {\n\t\t\t\techo \"error: codesign failed\" >&2\n\t\t\t\texit 1\n\t\t\t}\n\t\t\tbreak\n\t\tfi\n\t\tprev_arg=\"${arg}\"\n\tdone\nfi\n\n# Simulate how Delve locates debugserver\n# https://github.com/go-delve/delve/blob/65a6830eb7f0b882e0c52df0ef9585945ed55470/pkg/proc/gdbserial/gdbserver.go#L103-L129\n# 1. PATH (in this script, excluding the directory of this script to avoid recursion)\ncandidate_paths=\"$(echo \"${PATH}\" | tr ':' '\\n' | grep --fixed-strings --invert-match --line-regexp \"${script_dir}\" | paste -sd: -)\"\n# 2. CommandLineTools LLDB path\nCLT_path=\"/Library/Developer/CommandLineTools\"\ncandidate_paths=\"${candidate_paths}:${CLT_path}/Library/PrivateFrameworks/LLDB.framework/Versions/A/Resources/\"\n# 3. Xcode LLDB path (if Xcode is installed)\nif developer_dir=$(xcode-select --print-path 2>/dev/null) && [ \"${developer_dir}\" != \"${CLT_path}\" ]; then\n\t# Xcode is installed\n\tcandidate_paths=\"${candidate_paths}:${developer_dir}/../SharedFrameworks/LLDB.framework/Versions/A/Resources/\"\nfi\n# Find debugserver in the candidate paths\ndebugserver=$(PATH=\"${candidate_paths}\" command -v debugserver) || {\n\techo \"error: debugserver not found\" >&2\n\texit 1\n}\n# Execute debugserver with the original arguments\nexec \"${debugserver}\" \"$@\"\n"
  },
  {
    "path": "hack/common.inc.sh",
    "content": "# shellcheck shell=bash\ncleanup_cmd=\"\"\ntrap 'eval ${cleanup_cmd}' EXIT\nfunction defer {\n\t[ -n \"${cleanup_cmd}\" ] && cleanup_cmd=\"${cleanup_cmd}; \"\n\tcleanup_cmd=\"${cleanup_cmd}$1\"\n}\n\nfunction INFO() {\n\techo \"TEST| [INFO] $*\"\n}\n\nfunction WARNING() {\n\techo >&2 \"TEST| [WARNING] $*\"\n}\n\nfunction ERROR() {\n\techo >&2 \"TEST| [ERROR] $*\"\n}\n\nif [[ ${BASH_VERSINFO:-0} -lt 4 ]]; then\n\tERROR \"Bash version is too old: ${BASH_VERSION}\"\n\texit 1\nfi\n\n: \"${LIMA_HOME:=${HOME_HOST:-$HOME}/.lima}\"\n_IPERF3=iperf3\n# iperf3-darwin does some magic on macOS to avoid \"No route on host\" on macOS 15\n# https://github.com/lima-vm/socket_vmnet/issues/85\n[ \"$(uname -s)\" = \"Darwin\" ] && _IPERF3=\"iperf3-darwin\"\n: \"${IPERF3:=$_IPERF3}\"\n\n# Setup LIMA_TEMPLATES_PATH because the templates are not installed, but reference base templates\n# via template:_images/* and template:_default/*.\ntemplates_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../templates\" && pwd)\"\n: \"${LIMA_TEMPLATES_PATH:-$templates_dir}\"\nexport LIMA_TEMPLATES_PATH\n"
  },
  {
    "path": "hack/debug-cache.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\ncache_dir=\"${HOME}/Library/Caches\"\nif [ \"$(uname -s)\" != \"Darwin\" ]; then\n\tcache_dir=\"${HOME}/.cache\"\nfi\nif [ ! -e \"${cache_dir}/lima/download/by-url-sha256\" ]; then\n\techo \"No cache\"\n\texit 0\nfi\nfor f in \"${cache_dir}/lima/download/by-url-sha256/\"*; do\n\techo \"$f\"\n\tls -l \"$f\"\n\tcat \"${f}/url\"\n\techo\n\techo ---\ndone\n"
  },
  {
    "path": "hack/gogenerate/protoc.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# Generate Go code from a .proto file.\n# Expected to be called from `//go:generate` directive.\n\nset -eu\nif [ \"$#\" -ne 1 ]; then\n\techo >&2 \"Usage: $0 FILE\"\n\texit 1\nfi\n\nPROTO=\"$1\"                         ## a.proto\nBASE=\"$(basename \"$PROTO\" .proto)\" ## a\nPB_DESC=\"${BASE}.pb.desc\"          ## a.pb.desc\nPB_GO=\"${BASE}.pb.go\"              ## a.pb.go\nGRPC_PB_GO=\"${BASE}_grpc.pb.go\"    ## a_grpc.pb.go\n\nset -x\n\nprotoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative \"$PROTO\" --descriptor_set_out=\"$PB_DESC\"\n\n# -// - protoc             v6.32.0\n# +// - protoc version [omitted for reproducibility]\n#\n# perl is used because `sed -i` is not portable across BSD (macOS) and GNU.\nperl -pi -E 's@(^//.*protoc.*)[[:blank:]]+v[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+@\\1 [version omitted for reproducibility]@g' \"$PB_GO\" \"$GRPC_PB_GO\"\n"
  },
  {
    "path": "hack/inject-cmdline-to-template.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n#\n# This script does\n# 1. detect arch from template if not provided\n# 2. extract location by parsing template using arch\n# 3. get the image location\n# 4. check the image location is supported\n# 5. build the kernel and initrd location, digest, and cmdline\n# 6. inject the kernel and initrd location, digest, and cmdline to the template\n# 7. output kernel_location, kernel_digest, cmdline, initrd_location, initrd_digest\n\nset -eu -o pipefail\n\ntemplate=\"$1\"\nappending_options=\"$2\"\n# 1. detect arch from template if not provided\narch=\"${3:-$(yq '.arch // \"\"' \"${template}\")}\"\narch=\"${arch:-$(uname -m)}\"\n\n# normalize arch. amd64 -> x86_64, arm64 -> aarch64\ncase \"${arch}\" in\namd64 | x86_64) arch=x86_64 ;;\naarch64 | arm64) arch=aarch64 ;;\narmv7l | armhf) arch=armv7l ;;\nppc64el | ppc64le) arch=ppc64le ;;\ns390x) arch=s390x ;;\nriscv64) arch=riscv64 ;;\n*)\n\techo \"Unsupported arch: ${arch}\" >&2\n\texit 1\n\t;;\nesac\n\n# 2. extract location by parsing template using arch\nreadonly yq_filter=\"\n    .images[]|select(.arch == \\\"${arch}\\\")|.location\n\"\nparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n# 3. get the image location\nfunction check_location() {\n\tlocal location=$1 http_code\n\thttp_code=$(curl -sIL -w \"%{http_code}\" \"${location}\" -o /dev/null)\n\t[[ ${http_code} -eq 200 ]]\n}\nwhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\nreadonly locations=(\"${arr[@]}\")\nfor ((i = 0; i < ${#locations[@]}; i++)); do\n\t[[ ${locations[i]} != \"null\" ]] || continue\n\t# shellcheck disable=SC2310\n\tif check_location \"${locations[i]}\"; then\n\t\tlocation=${locations[i]}\n\t\tindex=${i}\n\t\tbreak\n\tfi\ndone\n\n# 4. check the image location is supported\ncase \"${location:-}\" in\nhttps://cloud-images.ubuntu.com/minimal/* | https://cloud-images.ubuntu.com/daily/server/minimal/*)\n\treadonly default_cmdline=\"root=/dev/vda1 ro console=tty1 console=ttyAMA0\"\n\t;;\nhttps://cloud-images.ubuntu.com/*)\n\treadonly default_cmdline=\"root=LABEL=cloudimg-rootfs ro console=tty1 console=ttyAMA0\"\n\t;;\n*)\n\techo \"Unsupported image location: ${location}\" >&2\n\texit 1\n\t;;\nesac\n\n# 5. build the kernel and initrd location, digest, and cmdline\nlocation_dirname=$(dirname \"${location}\")/unpacked\nsha256sums=$(curl -sSLf \"${location_dirname}/SHA256SUMS\")\nlocation_basename=$(basename \"${location}\")\n\n# cmdline\ncmdline=\"${default_cmdline} ${appending_options}\"\n\n# kernel\nkernel_basename=\"${location_basename/.img/-vmlinuz-generic}\"\nkernel_digest=$(awk \"/${kernel_basename}/{print \\\"sha256:\\\"\\$1}\" <<<\"${sha256sums}\")\nkernel_location=\"${location_dirname}/${kernel_basename}\"\n\n# initrd\ninitrd_basename=\"${location_basename/.img/-initrd-generic}\"\ninitrd_digest=$(awk \"/${initrd_basename}/{print \\\"sha256:\\\"\\$1}\" <<<\"${sha256sums}\")\ninitrd_location=\"${location_dirname}/${initrd_basename}\"\n\n# 6. inject the kernel and initrd location, digest, and cmdline to the template\nfunction inject_to() {\n\t# shellcheck disable=SC2034\n\tlocal template=$1 arch=$2 index=$3 key=$4 location=$5 digest=$6 cmdline=${7:-} fields=() IFS=,\n\t# shellcheck disable=SC2310\n\tcheck_location \"${location}\" || return 0\n\tfor field_name in location digest cmdline; do\n\t\t[[ -z ${!field_name} ]] || fields+=(\"\\\"${field_name}\\\": \\\"${!field_name}\\\"\")\n\tdone\n\tlimactl edit --log-level error --set \"setpath([(.images[] | select(.arch == \\\"${arch}\\\") | path)].[${index}] + \\\"${key}\\\"; { ${fields[*]}})\" \"${template}\"\n}\ninject_to \"${template}\" \"${arch}\" \"${index}\" \"kernel\" \"${kernel_location}\" \"${kernel_digest}\" \"${cmdline}\"\ninject_to \"${template}\" \"${arch}\" \"${index}\" \"initrd\" \"${initrd_location}\" \"${initrd_digest}\"\n\n# 7. output kernel_location, kernel_digest, cmdline, initrd_location, initrd_digest\nreadonly outputs=(kernel_location kernel_digest cmdline initrd_location initrd_digest)\nfor output in \"${outputs[@]}\"; do\n\techo \"${output}=${!output}\"\ndone\n"
  },
  {
    "path": "hack/install-qemu.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# Install qemu on GitHub Actions runner.\n# Not expected to be used outside GitHub Actions.\n\nset -eux\n\n# apt-get update has to be run beforehand\napt-get install -y --no-install-recommends ovmf qemu-system-x86 qemu-utils\nmodprobe kvm\n# `usermod -aG kvm ${SUDO_USER}` does not take an effect on GHA\nchown \"${SUDO_USER}\" /dev/kvm\nqemu-system-x86_64 --version\n"
  },
  {
    "path": "hack/ltag/bash.txt",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n"
  },
  {
    "path": "hack/ltag/dockerfile.txt",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n"
  },
  {
    "path": "hack/ltag/go.txt",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n"
  },
  {
    "path": "hack/ltag/makefile.txt",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n"
  },
  {
    "path": "hack/oss-fuzz-build.sh",
    "content": "#!/bin/bash -eu\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This script is used by OSS-Fuzz to build and run Limas fuzz tests continuously.\n# Limas OSS-Fuzz integration can be found here: https://github.com/google/oss-fuzz/tree/master/projects/lima\n# Modify https://github.com/google/oss-fuzz/blob/master/projects/lima/project.yaml for access management to Limas OSS-Fuzz crashes.\nprintf \"package store\\nimport _ \\\"github.com/AdamKorcz/go-118-fuzz-build/testing\\\"\\n\" >\"$SRC\"/lima/pkg/store/register.go\ngo mod tidy\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/store FuzzLoadYAMLByFilePath FuzzLoadYAMLByFilePath\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/store FuzzInspect FuzzInspect\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/nativeimgutil FuzzConvertToRaw FuzzConvertToRaw\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/cidata FuzzSetupEnv FuzzSetupEnv\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/iso9660util FuzzIsISO9660 FuzzIsISO9660\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/guestagent/procnettcp FuzzParse FuzzParse\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/yqutil FuzzEvaluateExpression FuzzEvaluateExpression\ncompile_native_go_fuzzer github.com/lima-vm/lima/v2/pkg/downloader FuzzDownload FuzzDownload\n"
  },
  {
    "path": "hack/test-mount-home.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\nscriptdir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# shellcheck source=common.inc.sh\nsource \"${scriptdir}/common.inc.sh\"\n\nif [ \"$#\" -ne 1 ]; then\n\tERROR \"Usage: $0 NAME\"\n\texit 1\nfi\n\nNAME=\"$1\"\nhometmp=\"${HOME_HOST:-$HOME}/lima-test-tmp\"\nhometmpguest=\"${HOME_GUEST:-$HOME}/lima-test-tmp\"\nINFO \"Testing home access (\\\"$hometmp\\\")\"\nrm -rf \"$hometmp\"\nmkdir -p \"$hometmp\"\ndefer \"rm -rf \\\"$hometmp\\\"\"\necho \"random-content-${RANDOM}\" >\"$hometmp/random\"\nexpected=\"$(cat \"$hometmp/random\")\"\ngot=\"$(limactl shell \"$NAME\" cat \"$hometmpguest/random\")\"\nINFO \"$hometmp/random: expected=${expected}, got=${got}\"\nif [ \"$got\" != \"$expected\" ]; then\n\tERROR \"Home directory is not shared?\"\n\texit 1\nfi\n"
  },
  {
    "path": "hack/test-nonplain-static-port-forward.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -euxo pipefail\n\nINSTANCE=nonplain-static-port-forward\nTEMPLATE=\"$(dirname \"$0\")/test-templates/test-misc.yaml\"\n\nlimactl delete -f \"$INSTANCE\" || true\n\nlimactl start --name=\"$INSTANCE\" --tty=false \"$TEMPLATE\"\n\nlimactl shell \"$INSTANCE\" -- bash -c 'until systemctl is-active --quiet nginx; do sleep 1; done'\nlimactl shell \"$INSTANCE\" -- bash -c 'until systemctl is-active --quiet test-server-9080; do sleep 1; done'\nlimactl shell \"$INSTANCE\" -- bash -c 'until systemctl is-active --quiet test-server-9070; do sleep 1; done'\n\ncurl -sSf http://127.0.0.1:9090 | grep -i 'nginx' && echo 'Static port forwarding (9090) works in normal mode!'\ncurl -sSf http://127.0.0.1:29080 | grep -i 'Dynamic port 9080' && echo 'Dynamic port forwarding (29080) works in normal mode!'\ncurl -sSf http://127.0.0.1:29070 | grep -i 'Dynamic port 9070' && echo 'Dynamic port forwarding (29070) works in normal mode!'\n\nlimactl delete -f \"$INSTANCE\"\necho \"All tests passed for normal mode - both static and dynamic ports work!\"\n# EOF\n"
  },
  {
    "path": "hack/test-plain-static-port-forward.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -euxo pipefail\n\nINSTANCE=plain-static-port-forward\nTEMPLATE=\"$(dirname \"$0\")/test-templates/test-misc.yaml\"\n\nlimactl delete -f \"$INSTANCE\" || true\n\nlimactl start --name=\"$INSTANCE\" --plain=true --tty=false \"$TEMPLATE\"\n\nlimactl shell \"$INSTANCE\" -- bash -c 'until systemctl is-active --quiet nginx; do sleep 1; done'\n\nif ! curl -sSf http://127.0.0.1:9090 | grep -i 'nginx'; then\n\techo 'ERROR: Static port forwarding (9090) does not work in plain mode!'\n\texit 1\nfi\necho 'Static port forwarding (9090) works in plain mode!'\n\nif curl -sSf http://127.0.0.1:29080 2>/dev/null; then\n\techo 'ERROR: Dynamic port 29080 should not be forwarded in plain mode!'\n\texit 1\nelse\n\techo 'Dynamic port 29080 is correctly NOT forwarded in plain mode.'\nfi\n\nif curl -sSf http://127.0.0.1:29070 2>/dev/null; then\n\techo 'ERROR: Dynamic port 29070 should not be forwarded in plain mode!'\n\texit 1\nelse\n\techo 'Dynamic port 29070 is correctly NOT forwarded in plain mode.'\nfi\n\nlimactl delete -f \"$INSTANCE\"\necho \"All tests passed for plain mode - only static ports work!\"\n# EOF\n"
  },
  {
    "path": "hack/test-port-forwarding.pl",
    "content": "#!/usr/bin/env perl\n\n# This script tests the port forwarding settings of lima. It has to be run\n# twice: once to update the instance yaml file with the port forwarding\n# rules (before the instance is started). And once when the instance is\n# running to perform the tests:\n#\n# ./hack/test-port-forwarding.pl templates/default.yaml\n# limactl --tty=false start templates/default.yaml\n# git restore templates/default.yaml\n# ./hack/test-port-forwarding.pl default [nc|socat [nc|socat]] [timeout]\n#\n# TODO: support for ipv6 host addresses\n\nuse strict;\nuse warnings;\n\nuse Config qw(%Config);\nuse File::Spec::Functions qw(catfile);\nuse IO::Handle qw();\nuse JSON::PP;\nuse Socket qw(inet_ntoa);\nuse Sys::Hostname qw(hostname);\n\nmy $connectionTimeout = 1; # seconds\n\nmy $instance = shift;\nmy $listener;\nmy $writer;\nwhile (my $arg = shift) {\n    if ($arg eq \"nc\" || $arg eq \"socat\") {\n        $listener = $arg unless defined $listener;\n        $writer = $arg if defined $listener && !defined $writer;\n    } elsif ($arg =~ /^\\d+$/) {\n        $connectionTimeout = $arg;\n    } else {\n        die \"Usage: $0 [instance|yaml-file] [nc|socat [nc|socat]] [timeout]\\n\";\n    }\n}\n$listener ||= \"nc\";\n$writer ||= $listener;\n\nmy $addr = scalar gethostbyname(hostname());\n# If hostname address cannot be determines, use localhost to trigger fallback to system_profiler lookup\nmy $ipv4 = length $addr ? inet_ntoa($addr) : \"127.0.0.1\";\nmy $ipv6 = \"\"; # todo\n\n# macOS GitHub runners seem to use \"localhost\" as the hostname\nif ($ipv4 eq \"127.0.0.1\" && $Config{osname} eq \"darwin\") {\n    $ipv4 = qx(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first');\n    chomp $ipv4;\n}\n\nmy $instDir = qx(limactl list \"$instance\" --yq .dir);\nchomp $instDir;\n# platform independent way to add trailing path separator\nmy $sockDir = catfile($instDir, \"sock\", \"\");\n\n# If $instance is a filename, add our portForwards to it to enable testing\nif (-f $instance) {\n    open(my $fh, \"+< $instance\") or die \"Can't open $instance for read/write: $!\";\n    my @yaml;\n    while (<$fh>) {\n        # Remove existing \"portForwards:\" section from the config file\n        my $seq = /^portForwards:/ ... /^[a-z]/;\n        next if $seq && $seq !~ /E0$/;\n        push @yaml, $_;\n    }\n    seek($fh, 0, 0);\n    truncate($fh, 0);\n    print $fh $_ for @yaml;\n    while (<DATA>) {\n        s/ipv4/$ipv4/g;\n        s/ipv6/$ipv6/g;\n        print $fh $_;\n    }\n    exit;\n}\n\n# Check if netcat is available before running tests\nmy $nc_path = `command -v nc 2>/dev/null`;\nchomp $nc_path;\nunless ($nc_path) {\n    die \"Error: 'nc' (netcat) is not installed on the host system.\\n\" .\n        \"Please install netcat to run this test script:\\n\" .\n        \"  - On macOS: brew install netcat\\n\" .\n        \"  - On Ubuntu/Debian: sudo apt-get install netcat\\n\" .\n        \"  - On RHEL/CentOS: sudo yum install nmap-ncat\\n\";\n}\n\n# Otherwise $instance must be the name of an already running instance that has been\n# configured with our portForwards settings.\n\nmy $instanceType = qx(limactl ls --json \"$instance\" | jq -r '.vmType' | sed s/x/x/);\nchomp $instanceType;\n\n# Get sshLocalPort for lima instance\nmy $sshLocalPort;\nopen(my $ls, \"limactl ls --json |\") or die;\nwhile (<$ls>) {\n    next unless /\"name\":\"$instance\"/;\n    ($sshLocalPort) = /\"sshLocalPort\":(\\d+)/ or die;\n    last;\n}\ndie \"Cannot determine sshLocalPort\" unless $sshLocalPort;\n\n# Extract forwarding tests from the \"portForwards\" section\nmy @test;\nwhile (<DATA>) {\n    chomp;\n    s/^\\s+#\\s*//;\n    next unless /^(forward|ignore)/;\n    if (/ipv6/ && !$ipv6) {\n        printf \"🚧 Not yet: # $_\\n\";\n        next;\n    }\n    s/sshLocalPort/$sshLocalPort/g;\n    s/ipv4/$ipv4/g;\n    s/ipv6/$ipv6/g;\n    s/sockDir\\//$sockDir/g;\n    # forward: 127.0.0.1 899 → 127.0.0.1 799\n    # ignore: 127.0.0.2 8888\n    /^(forward|ignore):\\s+([0-9.:]+)\\s+(\\d+)(?:\\s+→)?(?:\\s+(?:([0-9.:]+)(?:\\s+(\\d+))|(\\S+))?)?/;\n    die \"Cannot parse test '$_'\" unless $1;\n    my %test; @test{qw(mode guest_ip guest_port host_ip host_port host_socket)} = ($1, $2, $3, $4, $5, $6);\n\n    $test{host_ip} ||= \"127.0.0.1\";\n    $test{host_port} ||= $test{guest_port};\n    $test{host_socket} ||= \"\";\n    if ($test{mode} eq \"forward\" && $test{host_socket} eq \"\" && $test{host_port} < 1024 && $Config{osname} ne \"darwin\") {\n        printf \"🚧 Not supported on $Config{osname}: # $_\\n\";\n        next;\n    }\n    if ($test{mode} eq \"ignore\" && ($test{guest_ip} eq \"0.0.0.0\" || $test{guest_ip} eq \"127.0.0.1\") && \"$instanceType\" eq \"wsl2\") {\n        printf \"🚧 Not supported for $instanceType machines: # $_\\n\";\n        next;\n    }\n    if ($test{guest_ip} eq \"192.168.5.15\" && \"$instanceType\" eq \"wsl2\") {\n        printf \"🚧 Not supported for $instanceType machines: # $_\\n\";\n        next;\n    }\n    if ($test{host_socket} ne \"\" && $Config{osname} eq \"cygwin\") {\n        printf \"🚧 Not supported on $Config{osname}: # $_\\n\";\n        next;\n    }\n\n    my $remote = JoinHostPort($test{guest_ip},$test{guest_port});\n    my $local = $test{host_socket} eq \"\" ? JoinHostPort($test{host_ip},$test{host_port}) : $test{host_socket};\n    if ($test{mode} eq \"ignore\") {\n        $test{log_msg} = \"Not forwarding TCP $remote\";\n    }\n    else {\n        $test{log_msg} = \"Forwarding TCP from $remote to $local\";\n    }\n    push @test, \\%test;\n}\n\nopen(my $lima, \"| limactl shell --workdir / $instance\")\n  or die \"Can't run lima shell on $instance: $!\";\n$lima->autoflush;\n\nprint $lima <<'EOF';\nset -e\ncd $HOME\nsudo pkill -x nc || true\nsudo pkill -x socat || true\nrm -f nc.* socat.*\nEOF\n\n# Give the hostagent some time to remove any port forwards from a previous (crashed?) test run\nsleep 5;\n\n# Record current log size, so we can skip prior output\n$ENV{HOME_HOST} ||= \"$ENV{HOME}\";\n$ENV{LIMA_HOME} ||= \"$ENV{HOME_HOST}/.lima\";\nmy $ha_stdout_log = \"$ENV{LIMA_HOME}/$instance/ha.stdout.log\";\nmy $ha_stderr_log = \"$ENV{LIMA_HOME}/$instance/ha.stderr.log\";\nmy $ha_stdout_log_size = -s $ha_stdout_log or die;\nmy $ha_stderr_log_size = -s $ha_stderr_log or die;\n\n# Setup a netcat listener on the guest for each test\nforeach my $id (0..@test-1) {\n    my $test = $test[$id];\n    my $cmd;\n    if ($listener eq \"nc\") {\n        $cmd = \"nc -l $test->{guest_ip} $test->{guest_port}\";\n        if ($instance =~ /^alpine/) {\n            $cmd = \"nc -l -s $test->{guest_ip} -p $test->{guest_port}\";\n        }\n    } elsif ($listener eq \"socat\") {\n        my $proto = $test->{guest_ip} =~ /:/ ? \"TCP6\" : \"TCP\";\n        $cmd = \"socat -u $proto-LISTEN:$test->{guest_port},bind=$test->{guest_ip} STDOUT\";\n    }\n\n    my $sudo = $test->{guest_port} < 1024 ? \"sudo \" : \"\";\n    print $lima \"${sudo}${cmd} >$listener.${id} 2>/dev/null &\\n\";\n}\n\n# Make sure the guest- and hostagents had enough time to set up the forwards\nsleep 5;\n\n# Try to reach each listener from the host\nforeach my $test (@test) {\n    next if $test->{host_port} == $sshLocalPort;\n    my $cmd;\n    if ($writer eq \"nc\") {\n        if ($Config{osname} eq \"darwin\") {\n            # macOS nc doesn't support -w for connection timeout, so use -G instead\n            $cmd = $test->{host_socket} eq \"\" ? \"nc -G $connectionTimeout $test->{host_ip} $test->{host_port}\" : \"nc -G $connectionTimeout -U $test->{host_socket}\";\n        } else {\n            $cmd = $test->{host_socket} eq \"\" ? \"nc -w $connectionTimeout $test->{host_ip} $test->{host_port}\" : \"nc -w $connectionTimeout -U $test->{host_socket}\";\n        }\n    } elsif ($writer eq \"socat\") {\n        my $tcp_dest = $test->{host_ip} =~ /:/ ? \"TCP6:[$test->{host_ip}]:$test->{host_port}\" : \"TCP:$test->{host_ip}:$test->{host_port}\";\n        $cmd = $test->{host_socket} eq \"\" ? \"socat -u STDIN $tcp_dest,connect-timeout=$connectionTimeout\" : \"socat -u STDIN UNIX-CONNECT:$test->{host_socket}\";\n    }\n    print \"Running: $cmd\\n\";\n    open(my $netcat, \"| $cmd\") or die \"Can't run '$cmd': $!\";\n    print $netcat \"$test->{log_msg}\\n\";\n    # Don't check for errors on close; macOS nc seems to return non-zero exit code even on success\n    close($netcat);\n}\n\n# Extract forwarding log messages from hostagent JSON event log\nmy $json_parser = JSON::PP->new->utf8->relaxed;\n\nopen(my $log, \"< $ha_stdout_log\") or die \"Can't read $ha_stdout_log: $!\";\nseek($log, $ha_stdout_log_size, 0) or die \"Can't seek $ha_stdout_log to $ha_stdout_log_size: $!\";\nmy %seen;\nmy %failed_to_listen_tcp;\n\nwhile (<$log>) {\n    chomp;\n    next unless /^\\s*\\{/; # Skip non-JSON lines\n\n    my $event = eval { $json_parser->decode($_) };\n    next unless $event;\n\n    my $pf = $event->{status}{portForward};\n    next unless $pf && $pf->{type};\n\n    my $type = $pf->{type};\n    my $protocol = uc($pf->{protocol} || \"tcp\");\n    my $guest_addr = $pf->{guestAddr} || \"\";\n    my $host_addr = $pf->{hostAddr} || \"\";\n    my $error = $pf->{error} || \"\";\n\n    if ($type eq \"forwarding\") {\n        my $msg = \"Forwarding $protocol from $guest_addr to $host_addr\";\n        $seen{$msg}++;\n    } elsif ($type eq \"not-forwarding\") {\n        my $msg = \"Not forwarding $protocol $guest_addr\";\n        $seen{$msg}++;\n    } elsif ($type eq \"failed\" && $error =~ /listen tcp/) {\n        # Extract the address from the error message\n        if ($error =~ /listen tcp (.*?:\\d+):/) {\n            my $addr = $1;\n            $failed_to_listen_tcp{$addr} = \"failed to listen tcp: $error\";\n        }\n    }\n}\nclose $log or die;\n\n# Also check stderr log for failed_to_listen_tcp messages (these may not be in JSON events)\nopen(my $stderr_log, \"< $ha_stderr_log\") or die \"Can't read $ha_stderr_log: $!\";\nseek($stderr_log, $ha_stderr_log_size, 0) or die \"Can't seek $ha_stderr_log to $ha_stderr_log_size: $!\";\nwhile (<$stderr_log>) {\n    $failed_to_listen_tcp{$2}=$1 if /(failed to listen tcp: listen tcp (.*?:\\d+):[^\"]+)/;\n}\nclose $stderr_log or die;\n\nmy $rc = 0;\nmy %expected;\nforeach my $id (0..@test-1) {\n    my $test = $test[$id];\n    my $err = \"\";\n    $expected{$test->{log_msg}}++;\n    unless ($seen{$test->{log_msg}}) {\n        $err .= \"\\n   Message missing from ha.stdout.log (JSON events)\";\n    }\n    my $log = qx(limactl shell --workdir / $instance sh -c \"cd; cat $listener.$id\");\n    chomp $log;\n    if ($test->{mode} eq \"forward\" && $test->{log_msg} ne $log) {\n        $err .= \"\\n   Guest received: '$log'\";\n    }\n    if ($test->{mode} eq \"ignore\" && $log) {\n        $err .= \"\\n   Guest received: '$log' (instead of nothing)\";\n    }\n    printf \"%s %s%s\\n\", ($err ? \"❌\" : \"✅\"), $test->{log_msg}, $err;\n    $rc = 1 if $err;\n}\n\nforeach (keys %seen) {\n    next if $expected{$_};\n    # Should this be an error? Really should only happen if something else failed as well.\n    print \"😕 Unexpected log message: $_\\n\";\n}\n\nif (%failed_to_listen_tcp) {\n    foreach (keys %failed_to_listen_tcp) {\n        print \"⚠️  $failed_to_listen_tcp{$_}\\n\";\n    }\n    my @tcp_list = keys %failed_to_listen_tcp;\n    if ($Config{osname} eq \"darwin\") {\n        my @lsof_args = map { \"-iTCP\\@$_\" } @tcp_list;\n        print `lsof -P @lsof_args`;\n    } elsif ($Config{osname} eq \"linux\") {\n        my @lss_args = map { \"src = $_\" } @tcp_list;\n        my $ss_expression = join(\" or \", @lss_args);\n        print `sudo ss -lnpt \"$ss_expression\"`;\n    } elsif ($Config{osname} eq \"cygwin\") {\n        my @awk_args = map { \"-e'/$_/'\" } @tcp_list;\n        print `netstat -aon | awk -e'/^ +Proto/' @awk_args`;\n    }\n}\n\n# Cleanup remaining netcat instances (and port forwards)\nprint $lima \"sudo pkill -x $listener\";\n\nexit $rc;\n\nsub JoinHostPort {\n    my($host,$port) = @_;\n    $host = \"[$host]\" if $host =~ /:/;\n    return \"$host:$port\";\n}\n\n# This YAML section includes port forwarding `rules` for the guest- and hostagents,\n# with interleaved `tests` (in comments) that are executed by this script. The strings\n# \"ipv4\" and \"ipv6\" will be replaced by the actual host ipv4 and ipv6 addresses.\n__DATA__\nportForwards:\n# We can't test that port 22 will be blocked because the guestagent has\n# been ignoring it since startup, so the log message is in the part of\n# the log we skipped.\n# skip: 127.0.0.1 22 → 127.0.0.1 2222\n# ignore: 127.0.0.1 sshLocalPort\n\n- guestIP: 127.0.0.2\n  guestPortRange: [3000, 3009]\n  hostPortRange: [2000, 2009]\n  ignore: true\n\n- guestIP: 0.0.0.0\n  guestIPMustBeZero: false\n  guestPortRange: [3010, 3019]\n  hostPortRange: [2010, 2019]\n  ignore: true\n\n- guestIP: 0.0.0.0\n  guestIPMustBeZero: false\n  guestPortRange: [3000, 3029]\n  hostPortRange: [2000, 2029]\n\n# The following rule is completely shadowed by the previous one and has no effect\n- guestIP: 0.0.0.0\n  guestIPMustBeZero: false\n  guestPortRange: [3020, 3029]\n  hostPortRange: [2020, 2029]\n  ignore: true\n\n  # ignore:  127.0.0.2 3000\n  # forward: 127.0.0.3 3001 → 127.0.0.1 2001\n\n  # Blocking 127.0.0.2 cannot block forwarding from 0.0.0.0\n  # forward: 0.0.0.0   3002 → 127.0.0.1 2002\n\n  # Blocking 0.0.0.0 will block forwarding from any interface because guestIPMustBeZero is false\n  # ignore: 0.0.0.0   3010\n  # ignore: 127.0.0.1 3011\n\n  # Forwarding from 0.0.0.0 works for any interface (including IPv6)\n  # The \"ignore\" rule above has no effect because the previous rule already matched.\n  # forward: 127.0.0.2 3020 → 127.0.0.1 2020\n  # forward: 127.0.0.1 3021 → 127.0.0.1 2021\n  # forward: 0.0.0.0   3022 → 127.0.0.1 2022\n  # forward: ::        3023 → 127.0.0.1 2023\n  # forward: ::1       3024 → 127.0.0.1 2024\n\n- guestPortRange: [3030, 3039]\n  hostPortRange: [2030, 2039]\n  hostIP: ipv4\n\n  # forward: 127.0.0.1 3030 → ipv4 2030\n  # forward: 0.0.0.0   3031 → ipv4 2031\n  # forward: ::        3032 → ipv4 2032\n  # forward: ::1       3033 → ipv4 2033\n\n- guestPortRange: [300, 304]\n\n  # forward: 127.0.0.1    300 → 127.0.0.1 300\n  # forward: 0.0.0.0      301 → 127.0.0.1 301\n  # forward: ::           302 → 127.0.0.1 302\n  # forward: ::1          303 → 127.0.0.1 303\n  # ignore:  192.168.5.15 304 → 127.0.0.1 304\n\n- guestPortRange: [305, 309]\n  guestIPMustBeZero: false\n\n  # forward: 127.0.0.1    325 → 127.0.0.1 325\n  # forward: 0.0.0.0      326 → 127.0.0.1 326\n  # forward: ::           327 → 127.0.0.1 327\n  # forward: ::1          328 → 127.0.0.1 328\n  # ignore:  192.168.5.15 329 → 127.0.0.1 329\n\n- guestPortRange: [310, 314]\n  hostIP: 0.0.0.0\n\n  # forward: 127.0.0.1    310 → 0.0.0.0 310\n  # forward: 0.0.0.0      311 → 0.0.0.0 311\n  # forward: ::           312 → 0.0.0.0 312\n  # forward: ::1          313 → 0.0.0.0 313\n  # ignore:  192.168.5.15 314 → 0.0.0.0 314\n\n- guestPortRange: [315, 319]\n  guestIPMustBeZero: false\n  hostIP: 0.0.0.0\n\n  # forward: 127.0.0.1    315 → 0.0.0.0 315\n  # forward: 0.0.0.0      316 → 0.0.0.0 316\n  # forward: ::           317 → 0.0.0.0 317\n  # forward: ::1          318 → 0.0.0.0 318\n  # ignore:  192.168.5.15 319 → 0.0.0.0 319\n\n  # Things we can't test:\n  # - Accessing a forward from a different interface (e.g. connect to ipv4 to connect to 0.0.0.0)\n  # - failed forward to privileged port\n\n\n- guestIP: \"192.168.5.15\"\n  guestPortRange: [4000, 4009]\n  hostIP: \"ipv4\"\n\n  # forward: 192.168.5.15 4000 → ipv4 4000\n\n- guestIP: \"::1\"\n  guestPortRange: [4010, 4019]\n  hostIP: \"::\"\n\n  # forward: ::1 4010 → :: 4010\n\n- guestIP: \"::\"\n  guestPortRange: [4020, 4029]\n  hostIP: \"ipv4\"\n\n  # forward: 127.0.0.1    4020 → ipv4 4020\n  # forward: 127.0.0.2    4021 → ipv4 4021\n  # forward: 192.168.5.15 4022 → ipv4 4022\n  # forward: 0.0.0.0      4023 → ipv4 4023\n  # forward: ::           4024 → ipv4 4024\n  # forward: ::1          4025 → ipv4 4025\n\n- guestIP: \"0.0.0.0\"\n  guestIPMustBeZero: false\n  guestPortRange: [4030, 4039]\n  hostIP: \"ipv4\"\n\n  # forward: 127.0.0.1    4030 → ipv4 4030\n  # forward: 127.0.0.2    4031 → ipv4 4031\n  # forward: 192.168.5.15 4032 → ipv4 4032\n  # forward: 0.0.0.0      4033 → ipv4 4033\n  # forward: ::           4034 → ipv4 4034\n  # forward: ::1          4035 → ipv4 4035\n\n- guestIPMustBeZero: true\n  guestPortRange: [4040, 4049]\n\n- guestIP: \"0.0.0.0\"\n  guestIPMustBeZero: false\n  guestPortRange: [4040, 4049]\n  ignore: true\n\n  # forward: 0.0.0.0        4040 → 127.0.0.1 4040\n  # forward: ::             4041 → 127.0.0.1 4041\n  # ignore:  127.0.0.1      4043 → 127.0.0.1 4043\n  # ignore:  192.168.5.15   4044 → 127.0.0.1 4044\n\n# This rule exist to test `nerdctl run` binding to 0.0.0.0 by default,\n# and making sure it gets forwarded to the external host IP.\n# The actual test code is in test-example.sh in the \"port-forwarding\" block.\n- guestIPMustBeZero: true\n  guestPort: 8888\n  hostIP: 0.0.0.0\n\n- guestPort: 5000\n  hostSocket: port5000.sock\n\n  # forward: 127.0.0.1    5000 → sockDir/port5000.sock\n\n- guestPort: 5001\n  hostSocket: port5001.sock\n\n  # ignore:  192.168.5.15 5001 → sockDir/port5001.sock\n\n- guestPort: 5002\n  guestIPMustBeZero: false\n  hostSocket: port5002.sock\n\n  # forward: 127.0.0.1    5002 → sockDir/port5002.sock\n\n- guestPort: 5003\n  guestIPMustBeZero: false\n  hostSocket: port5003.sock\n\n  # ignore:  192.168.5.15 5003 → sockDir/port5003.sock\n"
  },
  {
    "path": "hack/test-selinux.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\nscriptdir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# shellcheck source=common.inc.sh\nsource \"${scriptdir}/common.inc.sh\"\n\nif [ \"$#\" -ne 1 ]; then\n\tERROR \"Usage: $0 NAME\"\n\texit 1\nfi\n\nNAME=\"$1\"\n##########################################################################################\n## When using vz & virtiofs, initially container_file_t selinux label\n## was considered which works perfectly for container work loads\n## but it might break for other work loads if the process is running with\n## different label. Also these are the remote mounts from the host machine,\n## so keeping the label as nfs_t fits right. Package container-selinux by\n## default adds rules for nfs_t context which allows container workloads to work as well.\n## https://github.com/lima-vm/lima/pull/1965\n##\n## With integration[https://github.com/lima-vm/lima/pull/2474] with systemd-binfmt,\n## the existing \"nfs_t\" selinux label for Rosetta is causing issues while registering it.\n## This behaviour needs to be fixed by setting the label as \"bin_t\"\n## https://github.com/lima-vm/lima/pull/2630\n##########################################################################################\nINFO \"Testing secontext is set for rosetta\"\nexpected=\"context=system_u:object_r:bin_t:s0\"\n#Skip Rosetta checks for x86 GHA mac runners\nif [[ \"$(uname)\" == \"Darwin\" && \"$(arch)\" == \"arm64\" ]]; then\n\tINFO \"Testing secontext is set for rosetta mounts\"\n\tgot=$(limactl shell \"$NAME\" mount | grep \"rosetta\" | awk '{print $6}')\n\tINFO \"secontext rosetta: expected=${expected}, got=${got}\"\n\tif [[ $got != *$expected* ]]; then\n\t\tERROR \"secontext for rosetta mount is not set or Invalid\"\n\t\texit 1\n\tfi\nfi\nINFO \"Testing secontext is set for bind mounts\"\nexpected=\"context=system_u:object_r:nfs_t:s0\"\nINFO \"Checking in mounts\"\ngot=$(limactl shell \"$NAME\" mount | grep \"$HOME\" | awk '{print $6}')\nINFO \"secontext ${HOME}: expected=${expected}, got=${got}\"\nif [[ $got != *$expected* ]]; then\n\tERROR \"secontext for \\\"$HOME\\\" dir is not set or Invalid\"\n\texit 1\nfi\ngot=$(limactl shell \"$NAME\" mount | grep \"/tmp/lima\" | awk '{print $6}')\nINFO \"secontext /tmp/lima: expected=${expected}, got=${got}\"\nif [[ $got != *$expected* ]]; then\n\tERROR 'secontext for \"/tmp/lima\" dir is not set or Invalid'\n\texit 1\nfi\nINFO \"Checking in fstab file\"\nexpected='context=\"system_u:object_r:nfs_t:s0\"'\ngot=$(limactl shell \"$NAME\" cat /etc/fstab | grep \"$HOME\" | awk '{print $4}')\nINFO \"secontext ${HOME}: expected=${expected}, got=${got}\"\nif [[ $got != *$expected* ]]; then\n\tERROR \"secontext for \\\"$HOME\\\" dir is not set or Invalid\"\n\texit 1\nfi\ngot=$(limactl shell \"$NAME\" cat /etc/fstab | grep \"/tmp/lima\" | awk '{print $4}')\nINFO \"secontext /tmp/lima: expected=${expected}, got=${got}\"\nif [[ $got != *$expected* ]]; then\n\tERROR 'secontext for \"/tmp/lima\" dir is not set or Invalid'\n\texit 1\nfi\n"
  },
  {
    "path": "hack/test-templates/alpine-iso-9p-writable.yaml",
    "content": "# Background: https://github.com/lima-vm/lima/pull/2234\n# Should be tested on a Linux host\nimages:\n- location: \"https://github.com/lima-vm/alpine-lima/releases/download/v0.2.37/alpine-lima-std-3.19.0-x86_64.iso\"\n  arch: \"x86_64\"\n  digest: \"sha512:568852df405e6b9858e678171a9894c058f483df0b0570c22cf33fc75f349ba6cc5bb3d50188180d8c31faaf53400fe884ca3e5f949961b03b2bf53e65de88d7\"\n- location: \"https://github.com/lima-vm/alpine-lima/releases/download/v0.2.37/alpine-lima-std-3.19.0-aarch64.iso\"\n  arch: \"aarch64\"\n  digest: \"sha512:3a4bd5ad0201f503e9bb9f3b812aa0df292e2e099148c0323d23244046ad199a2946ef9e0619fec28726bfdcc528233f43c3b4b036c9e06e92ac730d579f0ca3\"\n\nmountType: \"9p\"\nmounts:\n- location: \"~\"\n  writable: true\n- location: \"/tmp/lima test dir with spaces\"\n  writable: true\n- location: \"/tmp/lima\"\n  writable: true\n\ncontainerd:\n  system: false\n  user: false\n"
  },
  {
    "path": "hack/test-templates/net-user-v2.yaml",
    "content": "# A template to run lima instance with experimental user-v2 network enabled\n# This template requires Lima v0.16.0 or later.\nimages:\n- location: \"https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img\"\n  arch: \"x86_64\"\n- location: \"https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img\"\n  arch: \"aarch64\"\n\nhostResolver:\n  hosts:\n    host.docker.internal: host.lima.internal\nmounts:\n- location: \"~\"\n- location: \"/tmp/lima\"\n  writable: true\nnetworks:\n- lima: user-v2\n"
  },
  {
    "path": "hack/test-templates/test-misc.yaml",
    "content": "# The test template for testing misc configurations:\n# - disk\n# - snapshots\n# - (More to come)\n#\nbase: template:ubuntu-22.04\n\n# 9p is not compatible with `limactl snapshot`\nmountTypesUnsupported: [\"9p\"]\nmounts:\n- location: \"~\"\n  writable: true\n- location: \"/tmp/lima test dir with spaces\"\n  writable: true\n\nparam:\n  ANSIBLE: ansible\n  BOOT: boot\n  DEPENDENCY: dependency\n  PROBE: probe\n  SYSTEM: system\n  USER: user\n  YQ: yq\n\nprovision:\n- mode: ansible\n  playbook: ./hack/ansible-test.yaml\n- mode: boot\n  script: \"touch /tmp/param-$PARAM_BOOT\"\n- mode: dependency\n  script: \"touch /tmp/param-$PARAM_DEPENDENCY\"\n- mode: system\n  script: |\n    touch /tmp/param-$PARAM_SYSTEM\n\n    # port forwarding test setup\n    apt-get update\n    apt-get install -y nginx python3\n    systemctl enable nginx\n    systemctl start nginx\n\n    cat > /etc/systemd/system/test-server-9080.service << 'EOF'\n    [Unit]\n    Description=Test Server on Port 9080\n    After=network.target\n\n    [Service]\n    Type=simple\n    User=root\n    ExecStart=/usr/bin/python3 -m http.server 9080 --bind 127.0.0.1\n    Restart=always\n\n    [Install]\n    WantedBy=multi-user.target\n    EOF\n\n    cat > /etc/systemd/system/test-server-9070.service << 'EOF'\n    [Unit]\n    Description=Test Server on Port 9070\n    After=network.target\n\n    [Service]\n    Type=simple\n    User=root\n    ExecStart=/usr/bin/python3 -m http.server 9070 --bind 127.0.0.1\n    Restart=always\n\n    [Install]\n    WantedBy=multi-user.target\n    EOF\n\n    mkdir -p /var/www/html-9080\n    mkdir -p /var/www/html-9070\n\n    echo '<html><body><h1>Dynamic port 9080</h1></body></html>' > /var/www/html-9080/index.html\n    echo '<html><body><h1>Dynamic port 9070</h1></body></html>' > /var/www/html-9070/index.html\n\n    systemctl daemon-reload\n    systemctl enable test-server-9080\n    systemctl enable test-server-9070\n    systemctl start test-server-9080\n    systemctl start test-server-9070\n\n- mode: user\n  script: \"touch /tmp/param-$PARAM_USER\"\n- mode: data\n  path: /etc/sysctl.d/99-inotify.conf\n  content: |\n    fs.inotify.max_user_watches = 524288\n    fs.inotify.max_user_instances = 512\n- mode: yq\n  path: \"/tmp/param-{{.Param.YQ}}.json\"\n  expression: .YQ = \"{{.Param.YQ}}\"\n\nprobes:\n- mode: readiness\n  script: |\n    #!/bin/sh\n    touch /tmp/param-$PARAM_PROBE\n\n# in order to use this example, you must first create the disks. run:\n# $ limactl disk create data --size 10G\n# $ limactl disk create swap --size 2G\nadditionalDisks:\n- \"data\"\n- name: \"swap\"\n  format: true\n  fsType: swap\n\nuser:\n  name: john\n  comment: John Doe\n  home: \"/home/{{.User}}-{{.User}}\"\n  uid: 4711\n  # Ubuntu has identical /bin/bash and /usr/bin/bash\n  shell: /usr/bin/bash\n\nportForwards:\n- guestPort: 80\n  hostPort: 9090\n  static: true\n- guestPort: 9080\n  hostPort: 29080\n  static: false\n- guestPort: 9070\n  hostPort: 29070\n  static: false\n"
  },
  {
    "path": "hack/test-templates.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# will prevent msys2 converting Linux path arguments into Windows paths before passing to limactl\nexport MSYS2_ARG_CONV_EXCL='*'\n\nscriptdir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# shellcheck source=common.inc.sh\nsource \"${scriptdir}/common.inc.sh\"\n\nif [ \"$#\" -ne 1 ]; then\n\tERROR \"Usage: $0 FILE.yaml\"\n\texit 1\nfi\n\n# Resolve any ../ fragments in the filename because they are invalid in relative template locators\nFILE=\"$(cd \"$(dirname \"$1\")\" && pwd)/$(basename \"$1\")\"\nNAME=\"$(basename -s .yaml \"$FILE\")\"\nOS_HOST=\"$(uname -o)\"\n\n# On Windows $HOME of the bash runner, %USERPROFILE% of the host machine and mounting point in the guest machine\n# are all different folders. This will handle path differences, when values are explicitly set.\nHOME_HOST=${HOME_HOST:-$HOME}\nHOME_GUEST=${HOME_GUEST:-$HOME}\nFILE_HOST=$FILE\nif [ \"${OS_HOST}\" = \"Msys\" ]; then\n\tFILE_HOST=\"$(cygpath -w \"$FILE\")\"\nfi\n\nINFO \"Validating \\\"$FILE_HOST\\\"\"\nlimactl validate \"$FILE_HOST\"\n\nLIMACTL_CREATE=(limactl --tty=false create)\n\nCONTAINER_ENGINE=\"nerdctl\"\n\ndeclare -A CHECKS=(\n\t[\"proxy-settings\"]=\"1\"\n\t[\"systemd\"]=\"1\"\n\t[\"mount-home\"]=\"1\"\n\t[\"container-engine\"]=\"1\"\n\t[\"restart\"]=\"1\"\n\t# snapshot tests are too flaky (especially with archlinux)\n\t[\"snapshot-online\"]=\"\"\n\t[\"snapshot-offline\"]=\"\"\n\t[\"clone\"]=\"\"\n\t[\"port-forwards\"]=\"1\"\n\t[\"vmnet\"]=\"\"\n\t[\"disk\"]=\"\"\n\t[\"user-v2\"]=\"\"\n\t[\"mount-path-with-spaces\"]=\"\"\n\t[\"provision-data\"]=\"\"\n\t[\"provision-yq\"]=\"\"\n\t[\"param-env-variables\"]=\"\"\n\t[\"set-user\"]=\"\"\n\t[\"preserve-env\"]=\"1\"\n\t[\"static-port-forwards\"]=\"\"\n\t[\"ssh-over-vsock\"]=\"\"\n)\n\ncase \"$NAME\" in\n\"default\")\n\t# CI failure:\n\t# \"[hostagent] failed to confirm whether /c/Users/runneradmin [remote] is successfully mounted\"\n\t[ \"${OS_HOST}\" = \"Msys\" ] && CHECKS[\"mount-home\"]=\n\t[ \"${OS_HOST}\" = \"Darwin\" ] && CHECKS[\"ssh-over-vsock\"]=\"1\"\n\t;;\n\"alpine\"*)\n\tWARNING \"Alpine does not support systemd\"\n\tCHECKS[\"systemd\"]=\n\tCHECKS[\"container-engine\"]=\n\t[ \"$NAME\" = \"alpine-iso-9p-writable\" ] && CHECKS[\"mount-path-with-spaces\"]=\"1\"\n\t;;\n\"k3s\")\n\tERROR \"File \\\"$FILE\\\" is not testable with this script\"\n\texit 1\n\t;;\n\"test-misc\")\n\tCHECKS[\"disk\"]=1\n\tCHECKS[\"snapshot-online\"]=\"1\"\n\tCHECKS[\"snapshot-offline\"]=\"1\"\n\tCHECKS[\"clone\"]=\"1\"\n\tCHECKS[\"mount-path-with-spaces\"]=\"1\"\n\tCHECKS[\"provision-data\"]=\"1\"\n\tCHECKS[\"provision-yq\"]=\"1\"\n\tCHECKS[\"param-env-variables\"]=\"1\"\n\tCHECKS[\"set-user\"]=\"1\"\n\tCHECKS[\"static-port-forwards\"]=\"1\"\n\t;;\n\"docker\")\n\tCONTAINER_ENGINE=\"docker\"\n\t;;\n\"wsl2\")\n\t# TODO https://github.com/lima-vm/lima/issues/3268\n\tCHECKS[\"proxy-settings\"]=\n\t;;\nesac\n\nif limactl ls -q \"$NAME\" 2>/dev/null; then\n\tERROR \"Instance $NAME already exists\"\n\texit 1\nfi\n\ncase \"$(limactl tmpl yq \"$FILE_HOST\" '.networks[].lima')\" in\n\"shared\")\n\tCHECKS[\"vmnet\"]=1\n\t;;\n\"user-v2\")\n\tCHECKS[\"port-forwards\"]=\"\"\n\tCHECKS[\"user-v2\"]=1\n\t;;\nesac\n\nif [[ -n ${CHECKS[\"port-forwards\"]} ]]; then\n\ttmpconfig=\"$HOME_HOST/lima-config-tmp\"\n\tmkdir -p \"${tmpconfig}\"\n\tdefer \"rm -rf \\\"$tmpconfig\\\"\"\n\ttmpfile=\"${tmpconfig}/${NAME}.yaml\"\n\tcp \"$FILE\" \"${tmpfile}\"\n\tFILE=\"${tmpfile}\"\n\tFILE_HOST=$FILE\n\tif [ \"${OS_HOST}\" = \"Msys\" ]; then\n\t\tFILE_HOST=\"$(cygpath -w \"$FILE\")\"\n\tfi\n\n\tINFO \"Setup port forwarding rules for testing in \\\"${FILE}\\\"\"\n\t\"${scriptdir}/test-port-forwarding.pl\" \"${FILE}\"\n\tINFO \"Validating \\\"$FILE_HOST\\\"\"\n\tlimactl validate \"$FILE_HOST\"\nfi\n\nINFO \"Make sure template embedding copies \\\"$FILE_HOST\\\" exactly\"\ndiff -u <(echo -n \"base: $FILE_HOST\" | limactl tmpl copy --embed - -) \"$FILE_HOST\"\n\nfunction diagnose() {\n\tNAME=\"$1\"\n\tset -x +e\n\ttail \"$HOME_HOST/.lima/${NAME}\"/*.log\n\tlimactl shell \"$NAME\" systemctl --no-pager status\n\tlimactl shell \"$NAME\" systemctl --no-pager\n\tmkdir -p failure-logs\n\tcp -pf \"$HOME_HOST/.lima/${NAME}\"/*.log failure-logs/\n\tlimactl shell \"$NAME\" sudo cat /var/log/cloud-init-output.log | tee failure-logs/cloud-init-output.log\n\tlimactl shell \"$NAME\" sh -c \"command -v journalctl >/dev/null && sudo journalctl --no-pager\" >failure-logs/journal.log\n\tset +x -e\n}\n\nexport ftp_proxy=http://localhost:2121\n\nINFO \"Creating \\\"$NAME\\\" from \\\"$FILE_HOST\\\"\"\ndefer \"limactl delete -f \\\"$NAME\\\"\"\n\nif [[ -n ${CHECKS[\"disk\"]} ]]; then\n\tif [[ -z \"$(limactl disk ls data --json 2>/dev/null)\" ]]; then\n\t\tdefer \"limactl disk delete data\"\n\t\tlimactl disk create data --size 10G\n\tfi\n\tif ! limactl disk ls | grep -q \"^swap\\s\"; then\n\t\tdefer \"limactl disk delete swap\"\n\t\tlimactl disk create swap --size 2G\n\tfi\nfi\n\nset -x\n# shellcheck disable=SC2086\n\"${LIMACTL_CREATE[@]}\" ${LIMACTL_CREATE_ARGS:-} \"$FILE_HOST\"\nset +x\n\nif [[ -n ${CHECKS[\"mount-path-with-spaces\"]} ]]; then\n\tmkdir -p \"/tmp/lima test dir with spaces\"\n\techo \"test file content\" >\"/tmp/lima test dir with spaces/test file\"\nfi\n\nINFO \"Starting \\\"$NAME\\\"\"\nset -x\nif ! limactl start \"$NAME\"; then\n\tERROR \"Failed to start \\\"$NAME\\\"\"\n\tdiagnose \"$NAME\"\n\texit 1\nfi\n\nlimactl shell \"$NAME\" uname -a\n\nlimactl shell \"$NAME\" cat /etc/os-release\nset +x\n\nINFO \"Testing that host home is not wiped out\"\n[ -e \"$HOME_HOST/.lima\" ]\n\nif [[ -n ${CHECKS[\"mount-path-with-spaces\"]} ]]; then\n\tINFO 'Testing that \"/tmp/lima test dir with spaces\" is not wiped out'\n\t[ \"$(cat \"/tmp/lima test dir with spaces/test file\")\" = \"test file content\" ]\n\t[ \"$(limactl shell \"$NAME\" cat \"/tmp/lima test dir with spaces/test file\")\" = \"test file content\" ]\nfi\n\nif [[ -n ${CHECKS[\"provision-data\"]} ]]; then\n\tINFO 'Testing that /etc/sysctl.d/99-inotify.conf was created successfully on provision'\n\tlimactl shell \"$NAME\" grep -q fs.inotify.max_user_watches /etc/sysctl.d/99-inotify.conf\nfi\n\nif [[ -n ${CHECKS[\"provision-yq\"]} ]]; then\n\tINFO 'Testing that /tmp/param-yq.json was created successfully on provision'\n\tlimactl shell \"$NAME\" grep -q '\"YQ\": \"yq\"' /tmp/param-yq.json\nfi\n\nif [[ -n ${CHECKS[\"param-env-variables\"]} ]]; then\n\tINFO 'Testing that PARAM env variables are exported to all types of provisioning scripts and probes'\n\tlimactl shell \"$NAME\" test -e /tmp/param-ansible\n\tlimactl shell \"$NAME\" test -e /tmp/param-boot\n\tlimactl shell \"$NAME\" test -e /tmp/param-dependency\n\tlimactl shell \"$NAME\" test -e /tmp/param-probe\n\tlimactl shell \"$NAME\" test -e /tmp/param-system\n\tlimactl shell \"$NAME\" test -e /tmp/param-user\nfi\n\nif [[ -n ${CHECKS[\"set-user\"]} ]]; then\n\tINFO 'Testing that user settings can be provided by lima.yaml'\n\tlimactl shell \"$NAME\" grep \"^john:x:4711:4711:John Doe:/home/john-john:/usr/bin/bash\" /etc/passwd\nfi\n\nif [[ -n ${CHECKS[\"proxy-settings\"]} ]]; then\n\tINFO \"Testing proxy settings are imported\"\n\tgot=$(limactl shell \"$NAME\" env | grep FTP_PROXY)\n\t# Expected: FTP_PROXY is set in addition to ftp_proxy, localhost is replaced\n\t# by the gateway address, and the value is set immediately without a restart\n\tgatewayIp=$(limactl shell \"$NAME\" ip route show 0.0.0.0/0 dev eth0 | cut -d\\  -f3)\n\texpected=\"FTP_PROXY=http://${gatewayIp}:2121\"\n\tINFO \"FTP_PROXY: expected=${expected} got=${got}\"\n\tif [ \"$got\" != \"$expected\" ]; then\n\t\tERROR \"proxy environment variable not set to correct value\"\n\t\texit 1\n\tfi\nfi\n\nINFO \"Testing limactl copy command\"\ntmpdir=\"$(mktemp -d \"${TMPDIR:-/tmp}\"/lima-test-templates.XXXXXX)\"\ndefer \"rm -rf \\\"$tmpdir\\\"\"\ntmpfile=\"$tmpdir/lima-hostname\"\nrm -f \"$tmpfile\"\ntmpfile_host=$tmpfile\nif [ \"${OS_HOST}\" = \"Msys\" ]; then\n\ttmpfile_host=\"$(cygpath -w \"$tmpfile\")\"\nfi\nlimactl cp \"$NAME\":/etc/hostname \"$tmpfile_host\"\nexpected=\"$(limactl shell \"$NAME\" cat /etc/hostname)\"\ngot=\"$(cat \"$tmpfile\")\"\nINFO \"/etc/hostname: expected=${expected}, got=${got}\"\nif [ \"$got\" != \"$expected\" ]; then\n\tERROR \"copy command did not fetch the file\"\n\texit 1\nfi\n\nINFO \"Testing limactl copy command with scp backend\"\ntmpfile_scp=\"$tmpdir/lima-hostname-scp\"\nrm -f \"$tmpfile_scp\"\ntmpfile_scp_host=$tmpfile_scp\nif [ \"${OS_HOST}\" = \"Msys\" ]; then\n\ttmpfile_scp_host=\"$(cygpath -w \"$tmpfile_scp\")\"\nfi\nlimactl cp --backend=scp \"$NAME\":/etc/hostname \"$tmpfile_scp_host\"\nexpected=\"$(limactl shell \"$NAME\" cat /etc/hostname)\"\ngot=\"$(cat \"$tmpfile_scp\")\"\nINFO \"/etc/hostname (scp): expected=${expected}, got=${got}\"\nif [ \"$got\" != \"$expected\" ]; then\n\tERROR \"copy command with scp backend did not fetch the file\"\n\texit 1\nfi\n\nif command -v rsync >/dev/null && limactl shell \"$NAME\" command -v rsync >/dev/null 2>&1; then\n\tINFO \"Testing limactl copy command with rsync backend\"\n\ttmpfile_rsync=\"$tmpdir/lima-hostname-rsync\"\n\trm -f \"$tmpfile_rsync\"\n\ttmpfile_rsync_host=$tmpfile_rsync\n\tif [ \"${OS_HOST}\" = \"Msys\" ]; then\n\t\ttmpfile_rsync_host=\"$(cygpath -w \"$tmpfile_rsync\")\"\n\tfi\n\tlimactl cp --backend=rsync \"$NAME\":/etc/hostname \"$tmpfile_rsync_host\"\n\texpected=\"$(limactl shell \"$NAME\" cat /etc/hostname)\"\n\tgot=\"$(cat \"$tmpfile_rsync\")\"\n\tINFO \"/etc/hostname (rsync): expected=${expected}, got=${got}\"\n\tif [ \"$got\" != \"$expected\" ]; then\n\t\tERROR \"copy command with rsync backend did not fetch the file\"\n\t\texit 1\n\tfi\n\n\tINFO \"Testing limactl copy command with rsync backend (verbose, recursive)\"\n\ttestdir=\"$tmpdir/test-rsync-dir\"\n\tmkdir -p \"$testdir\"\n\techo \"test content\" >\"$testdir/testfile.txt\"\n\tlimactl cp --backend=rsync -r -v \"$testdir\" \"$NAME\":/tmp/\n\tif ! limactl shell \"$NAME\" test -f /tmp/test-rsync-dir/testfile.txt; then\n\t\tERROR \"rsync recursive copy failed\"\n\t\texit 1\n\tfi\n\trsync_content=\"$(limactl shell \"$NAME\" cat /tmp/test-rsync-dir/testfile.txt)\"\n\tif [ \"$rsync_content\" != \"test content\" ]; then\n\t\tERROR \"rsync file content mismatch\"\n\t\texit 1\n\tfi\nelse\n\tINFO \"Skipping rsync backend test (rsync not available on host or guest)\"\nfi\n\nINFO \"Testing limactl command with escaped characters\"\nlimactl shell \"$NAME\" bash -c \"$(echo -e '\\n\\techo foo\\n\\techo bar')\"\n\nINFO \"Testing limactl command with quotes\"\nlimactl shell \"$NAME\" bash -c \"echo 'foo \\\"bar\\\"'\"\n\nif [[ -n ${CHECKS[\"systemd\"]} ]]; then\n\tset -x\n\tif ! limactl shell \"$NAME\" systemctl is-system-running --wait; then\n\t\tERROR '\"systemctl is-system-running\" failed'\n\t\tdiagnose \"$NAME\"\n\t\texit 1\n\tfi\n\tset +x\nfi\n\nif [[ -n ${CHECKS[\"mount-home\"]} ]]; then\n\t\"${scriptdir}\"/test-mount-home.sh \"$NAME\"\nfi\n\nif [[ -n ${CHECKS[\"ssh-over-vsock\"]} ]]; then\n\tif [[ \"$(limactl ls \"${NAME}\" --yq .vmType)\" == \"vz\" ]]; then\n\t\tINFO \"Testing SSH over vsock\"\n\t\tset -x\n\t\tlog_file=\"$HOME_HOST/.lima/${NAME}/ha.stdout.log\"\n\n\t\t# Helper function to check vsock events in the log file\n\t\t# $1: event_type to check for\n\t\tcheck_vsock_event() {\n\t\t\tlocal event_type=\"$1\"\n\t\t\tif jq -e --arg type \"$event_type\" 'select(.status.vsock.type == $type)' \"$log_file\" >/dev/null 2>&1; then\n\t\t\t\treturn 0\n\t\t\tfi\n\t\t\treturn 1\n\t\t}\n\n\t\tINFO \"Testing .ssh.overVsock=true configuration\"\n\t\tlimactl stop \"${NAME}\"\n\t\t# Detection of the SSH server on VSOCK may fail; however, a failing log indicates that controlling detection via the environment variable works as expected.\n\t\tlimactl start --set '.ssh.overVsock=true' \"${NAME}\"\n\t\tif ! check_vsock_event \"started\" && ! check_vsock_event \"failed\"; then\n\t\t\tset +x\n\t\t\tdiagnose \"${NAME}\"\n\t\t\tERROR \".ssh.overVsock=true did not enable vsock forwarder\"\n\t\t\texit 1\n\t\tfi\n\t\tINFO 'Testing .ssh.overVsock=null configuration'\n\t\tlimactl stop \"${NAME}\"\n\t\t# Detection of the SSH server on VSOCK may fail; however, a failing log indicates that controlling detection via the environment variable works as expected.\n\t\tlimactl start --set '.ssh.overVsock=null' \"${NAME}\"\n\t\tif ! check_vsock_event \"started\" && ! check_vsock_event \"failed\"; then\n\t\t\tset +x\n\t\t\tdiagnose \"${NAME}\"\n\t\t\tERROR \".ssh.overVsock=null did not enable vsock forwarder\"\n\t\t\texit 1\n\t\tfi\n\t\tINFO \"Testing .ssh.overVsock=false configuration\"\n\t\tlimactl stop \"${NAME}\"\n\t\tlimactl start --set '.ssh.overVsock=false' \"${NAME}\"\n\t\tif ! check_vsock_event \"skipped\"; then\n\t\t\tset +x\n\t\t\tdiagnose \"${NAME}\"\n\t\t\tERROR \".ssh.overVsock=false did not disable vsock forwarder\"\n\t\t\texit 1\n\t\tfi\n\t\tset +x\n\tfi\nfi\n\n# Use GHCR and ECR to avoid hitting Docker Hub rate limit\nnginx_image=\"ghcr.io/stargz-containers/nginx:1.19-alpine-org\"\nalpine_image=\"ghcr.io/containerd/alpine:3.14.0\"\ncoredns_image=\"public.ecr.aws/eks-distro/coredns/coredns:v1.12.2-eks-1-31-latest\"\n\nif [[ -n ${CHECKS[\"container-engine\"]} ]]; then\n\tsudo=\"\"\n\t# Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future.\n\tif [[ \"$(limactl ls \"${NAME}\" --yq .vmType)\" == \"wsl2\" ]]; then\n\t\tsudo=\"sudo\"\n\tfi\n\tINFO \"Run a nginx container with port forwarding 127.0.0.1:8080\"\n\tset -x\n\tif ! limactl shell \"$NAME\" $sudo $CONTAINER_ENGINE info; then\n\t\tlimactl shell \"$NAME\" cat /var/log/cloud-init-output.log\n\t\tERROR \"\\\"${CONTAINER_ENGINE} info\\\" failed\"\n\t\texit 1\n\tfi\n\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE pull --quiet ${nginx_image}\n\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE run -d --name nginx -p 127.0.0.1:8080:80 ${nginx_image}\n\n\ttimeout 3m bash -euxc \"until curl -f --retry 30 --retry-connrefused http://127.0.0.1:8080; do sleep 3; done\"\n\n\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE rm -f nginx\n\n\tif [ \"${OS_HOST}\" != \"Msys\" ]; then\n\t\t# TODO: support UDP on Windows\n\t\tINFO \"Run a coredns container with port forwarding 127.0.0.1:10053/udp\"\n\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE pull --quiet ${coredns_image}\n\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE run -d --name coredns -p 127.0.0.1:10053:53/udp ${coredns_image}\n\t\tdig @127.0.0.1 -p 10053 lima-vm.io\n\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE rm -f coredns\n\tfi\n\n\tset +x\n\tif [[ -n ${CHECKS[\"mount-home\"]} ]]; then\n\t\thometmp=\"$HOME_HOST/lima-container-engine-test-tmp\"\n\t\thometmpguest=\"$HOME_GUEST/lima-container-engine-test-tmp\"\n\t\t# test for https://github.com/lima-vm/lima/issues/187\n\t\tINFO \"Testing home bind mount (\\\"$hometmp\\\")\"\n\t\trm -rf \"$hometmp\"\n\t\tmkdir -p \"$hometmp\"\n\t\tdefer \"rm -rf \\\"$hometmp\\\"\"\n\t\tset -x\n\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE pull --quiet ${alpine_image}\n\t\techo \"random-content-${RANDOM}\" >\"$hometmp/random\"\n\t\texpected=\"$(cat \"$hometmp/random\")\"\n\t\tgot=\"$(limactl shell \"$NAME\" $sudo $CONTAINER_ENGINE run --rm -v \"$hometmpguest/random\":/mnt/foo ${alpine_image} cat /mnt/foo)\"\n\t\tINFO \"$hometmp/random: expected=${expected}, got=${got}\"\n\t\tif [ \"$got\" != \"$expected\" ]; then\n\t\t\tERROR \"Home directory is not shared?\"\n\t\t\texit 1\n\t\tfi\n\t\tset +x\n\tfi\nfi\n\nif [[ -n ${CHECKS[\"port-forwards\"]} ]]; then\n\tPORT_FORWARDING_CONNECTION_TIMEOUT=1\n\tINFO \"Testing port forwarding rules using netcat and socat with connection timeout ${PORT_FORWARDING_CONNECTION_TIMEOUT}s\"\n\tset -x\n\tif [[ ${NAME} == \"alpine\"* ]]; then\n\t\tlimactl shell \"${NAME}\" sudo apk add socat\n\tfi\n\tif [[ ${NAME} == \"archlinux\" ]]; then\n\t\tlimactl shell \"${NAME}\" sudo pacman -Syu --noconfirm openbsd-netcat socat\n\tfi\n\tif [[ ${NAME} == \"debian\" || ${NAME} == \"default\" || ${NAME} == \"docker\" || ${NAME} == \"test-misc\" ]]; then\n\t\tlimactl shell \"${NAME}\" sudo apt-get install -y netcat-openbsd socat\n\tfi\n\tif [[ ${NAME} == \"fedora\" || ${NAME} == \"wsl2\" ]]; then\n\t\tlimactl shell \"${NAME}\" sudo dnf install -y nc socat\n\tfi\n\tif [[ ${NAME} == \"opensuse\" ]]; then\n\t\tlimactl shell \"${NAME}\" sudo zypper in -y netcat-openbsd socat\n\tfi\n\tif limactl shell \"${NAME}\" command -v dnf; then\n\t\tlimactl shell \"${NAME}\" sudo dnf install -y nc socat\n\tfi\n\tif \"${scriptdir}/test-port-forwarding.pl\" \"${NAME}\" socat $PORT_FORWARDING_CONNECTION_TIMEOUT; then\n\t\tINFO \"Port forwarding rules work\"\n\telse\n\t\tERROR \"Port forwarding rules do not work with socat\"\n\t\tdiagnose \"$NAME\"\n\t\texit 1\n\tfi\n\n\tif [[ -n ${CHECKS[\"container-engine\"]} || ${NAME} == \"alpine\"* ]]; then\n\t\tINFO \"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)\"\n\t\tif [ \"$(uname)\" = \"Darwin\" ]; then\n\t\t\t# macOS runners seem to use `localhost` as the hostname, so the perl lookup just returns `127.0.0.1`\n\t\t\thostip=$(system_profiler SPNetworkDataType -json | jq -r 'first(.SPNetworkDataType[] | select(.ip_address) | .ip_address) | first')\n\t\telse\n\t\t\thostip=$(perl -MSocket -MSys::Hostname -E 'say inet_ntoa(scalar gethostbyname(hostname()))')\n\t\tfi\n\t\tif [ -n \"${hostip}\" ]; then\n\t\t\tsudo=\"\"\n\t\t\tif [[ ${NAME} == \"alpine\"* ]]; then\n\t\t\t\tarch=$(limactl info | jq -r .defaultTemplate.arch)\n\t\t\t\tnerdctl=$(limactl info | jq -r \".defaultTemplate.containerd.archives[] | select(.arch==\\\"$arch\\\").location\")\n\t\t\t\tcurl -Lso nerdctl-full.tgz \"${nerdctl}\"\n\t\t\t\tlimactl shell \"$NAME\" sudo apk add containerd\n\t\t\t\tlimactl shell \"$NAME\" sudo rc-service containerd start\n\t\t\t\tlimactl shell \"$NAME\" sudo tar xzf \"${PWD}/nerdctl-full.tgz\" -C /usr/local\n\t\t\t\trm nerdctl-full.tgz\n\t\t\t\tsudo=\"sudo\"\n\t\t\tfi\n\t\t\t# Currently WSL2 machines only support privileged engine. This requirement might be lifted in the future.\n\t\t\tif [[ \"$(limactl ls \"${NAME}\" --yq .vmType)\" == \"wsl2\" ]]; then\n\t\t\t\tsudo=\"sudo\"\n\t\t\tfi\n\t\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE info\n\t\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE pull --quiet ${nginx_image}\n\n\t\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE run -d --name nginx -p 8888:80 ${nginx_image}\n\t\t\ttimeout 3m bash -euxc \"until curl -f --retry 30 --retry-connrefused http://${hostip}:8888; do sleep 3; done\"\n\t\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE rm -f nginx\n\n\t\t\tif [ \"$(uname)\" = \"Darwin\" ]; then\n\t\t\t\t# Only macOS can bind to port 80 without root\n\t\t\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE run -d --name nginx -p 127.0.0.1:80:80 ${nginx_image}\n\t\t\t\ttimeout 3m bash -euxc \"until curl -f --retry 30 --retry-connrefused http://localhost:80; do sleep 3; done\"\n\t\t\t\tlimactl shell \"$NAME\" $sudo $CONTAINER_ENGINE rm -f nginx\n\t\t\tfi\n\t\tfi\n\t\tif [[ ${NAME} != \"alpine\"* ]] && command -v w3m >/dev/null; then\n\t\t\tINFO \"Testing https://github.com/lima-vm/lima/issues/3685 ([gRPC portfwd] client connection is not closed immediately when server closed the connection)\"\n\t\t\t# Skip the test on Alpine, as systemd-run is missing\n\t\t\t# Skip the test on WSL2, as port forwarding is half broken https://github.com/lima-vm/lima/pull/3686#issuecomment-3034842616\n\t\t\tlimactl shell \"$NAME\" systemd-run --user python3 -m http.server 3685\n\t\t\t# curl is not enough to reproduce https://github.com/lima-vm/lima/issues/3685\n\t\t\t# `w3m -dump` exits with status code 0 even on \"Can't load\" error.\n\t\t\ttimeout 30s bash -euxc \"until w3m -dump http://localhost:3685 | grep -v \\\"w3m: Can't load\\\"; do sleep 3; done\"\n\t\tfi\n\tfi\n\tset +x\nfi\n\nif [[ -n ${CHECKS[\"vmnet\"]} ]]; then\n\tINFO \"Testing vmnet functionality\"\n\tguestip=\"$(limactl shell \"$NAME\" ip -4 -j addr show dev lima0 | jq -r '.[0].addr_info[0].local')\"\n\tINFO \"Pinging the guest IP ${guestip}\"\n\tset -x\n\tping -c 3 \"$guestip\"\n\tset +x\n\tINFO \"Benchmarking with iperf3\"\n\tset -x\n\tlimactl shell \"$NAME\" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y iperf3\n\tlimactl shell \"$NAME\" iperf3 -s -1 -D\n\t${IPERF3} -c \"$guestip\"\n\tset +x\n\t# 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)\nfi\n\nif [[ -n ${CHECKS[\"disk\"]} ]]; then\n\tINFO \"Testing disk is attached\"\n\tset -x\n\tif ! limactl shell \"$NAME\" lsblk --output NAME,MOUNTPOINT | grep -q \"/mnt/lima-data\"; then\n\t\tERROR \"Disk is not mounted\"\n\t\texit 1\n\tfi\n\tif ! limactl shell \"$NAME\" lsblk --output NAME,MOUNTPOINT | grep -q \"\\[SWAP\\]\"; then\n\t\tERROR \"Disk is not mounted\"\n\t\texit 1\n\tfi\n\tset +x\nfi\n\nif [[ -n ${CHECKS[\"restart\"]} ]]; then\n\tINFO \"Create file in the guest home directory and verify that it still exists after a restart\"\n\t# shellcheck disable=SC2016\n\tlimactl shell \"$NAME\" sh -c 'touch $HOME/sweet-home'\n\tif [[ -n ${CHECKS[\"disk\"]} ]]; then\n\t\tINFO \"Create file in disk and verify that it still exists when it is reattached\"\n\t\tlimactl shell \"$NAME\" sudo sh -c 'touch /mnt/lima-data/sweet-disk'\n\tfi\n\n\tINFO \"Stopping \\\"$NAME\\\"\"\n\tlimactl stop \"$NAME\"\n\tsleep 3\n\n\tif [[ -n ${CHECKS[\"disk\"]} ]]; then\n\t\tINFO \"Resize disk and verify that partition and fs size are increased\"\n\t\tlimactl disk resize data --size 11G\n\tfi\n\n\texport ftp_proxy=my.proxy:8021\n\tINFO \"Restarting \\\"$NAME\\\"\"\n\tif ! limactl start \"$NAME\"; then\n\t\tERROR \"Failed to start \\\"$NAME\\\"\"\n\t\tdiagnose \"$NAME\"\n\t\texit 1\n\tfi\n\n\tINFO \"Make sure proxy setting is updated\"\n\tgot=$(limactl shell \"$NAME\" env | grep FTP_PROXY)\n\texpected=\"FTP_PROXY=my.proxy:8021\"\n\tINFO \"FTP_PROXY: expected=${expected} got=${got}\"\n\tif [ \"$got\" != \"$expected\" ]; then\n\t\tERROR \"proxy environment variable not set to correct value\"\n\t\texit 1\n\tfi\n\n\t# shellcheck disable=SC2016\n\tif ! limactl shell \"$NAME\" sh -c 'test -f $HOME/sweet-home'; then\n\t\tERROR \"Guest home directory does not persist across restarts\"\n\t\texit 1\n\tfi\n\n\tif [[ -n ${CHECKS[\"disk\"]} ]]; then\n\t\tif ! limactl shell \"$NAME\" sh -c 'test -f /mnt/lima-data/sweet-disk'; then\n\t\t\tERROR \"Disk does not persist across restarts\"\n\t\t\texit 1\n\t\tfi\n\t\tif ! limactl shell \"$NAME\" sh -c 'df -h /mnt/lima-data/ --output=size | grep -q 11G'; then\n\t\t\tERROR \"Disk FS does not resized after restart\"\n\t\t\texit 1\n\t\tfi\n\tfi\nfi\n\nif [[ -n ${CHECKS[\"user-v2\"]} ]]; then\n\tINFO \"Testing user-v2 network\"\n\tsecondvm=\"$NAME-1\"\n\t\"${LIMACTL_CREATE[@]}\" --set \".additionalDisks=null\" \"$FILE_HOST\" --name \"$secondvm\"\n\tif ! limactl start \"$secondvm\"; then\n\t\tERROR \"Failed to start \\\"$secondvm\\\"\"\n\t\tdiagnose \"$secondvm\"\n\t\texit 1\n\tfi\n\tsecondvmDNS=\"lima-$secondvm.internal\"\n\tINFO \"DNS of $secondvm is $secondvmDNS\"\n\tset -x\n\tif ! limactl shell \"$NAME\" ping -c 1 \"$secondvmDNS\"; then\n\t\tERROR \"Failed to do vm->vm communication via user-v2\"\n\t\tINFO \"Stopping \\\"$secondvm\\\"\"\n\t\tlimactl stop \"$secondvm\"\n\t\tINFO \"Deleting \\\"$secondvm\\\"\"\n\t\tlimactl delete \"$secondvm\"\n\t\texit 1\n\tfi\n\tINFO \"Stopping \\\"$secondvm\\\"\"\n\tlimactl stop \"$secondvm\"\n\tINFO \"Deleting \\\"$secondvm\\\"\"\n\tlimactl delete \"$secondvm\"\n\tset +x\nfi\nif [[ -n ${CHECKS[\"snapshot-online\"]} ]]; then\n\tINFO \"Testing online snapshots\"\n\tlimactl shell \"$NAME\" sh -c 'echo foo > /tmp/test'\n\tlimactl snapshot create \"$NAME\" --tag snap1\n\tgot=$(limactl snapshot list \"$NAME\" --quiet)\n\texpected=\"snap1\"\n\tINFO \"snapshot list: expected=${expected} got=${got}\"\n\tif [ \"$got\" != \"$expected\" ]; then\n\t\tERROR \"snapshot list did not return expected value\"\n\t\texit 1\n\tfi\n\tlimactl shell \"$NAME\" sh -c 'echo bar > /tmp/test'\n\tlimactl snapshot apply \"$NAME\" --tag snap1\n\tgot=$(limactl shell \"$NAME\" cat /tmp/test)\n\texpected=\"foo\"\n\tINFO \"snapshot apply: expected=${expected} got=${got}\"\n\tif [ \"$got\" != \"$expected\" ]; then\n\t\tERROR \"snapshot apply did not restore snapshot\"\n\t\texit 1\n\tfi\n\tlimactl snapshot delete \"$NAME\" --tag snap1\n\tlimactl shell \"$NAME\" rm /tmp/test\nfi\nif [[ -n ${CHECKS[\"snapshot-offline\"]} ]]; then\n\tINFO \"Testing offline snapshots\"\n\tlimactl stop \"$NAME\"\n\tsleep 3\n\tlimactl snapshot create \"$NAME\" --tag snap2\n\tgot=$(limactl snapshot list \"$NAME\" --quiet)\n\texpected=\"snap2\"\n\tINFO \"snapshot list: expected=${expected} got=${got}\"\n\tif [ \"$got\" != \"$expected\" ]; then\n\t\tERROR \"snapshot list did not return expected value\"\n\t\texit 1\n\tfi\n\tlimactl snapshot apply \"$NAME\" --tag snap2\n\tlimactl snapshot delete \"$NAME\" --tag snap2\n\tlimactl start \"$NAME\"\nfi\nif [[ -n ${CHECKS[\"clone\"]} ]]; then\n\tINFO \"Testing cloning\"\n\tlimactl stop \"$NAME\"\n\tsleep 3\n\t# [hostagent] could not attach disk \\\"data\\\", in use by instance \\\"test-misc-clone\\\"\n\tlimactl clone --set '.additionalDisks = null' \"$NAME\" \"${NAME}-clone\"\n\tlimactl start \"${NAME}-clone\"\n\t[ \"$(limactl shell \"${NAME}-clone\" hostname)\" = \"lima-${NAME}-clone\" ]\n\tlimactl start \"$NAME\"\nfi\n\nif [[ $NAME == \"fedora\" && \"$(limactl ls \"${NAME}\" --yq .vmType)\" == \"vz\" ]]; then\n\t\"${scriptdir}\"/test-selinux.sh \"$NAME\"\nfi\n\nINFO \"Stopping \\\"$NAME\\\"\"\nlimactl stop \"$NAME\"\nsleep 3\n\nINFO \"Deleting \\\"$NAME\\\"\"\nlimactl delete \"$NAME\"\n\nif [[ -n ${CHECKS[\"mount-path-with-spaces\"]} ]]; then\n\trm -rf \"/tmp/lima test dir with spaces\"\nfi\n\nif [[ -n ${CHECKS[\"static-port-forwards\"]} ]]; then\n\tINFO \"Testing static port forwarding functionality\"\n\t\"${scriptdir}/test-plain-static-port-forward.sh\" \"$NAME\"\n\t\"${scriptdir}/test-nonplain-static-port-forward.sh\" \"$NAME\"\n\tINFO \"All static port forwarding tests passed!\"\nfi\n"
  },
  {
    "path": "hack/test-upgrade.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\nscriptdir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n# shellcheck source=common.inc.sh\nsource \"${scriptdir}/common.inc.sh\"\ncd \"${scriptdir}/..\"\n\nif [ \"$#\" -ne 2 ]; then\n\tERROR \"Usage: $0 OLDVER NEWVER\"\n\texit 1\nfi\n\nOLDVER=\"$1\"\nNEWVER=\"$2\"\n\nPREFIX=\"/usr/local\"\nfunction install_lima() {\n\tver=\"$1\"\n\tgit checkout \"${ver}\"\n\tmake clean\n\tmake\n\tif [ -w \"${PREFIX}/bin\" ] && [ -w \"${PREFIX}/share\" ] && [ -w \"${PREFIX}/libexec\" ]; then\n\t\tmake install\n\telse\n\t\tsudo make install\n\tfi\n}\n\nfunction install_lima_binary() {\n\tver=\"$1\"\n\ttar=\"tar\"\n\tif [ ! -w \"${PREFIX}/bin\" ] || [ ! -w \"${PREFIX}/share\" ] || [ ! -w \"${PREFIX}/libexec\" ]; then\n\t\ttar=\"sudo ${tar}\"\n\tfi\n\tcurl -fsSL \"https://github.com/lima-vm/lima/releases/download/${ver}/lima-${ver:1}-$(uname -s)-$(uname -m).tar.gz\" | ${tar} Cxzvm \"${PREFIX}\"\n}\n\nfunction uninstall_lima() {\n\tfiles=\"${PREFIX}/bin/lima ${PREFIX}/bin/limactl ${PREFIX}/share/lima ${PREFIX}/share/doc/lima ${PREFIX}/libexec/lima\"\n\tif [ -w \"${PREFIX}/bin\" ] && [ -w \"${PREFIX}/share\" ] && [ -w \"${PREFIX}/libexec\" ]; then\n\t\t# shellcheck disable=SC2086\n\t\trm -rf $files\n\telse\n\t\t# shellcheck disable=SC2086\n\t\tsudo rm -rf $files\n\tfi\n}\n\nfunction show_lima_log() {\n\ttail -n 100 ~/.lima/\"${LIMA_INSTANCE}\"/*.log || true\n\tmkdir -p failure-logs\n\tcp -pf ~/.lima/\"${LIMA_INSTANCE}\"/*.log failure-logs/ || true\n\tlimactl shell \"${LIMA_INSTANCE}\" sudo cat /var/log/cloud-init-output.log | tee failure-logs/cloud-init-output.log || true\n}\n\nINFO \"Uninstalling lima\"\nuninstall_lima\n\nINFO \"Installing the old Lima ${OLDVER}\"\ninstall_lima_binary \"${OLDVER}\" || install_lima \"${OLDVER}\"\n\nexport LIMA_INSTANCE=\"test-upgrade\"\n\nINFO \"Creating an instance \\\"${LIMA_INSTANCE}\\\" with the old Lima\"\ndefer \"show_lima_log;limactl delete -f \\\"${LIMA_INSTANCE}\\\"\"\nlimactl start --tty=false --name=\"${LIMA_INSTANCE}\" template://ubuntu-lts || (\n\tshow_lima_log\n\texit 1\n)\nlima nerdctl info\n\nimage_name=\"lima-test-upgrade-containerd-${RANDOM}\"\nimage_context=\"${HOME}/${image_name}\"\nINFO \"Building containerd image \\\"${image_name}\\\" from \\\"${image_context}\\\"\"\ndefer \"rm -rf \\\"${image_context}\\\"\"\nmkdir -p \"${image_context}\"\ncat <<EOF >\"${image_context}\"/Dockerfile\n# Use GHCR to avoid hitting Docker Hub rate limit\nFROM ghcr.io/containerd/alpine:3.14.0\nCMD [\"echo\", \"Built with Lima ${OLDVER}\"]\nEOF\nlima nerdctl build -t \"${image_name}\" \"${image_context}\"\nlima nerdctl run --rm \"${image_name}\"\n\nINFO \"Stopping the instance\"\nlimactl stop \"${LIMA_INSTANCE}\"\n\nINFO \"==============================================================================\"\n\nINFO \"Installing the new Lima ${NEWVER}\"\ninstall_lima \"${NEWVER}\"\n\nINFO \"Editing the instance to specify vm-type as qemu explicitly\"\nlimactl edit --vm-type=qemu \"${LIMA_INSTANCE}\"\n\nINFO \"Restarting the instance\"\nlimactl start --tty=false --vm-type=qemu \"${LIMA_INSTANCE}\" || show_lima_log\nlima nerdctl info\n\nINFO \"Confirming that the host filesystem is still mounted\"\n\"${scriptdir}\"/test-mount-home.sh \"${LIMA_INSTANCE}\"\n\nINFO \"Confirming that the image \\\"${image_name}\\\" still exists\"\nlima nerdctl run --rm \"${image_name}\"\n"
  },
  {
    "path": "hack/toolexec-for-codesign.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This script is used to wrap the compiler and linker commands in the build\n# process. It captures the output of the command and logs it to a file.\n# The script's primary purpose is codesigning the output of the linker command\n# with the entitlements file if it exists.\n# If the OS is macOS, the result of the command is 0, the entitlements file\n# exists, and codesign is available, sign the output of the linker command with\n# the entitlements file.\n#\n# Usage:\n#   go build -toolexec hack/toolexec-to-codesign.sh\n\nrepository_root=\"$(dirname \"$(dirname \"$0\")\")\"\nlogfile=\"${repository_root}/.toolexec-to-codesign.log\"\n\necho $$: cmd: \"$@\" >>\"${logfile}\"\n\noutput=\"$(\"$@\")\"\nresult=$?\n\necho $$: output: \"${output}\" >>\"${logfile}\"\n\nentitlements=\"${repository_root}/vz.entitlements\"\n\n# If the OS is macOS, the result of the command is 0, the entitlements file\n# exists, and codesign is available, sign the output of the linker command.\nif OS=$(uname -s) && [ \"${OS}\" = \"Darwin\" ] && [ \"${result}\" -eq 0 ] && [ -f \"${entitlements}\" ] && command -v codesign >/dev/null 2>&1; then\n\t# Check if the command is a linker command.\n\tcase \"$1\" in\n\t*link)\n\t\tshift\n\t\t# Find a parameter that is a output file.\n\t\twhile [ $# -gt 1 ]; do\n\t\t\tcase \"$1\" in\n\t\t\t-o)\n\t\t\t\t# If the output file is a executable, sign it with the entitlements file.\n\t\t\t\tif [ -x \"$2\" ]; then\n\t\t\t\t\tcodesign_output=\"$(codesign -v --entitlements \"${entitlements}\" -s - \"$2\" 2>&1)\"\n\t\t\t\t\techo \"$$: ${codesign_output}\" >>\"${logfile}\"\n\t\t\t\tfi\n\t\t\t\tbreak\n\t\t\t\t;;\n\t\t\t*) shift ;;\n\t\t\tesac\n\t\tdone\n\t\t;;\n\t*) ;;\n\tesac\nfi\n\n# Print the output of the command and exit with the result of the command.\necho \"${output}\"\nexit \"${result}\"\n"
  },
  {
    "path": "hack/tools/go.mod",
    "content": "module tools\n\ngo 1.25.0\n\n// Should be in sync with pinversion.go\ntool (\n\tgithub.com/containerd/ltag\n\tgithub.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker\n\tgithub.com/golangci/golangci-lint/v2/cmd/golangci-lint\n\tgithub.com/jandubois/nobin\n\tgithub.com/yoheimuta/protolint/cmd/protolint\n\tgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc\n\tgoogle.golang.org/protobuf/cmd/protoc-gen-go\n\tmvdan.cc/sh/v3/cmd/shfmt\n)\n\nrequire (\n\tgithub.com/containerd/ltag v0.3.0\n\tgithub.com/golangci/golangci-lint/v2 v2.11.3\n\tgithub.com/jandubois/nobin v0.8.0\n\tgithub.com/yoheimuta/protolint v0.56.4\n\tgoogle.golang.org/grpc v1.79.3\n\tgoogle.golang.org/protobuf v1.36.11\n\tmvdan.cc/sh/v3 v3.13.0\n)\n\nrequire (\n\t4d63.com/gocheckcompilerdirectives v1.3.0 // indirect\n\t4d63.com/gochecknoglobals v0.2.2 // indirect\n\tcodeberg.org/chavacava/garif v0.2.0 // indirect\n\tcodeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect\n\tdev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect\n\tdev.gaijin.team/go/golib v0.6.0 // indirect\n\tgithub.com/4meepo/tagalign v1.4.3 // indirect\n\tgithub.com/Abirdcfly/dupword v0.1.7 // indirect\n\tgithub.com/AdminBenni/iota-mixing v1.0.0 // indirect\n\tgithub.com/AlwxSin/noinlineerr v1.0.5 // indirect\n\tgithub.com/Antonboom/errname v1.1.1 // indirect\n\tgithub.com/Antonboom/nilnil v1.1.1 // indirect\n\tgithub.com/Antonboom/testifylint v1.6.4 // indirect\n\tgithub.com/BurntSushi/toml v1.6.0 // indirect\n\tgithub.com/Djarvur/go-err113 v0.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/MirrexOne/unqueryvet v1.5.4 // indirect\n\tgithub.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect\n\tgithub.com/alecthomas/chroma/v2 v2.23.1 // indirect\n\tgithub.com/alecthomas/go-check-sumtype v0.3.1 // indirect\n\tgithub.com/alexkohler/nakedret/v2 v2.0.6 // indirect\n\tgithub.com/alexkohler/prealloc v1.1.0 // indirect\n\tgithub.com/alfatraining/structtag v1.0.0 // indirect\n\tgithub.com/alingse/asasalint v0.0.11 // indirect\n\tgithub.com/alingse/nilnesserr v0.2.0 // indirect\n\tgithub.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect\n\tgithub.com/ashanbrown/makezero/v2 v2.1.0 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/beorn7/perks v1.0.1 // indirect\n\tgithub.com/bkielbasa/cyclop v1.2.3 // indirect\n\tgithub.com/blizzy78/varnamelen v0.8.0 // indirect\n\tgithub.com/bmatcuk/doublestar/v4 v4.10.0 // indirect\n\tgithub.com/bombsimon/wsl/v4 v4.7.0 // indirect\n\tgithub.com/bombsimon/wsl/v5 v5.6.0 // indirect\n\tgithub.com/breml/bidichk v0.3.3 // indirect\n\tgithub.com/breml/errchkjson v0.4.1 // indirect\n\tgithub.com/butuzov/ireturn v0.4.0 // indirect\n\tgithub.com/butuzov/mirror v1.3.0 // indirect\n\tgithub.com/catenacyber/perfsprint v0.10.1 // indirect\n\tgithub.com/ccojocar/zxcvbn-go v1.0.4 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/charithe/durationcheck v0.0.11 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect\n\tgithub.com/charmbracelet/lipgloss v1.1.0 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.10.1 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect\n\tgithub.com/charmbracelet/x/term v0.2.1 // indirect\n\tgithub.com/chavacava/garif v0.1.0 // indirect\n\tgithub.com/ckaznocha/intrange v0.3.1 // indirect\n\tgithub.com/curioswitch/go-reassign v0.3.0 // indirect\n\tgithub.com/daixiang0/gci v0.13.7 // indirect\n\tgithub.com/dave/dst v0.27.3 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/denis-tingaikin/go-header v0.5.0 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/editorconfig-checker/editorconfig-checker/v3 v3.6.1 // indirect\n\tgithub.com/editorconfig/editorconfig-core-go/v2 v2.6.4 // indirect\n\tgithub.com/ettle/strcase v0.2.0 // indirect\n\tgithub.com/fatih/color v1.18.0 // indirect\n\tgithub.com/fatih/structtag v1.2.0 // indirect\n\tgithub.com/firefart/nonamedreturns v1.0.6 // indirect\n\tgithub.com/fsnotify/fsnotify v1.5.4 // indirect\n\tgithub.com/fzipp/gocyclo v0.6.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.12 // indirect\n\tgithub.com/gertd/go-pluralize v0.2.1 // indirect\n\tgithub.com/ghostiam/protogetter v0.3.20 // indirect\n\tgithub.com/go-critic/go-critic v0.14.3 // indirect\n\tgithub.com/go-toolsmith/astcast v1.1.0 // indirect\n\tgithub.com/go-toolsmith/astcopy v1.1.0 // indirect\n\tgithub.com/go-toolsmith/astequal v1.2.0 // indirect\n\tgithub.com/go-toolsmith/astfmt v1.1.0 // indirect\n\tgithub.com/go-toolsmith/astp v1.1.0 // indirect\n\tgithub.com/go-toolsmith/strparse v1.1.0 // indirect\n\tgithub.com/go-toolsmith/typep v1.1.0 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/go-xmlfmt/xmlfmt v1.1.3 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/godoc-lint/godoc-lint v0.11.2 // indirect\n\tgithub.com/gofrs/flock v0.13.0 // indirect\n\tgithub.com/golang/protobuf v1.5.4 // indirect\n\tgithub.com/golangci/asciicheck v0.5.0 // indirect\n\tgithub.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect\n\tgithub.com/golangci/go-printf-func-name v0.1.1 // indirect\n\tgithub.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect\n\tgithub.com/golangci/golines v0.15.0 // indirect\n\tgithub.com/golangci/misspell v0.8.0 // indirect\n\tgithub.com/golangci/plugin-module-register v0.1.2 // indirect\n\tgithub.com/golangci/revgrep v0.8.0 // indirect\n\tgithub.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect\n\tgithub.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/renameio/v2 v2.0.2 // indirect\n\tgithub.com/gordonklaus/ineffassign v0.2.0 // indirect\n\tgithub.com/gostaticanalysis/analysisutil v0.7.1 // indirect\n\tgithub.com/gostaticanalysis/comment v1.5.0 // indirect\n\tgithub.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect\n\tgithub.com/gostaticanalysis/nilerr v0.1.2 // indirect\n\tgithub.com/hashicorp/go-hclog v1.6.3 // indirect\n\tgithub.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect\n\tgithub.com/hashicorp/go-plugin v1.6.3 // indirect\n\tgithub.com/hashicorp/go-version v1.8.0 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/hashicorp/hcl v1.0.0 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/hexops/gotextdiff v1.0.3 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jgautheron/goconst v1.8.2 // indirect\n\tgithub.com/jingyugao/rowserrcheck v1.1.1 // indirect\n\tgithub.com/jjti/go-spancheck v0.6.5 // indirect\n\tgithub.com/julz/importas v0.2.0 // indirect\n\tgithub.com/karamaru-alpha/copyloopvar v1.2.2 // indirect\n\tgithub.com/kisielk/errcheck v1.10.0 // indirect\n\tgithub.com/kkHAIKE/contextcheck v1.1.6 // indirect\n\tgithub.com/kulti/thelper v0.7.1 // indirect\n\tgithub.com/kunwardeep/paralleltest v1.0.15 // indirect\n\tgithub.com/lasiar/canonicalheader v1.1.2 // indirect\n\tgithub.com/ldez/exptostd v0.4.5 // indirect\n\tgithub.com/ldez/gomoddirectives v0.8.0 // indirect\n\tgithub.com/ldez/grignotin v0.10.1 // indirect\n\tgithub.com/ldez/structtags v0.6.1 // indirect\n\tgithub.com/ldez/tagliatelle v0.7.2 // indirect\n\tgithub.com/ldez/usetesting v0.5.0 // indirect\n\tgithub.com/leonklingele/grouper v1.1.2 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/macabu/inamedparam v0.2.0 // indirect\n\tgithub.com/magiconair/properties v1.8.6 // indirect\n\tgithub.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect\n\tgithub.com/manuelarte/funcorder v0.5.0 // indirect\n\tgithub.com/maratori/testableexamples v1.0.1 // indirect\n\tgithub.com/maratori/testpackage v1.1.2 // indirect\n\tgithub.com/matoous/godox v1.1.0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect\n\tgithub.com/mgechev/revive v1.15.0 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/mapstructure v1.5.0 // indirect\n\tgithub.com/moricho/tparallel v0.3.2 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/nakabonne/nestif v0.3.1 // indirect\n\tgithub.com/nishanths/exhaustive v0.12.0 // indirect\n\tgithub.com/nishanths/predeclared v0.2.2 // indirect\n\tgithub.com/nunnatsa/ginkgolinter v0.23.0 // indirect\n\tgithub.com/oklog/run v1.2.0 // indirect\n\tgithub.com/pelletier/go-toml v1.9.5 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/prometheus/client_golang v1.12.1 // indirect\n\tgithub.com/prometheus/client_model v0.2.0 // indirect\n\tgithub.com/prometheus/common v0.32.1 // indirect\n\tgithub.com/prometheus/procfs v0.7.3 // indirect\n\tgithub.com/quasilyte/go-ruleguard v0.4.5 // indirect\n\tgithub.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect\n\tgithub.com/quasilyte/gogrep v0.5.0 // indirect\n\tgithub.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect\n\tgithub.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect\n\tgithub.com/raeperd/recvcheck v0.2.0 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/ryancurrah/gomodguard v1.4.1 // indirect\n\tgithub.com/ryanrolds/sqlclosecheck v0.5.1 // indirect\n\tgithub.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect\n\tgithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect\n\tgithub.com/sashamelentyev/interfacebloat v1.1.0 // indirect\n\tgithub.com/sashamelentyev/usestdlibvars v1.29.0 // indirect\n\tgithub.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/sivchari/containedctx v1.0.3 // indirect\n\tgithub.com/sonatard/noctx v0.5.0 // indirect\n\tgithub.com/sourcegraph/go-diff v0.7.0 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.5.0 // indirect\n\tgithub.com/spf13/cobra v1.10.2 // indirect\n\tgithub.com/spf13/jwalterweatherman v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.12.0 // indirect\n\tgithub.com/ssgreg/nlreturn/v2 v2.2.1 // indirect\n\tgithub.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect\n\tgithub.com/stretchr/objx v0.5.2 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgithub.com/subosito/gotenv v1.4.1 // indirect\n\tgithub.com/tetafro/godot v1.5.4 // indirect\n\tgithub.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect\n\tgithub.com/timonwong/loggercheck v0.11.0 // indirect\n\tgithub.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect\n\tgithub.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect\n\tgithub.com/ultraware/funlen v0.2.0 // indirect\n\tgithub.com/ultraware/whitespace v0.2.0 // indirect\n\tgithub.com/uudashr/gocognit v1.2.1 // indirect\n\tgithub.com/uudashr/iface v1.4.1 // indirect\n\tgithub.com/wlynxg/chardet v1.0.4 // indirect\n\tgithub.com/xen0n/gosmopolitan v1.3.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/yagipy/maintidx v1.0.0 // indirect\n\tgithub.com/yeya24/promlinter v0.3.0 // indirect\n\tgithub.com/ykadowak/zerologlint v0.1.5 // indirect\n\tgithub.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect\n\tgitlab.com/bosi/decorder v0.4.2 // indirect\n\tgo-simpler.org/musttag v0.14.0 // indirect\n\tgo-simpler.org/sloglint v0.11.1 // indirect\n\tgo.augendre.info/arangolint v0.4.0 // indirect\n\tgo.augendre.info/fatcontext v0.9.0 // indirect\n\tgo.uber.org/multierr v1.10.0 // indirect\n\tgo.uber.org/zap v1.27.0 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect\n\tgolang.org/x/mod v0.33.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/term v0.41.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect\n\tgopkg.in/ini.v1 v1.67.0 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\thonnef.co/go/tools v0.7.0 // indirect\n\tmvdan.cc/editorconfig v0.3.0 // indirect\n\tmvdan.cc/gofumpt v0.9.2 // indirect\n\tmvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect\n)\n"
  },
  {
    "path": "hack/tools/go.sum",
    "content": "4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=\n4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=\n4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=\n4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncodeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY=\ncodeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ=\ncodeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=\ncodeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=\ndev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=\ndev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=\ndev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo=\ndev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8=\ngithub.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=\ngithub.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ=\ngithub.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4=\ngithub.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=\ngithub.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=\ngithub.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=\ngithub.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=\ngithub.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q=\ngithub.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ=\ngithub.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ=\ngithub.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II=\ngithub.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=\ngithub.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=\ngithub.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=\ngithub.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=\ngithub.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=\ngithub.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=\ngithub.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=\ngithub.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=\ngithub.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=\ngithub.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=\ngithub.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=\ngithub.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=\ngithub.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=\ngithub.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=\ngithub.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=\ngithub.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=\ngithub.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=\ngithub.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=\ngithub.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=\ngithub.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=\ngithub.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=\ngithub.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo=\ngithub.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c=\ngithub.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE=\ngithub.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=\ngithub.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=\ngithub.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=\ngithub.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=\ngithub.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=\ngithub.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8=\ngithub.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU=\ngithub.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=\ngithub.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=\ngithub.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=\ngithub.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=\ngithub.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=\ngithub.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=\ngithub.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E=\ngithub.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70=\ngithub.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=\ngithub.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=\ngithub.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=\ngithub.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=\ngithub.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=\ngithub.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=\ngithub.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=\ngithub.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=\ngithub.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=\ngithub.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=\ngithub.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=\ngithub.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/containerd/ltag v0.3.0 h1:AbeBQAGLwWxWVkgtLblT5Zd5fFW1+45On3+RvuZO+Go=\ngithub.com/containerd/ltag v0.3.0/go.mod h1:VEpXtwQK+FDdhegH7NLRJM5gzdHtHWDztP1YoZxWJlQ=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=\ngithub.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=\ngithub.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=\ngithub.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=\ngithub.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=\ngithub.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=\ngithub.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=\ngithub.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=\ngithub.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/editorconfig-checker/editorconfig-checker/v3 v3.6.1 h1:jdfYul8o1YpAb0tjNBcfJsVFwTn1s4qis4ec5Feuhhs=\ngithub.com/editorconfig-checker/editorconfig-checker/v3 v3.6.1/go.mod h1:3M/pJVyyr63yWjRWOFpPqKNAzj6JaE/I/+dNDfgN+3I=\ngithub.com/editorconfig/editorconfig-core-go/v2 v2.6.4 h1:CHwUbBVVyKWRX9kt5A/OtwhYUJB32DrFp9xzmjR6cac=\ngithub.com/editorconfig/editorconfig-core-go/v2 v2.6.4/go.mod h1:JWRVKHdVW+dkv6F8p+xGCa6a+TyMrqsFbFkSs/aQkrQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=\ngithub.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=\ngithub.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=\ngithub.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=\ngithub.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo=\ngithub.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=\ngithub.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=\ngithub.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=\ngithub.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=\ngithub.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=\ngithub.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=\ngithub.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=\ngithub.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=\ngithub.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=\ngithub.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=\ngithub.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=\ngithub.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=\ngithub.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=\ngithub.com/gkampitakis/go-snaps v0.5.19 h1:hUJlCQOpTt1M+kSisMwioDWZDWpDtdAvUhvWCx1YGW0=\ngithub.com/gkampitakis/go-snaps v0.5.19/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs=\ngithub.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=\ngithub.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=\ngithub.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=\ngithub.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=\ngithub.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=\ngithub.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=\ngithub.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=\ngithub.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=\ngithub.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=\ngithub.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=\ngithub.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=\ngithub.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=\ngithub.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=\ngithub.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=\ngithub.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=\ngithub.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=\ngithub.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=\ngithub.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=\ngithub.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=\ngithub.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=\ngithub.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=\ngithub.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=\ngithub.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM=\ngithub.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo=\ngithub.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=\ngithub.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=\ngithub.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=\ngithub.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=\ngithub.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=\ngithub.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=\ngithub.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=\ngithub.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=\ngithub.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=\ngithub.com/golangci/golangci-lint/v2 v2.11.3 h1:ySX1GtLwlwOEzcLKJifI/aIVesrcHDno+5mrro8rWes=\ngithub.com/golangci/golangci-lint/v2 v2.11.3/go.mod h1:HmDEVZuxz77cNLumPfNNHAFyMX/b7IbA0tpmAbwiVfo=\ngithub.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=\ngithub.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=\ngithub.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=\ngithub.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=\ngithub.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=\ngithub.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=\ngithub.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=\ngithub.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=\ngithub.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=\ngithub.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=\ngithub.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=\ngithub.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/renameio/v2 v2.0.2 h1:qKZs+tfn+arruZZhQ7TKC/ergJunuJicWS6gLDt/dGw=\ngithub.com/google/renameio/v2 v2.0.2/go.mod h1:OX+G6WHHpHq3NVj7cAOleLOwJfcQ1s3uUJQCrr78SWo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=\ngithub.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=\ngithub.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=\ngithub.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=\ngithub.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=\ngithub.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=\ngithub.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=\ngithub.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=\ngithub.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=\ngithub.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU=\ngithub.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA=\ngithub.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=\ngithub.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=\ngithub.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=\ngithub.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=\ngithub.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=\ngithub.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=\ngithub.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=\ngithub.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg=\ngithub.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=\ngithub.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jandubois/nobin v0.8.0 h1:nJ4UDkh14goFC19EqayuH5DtdqjWI0QugZss262HXus=\ngithub.com/jandubois/nobin v0.8.0/go.mod h1:qRcr5FDrIKDLCS6TFd7LzH6j+SDRjElpmmhVQeZLTWM=\ngithub.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4=\ngithub.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako=\ngithub.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=\ngithub.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=\ngithub.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=\ngithub.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=\ngithub.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=\ngithub.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=\ngithub.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=\ngithub.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=\ngithub.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=\ngithub.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=\ngithub.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=\ngithub.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw=\ngithub.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=\ngithub.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=\ngithub.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=\ngithub.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w=\ngithub.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=\ngithub.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=\ngithub.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=\ngithub.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=\ngithub.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=\ngithub.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk=\ngithub.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q=\ngithub.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=\ngithub.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=\ngithub.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=\ngithub.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=\ngithub.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk=\ngithub.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI=\ngithub.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=\ngithub.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=\ngithub.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=\ngithub.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=\ngithub.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=\ngithub.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=\ngithub.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=\ngithub.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=\ngithub.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8=\ngithub.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA=\ngithub.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=\ngithub.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=\ngithub.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=\ngithub.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc=\ngithub.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=\ngithub.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=\ngithub.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=\ngithub.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=\ngithub.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=\ngithub.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q=\ngithub.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=\ngithub.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=\ngithub.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=\ngithub.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=\ngithub.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=\ngithub.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=\ngithub.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=\ngithub.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8=\ngithub.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=\ngithub.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=\ngithub.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=\ngithub.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=\ngithub.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=\ngithub.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=\ngithub.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=\ngithub.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=\ngithub.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=\ngithub.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=\ngithub.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=\ngithub.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=\ngithub.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=\ngithub.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=\ngithub.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=\ngithub.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=\ngithub.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=\ngithub.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=\ngithub.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=\ngithub.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=\ngithub.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=\ngithub.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=\ngithub.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=\ngithub.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=\ngithub.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=\ngithub.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=\ngithub.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI=\ngithub.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=\ngithub.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=\ngithub.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=\ngithub.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=\ngithub.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=\ngithub.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=\ngithub.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=\ngithub.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=\ngithub.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=\ngithub.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=\ngithub.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 h1:AoLtJX4WUtZkhhUUMFy3GgecAALp/Mb4S1iyQOA2s0U=\ngithub.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08/go.mod h1:+XLCJiRE95ga77XInNELh2M6zQP+PdqiT9Zpm0D9Wpk=\ngithub.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=\ngithub.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=\ngithub.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=\ngithub.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=\ngithub.com/sonatard/noctx v0.5.0 h1:e/jdaqAsuWVOKQ0P6NWiIdDNHmHT5SwuuSfojFjzwrw=\ngithub.com/sonatard/noctx v0.5.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=\ngithub.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=\ngithub.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=\ngithub.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=\ngithub.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=\ngithub.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=\ngithub.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=\ngithub.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=\ngithub.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=\ngithub.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=\ngithub.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=\ngithub.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=\ngithub.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=\ngithub.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=\ngithub.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg=\ngithub.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=\ngithub.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=\ngithub.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk=\ngithub.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=\ngithub.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=\ngithub.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=\ngithub.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=\ngithub.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=\ngithub.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=\ngithub.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=\ngithub.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=\ngithub.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=\ngithub.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=\ngithub.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=\ngithub.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=\ngithub.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=\ngithub.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU=\ngithub.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=\ngithub.com/wlynxg/chardet v1.0.4 h1:hkI71Dx8v3RiAz3XKV5lJEh9QfKo7xXKUmYJQeIMlpo=\ngithub.com/wlynxg/chardet v1.0.4/go.mod h1:HLQMNsa0w4MkH2e7waQaFD+Yh85riFFTLhFtP8fsdbQ=\ngithub.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=\ngithub.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=\ngithub.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=\ngithub.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=\ngithub.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=\ngithub.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=\ngithub.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=\ngithub.com/yoheimuta/go-protoparser/v4 v4.14.2 h1:/P/LlX1CF9NaTWEltGcIZVvNlPbhABuAnBtAWpb3+74=\ngithub.com/yoheimuta/go-protoparser/v4 v4.14.2/go.mod h1:AHNNnSWnb0UoL4QgHPiOAg2BniQceFscPI5X/BZNHl8=\ngithub.com/yoheimuta/protolint v0.56.4 h1:FWvXjVNRaKJWJFxsnilRZhfQ4tc3KS8VVGWecxnLXLo=\ngithub.com/yoheimuta/protolint v0.56.4/go.mod h1:XrnOc0O5mckLR1GAOjqMPdb3R3ZEfLkMpLoq5RxxoG0=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=\ngitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=\ngo-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=\ngo-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=\ngo-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=\ngo-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=\ngo-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s=\ngo-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ=\ngo.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=\ngo.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=\ngo.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE=\ngo.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=\ngo.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=\ngo.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=\ngo.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=\ngolang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=\ngolang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=\ngolang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=\ngolang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=\ngolang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk=\ngolang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=\ngolang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=\ngolang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=\ngolang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=\ngolang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=\ngolang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=\ngolang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=\ngolang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=\ngolang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=\ngolang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=\nhonnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=\nmvdan.cc/editorconfig v0.3.0 h1:D1D2wLYEYGpawWT5SpM5pRivgEgXjtEXwC9MWhEY0gQ=\nmvdan.cc/editorconfig v0.3.0/go.mod h1:NcJHuDtNOTEJ6251indKiWuzK6+VcrMuLzGMLKBFupQ=\nmvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4=\nmvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s=\nmvdan.cc/sh/v3 v3.13.0 h1:dSfq/MVsY4w0Vsi6Lbs0IcQquMVqLdKLESAOZjuHdLg=\nmvdan.cc/sh/v3 v3.13.0/go.mod h1:KV1GByGPc/Ho0X1E6Uz9euhsIQEj4hwyKnodLlFLoDM=\nmvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI=\nmvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\n"
  },
  {
    "path": "hack/tools/pinversion.go",
    "content": "//go:build tools\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package tools is used to explicitly pin tool versions.\n// It's needed to work around @dependabot's lack of upgrading indirect dependencies.\npackage tools\n\nimport (\n\t_ \"github.com/containerd/ltag\"\n\t_ \"github.com/golangci/golangci-lint/v2/pkg/exitcodes\"\n\t_ \"github.com/jandubois/nobin/version\"\n\t_ \"github.com/yoheimuta/protolint/lib\"\n\t_ \"google.golang.org/grpc\"\n\t_ \"google.golang.org/protobuf/proto\"\n\t_ \"mvdan.cc/sh/v3/pattern\"\n)\n"
  },
  {
    "path": "hack/update-template-almalinux-kitten.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction almalinux_kitten_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the AlmaLinux Kitten image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version-major <major version>] <template.yaml>...\n\nDescription:\n  This script updates the AlmaLinux Kitten image location in the specified templates.\n  If the image location in the template contains a minor version and release date in the URL,\n  the script replaces it with the latest available minor version and date.\n\n  Image location basename format:\n\n\tAlmaLinux-Kitten-GenericCloud-<major version>-[latest|<date>.<release>].<arch>.qcow2\n\n  Published AlmaLinux Kitten image information is fetched from the following URLs:\n\n    https://kitten.repo.almalinux.org/<major version>-kitten/cloud/<arch>/images/\n\n  To parsing html, this script requires 'htmlq' or 'pup' command.\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the AlmaLinux Kitten image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the AlmaLinux Kitten image location in ~/.lima/almalinux-kitten/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/almalinux-kitten/lima.yaml\n  $ limactl factory-reset almalinux-kitten\n\n  Update the AlmaLinux Kitten image location to major version 10 in ~/.lima/almalinux-kitten/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version-major 10 ~/.lima/almalinux-kitten/lima.yaml\n  $ limactl factory-reset almalinux-kitten\n\nFlags:\n  --version-major <version>     Use the specified version. The version must be 10 or later.\n  -h, --help              Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction almalinux_kitten_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\n\t\t\"^https://kitten\\\\.repo\\\\.almalinux\\\\.org/(?<path_version>\\\\d+)-kitten/cloud/(?<path_arch>[^/]+)/images/\" +\n\t\t\"AlmaLinux-Kitten-(?<target_vendor>.*)-(?<major_version>\\\\d+)-\" +\n\t\t\"(latest|(?<date>\\\\d{8}(\\\\\\\\d+)?))\\\\.(?<release>\\\\d+)\\\\.(?<arch>[^.]+).(?<file_extension>.*)$\"\n\t;\"x\")\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\n\tjq -e '.path_arch == .arch' <<<\"${url_spec}\" >/dev/null ||\n\t\terror_exit \"Validation failed: .path_arch != .arch: ${location}\"\n\techo \"${url_spec}\"\n}\n\nreadonly almalinux_kitten_jq_filter_directory='\"https://kitten.repo.almalinux.org/\\(.path_version)-kitten/cloud/\\(.path_arch)/images/\"'\nreadonly almalinux_kitten_jq_filter_filename='\"AlmaLinux-Kitten-\\(.target_vendor)-\\(.major_version)-\\(if .date then .date + \".0\" else \"latest\" end).\\(.arch).\\(.file_extension)\"'\n\n# print the location for the given URL spec\nfunction almalinux_kitten_location_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${almalinux_kitten_jq_filter_directory} + ${almalinux_kitten_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction almalinux_kitten_image_directory_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${almalinux_kitten_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image directory for ${url_spec}\"\n}\n\nfunction almalinux_kitten_image_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${almalinux_kitten_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image filename for ${url_spec}\"\n}\n\n#\nfunction almalinux_kitten_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 arch major_version_url_spec major_version_image_directory downloaded_page links_in_page latest_minor_version_info\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\t# to detect minor version updates, we need to get the major version URL\n\tmajor_version_url_spec=$(jq -e -r '.path_version = .major_version' <<<\"${url_spec}\")\n\tmajor_version_image_directory=$(almalinux_kitten_image_directory_from_url_spec \"${major_version_url_spec}\")\n\tdownloaded_page=$(download_to_cache \"${major_version_image_directory}\")\n\tif command -v htmlq >/dev/null; then\n\t\tlinks_in_page=$(htmlq 'pre a' --attribute href <\"${downloaded_page}\")\n\telif command -v pup >/dev/null; then\n\t\tlinks_in_page=$(pup 'pre a attr{href}' <\"${downloaded_page}\")\n\telse\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from ${major_version_image_directory}\"\n\tfi\n\tlatest_minor_version_info=$(jq -e -Rrs --argjson spec \"${url_spec}\" '\n\t\t[\n\t\t\tsplit(\"\\n\").[] |\n\t\t\tcapture(\n\t\t\t\t\"^AlmaLinux-Kitten-\\($spec.target_vendor)-\" +\n\t\t\t\t\"(?<major_minor_version>\\($spec.major_version))-\" +\n\t\t\t\t\"(?<date>\\\\d{8})\\\\.(?<release>\\\\d)+\\\\.\\($spec.arch)\\\\.\\($spec.file_extension)$\"\n\t\t\t\t;\"x\"\n\t\t\t) |\n\t\t\t.version_number_array = ([.major_minor_version | scan(\"\\\\d+\") | tonumber])\n\t\t] | sort_by(.version_number_array, .date_and_ci_job_id) | last\n\t' <<<\"${links_in_page}\")\n\t[[ -n ${latest_minor_version_info} ]] || return\n\tlocal newer_url_spec location directory checksum_location downloaded_sha256sum filename digest\n\t# prefer the major_minor_version in the path\n\tnewer_url_spec=$(jq -e -r \". + ${latest_minor_version_info} | .path_version = .major_minor_version\" <<<\"${url_spec}\")\n\tlocation=$(almalinux_kitten_location_from_url_spec \"${newer_url_spec}\")\n\tdirectory=$(almalinux_kitten_image_directory_from_url_spec \"${newer_url_spec}\")\n\tchecksum_location=\"${directory}CHECKSUM\"\n\tdownloaded_sha256sum=$(download_to_cache \"${checksum_location}\")\n\tfilename=$(almalinux_kitten_image_filename_from_url_spec \"${newer_url_spec}\")\n\tdigest=$(awk \"/${filename}/{print \\\"sha256:\\\"\\$1}\" \"${downloaded_sha256sum}\")\n\t[[ -n ${digest} ]] || error_exit \"Failed to get the SHA256 digest for ${filename}\"\n\tjson_vars location arch digest\n}\n\nfunction almalinux_kitten_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(almalinux_kitten_url_spec_from_location \"${location}\")\n\tjq -r '[\"almalinux-kitten\", .major_minor_version // .major_version, .target_vendor,\n\t\tif .date then \"timestamped\" else \"latest\" end,\n\t\t.arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction almalinux_kitten_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-\"{}\"} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on AlmaLinux Kitten\" >&2\n\turl_spec=$(almalinux_kitten_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\tif jq -e '.date' <<<\"${url_spec}\" >/dev/null; then\n\t\timage_entry=$(almalinux_kitten_latest_image_entry_for_url_spec \"${url_spec}\")\n\telse\n\t\timage_entry=$(\n\t\t\t# shellcheck disable=SC2030\n\t\t\tlocation=$(almalinux_kitten_location_from_url_spec \"${url_spec}\")\n\t\t\tlocation=$(validate_url_without_redirect \"${location}\")\n\t\t\tarch=$(jq -r '.path_arch' <<<\"${url_spec}\")\n\t\t\tjson_vars location arch\n\t\t)\n\tfi\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from https://kitten.repo.almalinux.org/<version>-kitten/cloud/<arch>/images/\"\n\tfi\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\techo \"Please install 'htmlq' or 'pup' to list images from https://kitten.repo.almalinux.org/<version>-kitten/cloud/<arch>/images/\" >&2\n\telif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"almalinux_kitten\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"almalinux_kitten\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding=\"{}\"\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\talmalinux_kitten_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version-major)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding=$(\n\t\t\t\tmajor_version=\"${2%%.*}\"\n\t\t\t\t[[ ${major_version} -ge 10 ]] || error_exit \"AlmaLinux Kitten major version must be 8 or later\"\n\t\t\t\t# shellcheck disable=2034\n\t\t\t\tpath_version=\"${major_version}\"\n\t\t\t\tjson_vars path_version major_version <<<\"${overriding}\"\n\t\t\t)\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major requires a value\"\n\t\tfi\n\t\t;;\n\t--version-major=*)\n\t\toverriding=$(\n\t\t\tmajor_version=\"${1#*=}\"\n\t\t\tmajor_version=\"${major_version%%.*}\"\n\t\t\t[[ ${major_version} -ge 10 ]] || error_exit \"AlmaLinux Kitten major version must be 8 or later\"\n\t\t\t# shellcheck disable=2034\n\t\t\tpath_version=\"${major_version}\"\n\t\t\tjson_vars path_version major_version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\talmalinux_kitten_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\talmalinux_kitten_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\talmalinux_kitten_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-almalinux.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction almalinux_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the AlmaLinux image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version-major <major version>] <template.yaml>...\n\nDescription:\n  This script updates the AlmaLinux image location in the specified templates.\n  If the image location in the template contains a minor version and release date in the URL,\n  the script replaces it with the latest available minor version and date.\n\n  Image location basename format:\n\n\tAlmaLinux-<major version>-GenericCloud-[latest|<major version>.<minor version>-<date>].<arch>.qcow2\n\n  Published AlmaLinux image information is fetched from the following URLs:\n\n    https://repo.almalinux.org/almalinux/<major version>/cloud/<arch>/images/\n\n  To parsing html, this script requires 'htmlq' or 'pup' command.\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the AlmaLinux image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the AlmaLinux image location in ~/.lima/almalinux/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/almalinux/lima.yaml\n  $ limactl factory-reset almalinux\n\n  Update the AlmaLinux image location to major version 9 in ~/.lima/almalinux/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version-major 9 ~/.lima/almalinux/lima.yaml\n  $ limactl factory-reset almalinux\n\nFlags:\n  --version-major <version>     Use the specified version. The version must be 8 or later.\n  -h, --help              Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction almalinux_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\n\t\t\"^https://repo\\\\.almalinux\\\\.org/almalinux/(?<path_version>\\\\d+(\\\\.\\\\d+)?)/cloud/(?<path_arch>[^/]+)/images/\" +\n\t\t\"AlmaLinux-(?<major_version>\\\\d+)-(?<target_vendor>.*)-\" +\n\t\t\"(latest|(?<major_minor_version>\\\\d+\\\\.\\\\d+)-(?<date>\\\\d{8})(?:\\\\.\\\\d+)?)\\\\.(?<arch>[^.]+).(?<file_extension>.*)$\"\n\t;\"x\")\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\n\tjq -e '.path_arch == .arch' <<<\"${url_spec}\" >/dev/null ||\n\t\terror_exit \"Validation failed: .path_arch != .arch: ${location}\"\n\techo \"${url_spec}\"\n}\n\nreadonly almalinux_jq_filter_directory='\"https://repo.almalinux.org/almalinux/\\(.path_version)/cloud/\\(.path_arch)/images/\"'\nreadonly almalinux_jq_filter_filename='\"AlmaLinux-\\(.major_version)-\\(.target_vendor)-\\(if .date then .major_minor_version + \"-\" + .date else \"latest\" end).\\(.arch).\\(.file_extension)\"'\n\n# print the location for the given URL spec\nfunction almalinux_location_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${almalinux_jq_filter_directory} + ${almalinux_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction almalinux_image_directory_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${almalinux_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image directory for ${url_spec}\"\n}\n\nfunction almalinux_image_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${almalinux_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image filename for ${url_spec}\"\n}\n\n#\nfunction almalinux_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 arch major_version_url_spec major_version_image_directory downloaded_page links_in_page latest_minor_version_info\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\t# to detect minor version updates, we need to get the major version URL\n\tmajor_version_url_spec=$(jq -e -r '.path_version = .major_version' <<<\"${url_spec}\")\n\tmajor_version_image_directory=$(almalinux_image_directory_from_url_spec \"${major_version_url_spec}\")\n\tdownloaded_page=$(download_to_cache \"${major_version_image_directory}\")\n\tif command -v htmlq >/dev/null; then\n\t\tlinks_in_page=$(htmlq 'pre a' --attribute href <\"${downloaded_page}\")\n\telif command -v pup >/dev/null; then\n\t\tlinks_in_page=$(pup 'pre a attr{href}' <\"${downloaded_page}\")\n\telse\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from ${major_version_image_directory}\"\n\tfi\n\tlatest_minor_version_info=$(jq -e -Rrs --argjson spec \"${url_spec}\" '\n\t\t[\n\t\t\tsplit(\"\\n\").[] |\n\t\t\tcapture(\n\t\t\t\t\"^AlmaLinux-\\($spec.major_version)-\\($spec.target_vendor)-\" +\n\t\t\t\t\"(?<major_minor_version>\\($spec.major_version)\\\\.\\\\d+)-\" +\n\t\t\t\t\"(?<date>\\\\d{8}(?:\\\\.\\\\d)?)\\\\.\\($spec.arch)\\\\.\\($spec.file_extension)$\"\n\t\t\t\t;\"x\"\n\t\t\t) |\n\t\t\t.version_number_array = ([.major_minor_version | scan(\"\\\\d+\") | tonumber])\n\t\t] | sort_by(.version_number_array, .date_and_ci_job_id) | last\n\t' <<<\"${links_in_page}\")\n\t[[ -n ${latest_minor_version_info} ]] || return\n\tlocal newer_url_spec location directory checksum_location downloaded_sha256sum filename digest\n\t# prefer the major_minor_version in the path\n\tnewer_url_spec=$(jq -e -r \". + ${latest_minor_version_info} | .path_version = .major_minor_version\" <<<\"${url_spec}\")\n\tlocation=$(almalinux_location_from_url_spec \"${newer_url_spec}\")\n\tdirectory=$(almalinux_image_directory_from_url_spec \"${newer_url_spec}\")\n\tchecksum_location=\"${directory}CHECKSUM\"\n\tdownloaded_sha256sum=$(download_to_cache \"${checksum_location}\")\n\tfilename=$(almalinux_image_filename_from_url_spec \"${newer_url_spec}\")\n\tdigest=$(awk \"/${filename}/{print \\\"sha256:\\\"\\$1}\" \"${downloaded_sha256sum}\")\n\t[[ -n ${digest} ]] || error_exit \"Failed to get the SHA256 digest for ${filename}\"\n\tjson_vars location arch digest\n}\n\nfunction almalinux_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(almalinux_url_spec_from_location \"${location}\")\n\tjq -r '[\"almalinux\", .major_minor_version // .major_version, .target_vendor,\n\t\tif .date then \"timestamped\" else \"latest\" end,\n\t\t.arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction almalinux_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-\"{}\"} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on AlmaLinux\" >&2\n\turl_spec=$(almalinux_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\tif jq -e '.date' <<<\"${url_spec}\" >/dev/null; then\n\t\timage_entry=$(almalinux_latest_image_entry_for_url_spec \"${url_spec}\")\n\telse\n\t\timage_entry=$(\n\t\t\t# shellcheck disable=SC2030\n\t\t\tlocation=$(almalinux_location_from_url_spec \"${url_spec}\")\n\t\t\tlocation=$(validate_url_without_redirect \"${location}\")\n\t\t\tarch=$(jq -r '.path_arch' <<<\"${url_spec}\")\n\t\t\tjson_vars location arch\n\t\t)\n\tfi\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from https://repo.almalinux.org/almalinux/<version>/cloud/<arch>/images/\"\n\tfi\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\techo \"Please install 'htmlq' or 'pup' to list images from https://repo.almalinux.org/almalinux/<version>/cloud/<arch>/images/\" >&2\n\telif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"almalinux\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"almalinux\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding=\"{}\"\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\talmalinux_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version-major)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding=$(\n\t\t\t\tmajor_version=\"${2%%.*}\"\n\t\t\t\t[[ ${major_version} -ge 8 ]] || error_exit \"AlmaLinux major version must be 8 or later\"\n\t\t\t\t# shellcheck disable=2034\n\t\t\t\tpath_version=\"${major_version}\"\n\t\t\t\tjson_vars path_version major_version <<<\"${overriding}\"\n\t\t\t)\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major requires a value\"\n\t\tfi\n\t\t;;\n\t--version-major=*)\n\t\toverriding=$(\n\t\t\tmajor_version=\"${1#*=}\"\n\t\t\tmajor_version=\"${major_version%%.*}\"\n\t\t\t[[ ${major_version} -ge 8 ]] || error_exit \"AlmaLinux major version must be 8 or later\"\n\t\t\t# shellcheck disable=2034\n\t\t\tpath_version=\"${major_version}\"\n\t\t\tjson_vars path_version major_version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\talmalinux_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\talmalinux_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\talmalinux_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-alpine.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction alpine_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the Alpine Linux image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version-major-minor (<major>.<minor>|latest-stable)|--version-major <major> --version-minor <minor>] <template.yaml>...\n\nDescription:\n  This script updates the Alpine Linux image location in the specified templates.\n  Image location basename format:\n\n    <target vendor>_alpine-<version>-<arch>-<firmware>-<bootstrap>[-<machine>]-<image revision>.qcow2\n\n  Published Alpine Linux image information is fetched from the following URLs:\n\n    latest-stable: https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/cloud\n    <major>.<minor>: https://dl-cdn.alpinelinux.org/alpine/v<major>.<minor>/releases/cloud\n\n  To parsing html, this script requires 'htmlq' or 'pup' command.\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the Alpine Linux image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Alpine Linux image location to version 3.18 in ~/.lima/alpine/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version-major-minor 3.18 ~/.lima/alpine/lima.yaml\n  $ limactl factory-reset alpine\n\nFlags:\n  --version-major-minor (<major>.<minor>|latest-stable)  Use the specified <major>.<minor> version or alias \"latest-stable\".\n                                                         The <major>.<minor> version must be 3.18 or later.\n  --version-major <major> --version-minor <minor>        Use the specified <major> and <minor> version.\n  -h, --help                                             Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction alpine_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\"\n\t\t^https://dl-cdn\\\\.alpinelinux\\\\.org/alpine/(?<path_version>v\\\\d+\\\\.\\\\d+|latest-stable)/releases/cloud/\n\t\t(?<target_vendor>[^_]+)_alpine-(?<version>\\\\d+\\\\.\\\\d+\\\\.\\\\d+)-(?<arch>[^-]+)-\n\t\t(?<firmware>[^-]+)-(?<bootstrap>[^-]+)(-(?<machine>metal|vm))?-(?<image_revision>r\\\\d+)\\\\.(?<file_extension>.*)$\n\t\";\"x\")\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\techo \"${url_spec}\"\n}\n\nreadonly alpine_jq_filter_directory='\"https://dl-cdn.alpinelinux.org/alpine/\\(.path_version)/releases/cloud/\"'\nreadonly alpine_jq_filter_filename='\n\t\"\\(.target_vendor)_alpine-\\(.version)-\\(.arch)-\\(.firmware)-\\(.bootstrap)\" +\n\t\"\\(if .machine then \"-\" + .machine else \"\" end)-\\(.image_revision).\\(.file_extension)\"\n'\n\n# print the location for the given URL spec\nfunction alpine_location_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${alpine_jq_filter_directory} + ${alpine_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction alpine_image_directory_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${alpine_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image directory for ${url_spec}\"\n}\n\nfunction alpine_image_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${alpine_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image filename for ${url_spec}\"\n}\n\n#\nfunction alpine_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 arch image_directory downloaded_page links_in_page latest_version_info\n\t# shellcheck disable=SC2034\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\timage_directory=$(alpine_image_directory_from_url_spec \"${url_spec}\")\n\tdownloaded_page=$(download_to_cache \"${image_directory}\")\n\tif command -v htmlq >/dev/null; then\n\t\tlinks_in_page=$(htmlq 'pre a' --attribute href <\"${downloaded_page}\")\n\telif command -v pup >/dev/null; then\n\t\tlinks_in_page=$(pup 'pre a attr{href}' <\"${downloaded_page}\")\n\telse\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from ${image_directory}\"\n\tfi\n\tlatest_version_info=$(jq -e -Rrs --argjson spec \"${url_spec}\" '\n\t\t[\n\t\t\tsplit(\"\\n\").[] |\n\t\t\tcapture(\n\t\t\t\t\"^\\($spec.target_vendor)_alpine-(?<version>\\\\d+\\\\.\\\\d+\\\\.\\\\d+)-\\($spec.arch)-\" +\n\t\t\t\t\"\\($spec.firmware)-\\($spec.bootstrap)\\(if $spec.machine then \"-\" + $spec.machine else \"\" end)-\" +\n\t\t\t\t\"(?<image_revision>r\\\\d+)\\\\.\\($spec.file_extension)\"\n\t\t\t\t;\"x\"\n\t\t\t) |\n\t\t\t.version_number_array = ([.version | scan(\"\\\\d+\") | tonumber])\n\t\t] | sort_by(.version_number_array, .image_revision) | last\n\t' <<<\"${links_in_page}\")\n\t[[ -n ${latest_version_info} ]] || return\n\tlocal newer_url_spec location sha512sum_location downloaded_sha256sum filename digest\n\t# prefer the v<major>.<minor> in the path\n\tnewer_url_spec=$(jq -e -r \". + ${latest_version_info} | .path_version = \\\"v\\\" + (.version_number_array[:2]|map(tostring)|join(\\\".\\\"))\" <<<\"${url_spec}\")\n\tlocation=$(alpine_location_from_url_spec \"${newer_url_spec}\")\n\tlocation=$(validate_url_without_redirect \"${location}\")\n\tsha512sum_location=\"${location}.sha512\"\n\tdownloaded_sha256sum=$(download_to_cache \"${sha512sum_location}\")\n\tfilename=$(alpine_image_filename_from_url_spec \"${newer_url_spec}\")\n\tdigest=\"sha512:$(<\"${downloaded_sha256sum}\")\"\n\t[[ -n ${digest} ]] || error_exit \"Failed to get the digest for ${filename}\"\n\tjson_vars location arch digest\n}\n\nfunction alpine_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(alpine_url_spec_from_location \"${location}\")\n\tjq -r '[\"alpine\", .path_version, .target_vendor, .arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction alpine_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-'{\"path_version\":\"latest-stable\"}'} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on Alpine Linux\" >&2\n\turl_spec=$(alpine_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\timage_entry=$(alpine_latest_image_entry_for_url_spec \"${url_spec}\")\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from https://dl-cdn.alpinelinux.org/alpine/<version>/releases/cloud/\"\n\tfi\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\techo \"Please install 'htmlq' or 'pup' to list images from https://dl-cdn.alpinelinux.org/alpine/<version>/releases/cloud/\" >&2\n\telif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"alpine\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"alpine\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding='{}'\ndeclare version_major='' version_minor=''\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\talpine_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version-major-minor)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major-minor requires a value\"\n\t\tfi\n\t\t;&\n\t--version-major-minor=*)\n\t\tversion=${version:-${1#*=}}\n\t\toverriding=$(\n\t\t\tversion=\"${version#v}\"\n\t\t\tif [[ ${version} =~ ^v?[0-9]+.[0-9]+ ]]; then\n\t\t\t\tversion=\"$(echo \"${version}\" | cut -d. -f1-2)\"\n\t\t\t\t[[ ${version%%.*} -gt 3 || (${version%%.*} -eq 3 && ${version#*.} -ge 18) ]] || error_exit \"Alpine Linux version must be 3.18 or later\"\n\t\t\t\tpath_version=\"v${version}\"\n\t\t\telif [[ ${version} == \"latest-stable\" ]]; then\n\t\t\t\tpath_version=\"latest-stable\"\n\t\t\telse\n\t\t\t\terror_exit \"--version-major-minor requires a value in the format <major>.<minor> or latest-stable\"\n\t\t\tfi\n\t\t\tjson_vars path_version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t--version-major)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion_major=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major requires a value\"\n\t\tfi\n\t\t;&\n\t--version-major=*)\n\t\tversion_major=${version_major:-${1#*=}}\n\t\t[[ ${version_major} =~ ^[0-9]+$ ]] || error_exit \"Please specify --version-major in numbers\"\n\t\t;;\n\t--version-minor)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion_minor=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-minor requires a value\"\n\t\tfi\n\t\t;&\n\t--version-minor=*)\n\t\tversion_minor=${version_minor:-${1#*=}}\n\t\t[[ ${version_minor} =~ ^[0-9]+$ ]] || error_exit \"Please specify --version-minor in numbers\"\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif ! jq -e '.path_version' <<<\"${overriding}\" >/dev/null; then # --version-major-minor is not specified\n\tif [[ -n ${version_major} && -n ${version_minor} ]]; then\n\t\t[[ ${version_major} -gt 3 || (${version_major} -eq 3 && ${version_minor} -ge 18) ]] || error_exit \"Alpine Linux version must be 3.18 or later\"\n\t\t# shellcheck disable=2034\n\t\tpath_version=\"v${version_major}.${version_minor}\"\n\t\toverriding=$(json_vars path_version <<<\"${overriding}\")\n\telif [[ -n ${version_major} ]]; then\n\t\terror_exit \"--version-minor is required when --version-major is specified\"\n\telif [[ -n ${version_minor} ]]; then\n\t\terror_exit \"--version-major is required when --version-minor is specified\"\n\tfi\nelif [[ -n ${version_major} || -n ${version_minor} ]]; then # --version-major-minor is specified\n\techo \"Ignoring --version-major and --version-minor because --version-major-minor is specified\" >&2\nfi\n[[ ${overriding} == \"{}\" ]] && overriding='{\"path_version\":\"latest-stable\"}'\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\talpine_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\talpine_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\talpine_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-archlinux.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction archlinux_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the Arch-Linux image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") <template.yaml>...\n\nDescription:\n  This script updates the Arch-Linux image location in the specified templates.\n  If the image location in the template contains a release date in the URL, the script replaces it with the latest available date.\n\n  Image location basename format: Arch-Linux-<arch>-cloudimg[-<date>.<CI_JOB_ID>].qcow2\n\n  Published Arch-Linux image information is fetched from the following URLs:\n\n    x86_64:\n      listing: https://gitlab.archlinux.org/api/v4/projects/archlinux%2Farch-boxes/packages\n      details: https://gitlab.archlinux.org/api/v4/projects/archlinux%2Farch-boxes/packages/:package_id/package_files\n\n    aarch64:\n      https://github.com/mcginty/arch-boxes-arm/releases/\n\n  Using 'gh' CLI tool for fetching the latest release from GitHub.\n\nExamples:\n  Update the Arch-Linux image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Arch-Linux image location in ~/.lima/archlinux/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/archlinux/lima.yaml\n  $ limactl factory-reset archlinux\n\nFlags:\n  -h, --help              Print this help message\nHELP\n}\n\n# print the URL spec for the given location\n# shellcheck disable=SC2034\nfunction archlinux_url_spec_from_location() {\n\tlocal location=$1 location_basename arch flavor source date_and_ci_job_id='' file_extension\n\tlocation_basename=$(basename \"${location}\")\n\tarch=$(echo \"${location_basename}\" | cut -d- -f3)\n\tflavor=$(echo \"${location_basename}\" | cut -d- -f4 | cut -d. -f1)\n\tcase \"${location}\" in\n\thttps://geo.mirror.pkgbuild.com/images/*)\n\t\tsource=\"geo.mirror.pkgbuild.com\"\n\t\tlocal -r date_and_ci_job_id_pattern='[0-9]{8}\\.[0-9]+'\n\t\tif [[ ${location} =~ ${date_and_ci_job_id_pattern} ]]; then\n\t\t\tdate_and_ci_job_id=\"${BASH_REMATCH[0]}\"\n\t\tfi\n\t\tif [[ ${location_basename} =~ ${date_and_ci_job_id_pattern} ]]; then\n\t\t\tfile_extension=${location_basename##*\"${BASH_REMATCH[0]}\".}\n\t\telse\n\t\t\tfile_extension=${location_basename#*.}\n\t\tfi\n\t\t;;\n\thttps://github.com/mcginty/arch-boxes-arm/releases/download/*)\n\t\tsource=\"github.com/mcginty/arch-boxes-arm\"\n\t\tlocal -r date_pattern='[0-9]{8}'\n\t\tif [[ ${location} =~ ${date_pattern} ]]; then\n\t\t\tdate_and_ci_job_id=\"${BASH_REMATCH[0]}\"\n\t\t\tfile_extension=${location_basename#*\"${date_and_ci_job_id}\".*.}\n\t\telse\n\t\t\terror_exit \"Failed to extract date from ${location}\"\n\t\tfi\n\t\t;;\n\t*)\n\t\t# Unsupported location\n\t\treturn 1\n\t\t;;\n\tesac\n\tjson_vars source arch flavor date_and_ci_job_id file_extension\n}\n\n# print the location for the given URL spec\nfunction archlinux_location_from_url_spec() {\n\tlocal url_spec=$1 source arch flavor date_and_ci_job_id file_extension location=''\n\tsource=$(jq -r '.source' <<<\"${url_spec}\")\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\tflavor=$(jq -r '.flavor' <<<\"${url_spec}\")\n\tdate_and_ci_job_id=$(jq -r '.date_and_ci_job_id' <<<\"${url_spec}\")\n\n\tfile_extension=$(jq -r '.file_extension' <<<\"${url_spec}\")\n\tcase \"${source}\" in\n\tgeo.mirror.pkgbuild.com)\n\t\tlocation=\"https://geo.mirror.pkgbuild.com/images/\"\n\t\tif [[ -n ${date_and_ci_job_id} ]]; then\n\t\t\tlocation+=\"v${date_and_ci_job_id}/Arch-Linux-${arch}-${flavor}-${date_and_ci_job_id}.${file_extension}\"\n\t\telse\n\t\t\tlocation+=\"latest/Arch-Linux-${arch}-${flavor}.${file_extension}\"\n\t\tfi\n\t\t;;\n\tgithub.com/mcginty/arch-boxes-arm) ;;\n\t*)\n\t\terror_exit \"Unsupported source: ${source}\"\n\t\t;;\n\tesac\n\techo \"${location}\"\n}\n\n# returns the image entry for the latest image in the gitlab mirror\nfunction archlinux_image_entry_for_image_kernel_gitlab_mirror() {\n\tlocal 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\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\tif ! jq -e '.date_and_ci_job_id' <<<\"${url_spec}\" >/dev/null; then\n\t\tjson_vars location arch\n\t\treturn 1\n\tfi\n\tflavor=$(jq -r '.flavor' <<<\"${url_spec}\")\n\tdate_and_ci_job_id=$(jq -r '.date_and_ci_job_id' <<<\"${url_spec}\")\n\tfile_extension=$(jq -r '.file_extension' <<<\"${url_spec}\")\n\tgitlab_package_api_base=\"https://gitlab.archlinux.org/api/v4/projects/archlinux%2Farch-boxes/packages\"\n\tlatest_package_id=$(curl --silent --show-error \"${gitlab_package_api_base}\" | jq -r 'last|.id') || error_exit \"Failed to fetch latest package_id\"\n\tjq_filter=\"\n        .[]|select(.file_name|test(\\\"^Arch-Linux-${arch}-${flavor}-.*\\\\\\.${file_extension}\\$\\\"))\n    \"\n\tlatest_package_file=$(curl -s \"${gitlab_package_api_base}/${latest_package_id}/package_files\" | jq -r \"${jq_filter}\") ||\n\t\terror_exit \"Failed to fetch latest package_file\"\n\tfile_name=$(jq -r '.file_name' <<<\"${latest_package_file}\")\n\tdigest=\"sha256:$(jq -r '.file_sha256' <<<\"${latest_package_file}\")\"\n\tlocal -r date_and_ci_job_id_pattern='[0-9]{8}\\.[0-9]+'\n\t[[ ${file_name} =~ ${date_and_ci_job_id_pattern} ]] || error_exit \"Failed to extract date_and_ci_job_id from ${file_name}\"\n\tdate_and_ci_job_id=\"${BASH_REMATCH[0]}\"\n\tupdated_url_spec=$(json_vars date_and_ci_job_id <<<\"${url_spec}\")\n\tlocation=$(archlinux_location_from_url_spec \"${updated_url_spec}\")\n\tlocation=$(validate_url_without_redirect \"${location}\")\n\tjson_vars location arch digest\n}\n\n# returns the image entry for the latest image in the GitHub repo\nfunction archlinux_image_entry_for_image_kernel_github_com() {\n\tlocal location=$1 url_spec=$2 arch flavor file_extension repo jq_filter latest_location downloaded_img digest\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\tif ! jq -e '.date_and_ci_job_id' <<<\"${url_spec}\" >/dev/null; then\n\t\tjson_vars location arch\n\t\treturn 1\n\tfi\n\tflavor=$(jq -r '.flavor' <<<\"${url_spec}\")\n\tfile_extension=$(jq -r '.file_extension' <<<\"${url_spec}\")\n\tcommand -v gh >/dev/null || error_exit \"gh is required for fetching the latest release from GitHub, but it's not installed\"\n\tlocal -r repo_pattern='github.com/(.*)/releases/download/(.*)'\n\tif [[ ${location} =~ ${repo_pattern} ]]; then\n\t\trepo=\"${BASH_REMATCH[1]}\"\n\telse\n\t\terror_exit \"Failed to extract repo and release from ${location}\"\n\tfi\n\tjq_filter=\".assets[]|select(.name|test(\\\"^Arch-Linux-${arch}-${flavor}-.*\\\\\\.${file_extension}\\$\\\"))|.url\"\n\tlatest_location=$(gh release view --repo \"${repo}\" --json assets --jq \"${jq_filter}\")\n\t[[ -n ${latest_location} ]] || error_exit \"Failed to fetch the latest release URL from ${repo}\"\n\tif [[ ${location} == \"${latest_location}\" ]]; then\n\t\tjson_vars location arch\n\t\treturn\n\tfi\n\tlocation=${latest_location}\n\tdownloaded_img=$(download_to_cache_without_redirect \"${latest_location}\")\n\t# shellcheck disable=SC2034\n\tdigest=\"sha512:$(sha512sum \"${downloaded_img}\" | cut -d' ' -f1)\"\n\tjson_vars location arch digest\n}\n\nfunction archlinux_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(archlinux_url_spec_from_location \"${location}\")\n\tjq -r '[\"archlinux\", .source, .arch, .date_and_ci_job_id // empty]|join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction archlinux_image_entry_for_image_kernel() {\n\tlocal location=$1 url_spec source image_entry=''\n\turl_spec=$(archlinux_url_spec_from_location \"${location}\")\n\tsource=$(jq -r '.source' <<<\"${url_spec}\")\n\tcase \"${source}\" in\n\tgeo.mirror.pkgbuild.com)\n\t\timage_entry=$(archlinux_image_entry_for_image_kernel_gitlab_mirror \"${location}\" \"${url_spec}\")\n\t\t;;\n\tgithub.com/mcginty/arch-boxes-arm)\n\t\timage_entry=$(archlinux_image_entry_for_image_kernel_github_com \"${location}\" \"${url_spec}\")\n\t\t;;\n\t*) error_exit \"Unsupported source: ${source}\" ;;\n\tesac\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"archlinux\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"archlinux\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding=\"{}\"\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tarchlinux_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tarchlinux_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tarchlinux_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\tarchlinux_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-centos-stream.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction centos_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the CentOS Stream image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version <version>] <template.yaml>...\n\nDescription:\n  This script updates the CentOS Stream image location in the specified templates.\n  If the image location in the template contains a release date in the URL, the script replaces it with the latest available date.\n\n  Image location basename format: CentOS-Stream-GenericCloud-<version>-[latest|<date>.0].<arch>.qcow2\n\n  Published CentOS Stream image information is fetched from the following URLs:\n\n    https://cloud.centos.org/centos/<major version>-stream/<arch>/images/\n\n  To parsing html, this script requires 'htmlq' or 'pup' command.\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the CentOS Stream image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the CentOS Stream image location in ~/.lima/centos/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/centos/lima.yaml\n  $ limactl factory-reset centos\n\n  Update the CentOS Stream image location to 9-Stream in ~/.lima/centos/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version 9-stream ~/.lima/centos/lima.yaml\n  $ limactl factory-reset centos\n\nFlags:\n  --version <version>     Use the specified version. The version must be 8 or later.\n  -h, --help              Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction centos_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\n\t\t\"^https://cloud\\\\.centos\\\\.org/centos/(?<path_version>\\\\d+)-stream/(?<path_arch>[^/]+)/images/\" +\n\t\t\"CentOS-Stream-(?<target_vendor>.*)-(?<version>\\\\d+(\\\\.[.\\\\d]+)?)-\" +\n\t\t\"(latest|(?<date_and_ci_job_id>\\\\d{8}\\\\.\\\\d+))\\\\.(?<arch>[^.]+).(?<file_extension>.*)$\"\n\t;\"x\")'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\tjq -e '.path_version == .version' <<<\"${url_spec}\" >/dev/null ||\n\t\terror_exit \"Validation failed: .path_version != .version: ${location}\"\n\tjq -e '.path_arch == .arch' <<<\"${url_spec}\" >/dev/null ||\n\t\terror_exit \"Validation failed: .path_arch != .arch: ${location}\"\n\techo \"${url_spec}\"\n}\n\nreadonly centos_jq_filter_directory='\"https://cloud.centos.org/centos/\\(.version)-stream/\\(.path_arch)/images/\"'\nreadonly centos_jq_filter_filename='\"CentOS-Stream-\\(.target_vendor)-\\(.version)-\\(.date_and_ci_job_id // \"latest\").\\(.arch).\\(.file_extension)\"'\n\n# print the location for the given URL spec\nfunction centos_location_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${centos_jq_filter_directory} + ${centos_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction centos_image_directory_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${centos_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image directory for ${url_spec}\"\n}\n\nfunction centos_image_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${centos_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image filename for ${url_spec}\"\n}\n\n#\nfunction centos_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 version arch image_directory downloaded_page links_in_page latest_info\n\tversion=$(jq -r '.version' <<<\"${url_spec}\")\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\timage_directory=$(centos_image_directory_from_url_spec \"${url_spec}\")\n\tdownloaded_page=$(download_to_cache \"${image_directory}\")\n\tif command -v htmlq >/dev/null; then\n\t\tlinks_in_page=$(htmlq 'td.indexcolname a' --attribute href <\"${downloaded_page}\")\n\telif command -v pup >/dev/null; then\n\t\tlinks_in_page=$(pup 'td[class=indexcolname] a attr{href}' <\"${downloaded_page}\")\n\telse\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from https://cloud.centos.org/centos/${version}/${arch}/images/\"\n\tfi\n\tlatest_info=$(jq -e -Rrs --argjson spec \"${url_spec}\" '\n\t\t[\n\t\t\tsplit(\"\\n\").[] |\n\t\t\tcapture(\n\t\t\t\t\"^CentOS-Stream-\\($spec.target_vendor)-\\($spec.version)-(?<date_and_ci_job_id>\\\\d{8}\\\\.\\\\d+)\\\\.\\($spec.arch)\\\\.\\($spec.file_extension)$\"\n\t\t\t\t;\"x\"\n\t\t\t)\n\t\t] | sort_by(.date_and_ci_job_id) | last\n\t' <<<\"${links_in_page}\")\n\t[[ -n ${latest_info} ]] || return\n\tlocal newer_url_spec location sha256sum_location downloaded_sha256sum filename digest\n\tnewer_url_spec=$(jq -e -r \". + ${latest_info}\" <<<\"${url_spec}\")\n\tlocation=$(centos_location_from_url_spec \"${newer_url_spec}\")\n\tsha256sum_location=\"${location}.SHA256SUM\"\n\tdownloaded_sha256sum=$(download_to_cache \"${sha256sum_location}\")\n\tfilename=$(centos_image_filename_from_url_spec \"${newer_url_spec}\")\n\tdigest=\"sha256:$(awk \"/SHA256 \\(${filename}\\) =/{print \\$4}\" \"${downloaded_sha256sum}\")\"\n\t[[ -n ${digest} ]] || error_exit \"Failed to get the SHA256 digest for ${filename}\"\n\tjson_vars location arch digest\n}\n\nfunction centos_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(centos_url_spec_from_location \"${location}\")\n\tjq -r '[\"centos\", .version, .target_vendor,\n\t\tif .date_and_ci_job_id then \"timestamped\" else \"latest\" end,\n\t\t.arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction centos_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-\"{}\"} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on CentOS Stream\" >&2\n\turl_spec=$(centos_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\tif jq -e '.date_and_ci_job_id' <<<\"${url_spec}\" >/dev/null; then\n\t\timage_entry=$(centos_latest_image_entry_for_url_spec \"${url_spec}\")\n\telse\n\t\timage_entry=$(\n\t\t\t# shellcheck disable=SC2030\n\t\t\tlocation=$(centos_location_from_url_spec \"${url_spec}\")\n\t\t\tlocation=$(validate_url_without_redirect \"${location}\")\n\t\t\tarch=$(jq -r '.path_arch' <<<\"${url_spec}\")\n\t\t\tjson_vars location arch\n\t\t)\n\tfi\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from https://cloud.centos.org/centos/<version>/<arch>/images/\"\n\tfi\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\techo \"Please install 'htmlq' or 'pup' to list images from https://cloud.centos.org/centos/<version>/<arch>/images/\" >&2\n\telif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"centos\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"centos\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding=\"{}\"\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tcentos_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding=$(\n\t\t\t\tversion=\"${2%%-*}\"\n\t\t\t\t[[ ${version} -ge 8 ]] || error_exit \"CentOS Stream version must be 8 or later\"\n\t\t\t\tjson_vars version <<<\"${overriding}\"\n\t\t\t)\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version requires a value\"\n\t\tfi\n\t\t;;\n\t--version=*)\n\t\toverriding=$(\n\t\t\tversion=\"${1#*=}\"\n\t\t\tversion=\"${version%%-*}\"\n\t\t\t[[ ${version} -ge 8 ]] || error_exit \"CentOS Stream version must be 8 or later\"\n\t\t\tjson_vars version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tcentos_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tcentos_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\tcentos_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-debian.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction debian_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the Debian image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--backports[=<bool>]] [--daily[=<bool>]] [--timestamped[=<bool>]] [--version <version>] <template.yaml>...\n\nDescription:\n  This script updates the Debian image location in the specified templates.\n  If the image location in the template contains a release date in the URL, the script replaces it with the latest available date.\n  If no flags are specified, the script uses the version from the image location basename in the template.\n\n  Image location basename format: debian-<version>[-backports]-genericcloud-<arch>[-daily][-<timestamp>].qcow2\n\n  Published Debian image information is fetched from the following URLs:\n\n    https://cloud.debian.org/images/cloud/<codename>[-backports]/[daily/](latest|<timestamp>)/debian-<version>[-backports]-genericcloud-<arch>[-daily][-<timestamp>].json\n\n  The downloaded JSON file will be cached in the Lima cache directory.\n\nExamples:\n  Update the Debian image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Debian image location in ~/.lima/debian/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/debian/lima.yaml\n  $ limactl factory-reset debian\n\n  Update the Debian image location to debian-13-genericcloud-<arch>.qcow2 in ~/.lima/debian/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version trixie ~/.lima/debian/lima.yaml\n  $ limactl factory-reset debian\n\nFlags:\n  --backports[=<bool>]    Use the backports image\n                          The boolean value can be true, false, 1, or 0\n  --daily[=<bool>]        Use the daily image\n  --timestamped[=<bool>]  Use the timestamped image\n  --version <version>     Use the specified version\n                          The version can be a codename, version number, or alias (testing, stable, oldstable)\n  -h, --help              Print this help message\nHELP\n}\n\nreadonly debian_base_url=https://cloud.debian.org/images/cloud/\n\nreadonly debian_target_vendor=genericcloud\n\nreadonly -A debian_version_to_codename=(\n\t[10]=buster\n\t[11]=bullseye\n\t[12]=bookworm\n\t[13]=trixie\n\t[14]=forky\n)\n\ndeclare -A debian_codename_to_version\nfunction debian_setup_codename_to_version() {\n\tlocal version codename\n\tfor version in \"${!debian_version_to_codename[@]}\"; do\n\t\tcodename=${debian_version_to_codename[${version}]}\n\t\tdebian_codename_to_version[${codename}]=\"${version}\"\n\tdone\n\treadonly -A debian_codename_to_version\n}\ndebian_setup_codename_to_version\n\nreadonly -A debian_alias_to_codename=(\n\t[testing]=trixie\n\t[stable]=bookworm\n\t[oldstable]=bullseye\n)\n\n# debian_downloaded_json downloads the JSON file for the given url_spec(JSON) and caches it\n# e.g.\n# ```console\n# debian_downloaded_json '{\"backports\":false,\"daily\":false,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n#\n# ```\nfunction debian_downloaded_json() {\n\tlocal url_spec=$1 json_url_spec json_url\n\tjson_url_spec=$(jq -r '. | del(.timestamp) | .file_extension = \"json\"' <<<\"${url_spec}\") || error_exit \"Failed to create JSON URL spec\"\n\tjson_url=$(debian_location_from_url_spec \"${json_url_spec}\")\n\tdownload_to_cache \"${json_url}\"\n}\n\nfunction debian_digest_from_upload_entry() {\n\tlocal upload_entry=$1 debian_digest digest\n\tdebian_digest=$(jq -e -r '.metadata.annotations.\"cloud.debian.org/digest\"' <<<\"${upload_entry}\") ||\n\t\terror_exit \"Failed to get the digest from ${upload_entry}\"\n\tcase \"${debian_digest%:*}\" in\n\tsha512) digest=$(echo \"${debian_digest#*:}==\" | base64 -d | xxd -p -c -) ||\n\t\terror_exit \"Failed to decode the digest from ${debian_digest}\" ;;\n\t*) error_exit \"Unsupported digest type: ${debian_digest%:*}\" ;;\n\tesac\n\techo \"${debian_digest/:*/:}${digest}\"\n}\n\n# debian_image_url_timestamped prints the latest image URL and its digest for the given flavor, version, arch, and path suffix.\nfunction debian_image_url_timestamped() {\n\tlocal url_spec=$1 debian_downloaded_json jq_filter upload_entry timestamp timestamped_url_spec location arch digest\n\tdebian_downloaded_json=$(debian_downloaded_json \"${url_spec}\")\n\t# shellcheck disable=SC2016\n\tjq_filter='\n\t\t[.items[]|select(.kind == \"Upload\")|\n\t\tselect(.metadata.labels.\"upload.cloud.debian.org/image-format\" == $ARGS.named.url_spec.image_format)]|first\n\t'\n\tupload_entry=$(jq -e -r --argjson url_spec \"${url_spec}\" \"${jq_filter}\" \"${debian_downloaded_json}\") ||\n\t\terror_exit \"Failed to find the upload entry from ${debian_downloaded_json}\"\n\ttimestamp=$(jq -e -r '.metadata.labels.\"cloud.debian.org/version\"' <<<\"${upload_entry}\") ||\n\t\terror_exit \"Failed to get the timestamp from ${upload_entry}\"\n\ttimestamped_url_spec=$(json_vars timestamp <<<\"${url_spec}\")\n\tlocation=$(debian_location_from_url_spec \"${timestamped_url_spec}\")\n\tlocation=$(validate_url_without_redirect \"${location}\")\n\tarch=$(jq -e -r '.arch' <<<\"${url_spec}\") || error_exit \"missing arch in ${url_spec}\"\n\tarch=$(limayaml_arch \"${arch}\")\n\tdigest=$(debian_digest_from_upload_entry \"${upload_entry}\")\n\tjson_vars location arch digest\n}\n\n# debian_image_url_not_timestamped prints the release image URL for the given url_spec(JSON)\nfunction debian_image_url_not_timestamped() {\n\tlocal url_spec=$1 location arch\n\tlocation=$(debian_location_from_url_spec \"${url_spec}\")\n\tlocation=$(validate_url_without_redirect \"${location}\")\n\tarch=$(jq -e -r '.arch' <<<\"${url_spec}\") || error_exit \"missing arch in ${url_spec}\"\n\tarch=$(limayaml_arch \"${arch}\")\n\tjson_vars location arch\n}\n\n# debian_version_resolve_aliases resolves the version aliases.\n# e.g.\n# ```console\n# debian_version_resolve_aliases testing\n# 13\n# debian_version_resolve_aliases stable\n# 12\n# debian_version_resolve_aliases oldstable\n# 11\n# debian_version_resolve_aliases bookworm\n# 12\n# debian_version_resolve_aliases 10\n# 10\n# debian_version_resolve_aliases ''\n#\n# ```\nfunction debian_version_resolve_aliases() {\n\tlocal version=$1\n\t[[ -v debian_alias_to_codename[${version}] ]] && version=${debian_alias_to_codename[${version}]}\n\t[[ -v debian_codename_to_version[${version}] ]] && version=${debian_codename_to_version[${version}]}\n\t[[ -v debian_version_to_codename[${version}] ]] || error_exit \"Unsupported version: ${version}\"\n\t[[ -z ${version} ]] || echo \"${version}\"\n}\n\nfunction debian_arch_from_location_basename() {\n\tlocal location=$1 location_basename arch\n\tlocation_basename=$(basename \"${location}\")\n\tlocation_basename=${location_basename/-backports/}\n\tarch=$(echo \"${location_basename}\" | cut -d- -f4 | cut -d. -f1)\n\t[[ -n ${arch} ]] || error_exit \"Failed to get arch from ${location}\"\n\techo \"${arch}\"\n}\n\nfunction debian_file_extension_from_location_basename() {\n\tlocal location=$1 location_basename file_extension\n\tlocation_basename=$(basename \"${location}\")\n\tfile_extension=$(echo \"${location_basename}\" | cut -d. -f2-) # remove the first field\n\t[[ -n ${file_extension} ]] || error_exit \"Failed to get file extension from ${location}\"\n\techo \"${file_extension}\"\n}\n\nfunction debian_image_format_from_file_extension() {\n\tlocal file_extension=$1\n\tcase \"${file_extension}\" in\n\tjson) echo \"json\" ;;\n\tqcow2) echo \"qcow2\" ;;\n\traw) echo \"raw\" ;;\n\ttar.xz) echo \"internal\" ;;\n\t*) error_exit \"Unsupported file extension: ${file_extension}\" ;;\n\tesac\n}\n\n# debian_url_spec_from_location returns the URL spec for the given location.\n# If the location is not supported, it returns 1.\n# e.g.\n# ```console\n# debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2\n# {\"backports\":false,\"daily\":false,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/20241004-1890/debian-12-generic-amd64-20241004-1890.qcow2\n# {\"backports\":false,\"daily\":false,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/daily/latest/debian-12-genericcloud-amd64-daily.qcow2\n# {\"backports\":false,\"daily\":true,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm/daily/20241019-1905/debian-12-genericcloud-amd64-daily-20241019-1905.qcow2\n# {\"backports\":false,\"daily\":true,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm-backports/latest/debian-12-backports-genericcloud-amd64.qcow2\n# {\"backports\":true,\"daily\":false,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm-backports/20241004-1890/debian-12-backports-genericcloud-amd64-20241004-1890.qcow2\n# {\"backports\":true,\"daily\":false,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# debian_url_spec_from_location https://cloud.debian.org/images/cloud/bookworm-backports/daily/latest/debian-12-backports-genericcloud-amd64-daily.qcow2\n# {\"backports\":true,\"daily\":true,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# 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\n# {\"backports\":true,\"daily\":true,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\",\"image_format\":\"qcow2\"}\n# ```\n# shellcheck disable=SC2034\nfunction debian_url_spec_from_location() {\n\tlocal location=$1 backports=false daily=false timestamp='' codename version='' arch file_extension image_format\n\tlocal -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]'\n\tcase \"${location}\" in\n\t${debian_base_url}*-backports/*) backports=true ;;&\n\t${debian_base_url}*/daily/*) daily=true ;;&\n\t${debian_base_url}*/${timestamp_pattern}/*) [[ ${location} =~ ${timestamp_pattern} ]] && timestamp=${BASH_REMATCH[0]} ;;\n\t${debian_base_url}*/latest/*) timestamp='' ;;\n\t*)\n\t\t# echo \"Unsupported image location: ${location}\" >&2\n\t\treturn 1\n\t\t;;\n\tesac\n\tcodename=$(echo \"${location#\"${debian_base_url}\"}\" | cut -d/ -f1 | cut -d- -f1)\n\t[[ -v debian_codename_to_version[${codename}] ]] || error_exit \"Unknown codename: ${codename}\"\n\tversion=${debian_codename_to_version[${codename}]}\n\tarch=$(debian_arch_from_location_basename \"${location}\")\n\tfile_extension=$(debian_file_extension_from_location_basename \"${location}\")\n\timage_format=$(debian_image_format_from_file_extension \"${file_extension}\")\n\tjson_vars backports daily timestamp version arch file_extension image_format\n}\n\n# debian_location_from_url_spec returns the location for the given URL spec.\n# e.g.\n# ```console\n# debian_location_from_url_spec '{\"backports\":false,\"daily\":false,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2\n# debian_location_from_url_spec '{\"backports\":false,\"daily\":false,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm/20241019-1905/debian-12-generic-amd64-20241019-1905.qcow2\n# debian_location_from_url_spec '{\"backports\":false,\"daily\":true,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm/daily/latest/debian-12-genericcloud-amd64-daily.qcow2\n# debian_location_from_url_spec '{\"backports\":false,\"daily\":true,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm/daily/20241019-1905/debian-12-generic-amd64-daily-20241019-1905.qcow2\n# debian_location_from_url_spec '{\"backports\":true,\"daily\":false,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm-backports/latest/debian-12-backports-genericcloud-amd64.qcow2\n# debian_location_from_url_spec '{\"backports\":true,\"daily\":false,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm-backports/20241019-1905/debian-12-backports-genericcloud-amd64-20241019-1905.qcow2\n# debian_location_from_url_spec '{\"backports\":true,\"daily\":true,\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm-backports/daily/latest/debian-12-backports-genericcloud-amd64-daily.qcow2\n# debian_location_from_url_spec '{\"backports\":true,\"daily\":true,\"timestamp\":\"20241019-1905\",\"version\":12,\"arch\":\"amd64\",\"file_extension\":\"qcow2\"}'\n# https://cloud.debian.org/images/cloud/bookworm-backports/daily/20241019-1905/debian-12-backports-genericcloud-amd64-daily-20241019-1905.qcow2\n# ```\nfunction debian_location_from_url_spec() {\n\tlocal url_spec=$1 base_url version backports daily timestamp arch file_extension\n\tbase_url=${debian_base_url}\n\tversion=$(jq -e -r '.version' <<<\"${url_spec}\")\n\t[[ -v debian_version_to_codename[${version}] ]] || error_exit \"Unsupported version: ${version}\"\n\tbase_url+=${debian_version_to_codename[${version}]}\n\tbackports=$(jq -r 'if .backports then \"-backports\" else empty end' <<<\"${url_spec}\")\n\tbase_url+=${backports}/\n\tdaily=$(jq -r 'if .daily then \"daily\" else empty end' <<<\"${url_spec}\")\n\tbase_url+=${daily:+${daily}/}\n\ttimestamp=$(jq -r 'if .timestamp then .timestamp else empty end' <<<\"${url_spec}\")\n\tbase_url+=${timestamp:-latest}/\n\tarch=$(jq -e -r '.arch' <<<\"${url_spec}\")\n\tfile_extension=$(jq -e -r '.file_extension' <<<\"${url_spec}\")\n\tbase_url+=debian-${version}${backports}-${debian_target_vendor}-${arch}${daily:+-${daily}}${timestamp:+-${timestamp}}.${file_extension}\n\techo \"${base_url}\"\n}\n\n# debian_cache_key_for_image_kernel_overriding returns the cache key for the given location, kernel_location, flavor, and version.\n# If the image location is not supported, it returns 1.\n# kernel_location is not validated.\n# e.g.\n# ```console\n# 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\n# debian_latest_24.04-minimal-amd64-release-.img\n# 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://...\n# debian_latest_with_kernel_24.04-minimal-amd64-release-.img\n# debian_cache_key_for_image_kernel_overriding https://cloud-images.debian.com/releases/24.04/release/debian-24.04-server-cloudimg-amd64.img null\n# debian_release_24.04-server-amd64-.img\n# ```\nfunction debian_cache_key_for_image_kernel_overriding() {\n\tlocal location=$1 kernel_location=${2:-null} url_spec with_kernel='' version backports arch daily timestamped file_extension\n\turl_spec=$(debian_url_spec_from_location \"${location}\")\n\t[[ ${kernel_location} != \"null\" ]] && with_kernel=_with_kernel\n\tversion=$(jq -r '.version|if . then \"-\\(.)\" else empty end' <<<\"${url_spec}\")\n\tbackports=$(jq -r 'if .backports then \"-backports\" else empty end' <<<\"${url_spec}\")\n\tarch=$(jq -e -r '.arch' <<<\"${url_spec}\")\n\tdaily=$(jq -r 'if .daily then \"-daily\" else empty end' <<<\"${url_spec}\")\n\ttimestamped=$(jq -r 'if .timestamp then \"-timestamped\" else empty end' <<<\"${url_spec}\")\n\tfile_extension=$(jq -e -r '.file_extension' <<<\"${url_spec}\")\n\techo \"debian${with_kernel}${version}${backports}-${debian_target_vendor}-${arch}${daily}${timestamped}.${file_extension}\"\n}\n\nfunction debian_image_entry_for_image_kernel_overriding() {\n\tlocal location=$1 kernel_location=$2 overriding=${3:-\"{}\"} url_spec timestamped\n\t[[ ${kernel_location} == \"null\" ]] || error_exit \"Updating image with kernel is not supported\"\n\turl_spec=$(debian_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\ttimestamped=$(jq -r 'if .timestamp then \"timestamped\" else \"not_timestamped\" end' <<<\"${url_spec}\")\n\n\tlocal image_entry\n\timage_entry=$(debian_image_url_\"${timestamped}\" \"${url_spec}\")\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"debian\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"debian\")\n\tfi\n\t# required functions for Debian\n\tfunction debian_cache_key_for_image_kernel() { debian_cache_key_for_image_kernel_overriding \"$@\"; }\n\tfunction debian_image_entry_for_image_kernel() { debian_image_entry_for_image_kernel_overriding \"$@\"; }\n\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding=\"{}\"\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tdebian_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--backports | --daily | --timestamped)\n\t\toverriding=$(json \"${1#--}\" true <<<\"${overriding}\")\n\t\t;;\n\t--backports=* | --daily=* | --timestamped=*)\n\t\toverriding=$(\n\t\t\tkey=${1#--} value=$(validate_boolean \"${1#*=}\")\n\t\t\tjson \"${key%%=*}\" \"${value}\" <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t--version)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding=$(\n\t\t\t\tversion=$(debian_version_resolve_aliases \"$2\")\n\t\t\t\tjson_vars version <<<\"${overriding}\"\n\t\t\t)\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version requires a value\"\n\t\tfi\n\t\t;;\n\t--version=*)\n\t\toverriding=$(\n\t\t\tversion=$(debian_version_resolve_aliases \"${1#*=}\")\n\t\t\tjson_vars version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tdebian_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tdebian_cache_key_for_image_kernel_overriding \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\tdebian_image_entry_for_image_kernel_overriding \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-fedora.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction fedora_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the Fedora Linux image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version (<version number>|release|development[/<version number>]|rawhide)] <template.yaml>...\n\nDescription:\n  This script updates the Fedora Linux image location in the specified templates.\n  Image location basename format:\n\n    Fedora-Cloud-Base[-<target vendor>]-<version>-<build info>.<arch>.qcow2\n    Fedora-Cloud-Base[-<target vendor>].<arch>-<version>-<build info>.qcow2\n\n  Published Fedora Linux image information is fetched from the following URL:\n\n    ${fedora_image_list_url}\n\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the Fedora Linux image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Fedora Linux image location to version 41 in ~/.lima/fedora/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version 41 ~/.lima/fedora/lima.yaml\n  $ limactl factory-reset fedora\n\nFlags:\n  --version <version> Use the specified version.\n                      The version must be <version number>, 'release', 'development[/<version number>]', or 'rawhide'.\n  -h, --help          Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction fedora_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\"\n\t\t\t^https://download\\\\.fedoraproject\\\\.org/pub/fedora/linux/(?<path_version>(releases|development)/(\\\\d+|rawhide))/Cloud/(?<path_arch>[^/]+)/images/\n\t\t\tFedora-Cloud-Base(?<target_vendor>-Generic)?(\n\t\t\t\t(-(?<version_before_arch>\\\\d+|Rawhide)-(?<build_info_before_arch>[^-]+)(?<arch_postfix>\\\\.[^.]+))|\n\t\t\t\t((?<arch_prefix>\\\\.[^-]+)-(?<version_after_arch>\\\\d+|Rawhide)-(?<build_info_after_arch>[^-]+))\n\t\t\t)\\\\.(?<file_extension>.*)$\n\t\t\";\"x\") |\n\t\t.version = (.version_before_arch // .version_after_arch) |\n\t\t.build_info = (.build_info_before_arch // .build_info_after_arch ) |\n\t\tmap_values(. // empty) # remove null values\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\techo \"${url_spec}\"\n}\n\nreadonly fedora_jq_filter_directory='\"https://download.fedoraproject.org/pub/fedora/linux/\\(.path_version)/Cloud/\\(.path_arch)/images/\"'\nreadonly fedora_jq_filter_filename='\n\t\"Fedora-Cloud-Base\\(.target_vendor // \"\")\\(.arch_prefix // \"\")-\\(.version)-\\(.build_info)\\(.arch_postfix // \"\").\\(.file_extension)\"\n'\n\nreadonly fedora_jq_filter_checksum_filename='\n\t\"Fedora-Cloud-\\(\n\t\tif .path_version|startswith(\"development/\") then\n\t\t\t\"images-\\(.version)-\\(.path_arch)-\\(.build_info)\"\n\t\telse\n\t\t\t\"\\(.version)-\\(.build_info)-\\(.path_arch)\"\n\t\tend\n\t)-CHECKSUM\"\n'\n\n# print the location for the given URL spec\nfunction fedora_location_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${fedora_jq_filter_directory} + ${fedora_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction fedora_image_directory_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${fedora_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image directory for ${url_spec}\"\n}\n\nfunction fedora_image_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${fedora_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image filename for ${url_spec}\"\n}\n\nfunction fedora_image_checksum_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${fedora_jq_filter_checksum_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the checksum filename for ${url_spec}\"\n}\n\nreadonly fedora_image_list_url='https://dl.fedoraproject.org/pub/fedora/imagelist-fedora'\n#\nfunction fedora_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 arch image_list spec_for_query latest_version_info\n\t# shellcheck disable=SC2034\n\tarch=$(jq -r '.path_arch' <<<\"${url_spec}\")\n\timage_list=$(download_to_cache \"${fedora_image_list_url}\")\n\tspec_for_query=$(jq -r '. | {path_version, path_arch, file_extension}' <<<\"${url_spec}\")\n\tlatest_version_info=$(jq -e -Rrs --argjson spec \"${spec_for_query}\" '\n\t\t[\n\t\t\tsplit(\"\\n\").[] |\n\t\t\tcapture(\"\n\t\t\t\t^\\\\./linux/(?<path_version>\\($spec.path_version))/Cloud/\\($spec.path_arch)/images/\n\t\t\t\tFedora-Cloud-Base(?<target_vendor>-Generic)?(\n\t\t\t\t\t-(?<version_before_arch>\\\\d+|Rawhide)-(?<build_info_before_arch>[^-]+)(?<arch_postfix>\\\\.\\($spec.path_arch))|\n\t\t\t\t\t(?<arch_prefix>\\\\.\\($spec.path_arch))-(?<version_after_arch>\\\\d+|Rawhide)-(?<build_info_after_arch>[^-]+)\n\t\t\t\t)\\\\.\\($spec.file_extension)$\n\t\t\t\";\"x\") |\n\t\t\t.version = (.version_before_arch // .version_after_arch) |\n\t\t\t.build_info = (.build_info_before_arch // .build_info_after_arch) |\n\t\t\t# do not remove null values. we need them for creating newer_url_spec\n\t\t\t# map_values(. // empty) |\n\t\t\t.version_number_array = ([(if (.version|test(\"\\\\d+\")) then (.version|tonumber) else .version end)] + [.build_info | scan(\"\\\\d+\") | tonumber])\n\t\t] | sort_by(.version_number_array) | last\n\t' <\"${image_list}\" || error_exit \"Failed to get the latest version info for ${spec_for_query}\")\n\t[[ -n ${latest_version_info} ]] || return\n\tlocal newer_url_spec directory filename location sha512sum_location downloaded_sha256sum digest\n\t# prefer the v<major>.<minor> in the path\n\tnewer_url_spec=$(jq -e -r \". + ${latest_version_info}\" <<<\"${url_spec}\")\n\tdirectory=$(fedora_image_directory_from_url_spec \"${newer_url_spec}\")\n\tfilename=$(fedora_image_filename_from_url_spec \"${newer_url_spec}\")\n\tlocation=\"${directory}${filename}\"\n\t# validate the location. use original url since the location may be redirected to some mirror\n\tlocation=$(validate_url_without_redirect \"${location}\")\n\tsha512sum_location=\"${directory}$(fedora_image_checksum_filename_from_url_spec \"${newer_url_spec}\")\"\n\t# download the checksum file and get the sha256sum\n\t# cache original url since the checksum file may be redirected to some mirror\n\tdownloaded_sha256sum=$(download_to_cache_without_redirect \"${sha512sum_location}\")\n\tdigest=\"sha256:$(awk \"/SHA256 \\(${filename}\\) =/{print \\$4}\" \"${downloaded_sha256sum}\")\"\n\t[[ -n ${digest} ]] || error_exit \"Failed to get the digest for ${filename}\"\n\tjson_vars location arch digest\n}\n\nfunction fedora_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(fedora_url_spec_from_location \"${location}\")\n\tjq -r '[\"fedora\", .path_version, .target_vendor, .path_arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction fedora_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-'{}'} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on Fedora Linux\" >&2\n\turl_spec=$(fedora_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\timage_entry=$(fedora_latest_image_entry_for_url_spec \"${url_spec}\")\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"fedora\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"fedora\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding='{}'\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tfedora_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version requires a value\"\n\t\tfi\n\t\t;&\n\t--version=*)\n\t\tversion=${version:-${1#*=}}\n\t\toverriding=$(\n\t\t\t[[ ${version} =~ ^[0-9]+$ ]] && path_version=\"releases/${version}\"\n\t\t\t[[ ${version} =~ ^releases?$ ]] && path_version=\"releases/\\d+\"\n\t\t\t[[ ${version} == \"development\" ]] && path_version=\"development/\\d+\"\n\t\t\t[[ ${version} =~ ^(releases|development)/([0-9]+)$ ]] && path_version=\"${version}\"\n\t\t\t[[ ${version} =~ ^(development/)?rawhide$ ]] && path_version=\"development/rawhide\"\n\t\t\t[[ -n ${path_version:-} ]] ||\n\t\t\t\terror_exit \"The version must be <version number>, 'release', 'development[/<version number>'], or 'rawhide'.\"\n\t\t\tjson_vars path_version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tfedora_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tfedora_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\tfedora_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-freebsd.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction freebsd_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the FreeBSD image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version <major>.<minor>] <template.yaml>...\n\nDescription:\n  This script updates the FreeBSD image location in the specified templates.\n  Image location basename format:\n\n    FreeBSD-<version>-RELEASE-<arch>[-BASIC-CLOUDINIT]-<fs>.<format>.xz\n\n  Published FreeBSD image information is fetched from the following URL:\n\n    ${freebsd_archive_url}\n\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the FreeBSD image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the FreeBSD image location to version 14.3 in ~/.lima/freebsd/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version 14.3 ~/.lima/freebsd/lima.yaml\n  $ limactl factory-reset freebsd\n\nFlags:\n  --version <major>.<minor>  Use the specified <major>.<minor> version.\n  -h, --help                 Print this help message\nHELP\n}\n\n# ftp-archive.freebsd.org doesn't seem to support HTTPS\nreadonly freebsd_archive_url='http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/'\n\n# freebsd_url_spec_from_location prints the URL spec for the given location.\n# If the location is not supported, it returns 1.\n# e.g.\n# ```console\n# 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\n# {\"version\":\"15.0\",\"dir_arch\":\"amd64\",\"filename_arch\":\"amd64\",\"cloudinit\":true,\"fs\":\"zfs\",\"format\":\"raw\"}\n# 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\n# {\"version\":\"15.0\",\"dir_arch\":\"aarch64\",\"filename_arch\":\"arm64-aarch64\",\"cloudinit\":true,\"fs\":\"zfs\",\"format\":\"raw\"}\n# ```\nfunction freebsd_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\"\n\t\t^http://ftp-archive\\\\.freebsd\\\\.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/\n\t\t(?<version>\\\\d+\\\\.\\\\d+)-RELEASE/(?<dir_arch>[^/]+)/Latest/\n\t\tFreeBSD-\\\\d+\\\\.\\\\d+-RELEASE-(?<filename_arch>arm64-aarch64|riscv-riscv64|amd64)\n\t\t(?<cloudinit>-BASIC-CLOUDINIT)?-(?<fs>zfs|ufs)\\\\.(?<format>raw|qcow2)\\\\.xz$\n\t\";\"x\") | .cloudinit = (.cloudinit != null)\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\techo \"${url_spec}\"\n}\n\nreadonly freebsd_jq_filter_directory='\"http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/VM-IMAGES/\\(.version)-RELEASE/\\(.dir_arch)/Latest/\"'\nreadonly freebsd_jq_filter_filename='\n\t\"FreeBSD-\\(.version)-RELEASE-\\(.filename_arch)\\(if .cloudinit then \"-BASIC-CLOUDINIT\" else \"\" end)-\\(.fs).\\(.format).xz\"\n'\n\n# freebsd_location_from_url_spec prints the location for the given URL spec.\nfunction freebsd_location_from_url_spec() {\n\tlocal url_spec=$1\n\tjq -e -r \"${freebsd_jq_filter_directory} + ${freebsd_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction freebsd_directory_from_url_spec() {\n\tlocal url_spec=$1\n\tjq -e -r \"${freebsd_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the directory for ${url_spec}\"\n}\n\nfunction freebsd_filename_from_url_spec() {\n\tlocal url_spec=$1\n\tjq -e -r \"${freebsd_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the filename for ${url_spec}\"\n}\n\nfunction freebsd_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 releases_page latest_version newer_url_spec location filename checksum_url downloaded_checksum digest arch\n\treleases_page=$(download_to_cache \"${freebsd_archive_url}\")\n\n\t# Find the latest RELEASE version with the same major version as in url_spec\n\tlatest_version=$(jq -e -Rrs --argjson spec \"${url_spec}\" '\n\t\t[\n\t\t\tsplit(\"\\n\").[] |\n\t\t\tselect(test(\"href=\\\"\\\\d+\\\\.\\\\d+-RELEASE/\\\"\")) |\n\t\t\tcapture(\"href=\\\"(?<ver>\\\\d+\\\\.\\\\d+)-RELEASE/\\\"\") |\n\t\t\t.ver |\n\t\t\tselect((split(\".\")[0] | tonumber) == ($spec.version | split(\".\")[0] | tonumber))\n\t\t] |\n\t\tsort_by(split(\".\") | map(tonumber)) |\n\t\tlast\n\t' <\"${releases_page}\") ||\n\t\terror_exit \"No RELEASE found for FreeBSD $(jq -r '.version | split(\".\")[0]' <<<\"${url_spec}\").x in ${freebsd_archive_url}\"\n\t[[ -n ${latest_version} ]] || return\n\tnewer_url_spec=$(jq -e -r \". + {version: \\\"${latest_version}\\\"}\" <<<\"${url_spec}\")\n\tfilename=$(freebsd_filename_from_url_spec \"${newer_url_spec}\")\n\tchecksum_url=\"$(freebsd_directory_from_url_spec \"${newer_url_spec}\")CHECKSUM.SHA256\"\n\tdownloaded_checksum=$(download_to_cache \"${checksum_url}\")\n\tdigest=\"sha256:$(awk \"/SHA256 \\\\(${filename}\\\\) =/{print \\$4}\" \"${downloaded_checksum}\")\"\n\t[[ -n ${digest} ]] || error_exit \"Failed to get the digest for ${filename}\"\n\tlocation=$(freebsd_location_from_url_spec \"${newer_url_spec}\")\n\tlocation=$(validate_url \"${location}\")\n\tarch=$(jq -e -r '.dir_arch' <<<\"${newer_url_spec}\")\n\tarch=$(limayaml_arch \"${arch}\")\n\tjson_vars location arch digest\n}\n\nfunction freebsd_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(freebsd_url_spec_from_location \"${location}\")\n\tjq -r '[\"freebsd\", (.version | split(\".\")[0]), .dir_arch, (if .cloudinit then \"cloudinit\" else \"\" end), .fs, .format] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction freebsd_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-'{}'} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on FreeBSD\" >&2\n\turl_spec=$(freebsd_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\timage_entry=$(freebsd_latest_image_entry_for_url_spec \"${url_spec}\")\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"freebsd\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"freebsd\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding='{}'\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tfreebsd_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version requires a value\"\n\t\tfi\n\t\t;&\n\t--version=*)\n\t\tversion=${version:-${1#*=}}\n\t\toverriding=$(\n\t\t\t[[ ${version} =~ ^[0-9]+\\.[0-9]+$ ]] ||\n\t\t\t\terror_exit \"The version must be in the format <major>.<minor>\"\n\t\t\tjson_vars version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tfreebsd_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tfreebsd_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\tfreebsd_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-macos.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction macos_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the macOS image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") <template.yaml>...\n\nDescription:\n  This script updates the macOS image location in the specified templates.\n  Image location format:\n\n    https://updates.cdn-apple.com/.../UniversalMac_<version>_<build>_Restore.ipsw\n\n  Published macOS image information (URL and SHA256 digest) is fetched from\n  the ipsw.me API:\n\n    https://api.ipsw.me/v4/device/VirtualMac2,1\n\n  The downloaded JSON will be cached in the Lima cache directory.\n\nExamples:\n  Update the macOS image location in templates/_images/macos-*.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/_images/macos-*.yaml\n\nFlags:\n  -h, --help           Print this help message\nHELP\n}\n\n# URL of the ipsw.me device API endpoint for Apple Virtual Machine 1 (VirtualMac2,1).\n# This returns all available macOS IPSW firmwares with URL and SHA256 digest.\nreadonly macos_ipsw_me_device_url=\"https://api.ipsw.me/v4/device/VirtualMac2,1\"\n\n# macos_url_spec_from_location prints the URL spec for the given location.\n# If the location is not a macOS IPSW URL from Apple's CDN, it returns 1.\n# e.g.\n# ```console\n# 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\n# {\"version\":\"15.6\",\"major_version\":\"15\",\"build\":\"24G84\"}\n# 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\n# {\"version\":\"15.6.1\",\"major_version\":\"15\",\"build\":\"24G90\"}\n# ```\nfunction macos_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\"\n\t\t^https://updates\\\\.cdn-apple\\\\.com/[^/]+/fullrestores/[^/]+/[^/]+/\n\t\tUniversalMac_(?<version>(?<major_version>\\\\d+)(?:\\\\.\\\\d+)+)_(?<build>[^_]+)_Restore\\\\.ipsw$\n\t\";\"x\")\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\techo \"${url_spec}\"\n}\n\n# macos_latest_image_entry_for_url_spec prints the latest image entry for the given URL spec.\n# e.g.\n# ```console\n# macos_latest_image_entry_for_url_spec '{\"major_version\":\"15\"}'\n# {\"location\":\"https://updates.cdn-apple.com/.../UniversalMac_15.6.1_24G90_Restore.ipsw\",\"arch\":\"aarch64\",\"digest\":\"sha256:...\"}\n# ```\nfunction macos_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 major_version ipsw_me_file latest_entry location digest arch=\"aarch64\"\n\tmajor_version=$(jq -r '.major_version' <<<\"${url_spec}\")\n\tipsw_me_file=$(download_to_cache \"${macos_ipsw_me_device_url}\")\n\tlatest_entry=$(jq -e -r --arg major \"${major_version}\" '\n\t\t.firmwares |\n\t\t[.[] | select(.version | test(\"^\" + $major + \"\\\\.\"))] |\n\t\tsort_by(.releasedate) |\n\t\tlast\n\t' \"${ipsw_me_file}\")\n\t[[ -n ${latest_entry} && ${latest_entry} != \"null\" ]] ||\n\t\terror_exit \"Failed to get the latest macOS ${major_version} image from ${macos_ipsw_me_device_url}\"\n\tlocation=$(jq -r '.url' <<<\"${latest_entry}\")\n\tdigest=$(jq -r '.sha256sum // \"\" | if . != \"\" then \"sha256:\" + . else \"\" end' <<<\"${latest_entry}\")\n\tjson_vars location arch digest\n}\n\nfunction macos_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(macos_url_spec_from_location \"${location}\")\n\tjq -r '[\"macos\", .major_version] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction macos_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on macOS\" >&2\n\turl_spec=$(macos_url_spec_from_location \"${location}\")\n\timage_entry=$(macos_latest_image_entry_for_url_spec \"${url_spec}\")\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"macos\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"macos\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tmacos_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tmacos_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tmacos_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\tmacos_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-opensuse.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction opensuse_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the openSUSE Linux image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version-major-minor (<major>.<minor>|current|stable|tumbleweed)|--version-major <major> --version-minor <minor>] <template.yaml>...\n\nDescription:\n  This script updates the openSUSE Linux image location in the specified templates.\n  Image location basename format:\n\n    openSUSE-(Leap-<major minor version>|Tumbleweed)-Minimal-VM.<arch>-Cloud.qcow2\n\n  Published openSUSE Linux image information is fetched from the following URLs:\n\n    Leap:\n\t  <major>.<minor>: https://download.opensuse.org/distribution/leap/<major>.<minor>/appliances/?jsontable\n\t  current: https://download.opensuse.org/distribution/openSUSE-current/appliances/?jsontable\n\t  stable: https://download.opensuse.org/distribution/openSUSE-stable/appliances/?jsontable\n\n    Tumbleweed:\n      x86_64: https://download.opensuse.org/tumbleweed/appliances/?jsontable\n\t  not x86_64: https://download.opensuse.org/ports/<arch>/tumbleweed/appliances/?jsontable\n\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the openSUSE Linux image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the openSUSE Linux image location to version 15.6 in ~/.lima/opensuse/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version-major-minor 15.6 ~/.lima/opensuse/lima.yaml\n  $ limactl factory-reset opensuse\n\n  Update the openSUSE Linux image location to tumbleweed in ~/.lima/opensuse/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version-major-minor tumbleweed ~/.lima/opensuse/lima.yaml\n  $ limactl factory-reset opensuse\n\nFlags:\n  --version-major-minor (<major>.<minor>|current|stable|tumbleweed) Use the specified <major>.<minor> version or\n                                                                    aliases \"current\", \"stable\", or \"tumbleweed\".\n                                                                    The <major>.<minor> version must be 15.0 or later.\n  --version-major <major> --version-minor <minor>                   Use the specified <major> and <minor> version.\n  -h, --help                                                        Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction opensuse_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\"\n\t\t^https://download\\\\.opensuse\\\\.org/(?:\n\t\t\tdistribution/(?:\n\t\t\t\tleap/(?<path_version_leap>\\\\d+\\\\.\\\\d+)|\n\t\t\t\topenSUSE-(?<path_version_leap_alias>current|stable)\n\t\t\t)|\n\t\t\t(?:ports/aarch64/)?(?<path_version_tumbleweed>tumbleweed)\n\t\t)/appliances/\n\t\topenSUSE-(?<version>Leap-\\\\d+\\\\.\\\\d+|Tumbleweed)-Minimal-VM\n\t\t\\\\.(?<arch>[^-]+)(?<major_minor_patch>-\\\\d+\\\\.\\\\d+\\\\.\\\\d+)?-(?<target_vendor>.*)(?<build_info>-Build\\\\d+\\\\.\\\\d+)?\\\\.(?<file_extension>.*)$\n\t\";\"x\") |\n\t.path_version = (.path_version_leap // .path_version_leap_alias // .path_version_tumbleweed)\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\techo \"${url_spec}\"\n}\n\nreadonly opensuse_jq_filter_directory='\"https://download.opensuse.org/\\(\n\tif .path_version == \"tumbleweed\" then\n\t\tif .arch != \"x86_64\" then\n\t\t\t\"ports/\\(.arch)/\"\n\t\telse\n\t\t\t\"\"\n\t\tend + \"tumbleweed\"\n\telse\n\t\t\"distribution/\" +\n\t\tif .path_version == \"current\" or .path_version == \"stable\" then\n\t\t\t\"openSUSE-\\(.path_version)\"\n\t\telse\n\t\t\t\"leap/\\(.path_version)\"\n\t\tend\n\tend\n)/appliances/\"'\nreadonly opensuse_jq_filter_filename='\n\t\"openSUSE-\\(\n\t\tif .version == \"tumbleweed\" then \"Tumbleweed\" else \"Leap-\\(.version)\" end\n\t)-Minimal-VM.\\(.arch)\\(\n\t\tif .major_minor_patch then .major_minor_patch else \"\" end\n\t)-\\(.target_vendor)\\(\n\t\tif .build_info then .build_info else \"\" end\n\t).\\(.file_extension)\"\n'\n\n# print the location for the given URL spec\nfunction opensuse_location_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${opensuse_jq_filter_directory} + ${opensuse_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction opensuse_image_directory_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${opensuse_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image directory for ${url_spec}\"\n}\n\nfunction opensuse_image_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${opensuse_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image filename for ${url_spec}\"\n}\n\nfunction opensuse_json_url_from_url_spec() {\n\tlocal -r url_spec=$1\n\tlocal json_url\n\tjson_url=\"$(opensuse_image_directory_from_url_spec \"${url_spec}\")?jsontable\"\n\techo \"${json_url}\"\n}\n\n#\nfunction opensuse_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 arch json_url downloaded_json latest_version_info\n\t# shellcheck disable=SC2034\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\tjson_url=\"$(opensuse_image_directory_from_url_spec \"${url_spec}\")?jsontable\"\n\tdownloaded_json=$(download_to_cache \"${json_url}\")\n\tlatest_version_info=$(jq -e -r --argjson spec \"${url_spec}\" '\n\t\t[\n\t\t\t.data |sort_by(.mtime)|.[].name|\n\t\t\tif $spec.major_minor_patch then\n\t\t\t\tcapture(\n\t\t\t\t\t\"^openSUSE-(?:Leap-(?<version_leap>\\\\d+\\\\.\\\\d+)|(?<version_tumbleweed>Tumbleweed))-Minimal-VM\n\t\t\t\t\t\\\\.\\($spec.arch)(?<major_minor_patch>-\\\\d+\\\\.\\\\d+\\\\.\\\\d+)?-\\($spec.target_vendor)(?<build_info>-Build\\\\d+\\\\.\\\\d+)?\\\\.\\($spec.file_extension)$\"\n\t\t\t\t\t;\"x\"\n\t\t\t\t)\n\t\t\telse\n\t\t\t\tcapture(\n\t\t\t\t\t\"^openSUSE-(?:Leap-(?<version_leap>\\\\d+\\\\.\\\\d+)|(?<version_tumbleweed>Tumbleweed))-Minimal-VM\n\t\t\t\t\t\\\\.\\($spec.arch)-\\($spec.target_vendor)\\\\.\\($spec.file_extension)$\"\n\t\t\t\t\t;\"x\"\n\t\t\t\t)\n\t\t\tend |\n\t\t\t.version = (.version_leap // (.version_tumbleweed|ascii_downcase)) |\n\t\t\t.version_number_array = ([.version | scan(\"\\\\d+\") | tonumber])\n\t\t] | sort_by(.version_number_array, .image_revision) | last\n\t' <\"${downloaded_json}\")\n\t[[ -n ${latest_version_info} ]] || return\n\tlocal newer_url_spec location\n\t# prefer the <major>.<minor> in the path\n\tnewer_url_spec=$(jq -e -r \". + ${latest_version_info} | .path_version = .version\" <<<\"${url_spec}\")\n\tlocation=$(opensuse_location_from_url_spec \"${newer_url_spec}\")\n\tlocation=$(validate_url_without_redirect \"${location}\")\n\n\t# Digest is not used here because URLs containing dates are not retained long-term.\n\t# Instead, URLs without dates must be used, and their content is often updated without a URL change,\n\t# resulting in only the digest being updated. Therefore, recording the digest is not meaningful.\n\t#\n\t# local sha256sum_location downloaded_sha256sum filename digest\n\t# sha256sum_location=\"${location}.sha256\"\n\t# downloaded_sha256sum=$(download_to_cache \"${sha256sum_location}\")\n\t# filename=$(opensuse_image_filename_from_url_spec \"${newer_url_spec}\")\n\t# digest=\"sha256:$(awk '{print $1}' <\"${downloaded_sha256sum}\")\"\n\t# [[ -n ${digest} ]] || error_exit \"Failed to get the digest for ${filename}\"\n\tjson_vars location arch # digest\n}\n\nfunction opensuse_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(opensuse_url_spec_from_location \"${location}\")\n\tjq -r '[\"opensuse\", .path_version, .target_vendor, .arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction opensuse_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 url_spec path_version overriding image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on openSUSE Linux\" >&2\n\turl_spec=$(opensuse_url_spec_from_location \"${location}\")\n\tpath_version=$(jq -r '.path_version' <<<\"${url_spec}\")\n\tif [[ ${path_version} == \"tumbleweed\" ]]; then\n\t\toverriding=${3:-'{\"path_version\":\"tumbleweed\"}'}\n\telse\n\t\toverriding=${3:-'{\"path_version\":\"stable\"}'}\n\tfi\n\turl_spec=$(jq -r '. + '\"${overriding}\" <<<\"${url_spec}\")\n\timage_entry=$(opensuse_latest_image_entry_for_url_spec \"${url_spec}\")\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"opensuse\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"opensuse\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding='{}'\ndeclare version_major='' version_minor=''\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\topensuse_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version-major-minor)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major-minor requires a value\"\n\t\tfi\n\t\t;&\n\t--version-major-minor=*)\n\t\tversion=${version:-${1#*=}}\n\t\toverriding=$(\n\t\t\tversion=\"${version#v}\"\n\t\t\tif [[ ${version} =~ ^v?[0-9]+.[0-9]+ ]]; then\n\t\t\t\tversion=\"$(echo \"${version}\" | cut -d. -f1-2)\"\n\t\t\t\t[[ ${version%%.*} -ge 15 ]] || error_exit \"openSUSE Linux version must be 15.0 or later\"\n\t\t\t\tpath_version=\"${version}\"\n\t\t\telif [[ ${version} == \"current\" || ${version} == \"stable\" || ${version} == \"tumbleweed\" ]]; then\n\t\t\t\tpath_version=${version}\n\t\t\telse\n\t\t\t\terror_exit \"--version-major-minor requires a value in the format <major>.<minor>, current, stable, or tumbleweed\"\n\t\t\tfi\n\t\t\tjson_vars path_version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t--version-major)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion_major=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major requires a value\"\n\t\tfi\n\t\t;&\n\t--version-major=*)\n\t\tversion_major=${version_major:-${1#*=}}\n\t\t[[ ${version_major} =~ ^[0-9]+$ ]] || error_exit \"Please specify --version-major in numbers\"\n\t\t;;\n\t--version-minor)\n\t\tif [[ -n ${2:-} && $2 != -* ]]; then\n\t\t\tversion_minor=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-minor requires a value\"\n\t\tfi\n\t\t;&\n\t--version-minor=*)\n\t\tversion_minor=${version_minor:-${1#*=}}\n\t\t[[ ${version_minor} =~ ^[0-9]+$ ]] || error_exit \"Please specify --version-minor in numbers\"\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif ! jq -e '.path_version' <<<\"${overriding}\" >/dev/null; then # --version-major-minor is not specified\n\tif [[ -n ${version_major} && -n ${version_minor} ]]; then\n\t\t[[ ${version_major} -ge 15 ]] || error_exit \"openSUSE Linux version must be 15.0 or later\"\n\t\t# shellcheck disable=2034\n\t\tpath_version=\"${version_major}.${version_minor}\"\n\t\toverriding=$(json_vars path_version <<<\"${overriding}\")\n\telif [[ -n ${version_major} ]]; then\n\t\terror_exit \"--version-minor is required when --version-major is specified\"\n\telif [[ -n ${version_minor} ]]; then\n\t\terror_exit \"--version-major is required when --version-minor is specified\"\n\tfi\nelif [[ -n ${version_major} || -n ${version_minor} ]]; then # --version-major-minor is specified\n\techo \"Ignoring --version-major and --version-minor because --version-major-minor is specified\" >&2\nfi\n[[ ${overriding} == \"{}\" ]] && overriding=''\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\topensuse_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\topensuse_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\topensuse_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-oraclelinux.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction oraclelinux_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the Oracle Linux image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version-major <major version>] <template.yaml>...\n\nDescription:\n  This script updates the Oracle Linux image location in the specified templates.\n  Image location basename format:\n\n\tOL<major version>U<minor version>_<arch>-kvm[-cloud]-b<build number>.qcow2\n\n  Published Oracle Linux image information is fetched from the following URLs:\n\n\tOL8:\n\t  x86_64: https://yum.oracle.com/templates/OracleLinux/ol8-template.json\n\t  aarch64: https://yum.oracle.com/templates/OracleLinux/ol8_aarch64-cloud-template.json\n\n\tOL9:\n\t  x86_64: https://yum.oracle.com/templates/OracleLinux/ol9-template.json\n\t  aarch64: https://yum.oracle.com/templates/OracleLinux/ol9_aarch64-cloud-template.json\n\n\tOL10:\n\t  x86_64: https://yum.oracle.com/templates/OracleLinux/ol10-template.json\n\t  aarch64: https://yum.oracle.com/templates/OracleLinux/ol10_aarch64-cloud-template.json\n\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the Oracle Linux image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Oracle Linux image location to major version 9 in ~/.lima/oraclelinux/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version-major 9 ~/.lima/oraclelinux/lima.yaml\n  $ limactl factory-reset oraclelinux\n\nFlags:\n  --version-major <major version>  Use the specified Oracle Linux <major version>.\n                                   The major version must be 7+ for x86_64 or 8+ for aarch64.\n  -h, --help                       Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction oraclelinux_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\"\n\t\t^https://yum\\\\.oracle\\\\.com/templates/OracleLinux/OL(?<path_major_version>\\\\d+)/u(?<path_minor_version>\\\\d+)/(?<path_arch>[^/]+)/\n\t\tOL(?<major_version>\\\\d+)U(?<minor_version>\\\\d+)_(?<arch>[^-]+)-(?<type>[^-]+)(?<cloud>-cloud)?-b(?<build_number>\\\\d+)\\\\.(?<file_extension>.*)$\n\t\";\"x\")\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\techo \"${url_spec}\"\n}\n\nreadonly oraclelinux_jq_filter_json_url='\n\t\"https://yum.oracle.com/templates/OracleLinux/\" +\n\t\"ol\\(.path_major_version)\\(if .path_arch != \"x86_64\" then \"_\" + .path_arch else \"\" end)\\(.cloud // \"\")-template.json\"\n'\n\nfunction oraclelinux_json_url_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${oraclelinux_jq_filter_json_url}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the JSON url for ${url_spec}\"\n}\n\nfunction oraclelinux_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 arch json_url downloaded_json latest_version_info\n\t# shellcheck disable=SC2034\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\tjson_url=$(oraclelinux_json_url_from_url_spec \"${url_spec}\")\n\tdownloaded_json=$(download_to_cache \"${json_url}\")\n\tlatest_version_info=\"$(jq -e -r --argjson spec \"${url_spec}\" '{\n\t\tlocation: (\"https://yum.oracle.com\" + .base_url + \"/\" + .[$spec.type].image),\n\t\tsha256: (\"sha256:\" + .[$spec.type].sha256)\n\t}' <\"${downloaded_json}\")\"\n\t[[ -n ${latest_version_info} ]] || return\n\tlocal location digest\n\t# prefer the v<major>.<minor> in the path\n\tlocation=$(jq -e -r '.location' <<<\"${latest_version_info}\")\n\tlocation=$(validate_url_without_redirect \"${location}\")\n\t# shellcheck disable=SC2034\n\tdigest=$(jq -e -r '.sha256' <<<\"${latest_version_info}\")\n\tjson_vars location arch digest\n}\n\nfunction oraclelinux_cache_key_for_image_kernel() {\n\tlocal location=$1 overriding=${3:-\"{}\"} url_spec\n\turl_spec=$(oraclelinux_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\tjq -r '[\"oraclelinux\", .path_major_version, .type, .cloud // empty, .arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction oraclelinux_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-\"{}\"} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on Oracle Linux\" >&2\n\turl_spec=$(oraclelinux_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\timage_entry=$(oraclelinux_latest_image_entry_for_url_spec \"${url_spec}\")\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"oraclelinux\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"oraclelinux\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding='{}'\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\toraclelinux_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version-major)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding=$(\n\t\t\t\tpath_major_version=\"${2}\"\n\t\t\t\t[[ ${path_major_version} =~ ^[0-9]+$ ]] || error_exit \"Oracle Linux major version must be a number\"\n\t\t\t\t[[ ${path_major_version} -eq 7 ]] && echo 'Oracle Linux major version 7 exists only for x86_64. It may fail for aarch64.' >&2\n\t\t\t\t[[ ${path_major_version} -gt 7 ]] || error_exit \"Oracle Linux major version must be 7+ for x86_64 or 8+ for aarch64\"\n\t\t\t\tjson_vars path_major_version <<<\"${overriding}\"\n\t\t\t)\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major requires a value\"\n\t\tfi\n\t\t;;\n\t--version-major=*)\n\t\toverriding=$(\n\t\t\tpath_major_version=\"${1#*=}\"\n\t\t\t[[ ${path_major_version} =~ ^[0-9]+$ ]] || error_exit \"Oracle Linux major version must be a number\"\n\t\t\t[[ ${path_major_version} -eq 7 ]] && echo 'Oracle Linux major version 7 exists only for x86_64. It may fail for aarch64.' >&2\n\t\t\t[[ ${path_major_version} -gt 7 ]] || error_exit \"Oracle Linux major version must be 7+ for x86_64 or 8+ for aarch64\"\n\t\t\tjson_vars path_major_version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\toraclelinux_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\toraclelinux_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\toraclelinux_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-rocky.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction rocky_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the Rocky Linux image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--version-major <major version>] <template.yaml>...\n\nDescription:\n  This script updates the Rocky Linux image location in the specified templates.\n  If the image location in the template contains a minor version, release date, and job id in the URL,\n  the script replaces it with the latest available minor version, release date, and job id.\n\n  Image location basename format:\n\n    Rocky-<major version>-GenericCloud[.latest|-<type>.latest|-<major version>.<minor version>-<date>.<job id?>].<arch>.qcow2\n\n  Published Rocky Linux image information is fetched from the following URLs:\n\n    https://dl.rockylinux.org/pub/rocky/<major version>/images/<arch>/\n\n  To parsing html, this script requires 'htmlq' or 'pup' command.\n  The downloaded files will be cached in the Lima cache directory.\n\nExamples:\n  Update the Rocky Linux image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Rocky Linux image location in ~/.lima/rocky/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/rocky/lima.yaml\n  $ limactl factory-reset rocky\n\n  Update the Rocky Linux image location to major version 9 in ~/.lima/rocky/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --version-major 9 ~/.lima/rocky/lima.yaml\n  $ limactl factory-reset rocky\n\nFlags:\n  --version-major <version>     Use the specified version. The version must be 8 or later.\n  -h, --help              Print this help message\nHELP\n}\n\n# print the URL spec for the given location\nfunction rocky_url_spec_from_location() {\n\tlocal location=$1 jq_filter url_spec\n\tjq_filter='capture(\"\n\t\t^https://dl\\\\.rockylinux\\\\.org/pub/rocky/(?<path_version>\\\\d+(\\\\.\\\\d+)?)/images/(?<path_arch>[^/]+)/\n\t\tRocky-(?<major_version>\\\\d+)-(?<target_vendor>.*)\n\t\t(\n\t\t\t-(?<type>.*)-(?<major_minor_version>\\\\d+\\\\.\\\\d+)-(?<date_and_ci_job_id>\\\\d{8}\\\\.\\\\d+)|\n\t\t\t-(?<type_latest>.*)\\\\.latest|\n\t\t\t\\\\.latest\n\t\t)\\\\.(?<arch>[^.]+).(?<file_extension>.*)$\n\t\";\"x\")\n\t'\n\turl_spec=$(jq -e -r \"${jq_filter}\" <<<\"\\\"${location}\\\"\")\n\n\tjq -e '.path_arch == .arch' <<<\"${url_spec}\" >/dev/null ||\n\t\terror_exit \"Validation failed: .path_arch != .arch: ${location}\"\n\techo \"${url_spec}\"\n}\n\nreadonly rocky_jq_filter_directory='\"https://dl.rockylinux.org/pub/rocky/\\(.path_version)/images/\\(.path_arch)/\"'\nreadonly rocky_jq_filter_filename='\n\t\"Rocky-\\(.major_version)-\\(.target_vendor)\\(\n\t\tif .date_and_ci_job_id then\n\t\t\t\"-\\(.type)-\\(.major_minor_version)-\\(.date_and_ci_job_id)\"\n\t\telse\n\t\t\tif .type then\n\t\t\t\t\"-\\(.type_latest).latest\"\n\t\t\telse\n\t\t\t\t\".latest\"\n\t\t\tend\n\t\tend\n\t).\\(.arch).\\(.file_extension)\"\n'\n\n# print the location for the given URL spec\nfunction rocky_location_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${rocky_jq_filter_directory} + ${rocky_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the location for ${url_spec}\"\n}\n\nfunction rocky_image_directory_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${rocky_jq_filter_directory}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image directory for ${url_spec}\"\n}\n\nfunction rocky_image_filename_from_url_spec() {\n\tlocal -r url_spec=$1\n\tjq -e -r \"${rocky_jq_filter_filename}\" <<<\"${url_spec}\" ||\n\t\terror_exit \"Failed to get the image filename for ${url_spec}\"\n}\n\n#\nfunction rocky_latest_image_entry_for_url_spec() {\n\tlocal url_spec=$1 arch major_version_url_spec major_version_image_directory downloaded_page links_in_page latest_minor_version_info\n\tarch=$(jq -r '.arch' <<<\"${url_spec}\")\n\t# to detect minor version updates, we need to get the major version URL\n\tmajor_version_url_spec=$(jq -e -r '.path_version = .major_version' <<<\"${url_spec}\")\n\tmajor_version_image_directory=$(rocky_image_directory_from_url_spec \"${major_version_url_spec}\")\n\tdownloaded_page=$(download_to_cache \"${major_version_image_directory}\")\n\tif command -v htmlq >/dev/null; then\n\t\tlinks_in_page=$(htmlq 'pre a' --attribute href <\"${downloaded_page}\")\n\telif command -v pup >/dev/null; then\n\t\tlinks_in_page=$(pup 'pre a attr{href}' <\"${downloaded_page}\")\n\telse\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from ${major_version_image_directory}\"\n\tfi\n\tlatest_minor_version_info=$(jq -e -Rrs --argjson spec \"${url_spec}\" '\n\t\t[\n\t\t\tsplit(\"\\n\").[] |\n\t\t\tcapture(\n\t\t\t\t\"^Rocky-\\($spec.major_version)-\\($spec.target_vendor)-\\($spec.type)\" +\n\t\t\t\t\"-(?<major_minor_version>\\($spec.major_version)\\\\.\\\\d+)\" +\n\t\t\t\t\"-(?<date_and_ci_job_id>\\\\d{8}\\\\.\\\\d+)\\\\.\\($spec.arch)\\\\.\\($spec.file_extension)$\"\n\t\t\t\t;\"x\"\n\t\t\t) |\n\t\t\t.version_number_array = ([.major_minor_version | scan(\"\\\\d+\") | tonumber])\n\t\t] | sort_by(.version_number_array, .date_and_ci_job_id) | last\n\t' <<<\"${links_in_page}\")\n\t[[ -n ${latest_minor_version_info} ]] || return\n\tlocal newer_url_spec location sha256sum_location downloaded_sha256sum filename digest\n\t# prefer the major_minor_version in the path\n\tnewer_url_spec=$(jq -e -r \". + ${latest_minor_version_info} | .path_version = .major_minor_version\" <<<\"${url_spec}\")\n\tlocation=$(rocky_location_from_url_spec \"${newer_url_spec}\")\n\tsha256sum_location=\"${location}.CHECKSUM\"\n\tdownloaded_sha256sum=$(download_to_cache \"${sha256sum_location}\")\n\tfilename=$(rocky_image_filename_from_url_spec \"${newer_url_spec}\")\n\tdigest=\"sha256:$(awk \"/SHA256 \\(${filename}\\) =/{print \\$4}\" \"${downloaded_sha256sum}\")\"\n\t[[ -n ${digest} ]] || error_exit \"Failed to get the SHA256 digest for ${filename}\"\n\tjson_vars location arch digest\n}\n\nfunction rocky_cache_key_for_image_kernel() {\n\tlocal location=$1 url_spec\n\turl_spec=$(rocky_url_spec_from_location \"${location}\")\n\tjq -r '[\"rocky\", .major_minor_version // .major_version, .target_vendor,\n\t\tif .date_and_ci_job_id then \"timestamped\" else \"latest\" end,\n\t\t.arch, .file_extension] | join(\":\")' <<<\"${url_spec}\"\n}\n\nfunction rocky_image_entry_for_image_kernel() {\n\tlocal location=$1 kernel_is_not_supported=$2 overriding=${3:-\"{}\"} url_spec image_entry=''\n\t[[ ${kernel_is_not_supported} == \"null\" ]] || echo \"Updating kernel information is not supported on Rocky Linux\" >&2\n\turl_spec=$(rocky_url_spec_from_location \"${location}\" | jq -r \". + ${overriding}\")\n\tif jq -e '.date_and_ci_job_id' <<<\"${url_spec}\" >/dev/null; then\n\t\timage_entry=$(rocky_latest_image_entry_for_url_spec \"${url_spec}\")\n\telse\n\t\timage_entry=$(\n\t\t\t# shellcheck disable=SC2030\n\t\t\tlocation=$(rocky_location_from_url_spec \"${url_spec}\")\n\t\t\tlocation=$(validate_url_without_redirect \"${location}\")\n\t\t\tarch=$(jq -r '.path_arch' <<<\"${url_spec}\")\n\t\t\tjson_vars location arch\n\t\t)\n\tfi\n\t# shellcheck disable=SC2031\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\terror_exit \"Please install 'htmlq' or 'pup' to list images from https://dl.rockylinux.org/pub/rocky/<version>/images/<arch>/\"\n\tfi\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif ! command -v htmlq >/dev/null && ! command -v pup >/dev/null; then\n\t\techo \"Please install 'htmlq' or 'pup' to list images from https://dl.rockylinux.org/pub/rocky/<version>/images/<arch>/\" >&2\n\telif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"rocky\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"rocky\")\n\tfi\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding=\"{}\"\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\trocky_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--version-major)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding=$(\n\t\t\t\tmajor_version=\"${2%%.*}\"\n\t\t\t\t[[ ${major_version} -ge 8 ]] || error_exit \"Rocky Linux major version must be 8 or later\"\n\t\t\t\t# shellcheck disable=2034\n\t\t\t\tpath_version=\"${major_version}\"\n\t\t\t\tjson_vars path_version major_version <<<\"${overriding}\"\n\t\t\t)\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version-major requires a value\"\n\t\tfi\n\t\t;;\n\t--version-major=*)\n\t\toverriding=$(\n\t\t\tmajor_version=\"${1#*=}\"\n\t\t\tmajor_version=\"${major_version%%.*}\"\n\t\t\t[[ ${major_version} -ge 8 ]] || error_exit \"Rocky Linux major version must be 8 or later\"\n\t\t\t# shellcheck disable=2034\n\t\t\tpath_version=\"${major_version}\"\n\t\t\tjson_vars path_version major_version <<<\"${overriding}\"\n\t\t)\n\t\t;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\n\t[[ -z ${overriding} ]] && overriding=\"{}\"\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\trocky_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\trocky_cache_key_for_image_kernel \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\trocky_image_entry_for_image_kernel \"${location}\" \"${kernel_location}\" \"${overriding}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template-ubuntu.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction ubuntu_print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the Ubuntu image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") [--flavor <flavor>|--minimal|--server] [--version <version>] <template.yaml>...\n\nDescription:\n  This script updates the Ubuntu image location in the specified templates.\n  If the image location in the template contains a release date in the URL, the script replaces it with the latest available date.\n  If no flags are specified, the script uses the flavor and version from the image location basename in the template.\n\n  Image location basename format: ubuntu-<version>-<flavor>-cloudimg-<arch>.img\n\n  Released Ubuntu image information is fetched from the following URLs:\n\n    Server: https://cloud-images.ubuntu.com/releases/stream/v1/com.ubuntu.cloud:released:download.json\n    Minimal: https://cloud-images.ubuntu.com/minimal/releases/stream/v1/com.ubuntu.cloud:released:download.json\n\n  The downloaded JSON file will be cached in the Lima cache directory.\n\nExamples:\n  Update the Ubuntu image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Ubuntu image location in ~/.lima/ubuntu/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/ubuntu/lima.yaml\n  $ limactl factory-reset ubuntu\n\n  Update the Ubuntu image location to ubuntu-24.04-minimal-cloudimg-<arch>.img in ~/.lima/docker/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") --minimal --version 24.04 ~/.lima/docker/lima.yaml\n  $ limactl factory-reset docker\n\nFlags:\n  --flavor <flavor>    Use the specified flavor image\n  --server             Shortcut for --flavor server\n  --minimal            Shortcut for --flavor minimal\n  --version <version>  Use the specified version\n                       The version can be an alias: latest, latest_lts, or lts.\n  -h, --help           Print this help message\nHELP\n}\n\nreadonly -A ubuntu_base_urls=(\n\t[\"minimal\"]=https://cloud-images.ubuntu.com/minimal/releases/\n\t[\"server\"]=https://cloud-images.ubuntu.com/releases/\n\t[\"daily\"]=https://cloud-images.ubuntu.com/daily/\n\t[\"daily:minimal\"]=https://cloud-images.ubuntu.com/daily/server/minimal/daily/\n)\n\n# ubuntu_base_url prints the base URL for the given flavor.\n# e.g.\n# ```console\n# ubuntu_base_url minimal\n# https://cloud-images.ubuntu.com/minimal/releases/\n# ```\nfunction ubuntu_base_url() {\n\t[[ -v ubuntu_base_urls[$1] ]] || error_exit \"Unsupported flavor: $1\"\n\techo \"${ubuntu_base_urls[$1]}\"\n}\n\n# ubuntu_downloaded_json downloads the JSON file for the given flavor and prints the path.\n# e.g.\n# ```console\n# ubuntu_downloaded_json server\n# /Users/user/Library/Caches/lima/download/by-url-sha256/255f982f5bbda07f5377369093e21c506d7240f5ba901479bdadfa205ddafb01/data\n# ```\nfunction ubuntu_downloaded_json() {\n\tlocal flavor=$1 base_url json_url\n\tcase \"${flavor}\" in\n\tdaily*)\n\t\tjson_url=$(ubuntu_base_url \"${flavor}\")streams/v1/com.ubuntu.cloud:daily:download.json\n\t\t;;\n\t*)\n\t\tjson_url=$(ubuntu_base_url \"${flavor}\")streams/v1/com.ubuntu.cloud:released:download.json\n\t\t;;\n\tesac\n\tdownload_to_cache \"${json_url}\"\n}\n# ubuntu_image_url_try_replace_release_with_version tries to replace the release with the version in the URL.\n# If the URL is valid, it prints the URL with the version.\nfunction ubuntu_image_url_try_replace_release_with_version() {\n\tlocal location=$1 release=$2 version=$3 location_using_version\n\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\tlocation_using_version=$(\n\t\tset -e\n\t\tvalidate_url \"${location/\\/${release}\\//\\/${version}\\/}\" 2>/dev/null\n\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t# shellcheck disable=2181\n\tif [[ $? -eq 0 ]]; then\n\t\techo \"${location_using_version}\"\n\telse\n\t\techo \"${location}\"\n\tfi\n\tset -e\n}\n\n# ubuntu_image_url_latest prints the latest image URL and its digest for the given flavor, version, arch, and path suffix.\nfunction ubuntu_image_url_latest() {\n\tlocal flavor=$1 version=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json jq_filter location_digest_release\n\tbase_url=$(ubuntu_base_url \"${flavor}\")\n\tubuntu_downloaded_json=$(ubuntu_downloaded_json \"${flavor}\")\n\tjq_filter=\"\n        [\n            .products[\\\"com.ubuntu.cloud:${flavor}:${version}:${arch}\\\"] |\n            .release as \\$release |\n            .versions[]?.items[] | select(.path | endswith(\\\"${path_suffix}\\\")) |\n            [\\\"${base_url}\\\"+.path, \\\"sha256:\\\"+.sha256, \\$release] | @tsv\n        ] | last\n    \"\n\tlocation_digest_release=$(jq -r \"${jq_filter}\" \"${ubuntu_downloaded_json}\")\n\t[[ ${location_digest_release} != \"null\" ]] ||\n\t\terror_exit \"The URL for ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}.\"\n\tlocal location digest release location_using_version\n\tread -r location digest release <<<\"${location_digest_release}\"\n\tlocation=$(validate_url \"${location}\")\n\tlocation=$(ubuntu_image_url_try_replace_release_with_version \"${location}\" \"${release}\" \"${version}\")\n\tarch=$(limayaml_arch \"${arch}\")\n\tjson_vars location arch digest\n}\n\n# ubuntu_image_url_release prints the release image URL for the given flavor, version, arch, and path suffix.\nfunction ubuntu_image_url_release() {\n\tlocal flavor=$1 version=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json\n\tbase_url=$(ubuntu_base_url \"${flavor}\")\n\tubuntu_downloaded_json=$(ubuntu_downloaded_json \"${flavor}\")\n\tlocal jq_filter release location\n\tjq_filter=\"\n\t\t[\n            .products | to_entries[] as \\$product_entry |\n            \\$product_entry.value| select(.version == \\\"${version}\\\") |\n            .release\n\t\t] | first\n    \"\n\trelease=$(jq -r \"${jq_filter}\" \"${ubuntu_downloaded_json}\")\n\t[[ ${release} != \"null\" ]] ||\n\t\terror_exit \"The URL for ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}.\"\n\tlocation=$(validate_url \"${base_url}${release}/release/ubuntu-${version}-${flavor}-cloudimg-${arch}${path_suffix}\")\n\tlocation=$(ubuntu_image_url_try_replace_release_with_version \"${location}\" \"${release}\" \"${version}\")\n\tarch=$(limayaml_arch \"${arch}\")\n\tjson_vars location arch\n}\n\n# ubuntu_image_url_daily prints the daily image URL and its digest for the given flavor, codename, arch, and path suffix.\nfunction ubuntu_image_url_daily() {\n\tlocal flavor=$1 codename=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json products_flavor jq_filter location_digest_version\n\tbase_url=$(ubuntu_base_url \"${flavor}\")\n\tubuntu_downloaded_json=$(ubuntu_downloaded_json \"${flavor}\")\n\tcase \"${flavor}\" in\n\tdaily:minimal) products_flavor=\"minimal\" ;;\n\t*) products_flavor=\"server\" ;;\n\tesac\n\tjq_filter=\"\n        [\n            .products | to_entries[] as \\$product_entry |\n            \\$product_entry.value| select(.release == \\\"${codename}\\\" and .arch == \\\"${arch}\\\") |\n            .version as \\$version |\n            .versions[]?.items[] | select(.path | endswith(\\\"${path_suffix}\\\")) |\n            [\\\"${base_url}\\\"+.path, \\\"sha256:\\\"+.sha256, \\$version] | @tsv\n        ] | last\n    \"\n\tlocation_digest_version=$(jq -r \"${jq_filter}\" \"${ubuntu_downloaded_json}\")\n\t[[ ${location_digest_version} != \"null\" ]] ||\n\t\terror_exit \"The URL for ${codename}-${products_flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}.\"\n\tlocal location digest version location_using_version\n\tread -r location digest version <<<\"${location_digest_version}\"\n\tlocation=$(validate_url \"${location}\")\n\tlocation=$(ubuntu_image_url_try_replace_release_with_version \"${location}\" \"${codename}\" \"${version}\")\n\tarch=$(limayaml_arch \"${arch}\")\n\tjson_vars location arch digest\n}\n\n# ubuntu_image_url_current prints the daily current image URL for the given flavor, codename, arch, and path suffix.\nfunction ubuntu_image_url_current() {\n\tlocal flavor=$1 codename=$2 arch=$3 path_suffix=$4 base_url ubuntu_downloaded_json path_prefix products_flavor\n\tbase_url=$(ubuntu_base_url \"${flavor}\")\n\tubuntu_downloaded_json=$(ubuntu_downloaded_json \"${flavor}\")\n\tcase \"${flavor}\" in\n\tdaily:minimal)\n\t\tpath_prefix=\"\"\n\t\tproducts_flavor=\"minimal\"\n\t\t;;\n\t*)\n\t\tpath_prefix=\"server/\"\n\t\tproducts_flavor=\"server\"\n\t\t;;\n\tesac\n\tlocal jq_filter version location\n\tjq_filter=\"\n\t\t[\n            .products | to_entries[] as \\$product_entry |\n            \\$product_entry.value| select(.release == \\\"${codename}\\\" and .arch == \\\"${arch}\\\") |\n            .version\n\t\t] | first\n    \"\n\tversion=$(jq -r \"${jq_filter}\" \"${ubuntu_downloaded_json}\")\n\t[[ ${version} != \"null\" ]] ||\n\t\terror_exit \"The URL for ubuntu-${version}-${products_flavor}-cloudimg-${arch}${path_suffix} is not provided at ${ubuntu_base_urls[${flavor}]}.\"\n\tlocation=$(validate_url \"${base_url}${path_prefix}${codename}/current/${codename}-${products_flavor}-cloudimg-${arch}${path_suffix}\")\n\tlocation=$(ubuntu_image_url_try_replace_release_with_version \"${location}\" \"${codename}\" \"${version}\")\n\tarch=$(limayaml_arch \"${arch}\")\n\tjson_vars location arch\n}\n\nfunction ubuntu_file_info() {\n\tlocal location=$1 location_dirname sha256sums location_basename digest\n\tlocation=$(validate_url \"${location}\")\n\tlocation_dirname=$(dirname \"${location}\")\n\tsha256sums=$(download_to_cache \"${location_dirname}/SHA256SUMS\")\n\tlocation_basename=$(basename \"${location}\")\n\t# shellcheck disable=SC2034\n\tdigest=${location+$(awk \"/${location_basename}/{print \\\"sha256:\\\"\\$1}\" \"${sha256sums}\")}\n\tjson_vars location digest\n}\n\n# ubuntu_image_entry_with_kernel_info prints image entry with kernel and initrd info.\n# $1: image_entry\n# e.g.\n# ```console\n# 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\"}'\n# {\"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:...\"}}\n# ```\n# shellcheck disable=SC2034\nfunction ubuntu_image_entry_with_kernel_info() {\n\tlocal image_entry=$1 location\n\tlocation=$(jq -e -r '.location' <<<\"${image_entry}\")\n\tlocal location_dirname location_basename location_prefix\n\tlocation_dirname=$(dirname \"${location}\")/unpacked\n\tlocation_basename=$(basename \"${location}\")\n\tcase \"${location_basename}\" in\n\tubuntu*) location_prefix=\"${location_dirname}/$(echo \"${location_basename}\" | cut -d- -f1-5 | cut -d. -f1-2)\" ;;\n\t*) location_prefix=\"${location_dirname}/$(echo \"${location_basename}\" | cut -d- -f1-4 | cut -d. -f1)\" ;;\n\tesac\n\n\tlocal kernel initrd\n\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\tkernel=$(\n\t\tset -e\n\t\tubuntu_file_info \"${location_prefix}-vmlinuz-generic\"\n\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t# shellcheck disable=2181\n\t[[ $? -eq 0 ]] || error_exit \"kernel image not found at ${location_prefix}-vmlinuz-generic\"\n\tinitrd=$(\n\t\tset -e\n\t\tubuntu_file_info \"${location_prefix}-initrd-generic\" 2>/dev/null\n\t) # may not exist\n\tset -e\n\tjson_vars kernel initrd <<<\"${image_entry}\"\n}\n\nfunction ubuntu_flavor_from_location_basename() {\n\tlocal location=$1 location_basename flavor\n\tlocation_basename=$(basename \"${location}\")\n\tcase \"${location_basename}\" in\n\tubuntu*) flavor=$(echo \"${location_basename}\" | cut -d- -f3) ;;\n\t*)\n\t\tflavor=$(echo \"${location_basename}\" | cut -d- -f2)\n\t\tcase \"${flavor}\" in\n\t\tminimal) flavor=\"daily:minimal\" ;;\n\t\t*) flavor=\"daily\" ;;\n\t\tesac\n\t\t;;\n\tesac\n\t[[ -n ${flavor} ]] || error_exit \"Failed to get flavor from ${location}\"\n\techo \"${flavor}\"\n}\n\n# ubuntu_version_from_location_basename prints the version from the location basename.\n# On daily images, it prints the codename.\nfunction ubuntu_version_from_location_basename() {\n\tlocal location=$1 location_basename version\n\tlocation_basename=$(basename \"${location}\")\n\tcase \"${location_basename}\" in\n\tubuntu*) version=$(echo \"${location_basename}\" | cut -d- -f2) ;;\n\t*) version=$(echo \"${location_basename}\" | cut -d- -f1) ;;\n\tesac\n\t[[ -n ${version} ]] || error_exit \"Failed to get version from ${location}\"\n\techo \"${version}\"\n}\n\n# ubuntu_version_latest_lts prints the latest LTS version for the given flavor.\n# e.g.\n# ```console\n# ubuntu_version_latest_lts minimal\n# 24.04\n# ```\nfunction ubuntu_version_latest_lts() {\n\tlocal flavor=${1:-server}\n\tubuntu_downloaded_json=$(ubuntu_downloaded_json \"${flavor}\")\n\tjq -e -r '[.products[]|.version|select(endswith(\".04\"))]|last // empty' \"${ubuntu_downloaded_json}\"\n}\n\n# ubuntu_version_latest prints the latest version for the given flavor.\n# e.g.\n# ```console\n# ubuntu_version_latest minimal\n# 24.10\n# ```\nfunction ubuntu_version_latest() {\n\tlocal flavor=${1:-server}\n\tubuntu_downloaded_json=$(ubuntu_downloaded_json \"${flavor}\")\n\tjq -e -r '[.products[]|.version]|last // empty' \"${ubuntu_downloaded_json}\"\n}\n\n# ubuntu_version_resolve_aliases resolves the version aliases.\n# e.g.\n# ```console\n# ubuntu_version_resolve_aliases https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img minimal latest\n# 24.10\n# 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\n# 24.04\n# ubuntu_version_resolve_aliases https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img\n#\n# ```\nfunction ubuntu_version_resolve_aliases() {\n\tlocal location=$1 flavor version\n\tflavor=${2:-$(ubuntu_flavor_from_location_basename \"${location}\")}\n\tversion=${3:-}\n\tcase \"${version}\" in\n\tlatest_lts | lts) ubuntu_version_latest_lts \"${flavor}\" ;;\n\tlatest) ubuntu_version_latest \"${flavor}\" ;;\n\t*) echo \"${version}\" ;;\n\tesac\n}\n\nfunction ubuntu_arch_from_location_basename() {\n\tlocal location=$1 location_basename arch\n\tlocation_basename=$(basename \"${location}\")\n\tcase \"${location_basename}\" in\n\tubuntu*) arch=$(echo \"${location_basename}\" | cut -d- -f5 | cut -d. -f1) ;;\n\t*) arch=$(echo \"${location_basename}\" | cut -d- -f4 | cut -d. -f1) ;;\n\tesac\n\t[[ -n ${arch} ]] || error_exit \"Failed to get arch from ${location}\"\n\techo \"${arch}\"\n}\n\nfunction ubuntu_path_suffix_from_location_basename() {\n\tlocal location=$1 arch path_suffix\n\tarch=$(ubuntu_arch_from_location_basename \"${location}\")\n\tpath_suffix=\"${location##*\"${arch}\"}\"\n\t[[ -n ${path_suffix} ]] || error_exit \"Failed to get path suffix from ${location}\"\n\techo \"${path_suffix}\"\n}\n\n# ubuntu_location_url_spec prints the URL spec for the given location.\n# If the location is not supported, it returns 1.\n# e.g.\n# ```console\n# ubuntu_location_url_spec https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img\n# latest\n# ubuntu_location_url_spec https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img\n# release\n# ```\nfunction ubuntu_location_url_spec() {\n\tlocal location=$1 url_spec\n\tcase \"${location}\" in\n\thttps://cloud-images.ubuntu.com/minimal/releases/*/release/*) url_spec=release ;;\n\thttps://cloud-images.ubuntu.com/minimal/releases/*/release-*/*) url_spec=latest ;;\n\thttps://cloud-images.ubuntu.com/releases/*/release/*) url_spec=release ;;\n\thttps://cloud-images.ubuntu.com/releases/*/release-*/*) url_spec=latest ;;\n\thttps://cloud-images.ubuntu.com/daily/server/minimal/daily/*/current/*) url_spec=current ;;\n\thttps://cloud-images.ubuntu.com/daily/server/minimal/daily/*/*/*) url_spec=daily ;;\n\thttps://cloud-images.ubuntu.com/daily/server/*/current/*) url_spec=current ;;\n\thttps://cloud-images.ubuntu.com/daily/server/*/*/*) url_spec=daily ;;\n\t*)\n\t\t# echo \"Unsupported image location: ${location}\" >&2\n\t\treturn 1\n\t\t;;\n\tesac\n\techo \"${url_spec}\"\n}\n\n# ubuntu_cache_key_for_image_kernel_flavor_version prints the cache key for the given location, kernel_location, flavor, and version.\n# If the image location is not supported, it returns 1.\n# kernel_location is not validated.\n# e.g.\n# ```console\n# 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\n# ubuntu_latest_24.04-minimal-amd64-release-.img\n# 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://...\n# ubuntu_latest_with_kernel_24.04-minimal-amd64-release-.img\n# 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\n# ubuntu_release_24.04-server-amd64-.img\n# ```\nfunction ubuntu_cache_key_for_image_kernel_flavor_version() {\n\tlocal location=$1 kernel_location=${2:-null} url_spec with_kernel='' flavor version arch path_suffix\n\turl_spec=$(ubuntu_location_url_spec \"${location}\")\n\t[[ ${kernel_location} != \"null\" ]] && with_kernel=_with_kernel\n\tflavor=$(ubuntu_flavor_from_location_basename \"${location}\")\n\tversion=$(ubuntu_version_from_location_basename \"${location}\")\n\tarch=$(ubuntu_arch_from_location_basename \"${location}\")\n\tpath_suffix=$(ubuntu_path_suffix_from_location_basename \"${location}\")\n\techo \"ubuntu_${url_spec}${with_kernel}_${version}-${flavor}-${arch}-${path_suffix}\"\n}\n\nfunction ubuntu_image_entry_for_image_kernel_flavor_version() {\n\tlocal location=$1 kernel_location=$2 url_spec\n\turl_spec=$(ubuntu_location_url_spec \"${location}\")\n\n\tlocal flavor version arch path_suffix\n\tflavor=${3:-$(ubuntu_flavor_from_location_basename \"${location}\")}\n\tversion=${4:-$(ubuntu_version_from_location_basename \"${location}\")}\n\tarch=$(ubuntu_arch_from_location_basename \"${location}\")\n\tpath_suffix=$(ubuntu_path_suffix_from_location_basename \"${location}\")\n\n\tlocal image_entry\n\timage_entry=$(ubuntu_image_url_\"${url_spec}\" \"${flavor}\" \"${version}\" \"${arch}\" \"${path_suffix}\")\n\tif [[ -z ${image_entry} ]]; then\n\t\terror_exit \"Failed to get the ${url_spec} image location for ${location}\"\n\telif jq -e \".location == \\\"${location}\\\"\" <<<\"${image_entry}\" >/dev/null; then\n\t\techo \"Image location is up-to-date: ${location}\" >&2\n\telif [[ ${kernel_location} != \"null\" ]]; then\n\t\tubuntu_image_entry_with_kernel_info \"${image_entry}\"\n\telse\n\t\techo \"${image_entry}\"\n\tfi\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# shellcheck source=/dev/null # avoid shellcheck hangs on source looping\n\t. \"${scriptdir}/update-template.sh\"\nelse\n\t# this script is sourced\n\tif [[ -v SUPPORTED_DISTRIBUTIONS ]]; then\n\t\tSUPPORTED_DISTRIBUTIONS+=(\"ubuntu\")\n\telse\n\t\tdeclare -a SUPPORTED_DISTRIBUTIONS=(\"ubuntu\")\n\tfi\n\t# required functions for Ubuntu\n\tfunction ubuntu_cache_key_for_image_kernel() { ubuntu_cache_key_for_image_kernel_flavor_version \"$@\"; }\n\tfunction ubuntu_image_entry_for_image_kernel() { ubuntu_image_entry_for_image_kernel_flavor_version \"$@\"; }\n\n\treturn 0\nfi\n\ndeclare -a templates=()\ndeclare overriding_flavor=\ndeclare overriding_version=\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tubuntu_print_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t--flavor)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding_flavor=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--flavor requires a value\"\n\t\tfi\n\t\t;;\n\t--flavor=*) overriding_flavor=\"${1#*=}\" ;;\n\t--minimal) overriding_flavor=\"minimal\" ;;\n\t--server) overriding_flavor=\"server\" ;;\n\t--version)\n\t\tif [[ -n $2 && $2 != -* ]]; then\n\t\t\toverriding_version=\"$2\"\n\t\t\tshift\n\t\telse\n\t\t\terror_exit \"--version requires a value\"\n\t\tfi\n\t\t;;\n\t--version=*) overriding_version=\"${1#*=}\" ;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tubuntu_print_help\n\texit 0\nfi\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\toverriding_version=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tubuntu_version_resolve_aliases \"${location}\" \"${overriding_flavor}\" \"${overriding_version}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tcache_key=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tubuntu_cache_key_for_image_kernel_flavor_version \"${location}\" \"${kernel_location}\"\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\timage_entry=$(\n\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\telse\n\t\t\t\tubuntu_image_entry_for_image_kernel_flavor_version \"${location}\" \"${kernel_location}\" \"${overriding_flavor}\" \"${overriding_version}\"\n\t\t\tfi\n\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t# shellcheck disable=2181\n\t\t[[ $? -eq 0 ]] || continue\n\t\tset -e\n\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\techo \"${image_entry}\" | jq\n\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\" \"${template}\"\n\t\tfi\n\tdone\ndone\n"
  },
  {
    "path": "hack/update-template.sh",
    "content": "#!/usr/bin/env bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Functions in this script assume error handling with 'set -e'.\n# To ensure 'set -e' works correctly:\n# - Use 'set +e' before assignments and '$(set -e; <function>)' to capture output without exiting on errors.\n# - Avoid calling functions directly in conditions to prevent disabling 'set -e'.\n# - Use 'shopt -s inherit_errexit' (Bash 4.4+) to avoid repeated 'set -e' in all '$(...)'.\nshopt -s inherit_errexit || error_exit \"inherit_errexit not supported. Please use bash 4.4 or later.\"\n\nfunction print_help() {\n\tcat <<HELP\n$(basename \"${BASH_SOURCE[0]}\"): Update the image location in the specified templates\n\nUsage:\n  $(basename \"${BASH_SOURCE[0]}\") <template.yaml>...\n\nDescription:\n  This script updates the image location in the specified templates.\n  If the image location in the template contains a release date in the URL, the script replaces it with the latest available date.\n\nExamples:\n  Update the Ubuntu image location in templates/**.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") templates/**.yaml\n\n  Update the Ubuntu image location in ~/.lima/ubuntu/lima.yaml:\n  $ $(basename \"${BASH_SOURCE[0]}\") ~/.lima/ubuntu/lima.yaml\n  $ limactl factory-reset ubuntu\n\nFlags:\n  -h, --help           Print this help message\nHELP\n}\n\n# json prints the JSON object with the given arguments.\n# json [key value ...]\n# if the value is empty, both key and value are omitted.\n# e.g.\n# ```console\n# json location https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img arch amd64 digest sha256:...\n# {\"location\":\"https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img\",\"arch\":\"amd64\",\"digest\":\"sha256:...\"}\n# ```\nfunction json() {\n\tlocal args=() pattern='^(\\[.*\\]|\\{.*\\}|true|false|[0-9]+)$' value\n\t[[ ! -p /dev/stdin ]] && args+=(--null-input)\n\twhile [[ $# -gt 0 ]]; do\n\t\tvalue=\"${2-}\"\n\t\tif [[ ${value} =~ ${pattern} ]]; then\n\t\t\targs+=(--argjson \"${1}\" \"${value}\")\n\t\telif [[ -n ${value} ]]; then\n\t\t\targs+=(--arg \"${1}\" \"${value}\")\n\t\tfi # omit empty values\n\t\tshift\n\t\tshift # shift 2 does not work when $# is 1\n\tdone\n\tjq -c \"${args[@]}\" '. + $ARGS.named | if . == {} then empty else . end'\n}\n\n# json_vars prints the JSON object with the given variable names.\n# e.g.\n# ```console\n# location=https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img\n# arch=amd64\n# digest=sha256:...\n# json_vars location arch digest\n# {\"location\":\"https://cloud-images.ubuntu.com/minimal/releases/24.04/release-20210914/ubuntu-24.04-minimal-cloudimg-amd64.img\",\"arch\":\"amd64\",\"digest\":\"sha256:...\"}\n# ```\nfunction json_vars() {\n\tlocal args=() var\n\tfor var in \"$@\"; do\n\t\t[[ -v ${var} ]] || error_exit \"${var} is not set\"\n\t\targs+=(\"${var}\" \"${!var}\")\n\tdone\n\tjson \"${args[@]}\"\n}\n\n# limayaml_arch prints the arch in the lima.yaml format\nfunction limayaml_arch() {\n\tlocal arch=$1\n\tarch=${arch/amd64/x86_64}\n\tarch=${arch/arm64/aarch64}\n\tarch=${arch/armhf/armv7l}\n\tarch=${arch/ppc64el/ppc64le}\n\techo \"${arch}\"\n}\n\nfunction validate_boolean() {\n\tlocal value=$1\n\tcase \"${value}\" in\n\t'') ;;\n\ttrue | 1) echo true ;;\n\tfalse | 0) echo false ;;\n\t*) error_exit \"Invalid boolean value: ${value}\" ;;\n\tesac\n}\n\n# validate_url checks if the URL is valid and prints the location if it is.\n# If the URL is redirected, it prints the redirected location.\n# e.g.\n# ```console\n# validate_url https://cloud-images.ubuntu.com/server/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img\n# https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img\n# ```\nfunction validate_url() {\n\tlocal url=$1\n\tcode_location=$(curl -sSL -o /dev/null -I -w \"%{http_code}\\t%{url_effective}\" \"${url}\")\n\tread -r code location <<<\"${code_location}\"\n\t[[ ${code} -eq 200 ]] || error_exit \"[${code}]: The image is not available for download from ${location}\"\n\techo \"${location}\"\n}\n\n# validate_url_without_redirect checks if the URL is valid and prints the location if it is.\n# If the URL is redirected, it prints the URL before the redirection.\n# e.g.\n# ```console\n# validate_url_without_redirect https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-arm64.qcow2\n# https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-arm64.qcow2\n# ```\n# cloud.debian.org may be redirected to other domains(e.g. chuangtzu.ftp.acc.umu.se), but we want to use the original URL.\nfunction validate_url_without_redirect() {\n\tlocal url=$1 location\n\tlocation=$(validate_url \"${url}\")\n\t[[ -n ${location} ]] || error_exit \"The image is not available for download from ${url}\"\n\techo \"${url}\"\n}\n\n# check if the script is executed or sourced\n# shellcheck disable=SC1091\nif [[ ${BASH_SOURCE[0]} == \"${0}\" ]]; then\n\tscriptdir=$(dirname \"${BASH_SOURCE[0]}\")\n\t# shellcheck source=./cache-common-inc.sh\n\t. \"${scriptdir}/cache-common-inc.sh\"\n\n\t# Scripts for each distribution are expected to:\n\t# - Add their identifier to the SUPPORTED_DISTRIBUTIONS array.\n\t# - Register the following functions:\n\t#   - ${distribution}_cache_key_for_image_kernel\n\t#     - Arguments: location, kernel_location\n\t#     - Returns: cache_key (string)\n\t#     - Exits with an error if the image location is not supported.\n\t#   - ${distribution}_image_entry_for_image_kernel\n\t#     - Arguments: location, kernel_location\n\t#     - Returns: image_entry (JSON object)\n\t#\t  - Exits with an error if the image location is not supported.\n\tdeclare -a SUPPORTED_DISTRIBUTIONS=()\n\n\t# shellcheck source=./update-template-ubuntu.sh\n\t. \"${scriptdir}/update-template-ubuntu.sh\"\n\t# shellcheck source=./update-template-debian.sh\n\t. \"${scriptdir}/update-template-debian.sh\"\n\t# shellcheck source=./update-template-archlinux.sh\n\t. \"${scriptdir}/update-template-archlinux.sh\"\n\t# shellcheck source=./update-template-centos-stream.sh\n\t. \"${scriptdir}/update-template-centos-stream.sh\"\n\t# shellcheck source=./update-template-almalinux.sh\n\t. \"${scriptdir}/update-template-almalinux.sh\"\n\t# shellcheck source=./update-template-almalinux-kitten.sh\n\t. \"${scriptdir}/update-template-almalinux-kitten.sh\"\n\t# shellcheck source=./update-template-rocky.sh\n\t. \"${scriptdir}/update-template-rocky.sh\"\n\t# shellcheck source=./update-template-alpine.sh\n\t. \"${scriptdir}/update-template-alpine.sh\"\n\t# shellcheck source=./update-template-oraclelinux.sh\n\t. \"${scriptdir}/update-template-oraclelinux.sh\"\n\t# shellcheck source=./update-template-fedora.sh\n\t. \"${scriptdir}/update-template-fedora.sh\"\n\t# shellcheck source=./update-template-opensuse.sh\n\t. \"${scriptdir}/update-template-opensuse.sh\"\n\t# shellcheck source=./update-template-freebsd.sh\n\t. \"${scriptdir}/update-template-freebsd.sh\"\n\t# shellcheck source=./update-template-macos.sh\n\t. \"${scriptdir}/update-template-macos.sh\"\nelse\n\t# this script is sourced\n\treturn 0\nfi\n\ndeclare -a templates=()\nwhile [[ $# -gt 0 ]]; do\n\tcase \"$1\" in\n\t-h | --help)\n\t\tprint_help\n\t\texit 0\n\t\t;;\n\t-d | --debug) set -x ;;\n\t*.yaml) templates+=(\"$1\") ;;\n\t*)\n\t\terror_exit \"Unknown argument: $1\"\n\t\t;;\n\tesac\n\tshift\ndone\n\nif [[ ${#templates[@]} -eq 0 ]]; then\n\tprint_help\n\texit 0\nfi\n\ndeclare -a distributions=()\n# Check if the distribution has the required functions\nfor distribution in \"${SUPPORTED_DISTRIBUTIONS[@]}\"; do\n\tif declare -f \"${distribution}_cache_key_for_image_kernel\" >/dev/null &&\n\t\tdeclare -f \"${distribution}_image_entry_for_image_kernel\" >/dev/null; then\n\t\tdistributions+=(\"${distribution}\")\n\tfi\ndone\n[[ ${#distributions[@]} -gt 0 ]] || error_exit \"No supported distributions found\"\n\ndeclare -A image_entry_cache=()\n\nfor template in \"${templates[@]}\"; do\n\techo \"Processing ${template}\"\n\t# 1. extract location by parsing template using arch\n\tyq_filter=\"\n\t\t.images[] | [.location, .kernel.location, .kernel.cmdline] | @tsv\n\t\"\n\tparsed=$(yq eval \"${yq_filter}\" \"${template}\")\n\n\t# 3. get the image location\n\tarr=()\n\twhile IFS= read -r line; do arr+=(\"${line}\"); done <<<\"${parsed}\"\n\tlocations=(\"${arr[@]}\")\n\tfor ((index = 0; index < ${#locations[@]}; index++)); do\n\t\t[[ ${locations[index]} != \"null\" ]] || continue\n\t\tset -e\n\t\tIFS=$'\\t' read -r location kernel_location kernel_cmdline <<<\"${locations[index]}\"\n\t\tfor distribution in \"${distributions[@]}\"; do\n\t\t\tset +e # Disable 'set -e' to avoid exiting on error for the next assignment.\n\t\t\tcache_key=$(\n\t\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\t\t\"${distribution}_cache_key_for_image_kernel\" \"${location}\" \"${kernel_location}\"\n\t\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t\t# shellcheck disable=2181\n\t\t\t[[ $? -eq 0 ]] || continue\n\t\t\timage_entry=$(\n\t\t\t\tset -e # Enable 'set -e' for the next command.\n\t\t\t\tif [[ -v image_entry_cache[${cache_key}] ]]; then\n\t\t\t\t\techo \"${image_entry_cache[${cache_key}]}\"\n\t\t\t\telse\n\t\t\t\t\t\"${distribution}_image_entry_for_image_kernel\" \"${location}\" \"${kernel_location}\"\n\t\t\t\tfi\n\t\t\t) # Check exit status separately to prevent disabling 'set -e' by using the function call in the condition.\n\t\t\t# shellcheck disable=2181\n\t\t\t[[ $? -eq 0 ]] || continue\n\t\t\tset -e\n\t\t\timage_entry_cache[${cache_key}]=\"${image_entry}\"\n\t\t\tif [[ -n ${image_entry} ]]; then\n\t\t\t\t[[ ${kernel_cmdline} != \"null\" ]] &&\n\t\t\t\t\tjq -e 'has(\"kernel\")' <<<\"${image_entry}\" >/dev/null &&\n\t\t\t\t\timage_entry=$(jq \".kernel.cmdline = \\\"${kernel_cmdline}\\\"\" <<<\"${image_entry}\")\n\t\t\t\techo \"${image_entry}\" | jq\n\t\t\t\tlimactl edit --log-level error --set \"\n\t\t\t\t\t.images[${index}] = ${image_entry}|\n\t\t\t\t\t(.images[${index}] | ..) style = \\\"double\\\"\n\t\t\t\t\" \"${template}\"\n\t\t\tfi\n\t\tdone\n\tdone\ndone\n"
  },
  {
    "path": "hack/validate-artifact.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n#\n# This script validates that lima-<VERSION>-Darwin-arm64.tar.gz\n# contains lima-guestagent.Linux-aarch64\n# but does not contain share/lima/lima-guestagent.Linux-x86_64\n\nset -eu -o pipefail\n\nmust_contain() {\n\ttmp=\"$(mktemp)\"\n\ttar tzf \"$1\" >\"$tmp\"\n\tif ! grep -q \"$2\" \"$tmp\"; then\n\t\techo >&2 \"ERROR: $1 must contain $2\"\n\t\tcat \"$tmp\"\n\t\trm -f \"$tmp\"\n\t\texit 1\n\tfi\n\trm -f \"$tmp\"\n}\n\nmust_not_contain() {\n\ttmp=\"$(mktemp)\"\n\ttar tzf \"$1\" >\"$tmp\"\n\tif grep -q \"$2\" \"$tmp\"; then\n\t\techo >&2 \"ERROR: $1 must not contain $2\"\n\t\tcat \"$tmp\"\n\t\trm -f \"$tmp\"\n\t\texit 1\n\tfi\n\trm -f \"$tmp\"\n}\n\nvalidate_artifact() {\n\tFILE=\"$1\"\n\tMYARCH=\"x86_64\"\n\tOTHERARCH=\"aarch64\"\n\tif [[ $FILE == *\"aarch64\"* || $FILE == *\"arm64\"* ]]; then\n\t\tMYARCH=\"aarch64\"\n\t\tOTHERARCH=\"x86_64\"\n\tfi\n\tif [[ $FILE == *\"go-mod-vendor.tar.gz\" ]]; then\n\t\t: NOP\n\telif [[ $FILE == *\"lima-additional-guestagents\"*\".tar.gz\" ]]; then\n\t\tmust_not_contain \"$FILE\" \"lima-guestagent.Linux-$MYARCH\"\n\t\tmust_contain \"$FILE\" \"lima-guestagent.Linux-$OTHERARCH\"\n\telif [[ $FILE == *\"lima-\"*\".tar.gz\" ]]; then\n\t\tmust_not_contain \"$FILE\" \"lima-guestagent.Linux-$OTHERARCH\"\n\t\tmust_contain \"$FILE\" \"lima-guestagent.Linux-$MYARCH\"\n\telse\n\t\techo >&2 \"ERROR: Unexpected file: $FILE\"\n\t\texit 1\n\tfi\n}\n\nfor FILE in \"$@\"; do\n\tvalidate_artifact \"$FILE\"\ndone\n"
  },
  {
    "path": "pkg/apfs/chown.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage apfs\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"os\"\n\t\"strings\"\n)\n\nvar le = binary.LittleEndian\n\n// Chown sets the owner and group of files on an unmounted APFS disk image.\n// volumeRole selects the target volume (e.g., VolRoleData).\n// Paths are relative to the volume root (e.g., \"Library/LaunchDaemons/foo.plist\").\nfunc Chown(diskPath string, volumeRole uint16, uid, gid uint32, paths ...string) error {\n\tc, err := openContainer(diskPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer c.close()\n\n\tvol, err := c.findVolume(volumeRole)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfsRootPhys, err := c.omapLookup(vol.omapTreeAddr, vol.rootTreeOID, vol.latestXID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"resolving filesystem root tree OID %d: %w\", vol.rootTreeOID, err)\n\t}\n\n\tfor _, path := range paths {\n\t\tinodeNum, err := c.resolvePath(fsRootPhys, vol.omapTreeAddr, vol.latestXID, path)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"resolving path %q: %w\", path, err)\n\t\t}\n\t\tif err := c.chownInode(fsRootPhys, vol.omapTreeAddr, vol.latestXID, inodeNum, uid, gid); err != nil {\n\t\t\treturn fmt.Errorf(\"chown inode %d (%q): %w\", inodeNum, path, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// container holds the open disk image file, the byte offset where\n// the APFS container starts (nonzero for GPT-partitioned disks),\n// and the APFS block size.\ntype container struct {\n\tf          *os.File\n\tbaseOffset int64 // byte offset of APFS container within file\n\tblockSize  uint32\n}\n\n// volumeInfo holds resolved volume information.\ntype volumeInfo struct {\n\tomapTreeAddr uint64 // physical address of volume omap B-tree root\n\trootTreeOID  uint64 // virtual OID of filesystem B-tree root\n\tlatestXID    uint64 // transaction ID of the volume superblock\n}\n\nfunc openContainer(path string) (*container, error) {\n\tf, err := os.OpenFile(path, os.O_RDWR, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open disk: %w\", err)\n\t}\n\tc := &container{f: f}\n\n\thdr := make([]byte, 4096) // minimum APFS block size\n\tif _, err := f.ReadAt(hdr, 0); err != nil {\n\t\tf.Close()\n\t\treturn nil, fmt.Errorf(\"read block 0: %w\", err)\n\t}\n\n\tif le.Uint32(hdr[nxMagicOff:]) == nxMagic {\n\t\t// Raw APFS container (no partition table).\n\t\tc.blockSize = le.Uint32(hdr[nxBlockSizeOff:])\n\t} else {\n\t\t// Look for a GPT partition table and find the APFS partition.\n\t\toffset, err := findAPFSPartitionGPT(f)\n\t\tif err != nil {\n\t\t\tf.Close()\n\t\t\treturn nil, fmt.Errorf(\"finding APFS partition: %w\", err)\n\t\t}\n\t\tc.baseOffset = offset\n\t\tif _, err := f.ReadAt(hdr, offset); err != nil {\n\t\t\tf.Close()\n\t\t\treturn nil, fmt.Errorf(\"read APFS superblock at offset %d: %w\", offset, err)\n\t\t}\n\t\tif le.Uint32(hdr[nxMagicOff:]) != nxMagic {\n\t\t\tf.Close()\n\t\t\treturn nil, fmt.Errorf(\"APFS partition at offset %d has bad magic\", offset)\n\t\t}\n\t\tc.blockSize = le.Uint32(hdr[nxBlockSizeOff:])\n\t}\n\n\tif c.blockSize < 4096 {\n\t\tf.Close()\n\t\treturn nil, fmt.Errorf(\"invalid block size %d\", c.blockSize)\n\t}\n\treturn c, nil\n}\n\n// GPT constants.\nconst (\n\tgptHeaderSignature = \"EFI PART\"\n\tgptLBASectorSize   = 512\n)\n\n// apfsPartTypeGUID is the APFS Container partition type GUID as stored\n// on disk (mixed-endian encoding per GPT spec).\n// 7C3457EF-0000-11AA-AA11-00306543ECAC.\nvar apfsPartTypeGUID = [16]byte{\n\t0xEF, 0x57, 0x34, 0x7C, // time_low (LE)\n\t0x00, 0x00, // time_mid (LE)\n\t0xAA, 0x11, // time_hi_and_version (LE)\n\t0xAA, 0x11, // clock_seq\n\t0x00, 0x30, 0x65, 0x43, 0xEC, 0xAC, // node\n}\n\n// findAPFSPartitionGPT reads a GPT partition table and returns the\n// byte offset of the first APFS Container partition.\nfunc findAPFSPartitionGPT(f *os.File) (int64, error) {\n\t// Read GPT header at LBA 1.\n\tgptHdr := make([]byte, gptLBASectorSize)\n\tif _, err := f.ReadAt(gptHdr, gptLBASectorSize); err != nil {\n\t\treturn 0, fmt.Errorf(\"reading GPT header: %w\", err)\n\t}\n\tif string(gptHdr[0:8]) != gptHeaderSignature {\n\t\treturn 0, fmt.Errorf(\"no GPT header found (expected %q)\", gptHeaderSignature)\n\t}\n\n\tpartEntryLBA := le.Uint64(gptHdr[72:])\n\tnumEntries := le.Uint32(gptHdr[80:])\n\tentrySize := le.Uint32(gptHdr[84:])\n\n\tentryBuf := make([]byte, entrySize)\n\tfor i := range numEntries {\n\t\toff := int64(partEntryLBA)*gptLBASectorSize + int64(i)*int64(entrySize)\n\t\tif _, err := f.ReadAt(entryBuf, off); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"reading GPT entry %d: %w\", i, err)\n\t\t}\n\n\t\tvar typeGUID [16]byte\n\t\tcopy(typeGUID[:], entryBuf[0:16])\n\t\tif typeGUID == apfsPartTypeGUID {\n\t\t\tfirstLBA := le.Uint64(entryBuf[32:])\n\t\t\treturn int64(firstLBA) * gptLBASectorSize, nil\n\t\t}\n\n\t\t// Stop at empty entries.\n\t\tallZero := true\n\t\tfor _, b := range typeGUID {\n\t\t\tif b != 0 {\n\t\t\t\tallZero = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif allZero {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn 0, errors.New(\"no APFS partition found in GPT\")\n}\n\nfunc (c *container) close() {\n\tc.f.Close()\n}\n\nfunc (c *container) readBlock(addr uint64) ([]byte, error) {\n\tbuf := make([]byte, c.blockSize)\n\t_, err := c.f.ReadAt(buf, c.baseOffset+int64(addr)*int64(c.blockSize))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read block %d: %w\", addr, err)\n\t}\n\treturn buf, nil\n}\n\nfunc (c *container) writeBlock(addr uint64, data []byte) error {\n\t_, err := c.f.WriteAt(data, c.baseOffset+int64(addr)*int64(c.blockSize))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"write block %d: %w\", addr, err)\n\t}\n\treturn nil\n}\n\n// latestSuperblock scans the checkpoint descriptor area for the\n// superblock with the highest valid transaction ID.\nfunc (c *container) latestSuperblock() ([]byte, error) {\n\t// Read block 0 at full block size to get checkpoint descriptor area info.\n\tblock0, err := c.readBlock(0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := verifyChecksum(block0); err != nil {\n\t\treturn nil, fmt.Errorf(\"block 0 checksum: %w\", err)\n\t}\n\n\tdescBase := le.Uint64(block0[nxXPDescBaseOff:])\n\tdescBlocks := le.Uint32(block0[nxXPDescBlocksOff:]) & nxXPDescBlocksMask\n\n\tvar bestBlock []byte\n\tvar bestXID uint64\n\n\tfor i := range descBlocks {\n\t\tblk, err := c.readBlock(descBase + uint64(i))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif verifyChecksum(blk) != nil {\n\t\t\tcontinue\n\t\t}\n\t\toType := le.Uint32(blk[objTypeOff:]) & objTypeMask\n\t\tif oType != objectTypeNXSuperblock {\n\t\t\tcontinue\n\t\t}\n\t\tif le.Uint32(blk[nxMagicOff:]) != nxMagic {\n\t\t\tcontinue\n\t\t}\n\t\txid := le.Uint64(blk[objXIDOff:])\n\t\tif xid > bestXID {\n\t\t\tbestXID = xid\n\t\t\tbestBlock = blk\n\t\t}\n\t}\n\tif bestBlock == nil {\n\t\treturn nil, errors.New(\"no valid container superblock found in checkpoint area\")\n\t}\n\treturn bestBlock, nil\n}\n\n// findVolume locates the volume with the given role.\nfunc (c *container) findVolume(role uint16) (*volumeInfo, error) {\n\tsb, err := c.latestSuperblock()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading container superblock: %w\", err)\n\t}\n\n\t// Read the container omap to resolve volume virtual OIDs.\n\tcontainerOmapAddr := le.Uint64(sb[nxOmapOIDOff:])\n\tcontainerOmap, err := c.readBlock(containerOmapAddr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"reading container omap at %d: %w\", containerOmapAddr, err)\n\t}\n\tcontainerOmapTreeAddr := le.Uint64(containerOmap[omapTreeOIDOff:])\n\n\tcontainerXID := le.Uint64(sb[objXIDOff:])\n\n\tfor i := range nxMaxFileSystems {\n\t\toff := nxFSOIDOff + i*8\n\t\tvolOID := le.Uint64(sb[off:])\n\t\tif volOID == 0 {\n\t\t\tcontinue\n\t\t}\n\t\t// Resolve virtual OID through container omap.\n\t\tvolPhysAddr, err := c.omapLookup(containerOmapTreeAddr, volOID, containerXID)\n\t\tif err != nil {\n\t\t\tcontinue // skip volumes we can't resolve\n\t\t}\n\t\tvolBlock, err := c.readBlock(volPhysAddr)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif verifyChecksum(volBlock) != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif le.Uint32(volBlock[apfsMagicOff:]) != apfsMagic {\n\t\t\tcontinue\n\t\t}\n\t\tvolRole := le.Uint16(volBlock[apfsRoleOff:])\n\t\tif volRole == role {\n\t\t\tomapAddr := le.Uint64(volBlock[apfsOmapOIDOff:])\n\t\t\tomapBlock, err := c.readBlock(omapAddr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"reading volume omap: %w\", err)\n\t\t\t}\n\t\t\treturn &volumeInfo{\n\t\t\t\tomapTreeAddr: le.Uint64(omapBlock[omapTreeOIDOff:]),\n\t\t\t\trootTreeOID:  le.Uint64(volBlock[apfsRootTreeOIDOff:]),\n\t\t\t\tlatestXID:    le.Uint64(volBlock[objXIDOff:]),\n\t\t\t}, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"no volume with role %#x found\", role)\n}\n\n// omapLookup searches the omap B-tree for a virtual OID, returning\n// the physical address from the entry with the highest xid <= maxXID.\nfunc (c *container) omapLookup(omapTreeAddr, oid, maxXID uint64) (uint64, error) {\n\tblk, err := c.readBlock(omapTreeAddr)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tfor {\n\t\tif verifyChecksum(blk) != nil {\n\t\t\treturn 0, errors.New(\"omap node checksum failed\")\n\t\t}\n\t\tif err := verifyBTreeNodeType(blk); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"omap node: %w\", err)\n\t\t}\n\t\tflags := le.Uint16(blk[btnFlagsOff:])\n\t\tnkeys := le.Uint32(blk[btnNKeysOff:])\n\t\ttspOff := le.Uint16(blk[btnTableSpaceOff:])\n\t\ttspLen := le.Uint16(blk[btnTableSpaceOff+2:])\n\n\t\ttocStart := btnDataOff + uint32(tspOff)\n\t\tkeyAreaStart := tocStart + uint32(tspLen)\n\n\t\tisLeaf := flags&btnodeLeaf != 0\n\t\tisFixedKV := flags&btnodeFixedKVSize != 0\n\t\tisRoot := flags&btnodeRoot != 0\n\n\t\tvalueAreaEnd := c.blockSize\n\t\tif isRoot {\n\t\t\tvalueAreaEnd -= btreeInfoSize\n\t\t}\n\n\t\tif isLeaf {\n\t\t\t// Search for matching oid with highest xid <= maxXID.\n\t\t\tvar bestPhysAddr uint64\n\t\t\tvar bestXID uint64\n\t\t\tfound := false\n\n\t\t\tfor i := range nkeys {\n\t\t\t\tkOff, vOff := c.readTocEntry(blk, tocStart, i, isFixedKV)\n\t\t\t\tkeyStart := keyAreaStart + kOff\n\t\t\t\tentryOID := le.Uint64(blk[keyStart:])\n\t\t\t\tentryXID := le.Uint64(blk[keyStart+8:])\n\n\t\t\t\tif entryOID == oid && entryXID <= maxXID {\n\t\t\t\t\t// Value: ov_flags(4) + ov_size(4) + ov_paddr(8).\n\t\t\t\t\tvalStart := valueAreaEnd - vOff\n\t\t\t\t\tphysAddr := le.Uint64(blk[valStart+8:])\n\t\t\t\t\tif !found || entryXID > bestXID {\n\t\t\t\t\t\tbestPhysAddr = physAddr\n\t\t\t\t\t\tbestXID = entryXID\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\treturn 0, fmt.Errorf(\"omap entry for OID %d not found\", oid)\n\t\t\t}\n\t\t\treturn bestPhysAddr, nil\n\t\t}\n\n\t\t// Internal node: find the last key <= (oid, maxXID) and descend.\n\t\tchildIdx := uint32(0)\n\t\tfor i := range nkeys {\n\t\t\tkOff, _ := c.readTocEntry(blk, tocStart, i, isFixedKV)\n\t\t\tkeyStart := keyAreaStart + kOff\n\t\t\tentryOID := le.Uint64(blk[keyStart:])\n\t\t\tentryXID := le.Uint64(blk[keyStart+8:])\n\n\t\t\tcmp := compareOmapKey(entryOID, entryXID, oid, maxXID)\n\t\t\tif cmp <= 0 {\n\t\t\t\tchildIdx = i\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Read child pointer (physical address, 8 bytes).\n\t\t_, vOff := c.readTocEntry(blk, tocStart, childIdx, isFixedKV)\n\t\tchildAddr := le.Uint64(blk[valueAreaEnd-vOff:])\n\n\t\tblk, err = c.readBlock(childAddr)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"reading omap child node: %w\", err)\n\t\t}\n\t}\n}\n\n// readTocEntry reads the key and value offsets from a ToC entry.\n// For fixed-KV nodes (kvoff_t): 4 bytes per entry (k_off u16, v_off u16).\n// For variable-KV nodes (kvloc_t): 8 bytes per entry (k.off u16, k.len u16, v.off u16, v.len u16).\n// Returns keyOffset and valueOffset (both relative to their respective areas).\nfunc (c *container) readTocEntry(blk []byte, tocStart, index uint32, fixedKV bool) (keyOff, valOff uint32) {\n\tif fixedKV {\n\t\toff := tocStart + index*4\n\t\treturn uint32(le.Uint16(blk[off:])), uint32(le.Uint16(blk[off+2:]))\n\t}\n\toff := tocStart + index*8\n\treturn uint32(le.Uint16(blk[off:])), uint32(le.Uint16(blk[off+4:]))\n}\n\n// verifyBTreeNodeType checks that a block's object type indicates a\n// B-tree node: OBJECT_TYPE_BTREE (0x02, root) or OBJECT_TYPE_BTREE_NODE\n// (0x03, non-root).\nfunc verifyBTreeNodeType(blk []byte) error {\n\toType := le.Uint32(blk[objTypeOff:]) & objTypeMask\n\tif oType != 0x02 && oType != 0x03 {\n\t\treturn fmt.Errorf(\"expected B-tree node type (2 or 3), got %#x\", oType)\n\t}\n\treturn nil\n}\n\nfunc compareOmapKey(oid1, xid1, oid2, xid2 uint64) int {\n\tif oid1 < oid2 {\n\t\treturn -1\n\t}\n\tif oid1 > oid2 {\n\t\treturn 1\n\t}\n\tif xid1 < xid2 {\n\t\treturn -1\n\t}\n\tif xid1 > xid2 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// compareFSKeyHeader compares two filesystem B-tree key headers.\n// APFS sorts filesystem keys by (obj_id, type), not by the raw uint64.\nfunc compareFSKeyHeader(a, b uint64) int {\n\taID := a & objIDMask\n\tbID := b & objIDMask\n\tif aID < bID {\n\t\treturn -1\n\t}\n\tif aID > bID {\n\t\treturn 1\n\t}\n\taType := a >> objTypeShift\n\tbType := b >> objTypeShift\n\tif aType < bType {\n\t\treturn -1\n\t}\n\tif aType > bType {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nvar crc32cTable = crc32.MakeTable(crc32.Castagnoli)\n\n// drecNameHash computes the 22-bit directory record name hash as stored\n// in the upper bits of j_drec_hashed_key_t.name_len_and_hash.\n//\n// Per the Apple APFS Reference: NFD-normalize the name, encode as UTF-32LE\n// (without null terminator), compute CRC-32C, complement, keep low 22 bits.\n// The complement cancels with the CRC's standard finalization XOR, so we\n// use ^crc32.Checksum to get the raw CRC.\nfunc drecNameHash(name string) uint32 {\n\trunes := []rune(strings.ToLower(name))\n\tbuf := make([]byte, len(runes)*4)\n\tfor i, r := range runes {\n\t\tle.PutUint32(buf[i*4:], uint32(r))\n\t}\n\treturn ^crc32.Checksum(buf, crc32cTable) & 0x3FFFFF\n}\n\n// compareDrecKey compares a directory record key from a B-tree node\n// against a target (parentCNID, name). Returns <0, 0, or >0.\nfunc (c *container) compareDrecKey(blk []byte, keyStart uint32, targetKeyHeader uint64, targetName string, targetHash uint32) int {\n\tkeyHeader := le.Uint64(blk[keyStart:])\n\tcmp := compareFSKeyHeader(keyHeader, targetKeyHeader)\n\tif cmp != 0 {\n\t\treturn cmp\n\t}\n\t// Headers match (same parent CNID and type DIR_REC).\n\t// Compare name hash for hashed keys, or name directly for non-hashed.\n\tval32 := le.Uint32(blk[keyStart+8:])\n\tif val32&0xFFFFFC00 != 0 {\n\t\t// Hashed key: compare hash values (upper 22 bits).\n\t\tentryHash := (val32 >> 10) & 0x3FFFFF\n\t\tif entryHash < targetHash {\n\t\t\treturn -1\n\t\t}\n\t\tif entryHash > targetHash {\n\t\t\treturn 1\n\t\t}\n\t\t// Hash collision: compare actual names.\n\t\tentryName := c.readDrecName(blk, keyStart+8)\n\t\treturn strings.Compare(strings.ToLower(entryName), strings.ToLower(targetName))\n\t}\n\t// Non-hashed key: compare names directly.\n\tentryName := c.readDrecName(blk, keyStart+8)\n\treturn strings.Compare(entryName, targetName)\n}\n\n// resolvePath walks path components from the volume root, returning\n// the inode number of the final path element.\nfunc (c *container) resolvePath(fsRootPhys, omapTreeAddr, maxXID uint64, path string) (uint64, error) {\n\tparts := strings.Split(strings.Trim(path, \"/\"), \"/\")\n\tcnid := uint64(rootDirInodeNum)\n\n\tfor _, name := range parts {\n\t\tif name == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tfileID, err := c.lookupDirEntry(fsRootPhys, omapTreeAddr, maxXID, cnid, name)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"looking up %q in directory (cnid %d): %w\", name, cnid, err)\n\t\t}\n\t\tcnid = fileID\n\t}\n\treturn cnid, nil\n}\n\n// lookupDirEntry searches the filesystem B-tree for a directory record\n// matching parentCNID and name, returning the file_id from j_drec_val_t.\nfunc (c *container) lookupDirEntry(fsRootPhys, omapTreeAddr, maxXID, parentCNID uint64, name string) (uint64, error) {\n\ttargetKeyHeader := (uint64(apfsTypeDirRec) << objTypeShift) | (parentCNID & objIDMask)\n\ttargetHash := drecNameHash(name)\n\n\tblk, err := c.readBlock(fsRootPhys)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tfor {\n\t\tif verifyChecksum(blk) != nil {\n\t\t\treturn 0, errors.New(\"filesystem B-tree node checksum failed\")\n\t\t}\n\t\tif err := verifyBTreeNodeType(blk); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"filesystem B-tree node: %w\", err)\n\t\t}\n\t\tflags := le.Uint16(blk[btnFlagsOff:])\n\t\tnkeys := le.Uint32(blk[btnNKeysOff:])\n\t\ttspOff := le.Uint16(blk[btnTableSpaceOff:])\n\t\ttspLen := le.Uint16(blk[btnTableSpaceOff+2:])\n\n\t\ttocStart := btnDataOff + uint32(tspOff)\n\t\tkeyAreaStart := tocStart + uint32(tspLen)\n\n\t\tisLeaf := flags&btnodeLeaf != 0\n\t\tisFixedKV := flags&btnodeFixedKVSize != 0\n\t\tisRoot := flags&btnodeRoot != 0\n\n\t\tvalueAreaEnd := c.blockSize\n\t\tif isRoot {\n\t\t\tvalueAreaEnd -= btreeInfoSize\n\t\t}\n\n\t\tif isLeaf {\n\t\t\tfor i := range nkeys {\n\t\t\t\tkOff, vOff := c.readTocEntry(blk, tocStart, i, isFixedKV)\n\t\t\t\tkeyStart := keyAreaStart + kOff\n\t\t\t\tkeyHeader := le.Uint64(blk[keyStart:])\n\n\t\t\t\tif keyHeader != targetKeyHeader {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Parse the directory record key name.\n\t\t\t\tentryName := c.readDrecName(blk, keyStart+8)\n\t\t\t\tif entryName != name {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Read file_id from j_drec_val_t (first 8 bytes of value).\n\t\t\t\tvalStart := valueAreaEnd - vOff\n\t\t\t\treturn le.Uint64(blk[valStart:]), nil\n\t\t\t}\n\t\t\treturn 0, fmt.Errorf(\"directory entry %q not found\", name)\n\t\t}\n\n\t\t// Internal node: find the child to descend into.\n\t\t// The last key <= our target determines the child.\n\t\t// For DIR_REC keys with matching headers, we also compare the\n\t\t// name hash to find the correct subtree.\n\t\tchildIdx := uint32(0)\n\t\tfor i := range nkeys {\n\t\t\tkOff, _ := c.readTocEntry(blk, tocStart, i, isFixedKV)\n\t\t\tkeyStart := keyAreaStart + kOff\n\t\t\tcmp := c.compareDrecKey(blk, keyStart, targetKeyHeader, name, targetHash)\n\t\t\tif cmp <= 0 {\n\t\t\t\tchildIdx = i\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// Read child OID (virtual) and resolve through omap.\n\t\t_, vOff := c.readTocEntry(blk, tocStart, childIdx, isFixedKV)\n\t\tchildOID := le.Uint64(blk[valueAreaEnd-vOff:])\n\t\tchildPhys, err := c.omapLookup(omapTreeAddr, childOID, maxXID)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"resolving child OID %d: %w\", childOID, err)\n\t\t}\n\t\tblk, err = c.readBlock(childPhys)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n}\n\n// readDrecName reads a directory record name from the key data\n// starting at nameFieldOff. Handles both hashed (j_drec_hashed_key_t,\n// 4-byte name_len_and_hash) and non-hashed (j_drec_key_t, 2-byte\n// name_len) formats.\nfunc (c *container) readDrecName(blk []byte, nameFieldOff uint32) string {\n\t// Heuristic: if the 4-byte field at nameFieldOff has its upper\n\t// bits set (hash), it's a hashed key. For non-hashed keys, the\n\t// 2-byte name_len is followed by the name bytes.\n\t//\n\t// In j_drec_hashed_key_t: name_len_and_hash (uint32) where\n\t// lower 10 bits = name length including null terminator.\n\t// In j_drec_key_t: name_len (uint16) = name length including\n\t// null terminator.\n\t//\n\t// We distinguish them by checking: if the upper 22 bits of the\n\t// first uint32 are nonzero, it's hashed.\n\tval32 := le.Uint32(blk[nameFieldOff:])\n\tif val32&0xFFFFFC00 != 0 {\n\t\t// Hashed: lower 10 bits = length (including null terminator).\n\t\tnameLen := int(val32 & 0x3FF)\n\t\tnameStart := nameFieldOff + 4\n\t\tif nameLen > 1 {\n\t\t\treturn string(blk[nameStart : nameStart+uint32(nameLen-1)])\n\t\t}\n\t\treturn \"\"\n\t}\n\t// Non-hashed: 2-byte name_len.\n\tnameLen := int(le.Uint16(blk[nameFieldOff:]))\n\tnameStart := nameFieldOff + 2\n\tif nameLen > 1 {\n\t\treturn string(blk[nameStart : nameStart+uint32(nameLen-1)])\n\t}\n\treturn \"\"\n}\n\n// chownInode finds an inode in the filesystem B-tree and modifies\n// its owner and group fields.\nfunc (c *container) chownInode(fsRootPhys, omapTreeAddr, maxXID, inodeNum uint64, uid, gid uint32) error {\n\ttargetKeyHeader := (uint64(apfsTypeInode) << objTypeShift) | (inodeNum & objIDMask)\n\n\tblkAddr := fsRootPhys\n\tblk, err := c.readBlock(blkAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Walk down to the leaf containing the inode.\n\tfor {\n\t\tif verifyChecksum(blk) != nil {\n\t\t\treturn errors.New(\"filesystem B-tree node checksum failed\")\n\t\t}\n\t\tif err := verifyBTreeNodeType(blk); err != nil {\n\t\t\treturn fmt.Errorf(\"filesystem B-tree node: %w\", err)\n\t\t}\n\t\tflags := le.Uint16(blk[btnFlagsOff:])\n\t\tnkeys := le.Uint32(blk[btnNKeysOff:])\n\t\ttspOff := le.Uint16(blk[btnTableSpaceOff:])\n\t\ttspLen := le.Uint16(blk[btnTableSpaceOff+2:])\n\n\t\ttocStart := btnDataOff + uint32(tspOff)\n\t\tkeyAreaStart := tocStart + uint32(tspLen)\n\n\t\tisLeaf := flags&btnodeLeaf != 0\n\t\tisFixedKV := flags&btnodeFixedKVSize != 0\n\t\tisRoot := flags&btnodeRoot != 0\n\n\t\tvalueAreaEnd := c.blockSize\n\t\tif isRoot {\n\t\t\tvalueAreaEnd -= btreeInfoSize\n\t\t}\n\n\t\tif isLeaf {\n\t\t\tfor i := range nkeys {\n\t\t\t\tkOff, vOff := c.readTocEntry(blk, tocStart, i, isFixedKV)\n\t\t\t\tkeyStart := keyAreaStart + kOff\n\t\t\t\tkeyHeader := le.Uint64(blk[keyStart:])\n\n\t\t\t\tif keyHeader != targetKeyHeader {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Found the inode. Validate before writing.\n\t\t\t\tvalStart := valueAreaEnd - vOff\n\n\t\t\t\tif valStart+inodeGroupOff+4 > c.blockSize {\n\t\t\t\t\treturn fmt.Errorf(\"inode %d value exceeds block boundary\", inodeNum)\n\t\t\t\t}\n\n\t\t\t\tif privateID := le.Uint64(blk[valStart+inodePrivateIDOff:]); privateID != inodeNum {\n\t\t\t\t\treturn fmt.Errorf(\"inode %d has mismatched private_id %d\", inodeNum, privateID)\n\t\t\t\t}\n\n\t\t\t\tcurrentUID := le.Uint32(blk[valStart+inodeOwnerOff:])\n\t\t\t\tcurrentGID := le.Uint32(blk[valStart+inodeGroupOff:])\n\t\t\t\tif currentUID != noownersPlaceholderID || currentGID != noownersPlaceholderID {\n\t\t\t\t\treturn fmt.Errorf(\"inode %d has ownership %d:%d, expected %d:%d from noowners mount\",\n\t\t\t\t\t\tinodeNum, currentUID, currentGID, noownersPlaceholderID, noownersPlaceholderID)\n\t\t\t\t}\n\n\t\t\t\tle.PutUint32(blk[valStart+inodeOwnerOff:], uid)\n\t\t\t\tle.PutUint32(blk[valStart+inodeGroupOff:], gid)\n\t\t\t\tupdateChecksum(blk)\n\t\t\t\treturn c.writeBlock(blkAddr, blk)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"inode %d not found in filesystem B-tree\", inodeNum)\n\t\t}\n\n\t\t// Internal node: descend.\n\t\tchildIdx := uint32(0)\n\t\tfor i := range nkeys {\n\t\t\tkOff, _ := c.readTocEntry(blk, tocStart, i, isFixedKV)\n\t\t\tkeyStart := keyAreaStart + kOff\n\t\t\tkeyHeader := le.Uint64(blk[keyStart:])\n\n\t\t\tif compareFSKeyHeader(keyHeader, targetKeyHeader) <= 0 {\n\t\t\t\tchildIdx = i\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t_, vOff := c.readTocEntry(blk, tocStart, childIdx, isFixedKV)\n\t\tchildOID := le.Uint64(blk[valueAreaEnd-vOff:])\n\t\tchildPhys, err := c.omapLookup(omapTreeAddr, childOID, maxXID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"resolving child OID %d: %w\", childOID, err)\n\t\t}\n\t\tblkAddr = childPhys\n\t\tblk, err = c.readBlock(blkAddr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/apfs/chown_darwin_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage apfs\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\n// TestChownIntegration creates a real APFS disk image with hdiutil,\n// writes a test file, runs Chown on the raw image, and verifies\n// ownership via os.Stat after remounting with ownership enabled.\n// No root required.\nfunc TestChownIntegration(t *testing.T) {\n\ttmpDir := t.TempDir()\n\timgPath := filepath.Join(tmpDir, \"test.dmg\")\n\n\t// Create a 64MB APFS disk image (GPT + APFS container).\n\tctx := t.Context()\n\tcmd := exec.CommandContext(ctx, \"hdiutil\", \"create\",\n\t\t\"-size\", \"64m\",\n\t\t\"-fs\", \"APFS\",\n\t\t\"-volname\", \"TestVol\",\n\t\timgPath,\n\t)\n\tout, err := cmd.CombinedOutput()\n\tassert.NilError(t, err, \"hdiutil create failed: %s\", out)\n\n\t// Attach, write a test file, and detach. Default mount uses\n\t// noowners, so the on-disk UID will be 99 (nobody).\n\tmntDir := filepath.Join(tmpDir, \"mnt\")\n\tcmd = exec.CommandContext(ctx, \"hdiutil\", \"attach\", imgPath, \"-mountpoint\", mntDir)\n\tout, err = cmd.CombinedOutput()\n\tassert.NilError(t, err, \"hdiutil attach failed: %s\", out)\n\n\tassert.NilError(t, os.WriteFile(filepath.Join(mntDir, \"testfile.txt\"), []byte(\"hello\"), 0o644))\n\n\tcmd = exec.CommandContext(ctx, \"hdiutil\", \"detach\", mntDir)\n\tout, err = cmd.CombinedOutput()\n\tassert.NilError(t, err, \"hdiutil detach failed: %s\", out)\n\n\t// Change ownership to root:wheel via raw APFS modification.\n\t// hdiutil creates a single volume with role=0 (APFS_VOL_ROLE_NONE).\n\tassert.NilError(t, Chown(imgPath, 0, 0, 0, \"testfile.txt\"))\n\n\t// Verify filesystem integrity after raw block modification.\n\tcmd = exec.CommandContext(ctx, \"hdiutil\", \"attach\", imgPath, \"-nomount\")\n\tout, err = cmd.CombinedOutput()\n\tassert.NilError(t, err, \"hdiutil attach -nomount failed: %s\", out)\n\tdev := strings.Fields(string(out))[0]\n\tcmd = exec.CommandContext(ctx, \"fsck_apfs\", \"-n\", dev)\n\tout, err = cmd.CombinedOutput()\n\tassert.NilError(t, err, \"fsck_apfs failed: %s\", out)\n\tcmd = exec.CommandContext(ctx, \"hdiutil\", \"detach\", dev)\n\tout, err = cmd.CombinedOutput()\n\tassert.NilError(t, err, \"hdiutil detach failed: %s\", out)\n\n\t// Re-attach with ownership enabled so os.Stat reflects on-disk UIDs.\n\tcmd = exec.CommandContext(ctx, \"hdiutil\", \"attach\", imgPath,\n\t\t\"-mountpoint\", mntDir, \"-owners\", \"on\")\n\tout, err = cmd.CombinedOutput()\n\tassert.NilError(t, err, \"hdiutil re-attach failed: %s\", out)\n\tdefer func() {\n\t\t_ = exec.CommandContext(ctx, \"hdiutil\", \"detach\", mntDir, \"-force\").Run()\n\t}()\n\n\tfi, err := os.Stat(filepath.Join(mntDir, \"testfile.txt\"))\n\tassert.NilError(t, err)\n\tstat := fi.Sys().(*syscall.Stat_t)\n\tassert.Equal(t, stat.Uid, uint32(0), \"expected uid=0, got uid=%d\", stat.Uid)\n\tassert.Equal(t, stat.Gid, uint32(0), \"expected gid=0, got gid=%d\", stat.Gid)\n}\n"
  },
  {
    "path": "pkg/apfs/fletcher64.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage apfs\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n)\n\nconst fletcher64Mod = 0xFFFFFFFF\n\n// fletcher64 computes the APFS Fletcher-64 checksum over block[8:].\nfunc fletcher64(block []byte) uint64 {\n\tvar sum1, sum2 uint64\n\tfor i := 8; i+3 < len(block); i += 4 {\n\t\tsum1 = (sum1 + uint64(binary.LittleEndian.Uint32(block[i:]))) % fletcher64Mod\n\t\tsum2 = (sum2 + sum1) % fletcher64Mod\n\t}\n\tckLow := fletcher64Mod - ((sum1 + sum2) % fletcher64Mod)\n\tckHigh := fletcher64Mod - ((sum1 + ckLow) % fletcher64Mod)\n\treturn (ckHigh << 32) | ckLow\n}\n\n// verifyChecksum returns an error if the stored checksum does not match.\nfunc verifyChecksum(block []byte) error {\n\tstored := binary.LittleEndian.Uint64(block[objChecksumOff:])\n\tcomputed := fletcher64(block)\n\tif stored != computed {\n\t\treturn fmt.Errorf(\"checksum mismatch: stored %#x, computed %#x\", stored, computed)\n\t}\n\treturn nil\n}\n\n// updateChecksum recomputes and stores the Fletcher-64 checksum.\nfunc updateChecksum(block []byte) {\n\tbinary.LittleEndian.PutUint64(block[objChecksumOff:], fletcher64(block))\n}\n"
  },
  {
    "path": "pkg/apfs/fletcher64_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage apfs\n\nimport (\n\t\"encoding/binary\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestFletcher64(t *testing.T) {\n\t// Construct a minimal 4096-byte block and verify the checksum\n\t// round-trips: compute -> store -> verify.\n\tblock := make([]byte, 4096)\n\tfor i := 8; i < len(block); i++ {\n\t\tblock[i] = byte(i % 251)\n\t}\n\tupdateChecksum(block)\n\tassert.NilError(t, verifyChecksum(block))\n\n\t// Corrupt one byte and verify detection.\n\tblock[100]++\n\tassert.Assert(t, verifyChecksum(block) != nil, \"verifyChecksum should have failed after corruption\")\n}\n\nfunc TestFletcher64KnownVector(t *testing.T) {\n\t// A block of all zeros (except checksum) should produce a\n\t// deterministic checksum. Verify consistency.\n\tblock := make([]byte, 4096)\n\tck := fletcher64(block)\n\tupdateChecksum(block)\n\tstored := binary.LittleEndian.Uint64(block[0:])\n\tassert.Equal(t, stored, ck)\n\tassert.NilError(t, verifyChecksum(block))\n}\n"
  },
  {
    "path": "pkg/apfs/types.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package apfs provides minimal APFS disk image manipulation.\n// It reads on-disk structures at known byte offsets per the Apple APFS\n// Reference and modifies inode UID/GID fields directly, bypassing\n// the kernel VFS.\npackage apfs\n\n// Magic numbers.\nconst (\n\tnxMagic   = 0x4253584E // \"NXSB\"\n\tapfsMagic = 0x42535041 // \"APSB\"\n)\n\n// Object types (lower 16 bits of o_type).\nconst (\n\tobjectTypeNXSuperblock    = 0x01\n\tobjectTypeCheckpointMap   = 0x0C\n\tobjectTypeOmap            = 0x0B\n\tobjectTypeBTreeNode       = 0x02\n\tobjectTypeFS              = 0x0D\n\tobjectTypeFSTree          = 0x0E\n\tobjectTypeBlockRefTree    = 0x0F\n\tobjectTypeOmapSnapshot    = 0x10\n\tobjectTypeNXReaperFS      = 0x12\n\tobjectTypeNXReaperListKey = 0x13\n)\n\n// Object storage classes (upper 16 bits of o_type, shifted).\nconst (\n\tobjPhysical  = 0x00000000\n\tobjEphemeral = 0x80000000\n\tobjVirtual   = 0x00000000\n\tobjTypeMask  = 0x0000FFFF\n\tobjFlagsMask = 0xFFFF0000\n)\n\n// B-tree node flags.\nconst (\n\tbtnodeRoot        = 0x0001\n\tbtnodeLeaf        = 0x0002\n\tbtnodeFixedKVSize = 0x0004\n)\n\n// Filesystem object types (upper 4 bits of j_key_t.obj_id_and_type).\nconst (\n\tapfsTypeInode  = 3\n\tapfsTypeDirRec = 9\n)\n\n// Filesystem key masks.\nconst (\n\tobjIDMask    = 0x0FFFFFFFFFFFFFFF\n\tobjTypeShift = 60\n)\n\n// Well-known inode numbers.\nconst (\n\trootDirInodeNum = 2\n)\n\n// noownersPlaceholderID is the on-disk UID and GID that APFS stores\n// for files on volumes mounted with the noowners option.\nconst noownersPlaceholderID = 99\n\n// VolRoleData is the APFS volume role for \"Data\" volumes.\nconst VolRoleData = 0x0040 // APFS_VOL_ROLE_DATA (shifted representation: (1 << 2) << 4)\n\n// Container superblock (nx_superblock_t) field offsets from block start.\nconst (\n\tnxMagicOff         = 32  // uint32\n\tnxBlockSizeOff     = 36  // uint32\n\tnxXPDescBlocksOff  = 104 // uint32 (mask 0x7FFFFFFF for count)\n\tnxXPDescBaseOff    = 112 // uint64 (physical block address)\n\tnxXPDescIndexOff   = 136 // uint32\n\tnxXPDescLenOff     = 140 // uint32\n\tnxOmapOIDOff       = 160 // uint64 (physical)\n\tnxFSOIDOff         = 184 // uint64[100] (virtual OIDs)\n\tnxMaxFileSystems   = 100\n\tnxXPDescBlocksMask = 0x7FFFFFFF\n)\n\n// Object header (obj_phys_t) field offsets.\nconst (\n\tobjChecksumOff = 0  // uint64\n\tobjOIDOff      = 8  // uint64\n\tobjXIDOff      = 16 // uint64\n\tobjTypeOff     = 24 // uint32\n\tobjSubtypeOff  = 28 // uint32\n\tobjHeaderSize  = 32\n)\n\n// Volume superblock (apfs_superblock_t) field offsets from block start.\nconst (\n\tapfsMagicOff       = 32  // uint32\n\tapfsOmapOIDOff     = 128 // uint64 (physical)\n\tapfsRootTreeOIDOff = 136 // uint64 (virtual)\n\tapfsVolNameOff     = 704 // 256 bytes UTF-8\n\tapfsRoleOff        = 964 // uint16\n)\n\n// Object map (omap_phys_t) field offsets from block start.\nconst (\n\tomapTreeOIDOff = 48 // uint64 (physical)\n)\n\n// B-tree node (btree_node_phys_t) field offsets from block start.\nconst (\n\tbtnFlagsOff      = 32 // uint16\n\tbtnLevelOff      = 34 // uint16\n\tbtnNKeysOff      = 36 // uint32\n\tbtnTableSpaceOff = 40 // nloc_t: off uint16 + len uint16\n\tbtnDataOff       = 56 // start of btn_data[]\n)\n\n// btree_info_t size (sits at end of root node block).\nconst btreeInfoSize = 40\n\n// j_inode_val_t field offsets (from start of value data).\nconst (\n\tinodeParentIDOff  = 0  // uint64\n\tinodePrivateIDOff = 8  // uint64\n\tinodeOwnerOff     = 72 // uint32\n\tinodeGroupOff     = 76 // uint32\n)\n"
  },
  {
    "path": "pkg/autostart/autostart.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package autostart manage start at login unit files for darwin/linux\npackage autostart\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n// IsRegistered checks if the instance is registered to start at login.\nfunc IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error) {\n\treturn manager().IsRegistered(ctx, inst)\n}\n\n// RegisterToStartAtLogin creates a start-at-login entry for the instance.\nfunc RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error {\n\treturn manager().RegisterToStartAtLogin(ctx, inst)\n}\n\n// UnregisterFromStartAtLogin deletes the start-at-login entry for the instance.\nfunc UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error {\n\treturn manager().UnregisterFromStartAtLogin(ctx, inst)\n}\n\n// AutoStartedIdentifier returns the identifier if the current process was started by the autostart manager.\nfunc AutoStartedIdentifier() string {\n\treturn manager().AutoStartedIdentifier()\n}\n\n// RequestStart requests to start the instance by identifier.\nfunc RequestStart(ctx context.Context, inst *limatype.Instance) error {\n\treturn manager().RequestStart(ctx, inst)\n}\n\n// RequestStop requests to stop the instance by identifier.\nfunc RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) {\n\treturn manager().RequestStop(ctx, inst)\n}\n\ntype autoStartManager interface {\n\t// Registration\n\tIsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error)\n\tRegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error\n\tUnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error\n\n\t// Status\n\tAutoStartedIdentifier() string\n\n\t// Operation\n\t// RequestStart requests to start the instance by identifier.\n\tRequestStart(ctx context.Context, inst *limatype.Instance) error\n\t// RequestStop requests to stop the instance by identifier.\n\tRequestStop(ctx context.Context, inst *limatype.Instance) (bool, error)\n}\n\nvar manager = sync.OnceValue(Manager)\n"
  },
  {
    "path": "pkg/autostart/autostart_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage autostart\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart/launchd\"\n\t\"github.com/lima-vm/lima/v2/pkg/autostart/systemd\"\n)\n\nvar (\n\tLaunchd = &TemplateFileBasedManager{\n\t\tfilePath:              launchd.GetPlistPath,\n\t\ttemplate:              launchd.Template,\n\t\tenabler:               launchd.EnableDisableService,\n\t\tautoStartedIdentifier: launchd.AutoStartedServiceName,\n\t\trequestStart:          launchd.RequestStart,\n\t\trequestStop:           launchd.RequestStop,\n\t}\n\tSystemd = &TemplateFileBasedManager{\n\t\tfilePath:              systemd.GetUnitPath,\n\t\ttemplate:              systemd.Template,\n\t\tenabler:               systemd.EnableDisableUnit,\n\t\tautoStartedIdentifier: systemd.AutoStartedUnitName,\n\t\trequestStart:          systemd.RequestStart,\n\t\trequestStop:           systemd.RequestStop,\n\t}\n)\n\nfunc TestRenderTemplate(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"skipping testing on windows host\")\n\t}\n\ttests := []struct {\n\t\tManager       *TemplateFileBasedManager\n\t\tName          string\n\t\tInstanceName  string\n\t\tExpected      string\n\t\tWorkDir       string\n\t\tGetExecutable func() (string, error)\n\t}{\n\t\t{\n\t\t\tManager:      Launchd,\n\t\t\tName:         \"render darwin launchd plist\",\n\t\t\tInstanceName: \"default\",\n\t\t\tExpected: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>Label</key>\n\t<string>io.lima-vm.autostart.default</string>\n\t<key>ProgramArguments</key>\n\t<array>\n\t\t<string>/limactl</string>\n\t\t<string>start</string>\n\t\t<string>default</string>\n\t\t<string>--foreground</string>\n\t</array>\n\t<key>RunAtLoad</key>\n\t<true/>\n\t<key>StandardErrorPath</key>\n\t<string>launchd.stderr.log</string>\n\t<key>StandardOutPath</key>\n\t<string>launchd.stdout.log</string>\n\t<key>WorkingDirectory</key>\n\t<string>/some/path</string>\n\t<key>ProcessType</key>\n\t<string>Background</string>\n</dict>\n</plist>\n`,\n\t\t\tGetExecutable: func() (string, error) {\n\t\t\t\treturn \"/limactl\", nil\n\t\t\t},\n\t\t\tWorkDir: \"/some/path\",\n\t\t},\n\t\t{\n\t\t\tManager:      Systemd,\n\t\t\tName:         \"render linux systemd service\",\n\t\t\tInstanceName: \"default\",\n\t\t\tExpected: `[Unit]\nDescription=Lima - Linux virtual machines, with a focus on running containers.\nDocumentation=man:lima(1)\n\n[Service]\nExecStart=/limactl start %i --foreground\nWorkingDirectory=%h\nType=simple\nTimeoutSec=10\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n`,\n\t\t\tGetExecutable: func() (string, error) {\n\t\t\t\treturn \"/limactl\", nil\n\t\t\t},\n\t\t\tWorkDir: \"/some/path\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.Name, func(t *testing.T) {\n\t\t\ttmpl, err := tt.Manager.renderTemplate(tt.InstanceName, tt.WorkDir, tt.GetExecutable)\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, string(tmpl), tt.Expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/autostart/launchd/io.lima-vm.autostart.INSTANCE.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>Label</key>\n\t<string>io.lima-vm.autostart.{{ .Instance }}</string>\n\t<key>ProgramArguments</key>\n\t<array>\n\t\t<string>{{ .Binary }}</string>\n\t\t<string>start</string>\n\t\t<string>{{ .Instance }}</string>\n\t\t<string>--foreground</string>\n\t</array>\n\t<key>RunAtLoad</key>\n\t<true/>\n\t<key>StandardErrorPath</key>\n\t<string>launchd.stderr.log</string>\n\t<key>StandardOutPath</key>\n\t<string>launchd.stdout.log</string>\n\t<key>WorkingDirectory</key>\n\t<string>{{ .WorkDir }}</string>\n\t<key>ProcessType</key>\n\t<string>Background</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "pkg/autostart/launchd/launchd.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage launchd\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n//go:embed io.lima-vm.autostart.INSTANCE.plist\nvar Template string\n\n// GetPlistPath returns the path to the launchd plist file for the given instance name.\nfunc GetPlistPath(instName string) string {\n\treturn fmt.Sprintf(\"%s/Library/LaunchAgents/%s.plist\", os.Getenv(\"HOME\"), ServiceNameFrom(instName))\n}\n\n// ServiceNameFrom returns the launchd service name for the given instance name.\nfunc ServiceNameFrom(instName string) string {\n\treturn fmt.Sprintf(\"io.lima-vm.autostart.%s\", instName)\n}\n\n// EnableDisableService enables or disables the launchd service for the given instance name.\nfunc EnableDisableService(ctx context.Context, enable bool, instName string) error {\n\taction := \"enable\"\n\tif !enable {\n\t\taction = \"disable\"\n\t}\n\treturn launchctl(ctx, action, serviceTarget(instName))\n}\n\nfunc launchctl(ctx context.Context, args ...string) error {\n\tcmd := exec.CommandContext(ctx, \"launchctl\", args...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tlogrus.Debugf(\"running command: %v\", cmd.Args)\n\treturn cmd.Run()\n}\n\nfunc launchctlWithoutOutput(ctx context.Context, args ...string) error {\n\tcmd := exec.CommandContext(ctx, \"launchctl\", args...)\n\tlogrus.Debugf(\"running command without output: %v\", cmd.Args)\n\treturn cmd.Run()\n}\n\n// AutoStartedServiceName returns the launchd service name if the instance is started by launchd.\nfunc AutoStartedServiceName() string {\n\t// Assume the instance is started by launchd if XPC_SERVICE_NAME is set and not \"0\".\n\t// To confirm it is actually started by launchd, it needs to use `launch_activate_socket`.\n\t// But that requires actual socket activation setup in the plist file.\n\t// So we just check XPC_SERVICE_NAME here.\n\tif xpcServiceName := os.Getenv(\"XPC_SERVICE_NAME\"); xpcServiceName != \"0\" {\n\t\treturn xpcServiceName\n\t}\n\treturn \"\"\n}\n\nvar domainTarget = sync.OnceValue(func() string {\n\treturn fmt.Sprintf(\"gui/%d\", os.Getuid())\n})\n\nfunc serviceTarget(instName string) string {\n\treturn fmt.Sprintf(\"%s/%s\", domainTarget(), ServiceNameFrom(instName))\n}\n\nfunc RequestStart(ctx context.Context, inst *limatype.Instance) error {\n\t// Call `launchctl bootout` first, because instance may be stopped without unloading the plist file.\n\t// If the plist file is not unloaded, `launchctl bootstrap` will fail.\n\t_ = launchctlWithoutOutput(ctx, \"bootout\", serviceTarget(inst.Name))\n\t// If disabled, `launchctl bootstrap` will fail.\n\t_ = EnableDisableService(ctx, true, inst.Name)\n\tif err := launchctl(ctx, \"bootstrap\", domainTarget(), GetPlistPath(inst.Name)); err != nil {\n\t\treturn fmt.Errorf(\"failed to start the instance %q via launchctl: %w\", inst.Name, err)\n\t}\n\treturn nil\n}\n\nfunc RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) {\n\tlogrus.Debugf(\"AutoStartedIdentifier=%q, ServiceNameFrom=%q\", inst.AutoStartedIdentifier, ServiceNameFrom(inst.Name))\n\tif inst.AutoStartedIdentifier == ServiceNameFrom(inst.Name) {\n\t\tlogrus.Infof(\"Stopping the instance %q started by launchd\", inst.Name)\n\t\tif err := launchctl(ctx, \"bootout\", serviceTarget(inst.Name)); err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to stop the instance %q via launchctl: %w\", inst.Name, err)\n\t\t}\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n"
  },
  {
    "path": "pkg/autostart/launchd/launchd_test.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage launchd\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestGetPlistPath(t *testing.T) {\n\ttests := []struct {\n\t\tName         string\n\t\tInstanceName string\n\t\tExpected     string\n\t}{\n\t\t{\n\t\t\tName:         \"darwin with docker instance name\",\n\t\t\tInstanceName: \"docker\",\n\t\t\tExpected:     \"Library/LaunchAgents/io.lima-vm.autostart.docker.plist\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.Name, func(t *testing.T) {\n\t\t\tassert.Check(t, strings.HasSuffix(GetPlistPath(tt.InstanceName), tt.Expected))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/autostart/managers.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package autostart manage start at login unit files for darwin/linux\npackage autostart\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/textutil\"\n)\n\ntype notSupportedManager struct{}\n\nvar _ autoStartManager = (*notSupportedManager)(nil)\n\nvar ErrNotSupported = fmt.Errorf(\"autostart is not supported on %s\", runtime.GOOS)\n\nfunc (*notSupportedManager) IsRegistered(_ context.Context, _ *limatype.Instance) (bool, error) {\n\treturn false, ErrNotSupported\n}\n\nfunc (*notSupportedManager) RegisterToStartAtLogin(_ context.Context, _ *limatype.Instance) error {\n\treturn ErrNotSupported\n}\n\nfunc (*notSupportedManager) UnregisterFromStartAtLogin(_ context.Context, _ *limatype.Instance) error {\n\treturn ErrNotSupported\n}\n\nfunc (*notSupportedManager) AutoStartedIdentifier() string {\n\treturn \"\"\n}\n\nfunc (*notSupportedManager) RequestStart(_ context.Context, _ *limatype.Instance) error {\n\treturn ErrNotSupported\n}\n\nfunc (*notSupportedManager) RequestStop(_ context.Context, _ *limatype.Instance) (bool, error) {\n\treturn false, ErrNotSupported\n}\n\n// TemplateFileBasedManager is an autostart manager that uses a template file to create the autostart entry.\ntype TemplateFileBasedManager struct {\n\tenabler               func(ctx context.Context, enable bool, instName string) error\n\tfilePath              func(instName string) string\n\ttemplate              string\n\tautoStartedIdentifier func() string\n\trequestStart          func(ctx context.Context, inst *limatype.Instance) error\n\trequestStop           func(ctx context.Context, inst *limatype.Instance) (bool, error)\n}\n\nvar _ autoStartManager = (*TemplateFileBasedManager)(nil)\n\nfunc (t *TemplateFileBasedManager) IsRegistered(_ context.Context, inst *limatype.Instance) (bool, error) {\n\tif t.filePath == nil {\n\t\treturn false, errors.New(\"no filePath function available\")\n\t}\n\tautostartFilePath := t.filePath(inst.Name)\n\tif _, err := os.Stat(autostartFilePath); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\nfunc (t *TemplateFileBasedManager) RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error {\n\tif _, err := t.IsRegistered(ctx, inst); err != nil {\n\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t}\n\tcontent, err := t.renderTemplate(inst.Name, inst.Dir, os.Executable)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to render the autostart entry for instance %q: %w\", inst.Name, err)\n\t}\n\tentryFilePath := t.filePath(inst.Name)\n\tif err := os.MkdirAll(filepath.Dir(entryFilePath), os.ModePerm); err != nil {\n\t\treturn fmt.Errorf(\"failed to create the directory for the autostart entry for instance %q: %w\", inst.Name, err)\n\t}\n\tif err := os.WriteFile(entryFilePath, content, 0o644); err != nil {\n\t\treturn fmt.Errorf(\"failed to write the autostart entry for instance %q: %w\", inst.Name, err)\n\t}\n\tif t.enabler != nil {\n\t\treturn t.enabler(ctx, true, inst.Name)\n\t}\n\treturn nil\n}\n\nfunc (t *TemplateFileBasedManager) UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error {\n\tif registered, err := t.IsRegistered(ctx, inst); err != nil {\n\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t} else if !registered {\n\t\treturn nil\n\t}\n\tif t.enabler != nil {\n\t\tif err := t.enabler(ctx, false, inst.Name); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to disable the autostart entry for instance %q: %w\", inst.Name, err)\n\t\t}\n\t}\n\tif err := os.Remove(t.filePath(inst.Name)); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove the autostart entry for instance %q: %w\", inst.Name, err)\n\t}\n\treturn nil\n}\n\nfunc (t *TemplateFileBasedManager) renderTemplate(instName, workDir string, getExecutable func() (string, error)) ([]byte, error) {\n\tif t.template == \"\" {\n\t\treturn nil, errors.New(\"no template available\")\n\t}\n\tselfExeAbs, err := getExecutable()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn textutil.ExecuteTemplate(\n\t\tt.template,\n\t\tmap[string]string{\n\t\t\t\"Binary\":   selfExeAbs,\n\t\t\t\"Instance\": instName,\n\t\t\t\"WorkDir\":  workDir,\n\t\t})\n}\n\nfunc (t *TemplateFileBasedManager) AutoStartedIdentifier() string {\n\tif t.autoStartedIdentifier != nil {\n\t\treturn t.autoStartedIdentifier()\n\t}\n\treturn \"\"\n}\n\nfunc (t *TemplateFileBasedManager) RequestStart(ctx context.Context, inst *limatype.Instance) error {\n\tif t.requestStart == nil {\n\t\treturn errors.New(\"no RequestStart function available\")\n\t}\n\treturn t.requestStart(ctx, inst)\n}\n\nfunc (t *TemplateFileBasedManager) RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) {\n\tif t.requestStop == nil {\n\t\treturn false, errors.New(\"no RequestStop function available\")\n\t}\n\treturn t.requestStop(ctx, inst)\n}\n"
  },
  {
    "path": "pkg/autostart/managers_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage autostart\n\nimport \"github.com/lima-vm/lima/v2/pkg/autostart/launchd\"\n\n// Manager returns the autostart manager for Darwin.\nfunc Manager() autoStartManager {\n\treturn &TemplateFileBasedManager{\n\t\tfilePath:              launchd.GetPlistPath,\n\t\ttemplate:              launchd.Template,\n\t\tenabler:               launchd.EnableDisableService,\n\t\tautoStartedIdentifier: launchd.AutoStartedServiceName,\n\t\trequestStart:          launchd.RequestStart,\n\t\trequestStop:           launchd.RequestStop,\n\t}\n}\n"
  },
  {
    "path": "pkg/autostart/managers_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage autostart\n\nimport \"github.com/lima-vm/lima/v2/pkg/autostart/systemd\"\n\n// Manager returns the autostart manager for Linux.\nfunc Manager() autoStartManager {\n\tif systemd.IsRunningSystemd() {\n\t\treturn &TemplateFileBasedManager{\n\t\t\tfilePath:              systemd.GetUnitPath,\n\t\t\ttemplate:              systemd.Template,\n\t\t\tenabler:               systemd.EnableDisableUnit,\n\t\t\tautoStartedIdentifier: systemd.AutoStartedUnitName,\n\t\t\trequestStart:          systemd.RequestStart,\n\t\t\trequestStop:           systemd.RequestStop,\n\t\t}\n\t}\n\t// TODO: add support for non-systemd Linux distros\n\treturn &notSupportedManager{}\n}\n"
  },
  {
    "path": "pkg/autostart/managers_others.go",
    "content": "//go:build !darwin && !linux\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage autostart\n\n// Manager returns a notSupportedManager for unsupported OSes.\nfunc Manager() autoStartManager {\n\treturn &notSupportedManager{}\n}\n"
  },
  {
    "path": "pkg/autostart/systemd/lima-vm@INSTANCE.service",
    "content": "[Unit]\nDescription=Lima - Linux virtual machines, with a focus on running containers.\nDocumentation=man:lima(1)\n\n[Service]\nExecStart={{.Binary}} start %i --foreground\nWorkingDirectory=%h\nType=simple\nTimeoutSec=10\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n"
  },
  {
    "path": "pkg/autostart/systemd/systemd.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage systemd\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n//go:embed lima-vm@INSTANCE.service\nvar Template string\n\n// GetUnitPath returns the path to the systemd unit file for the given instance name.\nfunc GetUnitPath(instName string) string {\n\t// Use instance name as argument to systemd service\n\t// Instance name available in unit file as %i\n\txdgConfigHome := os.Getenv(\"XDG_CONFIG_HOME\")\n\tif xdgConfigHome == \"\" {\n\t\txdgConfigHome = filepath.Join(os.Getenv(\"HOME\"), \".config\")\n\t}\n\treturn fmt.Sprintf(\"%s/systemd/user/%s\", xdgConfigHome, UnitNameFrom(instName))\n}\n\n// UnitNameFrom returns the systemd service name for the given instance name.\nfunc UnitNameFrom(instName string) string {\n\treturn fmt.Sprintf(\"lima-vm@%s.service\", instName)\n}\n\n// EnableDisableUnit enables or disables the systemd service for the given instance name.\nfunc EnableDisableUnit(ctx context.Context, enable bool, instName string) error {\n\taction := \"enable\"\n\tif !enable {\n\t\taction = \"disable\"\n\t}\n\treturn systemctl(ctx, \"--user\", action, UnitNameFrom(instName))\n}\n\nfunc systemctl(ctx context.Context, args ...string) error {\n\tcmd := exec.CommandContext(ctx, \"systemctl\", args...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tlogrus.Debugf(\"running command: %v\", cmd.Args)\n\treturn cmd.Run()\n}\n\n// AutoStartedUnitName returns the systemd service name if the instance is started by systemd.\nfunc AutoStartedUnitName() string {\n\treturn CurrentUnitName()\n}\n\nfunc RequestStart(ctx context.Context, inst *limatype.Instance) error {\n\tif err := systemctl(ctx, \"--user\", \"start\", UnitNameFrom(inst.Name)); err != nil {\n\t\treturn fmt.Errorf(\"failed to start the instance %q via systemctl: %w\", inst.Name, err)\n\t}\n\treturn nil\n}\n\nfunc RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) {\n\tif inst.AutoStartedIdentifier == UnitNameFrom(inst.Name) {\n\t\tlogrus.Infof(\"Stopping the instance %q started by systemd\", inst.Name)\n\t\tif err := systemctl(ctx, \"--user\", \"stop\", inst.AutoStartedIdentifier); err != nil {\n\t\t\treturn false, fmt.Errorf(\"failed to stop the instance %q via systemctl: %w\", inst.Name, err)\n\t\t}\n\t\treturn true, nil\n\t}\n\treturn false, nil\n}\n"
  },
  {
    "path": "pkg/autostart/systemd/systemd_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage systemd\n\nimport (\n\t\"github.com/coreos/go-systemd/v22/util\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc CurrentUnitName() string {\n\tunit, err := util.CurrentUnitName()\n\tif err != nil {\n\t\tlogrus.WithError(err).Debug(\"cannot determine current systemd unit name\")\n\t}\n\treturn unit\n}\n\nfunc IsRunningSystemd() bool {\n\treturn util.IsRunningSystemd()\n}\n"
  },
  {
    "path": "pkg/autostart/systemd/systemd_others.go",
    "content": "//go:build !linux\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage systemd\n\nfunc CurrentUnitName() string {\n\treturn \"\"\n}\n\nfunc IsRunningSystemd() bool {\n\treturn false\n}\n"
  },
  {
    "path": "pkg/autostart/systemd/systemd_test.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage systemd\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestGetUnitPath(t *testing.T) {\n\ttests := []struct {\n\t\tName         string\n\t\tInstanceName string\n\t\tExpected     string\n\t}{\n\t\t{\n\t\t\tName:         \"linux with docker instance name\",\n\t\t\tInstanceName: \"docker\",\n\t\t\tExpected:     \".config/systemd/user/lima-vm@docker.service\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.Name, func(t *testing.T) {\n\t\t\tassert.Check(t, strings.HasSuffix(GetUnitPath(tt.InstanceName), tt.Expected))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/bicopy/bicopy.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://raw.githubusercontent.com/norouter/norouter/v0.6.5/pkg/agent/bicopy/bicopy.go\n/*\n   Copyright (C) NoRouter authors.\n\n   Copyright (C) libnetwork authors.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n*/\n\npackage bicopy\n\nimport (\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Bicopy is from https://github.com/rootless-containers/rootlesskit/blob/v0.10.1/pkg/port/builtin/parent/tcp/tcp.go#L73-L104\n// (originally from libnetwork, Apache License 2.0).\nfunc Bicopy(x, y io.ReadWriter, quit <-chan struct{}) {\n\ttype closeReader interface {\n\t\tCloseRead() error\n\t}\n\ttype closeWriter interface {\n\t\tCloseWrite() error\n\t}\n\tvar wg sync.WaitGroup\n\tbroker := func(to, from io.ReadWriter) {\n\t\tif _, err := io.Copy(to, from); err != nil {\n\t\t\tlogrus.WithError(err).Debug(\"failed to call io.Copy\")\n\t\t}\n\t\tif fromCR, ok := from.(closeReader); ok {\n\t\t\tif err := fromCR.CloseRead(); err != nil {\n\t\t\t\tlogrus.WithError(err).Debug(\"failed to call CloseRead\")\n\t\t\t}\n\t\t}\n\t\tif toCW, ok := to.(closeWriter); ok {\n\t\t\tif err := toCW.CloseWrite(); err != nil {\n\t\t\t\tlogrus.WithError(err).Debug(\"failed to call CloseWrite\")\n\t\t\t}\n\t\t}\n\t\twg.Done()\n\t}\n\n\twg.Add(2)\n\tgo broker(x, y)\n\tgo broker(y, x)\n\tfinish := make(chan struct{})\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(finish)\n\t}()\n\n\tselect {\n\tcase <-quit:\n\tcase <-finish:\n\t}\n\tif xCloser, ok := x.(io.Closer); ok {\n\t\tif err := xCloser.Close(); err != nil {\n\t\t\tlogrus.WithError(err).Debug(\"failed to call xCloser.Close\")\n\t\t}\n\t}\n\tif yCloser, ok := y.(io.Closer); ok {\n\t\tif err := yCloser.Close(); err != nil {\n\t\t\tlogrus.WithError(err).Debug(\"failed to call yCloser.Close\")\n\t\t}\n\t}\n\t<-finish\n\t// TODO: return copied bytes\n}\n"
  },
  {
    "path": "pkg/cacheutil/cacheutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cacheutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/downloader\"\n\t\"github.com/lima-vm/lima/v2/pkg/fileutils\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n// NerdctlArchive returns the basename of the archive.\nfunc NerdctlArchive(y *limatype.LimaYAML) string {\n\tif *y.Containerd.System || *y.Containerd.User {\n\t\tfor _, f := range y.Containerd.Archives {\n\t\t\tif f.Arch == *y.Arch {\n\t\t\t\treturn path.Base(f.Location)\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// EnsureNerdctlArchiveCache prefetches the nerdctl-full-VERSION-GOOS-GOARCH.tar.gz archive\n// into the cache before launching the hostagent process, so that we can show the progress in tty.\n// https://github.com/lima-vm/lima/issues/326\nfunc EnsureNerdctlArchiveCache(ctx context.Context, y *limatype.LimaYAML, created bool) (string, error) {\n\tif !*y.Containerd.System && !*y.Containerd.User {\n\t\t// nerdctl archive is not needed\n\t\treturn \"\", nil\n\t}\n\n\terrs := make([]error, len(y.Containerd.Archives))\n\tfor i, f := range y.Containerd.Archives {\n\t\t// Skip downloading again if the file is already in the cache\n\t\tif created && f.Arch == *y.Arch && !downloader.IsLocal(f.Location) {\n\t\t\tpath, err := fileutils.CachedFile(f)\n\t\t\tif err == nil {\n\t\t\t\treturn path, nil\n\t\t\t}\n\t\t}\n\t\tpath, err := fileutils.DownloadFile(ctx, \"\", f, false, \"the nerdctl archive\", *y.Arch)\n\t\tif err != nil {\n\t\t\terrs[i] = err\n\t\t\tcontinue\n\t\t}\n\t\tif path == \"\" {\n\t\t\tif downloader.IsLocal(f.Location) {\n\t\t\t\treturn f.Location, nil\n\t\t\t}\n\t\t\treturn \"\", fmt.Errorf(\"cache did not contain %q\", f.Location)\n\t\t}\n\t\treturn path, nil\n\t}\n\n\treturn \"\", fileutils.Errors(errs)\n}\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.FreeBSD/05-lima-mounts.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# Populate mounts from the cidata.\n# This script is a workaround until nuageinit supports mounts.\n\nset -eux\n\nif ! grep -q '#LIMA-START' /etc/fstab; then\n\t\"${LIMA_CIDATA_MNT}\"/util.FreeBSD/print_cidata_fstab.lua >/etc/fstab.lima.tmp\n\tif [ -s /etc/fstab.lima.tmp ]; then\n\t\t{\n\t\t\techo \"#LIMA-START\"\n\t\t\tcat /etc/fstab.lima.tmp\n\t\t\techo \"#LIMA-END\"\n\t\t} >>/etc/fstab\n\tfi\n\trm -f /etc/fstab.lima.tmp\nfi\n\n# Run mkdir on every boot, as the mount points may be on tmpfs\nawk '/^[^#]/ && $2 != \"none\" {print $2}' /etc/fstab | while IFS= read -r line; do\n\tmkdir -p \"${line}\"\ndone\nmount -a\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-alpine-user-group.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\ntest -f /etc/alpine-release || exit 0\n\n# Make sure that root is in the sudoers file.\n# This is needed to run the user provisioning scripts.\nSUDOERS=/etc/sudoers.d/00-root-user\nif [ ! -f $SUDOERS ]; then\n\techo \"root ALL=(ALL) NOPASSWD:ALL\" >$SUDOERS\n\tchmod 660 $SUDOERS\nfi\n\n# Remove the user embedded in the image,\n# and use cloud-init for users and groups.\nif [ \"$LIMA_CIDATA_USER\" != \"alpine\" ]; then\n\tif [ \"$(id -u alpine 2>&1)\" = \"1000\" ]; then\n\t\tuserdel alpine\n\t\trmdir /home/alpine\n\t\tcloud-init clean --logs\n\t\treboot\n\tfi\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-check-rtc-and-wait-ntp.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu\n\n# In vz, the VM lacks an RTC when booting with a kernel image (see: https://developer.apple.com/forums/thread/760344).\n# This causes incorrect system time until NTP synchronizes it, leading to TLS errors.\n# To avoid TLS errors, this script waits for NTP synchronization if RTC is unavailable.\ntest ! -c /dev/rtc0 || exit 0\n\n# This script is intended for services running with systemd.\ncommand -v systemctl >/dev/null 2>&1 || exit 0\n\necho_with_time_usec() {\n\ttime_usec=$(timedatectl show --property=TimeUSec)\n\techo \"${time_usec}, ${1}\"\n}\n\n# For the first boot, where the above setting is not yet active, wait for NTP synchronization here.\nmax_retry=60 retry=0\nuntil ntp_synchronized=$(timedatectl show --property=NTPSynchronized --value) && [ \"${ntp_synchronized}\" = \"yes\" ] ||\n\t[ \"${retry}\" -gt \"${max_retry}\" ]; do\n\tif [ \"${retry}\" -eq 0 ]; then\n\t\t# If /dev/rtc is not available, the system time set during the Linux kernel build is used.\n\t\t# The larger the difference between this system time and the NTP server time, the longer the NTP synchronization will take.\n\t\t# By setting the system time to the modification time of the reference file, which is likely to be closer to the actual time,\n\t\treference_file=\"${LIMA_CIDATA_MNT:-/mnt/lima-cidata}/user-data\"\n\t\t[ -f \"${reference_file}\" ] || reference_file=\"${0}\"\n\n\t\t# the NTP synchronization time can be shortened.\n\t\techo_with_time_usec \"Setting the system time to the modification time of ${reference_file}.\"\n\n\t\t# To set the time to a specified time, it is necessary to stop systemd-timesyncd.\n\t\tsystemctl stop systemd-timesyncd\n\n\t\t# Since `timedatectl set-time` fails if systemd-timesyncd is not stopped,\n\t\t# ensure that it is completely stopped before proceeding.\n\t\tuntil pid_of_timesyncd=$(systemctl show systemd-timesyncd --property=MainPID --value) && [ \"${pid_of_timesyncd}\" -eq 0 ]; do\n\t\t\techo_with_time_usec \"Waiting for systemd-timesyncd to stop...\"\n\t\t\tsleep 1\n\t\tdone\n\n\t\t# Set the system time to the modification time of the reference file.\n\t\tmodification_time=$(stat -c %y \"${reference_file}\")\n\t\techo_with_time_usec \"Setting the system time to ${modification_time}.\"\n\t\ttimedatectl set-time \"${modification_time}\"\n\n\t\t# Restart systemd-timesyncd\n\t\tsystemctl start systemd-timesyncd\n\telse\n\t\techo_with_time_usec \"Waiting for NTP synchronization...\"\n\tfi\n\tretry=$((retry + 1))\n\tsleep 1\ndone\n# Print the result of NTP synchronization\nntp_message=$(timedatectl show-timesync --property=NTPMessage)\nif [ \"${ntp_synchronized}\" = \"yes\" ]; then\n\techo_with_time_usec \"NTP synchronization complete.\"\n\techo \"${ntp_message}\"\nelse\n\techo_with_time_usec \"NTP synchronization timed out.\"\n\techo \"${ntp_message}\"\n\texit 1\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-guest-home.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu\n\n# The default guest home directory is \"/home/${LIMA_CIDATA_USER}.guest\" since Lima 2.1.0.\n# The path was previously \"/home/${LIMA_CIDATA_USER}.linux\".\nif [ -d \"/home/${LIMA_CIDATA_USER}.guest\" ] && [ ! -e \"/home/${LIMA_CIDATA_USER}.linux\" ]; then\n\tln -s \"${LIMA_CIDATA_USER}.guest\" \"/home/${LIMA_CIDATA_USER}.linux\"\nfi\nif [ -d \"/home/${LIMA_CIDATA_USER}.linux\" ] && [ ! -e \"/home/${LIMA_CIDATA_USER}.guest\" ]; then\n\tln -s \"${LIMA_CIDATA_USER}.linux\" \"/home/${LIMA_CIDATA_USER}.guest\"\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-modprobe.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# Load modules as soon as the cloud-init starts up.\n# Because Arch Linux removes kernel module files when the kernel package was updated during running cloud-init :(\n\nset -eu\n\ncommand -v modprobe >/dev/null 2>&1 || exit 0\n\nfor f in \\\n\tfuse \\\n\ttun tap \\\n\tbridge veth \\\n\tip_tables ip6_tables iptable_nat ip6table_nat iptable_filter ip6table_filter \\\n\tnf_tables \\\n\tx_tables xt_MASQUERADE xt_addrtype xt_comment xt_conntrack xt_mark xt_multiport xt_nat xt_tcpudp \\\n\toverlay; do\n\techo \"Loading kernel module \\\"$f\\\"\"\n\tif ! modprobe \"$f\"; then\n\t\techo >&2 \"Failed to load \\\"$f\\\" (negligible if it is built-in the kernel)\"\n\tfi\ndone\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/00-reboot-if-required.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\n[ \"$LIMA_CIDATA_UPGRADE_PACKAGES\" = \"1\" ] || exit 0\n\n# Check if cloud-init forgot to reboot_if_required\n# (only implemented for apt at the moment, not dnf)\n\nif command -v dnf >/dev/null 2>&1; then\n\t# dnf-utils needs to be installed, for needs-restarting\n\tif dnf -h needs-restarting >/dev/null 2>&1; then\n\t\t# check-update returns \"false\" (100) if updates (!)\n\t\tset +e\n\t\tdnf check-update >/dev/null\n\t\tif [ \"$?\" != \"1\" ]; then\n\t\t\t# needs-restarting messages are translated _()\n\t\t\texport LC_ALL=C.UTF-8\n\t\t\tlogfile=$(mktemp)\n\t\t\t# needs-restarting returns \"false\" if needed (!)\n\t\t\tset -o pipefail\n\t\t\tdnf needs-restarting -r | tee \"$logfile\"\n\t\t\tif [ \"$?\" = \"1\" ]; then\n\t\t\t\tif grep -q \"Reboot is required\" \"$logfile\"; then\n\t\t\t\t\tsystemctl reboot\n\t\t\t\tfi\n\t\t\tfi\n\t\t\trm \"$logfile\"\n\t\tfi\n\tfi\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/01-alpine-ash-as-bash.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This script pretends that /bin/ash can be used as /bin/bash, so all following\n# cloud-init scripts can use `#!/bin/bash` and `set -o pipefail`.\ntest -f /etc/alpine-release || exit 0\n\n# If bash already exists, do nothing.\ntest -x /bin/bash && exit 0\n\n# Redirect bash to ash (built with CONFIG_ASH_BASH_COMPAT) and hope for the best :)\n# It does support `set -o pipefail`, but not `[[`.\n# /bin/bash can't be a symlink because /bin/ash is a symlink to /bin/busybox\ncat >/bin/bash <<'EOF'\n#!/bin/sh\nexec /bin/ash \"$@\"\nEOF\nchmod +x /bin/bash\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/04-persistent-data-volume.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# bash is used for enabling `set -o pipefail`.\n# NOTE: On Alpine, /bin/bash is ash with ASH_BASH_COMPAT, not GNU bash\nset -eux -o pipefail\n\n# Restrict the rest of this script to Alpine until it has been tested with other distros\ntest -f /etc/alpine-release || exit 0\n\n# Nothing to do unless we are running from a ramdisk\n[ \"$(awk '$2 == \"/\" {print $3}' /proc/mounts)\" != \"tmpfs\" ] && exit 0\n\n# Data directories that should be persisted across reboots\nDATADIRS=\"/etc /home /root /tmp /usr/local /var/lib\"\n\n# Prepare mnt.sh (used for restoring mounts later)\necho \"#!/bin/sh\" >/mnt.sh\necho \"set -eux\" >>/mnt.sh\nfor DIR in ${DATADIRS}; do\n\twhile IFS= read -r LINE; do\n\t\t[ -z \"$LINE\" ] && continue\n\t\tMNTDEV=\"$(echo \"${LINE}\" | awk '{print $1}')\"\n\t\t# unmangle \" \\t\\n\\\\#\"\n\t\t# https://github.com/torvalds/linux/blob/v6.6/fs/proc_namespace.c#L89\n\t\tMNTPNT=\"$(echo \"${LINE}\" | awk '{print $2}' | sed -e 's/\\\\040/ /g; s/\\\\011/\\t/g; s/\\\\012/\\n/g; s/\\\\134/\\\\/g; s/\\\\043/#/g')\"\n\t\t# Ignore if MNTPNT is neither DIR nor a parent directory of DIR.\n\t\t# It is not a parent if MNTPNT doesn't start with DIR, or the first\n\t\t# character after DIR isn't a slash.\n\t\tWITHOUT_DIR=\"${MNTPNT#\"$DIR\"}\"\n\t\t# shellcheck disable=SC2166\n\t\t[ \"$MNTPNT\" != \"$DIR\" ] && [ \"$MNTPNT\" == \"$WITHOUT_DIR\" -o \"${WITHOUT_DIR::1}\" != \"/\" ] && continue\n\t\tMNTTYPE=\"$(echo \"${LINE}\" | awk '{print $3}')\"\n\t\t[ \"${MNTTYPE}\" = \"ext4\" ] && continue\n\t\t[ \"${MNTTYPE}\" = \"tmpfs\" ] && continue\n\t\tMNTOPTS=\"$(echo \"${LINE}\" | awk '{print $4}')\"\n\t\tif [ \"${MNTTYPE}\" = \"9p\" ]; then\n\t\t\t# https://github.com/torvalds/linux/blob/v6.6/fs/9p/v9fs.h#L61\n\t\t\tMNTOPTS=\"$(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,/')\"\n\t\tfi\n\t\t# Before mv, unmount filesystems (virtiofs, 9p, etc.) below \"${DIR}\", otherwise host mounts will be wiped out\n\t\t# https://github.com/rancher-sandbox/rancher-desktop/issues/6582\n\t\tumount \"${MNTPNT}\" || exit 1\n\t\tMNTPNT=${MNTPNT//\\\\/\\\\\\\\}\n\t\tMNTPNT=${MNTPNT//\\\"/\\\\\\\"}\n\t\techo \"mount -t \\\"${MNTTYPE}\\\" -o \\\"${MNTOPTS}\\\" \\\"${MNTDEV}\\\" \\\"${MNTPNT}\\\"\" >>/mnt.sh\n\tdone </proc/mounts\ndone\nchmod +x /mnt.sh\n\nmkdir -p /mnt/data\nif [ -e /dev/disk/by-label/data-volume ]; then\n\t# Find which disk is data volume on\n\tDATA_DISK=$(blkid | grep \"data-volume\" | awk '{split($0,s,\":\"); sub(/\\d$/, \"\", s[1]); print s[1]};')\n\t# growpart command may be missing in older VMs\n\tif command -v growpart >/dev/null 2>&1 && command -v resize2fs >/dev/null 2>&1; then\n\t\t# Automatically expand the data volume filesystem\n\t\tgrowpart \"$DATA_DISK\" 1 || true\n\t\t# Only resize when filesystem is in a healthy state\n\t\tif e2fsck -f -p /dev/disk/by-label/data-volume; then\n\t\t\tresize2fs /dev/disk/by-label/data-volume || true\n\t\tfi\n\tfi\n\t# Mount data volume\n\tmount -t ext4 /dev/disk/by-label/data-volume /mnt/data\n\t# Update /etc files that might have changed during this boot\n\tcp /etc/network/interfaces /mnt/data/etc/network/\n\tcp /etc/resolv.conf /mnt/data/etc/\n\tif [ -f /etc/localtime ]; then\n\t\t# Preserve symlink\n\t\tcp -d /etc/localtime /mnt/data/etc/\n\t\t# setup-timezone copies the single zoneinfo file into /etc/zoneinfo and targets the symlink there\n\t\tif [ -d /etc/zoneinfo ]; then\n\t\t\trm -rf /mnt/data/etc/zoneinfo\n\t\t\tcp -r /etc/zoneinfo /mnt/data/etc\n\t\tfi\n\tfi\n\tif [ -f /etc/timezone ]; then\n\t\tcp /etc/timezone /mnt/data/etc/\n\tfi\n\t# TODO there are probably others that should be updated as well\nelse\n\t# Find an unpartitioned disk and create data-volume\n\tDISKS=$(lsblk --list --noheadings --output name,type | awk '$2 == \"disk\" {print $1}')\n\tfor DISK in ${DISKS}; do\n\t\tIN_USE=false\n\t\t# Looking for a disk that is not mounted or partitioned\n\t\t# shellcheck disable=SC2013\n\t\tfor PART in $(awk '/^\\/dev\\// {gsub(\"/dev/\", \"\"); print $1}' /proc/mounts); do\n\t\t\tif [ \"${DISK}\" == \"${PART}\" ] || [ -e /sys/block/\"${DISK}\"/\"${PART}\" ]; then\n\t\t\t\tIN_USE=true\n\t\t\t\tbreak\n\t\t\tfi\n\t\tdone\n\t\tif [ \"${IN_USE}\" == \"false\" ]; then\n\t\t\techo 'type=83' | sfdisk --label dos /dev/\"${DISK}\"\n\t\t\tPART=$(lsblk --list /dev/\"${DISK}\" --noheadings --output name,type | awk '$2 == \"part\" {print $1}')\n\t\t\tmkfs.ext4 -L data-volume /dev/\"${PART}\"\n\t\t\tmount -t ext4 /dev/disk/by-label/data-volume /mnt/data\n\t\t\t# setup apk package cache\n\t\t\tmkdir -p /mnt/data/apk/cache\n\t\t\tmkdir -p /etc/apk\n\t\t\tln -s /mnt/data/apk/cache /etc/apk/cache\n\t\t\t# Move all persisted directories to the data volume\n\t\t\tfor DIR in ${DATADIRS}; do\n\t\t\t\tDEST=\"/mnt/data$(dirname \"${DIR}\")\"\n\t\t\t\tmkdir -p \"${DIR}\" \"${DEST}\"\n\t\t\t\tmv \"${DIR}\" \"${DEST}\"\n\t\t\tdone\n\t\t\t# Make sure all data moved to the persistent volume has been committed to disk\n\t\t\tsync\n\t\t\tbreak\n\t\tfi\n\tdone\nfi\nfor DIR in ${DATADIRS}; do\n\tif [ -d /mnt/data\"${DIR}\" ]; then\n\t\tmkdir -p \"${DIR}\"\n\t\tmount --bind /mnt/data\"${DIR}\" \"${DIR}\"\n\tfi\ndone\n# Remount submounts on top of the new ${DIR}\n/mnt.sh\n# Reinstall packages from /mnt/data/apk/cache into the RAM disk\napk fix --no-network\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-disks.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux -o pipefail\n\ntest \"$LIMA_CIDATA_DISKS\" -gt 0 || exit 0\n\nget_disk_var() {\n\tdiskvarname=\"LIMA_CIDATA_DISK_${1}_${2}\"\n\teval echo \\$\"$diskvarname\"\n}\n\nfor i in $(seq 0 $((LIMA_CIDATA_DISKS - 1))); do\n\tDISK_NAME=\"$(get_disk_var \"$i\" \"NAME\")\"\n\tDEVICE_NAME=\"$(get_disk_var \"$i\" \"DEVICE\")\"\n\tFORMAT_DISK=\"$(get_disk_var \"$i\" \"FORMAT\")\"\n\tFORMAT_FSTYPE=\"$(get_disk_var \"$i\" \"FSTYPE\")\"\n\tFORMAT_FSARGS=\"$(get_disk_var \"$i\" \"FSARGS\")\"\n\n\ttest -n \"$FORMAT_DISK\" || FORMAT_DISK=true\n\ttest -n \"$FORMAT_FSTYPE\" || FORMAT_FSTYPE=ext4\n\n\t# first time setup\n\tif [[ ! -b \"/dev/disk/by-label/lima-${DISK_NAME}\" ]]; then\n\t\tif $FORMAT_DISK; then\n\t\t\tif [ \"$FORMAT_FSTYPE\" == \"swap\" ]; then\n\t\t\t\techo 'type=swap' | sfdisk --label gpt \"/dev/${DEVICE_NAME}\"\n\t\t\t\t# shellcheck disable=SC2086\n\t\t\t\tmkswap $FORMAT_FSARGS -L \"lima-${DISK_NAME}\" \"/dev/${DEVICE_NAME}1\"\n\t\t\telse\n\t\t\t\techo 'type=linux' | sfdisk --label gpt \"/dev/${DEVICE_NAME}\"\n\t\t\t\t# shellcheck disable=SC2086\n\t\t\t\tmkfs.$FORMAT_FSTYPE $FORMAT_FSARGS -L \"lima-${DISK_NAME}\" \"/dev/${DEVICE_NAME}1\"\n\t\t\tfi\n\t\tfi\n\tfi\n\n\tif [ \"$FORMAT_FSTYPE\" == \"swap\" ]; then\n\t\tswapon \"/dev/${DEVICE_NAME}1\"\n\telse\n\t\tmkdir -p \"/mnt/lima-${DISK_NAME}\"\n\t\tmount -t \"$FORMAT_FSTYPE\" \"/dev/${DEVICE_NAME}1\" \"/mnt/lima-${DISK_NAME}\"\n\tfi\n\tif command -v growpart >/dev/null 2>&1 && command -v resize2fs >/dev/null 2>&1; then\n\t\tgrowpart \"/dev/${DEVICE_NAME}\" 1 || true\n\t\t# Only resize when filesystem is in a healthy state\n\t\tif command -v \"fsck.$FORMAT_FSTYPE\" -f -p \"/dev/disk/by-label/lima-${DISK_NAME}\"; then\n\t\t\tif [[ $FORMAT_FSTYPE == \"ext2\" || $FORMAT_FSTYPE == \"ext3\" || $FORMAT_FSTYPE == \"ext4\" ]]; then\n\t\t\t\tresize2fs \"/dev/disk/by-label/lima-${DISK_NAME}\" || true\n\t\t\telif [ \"$FORMAT_FSTYPE\" == \"xfs\" ]; then\n\t\t\t\txfs_growfs \"/dev/disk/by-label/lima-${DISK_NAME}\" || true\n\t\t\telse\n\t\t\t\techo >&2 \"WARNING: unknown fs '$FORMAT_FSTYPE'. FS will not be grew up automatically\"\n\t\t\tfi\n\t\tfi\n\tfi\ndone\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/05-lima-mounts.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux -o pipefail\n\n# Check if mount type is virtiofs and vm type as vz\nif ! [[ ${LIMA_CIDATA_VMTYPE} == \"vz\" && ${LIMA_CIDATA_MOUNTTYPE} == \"virtiofs\" ]]; then\n\texit 0\nfi\n\n# Update fstab entries and unmount/remount the volumes with secontext options\n# when selinux is enabled in kernel\nif [ -d /sys/fs/selinux ]; then\n\tLABEL_BIN=\"system_u:object_r:bin_t:s0\"\n\tLABEL_NFS=\"system_u:object_r:nfs_t:s0\"\n\t# shellcheck disable=SC2013\n\tfor line in $(grep -n virtiofs </etc/fstab | cut -d':' -f1); do\n\t\tOPTIONS=$(awk -v line=\"$line\" 'NR==line {print $4}' /etc/fstab)\n\t\tTAG=$(awk -v line=\"$line\" 'NR==line {print $1}' /etc/fstab)\n\t\tMOUNT_OPTIONS=$(mount | grep \"${TAG}\" | awk '{print $6}')\n\t\tif [[ ${OPTIONS} != *\"context\"* ]]; then\n\t\t\t##########################################################################################\n\t\t\t## When using vz & virtiofs, initially container_file_t selinux label\n\t\t\t## was considered which works perfectly for container work loads\n\t\t\t## but it might break for other work loads if the process is running with\n\t\t\t## different label. Also these are the remote mounts from the host machine,\n\t\t\t## so keeping the label as nfs_t fits right. Package container-selinux by\n\t\t\t## default adds rules for nfs_t context which allows container workloads to work as well.\n\t\t\t## https://github.com/lima-vm/lima/pull/1965\n\t\t\t##\n\t\t\t## With integration[https://github.com/lima-vm/lima/pull/2474] with systemd-binfmt,\n\t\t\t## the existing \"nfs_t\" selinux label for Rosetta is causing issues while registering it.\n\t\t\t## This behaviour needs to be fixed by setting the label as \"bin_t\"\n\t\t\t## https://github.com/lima-vm/lima/pull/2630\n\t\t\t##########################################################################################\n\t\t\tif [[ ${TAG} == *\"rosetta\"* ]]; then\n\t\t\t\tlabel=${LABEL_BIN}\n\t\t\telse\n\t\t\t\tlabel=${LABEL_NFS}\n\t\t\tfi\n\t\t\tsed -i -e \"$line\"\"s/comment=cloudconfig/comment=cloudconfig,context=\\\"$label\\\"/g\" /etc/fstab\n\t\t\tif [[ ${MOUNT_OPTIONS} != *\"$label\"* ]]; then\n\t\t\t\tMOUNT_POINT=$(awk -v line=\"$line\" 'NR==line {print $2}' /etc/fstab)\n\t\t\t\tOPTIONS=$(awk -v line=\"$line\" 'NR==line {print $4}' /etc/fstab)\n\n\t\t\t\t#########################################################\n\t\t\t\t## We need to migrate existing users of Fedora having\n\t\t\t\t## Rosetta mounted from nfs_t to bin_t by unregistering\n\t\t\t\t## it from binfmt before remounting\n\t\t\t\t#########################################################\n\t\t\t\tif [[ ${TAG} == *\"rosetta\"* && ${MOUNT_OPTIONS} == *\"${LABEL_NFS}\"* ]]; then\n\t\t\t\t\t[ ! -f \"/proc/sys/fs/binfmt_misc/rosetta\" ] || echo -1 >/proc/sys/fs/binfmt_misc/rosetta\n\t\t\t\tfi\n\t\t\t\tumount \"${TAG}\"\n\t\t\t\tmount -t virtiofs \"${TAG}\" \"${MOUNT_POINT}\" -o \"${OPTIONS}\"\n\t\t\tfi\n\t\tfi\n\tdone\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/06-enable-mdns-on-systemd.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux -o pipefail\n\n# Do nothing on OpenRC systems (e.g., Alpine Linux)\nif [[ -f /sbin/openrc-run ]]; then\n\texit 0\nfi\n\n# It depends on systemd-resolved\ncommand -v systemctl >/dev/null 2>&1 || exit 0\ncommand -v resolvectl >/dev/null 2>&1 || exit 0\n\n# Configure systemd-resolved to enable mDNS resolution globally\nenable_mdns_conf_path=/etc/systemd/resolved.conf.d/00-lima-enable-mdns.conf\nenable_mdns_conf_content=\"[Resolve]\nMulticastDNS=yes\n\"\n# Create /etc/systemd/resolved.conf.d/00-lima-enable-mdns.conf if its content is different\nif [ \"$(echo \"${enable_mdns_conf_content}\" | sha256sum)\" != \"$(sha256sum <\"${enable_mdns_conf_path}\" 2>/dev/null)\" ]; then\n\tmkdir -p \"$(dirname \"${enable_mdns_conf_path}\")\"\n\techo \"${enable_mdns_conf_content}\" >\"${enable_mdns_conf_path}\"\n\tsystemctl daemon-reload\n\tsystemctl restart systemd-resolved.service\nfi\n\n# On Ubuntu, systemd.network's configuration won't work.\n# See: https://unix.stackexchange.com/a/652582\n# So we need to enable mDNS per-link using resolvectl.\nfor iface in $(resolvectl status | sed -n -E 's/^Link +[0-9]+ \\(([^)]+)\\)/\\1/p'); do\n\t# This setting is volatile and will be lost on reboot, so we need to set it every time\n\tresolvectl mdns \"${iface}\" yes\ndone\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/06-etc-hosts.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux -o pipefail\n\n# Define host.lima.internal in case the hostResolver is disabled. When using\n# the hostResolver, the name is provided by the lima resolver itself because\n# it doesn't have access to /etc/hosts inside the VM.\nsed -i '/host.lima.internal/d' /etc/hosts\necho -e \"${LIMA_CIDATA_SLIRP_GATEWAY}\\thost.lima.internal\" >>/etc/hosts\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/07-etc-environment.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\n# /etc/environment must be written after 04-persistent-data-volume.sh has run to\n# make sure the changes on a restart are applied to the persisted version.\n\norig=$(test ! -f /etc/environment || cat /etc/environment)\nif [ -e /etc/environment ]; then\n\tsed -i '/#LIMA-START/,/#LIMA-END/d' /etc/environment\nfi\ncat \"${LIMA_CIDATA_MNT}/etc_environment\" >>/etc/environment\n\n# It is possible that a requirements script has started an ssh session before\n# /etc/environment was updated, so we need to kill it to make sure it will\n# restart with the updated environment before \"linger\" is being enabled.\n\nif command -v loginctl >/dev/null 2>&1 && [ \"${orig}\" != \"$(cat /etc/environment)\" ]; then\n\tloginctl terminate-user \"${LIMA_CIDATA_USER}\" || true\nfi\n\n# Signal that provisioning is done. The instance ID changes on every boot,\n# so any value from a previous boot cycle will be different.\necho \"${LIMA_CIDATA_IID}\" >/run/lima-ssh-ready\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/08-shell-prompt.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\n# This script is only intended for the default.yaml image, which is based on Ubuntu LTS\n\nif [ \"${LIMA_CIDATA_NAME}\" = \"default\" ] && command -v patch >/dev/null 2>&1 && grep -q color_prompt \"${LIMA_CIDATA_HOME}/.bashrc\"; then\n\n\t! grep -q \"^# Lima PS1\" \"${LIMA_CIDATA_HOME}/.bashrc\" || exit 0\n\n\t# Change the default shell prompt from \"green\" to \"lime green\" (#32CD32)\n\n\tpatch --forward -r - \"${LIMA_CIDATA_HOME}/.bashrc\" <<'EOF'\n@@ -37,7 +37,11 @@\n\n # set a fancy prompt (non-color, unless we know we \"want\" color)\n case \"$TERM\" in\n-    xterm-color|*-256color) color_prompt=yes;;\n+    xterm-color) color_prompt=yes;;\n+    *-256color)  color_prompt=256;;\n+esac\n+case \"$COLORTERM\" in\n+    truecolor) color_prompt=true;;\n esac\n\n # uncomment for a colored prompt, if the terminal has the capability; turned\n@@ -56,7 +60,12 @@\n     fi\n fi\n\n-if [ \"$color_prompt\" = yes ]; then\n+# Lima PS1: set color to lime green\n+if [ \"$color_prompt\" = true ]; then\n+    PS1='${debian_chroot:+($debian_chroot)}\\[\\033[38;2;50;205;50m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ '\n+elif [ \"$color_prompt\" = 256 ]; then\n+    PS1='${debian_chroot:+($debian_chroot)}\\[\\033[38;5;77m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ '\n+elif [ \"$color_prompt\" = yes ]; then\n     PS1='${debian_chroot:+($debian_chroot)}\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ '\n else\n     PS1='${debian_chroot:+($debian_chroot)}\\u@\\h:\\w\\$ '\nEOF\n\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/09-host-dns-setup.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\nreadonly chain=LIMADNS\n\nchain_exists() {\n\tiptables --table nat -n --list \"${chain}\" >/dev/null 2>&1\n}\n\n# Wait until iptables has been installed; 35-configure-packages.sh will call this script again\nif command -v iptables >/dev/null 2>&1; then\n\tif ! chain_exists; then\n\t\tiptables --table nat --new-chain ${chain}\n\t\tiptables --table nat --insert PREROUTING 1 --jump \"${chain}\"\n\t\tiptables --table nat --insert OUTPUT 1 --jump \"${chain}\"\n\tfi\n\n\t# Remove old rules\n\tiptables --table nat --flush ${chain}\n\t# Add rules for the existing ip:port\n\tif [ -n \"${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}\" ] && [ \"${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}\" -ne 0 ]; then\n\t\tiptables --table nat --append \"${chain}\" --destination \"${LIMA_CIDATA_SLIRP_DNS}\" --protocol udp --dport 53 --jump DNAT \\\n\t\t\t--to-destination \"${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}\"\n\tfi\n\tif [ -n \"${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}\" ] && [ \"${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}\" -ne 0 ]; then\n\t\tiptables --table nat --append \"${chain}\" --destination \"${LIMA_CIDATA_SLIRP_DNS}\" --protocol tcp --dport 53 --jump DNAT \\\n\t\t\t--to-destination \"${LIMA_CIDATA_SLIRP_GATEWAY}:${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}\"\n\tfi\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/10-alpine-prep.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\n# This script prepares Alpine for lima; there is nothing in here for other distros\ntest -f /etc/alpine-release || exit 0\n\n# Configure apk repos\nBRANCH=edge\nVERSION_ID=$(awk -F= '$1==\"VERSION_ID\" {print $2}' /etc/os-release)\ncase ${VERSION_ID} in\n*_alpha* | *_beta*) BRANCH=edge ;;\n*.*.*) BRANCH=v${VERSION_ID%.*} ;;\nesac\n\nfor REPO in main community; do\n\tURL=\"https://dl-cdn.alpinelinux.org/alpine/${BRANCH}/${REPO}\"\n\tif ! grep -q \"^${URL}$\" /etc/apk/repositories; then\n\t\techo \"${URL}\" >>/etc/apk/repositories\n\tfi\ndone\n\n# Alpine comes with doas instead of sudo\nif ! command -v sudo >/dev/null 2>&1; then\n\tapk add sudo\nfi\n\n# Alpine doesn't use PAM so we need to explicitly allow public key auth\nusermod -p '*' \"${LIMA_CIDATA_USER}\"\n\n# Alpine disables TCP forwarding, which is needed by the lima-guestagent\nsed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config\n# Enable PAM so as to load /etc/environment via pam_env\nsed -i 's/#UsePAM no/UsePAM yes/g' /etc/ssh/sshd_config\nrc-service --ifstarted sshd reload\n\n# mount /sys/fs/cgroup\nrc-service cgroups start\n\n# `limactl stop` tells acpid to powerdown\nrc-update add acpid\nrc-service acpid start\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/11-colorterm-environment.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\nif [ -d /etc/ssh/sshd_config.d ]; then\n\tif [ -e /etc/ssh/sshd_config.d/10-acceptenv-colorterm.conf ]; then\n\t\texit 0\n\tfi\n\n\t# accept any incoming COLORTERM environment variable\n\techo \"AcceptEnv COLORTERM\" >/etc/ssh/sshd_config.d/10-acceptenv-colorterm.conf\nelif [ -e /etc/ssh/sshd_config ]; then\n\tif grep -q \"COLORTERM\" /etc/ssh/sshd_config; then\n\t\texit 0\n\tfi\n\n\t# accept any incoming COLORTERM environment variable\n\tsed -i 's/^AcceptEnv LANG LC_\\*$/AcceptEnv COLORTERM LANG LC_*/' /etc/ssh/sshd_config\nelse\n\texit 0\nfi\n\nif [ -f /sbin/openrc-run ]; then\n\tif [ -f etc/init.d/ssh ]; then\n\t\trc-service --ifstarted ssh reload\n\telif [ -f etc/init.d/sshd ]; then\n\t\trc-service --ifstarted sshd reload\n\tfi\nelif command -v systemctl >/dev/null 2>&1; then\n\tif systemctl -q is-active ssh; then\n\t\tsystemctl reload ssh\n\telif systemctl -q is-active sshd; then\n\t\tsystemctl reload sshd\n\tfi\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/20-rootless-base.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\n# This script does not work unless systemd is available\ncommand -v systemctl >/dev/null 2>&1 || exit 0\n\nif [ -O \"${LIMA_CIDATA_HOME}\" ]; then\n\t# Fix ownership of the user home directory when created by root.\n\t# In cases where mount points exist in the user's home directory, the home directory and\n\t# the mount points are created by root before the user is created. This leads to the home\n\t# directory being owned by root.\n\t# Following commands fix the ownership of the home directory and its contents (on the same filesystem)\n\t# is updated to the correct user.\n\t# shellcheck disable=SC2046 # it fails if find results are quoted.\n\tchown \"${LIMA_CIDATA_USER}\" $(find \"${LIMA_CIDATA_HOME}\" -xdev) ||\n\t\ttrue # Ignore errors because changing owner of the mount points may fail but it is not critical.\nfi\n\n# Set up env\nfor f in .profile .bashrc .zshrc; do\n\tif ! grep -q \"# Lima BEGIN\" \"${LIMA_CIDATA_HOME}/$f\"; then\n\t\tcat >>\"${LIMA_CIDATA_HOME}/$f\" <<EOF\n# Lima BEGIN\n# Make sure iptables and mount.fuse3 are available\nPATH=\"\\$PATH:/usr/sbin:/sbin\"\nexport PATH\nEOF\n\t\tif compare_version.sh \"$(uname -r)\" -lt \"5.13\"; then\n\t\t\tcat >>\"${LIMA_CIDATA_HOME}/$f\" <<EOF\n# fuse-overlayfs is the most stable snapshotter for rootless, on kernel < 5.13\n# https://github.com/lima-vm/lima/issues/383\n# https://rootlesscontaine.rs/how-it-works/overlayfs/\nCONTAINERD_SNAPSHOTTER=\"fuse-overlayfs\"\nexport CONTAINERD_SNAPSHOTTER\nEOF\n\t\tfi\n\t\tcat >>\"${LIMA_CIDATA_HOME}/$f\" <<EOF\n# Lima END\nEOF\n\t\tchown \"${LIMA_CIDATA_USER}\" \"${LIMA_CIDATA_HOME}/$f\"\n\tfi\ndone\n# Enable cgroup delegation (only meaningful on cgroup v2)\nif [ ! -e \"/etc/systemd/system/user@.service.d/lima.conf\" ]; then\n\tmkdir -p \"/etc/systemd/system/user@.service.d\"\n\tcat >\"/etc/systemd/system/user@.service.d/lima.conf\" <<EOF\n[Service]\nDelegate=yes\nEOF\nfi\nsystemctl daemon-reload\n\n# Set up sysctl\nsysctl_conf=\"/etc/sysctl.d/99-lima.conf\"\nif [ ! -e \"${sysctl_conf}\" ]; then\n\tif [ -e \"/proc/sys/kernel/unprivileged_userns_clone\" ]; then\n\t\techo \"kernel.unprivileged_userns_clone=1\" >>\"${sysctl_conf}\"\n\tfi\n\techo \"net.ipv4.ping_group_range = 0 2147483647\" >>\"${sysctl_conf}\"\n\techo \"net.ipv4.ip_unprivileged_port_start=0\" >>\"${sysctl_conf}\"\n\tsysctl --system\nfi\n\n# Set up subuid\nfor f in /etc/subuid /etc/subgid; do\n\t# systemd-homed expects the subuid range to be within 524288-1878982656 (0x80000-0x6fff0000).\n\t# See userdbctl.\n\t# 1073741824 (1G) is just an arbitrary number.\n\t# 1073741825-1878982656 is left blank for additional accounts.\n\tsubuid_begin=524288\n\t# https://github.com/moby/moby/issues/49810#issuecomment-2808108191\n\t[ \"${LIMA_CIDATA_UID}\" -ge \"${subuid_begin}\" ] && subuid_begin=\"$((LIMA_CIDATA_UID + 1))\"\n\tgrep -qw \"${LIMA_CIDATA_USER}\" $f || echo \"${LIMA_CIDATA_USER}:${subuid_begin}:1073741824\" >>$f\ndone\n\n# Start systemd session\nsystemctl start systemd-logind.service\nloginctl enable-linger \"${LIMA_CIDATA_USER}\"\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/25-guestagent-base.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\nif [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"reverse-sshfs\" ]; then\n\t# Create mount points\n\t# NOTE: Busybox sh does not support `for ((i=0;i<$N;i++))` form\n\tfor f in $(seq 0 $((LIMA_CIDATA_MOUNTS - 1))); do\n\t\tmountpointvar=\"LIMA_CIDATA_MOUNTS_${f}_MOUNTPOINT\"\n\t\tmountpoint=\"$(eval echo \\$\"$mountpointvar\")\"\n\t\tmkdir -p \"${mountpoint}\"\n\t\tgid=$(id -g \"${LIMA_CIDATA_USER}\")\n\t\tchown \"${LIMA_CIDATA_UID}:${gid}\" \"${mountpoint}\"\n\tdone\nfi\n\n# Install or update the guestagent binary\nmkdir -p \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin\nguestagent_updated=false\nif [ \"$(sha256sum <\"${LIMA_CIDATA_MNT}\"/lima-guestagent)\" = \"$(sha256sum <\"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/lima-guestagent 2>/dev/null)\" ]; then\n\techo \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent is up-to-date\"\nelse\n\tinstall -m 755 \"${LIMA_CIDATA_MNT}\"/lima-guestagent \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/lima-guestagent\n\tguestagent_updated=true\nfi\n\n# Launch the guestagent service\nif [ -f /sbin/openrc-run ]; then\n\tprint_config() {\n\t\t# Convert .env to conf.d by wrapping values in double quotes.\n\t\t# Split the variable and value at the first \"=\" to handle cases where the value contains additional \"=\" characters.\n\t\tsed -E 's/^([^=]+)=(.*)/\\1=\"\\2\"/' \"${LIMA_CIDATA_MNT}/lima.env\"\n\t}\n\tprint_script() {\n\t\t# the openrc lima-guestagent service script\n\t\tcat <<-'EOF'\n\t\t\t#!/sbin/openrc-run\n\t\t\tsupervisor=supervise-daemon\n\n\t\t\tlog_file=\"${log_file:-/var/log/${RC_SVCNAME}.log}\"\n\t\t\terr_file=\"${err_file:-${log_file}}\"\n\t\t\tlog_mode=\"${log_mode:-0644}\"\n\t\t\tlog_owner=\"${log_owner:-root:root}\"\n\n\t\t\tsupervise_daemon_args=\"${supervise_daemon_opts:---stderr \\\"${err_file}\\\" --stdout \\\"${log_file}\\\"}\"\n\n\t\t\tname=\"lima-guestagent\"\n\t\t\tdescription=\"Forward ports to the lima-hostagent\"\n\n\t\t\tcommand=${LIMA_CIDATA_GUEST_INSTALL_PREFIX}/bin/lima-guestagent\n\t\t\tcommand_args=\"daemon --debug=${LIMA_CIDATA_DEBUG} --vsock-port \\\"${LIMA_CIDATA_VSOCK_PORT}\\\" --virtio-port \\\"${LIMA_CIDATA_VIRTIO_PORT}\\\"\"\n\t\t\tcommand_background=true\n\t\t\tpidfile=\"/run/lima-guestagent.pid\"\n\t\tEOF\n\t}\n\tif [ \"${guestagent_updated}\" = \"false\" ] &&\n\t\t[ \"$(print_config | sha256sum)\" = \"$(sha256sum </etc/conf.d/lima-guestagent 2>/dev/null)\" ] &&\n\t\t[ \"$(print_script | sha256sum)\" = \"$(sha256sum </etc/init.d/lima-guestagent 2>/dev/null)\" ]; then\n\t\techo \"lima-guestagent service already up-to-date\"\n\t\texit 0\n\tfi\n\n\tprint_config >/etc/conf.d/lima-guestagent\n\tprint_script >/etc/init.d/lima-guestagent\n\tchmod 755 /etc/init.d/lima-guestagent\n\n\trc-update add lima-guestagent default\n\trc-service --ifstarted lima-guestagent restart # restart if running, otherwise do nothing\n\trc-service --ifstopped lima-guestagent start   # start if not running, otherwise do nothing\nelse\n\t# Remove legacy systemd service\n\trm -f \"${LIMA_CIDATA_HOME}/.config/systemd/user/lima-guestagent.service\"\n\n\tif [ \"${LIMA_CIDATA_VSOCK_PORT}\" != \"0\" ]; then\n\t\tsudo \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/lima-guestagent install-systemd --debug=\"${LIMA_CIDATA_DEBUG}\" --guestagent-updated=\"${guestagent_updated}\" --vsock-port \"${LIMA_CIDATA_VSOCK_PORT}\"\n\telif [ \"${LIMA_CIDATA_VIRTIO_PORT}\" != \"\" ]; then\n\t\tsudo \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/lima-guestagent install-systemd --debug=\"${LIMA_CIDATA_DEBUG}\" --guestagent-updated=\"${guestagent_updated}\" --virtio-port \"${LIMA_CIDATA_VIRTIO_PORT}\"\n\telse\n\t\tsudo \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/lima-guestagent install-systemd --debug=\"${LIMA_CIDATA_DEBUG}\" --guestagent-updated=\"${guestagent_updated}\"\n\tfi\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/30-install-packages.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\nINSTALL_IPTABLES=0\nif [ \"${LIMA_CIDATA_CONTAINERD_SYSTEM}\" = 1 ] || [ \"${LIMA_CIDATA_CONTAINERD_USER}\" = 1 ]; then\n\tINSTALL_IPTABLES=1\nfi\nif [ \"${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}\" -ne 0 ] || [ \"${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}\" -ne 0 ]; then\n\tINSTALL_IPTABLES=1\nfi\n\n# Install minimum dependencies\n# Run any user provided dependency scripts first\nif [ -d \"${LIMA_CIDATA_MNT}\"/provision.dependency ]; then\n\techo \"Detected dependency provisioning scripts, running before default dependency installation\"\n\tCODE=0\n\tfor f in \"${LIMA_CIDATA_MNT}\"/provision.dependency/*; do\n\t\tif ! \"$f\"; then\n\t\t\tCODE=1\n\t\tfi\n\tdone\n\tif [ $CODE != 0 ]; then\n\t\texit \"$CODE\"\n\tfi\nfi\n\n# apt-get detected through the first bytes of apt-get binary to ensure we're\n# matching to an actual binary and not a wrapper script. This case is an issue\n# on OpenSuse which wraps its own package manager in to a script named apt-get\n# to mimic certain options but doesn't offer full parameters compatibility\n# See : https://github.com/lima-vm/lima/pull/1014\nif [ \"${LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION}\" = 1 ]; then\n\techo \"LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION is set, skipping regular dependency installation\"\n\texit 0\nfi\n\nREMOUNT_VIRTIOFS=0\n\nif head -c 4 \"$(command -v apt-get)\" | grep -qP '\\x7fELF' >/dev/null 2>&1; then\n\tpkgs=\"\"\n\tif [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"reverse-sshfs\" ]; then\n\t\tif [ \"${LIMA_CIDATA_MOUNTS}\" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} sshfs\"\n\t\tfi\n\tfi\n\tif [ \"${INSTALL_IPTABLES}\" = 1 ] && [ ! -e /usr/sbin/iptables ]; then\n\t\tpkgs=\"${pkgs} iptables\"\n\tfi\n\tif [ \"${LIMA_CIDATA_CONTAINERD_USER}\" = 1 ] && ! command -v newuidmap >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} uidmap fuse3 dbus-user-session\"\n\tfi\n\tif ! command -v rsync >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} rsync\"\n\tfi\n\tif [ -n \"${pkgs}\" ]; then\n\t\tDEBIAN_FRONTEND=noninteractive\n\t\texport DEBIAN_FRONTEND\n\t\tapt-get update\n\t\t# shellcheck disable=SC2086\n\t\tapt-get install -y --no-upgrade --no-install-recommends -q ${pkgs}\n\tfi\nelif command -v dnf >/dev/null 2>&1; then\n\tpkgs=\"\"\n\textrapkgs=\"\"\n\tif ! command -v tar >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} tar\"\n\tfi\n\tif [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"reverse-sshfs\" ]; then\n\t\tif [ \"${LIMA_CIDATA_MOUNTS}\" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then\n\t\t\t# fuse-sshfs is not included in EL\n\t\t\textrapkgs=\"${extrapkgs} fuse-sshfs\"\n\t\tfi\n\tfi\n\tif [ \"${INSTALL_IPTABLES}\" = 1 ] && [ ! -e /usr/sbin/iptables ]; then\n\t\tpkgs=\"${pkgs} iptables\"\n\tfi\n\tif [ \"${LIMA_CIDATA_CONTAINERD_USER}\" = 1 ]; then\n\t\tif ! command -v newuidmap >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} shadow-utils\"\n\t\tfi\n\t\tif ! command -v mount.fuse3 >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} fuse3\"\n\t\tfi\n\tfi\n\tif ! command -v rsync >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} rsync\"\n\tfi\n\tif [ -n \"${pkgs}\" ] || [ -n \"${extrapkgs}\" ]; then\n\t\tdnf_install_flags=\"-y --setopt=install_weak_deps=False\"\n\t\tepel_install_flags=\"\"\n\t\tif grep -q \"Oracle Linux Server release 8\" /etc/system-release; then\n\t\t\t# repo flag instead of enable repo to reduce metadata syncing on slow Oracle repos\n\t\t\tdnf_install_flags=\"${dnf_install_flags} --repo ol8_baseos_latest --repo ol8_codeready_builder\"\n\t\telif grep -q \"release 8\" /etc/system-release; then\n\t\t\tdnf_install_flags=\"${dnf_install_flags} --enablerepo powertools\"\n\t\telif grep -q \"Oracle Linux Server release 9\" /etc/system-release; then\n\t\t\t# shellcheck disable=SC2086\n\t\t\tdnf install ${dnf_install_flags} oracle-epel-release-el9\n\t\t\tdnf config-manager --disable ol9_developer_EPEL >/dev/null 2>&1\n\t\t\tepel_install_flags=\"${epel_install_flags} --enablerepo ol9_developer_EPEL\"\n\t\telif grep -q \"Oracle Linux Server release 10\" /etc/system-release; then\n\t\t\toraclelinux_version=\"$(awk '{print $5}' /etc/system-release)\"\n\t\t\toraclelinux_version_major=$(echo \"$oraclelinux_version\" | cut -d. -f1)\n\t\t\toraclelinux_version_minor=$(echo \"$oraclelinux_version\" | cut -d. -f2)\n\t\t\toraclelinux_epel_repo=\"ol${oraclelinux_version_major}_u${oraclelinux_version_minor}_developer_EPEL\"\n\t\t\t# shellcheck disable=SC2086\n\t\t\tdnf install ${dnf_install_flags} oracle-epel-release-el${oraclelinux_version_major}\n\t\t\tdnf config-manager --disable \"$oraclelinux_epel_repo\" >/dev/null 2>&1 || true\n\t\t\tepel_install_flags=\"${epel_install_flags} --enablerepo $oraclelinux_epel_repo\"\n\t\telif grep -q -E \"release (9|10)\" /etc/system-release; then\n\t\t\t# shellcheck disable=SC2086\n\t\t\tdnf install ${dnf_install_flags} epel-release\n\t\t\t# Disable the OpenH264 repository as well, by default\n\t\t\tdnf config-manager --disable epel\\* >/dev/null 2>&1\n\t\t\tepel_install_flags=\"${epel_install_flags} --enablerepo epel\"\n\t\tfi\n\t\tif grep -q \"Oracle Linux Server\" /etc/system-release && [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"virtiofs\" ]; then\n\t\t\t# Enable repositories like \"ol9_UEKR7\"\n\t\t\tfor repo in $(dnf -q repolist | awk '/UEK/NR>1{print $1}'); do\n\t\t\t\tepel_install_flags=\"${epel_install_flags} --enablerepo ${repo}\"\n\t\t\tdone\n\t\t\textrapkgs=\"${extrapkgs} kernel-uek-modules-$(uname -r)\"\n\t\t\tREMOUNT_VIRTIOFS=1\n\t\tfi\n\t\tif [ -n \"${pkgs}\" ]; then\n\t\t\t# shellcheck disable=SC2086\n\t\t\tdnf install ${dnf_install_flags} ${pkgs}\n\t\tfi\n\t\tif [ -n \"${extrapkgs}\" ]; then\n\t\t\t# shellcheck disable=SC2086\n\t\t\tdnf install ${dnf_install_flags} ${epel_install_flags} ${extrapkgs}\n\t\tfi\n\tfi\n\tif [ \"${LIMA_CIDATA_CONTAINERD_USER}\" = 1 ] && [ ! -e /usr/bin/fusermount ]; then\n\t\t# Workaround for https://github.com/containerd/stargz-snapshotter/issues/340\n\t\tln -s fusermount3 /usr/bin/fusermount\n\tfi\nelif command -v yum >/dev/null 2>&1; then\n\techo \"DEPRECATED: CentOS7 and others RHEL-like version 7 are unsupported and might be removed or stop to work in future lima releases\"\n\tpkgs=\"\"\n\tyum_install_flags=\"-y\"\n\tif ! rpm -ql epel-release >/dev/null 2>&1; then\n\t\tyum install ${yum_install_flags} epel-release\n\tfi\n\tif ! command -v tar >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} tar\"\n\tfi\n\tif [ \"${LIMA_CIDATA_MOUNTS}\" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} fuse-sshfs\"\n\tfi\n\tif [ \"${INSTALL_IPTABLES}\" = 1 ] && [ ! -e /usr/sbin/iptables ]; then\n\t\tpkgs=\"${pkgs} iptables\"\n\tfi\n\tif [ \"${LIMA_CIDATA_CONTAINERD_USER}\" = 1 ]; then\n\t\tif ! command -v newuidmap >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} shadow-utils\"\n\t\tfi\n\t\tif ! command -v mount.fuse3 >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} fuse3\"\n\t\tfi\n\tfi\n\tif ! command -v rsync >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} rsync\"\n\tfi\n\tif [ -n \"${pkgs}\" ]; then\n\t\t# shellcheck disable=SC2086\n\t\tyum install ${yum_install_flags} ${pkgs}\n\t\tyum-config-manager --disable epel >/dev/null 2>&1\n\tfi\nelif command -v pacman >/dev/null 2>&1; then\n\tpkgs=\"\"\n\tif [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"reverse-sshfs\" ]; then\n\t\tif [ \"${LIMA_CIDATA_MOUNTS}\" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} sshfs\"\n\t\tfi\n\tfi\n\tif ! command -v rsync >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} rsync\"\n\tfi\n\t# other dependencies are preinstalled on Arch Linux\n\tif [ -n \"${pkgs}\" ]; then\n\t\t# shellcheck disable=SC2086\n\t\tpacman -Sy --noconfirm ${pkgs}\n\tfi\nelif command -v zypper >/dev/null 2>&1; then\n\tpkgs=\"\"\n\tif [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"reverse-sshfs\" ]; then\n\t\tif [ \"${LIMA_CIDATA_MOUNTS}\" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} sshfs\"\n\t\tfi\n\tfi\n\tif [ \"${INSTALL_IPTABLES}\" = 1 ] && [ ! -e /usr/sbin/iptables ]; then\n\t\tpkgs=\"${pkgs} iptables\"\n\tfi\n\tif [ \"${LIMA_CIDATA_CONTAINERD_USER}\" = 1 ] && ! command -v mount.fuse3 >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} fuse3\"\n\tfi\n\tif ! command -v rsync >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} rsync\"\n\tfi\n\tif [ -n \"${pkgs}\" ]; then\n\t\t# shellcheck disable=SC2086\n\t\tzypper --non-interactive install -y --no-recommends ${pkgs}\n\tfi\nelif command -v apk >/dev/null 2>&1; then\n\tpkgs=\"\"\n\tif [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"reverse-sshfs\" ]; then\n\t\tif [ \"${LIMA_CIDATA_MOUNTS}\" -gt 0 ] && ! command -v sshfs >/dev/null 2>&1; then\n\t\t\tpkgs=\"${pkgs} sshfs\"\n\t\tfi\n\tfi\n\tif [ \"${INSTALL_IPTABLES}\" = 1 ] && ! command -v iptables >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} iptables\"\n\tfi\n\tif ! command -v rsync >/dev/null 2>&1; then\n\t\tpkgs=\"${pkgs} rsync\"\n\tfi\n\tif [ -n \"${pkgs}\" ]; then\n\t\tapk update\n\t\t# shellcheck disable=SC2086\n\t\tapk add ${pkgs}\n\tfi\nfi\n\nif [ \"${REMOUNT_VIRTIOFS}\" = 1 ]; then\n\tmount -t virtiofs -a\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/35-setup-packages.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n\nupdate_fuse_conf() {\n\t# Modify /etc/fuse.conf (/etc/fuse3.conf) to allow \"-o allow_root\"\n\tif [ \"${LIMA_CIDATA_MOUNTS}\" -gt 0 ]; then\n\t\tfuse_conf=\"/etc/fuse.conf\"\n\t\tif [ -e /etc/fuse3.conf ]; then\n\t\t\tfuse_conf=\"/etc/fuse3.conf\"\n\t\tfi\n\t\tif ! grep -q \"^user_allow_other\" \"${fuse_conf}\"; then\n\t\t\techo \"user_allow_other\" >>\"${fuse_conf}\"\n\t\tfi\n\tfi\n}\n\n# update_fuse_conf has to be called after installing all the packages,\n# otherwise apt-get fails with conflict\nif [ \"${LIMA_CIDATA_MOUNTTYPE}\" = \"reverse-sshfs\" ]; then\n\tupdate_fuse_conf\nfi\n\nSETUP_DNS=0\nif [ -n \"${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}\" ] && [ \"${LIMA_CIDATA_UDP_DNS_LOCAL_PORT}\" -ne 0 ]; then\n\tSETUP_DNS=1\nfi\nif [ -n \"${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}\" ] && [ \"${LIMA_CIDATA_TCP_DNS_LOCAL_PORT}\" -ne 0 ]; then\n\tSETUP_DNS=1\nfi\nif [ \"${SETUP_DNS}\" = 1 ]; then\n\t# Try to setup iptables rule again, in case we just installed iptables\n\t\"${LIMA_CIDATA_MNT}/boot.Linux/09-host-dns-setup.sh\"\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.Linux/40-install-containerd.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux\n: \"${CONTAINERD_NAMESPACE:=default}\"\n# Overridable in .bashrc\n: \"${CONTAINERD_SNAPSHOTTER:=overlayfs}\"\n\nif [ \"${LIMA_CIDATA_CONTAINERD_SYSTEM}\" != 1 ] && [ \"${LIMA_CIDATA_CONTAINERD_USER}\" != 1 ]; then\n\texit 0\nfi\n\n# This script does not work unless systemd is available\ncommand -v systemctl >/dev/null 2>&1 || exit 0\n\n# Extract bin/nerdctl and compare whether it is newer than the current /usr/local/bin/nerdctl (if already exists).\n# Takes 4-5 seconds. (FIXME: optimize)\ntmp_extract_nerdctl=\"$(mktemp -d)\"\ntar Cxaf \"${tmp_extract_nerdctl}\" \"${LIMA_CIDATA_MNT}\"/\"${LIMA_CIDATA_CONTAINERD_ARCHIVE}\" bin/nerdctl\n\nif [ ! -f \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/nerdctl ] || [[ \"${tmp_extract_nerdctl}\"/bin/nerdctl -nt \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/nerdctl ]]; then\n\tif [ -f \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/nerdctl ]; then\n\t\t(\n\t\t\tset +e\n\t\t\techo \"Upgrading existing nerdctl\"\n\t\t\techo \"- Old: $(\"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\"/bin/nerdctl --version)\"\n\t\t\techo \"- New: $(\"${tmp_extract_nerdctl}\"/bin/nerdctl --version)\"\n\t\t\tsystemctl disable --now containerd default-buildkit stargz-snapshotter\n\t\t\tsudo -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\n\t\t\tsudo -iu \"${LIMA_CIDATA_USER}\" \"XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}\" \"PATH=${PATH}\" containerd-rootless-setuptool.sh uninstall\n\t\t)\n\tfi\n\ttar Cxaf \"${LIMA_CIDATA_GUEST_INSTALL_PREFIX}\" \"${LIMA_CIDATA_MNT}\"/\"${LIMA_CIDATA_CONTAINERD_ARCHIVE}\"\n\n\tmkdir -p /etc/bash_completion.d\n\tnerdctl completion bash >/etc/bash_completion.d/nerdctl\n\t# TODO: enable zsh completion too\nfi\n\nrm -rf \"${tmp_extract_nerdctl}\"\n\nif [ \"${LIMA_CIDATA_CONTAINERD_SYSTEM}\" = 1 ]; then\n\tif [ ! -e /etc/containerd/config.toml ]; then\n\t\tmkdir -p /etc/containerd\n\t\tcat >\"/etc/containerd/config.toml\" <<EOF\n  version = 2\n  # TODO: remove imports after upgrading containerd to v2.2, as\n  # conf.d is set by default since v2.2.\n  imports = ['/etc/containerd/conf.d/*.toml']\n  [plugins.\"io.containerd.grpc.v1.cri\"]\n    enable_cdi = true\n  [proxy_plugins]\n    [proxy_plugins.\"stargz\"]\n      type = \"snapshot\"\n      address = \"/run/containerd-stargz-grpc/containerd-stargz-grpc.sock\"\nEOF\n\tfi\n\tif [ ! -e /etc/buildkit/buildkitd.toml ]; then\n\t\tmkdir -p /etc/buildkit\n\t\tcat >\"/etc/buildkit/buildkitd.toml\" <<EOF\n[worker.oci]\n  enabled = false\n\n[worker.containerd]\n  enabled = true\n  namespace = \"${CONTAINERD_NAMESPACE}\"\n  snapshotter = \"${CONTAINERD_SNAPSHOTTER}\"\nEOF\n\tfi\n\tsystemctl enable --now containerd buildkit stargz-snapshotter\nfi\n\nif [ \"${LIMA_CIDATA_CONTAINERD_USER}\" = 1 ]; then\n\tif [ ! -e \"${LIMA_CIDATA_HOME}/.config/containerd/config.toml\" ]; then\n\t\tmkdir -p \"${LIMA_CIDATA_HOME}/.config/containerd\"\n\t\tcat >\"${LIMA_CIDATA_HOME}/.config/containerd/config.toml\" <<EOF\n  version = 2\n  [plugins.\"io.containerd.grpc.v1.cri\"]\n    enable_cdi = true\n  [proxy_plugins]\n    [proxy_plugins.\"fuse-overlayfs\"]\n      type = \"snapshot\"\n      address = \"/run/user/${LIMA_CIDATA_UID}/containerd-fuse-overlayfs.sock\"\n    [proxy_plugins.\"stargz\"]\n      type = \"snapshot\"\n      address = \"/run/user/${LIMA_CIDATA_UID}/containerd-stargz-grpc/containerd-stargz-grpc.sock\"\nEOF\n\t\tchown -R \"${LIMA_CIDATA_USER}\" \"${LIMA_CIDATA_HOME}/.config\"\n\tfi\n\tselinux=\n\tif command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then\n\t\tselinux=1\n\tfi\n\tif [ ! -e \"/etc/apparmor.d/usr.local.bin.rootlesskit\" ] && [ -e \"/etc/apparmor.d/abi/4.0\" ] && [ -e \"/proc/sys/kernel/apparmor_restrict_unprivileged_userns\" ]; then\n\t\tcat >\"/etc/apparmor.d/usr.local.bin.rootlesskit\" <<EOF\n# Ubuntu 23.10 introduced kernel.apparmor_restrict_unprivileged_userns\n# to restrict unsharing user namespaces:\n# https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces\n#\n# kernel.apparmor_restrict_unprivileged_userns is still opt-in in Ubuntu 23.10,\n# but it is expected to be enabled in future releases of Ubuntu.\nabi <abi/4.0>,\ninclude <tunables/global>\n\n/usr/local/bin/rootlesskit flags=(unconfined) {\n  userns,\n\n  # Site-specific additions and overrides. See local/README for details.\n  include if exists <local/usr.local.bin.rootlesskit>\n}\nEOF\n\t\tsystemctl restart apparmor.service\n\tfi\n\tif [ ! -e \"${LIMA_CIDATA_HOME}/.config/systemd/user/containerd.service\" ]; then\n\t\tuntil [ -e \"/run/user/${LIMA_CIDATA_UID}/systemd/private\" ]; do sleep 3; done\n\t\tif [ -n \"$selinux\" ]; then\n\t\t\techo \"Temporarily disabling SELinux, during installing containerd units\"\n\t\t\tsetenforce 0\n\t\tfi\n\t\tif [ \"$(sudo -iu \"${LIMA_CIDATA_USER}\" sh -ec 'systemctl --user show --property=RefuseManualStart --value dbus')\" != \"yes\" ]; then\n\t\t\tsudo -iu \"${LIMA_CIDATA_USER}\" \"XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}\" systemctl --user enable --now dbus\n\t\tfi\n\t\tsudo -iu \"${LIMA_CIDATA_USER}\" \"XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}\" \"PATH=${PATH}\" containerd-rootless-setuptool.sh install\n\t\tsudo -iu \"${LIMA_CIDATA_USER}\" \"XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}\" \"PATH=${PATH}\" \\\n\t\t\t\"CONTAINERD_NAMESPACE=${CONTAINERD_NAMESPACE}\" \"CONTAINERD_SNAPSHOTTER=${CONTAINERD_SNAPSHOTTER}\" \\\n\t\t\tcontainerd-rootless-setuptool.sh install-buildkit-containerd\n\n\t\t# $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.\n\t\tif [ \"$(sudo -iu \"${LIMA_CIDATA_USER}\" sh -ec 'echo $CONTAINERD_SNAPSHOTTER')\" = \"fuse-overlayfs\" ]; then\n\t\t\tsudo -iu \"${LIMA_CIDATA_USER}\" \"XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}\" \"PATH=${PATH}\" containerd-rootless-setuptool.sh install-fuse-overlayfs\n\t\tfi\n\n\t\tif compare_version.sh \"$(uname -r)\" -ge \"5.13\"; then\n\t\t\tsudo -iu \"${LIMA_CIDATA_USER}\" \"XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}\" \"PATH=${PATH}\" containerd-rootless-setuptool.sh install-stargz\n\t\telse\n\t\t\techo >&2 \"WARNING: the guest kernel seems older than 5.13. Skipping installing rootless stargz.\"\n\t\tfi\n\t\tif [ -n \"$selinux\" ]; then\n\t\t\techo \"Restoring SELinux\"\n\t\t\tsetenforce 1\n\t\tfi\n\tfi\nfi\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.essential.FreeBSD/00-freebsd-user-group.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This script ensures that the user is created with the expected UID.\n# This script is needed because nuageinit does not create the user with the expected UID.\n\nset -eu\n\n[ \"$(stat -f %u \"${LIMA_CIDATA_HOME}\")\" = \"${LIMA_CIDATA_UID}\" ] && exit 0\n\npw usermod -n \"${LIMA_CIDATA_USER}\" -u \"${LIMA_CIDATA_UID}\"\ngid=\"$(id -g \"${LIMA_CIDATA_USER}\")\"\nchown -R \"${LIMA_CIDATA_UID}:${gid}\" \"${LIMA_CIDATA_HOME}\"\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/boot.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu\n\nINFO() {\n\techo \"LIMA $(date -Iseconds)| $*\"\n}\n\nWARNING() {\n\techo \"LIMA $(date -Iseconds)| WARNING: $*\"\n}\n\nUNAME=\"$(uname -s)\"\n\nRUN=\"/run\"\nif [ \"${UNAME}\" != \"Linux\" ]; then\n\tRUN=\"/var/run\"\nfi\nrm -f \"${RUN}/lima-boot-done\"\n\n# shellcheck disable=SC2163\nwhile read -r line; do [ -n \"$line\" ] && export \"$line\"; done <\"${LIMA_CIDATA_MNT}\"/lima.env\n# shellcheck disable=SC2163\nwhile read -r line; do [ -n \"$line\" ] && export \"$line\"; done <\"${LIMA_CIDATA_MNT}\"/param.env\n\n# shellcheck disable=SC2163\nwhile read -r line; do\n\t# pam_env implementation:\n\t# - '#' is treated the same as newline; terminates value\n\t# - skip leading tabs and spaces\n\t# - skip leading \"export \" prefix (only single space)\n\t# - skip leading quote ('\\'' or '\"') on the value side\n\t# - skip trailing quote only if leading quote has been skipped;\n\t#   quotes don't need to match; trailing quote may be omitted\n\tline=\"$(echo \"$line\" | sed -E \"s/^[ \\\\t]*(export )?//; s/#.*//; s/(^[^=]+=)[\\\"'](.*[^\\\"'])?[\\\"']?$/\\1\\2/\")\"\n\t[ -n \"$line\" ] && export \"$line\"\ndone <\"${LIMA_CIDATA_MNT}\"/etc_environment\n\nPATH=\"${LIMA_CIDATA_MNT}\"/util:\"${PATH}\"\nexport PATH\n\nCODE=0\n\n# Don't make any changes to /etc or /var/lib until boot.Linux/04-persistent-data-volume.sh\n# has run because it might move the directories to /mnt/data on first boot. In that\n# case changes made on restart would be lost.\n\nrun_boot_scripts() {\n\tboot=\"$1\"\n\tif [ -e \"${boot}\" ]; then\n\t\tfor f in \"${boot}\"/*.sh; do\n\t\t\tINFO \"Executing $f\"\n\t\t\tif ! \"$f\"; then\n\t\t\t\tWARNING \"Failed to execute $f\"\n\t\t\t\tCODE=1\n\t\t\tfi\n\t\tdone\n\tfi\n}\n\n# The boot.essential.${UNAME} scripts are executed in plain mode too.\nrun_boot_scripts \"${LIMA_CIDATA_MNT}/boot.essential.${UNAME}\"\n\nif [ \"$LIMA_CIDATA_PLAIN\" = \"1\" ]; then\n\tINFO \"Plain mode. Skipping to run non-essential boot scripts. Provisioning scripts will be still executed. Guest agent will not be running.\"\nelse\n\trun_boot_scripts \"${LIMA_CIDATA_MNT}/boot.${UNAME}\"\nfi\n\n# indirect variable lookup, like ${!var} in bash\nderef() {\n\teval echo \\$\"$1\"\n}\n\nif [ -d \"${LIMA_CIDATA_MNT}\"/provision.data ]; then\n\tfor f in \"${LIMA_CIDATA_MNT}\"/provision.data/*; do\n\t\tfilename=$(basename \"$f\")\n\t\toverwrite=$(deref \"LIMA_CIDATA_DATAFILE_${filename}_OVERWRITE\")\n\t\towner=$(deref \"LIMA_CIDATA_DATAFILE_${filename}_OWNER\")\n\t\tpath=$(deref \"LIMA_CIDATA_DATAFILE_${filename}_PATH\")\n\t\tpermissions=$(deref \"LIMA_CIDATA_DATAFILE_${filename}_PERMISSIONS\")\n\t\tuser=\"${owner%%:*}\"\n\t\tif [ -e \"$path\" ] && [ \"$overwrite\" = \"false\" ]; then\n\t\t\tINFO \"Not overwriting $path\"\n\t\telse\n\t\t\tINFO \"Copying $f to $path\"\n\t\t\tif ! sudo -iu \"${user}\" mkdir -p \"$(dirname \"$path\")\"; then\n\t\t\t\tWARNING \"Failed to create directory for ${path} (as user ${user})\"\n\t\t\t\tWARNING \"Falling back to creating directory as root to maintain compatibility\"\n\t\t\t\tmkdir -p \"$(dirname \"$path\")\"\n\t\t\tfi\n\t\t\tcp \"$f\" \"$path\"\n\t\t\tchown \"$owner\" \"$path\"\n\t\t\tchmod \"$permissions\" \"$path\"\n\t\tfi\n\tdone\nfi\n\nif [ -d \"${LIMA_CIDATA_MNT}\"/provision.yq ]; then\n\tyq=\"${LIMA_CIDATA_MNT}/lima-guestagent yq\"\n\tfor f in \"${LIMA_CIDATA_MNT}\"/provision.yq/*; do\n\t\tfilename=$(basename \"${f}\")\n\t\tformat=$(deref \"LIMA_CIDATA_YQ_PROVISION_${filename}_FORMAT\")\n\t\towner=$(deref \"LIMA_CIDATA_YQ_PROVISION_${filename}_OWNER\")\n\t\tpath=$(deref \"LIMA_CIDATA_YQ_PROVISION_${filename}_PATH\")\n\t\tpermissions=$(deref \"LIMA_CIDATA_YQ_PROVISION_${filename}_PERMISSIONS\")\n\t\tuser=\"${owner%%:*}\"\n\t\t# Creating intermediate directories may fail if the user does not have permission.\n\t\t# TODO: Create intermediate directories with the specified group ownership.\n\t\tif ! sudo -iu \"${user}\" mkdir -p \"$(dirname \"${path}\")\"; then\n\t\t\tWARNING \"Failed to create directory for ${path} (as user ${user})\"\n\t\t\tCODE=1\n\t\t\tcontinue\n\t\tfi\n\t\t# Since CIDATA is mounted with dmode=700,fmode=700,\n\t\t# `lima-guestagent yq` cannot be executed by non-root users,\n\t\t# and provision.yq/* files cannot be read by non-root users.\n\t\tif [ -f \"${path}\" ]; then\n\t\t\tINFO \"Updating ${path}\"\n\t\t\t# If the user does not have write permission, it should fail.\n\t\t\t# This avoids changes being made by the wrong user.\n\t\t\tif ! sudo -iu \"${user}\" test -w \"${path}\"; then\n\t\t\t\tWARNING \"File ${path} is not writable by user ${user}\"\n\t\t\t\tCODE=1\n\t\t\t\tcontinue\n\t\t\tfi\n\t\t\t# Relies on the fact that yq does not change the owner of the existing file.\n\t\t\tif ! ${yq} --inplace --from-file \"${f}\" --input-format \"${format}\" --output-format \"${format}\" \"${path}\"; then\n\t\t\t\tWARNING \"Failed to update ${path} (as user ${user})\"\n\t\t\t\tCODE=1\n\t\t\t\tcontinue\n\t\t\tfi\n\t\telse\n\t\t\tif [ \"${format}\" = \"auto\" ]; then\n\t\t\t\t# yq can't determine the output format from non-existing files\n\t\t\t\tcase \"${path}\" in\n\t\t\t\t*.csv) format=csv ;;\n\t\t\t\t*.ini) format=ini ;;\n\t\t\t\t*.json) format=json ;;\n\t\t\t\t*.properties) format=properties ;;\n\t\t\t\t*.toml) format=toml ;;\n\t\t\t\t*.tsv) format=tsv ;;\n\t\t\t\t*.xml) format=xml ;;\n\t\t\t\t*.yaml | *.yml) format=yaml ;;\n\t\t\t\t*)\n\t\t\t\t\tformat=yaml\n\t\t\t\t\tWARNING \"Cannot determine file type for ${path}, using yaml format\"\n\t\t\t\t\t;;\n\t\t\t\tesac\n\t\t\tfi\n\t\t\tINFO \"Creating ${path}\"\n\t\t\tif ! ${yq} --null-input --from-file \"${f}\" --output-format \"${format}\" | sudo -iu \"${user}\" tee \"${path}\"; then\n\t\t\t\tWARNING \"Failed to create ${path} (as user ${user})\"\n\t\t\t\tCODE=1\n\t\t\t\tcontinue\n\t\t\tfi\n\t\tfi\n\t\tif ! sudo -iu \"${user}\" chown \"${owner}\" \"${path}\"; then\n\t\t\tWARNING \"Failed to set owner for ${path} (as user ${user})\"\n\t\t\tCODE=1\n\t\tfi\n\t\tif ! sudo -iu \"${user}\" chmod \"${permissions}\" \"${path}\"; then\n\t\t\tWARNING \"Failed to set permissions for ${path} (as user ${user})\"\n\t\t\tCODE=1\n\t\tfi\n\tdone\nfi\n\nif [ -d \"${LIMA_CIDATA_MNT}\"/provision.system ]; then\n\tfor f in \"${LIMA_CIDATA_MNT}\"/provision.system/*; do\n\t\tINFO \"Executing $f\"\n\t\tif ! \"$f\"; then\n\t\t\tWARNING \"Failed to execute $f\"\n\t\t\tCODE=1\n\t\tfi\n\tdone\nfi\n\nUSER_SCRIPT=\"${LIMA_CIDATA_HOME}/.lima-user-script\"\nif [ -d \"${LIMA_CIDATA_MNT}\"/provision.user ]; then\n\tif [ \"$UNAME\" = \"Linux\" ] && [ ! -f /sbin/openrc-run ]; then\n\t\tuntil [ -e \"/run/user/${LIMA_CIDATA_UID}/systemd/private\" ]; do sleep 3; done\n\tfi\n\tparams=$(grep -o '^PARAM_[^=]*' \"${LIMA_CIDATA_MNT}\"/param.env | paste -sd , -)\n\tfor f in \"${LIMA_CIDATA_MNT}\"/provision.user/*; do\n\t\tINFO \"Executing $f (as user ${LIMA_CIDATA_USER})\"\n\t\tcp \"$f\" \"${USER_SCRIPT}\"\n\t\tchown \"${LIMA_CIDATA_USER}\" \"${USER_SCRIPT}\"\n\t\tchmod 755 \"${USER_SCRIPT}\"\n\t\tXDG_RUNTIME_DIR_ENV=\n\t\tif [ \"${UNAME}\" = \"Linux\" ]; then\n\t\t\tXDG_RUNTIME_DIR_ENV=\"XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}\"\n\t\tfi\n\t\t# shellcheck disable=SC2086\n\t\tif ! sudo -iu \"${LIMA_CIDATA_USER}\" \"--preserve-env=${params}\" $XDG_RUNTIME_DIR_ENV \"${USER_SCRIPT}\"; then\n\t\t\tWARNING \"Failed to execute $f (as user ${LIMA_CIDATA_USER})\"\n\t\t\tCODE=1\n\t\tfi\n\t\trm \"${USER_SCRIPT}\"\n\tdone\nfi\n\n# Signal that provisioning is done. The instance ID changes on every boot,\n# so any value from a previous boot cycle will be different.\necho \"${LIMA_CIDATA_IID}\" >\"${RUN}/lima-boot-done\"\n\nINFO \"Exiting with code $CODE\"\nexit \"$CODE\"\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/etc_environment",
    "content": "#LIMA-START\n{{- range $key, $val := .Env}}\n{{$key}}={{$val}}\n{{- end}}\n#LIMA-END\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/lima.env",
    "content": "LIMA_CIDATA_DEBUG={{ .Debug }}\nLIMA_CIDATA_IID={{ .IID }}\nLIMA_CIDATA_NAME={{ .Name }}\nLIMA_CIDATA_USER={{ .User }}\nLIMA_CIDATA_UID={{ .UID }}\nLIMA_CIDATA_COMMENT={{ .Comment }}\nLIMA_CIDATA_HOME={{ .Home}}\nLIMA_CIDATA_SHELL={{ .Shell }}\nLIMA_CIDATA_HOSTHOME_MOUNTPOINT={{ .HostHomeMountPoint }}\nLIMA_CIDATA_MOUNTS={{ len .Mounts }}\n{{- range $i, $val := .Mounts}}\nLIMA_CIDATA_MOUNTS_{{$i}}_MOUNTPOINT={{$val.MountPoint}}\n{{- end}}\nLIMA_CIDATA_MOUNTTYPE={{ .MountType }}\nLIMA_CIDATA_DISKS={{ len .Disks }}\n{{- range $i, $disk := .Disks}}\nLIMA_CIDATA_DISK_{{$i}}_NAME={{$disk.Name}}\nLIMA_CIDATA_DISK_{{$i}}_DEVICE={{$disk.Device}}\nLIMA_CIDATA_DISK_{{$i}}_FORMAT={{$disk.Format}}\nLIMA_CIDATA_DISK_{{$i}}_FSTYPE={{$disk.FSType}}\nLIMA_CIDATA_DISK_{{$i}}_FSARGS={{range $j, $arg := $disk.FSArgs}}{{if $j}} {{end}}{{$arg}}{{end}}\n{{- end}}\n{{- range $dataFile := .DataFiles}}\nLIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_OVERWRITE={{$dataFile.Overwrite}}\nLIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_OWNER={{$dataFile.Owner}}\nLIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_PATH={{$dataFile.Path}}\nLIMA_CIDATA_DATAFILE_{{$dataFile.FileName}}_PERMISSIONS={{$dataFile.Permissions}}\n{{- end}}\n{{- range $yqProvision := .YQProvisions}}\nLIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_FORMAT={{$yqProvision.Format}}\nLIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_OWNER={{$yqProvision.Owner}}\nLIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_PATH={{$yqProvision.Path}}\nLIMA_CIDATA_YQ_PROVISION_{{$yqProvision.FileName}}_PERMISSIONS={{$yqProvision.Permissions}}\n{{- end}}\nLIMA_CIDATA_GUEST_INSTALL_PREFIX={{ .GuestInstallPrefix }}\n{{- if .UpgradePackages}}\nLIMA_CIDATA_UPGRADE_PACKAGES=1\n{{- else}}\nLIMA_CIDATA_UPGRADE_PACKAGES=\n{{- end}}\n{{- if .Containerd.User}}\nLIMA_CIDATA_CONTAINERD_USER=1\n{{- else}}\nLIMA_CIDATA_CONTAINERD_USER=\n{{- end}}\n{{- if .Containerd.System}}\nLIMA_CIDATA_CONTAINERD_SYSTEM=1\n{{- else}}\nLIMA_CIDATA_CONTAINERD_SYSTEM=\n{{- end}}\n{{- if or .Containerd.User .Containerd.System}}\nLIMA_CIDATA_CONTAINERD_ARCHIVE={{.Containerd.Archive}}\n{{- end}}\nLIMA_CIDATA_SLIRP_DNS={{.SlirpDNS}}\nLIMA_CIDATA_SLIRP_GATEWAY={{.SlirpGateway}}\nLIMA_CIDATA_SLIRP_IP_ADDRESS={{.SlirpIPAddress}}\nLIMA_CIDATA_UDP_DNS_LOCAL_PORT={{.UDPDNSLocalPort}}\nLIMA_CIDATA_TCP_DNS_LOCAL_PORT={{.TCPDNSLocalPort}}\nLIMA_CIDATA_ROSETTA_ENABLED={{.RosettaEnabled}}\nLIMA_CIDATA_ROSETTA_BINFMT={{.RosettaBinFmt}}\n{{- if .SkipDefaultDependencyResolution}}\nLIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION=1\n{{- else}}\nLIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION=\n{{- end}}\nLIMA_CIDATA_VMTYPE={{ .VMType }}\nLIMA_CIDATA_VSOCK_PORT={{ .VSockPort }}\nLIMA_CIDATA_VIRTIO_PORT={{ .VirtioPort}}\n{{- if .Plain}}\nLIMA_CIDATA_PLAIN=1\n{{- else}}\nLIMA_CIDATA_PLAIN=\n{{- end}}\n{{- if .NoCloudInit}}\nLIMA_CIDATA_NO_CLOUD_INIT=1\n{{- else}}\nLIMA_CIDATA_NO_CLOUD_INIT=\n{{- end}}\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/meta-data",
    "content": "instance-id: {{.IID}}\nlocal-hostname: {{.Hostname}}\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/network-config",
    "content": "version: 2\nethernets:\n  {{- range $nw := .Networks}}\n  {{$nw.Interface}}:\n    match:\n      macaddress: '{{$nw.MACAddress}}'\n    dhcp4: true\n    set-name: {{$nw.Interface}}\n    dhcp4-overrides:\n      route-metric: {{$nw.Metric}}\n    dhcp-identifier: mac\n    {{- if and (eq $nw.Interface $.SlirpNICName) (gt (len $.DNSAddresses) 0) }}\n    nameservers:\n      addresses:\n      {{- range $ns := $.DNSAddresses }}\n      - {{$ns}}\n      {{- end }}\n    {{- end }}\n  {{- end }}\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/param.env",
    "content": "{{range $key, $val := .Param -}}\nPARAM_{{ $key }}={{ $val }}\n{{end -}}\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/user-data",
    "content": "#cloud-config\n# vim:syntax=yaml\n\ngrowpart:\n  mode: auto\n  devices: ['/']\n\n{{- if eq .OS \"FreeBSD\" }}\npackages:\n  # boot.sh depends on sudo.\n  # TODO: consider replacing sudo with doas.\n  # FIXME: The hostagent script depends on sudo too.\n  # https://github.com/lima-vm/lima/issues/4594\n  - sudo\n{{- end }}\n\n{{- if .UpgradePackages }}\npackage_update: true\npackage_upgrade: true\npackage_reboot_if_required: true\n{{- end }}\n\n{{- if or .RosettaEnabled (and .Mounts (or (eq .MountType \"9p\") (eq .MountType \"virtiofs\"))) }}\nmounts:\n  {{- if .RosettaEnabled }}{{/* Mount the rosetta volume before systemd-binfmt.service(8) starts */}}\n- [vz-rosetta, /mnt/lima-rosetta, virtiofs, defaults, \"0\", \"0\"]\n  {{- end }}\n  {{- if and .Mounts (or (eq .MountType \"9p\") (eq .MountType \"virtiofs\")) }}\n    {{- range $m := $.Mounts}}\n- [{{$m.Tag}}, {{$m.MountPoint}}, {{$m.Type}}, \"{{$m.Options}}\", \"0\", \"0\"]\n    {{- end }}\n  {{- end }}\n{{- end }}\n\n{{- if .TimeZone }}\ntimezone: {{.TimeZone}}\n{{- end }}\n\nusers:\n  - name: \"{{.User}}\"\n{{- if ne .OS \"FreeBSD\" }}\n    # nuageinit does not support specifying the UID.\n    # The UID is fixed up in boot.essential.FreeBSD/00-freebsd-user-group.sh\n    uid: \"{{.UID}}\"\n{{- end }}\n{{- if .Comment }}\n    gecos: {{ printf \"%q\" .Comment }}\n{{- end }}\n    homedir: \"{{.Home}}\"\n    shell: {{.Shell}}\n{{- if eq .OS \"Darwin\" }}\n    {{/* On macOS, the password is not locked so as to allow GUI login. */}}\n    {{/* Since the user can run sudo with their own password, basically we don't need to set up passwordless sudo. */}}\n    {{/* However, it is still configured to allow `/sbin/shutdown -h now` without password, as it is invoked by `limactl stop` for graceful shutdown. */}}\n    {{/* (Why doesn't macOS VM support graceful shutdown?) */}}\n    sudo: ALL=(ALL) NOPASSWD:/sbin/shutdown -h now\n{{- else }}\n    sudo: ALL=(ALL) NOPASSWD:ALL\n    {{- if eq .OS \"FreeBSD\" }}\n    groups:\n    - wheel\n    doas: permit nopass :wheel\n    {{- end}}\n    lock_passwd: true\n{{- end }}\n{{- if eq .OS \"FreeBSD\" }}\n    ssh_authorized_keys:\n{{- else }}\n    ssh-authorized-keys:\n{{- end }}\n    {{- range $val := .SSHPubKeys }}\n      - {{ printf \"%q\" $val }}\n    {{- end }}\n\n{{- if .BootScripts }}\nwrite_files:\n - content: |\n      #!/bin/sh\n      set -eux\n      LIMA_CIDATA_MNT=\"/mnt/lima-cidata\"\n      UNAME=\"$(uname -s)\"\n      if [ \"${UNAME}\" = \"Darwin\" ]; then\n        LIMA_CIDATA_MNT=\"/Volumes/cidata\"\n        # Should have been mounted automatically\n      elif [ \"${UNAME}\" = \"FreeBSD\" ]; then\n        LIMA_CIDATA_DEV=\"/dev/iso9660/cidata\"\n        if [ ! -e \"${LIMA_CIDATA_DEV}\" ]; then\n          # When the iso is created with `hdiutil` on macOS,\n          # apparently the volume name becomes \"CIDATA\" not \"cidata\"\n          LIMA_CIDATA_DEV=\"/dev/iso9660/CIDATA\"\n        fi\n        mkdir -p -m 700 \"${LIMA_CIDATA_MNT}\"\n        mount_cd9660 -G wheel -U root -m 0700 -o ro,exec \"${LIMA_CIDATA_DEV}\" \"${LIMA_CIDATA_MNT}\"\n      elif [ \"${UNAME}\" = \"Linux\" ]; then\n        LIMA_CIDATA_DEV=\"/dev/disk/by-label/cidata\"\n        mkdir -p -m 700 \"${LIMA_CIDATA_MNT}\"\n        mount -o ro,mode=0700,dmode=0700,overriderockperm,exec,uid=0 \"${LIMA_CIDATA_DEV}\" \"${LIMA_CIDATA_MNT}\"\n      else\n        echo \"Unsupported OS: ${UNAME}\" >&2\n        exit 1\n      fi\n      export LIMA_CIDATA_MNT\n      exec \"${LIMA_CIDATA_MNT}\"/boot.sh\n{{- if or (eq .OS \"Darwin\") (eq .OS \"FreeBSD\") }}\n   owner: root:wheel\n{{- else }}\n   owner: root:root\n{{- end }}\n{{- if eq .OS \"FreeBSD\" }}\n   # nuageinit requires the path to be under an existing directory\n   path: /usr/sbin/lima-freebsd-init.sh\n{{- else }}\n   path: /var/lib/cloud/scripts/per-boot/00-lima.boot.sh\n{{- end }}\n   permissions: '0755'\n{{- if eq .OS \"FreeBSD\" }}\n  # nuageinit does not run /var/lib/cloud/scripts/per-boot/* scripts\n - content: |\n      #!/bin/sh\n\n      # PROVIDE: lima_freebsd_init\n      # REQUIRE: DAEMON\n      # BEFORE: LOGIN\n\n      . /etc/rc.subr\n\n      name=\"lima_freebsd_init\"\n      rcvar=\"lima_freebsd_init_enable\"\n      command=\"/usr/sbin/lima-freebsd-init.sh\"\n\n      load_rc_config \"$name\"\n      run_rc_command \"$1\"\n   owner: root:wheel\n   path: /etc/rc.d/lima_freebsd_init\n   permissions: '0755'\n - content: |\n      lima_freebsd_init_enable=\"YES\"\n   owner: root:wheel\n   path: /etc/rc.conf.d/lima_freebsd_init\n   permissions: '0644'\n{{- end }}\n{{- end }}\n\n{{- if .DNSAddresses }}\n# This has no effect on systems using systemd-resolved, but is used\n# on e.g. Alpine to set up /etc/resolv.conf on first boot.\n\nmanage_resolv_conf: true\n\nresolv_conf:\n  nameservers:\n  {{- range $ns := $.DNSAddresses }}\n  - {{$ns}}\n  {{- end }}\n{{- end }}\n\n{{- if or .CACerts.RemoveDefaults .CACerts.Trusted }}\n{{ with .CACerts }}\nca_certs:\n  {{- if .RemoveDefaults }}\n  remove_defaults: {{ .RemoveDefaults }}\n  {{- end }}\n  {{- if .Trusted}}\n  trusted:\n  {{- range $cert := .Trusted }}\n  - |\n    {{- range $line := $cert.Lines }}\n    {{ $line }}\n    {{- end }}\n    {{- end }}\n  {{- end }}\n  {{- end }}\n{{- end }}\n\n{{- if .BootCmds }}\nbootcmd:\n  {{- range $cmd := $.BootCmds }}\n- |\n  # We need to embed the params.env as a here-doc because /mnt/lima-cidata is not yet mounted\n  while read -r line; do [ -n \"$line\" ] && export \"$line\"; done <<'EOF'\n    {{- range $key, $val := $.Param }}\n  PARAM_{{ $key }}={{ $val }}\n    {{- end }}\n  EOF\n    {{- range $line := $cmd.Lines }}\n  {{ $line }}\n    {{- end }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/util/compare_version.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu\n\n: \"${SELFTEST:=}\"\nif [ -n \"$SELFTEST\" ]; then\n\tunset SELFTEST\n\techo >&2 \"=== Running positive tests ===\"\n\t(\n\t\tset -x\n\t\t\"$0\" 0.1.2 -eq 0.1.2\n\t\t\"$0\" 0.1.2 -ne 0.1.3\n\t\t\"$0\" 0.1.2 -ge 0.1.1\n\t\t\"$0\" 0.1.2 -ge 0.1.2\n\t\t\"$0\" 0.1.10 -ge 0.1.9\n\t\t\"$0\" 0.1.2 -gt 0.1.1\n\t\t\"$0\" 0.1.10 -gt 0.1.9\n\t\t\"$0\" 0.1.2 -le 0.1.2\n\t\t\"$0\" 0.1.2 -le 0.1.3\n\t\t\"$0\" 0.1.2 -le 0.1.10\n\t\t\"$0\" 0.1.2 -lt 0.1.3\n\t\t\"$0\" 0.1.2 -lt 0.1.10\n\t)\n\techo >&2 \"=== Running negative tests ===\"\n\t(\n\t\tset -x\n\t\t\"$0\" 0.1.2 -eq 0.1.1 && false\n\t\t\"$0\" 0.1.2 -ne 0.1.2 && false\n\t\t\"$0\" 0.1.2 -ge 0.1.3 && false\n\t\t\"$0\" 0.1.2 -gt 0.1.2 && false\n\t\t\"$0\" 0.1.2 -le 0.1.1 && false\n\t\t\"$0\" 0.1.2 -lt 0.1.2 && false\n\t\ttrue\n\t)\n\texit 0\nfi\n\nif [ \"$#\" -ne 3 ]; then\n\techo >&2 \"Usage: $0 VERSION-A OP VERSION-B\"\n\techo >&2 \"Implemented operators: -eq, -ne, -ge, -gt, -le, -lt\"\n\techo >&2 \"\"\n\techo >&2 \"Example: $0 1.2.10 -ge 1.2.9\"\n\texit 1\nfi\n\nversion_a=\"$1\"\nop=\"$2\"\nversion_b=\"$3\"\n\nsorted=\"$(echo -ne \"${version_a}\\n${version_b}\\n\" | sort -V -r | head -n1)\"\ncase \"${op}\" in\n-eq)\n\t[ \"${version_a}\" = \"${version_b}\" ]\n\t;;\n-ne)\n\t[ \"${version_a}\" != \"${version_b}\" ]\n\t;;\n-ge)\n\t[ \"${version_a}\" = \"${sorted}\" ]\n\t;;\n-gt)\n\t[ \"${version_a}\" = \"${sorted}\" ] && [ \"${version_a}\" != \"${version_b}\" ]\n\t;;\n-le)\n\t[ \"${version_b}\" = \"${sorted}\" ]\n\t;;\n-lt)\n\t[ \"${version_b}\" = \"${sorted}\" ] && [ \"${version_a}\" != \"${version_b}\" ]\n\t;;\n*)\n\techo \"Unknown operator \\\"$op\\\"\"\n\texit 1\n\t;;\nesac\n"
  },
  {
    "path": "pkg/cidata/cidata.TEMPLATE.d/util.FreeBSD/print_cidata_fstab.lua",
    "content": "#!/usr/libexec/flua\nlocal yaml = require(\"lyaml\")\nlocal fpath = \"/mnt/lima-cidata/user-data\"\nlocal f = io.open(fpath, \"r\")\nif not f then\n\terror(\"Could not open \" .. fpath)\nend\nlocal content = f:read(\"*a\")\nf:close()\nlocal config = yaml.load(content)\nfor i, row in ipairs(config.mounts) do\n\tfor j, value in ipairs(row) do\n\t\tio.write(value, \"\\t\")\n\tend\n\tio.write(\"\\n\")\nend\n"
  },
  {
    "path": "pkg/cidata/cidata.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cidata\n\nimport (\n\t\"compress/gzip\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/debugutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance/hostname\"\n\t\"github.com/lima-vm/lima/v2/pkg/iso9660util\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/localpathutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n)\n\nvar netLookupIP = func(host string) []net.IP {\n\tips, err := net.LookupIP(host)\n\tif err != nil {\n\t\tlogrus.Debugf(\"net.LookupIP %s: %s\", host, err)\n\t\treturn nil\n\t}\n\n\treturn ips\n}\n\nfunc setupEnv(instConfigEnv map[string]string, propagateProxyEnv bool, slirpGateway string) (map[string]string, error) {\n\t// Start with the proxy variables from the system settings.\n\tenv, err := osutil.ProxySettings()\n\tif err != nil {\n\t\treturn env, err\n\t}\n\t// env.* settings from lima.yaml override system settings without giving a warning\n\tmaps.Copy(env, instConfigEnv)\n\t// Current process environment setting override both system settings and env.*\n\tlowerVars := []string{\"ftp_proxy\", \"http_proxy\", \"https_proxy\", \"no_proxy\"}\n\tupperVars := make([]string, len(lowerVars))\n\tfor i, name := range lowerVars {\n\t\tupperVars[i] = strings.ToUpper(name)\n\t}\n\tif propagateProxyEnv {\n\t\tfor _, name := range append(lowerVars, upperVars...) {\n\t\t\tif value, ok := os.LookupEnv(name); ok {\n\t\t\t\tif _, ok := env[name]; ok && value != env[name] {\n\t\t\t\t\tlogrus.Infof(\"Overriding %q value %q with %q from limactl process environment\",\n\t\t\t\t\t\tname, env[name], value)\n\t\t\t\t}\n\t\t\t\tenv[name] = value\n\t\t\t}\n\t\t}\n\t}\n\t// Replace IP that IsLoopback in proxy settings with the gateway address\n\t// Delete settings with empty values, so the user can choose to ignore system settings.\n\tfor _, name := range append(lowerVars, upperVars...) {\n\t\tvalue, ok := env[name]\n\t\tif ok && value == \"\" {\n\t\t\tdelete(env, name)\n\t\t} else if ok && !strings.EqualFold(name, \"no_proxy\") {\n\t\t\tu, err := url.Parse(value)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Warnf(\"Ignoring invalid proxy %q=%v: %s\", name, value, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, ip := range netLookupIP(u.Hostname()) {\n\t\t\t\tif ip.IsLoopback() {\n\t\t\t\t\tnewHost := slirpGateway\n\t\t\t\t\tif u.Port() != \"\" {\n\t\t\t\t\t\tnewHost = net.JoinHostPort(newHost, u.Port())\n\t\t\t\t\t}\n\t\t\t\t\tu.Host = newHost\n\t\t\t\t\tvalue = u.String()\n\t\t\t\t}\n\t\t\t}\n\t\t\tif value != env[name] {\n\t\t\t\tlogrus.Infof(\"Replacing %q value %q with %q\", name, env[name], value)\n\t\t\t\tenv[name] = value\n\t\t\t}\n\t\t}\n\t}\n\t// Make sure uppercase variants have the same value as lowercase ones.\n\t// If both are set, the lowercase variant value takes precedence.\n\tfor _, lowerName := range lowerVars {\n\t\tupperName := strings.ToUpper(lowerName)\n\t\tif _, ok := env[lowerName]; ok {\n\t\t\tif _, ok := env[upperName]; ok && env[lowerName] != env[upperName] {\n\t\t\t\tlogrus.Warnf(\"Changing %q value from %q to %q to match %q\",\n\t\t\t\t\tupperName, env[upperName], env[lowerName], lowerName)\n\t\t\t}\n\t\t\tenv[upperName] = env[lowerName]\n\t\t} else if _, ok := env[upperName]; ok {\n\t\t\tenv[lowerName] = env[upperName]\n\t\t}\n\t}\n\treturn env, nil\n}\n\nfunc templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string, noCloudInit, rosettaEnabled, rosettaBinFmt bool) (*TemplateArgs, error) {\n\tif err := limayaml.Validate(instConfig, false); err != nil {\n\t\treturn nil, err\n\t}\n\tarchive := \"nerdctl-full.tgz\"\n\targs := TemplateArgs{\n\t\tDebug:              debugutil.Debug,\n\t\tOS:                 *instConfig.OS,\n\t\tBootScripts:        bootScripts,\n\t\tName:               name,\n\t\tHostname:           hostname.FromInstName(name), // TODO: support customization\n\t\tUser:               *instConfig.User.Name,\n\t\tComment:            removeControlChars(*instConfig.User.Comment),\n\t\tHome:               *instConfig.User.Home,\n\t\tShell:              *instConfig.User.Shell,\n\t\tUID:                *instConfig.User.UID,\n\t\tGuestInstallPrefix: *instConfig.GuestInstallPrefix,\n\t\tUpgradePackages:    *instConfig.UpgradePackages,\n\t\tContainerd:         Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User, Archive: archive},\n\t\tSlirpNICName:       networks.SlirpNICName,\n\n\t\tVMType:         *instConfig.VMType,\n\t\tVSockPort:      vsockPort,\n\t\tVirtioPort:     virtioPort,\n\t\tRosettaEnabled: rosettaEnabled,\n\t\tRosettaBinFmt:  rosettaBinFmt,\n\t\tPlain:          *instConfig.Plain,\n\t\tTimeZone:       *instConfig.TimeZone,\n\t\tNoCloudInit:    noCloudInit,\n\t\tParam:          instConfig.Param,\n\t}\n\n\tfirstUsernetIndex := limayaml.FirstUsernetIndex(instConfig)\n\tvar subnet net.IP\n\tvar err error\n\n\tif firstUsernetIndex != -1 {\n\t\tusernetName := instConfig.Networks[firstUsernetIndex].Lima\n\t\tsubnet, err = usernet.Subnet(usernetName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\targs.SlirpGateway = usernet.GatewayIP(subnet)\n\t\targs.SlirpDNS = usernet.GatewayIP(subnet)\n\t} else {\n\t\tsubnet, _, err = net.ParseCIDR(networks.SlirpNetwork)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\targs.SlirpGateway = usernet.GatewayIP(subnet)\n\t\tif *instConfig.VMType == limatype.VZ {\n\t\t\targs.SlirpDNS = usernet.GatewayIP(subnet)\n\t\t} else {\n\t\t\targs.SlirpDNS = usernet.DNSIP(subnet)\n\t\t}\n\t\targs.SlirpIPAddress = networks.SlirpIPAddress\n\t}\n\n\t// change instance id on every boot so network config will be processed again\n\targs.IID = fmt.Sprintf(\"iid-%d\", time.Now().Unix())\n\n\tpubKeys, err := sshutil.DefaultPubKeys(ctx, *instConfig.SSH.LoadDotSSHPubKeys)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(pubKeys) == 0 {\n\t\treturn nil, errors.New(\"no SSH key was found, run `ssh-keygen`\")\n\t}\n\tfor _, f := range pubKeys {\n\t\targs.SSHPubKeys = append(args.SSHPubKeys, f.Content)\n\t}\n\n\tvar fstype string\n\tswitch *instConfig.MountType {\n\tcase limatype.REVSSHFS:\n\t\tfstype = \"sshfs\"\n\tcase limatype.NINEP:\n\t\tfstype = \"9p\"\n\t\tif *instConfig.OS == limatype.FREEBSD {\n\t\t\tfstype = \"p9fs\"\n\t\t}\n\tcase limatype.VIRTIOFS:\n\t\tfstype = \"virtiofs\"\n\t}\n\thostHome, err := localpathutil.Expand(\"~\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, f := range instConfig.Mounts {\n\t\ttag := limayaml.MountTag(f.Location, *f.MountPoint)\n\t\toptions := \"rw\"\n\t\tif *instConfig.OS == limatype.LINUX {\n\t\t\toptions = \"defaults\"\n\t\t}\n\t\tswitch fstype {\n\t\tcase \"9p\", \"p9fs\", \"virtiofs\":\n\t\t\toptions = \"ro\"\n\t\t\tif *f.Writable {\n\t\t\t\toptions = \"rw\"\n\t\t\t}\n\t\t\tif fstype == \"9p\" {\n\t\t\t\toptions += \",trans=virtio\"\n\t\t\t\toptions += fmt.Sprintf(\",version=%s\", *f.NineP.ProtocolVersion)\n\t\t\t\tmsize, err := units.RAMInBytes(*f.NineP.Msize)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"failed to parse msize for %q: %w\", f.Location, err)\n\t\t\t\t}\n\t\t\t\toptions += fmt.Sprintf(\",msize=%d\", msize)\n\t\t\t\toptions += fmt.Sprintf(\",cache=%s\", *f.NineP.Cache)\n\t\t\t}\n\t\t\t// don't fail the boot, if virtfs is not available\n\t\t\tswitch *instConfig.OS {\n\t\t\tcase limatype.LINUX:\n\t\t\t\toptions += \",nofail\"\n\t\t\tcase limatype.FREEBSD:\n\t\t\t\toptions += \",failok\"\n\t\t\t}\n\t\t}\n\t\targs.Mounts = append(args.Mounts, Mount{Tag: tag, MountPoint: *f.MountPoint, Type: fstype, Options: options})\n\t\tif f.Location == hostHome {\n\t\t\targs.HostHomeMountPoint = *f.MountPoint\n\t\t}\n\t}\n\n\tswitch *instConfig.MountType {\n\tcase limatype.REVSSHFS:\n\t\targs.MountType = \"reverse-sshfs\"\n\tcase limatype.NINEP:\n\t\targs.MountType = \"9p\"\n\tcase limatype.VIRTIOFS:\n\t\targs.MountType = \"virtiofs\"\n\t}\n\n\tfor i, d := range instConfig.AdditionalDisks {\n\t\tformat := true\n\t\tif d.Format != nil {\n\t\t\tformat = *d.Format\n\t\t}\n\t\tfstype := \"\"\n\t\tif d.FSType != nil {\n\t\t\tfstype = *d.FSType\n\t\t}\n\t\targs.Disks = append(args.Disks, Disk{\n\t\t\tName:   d.Name,\n\t\t\tDevice: diskDeviceNameFromOrder(i),\n\t\t\tFormat: format,\n\t\t\tFSType: fstype,\n\t\t\tFSArgs: d.FSArgs,\n\t\t})\n\t}\n\n\targs.Networks = append(args.Networks, Network{MACAddress: limayaml.MACAddress(instDir), Interface: networks.SlirpNICName, Metric: 200})\n\tfor i, nw := range instConfig.Networks {\n\t\tif i == firstUsernetIndex {\n\t\t\tcontinue\n\t\t}\n\t\targs.Networks = append(args.Networks, Network{MACAddress: nw.MACAddress, Interface: nw.Interface, Metric: *nw.Metric})\n\t}\n\n\targs.Env, err = setupEnv(instConfig.Env, *instConfig.PropagateProxyEnv, args.SlirpGateway)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch {\n\tcase len(instConfig.DNS) > 0:\n\t\tfor _, addr := range instConfig.DNS {\n\t\t\targs.DNSAddresses = append(args.DNSAddresses, addr.String())\n\t\t}\n\tcase firstUsernetIndex != -1 || *instConfig.VMType == limatype.VZ:\n\t\targs.DNSAddresses = append(args.DNSAddresses, args.SlirpDNS)\n\tcase *instConfig.HostResolver.Enabled:\n\t\targs.UDPDNSLocalPort = udpDNSLocalPort\n\t\targs.TCPDNSLocalPort = tcpDNSLocalPort\n\t\targs.DNSAddresses = append(args.DNSAddresses, args.SlirpDNS)\n\tdefault:\n\t\targs.DNSAddresses, err = osutil.DNSAddresses()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\targs.CACerts.RemoveDefaults = instConfig.CACertificates.RemoveDefaults\n\n\tfor _, path := range instConfig.CACertificates.Files {\n\t\texpanded, err := localpathutil.Expand(path)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcontent, err := os.ReadFile(expanded)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcert := getCert(string(content))\n\t\targs.CACerts.Trusted = append(args.CACerts.Trusted, cert)\n\t}\n\n\tfor _, content := range instConfig.CACertificates.Certs {\n\t\tcert := getCert(content)\n\t\targs.CACerts.Trusted = append(args.CACerts.Trusted, cert)\n\t}\n\n\t// Remove empty caCerts (default values) from configuration yaml\n\tif !*args.CACerts.RemoveDefaults && len(args.CACerts.Trusted) == 0 {\n\t\targs.CACerts.RemoveDefaults = nil\n\t\targs.CACerts.Trusted = nil\n\t}\n\n\targs.BootCmds = getBootCmds(instConfig.Provision)\n\n\tfor i, f := range instConfig.Provision {\n\t\tif f.Mode == limatype.ProvisionModeDependency && *f.SkipDefaultDependencyResolution {\n\t\t\targs.SkipDefaultDependencyResolution = true\n\t\t}\n\t\tif f.Mode == limatype.ProvisionModeData {\n\t\t\targs.DataFiles = append(args.DataFiles, DataFile{\n\t\t\t\tFileName:    fmt.Sprintf(\"%08d\", i),\n\t\t\t\tOverwrite:   strconv.FormatBool(*f.Overwrite),\n\t\t\t\tOwner:       *f.Owner,\n\t\t\t\tPath:        *f.Path,\n\t\t\t\tPermissions: *f.Permissions,\n\t\t\t})\n\t\t}\n\t\tif f.Mode == limatype.ProvisionModeYQ {\n\t\t\targs.YQProvisions = append(args.YQProvisions, YQProvision{\n\t\t\t\tFileName:    fmt.Sprintf(\"%08d\", i),\n\t\t\t\tFormat:      *f.Format,\n\t\t\t\tOwner:       *f.Owner,\n\t\t\t\tPath:        *f.Path,\n\t\t\t\tPermissions: *f.Permissions,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn &args, nil\n}\n\nfunc GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *limatype.LimaYAML) error {\n\targs, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, \"\", false, false, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// mounts are not included here\n\targs.Mounts = nil\n\t// resolv_conf is not included here\n\targs.DNSAddresses = nil\n\n\tif err := ValidateTemplateArgs(args); err != nil {\n\t\treturn err\n\t}\n\n\tconfig, err := ExecuteTemplateCloudConfig(args)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tos.RemoveAll(filepath.Join(instDir, filenames.CloudConfig)) // delete existing\n\treturn os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444)\n}\n\n// GenerateISO9660 generates the cidata ISO9660 image (or directory, for noCloudInit)\n// in instDir. It returns the instance ID, which changes on every boot.\nfunc 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) {\n\targs, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort, noCloudInit, rosettaEnabled, rosettaBinFmt)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := ValidateTemplateArgs(args); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tlayout, err := ExecuteTemplateCIDataISO(args)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdriverScripts, err := drv.BootScripts()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get boot scripts: %w\", err)\n\t}\n\n\tfor filename, content := range driverScripts {\n\t\tlayoutPath := path.Join(\"boot.Linux\", filename)\n\t\tif strings.Contains(filename, \"/\") {\n\t\t\t// When the filename contains a slash, it must be in the format of \"boot.<OS>/<SCRIPT>\"\n\t\t\tif !strings.HasPrefix(filename, \"boot.\") || strings.Count(filename, \"/\") != 1 {\n\t\t\t\treturn \"\", fmt.Errorf(\"invalid boot script filename %q: must be in format 'boot.<OS>/<SCRIPT>'\", filename)\n\t\t\t}\n\t\t\tlayoutPath = filename\n\t\t} else {\n\t\t\tlogrus.Warnf(\"Boot script filename %q does not contain '/', treating it as a script for Linux and prefixing with 'boot.Linux/'\", filename)\n\t\t}\n\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\tPath:   layoutPath,\n\t\t\tReader: strings.NewReader(string(content)),\n\t\t})\n\t}\n\n\tfor i, f := range instConfig.Provision {\n\t\tswitch f.Mode {\n\t\tcase limatype.ProvisionModeSystem, limatype.ProvisionModeUser, limatype.ProvisionModeDependency:\n\t\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\t\tPath:   fmt.Sprintf(\"provision.%s/%08d\", f.Mode, i),\n\t\t\t\tReader: strings.NewReader(*f.Script),\n\t\t\t})\n\t\tcase limatype.ProvisionModeData:\n\t\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\t\tPath:   fmt.Sprintf(\"provision.%s/%08d\", f.Mode, i),\n\t\t\t\tReader: strings.NewReader(*f.Content),\n\t\t\t})\n\t\tcase limatype.ProvisionModeYQ:\n\t\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\t\tPath:   fmt.Sprintf(\"provision.%s/%08d\", f.Mode, i),\n\t\t\t\tReader: strings.NewReader(*f.Expression),\n\t\t\t})\n\t\tcase limatype.ProvisionModeBoot:\n\t\t\tcontinue\n\t\tcase limatype.ProvisionModeAnsible:\n\t\t\tcontinue\n\t\tdefault:\n\t\t\treturn \"\", fmt.Errorf(\"unknown provision mode %q\", f.Mode)\n\t\t}\n\t}\n\n\tif guestAgentBinary != \"\" {\n\t\tvar guestAgent io.ReadCloser\n\t\tif strings.HasSuffix(guestAgentBinary, \".gz\") {\n\t\t\tlogrus.Debugf(\"Decompressing %s\", guestAgentBinary)\n\t\t\tguestAgentGz, err := os.Open(guestAgentBinary)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tdefer guestAgentGz.Close()\n\t\t\tguestAgent, err = gzip.NewReader(guestAgentGz)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t} else {\n\t\t\tguestAgent, err = os.Open(guestAgentBinary)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t}\n\n\t\tdefer guestAgent.Close()\n\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\tPath:   \"lima-guestagent\",\n\t\t\tReader: guestAgent,\n\t\t})\n\t}\n\n\tif nerdctlArchive != \"\" {\n\t\tnftgz := args.Containerd.Archive\n\t\tnftgzR, err := os.Open(nerdctlArchive)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdefer nftgzR.Close()\n\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\t// ISO9660 requires len(Path) <= 30\n\t\t\tPath:   nftgz,\n\t\t\tReader: nftgzR,\n\t\t})\n\t}\n\n\tif noCloudInit {\n\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\tPath:   \"ssh_authorized_keys\",\n\t\t\tReader: strings.NewReader(strings.Join(args.SSHPubKeys, \"\\n\")),\n\t\t})\n\t\treturn args.IID, writeCIDataDir(filepath.Join(instDir, filenames.CIDataISODir), layout)\n\t}\n\n\tvar iso9660Options []iso9660util.WriteOpt\n\tswitch *instConfig.OS {\n\tcase limatype.DARWIN, limatype.FREEBSD:\n\t\t// Darwin: Without Joliet, all the file names will be in upper case, and the hiphen will be replaced with underscore\n\t\t// FreeBSD: Without Joliet, the files are not executable\n\t\tiso9660Options = append(iso9660Options, iso9660util.WithJoliet())\n\t}\n\treturn args.IID, iso9660util.Write(filepath.Join(instDir, filenames.CIDataISO), \"cidata\", layout, iso9660Options...)\n}\n\nfunc removeControlChars(s string) string {\n\tout := make([]rune, 0, len(s))\n\tfor _, r := range s {\n\t\tif unicode.IsPrint(r) {\n\t\t\tout = append(out, r)\n\t\t}\n\t}\n\treturn string(out)\n}\n\nfunc getCert(content string) Cert {\n\tlines := []string{}\n\tfor line := range strings.SplitSeq(content, \"\\n\") {\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tlines = append(lines, strings.TrimSpace(line))\n\t}\n\t// return lines\n\treturn Cert{Lines: lines}\n}\n\nfunc getBootCmds(p []limatype.Provision) []BootCmds {\n\tvar bootCmds []BootCmds\n\tfor _, f := range p {\n\t\tif f.Mode == limatype.ProvisionModeBoot {\n\t\t\tbootCmds = append(bootCmds, BootCmds{Lines: strings.Split(*f.Script, \"\\n\")})\n\t\t}\n\t}\n\treturn bootCmds\n}\n\nfunc diskDeviceNameFromOrder(order int) string {\n\treturn fmt.Sprintf(\"vd%c\", int('b')+order)\n}\n\nfunc writeCIDataDir(rootPath string, layout []iso9660util.Entry) error {\n\tslices.SortFunc(layout, func(a, b iso9660util.Entry) int {\n\t\treturn strings.Compare(strings.ToLower(a.Path), strings.ToLower(b.Path))\n\t})\n\n\tif err := os.RemoveAll(rootPath); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, e := range layout {\n\t\tif dir := path.Dir(e.Path); dir != \"\" && dir != \"/\" {\n\t\t\tif err := os.MkdirAll(filepath.Join(rootPath, dir), 0o700); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tf, err := os.OpenFile(filepath.Join(rootPath, e.Path), os.O_CREATE|os.O_RDWR, 0o700)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := io.Copy(f, e.Reader); err != nil {\n\t\t\t_ = f.Close()\n\t\t\treturn err\n\t\t}\n\t\t_ = f.Close()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/cidata/cidata_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cidata\n\nimport (\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n)\n\nfunc fakeLookupIP(_ string) []net.IP {\n\treturn []net.IP{net.IPv4(127, 0, 0, 0)}\n}\n\nfunc TestSetupEnv(t *testing.T) {\n\tnetLookupIP = fakeLookupIP\n\turlMustParse := func(urlStr string) *url.URL {\n\t\tu, err := url.Parse(urlStr)\n\t\tassert.NilError(t, err)\n\t\treturn u\n\t}\n\n\thttpProxyCases := []*url.URL{\n\t\turlMustParse(\"http://127.0.0.1\"),\n\t\turlMustParse(\"http://127.0.0.1:8080\"),\n\t\turlMustParse(\"https://127.0.0.1:8080\"),\n\t\turlMustParse(\"sock4://127.0.0.1:8080\"),\n\t\turlMustParse(\"sock5://127.0.0.1:8080\"),\n\t\turlMustParse(\"http://127.0.0.1:8080/\"),\n\t\turlMustParse(\"http://127.0.0.1:8080/path\"),\n\t\turlMustParse(\"http://localhost:8080\"),\n\t\turlMustParse(\"http://localhost:8080/\"),\n\t\turlMustParse(\"http://localhost:8080/path\"),\n\t\turlMustParse(\"http://docker.for.mac.localhost:8080\"),\n\t\turlMustParse(\"http://docker.for.mac.localhost:8080/\"),\n\t\turlMustParse(\"http://docker.for.mac.localhost:8080/path\"),\n\t}\n\n\tfor _, httpProxy := range httpProxyCases {\n\t\tt.Run(httpProxy.Host, func(t *testing.T) {\n\t\t\tenvKey := \"http_proxy\"\n\t\t\tenvValue := httpProxy.String()\n\t\t\tenvs, err := setupEnv(map[string]string{envKey: envValue}, false, networks.SlirpGateway)\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, envs[envKey], strings.ReplaceAll(envValue, httpProxy.Hostname(), networks.SlirpGateway))\n\t\t})\n\t}\n}\n\nfunc TestSetupInvalidEnv(t *testing.T) {\n\tenvKey := \"http_proxy\"\n\tenvValue := \"://localhost:8080\"\n\tenvs, err := setupEnv(map[string]string{envKey: envValue}, false, networks.SlirpGateway)\n\tassert.NilError(t, err)\n\tassert.Equal(t, envs[envKey], envValue)\n}\n"
  },
  {
    "path": "pkg/cidata/cloudinittypes/metadata.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cloudinittypes\n\ntype MetaData struct {\n\tInstanceID    string `yaml:\"instance-id,omitempty\"`\n\tLocalHostname string `yaml:\"local-hostname,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/cidata/cloudinittypes/userdata.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cloudinittypes\n\ntype UserData struct {\n\tGrowpart *Growpart `yaml:\"growpart,omitempty\"`\n\n\tPackageUpdate           bool `yaml:\"package_update,omitempty\"`\n\tPackageUpgrade          bool `yaml:\"package_upgrade,omitempty\"`\n\tPackageRebootIfRequired bool `yaml:\"package_reboot_if_required,omitempty\"`\n\n\tMounts [][]string `yaml:\"mounts,omitempty\"`\n\n\tTimezone string `yaml:\"timezone,omitempty\"`\n\n\tUsers []User `yaml:\"users,omitempty\"`\n\n\tWriteFiles []WriteFile `yaml:\"write_files,omitempty\"`\n\n\tManageResolvConf bool        `yaml:\"manage_resolv_conf,omitempty\"`\n\tResolvConf       *ResolvConf `yaml:\"resolv_conf,omitempty\"`\n\n\tCACerts *CACerts `yaml:\"ca_certs,omitempty\"`\n\n\tBootCmd []string `yaml:\"bootcmd,omitempty\"`\n}\n\ntype Growpart struct {\n\tMode    string   `yaml:\"mode\"`\n\tDevices []string `yaml:\"devices\"`\n}\n\ntype User struct {\n\tName              string   `yaml:\"name,omitempty\"`\n\tGecos             string   `yaml:\"gecos,omitempty\"`\n\tUID               string   `yaml:\"uid,omitempty\"` // TODO: check if int is allowed too\n\tHomedir           string   `yaml:\"homedir,omitempty\"`\n\tShell             string   `yaml:\"shell,omitempty\"`\n\tSudo              string   `yaml:\"sudo,omitempty\"` // TODO: allow []string as well\n\tLockPasswd        string   `yaml:\"lock_passwd,omitempty\"`\n\tSSHAuthorizedKeys []string `yaml:\"ssh-authorized-keys,omitempty\"`\n}\n\ntype WriteFile struct {\n\tContent     string `yaml:\"content\"`\n\tOwner       string `yaml:\"owner,omitempty\"`\n\tPath        string `yaml:\"path\"`\n\tPermissions string `yaml:\"permissions,omitempty\"` // TODO: check if int is allowed too\n}\n\ntype ResolvConf struct {\n\tNameservers []string `yaml:\"nameservers,omitempty\"`\n}\n\ntype CACerts struct {\n\tRemoveDefaults bool     `yaml:\"remove_defaults,omitempty\"`\n\tTrusted        []string `yaml:\"trusted,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/cidata/fuzz_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cidata\n\nimport (\n\t\"testing\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n)\n\nfunc FuzzSetupEnv(f *testing.F) {\n\tf.Fuzz(func(_ *testing.T, suffix string, localhost bool) {\n\t\tprefix := \"http://127.0.0.1:8080/\"\n\t\tif localhost {\n\t\t\tprefix = \"http://localhost:8080/\"\n\t\t}\n\t\t_, _ = setupEnv(map[string]string{\"http_proxy\": prefix + suffix}, false, networks.SlirpGateway)\n\t})\n}\n"
  },
  {
    "path": "pkg/cidata/template.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cidata\n\nimport (\n\t\"bytes\"\n\t\"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"path\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/identifiers\"\n\t\"github.com/lima-vm/lima/v2/pkg/iso9660util\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/textutil\"\n)\n\n//go:embed cidata.TEMPLATE.d\nvar templateFS embed.FS\n\nconst templateFSRoot = \"cidata.TEMPLATE.d\"\n\ntype CACerts struct {\n\tRemoveDefaults *bool\n\tTrusted        []Cert\n}\n\ntype Cert struct {\n\tLines []string\n}\n\ntype Containerd struct {\n\tSystem  bool\n\tUser    bool\n\tArchive string\n}\ntype Network struct {\n\tMACAddress string\n\tInterface  string\n\tMetric     uint32\n}\ntype Mount struct {\n\tTag        string\n\tMountPoint string // abs path, accessible by the User\n\tType       string\n\tOptions    string\n}\ntype BootCmds struct {\n\tLines []string\n}\n\ntype DataFile struct {\n\tFileName    string\n\tOverwrite   string\n\tOwner       string\n\tPath        string\n\tPermissions string\n}\n\ntype YQProvision struct {\n\tFileName    string\n\tFormat      string\n\tOwner       string\n\tPath        string\n\tPermissions string\n}\n\ntype Disk struct {\n\tName   string\n\tDevice string\n\tFormat bool\n\tFSType string\n\tFSArgs []string\n}\ntype TemplateArgs struct {\n\tDebug                           bool\n\tOS                              limatype.OS\n\tName                            string // instance name\n\tHostname                        string // instance hostname\n\tIID                             string // instance id\n\tUser                            string // user name\n\tComment                         string // user information\n\tHome                            string // home directory\n\tShell                           string // login shell\n\tUID                             uint32\n\tSSHPubKeys                      []string\n\tMounts                          []Mount\n\tMountType                       string\n\tDisks                           []Disk\n\tGuestInstallPrefix              string\n\tUpgradePackages                 bool\n\tContainerd                      Containerd\n\tNetworks                        []Network\n\tSlirpNICName                    string\n\tSlirpGateway                    string\n\tSlirpDNS                        string\n\tSlirpIPAddress                  string\n\tUDPDNSLocalPort                 int\n\tTCPDNSLocalPort                 int\n\tEnv                             map[string]string\n\tParam                           map[string]string\n\tBootScripts                     bool\n\tDataFiles                       []DataFile\n\tYQProvisions                    []YQProvision\n\tDNSAddresses                    []string\n\tCACerts                         CACerts\n\tHostHomeMountPoint              string\n\tBootCmds                        []BootCmds\n\tRosettaEnabled                  bool\n\tRosettaBinFmt                   bool\n\tSkipDefaultDependencyResolution bool\n\tVMType                          string\n\tVSockPort                       int\n\tVirtioPort                      string\n\tPlain                           bool\n\tTimeZone                        string\n\tNoCloudInit                     bool\n}\n\nfunc ValidateTemplateArgs(args *TemplateArgs) error {\n\tif err := identifiers.Validate(args.Name); err != nil {\n\t\treturn err\n\t}\n\t// args.User is intentionally not validated here; the user can override with any name they want\n\t// limayaml.FillDefault will validate the default (local) username, but not an explicit setting\n\tif args.User == \"root\" {\n\t\treturn errors.New(\"field User must not be \\\"root\\\"\")\n\t}\n\tif args.UID == 0 {\n\t\treturn errors.New(\"field UID must not be 0\")\n\t}\n\tif args.Home == \"\" {\n\t\treturn errors.New(\"field Home must be set\")\n\t}\n\tif args.Shell == \"\" {\n\t\treturn errors.New(\"field Shell must be set\")\n\t}\n\tif len(args.SSHPubKeys) == 0 {\n\t\treturn errors.New(\"field SSHPubKeys must be set\")\n\t}\n\tfor i, m := range args.Mounts {\n\t\tf := m.MountPoint\n\t\tif !path.IsAbs(f) {\n\t\t\treturn fmt.Errorf(\"field mounts[%d] must be absolute, got %q\", i, f)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ExecuteTemplateCloudConfig(args *TemplateArgs) ([]byte, error) {\n\tif err := ValidateTemplateArgs(args); err != nil {\n\t\treturn nil, err\n\t}\n\n\tuserData, err := templateFS.ReadFile(path.Join(templateFSRoot, \"user-data\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcloudConfigYaml := string(userData)\n\treturn textutil.ExecuteTemplate(cloudConfigYaml, args)\n}\n\nfunc ExecuteTemplateCIDataISO(args *TemplateArgs) ([]iso9660util.Entry, error) {\n\tif err := ValidateTemplateArgs(args); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfsys, err := fs.Sub(templateFS, templateFSRoot)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar layout []iso9660util.Entry\n\twalkFn := func(path string, d fs.DirEntry, walkErr error) error {\n\t\tif walkErr != nil {\n\t\t\treturn walkErr\n\t\t}\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\tif !d.Type().IsRegular() {\n\t\t\treturn fmt.Errorf(\"got non-regular file %q\", path)\n\t\t}\n\t\ttemplateB, err := fs.ReadFile(fsys, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tb, err := textutil.ExecuteTemplate(string(templateB), args)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlayout = append(layout, iso9660util.Entry{\n\t\t\tPath:   path,\n\t\t\tReader: bytes.NewReader(b),\n\t\t})\n\t\treturn nil\n\t}\n\n\tif err := fs.WalkDir(fsys, \".\", walkFn); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn layout, nil\n}\n"
  },
  {
    "path": "pkg/cidata/template_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage cidata\n\nimport (\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nvar defaultRemoveDefaults = false\n\nfunc TestConfig(t *testing.T) {\n\targs := &TemplateArgs{\n\t\tName:    \"default\",\n\t\tUser:    \"foo\",\n\t\tUID:     501,\n\t\tComment: \"Foo\",\n\t\tHome:    \"/home/foo.guest\",\n\t\tShell:   \"/bin/bash\",\n\t\tSSHPubKeys: []string{\n\t\t\t\"ssh-rsa dummy foo@example.com\",\n\t\t},\n\t\tMountType: \"reverse-sshfs\",\n\t}\n\tconfig, err := ExecuteTemplateCloudConfig(args)\n\tassert.NilError(t, err)\n\tt.Log(string(config))\n\tassert.Assert(t, !strings.Contains(string(config), \"ca_certs:\"))\n\tassert.Assert(t, !strings.Contains(string(config), \"mounts:\"))\n}\n\nfunc TestConfigCACerts(t *testing.T) {\n\targs := &TemplateArgs{\n\t\tName:    \"default\",\n\t\tUser:    \"foo\",\n\t\tUID:     501,\n\t\tComment: \"Foo\",\n\t\tHome:    \"/home/foo.guest\",\n\t\tShell:   \"/bin/bash\",\n\t\tSSHPubKeys: []string{\n\t\t\t\"ssh-rsa dummy foo@example.com\",\n\t\t},\n\t\tMountType: \"reverse-sshfs\",\n\t\tCACerts: CACerts{\n\t\t\tRemoveDefaults: &defaultRemoveDefaults,\n\t\t},\n\t}\n\tconfig, err := ExecuteTemplateCloudConfig(args)\n\tassert.NilError(t, err)\n\tt.Log(string(config))\n\tassert.Assert(t, strings.Contains(string(config), \"ca_certs:\"))\n}\n\nvar defaultMounts = []Mount{\n\t{MountPoint: \"/home/foo.guest\", Tag: \"mount0\", Type: \"virtiofs\", Options: \"ro\"},\n\t{MountPoint: \"/tmp/lima\", Tag: \"mount1\", Type: \"virtiofs\"},\n}\n\nfunc TestConfigMounts(t *testing.T) {\n\targs := &TemplateArgs{\n\t\tName:    \"default\",\n\t\tUser:    \"foo\",\n\t\tUID:     501,\n\t\tComment: \"Foo\",\n\t\tHome:    \"/home/foo.guest\",\n\t\tShell:   \"/bin/bash\",\n\t\tSSHPubKeys: []string{\n\t\t\t\"ssh-rsa dummy foo@example.com\",\n\t\t},\n\t\tMountType: \"virtiofs\", // override\n\t\tMounts:    defaultMounts,\n\t}\n\tconfig, err := ExecuteTemplateCloudConfig(args)\n\tassert.NilError(t, err)\n\tt.Log(string(config))\n\tassert.Assert(t, strings.Contains(string(config), \"mounts:\"))\n}\n\nfunc TestConfigMountsNone(t *testing.T) {\n\targs := &TemplateArgs{\n\t\tName:    \"default\",\n\t\tUser:    \"foo\",\n\t\tUID:     501,\n\t\tComment: \"Foo\",\n\t\tHome:    \"/home/foo.guest\",\n\t\tShell:   \"/bin/bash\",\n\t\tSSHPubKeys: []string{\n\t\t\t\"ssh-rsa dummy foo@example.com\",\n\t\t},\n\t\tMountType: \"virtiofs\", // override\n\t\tMounts:    []Mount{},\n\t}\n\tconfig, err := ExecuteTemplateCloudConfig(args)\n\tassert.NilError(t, err)\n\tt.Log(string(config))\n\tassert.Assert(t, !strings.Contains(string(config), \"mounts:\"))\n}\n\nfunc TestTemplate(t *testing.T) {\n\targs := &TemplateArgs{\n\t\tName:  \"default\",\n\t\tUser:  \"foo\",\n\t\tUID:   501,\n\t\tHome:  \"/home/foo.guest\",\n\t\tShell: \"/bin/bash\",\n\t\tSSHPubKeys: []string{\n\t\t\t\"ssh-rsa dummy foo@example.com\",\n\t\t},\n\t\tMounts: []Mount{\n\t\t\t{MountPoint: \"/Users/dummy\"},\n\t\t\t{MountPoint: \"/Users/dummy/lima\"},\n\t\t},\n\t\tMountType: \"reverse-sshfs\",\n\t\tCACerts: CACerts{\n\t\t\tRemoveDefaults: &defaultRemoveDefaults,\n\t\t\tTrusted:        []Cert{},\n\t\t},\n\t}\n\tlayout, err := ExecuteTemplateCIDataISO(args)\n\tassert.NilError(t, err)\n\tfor _, f := range layout {\n\t\tt.Logf(\"=== %q ===\", f.Path)\n\t\tb, err := io.ReadAll(f.Reader)\n\t\tassert.NilError(t, err)\n\t\tt.Log(string(b))\n\t\tif f.Path == \"user-data\" {\n\t\t\t// mounted later\n\t\t\tassert.Assert(t, !strings.Contains(string(b), \"mounts:\"))\n\t\t\t// ca_certs:\n\t\t\tassert.Assert(t, !strings.Contains(string(b), \"trusted:\"))\n\t\t}\n\t}\n}\n\nfunc TestTemplate9p(t *testing.T) {\n\targs := &TemplateArgs{\n\t\tName:  \"default\",\n\t\tUser:  \"foo\",\n\t\tUID:   501,\n\t\tHome:  \"/home/foo.guest\",\n\t\tShell: \"/bin/bash\",\n\t\tSSHPubKeys: []string{\n\t\t\t\"ssh-rsa dummy foo@example.com\",\n\t\t},\n\t\tMounts: []Mount{\n\t\t\t{Tag: \"mount0\", MountPoint: \"/Users/dummy\", Type: \"9p\", Options: \"ro,trans=virtio\"},\n\t\t\t{Tag: \"mount1\", MountPoint: \"/Users/dummy/lima\", Type: \"9p\", Options: \"rw,trans=virtio\"},\n\t\t},\n\t\tMountType: \"9p\",\n\t\tCACerts: CACerts{\n\t\t\tRemoveDefaults: &defaultRemoveDefaults,\n\t\t},\n\t}\n\tlayout, err := ExecuteTemplateCIDataISO(args)\n\tassert.NilError(t, err)\n\tfor _, f := range layout {\n\t\tt.Logf(\"=== %q ===\", f.Path)\n\t\tb, err := io.ReadAll(f.Reader)\n\t\tassert.NilError(t, err)\n\t\tt.Log(string(b))\n\t\tif f.Path == \"user-data\" {\n\t\t\t// mounted at boot\n\t\t\tassert.Assert(t, strings.Contains(string(b), \"mounts:\"))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/copytool/copytool.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage copytool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\ntype Backend string\n\nconst (\n\tBackendAuto  Backend = \"auto\"\n\tBackendRsync Backend = \"rsync\"\n\tBackendSCP   Backend = \"scp\"\n)\n\ntype Path struct {\n\tInstanceName string\n\tPath         string\n\tIsRemote     bool\n\tInstance     *limatype.Instance\n}\n\n// Options contains common options for copy operations. This might not be a complete list; more options can be added as needed.\ntype Options struct {\n\tRecursive      bool\n\tVerbose        bool\n\tAdditionalArgs []string // Make sure that the additional args are valid for a specific tool and escaped before passing them here.\n}\n\n// CopyTool is the interface for copy tool implementations.\ntype CopyTool interface {\n\t// Name returns the name of the copy tool.\n\tName() string\n\t// Command builds and returns the exec.Cmd for the copy operation. If opts is set, it is used for this invocation instead of the tool's Options set during initialization, without modifying the stored Options.\n\tCommand(ctx context.Context, paths []string, opts *Options) (*exec.Cmd, error)\n\t// IsAvailableOnGuest checks if the tool is available on the specified guest instance.\n\tIsAvailableOnGuest(ctx context.Context, paths []string) bool\n}\n\n// New creates a new CopyTool based on the specified backend.\nfunc New(ctx context.Context, backend string, paths []string, opts *Options) (CopyTool, error) {\n\tswitch Backend(backend) {\n\tcase BackendSCP:\n\t\treturn newSCPTool(opts)\n\tcase BackendRsync:\n\t\trsync, err := newRsyncTool(opts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif !rsync.IsAvailableOnGuest(ctx, paths) {\n\t\t\treturn nil, errors.New(\"rsync not available on guest(s)\")\n\t\t}\n\t\treturn rsync, nil\n\tcase BackendAuto:\n\t\tvar (\n\t\t\ttool CopyTool\n\t\t\terr  error\n\t\t)\n\t\ttool, err = newRsyncTool(opts)\n\t\tif err == nil {\n\t\t\tif tool.IsAvailableOnGuest(ctx, paths) {\n\t\t\t\treturn tool, nil\n\t\t\t}\n\t\t\tlogrus.Debugf(\"rsync not available on guest(s), falling back to scp\")\n\t\t} else {\n\t\t\tlogrus.Debugf(\"rsync not found on host, falling back to scp: %v\", err)\n\t\t}\n\n\t\ttool, err = newSCPTool(opts)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"neither rsync nor scp found on host: %w\", err)\n\t\t}\n\t\treturn tool, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid backend %q, must be one of: scp, rsync, auto\", backend)\n\t}\n}\n\nfunc parseCopyPaths(ctx context.Context, paths []string) ([]*Path, error) {\n\tvar copyPaths []*Path\n\n\tfor _, path := range paths {\n\t\tcp := &Path{}\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tif filepath.IsAbs(path) {\n\t\t\t\tvar err error\n\t\t\t\tpath, err = ioutilx.WindowsSubsystemPath(ctx, path)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tpath = filepath.ToSlash(path)\n\t\t\t}\n\t\t}\n\n\t\tparts := strings.SplitN(path, \":\", 2)\n\t\tswitch len(parts) {\n\t\tcase 1:\n\t\t\tcp.Path = path\n\t\t\tcp.IsRemote = false\n\t\tcase 2:\n\t\t\tcp.InstanceName = parts[0]\n\t\t\tcp.Path = parts[1]\n\t\t\tcp.IsRemote = true\n\n\t\t\tinst, err := store.Inspect(ctx, cp.InstanceName)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\t\treturn nil, fmt.Errorf(\"instance %q does not exist, run `limactl create %s` to create a new instance\", cp.InstanceName, cp.InstanceName)\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif inst.Status == limatype.StatusStopped {\n\t\t\t\treturn nil, fmt.Errorf(\"instance %q is stopped, run `limactl start %s` to start the instance\", cp.InstanceName, cp.InstanceName)\n\t\t\t}\n\t\t\tcp.Instance = inst\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"path %q contains multiple colons\", path)\n\t\t}\n\n\t\tcopyPaths = append(copyPaths, cp)\n\t}\n\n\treturn copyPaths, nil\n}\n"
  },
  {
    "path": "pkg/copytool/copytool_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage copytool\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\n// TestCommandDoesNotMutateOptions verifies that passing opts to Command() does not\n// overwrite the tool's stored Options for subsequent calls.\nfunc TestCommandDoesNotMutateOptions(t *testing.T) {\n\tinitial := &Options{Verbose: false, Recursive: false}\n\ttool, err := newRsyncTool(initial)\n\tif err != nil {\n\t\tt.Skip(\"rsync not found:\", err)\n\t}\n\n\toverride := &Options{Verbose: true, Recursive: true}\n\t// Use local paths to avoid instance lookup\n\t_, _ = tool.Command(t.Context(), []string{\"/tmp/src\", \"/tmp/dst\"}, override)\n\n\tassert.Equal(t, tool.Options.Verbose, false, \"Command() must not mutate stored Options.Verbose\")\n\tassert.Equal(t, tool.Options.Recursive, false, \"Command() must not mutate stored Options.Recursive\")\n}\n"
  },
  {
    "path": "pkg/copytool/rsync.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage copytool\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"al.essio.dev/pkg/shellescape\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n)\n\ntype rsyncTool struct {\n\ttoolPath string\n\tOptions  *Options\n}\n\nfunc newRsyncTool(opts *Options) (*rsyncTool, error) {\n\ttoolPath, err := exec.LookPath(\"rsync\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"rsync not found on host: %w\", err)\n\t}\n\treturn &rsyncTool{toolPath: toolPath, Options: opts}, nil\n}\n\nfunc (t *rsyncTool) Name() string {\n\treturn t.toolPath\n}\n\nfunc (t *rsyncTool) IsAvailableOnGuest(ctx context.Context, paths []string) bool {\n\tcopyPaths, err := parseCopyPaths(ctx, paths)\n\tif err != nil {\n\t\tlogrus.Debugf(\"failed to parse copy paths for rsync availability check: %v\", err)\n\t\treturn false\n\t}\n\tinstances := make(map[string]*limatype.Instance)\n\n\tfor _, cp := range copyPaths {\n\t\tif cp.IsRemote {\n\t\t\tinstances[cp.InstanceName] = cp.Instance\n\t\t}\n\t}\n\n\tfor instName, inst := range instances {\n\t\tif !checkRsyncOnGuest(ctx, inst) {\n\t\t\tlogrus.Debugf(\"rsync not available on instance %q\", instName)\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc checkRsyncOnGuest(ctx context.Context, inst *limatype.Instance) bool {\n\tsshExe, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\tlogrus.Debugf(\"failed to create SSH executable: %v\", err)\n\t\treturn false\n\t}\n\tsshOpts, err := sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)\n\tif err != nil {\n\t\tlogrus.Debugf(\"failed to get SSH options for rsync check: %v\", err)\n\t\treturn false\n\t}\n\n\tsshArgs := append([]string{}, sshExe.Args...)\n\tsshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)\n\tcheckCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...)\n\tcheckCmd.Args = append(checkCmd.Args,\n\t\t\"-p\", fmt.Sprintf(\"%d\", inst.SSHLocalPort),\n\t\t*inst.Config.User.Name+\"@\"+inst.SSHAddress,\n\t\t\"command -v rsync >/dev/null 2>&1\",\n\t)\n\n\terr = checkCmd.Run()\n\treturn err == nil\n}\n\nfunc (t *rsyncTool) Command(ctx context.Context, paths []string, opts *Options) (*exec.Cmd, error) {\n\tcopyPaths, err := parseCopyPaths(ctx, paths)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\teffectiveOpts := t.Options\n\tif opts != nil {\n\t\teffectiveOpts = opts\n\t}\n\n\trsyncFlags := []string{\"-a\"}\n\n\tif effectiveOpts.Verbose {\n\t\trsyncFlags = append(rsyncFlags, \"-v\", \"--progress\")\n\t} else {\n\t\trsyncFlags = append(rsyncFlags, \"-q\")\n\t}\n\n\tif effectiveOpts.Recursive {\n\t\trsyncFlags = append(rsyncFlags, \"-r\")\n\t}\n\n\tif effectiveOpts.AdditionalArgs != nil {\n\t\trsyncFlags = append(rsyncFlags, effectiveOpts.AdditionalArgs...)\n\t}\n\n\trsyncArgs := make([]string, 0, len(rsyncFlags)+len(copyPaths))\n\trsyncArgs = append(rsyncArgs, rsyncFlags...)\n\n\tvar sshCmd string\n\tvar remoteInstance *limatype.Instance\n\n\tfor _, cp := range copyPaths {\n\t\tif cp.IsRemote {\n\t\t\tif remoteInstance == nil {\n\t\t\t\tremoteInstance = cp.Instance\n\t\t\t\tsshExe, err := sshutil.NewSSHExe()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tsshOpts, err := sshutil.SSHOpts(ctx, sshExe, cp.Instance.Dir, *cp.Instance.Config.User.Name, false, false, false, false)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tsshArgs := []string{sshExe.Exe}\n\t\t\t\tsshArgs = append(sshArgs, sshExe.Args...)\n\t\t\t\tsshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)\n\t\t\t\tsshArgs = append(sshArgs, \"-p\", fmt.Sprintf(\"%d\", cp.Instance.SSHLocalPort))\n\n\t\t\t\tquotedSSHArgs := make([]string, len(sshArgs))\n\t\t\t\tfor i, arg := range sshArgs {\n\t\t\t\t\tquotedSSHArgs[i] = shellescape.Quote(arg)\n\t\t\t\t}\n\t\t\t\tsshCmd = strings.Join(quotedSSHArgs, \" \")\n\t\t\t}\n\t\t}\n\t}\n\n\tif sshCmd != \"\" {\n\t\trsyncArgs = append(rsyncArgs, \"-e\", sshCmd)\n\t}\n\n\t// Handle trailing slash for directory copies to keep consistent behavior with scp,\n\t// which was the original implementation of `limactl copy -r`.\n\t// https://github.com/lima-vm/lima/issues/4468\n\tif effectiveOpts.Recursive {\n\t\tfor i, cp := range copyPaths {\n\t\t\t//nolint:modernize // stringscutprefix: HasSuffix + TrimSuffix can be simplified to CutSuffix\n\t\t\tif strings.HasSuffix(cp.Path, \"/\") {\n\t\t\t\tif cp.IsRemote {\n\t\t\t\t\tfor j, cp2 := range copyPaths {\n\t\t\t\t\t\tif i != j {\n\t\t\t\t\t\t\tcp2.Path = strings.TrimSuffix(cp2.Path, \"/\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcp.Path = strings.TrimSuffix(cp.Path, \"/\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcp.Path += \"/\"\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, cp := range copyPaths {\n\t\tif cp.IsRemote {\n\t\t\trsyncArgs = append(rsyncArgs, fmt.Sprintf(\"%s:%s\", *cp.Instance.Config.User.Name+\"@\"+cp.Instance.SSHAddress, cp.Path))\n\t\t} else {\n\t\t\trsyncArgs = append(rsyncArgs, cp.Path)\n\t\t}\n\t}\n\n\treturn exec.CommandContext(ctx, t.toolPath, rsyncArgs...), nil\n}\n"
  },
  {
    "path": "pkg/copytool/scp.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage copytool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\n\t\"github.com/coreos/go-semver/semver\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n)\n\ntype scpTool struct {\n\ttoolPath string\n\tOptions  *Options\n}\n\nfunc newSCPTool(opts *Options) (*scpTool, error) {\n\tpath, err := exec.LookPath(\"scp\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"scp not found on host: %w\", err)\n\t}\n\treturn &scpTool{toolPath: path, Options: opts}, nil\n}\n\nfunc (t *scpTool) Name() string {\n\treturn t.toolPath\n}\n\nfunc (t *scpTool) IsAvailableOnGuest(_ context.Context, _ []string) bool {\n\t// scp is typically available on all systems with SSH\n\treturn true\n}\n\nfunc (t *scpTool) Command(ctx context.Context, paths []string, opts *Options) (*exec.Cmd, error) {\n\tcopyPaths, err := parseCopyPaths(ctx, paths)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\teffectiveOpts := t.Options\n\tif opts != nil {\n\t\teffectiveOpts = opts\n\t}\n\n\tinstances := make(map[string]*limatype.Instance)\n\tscpFlags := []string{}\n\tscpArgs := []string{}\n\n\tif effectiveOpts.Verbose {\n\t\tscpFlags = append(scpFlags, \"-v\")\n\t} else {\n\t\tscpFlags = append(scpFlags, \"-q\")\n\t}\n\n\tif effectiveOpts.Recursive {\n\t\tscpFlags = append(scpFlags, \"-r\")\n\t}\n\n\tif effectiveOpts.AdditionalArgs != nil {\n\t\tscpFlags = append(scpFlags, effectiveOpts.AdditionalArgs...)\n\t}\n\n\t// this assumes that ssh and scp come from the same place, but scp has no -V\n\tsshExeForVersion, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlegacySSH := sshutil.DetectOpenSSHVersion(ctx, sshExeForVersion).LessThan(*semver.New(\"8.0.0\"))\n\n\tfor _, cp := range copyPaths {\n\t\tif cp.IsRemote {\n\t\t\tif legacySSH {\n\t\t\t\tscpFlags = append(scpFlags, \"-P\", fmt.Sprintf(\"%d\", cp.Instance.SSHLocalPort))\n\t\t\t\tscpArgs = append(scpArgs, fmt.Sprintf(\"%s:%s\", *cp.Instance.Config.User.Name+\"@\"+cp.Instance.SSHAddress, cp.Path))\n\t\t\t} else {\n\t\t\t\tscpArgs = append(scpArgs, fmt.Sprintf(\"scp://%s:%d/%s\", *cp.Instance.Config.User.Name+\"@\"+cp.Instance.SSHAddress, cp.Instance.SSHLocalPort, cp.Path))\n\t\t\t}\n\t\t\tinstances[cp.InstanceName] = cp.Instance\n\t\t} else {\n\t\t\tscpArgs = append(scpArgs, cp.Path)\n\t\t}\n\t}\n\n\tif legacySSH && len(instances) > 1 {\n\t\treturn nil, errors.New(\"more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher\")\n\t}\n\n\tscpFlags = append(scpFlags, \"-3\", \"--\")\n\tscpArgs = append(scpFlags, scpArgs...)\n\n\tvar sshOpts []string\n\tif len(instances) == 1 {\n\t\t// Only one (instance) host is involved; we can use the instance-specific\n\t\t// arguments such as ControlPath.  This is preferred as we can multiplex\n\t\t// sessions without re-authenticating (MaxSessions permitting).\n\t\tfor _, inst := range instances {\n\t\t\tsshExe, err := sshutil.NewSSHExe()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tsshOpts, err = sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Copying among multiple hosts; we can't pass in host-specific options.\n\t\tsshExe, err := sshutil.NewSSHExe()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tsshOpts, err = sshutil.CommonOpts(ctx, sshExe, false)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tsshArgs := sshutil.SSHArgsFromOpts(sshOpts)\n\n\treturn exec.CommandContext(ctx, t.toolPath, append(sshArgs, scpArgs...)...), nil\n}\n"
  },
  {
    "path": "pkg/debugutil/debugutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage debugutil\n\n// Debug is set via main.\nvar Debug bool\n"
  },
  {
    "path": "pkg/downloader/downloader.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage downloader\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/cheggaaa/pb/v3\"\n\t\"github.com/containerd/continuity/fs\"\n\t\"github.com/opencontainers/go-digest\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/httpclientutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/localpathutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/lockutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/progressbar\"\n)\n\n// HideProgress is used only for testing.\nvar HideProgress bool\n\n// hideBar is used only for testing.\nfunc hideBar(bar *progressbar.ProgressBar) {\n\tbar.Set(pb.Static, true)\n}\n\ntype Status = string\n\nconst (\n\tStatusUnknown    Status = \"\"\n\tStatusDownloaded Status = \"downloaded\"\n\tStatusSkipped    Status = \"skipped\"\n\tStatusUsedCache  Status = \"used-cache\"\n)\n\ntype Result struct {\n\tStatus          Status\n\tCachePath       string // \"/Users/foo/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>/data\"\n\tLastModified    time.Time\n\tContentType     string\n\tValidatedDigest bool\n}\n\ntype options struct {\n\tcacheDir       string // default: empty (disables caching)\n\tdecompress     bool   // default: false (keep compression)\n\tdescription    string // default: url\n\texpectedDigest digest.Digest\n}\n\nfunc (o *options) apply(opts []Opt) error {\n\tfor _, f := range opts {\n\t\tif err := f(o); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\ntype Opt func(*options) error\n\n// WithCache enables caching using filepath.Join(os.UserCacheDir(), \"lima\") as the cache dir.\nfunc WithCache() Opt {\n\treturn func(o *options) error {\n\t\tucd, err := os.UserCacheDir()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcacheDir := filepath.Join(ucd, \"lima\")\n\t\treturn WithCacheDir(cacheDir)(o)\n\t}\n}\n\n// WithCacheDir enables caching using the specified dir.\n// Empty value disables caching.\nfunc WithCacheDir(cacheDir string) Opt {\n\treturn func(o *options) error {\n\t\to.cacheDir = cacheDir\n\t\treturn nil\n\t}\n}\n\n// WithDescription adds a user description of the download.\nfunc WithDescription(description string) Opt {\n\treturn func(o *options) error {\n\t\to.description = description\n\t\treturn nil\n\t}\n}\n\n// WithDecompress decompress the download from the cache.\nfunc WithDecompress(decompress bool) Opt {\n\treturn func(o *options) error {\n\t\to.decompress = decompress\n\t\treturn nil\n\t}\n}\n\n// WithExpectedDigest is used to validate the downloaded file against the expected digest.\n//\n// The digest is not verified in the following cases:\n//   - The digest was not specified.\n//   - The file already exists in the local target path.\n//\n// When the `data` file exists in the cache dir with `<ALGO>.digest` file,\n// the digest is verified by comparing the content of `<ALGO>.digest` with the expected\n// digest string. So, the actual digest of the `data` file is not computed.\nfunc WithExpectedDigest(expectedDigest digest.Digest) Opt {\n\treturn func(o *options) error {\n\t\tif expectedDigest != \"\" {\n\t\t\tif err := expectedDigest.Validate(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\to.expectedDigest = expectedDigest\n\t\treturn nil\n\t}\n}\n\nfunc readFile(path string) string {\n\tif path == \"\" {\n\t\treturn \"\"\n\t}\n\tif _, err := os.Stat(path); err != nil {\n\t\treturn \"\"\n\t}\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn string(b)\n}\n\nfunc readTime(path string) time.Time {\n\tif path == \"\" {\n\t\treturn time.Time{}\n\t}\n\tif _, err := os.Stat(path); err != nil {\n\t\treturn time.Time{}\n\t}\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn time.Time{}\n\t}\n\tt, err := time.Parse(http.TimeFormat, string(b))\n\tif err != nil {\n\t\treturn time.Time{}\n\t}\n\treturn t\n}\n\n// Download downloads the remote resource into the local path.\n//\n// Download caches the remote resource if WithCache or WithCacheDir option is specified.\n// Local files are not cached.\n//\n// When the local path already exists, Download returns Result with StatusSkipped.\n// (So, the local path cannot be set to /dev/null for \"caching only\" mode.)\n//\n// The local path can be an empty string for \"caching only\" mode.\nfunc Download(ctx context.Context, local, remote string, opts ...Opt) (*Result, error) {\n\tvar o options\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar localPath string\n\tif local == \"\" {\n\t\tif o.cacheDir == \"\" {\n\t\t\treturn nil, errors.New(\"caching-only mode requires the cache directory to be specified\")\n\t\t}\n\t} else {\n\t\tvar err error\n\t\tlocalPath, err = canonicalLocalPath(local)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif _, err := os.Stat(localPath); err == nil {\n\t\t\tlogrus.Debugf(\"file %q already exists, skipping downloading from %q (and skipping digest validation)\", localPath, remote)\n\t\t\tres := &Result{\n\t\t\t\tStatus:          StatusSkipped,\n\t\t\t\tValidatedDigest: false,\n\t\t\t}\n\t\t\treturn res, nil\n\t\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tlocalPathDir := filepath.Dir(localPath)\n\t\tif err := os.MkdirAll(localPathDir, 0o755); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\text := path.Ext(remote)\n\tif IsLocal(remote) {\n\t\tif err := copyLocal(ctx, localPath, remote, ext, o.decompress, o.description, o.expectedDigest); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres := &Result{\n\t\t\tStatus:          StatusDownloaded,\n\t\t\tValidatedDigest: o.expectedDigest != \"\",\n\t\t}\n\t\treturn res, nil\n\t}\n\n\tif o.cacheDir == \"\" {\n\t\tif err := downloadHTTP(ctx, localPath, \"\", \"\", remote, o.description, o.expectedDigest); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tres := &Result{\n\t\t\tStatus:          StatusDownloaded,\n\t\t\tValidatedDigest: o.expectedDigest != \"\",\n\t\t}\n\t\treturn res, nil\n\t}\n\n\tshad := cacheDirectoryPath(o.cacheDir, remote)\n\tif err := os.MkdirAll(shad, 0o700); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar res *Result\n\terr := lockutil.WithDirLock(shad, func() error {\n\t\tvar err error\n\t\tres, err = getCached(ctx, localPath, remote, o)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif res != nil {\n\t\t\treturn nil\n\t\t}\n\t\tres, err = fetch(ctx, localPath, remote, o)\n\t\treturn err\n\t})\n\treturn res, err\n}\n\n// getCached tries to copy the file from the cache to local path. Return result,\n// nil if the file was copied, nil, nil if the file is not in the cache or the\n// cache needs update, or nil, error on fatal error.\nfunc getCached(ctx context.Context, localPath, remote string, o options) (*Result, error) {\n\tshad := cacheDirectoryPath(o.cacheDir, remote)\n\tshadData := filepath.Join(shad, \"data\")\n\tshadTime := filepath.Join(shad, \"time\")\n\tshadType := filepath.Join(shad, \"type\")\n\tshadDigest, err := cacheDigestPath(shad, o.expectedDigest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif _, err := os.Stat(shadData); err != nil {\n\t\treturn nil, nil\n\t}\n\text := path.Ext(remote)\n\tlogrus.Debugf(\"file %q is cached as %q\", localPath, shadData)\n\tif _, err := os.Stat(shadDigest); err == nil {\n\t\tlogrus.Debugf(\"Comparing digest %q with the cached digest file %q, not computing the actual digest of %q\",\n\t\t\to.expectedDigest, shadDigest, shadData)\n\t\tif err := validateCachedDigest(shadDigest, o.expectedDigest); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := copyLocal(ctx, localPath, shadData, ext, o.decompress, \"\", \"\"); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif match, lmCached, lmRemote, err := matchLastModified(ctx, shadTime, remote); err != nil {\n\t\t\tlogrus.WithError(err).Info(\"Failed to retrieve last-modified for cached digest-less image; using cached image.\")\n\t\t} else if match {\n\t\t\tif err := copyLocal(ctx, localPath, shadData, ext, o.decompress, o.description, o.expectedDigest); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\tlogrus.Infof(\"Re-downloading digest-less image: last-modified mismatch (cached: %q, remote: %q)\", lmCached, lmRemote)\n\t\t\treturn nil, nil\n\t\t}\n\t}\n\tres := &Result{\n\t\tStatus:          StatusUsedCache,\n\t\tCachePath:       shadData,\n\t\tLastModified:    readTime(shadTime),\n\t\tContentType:     readFile(shadType),\n\t\tValidatedDigest: o.expectedDigest != \"\",\n\t}\n\treturn res, nil\n}\n\n// fetch downloads remote to the cache and copy the cached file to local path.\nfunc fetch(ctx context.Context, localPath, remote string, o options) (*Result, error) {\n\tshad := cacheDirectoryPath(o.cacheDir, remote)\n\tshadData := filepath.Join(shad, \"data\")\n\tshadTime := filepath.Join(shad, \"time\")\n\tshadType := filepath.Join(shad, \"type\")\n\tshadDigest, err := cacheDigestPath(shad, o.expectedDigest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\text := path.Ext(remote)\n\tshadURL := filepath.Join(shad, \"url\")\n\tif err := os.WriteFile(shadURL, []byte(remote), 0o644); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := downloadHTTP(ctx, shadData, shadTime, shadType, remote, o.description, o.expectedDigest); err != nil {\n\t\treturn nil, err\n\t}\n\tif shadDigest != \"\" && o.expectedDigest != \"\" {\n\t\tif err := os.WriteFile(shadDigest, []byte(o.expectedDigest.String()), 0o644); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// no need to pass the digest to copyLocal(), as we already verified the digest\n\tif err := copyLocal(ctx, localPath, shadData, ext, o.decompress, \"\", \"\"); err != nil {\n\t\treturn nil, err\n\t}\n\tres := &Result{\n\t\tStatus:          StatusDownloaded,\n\t\tCachePath:       shadData,\n\t\tLastModified:    readTime(shadTime),\n\t\tContentType:     readFile(shadType),\n\t\tValidatedDigest: o.expectedDigest != \"\",\n\t}\n\treturn res, nil\n}\n\n// Cached checks if the remote resource is in the cache.\n//\n// Download caches the remote resource if WithCache or WithCacheDir option is specified.\n// Local files are not cached.\n//\n// When the cache path already exists, Cached returns Result with StatusUsedCache.\nfunc Cached(remote string, opts ...Opt) (*Result, error) {\n\tvar o options\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\tif o.cacheDir == \"\" {\n\t\treturn nil, errors.New(\"caching-only mode requires the cache directory to be specified\")\n\t}\n\tif IsLocal(remote) {\n\t\treturn nil, errors.New(\"local files are not cached\")\n\t}\n\n\tshad := cacheDirectoryPath(o.cacheDir, remote)\n\tshadData := filepath.Join(shad, \"data\")\n\tshadTime := filepath.Join(shad, \"time\")\n\tshadType := filepath.Join(shad, \"type\")\n\tshadDigest, err := cacheDigestPath(shad, o.expectedDigest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Checking if data file exists is safe without locking.\n\tif _, err := os.Stat(shadData); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// But validating the digest or the data file must take the lock to avoid races\n\t// with parallel downloads.\n\tif err := os.MkdirAll(shad, 0o700); err != nil {\n\t\treturn nil, err\n\t}\n\terr = lockutil.WithDirLock(shad, func() error {\n\t\tif _, err := os.Stat(shadDigest); err != nil {\n\t\t\tif err := validateCachedDigest(shadDigest, o.expectedDigest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif err := validateLocalFileDigest(shadData, o.expectedDigest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres := &Result{\n\t\tStatus:          StatusUsedCache,\n\t\tCachePath:       shadData,\n\t\tLastModified:    readTime(shadTime),\n\t\tContentType:     readFile(shadType),\n\t\tValidatedDigest: o.expectedDigest != \"\",\n\t}\n\treturn res, nil\n}\n\n// cacheDirectoryPath returns the cache subdirectory path.\n//   - \"url\" file contains the url\n//   - \"data\" file contains the data\n//   - \"time\" file contains the time (Last-Modified header)\n//   - \"type\" file contains the type (Content-Type header)\nfunc cacheDirectoryPath(cacheDir, remote string) string {\n\treturn filepath.Join(cacheDir, \"download\", \"by-url-sha256\", CacheKey(remote))\n}\n\n// cacheDigestPath returns the cache digest file path.\n//   - \"<ALGO>.digest\" contains the digest\nfunc cacheDigestPath(shad string, expectedDigest digest.Digest) (string, error) {\n\tshadDigest := \"\"\n\tif expectedDigest != \"\" {\n\t\talgo := expectedDigest.Algorithm().String()\n\t\tif strings.Contains(algo, \"/\") || strings.Contains(algo, \"\\\\\") {\n\t\t\treturn \"\", fmt.Errorf(\"invalid digest algorithm %q\", algo)\n\t\t}\n\t\tshadDigest = filepath.Join(shad, algo+\".digest\")\n\t}\n\treturn shadDigest, nil\n}\n\nfunc IsLocal(s string) bool {\n\treturn !strings.Contains(s, \"://\") || strings.HasPrefix(s, \"file://\")\n}\n\n// canonicalLocalPath canonicalizes the local path string.\n//   - Make sure the file has no scheme, or the `file://` scheme\n//   - If it has the `file://` scheme, strip the scheme and make sure the filename is absolute\n//   - Expand a leading `~`, or convert relative to absolute name\nfunc canonicalLocalPath(s string) (string, error) {\n\tif s == \"\" {\n\t\treturn \"\", errors.New(\"got empty path\")\n\t}\n\tif !IsLocal(s) {\n\t\treturn \"\", fmt.Errorf(\"got non-local path: %q\", s)\n\t}\n\tif res, ok := strings.CutPrefix(s, \"file://\"); ok {\n\t\tif !filepath.IsAbs(res) {\n\t\t\treturn \"\", fmt.Errorf(\"got non-absolute path %q\", res)\n\t\t}\n\t\treturn res, nil\n\t}\n\treturn localpathutil.Expand(s)\n}\n\nfunc copyLocal(ctx context.Context, dst, src, ext string, decompress bool, description string, expectedDigest digest.Digest) error {\n\tsrcPath, err := canonicalLocalPath(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif expectedDigest != \"\" {\n\t\tlogrus.Debugf(\"verifying digest of local file %q (%s)\", srcPath, expectedDigest)\n\t}\n\tif err := validateLocalFileDigest(srcPath, expectedDigest); err != nil {\n\t\treturn err\n\t}\n\n\tif dst == \"\" {\n\t\t// empty dst means caching-only mode\n\t\treturn nil\n\t}\n\tdstPath, err := canonicalLocalPath(dst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif decompress {\n\t\tcommand := decompressor(ext)\n\t\tif command != \"\" {\n\t\t\treturn decompressLocal(ctx, command, dstPath, srcPath, ext, description)\n\t\t}\n\t\tcommandByMagic := decompressorByMagic(srcPath)\n\t\tif commandByMagic != \"\" {\n\t\t\treturn decompressLocal(ctx, commandByMagic, dstPath, srcPath, ext, description)\n\t\t}\n\t}\n\t// TODO: progress bar for copy\n\treturn fs.CopyFile(dstPath, srcPath)\n}\n\nfunc decompressor(ext string) string {\n\tswitch ext {\n\tcase \".gz\":\n\t\treturn \"gzip\"\n\tcase \".bz2\":\n\t\treturn \"bzip2\"\n\tcase \".xz\":\n\t\treturn \"xz\"\n\tcase \".zst\":\n\t\treturn \"zstd\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc decompressorByMagic(file string) string {\n\tf, err := os.Open(file)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tdefer f.Close()\n\theader := make([]byte, 6)\n\tif _, err := f.Read(header); err != nil {\n\t\treturn \"\"\n\t}\n\tif _, err := f.Seek(0, io.SeekStart); err != nil {\n\t\treturn \"\"\n\t}\n\tif bytes.HasPrefix(header, []byte{0x1f, 0x8b}) {\n\t\treturn \"gzip\"\n\t}\n\tif bytes.HasPrefix(header, []byte{0x42, 0x5a}) {\n\t\treturn \"bzip2\"\n\t}\n\tif bytes.HasPrefix(header, []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00}) {\n\t\treturn \"xz\"\n\t}\n\tif bytes.HasPrefix(header, []byte{0x28, 0xb5, 0x2f, 0xfd}) {\n\t\treturn \"zstd\"\n\t}\n\treturn \"\"\n}\n\nfunc decompressLocal(ctx context.Context, decompressCmd, dst, src, ext, description string) error {\n\tlogrus.Infof(\"decompressing %s with %v\", ext, decompressCmd)\n\n\tst, err := os.Stat(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbar, err := progressbar.New(st.Size())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif HideProgress {\n\t\thideBar(bar)\n\t}\n\n\tin, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer in.Close()\n\tout, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o644)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer out.Close()\n\tbuf := new(bytes.Buffer)\n\tcmd := exec.CommandContext(ctx, decompressCmd, \"-d\") // -d --decompress\n\tcmd.Stdin = bar.NewProxyReader(in)\n\tcmd.Stdout = out\n\tcmd.Stderr = buf\n\tif !HideProgress {\n\t\tif description == \"\" {\n\t\t\tdescription = filepath.Base(src)\n\t\t}\n\t\tlogrus.Infof(\"Decompressing %s\\n\", description)\n\t}\n\tbar.Start()\n\terr = cmd.Run()\n\tif err != nil {\n\t\tvar exitErr *exec.ExitError\n\t\tif errors.As(err, &exitErr) {\n\t\t\texitErr.Stderr = buf.Bytes()\n\t\t}\n\t}\n\tbar.Finish()\n\treturn err\n}\n\nfunc validateCachedDigest(shadDigest string, expectedDigest digest.Digest) error {\n\tif expectedDigest == \"\" {\n\t\treturn nil\n\t}\n\tshadDigestB, err := os.ReadFile(shadDigest)\n\tif err != nil {\n\t\treturn err\n\t}\n\tshadDigestS := strings.TrimSpace(string(shadDigestB))\n\tif shadDigestS != expectedDigest.String() {\n\t\treturn fmt.Errorf(\"expected digest %q, got %q\", expectedDigest, shadDigestS)\n\t}\n\treturn nil\n}\n\nfunc validateLocalFileDigest(localPath string, expectedDigest digest.Digest) error {\n\tif localPath == \"\" {\n\t\treturn errors.New(\"validateLocalFileDigest: got empty localPath\")\n\t}\n\tif expectedDigest == \"\" {\n\t\treturn nil\n\t}\n\talgo := expectedDigest.Algorithm()\n\tif !algo.Available() {\n\t\treturn fmt.Errorf(\"expected digest algorithm %q is not available\", algo)\n\t}\n\tr, err := os.Open(localPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer r.Close()\n\tactualDigest, err := algo.FromReader(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif actualDigest != expectedDigest {\n\t\treturn fmt.Errorf(\"expected digest %q, got %q\", expectedDigest, actualDigest)\n\t}\n\treturn nil\n}\n\n// mathLastModified takes params:\n//   - ctx: context for calling httpclientutil.Head\n//   - lastModifiedPath: path of the cached last-modified time file\n//   - url: URL to fetch the last-modified time\n//\n// returns:\n//   - matched: whether the last-modified time matches\n//   - lmCached: last-modified time string from the lastModifiedPath\n//   - lmRemote: last-modified time string from the URL\n//   - err: error if fetching the last-modified time from the URL fails\nfunc matchLastModified(ctx context.Context, lastModifiedPath, url string) (matched bool, lmCached, lmRemote string, err error) {\n\tlmCached = readFile(lastModifiedPath)\n\tif lmCached == \"\" {\n\t\treturn false, \"<not cached>\", \"<not checked>\", nil\n\t}\n\tresp, err := httpclientutil.Head(ctx, http.DefaultClient, url)\n\tif err != nil {\n\t\treturn false, lmCached, \"<failed to fetch remote>\", err\n\t}\n\tdefer resp.Body.Close()\n\tlmRemote = resp.Header.Get(\"Last-Modified\")\n\tif lmRemote == \"\" {\n\t\treturn false, lmCached, \"<missing Last-Modified header>\", nil\n\t}\n\tlmCachedTime, errParsingCachedTime := time.Parse(http.TimeFormat, lmCached)\n\tlmRemoteTime, errParsingRemoteTime := time.Parse(http.TimeFormat, lmRemote)\n\tif errParsingCachedTime != nil && errParsingRemoteTime != nil {\n\t\t// both time strings are failed to parse, so compare them as strings\n\t\treturn lmCached == lmRemote, lmCached, lmRemote, nil\n\t} else if errParsingCachedTime == nil && errParsingRemoteTime == nil {\n\t\t// both time strings are successfully parsed, so compare them as times\n\t\treturn lmRemoteTime.Equal(lmCachedTime), lmCached, lmRemote, nil\n\t}\n\t// ignore parsing errors for either time string and assume they are different\n\treturn false, lmCached, lmRemote, nil\n}\n\nfunc downloadHTTP(ctx context.Context, localPath, lastModified, contentType, url, description string, expectedDigest digest.Digest) error {\n\tif localPath == \"\" {\n\t\treturn errors.New(\"downloadHTTP: got empty localPath\")\n\t}\n\tlogrus.Debugf(\"downloading %q into %q\", url, localPath)\n\n\tresp, err := httpclientutil.Get(ctx, http.DefaultClient, url)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif lastModified != \"\" {\n\t\tlm := resp.Header.Get(\"Last-Modified\")\n\t\tif err := os.WriteFile(lastModified, []byte(lm), 0o644); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif contentType != \"\" {\n\t\tct := resp.Header.Get(\"Content-Type\")\n\t\tif err := os.WriteFile(contentType, []byte(ct), 0o644); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tdefer resp.Body.Close()\n\tbar, err := progressbar.New(resp.ContentLength)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif HideProgress {\n\t\thideBar(bar)\n\t}\n\n\tlocalPathTmp := perProcessTempfile(localPath)\n\tfileWriter, err := os.Create(localPathTmp)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fileWriter.Close()\n\tdefer os.RemoveAll(localPathTmp)\n\n\twriters := []io.Writer{fileWriter}\n\tvar digester digest.Digester\n\tif expectedDigest != \"\" {\n\t\talgo := expectedDigest.Algorithm()\n\t\tif !algo.Available() {\n\t\t\treturn fmt.Errorf(\"unsupported digest algorithm %q\", algo)\n\t\t}\n\t\tdigester = algo.Digester()\n\t\thasher := digester.Hash()\n\t\twriters = append(writers, hasher)\n\t}\n\tmultiWriter := io.MultiWriter(writers...)\n\n\tif !HideProgress {\n\t\tif description == \"\" {\n\t\t\tdescription = url\n\t\t}\n\t\t// stderr corresponds to the progress bar output\n\t\tfmt.Fprintf(os.Stderr, \"Downloading %s\\n\", description)\n\t}\n\tbar.Start()\n\tif _, err := io.Copy(multiWriter, bar.NewProxyReader(resp.Body)); err != nil {\n\t\treturn err\n\t}\n\tbar.Finish()\n\n\tif digester != nil {\n\t\tactualDigest := digester.Digest()\n\t\tif actualDigest != expectedDigest {\n\t\t\treturn fmt.Errorf(\"expected digest %q, got %q\", expectedDigest, actualDigest)\n\t\t}\n\t}\n\n\tif err := fileWriter.Sync(); err != nil {\n\t\treturn err\n\t}\n\tif err := fileWriter.Close(); err != nil {\n\t\treturn err\n\t}\n\n\treturn os.Rename(localPathTmp, localPath)\n}\n\nvar tempfileCount atomic.Uint64\n\n// To allow parallel download we use a per-process unique suffix for temporary\n// files. Renaming the temporary file to the final file is safe without\n// synchronization on posix.\n// To make it easy to test we also include a counter ensuring that each\n// temporary file is unique in the same process.\n// https://github.com/lima-vm/lima/issues/2722\nfunc perProcessTempfile(path string) string {\n\treturn fmt.Sprintf(\"%s.tmp.%d.%d\", path, os.Getpid(), tempfileCount.Add(1))\n}\n\n// CacheEntries returns a map of cache entries.\n// The key is the SHA256 of the URL.\n// The value is the path to the cache entry.\nfunc CacheEntries(opts ...Opt) (map[string]string, error) {\n\tentries := make(map[string]string)\n\tvar o options\n\tif err := o.apply(opts); err != nil {\n\t\treturn nil, err\n\t}\n\tif o.cacheDir == \"\" {\n\t\treturn entries, nil\n\t}\n\tdownloadDir := filepath.Join(o.cacheDir, \"download\", \"by-url-sha256\")\n\t_, err := os.Stat(downloadDir)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn entries, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tcacheEntries, err := os.ReadDir(downloadDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, entry := range cacheEntries {\n\t\tentries[entry.Name()] = filepath.Join(downloadDir, entry.Name())\n\t}\n\treturn entries, nil\n}\n\n// CacheKey returns the key for a cache entry of the remote URL.\nfunc CacheKey(remote string) string {\n\treturn fmt.Sprintf(\"%x\", sha256.Sum256([]byte(remote)))\n}\n\n// RemoveAllCacheDir removes the cache directory.\nfunc RemoveAllCacheDir(opts ...Opt) error {\n\tvar o options\n\tif err := o.apply(opts); err != nil {\n\t\treturn err\n\t}\n\tif o.cacheDir == \"\" {\n\t\treturn nil\n\t}\n\tlogrus.Infof(\"Pruning %q\", o.cacheDir)\n\treturn os.RemoveAll(o.cacheDir)\n}\n"
  },
  {
    "path": "pkg/downloader/downloader_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage downloader\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/opencontainers/go-digest\"\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestMain(m *testing.M) {\n\tHideProgress = true\n\tm.Run()\n}\n\ntype downloadResult struct {\n\tr   *Result\n\terr error\n}\n\n// We expect only few parallel downloads. Testing with larger number to find\n// races quicker. 20 parallel downloads take about 120 milliseconds on M1 Pro.\nconst parallelDownloads = 20\n\nfunc TestDownloadRemote(t *testing.T) {\n\tts := httptest.NewServer(http.FileServer(http.Dir(\"testdata\")))\n\tt.Cleanup(ts.Close)\n\tdummyRemoteFileURL := ts.URL + \"/downloader.txt\"\n\tconst dummyRemoteFileDigest = \"sha256:380481d26f897403368be7cb86ca03a4bc14b125bfaf2b93bff809a5a2ad717e\"\n\tdummyRemoteFileStat, err := os.Stat(filepath.Join(\"testdata\", \"downloader.txt\"))\n\tassert.NilError(t, err)\n\n\tt.Run(\"without cache\", func(t *testing.T) {\n\t\tt.Run(\"without digest\", func(t *testing.T) {\n\t\t\tctx := t.Context()\n\t\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\t\tr, err := Download(ctx, localPath, dummyRemoteFileURL)\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\t\t// download again, make sure StatusSkippedIsReturned\n\t\t\tr, err = Download(ctx, localPath, dummyRemoteFileURL)\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusSkipped, r.Status)\n\t\t})\n\t\tt.Run(\"with digest\", func(t *testing.T) {\n\t\t\tctx := t.Context()\n\t\t\twrongDigest := digest.Digest(\"sha256:8313944efb4f38570c689813f288058b674ea6c487017a5a4738dc674b65f9d9\")\n\t\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\t\t_, err := Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(wrongDigest))\n\t\t\tassert.ErrorContains(t, err, \"expected digest\")\n\n\t\t\twrongDigest2 := digest.Digest(\"8313944efb4f38570c689813f288058b674ea6c487017a5a4738dc674b65f9d9\")\n\t\t\t_, err = Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(wrongDigest2))\n\t\t\tassert.ErrorContains(t, err, \"invalid checksum digest format\")\n\n\t\t\tr, err := Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\t\tr, err = Download(ctx, localPath, dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusSkipped, r.Status)\n\t\t})\n\t})\n\tt.Run(\"with cache\", func(t *testing.T) {\n\t\tt.Run(\"serial\", func(t *testing.T) {\n\t\t\tctx := t.Context()\n\t\t\tcacheDir := filepath.Join(t.TempDir(), \"cache\")\n\t\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\t\tr, err := Download(ctx, localPath, dummyRemoteFileURL,\n\t\t\t\tWithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\t\tr, err = Download(ctx, localPath, dummyRemoteFileURL,\n\t\t\t\tWithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusSkipped, r.Status)\n\n\t\t\tlocalPath2 := localPath + \"-2\"\n\t\t\tr, err = Download(ctx, localPath2, dummyRemoteFileURL,\n\t\t\t\tWithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\t\t})\n\t\tt.Run(\"parallel\", func(t *testing.T) {\n\t\t\tctx := t.Context()\n\t\t\tcacheDir := filepath.Join(t.TempDir(), \"cache\")\n\t\t\tresults := make(chan downloadResult, parallelDownloads)\n\t\t\tfor range parallelDownloads {\n\t\t\t\tgo func() {\n\t\t\t\t\t// Parallel download is supported only for different instances with unique localPath.\n\t\t\t\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\t\t\t\tr, err := Download(ctx, localPath, dummyRemoteFileURL,\n\t\t\t\t\t\tWithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\t\t\t\tresults <- downloadResult{r, err}\n\t\t\t\t}()\n\t\t\t}\n\t\t\t// Only one thread should download, the rest should use the cache.\n\t\t\tdownloaded, cached := countResults(t, results)\n\t\t\tassert.Equal(t, downloaded, 1)\n\t\t\tassert.Equal(t, cached, parallelDownloads-1)\n\t\t})\n\t})\n\tt.Run(\"caching-only mode\", func(t *testing.T) {\n\t\tt.Run(\"serial\", func(t *testing.T) {\n\t\t\tctx := t.Context()\n\t\t\t_, err := Download(ctx, \"\", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))\n\t\t\tassert.ErrorContains(t, err, \"cache directory to be specified\")\n\n\t\t\tcacheDir := filepath.Join(t.TempDir(), \"cache\")\n\t\t\tr, err := Download(ctx, \"\", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest),\n\t\t\t\tWithCacheDir(cacheDir))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\t\tr, err = Download(ctx, \"\", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest),\n\t\t\t\tWithCacheDir(cacheDir))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\n\t\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\t\tr, err = Download(ctx, localPath, dummyRemoteFileURL,\n\t\t\t\tWithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\t\t})\n\t\tt.Run(\"parallel\", func(t *testing.T) {\n\t\t\tctx := t.Context()\n\t\t\tcacheDir := filepath.Join(t.TempDir(), \"cache\")\n\t\t\tresults := make(chan downloadResult, parallelDownloads)\n\t\t\tfor range parallelDownloads {\n\t\t\t\tgo func() {\n\t\t\t\t\tr, err := Download(ctx, \"\", dummyRemoteFileURL,\n\t\t\t\t\t\tWithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\t\t\t\tresults <- downloadResult{r, err}\n\t\t\t\t}()\n\t\t\t}\n\t\t\t// Only one thread should download, the rest should use the cache.\n\t\t\tdownloaded, cached := countResults(t, results)\n\t\t\tassert.Equal(t, downloaded, 1)\n\t\t\tassert.Equal(t, cached, parallelDownloads-1)\n\t\t})\n\t})\n\tt.Run(\"cached\", func(t *testing.T) {\n\t\tctx := t.Context()\n\t\t_, err := Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))\n\t\tassert.ErrorContains(t, err, \"cache directory to be specified\")\n\n\t\tcacheDir := filepath.Join(t.TempDir(), \"cache\")\n\t\tr, err := Download(ctx, \"\", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\tr, err = Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\t\tassert.Assert(t, strings.HasPrefix(r.CachePath, cacheDir), \"expected %s to be in %s\", r.CachePath, cacheDir)\n\n\t\twrongDigest := digest.Digest(\"sha256:8313944efb4f38570c689813f288058b674ea6c487017a5a4738dc674b65f9d9\")\n\t\t_, err = Cached(dummyRemoteFileURL, WithExpectedDigest(wrongDigest), WithCacheDir(cacheDir))\n\t\tassert.ErrorContains(t, err, \"expected digest\")\n\t})\n\tt.Run(\"metadata\", func(t *testing.T) {\n\t\tctx := t.Context()\n\t\t_, err := Cached(dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest))\n\t\tassert.ErrorContains(t, err, \"cache directory to be specified\")\n\n\t\tcacheDir := filepath.Join(t.TempDir(), \"cache\")\n\t\tr, err := Download(ctx, \"\", dummyRemoteFileURL, WithExpectedDigest(dummyRemoteFileDigest), WithCacheDir(cacheDir))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\t\tassert.Equal(t, dummyRemoteFileStat.ModTime().Truncate(time.Second).UTC(), r.LastModified)\n\t\tassert.Equal(t, \"text/plain; charset=utf-8\", r.ContentType)\n\t})\n}\n\nfunc countResults(t *testing.T, results chan downloadResult) (downloaded, cached int) {\n\tt.Helper()\n\tfor range parallelDownloads {\n\t\tresult := <-results\n\t\tif result.err != nil {\n\t\t\tt.Errorf(\"Download failed: %s\", result.err)\n\t\t} else {\n\t\t\tswitch result.r.Status {\n\t\t\tcase StatusDownloaded:\n\t\t\t\tdownloaded++\n\t\t\tcase StatusUsedCache:\n\t\t\t\tcached++\n\t\t\tdefault:\n\t\t\t\tt.Errorf(\"Unexpected download status %q\", result.r.Status)\n\t\t\t}\n\t\t}\n\t}\n\treturn downloaded, cached\n}\n\nfunc TestRedownloadRemote(t *testing.T) {\n\tremoteDir := t.TempDir()\n\tts := httptest.NewServer(http.FileServer(http.Dir(remoteDir)))\n\tt.Cleanup(ts.Close)\n\n\tdownloadDir := t.TempDir()\n\n\tcacheOpt := WithCacheDir(t.TempDir())\n\n\tt.Run(\"digest-less\", func(t *testing.T) {\n\t\tctx := t.Context()\n\t\tremoteFile := filepath.Join(remoteDir, \"digest-less.txt\")\n\t\tassert.NilError(t, os.WriteFile(remoteFile, []byte(\"digest-less\"), 0o644))\n\t\tassert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now().Add(-time.Hour)))\n\t\topt := []Opt{cacheOpt}\n\n\t\t// Download on the first call\n\t\tr, err := Download(ctx, filepath.Join(downloadDir, \"1\"), ts.URL+\"/digest-less.txt\", opt...)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\t// Next download will use the cached download\n\t\tr, err = Download(ctx, filepath.Join(downloadDir, \"2\"), ts.URL+\"/digest-less.txt\", opt...)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\n\t\t// Modifying remote file will cause redownload\n\t\tassert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now()))\n\t\tr, err = Download(ctx, filepath.Join(downloadDir, \"3\"), ts.URL+\"/digest-less.txt\", opt...)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\t// Next download will use the cached download\n\t\tr, err = Download(ctx, filepath.Join(downloadDir, \"4\"), ts.URL+\"/digest-less.txt\", opt...)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\t})\n\n\tt.Run(\"has-digest\", func(t *testing.T) {\n\t\tctx := t.Context()\n\t\tremoteFile := filepath.Join(remoteDir, \"has-digest.txt\")\n\t\tbytes := []byte(\"has-digest\")\n\t\tassert.NilError(t, os.WriteFile(remoteFile, bytes, 0o644))\n\t\tassert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now().Add(-time.Hour)))\n\n\t\tdigester := digest.SHA256.Digester()\n\t\t_, err := digester.Hash().Write(bytes)\n\t\tassert.NilError(t, err)\n\t\topt := []Opt{cacheOpt, WithExpectedDigest(digester.Digest())}\n\n\t\tr, err := Download(ctx, filepath.Join(downloadDir, \"has-digest1.txt\"), ts.URL+\"/has-digest.txt\", opt...)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\t\tr, err = Download(ctx, filepath.Join(downloadDir, \"has-digest2.txt\"), ts.URL+\"/has-digest.txt\", opt...)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\n\t\t// modifying remote file won't cause redownload because expected digest is provided\n\t\tassert.NilError(t, os.Chtimes(remoteFile, time.Now(), time.Now()))\n\t\tr, err = Download(ctx, filepath.Join(downloadDir, \"has-digest3.txt\"), ts.URL+\"/has-digest.txt\", opt...)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusUsedCache, r.Status)\n\t})\n}\n\nfunc TestDownloadLocal(t *testing.T) {\n\tconst emptyFileDigest = \"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"\n\tconst testDownloadLocalDigest = \"sha256:0c1e0fba69e8919b306d030bf491e3e0c46cf0a8140ff5d7516ba3a83cbea5b3\"\n\n\tt.Run(\"without digest\", func(t *testing.T) {\n\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\tlocalFile := filepath.Join(t.TempDir(), \"test-file\")\n\t\tf, err := os.Create(localFile)\n\t\tassert.NilError(t, err)\n\t\tt.Cleanup(func() { _ = f.Close() })\n\t\ttestLocalFileURL := \"file://\" + localFile\n\n\t\tr, err := Download(t.Context(), localPath, testLocalFileURL)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\t})\n\n\tt.Run(\"with file digest\", func(t *testing.T) {\n\t\tctx := t.Context()\n\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\tlocalTestFile := filepath.Join(t.TempDir(), \"some-file\")\n\t\ttestDownloadFileContents := []byte(\"TestDownloadLocal\")\n\n\t\tassert.NilError(t, os.WriteFile(localTestFile, testDownloadFileContents, 0o644))\n\t\ttestLocalFileURL := \"file://\" + localTestFile\n\t\twrongDigest := digest.Digest(emptyFileDigest)\n\n\t\t_, err := Download(ctx, localPath, testLocalFileURL, WithExpectedDigest(wrongDigest))\n\t\tassert.ErrorContains(t, err, \"expected digest\")\n\n\t\tr, err := Download(ctx, localPath, testLocalFileURL, WithExpectedDigest(testDownloadLocalDigest))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\t})\n\n\tt.Run(\"cached\", func(t *testing.T) {\n\t\tlocalFile := filepath.Join(t.TempDir(), \"test-file\")\n\t\tf, err := os.Create(localFile)\n\t\tassert.NilError(t, err)\n\t\tt.Cleanup(func() { _ = f.Close() })\n\t\ttestLocalFileURL := \"file://\" + localFile\n\n\t\tcacheDir := filepath.Join(t.TempDir(), \"cache\")\n\t\t_, err = Cached(testLocalFileURL, WithCacheDir(cacheDir))\n\t\tassert.ErrorContains(t, err, \"not cached\")\n\t})\n}\n\nfunc TestDownloadCompressed(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\t// FIXME: `assertion failed: error is not nil: exec: \"gzip\": executable file not found in %PATH%`\n\t\tt.Skip(\"Skipping on windows\")\n\t}\n\n\tt.Run(\"gzip\", func(t *testing.T) {\n\t\tctx := t.Context()\n\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\tlocalFile := filepath.Join(t.TempDir(), \"test-file\")\n\t\ttestDownloadCompressedContents := []byte(\"TestDownloadCompressed\")\n\t\tassert.NilError(t, os.WriteFile(localFile, testDownloadCompressedContents, 0o644))\n\t\tassert.NilError(t, exec.CommandContext(ctx, \"gzip\", localFile).Run())\n\t\tlocalFile += \".gz\"\n\t\ttestLocalFileURL := \"file://\" + localFile\n\n\t\tr, err := Download(ctx, localPath, testLocalFileURL, WithDecompress(true))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\tgot, err := os.ReadFile(localPath)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, string(got), string(testDownloadCompressedContents))\n\t})\n\n\tt.Run(\"bzip2\", func(t *testing.T) {\n\t\tctx := t.Context()\n\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\tlocalFile := filepath.Join(t.TempDir(), \"test-file\")\n\t\ttestDownloadCompressedContents := []byte(\"TestDownloadCompressed\")\n\t\tassert.NilError(t, os.WriteFile(localFile, testDownloadCompressedContents, 0o644))\n\t\tassert.NilError(t, exec.CommandContext(ctx, \"bzip2\", localFile).Run())\n\t\tlocalFile += \".bz2\"\n\t\ttestLocalFileURL := \"file://\" + localFile\n\n\t\tr, err := Download(ctx, localPath, testLocalFileURL, WithDecompress(true))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\tgot, err := os.ReadFile(localPath)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, string(got), string(testDownloadCompressedContents))\n\t})\n\n\tt.Run(\"unknown decompressor\", func(t *testing.T) {\n\t\tlocalPath := filepath.Join(t.TempDir(), t.Name())\n\t\tlocalFile := filepath.Join(t.TempDir(), \"test-file.rar\")\n\t\ttestDownloadCompressedContents := []byte(\"TestDownloadCompressed\")\n\t\tassert.NilError(t, os.WriteFile(localFile, testDownloadCompressedContents, 0o644))\n\t\ttestLocalFileURL := \"file://\" + localFile\n\n\t\tr, err := Download(t.Context(), localPath, testLocalFileURL, WithDecompress(true))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, StatusDownloaded, r.Status)\n\n\t\tgot, err := os.ReadFile(localPath)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, string(got), string(testDownloadCompressedContents))\n\t})\n}\n"
  },
  {
    "path": "pkg/downloader/fuzz_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage downloader\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/opencontainers/go-digest\"\n\t\"gotest.tools/v3/assert\"\n)\n\nvar algorithm = digest.Algorithm(\"sha256\")\n\nfunc FuzzDownload(f *testing.F) {\n\tf.Fuzz(func(t *testing.T, fileContents []byte, checkDigest bool) {\n\t\tlocalFile := filepath.Join(t.TempDir(), \"localFile\")\n\t\tremoteFile := filepath.Join(t.TempDir(), \"remoteFile\")\n\t\terr := os.WriteFile(remoteFile, fileContents, 0o600)\n\t\tassert.NilError(t, err)\n\t\ttestLocalFileURL := \"file://\" + remoteFile\n\t\tif checkDigest {\n\t\t\td := algorithm.FromBytes(fileContents)\n\t\t\t_, _ = Download(t.Context(), localFile, testLocalFileURL, WithExpectedDigest(d))\n\t\t} else {\n\t\t\t_, _ = Download(t.Context(), localFile, testLocalFileURL)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pkg/downloader/testdata/downloader.txt",
    "content": "This file is used for testing Download function.\n"
  },
  {
    "path": "pkg/driver/driver.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage driver\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n// VsockEventEmitter is an optional interface for drivers to emit vsock events.\ntype VsockEventEmitter interface {\n\tSetVsockEventCallback(callback func(*events.VsockEvent))\n}\n\n// Lifecycle defines basic lifecycle operations.\ntype Lifecycle interface {\n\t// Validate returns error if the current driver isn't support for given config\n\tValidate(_ context.Context) error\n\n\t// Create is called on creating the instance for the first time.\n\t// (e.g., creating \"vz-identifier\" file)\n\t//\n\t// Create MUST return nil when it is called against an existing instance.\n\t//\n\t// Create does not create the disks.\n\tCreate(_ context.Context) error\n\n\t// CreateDisk returns error if the current driver fails in creating disk\n\tCreateDisk(_ context.Context) error\n\n\t// Start is used for booting the vm using driver instance\n\t// It returns a chan error on successful boot\n\t// The second argument may contain error occurred while starting driver\n\tStart(_ context.Context) (chan error, error)\n\n\t// Stop will terminate the running vm instance.\n\t// It returns error if there are any errors during Stop\n\tStop(_ context.Context) error\n\n\tDelete(_ context.Context) error\n\n\tInspectStatus(_ context.Context, inst *limatype.Instance) string\n\n\t// BootScripts returns the content of boot scripts to be injected into the vm.\n\t// The key must be \"boot.<OS>/<SCRIPT>\" or \"<SCRIPT>\" (deprecated alias for \"boot.Linux/<SCRIPT>\").\n\tBootScripts() (map[string][]byte, error)\n}\n\n// GUI defines GUI-related operations.\ntype GUI interface {\n\t// RunGUI is for starting GUI synchronously by hostagent. This method should be wait and return only after vm terminates\n\t// It returns error if there are any failures\n\tRunGUI() error\n\n\tChangeDisplayPassword(ctx context.Context, password string) error\n\tDisplayConnection(ctx context.Context) (string, error)\n}\n\n// SnapshotManager defines operations for managing snapshots.\ntype SnapshotManager interface {\n\tCreateSnapshot(ctx context.Context, tag string) error\n\tApplySnapshot(ctx context.Context, tag string) error\n\tDeleteSnapshot(ctx context.Context, tag string) error\n\tListSnapshots(ctx context.Context) (string, error)\n}\n\n// GuestAgent defines operations for the guest agent.\ntype GuestAgent interface {\n\t// ForwardGuestAgent returns if the guest agent sock needs forwarding by host agent.\n\tForwardGuestAgent() bool\n\n\t// GuestAgentConn returns the guest agent connection, or nil (if forwarded by ssh).\n\tGuestAgentConn(_ context.Context) (net.Conn, string, error)\n}\n\n// Driver interface is used by hostagent for managing vm.\ntype Driver interface {\n\tLifecycle\n\tGUI\n\tSnapshotManager\n\tGuestAgent\n\n\tInfo() Info\n\n\t// Configure sets the configuration for the instance.\n\t// TODO: merge Configure and FillConfig?\n\t// Or come up with a better name to clarify the difference.\n\tConfigure(inst *limatype.Instance) *ConfiguredDriver\n\n\t// FillConfig fills and validates the configuration for the instance.\n\t// The config is not set to the instance.\n\tFillConfig(ctx context.Context, cfg *limatype.LimaYAML, filePath string) error\n\n\tSSHAddress(ctx context.Context) (string, error)\n\n\t// AdditionalSetupForSSH provides additional setup required for SSH connection.\n\t// It is called after VM is started, before first SSH connection.\n\t// It returns an error if the setup fails.\n\tAdditionalSetupForSSH(ctx context.Context) error\n}\n\ntype ConfiguredDriver struct {\n\tDriver\n}\n\ntype Info struct {\n\tName        string         `json:\"name\"`\n\tVsockPort   int            `json:\"vsockPort\"`\n\tVirtioPort  string         `json:\"virtioPort\"`\n\tInstanceDir string         `json:\"instanceDir,omitempty\"`\n\tFeatures    DriverFeatures `json:\"features\"`\n}\n\ntype DriverFeatures struct {\n\tCanRunGUI            bool `json:\"canRunGui,omitempty\"`\n\tDynamicSSHAddress    bool `json:\"dynamicSSHAddress\"`\n\tStaticSSHPort        bool `json:\"staticSSHPort\"`\n\tSkipSocketForwarding bool `json:\"skipSocketForwarding\"`\n\tNoCloudInit          bool `json:\"noCloudInit\"`\n\tRosettaEnabled       bool `json:\"rosettaEnabled\"`\n\tRosettaBinFmt        bool `json:\"rosettaBinFmt\"`\n}\n"
  },
  {
    "path": "pkg/driver/external/client/client.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n\n\tpb \"github.com/lima-vm/lima/v2/pkg/driver/external\"\n)\n\ntype DriverClient struct {\n\tsocketPath string\n\tConn       *grpc.ClientConn\n\tDriverSvc  pb.DriverClient\n\tlogger     *logrus.Logger\n}\n\nfunc NewDriverClient(socketPath string, logger *logrus.Logger) (*DriverClient, error) {\n\topts := []grpc.DialOption{\n\t\tgrpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(16 << 20)),\n\t\tgrpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(16 << 20)),\n\t\tgrpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {\n\t\t\tvar dialer net.Dialer\n\t\t\treturn dialer.DialContext(ctx, \"unix\", socketPath)\n\t\t}),\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t}\n\n\t//nolint:staticcheck // grpc.Dial is used for compatibility reasons\n\tconn, err := grpc.Dial(\"unix://\"+socketPath, opts...)\n\tif err != nil {\n\t\tlogger.Errorf(\"failed to dial gRPC driver client connection: %v\", err)\n\t\treturn nil, err\n\t}\n\n\tdriverSvc := pb.NewDriverClient(conn)\n\n\treturn &DriverClient{\n\t\tsocketPath: socketPath,\n\t\tConn:       conn,\n\t\tDriverSvc:  driverSvc,\n\t\tlogger:     logger,\n\t}, nil\n}\n"
  },
  {
    "path": "pkg/driver/external/client/methods.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net\"\n\t\"time\"\n\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\tpb \"github.com/lima-vm/lima/v2/pkg/driver/external\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nfunc (d *DriverClient) Validate(ctx context.Context) error {\n\td.logger.Debug(\"Validating driver for the given config\")\n\n\tctx, cancel := context.WithTimeout(ctx, 10*time.Second)\n\tdefer cancel()\n\t_, err := d.DriverSvc.Validate(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Validation failed\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"Driver validated successfully\")\n\treturn nil\n}\n\nfunc (d *DriverClient) Create(ctx context.Context) error {\n\td.logger.Debug(\"Initializing driver instance\")\n\n\t_, err := d.DriverSvc.Create(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Initialization failed\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"Driver instance initialized successfully\")\n\treturn nil\n}\n\nfunc (d *DriverClient) CreateDisk(ctx context.Context) error {\n\td.logger.Debug(\"Creating disk for the instance\")\n\n\t_, err := d.DriverSvc.CreateDisk(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Disk creation failed\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"Disk created successfully\")\n\treturn nil\n}\n\n// Start initiates the driver instance and receives streaming responses. It blocks until\n// receiving the initial success response, then spawns a goroutine to consume subsequent\n// error messages from the stream. Any errors from the driver are sent to the channel.\nfunc (d *DriverClient) Start(ctx context.Context) (chan error, error) {\n\td.logger.Debug(\"Starting driver instance\")\n\n\tstream, err := d.DriverSvc.Start(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to start driver instance\")\n\t\treturn nil, err\n\t}\n\n\t// Blocking to receive an initial response to ensure Start() is initiated\n\t// at the server-side.\n\tinitialResp, err := stream.Recv()\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Error receiving initial response from driver start\")\n\t\treturn nil, err\n\t}\n\tif !initialResp.Success {\n\t\treturn nil, errors.New(initialResp.Error)\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tif closeErr := stream.CloseSend(); closeErr != nil {\n\t\t\td.logger.WithError(closeErr).Warn(\"Failed to close stream\")\n\t\t}\n\t}()\n\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\tfor {\n\t\t\trespStream, err := stream.Recv()\n\t\t\tif err != nil {\n\t\t\t\td.logger.Infof(\"Error receiving response from driver: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\td.logger.Debugf(\"Received response: %v\", respStream)\n\t\t\tif !respStream.Success {\n\t\t\t\terrCh <- errors.New(respStream.Error)\n\t\t\t} else {\n\t\t\t\tclose(errCh)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\td.logger.Debug(\"Driver instance started successfully\")\n\treturn errCh, nil\n}\n\nfunc (d *DriverClient) Stop(ctx context.Context) error {\n\td.logger.Debug(\"Stopping driver instance\")\n\n\t_, err := d.DriverSvc.Stop(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to stop driver instance\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"Driver instance stopped successfully\")\n\treturn nil\n}\n\nfunc (d *DriverClient) Delete(ctx context.Context) error {\n\td.logger.Debug(\"Deleting driver instance\")\n\n\t_, err := d.DriverSvc.Delete(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to deleted driver instance\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"Driver instance deleted successfully\")\n\treturn nil\n}\n\nfunc (d *DriverClient) FillConfig(_ context.Context, _ *limatype.LimaYAML, _ string) error {\n\treturn errors.New(\"pre-configured driver action not implemented in client driver\")\n}\n\nfunc (d *DriverClient) RunGUI() error {\n\td.logger.Debug(\"Running GUI for the driver instance\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\t_, err := d.DriverSvc.RunGUI(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to run GUI\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"GUI started successfully\")\n\treturn nil\n}\n\nfunc (d *DriverClient) ChangeDisplayPassword(ctx context.Context, password string) error {\n\td.logger.Debug(\"Changing display password for the driver instance\")\n\n\t_, err := d.DriverSvc.ChangeDisplayPassword(ctx, &pb.ChangeDisplayPasswordRequest{\n\t\tPassword: password,\n\t})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to change display password\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"Display password changed successfully\")\n\treturn nil\n}\n\nfunc (d *DriverClient) DisplayConnection(ctx context.Context) (string, error) {\n\td.logger.Debug(\"Getting display connection for the driver instance\")\n\n\tresp, err := d.DriverSvc.GetDisplayConnection(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to get display connection\")\n\t\treturn \"\", err\n\t}\n\n\td.logger.Debugf(\"Display connection retrieved: %s\", resp.Connection)\n\treturn resp.Connection, nil\n}\n\nfunc (d *DriverClient) CreateSnapshot(ctx context.Context, tag string) error {\n\td.logger.Debugf(\"Creating snapshot with tag: %s\", tag)\n\n\t_, err := d.DriverSvc.CreateSnapshot(ctx, &pb.CreateSnapshotRequest{\n\t\tTag: tag,\n\t})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to create snapshot\")\n\t\treturn err\n\t}\n\n\td.logger.Debugf(\"Snapshot '%s' created successfully\", tag)\n\treturn nil\n}\n\nfunc (d *DriverClient) ApplySnapshot(ctx context.Context, tag string) error {\n\td.logger.Debugf(\"Applying snapshot with tag: %s\", tag)\n\n\t_, err := d.DriverSvc.ApplySnapshot(ctx, &pb.ApplySnapshotRequest{\n\t\tTag: tag,\n\t})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to apply snapshot\")\n\t\treturn err\n\t}\n\n\td.logger.Debugf(\"Snapshot '%s' applied successfully\", tag)\n\treturn nil\n}\n\nfunc (d *DriverClient) DeleteSnapshot(ctx context.Context, tag string) error {\n\td.logger.Debugf(\"Deleting snapshot with tag: %s\", tag)\n\n\t_, err := d.DriverSvc.DeleteSnapshot(ctx, &pb.DeleteSnapshotRequest{\n\t\tTag: tag,\n\t})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to delete snapshot\")\n\t\treturn err\n\t}\n\n\td.logger.Debugf(\"Snapshot '%s' deleted successfully\", tag)\n\treturn nil\n}\n\nfunc (d *DriverClient) ListSnapshots(ctx context.Context) (string, error) {\n\td.logger.Debug(\"Listing snapshots\")\n\n\tresp, err := d.DriverSvc.ListSnapshots(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to list snapshots\")\n\t\treturn \"\", err\n\t}\n\n\td.logger.Debugf(\"Snapshots listed successfully: %s\", resp.Snapshots)\n\treturn resp.Snapshots, nil\n}\n\nfunc (d *DriverClient) ForwardGuestAgent() bool {\n\td.logger.Debug(\"Checking if guest agent needs to be forwarded\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tresp, err := d.DriverSvc.ForwardGuestAgent(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to check guest agent forwarding\")\n\t\treturn false\n\t}\n\n\treturn resp.ShouldForward\n}\n\nfunc (d *DriverClient) GuestAgentConn(ctx context.Context) (net.Conn, string, error) {\n\td.logger.Info(\"Getting guest agent connection\")\n\t_, err := d.DriverSvc.GuestAgentConn(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to get guest agent connection\")\n\t\treturn nil, \"\", err\n\t}\n\n\treturn nil, \"\", nil\n}\n\nfunc (d *DriverClient) Info() driver.Info {\n\td.logger.Debug(\"Getting driver info\")\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\tresp, err := d.DriverSvc.Info(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to get driver info\")\n\t\treturn driver.Info{}\n\t}\n\n\tvar info driver.Info\n\tif err := json.Unmarshal(resp.InfoJson, &info); err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to unmarshal driver info\")\n\t\treturn driver.Info{}\n\t}\n\n\td.logger.Debugf(\"Driver info retrieved: %+v\", info)\n\treturn info\n}\n\nfunc (d *DriverClient) Configure(inst *limatype.Instance) *driver.ConfiguredDriver {\n\td.logger.Debugf(\"Setting config for instance %s with SSH local port %d\", inst.Name, inst.SSHLocalPort)\n\n\tinstJSON, err := inst.MarshalJSON()\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to marshal instance config\")\n\t\treturn nil\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n\tdefer cancel()\n\n\t_, err = d.DriverSvc.Configure(ctx, &pb.SetConfigRequest{\n\t\tInstanceConfigJson: instJSON,\n\t})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to set config\")\n\t\treturn nil\n\t}\n\n\td.logger.Debugf(\"Config set successfully for instance %s\", inst.Name)\n\treturn &driver.ConfiguredDriver{\n\t\tDriver: d,\n\t}\n}\n\nfunc (d *DriverClient) InspectStatus(_ context.Context, _ *limatype.Instance) string {\n\treturn \"\"\n}\n\nfunc (d *DriverClient) SSHAddress(ctx context.Context) (string, error) {\n\td.logger.Debug(\"Getting SSH address for the driver instance\")\n\n\tresp, err := d.DriverSvc.SSHAddress(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to get SSH address\")\n\t\treturn \"\", err\n\t}\n\n\td.logger.Debugf(\"SSH address retrieved: %s\", resp.Address)\n\treturn resp.Address, nil\n}\n\nfunc (d *DriverClient) BootScripts() (map[string][]byte, error) {\n\td.logger.Debug(\"Getting boot scripts for the driver instance\")\n\tresp, err := d.DriverSvc.BootScripts(context.Background(), &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to get boot scripts\")\n\t\treturn nil, err\n\t}\n\n\td.logger.Debugf(\"Boot scripts retrieved successfully: %d scripts\", len(resp.Scripts))\n\treturn resp.Scripts, nil\n}\n\nfunc (d *DriverClient) AdditionalSetupForSSH(ctx context.Context) error {\n\td.logger.Debug(\"Performing additional setup for SSH connection\")\n\n\t_, err := d.DriverSvc.AdditionalSetupForSSH(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\td.logger.WithError(err).Error(\"Failed to perform additional setup for SSH\")\n\t\treturn err\n\t}\n\n\td.logger.Debug(\"Additional setup for SSH completed successfully\")\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driver/external/driver.pb.desc",
    "content": "\n\u0010\n\fdriver.proto\u001a\u001bgoogle/protobuf/empty.proto\"\u0001\n\u0013BootScriptsResponse\u0012;\n\u0007scripts\u0018\u0001 \u0003(\u000b2!.BootScriptsResponse.ScriptsEntryR\u0007scripts\u001a:\n\fScriptsEntry\u0012\u0010\n\u0003key\u0018\u0001 \u0001(\tR\u0003key\u0012\u0014\n\u0005value\u0018\u0002 \u0001(\fR\u0005value:\u00028\u0001\".\n\u0012SSHAddressResponse\u0012\u0018\n\u0007address\u0018\u0001 \u0001(\tR\u0007address\"+\n\fInfoResponse\u0012\u001b\n\tinfo_json\u0018\u0001 \u0001(\fR\binfoJson\"?\n\rStartResponse\u0012\u0018\n\u0007success\u0018\u0001 \u0001(\bR\u0007success\u0012\u0014\n\u0005error\u0018\u0002 \u0001(\tR\u0005error\"D\n\u0010SetConfigRequest\u00120\n\u0014instance_config_json\u0018\u0001 \u0001(\fR\u0012instanceConfigJson\":\n\u001cChangeDisplayPasswordRequest\u0012\u001a\n\bpassword\u0018\u0001 \u0001(\tR\bpassword\">\n\u001cGetDisplayConnectionResponse\u0012\u001e\n\nconnection\u0018\u0001 \u0001(\tR\nconnection\")\n\u0015CreateSnapshotRequest\u0012\u0010\n\u0003tag\u0018\u0001 \u0001(\tR\u0003tag\"(\n\u0014ApplySnapshotRequest\u0012\u0010\n\u0003tag\u0018\u0001 \u0001(\tR\u0003tag\")\n\u0015DeleteSnapshotRequest\u0012\u0010\n\u0003tag\u0018\u0001 \u0001(\tR\u0003tag\"5\n\u0015ListSnapshotsResponse\u0012\u001c\n\tsnapshots\u0018\u0001 \u0001(\tR\tsnapshots\"B\n\u0019ForwardGuestAgentResponse\u0012%\n\u000eshould_forward\u0018\u0001 \u0001(\bR\rshouldForward2\t\n\u0006Driver\u0012:\n\bValidate\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.Empty\u00128\n\u0006Create\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.Empty\u0012<\n\nCreateDisk\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.Empty\u00121\n\u0005Start\u0012\u0016.google.protobuf.Empty\u001a\u000e.StartResponse0\u0001\u00126\n\u0004Stop\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.Empty\u00128\n\u0006Delete\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.Empty\u0012;\n\u000bBootScripts\u0012\u0016.google.protobuf.Empty\u001a\u0014.BootScriptsResponse\u00128\n\u0006RunGUI\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.Empty\u0012N\n\u0015ChangeDisplayPassword\u0012\u001d.ChangeDisplayPasswordRequest\u001a\u0016.google.protobuf.Empty\u0012M\n\u0014GetDisplayConnection\u0012\u0016.google.protobuf.Empty\u001a\u001d.GetDisplayConnectionResponse\u0012@\n\u000eCreateSnapshot\u0012\u0016.CreateSnapshotRequest\u001a\u0016.google.protobuf.Empty\u0012>\n\rApplySnapshot\u0012\u0015.ApplySnapshotRequest\u001a\u0016.google.protobuf.Empty\u0012@\n\u000eDeleteSnapshot\u0012\u0016.DeleteSnapshotRequest\u001a\u0016.google.protobuf.Empty\u0012?\n\rListSnapshots\u0012\u0016.google.protobuf.Empty\u001a\u0016.ListSnapshotsResponse\u0012G\n\u0011ForwardGuestAgent\u0012\u0016.google.protobuf.Empty\u001a\u001a.ForwardGuestAgentResponse\u0012@\n\u000eGuestAgentConn\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.Empty\u00126\n\tConfigure\u0012\u0011.SetConfigRequest\u001a\u0016.google.protobuf.Empty\u0012-\n\u0004Info\u0012\u0016.google.protobuf.Empty\u001a\r.InfoResponse\u00129\n\nSSHAddress\u0012\u0016.google.protobuf.Empty\u001a\u0013.SSHAddressResponse\u0012G\n\u0015AdditionalSetupForSSH\u0012\u0016.google.protobuf.Empty\u001a\u0016.google.protobuf.EmptyB0Z.github.com/lima-vm/lima/v2/pkg/driver/externalb\u0006proto3"
  },
  {
    "path": "pkg/driver/external/driver.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go [version omitted for reproducibility]\n// \tprotoc        [version omitted for reproducibility]\n// source: driver.proto\n\npackage external\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype BootScriptsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tScripts       map[string][]byte      `protobuf:\"bytes,1,rep,name=scripts,proto3\" json:\"scripts,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *BootScriptsResponse) Reset() {\n\t*x = BootScriptsResponse{}\n\tmi := &file_driver_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *BootScriptsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*BootScriptsResponse) ProtoMessage() {}\n\nfunc (x *BootScriptsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use BootScriptsResponse.ProtoReflect.Descriptor instead.\nfunc (*BootScriptsResponse) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *BootScriptsResponse) GetScripts() map[string][]byte {\n\tif x != nil {\n\t\treturn x.Scripts\n\t}\n\treturn nil\n}\n\ntype SSHAddressResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAddress       string                 `protobuf:\"bytes,1,opt,name=address,proto3\" json:\"address,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *SSHAddressResponse) Reset() {\n\t*x = SSHAddressResponse{}\n\tmi := &file_driver_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SSHAddressResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SSHAddressResponse) ProtoMessage() {}\n\nfunc (x *SSHAddressResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SSHAddressResponse.ProtoReflect.Descriptor instead.\nfunc (*SSHAddressResponse) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *SSHAddressResponse) GetAddress() string {\n\tif x != nil {\n\t\treturn x.Address\n\t}\n\treturn \"\"\n}\n\ntype InfoResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tInfoJson      []byte                 `protobuf:\"bytes,1,opt,name=info_json,json=infoJson,proto3\" json:\"info_json,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *InfoResponse) Reset() {\n\t*x = InfoResponse{}\n\tmi := &file_driver_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *InfoResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*InfoResponse) ProtoMessage() {}\n\nfunc (x *InfoResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use InfoResponse.ProtoReflect.Descriptor instead.\nfunc (*InfoResponse) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *InfoResponse) GetInfoJson() []byte {\n\tif x != nil {\n\t\treturn x.InfoJson\n\t}\n\treturn nil\n}\n\n// StartResponse is a streamed response for Start() RPC. It tries to mimic\n// errChan from pkg/driver/driver.go. The server sends an initial response\n// with success=true when Start() is initiated. If errors occur, they are\n// sent as success=false with the error field populated. When the error channel\n// closes, a final success=true message is sent.\ntype StartResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSuccess       bool                   `protobuf:\"varint,1,opt,name=success,proto3\" json:\"success,omitempty\"`\n\tError         string                 `protobuf:\"bytes,2,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *StartResponse) Reset() {\n\t*x = StartResponse{}\n\tmi := &file_driver_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *StartResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*StartResponse) ProtoMessage() {}\n\nfunc (x *StartResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use StartResponse.ProtoReflect.Descriptor instead.\nfunc (*StartResponse) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *StartResponse) GetSuccess() bool {\n\tif x != nil {\n\t\treturn x.Success\n\t}\n\treturn false\n}\n\nfunc (x *StartResponse) GetError() string {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn \"\"\n}\n\ntype SetConfigRequest struct {\n\tstate              protoimpl.MessageState `protogen:\"open.v1\"`\n\tInstanceConfigJson []byte                 `protobuf:\"bytes,1,opt,name=instance_config_json,json=instanceConfigJson,proto3\" json:\"instance_config_json,omitempty\"`\n\tunknownFields      protoimpl.UnknownFields\n\tsizeCache          protoimpl.SizeCache\n}\n\nfunc (x *SetConfigRequest) Reset() {\n\t*x = SetConfigRequest{}\n\tmi := &file_driver_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *SetConfigRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*SetConfigRequest) ProtoMessage() {}\n\nfunc (x *SetConfigRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use SetConfigRequest.ProtoReflect.Descriptor instead.\nfunc (*SetConfigRequest) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *SetConfigRequest) GetInstanceConfigJson() []byte {\n\tif x != nil {\n\t\treturn x.InstanceConfigJson\n\t}\n\treturn nil\n}\n\ntype ChangeDisplayPasswordRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tPassword      string                 `protobuf:\"bytes,1,opt,name=password,proto3\" json:\"password,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ChangeDisplayPasswordRequest) Reset() {\n\t*x = ChangeDisplayPasswordRequest{}\n\tmi := &file_driver_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ChangeDisplayPasswordRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ChangeDisplayPasswordRequest) ProtoMessage() {}\n\nfunc (x *ChangeDisplayPasswordRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ChangeDisplayPasswordRequest.ProtoReflect.Descriptor instead.\nfunc (*ChangeDisplayPasswordRequest) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *ChangeDisplayPasswordRequest) GetPassword() string {\n\tif x != nil {\n\t\treturn x.Password\n\t}\n\treturn \"\"\n}\n\ntype GetDisplayConnectionResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tConnection    string                 `protobuf:\"bytes,1,opt,name=connection,proto3\" json:\"connection,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *GetDisplayConnectionResponse) Reset() {\n\t*x = GetDisplayConnectionResponse{}\n\tmi := &file_driver_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *GetDisplayConnectionResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*GetDisplayConnectionResponse) ProtoMessage() {}\n\nfunc (x *GetDisplayConnectionResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use GetDisplayConnectionResponse.ProtoReflect.Descriptor instead.\nfunc (*GetDisplayConnectionResponse) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *GetDisplayConnectionResponse) GetConnection() string {\n\tif x != nil {\n\t\treturn x.Connection\n\t}\n\treturn \"\"\n}\n\ntype CreateSnapshotRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTag           string                 `protobuf:\"bytes,1,opt,name=tag,proto3\" json:\"tag,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CreateSnapshotRequest) Reset() {\n\t*x = CreateSnapshotRequest{}\n\tmi := &file_driver_proto_msgTypes[7]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CreateSnapshotRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CreateSnapshotRequest) ProtoMessage() {}\n\nfunc (x *CreateSnapshotRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[7]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CreateSnapshotRequest.ProtoReflect.Descriptor instead.\nfunc (*CreateSnapshotRequest) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{7}\n}\n\nfunc (x *CreateSnapshotRequest) GetTag() string {\n\tif x != nil {\n\t\treturn x.Tag\n\t}\n\treturn \"\"\n}\n\ntype ApplySnapshotRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTag           string                 `protobuf:\"bytes,1,opt,name=tag,proto3\" json:\"tag,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ApplySnapshotRequest) Reset() {\n\t*x = ApplySnapshotRequest{}\n\tmi := &file_driver_proto_msgTypes[8]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ApplySnapshotRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ApplySnapshotRequest) ProtoMessage() {}\n\nfunc (x *ApplySnapshotRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[8]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ApplySnapshotRequest.ProtoReflect.Descriptor instead.\nfunc (*ApplySnapshotRequest) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{8}\n}\n\nfunc (x *ApplySnapshotRequest) GetTag() string {\n\tif x != nil {\n\t\treturn x.Tag\n\t}\n\treturn \"\"\n}\n\ntype DeleteSnapshotRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tTag           string                 `protobuf:\"bytes,1,opt,name=tag,proto3\" json:\"tag,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *DeleteSnapshotRequest) Reset() {\n\t*x = DeleteSnapshotRequest{}\n\tmi := &file_driver_proto_msgTypes[9]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *DeleteSnapshotRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*DeleteSnapshotRequest) ProtoMessage() {}\n\nfunc (x *DeleteSnapshotRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[9]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use DeleteSnapshotRequest.ProtoReflect.Descriptor instead.\nfunc (*DeleteSnapshotRequest) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{9}\n}\n\nfunc (x *DeleteSnapshotRequest) GetTag() string {\n\tif x != nil {\n\t\treturn x.Tag\n\t}\n\treturn \"\"\n}\n\ntype ListSnapshotsResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tSnapshots     string                 `protobuf:\"bytes,1,opt,name=snapshots,proto3\" json:\"snapshots,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ListSnapshotsResponse) Reset() {\n\t*x = ListSnapshotsResponse{}\n\tmi := &file_driver_proto_msgTypes[10]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ListSnapshotsResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ListSnapshotsResponse) ProtoMessage() {}\n\nfunc (x *ListSnapshotsResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[10]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ListSnapshotsResponse.ProtoReflect.Descriptor instead.\nfunc (*ListSnapshotsResponse) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{10}\n}\n\nfunc (x *ListSnapshotsResponse) GetSnapshots() string {\n\tif x != nil {\n\t\treturn x.Snapshots\n\t}\n\treturn \"\"\n}\n\ntype ForwardGuestAgentResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tShouldForward bool                   `protobuf:\"varint,1,opt,name=should_forward,json=shouldForward,proto3\" json:\"should_forward,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ForwardGuestAgentResponse) Reset() {\n\t*x = ForwardGuestAgentResponse{}\n\tmi := &file_driver_proto_msgTypes[11]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ForwardGuestAgentResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ForwardGuestAgentResponse) ProtoMessage() {}\n\nfunc (x *ForwardGuestAgentResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_driver_proto_msgTypes[11]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ForwardGuestAgentResponse.ProtoReflect.Descriptor instead.\nfunc (*ForwardGuestAgentResponse) Descriptor() ([]byte, []int) {\n\treturn file_driver_proto_rawDescGZIP(), []int{11}\n}\n\nfunc (x *ForwardGuestAgentResponse) GetShouldForward() bool {\n\tif x != nil {\n\t\treturn x.ShouldForward\n\t}\n\treturn false\n}\n\nvar File_driver_proto protoreflect.FileDescriptor\n\nconst file_driver_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\fdriver.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\\"\\x8e\\x01\\n\" +\n\t\"\\x13BootScriptsResponse\\x12;\\n\" +\n\t\"\\ascripts\\x18\\x01 \\x03(\\v2!.BootScriptsResponse.ScriptsEntryR\\ascripts\\x1a:\\n\" +\n\t\"\\fScriptsEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\fR\\x05value:\\x028\\x01\\\".\\n\" +\n\t\"\\x12SSHAddressResponse\\x12\\x18\\n\" +\n\t\"\\aaddress\\x18\\x01 \\x01(\\tR\\aaddress\\\"+\\n\" +\n\t\"\\fInfoResponse\\x12\\x1b\\n\" +\n\t\"\\tinfo_json\\x18\\x01 \\x01(\\fR\\binfoJson\\\"?\\n\" +\n\t\"\\rStartResponse\\x12\\x18\\n\" +\n\t\"\\asuccess\\x18\\x01 \\x01(\\bR\\asuccess\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\x02 \\x01(\\tR\\x05error\\\"D\\n\" +\n\t\"\\x10SetConfigRequest\\x120\\n\" +\n\t\"\\x14instance_config_json\\x18\\x01 \\x01(\\fR\\x12instanceConfigJson\\\":\\n\" +\n\t\"\\x1cChangeDisplayPasswordRequest\\x12\\x1a\\n\" +\n\t\"\\bpassword\\x18\\x01 \\x01(\\tR\\bpassword\\\">\\n\" +\n\t\"\\x1cGetDisplayConnectionResponse\\x12\\x1e\\n\" +\n\t\"\\n\" +\n\t\"connection\\x18\\x01 \\x01(\\tR\\n\" +\n\t\"connection\\\")\\n\" +\n\t\"\\x15CreateSnapshotRequest\\x12\\x10\\n\" +\n\t\"\\x03tag\\x18\\x01 \\x01(\\tR\\x03tag\\\"(\\n\" +\n\t\"\\x14ApplySnapshotRequest\\x12\\x10\\n\" +\n\t\"\\x03tag\\x18\\x01 \\x01(\\tR\\x03tag\\\")\\n\" +\n\t\"\\x15DeleteSnapshotRequest\\x12\\x10\\n\" +\n\t\"\\x03tag\\x18\\x01 \\x01(\\tR\\x03tag\\\"5\\n\" +\n\t\"\\x15ListSnapshotsResponse\\x12\\x1c\\n\" +\n\t\"\\tsnapshots\\x18\\x01 \\x01(\\tR\\tsnapshots\\\"B\\n\" +\n\t\"\\x19ForwardGuestAgentResponse\\x12%\\n\" +\n\t\"\\x0eshould_forward\\x18\\x01 \\x01(\\bR\\rshouldForward2\\xf2\\t\\n\" +\n\t\"\\x06Driver\\x12:\\n\" +\n\t\"\\bValidate\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.Empty\\x128\\n\" +\n\t\"\\x06Create\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.Empty\\x12<\\n\" +\n\t\"\\n\" +\n\t\"CreateDisk\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.Empty\\x121\\n\" +\n\t\"\\x05Start\\x12\\x16.google.protobuf.Empty\\x1a\\x0e.StartResponse0\\x01\\x126\\n\" +\n\t\"\\x04Stop\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.Empty\\x128\\n\" +\n\t\"\\x06Delete\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.Empty\\x12;\\n\" +\n\t\"\\vBootScripts\\x12\\x16.google.protobuf.Empty\\x1a\\x14.BootScriptsResponse\\x128\\n\" +\n\t\"\\x06RunGUI\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.Empty\\x12N\\n\" +\n\t\"\\x15ChangeDisplayPassword\\x12\\x1d.ChangeDisplayPasswordRequest\\x1a\\x16.google.protobuf.Empty\\x12M\\n\" +\n\t\"\\x14GetDisplayConnection\\x12\\x16.google.protobuf.Empty\\x1a\\x1d.GetDisplayConnectionResponse\\x12@\\n\" +\n\t\"\\x0eCreateSnapshot\\x12\\x16.CreateSnapshotRequest\\x1a\\x16.google.protobuf.Empty\\x12>\\n\" +\n\t\"\\rApplySnapshot\\x12\\x15.ApplySnapshotRequest\\x1a\\x16.google.protobuf.Empty\\x12@\\n\" +\n\t\"\\x0eDeleteSnapshot\\x12\\x16.DeleteSnapshotRequest\\x1a\\x16.google.protobuf.Empty\\x12?\\n\" +\n\t\"\\rListSnapshots\\x12\\x16.google.protobuf.Empty\\x1a\\x16.ListSnapshotsResponse\\x12G\\n\" +\n\t\"\\x11ForwardGuestAgent\\x12\\x16.google.protobuf.Empty\\x1a\\x1a.ForwardGuestAgentResponse\\x12@\\n\" +\n\t\"\\x0eGuestAgentConn\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.Empty\\x126\\n\" +\n\t\"\\tConfigure\\x12\\x11.SetConfigRequest\\x1a\\x16.google.protobuf.Empty\\x12-\\n\" +\n\t\"\\x04Info\\x12\\x16.google.protobuf.Empty\\x1a\\r.InfoResponse\\x129\\n\" +\n\t\"\\n\" +\n\t\"SSHAddress\\x12\\x16.google.protobuf.Empty\\x1a\\x13.SSHAddressResponse\\x12G\\n\" +\n\t\"\\x15AdditionalSetupForSSH\\x12\\x16.google.protobuf.Empty\\x1a\\x16.google.protobuf.EmptyB0Z.github.com/lima-vm/lima/v2/pkg/driver/externalb\\x06proto3\"\n\nvar (\n\tfile_driver_proto_rawDescOnce sync.Once\n\tfile_driver_proto_rawDescData []byte\n)\n\nfunc file_driver_proto_rawDescGZIP() []byte {\n\tfile_driver_proto_rawDescOnce.Do(func() {\n\t\tfile_driver_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_driver_proto_rawDesc), len(file_driver_proto_rawDesc)))\n\t})\n\treturn file_driver_proto_rawDescData\n}\n\nvar file_driver_proto_msgTypes = make([]protoimpl.MessageInfo, 13)\nvar file_driver_proto_goTypes = []any{\n\t(*BootScriptsResponse)(nil),          // 0: BootScriptsResponse\n\t(*SSHAddressResponse)(nil),           // 1: SSHAddressResponse\n\t(*InfoResponse)(nil),                 // 2: InfoResponse\n\t(*StartResponse)(nil),                // 3: StartResponse\n\t(*SetConfigRequest)(nil),             // 4: SetConfigRequest\n\t(*ChangeDisplayPasswordRequest)(nil), // 5: ChangeDisplayPasswordRequest\n\t(*GetDisplayConnectionResponse)(nil), // 6: GetDisplayConnectionResponse\n\t(*CreateSnapshotRequest)(nil),        // 7: CreateSnapshotRequest\n\t(*ApplySnapshotRequest)(nil),         // 8: ApplySnapshotRequest\n\t(*DeleteSnapshotRequest)(nil),        // 9: DeleteSnapshotRequest\n\t(*ListSnapshotsResponse)(nil),        // 10: ListSnapshotsResponse\n\t(*ForwardGuestAgentResponse)(nil),    // 11: ForwardGuestAgentResponse\n\tnil,                                  // 12: BootScriptsResponse.ScriptsEntry\n\t(*emptypb.Empty)(nil),                // 13: google.protobuf.Empty\n}\nvar file_driver_proto_depIdxs = []int32{\n\t12, // 0: BootScriptsResponse.scripts:type_name -> BootScriptsResponse.ScriptsEntry\n\t13, // 1: Driver.Validate:input_type -> google.protobuf.Empty\n\t13, // 2: Driver.Create:input_type -> google.protobuf.Empty\n\t13, // 3: Driver.CreateDisk:input_type -> google.protobuf.Empty\n\t13, // 4: Driver.Start:input_type -> google.protobuf.Empty\n\t13, // 5: Driver.Stop:input_type -> google.protobuf.Empty\n\t13, // 6: Driver.Delete:input_type -> google.protobuf.Empty\n\t13, // 7: Driver.BootScripts:input_type -> google.protobuf.Empty\n\t13, // 8: Driver.RunGUI:input_type -> google.protobuf.Empty\n\t5,  // 9: Driver.ChangeDisplayPassword:input_type -> ChangeDisplayPasswordRequest\n\t13, // 10: Driver.GetDisplayConnection:input_type -> google.protobuf.Empty\n\t7,  // 11: Driver.CreateSnapshot:input_type -> CreateSnapshotRequest\n\t8,  // 12: Driver.ApplySnapshot:input_type -> ApplySnapshotRequest\n\t9,  // 13: Driver.DeleteSnapshot:input_type -> DeleteSnapshotRequest\n\t13, // 14: Driver.ListSnapshots:input_type -> google.protobuf.Empty\n\t13, // 15: Driver.ForwardGuestAgent:input_type -> google.protobuf.Empty\n\t13, // 16: Driver.GuestAgentConn:input_type -> google.protobuf.Empty\n\t4,  // 17: Driver.Configure:input_type -> SetConfigRequest\n\t13, // 18: Driver.Info:input_type -> google.protobuf.Empty\n\t13, // 19: Driver.SSHAddress:input_type -> google.protobuf.Empty\n\t13, // 20: Driver.AdditionalSetupForSSH:input_type -> google.protobuf.Empty\n\t13, // 21: Driver.Validate:output_type -> google.protobuf.Empty\n\t13, // 22: Driver.Create:output_type -> google.protobuf.Empty\n\t13, // 23: Driver.CreateDisk:output_type -> google.protobuf.Empty\n\t3,  // 24: Driver.Start:output_type -> StartResponse\n\t13, // 25: Driver.Stop:output_type -> google.protobuf.Empty\n\t13, // 26: Driver.Delete:output_type -> google.protobuf.Empty\n\t0,  // 27: Driver.BootScripts:output_type -> BootScriptsResponse\n\t13, // 28: Driver.RunGUI:output_type -> google.protobuf.Empty\n\t13, // 29: Driver.ChangeDisplayPassword:output_type -> google.protobuf.Empty\n\t6,  // 30: Driver.GetDisplayConnection:output_type -> GetDisplayConnectionResponse\n\t13, // 31: Driver.CreateSnapshot:output_type -> google.protobuf.Empty\n\t13, // 32: Driver.ApplySnapshot:output_type -> google.protobuf.Empty\n\t13, // 33: Driver.DeleteSnapshot:output_type -> google.protobuf.Empty\n\t10, // 34: Driver.ListSnapshots:output_type -> ListSnapshotsResponse\n\t11, // 35: Driver.ForwardGuestAgent:output_type -> ForwardGuestAgentResponse\n\t13, // 36: Driver.GuestAgentConn:output_type -> google.protobuf.Empty\n\t13, // 37: Driver.Configure:output_type -> google.protobuf.Empty\n\t2,  // 38: Driver.Info:output_type -> InfoResponse\n\t1,  // 39: Driver.SSHAddress:output_type -> SSHAddressResponse\n\t13, // 40: Driver.AdditionalSetupForSSH:output_type -> google.protobuf.Empty\n\t21, // [21:41] is the sub-list for method output_type\n\t1,  // [1:21] is the sub-list for method input_type\n\t1,  // [1:1] is the sub-list for extension type_name\n\t1,  // [1:1] is the sub-list for extension extendee\n\t0,  // [0:1] is the sub-list for field type_name\n}\n\nfunc init() { file_driver_proto_init() }\nfunc file_driver_proto_init() {\n\tif File_driver_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_driver_proto_rawDesc), len(file_driver_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   13,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_driver_proto_goTypes,\n\t\tDependencyIndexes: file_driver_proto_depIdxs,\n\t\tMessageInfos:      file_driver_proto_msgTypes,\n\t}.Build()\n\tFile_driver_proto = out.File\n\tfile_driver_proto_goTypes = nil\n\tfile_driver_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "pkg/driver/external/driver.proto",
    "content": "syntax = \"proto3\";\n\nimport \"google/protobuf/empty.proto\";\n\noption go_package = \"github.com/lima-vm/lima/v2/pkg/driver/external\";\n\nservice Driver {\n  rpc Validate(google.protobuf.Empty) returns (google.protobuf.Empty);\n  rpc Create(google.protobuf.Empty) returns (google.protobuf.Empty);\n  rpc CreateDisk(google.protobuf.Empty) returns (google.protobuf.Empty);\n  rpc Start(google.protobuf.Empty) returns (stream StartResponse);\n  rpc Stop(google.protobuf.Empty) returns (google.protobuf.Empty);\n  rpc Delete(google.protobuf.Empty) returns (google.protobuf.Empty);\n  rpc BootScripts(google.protobuf.Empty) returns (BootScriptsResponse);\n\n  rpc RunGUI(google.protobuf.Empty) returns (google.protobuf.Empty);\n  rpc ChangeDisplayPassword(ChangeDisplayPasswordRequest) returns (google.protobuf.Empty);\n  rpc GetDisplayConnection(google.protobuf.Empty) returns (GetDisplayConnectionResponse);\n\n  rpc CreateSnapshot(CreateSnapshotRequest) returns (google.protobuf.Empty);\n  rpc ApplySnapshot(ApplySnapshotRequest) returns (google.protobuf.Empty);\n  rpc DeleteSnapshot(DeleteSnapshotRequest) returns (google.protobuf.Empty);\n  rpc ListSnapshots(google.protobuf.Empty) returns (ListSnapshotsResponse);\n\n  rpc ForwardGuestAgent(google.protobuf.Empty) returns (ForwardGuestAgentResponse);\n  rpc GuestAgentConn(google.protobuf.Empty) returns (google.protobuf.Empty);\n\n  rpc Configure(SetConfigRequest) returns (google.protobuf.Empty);\n  rpc Info(google.protobuf.Empty) returns (InfoResponse);\n  rpc SSHAddress(google.protobuf.Empty) returns (SSHAddressResponse);\n\n  // AdditionalSetupForSSH provides additional setup required for SSH connection.\n  // It is called after VM is started, before first SSH connection.\n  rpc AdditionalSetupForSSH(google.protobuf.Empty) returns (google.protobuf.Empty);\n}\n\nmessage BootScriptsResponse {\n  map<string, bytes> scripts = 1;\n}\n\nmessage SSHAddressResponse {\n  string address = 1;\n}\n\nmessage InfoResponse{\n  bytes info_json = 1;\n}\n\n// StartResponse is a streamed response for Start() RPC. It tries to mimic\n// errChan from pkg/driver/driver.go. The server sends an initial response\n// with success=true when Start() is initiated. If errors occur, they are\n// sent as success=false with the error field populated. When the error channel\n// closes, a final success=true message is sent.\nmessage StartResponse {\n  bool success = 1;\n  string error = 2;\n}\n\nmessage SetConfigRequest {\n  bytes instance_config_json = 1;\n}\n\nmessage ChangeDisplayPasswordRequest {\n  string password = 1;\n}\n\nmessage GetDisplayConnectionResponse {\n  string connection = 1;\n}\n\nmessage CreateSnapshotRequest {\n  string tag = 1;\n}\n\nmessage ApplySnapshotRequest {\n  string tag = 1;\n}\n\nmessage DeleteSnapshotRequest {\n  string tag = 1;\n}\n\nmessage ListSnapshotsResponse {\n  string snapshots = 1;\n}\n\nmessage ForwardGuestAgentResponse {\n  bool should_forward = 1;\n}\n"
  },
  {
    "path": "pkg/driver/external/driver_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc [version omitted for reproducibility]\n// - protoc             [version omitted for reproducibility]\n// source: driver.proto\n\npackage external\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tDriver_Validate_FullMethodName              = \"/Driver/Validate\"\n\tDriver_Create_FullMethodName                = \"/Driver/Create\"\n\tDriver_CreateDisk_FullMethodName            = \"/Driver/CreateDisk\"\n\tDriver_Start_FullMethodName                 = \"/Driver/Start\"\n\tDriver_Stop_FullMethodName                  = \"/Driver/Stop\"\n\tDriver_Delete_FullMethodName                = \"/Driver/Delete\"\n\tDriver_BootScripts_FullMethodName           = \"/Driver/BootScripts\"\n\tDriver_RunGUI_FullMethodName                = \"/Driver/RunGUI\"\n\tDriver_ChangeDisplayPassword_FullMethodName = \"/Driver/ChangeDisplayPassword\"\n\tDriver_GetDisplayConnection_FullMethodName  = \"/Driver/GetDisplayConnection\"\n\tDriver_CreateSnapshot_FullMethodName        = \"/Driver/CreateSnapshot\"\n\tDriver_ApplySnapshot_FullMethodName         = \"/Driver/ApplySnapshot\"\n\tDriver_DeleteSnapshot_FullMethodName        = \"/Driver/DeleteSnapshot\"\n\tDriver_ListSnapshots_FullMethodName         = \"/Driver/ListSnapshots\"\n\tDriver_ForwardGuestAgent_FullMethodName     = \"/Driver/ForwardGuestAgent\"\n\tDriver_GuestAgentConn_FullMethodName        = \"/Driver/GuestAgentConn\"\n\tDriver_Configure_FullMethodName             = \"/Driver/Configure\"\n\tDriver_Info_FullMethodName                  = \"/Driver/Info\"\n\tDriver_SSHAddress_FullMethodName            = \"/Driver/SSHAddress\"\n\tDriver_AdditionalSetupForSSH_FullMethodName = \"/Driver/AdditionalSetupForSSH\"\n)\n\n// DriverClient is the client API for Driver service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype DriverClient interface {\n\tValidate(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tCreate(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tCreateDisk(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tStart(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StartResponse], error)\n\tStop(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tDelete(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tBootScripts(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*BootScriptsResponse, error)\n\tRunGUI(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tChangeDisplayPassword(ctx context.Context, in *ChangeDisplayPasswordRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tGetDisplayConnection(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetDisplayConnectionResponse, error)\n\tCreateSnapshot(ctx context.Context, in *CreateSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tApplySnapshot(ctx context.Context, in *ApplySnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tDeleteSnapshot(ctx context.Context, in *DeleteSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tListSnapshots(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListSnapshotsResponse, error)\n\tForwardGuestAgent(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ForwardGuestAgentResponse, error)\n\tGuestAgentConn(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tConfigure(ctx context.Context, in *SetConfigRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)\n\tInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InfoResponse, error)\n\tSSHAddress(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SSHAddressResponse, error)\n\t// AdditionalSetupForSSH provides additional setup required for SSH connection.\n\t// It is called after VM is started, before first SSH connection.\n\tAdditionalSetupForSSH(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)\n}\n\ntype driverClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewDriverClient(cc grpc.ClientConnInterface) DriverClient {\n\treturn &driverClient{cc}\n}\n\nfunc (c *driverClient) Validate(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_Validate_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) Create(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_Create_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) CreateDisk(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_CreateDisk_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) Start(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StartResponse], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &Driver_ServiceDesc.Streams[0], Driver_Start_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[emptypb.Empty, StartResponse]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Driver_StartClient = grpc.ServerStreamingClient[StartResponse]\n\nfunc (c *driverClient) Stop(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_Stop_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) Delete(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_Delete_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) BootScripts(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*BootScriptsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(BootScriptsResponse)\n\terr := c.cc.Invoke(ctx, Driver_BootScripts_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) RunGUI(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_RunGUI_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) ChangeDisplayPassword(ctx context.Context, in *ChangeDisplayPasswordRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_ChangeDisplayPassword_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) GetDisplayConnection(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*GetDisplayConnectionResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(GetDisplayConnectionResponse)\n\terr := c.cc.Invoke(ctx, Driver_GetDisplayConnection_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) CreateSnapshot(ctx context.Context, in *CreateSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_CreateSnapshot_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) ApplySnapshot(ctx context.Context, in *ApplySnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_ApplySnapshot_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) DeleteSnapshot(ctx context.Context, in *DeleteSnapshotRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_DeleteSnapshot_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) ListSnapshots(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListSnapshotsResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ListSnapshotsResponse)\n\terr := c.cc.Invoke(ctx, Driver_ListSnapshots_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) ForwardGuestAgent(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ForwardGuestAgentResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(ForwardGuestAgentResponse)\n\terr := c.cc.Invoke(ctx, Driver_ForwardGuestAgent_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) GuestAgentConn(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_GuestAgentConn_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) Configure(ctx context.Context, in *SetConfigRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_Configure_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) Info(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*InfoResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(InfoResponse)\n\terr := c.cc.Invoke(ctx, Driver_Info_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) SSHAddress(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SSHAddressResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(SSHAddressResponse)\n\terr := c.cc.Invoke(ctx, Driver_SSHAddress_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *driverClient) AdditionalSetupForSSH(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(emptypb.Empty)\n\terr := c.cc.Invoke(ctx, Driver_AdditionalSetupForSSH_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// DriverServer is the server API for Driver service.\n// All implementations must embed UnimplementedDriverServer\n// for forward compatibility.\ntype DriverServer interface {\n\tValidate(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tCreate(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tCreateDisk(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tStart(*emptypb.Empty, grpc.ServerStreamingServer[StartResponse]) error\n\tStop(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tDelete(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tBootScripts(context.Context, *emptypb.Empty) (*BootScriptsResponse, error)\n\tRunGUI(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tChangeDisplayPassword(context.Context, *ChangeDisplayPasswordRequest) (*emptypb.Empty, error)\n\tGetDisplayConnection(context.Context, *emptypb.Empty) (*GetDisplayConnectionResponse, error)\n\tCreateSnapshot(context.Context, *CreateSnapshotRequest) (*emptypb.Empty, error)\n\tApplySnapshot(context.Context, *ApplySnapshotRequest) (*emptypb.Empty, error)\n\tDeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*emptypb.Empty, error)\n\tListSnapshots(context.Context, *emptypb.Empty) (*ListSnapshotsResponse, error)\n\tForwardGuestAgent(context.Context, *emptypb.Empty) (*ForwardGuestAgentResponse, error)\n\tGuestAgentConn(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tConfigure(context.Context, *SetConfigRequest) (*emptypb.Empty, error)\n\tInfo(context.Context, *emptypb.Empty) (*InfoResponse, error)\n\tSSHAddress(context.Context, *emptypb.Empty) (*SSHAddressResponse, error)\n\t// AdditionalSetupForSSH provides additional setup required for SSH connection.\n\t// It is called after VM is started, before first SSH connection.\n\tAdditionalSetupForSSH(context.Context, *emptypb.Empty) (*emptypb.Empty, error)\n\tmustEmbedUnimplementedDriverServer()\n}\n\n// UnimplementedDriverServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedDriverServer struct{}\n\nfunc (UnimplementedDriverServer) Validate(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Validate not implemented\")\n}\nfunc (UnimplementedDriverServer) Create(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Create not implemented\")\n}\nfunc (UnimplementedDriverServer) CreateDisk(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method CreateDisk not implemented\")\n}\nfunc (UnimplementedDriverServer) Start(*emptypb.Empty, grpc.ServerStreamingServer[StartResponse]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method Start not implemented\")\n}\nfunc (UnimplementedDriverServer) Stop(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Stop not implemented\")\n}\nfunc (UnimplementedDriverServer) Delete(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Delete not implemented\")\n}\nfunc (UnimplementedDriverServer) BootScripts(context.Context, *emptypb.Empty) (*BootScriptsResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method BootScripts not implemented\")\n}\nfunc (UnimplementedDriverServer) RunGUI(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method RunGUI not implemented\")\n}\nfunc (UnimplementedDriverServer) ChangeDisplayPassword(context.Context, *ChangeDisplayPasswordRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method ChangeDisplayPassword not implemented\")\n}\nfunc (UnimplementedDriverServer) GetDisplayConnection(context.Context, *emptypb.Empty) (*GetDisplayConnectionResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method GetDisplayConnection not implemented\")\n}\nfunc (UnimplementedDriverServer) CreateSnapshot(context.Context, *CreateSnapshotRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method CreateSnapshot not implemented\")\n}\nfunc (UnimplementedDriverServer) ApplySnapshot(context.Context, *ApplySnapshotRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method ApplySnapshot not implemented\")\n}\nfunc (UnimplementedDriverServer) DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method DeleteSnapshot not implemented\")\n}\nfunc (UnimplementedDriverServer) ListSnapshots(context.Context, *emptypb.Empty) (*ListSnapshotsResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method ListSnapshots not implemented\")\n}\nfunc (UnimplementedDriverServer) ForwardGuestAgent(context.Context, *emptypb.Empty) (*ForwardGuestAgentResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method ForwardGuestAgent not implemented\")\n}\nfunc (UnimplementedDriverServer) GuestAgentConn(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method GuestAgentConn not implemented\")\n}\nfunc (UnimplementedDriverServer) Configure(context.Context, *SetConfigRequest) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Configure not implemented\")\n}\nfunc (UnimplementedDriverServer) Info(context.Context, *emptypb.Empty) (*InfoResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method Info not implemented\")\n}\nfunc (UnimplementedDriverServer) SSHAddress(context.Context, *emptypb.Empty) (*SSHAddressResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SSHAddress not implemented\")\n}\nfunc (UnimplementedDriverServer) AdditionalSetupForSSH(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method AdditionalSetupForSSH not implemented\")\n}\nfunc (UnimplementedDriverServer) mustEmbedUnimplementedDriverServer() {}\nfunc (UnimplementedDriverServer) testEmbeddedByValue()                {}\n\n// UnsafeDriverServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to DriverServer will\n// result in compilation errors.\ntype UnsafeDriverServer interface {\n\tmustEmbedUnimplementedDriverServer()\n}\n\nfunc RegisterDriverServer(s grpc.ServiceRegistrar, srv DriverServer) {\n\t// If the following call pancis, it indicates UnimplementedDriverServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&Driver_ServiceDesc, srv)\n}\n\nfunc _Driver_Validate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).Validate(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_Validate_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).Validate(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).Create(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_Create_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).Create(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_CreateDisk_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).CreateDisk(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_CreateDisk_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).CreateDisk(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_Start_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(emptypb.Empty)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(DriverServer).Start(m, &grpc.GenericServerStream[emptypb.Empty, StartResponse]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype Driver_StartServer = grpc.ServerStreamingServer[StartResponse]\n\nfunc _Driver_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).Stop(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_Stop_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).Stop(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).Delete(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_Delete_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).Delete(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_BootScripts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).BootScripts(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_BootScripts_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).BootScripts(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_RunGUI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).RunGUI(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_RunGUI_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).RunGUI(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_ChangeDisplayPassword_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ChangeDisplayPasswordRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).ChangeDisplayPassword(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_ChangeDisplayPassword_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).ChangeDisplayPassword(ctx, req.(*ChangeDisplayPasswordRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_GetDisplayConnection_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).GetDisplayConnection(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_GetDisplayConnection_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).GetDisplayConnection(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_CreateSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CreateSnapshotRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).CreateSnapshot(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_CreateSnapshot_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).CreateSnapshot(ctx, req.(*CreateSnapshotRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_ApplySnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ApplySnapshotRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).ApplySnapshot(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_ApplySnapshot_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).ApplySnapshot(ctx, req.(*ApplySnapshotRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_DeleteSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(DeleteSnapshotRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).DeleteSnapshot(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_DeleteSnapshot_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).DeleteSnapshot(ctx, req.(*DeleteSnapshotRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_ListSnapshots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).ListSnapshots(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_ListSnapshots_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).ListSnapshots(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_ForwardGuestAgent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).ForwardGuestAgent(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_ForwardGuestAgent_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).ForwardGuestAgent(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_GuestAgentConn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).GuestAgentConn(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_GuestAgentConn_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).GuestAgentConn(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_Configure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(SetConfigRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).Configure(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_Configure_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).Configure(ctx, req.(*SetConfigRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_Info_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).Info(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_Info_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).Info(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_SSHAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).SSHAddress(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_SSHAddress_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).SSHAddress(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _Driver_AdditionalSetupForSSH_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(DriverServer).AdditionalSetupForSSH(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: Driver_AdditionalSetupForSSH_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(DriverServer).AdditionalSetupForSSH(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// Driver_ServiceDesc is the grpc.ServiceDesc for Driver service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar Driver_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"Driver\",\n\tHandlerType: (*DriverServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Validate\",\n\t\t\tHandler:    _Driver_Validate_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Create\",\n\t\t\tHandler:    _Driver_Create_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateDisk\",\n\t\t\tHandler:    _Driver_CreateDisk_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Stop\",\n\t\t\tHandler:    _Driver_Stop_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Delete\",\n\t\t\tHandler:    _Driver_Delete_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"BootScripts\",\n\t\t\tHandler:    _Driver_BootScripts_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"RunGUI\",\n\t\t\tHandler:    _Driver_RunGUI_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ChangeDisplayPassword\",\n\t\t\tHandler:    _Driver_ChangeDisplayPassword_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GetDisplayConnection\",\n\t\t\tHandler:    _Driver_GetDisplayConnection_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"CreateSnapshot\",\n\t\t\tHandler:    _Driver_CreateSnapshot_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ApplySnapshot\",\n\t\t\tHandler:    _Driver_ApplySnapshot_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"DeleteSnapshot\",\n\t\t\tHandler:    _Driver_DeleteSnapshot_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ListSnapshots\",\n\t\t\tHandler:    _Driver_ListSnapshots_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"ForwardGuestAgent\",\n\t\t\tHandler:    _Driver_ForwardGuestAgent_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"GuestAgentConn\",\n\t\t\tHandler:    _Driver_GuestAgentConn_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Configure\",\n\t\t\tHandler:    _Driver_Configure_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"Info\",\n\t\t\tHandler:    _Driver_Info_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SSHAddress\",\n\t\t\tHandler:    _Driver_SSHAddress_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"AdditionalSetupForSSH\",\n\t\t\tHandler:    _Driver_AdditionalSetupForSSH_Handler,\n\t\t},\n\t},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"Start\",\n\t\t\tHandler:       _Driver_Start_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t},\n\tMetadata: \"driver.proto\",\n}\n"
  },
  {
    "path": "pkg/driver/external/gen.go",
    "content": "//go:generate ../../../hack/gogenerate/protoc.sh driver.proto\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage external\n"
  },
  {
    "path": "pkg/driver/external/server/methods.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"maps\"\n\t\"net\"\n\t\"path/filepath\"\n\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/bicopy\"\n\tpb \"github.com/lima-vm/lima/v2/pkg/driver/external\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\nfunc (s *DriverServer) Start(_ *emptypb.Empty, stream pb.Driver_StartServer) error {\n\ts.logger.Debug(\"Received Start request\")\n\terrChan, err := s.driver.Start(stream.Context())\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"Start failed\")\n\t\tif sendErr := stream.Send(&pb.StartResponse{Success: false, Error: err.Error()}); sendErr != nil {\n\t\t\ts.logger.WithError(sendErr).Error(\"Failed to send error response\")\n\t\t\treturn status.Errorf(codes.Internal, \"failed to send error response: %v\", sendErr)\n\t\t}\n\t\treturn status.Errorf(codes.Internal, \"failed to start driver: %v\", err)\n\t}\n\n\t// First send a success response upon receiving the errChan to unblock the client\n\t// and start receiving errors (if any).\n\tif err := stream.Send(&pb.StartResponse{Success: true}); err != nil {\n\t\ts.logger.WithError(err).Error(\"Failed to send success response\")\n\t\treturn status.Errorf(codes.Internal, \"failed to send success response: %v\", err)\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase err, ok := <-errChan:\n\t\t\tif !ok {\n\t\t\t\ts.logger.Debug(\"Start error channel closed\")\n\t\t\t\tif err := stream.Send(&pb.StartResponse{Success: true}); err != nil {\n\t\t\t\t\ts.logger.WithError(err).Error(\"Failed to send success response\")\n\t\t\t\t\treturn status.Errorf(codes.Internal, \"failed to send success response: %v\", err)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\ts.logger.WithError(err).Error(\"Error during Start\")\n\t\t\t\tif err := stream.Send(&pb.StartResponse{Error: err.Error(), Success: false}); err != nil {\n\t\t\t\t\ts.logger.WithError(err).Error(\"Failed to send response\")\n\t\t\t\t\treturn status.Errorf(codes.Internal, \"failed to send error response: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\tcase <-stream.Context().Done():\n\t\t\ts.logger.Debug(\"Stream context done, stopping Start\")\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (s *DriverServer) Configure(_ context.Context, req *pb.SetConfigRequest) (*emptypb.Empty, error) {\n\ts.logger.Debugf(\"Received SetConfig request\")\n\tvar inst limatype.Instance\n\n\tif err := inst.UnmarshalJSON(req.InstanceConfigJson); err != nil {\n\t\ts.logger.WithError(err).Error(\"Failed to unmarshal InstanceConfigJson\")\n\t\treturn &emptypb.Empty{}, err\n\t}\n\n\t_ = s.driver.Configure(&inst)\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *DriverServer) GuestAgentConn(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received GuestAgentConn request\")\n\tconn, connType, err := s.driver.GuestAgentConn(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"GuestAgentConn failed\")\n\t\treturn nil, err\n\t}\n\n\tif connType != \"unix\" {\n\t\tproxySocketPath := filepath.Join(s.driver.Info().InstanceDir, filenames.GuestAgentSock)\n\n\t\tvar lc net.ListenConfig\n\t\tlistener, err := lc.Listen(ctx, \"unix\", proxySocketPath)\n\t\tif err != nil {\n\t\t\ts.logger.WithError(err).Error(\"Failed to create proxy socket\")\n\t\t\treturn nil, err\n\t\t}\n\n\t\tgo func() {\n\t\t\tdefer listener.Close()\n\t\t\tdefer conn.Close()\n\n\t\t\tproxyConn, err := listener.Accept()\n\t\t\tif err != nil {\n\t\t\t\ts.logger.WithError(err).Error(\"Failed to accept proxy connection\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tbicopy.Bicopy(conn, proxyConn, nil)\n\t\t}()\n\t}\n\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *DriverServer) Info(_ context.Context, _ *emptypb.Empty) (*pb.InfoResponse, error) {\n\ts.logger.Debug(\"Received GetInfo request\")\n\tinfo := s.driver.Info()\n\n\tinfoJSON, err := json.Marshal(info)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"Failed to marshal driver info\")\n\t\treturn nil, status.Errorf(codes.Internal, \"failed to marshal driver info: %v\", err)\n\t}\n\n\treturn &pb.InfoResponse{\n\t\tInfoJson: infoJSON,\n\t}, nil\n}\n\nfunc (s *DriverServer) Validate(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debugf(\"Received Validate request\")\n\terr := s.driver.Validate(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"Validation failed\")\n\t\treturn empty, err\n\t}\n\ts.logger.Debug(\"Validation succeeded\")\n\treturn empty, nil\n}\n\nfunc (s *DriverServer) Create(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received Initialize request\")\n\terr := s.driver.Create(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"Initialization failed\")\n\t\treturn empty, err\n\t}\n\ts.logger.Debug(\"Initialization succeeded\")\n\treturn empty, nil\n}\n\nfunc (s *DriverServer) CreateDisk(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received CreateDisk request\")\n\terr := s.driver.CreateDisk(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"CreateDisk failed\")\n\t\treturn empty, err\n\t}\n\ts.logger.Debug(\"CreateDisk succeeded\")\n\treturn empty, nil\n}\n\nfunc (s *DriverServer) Stop(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received Stop request\")\n\terr := s.driver.Stop(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"Stop failed\")\n\t\treturn empty, err\n\t}\n\ts.logger.Debug(\"Stop succeeded\")\n\treturn empty, nil\n}\n\nfunc (s *DriverServer) RunGUI(_ context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received RunGUI request\")\n\terr := s.driver.RunGUI()\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"RunGUI failed\")\n\t\treturn empty, err\n\t}\n\ts.logger.Debug(\"RunGUI succeeded\")\n\treturn empty, nil\n}\n\nfunc (s *DriverServer) Delete(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received Delete request\")\n\terr := s.driver.Delete(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"Delete failed\")\n\t\treturn empty, err\n\t}\n\ts.logger.Debug(\"Delete succeeded\")\n\treturn empty, nil\n}\n\nfunc (s *DriverServer) BootScripts(_ context.Context, _ *emptypb.Empty) (*pb.BootScriptsResponse, error) {\n\ts.logger.Debug(\"Received BootScripts request\")\n\tscripts, err := s.driver.BootScripts()\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"BootScripts failed\")\n\t\treturn nil, err\n\t}\n\n\tresp := &pb.BootScriptsResponse{\n\t\tScripts: make(map[string][]byte),\n\t}\n\n\tmaps.Copy(resp.Scripts, scripts)\n\n\ts.logger.Debugf(\"BootScripts succeeded with %d scripts\", len(resp.Scripts))\n\treturn resp, nil\n}\n\nfunc (s *DriverServer) SSHAddress(ctx context.Context, _ *emptypb.Empty) (*pb.SSHAddressResponse, error) {\n\ts.logger.Debug(\"Received SSHAddress request\")\n\taddress, err := s.driver.SSHAddress(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"SSHAddress failed\")\n\t\treturn nil, err\n\t}\n\n\ts.logger.Debugf(\"SSHAddress succeeded with address: %s\", address)\n\treturn &pb.SSHAddressResponse{Address: address}, nil\n}\n\nfunc (s *DriverServer) ChangeDisplayPassword(ctx context.Context, req *pb.ChangeDisplayPasswordRequest) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received ChangeDisplayPassword request\")\n\terr := s.driver.ChangeDisplayPassword(ctx, req.Password)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"ChangeDisplayPassword failed\")\n\t\treturn &emptypb.Empty{}, err\n\t}\n\ts.logger.Debug(\"ChangeDisplayPassword succeeded\")\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *DriverServer) GetDisplayConnection(ctx context.Context, _ *emptypb.Empty) (*pb.GetDisplayConnectionResponse, error) {\n\ts.logger.Debug(\"Received GetDisplayConnection request\")\n\tconn, err := s.driver.DisplayConnection(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"GetDisplayConnection failed\")\n\t\treturn nil, err\n\t}\n\ts.logger.Debug(\"GetDisplayConnection succeeded\")\n\treturn &pb.GetDisplayConnectionResponse{Connection: conn}, nil\n}\n\nfunc (s *DriverServer) CreateSnapshot(ctx context.Context, req *pb.CreateSnapshotRequest) (*emptypb.Empty, error) {\n\ts.logger.Debugf(\"Received CreateSnapshot request with tag: %s\", req.Tag)\n\terr := s.driver.CreateSnapshot(ctx, req.Tag)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"CreateSnapshot failed\")\n\t\treturn &emptypb.Empty{}, err\n\t}\n\ts.logger.Debug(\"CreateSnapshot succeeded\")\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *DriverServer) ApplySnapshot(ctx context.Context, req *pb.ApplySnapshotRequest) (*emptypb.Empty, error) {\n\ts.logger.Debugf(\"Received ApplySnapshot request with tag: %s\", req.Tag)\n\terr := s.driver.ApplySnapshot(ctx, req.Tag)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"ApplySnapshot failed\")\n\t\treturn &emptypb.Empty{}, err\n\t}\n\ts.logger.Debug(\"ApplySnapshot succeeded\")\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *DriverServer) DeleteSnapshot(ctx context.Context, req *pb.DeleteSnapshotRequest) (*emptypb.Empty, error) {\n\ts.logger.Debugf(\"Received DeleteSnapshot request with tag: %s\", req.Tag)\n\terr := s.driver.DeleteSnapshot(ctx, req.Tag)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"DeleteSnapshot failed\")\n\t\treturn &emptypb.Empty{}, err\n\t}\n\ts.logger.Debug(\"DeleteSnapshot succeeded\")\n\treturn &emptypb.Empty{}, nil\n}\n\nfunc (s *DriverServer) ListSnapshots(ctx context.Context, _ *emptypb.Empty) (*pb.ListSnapshotsResponse, error) {\n\ts.logger.Debug(\"Received ListSnapshots request\")\n\tsnapshots, err := s.driver.ListSnapshots(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"ListSnapshots failed\")\n\t\treturn nil, err\n\t}\n\ts.logger.Debug(\"ListSnapshots succeeded\")\n\treturn &pb.ListSnapshotsResponse{Snapshots: snapshots}, nil\n}\n\nfunc (s *DriverServer) ForwardGuestAgent(_ context.Context, _ *emptypb.Empty) (*pb.ForwardGuestAgentResponse, error) {\n\ts.logger.Debug(\"Received ForwardGuestAgent request\")\n\treturn &pb.ForwardGuestAgentResponse{ShouldForward: s.driver.ForwardGuestAgent()}, nil\n}\n\nfunc (s *DriverServer) AdditionalSetupForSSH(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {\n\ts.logger.Debug(\"Received AdditionalSetupForSSH request\")\n\terr := s.driver.AdditionalSetupForSSH(ctx)\n\tif err != nil {\n\t\ts.logger.WithError(err).Error(\"AdditionalSetupForSSH failed\")\n\t\treturn &emptypb.Empty{}, err\n\t}\n\ts.logger.Debug(\"AdditionalSetupForSSH succeeded\")\n\treturn &emptypb.Empty{}, nil\n}\n"
  },
  {
    "path": "pkg/driver/external/server/server.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/keepalive\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\tpb \"github.com/lima-vm/lima/v2/pkg/driver/external\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/client\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/registry\"\n)\n\ntype DriverServer struct {\n\tpb.UnimplementedDriverServer\n\tdriver driver.Driver\n\tlogger *logrus.Logger\n}\n\ntype listenerTracker struct {\n\tnet.Listener\n\tconnected chan struct{}\n\tonce      sync.Once\n}\n\nfunc (t *listenerTracker) Accept() (net.Conn, error) {\n\tc, err := t.Listener.Accept()\n\tif err == nil {\n\t\tt.once.Do(func() { close(t.connected) })\n\t}\n\treturn c, err\n}\n\nfunc Serve(ctx context.Context, driver driver.Driver) {\n\tpreConfiguredDriverAction := flag.Bool(\"pre-driver-action\", false, \"Run pre-driver action before starting the gRPC server\")\n\tinspectStatus := flag.Bool(\"inspect-status\", false, \"Inspect status of the driver\")\n\tflag.Parse() //nolint:revive // Serve is intended to be called from external driver's main()\n\tif *preConfiguredDriverAction {\n\t\thandlePreConfiguredDriverAction(ctx, driver)\n\t\treturn\n\t}\n\tif *inspectStatus {\n\t\thandleInspectStatus(driver)\n\t\treturn\n\t}\n\n\tlogger := logrus.New()\n\tlogger.SetLevel(logrus.DebugLevel)\n\n\tsocketPath := filepath.Join(os.TempDir(), fmt.Sprintf(\"lima-driver-%s-%d.sock\", driver.Info().Name, os.Getpid()))\n\n\tdefer func() {\n\t\tif err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {\n\t\t\tlogger.Warnf(\"Failed to remove socket file: %v\", err)\n\t\t}\n\t}()\n\n\tif err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {\n\t\tlogger.Fatalf(\"Failed to remove existing socket file: %v\", err)\n\t}\n\n\tvar lc net.ListenConfig\n\tlistener, err := lc.Listen(ctx, \"unix\", socketPath)\n\tif err != nil {\n\t\tlogger.Fatalf(\"Failed to listen on Unix socket: %v\", err)\n\t}\n\tdefer listener.Close()\n\n\ttListener := &listenerTracker{\n\t\tListener:  listener,\n\t\tconnected: make(chan struct{}),\n\t}\n\n\toutput := map[string]string{\"socketPath\": socketPath}\n\tif err := json.NewEncoder(os.Stdout).Encode(output); err != nil {\n\t\tlogger.Fatalf(\"Failed to encode socket path as JSON: %v\", err)\n\t}\n\n\tkaProps := keepalive.ServerParameters{\n\t\tTime:    10 * time.Second,\n\t\tTimeout: 30 * time.Second,\n\t}\n\n\tkaPolicy := keepalive.EnforcementPolicy{\n\t\tMinTime:             10 * time.Second,\n\t\tPermitWithoutStream: true,\n\t}\n\n\tserver := grpc.NewServer(\n\t\tgrpc.KeepaliveParams(kaProps),\n\t\tgrpc.KeepaliveEnforcementPolicy(kaPolicy),\n\t)\n\n\tpb.RegisterDriverServer(server, &DriverServer{\n\t\tdriver: driver,\n\t\tlogger: logger,\n\t})\n\n\tsigs := make(chan os.Signal, 1)\n\tsignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)\n\n\tshutdownCh := make(chan struct{})\n\tvar closeOnce sync.Once\n\tcloseShutdown := func() { closeOnce.Do(func() { close(shutdownCh) }) }\n\n\tgo func() {\n\t\t<-sigs\n\t\tlogger.Info(\"Received shutdown signal, stopping server...\")\n\t\tcloseShutdown()\n\t}()\n\tgo func() {\n\t\ttimer := time.NewTimer(60 * time.Second)\n\t\tdefer timer.Stop()\n\n\t\tselect {\n\t\tcase <-tListener.connected:\n\t\t\tlogger.Debug(\"Client connected; disabling 60s startup shutdown\")\n\t\t\treturn\n\t\tcase <-timer.C:\n\t\t\tlogger.Info(\"No client connected within 60 seconds, shutting down server...\")\n\t\t\tcloseShutdown()\n\t\tcase <-shutdownCh:\n\t\t\treturn\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tlogger.Infof(\"Starting external driver server for %s\", driver.Info().Name)\n\t\tlogger.Infof(\"Server starting on Unix socket: %s\", socketPath)\n\t\tif err := server.Serve(tListener); err != nil {\n\t\t\tif errors.Is(err, grpc.ErrServerStopped) {\n\t\t\t\tlogger.Errorf(\"Server stopped: %v\", err)\n\t\t\t} else {\n\t\t\t\tlogger.Errorf(\"Failed to serve: %v\", err)\n\t\t\t}\n\t\t}\n\t}()\n\n\t<-shutdownCh\n\tserver.GracefulStop()\n}\n\nfunc handleInspectStatus(driver driver.Driver) {\n\tdecoder := json.NewDecoder(os.Stdin)\n\tencoder := json.NewEncoder(os.Stdout)\n\n\tvar payload []byte\n\tif err := decoder.Decode(&payload); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to decode instance payload from stdin: %v\", err)\n\t}\n\n\tvar inst limatype.Instance\n\tif err := inst.UnmarshalJSON(payload); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to unmarshal instance: %v\", err)\n\t}\n\n\tstatus := driver.InspectStatus(context.Background(), &inst)\n\tinst.Status = status\n\n\tresp, err := inst.MarshalJSON()\n\tif err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to marshal instance response: %v\", err)\n\t}\n\n\tif err := encoder.Encode(resp); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to encode instance response: %v\", err)\n\t}\n}\n\nfunc handlePreConfiguredDriverAction(ctx context.Context, driver driver.Driver) {\n\tdecoder := json.NewDecoder(os.Stdin)\n\tencoder := json.NewEncoder(os.Stdout)\n\n\tvar payload limatype.PreConfiguredDriverPayload\n\tif err := decoder.Decode(&payload); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to decode pre-configured driver payload from stdin: %v\", err)\n\t}\n\n\tconfig := &payload.Config\n\tif err := driver.FillConfig(ctx, config, payload.FilePath); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Failed to fill config: %v\", err)\n\t}\n\n\tif err := encoder.Encode(*config); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error encoding response: %v\", err)\n\t}\n}\n\n// Start begins the driver startup process. It sends an initial response to unblock\n// the client and then streams subsequent errors(if any), as the driver initializes.\n// A final success message is streamed upon successful completion.\nfunc Start(extDriver *registry.ExternalDriver, instName string) error {\n\textDriver.Logger.Debugf(\"Starting external driver at %s\", extDriver.Path)\n\tif instName == \"\" {\n\t\treturn errors.New(\"instance name cannot be empty\")\n\t}\n\textDriver.InstanceName = instName\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tcmd := exec.CommandContext(ctx, extDriver.Path)\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\tcancel()\n\t\treturn fmt.Errorf(\"failed to create stdout pipe for external driver: %w\", err)\n\t}\n\n\tinstanceDir, err := dirnames.InstanceDir(extDriver.InstanceName)\n\tif err != nil {\n\t\tcancel()\n\t\treturn fmt.Errorf(\"failed to determine instance directory: %w\", err)\n\t}\n\tlogPath := filepath.Join(instanceDir, filenames.ExternalDriverStderrLog)\n\tlogFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)\n\tif err != nil {\n\t\tcancel()\n\t\treturn fmt.Errorf(\"failed to open external driver log file: %w\", err)\n\t}\n\n\tcmd.Stderr = logFile\n\n\tif err := cmd.Start(); err != nil {\n\t\tcancel()\n\t\treturn fmt.Errorf(\"failed to start external driver: %w\", err)\n\t}\n\n\tdriverLogger := extDriver.Logger.WithField(\"driver\", extDriver.Name)\n\n\tscanner := bufio.NewScanner(stdout)\n\tvar socketPath string\n\tif scanner.Scan() {\n\t\tvar output map[string]string\n\t\tif err := json.Unmarshal(scanner.Bytes(), &output); err != nil {\n\t\t\tcancel()\n\t\t\tif err := cmd.Process.Kill(); err != nil {\n\t\t\t\tdriverLogger.Errorf(\"Failed to kill external driver process: %v\", err)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"failed to parse socket path JSON: %w\", err)\n\t\t}\n\t\tsocketPath = output[\"socketPath\"]\n\t} else {\n\t\tcancel()\n\t\tif err := cmd.Process.Kill(); err != nil {\n\t\t\tdriverLogger.Errorf(\"Failed to kill external driver process: %v\", err)\n\t\t}\n\t\treturn errors.New(\"failed to read socket path from driver\")\n\t}\n\textDriver.SocketPath = socketPath\n\n\tdriverClient, err := client.NewDriverClient(extDriver.SocketPath, extDriver.Logger)\n\tif err != nil {\n\t\tcancel()\n\t\tif err := cmd.Process.Kill(); err != nil {\n\t\t\tdriverLogger.Errorf(\"Failed to kill external driver process after client creation failure: %v\", err)\n\t\t}\n\t\treturn fmt.Errorf(\"failed to create driver client: %w\", err)\n\t}\n\n\textDriver.Command = cmd\n\textDriver.Client = driverClient\n\textDriver.Ctx = ctx\n\textDriver.CancelFunc = cancel\n\n\tdriverLogger.Debugf(\"External driver %s started successfully\", extDriver.Name)\n\treturn nil\n}\n\nfunc Stop(extDriver *registry.ExternalDriver) error {\n\tif extDriver.Command == nil {\n\t\treturn fmt.Errorf(\"external driver %s is not running\", extDriver.Name)\n\t}\n\n\textDriver.Logger.Debugf(\"Stopping external driver %s\", extDriver.Name)\n\tif extDriver.CancelFunc != nil {\n\t\textDriver.CancelFunc()\n\t}\n\tif err := extDriver.Command.Process.Signal(syscall.SIGTERM); err != nil {\n\t\textDriver.Logger.Errorf(\"Failed to kill external driver process: %v\", err)\n\t}\n\tif err := os.Remove(extDriver.SocketPath); err != nil && !os.IsNotExist(err) {\n\t\textDriver.Logger.Warnf(\"Failed to remove socket file: %v\", err)\n\t}\n\n\textDriver.Command = nil\n\textDriver.Client = nil\n\textDriver.Ctx = nil\n\textDriver.CancelFunc = nil\n\n\textDriver.Logger.Debugf(\"External driver %s stopped successfully\", extDriver.Name)\n\treturn nil\n}\n\nfunc StopAllExternalDrivers() {\n\tfor name, driver := range registry.ExternalDrivers {\n\t\tif driver.Command != nil && driver.Command.Process != nil {\n\t\t\tif err := Stop(driver); err != nil {\n\t\t\t\tlogrus.Errorf(\"Failed to stop external driver %s: %v\", name, err)\n\t\t\t} else {\n\t\t\t\tlogrus.Debugf(\"External driver %s stopped successfully\", name)\n\t\t\t}\n\t\t}\n\t\tdelete(registry.ExternalDrivers, name)\n\t}\n}\n"
  },
  {
    "path": "pkg/driver/krunkit/boot.Linux/00-add-user-to-video-render-group.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux -o pipefail\n\nu=\"${LIMA_CIDATA_USER:-$USER}\"\ngetent group render >/dev/null 2>&1 || groupadd -f render\ngetent group video >/dev/null 2>&1 || groupadd -f video\nsudo usermod -aG render \"$u\" || true\nsudo usermod -aG video \"$u\" || true\n"
  },
  {
    "path": "pkg/driver/krunkit/boot.Linux/01-gpu-device-perms.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux -o pipefail\n\n# Make DRM render/card nodes world-accessible\ninstall -d -m 0755 /etc/udev/rules.d\ncat >/etc/udev/rules.d/70-lima-drm.rules <<'EOF'\nKERNEL==\"render[D]*\", SUBSYSTEM==\"drm\", MODE=\"0666\"\nKERNEL==\"card*\", SUBSYSTEM==\"drm\", MODE=\"0666\"\nEOF\n\n# Apply to existing nodes now and future ones via udev\nudevadm control --reload || true\nudevadm trigger --subsystem-match=drm || true\n\nif [ -d /dev/dri ]; then\n\tchmod 0666 /dev/dri/render[D]* 2>/dev/null || true\n\tchmod 0666 /dev/dri/card* 2>/dev/null || true\nfi\n"
  },
  {
    "path": "pkg/driver/krunkit/errors_darwin_arm64.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage krunkit\n\nimport \"errors\"\n\nvar errUnimplemented = errors.New(\"unimplemented by the krunkit driver\")\n"
  },
  {
    "path": "pkg/driver/krunkit/krunkit_darwin_arm64.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage krunkit\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver/vz\"\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nconst logLevelInfo = \"3\"\n\n// Cmdline constructs the command line arguments for krunkit based on the instance configuration.\nfunc Cmdline(inst *limatype.Instance) (*exec.Cmd, error) {\n\tmemBytes, err := units.RAMInBytes(*inst.Config.Memory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\targs := []string{\n\t\t// Memory in MiB\n\t\t\"--memory\", strconv.FormatInt(memBytes/units.MiB, 10),\n\t\t\"--cpus\", fmt.Sprintf(\"%d\", *inst.Config.CPUs),\n\t\t\"--device\", fmt.Sprintf(\"virtio-serial,logFilePath=%s\", filepath.Join(inst.Dir, filenames.SerialLog)),\n\t\t\"--krun-log-level\", logLevelInfo,\n\t\t\"--restful-uri\", \"none://\",\n\n\t\t// First virtio-blk device is the boot disk\n\t\t\"--device\", fmt.Sprintf(\"virtio-blk,path=%s,format=raw\", filepath.Join(inst.Dir, filenames.Disk)),\n\t\t\"--device\", fmt.Sprintf(\"virtio-blk,path=%s\", filepath.Join(inst.Dir, filenames.CIDataISO)),\n\t}\n\n\t// Add additional disks\n\tif len(inst.Config.AdditionalDisks) > 0 {\n\t\tctx := context.Background()\n\t\tdiskUtil := proxyimgutil.NewDiskUtil(ctx)\n\t\tfor _, d := range inst.Config.AdditionalDisks {\n\t\t\tdisk, derr := store.InspectDisk(d.Name, d.FSType)\n\t\t\tif derr != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to load disk %q: %w\", d.Name, derr)\n\t\t\t}\n\t\t\tif disk.Instance != \"\" {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to run attach disk %q, in use by instance %q\", disk.Name, disk.Instance)\n\t\t\t}\n\t\t\tif lerr := disk.Lock(inst.Dir); lerr != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to lock disk %q: %w\", d.Name, lerr)\n\t\t\t}\n\t\t\textraDiskPath := filepath.Join(disk.Dir, filenames.DataDisk)\n\t\t\tlogrus.Infof(\"Mounting disk %q on %q\", disk.Name, disk.MountPoint)\n\t\t\tif cerr := diskUtil.Convert(ctx, raw.Type, extraDiskPath, extraDiskPath, nil, true); cerr != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to convert extra disk %q to raw: %w\", extraDiskPath, cerr)\n\t\t\t}\n\t\t\targs = append(args, \"--device\", fmt.Sprintf(\"virtio-blk,path=%s,format=raw\", extraDiskPath))\n\t\t}\n\t}\n\n\t// Network commands\n\tnetworkArgs, err := buildNetworkArgs(inst)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to build network arguments: %w\", err)\n\t}\n\n\t// File sharing commands\n\tif *inst.Config.MountType == limatype.VIRTIOFS {\n\t\tfor _, mount := range inst.Config.Mounts {\n\t\t\tif _, err := os.Stat(mount.Location); errors.Is(err, os.ErrNotExist) {\n\t\t\t\tif err := os.MkdirAll(mount.Location, 0o750); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\ttag := limayaml.MountTag(mount.Location, *mount.MountPoint)\n\t\t\tmountArg := fmt.Sprintf(\"virtio-fs,sharedDir=%s,mountTag=%s\", mount.Location, tag)\n\t\t\targs = append(args, \"--device\", mountArg)\n\t\t}\n\t}\n\n\targs = append(args, networkArgs...)\n\n\tif inst.Config.NestedVirtualization != nil && *inst.Config.NestedVirtualization {\n\t\targs = append(args, \"--nested\")\n\t}\n\n\tcmd := exec.CommandContext(context.Background(), vmType, args...)\n\n\treturn cmd, nil\n}\n\nfunc buildNetworkArgs(inst *limatype.Instance) ([]string, error) {\n\tvar args []string\n\n\t// Configure default usernetwork with limayaml.MACAddress(inst.Dir) for eth0 interface\n\tfirstUsernetIndex := limayaml.FirstUsernetIndex(inst.Config)\n\tif firstUsernetIndex == -1 {\n\t\t// slirp network using gvisor netstack\n\t\tkrunkitSock, err := usernet.SockWithDirectory(inst.Dir, \"\", usernet.FDSock)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tclient, err := vz.PassFDToUnix(krunkitSock)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\targs = append(args, \"--device\", fmt.Sprintf(\"virtio-net,type=unixgram,fd=%d,mac=%s\", client.Fd(), limayaml.MACAddress(inst.Dir)))\n\t}\n\n\tfor _, nw := range inst.Networks {\n\t\tvar sock string\n\t\tvar mac string\n\t\tif nw.Lima != \"\" {\n\t\t\tnwCfg, err := networks.LoadConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tswitch nw.Lima {\n\t\t\tcase networks.ModeUserV2:\n\t\t\t\tsock, err = usernet.Sock(nw.Lima, usernet.QEMUSock)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tmac = limayaml.MACAddress(inst.Dir)\n\t\t\tcase networks.ModeShared, networks.ModeBridged:\n\t\t\t\tsocketVMNetInstalled, err := nwCfg.IsDaemonInstalled(networks.SocketVMNet)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif !socketVMNetInstalled {\n\t\t\t\t\treturn nil, errors.New(\"socket_vmnet is not installed\")\n\t\t\t\t}\n\t\t\t\tsock, err = networks.Sock(nw.Lima)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tmac = nw.MACAddress\n\t\t\tdefault:\n\t\t\t\treturn nil, fmt.Errorf(\"invalid network spec %+v\", nw)\n\t\t\t}\n\t\t} else if nw.Socket != \"\" {\n\t\t\tsock = nw.Socket\n\t\t\tmac = nw.MACAddress\n\t\t} else {\n\t\t\treturn nil, fmt.Errorf(\"invalid network spec %+v\", nw)\n\t\t}\n\n\t\tdevice := fmt.Sprintf(\"virtio-net,type=unixstream,path=%s,mac=%s\", sock, mac)\n\t\targs = append(args, \"--device\", device)\n\t}\n\n\tif len(args) == 0 {\n\t\treturn args, errors.New(\"no socket_vmnet networks defined\")\n\t}\n\n\treturn args, nil\n}\n\nfunc startUsernet(ctx context.Context, inst *limatype.Instance) (*usernet.Client, context.CancelFunc, error) {\n\tif firstUsernetIndex := limayaml.FirstUsernetIndex(inst.Config); firstUsernetIndex != -1 {\n\t\treturn usernet.NewClientByName(inst.Config.Networks[firstUsernetIndex].Lima), nil, nil\n\t}\n\t// Start a in-process gvisor-tap-vsock\n\tendpointSock, err := usernet.SockWithDirectory(inst.Dir, \"\", usernet.EndpointSock)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tkrunkitSock, err := usernet.SockWithDirectory(inst.Dir, \"\", usernet.FDSock)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tos.RemoveAll(endpointSock)\n\tos.RemoveAll(krunkitSock)\n\tctx, cancel := context.WithCancel(ctx)\n\terr = usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{\n\t\tMTU:      1500,\n\t\tEndpoint: endpointSock,\n\t\tFdSocket: krunkitSock,\n\t\tAsync:    true,\n\t\tDefaultLeases: map[string]string{\n\t\t\tnetworks.SlirpIPAddress: limayaml.MACAddress(inst.Dir),\n\t\t},\n\t\tSubnet: networks.SlirpNetwork,\n\t})\n\tif err != nil {\n\t\tdefer cancel()\n\t\treturn nil, nil, err\n\t}\n\tsubnetIP, _, err := net.ParseCIDR(networks.SlirpNetwork)\n\treturn usernet.NewClient(endpointSock, subnetIP), cancel, err\n}\n"
  },
  {
    "path": "pkg/driver/krunkit/krunkit_driver_darwin_arm64.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage krunkit\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/executil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n)\n\ntype LimaKrunkitDriver struct {\n\tInstance     *limatype.Instance\n\tSSHLocalPort int\n\n\tusernetClient *usernet.Client\n\tstopUsernet   context.CancelFunc\n\tkrunkitCmd    *exec.Cmd\n\tkrunkitWaitCh chan error\n}\n\nvar (\n\t_      driver.Driver   = (*LimaKrunkitDriver)(nil)\n\tvmType limatype.VMType = \"krunkit\"\n)\n\nfunc New() *LimaKrunkitDriver {\n\treturn &LimaKrunkitDriver{}\n}\n\nfunc (l *LimaKrunkitDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriver {\n\tl.Instance = inst\n\tl.SSHLocalPort = inst.SSHLocalPort\n\n\treturn &driver.ConfiguredDriver{\n\t\tDriver: l,\n\t}\n}\n\nfunc (l *LimaKrunkitDriver) CreateDisk(ctx context.Context) error {\n\t// Krunkit also supports qcow2 disks but raw is faster to create and use.\n\treturn driverutil.EnsureDisk(ctx, l.Instance.Dir, *l.Instance.Config.Disk, raw.Type)\n}\n\nfunc (l *LimaKrunkitDriver) Start(ctx context.Context) (chan error, error) {\n\tif l.Instance.Config.SSH.OverVsock != nil && *l.Instance.Config.SSH.OverVsock {\n\t\tlogrus.Warn(\".ssh.overVsock is not implemented yet for krunkit driver\")\n\t}\n\n\tvar err error\n\tl.usernetClient, l.stopUsernet, err = startUsernet(ctx, l.Instance)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to start usernet: %w\", err)\n\t}\n\n\tkrunkitCmd, err := Cmdline(l.Instance)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to construct krunkit command line: %w\", err)\n\t}\n\t// Detach krunkit process from parent Lima process\n\tkrunkitCmd.SysProcAttr = executil.BackgroundSysProcAttr\n\n\tlogPath := filepath.Join(l.Instance.Dir, \"krunkit.log\")\n\tlogfile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open krunkit logfile: %w\", err)\n\t}\n\tkrunkitCmd.Stderr = logfile\n\n\tlogrus.Infof(\"Starting krun VM (hint: to watch the progress, see %q)\", logPath)\n\tlogrus.Infof(\"krunkitCmd.Args: %v\", krunkitCmd.Args)\n\n\tif err := krunkitCmd.Start(); err != nil {\n\t\tlogfile.Close()\n\t\treturn nil, errors.New(\"failed to start krunkitCmd\")\n\t}\n\n\tpidPath := filepath.Join(l.Instance.Dir, filenames.PIDFile(*l.Instance.Config.VMType))\n\tif err := os.WriteFile(pidPath, fmt.Appendf(nil, \"%d\\n\", krunkitCmd.Process.Pid), 0o644); err != nil {\n\t\tlogrus.WithError(err).Warn(\"Failed to write PID file\")\n\t}\n\n\tl.krunkitCmd = krunkitCmd\n\tl.krunkitWaitCh = make(chan error, 1)\n\tgo func() {\n\t\tdefer func() {\n\t\t\tlogfile.Close()\n\t\t\tos.RemoveAll(pidPath)\n\t\t\tclose(l.krunkitWaitCh)\n\t\t}()\n\t\tl.krunkitWaitCh <- krunkitCmd.Wait()\n\t}()\n\n\terr = l.usernetClient.ConfigureDriver(ctx, l.Instance, l.SSHLocalPort)\n\tif err != nil {\n\t\tl.krunkitWaitCh <- fmt.Errorf(\"failed to configure usernet: %w\", err)\n\t}\n\n\treturn l.krunkitWaitCh, nil\n}\n\nfunc (l *LimaKrunkitDriver) Stop(_ context.Context) error {\n\tif l.krunkitCmd == nil {\n\t\treturn nil\n\t}\n\n\tif err := l.krunkitCmd.Process.Signal(syscall.SIGTERM); err != nil {\n\t\tlogrus.WithError(err).Warn(\"Failed to send interrupt signal\")\n\t}\n\n\tgo func() {\n\t\tif l.usernetClient != nil {\n\t\t\t_ = l.usernetClient.UnExposeSSH(l.Instance.SSHLocalPort)\n\t\t}\n\t\tif l.stopUsernet != nil {\n\t\t\tl.stopUsernet()\n\t\t}\n\t}()\n\n\ttimeout := time.After(30 * time.Second)\n\tselect {\n\tcase <-l.krunkitWaitCh:\n\t\treturn nil\n\tcase <-timeout:\n\t\tif err := l.krunkitCmd.Process.Kill(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t<-l.krunkitWaitCh\n\t\treturn nil\n\t}\n}\n\nfunc (l *LimaKrunkitDriver) Validate(_ context.Context) error {\n\treturn validateConfig(l.Instance.Config)\n}\n\nfunc validateConfig(cfg *limatype.LimaYAML) error {\n\tif cfg == nil {\n\t\treturn errors.New(\"configuration is nil\")\n\t}\n\tmacOSProductVersion, err := osutil.ProductVersion()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif macOSProductVersion.LessThan(*semver.New(\"13.0.0\")) {\n\t\treturn errors.New(\"krunkit driver requires macOS 13 or higher to run\")\n\t}\n\tif cfg.Arch != nil && !limatype.IsNativeArch(*cfg.Arch) {\n\t\treturn fmt.Errorf(\"unsupported arch: %q (krunkit requires native arch)\", *cfg.Arch)\n\t}\n\tif _, err := exec.LookPath(vmType); err != nil {\n\t\treturn errors.New(\"krunkit CLI not found in PATH. Install it via:\\nbrew tap slp/krunkit\\nbrew install krunkit\")\n\t}\n\n\tif cfg.MountType != nil && (*cfg.MountType != limatype.VIRTIOFS && *cfg.MountType != limatype.REVSSHFS) {\n\t\treturn fmt.Errorf(\"field `mountType` must be %q or %q for krunkit driver, got %q\", limatype.VIRTIOFS, limatype.REVSSHFS, *cfg.MountType)\n\t}\n\n\tif cfg.NestedVirtualization != nil && *cfg.NestedVirtualization {\n\t\tif macOSProductVersion.LessThan(*semver.New(\"15.0.0\")) {\n\t\t\treturn errors.New(\"nested virtualization requires macOS 15 or newer\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc isFedoraConfigured(cfg *limatype.LimaYAML) bool {\n\tfor _, b := range cfg.Base {\n\t\tif strings.Contains(strings.ToLower(b.URL), \"fedora\") {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor _, img := range cfg.Images {\n\t\tif strings.Contains(strings.ToLower(img.Location), \"fedora\") {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (l *LimaKrunkitDriver) FillConfig(_ context.Context, cfg *limatype.LimaYAML, _ string) error {\n\tif cfg.MountType == nil {\n\t\tcfg.MountType = ptr.Of(limatype.VIRTIOFS)\n\t} else {\n\t\t*cfg.MountType = limatype.VIRTIOFS\n\t}\n\n\tif cfg.Arch == nil {\n\t\tcfg.Arch = ptr.Of(limatype.AARCH64)\n\t} else {\n\t\t*cfg.Arch = limatype.AARCH64\n\t}\n\n\tcfg.VMType = ptr.Of(vmType)\n\n\treturn validateConfig(cfg)\n}\n\n//go:embed boot.Linux/*.sh\nvar bootLinuxFS embed.FS\n\nfunc (l *LimaKrunkitDriver) BootScripts() (map[string][]byte, error) {\n\tscripts := make(map[string][]byte)\n\n\tentries, err := bootLinuxFS.ReadDir(\"boot.Linux\")\n\tif err == nil && !isFedoraConfigured(l.Instance.Config) {\n\t\tfor _, entry := range entries {\n\t\t\tif entry.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tentryPath := \"boot.Linux/\" + entry.Name()\n\n\t\t\tcontent, err := bootLinuxFS.ReadFile(entryPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tscripts[entryPath] = content\n\t\t}\n\t}\n\n\t// Disabled by krunkit driver for Fedora to make boot time faster\n\tif isFedoraConfigured(l.Instance.Config) {\n\t\tscripts[\"boot.Linux/00-reboot-if-required.sh\"] = []byte(`#!/bin/sh\nset -eu\nexit 0\n`)\n\t}\n\n\treturn scripts, nil\n}\n\nfunc (l *LimaKrunkitDriver) Create(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaKrunkitDriver) Info() driver.Info {\n\tvar info driver.Info\n\tinfo.Name = vmType\n\tif l.Instance != nil && l.Instance.Dir != \"\" {\n\t\tinfo.InstanceDir = l.Instance.Dir\n\t}\n\n\tinfo.Features = driver.DriverFeatures{\n\t\tDynamicSSHAddress:    false,\n\t\tSkipSocketForwarding: false,\n\t\tCanRunGUI:            false,\n\t}\n\treturn info\n}\n\nfunc (l *LimaKrunkitDriver) SSHAddress(_ context.Context) (string, error) {\n\treturn \"127.0.0.1\", nil\n}\n\nfunc (l *LimaKrunkitDriver) ForwardGuestAgent() bool {\n\treturn true\n}\n\nfunc (l *LimaKrunkitDriver) Delete(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaKrunkitDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string {\n\treturn \"\"\n}\n\nfunc (l *LimaKrunkitDriver) RunGUI() error {\n\treturn nil\n}\n\nfunc (l *LimaKrunkitDriver) ChangeDisplayPassword(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaKrunkitDriver) DisplayConnection(_ context.Context) (string, error) {\n\treturn \"\", errUnimplemented\n}\n\nfunc (l *LimaKrunkitDriver) CreateSnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaKrunkitDriver) ApplySnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaKrunkitDriver) DeleteSnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaKrunkitDriver) ListSnapshots(_ context.Context) (string, error) {\n\treturn \"\", errUnimplemented\n}\n\nfunc (l *LimaKrunkitDriver) Register(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaKrunkitDriver) Unregister(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaKrunkitDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) {\n\treturn nil, \"unix\", nil\n}\n\nfunc (l *LimaKrunkitDriver) AdditionalSetupForSSH(_ context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driver/qemu/entitlementutil/entitlementutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package entitlementutil provides a workaround for https://github.com/lima-vm/lima/issues/1742\npackage entitlementutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/uiutil\"\n)\n\n// IsSigned returns an error if the binary is not signed, or the sign is invalid,\n// or not associated with the \"com.apple.security.hypervisor\" entitlement.\nfunc IsSigned(ctx context.Context, qExe string) error {\n\tcmd := exec.CommandContext(ctx, \"codesign\", \"--verify\", qExe)\n\tout, err := cmd.CombinedOutput()\n\tlogrus.WithError(err).Debugf(\"Executed %v: out=%q\", cmd.Args, string(out))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w (out=%q)\", cmd.Args, err, string(out))\n\t}\n\n\tcmd = exec.CommandContext(ctx, \"codesign\", \"--display\", \"--entitlements\", \"-\", \"--xml\", qExe)\n\tout, err = cmd.CombinedOutput()\n\tlogrus.WithError(err).Debugf(\"Executed %v: out=%q\", cmd.Args, string(out))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w (out=%q)\", cmd.Args, err, string(out))\n\t}\n\tif !strings.Contains(string(out), \"com.apple.security.hypervisor\") {\n\t\treturn fmt.Errorf(\"binary %q seems signed but lacking the \\\"com.apple.security.hypervisor\\\" entitlement\", qExe)\n\t}\n\treturn nil\n}\n\nfunc Sign(ctx context.Context, qExe string) error {\n\tent, err := os.CreateTemp(\"\", \"lima-qemu-entitlements-*.xml\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create a temporary file for signing QEMU binary: %w\", err)\n\t}\n\tentName := ent.Name()\n\tdefer os.RemoveAll(entName)\n\tconst entXML = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.hypervisor</key>\n    <true/>\n  </dict>\n</plist>`\n\tif _, err = ent.WriteString(entXML); err != nil {\n\t\tent.Close()\n\t\treturn fmt.Errorf(\"failed to write to a temporary file %q for signing QEMU binary: %w\", entName, err)\n\t}\n\tent.Close()\n\tsignCmd := exec.CommandContext(ctx, \"codesign\", \"--sign\", \"-\", \"--entitlements\", entName, \"--force\", qExe)\n\tout, err := signCmd.CombinedOutput()\n\tlogrus.WithError(err).Debugf(\"Executed %v: out=%q\", signCmd.Args, string(out))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w (out=%q)\", signCmd.Args, err, string(out))\n\t}\n\treturn nil\n}\n\n// isColimaWrapper__useThisFunctionOnlyForPrintingHints returns true\n// if qExe is like \"/Users/<USER>/.colima/_wrapper/4e1b408f843d1c63afbbdcf80c40e4c88d33509f/bin/qemu-system-x86_64\".\n//\n// The result can be used *ONLY* for controlling hint messages.\n// DO NOT change the behavior of Lima depending on this result.\n//\n//nolint:revive,staticcheck // underscores in this function name intentionally added\nfunc isColimaWrapper__useThisFunctionOnlyForPrintingHints__(qExe string) bool {\n\treturn strings.Contains(qExe, \"/.colima/_wrapper/\")\n}\n\n// AskToSignIfNotSignedProperly asks to sign the QEMU binary with the \"com.apple.security.hypervisor\" entitlement.\n//\n// On Homebrew, QEMU binaries are usually already signed, but Homebrew's signing infrastructure is broken for Intel as of August 2023.\n// https://github.com/lima-vm/lima/issues/1742\nfunc AskToSignIfNotSignedProperly(ctx context.Context, qExe string) {\n\tif isSignedErr := IsSigned(ctx, qExe); isSignedErr != nil {\n\t\tlogrus.WithError(isSignedErr).Warnf(\"QEMU binary %q does not seem properly signed with the \\\"com.apple.security.hypervisor\\\" entitlement\", qExe)\n\t\tif isColimaWrapper__useThisFunctionOnlyForPrintingHints__(qExe) {\n\t\t\tlogrus.Info(\"Hint: the warning above is usually negligible for colima ( Printed due to https://github.com/abiosoft/colima/issues/796 )\")\n\t\t}\n\t\tvar ans bool\n\t\tif isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {\n\t\t\tmessage := fmt.Sprintf(\"Try to sign %q with the \\\"com.apple.security.hypervisor\\\" entitlement?\", qExe)\n\t\t\tvar askErr error\n\t\t\tans, askErr = uiutil.Confirm(message, true)\n\t\t\tif askErr != nil {\n\t\t\t\tlogrus.WithError(askErr).Warn(\"No answer was given\")\n\t\t\t}\n\t\t}\n\t\tif ans {\n\t\t\tif signErr := Sign(ctx, qExe); signErr != nil {\n\t\t\t\tlogrus.WithError(signErr).Warnf(\"Failed to sign %q\", qExe)\n\t\t\t} else {\n\t\t\t\tlogrus.Infof(\"Successfully signed %q with the \\\"com.apple.security.hypervisor\\\" entitlement\", qExe)\n\t\t\t}\n\t\t} else {\n\t\t\tlogrus.Warn(\"If QEMU does not start up, you may have to sign the QEMU binary with the \\\"com.apple.security.hypervisor\\\" entitlement manually. See https://github.com/lima-vm/lima/issues/1742 .\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/driver/qemu/qemu.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage qemu\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/digitalocean/go-qemu/qmp\"\n\t\"github.com/digitalocean/go-qemu/qmp/raw\"\n\t\"github.com/docker/go-units\"\n\t\"github.com/mattn/go-shellwords\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/fileutils\"\n\t\"github.com/lima-vm/lima/v2/pkg/iso9660util\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/qemuimgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\ntype Config struct {\n\tName         string\n\tInstanceDir  string\n\tLimaYAML     *limatype.LimaYAML\n\tSSHLocalPort int\n\tSSHAddress   string\n\tVirtioGA     bool\n}\n\n// minimumQemuVersion returns hardMin and softMin.\n//\n// hardMin is the hard minimum version of QEMU.\n// The driver immediately returns the error when QEMU is older than this version.\n//\n// softMin is the oldest recommended version of QEMU.\n// softMin must be >= hardMin.\n//\n// When updating this function, make sure to update\n// `website/content/en/docs/config/vmtype/qemu.md` too.\nfunc minimumQemuVersion() (hardMin, softMin semver.Version) {\n\tvar h, s string\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tswitch runtime.GOARCH {\n\t\tcase \"arm64\":\n\t\t\t// https://gitlab.com/qemu-project/qemu/-/issues/1990\n\t\t\th, s = \"8.2.1\", \"8.2.1\"\n\t\tdefault:\n\t\t\t// The code specific to QEMU < 7.0 on macOS (https://github.com/lima-vm/lima/pull/703)\n\t\t\t// was removed in https://github.com/lima-vm/lima/pull/3491\n\t\t\th, s = \"7.0.0\", \"8.2.1\"\n\t\t}\n\tdefault:\n\t\t// hardMin: Untested and maybe does not even work.\n\t\t// softMin: Ubuntu 22.04's QEMU. The oldest version that can be easily tested on GitHub Actions.\n\t\th, s = \"4.0.0\", \"6.2.0\"\n\t}\n\thardMin, softMin = *semver.New(h), *semver.New(s)\n\tif softMin.LessThan(hardMin) {\n\t\t// NOTREACHED\n\t\tlogrus.Fatalf(\"internal error: QEMU: soft minimum version %v must be >= hard minimum version %v\",\n\t\t\tsoftMin, hardMin)\n\t}\n\treturn hardMin, softMin\n}\n\n// EnsureDisk creates the VM disk from the downloaded image.\n// For ISO images, it renames the image to \"iso\" and creates an empty qcow2 disk.\n// For non-ISO images, it validates and renames the image to \"disk\".\nfunc EnsureDisk(ctx context.Context, cfg Config) error {\n\tdiskPath := filepath.Join(cfg.InstanceDir, filenames.Disk)\n\tif _, err := os.Stat(diskPath); err == nil || !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\timagePath := filepath.Join(cfg.InstanceDir, filenames.Image)\n\tisISO, err := iso9660util.IsISO9660(imagePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\timageInfo, err := qemuimgutil.GetInfo(ctx, imagePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get the information of %q: %w\", imagePath, err)\n\t}\n\tif err = qemuimgutil.AcceptableAsBaseDisk(imageInfo); err != nil {\n\t\treturn fmt.Errorf(\"file %q is not acceptable as a disk image: %w\", imagePath, err)\n\t}\n\tif imageInfo.Format == \"\" {\n\t\treturn fmt.Errorf(\"failed to inspect the format of %q\", imagePath)\n\t}\n\tif isISO {\n\t\tisoPath := filepath.Join(cfg.InstanceDir, filenames.ISO)\n\t\tif err = os.Rename(imagePath, isoPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdiskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk)\n\t\targs := []string{\"create\", \"-f\", \"qcow2\", diskPath, strconv.Itoa(int(diskSize))}\n\t\tcmd := exec.CommandContext(ctx, \"qemu-img\", args...)\n\t\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\t\t_ = os.Rename(isoPath, imagePath)\n\t\t\treturn fmt.Errorf(\"failed to run %v: %q: %w\", cmd.Args, string(out), err)\n\t\t}\n\t} else {\n\t\tif err = os.Rename(imagePath, diskPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc newQmpClient(cfg Config) (*qmp.SocketMonitor, error) {\n\tqmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)\n\tqmpClient, err := qmp.NewSocketMonitor(\"unix\", qmpSock, 5*time.Second)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn qmpClient, nil\n}\n\nfunc sendHmpCommand(cfg Config, cmd, tag string) (string, error) {\n\tqmpClient, err := newQmpClient(cfg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := qmpClient.Connect(); err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func() { _ = qmpClient.Disconnect() }()\n\trawClient := raw.NewMonitor(qmpClient)\n\tlogrus.Infof(\"Sending HMP %s command\", cmd)\n\thmc := fmt.Sprintf(\"%s %s\", cmd, tag)\n\treturn rawClient.HumanMonitorCommand(hmc, nil)\n}\n\nfunc execImgCommand(ctx context.Context, cfg Config, args ...string) (string, error) {\n\tdiskPath := filepath.Join(cfg.InstanceDir, filenames.Disk)\n\targs = append(args, diskPath)\n\tlogrus.Debugf(\"Running qemu-img %v command\", args)\n\tcmd := exec.CommandContext(ctx, \"qemu-img\", args...)\n\tb, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(b), err\n}\n\nfunc Del(ctx context.Context, cfg Config, run bool, tag string) error {\n\tif run {\n\t\tout, err := sendHmpCommand(cfg, \"delvm\", tag)\n\t\t// there can still be output, even if no error!\n\t\tif out != \"\" {\n\t\t\tlogrus.Warnf(\"output: %s\", strings.TrimSpace(out))\n\t\t}\n\t\treturn err\n\t}\n\t// -d  deletes a snapshot\n\t_, err := execImgCommand(ctx, cfg, \"snapshot\", \"-d\", tag)\n\treturn err\n}\n\nfunc Save(ctx context.Context, cfg Config, run bool, tag string) error {\n\tif run {\n\t\tout, err := sendHmpCommand(cfg, \"savevm\", tag)\n\t\t// there can still be output, even if no error!\n\t\tif out != \"\" {\n\t\t\tlogrus.Warnf(\"output: %s\", strings.TrimSpace(out))\n\t\t}\n\t\treturn err\n\t}\n\t// -c  creates a snapshot\n\t_, err := execImgCommand(ctx, cfg, \"snapshot\", \"-c\", tag)\n\treturn err\n}\n\nfunc Load(ctx context.Context, cfg Config, run bool, tag string) error {\n\tif run {\n\t\tout, err := sendHmpCommand(cfg, \"loadvm\", tag)\n\t\t// there can still be output, even if no error!\n\t\tif out != \"\" {\n\t\t\tlogrus.Warnf(\"output: %s\", strings.TrimSpace(out))\n\t\t}\n\t\treturn err\n\t}\n\t// -a  applies a snapshot\n\t_, err := execImgCommand(ctx, cfg, \"snapshot\", \"-a\", tag)\n\treturn err\n}\n\n// List returns a space-separated list of all snapshots, with header and newlines.\nfunc List(ctx context.Context, cfg Config, run bool) (string, error) {\n\tif run {\n\t\tout, err := sendHmpCommand(cfg, \"info\", \"snapshots\")\n\t\tif err == nil {\n\t\t\tout = strings.ReplaceAll(out, \"\\r\", \"\")\n\t\t\tout = strings.Replace(out, \"List of snapshots present on all disks:\\n\", \"\", 1)\n\t\t\tout = strings.Replace(out, \"There is no snapshot available.\\n\", \"\", 1)\n\t\t}\n\t\treturn out, err\n\t}\n\t// -l  lists all snapshots\n\targs := []string{\"snapshot\", \"-l\"}\n\tout, err := execImgCommand(ctx, cfg, args...)\n\tif err == nil {\n\t\t// remove the redundant heading, result is not machine-parseable\n\t\tout = strings.Replace(out, \"Snapshot list:\\n\", \"\", 1)\n\t}\n\treturn out, err\n}\n\nfunc argValue(args []string, key string) (string, bool) {\n\tif !strings.HasPrefix(key, \"-\") {\n\t\tpanic(fmt.Errorf(\"got unexpected key %q\", key))\n\t}\n\tfor i, s := range args {\n\t\tif s == key {\n\t\t\tif i == len(args)-1 {\n\t\t\t\treturn \"\", true\n\t\t\t}\n\t\t\tvalue := args[i+1]\n\t\t\tif strings.HasPrefix(value, \"-\") {\n\t\t\t\treturn \"\", true\n\t\t\t}\n\t\t\treturn value, true\n\t\t}\n\t}\n\treturn \"\", false\n}\n\n// appendArgsIfNoConflict can be used for: -cpu, -machine, -m, -boot ...\n// appendArgsIfNoConflict cannot be used for: -drive, -cdrom, ...\nfunc appendArgsIfNoConflict(args []string, k, v string) []string {\n\tif !strings.HasPrefix(k, \"-\") {\n\t\tpanic(fmt.Errorf(\"got unexpected key %q\", k))\n\t}\n\tswitch k {\n\tcase \"-drive\", \"-cdrom\", \"-chardev\", \"-blockdev\", \"-netdev\", \"-device\":\n\t\tpanic(fmt.Errorf(\"appendArgsIfNoConflict() must not be called with k=%q\", k))\n\t}\n\n\tif v == \"\" {\n\t\tif _, ok := argValue(args, k); ok {\n\t\t\treturn args\n\t\t}\n\t\treturn append(args, k)\n\t}\n\n\tif origV, ok := argValue(args, k); ok {\n\t\tlogrus.Warnf(\"Not adding QEMU argument %q %q, as it conflicts with %q %q\", k, v, k, origV)\n\t\treturn args\n\t}\n\treturn append(args, k, v)\n}\n\ntype features struct {\n\t// AccelHelp is the output of `qemu-system-x86_64 -accel help`\n\t// e.g. \"Accelerators supported in QEMU binary:\\ntcg\\nhax\\nhvf\\n\"\n\t// Not machine-readable, but checking strings.Contains() should be fine.\n\tAccelHelp []byte\n\t// NetdevHelp is the output of `qemu-system-x86_64 -netdev help`\n\t// e.g. \"Available netdev backend types:\\nsocket\\nhubport\\ntap\\nuser\\nvde\\nbridge\\vhost-user\\n\"\n\t// Not machine-readable, but checking strings.Contains() should be fine.\n\tNetdevHelp []byte\n\t// MachineHelp is the output of `qemu-system-x86_64 -machine help`\n\t// e.g. \"Supported machines are:\\nakita...\\n...virt-6.2...\\n...virt-7.0...\\n...\\n\"\n\t// Not machine-readable, but checking strings.Contains() should be fine.\n\tMachineHelp []byte\n\t// CPUHelp is the output of `qemu-system-x86_64 -cpu help`\n\t// e.g. \"Available CPUs:\\n...\\nx86 base...\\nx86 host...\\n...\\n\"\n\t// Not machine-readable, but checking strings.Contains() should be fine.\n\tCPUHelp []byte\n}\n\nfunc inspectFeatures(ctx context.Context, exe, machine string) (*features, error) {\n\tvar (\n\t\tf      features\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\tcmd := exec.CommandContext(ctx, exe, \"-M\", \"none\", \"-accel\", \"help\")\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run %v: stdout=%q, stderr=%q\", cmd.Args, stdout.String(), stderr.String())\n\t}\n\tf.AccelHelp = stdout.Bytes()\n\t// on older versions qemu will write \"help\" output to stderr\n\tif len(f.AccelHelp) == 0 {\n\t\tf.AccelHelp = stderr.Bytes()\n\t}\n\n\tcmd = exec.CommandContext(ctx, exe, \"-M\", \"none\", \"-netdev\", \"help\")\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\tlogrus.Warnf(\"failed to run %v: stdout=%q, stderr=%q\", cmd.Args, stdout.String(), stderr.String())\n\t} else {\n\t\tf.NetdevHelp = stdout.Bytes()\n\t\tif len(f.NetdevHelp) == 0 {\n\t\t\tf.NetdevHelp = stderr.Bytes()\n\t\t}\n\t}\n\n\tcmd = exec.CommandContext(ctx, exe, \"-machine\", \"help\")\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\tlogrus.Warnf(\"failed to run %v: stdout=%q, stderr=%q\", cmd.Args, stdout.String(), stderr.String())\n\t} else {\n\t\tf.MachineHelp = stdout.Bytes()\n\t\tif len(f.MachineHelp) == 0 {\n\t\t\tf.MachineHelp = stderr.Bytes()\n\t\t}\n\t}\n\n\t// Avoid error: \"No machine specified, and there is no default\"\n\tcmd = exec.CommandContext(ctx, exe, \"-cpu\", \"help\", \"-machine\", machine)\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\tlogrus.Warnf(\"failed to run %v: stdout=%q, stderr=%q\", cmd.Args, stdout.String(), stderr.String())\n\t} else {\n\t\tf.CPUHelp = stdout.Bytes()\n\t\tif len(f.CPUHelp) == 0 {\n\t\t\tf.CPUHelp = stderr.Bytes()\n\t\t}\n\t}\n\n\treturn &f, nil\n}\n\n// adjustMemBytesDarwinARM64HVF adjusts the memory to be <= 3 GiB, only when the following conditions are met:\n//\n// - Host OS   <  macOS 12.4\n// - Host Arch == arm64\n// - Accel     == hvf\n//\n// This adjustment is required for avoiding host kernel panic. The issue was fixed in macOS 12.4 Beta 1.\n// See https://github.com/lima-vm/lima/issues/795 https://gitlab.com/qemu-project/qemu/-/issues/903#note_911000975\nfunc adjustMemBytesDarwinARM64HVF(memBytes int64, accel string) int64 {\n\tconst safeSize = 3 * 1024 * 1024 * 1024 // 3 GiB\n\tif memBytes <= safeSize {\n\t\treturn memBytes\n\t}\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn memBytes\n\t}\n\tif runtime.GOARCH != \"arm64\" {\n\t\treturn memBytes\n\t}\n\tif accel != \"hvf\" {\n\t\treturn memBytes\n\t}\n\tmacOSProductVersion, err := osutil.ProductVersion()\n\tif err != nil {\n\t\tlogrus.Warn(err)\n\t\treturn memBytes\n\t}\n\tif !macOSProductVersion.LessThan(*semver.New(\"12.4.0\")) {\n\t\treturn memBytes\n\t}\n\tlogrus.Warnf(\"Reducing the guest memory from %s to %s, to avoid host kernel panic on macOS <= 12.3; \"+\n\t\t\"Please update macOS to 12.4 or later; \"+\n\t\t\"See https://github.com/lima-vm/lima/issues/795 for the further background.\",\n\t\tunits.BytesSize(float64(memBytes)), units.BytesSize(float64(safeSize)))\n\tmemBytes = safeSize\n\treturn memBytes\n}\n\n// qemuMachine returns string to use for -machine.\nfunc qemuMachine(arch limatype.Arch) string {\n\tif arch == limatype.X8664 {\n\t\treturn \"q35\"\n\t}\n\treturn \"virt\"\n}\n\n// audioDevice returns the default audio device.\nfunc audioDevice() string {\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\treturn \"coreaudio\"\n\tcase \"linux\":\n\t\treturn \"pa\" // pulseaudio\n\tcase \"windows\":\n\t\treturn \"dsound\"\n\t}\n\treturn \"oss\"\n}\n\nfunc defaultCPUType() limatype.CPUType {\n\t// x86_64 + TCG + max was previously unstable until 2021.\n\t// https://bugzilla.redhat.com/show_bug.cgi?id=1999700\n\t// https://bugs.launchpad.net/qemu/+bug/1748296\n\tdefaultX8664 := \"max\"\n\tif runtime.GOOS == \"windows\" && runtime.GOARCH == \"amd64\" {\n\t\t// https://github.com/lima-vm/lima/pull/3487#issuecomment-2846253560\n\t\t// > #931 intentionally prevented the code from setting it to max when running on Windows,\n\t\t// > and kept it at qemu64.\n\t\t//\n\t\t// TODO: remove this if \"max\" works with the latest qemu\n\t\tdefaultX8664 = \"qemu64\"\n\t}\n\tcpuType := map[limatype.Arch]string{\n\t\tlimatype.AARCH64: \"max\",\n\t\tlimatype.ARMV7L:  \"max\",\n\t\tlimatype.X8664:   defaultX8664,\n\t\tlimatype.PPC64LE: \"max\",\n\t\tlimatype.RISCV64: \"max\",\n\t\tlimatype.S390X:   \"max\",\n\t}\n\tfor arch := range cpuType {\n\t\tif limatype.IsNativeArch(arch) && Accel(arch) != \"tcg\" {\n\t\t\tif hasHostCPU() {\n\t\t\t\tcpuType[arch] = \"host\"\n\t\t\t}\n\t\t}\n\t\tif arch == limatype.X8664 && runtime.GOOS == \"darwin\" {\n\t\t\t// disable AVX-512, since it requires trapping instruction faults in guest\n\t\t\t// Enterprise Linux requires either v2 (SSE4) or v3 (AVX2), but not yet v4.\n\t\t\tcpuType[arch] += \",-avx512vl\"\n\n\t\t\t// Disable pdpe1gb on Intel Mac\n\t\t\t// https://github.com/lima-vm/lima/issues/1485\n\t\t\t// https://stackoverflow.com/a/72863744/5167443\n\t\t\tcpuType[arch] += \",-pdpe1gb\"\n\t\t}\n\t}\n\treturn cpuType\n}\n\nfunc resolveCPUType(y *limatype.LimaYAML) string {\n\tcpuType := defaultCPUType()\n\tvar overrideCPUType bool\n\tvar qemuOpts limatype.QEMUOpts\n\tif err := limayaml.Convert(y.VMOpts[limatype.QEMU], &qemuOpts, \"vmOpts.qemu\"); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %q\", y.VMOpts[limatype.QEMU])\n\t}\n\tfor k, v := range qemuOpts.CPUType {\n\t\tif !slices.Contains(limatype.ArchTypes, *y.Arch) {\n\t\t\tlogrus.Warnf(\"field `vmOpts.qemu.cpuType` uses unsupported arch %q\", k)\n\t\t\tcontinue\n\t\t}\n\t\tif v != \"\" {\n\t\t\toverrideCPUType = true\n\t\t\tcpuType[k] = v\n\t\t}\n\t}\n\tif overrideCPUType {\n\t\tqemuOpts.CPUType = cpuType\n\t\tif y.VMOpts == nil {\n\t\t\ty.VMOpts = limatype.VMOpts{}\n\t\t}\n\t\ty.VMOpts[limatype.QEMU] = qemuOpts\n\t}\n\n\treturn cpuType[*y.Arch]\n}\n\nfunc Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err error) {\n\ty := cfg.LimaYAML\n\texe, args, err = Exe(*y.Arch)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tfeatures, err := inspectFeatures(ctx, exe, qemuMachine(*y.Arch))\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tversion, err := getQemuVersion(ctx, exe)\n\tif err != nil {\n\t\tlogrus.WithError(err).Warning(\"Failed to detect QEMU version\")\n\t} else {\n\t\tlogrus.Debugf(\"QEMU version %s detected\", version.String())\n\t\thardMin, softMin := minimumQemuVersion()\n\t\tif version.LessThan(hardMin) {\n\t\t\tlogrus.Fatalf(\"QEMU %v is too old, %v or later required\", version, hardMin)\n\t\t}\n\t\tif version.LessThan(softMin) {\n\t\t\tlogrus.Warnf(\"QEMU %v is too old, %v or later is recommended\", version, softMin)\n\t\t}\n\t\tvar qemuOpts limatype.QEMUOpts\n\t\tif err := limayaml.Convert(y.VMOpts[limatype.QEMU], &qemuOpts, \"vmOpts.qemu\"); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %q\", y.VMOpts[limatype.QEMU])\n\t\t}\n\t\tif qemuOpts.MinimumVersion != nil && version.LessThan(*semver.New(*qemuOpts.MinimumVersion)) {\n\t\t\tlogrus.Fatalf(\"QEMU %v is too old, template requires %q or later\", version, *qemuOpts.MinimumVersion)\n\t\t}\n\t}\n\n\t// Architecture\n\taccel := Accel(*y.Arch)\n\tif !strings.Contains(string(features.AccelHelp), accel) {\n\t\treturn \"\", nil, fmt.Errorf(\"accelerator %q is not supported by %s\", accel, exe)\n\t}\n\n\t// Memory\n\tmemBytes, err := units.RAMInBytes(*y.Memory)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tmemBytes = adjustMemBytesDarwinARM64HVF(memBytes, accel)\n\targs = appendArgsIfNoConflict(args, \"-m\", strconv.Itoa(int(memBytes>>20)))\n\n\tif *y.MountType == limatype.VIRTIOFS {\n\t\targs = appendArgsIfNoConflict(args, \"-object\",\n\t\t\tfmt.Sprintf(\"memory-backend-file,id=virtiofs-shm,size=%s,mem-path=/dev/shm,share=on\", strconv.Itoa(int(memBytes))))\n\t\targs = appendArgsIfNoConflict(args, \"-numa\", \"node,memdev=virtiofs-shm\")\n\t}\n\n\t// CPU\n\tcpu := resolveCPUType(y)\n\tif runtime.GOOS == \"darwin\" && runtime.GOARCH == \"amd64\" {\n\t\tswitch {\n\t\tcase strings.HasPrefix(cpu, \"host\"), strings.HasPrefix(cpu, \"max\"):\n\t\t\tif !strings.Contains(cpu, \",-pdpe1gb\") {\n\t\t\t\tlogrus.Warnf(\"On Intel Mac, CPU type %q typically needs \\\",-pdpe1gb\\\" option (https://stackoverflow.com/a/72863744/5167443)\", cpu)\n\t\t\t}\n\t\t}\n\t}\n\t// `qemu-system-ppc64 -help` does not show \"max\", but it is actually accepted\n\tif cpu != \"max\" && !strings.Contains(string(features.CPUHelp), strings.Split(cpu, \",\")[0]) {\n\t\treturn \"\", nil, fmt.Errorf(\"cpu %q is not supported by %s\", cpu, exe)\n\t}\n\targs = appendArgsIfNoConflict(args, \"-cpu\", cpu)\n\n\t// Machine\n\tswitch *y.Arch {\n\tcase limatype.X8664:\n\t\tswitch accel {\n\t\tcase \"tcg\":\n\t\t\t// use q35 machine with vmware io port disabled.\n\t\t\targs = appendArgsIfNoConflict(args, \"-machine\", \"q35,vmport=off\")\n\t\t\t// use tcg accelerator with multi threading with 512MB translation block size\n\t\t\t// https://qemu-project.gitlab.io/qemu/devel/multi-thread-tcg.html?highlight=tcg\n\t\t\t// https://qemu-project.gitlab.io/qemu/system/invocation.html?highlight=tcg%20opts\n\t\t\t// this will make sure each vCPU will be backed by 1 host user thread.\n\t\t\targs = appendArgsIfNoConflict(args, \"-accel\", \"tcg,thread=multi,tb-size=512\")\n\t\t\t// This will disable CPU S3/S4 state.\n\t\t\targs = append(args, \"-global\", \"ICH9-LPC.disable_s3=1\")\n\t\t\targs = append(args, \"-global\", \"ICH9-LPC.disable_s4=1\")\n\t\tcase \"whpx\":\n\t\t\t// whpx: injection failed, MSI (0, 0) delivery: 0, dest_mode: 0, trigger mode: 0, vector: 0\n\t\t\targs = appendArgsIfNoConflict(args, \"-machine\", \"q35,accel=\"+accel+\",kernel-irqchip=off\")\n\t\tdefault:\n\t\t\targs = appendArgsIfNoConflict(args, \"-machine\", \"q35,accel=\"+accel)\n\t\t}\n\tcase limatype.AARCH64:\n\t\tmachine := \"virt,accel=\" + accel\n\t\targs = appendArgsIfNoConflict(args, \"-machine\", machine)\n\tcase limatype.RISCV64:\n\t\t// https://github.com/tianocore/edk2/blob/edk2-stable202408/OvmfPkg/RiscVVirt/README.md#test\n\t\t// > Note: the `acpi=off` machine property is specified because Linux guest\n\t\t// > support for ACPI (that is, the ACPI consumer side) is a work in progress.\n\t\t// > Currently, `acpi=off` is recommended unless you are developing ACPI support\n\t\t// > yourself.\n\t\tmachine := \"virt,acpi=off,accel=\" + accel\n\t\targs = appendArgsIfNoConflict(args, \"-machine\", machine)\n\tcase limatype.ARMV7L:\n\t\tmachine := \"virt,accel=\" + accel\n\t\targs = appendArgsIfNoConflict(args, \"-machine\", machine)\n\tcase limatype.PPC64LE:\n\t\tmachine := \"pseries,accel=\" + accel\n\t\targs = appendArgsIfNoConflict(args, \"-machine\", machine)\n\tcase limatype.S390X:\n\t\tmachine := \"s390-ccw-virtio,accel=\" + accel\n\t\targs = appendArgsIfNoConflict(args, \"-machine\", machine)\n\t}\n\n\t// SMP\n\targs = appendArgsIfNoConflict(args, \"-smp\",\n\t\tfmt.Sprintf(\"%d,sockets=1,cores=%d,threads=1\", *y.CPUs, *y.CPUs))\n\n\t// Firmware\n\tlegacyBIOS := *y.Firmware.LegacyBIOS\n\tif legacyBIOS && *y.Arch != limatype.X8664 && *y.Arch != limatype.ARMV7L {\n\t\tlogrus.Warnf(\"field `firmware.legacyBIOS` is not supported for architecture %q, ignoring\", *y.Arch)\n\t\tlegacyBIOS = false\n\t}\n\tnoFirmware := *y.Arch == limatype.PPC64LE || *y.Arch == limatype.S390X || legacyBIOS\n\tif !noFirmware {\n\t\tvar firmware string\n\t\tfirmwareInBios := runtime.GOOS == \"windows\"\n\t\tif envVar := os.Getenv(\"_LIMA_QEMU_UEFI_IN_BIOS\"); envVar != \"\" {\n\t\t\tb, err := strconv.ParseBool(envVar)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Warnf(\"invalid _LIMA_QEMU_UEFI_IN_BIOS value %q\", envVar)\n\t\t\t} else {\n\t\t\t\tfirmwareInBios = b\n\t\t\t}\n\t\t}\n\t\tfirmwareInBios = firmwareInBios && *y.Arch == limatype.X8664\n\t\tdownloadedFirmware := filepath.Join(cfg.InstanceDir, filenames.QemuEfiCodeFD)\n\t\tfirmwareWithVars := filepath.Join(cfg.InstanceDir, filenames.QemuEfiFullFD)\n\t\tif firmwareInBios {\n\t\t\tif _, stErr := os.Stat(firmwareWithVars); stErr == nil {\n\t\t\t\tfirmware = firmwareWithVars\n\t\t\t\tlogrus.Infof(\"Using existing firmware (%q)\", firmware)\n\t\t\t}\n\t\t} else {\n\t\t\tif _, stErr := os.Stat(downloadedFirmware); errors.Is(stErr, os.ErrNotExist) {\n\t\t\tloop:\n\t\t\t\tfor _, f := range y.Firmware.Images {\n\t\t\t\t\tswitch f.VMType {\n\t\t\t\t\tcase \"\", limatype.QEMU:\n\t\t\t\t\t\tif f.Arch == *y.Arch {\n\t\t\t\t\t\t\tif _, err = fileutils.DownloadFile(ctx, downloadedFirmware, f.File, true, \"UEFI code \"+f.Location, *y.Arch); err != nil {\n\t\t\t\t\t\t\t\tlogrus.WithError(err).Warnf(\"failed to download %q\", f.Location)\n\t\t\t\t\t\t\t\tcontinue loop\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfirmware = downloadedFirmware\n\t\t\t\t\t\t\tlogrus.Infof(\"Using firmware %q (downloaded from %q)\", firmware, f.Location)\n\t\t\t\t\t\t\tbreak loop\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfirmware = downloadedFirmware\n\t\t\t\tlogrus.Infof(\"Using existing firmware (%q)\", firmware)\n\t\t\t}\n\t\t}\n\t\tif firmware == \"\" {\n\t\t\tfirmware, err = getFirmware(exe, *y.Arch)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\t\t\tlogrus.Infof(\"Using system firmware (%q)\", firmware)\n\t\t\tif firmwareInBios {\n\t\t\t\tfirmwareVars, err := getFirmwareVars(exe, *y.Arch)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\tlogrus.Infof(\"Using system firmware vars (%q)\", firmwareVars)\n\t\t\t\tvarsFile, err := os.Open(firmwareVars)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\tdefer varsFile.Close()\n\t\t\t\tcodeFile, err := os.Open(firmware)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\tdefer codeFile.Close()\n\t\t\t\tresultFile, err := os.OpenFile(firmwareWithVars, os.O_CREATE|os.O_WRONLY, 0o644)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\tdefer resultFile.Close()\n\t\t\t\t_, err = io.Copy(resultFile, varsFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\t_, err = io.Copy(resultFile, codeFile)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\tfirmware = firmwareWithVars\n\t\t\t}\n\t\t}\n\t\tif firmware != \"\" {\n\t\t\tif firmwareInBios {\n\t\t\t\targs = append(args, \"-bios\", firmware)\n\t\t\t} else {\n\t\t\t\targs = append(args, \"-drive\", fmt.Sprintf(\"if=pflash,format=raw,readonly=on,file=%s\", firmware))\n\t\t\t}\n\t\t}\n\t}\n\n\t// Disk\n\tdiskPath := filepath.Join(cfg.InstanceDir, filenames.Disk)\n\tisoPath := filepath.Join(cfg.InstanceDir, filenames.ISO)\n\textraDisks := []string{}\n\tfor _, d := range y.AdditionalDisks {\n\t\tdiskName := d.Name\n\t\tdisk, err := store.InspectDisk(diskName, d.FSType)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"could not load disk %q: %q\", diskName, err)\n\t\t\treturn \"\", nil, err\n\t\t}\n\n\t\tlogrus.Infof(\"Mounting disk %q on %q\", diskName, disk.MountPoint)\n\t\tif err = disk.LockForInstance(cfg.InstanceDir); err != nil {\n\t\t\tlogrus.Errorf(\"could not attach disk %q: %s\", diskName, err)\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tdataDisk := filepath.Join(disk.Dir, filenames.DataDisk)\n\t\textraDisks = append(extraDisks, dataDisk)\n\t}\n\n\tif osutil.FileExists(diskPath) {\n\t\targs = append(args, \"-drive\", fmt.Sprintf(\"file=%s,if=virtio,discard=on\", diskPath))\n\t}\n\tif osutil.FileExists(isoPath) {\n\t\targs = appendArgsIfNoConflict(args, \"-boot\", \"order=d,splash-time=0,menu=on\")\n\t\targs = append(args, \"-drive\", fmt.Sprintf(\"file=%s,format=raw,media=cdrom,readonly=on\", isoPath))\n\t} else {\n\t\targs = appendArgsIfNoConflict(args, \"-boot\", \"order=c,splash-time=0,menu=on\")\n\t}\n\tfor _, extraDisk := range extraDisks {\n\t\targs = append(args, \"-drive\", fmt.Sprintf(\"file=%s,if=virtio,discard=on\", extraDisk))\n\t}\n\n\t// cloud-init\n\targs = append(args,\n\t\t\"-drive\", \"id=cdrom0,if=none,format=raw,readonly=on,file=\"+filepath.Join(cfg.InstanceDir, filenames.CIDataISO),\n\t\t\"-device\", \"virtio-scsi,id=scsi0\",\n\t\t\"-device\", \"scsi-cd,bus=scsi0.0,drive=cdrom0\")\n\n\t// Kernel\n\tkernel := filepath.Join(cfg.InstanceDir, filenames.Kernel)\n\tkernelCmdline := filepath.Join(cfg.InstanceDir, filenames.KernelCmdline)\n\tinitrd := filepath.Join(cfg.InstanceDir, filenames.Initrd)\n\tif _, err := os.Stat(kernel); err == nil {\n\t\targs = appendArgsIfNoConflict(args, \"-kernel\", kernel)\n\t}\n\tif b, err := os.ReadFile(kernelCmdline); err == nil {\n\t\targs = appendArgsIfNoConflict(args, \"-append\", string(b))\n\t}\n\tif _, err := os.Stat(initrd); err == nil {\n\t\targs = appendArgsIfNoConflict(args, \"-initrd\", initrd)\n\t}\n\n\t// Network\n\t// Configure default usernetwork with limayaml.MACAddress(driver.Instance.Dir) for eth0 interface\n\tfirstUsernetIndex := limayaml.FirstUsernetIndex(y)\n\tif firstUsernetIndex == -1 {\n\t\targs = append(args, \"-netdev\", fmt.Sprintf(\"user,id=net0,net=%s,dhcpstart=%s,hostfwd=tcp:%s:%d-:22\",\n\t\t\tnetworks.SlirpNetwork, networks.SlirpIPAddress, cfg.SSHAddress, cfg.SSHLocalPort))\n\t} else {\n\t\tqemuSock, err := usernet.Sock(y.Networks[firstUsernetIndex].Lima, usernet.QEMUSock)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\targs = append(args, \"-netdev\", fmt.Sprintf(\"socket,id=net0,fd={{ fd_connect %q }}\", qemuSock))\n\t}\n\tvirtioNet := \"virtio-net-pci\"\n\tif *y.Arch == limatype.S390X {\n\t\t// virtio-net-pci does not work on EL, while it works on Ubuntu\n\t\t// https://github.com/lima-vm/lima/pull/3319/files#r1986388345\n\t\tvirtioNet = \"virtio-net-ccw\"\n\t}\n\targs = append(args, \"-device\", virtioNet+\",netdev=net0,mac=\"+limayaml.MACAddress(cfg.InstanceDir))\n\n\tfor i, nw := range y.Networks {\n\t\tif nw.Lima != \"\" {\n\t\t\tnwCfg, err := networks.LoadConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\n\t\t\t// Handle usernet connections\n\t\t\tisUsernet, err := nwCfg.Usernet(nw.Lima)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\t\t\tif isUsernet {\n\t\t\t\tif i == firstUsernetIndex {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqemuSock, err := usernet.Sock(nw.Lima, usernet.QEMUSock)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\targs = append(args, \"-netdev\", fmt.Sprintf(\"socket,id=net%d,fd={{ fd_connect %q }}\", i+1, qemuSock))\n\t\t\t\targs = append(args, \"-device\", fmt.Sprintf(\"%s,netdev=net%d,mac=%s\", virtioNet, i+1, nw.MACAddress))\n\t\t\t} else {\n\t\t\t\tif runtime.GOOS != \"darwin\" {\n\t\t\t\t\treturn \"\", nil, fmt.Errorf(\"networks.yaml '%s' configuration is only supported on macOS right now\", nw.Lima)\n\t\t\t\t}\n\t\t\t\tlogrus.Debugf(\"Using socketVMNet (%q)\", nwCfg.Paths.SocketVMNet)\n\t\t\t\tsock, err := networks.Sock(nw.Lima)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", nil, err\n\t\t\t\t}\n\t\t\t\targs = append(args, \"-netdev\", fmt.Sprintf(\"socket,id=net%d,fd={{ fd_connect %q }}\", i+1, sock))\n\t\t\t\t// TODO: should we also validate that the socket exists, or do we rely on the\n\t\t\t\t// networks reconciler to throw an error when the network cannot start?\n\t\t\t}\n\t\t} else if nw.Socket != \"\" {\n\t\t\targs = append(args, \"-netdev\", fmt.Sprintf(\"socket,id=net%d,fd={{ fd_connect %q }}\", i+1, nw.Socket))\n\t\t} else {\n\t\t\treturn \"\", nil, fmt.Errorf(\"invalid network spec %+v\", nw)\n\t\t}\n\t\targs = append(args, \"-device\", fmt.Sprintf(\"%s,netdev=net%d,mac=%s\", virtioNet, i+1, nw.MACAddress))\n\t}\n\n\t// virtio-rng-pci accelerates starting up the OS, according to https://wiki.gentoo.org/wiki/QEMU/Options\n\targs = append(args, \"-device\", \"virtio-rng-pci\")\n\n\t// Input\n\tinput := \"mouse\"\n\n\t// Sound\n\tif *y.Audio.Device != \"\" {\n\t\tid := \"default\"\n\t\t// audio device\n\t\taudiodev := *y.Audio.Device\n\t\tif audiodev == \"default\" {\n\t\t\taudiodev = audioDevice()\n\t\t}\n\t\taudiodev += fmt.Sprintf(\",id=%s\", id)\n\t\targs = append(args, \"-audiodev\", audiodev)\n\t\t// audio controller\n\t\targs = append(args, \"-device\", \"ich9-intel-hda\")\n\t\t// audio codec\n\t\targs = append(args, \"-device\", fmt.Sprintf(\"hda-output,audiodev=%s\", id))\n\t}\n\t// Graphics\n\tif *y.Video.Display != \"\" {\n\t\tdisplay := *y.Video.Display\n\t\tif display == \"vnc\" {\n\t\t\tdisplay += \"=\" + *y.Video.VNC.Display\n\t\t\tdisplay += \",password=on\"\n\t\t\t// use tablet to avoid double cursors\n\t\t\tinput = \"tablet\"\n\t\t}\n\t\targs = appendArgsIfNoConflict(args, \"-display\", display)\n\t}\n\n\tif *y.Video.Display != \"none\" {\n\t\tswitch *y.Arch {\n\t\t// FIXME: use virtio-gpu on all the architectures\n\t\tcase limatype.X8664, limatype.RISCV64:\n\t\t\targs = append(args, \"-device\", \"virtio-vga\")\n\t\tdefault:\n\t\t\targs = append(args, \"-device\", \"virtio-gpu\")\n\t\t}\n\t\targs = append(args, \"-device\", \"virtio-keyboard-pci\")\n\t\targs = append(args, \"-device\", \"virtio-\"+input+\"-pci\")\n\t\targs = append(args, \"-device\", \"qemu-xhci,id=usb-bus\")\n\t}\n\n\t// Parallel\n\targs = append(args, \"-parallel\", \"none\")\n\n\t// Serial (default)\n\t// This is ttyS0 for Intel and RISC-V, ttyAMA0 for ARM.\n\tserialSock := filepath.Join(cfg.InstanceDir, filenames.SerialSock)\n\tif err := os.RemoveAll(serialSock); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tserialLog := filepath.Join(cfg.InstanceDir, filenames.SerialLog)\n\tif err := os.RemoveAll(serialLog); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tconst serialChardev = \"char-serial\"\n\targs = append(args, \"-chardev\", fmt.Sprintf(\"socket,id=%s,path=%s,server=on,wait=off,logfile=%s\", serialChardev, serialSock, serialLog))\n\targs = append(args, \"-serial\", \"chardev:\"+serialChardev)\n\n\t// Serial (PCI, ARM only)\n\t// On ARM, the default serial is ttyAMA0, this PCI serial is ttyS0.\n\t// https://gitlab.com/qemu-project/qemu/-/issues/1801#note_1494720586\n\tswitch *y.Arch {\n\tcase limatype.AARCH64, limatype.ARMV7L:\n\t\tserialpSock := filepath.Join(cfg.InstanceDir, filenames.SerialPCISock)\n\t\tif err := os.RemoveAll(serialpSock); err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tserialpLog := filepath.Join(cfg.InstanceDir, filenames.SerialPCILog)\n\t\tif err := os.RemoveAll(serialpLog); err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tconst serialpChardev = \"char-serial-pci\"\n\t\targs = append(args, \"-chardev\", fmt.Sprintf(\"socket,id=%s,path=%s,server=on,wait=off,logfile=%s\", serialpChardev, serialpSock, serialpLog))\n\t\targs = append(args, \"-device\", \"pci-serial,chardev=\"+serialpChardev)\n\t}\n\n\t// Serial (virtio)\n\tserialvSock := filepath.Join(cfg.InstanceDir, filenames.SerialVirtioSock)\n\tif err := os.RemoveAll(serialvSock); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tserialvLog := filepath.Join(cfg.InstanceDir, filenames.SerialVirtioLog)\n\tif err := os.RemoveAll(serialvLog); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tconst serialvChardev = \"char-serial-virtio\"\n\targs = append(args, \"-chardev\", fmt.Sprintf(\"socket,id=%s,path=%s,server=on,wait=off,logfile=%s\", serialvChardev, serialvSock, serialvLog))\n\t// max_ports=1 is required for https://github.com/lima-vm/lima/issues/1689 https://github.com/lima-vm/lima/issues/1691\n\tserialvMaxPorts := 1\n\tif *y.Arch == limatype.S390X {\n\t\tserialvMaxPorts++ // needed to avoid `virtio-serial-bus: Out-of-range port id specified, max. allowed: 0`\n\t}\n\targs = append(args, \"-device\", fmt.Sprintf(\"virtio-serial-pci,id=virtio-serial0,max_ports=%d\", serialvMaxPorts))\n\targs = append(args, \"-device\", fmt.Sprintf(\"virtconsole,chardev=%s,id=console0\", serialvChardev))\n\n\t// We also want to enable vsock here, but QEMU does not support vsock for macOS hosts\n\n\tif *y.MountType == limatype.NINEP || *y.MountType == limatype.VIRTIOFS {\n\t\tfor i, f := range y.Mounts {\n\t\t\ttag := limayaml.MountTag(f.Location, *f.MountPoint)\n\t\t\tif err := os.MkdirAll(f.Location, 0o755); err != nil {\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\n\t\t\tswitch *y.MountType {\n\t\t\tcase limatype.NINEP:\n\t\t\t\toptions := \"local\"\n\t\t\t\toptions += fmt.Sprintf(\",mount_tag=%s\", tag)\n\t\t\t\toptions += fmt.Sprintf(\",path=%s\", f.Location)\n\t\t\t\toptions += fmt.Sprintf(\",security_model=%s\", *f.NineP.SecurityModel)\n\t\t\t\tif !*f.Writable {\n\t\t\t\t\toptions += \",readonly=on\"\n\t\t\t\t}\n\t\t\t\targs = append(args, \"-virtfs\", options)\n\t\t\tcase limatype.VIRTIOFS:\n\t\t\t\t// Note that read-only mode is not supported on the QEMU/virtiofsd side yet:\n\t\t\t\t// https://gitlab.com/virtio-fs/virtiofsd/-/issues/97\n\t\t\t\tchardev := fmt.Sprintf(\"char-virtiofs-%d\", i)\n\t\t\t\tvhostSock := filepath.Join(cfg.InstanceDir, fmt.Sprintf(filenames.VhostSock, i))\n\t\t\t\targs = append(args, \"-chardev\", fmt.Sprintf(\"socket,id=%s,path=%s\", chardev, vhostSock))\n\n\t\t\t\toptions := \"vhost-user-fs-pci\"\n\t\t\t\toptions += fmt.Sprintf(\",queue-size=%d\", *f.Virtiofs.QueueSize)\n\t\t\t\toptions += fmt.Sprintf(\",chardev=%s\", chardev)\n\t\t\t\toptions += fmt.Sprintf(\",tag=%s\", tag)\n\t\t\t\targs = append(args, \"-device\", options)\n\t\t\t}\n\t\t}\n\t}\n\n\t// QMP\n\tqmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)\n\tif err := os.RemoveAll(qmpSock); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tconst qmpChardev = \"char-qmp\"\n\targs = append(args, \"-chardev\", fmt.Sprintf(\"socket,id=%s,path=%s,server=on,wait=off\", qmpChardev, qmpSock))\n\targs = append(args, \"-qmp\", \"chardev:\"+qmpChardev)\n\n\tif cfg.VirtioGA {\n\t\t// Guest agent via serialport\n\t\tguestSock := filepath.Join(cfg.InstanceDir, filenames.GuestAgentSock)\n\t\targs = append(args, \"-chardev\", fmt.Sprintf(\"socket,path=%s,server=on,wait=off,id=qga0\", guestSock))\n\t\targs = append(args, \"-device\", \"virtio-serial\")\n\t\targs = append(args, \"-device\", \"virtserialport,chardev=qga0,name=\"+filenames.VirtioPort)\n\t}\n\n\t// QEMU process\n\targs = append(args, \"-name\", \"lima-\"+cfg.Name)\n\targs = append(args, \"-pidfile\", filepath.Join(cfg.InstanceDir, filenames.PIDFile(*y.VMType)))\n\n\treturn exe, args, nil\n}\n\nfunc FindVirtiofsd(ctx context.Context, qemuExe string) (string, error) {\n\ttype vhostUserBackend struct {\n\t\tBackendType string `json:\"type\"`\n\t\tBinary      string `json:\"binary\"`\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconst relativePath = \"share/qemu/vhost-user\"\n\n\tbinDir := filepath.Dir(qemuExe)                  // \"/usr/local/bin\"\n\tusrDir := filepath.Dir(binDir)                   // \"/usr/local\"\n\tuserLocalDir := filepath.Join(homeDir, \".local\") // \"$HOME/.local\"\n\n\tcandidates := []string{\n\t\tfilepath.Join(userLocalDir, relativePath),\n\t\tfilepath.Join(usrDir, relativePath),\n\t}\n\n\tif usrDir != \"/usr\" {\n\t\tcandidates = append(candidates, filepath.Join(\"/usr\", relativePath))\n\t}\n\n\tfor _, vhostCfgsDir := range candidates {\n\t\tlogrus.Debugf(\"Checking vhost directory %s\", vhostCfgsDir)\n\n\t\tcfgEntries, err := os.ReadDir(vhostCfgsDir)\n\t\tif err != nil {\n\t\t\tlogrus.Debugf(\"Failed to list vhost directory: %v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, cfgEntry := range cfgEntries {\n\t\t\tlogrus.Debugf(\"Checking vhost vhostCfg %s\", cfgEntry.Name())\n\t\t\tif !strings.HasSuffix(cfgEntry.Name(), \".json\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar vhostCfg vhostUserBackend\n\t\t\tcontents, err := os.ReadFile(filepath.Join(vhostCfgsDir, cfgEntry.Name()))\n\t\t\tif err == nil {\n\t\t\t\terr = json.Unmarshal(contents, &vhostCfg)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Warnf(\"Failed to load vhost-user config %s: %v\", cfgEntry.Name(), err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogrus.Debugf(\"%v\", vhostCfg)\n\n\t\t\tif vhostCfg.BackendType != \"fs\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Only rust virtiofsd supports --version, so use that to make sure this isn't\n\t\t\t// QEMU's virtiofsd, which requires running as root.\n\t\t\tcmd := exec.CommandContext(ctx, vhostCfg.Binary, \"--version\")\n\t\t\toutput, err := cmd.CombinedOutput()\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Warnf(\"Failed to run %s --version (is this QEMU virtiofsd?): %s: %s\",\n\t\t\t\t\tvhostCfg.Binary, err, output)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn vhostCfg.Binary, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"failed to locate virtiofsd\")\n}\n\nfunc VirtiofsdCmdline(cfg Config, mountIndex int) ([]string, error) {\n\tmount := cfg.LimaYAML.Mounts[mountIndex]\n\n\tvhostSock := filepath.Join(cfg.InstanceDir, fmt.Sprintf(filenames.VhostSock, mountIndex))\n\t// qemu_driver has to wait for the socket to appear, so make sure any old ones are removed here.\n\tif err := os.Remove(vhostSock); err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\tlogrus.Warnf(\"Failed to remove old vhost socket: %v\", err)\n\t}\n\n\treturn []string{\n\t\t\"--socket-path\", vhostSock,\n\t\t\"--shared-dir\", mount.Location,\n\t}, nil\n}\n\n// qemuArch returns the arch string used by qemu.\nfunc qemuArch(arch limatype.Arch) string {\n\tswitch arch {\n\tcase limatype.ARMV7L:\n\t\treturn \"arm\"\n\tcase limatype.PPC64LE:\n\t\treturn \"ppc64\"\n\tdefault:\n\t\treturn arch\n\t}\n}\n\n// qemuEdk2 returns the arch string used by `/usr/local/share/qemu/edk2-*-code.fd`.\nfunc qemuEdk2Arch(arch limatype.Arch) string {\n\tif arch == limatype.RISCV64 {\n\t\treturn \"riscv\"\n\t}\n\treturn qemuArch(arch)\n}\n\nfunc Exe(arch limatype.Arch) (exe string, args []string, err error) {\n\texeBase := \"qemu-system-\" + qemuArch(arch)\n\tenvK := \"QEMU_SYSTEM_\" + strings.ToUpper(qemuArch(arch))\n\tif envV := os.Getenv(envK); envV != \"\" {\n\t\tss, err := shellwords.Parse(envV)\n\t\tif err != nil {\n\t\t\treturn \"\", nil, fmt.Errorf(\"failed to parse %s value %q: %w\", envK, envV, err)\n\t\t}\n\t\texeBase, args = ss[0], ss[1:]\n\t\tif len(args) != 0 {\n\t\t\tlogrus.Warnf(\"Specifying args (%v) via $%s is supported only for debugging!\", args, envK)\n\t\t}\n\t}\n\texe, err = exec.LookPath(exeBase)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn exe, args, nil\n}\n\nfunc Accel(arch limatype.Arch) string {\n\tif limatype.IsNativeArch(arch) {\n\t\tswitch runtime.GOOS {\n\t\tcase \"darwin\":\n\t\t\t// TODO: return \"tcg\" if HVF is not available\n\t\t\treturn \"hvf\"\n\t\tcase \"linux\":\n\t\t\tif _, err := os.Stat(\"/dev/kvm\"); err != nil {\n\t\t\t\tlogrus.WithError(err).Warn(\"/dev/kvm is not available. Disabling KVM. Expect very poor performance.\")\n\t\t\t\treturn \"tcg\"\n\t\t\t}\n\t\t\treturn \"kvm\"\n\t\tcase \"netbsd\":\n\t\t\treturn \"nvmm\"\n\t\tcase \"dragonfly\":\n\t\t\treturn \"nvmm\"\n\t\tcase \"windows\":\n\t\t\treturn \"whpx\"\n\t\t}\n\t}\n\treturn \"tcg\"\n}\n\nfunc parseQemuVersion(output string) (*semver.Version, error) {\n\tlines := strings.Split(output, \"\\n\")\n\tregex := regexp.MustCompile(`^QEMU emulator version (\\d+\\.\\d+\\.\\d+)`)\n\tmatches := regex.FindStringSubmatch(lines[0])\n\tif len(matches) == 2 {\n\t\treturn semver.New(matches[1]), nil\n\t}\n\treturn &semver.Version{}, fmt.Errorf(\"failed to parse %v\", output)\n}\n\nfunc getQemuVersion(ctx context.Context, qemuExe string) (*semver.Version, error) {\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\tcmd := exec.CommandContext(ctx, qemuExe, \"--version\")\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run %v: stdout=%q, stderr=%q\", cmd.Args, stdout.String(), stderr.String())\n\t}\n\n\treturn parseQemuVersion(stdout.String())\n}\n\nfunc getFirmware(qemuExe string, arch limatype.Arch) (string, error) {\n\tswitch arch {\n\tcase limatype.X8664, limatype.AARCH64, limatype.ARMV7L, limatype.RISCV64:\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unexpected architecture: %q\", arch)\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbinDir := filepath.Dir(qemuExe)                  // \"/usr/local/bin\"\n\tlocalDir := filepath.Dir(binDir)                 // \"/usr/local\"\n\tuserLocalDir := filepath.Join(homeDir, \".local\") // \"$HOME/.local\"\n\n\trelativePath := fmt.Sprintf(\"share/qemu/edk2-%s-code.fd\", qemuEdk2Arch(arch))\n\trelativePathWin := fmt.Sprintf(\"share/edk2-%s-code.fd\", qemuEdk2Arch(arch))\n\tcandidates := []string{\n\t\tfilepath.Join(userLocalDir, relativePath), // XDG-like\n\t\tfilepath.Join(localDir, relativePath),     // macOS (homebrew)\n\t\tfilepath.Join(binDir, relativePathWin),    // Windows installer\n\t}\n\n\tswitch arch {\n\tcase limatype.X8664:\n\t\t// Archlinux package \"edk2-ovmf\"\n\t\t// @see: https://archlinux.org/packages/extra/any/edk2-ovmf/files\n\t\tcandidates = append(candidates, \"/usr/share/edk2/x64/OVMF_CODE.4m.fd\")\n\t\t// Debian package \"ovmf\"\n\t\tcandidates = append(candidates, \"/usr/share/OVMF/OVMF_CODE.fd\")\n\t\tcandidates = append(candidates, \"/usr/share/OVMF/OVMF_CODE_4M.fd\")\n\t\t// Fedora package \"edk2-ovmf\"\n\t\tcandidates = append(candidates, \"/usr/share/edk2/ovmf/OVMF_CODE.fd\")\n\t\t// openSUSE package \"qemu-ovmf-x86_64\"\n\t\tcandidates = append(candidates, \"/usr/share/qemu/ovmf-x86_64.bin\")\n\tcase limatype.AARCH64:\n\t\t// Archlinux package \"edk2-aarch64\"\n\t\t// @see: https://archlinux.org/packages/extra/any/edk2-aarch64/files\n\t\tcandidates = append(candidates, \"/usr/share/edk2/aarch64/QEMU_CODE.fd\")\n\t\t// Debian package \"qemu-efi-aarch64\"\n\t\t// Fedora package \"edk2-aarch64\"\n\t\tcandidates = append(candidates, \"/usr/share/AAVMF/AAVMF_CODE.fd\")\n\t\t// Debian package \"qemu-efi-aarch64\" (unpadded, backwards compatibility)\n\t\tcandidates = append(candidates, \"/usr/share/qemu-efi-aarch64/QEMU_EFI.fd\")\n\tcase limatype.ARMV7L:\n\t\t// Archlinux package \"edk2-arm\"\n\t\t// @see: https://archlinux.org/packages/extra/any/edk2-arm/files\n\t\tcandidates = append(candidates, \"/usr/share/edk2/arm/QEMU_CODE.fd\")\n\t\t// Debian package \"qemu-efi-arm\"\n\t\t// Fedora package \"edk2-arm\"\n\t\tcandidates = append(candidates, \"/usr/share/AAVMF/AAVMF32_CODE.fd\")\n\tcase limatype.RISCV64:\n\t\t// Debian package \"qemu-efi-riscv64\"\n\t\tcandidates = append(candidates, \"/usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd\")\n\t\t// Fedora package \"edk2-riscv64\"\n\t\tcandidates = append(candidates, \"/usr/share/edk2/riscv/RISCV_VIRT_CODE.fd\")\n\t}\n\n\tlogrus.Debugf(\"firmware candidates = %v\", candidates)\n\n\tfor _, f := range candidates {\n\t\tif _, err := os.Stat(f); err == nil {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\n\tif arch == limatype.X8664 {\n\t\treturn \"\", fmt.Errorf(\"could not find firmware for %q (hint: try setting `firmware.legacyBIOS` to `true`)\", arch)\n\t}\n\tqemuArch := strings.TrimPrefix(filepath.Base(qemuExe), \"qemu-system-\")\n\treturn \"\", fmt.Errorf(\"could not find firmware for %q (hint: try copying the \\\"edk-%s-code.fd\\\" firmware to $HOME/.local/share/qemu/)\", arch, qemuArch)\n}\n\nfunc getFirmwareVars(qemuExe string, arch limatype.Arch) (string, error) {\n\tvar targetArch string\n\tswitch arch {\n\tcase limatype.X8664:\n\t\ttargetArch = \"i386\" // vars are unified between i386 and x86_64 and normally only former is bundled\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unexpected architecture: %q\", arch)\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbinDir := filepath.Dir(qemuExe)                  // \"/usr/local/bin\"\n\tlocalDir := filepath.Dir(binDir)                 // \"/usr/local\"\n\tuserLocalDir := filepath.Join(homeDir, \".local\") // \"$HOME/.local\"\n\n\trelativePath := fmt.Sprintf(\"share/qemu/edk2-%s-vars.fd\", qemuEdk2Arch(targetArch))\n\trelativePathWin := fmt.Sprintf(\"share/edk2-%s-vars.fd\", qemuEdk2Arch(targetArch))\n\tcandidates := []string{\n\t\tfilepath.Join(userLocalDir, relativePath), // XDG-like\n\t\tfilepath.Join(localDir, relativePath),     // macOS (homebrew)\n\t\tfilepath.Join(binDir, relativePathWin),    // Windows installer\n\t}\n\n\tlogrus.Debugf(\"firmware vars candidates = %v\", candidates)\n\n\tfor _, f := range candidates {\n\t\tif _, err := os.Stat(f); err == nil {\n\t\t\treturn f, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"could not find firmware vars for %q\", arch)\n}\n\nvar hasSMEDarwin = sync.OnceValue(func() bool {\n\tif runtime.GOOS != \"darwin\" || runtime.GOARCH != \"arm64\" {\n\t\treturn false\n\t}\n\t// golang.org/x/sys/cpu does not support inspecting the availability of SME yet\n\ts, err := osutil.Sysctl(context.Background(), \"hw.optional.arm.FEAT_SME\")\n\tif err != nil {\n\t\tlogrus.WithError(err).Debug(\"failed to check hw.optional.arm.FEAT_SME\")\n\t}\n\treturn s == \"1\"\n})\n\nfunc hasHostCPU() bool {\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tif hasSMEDarwin() {\n\t\t\t// [2025-02-05]\n\t\t\t// SME is available since Apple M4 running macOS 15.2, but it was broken on macOS 15.2.\n\t\t\t// It has been fixed in 15.3.\n\t\t\t//\n\t\t\t// https://github.com/lima-vm/lima/issues/3032\n\t\t\t// https://gitlab.com/qemu-project/qemu/-/issues/2665\n\t\t\t// https://gitlab.com/qemu-project/qemu/-/issues/2721\n\n\t\t\t// [2025-02-12]\n\t\t\t// SME got broken again after upgrading QEMU from 9.2.0 to 9.2.1 (Homebrew bottle).\n\t\t\t// Possibly this regression happened in some build process rather than in QEMU itself?\n\t\t\t// https://github.com/lima-vm/lima/issues/3226\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\tcase \"linux\":\n\t\treturn true\n\tcase \"netbsd\", \"windows\":\n\t\treturn false\n\t}\n\t// Not reached\n\treturn false\n}\n"
  },
  {
    "path": "pkg/driver/qemu/qemu_driver.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage qemu\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/digitalocean/go-qemu/qmp\"\n\t\"github.com/digitalocean/go-qemu/qmp/raw\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/qemu/entitlementutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/executil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n\t\"github.com/lima-vm/lima/v2/pkg/reflectutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/version/versionutil\"\n)\n\ntype LimaQemuDriver struct {\n\tInstance     *limatype.Instance\n\tSSHLocalPort int\n\tvSockPort    int\n\tvirtioPort   string\n\n\tqCmd    *exec.Cmd\n\tqWaitCh chan error\n\n\tvhostCmds []*exec.Cmd\n}\n\nvar _ driver.Driver = (*LimaQemuDriver)(nil)\n\nfunc New() *LimaQemuDriver {\n\t// virtserialport doesn't seem to work reliably: https://github.com/lima-vm/lima/issues/2064\n\t// but on Windows default Unix socket forwarding is not available\n\tvar virtioPort string\n\tvirtioPort = filenames.VirtioPort\n\tif runtime.GOOS != \"windows\" {\n\t\tvirtioPort = \"\"\n\t}\n\treturn &LimaQemuDriver{\n\t\tvSockPort:  0,\n\t\tvirtioPort: virtioPort,\n\t}\n}\n\nfunc (l *LimaQemuDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriver {\n\tl.Instance = inst\n\tl.SSHLocalPort = inst.SSHLocalPort\n\n\treturn &driver.ConfiguredDriver{\n\t\tDriver: l,\n\t}\n}\n\nfunc (l *LimaQemuDriver) Validate(ctx context.Context) error {\n\tif err := validateArch(ctx, l.Instance.Config); err != nil {\n\t\treturn err\n\t}\n\treturn validateConfig(l.Instance.Config)\n}\n\nfunc validateConfig(cfg *limatype.LimaYAML) error {\n\tif cfg == nil {\n\t\treturn errors.New(\"configuration is nil\")\n\t}\n\tif err := validateMountType(cfg); err != nil {\n\t\treturn err\n\t}\n\n\tfor i, nw := range cfg.Networks {\n\t\tif unknown := reflectutil.UnknownNonEmptyFields(nw,\n\t\t\t\"Lima\",\n\t\t\t\"Socket\",\n\t\t\t\"MACAddress\",\n\t\t\t\"Metric\",\n\t\t\t\"Interface\",\n\t\t); len(unknown) > 0 {\n\t\t\tlogrus.Warnf(\"vmType %s: ignoring networks[%d]: %+v\", *cfg.VMType, i, unknown)\n\t\t}\n\t}\n\n\tvar qemuOpts limatype.QEMUOpts\n\tif err := limayaml.Convert(cfg.VMOpts[limatype.QEMU], &qemuOpts, \"vmOpts.qemu\"); err != nil {\n\t\treturn err\n\t}\n\tif qemuOpts.MinimumVersion != nil {\n\t\tif _, err := semver.NewVersion(*qemuOpts.MinimumVersion); err != nil {\n\t\t\treturn fmt.Errorf(\"field `vmOpts.qemu.minimumVersion` must be a semvar value, got %q: %w\", *qemuOpts.MinimumVersion, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Helper method for checking the binary signature on macOS.\nfunc validateArch(ctx context.Context, cfg *limatype.LimaYAML) error {\n\tif runtime.GOOS == \"darwin\" {\n\t\tvar vmArch string\n\t\tif cfg.Arch != nil {\n\t\t\tvmArch = *cfg.Arch\n\t\t}\n\t\tif err := checkBinarySignature(ctx, vmArch); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Helper method for mount type validation.\nfunc validateMountType(cfg *limatype.LimaYAML) error {\n\tif cfg.MountType != nil && *cfg.MountType == limatype.VIRTIOFS && runtime.GOOS != \"linux\" {\n\t\treturn fmt.Errorf(\"field `mountType` must be %q or %q for QEMU driver on non-Linux, got %q\",\n\t\t\tlimatype.REVSSHFS, limatype.NINEP, *cfg.MountType)\n\t}\n\tif cfg.MountTypesUnsupported != nil && cfg.MountType != nil && slices.Contains(cfg.MountTypesUnsupported, *cfg.MountType) {\n\t\treturn fmt.Errorf(\"mount type %q is explicitly unsupported\", *cfg.MountType)\n\t}\n\tif runtime.GOOS == \"windows\" && cfg.MountType != nil && *cfg.MountType == limatype.NINEP {\n\t\treturn fmt.Errorf(\"mount type %q is not supported on Windows\", limatype.NINEP)\n\t}\n\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) FillConfig(_ context.Context, cfg *limatype.LimaYAML, filePath string) error {\n\tif cfg.VMType == nil {\n\t\tcfg.VMType = ptr.Of(limatype.QEMU)\n\t}\n\n\tinstDir := filepath.Dir(filePath)\n\n\tif cfg.Video.VNC.Display == nil || *cfg.Video.VNC.Display == \"\" {\n\t\tcfg.Video.VNC.Display = ptr.Of(\"127.0.0.1:0,to=9\")\n\t}\n\n\tvar qemuOpts limatype.QEMUOpts\n\tif err := limayaml.Convert(cfg.VMOpts[limatype.QEMU], &qemuOpts, \"vmOpts.qemu\"); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %q\", cfg.VMOpts[limatype.QEMU])\n\t}\n\tif qemuOpts.CPUType == nil {\n\t\tqemuOpts.CPUType = limatype.CPUType{}\n\t}\n\n\t//nolint:staticcheck // Migration of top-level CPUTYPE if specified\n\tif len(cfg.CPUType) > 0 {\n\t\tlogrus.Warn(\"The top-level `cpuType` field is deprecated and will be removed in a future release. Please migrate to `vmOpts.qemu.cpuType`.\")\n\t\tfor arch, v := range cfg.CPUType {\n\t\t\tif v == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif existing, ok := qemuOpts.CPUType[arch]; ok && existing != \"\" && existing != v {\n\t\t\t\tlogrus.Warnf(\"Conflicting cpuType for arch %q: top-level=%q, vmOpts.qemu=%q; using vmOpts.qemu value\", arch, v, existing)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tqemuOpts.CPUType[arch] = v\n\t\t}\n\t\tcfg.CPUType = nil\n\n\t\tvar opts any\n\t\tif err := limayaml.Convert(qemuOpts, &opts, \"\"); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %+v\", qemuOpts)\n\t\t}\n\t\tif cfg.VMOpts == nil {\n\t\t\tcfg.VMOpts = limatype.VMOpts{}\n\t\t}\n\t\tcfg.VMOpts[limatype.QEMU] = opts\n\t}\n\n\tmountTypesUnsupported := make(map[string]struct{})\n\tfor _, f := range cfg.MountTypesUnsupported {\n\t\tmountTypesUnsupported[f] = struct{}{}\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\t// QEMU for Windows does not support 9p\n\t\tmountTypesUnsupported[limatype.NINEP] = struct{}{}\n\t}\n\n\tif cfg.MountType == nil || *cfg.MountType == \"\" || *cfg.MountType == \"default\" {\n\t\tcfg.MountType = ptr.Of(limatype.NINEP)\n\t\tif _, ok := mountTypesUnsupported[limatype.NINEP]; ok {\n\t\t\t// Use REVSSHFS if the instance does not support 9p\n\t\t\tcfg.MountType = ptr.Of(limatype.REVSSHFS)\n\t\t} else if limayaml.IsExistingInstanceDir(instDir) && !versionutil.GreaterEqual(limayaml.ExistingLimaVersion(instDir), \"1.0.0\") {\n\t\t\t// Use REVSSHFS if the instance was created with Lima prior to v1.0\n\t\t\tcfg.MountType = ptr.Of(limatype.REVSSHFS)\n\t\t}\n\t}\n\n\tfor i := range cfg.Mounts {\n\t\tmount := &cfg.Mounts[i]\n\t\tif mount.Virtiofs.QueueSize == nil && *cfg.MountType == limatype.VIRTIOFS {\n\t\t\tcfg.Mounts[i].Virtiofs.QueueSize = ptr.Of(limayaml.DefaultVirtiofsQueueSize)\n\t\t}\n\t}\n\n\tif _, ok := mountTypesUnsupported[*cfg.MountType]; ok {\n\t\treturn fmt.Errorf(\"mount type %q is explicitly unsupported\", *cfg.MountType)\n\t}\n\n\treturn validateConfig(cfg)\n}\n\nfunc (l *LimaQemuDriver) BootScripts() (map[string][]byte, error) {\n\treturn nil, nil\n}\n\nfunc (l *LimaQemuDriver) CreateDisk(ctx context.Context) error {\n\tqCfg := Config{\n\t\tName:        l.Instance.Name,\n\t\tInstanceDir: l.Instance.Dir,\n\t\tLimaYAML:    l.Instance.Config,\n\t}\n\treturn EnsureDisk(ctx, qCfg)\n}\n\nfunc (l *LimaQemuDriver) Start(_ context.Context) (chan error, error) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tif l.qCmd == nil {\n\t\t\tcancel()\n\t\t}\n\t}()\n\n\tif l.Instance.Config.SSH.OverVsock != nil && *l.Instance.Config.SSH.OverVsock {\n\t\tlogrus.Warn(\".ssh.overVsock is not implemented yet for QEMU driver\")\n\t}\n\n\tqCfg := Config{\n\t\tName:         l.Instance.Name,\n\t\tInstanceDir:  l.Instance.Dir,\n\t\tLimaYAML:     l.Instance.Config,\n\t\tSSHLocalPort: l.SSHLocalPort,\n\t\tSSHAddress:   l.Instance.SSHAddress,\n\t\tVirtioGA:     l.virtioPort != \"\",\n\t}\n\tqExe, qArgs, err := Cmdline(ctx, qCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar vhostCmds []*exec.Cmd\n\tif *l.Instance.Config.MountType == limatype.VIRTIOFS {\n\t\tvhostExe, err := FindVirtiofsd(ctx, qExe)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor i := range l.Instance.Config.Mounts {\n\t\t\targs, err := VirtiofsdCmdline(qCfg, i)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tvhostCmds = append(vhostCmds, exec.CommandContext(ctx, vhostExe, args...))\n\t\t}\n\t}\n\n\tvar qArgsFinal []string\n\tapplier := &qArgTemplateApplier{}\n\tfor _, unapplied := range qArgs {\n\t\tapplied, err := applier.applyTemplate(unapplied)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tqArgsFinal = append(qArgsFinal, applied)\n\t}\n\tqCmd := exec.CommandContext(ctx, qExe, qArgsFinal...)\n\tqCmd.ExtraFiles = append(qCmd.ExtraFiles, applier.files...)\n\tqCmd.SysProcAttr = executil.BackgroundSysProcAttr\n\tqStdout, err := qCmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgo logPipeRoutine(qStdout, \"qemu[stdout]\")\n\tqStderr, err := qCmd.StderrPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgo logPipeRoutine(qStderr, \"qemu[stderr]\")\n\n\tfor i, vhostCmd := range vhostCmds {\n\t\tvhostStdout, err := vhostCmd.StdoutPipe()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgo logPipeRoutine(vhostStdout, fmt.Sprintf(\"virtiofsd-%d[stdout]\", i))\n\t\tvhostStderr, err := vhostCmd.StderrPipe()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tgo logPipeRoutine(vhostStderr, fmt.Sprintf(\"virtiofsd-%d[stderr]\", i))\n\t}\n\n\tfor i, vhostCmd := range vhostCmds {\n\t\tlogrus.Debugf(\"vhostCmd[%d].Args: %v\", i, vhostCmd.Args)\n\t\tif err := vhostCmd.Start(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvhostWaitCh := make(chan error)\n\t\tgo func() {\n\t\t\tvhostWaitCh <- vhostCmd.Wait()\n\t\t}()\n\n\t\tvhostSock := filepath.Join(l.Instance.Dir, fmt.Sprintf(filenames.VhostSock, i))\n\t\tvhostSockExists := false\n\t\tfor attempt := range 5 {\n\t\t\tlogrus.Debugf(\"Try waiting for %s to appear (attempt %d)\", vhostSock, attempt)\n\n\t\t\tif _, err := os.Stat(vhostSock); err != nil {\n\t\t\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\tlogrus.Warnf(\"Failed to check for vhost socket: %v\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvhostSockExists = true\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tretry := time.NewTimer(200 * time.Millisecond)\n\t\t\tselect {\n\t\t\tcase err = <-vhostWaitCh:\n\t\t\t\treturn nil, fmt.Errorf(\"virtiofsd never created vhost socket: %w\", err)\n\t\t\tcase <-retry.C:\n\t\t\t}\n\t\t}\n\n\t\tif !vhostSockExists {\n\t\t\treturn nil, fmt.Errorf(\"vhost socket %s never appeared\", vhostSock)\n\t\t}\n\n\t\tgo func() {\n\t\t\tif err := <-vhostWaitCh; err != nil {\n\t\t\t\tlogrus.Errorf(\"Error from virtiofsd instance #%d: %v\", i, err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tlogrus.Infof(\"Starting QEMU (hint: to watch the boot progress, see %q)\", filepath.Join(qCfg.InstanceDir, \"serial*.log\"))\n\tlogrus.Debugf(\"qCmd.Args: %v\", qCmd.Args)\n\tif err := qCmd.Start(); err != nil {\n\t\treturn nil, err\n\t}\n\tl.qCmd = qCmd\n\tl.qWaitCh = make(chan error, 1)\n\tgo func() {\n\t\tdefer close(l.qWaitCh)\n\t\tdefer cancel()\n\t\tl.qWaitCh <- qCmd.Wait()\n\t}()\n\tl.vhostCmds = vhostCmds\n\tgo func() {\n\t\tif usernetIndex := limayaml.FirstUsernetIndex(l.Instance.Config); usernetIndex != -1 {\n\t\t\tclient := usernet.NewClientByName(l.Instance.Config.Networks[usernetIndex].Lima)\n\t\t\terr := client.ConfigureDriver(ctx, l.Instance, l.SSHLocalPort)\n\t\t\tif err != nil {\n\t\t\t\tl.qWaitCh <- err\n\t\t\t}\n\t\t}\n\t}()\n\treturn l.qWaitCh, nil\n}\n\nfunc (l *LimaQemuDriver) Stop(ctx context.Context) error {\n\treturn l.shutdownQEMU(ctx, 3*time.Minute, l.qCmd, l.qWaitCh)\n}\n\nfunc (l *LimaQemuDriver) ChangeDisplayPassword(_ context.Context, password string) error {\n\treturn l.changeVNCPassword(password)\n}\n\nfunc (l *LimaQemuDriver) DisplayConnection(_ context.Context) (string, error) {\n\treturn l.getVNCDisplayPort()\n}\n\nfunc waitFileExists(path string, timeout time.Duration) error {\n\tstartWaiting := time.Now()\n\tfor {\n\t\t_, err := os.Stat(path)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn err\n\t\t}\n\t\tif time.Since(startWaiting) > timeout {\n\t\t\treturn fmt.Errorf(\"timeout waiting for %s\", path)\n\t\t}\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}\n\treturn nil\n}\n\n// Ask the user to sign the qemu binary with the \"com.apple.security.hypervisor\" if needed.\n// Workaround for https://github.com/lima-vm/lima/issues/1742\nfunc checkBinarySignature(ctx context.Context, vmArch string) error {\n\tmacOSProductVersion, err := osutil.ProductVersion()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// The codesign --xml option is only available on macOS Monterey and later\n\tif !macOSProductVersion.LessThan(*semver.New(\"12.0.0\")) && vmArch != \"\" {\n\t\tqExe, _, err := Exe(vmArch)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to find the QEMU binary for the architecture %q: %w\", vmArch, err)\n\t\t}\n\t\tif accel := Accel(vmArch); accel == \"hvf\" {\n\t\t\tentitlementutil.AskToSignIfNotSignedProperly(ctx, qExe)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) changeVNCPassword(password string) error {\n\tqmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock)\n\terr := waitFileExists(qmpSockPath, 30*time.Second)\n\tif err != nil {\n\t\treturn err\n\t}\n\tqmpClient, err := qmp.NewSocketMonitor(\"unix\", qmpSockPath, 5*time.Second)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := qmpClient.Connect(); err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = qmpClient.Disconnect() }()\n\trawClient := raw.NewMonitor(qmpClient)\n\terr = rawClient.ChangeVNCPassword(password)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) getVNCDisplayPort() (string, error) {\n\tqmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock)\n\tqmpClient, err := qmp.NewSocketMonitor(\"unix\", qmpSockPath, 5*time.Second)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := qmpClient.Connect(); err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func() { _ = qmpClient.Disconnect() }()\n\trawClient := raw.NewMonitor(qmpClient)\n\tinfo, err := rawClient.QueryVNC()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn *info.Service, nil\n}\n\nfunc (l *LimaQemuDriver) removeVNCFiles() error {\n\tvncfile := filepath.Join(l.Instance.Dir, filenames.VNCDisplayFile)\n\terr := os.RemoveAll(vncfile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvncpwdfile := filepath.Join(l.Instance.Dir, filenames.VNCPasswordFile)\n\terr = os.RemoveAll(vncpwdfile)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) killVhosts() error {\n\tvar errs []error\n\tfor i, vhost := range l.vhostCmds {\n\t\tif err := vhost.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to kill virtiofsd instance #%d: %w\", i, err))\n\t\t}\n\t}\n\n\treturn errors.Join(errs...)\n}\n\nfunc (l *LimaQemuDriver) shutdownQEMU(ctx context.Context, timeout time.Duration, qCmd *exec.Cmd, qWaitCh <-chan error) error {\n\t// \"power button\" refers to ACPI on the most archs, except RISC-V\n\tlogrus.Info(\"Shutting down QEMU with the power button\")\n\tif usernetIndex := limayaml.FirstUsernetIndex(l.Instance.Config); usernetIndex != -1 {\n\t\tclient := usernet.NewClientByName(l.Instance.Config.Networks[usernetIndex].Lima)\n\t\terr := client.UnExposeSSH(l.SSHLocalPort)\n\t\tif err != nil {\n\t\t\tlogrus.Warnf(\"Failed to remove SSH binding for port %d\", l.SSHLocalPort)\n\t\t}\n\t}\n\tqmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock)\n\tqmpClient, err := qmp.NewSocketMonitor(\"unix\", qmpSockPath, 5*time.Second)\n\tif err != nil {\n\t\tlogrus.WithError(err).Warnf(\"failed to open the QMP socket %q, forcibly killing QEMU\", qmpSockPath)\n\t\treturn l.killQEMU(ctx, timeout, qCmd, qWaitCh)\n\t}\n\tif err := qmpClient.Connect(); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"failed to connect to the QMP socket %q, forcibly killing QEMU\", qmpSockPath)\n\t\treturn l.killQEMU(ctx, timeout, qCmd, qWaitCh)\n\t}\n\tdefer func() { _ = qmpClient.Disconnect() }()\n\trawClient := raw.NewMonitor(qmpClient)\n\tlogrus.Info(\"Sending QMP system_powerdown command\")\n\tif err := rawClient.SystemPowerdown(); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"failed to send system_powerdown command via the QMP socket %q, forcibly killing QEMU\", qmpSockPath)\n\t\treturn l.killQEMU(ctx, timeout, qCmd, qWaitCh)\n\t}\n\ttimeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), timeout)\n\tdefer timeoutCancel()\n\n\tselect {\n\tcase qWaitErr, ok := <-qWaitCh:\n\t\tif !ok {\n\t\t\tlogrus.Info(\"QEMU wait channel was closed\")\n\t\t\t_ = l.removeVNCFiles()\n\t\t\treturn l.killVhosts()\n\t\t}\n\t\tentry := logrus.NewEntry(logrus.StandardLogger())\n\t\tif qWaitErr != nil {\n\t\t\tentry = entry.WithError(qWaitErr)\n\t\t}\n\t\tentry.Info(\"QEMU has exited\")\n\t\t_ = l.removeVNCFiles()\n\t\treturn errors.Join(qWaitErr, l.killVhosts())\n\tcase <-timeoutCtx.Done():\n\t\tif qCmd.ProcessState != nil {\n\t\t\tlogrus.Info(\"QEMU has already exited\")\n\t\t\t_ = l.removeVNCFiles()\n\t\t\treturn l.killVhosts()\n\t\t}\n\t\tlogrus.Warnf(\"QEMU did not exit in %v, forcibly killing QEMU\", timeout)\n\t\treturn l.killQEMU(ctx, timeout, qCmd, qWaitCh)\n\t}\n}\n\nfunc (l *LimaQemuDriver) killQEMU(_ context.Context, _ time.Duration, qCmd *exec.Cmd, qWaitCh <-chan error) error {\n\tvar qWaitErr error\n\tif qCmd.ProcessState == nil {\n\t\tif killErr := qCmd.Process.Kill(); killErr != nil {\n\t\t\tlogrus.WithError(killErr).Warn(\"failed to kill QEMU\")\n\t\t}\n\t\tqWaitErr = <-qWaitCh\n\t\tlogrus.WithError(qWaitErr).Info(\"QEMU has exited, after killing forcibly\")\n\t} else {\n\t\tlogrus.Info(\"QEMU has already exited\")\n\t}\n\tqemuPIDPath := filepath.Join(l.Instance.Dir, filenames.PIDFile(*l.Instance.Config.VMType))\n\t_ = os.RemoveAll(qemuPIDPath)\n\t_ = l.removeVNCFiles()\n\treturn errors.Join(qWaitErr, l.killVhosts())\n}\n\nfunc logPipeRoutine(r io.Reader, header string) {\n\tscanner := bufio.NewScanner(r)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tlogrus.Debugf(\"%s: %s\", header, line)\n\t}\n}\n\nfunc (l *LimaQemuDriver) DeleteSnapshot(ctx context.Context, tag string) error {\n\tqCfg := Config{\n\t\tName:        l.Instance.Name,\n\t\tInstanceDir: l.Instance.Dir,\n\t\tLimaYAML:    l.Instance.Config,\n\t}\n\treturn Del(ctx, qCfg, l.Instance.Status == limatype.StatusRunning, tag)\n}\n\nfunc (l *LimaQemuDriver) CreateSnapshot(ctx context.Context, tag string) error {\n\tqCfg := Config{\n\t\tName:        l.Instance.Name,\n\t\tInstanceDir: l.Instance.Dir,\n\t\tLimaYAML:    l.Instance.Config,\n\t}\n\treturn Save(ctx, qCfg, l.Instance.Status == limatype.StatusRunning, tag)\n}\n\nfunc (l *LimaQemuDriver) ApplySnapshot(ctx context.Context, tag string) error {\n\tqCfg := Config{\n\t\tName:        l.Instance.Name,\n\t\tInstanceDir: l.Instance.Dir,\n\t\tLimaYAML:    l.Instance.Config,\n\t}\n\treturn Load(ctx, qCfg, l.Instance.Status == limatype.StatusRunning, tag)\n}\n\nfunc (l *LimaQemuDriver) ListSnapshots(ctx context.Context) (string, error) {\n\tqCfg := Config{\n\t\tName:        l.Instance.Name,\n\t\tInstanceDir: l.Instance.Dir,\n\t\tLimaYAML:    l.Instance.Config,\n\t}\n\treturn List(ctx, qCfg, l.Instance.Status == limatype.StatusRunning)\n}\n\nfunc (l *LimaQemuDriver) GuestAgentConn(ctx context.Context) (net.Conn, string, error) {\n\tvar d net.Dialer\n\tdialContext, err := d.DialContext(ctx, \"unix\", filepath.Join(l.Instance.Dir, filenames.GuestAgentSock))\n\treturn dialContext, \"unix\", err\n}\n\ntype qArgTemplateApplier struct {\n\tfiles []*os.File\n}\n\nfunc (a *qArgTemplateApplier) applyTemplate(qArg string) (string, error) {\n\tif !strings.Contains(qArg, \"{{\") {\n\t\treturn qArg, nil\n\t}\n\tfuncMap := template.FuncMap{\n\t\t\"fd_connect\": func(v any) string {\n\t\t\tfn := func(v any) (string, error) {\n\t\t\t\ts, ok := v.(string)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"non-string argument %+v\", v)\n\t\t\t\t}\n\t\t\t\taddr, err := net.ResolveUnixAddr(\"unix\", s)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tconn, err := net.DialUnix(\"unix\", nil, addr)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tf, err := conn.File()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\tif err := conn.Close(); err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\ta.files = append(a.files, f)\n\t\t\t\tfd := len(a.files) + 2 // the first FD is 3\n\t\t\t\treturn strconv.Itoa(fd), nil\n\t\t\t}\n\t\t\tres, err := fn(v)\n\t\t\tif err != nil {\n\t\t\t\tpanic(fmt.Errorf(\"fd_connect: %w\", err))\n\t\t\t}\n\t\t\treturn res\n\t\t},\n\t}\n\ttmpl, err := template.New(\"\").Funcs(funcMap).Parse(qArg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar b bytes.Buffer\n\tif err := tmpl.Execute(&b, nil); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn b.String(), nil\n}\n\nfunc (l *LimaQemuDriver) Info() driver.Info {\n\tvar info driver.Info\n\tinfo.Name = \"qemu\"\n\tif l.Instance != nil && l.Instance.Dir != \"\" {\n\t\tinfo.InstanceDir = l.Instance.Dir\n\t}\n\tinfo.VirtioPort = l.virtioPort\n\tinfo.VsockPort = l.vSockPort\n\n\tinfo.Features = driver.DriverFeatures{\n\t\tDynamicSSHAddress:    false,\n\t\tSkipSocketForwarding: false,\n\t\tCanRunGUI:            false,\n\t}\n\treturn info\n}\n\nfunc (l *LimaQemuDriver) SSHAddress(_ context.Context) (string, error) {\n\treturn \"127.0.0.1\", nil\n}\n\nfunc (l *LimaQemuDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string {\n\treturn \"\"\n}\n\nfunc (l *LimaQemuDriver) Create(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) Delete(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) RunGUI() error {\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) Register(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) Unregister(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaQemuDriver) ForwardGuestAgent() bool {\n\t// if driver is not providing, use host agent\n\treturn l.vSockPort == 0 && l.virtioPort == \"\"\n}\n\nfunc (l *LimaQemuDriver) AdditionalSetupForSSH(_ context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driver/qemu/qemu_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage qemu\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestArgValue(t *testing.T) {\n\ttype testCase struct {\n\t\tkey           string\n\t\texpectedValue string\n\t\texpectedOK    bool\n\t}\n\targs := []string{\"-cpu\", \"foo\", \"-no-reboot\", \"-m\", \"2G\", \"-s\"}\n\ttestCases := []testCase{\n\t\t{\n\t\t\tkey:           \"-cpu\",\n\t\t\texpectedValue: \"foo\",\n\t\t\texpectedOK:    true,\n\t\t},\n\t\t{\n\t\t\tkey:           \"-no-reboot\",\n\t\t\texpectedValue: \"\",\n\t\t\texpectedOK:    true,\n\t\t},\n\t\t{\n\t\t\tkey:           \"-m\",\n\t\t\texpectedValue: \"2G\",\n\t\t\texpectedOK:    true,\n\t\t},\n\t\t{\n\t\t\tkey:           \"-machine\",\n\t\t\texpectedValue: \"\",\n\t\t\texpectedOK:    false,\n\t\t},\n\t\t{\n\t\t\tkey:           \"-s\",\n\t\t\texpectedValue: \"\",\n\t\t\texpectedOK:    true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tv, ok := argValue(args, tc.key)\n\t\tassert.Equal(t, tc.expectedValue, v)\n\t\tassert.Equal(t, tc.expectedOK, ok)\n\t}\n}\n\nfunc TestParseQemuVersion(t *testing.T) {\n\ttype testCase struct {\n\t\tversionOutput string\n\t\texpectedValue string\n\t\texpectedError string\n\t}\n\ttestCases := []testCase{\n\t\t{\n\t\t\t// old one line version\n\t\t\tversionOutput: \"QEMU emulator version 1.5.3 (qemu-kvm-1.5.3-175.el7_9.6), \" +\n\t\t\t\t\"Copyright (c) 2003-2008 Fabrice Bellard\\n\",\n\t\t\texpectedValue: \"1.5.3\",\n\t\t\texpectedError: \"\",\n\t\t},\n\t\t{\n\t\t\t// new two line version\n\t\t\tversionOutput: \"QEMU emulator version 8.0.0 (v8.0.0)\\n\" +\n\t\t\t\t\"Copyright (c) 2003-2022 Fabrice Bellard and the QEMU Project developers\\n\",\n\t\t\texpectedValue: \"8.0.0\",\n\t\t\texpectedError: \"\",\n\t\t},\n\t\t{\n\t\t\tversionOutput: \"foobar\",\n\t\t\texpectedValue: \"0.0.0\",\n\t\t\texpectedError: \"failed to parse\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tv, err := parseQemuVersion(tc.versionOutput)\n\t\tif tc.expectedError == \"\" {\n\t\t\tassert.NilError(t, err)\n\t\t} else {\n\t\t\tassert.ErrorContains(t, err, tc.expectedError)\n\t\t}\n\t\tassert.Equal(t, tc.expectedValue, v.String())\n\t}\n}\n"
  },
  {
    "path": "pkg/driver/qemu/register.go",
    "content": "//go:build !external_qemu\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage qemu\n\nimport \"github.com/lima-vm/lima/v2/pkg/registry\"\n\nfunc init() {\n\tregistry.Register(New())\n}\n"
  },
  {
    "path": "pkg/driver/vz/boot.Linux/05-rosetta-volume.sh",
    "content": "#!/bin/bash\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eux -o pipefail\n\nif [ \"$LIMA_CIDATA_ROSETTA_ENABLED\" != \"true\" ]; then\n\texit 0\nfi\n\nif [ -f /etc/alpine-release ]; then\n\trc-service procfs start --ifnotstarted\n\trc-service qemu-binfmt stop --ifexists --ifstarted\nfi\n\nbinfmt_entry=/proc/sys/fs/binfmt_misc/rosetta\nbinfmtd_conf=/usr/lib/binfmt.d/rosetta.conf\nif [ \"$LIMA_CIDATA_ROSETTA_BINFMT\" = \"true\" ]; then\n\trosetta_binfmt=\":rosetta:M::\\x7fELF\\x02\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x3e\\x00:\\xff\\xff\\xff\\xff\\xff\\xfe\\xfe\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff:/mnt/lima-rosetta/rosetta:OCF\"\n\n\t# If rosetta is not registered in binfmt_misc, register it.\n\t[ -f \"$binfmt_entry\" ] || echo \"$rosetta_binfmt\" >/proc/sys/fs/binfmt_misc/register\n\n\t# Create binfmt.d(5) configuration to prioritize rosetta even if qemu-user-static is installed on systemd based systems.\n\t# If the binfmt.d directory exists, consider systemd-binfmt.service(8) to be enabled and create the configuration file.\n\t[ ! -d \"$(dirname \"$binfmtd_conf\")\" ] || [ -f \"$binfmtd_conf\" ] || echo \"$rosetta_binfmt\" >\"$binfmtd_conf\"\nelse\n\t# unregister rosetta from binfmt_misc if it exists\n\t[ ! -f \"$binfmt_entry\" ] || echo -1 >\"$binfmt_entry\"\n\t# remove binfmt.d(5) configuration if it exists\n\t[ ! -f \"$binfmtd_conf\" ] || rm \"$binfmtd_conf\"\nfi\n\nif [ -x /mnt/lima-rosetta/rosettad ]; then\n\tCACHE_DIRECTORY=/var/cache/rosettad\n\tDEFAULT_SOCKET=${CACHE_DIRECTORY}/uds/rosetta.sock\n\tEXPECTED_SOCKET=/run/rosettad/rosetta.sock\n\n\t# Create rosettad service\n\tif [ -f /sbin/openrc-run ]; then\n\t\tcat >/etc/init.d/rosettad <<EOF\n#!/sbin/openrc-run\nname=\"rosettad\"\ndescription=\"Rosetta AOT Caching Daemon\"\nrequired_dirs=/mnt/lima-rosetta\nrequired_files=/mnt/lima-rosetta/rosettad\ncommand=/mnt/lima-rosetta/rosettad\ncommand_args=\"daemon ${CACHE_DIRECTORY}\"\ncommand_background=true\npidfile=\"/run/rosettad.pid\"\nstart_pre() {\n\t# To detect creation of the socket by rosettad, remove the old socket before starting\n\ttest ! -e \"${DEFAULT_SOCKET}\" || rm -f \"${DEFAULT_SOCKET}\"\n}\nstart_post() {\n\t# Set the socket permission to world-writable\n\twhile ! chmod -f go+w \"${DEFAULT_SOCKET}\"; do sleep 1; done\n\t# Create the symlink as expected by the configuration to enable Rosetta AOT caching\n\tmkdir -p \"$(dirname \"${EXPECTED_SOCKET}\")\"\n\tln -sf \"${DEFAULT_SOCKET}\" \"${EXPECTED_SOCKET}\"\n}\nEOF\n\t\tchmod 755 /etc/init.d/rosettad\n\t\trc-update add rosettad default\n\t\trc-service rosettad start\n\telse\n\t\tcat >/etc/systemd/system/rosettad.service <<EOF\n[Unit]\nDescription=Rosetta AOT Caching Daemon\nRequiresMountsFor=/mnt/lima-rosetta\n[Service]\nRuntimeDirectory=rosettad\nCacheDirectory=rosettad\n# To detect creation of the socket by rosettad, remove the old socket\nExecStartPre=sh -c \"test ! -e \\\"${DEFAULT_SOCKET}\\\" || rm -f \\\"${DEFAULT_SOCKET}\\\"\"\nExecStart=/mnt/lima-rosetta/rosettad daemon \"${CACHE_DIRECTORY}\"\n# Set the socket permission to world-writable and create the symlink as expected by the configuration to enable Rosetta AOT caching.\nExecStartPost=sh -c \"while ! chmod -f go+w \\\"${DEFAULT_SOCKET}\\\"; do sleep 1; done; ln -sf \\\"${DEFAULT_SOCKET}\\\" \\\"${EXPECTED_SOCKET}\\\"\"\nOOMPolicy=continue\nOOMScoreAdjust=-500\n[Install]\nWantedBy=default.target\nEOF\n\t\tsystemctl is-enabled rosettad || systemctl enable --now rosettad\n\tfi\n\n\t# Create CDI configuration for Rosetta\n\tmkdir -p /etc/cdi /var/run/cdi /etc/buildkit/cdi\n\tcat >/etc/cdi/rosetta.yaml <<EOF\ncdiVersion: \"0.6.0\"\nkind: \"lima-vm.io/rosetta\"\ndevices:\n- name: cached\n  containerEdits:\n    mounts:\n    - hostPath: /var/cache/rosettad/uds/rosetta.sock\n      containerPath: /run/rosettad/rosetta.sock\n      options: [bind]\nannotations:\n  org.mobyproject.buildkit.device.autoallow: true\nEOF\n\t# nerdctl requires user-specific CDI configuration directories\n\tmkdir -p \"${LIMA_CIDATA_HOME}/.config/cdi\"\n\tln -sf /etc/cdi/rosetta.yaml \"${LIMA_CIDATA_HOME}/.config/cdi/\"\n\tchown -R \"${LIMA_CIDATA_USER}\" \"${LIMA_CIDATA_HOME}/.config\"\nelse\n\t# Remove CDI configuration for Rosetta AOT Caching\n\t[ ! -f /etc/cdi/rosetta.yaml ] || rm /etc/cdi/rosetta.yaml\n\t[ ! -d \"${LIMA_CIDATA_HOME}/.config/cdi/rosetta.yaml\" ] || rm \"${LIMA_CIDATA_HOME}/.config/cdi/rosetta.yaml\"\nfi\n"
  },
  {
    "path": "pkg/driver/vz/errors_darwin.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport \"errors\"\n\n//nolint:revive,staticcheck,unused // false positives with proper nouns and GOARCH check\nvar (\n\terrRosettaUnsupported    = errors.New(\"Rosetta is unsupported on non-ARM64 hosts\")\n\terrMacOSGuestUnsupported = errors.New(\"macOS guest is unsupported on non-ARM64 hosts\")\n\terrUnimplemented         = errors.New(\"unimplemented\")\n)\n"
  },
  {
    "path": "pkg/driver/vz/network_darwin.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/balajiv113/fd\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc PassFDToUnix(unixSock string) (*os.File, error) {\n\tunixAddr, err := net.ResolveUnixAddr(\"unix\", unixSock)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tunixConn, err := net.DialUnix(\"unix\", nil, unixAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tserver, client, err := createSockPair()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = fd.Put(unixConn, server)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, nil\n}\n\n// DialQemu support connecting to QEMU supported network stack via unix socket.\n// Returns os.File, connected dgram connection to be used for vz.\nfunc DialQemu(ctx context.Context, unixSock string) (*os.File, error) {\n\tvar dialer net.Dialer\n\tunixConn, err := dialer.DialContext(ctx, \"unix\", unixSock)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tqemuConn := &qemuPacketConn{Conn: unixConn}\n\n\tserver, client, err := createSockPair()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdgramConn, err := net.FileConn(server)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvzConn := &packetConn{Conn: dgramConn}\n\n\tgo forwardPackets(qemuConn, vzConn)\n\n\treturn client, nil\n}\n\nfunc forwardPackets(qemuConn *qemuPacketConn, vzConn *packetConn) {\n\tdefer qemuConn.Close()\n\tdefer vzConn.Close()\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tif _, err := io.Copy(qemuConn, vzConn); err != nil {\n\t\t\tlogrus.Errorf(\"Failed to forward packets from VZ to VMNET: %s\", err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tif _, err := io.Copy(vzConn, qemuConn); err != nil {\n\t\t\tlogrus.Errorf(\"Failed to forward packets from VMNET to VZ: %s\", err)\n\t\t}\n\t}()\n\n\twg.Wait()\n}\n\n// qemuPacketConn converts raw network packet to a QEMU supported network packet.\ntype qemuPacketConn struct {\n\tnet.Conn\n}\n\n// Read reads a QEMU packet and returns the contained raw packet.  Returns (len,\n// nil) if a packet was read, and (0, err) on error. Errors means the protocol\n// is broken and the socket must be closed.\nfunc (c *qemuPacketConn) Read(b []byte) (n int, err error) {\n\tvar size uint32\n\tif err := binary.Read(c.Conn, binary.BigEndian, &size); err != nil {\n\t\t// Likely connection closed by peer.\n\t\treturn 0, err\n\t}\n\treturn io.ReadFull(c.Conn, b[:size])\n}\n\n// Write writes a QEMU packet containing the raw packet. Returns (len(b), nil)\n// if a packet was written, and (0, err) if a packet was not fully written.\n// Errors means the protocol is broken and the socket must be closed.\nfunc (c *qemuPacketConn) Write(b []byte) (int, error) {\n\tsize := len(b)\n\theader := uint32(size)\n\tif err := binary.Write(c.Conn, binary.BigEndian, header); err != nil {\n\t\treturn 0, err\n\t}\n\n\tfor len(b) != 0 {\n\t\tn, err := c.Conn.Write(b)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tb = b[n:]\n\t}\n\treturn size, nil\n}\n\n// Testing show that retries are very rare (e.g 24 of 62,499,008 packets) and\n// requires 1 or 2 retries to complete the write. A 100 microseconds sleep loop\n// consumes about 4% CPU on M1 Pro.\nconst writeRetryDelay = 100 * time.Microsecond\n\n// packetConn handles ENOBUFS errors when writing to unixgram socket.\ntype packetConn struct {\n\tnet.Conn\n}\n\n// Write writes a packet retrying on ENOBUFS errors.\nfunc (c *packetConn) Write(b []byte) (int, error) {\n\tvar retries uint64\n\tfor {\n\t\tn, err := c.Conn.Write(b)\n\t\tif n == 0 && err != nil && errors.Is(err, syscall.ENOBUFS) {\n\t\t\t// This is an expected condition on BSD based system. The kernel\n\t\t\t// does not support blocking until buffer space is available.\n\t\t\t// The only way to recover is to retry the call until it\n\t\t\t// succeeds, or drop the packet.\n\t\t\t// Handled in a similar way in gvisor-tap-vsock:\n\t\t\t// https://github.com/containers/gvisor-tap-vsock/issues/367\n\t\t\ttime.Sleep(writeRetryDelay)\n\t\t\tretries++\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif n < len(b) {\n\t\t\treturn n, errors.New(\"incomplete write to unixgram socket\")\n\t\t}\n\t\tif retries > 0 {\n\t\t\tlogrus.Debugf(\"Write completed after %d retries\", retries)\n\t\t}\n\t\treturn n, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/driver/vz/network_darwin_test.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nconst (\n\tvmnetMaxPacketSize = 1514\n\tpacketsCount       = 1000\n)\n\nfunc TestDialQemu(t *testing.T) {\n\tlistener, err := listenUnix(t.TempDir())\n\tassert.NilError(t, err)\n\tdefer listener.Close()\n\tt.Logf(\"Listening at %q\", listener.Addr())\n\n\terrc := make(chan error, 2)\n\n\t// Start the fake vmnet server.\n\tgo func() {\n\t\tt.Log(\"Fake vmnet started\")\n\t\terrc <- serveOneClient(listener)\n\t\tt.Log(\"Fake vmnet finished\")\n\t}()\n\n\t// Connect to the fake vmnet server.\n\tclient, err := DialQemu(t.Context(), listener.Addr().String())\n\tassert.NilError(t, err)\n\tt.Log(\"Connected to fake vment server\")\n\n\tdgramConn, err := net.FileConn(client)\n\tassert.NilError(t, err)\n\n\tvzConn := packetConn{Conn: dgramConn}\n\tdefer vzConn.Close()\n\n\tgo func() {\n\t\tt.Log(\"Sender started\")\n\t\tbuf := make([]byte, vmnetMaxPacketSize)\n\t\tfor i := range vmnetMaxPacketSize {\n\t\t\tbuf[i] = 0x55\n\t\t}\n\n\t\t// data packet format:\n\t\t//     0-4\t\tpacket number\n\t\t//     4-1514\t0x55 ...\n\t\tfor i := range packetsCount {\n\t\t\tbinary.BigEndian.PutUint32(buf, uint32(i))\n\t\t\tif _, err := vzConn.Write(buf); err != nil {\n\t\t\t\terrc <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tt.Logf(\"Sent %d data packets\", packetsCount)\n\n\t\t// quit packet format:\n\t\t//     0-4:     \"quit\"\n\t\tcopy(buf[:4], \"quit\")\n\t\tif _, err := vzConn.Write(buf[:4]); err != nil {\n\t\t\terrc <- err\n\t\t\treturn\n\t\t}\n\n\t\terrc <- nil\n\t\tt.Log(\"Sender finished\")\n\t}()\n\n\t// Read and verify packets to the server.\n\n\tbuf := make([]byte, vmnetMaxPacketSize)\n\n\tt.Log(\"Receiving and verifying data packets...\")\n\tfor i := range packetsCount {\n\t\tn, err := vzConn.Read(buf)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, n >= vmnetMaxPacketSize, \"unexpected number of bytes\")\n\n\t\tnumber := binary.BigEndian.Uint32(buf[:4])\n\t\tassert.Equal(t, number, uint32(i), \"unexpected packet\")\n\n\t\tfor j := 4; j < vmnetMaxPacketSize; j++ {\n\t\t\tassert.Equal(t, buf[j], byte(0x55), \"unexpected byte at offset %d\", j)\n\t\t}\n\t}\n\tt.Logf(\"Received and verified %d data packets\", packetsCount)\n\n\tfor range 2 {\n\t\terr := <-errc\n\t\tassert.NilError(t, err)\n\t}\n}\n\n// serveOneClient accepts one client and echo back received packets until a\n// \"quit\" packet is sent.\nfunc serveOneClient(listener *net.UnixListener) error {\n\tconn, err := listener.Accept()\n\tif err != nil {\n\t\treturn err\n\t}\n\tqemuConn := qemuPacketConn{Conn: conn}\n\tdefer qemuConn.Close()\n\n\tbuf := make([]byte, vmnetMaxPacketSize)\n\tfor {\n\t\tnr, err := qemuConn.Read(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif string(buf[:4]) == \"quit\" {\n\t\t\treturn nil\n\t\t}\n\t\tnw, err := qemuConn.Write(buf[:nr])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif nw != nr {\n\t\t\treturn fmt.Errorf(\"incomplete write: expected: %d, wrote: %d\", nr, nw)\n\t\t}\n\t}\n}\n\n// listenUnix creates and listen to unix socket under dir\nfunc listenUnix(dir string) (*net.UnixListener, error) {\n\tsock := filepath.Join(dir, \"sock\")\n\taddr, err := net.ResolveUnixAddr(\"unix\", sock)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn net.ListenUnix(\"unix\", addr)\n}\n"
  },
  {
    "path": "pkg/driver/vz/register.go",
    "content": "//go:build darwin && !no_vz && !external_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport \"github.com/lima-vm/lima/v2/pkg/registry\"\n\nfunc init() {\n\tregistry.Register(New())\n}\n"
  },
  {
    "path": "pkg/driver/vz/rosetta_directory_share.go",
    "content": "//go:build darwin && !arm64 && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"github.com/Code-Hex/vz/v3\"\n)\n\nfunc createRosettaDirectoryShareConfiguration() (*vz.VirtioFileSystemDeviceConfiguration, error) {\n\treturn nil, errRosettaUnsupported\n}\n"
  },
  {
    "path": "pkg/driver/vz/rosetta_directory_share_arm64.go",
    "content": "//go:build darwin && arm64 && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/Code-Hex/vz/v3\"\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\nfunc createRosettaDirectoryShareConfiguration() (*vz.VirtioFileSystemDeviceConfiguration, error) {\n\tconfig, err := vz.NewVirtioFileSystemDeviceConfiguration(\"vz-rosetta\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create a new virtio file system configuration: %w\", err)\n\t}\n\tavailability := vz.LinuxRosettaDirectoryShareAvailability()\n\tswitch availability {\n\tcase vz.LinuxRosettaAvailabilityNotSupported:\n\t\treturn nil, errRosettaUnsupported\n\tcase vz.LinuxRosettaAvailabilityNotInstalled:\n\t\tlogrus.Info(\"Installing rosetta...\")\n\t\tlogrus.Info(\"Hint: try `softwareupdate --install-rosetta` if Lima gets stuck here\")\n\t\tif err := vz.LinuxRosettaDirectoryShareInstallRosetta(); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to install rosetta: %w\", err)\n\t\t}\n\t\tlogrus.Info(\"Rosetta installation complete.\")\n\tcase vz.LinuxRosettaAvailabilityInstalled:\n\t\t// nothing to do\n\t}\n\n\trosettaShare, err := vz.NewLinuxRosettaDirectoryShare()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create a new rosetta directory share: %w\", err)\n\t}\n\tmacOSProductVersion, err := osutil.ProductVersion()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get macOS product version: %w\", err)\n\t}\n\tif !macOSProductVersion.LessThan(*semver.New(\"14.0.0\")) {\n\t\tcachingOption, err := vz.NewLinuxRosettaUnixSocketCachingOptions(\"/run/rosettad/rosetta.sock\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create a new rosetta directory share caching option: %w\", err)\n\t\t}\n\t\trosettaShare.SetOptions(cachingOption)\n\t}\n\tconfig.SetDirectoryShare(rosettaShare)\n\n\treturn config, nil\n}\n"
  },
  {
    "path": "pkg/driver/vz/vm_darwin.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"github.com/Code-Hex/vz/v3\"\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/docker/go-units\"\n\t\"github.com/lima-vm/go-qcow2reader\"\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"github.com/lima-vm/go-qcow2reader/image/asif\"\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\n// diskImageCachingMode is set to DiskImageCachingModeCached so as to avoid disk corruption on ARM:\n// - https://github.com/utmapp/UTM/issues/4840#issuecomment-1824340975\n// - https://github.com/utmapp/UTM/issues/4840#issuecomment-1824542732\n//\n// Eventually we may bring this back to DiskImageCachingModeAutomatic when the corruption issue is properly fixed.\nconst diskImageCachingMode = vz.DiskImageCachingModeCached\n\ntype virtualMachineWrapper struct {\n\t*vz.VirtualMachine\n\tmu      sync.Mutex\n\tstopped bool\n}\n\n// Hold all *os.File created via socketpair() so that they won't get garbage collected. f.FD() gets invalid if f gets garbage collected.\nvar vmNetworkFiles = make([]*os.File, 1)\n\nfunc startVM(ctx context.Context, inst *limatype.Instance, sshLocalPort int, onVsockEvent func(*events.VsockEvent)) (vm *virtualMachineWrapper, waitSSHLocalPortAccessible <-chan any, errCh chan error, err error) {\n\tusernetClient, stopUsernet, err := startUsernet(ctx, inst)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\n\tmachine, err := createVM(ctx, inst)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\n\terr = machine.Start()\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\n\twrapper := &virtualMachineWrapper{VirtualMachine: machine, stopped: false}\n\tnotifySSHLocalPortAccessible := make(chan any)\n\tsendErrCh := make(chan error)\n\n\tgo func() {\n\t\t// Handle errors via errCh and handle stop vm during context close\n\t\tdefer func() {\n\t\t\tfor i := range vmNetworkFiles {\n\t\t\t\tvmNetworkFiles[i].Close()\n\t\t\t}\n\t\t}()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tlogrus.Info(\"Context closed, stopping vm\")\n\t\t\t\tif machine.CanStop() {\n\t\t\t\t\t_, err := machine.RequestStop()\n\t\t\t\t\tlogrus.Errorf(\"Error while stopping the VM %q\", err)\n\t\t\t\t}\n\t\t\tcase newState := <-machine.StateChangedNotify():\n\t\t\t\tswitch newState {\n\t\t\t\tcase vz.VirtualMachineStateRunning:\n\t\t\t\t\tpidFile := filepath.Join(inst.Dir, filenames.PIDFile(*inst.Config.VMType))\n\t\t\t\t\tif _, err := os.Stat(pidFile); !errors.Is(err, os.ErrNotExist) {\n\t\t\t\t\t\tlogrus.Errorf(\"pidfile %q already exists\", pidFile)\n\t\t\t\t\t\tsendErrCh <- err\n\t\t\t\t\t}\n\t\t\t\t\tif err := os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())+\"\\n\"), 0o644); err != nil {\n\t\t\t\t\t\tlogrus.Errorf(\"error writing to pid fil %q\", pidFile)\n\t\t\t\t\t\tsendErrCh <- err\n\t\t\t\t\t}\n\t\t\t\t\tlogrus.Info(\"[VZ] - vm state change: running\")\n\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\tdefer close(notifySSHLocalPortAccessible)\n\t\t\t\t\t\tusernetSSHLocalPort := sshLocalPort\n\t\t\t\t\t\tuseSSHOverVsock := *inst.Config.OS == limatype.LINUX\n\t\t\t\t\t\tif inst.Config.SSH.OverVsock != nil {\n\t\t\t\t\t\t\tuseSSHOverVsock = *inst.Config.SSH.OverVsock\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif !useSSHOverVsock {\n\t\t\t\t\t\t\tlogrus.Info(\"ssh.overVsock is false, skipping detection of SSH server on vsock port\")\n\t\t\t\t\t\t\tif onVsockEvent != nil {\n\t\t\t\t\t\t\t\tonVsockEvent(&events.VsockEvent{\n\t\t\t\t\t\t\t\t\tType:   events.VsockEventSkipped,\n\t\t\t\t\t\t\t\t\tReason: \"ssh.overVsock is false\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if err := usernetClient.WaitOpeningSSHPort(ctx, inst); err == nil {\n\t\t\t\t\t\t\thostAddress := net.JoinHostPort(inst.SSHAddress, strconv.Itoa(usernetSSHLocalPort))\n\t\t\t\t\t\t\tif err := wrapper.startVsockForwarder(ctx, 22, hostAddress); err == nil {\n\t\t\t\t\t\t\t\tlogrus.Infof(\"Detected SSH server is listening on the vsock port; changed %s to proxy for the vsock port\", hostAddress)\n\t\t\t\t\t\t\t\tif onVsockEvent != nil {\n\t\t\t\t\t\t\t\t\tonVsockEvent(&events.VsockEvent{\n\t\t\t\t\t\t\t\t\t\tType:      events.VsockEventStarted,\n\t\t\t\t\t\t\t\t\t\tHostAddr:  hostAddress,\n\t\t\t\t\t\t\t\t\t\tVsockPort: 22,\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tusernetSSHLocalPort = 0 // disable gvisor ssh port forwarding\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tlogrus.WithError(err).WithField(\"hostAddress\", hostAddress).\n\t\t\t\t\t\t\t\t\tDebugf(\"Failed to start vsock forwarder (systemd is older than v256?)\")\n\t\t\t\t\t\t\t\tlogrus.Info(\"SSH server does not seem to be running on vsock port, using usernet forwarder\")\n\t\t\t\t\t\t\t\tif onVsockEvent != nil {\n\t\t\t\t\t\t\t\t\tonVsockEvent(&events.VsockEvent{\n\t\t\t\t\t\t\t\t\t\tType:   events.VsockEventFailed,\n\t\t\t\t\t\t\t\t\t\tReason: \"SSH server does not seem to be running on vsock port\",\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlogrus.WithError(err).Warn(\"Failed to wait for the guest SSH server to become available, falling back to usernet forwarder\")\n\t\t\t\t\t\t\tif onVsockEvent != nil {\n\t\t\t\t\t\t\t\tonVsockEvent(&events.VsockEvent{\n\t\t\t\t\t\t\t\t\tType:   events.VsockEventFailed,\n\t\t\t\t\t\t\t\t\tReason: \"Failed to wait for guest SSH server\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\terr := usernetClient.ConfigureDriver(ctx, inst, usernetSSHLocalPort)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tsendErrCh <- err\n\t\t\t\t\t\t}\n\t\t\t\t\t}()\n\t\t\t\tcase vz.VirtualMachineStateStopped:\n\t\t\t\t\tlogrus.Info(\"[VZ] - vm state change: stopped\")\n\t\t\t\t\twrapper.mu.Lock()\n\t\t\t\t\twrapper.stopped = true\n\t\t\t\t\twrapper.mu.Unlock()\n\t\t\t\t\t_ = usernetClient.UnExposeSSH(inst.SSHLocalPort)\n\t\t\t\t\tif stopUsernet != nil {\n\t\t\t\t\t\tstopUsernet()\n\t\t\t\t\t}\n\t\t\t\t\tsendErrCh <- errors.New(\"vz driver state stopped\")\n\t\t\t\tdefault:\n\t\t\t\t\tlogrus.Debugf(\"[VZ] - vm state change: %q\", newState)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\treturn wrapper, notifySSHLocalPortAccessible, sendErrCh, err\n}\n\nfunc startUsernet(ctx context.Context, inst *limatype.Instance) (*usernet.Client, context.CancelFunc, error) {\n\tif firstUsernetIndex := limayaml.FirstUsernetIndex(inst.Config); firstUsernetIndex != -1 {\n\t\tnwName := inst.Config.Networks[firstUsernetIndex].Lima\n\t\treturn usernet.NewClientByName(nwName), nil, nil\n\t}\n\t// Start a in-process gvisor-tap-vsock\n\tendpointSock, err := usernet.SockWithDirectory(inst.Dir, \"\", usernet.EndpointSock)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tvzSock, err := usernet.SockWithDirectory(inst.Dir, \"\", usernet.FDSock)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tos.RemoveAll(endpointSock)\n\tos.RemoveAll(vzSock)\n\tctx, cancel := context.WithCancel(ctx)\n\terr = usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{\n\t\tMTU:      1500,\n\t\tEndpoint: endpointSock,\n\t\tFdSocket: vzSock,\n\t\tAsync:    true,\n\t\tDefaultLeases: map[string]string{\n\t\t\tnetworks.SlirpIPAddress: limayaml.MACAddress(inst.Dir),\n\t\t},\n\t\tSubnet: networks.SlirpNetwork,\n\t})\n\tif err != nil {\n\t\tdefer cancel()\n\t\treturn nil, nil, err\n\t}\n\tsubnetIP, _, err := net.ParseCIDR(networks.SlirpNetwork)\n\treturn usernet.NewClient(endpointSock, subnetIP), cancel, err\n}\n\nfunc createVM(ctx context.Context, inst *limatype.Instance) (*vz.VirtualMachine, error) {\n\tvmConfig, err := createInitialConfig(inst)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachPlatformConfig(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachSerialPort(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachNetwork(ctx, inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachDisks(ctx, inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachDisplay(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachFolderMounts(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachAudio(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachOtherDevices(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvalidated, err := vmConfig.Validate()\n\tif !validated || err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn vz.NewVirtualMachine(vmConfig)\n}\n\n// createVMForMacInstaller is similar to createVM but only used for VZMacOSInstaller.\n// - Only the primary disk is attached.\n// - No network.\nfunc createVMForMacInstaller(_ context.Context, inst *limatype.Instance) (*vz.VirtualMachine, error) {\n\tvmConfig, err := createInitialConfig(inst)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachPlatformConfig(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Only attach the primary disk here. cidata.iso is not existent at this point.\n\tdisk := filepath.Join(inst.Dir, filenames.Disk)\n\tdiffDiskAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(disk, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdiskConfig, err := vz.NewVirtioBlockDeviceConfiguration(diffDiskAttachment)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvmConfig.SetStorageDevicesVirtualMachineConfiguration([]vz.StorageDeviceConfiguration{diskConfig})\n\n\tif err = attachDisplay(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = attachOtherDevices(inst, vmConfig); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvalidated, err := vmConfig.Validate()\n\tif !validated || err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn vz.NewVirtualMachine(vmConfig)\n}\n\nfunc createInitialConfig(inst *limatype.Instance) (*vz.VirtualMachineConfiguration, error) {\n\tbootLoader, err := bootLoader(inst)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbytes, err := units.RAMInBytes(*inst.Config.Memory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvmConfig, err := vz.NewVirtualMachineConfiguration(\n\t\tbootLoader,\n\t\tuint(*inst.Config.CPUs),\n\t\tuint64(bytes),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn vmConfig, nil\n}\n\nfunc newPlatformConfiguration(inst *limatype.Instance) (vz.PlatformConfiguration, error) {\n\tif *inst.Config.OS == limatype.DARWIN {\n\t\treturn newMacPlatformConfiguration(inst)\n\t}\n\n\tidentifierFile := filepath.Join(inst.Dir, filenames.VzIdentifier)\n\n\tmachineIdentifier, err := getGenericMachineIdentifier(identifierFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplatformConfig, err := vz.NewGenericPlatformConfiguration(vz.WithGenericMachineIdentifier(machineIdentifier))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn platformConfig, nil\n}\n\nfunc attachPlatformConfig(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {\n\tplatformConfig, err := newPlatformConfiguration(inst)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// nested virt\n\tif *inst.Config.NestedVirtualization {\n\t\tmacOSProductVersion, err := osutil.ProductVersion()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get macOS product version: %w\", err)\n\t\t}\n\n\t\tif macOSProductVersion.LessThan(*semver.New(\"15.0.0\")) {\n\t\t\treturn errors.New(\"nested virtualization requires macOS 15 or newer\")\n\t\t}\n\n\t\tif !vz.IsNestedVirtualizationSupported() {\n\t\t\treturn errors.New(\"nested virtualization is not supported on this device\")\n\t\t}\n\n\t\tgenericPlatformConfig, ok := platformConfig.(*vz.GenericPlatformConfiguration)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"nested virtualization is not supported on %T\", platformConfig)\n\t\t}\n\n\t\tif err := genericPlatformConfig.SetNestedVirtualizationEnabled(true); err != nil {\n\t\t\treturn fmt.Errorf(\"cannot enable nested virtualization: %w\", err)\n\t\t}\n\t}\n\n\tvmConfig.SetPlatformVirtualMachineConfiguration(platformConfig)\n\treturn nil\n}\n\nfunc attachSerialPort(inst *limatype.Instance, config *vz.VirtualMachineConfiguration) error {\n\tpath := filepath.Join(inst.Dir, filenames.SerialVirtioLog)\n\tserialPortAttachment, err := vz.NewFileSerialPortAttachment(path, false)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconsoleConfig, err := vz.NewVirtioConsoleDeviceSerialPortConfiguration(serialPortAttachment)\n\tconfig.SetSerialPortsVirtualMachineConfiguration([]*vz.VirtioConsoleDeviceSerialPortConfiguration{\n\t\tconsoleConfig,\n\t})\n\treturn err\n}\n\nfunc newVirtioFileNetworkDeviceConfiguration(file *os.File, macStr string) (*vz.VirtioNetworkDeviceConfiguration, error) {\n\tfileAttachment, err := vz.NewFileHandleNetworkDeviceAttachment(file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn newVirtioNetworkDeviceConfiguration(fileAttachment, macStr)\n}\n\nfunc newVirtioNetworkDeviceConfiguration(attachment vz.NetworkDeviceAttachment, macStr string) (*vz.VirtioNetworkDeviceConfiguration, error) {\n\tnetworkConfig, err := vz.NewVirtioNetworkDeviceConfiguration(attachment)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmac, err := net.ParseMAC(macStr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taddress, err := vz.NewMACAddress(mac)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnetworkConfig.SetMACAddress(address)\n\treturn networkConfig, nil\n}\n\nfunc attachNetwork(ctx context.Context, inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {\n\tvar configurations []*vz.VirtioNetworkDeviceConfiguration\n\n\t// Configure default usernetwork with limayaml.MACAddress(inst.Dir) for eth0 interface\n\tfirstUsernetIndex := limayaml.FirstUsernetIndex(inst.Config)\n\tif firstUsernetIndex == -1 {\n\t\t// slirp network using gvisor netstack\n\t\tvzSock, err := usernet.SockWithDirectory(inst.Dir, \"\", usernet.FDSock)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnetworkConn, err := PassFDToUnix(vzSock)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnetworkConfig, err := newVirtioFileNetworkDeviceConfiguration(networkConn, limayaml.MACAddress(inst.Dir))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfigurations = append(configurations, networkConfig)\n\t} else {\n\t\tvzSock, err := usernet.Sock(inst.Config.Networks[firstUsernetIndex].Lima, usernet.FDSock)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnetworkConn, err := PassFDToUnix(vzSock)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnetworkConfig, err := newVirtioFileNetworkDeviceConfiguration(networkConn, limayaml.MACAddress(inst.Dir))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfigurations = append(configurations, networkConfig)\n\t}\n\n\tfor i, nw := range inst.Networks {\n\t\tif nw.VZNAT != nil && *nw.VZNAT {\n\t\t\tattachment, err := vz.NewNATNetworkDeviceAttachment()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tnetworkConfig, err := newVirtioNetworkDeviceConfiguration(attachment, nw.MACAddress)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tconfigurations = append(configurations, networkConfig)\n\t\t} else if nw.Lima != \"\" {\n\t\t\tnwCfg, err := networks.LoadConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tisUsernet, err := nwCfg.Usernet(nw.Lima)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif isUsernet {\n\t\t\t\tif i == firstUsernetIndex {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tvzSock, err := usernet.Sock(nw.Lima, usernet.FDSock)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tclientFile, err := PassFDToUnix(vzSock)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tnetworkConfig, err := newVirtioFileNetworkDeviceConfiguration(clientFile, nw.MACAddress)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tconfigurations = append(configurations, networkConfig)\n\t\t\t} else {\n\t\t\t\tif runtime.GOOS != \"darwin\" {\n\t\t\t\t\treturn fmt.Errorf(\"networks.yaml '%s' configuration is only supported on macOS right now\", nw.Lima)\n\t\t\t\t}\n\t\t\t\tsocketVMNetOk, err := nwCfg.IsDaemonInstalled(networks.SocketVMNet)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif socketVMNetOk {\n\t\t\t\t\tlogrus.Debugf(\"Using socketVMNet (%q)\", nwCfg.Paths.SocketVMNet)\n\t\t\t\t\tsock, err := networks.Sock(nw.Lima)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tclientFile, err := DialQemu(ctx, sock)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tnetworkConfig, err := newVirtioFileNetworkDeviceConfiguration(clientFile, nw.MACAddress)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tconfigurations = append(configurations, networkConfig)\n\t\t\t\t}\n\t\t\t}\n\t\t} else if nw.Socket != \"\" {\n\t\t\tclientFile, err := DialQemu(ctx, nw.Socket)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tnetworkConfig, err := newVirtioFileNetworkDeviceConfiguration(clientFile, nw.MACAddress)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tconfigurations = append(configurations, networkConfig)\n\t\t}\n\t}\n\tvmConfig.SetNetworkDevicesVirtualMachineConfiguration(configurations)\n\treturn nil\n}\n\nfunc validateDiskFormat(diskPath string) error {\n\tf, err := os.Open(diskPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\timg, err := qcow2reader.Open(f)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to detect the format of %q: %w\", diskPath, err)\n\t}\n\tsupportedDiskTypes := []image.Type{raw.Type, asif.Type}\n\tif t := img.Type(); !slices.Contains(supportedDiskTypes, t) {\n\t\treturn fmt.Errorf(\"expected the format of %q to be one of %v, got %q\", diskPath, supportedDiskTypes, t)\n\t}\n\t// TODO: ensure that the disk is formatted with GPT or ISO9660\n\treturn nil\n}\n\nfunc attachDisks(ctx context.Context, inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {\n\tdiskPath := filepath.Join(inst.Dir, filenames.Disk)\n\tisoPath := filepath.Join(inst.Dir, filenames.ISO)\n\tciDataPath := filepath.Join(inst.Dir, filenames.CIDataISO)\n\tvar configurations []vz.StorageDeviceConfiguration\n\n\tif osutil.FileExists(diskPath) {\n\t\tif err := validateDiskFormat(diskPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdiskAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(diskPath, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdiskDev, err := vz.NewVirtioBlockDeviceConfiguration(diskAttachment)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfigurations = append(configurations, diskDev)\n\t}\n\tif osutil.FileExists(isoPath) {\n\t\tif err := validateDiskFormat(isoPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tisoAttachment, err := vz.NewDiskImageStorageDeviceAttachment(isoPath, true)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tisoDev, err := vz.NewUSBMassStorageDeviceConfiguration(isoAttachment)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tconfigurations = append(configurations, isoDev)\n\t}\n\n\tdiskUtil := proxyimgutil.NewDiskUtil(ctx)\n\n\tfor _, d := range inst.Config.AdditionalDisks {\n\t\tdiskName := d.Name\n\t\tdisk, err := store.InspectDisk(diskName, d.FSType)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to run load disk %q: %w\", diskName, err)\n\t\t}\n\n\t\tlogrus.Infof(\"Mounting disk %q on %q\", diskName, disk.MountPoint)\n\t\tif err = disk.LockForInstance(inst.Dir); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to attach disk %q: %w\", diskName, err)\n\t\t}\n\t\textraDiskPath := filepath.Join(disk.Dir, filenames.DataDisk)\n\t\t// ConvertToRaw is a NOP if no conversion is needed\n\t\tlogrus.Debugf(\"Converting extra disk %q to a raw disk (if it is not a raw)\", extraDiskPath)\n\n\t\tif err = diskUtil.Convert(ctx, raw.Type, extraDiskPath, extraDiskPath, nil, true); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to convert extra disk %q to a raw disk: %w\", extraDiskPath, err)\n\t\t}\n\t\textraDiskPathAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(extraDiskPath, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create disk attachment for extra disk %q: %w\", extraDiskPath, err)\n\t\t}\n\t\textraDisk, err := vz.NewVirtioBlockDeviceConfiguration(extraDiskPathAttachment)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create new virtio block device config for extra disk %q: %w\", extraDiskPath, err)\n\t\t}\n\t\tconfigurations = append(configurations, extraDisk)\n\t}\n\n\tif err := validateDiskFormat(ciDataPath); err != nil {\n\t\treturn err\n\t}\n\tciDataAttachment, err := vz.NewDiskImageStorageDeviceAttachment(ciDataPath, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tciData, err := vz.NewVirtioBlockDeviceConfiguration(ciDataAttachment)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconfigurations = append(configurations, ciData)\n\n\tvmConfig.SetStorageDevicesVirtualMachineConfiguration(configurations)\n\treturn nil\n}\n\nfunc attachDisplay(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {\n\tswitch *inst.Config.Video.Display {\n\tcase \"vz\", \"default\":\n\t\tvar graphicsDeviceConfiguration vz.GraphicsDeviceConfiguration\n\t\tif *inst.Config.OS == limatype.DARWIN {\n\t\t\tvar err error\n\t\t\tgraphicsDeviceConfiguration, err = newMacGraphicsDeviceConfiguration(1920, 1200, 80)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tvar err error\n\t\t\tgraphicsDeviceConfiguration, err = vz.NewVirtioGraphicsDeviceConfiguration()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tscanoutConfiguration, err := vz.NewVirtioGraphicsScanoutConfiguration(1920, 1200)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tgraphicsDeviceConfiguration.(*vz.VirtioGraphicsDeviceConfiguration).SetScanouts(scanoutConfiguration)\n\t\t}\n\n\t\tvmConfig.SetGraphicsDevicesVirtualMachineConfiguration([]vz.GraphicsDeviceConfiguration{\n\t\t\tgraphicsDeviceConfiguration,\n\t\t})\n\t\treturn nil\n\tcase \"none\":\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected video display %q\", *inst.Config.Video.Display)\n\t}\n}\n\nfunc directorySharingDevicesGeneric(origMounts []limatype.Mount) ([]vz.DirectorySharingDeviceConfiguration, error) {\n\tvar mounts []vz.DirectorySharingDeviceConfiguration\n\tfor _, mount := range origMounts {\n\t\tif _, err := os.Stat(mount.Location); errors.Is(err, os.ErrNotExist) {\n\t\t\terr := os.MkdirAll(mount.Location, 0o750)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tdirectory, err := vz.NewSharedDirectory(mount.Location, !*mount.Writable)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tshare, err := vz.NewSingleDirectoryShare(directory)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttag := limayaml.MountTag(mount.Location, *mount.MountPoint)\n\t\tconfig, err := vz.NewVirtioFileSystemDeviceConfiguration(tag)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tconfig.SetDirectoryShare(share)\n\t\tmounts = append(mounts, config)\n\t}\n\treturn mounts, nil\n}\n\nfunc directorySharingDevicesMacOS(origMounts []limatype.Mount) ([]vz.DirectorySharingDeviceConfiguration, error) {\n\tdirectories := make(map[string]*vz.SharedDirectory)\n\tfor _, mount := range origMounts {\n\t\tif _, err := os.Stat(mount.Location); errors.Is(err, os.ErrNotExist) {\n\t\t\terr := os.MkdirAll(mount.Location, 0o750)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tdirectory, err := vz.NewSharedDirectory(mount.Location, !*mount.Writable)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// The directory is mounted on `/Volumes/My Shared Files/<pseudoTag>`.\n\t\t// The actual virtiofs tag is allocated by vz.MacOSGuestAutomountTag().\n\t\t// https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdeviceconfiguration?language=objc#Automounting-shared-directories-in-macOS-VMs\n\t\tpseudoTag := limayaml.MountTag(mount.Location, *mount.MountPoint)\n\t\tdirectories[pseudoTag] = directory\n\t}\n\tshare, err := vz.NewMultipleDirectoryShare(directories)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttag, err := vz.MacOSGuestAutomountTag()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconfig, err := vz.NewVirtioFileSystemDeviceConfiguration(tag)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconfig.SetDirectoryShare(share)\n\treturn []vz.DirectorySharingDeviceConfiguration{config}, nil\n}\n\nfunc attachFolderMounts(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {\n\tvar mounts []vz.DirectorySharingDeviceConfiguration\n\tif *inst.Config.MountType == limatype.VIRTIOFS {\n\t\tvar err error\n\t\t// \"generic\" sharing devices are still mountable on macOS, but such mounts are\n\t\t// not accessible due to Operation not permitted\" errors.\n\t\tif *inst.Config.OS == limatype.DARWIN {\n\t\t\tmounts, err = directorySharingDevicesMacOS(inst.Config.Mounts)\n\t\t} else {\n\t\t\tmounts, err = directorySharingDevicesGeneric(inst.Config.Mounts)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar vzOpts limatype.VZOpts\n\tif err := limayaml.Convert(inst.Config.VMOpts[limatype.VZ], &vzOpts, \"vmOpts.vz\"); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %q\", inst.Config.VMOpts[limatype.VZ])\n\t}\n\n\tif vzOpts.Rosetta.Enabled != nil && *vzOpts.Rosetta.Enabled {\n\t\tlogrus.Info(\"Setting up Rosetta share\")\n\t\tdirectorySharingDeviceConfig, err := createRosettaDirectoryShareConfiguration()\n\t\tif err != nil {\n\t\t\tlogrus.Warnf(\"Unable to configure Rosetta: %s\", err)\n\t\t} else {\n\t\t\tmounts = append(mounts, directorySharingDeviceConfig)\n\t\t}\n\t}\n\n\tif len(mounts) > 0 {\n\t\tvmConfig.SetDirectorySharingDevicesVirtualMachineConfiguration(mounts)\n\t}\n\treturn nil\n}\n\nfunc attachAudio(inst *limatype.Instance, config *vz.VirtualMachineConfiguration) error {\n\tswitch *inst.Config.Audio.Device {\n\tcase \"vz\", \"default\":\n\t\toutputStream, err := vz.NewVirtioSoundDeviceHostOutputStreamConfiguration()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsoundDeviceConfiguration, err := vz.NewVirtioSoundDeviceConfiguration()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsoundDeviceConfiguration.SetStreams(outputStream)\n\t\tconfig.SetAudioDevicesVirtualMachineConfiguration([]vz.AudioDeviceConfiguration{\n\t\t\tsoundDeviceConfiguration,\n\t\t})\n\t\treturn nil\n\tcase \"\", \"none\":\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"unexpected audio device %q\", *inst.Config.Audio.Device)\n\t}\n}\n\nfunc attachOtherDevices(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {\n\tentropyConfig, err := vz.NewVirtioEntropyDeviceConfiguration()\n\tif err != nil {\n\t\treturn err\n\t}\n\tvmConfig.SetEntropyDevicesVirtualMachineConfiguration([]*vz.VirtioEntropyDeviceConfiguration{\n\t\tentropyConfig,\n\t})\n\n\tconfiguration, err := vz.NewVirtioTraditionalMemoryBalloonDeviceConfiguration()\n\tif err != nil {\n\t\treturn err\n\t}\n\tvmConfig.SetMemoryBalloonDevicesVirtualMachineConfiguration([]vz.MemoryBalloonDeviceConfiguration{\n\t\tconfiguration,\n\t})\n\n\tdeviceConfiguration, err := vz.NewVirtioSocketDeviceConfiguration()\n\tvmConfig.SetSocketDevicesVirtualMachineConfiguration([]vz.SocketDeviceConfiguration{\n\t\tdeviceConfiguration,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Set audio device\n\tinputAudioDeviceConfig, err := vz.NewVirtioSoundDeviceConfiguration()\n\tif err != nil {\n\t\treturn err\n\t}\n\tinputStream, err := vz.NewVirtioSoundDeviceHostInputStreamConfiguration()\n\tif err != nil {\n\t\treturn err\n\t}\n\tinputAudioDeviceConfig.SetStreams(\n\t\tinputStream,\n\t)\n\n\toutputAudioDeviceConfig, err := vz.NewVirtioSoundDeviceConfiguration()\n\tif err != nil {\n\t\treturn err\n\t}\n\toutputStream, err := vz.NewVirtioSoundDeviceHostOutputStreamConfiguration()\n\tif err != nil {\n\t\treturn err\n\t}\n\toutputAudioDeviceConfig.SetStreams(\n\t\toutputStream,\n\t)\n\tvmConfig.SetAudioDevicesVirtualMachineConfiguration([]vz.AudioDeviceConfiguration{\n\t\tinputAudioDeviceConfig,\n\t\toutputAudioDeviceConfig,\n\t})\n\n\t// Set pointing device\n\tvar pointingDeviceConfig vz.PointingDeviceConfiguration\n\tif *inst.Config.OS == limatype.DARWIN {\n\t\tpointingDeviceConfig, err = newMacPointingDeviceConfiguration()\n\t} else {\n\t\tpointingDeviceConfig, err = vz.NewUSBScreenCoordinatePointingDeviceConfiguration()\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tvmConfig.SetPointingDevicesVirtualMachineConfiguration([]vz.PointingDeviceConfiguration{\n\t\tpointingDeviceConfig,\n\t})\n\n\t// Set keyboard device\n\tvar keyboardDeviceConfig vz.KeyboardConfiguration\n\tif *inst.Config.OS == limatype.DARWIN {\n\t\tkeyboardDeviceConfig, err = newMacKeyboardConfiguration()\n\t} else {\n\t\tkeyboardDeviceConfig, err = vz.NewUSBKeyboardConfiguration()\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tvmConfig.SetKeyboardsVirtualMachineConfiguration([]vz.KeyboardConfiguration{\n\t\tkeyboardDeviceConfig,\n\t})\n\treturn nil\n}\n\ntype machineIdentifier interface {\n\tDataRepresentation() []byte\n}\n\nfunc getGenericMachineIdentifier(identifier string) (*vz.GenericMachineIdentifier, error) {\n\t// Empty VzIdentifier can be created on cloning an instance.\n\tif st, err := os.Stat(identifier); err != nil || (st != nil && st.Size() == 0) {\n\t\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\t\tmachineIdentifier, err := vz.NewGenericMachineIdentifier()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = os.WriteFile(identifier, machineIdentifier.DataRepresentation(), 0o666)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn machineIdentifier, nil\n\t}\n\treturn vz.NewGenericMachineIdentifierWithDataPath(identifier)\n}\n\nfunc bootLoader(inst *limatype.Instance) (vz.BootLoader, error) {\n\tif *inst.Config.OS == limatype.DARWIN {\n\t\treturn newMacOSBootLoader()\n\t}\n\tlinuxBootLoader, err := linuxBootLoader(inst)\n\tif linuxBootLoader != nil {\n\t\treturn linuxBootLoader, nil\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn nil, err\n\t}\n\n\tefiVariableStore, err := getEFI(inst)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlogrus.Debugf(\"Using EFI Boot Loader\")\n\treturn vz.NewEFIBootLoader(vz.WithEFIVariableStore(efiVariableStore))\n}\n\nfunc linuxBootLoader(inst *limatype.Instance) (*vz.LinuxBootLoader, error) {\n\tkernel := filepath.Join(inst.Dir, filenames.Kernel)\n\tkernelCmdline := filepath.Join(inst.Dir, filenames.KernelCmdline)\n\tinitrd := filepath.Join(inst.Dir, filenames.Initrd)\n\tif _, err := os.Stat(kernel); err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tlogrus.Debugf(\"Kernel file %q not found\", kernel)\n\t\t} else {\n\t\t\tlogrus.WithError(err).Debugf(\"Error while checking kernel file %q\", kernel)\n\t\t}\n\t\treturn nil, err\n\t}\n\tvar opt []vz.LinuxBootLoaderOption\n\tif b, err := os.ReadFile(kernelCmdline); err == nil {\n\t\tlogrus.Debugf(\"Using kernel command line %q\", string(b))\n\t\topt = append(opt, vz.WithCommandLine(string(b)))\n\t}\n\tif _, err := os.Stat(initrd); err == nil {\n\t\tlogrus.Debugf(\"Using initrd %q\", initrd)\n\t\topt = append(opt, vz.WithInitrd(initrd))\n\t}\n\tlogrus.Debugf(\"Using Linux Boot Loader with kernel %q\", kernel)\n\treturn vz.NewLinuxBootLoader(kernel, opt...)\n}\n\nfunc getEFI(inst *limatype.Instance) (*vz.EFIVariableStore, error) {\n\tefi := filepath.Join(inst.Dir, filenames.VzEfi)\n\tif _, err := os.Stat(efi); os.IsNotExist(err) {\n\t\treturn vz.NewEFIVariableStore(efi, vz.WithCreatingEFIVariableStore())\n\t}\n\treturn vz.NewEFIVariableStore(efi)\n}\n\nfunc createSockPair() (server, client *os.File, _ error) {\n\tpairs, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_DGRAM, 0)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tserverFD := pairs[0]\n\tclientFD := pairs[1]\n\n\tif err = syscall.SetsockoptInt(serverFD, syscall.SOL_SOCKET, syscall.SO_SNDBUF, 1*1024*1024); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err = syscall.SetsockoptInt(serverFD, syscall.SOL_SOCKET, syscall.SO_RCVBUF, 4*1024*1024); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err = syscall.SetsockoptInt(clientFD, syscall.SOL_SOCKET, syscall.SO_SNDBUF, 1*1024*1024); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err = syscall.SetsockoptInt(clientFD, syscall.SOL_SOCKET, syscall.SO_RCVBUF, 4*1024*1024); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tserver = os.NewFile(uintptr(serverFD), \"server\")\n\tclient = os.NewFile(uintptr(clientFD), \"client\")\n\truntime.SetFinalizer(server, func(*os.File) {\n\t\tlogrus.Debugf(\"Server network file GC'ed\")\n\t})\n\truntime.SetFinalizer(client, func(*os.File) {\n\t\tlogrus.Debugf(\"Client network file GC'ed\")\n\t})\n\tvmNetworkFiles = append(vmNetworkFiles, server, client)\n\treturn server, client, nil\n}\n\nfunc ensureIPSW(instDir string) error {\n\tipsw := filepath.Join(instDir, filenames.ImageIPSW)\n\tif osutil.FileExists(ipsw) {\n\t\treturn nil\n\t}\n\tipswBase := filepath.Join(instDir, filenames.Image)\n\tif _, err := os.Stat(ipswBase); err != nil {\n\t\treturn err\n\t}\n\t// The installer wants the file to have \".ipsw\" suffix.\n\t// The link is created as a hard link, as the installer does not accept symlinks.\n\tif err := os.Link(ipswBase, ipsw); err != nil {\n\t\treturn fmt.Errorf(\"failed to create hard link from %q to %q: %w\", ipswBase, ipsw, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driver/vz/vm_darwin_amd64.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"context\"\n\n\t\"github.com/Code-Hex/vz/v3\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nfunc getMacMachineIdentifier(_ string) (machineIdentifier, error) {\n\treturn nil, errMacOSGuestUnsupported\n}\n\nfunc newMacPlatformConfiguration(_ *limatype.Instance) (vz.PlatformConfiguration, error) {\n\treturn nil, errMacOSGuestUnsupported\n}\n\nfunc newMacGraphicsDeviceConfiguration(_, _, _ int64) (vz.GraphicsDeviceConfiguration, error) {\n\treturn nil, errMacOSGuestUnsupported\n}\n\nfunc newMacPointingDeviceConfiguration() (vz.PointingDeviceConfiguration, error) {\n\treturn nil, errMacOSGuestUnsupported\n}\n\nfunc newMacKeyboardConfiguration() (vz.KeyboardConfiguration, error) {\n\treturn nil, errMacOSGuestUnsupported\n}\n\nfunc newMacOSBootLoader() (vz.BootLoader, error) {\n\treturn nil, errMacOSGuestUnsupported\n}\n\nfunc installMacOS(_ context.Context, _ *vz.VirtualMachine, _ string) error {\n\treturn errMacOSGuestUnsupported\n}\n"
  },
  {
    "path": "pkg/driver/vz/vm_darwin_arm64.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/Code-Hex/vz/v3\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/progressbar\"\n)\n\nfunc getMacMachineIdentifier(identifier string) (machineIdentifier, error) {\n\t// Empty VzIdentifier can be created on cloning an instance.\n\tif st, err := os.Stat(identifier); err != nil || (st != nil && st.Size() == 0) {\n\t\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\t\tmachineIdentifier, err := vz.NewMacMachineIdentifier()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\terr = os.WriteFile(identifier, machineIdentifier.DataRepresentation(), 0o666)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn machineIdentifier, nil\n\t}\n\treturn vz.NewMacMachineIdentifierWithDataPath(identifier)\n}\n\nfunc newMacPlatformConfiguration(inst *limatype.Instance) (vz.PlatformConfiguration, error) {\n\tidentifierFile := filepath.Join(inst.Dir, filenames.VzIdentifier)\n\tmachineIdentifier, err := getMacMachineIdentifier(identifierFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar hwModelData []byte\n\thwModelFile := filepath.Join(inst.Dir, filenames.VzHwModel)\n\tif osutil.FileExists(hwModelFile) {\n\t\thwModelData, err = os.ReadFile(hwModelFile)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif err = ensureIPSW(inst.Dir); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tipsw := filepath.Join(inst.Dir, filenames.ImageIPSW)\n\t\tipswImage, err := vz.LoadMacOSRestoreImageFromPath(ipsw)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tipswMostFeatureFul := ipswImage.MostFeaturefulSupportedConfiguration()\n\t\thwModelData = ipswMostFeatureFul.HardwareModel().DataRepresentation()\n\t\tif err = os.WriteFile(hwModelFile, hwModelData, 0o666); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\thwModel, err := vz.NewMacHardwareModelWithData(hwModelData)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauxFile := filepath.Join(inst.Dir, filenames.VzAux)\n\tvar auxOps []vz.NewMacAuxiliaryStorageOption\n\tif !osutil.FileExists(auxFile) {\n\t\tauxOps = append(auxOps, vz.WithCreatingMacAuxiliaryStorage(hwModel))\n\t}\n\taux, err := vz.NewMacAuxiliaryStorage(auxFile, auxOps...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplatformConfig, err := vz.NewMacPlatformConfiguration(\n\t\tvz.WithMacMachineIdentifier(machineIdentifier.(*vz.MacMachineIdentifier)),\n\t\tvz.WithMacHardwareModel(hwModel),\n\t\tvz.WithMacAuxiliaryStorage(aux),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn platformConfig, nil\n}\n\nfunc newMacGraphicsDeviceConfiguration(x, y, pixelsPerInch int64) (vz.GraphicsDeviceConfiguration, error) {\n\tgraphicsDeviceConfiguration, err := vz.NewMacGraphicsDeviceConfiguration()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tscanoutConfiguration, err := vz.NewMacGraphicsDisplayConfiguration(x, y, pixelsPerInch)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgraphicsDeviceConfiguration.SetDisplays(scanoutConfiguration)\n\treturn graphicsDeviceConfiguration, nil\n}\n\nfunc newMacPointingDeviceConfiguration() (vz.PointingDeviceConfiguration, error) {\n\treturn vz.NewMacTrackpadConfiguration()\n}\n\nfunc newMacKeyboardConfiguration() (vz.KeyboardConfiguration, error) {\n\treturn vz.NewMacKeyboardConfiguration()\n}\n\nfunc newMacOSBootLoader() (vz.BootLoader, error) {\n\treturn vz.NewMacOSBootLoader()\n}\n\nfunc installMacOS(ctx context.Context, vm *vz.VirtualMachine, ipsw string) error {\n\tinstaller, err := vz.NewMacOSInstaller(vm, ipsw)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconst barResolution = 100\n\tbar, err := progressbar.New(barResolution)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbar.Start()\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tgo func() {\n\t\tticker := time.NewTicker(3 * time.Second)\n\t\tdefer ticker.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tlogrus.WithError(ctx.Err()).Info(\"cancelling macOS installation\")\n\t\t\t\treturn\n\t\t\tcase <-installer.Done():\n\t\t\t\tlogrus.WithError(ctx.Err()).Info(\"macOS installer exited\")\n\t\t\t\tbar.SetCurrent(barResolution)\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tprogress := installer.FractionCompleted() * barResolution\n\t\t\t\tbar.SetCurrent(int64(progress))\n\t\t\t}\n\t\t}\n\t}()\n\n\terr = installer.Install(ctx)\n\tbar.Finish()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstopped, err := vm.RequestStop()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to stop VM after installation: %w\", err)\n\t}\n\tif !stopped {\n\t\tlogrus.WithError(err).Warn(\"VM did not stop after installation\")\n\t\tif err = vm.Stop(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to force stop VM after installation: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driver/vz/vsock_forwarder.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\n\t\"github.com/inetaf/tcpproxy\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc (m *virtualMachineWrapper) startVsockForwarder(ctx context.Context, vsockPort uint32, hostAddress string) error {\n\t// Test if the vsock port is open\n\tconn, err := m.dialVsock(ctx, vsockPort)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconn.Close()\n\t// Start listening on localhost:hostPort and forward to vsock:vsockPort\n\t_, _, err = net.SplitHostPort(hostAddress)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar lc net.ListenConfig\n\tl, err := lc.Listen(ctx, \"tcp\", hostAddress)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tl.Close()\n\t}()\n\tlogrus.Infof(\"Started vsock forwarder: %s -> vsock:%d on VM\", hostAddress, vsockPort)\n\tgo func() {\n\t\tdefer l.Close()\n\t\tfor {\n\t\t\tconn, err := l.Accept()\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlogrus.WithError(err).Errorf(\"vsock forwarder accept error: %v\", err)\n\t\t\t} else {\n\t\t\t\tp := tcpproxy.DialProxy{\n\t\t\t\t\tDialContext: func(_ context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\t\t\treturn m.dialVsock(ctx, vsockPort)\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\tgo p.HandleConn(conn)\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}()\n\treturn nil\n}\n\nfunc (m *virtualMachineWrapper) dialVsock(_ context.Context, port uint32) (conn net.Conn, err error) {\n\tfor _, socket := range m.SocketDevices() {\n\t\tconn, err = socket.Connect(port)\n\t\tif err == nil {\n\t\t\treturn conn, nil\n\t\t}\n\t}\n\treturn nil, err\n}\n"
  },
  {
    "path": "pkg/driver/vz/vz_driver_darwin.go",
    "content": "//go:build darwin && !no_vz\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage vz\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/Code-Hex/vz/v3\"\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/docker/go-units\"\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"github.com/lima-vm/go-qcow2reader/image/asif\"\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestpatch/macos\"\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n\t\"github.com/lima-vm/lima/v2/pkg/reflectutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n)\n\nvar knownYamlProperties = []string{\n\t\"AdditionalDisks\",\n\t\"Arch\",\n\t\"Audio\",\n\t\"CACertificates\",\n\t\"Containerd\",\n\t\"CopyToHost\",\n\t\"CPUs\",\n\t\"CPUType\",\n\t\"Disk\",\n\t\"DNS\",\n\t\"Env\",\n\t\"Firmware\",\n\t\"GuestInstallPrefix\",\n\t\"HostResolver\",\n\t\"Images\",\n\t\"Memory\",\n\t\"Message\",\n\t\"MinimumLimaVersion\",\n\t\"Mounts\",\n\t\"MountType\",\n\t\"MountTypesUnsupported\",\n\t\"MountInotify\",\n\t\"NestedVirtualization\",\n\t\"Networks\",\n\t\"OS\",\n\t\"Param\",\n\t\"Plain\",\n\t\"PortForwards\",\n\t\"Probes\",\n\t\"PropagateProxyEnv\",\n\t\"Provision\",\n\t\"Rosetta\",\n\t\"SSH\",\n\t\"TimeZone\",\n\t\"UpgradePackages\",\n\t\"User\",\n\t\"Video\",\n\t\"VMType\",\n\t\"VMOpts\",\n}\n\nconst Enabled = true\n\ntype LimaVzDriver struct {\n\tInstance *limatype.Instance\n\n\tSSHLocalPort    int\n\tvSockPort       int\n\tvirtioPort      string\n\trosettaEnabled  bool\n\trosettaBinFmt   bool\n\tdiskImageFormat image.Type\n\n\tmachine                    *virtualMachineWrapper\n\twaitSSHLocalPortAccessible <-chan any\n\n\tonVsockEvent func(*events.VsockEvent)\n}\n\nvar (\n\t_ driver.Driver            = (*LimaVzDriver)(nil)\n\t_ driver.VsockEventEmitter = (*LimaVzDriver)(nil)\n)\n\n// SetVsockEventCallback implements driver.VsockEventEmitter.\nfunc (l *LimaVzDriver) SetVsockEventCallback(callback func(*events.VsockEvent)) {\n\tl.onVsockEvent = callback\n}\n\nfunc New() *LimaVzDriver {\n\treturn &LimaVzDriver{\n\t\tvSockPort:  2222,\n\t\tvirtioPort: \"\",\n\t}\n}\n\nfunc (l *LimaVzDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriver {\n\tl.Instance = inst\n\tl.SSHLocalPort = inst.SSHLocalPort\n\n\tif l.Instance.Config.MountType != nil {\n\t\tmountTypesUnsupported := make(map[string]struct{})\n\t\tfor _, f := range l.Instance.Config.MountTypesUnsupported {\n\t\t\tmountTypesUnsupported[f] = struct{}{}\n\t\t}\n\n\t\tif _, ok := mountTypesUnsupported[*l.Instance.Config.MountType]; ok {\n\t\t\t// We cannot return an error here, but Validate() will return it.\n\t\t\tlogrus.Warnf(\"Unsupported mount type: %q\", *l.Instance.Config.MountType)\n\t\t}\n\t}\n\n\tvar vzOpts limatype.VZOpts\n\tif l.Instance.Config.VMOpts[limatype.VZ] != nil {\n\t\tif err := limayaml.Convert(l.Instance.Config.VMOpts[limatype.VZ], &vzOpts, \"vmOpts.vz\"); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %q\", l.Instance.Config.VMOpts[limatype.VZ])\n\t\t}\n\t}\n\n\tif runtime.GOOS == \"darwin\" && limatype.IsNativeArch(limatype.AARCH64) {\n\t\tif vzOpts.Rosetta.Enabled != nil {\n\t\t\tl.rosettaEnabled = *vzOpts.Rosetta.Enabled\n\t\t}\n\t}\n\tif vzOpts.Rosetta.BinFmt != nil {\n\t\tl.rosettaBinFmt = *vzOpts.Rosetta.BinFmt\n\t}\n\tif vzOpts.DiskImageFormat != nil {\n\t\tl.diskImageFormat = *vzOpts.DiskImageFormat\n\t} else {\n\t\tl.diskImageFormat = raw.Type\n\t}\n\n\treturn &driver.ConfiguredDriver{\n\t\tDriver: l,\n\t}\n}\n\nfunc (l *LimaVzDriver) FillConfig(ctx context.Context, cfg *limatype.LimaYAML, _ string) error {\n\tif cfg.VMType == nil {\n\t\tcfg.VMType = ptr.Of(limatype.VZ)\n\t}\n\n\tif cfg.MountType == nil {\n\t\tcfg.MountType = ptr.Of(limatype.VIRTIOFS)\n\t}\n\n\tif cfg.SSH.OverVsock == nil {\n\t\tcfg.SSH.OverVsock = ptr.Of(*cfg.OS == limatype.LINUX)\n\t}\n\n\tvar vzOpts limatype.VZOpts\n\tif err := limayaml.Convert(cfg.VMOpts[limatype.VZ], &vzOpts, \"vmOpts.vz\"); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %q\", cfg.VMOpts[limatype.VZ])\n\t}\n\n\t//nolint:staticcheck // Migration of top-level Rosetta if specified\n\tif (vzOpts.Rosetta.Enabled == nil && vzOpts.Rosetta.BinFmt == nil) && (!isEmpty(cfg.Rosetta)) {\n\t\tlogrus.Debug(\"Migrating top-level Rosetta configuration to vmOpts.vz.rosetta\")\n\t\tvzOpts.Rosetta = cfg.Rosetta\n\t}\n\t//nolint:staticcheck // Warning about both top-level and vmOpts.vz.Rosetta being set\n\tif (vzOpts.Rosetta.Enabled != nil && vzOpts.Rosetta.BinFmt != nil) && (!isEmpty(cfg.Rosetta)) {\n\t\tlogrus.Warn(\"Both top-level 'rosetta' and 'vmOpts.vz.rosetta' are configured. Using vmOpts.vz.rosetta. Top-level 'rosetta' is deprecated.\")\n\t}\n\n\tif vzOpts.Rosetta.Enabled == nil {\n\t\tvzOpts.Rosetta.Enabled = ptr.Of(false)\n\t}\n\tif vzOpts.Rosetta.BinFmt == nil {\n\t\tvzOpts.Rosetta.BinFmt = ptr.Of(false)\n\t}\n\tif vzOpts.DiskImageFormat == nil {\n\t\tvzOpts.DiskImageFormat = ptr.Of(raw.Type)\n\t}\n\n\tvar opts any\n\tif err := limayaml.Convert(vzOpts, &opts, \"\"); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %+v\", vzOpts)\n\t}\n\tif cfg.VMOpts == nil {\n\t\tcfg.VMOpts = limatype.VMOpts{}\n\t}\n\tcfg.VMOpts[limatype.VZ] = opts\n\n\treturn validateConfig(ctx, cfg)\n}\n\nfunc isEmpty(r limatype.Rosetta) bool {\n\treturn r.Enabled == nil && r.BinFmt == nil\n}\n\n//go:embed boot.Linux/*.sh\nvar bootLinuxFS embed.FS\n\nfunc (l *LimaVzDriver) BootScripts() (map[string][]byte, error) {\n\tscripts := make(map[string][]byte)\n\n\tentries, err := bootLinuxFS.ReadDir(\"boot.Linux\")\n\tif err == nil {\n\t\tfor _, entry := range entries {\n\t\t\tif entry.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tentryPath := \"boot.Linux/\" + entry.Name()\n\n\t\t\tcontent, err := bootLinuxFS.ReadFile(entryPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tscripts[entryPath] = content\n\t\t}\n\t}\n\n\treturn scripts, nil\n}\n\nfunc (l *LimaVzDriver) Validate(ctx context.Context) error {\n\treturn validateConfig(ctx, l.Instance.Config)\n}\n\nfunc validateConfig(_ context.Context, cfg *limatype.LimaYAML) error {\n\tif cfg == nil {\n\t\treturn errors.New(\"configuration is nil\")\n\t}\n\tmacOSProductVersion, err := osutil.ProductVersion()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif macOSProductVersion.LessThan(*semver.New(\"13.0.0\")) {\n\t\treturn errors.New(\"VZ driver requires macOS 13 or higher to run\")\n\t}\n\tif runtime.GOARCH == \"amd64\" && macOSProductVersion.LessThan(*semver.New(\"15.5.0\")) {\n\t\tlogrus.Warnf(\"vmType %s: On Intel Mac, macOS 15.5 or later is required to run Linux 6.12 or later. \"+\n\t\t\t\"Update macOS, or change vmType to \\\"qemu\\\" if the VM does not start up. (https://github.com/lima-vm/lima/issues/3334)\",\n\t\t\t*cfg.VMType)\n\t}\n\tif cfg.MountType != nil && *cfg.MountType == limatype.NINEP {\n\t\treturn fmt.Errorf(\"field `mountType` must be %q or %q for VZ driver , got %q\", limatype.REVSSHFS, limatype.VIRTIOFS, *cfg.MountType)\n\t}\n\tif *cfg.Firmware.LegacyBIOS {\n\t\tlogrus.Warnf(\"vmType %s: ignoring `firmware.legacyBIOS`\", *cfg.VMType)\n\t}\n\tfor _, f := range cfg.Firmware.Images {\n\t\tswitch f.VMType {\n\t\tcase \"\", limatype.VZ:\n\t\t\tif f.Arch == *cfg.Arch {\n\t\t\t\treturn errors.New(\"`firmware.images` configuration is not supported for VZ driver\")\n\t\t\t}\n\t\t}\n\t}\n\tif unknown := reflectutil.UnknownNonEmptyFields(cfg, knownYamlProperties...); cfg.VMType != nil && len(unknown) > 0 {\n\t\tlogrus.Warnf(\"vmType %s: ignoring %+v\", *cfg.VMType, unknown)\n\t}\n\n\tif !limatype.IsNativeArch(*cfg.Arch) {\n\t\treturn fmt.Errorf(\"unsupported arch: %q\", *cfg.Arch)\n\t}\n\n\tfor i, image := range cfg.Images {\n\t\tif unknown := reflectutil.UnknownNonEmptyFields(image, \"File\", \"Kernel\", \"Initrd\"); len(unknown) > 0 {\n\t\t\tlogrus.Warnf(\"vmType %s: ignoring images[%d]: %+v\", *cfg.VMType, i, unknown)\n\t\t}\n\t}\n\n\tfor i, mount := range cfg.Mounts {\n\t\tif unknown := reflectutil.UnknownNonEmptyFields(mount, \"Location\",\n\t\t\t\"MountPoint\",\n\t\t\t\"Writable\",\n\t\t\t\"SSHFS\",\n\t\t\t\"NineP\",\n\t\t); len(unknown) > 0 {\n\t\t\tlogrus.Warnf(\"vmType %s: ignoring mounts[%d]: %+v\", *cfg.VMType, i, unknown)\n\t\t}\n\t}\n\n\tfor i, nw := range cfg.Networks {\n\t\tif unknown := reflectutil.UnknownNonEmptyFields(nw, \"VZNAT\",\n\t\t\t\"Lima\",\n\t\t\t\"Socket\",\n\t\t\t\"MACAddress\",\n\t\t\t\"Metric\",\n\t\t\t\"Interface\",\n\t\t); len(unknown) > 0 {\n\t\t\tlogrus.Warnf(\"vmType %s: ignoring networks[%d]: %+v\", *cfg.VMType, i, unknown)\n\t\t}\n\t}\n\n\tswitch audioDevice := *cfg.Audio.Device; audioDevice {\n\tcase \"\":\n\tcase \"vz\", \"default\", \"none\":\n\tdefault:\n\t\tlogrus.Warnf(\"field `audio.device` must be \\\"vz\\\", \\\"default\\\", or \\\"none\\\" for VZ driver, got %q\", audioDevice)\n\t}\n\n\tswitch videoDisplay := *cfg.Video.Display; videoDisplay {\n\tcase \"vz\", \"default\", \"none\":\n\tdefault:\n\t\tlogrus.Warnf(\"field `video.display` must be \\\"vz\\\", \\\"default\\\", or \\\"none\\\" for VZ driver , got %q\", videoDisplay)\n\t}\n\tvar vzOpts limatype.VZOpts\n\tif err := limayaml.Convert(cfg.VMOpts[limatype.VZ], &vzOpts, \"vmOpts.vz\"); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"Couldn't convert %q\", cfg.VMOpts[limatype.VZ])\n\t}\n\tswitch *vzOpts.DiskImageFormat {\n\tcase raw.Type:\n\tcase asif.Type:\n\t\tif macOSProductVersion.LessThan(*semver.New(\"26.0.0\")) {\n\t\t\treturn fmt.Errorf(\"vmOpts.vz.diskImageFormat=%q requires macOS 26 or higher to run, got %q\", asif.Type, macOSProductVersion)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"field `vmOpts.vz.diskImageFormat` must be %q or %q, got %q\", raw.Type, asif.Type, *vzOpts.DiskImageFormat)\n\t}\n\treturn nil\n}\n\nfunc (l *LimaVzDriver) Create(_ context.Context) error {\n\tidentifierFile := filepath.Join(l.Instance.Dir, filenames.VzIdentifier)\n\tif *l.Instance.Config.OS == limatype.DARWIN {\n\t\t_, err := getMacMachineIdentifier(identifierFile)\n\t\treturn err\n\t}\n\t_, err := getGenericMachineIdentifier(identifierFile)\n\treturn err\n}\n\nfunc (l *LimaVzDriver) CreateDisk(ctx context.Context) error {\n\tif *l.Instance.Config.OS == limatype.DARWIN {\n\t\tdisk := filepath.Join(l.Instance.Dir, filenames.Disk)\n\t\tif !osutil.FileExists(disk) {\n\t\t\tif err := l.createDiskMacOSGuest(ctx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tpatchedMarker := disk + \".patched\" // empty file\n\t\tif !osutil.FileExists(patchedMarker) {\n\t\t\tlogrus.Infof(\"Patching macOS disk %q\", disk)\n\t\t\tif err := macos.Patch(ctx, disk); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := os.WriteFile(patchedMarker, []byte{}, 0o644); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn driverutil.EnsureDisk(ctx, l.Instance.Dir, *l.Instance.Config.Disk, l.diskImageFormat)\n}\n\n// createDiskMacOSGuest creates `disk` and installs macOS from `image` on it.\n// The function must not be called if `disk` already exists.\n//\n// The function creates the following files:\n// - `image.ipsw`: hardlink to `image` (\".ipsw\" suffix is required by VZMacOSInstaller)\n// - `disk`: ASIF disk\n//\n// After successful installation, `image.ipsw` and `image` are removed.\nfunc (l *LimaVzDriver) createDiskMacOSGuest(ctx context.Context) error {\n\tdisk := filepath.Join(l.Instance.Dir, filenames.Disk)\n\n\tdiskSize, err := units.RAMInBytes(*l.Instance.Config.Disk)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid disk size %q: %w\", *l.Instance.Config.Disk, err)\n\t}\n\tif err := asifutil.NewASIF(disk, diskSize); err != nil {\n\t\treturn err\n\t}\n\n\tif err = ensureIPSW(l.Instance.Dir); err != nil {\n\t\treturn err\n\t}\n\tipsw := filepath.Join(l.Instance.Dir, filenames.ImageIPSW)\n\n\tvm, err := createVMForMacInstaller(ctx, l.Instance)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogrus.Info(\"Running macOS installer (takes a few minutes)\")\n\t// FIXME: do we need to run the installer for every new instance,\n\t// or can we safely reuse the installed disk image?\n\tif err := installMacOS(ctx, vm, ipsw); err != nil {\n\t\treturn fmt.Errorf(\"failed to install macOS: %w\", err)\n\t}\n\n\tfilesToClean := []string{\n\t\tipsw,\n\t\tfilepath.Join(l.Instance.Dir, filenames.Image),\n\t}\n\tfor _, file := range filesToClean {\n\t\tif err := os.RemoveAll(file); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"Failed to remove %q\", file)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (l *LimaVzDriver) Start(ctx context.Context) (chan error, error) {\n\tlogrus.Infof(\"Starting VZ (hint: to watch the boot progress, see %q)\", filepath.Join(l.Instance.Dir, \"serial*.log\"))\n\tvm, waitSSHLocalPortAccessible, errCh, err := startVM(ctx, l.Instance, l.SSHLocalPort, l.onVsockEvent)\n\tif err != nil {\n\t\tif errors.Is(err, vz.ErrUnsupportedOSVersion) {\n\t\t\treturn nil, fmt.Errorf(\"vz driver requires macOS 13 or higher to run: %w\", err)\n\t\t}\n\t\treturn nil, err\n\t}\n\tl.machine = vm\n\tl.waitSSHLocalPortAccessible = waitSSHLocalPortAccessible\n\n\treturn errCh, nil\n}\n\nfunc (l *LimaVzDriver) canRunGUI() bool {\n\tswitch *l.Instance.Config.Video.Display {\n\tcase \"vz\", \"default\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (l *LimaVzDriver) RunGUI() error {\n\tif l.canRunGUI() {\n\t\treturn l.machine.StartGraphicApplication(1920, 1200)\n\t}\n\treturn fmt.Errorf(\"RunGUI is not supported for the given driver '%s' and display '%s'\", \"vz\", *l.Instance.Config.Video.Display)\n}\n\nfunc (l *LimaVzDriver) requestStopViaSSH(ctx context.Context) error {\n\tsshExe, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd := exec.CommandContext(ctx, sshExe.Exe,\n\t\tappend(sshExe.Args, \"-F\", l.Instance.SSHConfigFile, l.Instance.Hostname, \"--\",\n\t\t\t\"sudo\", \"/sbin/shutdown\", \"-h\", \"now\")...)\n\tlogrus.Infof(\"Running shutdown command in the VM: %v\", cmd.Args)\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w (output=%s)\", cmd.Args, err, string(out))\n\t}\n\treturn nil\n}\n\nfunc (l *LimaVzDriver) Stop(ctx context.Context) error {\n\tlogrus.Info(\"Shutting down VZ\")\n\tcanStop := l.machine.CanRequestStop()\n\n\tif canStop {\n\t\t_, err := l.machine.RequestStop()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif *l.Instance.Config.OS == limatype.DARWIN {\n\t\t\t// macOS VM does not respond to l.machine.RequestStop(),\n\t\t\t// so we need to run `shutdown -h now` in the VM via SSH for graceful shutdown.\n\t\t\tif err := l.requestStopViaSSH(ctx); err != nil {\n\t\t\t\tlogrus.WithError(err).Warn(\"Failed to request shutdown via SSH\")\n\t\t\t}\n\t\t}\n\n\t\t// Most Linux machines shutdown within 5 seconds, but macOS machines can take longer.\n\t\ttimeout := time.After(30 * time.Second)\n\t\tticker := time.NewTicker(500 * time.Millisecond)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-timeout:\n\t\t\t\treturn errors.New(\"vz timeout while waiting for stop status\")\n\t\t\tcase <-ticker.C:\n\t\t\t\tl.machine.mu.Lock()\n\t\t\t\tstopped := l.machine.stopped\n\t\t\t\tl.machine.mu.Unlock()\n\t\t\t\tif stopped {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors.New(\"vz: CanRequestStop is not supported\")\n}\n\nfunc (l *LimaVzDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) {\n\tfor _, socket := range l.machine.SocketDevices() {\n\t\tconnect, err := socket.Connect(uint32(l.vSockPort))\n\t\treturn connect, \"vsock\", err\n\t}\n\n\treturn nil, \"\", errors.New(\"unable to connect to guest agent via vsock port 2222\")\n}\n\nfunc (l *LimaVzDriver) Info() driver.Info {\n\tvar info driver.Info\n\n\tinfo.Name = \"vz\"\n\tinfo.VsockPort = l.vSockPort\n\tinfo.VirtioPort = l.virtioPort\n\tif l.Instance != nil {\n\t\tinfo.InstanceDir = l.Instance.Dir\n\t}\n\n\tvar guiFlag bool\n\tif l.Instance != nil {\n\t\tguiFlag = l.canRunGUI()\n\t}\n\tinfo.Features = driver.DriverFeatures{\n\t\tDynamicSSHAddress:    false,\n\t\tSkipSocketForwarding: false,\n\t\tCanRunGUI:            guiFlag,\n\t\tRosettaEnabled:       l.rosettaEnabled,\n\t\tRosettaBinFmt:        l.rosettaBinFmt,\n\t}\n\treturn info\n}\n\nfunc (l *LimaVzDriver) SSHAddress(_ context.Context) (string, error) {\n\treturn \"127.0.0.1\", nil\n}\n\nfunc (l *LimaVzDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string {\n\treturn \"\"\n}\n\nfunc (l *LimaVzDriver) Delete(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaVzDriver) Register(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaVzDriver) Unregister(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaVzDriver) ChangeDisplayPassword(_ context.Context, _ string) error {\n\treturn nil\n}\n\nfunc (l *LimaVzDriver) DisplayConnection(_ context.Context) (string, error) {\n\treturn \"\", nil\n}\n\nfunc (l *LimaVzDriver) CreateSnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaVzDriver) ApplySnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaVzDriver) DeleteSnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaVzDriver) ListSnapshots(_ context.Context) (string, error) {\n\treturn \"\", errUnimplemented\n}\n\nfunc (l *LimaVzDriver) ForwardGuestAgent() bool {\n\t// If driver is not providing, use host agent\n\treturn l.vSockPort == 0 && l.virtioPort == \"\"\n}\n\nfunc (l *LimaVzDriver) AdditionalSetupForSSH(_ context.Context) error {\n\t<-l.waitSSHLocalPortAccessible\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driver/wsl2/boot.Linux/02-no-cloud-init-setup.sh",
    "content": "#!/bin/sh\n\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\n# This script replaces the cloud-init functionality of creating a user and setting its SSH keys\n# when cloud-init is not available\n[ \"$LIMA_CIDATA_NO_CLOUD_INIT\" = \"1\" ] || exit 0\n\n# create user\n# shellcheck disable=SC2153\nuseradd -u \"${LIMA_CIDATA_UID}\" \"${LIMA_CIDATA_USER}\" -c \"${LIMA_CIDATA_COMMENT}\" -d \"${LIMA_CIDATA_HOME}\" -m -s \"${LIMA_CIDATA_SHELL}\"\nLIMA_CIDATA_GID=$(id -g \"${LIMA_CIDATA_USER}\")\nmkdir \"${LIMA_CIDATA_HOME}\"/.ssh/\nchown \"${LIMA_CIDATA_UID}:${LIMA_CIDATA_GID}\" \"${LIMA_CIDATA_HOME}\"/.ssh/\nchmod 700 \"${LIMA_CIDATA_HOME}\"/.ssh/\ncp \"${LIMA_CIDATA_MNT}\"/ssh_authorized_keys \"${LIMA_CIDATA_HOME}\"/.ssh/authorized_keys\nchown \"${LIMA_CIDATA_UID}:${LIMA_CIDATA_GID}\" \"${LIMA_CIDATA_HOME}\"/.ssh/authorized_keys\nchmod 600 \"${LIMA_CIDATA_HOME}\"/.ssh/authorized_keys\n\n# add $LIMA_CIDATA_USER to sudoers\necho \"${LIMA_CIDATA_USER} ALL=(ALL) NOPASSWD:ALL\" | tee -a /etc/sudoers.d/99_lima_sudoers\n\n# symlink CIDATA to the hardcoded path for requirement checks (TODO: make this not hardcoded)\n[ \"$LIMA_CIDATA_MNT\" = \"/mnt/lima-cidata\" ] || ln -sfFn \"${LIMA_CIDATA_MNT}\" /mnt/lima-cidata\n"
  },
  {
    "path": "pkg/driver/wsl2/errors_windows.go",
    "content": "//go:build windows && !no_wsl\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage wsl2\n\nimport \"errors\"\n\nvar errUnimplemented = errors.New(\"unimplemented\")\n"
  },
  {
    "path": "pkg/driver/wsl2/fs.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage wsl2\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/fileutils\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\n// EnsureFs downloads the root fs.\nfunc EnsureFs(ctx context.Context, inst *limatype.Instance) error {\n\tbaseDisk := filepath.Join(inst.Dir, filenames.BaseDiskLegacy)\n\tif _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) {\n\t\tvar ensuredBaseDisk bool\n\t\terrs := make([]error, len(inst.Config.Images))\n\t\tfor i, f := range inst.Config.Images {\n\t\t\tif _, err := fileutils.DownloadFile(ctx, baseDisk, f.File, true, \"the image\", *inst.Config.Arch); err != nil {\n\t\t\t\terrs[i] = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tensuredBaseDisk = true\n\t\t\tbreak\n\t\t}\n\t\tif !ensuredBaseDisk {\n\t\t\treturn fileutils.Errors(errs)\n\t\t}\n\t}\n\tlogrus.Info(\"Download succeeded\")\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driver/wsl2/lima-init.TEMPLATE",
    "content": "#!/bin/bash\nset -eu\nexport LOG_FILE=/var/log/lima-init.log\nexec > >(tee $LOG_FILE) 2>&1\nexport LIMA_CIDATA_MNT=\"$(/usr/bin/wslpath '{{.CIDataPath}}')\"\nexec \"$LIMA_CIDATA_MNT/boot.sh\"\n"
  },
  {
    "path": "pkg/driver/wsl2/register.go",
    "content": "//go:build windows && !external_wsl2\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage wsl2\n\nimport \"github.com/lima-vm/lima/v2/pkg/registry\"\n\nfunc init() {\n\tregistry.Register(New())\n}\n"
  },
  {
    "path": "pkg/driver/wsl2/vm_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage wsl2\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/executil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/textutil\"\n)\n\n// startVM calls WSL to start a VM.\nfunc startVM(ctx context.Context, distroName string) error {\n\tout, err := executil.RunUTF16leCommand([]string{\n\t\t\"wsl.exe\",\n\t\t\"--distribution\",\n\t\tdistroName,\n\t}, executil.WithContext(ctx))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run `wsl.exe --distribution %s`: %w (out=%q)\",\n\t\t\tdistroName, err, out)\n\t}\n\treturn nil\n}\n\n// initVM calls WSL to import a new VM specifically for Lima.\nfunc initVM(ctx context.Context, instanceDir, distroName string) error {\n\tbaseDisk := filepath.Join(instanceDir, filenames.BaseDiskLegacy)\n\tlogrus.Infof(\"Importing distro from %q to %q\", baseDisk, instanceDir)\n\tout, err := executil.RunUTF16leCommand([]string{\n\t\t\"wsl.exe\",\n\t\t\"--import\",\n\t\tdistroName,\n\t\tinstanceDir,\n\t\tbaseDisk,\n\t}, executil.WithContext(ctx))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run `wsl.exe --import %s %s %s`: %w (out=%q)\",\n\t\t\tdistroName, instanceDir, baseDisk, err, out)\n\t}\n\treturn nil\n}\n\n// stopVM calls WSL to stop a running VM.\nfunc stopVM(ctx context.Context, distroName string) error {\n\tout, err := executil.RunUTF16leCommand([]string{\n\t\t\"wsl.exe\",\n\t\t\"--terminate\",\n\t\tdistroName,\n\t}, executil.WithContext(ctx))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run `wsl.exe --terminate %s`: %w (out=%q)\",\n\t\t\tdistroName, err, out)\n\t}\n\treturn nil\n}\n\n//go:embed lima-init.TEMPLATE\nvar limaBoot string\n\n// provisionVM starts Lima's boot process inside an already imported VM.\nfunc provisionVM(ctx context.Context, instanceDir, instanceName, distroName string, errCh chan<- error) error {\n\tciDataPath := filepath.Join(instanceDir, filenames.CIDataISODir)\n\tm := map[string]string{\n\t\t\"CIDataPath\": ciDataPath,\n\t}\n\tlimaBootB, err := textutil.ExecuteTemplate(limaBoot, m)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to construct wsl boot.sh script: %w\", err)\n\t}\n\tlimaBootFile, err := os.CreateTemp(\"\", \"lima-wsl2-boot-*.sh\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif _, err = limaBootFile.Write(limaBootB); err != nil {\n\t\tlimaBootFile.Close()\n\t\treturn err\n\t}\n\tlimaBootFileWinPath := limaBootFile.Name()\n\tif err = limaBootFile.Close(); err != nil {\n\t\treturn err\n\t}\n\t// path should be quoted and use \\\\ as separator\n\tbootFileWSLPath := strconv.Quote(limaBootFileWinPath)\n\tlimaBootFilePathOnLinuxB, err := exec.CommandContext(\n\t\tctx,\n\t\t\"wsl.exe\",\n\t\t\"-d\",\n\t\tdistroName,\n\t\t\"bash\",\n\t\t\"-c\",\n\t\tfmt.Sprintf(\"wslpath -u %s\", bootFileWSLPath),\n\t\tbootFileWSLPath,\n\t).Output()\n\tif err != nil {\n\t\tos.RemoveAll(limaBootFileWinPath)\n\t\t// this can return an error with an exit code, which causes it not to be logged\n\t\t// because main.handleExitCoder() traps it, so wrap the error\n\t\treturn fmt.Errorf(\"failed to run wslpath command: %w\", err)\n\t}\n\tlimaBootFileLinuxPath := strings.TrimSpace(string(limaBootFilePathOnLinuxB))\n\tgo func() {\n\t\tcmd := exec.CommandContext(\n\t\t\tctx,\n\t\t\t\"wsl.exe\",\n\t\t\t\"-d\",\n\t\t\tdistroName,\n\t\t\t\"bash\",\n\t\t\t\"-c\",\n\t\t\tlimaBootFileLinuxPath,\n\t\t)\n\t\tout, err := cmd.CombinedOutput()\n\t\tos.RemoveAll(limaBootFileWinPath)\n\t\tlogrus.Debugf(\"%v: %q\", cmd.Args, string(out))\n\t\tif err != nil {\n\t\t\terrCh <- fmt.Errorf(\n\t\t\t\t\"error running wslCommand that executes boot.sh (%v): %w, \"+\n\t\t\t\t\t\"check /var/log/lima-init.log for more details (out=%q)\", cmd.Args, err, string(out))\n\t\t}\n\n\t\tfor {\n\t\t\t<-ctx.Done()\n\t\t\tlogrus.Info(\"Context closed, stopping vm\")\n\t\t\tif status, err := getWslStatus(ctx, instanceName); err == nil &&\n\t\t\t\tstatus == limatype.StatusRunning {\n\t\t\t\t_ = stopVM(ctx, distroName)\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn err\n}\n\n// keepAlive runs a background process which in order to keep the WSL2 VM running in the background after launch.\nfunc keepAlive(ctx context.Context, distroName string, errCh chan<- error) {\n\tkeepAliveCmd := exec.CommandContext(\n\t\tctx,\n\t\t\"wsl.exe\",\n\t\t\"-d\",\n\t\tdistroName,\n\t\t\"bash\",\n\t\t\"-c\",\n\t\t\"nohup sleep 2147483647d >/dev/null 2>&1\",\n\t)\n\n\tgo func() {\n\t\tif err := keepAliveCmd.Run(); err != nil {\n\t\t\terrCh <- fmt.Errorf(\n\t\t\t\t\"error running wsl keepAlive command: %w\", err)\n\t\t}\n\t}()\n}\n\n// unregisterVM calls WSL to unregister a VM.\nfunc unregisterVM(ctx context.Context, distroName string) error {\n\tlogrus.Info(\"Unregistering WSL2 VM\")\n\tout, err := executil.RunUTF16leCommand([]string{\n\t\t\"wsl.exe\",\n\t\t\"--unregister\",\n\t\tdistroName,\n\t}, executil.WithContext(ctx))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run `wsl.exe --unregister %s`: %w (out=%q)\",\n\t\t\tdistroName, err, out)\n\t}\n\treturn nil\n}\n\n// GetWslStatus runs `wsl --list --verbose` and parses its output.\n// There are several possible outputs, all listed with their whitespace preserved output below.\n//\n// (1) Expected output if at least one distro is installed:\n// PS > wsl --list --verbose\n//\n//\tNAME      STATE           VERSION\n//\n// * Ubuntu    Stopped         2\n//\n// (2) Expected output when no distros are installed, but WSL is configured properly:\n// PS > wsl --list --verbose\n// Windows Subsystem for Linux has no installed distributions.\n//\n// Use 'wsl.exe --list --online' to list available distributions\n// and 'wsl.exe --install <Distro>' to install.\n//\n// Distributions can also be installed by visiting the Microsoft Store:\n// https://aka.ms/wslstore\n// Error code: Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND\n//\n// (3) Expected output when no distros are installed, and WSL2 has no kernel installed:\n//\n// PS > wsl --list --verbose\n// Windows Subsystem for Linux has no installed distributions.\n// Distributions can be installed by visiting the Microsoft Store:\n// https://aka.ms/wslstore\nfunc getWslStatus(ctx context.Context, instName string) (string, error) {\n\tdistroName := \"lima-\" + instName\n\tout, err := executil.RunUTF16leCommand([]string{\n\t\t\"wsl.exe\",\n\t\t\"--list\",\n\t\t\"--verbose\",\n\t}, executil.WithContext(ctx))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to run `wsl --list --verbose`, err: %w (out=%q)\", err, out)\n\t}\n\n\tif out == \"\" {\n\t\treturn limatype.StatusBroken, fmt.Errorf(\"failed to read instance state for instance %q, try running `wsl --list --verbose` to debug, err: %w\", instName, err)\n\t}\n\n\t// Check for edge cases first\n\tif strings.Contains(out, \"Windows Subsystem for Linux has no installed distributions.\") {\n\t\tif strings.Contains(out, \"Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND\") {\n\t\t\treturn limatype.StatusBroken, fmt.Errorf(\n\t\t\t\t\"failed to read instance state for instance %q because no distro is installed,\"+\n\t\t\t\t\t\"try running `wsl --install -d Ubuntu` and then re-running Lima\", instName)\n\t\t}\n\t\treturn limatype.StatusBroken, fmt.Errorf(\n\t\t\t\"failed to read instance state for instance %q because there is no WSL kernel installed,\"+\n\t\t\t\t\"this usually happens when WSL was installed for another user, but never for your user.\"+\n\t\t\t\t\"Try running `wsl --install -d Ubuntu` and `wsl --update`, and then re-running Lima\", instName)\n\t}\n\n\tvar instState string\n\twslListColsRegex := regexp.MustCompile(`\\s+`)\n\t// wsl --list --verbose may have different headers depending on localization, just split by line\n\tfor rows := range strings.SplitSeq(strings.ReplaceAll(out, \"\\r\\n\", \"\\n\"), \"\\n\") {\n\t\tcols := wslListColsRegex.Split(strings.TrimSpace(rows), -1)\n\t\tnameIdx := 0\n\t\t// '*' indicates default instance\n\t\tif cols[0] == \"*\" {\n\t\t\tnameIdx = 1\n\t\t}\n\t\tif cols[nameIdx] == distroName {\n\t\t\tinstState = cols[nameIdx+1]\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif instState == \"\" {\n\t\treturn limatype.StatusUninitialized, nil\n\t}\n\n\treturn instState, nil\n}\n\n// GetSSHAddress runs a hostname command to get the IP from inside of a wsl2 VM.\n//\n// Expected output (whitespace preserved, [] for optional):\n// PS > wsl -d <distroName> bash -c hostname -I | cut -d' ' -f1\n// 168.1.1.1 [10.0.0.1]\n// But busybox hostname does not implement --all-ip-addresses:\n// hostname: unrecognized option: I\nfunc getSSHAddress(ctx context.Context, instName string) (string, error) {\n\tdistroName := \"lima-\" + instName\n\t// Ubuntu\n\tcmd := exec.CommandContext(ctx, \"wsl.exe\", \"-d\", distroName, \"bash\", \"-c\", `hostname -I | cut -d ' ' -f1`)\n\tout, err := cmd.CombinedOutput()\n\tif err == nil {\n\t\treturn strings.TrimSpace(string(out)), nil\n\t}\n\t// Alpine\n\tcmd = exec.CommandContext(ctx, \"wsl.exe\", \"-d\", distroName, \"sh\", \"-c\", `ip route get 1 | awk '{gsub(\"^.*src \",\"\"); print $1; exit}'`)\n\tout, err = cmd.CombinedOutput()\n\tif err == nil {\n\t\treturn strings.TrimSpace(string(out)), nil\n\t}\n\t// fallback\n\tcmd = exec.CommandContext(ctx, \"wsl.exe\", \"-d\", distroName, \"hostname\", \"-i\")\n\tout, err = cmd.CombinedOutput()\n\tif err == nil {\n\t\tip := net.ParseIP(strings.TrimSpace(string(out)))\n\t\t// some distributions use \"127.0.1.1\" as the host IP, but we want something that we can route to here\n\t\tif ip != nil && !ip.IsLoopback() {\n\t\t\treturn strings.TrimSpace(string(out)), nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"failed to get hostname for instance %q, err: %w (out=%q)\", instName, err, string(out))\n}\n"
  },
  {
    "path": "pkg/driver/wsl2/wsl_driver_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage wsl2\n\nimport (\n\t\"context\"\n\t\"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"regexp\"\n\n\t\"github.com/Microsoft/go-winio\"\n\t\"github.com/Microsoft/go-winio/pkg/guid\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/freeport\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n\t\"github.com/lima-vm/lima/v2/pkg/reflectutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/windows\"\n)\n\nvar knownYamlProperties = []string{\n\t\"Arch\",\n\t\"Containerd\",\n\t\"CopyToHost\",\n\t\"CPUType\",\n\t\"Disk\",\n\t\"DNS\",\n\t\"Env\",\n\t\"HostResolver\",\n\t\"Images\",\n\t\"Message\",\n\t\"Mounts\",\n\t\"MountType\",\n\t\"Param\",\n\t\"Plain\",\n\t\"PortForwards\",\n\t\"Probes\",\n\t\"PropagateProxyEnv\",\n\t\"Provision\",\n\t\"SSH\",\n\t\"VMType\",\n}\n\nconst Enabled = true\n\ntype LimaWslDriver struct {\n\tInstance *limatype.Instance\n\n\tSSHLocalPort int\n\tvSockPort    int\n\tvirtioPort   string\n}\n\nvar _ driver.Driver = (*LimaWslDriver)(nil)\n\nfunc New() *LimaWslDriver {\n\tport, err := freeport.VSock()\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"failed to get free VSock port\")\n\t}\n\n\treturn &LimaWslDriver{\n\t\tvSockPort:  port,\n\t\tvirtioPort: \"\",\n\t}\n}\n\nfunc (l *LimaWslDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriver {\n\tl.Instance = inst\n\tl.SSHLocalPort = inst.SSHLocalPort\n\n\treturn &driver.ConfiguredDriver{\n\t\tDriver: l,\n\t}\n}\n\nfunc (l *LimaWslDriver) FillConfig(ctx context.Context, cfg *limatype.LimaYAML, _ string) error {\n\tif cfg.VMType == nil {\n\t\tcfg.VMType = ptr.Of(limatype.WSL2)\n\t}\n\tif cfg.MountType == nil {\n\t\tcfg.MountType = ptr.Of(limatype.WSLMount)\n\t}\n\treturn validateConfig(ctx, cfg)\n}\n\nfunc (l *LimaWslDriver) Validate(ctx context.Context) error {\n\treturn validateConfig(ctx, l.Instance.Config)\n}\n\nfunc validateConfig(_ context.Context, cfg *limatype.LimaYAML) error {\n\tif cfg == nil {\n\t\treturn errors.New(\"configuration is nil\")\n\t}\n\tif cfg.MountType != nil && *cfg.MountType != limatype.WSLMount {\n\t\treturn fmt.Errorf(\"field `mountType` must be %q for WSL2 driver, got %q\", limatype.WSLMount, *cfg.MountType)\n\t}\n\t// TODO: revise this list for WSL2\n\tif cfg.VMType != nil {\n\t\tif unknown := reflectutil.UnknownNonEmptyFields(cfg, knownYamlProperties...); len(unknown) > 0 {\n\t\t\tlogrus.Warnf(\"Ignoring: vmType %s: %+v\", *cfg.VMType, unknown)\n\t\t}\n\t}\n\n\tif !limatype.IsNativeArch(*cfg.Arch) {\n\t\treturn fmt.Errorf(\"unsupported arch: %q\", *cfg.Arch)\n\t}\n\n\tif cfg.VMType != nil {\n\t\tif cfg.Images != nil && cfg.Arch != nil {\n\t\t\t// TODO: real filetype checks\n\t\t\ttarFileRegex := regexp.MustCompile(`.*tar\\.*`)\n\t\t\tfor i, image := range cfg.Images {\n\t\t\t\tif unknown := reflectutil.UnknownNonEmptyFields(image, \"File\"); len(unknown) > 0 {\n\t\t\t\t\tlogrus.Warnf(\"Ignoring: vmType %s: images[%d]: %+v\", *cfg.VMType, i, unknown)\n\t\t\t\t}\n\t\t\t\tmatch := tarFileRegex.MatchString(image.Location)\n\t\t\t\tif image.Arch == *cfg.Arch && !match {\n\t\t\t\t\treturn fmt.Errorf(\"unsupported image type for vmType: %s, tarball root file system required: %q\", *cfg.VMType, image.Location)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif cfg.Mounts != nil {\n\t\t\tfor i, mount := range cfg.Mounts {\n\t\t\t\tif unknown := reflectutil.UnknownNonEmptyFields(mount); len(unknown) > 0 {\n\t\t\t\t\tlogrus.Warnf(\"Ignoring: vmType %s: mounts[%d]: %+v\", *cfg.VMType, i, unknown)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif cfg.Networks != nil {\n\t\t\tfor i, network := range cfg.Networks {\n\t\t\t\tif unknown := reflectutil.UnknownNonEmptyFields(network); len(unknown) > 0 {\n\t\t\t\t\tlogrus.Warnf(\"Ignoring: vmType %s: networks[%d]: %+v\", *cfg.VMType, i, unknown)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif cfg.Audio.Device != nil {\n\t\t\taudioDevice := *cfg.Audio.Device\n\t\t\tif audioDevice != \"\" {\n\t\t\t\tlogrus.Warnf(\"Ignoring: vmType %s: `audio.device`: %+v\", *cfg.VMType, audioDevice)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n//go:embed boot.Linux/*.sh\nvar bootLinuxFS embed.FS\n\nfunc (l *LimaWslDriver) BootScripts() (map[string][]byte, error) {\n\tscripts := make(map[string][]byte)\n\n\tentries, err := bootLinuxFS.ReadDir(\"boot.Linux\")\n\tif err != nil {\n\t\treturn scripts, err\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tentryPath := \"boot.Linux/\" + entry.Name()\n\n\t\tcontent, err := bootLinuxFS.ReadFile(entryPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tscripts[entryPath] = content\n\t}\n\n\treturn scripts, nil\n}\n\nfunc (l *LimaWslDriver) InspectStatus(ctx context.Context, inst *limatype.Instance) string {\n\tstatus, err := getWslStatus(ctx, inst.Name)\n\tif err != nil {\n\t\tinst.Status = limatype.StatusBroken\n\t\tinst.Errors = append(inst.Errors, err)\n\t} else {\n\t\tinst.Status = status\n\t}\n\n\tinst.SSHLocalPort = 22\n\n\tif inst.Status == limatype.StatusRunning {\n\t\tsshAddr, err := getSSHAddress(ctx, inst.Name)\n\t\tif err == nil {\n\t\t\tinst.SSHAddress = sshAddr\n\t\t} else {\n\t\t\tinst.Errors = append(inst.Errors, err)\n\t\t}\n\t}\n\n\treturn inst.Status\n}\n\nfunc (l *LimaWslDriver) Delete(ctx context.Context) error {\n\tdistroName := \"lima-\" + l.Instance.Name\n\tstatus, err := getWslStatus(ctx, l.Instance.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitch status {\n\tcase limatype.StatusRunning, limatype.StatusStopped, limatype.StatusBroken, limatype.StatusInstalling:\n\t\treturn unregisterVM(ctx, distroName)\n\t}\n\n\tlogrus.Info(\"WSL VM is not running or does not exist, skipping deletion\")\n\treturn nil\n}\n\nfunc (l *LimaWslDriver) Start(ctx context.Context) (chan error, error) {\n\tif l.Instance.Config.SSH.OverVsock != nil && *l.Instance.Config.SSH.OverVsock {\n\t\t// Probably never supportable for WSL2\n\t\tlogrus.Warn(\".ssh.overVsock is not supported for WSL2 driver\")\n\t}\n\n\tlogrus.Infof(\"Starting WSL VM\")\n\tstatus, err := getWslStatus(ctx, l.Instance.Name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdistroName := \"lima-\" + l.Instance.Name\n\n\tif status == limatype.StatusUninitialized {\n\t\tif err := EnsureFs(ctx, l.Instance); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := initVM(ctx, l.Instance.Dir, distroName); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\terrCh := make(chan error)\n\n\tif err := startVM(ctx, distroName); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := provisionVM(\n\t\tctx,\n\t\tl.Instance.Dir,\n\t\tl.Instance.Name,\n\t\tdistroName,\n\t\terrCh,\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\tkeepAlive(ctx, distroName, errCh)\n\n\treturn errCh, err\n}\n\n// CanRunGUI requires WSLg, which requires specific version of WSL2 to be installed.\n// TODO: Add check and add support for WSLg (instead of VNC) to hostagent.\nfunc (l *LimaWslDriver) canRunGUI() bool {\n\treturn false\n}\n\nfunc (l *LimaWslDriver) RunGUI() error {\n\treturn fmt.Errorf(\"RunGUI is not supported for the given driver '%s' and display '%s'\", \"wsl\", *l.Instance.Config.Video.Display)\n}\n\nfunc (l *LimaWslDriver) Stop(ctx context.Context) error {\n\tlogrus.Info(\"Shutting down WSL2 VM\")\n\tdistroName := \"lima-\" + l.Instance.Name\n\treturn stopVM(ctx, distroName)\n}\n\n// GuestAgentConn returns the guest agent connection, or nil (if forwarded by ssh).\n// As of 08-01-2024, github.com/mdlayher/vsock does not natively support vsock on\n// Windows, so use the winio library to create the connection.\nfunc (l *LimaWslDriver) GuestAgentConn(ctx context.Context) (net.Conn, string, error) {\n\tVMIDStr, err := windows.GetInstanceVMID(ctx, fmt.Sprintf(\"lima-%s\", l.Instance.Name))\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tVMIDGUID, err := guid.FromString(VMIDStr)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\tsockAddr := &winio.HvsockAddr{\n\t\tVMID:      VMIDGUID,\n\t\tServiceID: winio.VsockServiceID(uint32(l.vSockPort)),\n\t}\n\tconn, err := winio.Dial(ctx, sockAddr)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\treturn conn, \"vsock\", nil\n}\n\nfunc (l *LimaWslDriver) Info() driver.Info {\n\tvar info driver.Info\n\tinfo.Name = \"wsl2\"\n\tif l.Instance != nil {\n\t\tinfo.InstanceDir = l.Instance.Dir\n\t}\n\tinfo.VirtioPort = l.virtioPort\n\tinfo.VsockPort = l.vSockPort\n\n\tinfo.Features = driver.DriverFeatures{\n\t\tDynamicSSHAddress:    true,\n\t\tStaticSSHPort:        true,\n\t\tSkipSocketForwarding: true,\n\t\tNoCloudInit:          true,\n\t\tCanRunGUI:            l.canRunGUI(),\n\t}\n\treturn info\n}\n\nfunc (l *LimaWslDriver) SSHAddress(_ context.Context) (string, error) {\n\treturn \"127.0.0.1\", nil\n}\n\nfunc (l *LimaWslDriver) Create(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaWslDriver) CreateDisk(_ context.Context) error {\n\treturn nil\n}\n\nfunc (l *LimaWslDriver) ChangeDisplayPassword(_ context.Context, _ string) error {\n\treturn nil\n}\n\nfunc (l *LimaWslDriver) DisplayConnection(_ context.Context) (string, error) {\n\treturn \"\", nil\n}\n\nfunc (l *LimaWslDriver) CreateSnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaWslDriver) ApplySnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaWslDriver) DeleteSnapshot(_ context.Context, _ string) error {\n\treturn errUnimplemented\n}\n\nfunc (l *LimaWslDriver) ListSnapshots(_ context.Context) (string, error) {\n\treturn \"\", errUnimplemented\n}\n\nfunc (l *LimaWslDriver) ForwardGuestAgent() bool {\n\t// If driver is not providing, use host agent\n\treturn l.vSockPort == 0 && l.virtioPort == \"\"\n}\n\nfunc (l *LimaWslDriver) AdditionalSetupForSSH(_ context.Context) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driverutil/disk.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage driverutil\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/iso9660util\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\n// MigrateDiskLayout creates symlinks from the current filenames (disk, iso) to\n// the legacy filenames (diffdisk, basedisk) used by older Lima versions.\n// The original files are left in place so older Lima versions can still use them.\nfunc MigrateDiskLayout(instDir string) error {\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\tif osutil.FileExists(diskPath) {\n\t\treturn nil // already migrated or new instance\n\t}\n\n\tdiffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy)\n\tif osutil.FileExists(diffDiskPath) {\n\t\tlogrus.Infof(\"Creating symlink %q -> %q\", filenames.Disk, filenames.DiffDiskLegacy)\n\t\tif err := os.Symlink(filenames.DiffDiskLegacy, diskPath); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to symlink %q to %q: %w\", filenames.Disk, filenames.DiffDiskLegacy, err)\n\t\t}\n\t}\n\n\tbaseDiskPath := filepath.Join(instDir, filenames.BaseDiskLegacy)\n\tisoPath := filepath.Join(instDir, filenames.ISO)\n\tif osutil.FileExists(baseDiskPath) && !osutil.FileExists(isoPath) {\n\t\tisISO, err := iso9660util.IsISO9660(baseDiskPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif isISO {\n\t\t\tlogrus.Infof(\"Creating symlink %q -> %q\", filenames.ISO, filenames.BaseDiskLegacy)\n\t\t\tif err := os.Symlink(filenames.BaseDiskLegacy, isoPath); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to symlink %q to %q: %w\", filenames.ISO, filenames.BaseDiskLegacy, err)\n\t\t\t}\n\t\t}\n\t\t// Non-ISO basedisk is a legacy qcow2 backing file; leave it for QEMU to resolve.\n\t}\n\n\treturn nil\n}\n\n// EnsureDisk creates the VM disk from the downloaded image.\n// For ISO images, it renames the image to \"iso\" and creates an empty disk.\n// For non-ISO images, it converts the image to \"disk\" and removes the original.\nfunc EnsureDisk(ctx context.Context, instDir, diskSize string, diskImageFormat image.Type) error {\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\tif _, err := os.Stat(diskPath); err == nil || !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\n\timagePath := filepath.Join(instDir, filenames.Image)\n\tisISO, err := iso9660util.IsISO9660(imagePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdiskSizeInBytes, _ := units.RAMInBytes(diskSize)\n\tdiskUtil := proxyimgutil.NewDiskUtil(ctx)\n\n\tif isISO {\n\t\tisoPath := filepath.Join(instDir, filenames.ISO)\n\t\tif err := os.Rename(imagePath, isoPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf, err := os.Create(diskPath)\n\t\tif err != nil {\n\t\t\t_ = os.Rename(isoPath, imagePath)\n\t\t\treturn err\n\t\t}\n\t\tif err := f.Close(); err != nil {\n\t\t\tos.Remove(diskPath)\n\t\t\t_ = os.Rename(isoPath, imagePath)\n\t\t\treturn err\n\t\t}\n\t\tif err := diskUtil.Convert(ctx, diskImageFormat, diskPath, diskPath, &diskSizeInBytes, false); err != nil {\n\t\t\tos.Remove(diskPath)\n\t\t\t_ = os.Rename(isoPath, imagePath)\n\t\t\treturn fmt.Errorf(\"failed to create disk %q: %w\", diskPath, err)\n\t\t}\n\t} else {\n\t\tif err := diskUtil.Convert(ctx, diskImageFormat, imagePath, diskPath, &diskSizeInBytes, false); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to convert %q to %q: %w\", imagePath, diskPath, err)\n\t\t}\n\t\tos.Remove(imagePath)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/driverutil/disk_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage driverutil\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/lima-vm/go-qcow2reader\"\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/iso9660util\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\nconst (\n\ttypeRAW  = image.Type(\"raw\")\n\ttypeASIF = image.Type(\"asif\")\n)\n\nfunc writeMinimalISO(t *testing.T, path string) {\n\tt.Helper()\n\tentries := []iso9660util.Entry{\n\t\t{Path: \"/hello.txt\", Reader: strings.NewReader(\"hello world\")},\n\t}\n\tassert.NilError(t, iso9660util.Write(path, \"TESTISO\", entries))\n}\n\nfunc writeNonISO(t *testing.T, path string) {\n\tt.Helper()\n\tsize := 64 * 1024\n\tbuf := make([]byte, size)\n\tcopy(buf[0x8001:], \"XXXXX\")\n\tassert.NilError(t, os.WriteFile(path, buf, 0o644))\n}\n\nfunc sha256File(t *testing.T, path string) string {\n\tt.Helper()\n\tb, err := os.ReadFile(path)\n\tassert.NilError(t, err)\n\tsum := sha256.Sum256(b)\n\treturn hex.EncodeToString(sum[:])\n}\n\nfunc detectImageType(t *testing.T, path string) image.Type {\n\tt.Helper()\n\tf, err := os.Open(path)\n\tassert.NilError(t, err)\n\tdefer f.Close()\n\timg, err := qcow2reader.Open(f)\n\tassert.NilError(t, err)\n\treturn img.Type()\n}\n\nfunc checkDisk(t *testing.T, diskPath string, expectedType image.Type) {\n\tt.Helper()\n\tfi, err := os.Stat(diskPath)\n\tassert.NilError(t, err)\n\tassert.Assert(t, fi.Size() > 0)\n\tassert.Equal(t, detectImageType(t, diskPath), expectedType)\n}\n\nfunc assertSymlink(t *testing.T, path, expectedTarget string) {\n\tt.Helper()\n\ttarget, err := os.Readlink(path)\n\tassert.NilError(t, err)\n\tassert.Equal(t, target, expectedTarget)\n}\n\nfunc isMacOS26OrHigher() bool {\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn false\n\t}\n\tversion, err := osutil.ProductVersion()\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn version.Major >= 26\n}\n\nfunc TestEnsureDisk_WithISOImage(t *testing.T) {\n\tinstDir := t.TempDir()\n\timagePath := filepath.Join(instDir, filenames.Image)\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\tisoPath := filepath.Join(instDir, filenames.ISO)\n\n\tformats := []image.Type{typeRAW}\n\tif isMacOS26OrHigher() {\n\t\tformats = append(formats, typeASIF)\n\t}\n\n\tfor _, format := range formats {\n\t\twriteMinimalISO(t, imagePath)\n\t\timageHashBefore := sha256File(t, imagePath)\n\n\t\tassert.NilError(t, EnsureDisk(t.Context(), instDir, \"2MiB\", format))\n\n\t\t// image should have been renamed to iso\n\t\tassert.Assert(t, !osutil.FileExists(imagePath))\n\t\tassert.Equal(t, imageHashBefore, sha256File(t, isoPath))\n\t\tisISO, err := iso9660util.IsISO9660(isoPath)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, isISO)\n\n\t\t// disk should be a real file (empty data disk)\n\t\tcheckDisk(t, diskPath, format)\n\n\t\tassert.NilError(t, os.Remove(diskPath))\n\t\tassert.NilError(t, os.Remove(isoPath))\n\t}\n}\n\nfunc TestEnsureDisk_WithNonISOImage(t *testing.T) {\n\tinstDir := t.TempDir()\n\timagePath := filepath.Join(instDir, filenames.Image)\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\n\tformats := []image.Type{typeRAW}\n\tif isMacOS26OrHigher() {\n\t\tformats = append(formats, typeASIF)\n\t}\n\n\tfor _, format := range formats {\n\t\twriteNonISO(t, imagePath)\n\n\t\tassert.NilError(t, EnsureDisk(t.Context(), instDir, \"2MiB\", format))\n\n\t\t// image should have been consumed\n\t\tassert.Assert(t, !osutil.FileExists(imagePath))\n\n\t\t// disk should be the converted image\n\t\tcheckDisk(t, diskPath, format)\n\n\t\tassert.NilError(t, os.Remove(diskPath))\n\t}\n}\n\nfunc TestEnsureDisk_ExistingDisk(t *testing.T) {\n\tinstDir := t.TempDir()\n\timagePath := filepath.Join(instDir, filenames.Image)\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\n\twriteNonISO(t, imagePath)\n\n\tformats := []image.Type{typeRAW}\n\tif isMacOS26OrHigher() {\n\t\tformats = append(formats, typeASIF)\n\t}\n\n\tfor _, format := range formats {\n\t\tassert.NilError(t, os.WriteFile(diskPath, []byte(\"preexisting\"), 0o644))\n\t\torigHash := sha256File(t, diskPath)\n\t\tassert.NilError(t, EnsureDisk(t.Context(), instDir, \"2MiB\", format))\n\t\tassert.Equal(t, sha256File(t, diskPath), origHash)\n\t\tassert.NilError(t, os.Remove(diskPath))\n\t}\n}\n\nfunc TestMigrateDiskLayout_LegacyDiffDisk(t *testing.T) {\n\tinstDir := t.TempDir()\n\tdiffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy)\n\n\tassert.NilError(t, os.WriteFile(diffDiskPath, []byte(\"legacy-disk\"), 0o644))\n\torigHash := sha256File(t, diffDiskPath)\n\n\tassert.NilError(t, MigrateDiskLayout(instDir))\n\n\t// disk should be a symlink to diffdisk\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\tassertSymlink(t, diskPath, filenames.DiffDiskLegacy)\n\tassert.Equal(t, sha256File(t, diskPath), origHash)\n\n\t// diffdisk should still exist (untouched)\n\tassert.Assert(t, osutil.FileExists(diffDiskPath))\n}\n\nfunc TestMigrateDiskLayout_LegacyISOBaseDisk(t *testing.T) {\n\tinstDir := t.TempDir()\n\tbaseDiskPath := filepath.Join(instDir, filenames.BaseDiskLegacy)\n\tdiffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy)\n\n\twriteMinimalISO(t, baseDiskPath)\n\tbaseHash := sha256File(t, baseDiskPath)\n\tassert.NilError(t, os.WriteFile(diffDiskPath, []byte(\"legacy-disk\"), 0o644))\n\tdiffHash := sha256File(t, diffDiskPath)\n\n\tassert.NilError(t, MigrateDiskLayout(instDir))\n\n\t// disk should be a symlink to diffdisk\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\tassertSymlink(t, diskPath, filenames.DiffDiskLegacy)\n\tassert.Equal(t, sha256File(t, diskPath), diffHash)\n\n\t// iso should be a symlink to basedisk\n\tisoPath := filepath.Join(instDir, filenames.ISO)\n\tassertSymlink(t, isoPath, filenames.BaseDiskLegacy)\n\tassert.Equal(t, sha256File(t, isoPath), baseHash)\n\n\t// original files should still exist\n\tassert.Assert(t, osutil.FileExists(diffDiskPath))\n\tassert.Assert(t, osutil.FileExists(baseDiskPath))\n}\n\nfunc TestMigrateDiskLayout_LegacyNonISOBaseDisk(t *testing.T) {\n\tinstDir := t.TempDir()\n\tbaseDiskPath := filepath.Join(instDir, filenames.BaseDiskLegacy)\n\tdiffDiskPath := filepath.Join(instDir, filenames.DiffDiskLegacy)\n\n\twriteNonISO(t, baseDiskPath)\n\tbaseHash := sha256File(t, baseDiskPath)\n\tassert.NilError(t, os.WriteFile(diffDiskPath, []byte(\"legacy-disk\"), 0o644))\n\n\tassert.NilError(t, MigrateDiskLayout(instDir))\n\n\t// disk should be a symlink to diffdisk\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\tassertSymlink(t, diskPath, filenames.DiffDiskLegacy)\n\n\t// non-ISO basedisk should remain unchanged (qcow2 backing file)\n\tassert.Equal(t, sha256File(t, baseDiskPath), baseHash)\n\n\t// no iso symlink should be created\n\tisoPath := filepath.Join(instDir, filenames.ISO)\n\t_, err := os.Lstat(isoPath)\n\tassert.Assert(t, os.IsNotExist(err))\n}\n\nfunc TestMigrateDiskLayout_AlreadyMigrated(t *testing.T) {\n\tinstDir := t.TempDir()\n\tdiskPath := filepath.Join(instDir, filenames.Disk)\n\n\tassert.NilError(t, os.WriteFile(diskPath, []byte(\"current-disk\"), 0o644))\n\torigHash := sha256File(t, diskPath)\n\n\tassert.NilError(t, MigrateDiskLayout(instDir))\n\n\t// disk should be unchanged\n\tassert.Equal(t, sha256File(t, diskPath), origHash)\n}\n"
  },
  {
    "path": "pkg/driverutil/instance.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage driverutil\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/server\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/registry\"\n)\n\n// CreateConfiguredDriver creates a driver.ConfiguredDriver for the given instance.\nfunc CreateConfiguredDriver(inst *limatype.Instance, sshLocalPort int) (*driver.ConfiguredDriver, error) {\n\tlimaDriver := inst.Config.VMType\n\textDriver, intDriver, exists := registry.Get(*limaDriver)\n\tif !exists {\n\t\treturn nil, fmt.Errorf(\"unknown or unsupported VM type: %s\", *limaDriver)\n\t}\n\n\tif extDriver != nil {\n\t\textDriver.Logger.Debugf(\"Using external driver %q\", extDriver.Name)\n\t\tif extDriver.Client == nil || extDriver.Command == nil {\n\t\t\tlogrus.Debugf(\"Starting new instance of external driver %q\", extDriver.Name)\n\t\t\tif err := server.Start(extDriver, inst.Name); err != nil {\n\t\t\t\textDriver.Logger.Errorf(\"Failed to start external driver %q: %v\", extDriver.Name, err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t} else {\n\t\t\tlogrus.Debugf(\"Reusing existing external driver %q instance\", extDriver.Name)\n\t\t\textDriver.InstanceName = inst.Name\n\t\t}\n\n\t\tif !extDriver.Client.Info().Features.StaticSSHPort {\n\t\t\tinst.SSHLocalPort = sshLocalPort\n\t\t}\n\t\treturn extDriver.Client.Configure(inst), nil\n\t}\n\n\tlogrus.Debugf(\"Using internal driver %q\", intDriver.Info().Name)\n\tif !intDriver.Info().Features.StaticSSHPort {\n\t\tinst.SSHLocalPort = sshLocalPort\n\t}\n\treturn intDriver.Configure(inst), nil\n}\n"
  },
  {
    "path": "pkg/driverutil/vm.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage driverutil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os/exec\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/registry\"\n)\n\n// ResolveVMType sets the VMType field in the given LimaYAML if not already set.\n// It validates the configuration against the specified or default VMType.\nfunc ResolveVMType(ctx context.Context, y *limatype.LimaYAML, filePath string) error {\n\tif y.VMType != nil && *y.VMType != \"\" {\n\t\tif err := validateConfigAgainstDriver(ctx, y, filePath, *y.VMType); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlogrus.Debugf(\"Using specified vmType %q for %q\", *y.VMType, filePath)\n\t\treturn nil\n\t}\n\n\t// If VMType is not specified, we go with the default platform driver.\n\tvmType := limatype.DefaultDriver()\n\tif y.Arch != nil && !limatype.IsNativeArch(*y.Arch) {\n\t\tvmType = limatype.DefaultNonNativeArchDriver()\n\t}\n\treturn validateConfigAgainstDriver(ctx, y, filePath, vmType)\n}\n\nfunc validateConfigAgainstDriver(ctx context.Context, y *limatype.LimaYAML, filePath, vmType string) error {\n\textDriver, intDriver, exists := registry.Get(vmType)\n\tif !exists {\n\t\treturn fmt.Errorf(\"vmType %q is not a registered driver\", vmType)\n\t}\n\n\tif extDriver != nil {\n\t\treturn handlePreConfiguredDriverAction(ctx, y, extDriver.Path, filePath)\n\t}\n\n\tif err := intDriver.FillConfig(ctx, y, filePath); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc handlePreConfiguredDriverAction(ctx context.Context, y *limatype.LimaYAML, extDriverPath, filePath string) error {\n\tcmd := exec.CommandContext(ctx, extDriverPath, \"--pre-driver-action\")\n\n\tvar stderrBuf bytes.Buffer\n\tcmd.Stderr = &stderrBuf\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get stdout pipe: %w\", err)\n\t}\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get stdin pipe: %w\", err)\n\t}\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to start external driver: %w\", err)\n\t}\n\n\tencoder := json.NewEncoder(stdin)\n\tif err := encoder.Encode(limatype.PreConfiguredDriverPayload{\n\t\tConfig:   *y,\n\t\tFilePath: filePath,\n\t}); err != nil {\n\t\tstdin.Close()\n\t\treturn fmt.Errorf(\"failed to encode pre-configured driver payload: %w\", err)\n\t}\n\tstdin.Close()\n\n\tdecoder := json.NewDecoder(stdout)\n\tvar res limatype.LimaYAML\n\tif err := decoder.Decode(&res); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode pre-configured driver response: %w\", err)\n\t}\n\n\tif err := cmd.Wait(); err != nil {\n\t\tif stderrBuf.Len() > 0 {\n\t\t\treturn fmt.Errorf(\"pre-configured driver command failed: %w; stderr: %s\", err, stderrBuf.String())\n\t\t}\n\t\treturn fmt.Errorf(\"pre-configured driver command failed: %w\", err)\n\t}\n\n\tif stderrBuf.Len() > 0 {\n\t\tlogrus.Debugf(\"external driver stderr: %s\", stderrBuf.String())\n\t}\n\n\t*y = res\n\tlogrus.Debugf(\"Pre-configured driver action completed successfully for %q\", extDriverPath)\n\treturn nil\n}\n\nfunc InspectStatus(ctx context.Context, inst *limatype.Instance) (string, error) {\n\tif inst == nil || inst.Config == nil || inst.Config.VMType == nil {\n\t\treturn \"\", errors.New(\"instance or its configuration is not properly initialized\")\n\t}\n\n\textDriver, intDriver, exists := registry.Get(*inst.Config.VMType)\n\tif !exists {\n\t\treturn \"\", fmt.Errorf(\"unknown or unsupported VM type: %s\", *inst.Config.VMType)\n\t}\n\n\tif extDriver != nil {\n\t\tstatus, err := handleInspectStatusAction(ctx, inst, extDriver.Path)\n\t\tif err != nil {\n\t\t\textDriver.Logger.Errorf(\"Failed to inspect status for instance %q: %v\", inst.Name, err)\n\t\t\treturn \"\", err\n\t\t}\n\t\textDriver.Logger.Debugf(\"Instance %q inspected successfully with status: %s\", inst.Name, inst.Status)\n\t\treturn status, nil\n\t}\n\n\treturn intDriver.InspectStatus(ctx, inst), nil\n}\n\nfunc handleInspectStatusAction(ctx context.Context, inst *limatype.Instance, extDriverPath string) (string, error) {\n\tcmd := exec.CommandContext(ctx, extDriverPath, \"--inspect-status\")\n\n\tvar stderrBuf bytes.Buffer\n\tcmd.Stderr = &stderrBuf\n\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif err := cmd.Start(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tencoder := json.NewEncoder(stdin)\n\tpayload, err := inst.MarshalJSON()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to marshal instance config: %w\", err)\n\t}\n\tif err := encoder.Encode(payload); err != nil {\n\t\treturn \"\", err\n\t}\n\tstdin.Close()\n\n\tdecoder := json.NewDecoder(stdout)\n\tvar response []byte\n\tif err := decoder.Decode(&response); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar respInst limatype.Instance\n\tif err := respInst.UnmarshalJSON(response); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to unmarshal instance response: %w\", err)\n\t}\n\n\tif err := cmd.Wait(); err != nil {\n\t\tif stderrBuf.Len() > 0 {\n\t\t\treturn \"\", fmt.Errorf(\"inspect status command failed: %w; stderr: %s\", err, stderrBuf.String())\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"inspect status command failed: %w\", err)\n\t}\n\n\tif stderrBuf.Len() > 0 {\n\t\tlogrus.Debugf(\"external driver stderr: %s\", stderrBuf.String())\n\t}\n\n\t*inst = respInst\n\tlogrus.Debugf(\"Inspecting instance status action completed successfully for %q\", extDriverPath)\n\treturn inst.Status, nil\n}\n"
  },
  {
    "path": "pkg/editutil/editorcmd/editorcmd.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://raw.githubusercontent.com/norouter/norouter/v0.6.5/cmd/norouter/editorcmd/editorcmd.go\n/*\n   Copyright (C) NoRouter authors.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n*/\n\npackage editorcmd\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n)\n\n// Detect detects a text editor command.\n// Returns an empty string when no editor is found.\nfunc Detect() string {\n\tcandidates := []string{\n\t\tos.Getenv(\"VISUAL\"),\n\t\tos.Getenv(\"EDITOR\"),\n\t\t\"editor\",\n\t\t\"vim\",\n\t\t\"vi\",\n\t\t\"emacs\",\n\t}\n\tfor _, f := range candidates {\n\t\tif f == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tx, err := exec.LookPath(f)\n\t\tif err == nil {\n\t\t\treturn x\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/editutil/editutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage editutil\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/editutil/editorcmd\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\nfunc fileWarning(filename string) string {\n\tb, err := os.ReadFile(filename)\n\tif err != nil || len(b) == 0 {\n\t\treturn \"\"\n\t}\n\tvar sb strings.Builder\n\tsb.WriteString(\"# WARNING: \" + filename + \" includes the following settings,\\n\")\n\tsb.WriteString(\"# which are applied before applying this YAML:\\n\")\n\tsb.WriteString(\"# -----------\\n\")\n\tfor line := range strings.SplitSeq(strings.TrimSuffix(string(b), \"\\n\"), \"\\n\") {\n\t\tsb.WriteByte('#')\n\t\tif line != \"\" {\n\t\t\tsb.WriteString(\" \" + line)\n\t\t}\n\t\tsb.WriteByte('\\n')\n\t}\n\tsb.WriteString(\"# -----------\\n\\n\")\n\treturn sb.String()\n}\n\n// GenerateEditorWarningHeader generates the editor warning header.\nfunc GenerateEditorWarningHeader() string {\n\tvar s string\n\tconfigDir, err := dirnames.LimaConfigDir()\n\tif err != nil {\n\t\ts += \"# WARNING: failed to load the config dir\\n\"\n\t\ts += \"\\n\"\n\t\treturn s\n\t}\n\n\ts += fileWarning(filepath.Join(configDir, filenames.Default))\n\ts += fileWarning(filepath.Join(configDir, filenames.Override))\n\treturn s\n}\n\n// OpenEditor opens an editor, and returns the content (not path) of the modified yaml.\n//\n// OpenEditor returns nil when the file was saved as an empty file, optionally with whitespaces.\nfunc OpenEditor(ctx context.Context, content []byte, hdr string) ([]byte, error) {\n\teditor := editorcmd.Detect()\n\tif editor == \"\" {\n\t\treturn nil, errors.New(\"could not detect a text editor binary, try setting $EDITOR\")\n\t}\n\ttmpYAMLFile, err := os.CreateTemp(\"\", \"lima-editor-\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttmpYAMLPath := tmpYAMLFile.Name()\n\tdefer os.RemoveAll(tmpYAMLPath)\n\tif _, err := tmpYAMLFile.Write(append([]byte(hdr), content...)); err != nil {\n\t\ttmpYAMLFile.Close()\n\t\treturn nil, err\n\t}\n\tif err := tmpYAMLFile.Close(); err != nil {\n\t\treturn nil, err\n\t}\n\n\teditorCmd := exec.CommandContext(ctx, editor, tmpYAMLPath)\n\teditorCmd.Env = os.Environ()\n\teditorCmd.Stdin = os.Stdin\n\teditorCmd.Stdout = os.Stdout\n\teditorCmd.Stderr = os.Stderr\n\tlogrus.Debugf(\"opening editor %q for a file %q\", editor, tmpYAMLPath)\n\tif err := editorCmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"could not execute editor %q for a file %q: %w\", editor, tmpYAMLPath, err)\n\t}\n\tb, err := os.ReadFile(tmpYAMLPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmodifiedInclHdr := string(b)\n\tmodifiedExclHdr := strings.TrimPrefix(modifiedInclHdr, hdr)\n\tif strings.TrimSpace(modifiedExclHdr) == \"\" {\n\t\treturn nil, nil\n\t}\n\treturn []byte(modifiedExclHdr), nil\n}\n"
  },
  {
    "path": "pkg/envutil/envutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage envutil\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// defaultBlockList contains environment variables that should not be propagated by default.\nvar defaultBlockList = []string{\n\t\"BASH*\",\n\t\"DISPLAY\",\n\t\"DYLD_*\",\n\t\"EUID\",\n\t\"FPATH\",\n\t\"GID\",\n\t\"GROUP\",\n\t\"HOME\",\n\t\"HOSTNAME\",\n\t\"LD_*\",\n\t\"LOGNAME\",\n\t\"OLDPWD\",\n\t\"PATH\",\n\t\"PWD\",\n\t\"SHELL\",\n\t\"SHLVL\",\n\t\"SSH_*\",\n\t\"TERM\",\n\t\"TERMINFO\",\n\t\"TMPDIR\",\n\t\"UID\",\n\t\"USER\",\n\t\"XAUTHORITY\",\n\t\"XDG_*\",\n\t\"ZDOTDIR\",\n\t\"ZSH*\",\n\t\"_*\", // Variables starting with underscore are typically internal\n}\n\nfunc validatePattern(pattern string) error {\n\tinvalidChar := regexp.MustCompile(`([^a-zA-Z0-9_*])`)\n\tif matches := invalidChar.FindStringSubmatch(pattern); matches != nil {\n\t\tinvalidCharacter := matches[1]\n\t\tpos := strings.Index(pattern, invalidCharacter)\n\t\treturn fmt.Errorf(\"pattern %q contains invalid character %q at position %d\",\n\t\t\tpattern, invalidCharacter, pos)\n\t}\n\treturn nil\n}\n\n// getBlockList returns the list of environment variable patterns to be blocked.\nfunc getBlockList() []string {\n\tblockEnv := os.Getenv(\"LIMA_SHELLENV_BLOCK\")\n\tif blockEnv == \"\" {\n\t\treturn defaultBlockList\n\t}\n\n\tshouldAppend := strings.HasPrefix(blockEnv, \"+\")\n\tpatterns := parseEnvList(strings.TrimPrefix(blockEnv, \"+\"))\n\n\tfor _, pattern := range patterns {\n\t\tif err := validatePattern(pattern); err != nil {\n\t\t\tlogrus.Fatalf(\"Invalid LIMA_SHELLENV_BLOCK pattern: %v\", err)\n\t\t}\n\t}\n\n\tif shouldAppend {\n\t\treturn slices.Concat(defaultBlockList, patterns)\n\t}\n\treturn patterns\n}\n\n// getAllowList returns the list of environment variable patterns to be allowed.\nfunc getAllowList() []string {\n\tallowEnv := os.Getenv(\"LIMA_SHELLENV_ALLOW\")\n\tif allowEnv == \"\" {\n\t\treturn nil\n\t}\n\n\tpatterns := parseEnvList(allowEnv)\n\n\tfor _, pattern := range patterns {\n\t\tif err := validatePattern(pattern); err != nil {\n\t\t\tlogrus.Fatalf(\"Invalid LIMA_SHELLENV_ALLOW pattern: %v\", err)\n\t\t}\n\t}\n\n\treturn patterns\n}\n\nfunc parseEnvList(envList string) []string {\n\tparts := strings.Split(envList, \",\")\n\tresult := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tif trimmed := strings.TrimSpace(part); trimmed != \"\" {\n\t\t\tresult = append(result, trimmed)\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc matchesPattern(name, pattern string) bool {\n\tif pattern == name {\n\t\treturn true\n\t}\n\n\tregexPattern := strings.ReplaceAll(pattern, \"*\", \".*\")\n\tregexPattern = \"^\" + regexPattern + \"$\"\n\n\tmatch, err := regexp.MatchString(regexPattern, name)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn match\n}\n\nfunc matchesAnyPattern(name string, patterns []string) bool {\n\treturn slices.ContainsFunc(patterns, func(pattern string) bool {\n\t\treturn matchesPattern(name, pattern)\n\t})\n}\n\n// FilterEnvironment filters environment variables based on configuration from environment variables.\n// It returns a slice of environment variables that are not blocked by the current configuration.\n// The filtering is controlled by LIMA_SHELLENV_BLOCK and LIMA_SHELLENV_ALLOW environment variables.\nfunc FilterEnvironment() []string {\n\treturn filterEnvironmentWithLists(\n\t\tos.Environ(),\n\t\tgetAllowList(),\n\t\tgetBlockList(),\n\t)\n}\n\nfunc filterEnvironmentWithLists(env, allowList, blockList []string) []string {\n\tvar filtered []string\n\n\tfor _, envVar := range env {\n\t\tparts := strings.SplitN(envVar, \"=\", 2)\n\t\tif len(parts) != 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := parts[0]\n\n\t\tif len(allowList) > 0 && matchesAnyPattern(name, allowList) {\n\t\t\tfiltered = append(filtered, envVar)\n\t\t\tcontinue\n\t\t}\n\n\t\tif matchesAnyPattern(name, blockList) {\n\t\t\tlogrus.Debugf(\"Blocked env variable %q\", name)\n\t\t\tcontinue\n\t\t}\n\n\t\tfiltered = append(filtered, envVar)\n\t}\n\n\treturn filtered\n}\n\n// GetDefaultBlockList returns a copy of the default block list.\nfunc GetDefaultBlockList() []string {\n\treturn slices.Clone(defaultBlockList)\n}\n"
  },
  {
    "path": "pkg/envutil/envutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage envutil\n\nimport (\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc isUsingDefaultBlockList() bool {\n\tblockEnv := os.Getenv(\"LIMA_SHELLENV_BLOCK\")\n\treturn blockEnv == \"\" || strings.HasPrefix(blockEnv, \"+\")\n}\n\nfunc TestMatchesPattern(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tpattern  string\n\t\texpected bool\n\t}{\n\t\t{\"PATH\", \"PATH\", true},\n\t\t{\"PATH\", \"HOME\", false},\n\t\t{\"SSH_AUTH_SOCK\", \"SSH_*\", true},\n\t\t{\"SSH_AGENT_PID\", \"SSH_*\", true},\n\t\t{\"HOME\", \"SSH_*\", false},\n\t\t{\"XDG_CONFIG_HOME\", \"XDG_*\", true},\n\t\t{\"_LIMA_TEST\", \"_*\", true},\n\t\t{\"LIMA_HOME\", \"_*\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name+\"_matches_\"+tt.pattern, func(t *testing.T) {\n\t\t\tresult := matchesPattern(tt.name, tt.pattern)\n\t\t\tassert.Equal(t, result, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestMatchesAnyPattern(t *testing.T) {\n\tpatterns := []string{\"PATH\", \"SSH_*\", \"XDG_*\"}\n\n\ttests := []struct {\n\t\tname     string\n\t\texpected bool\n\t}{\n\t\t{\"PATH\", true},\n\t\t{\"HOME\", false},\n\t\t{\"SSH_AUTH_SOCK\", true},\n\t\t{\"XDG_CONFIG_HOME\", true},\n\t\t{\"USER\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := matchesAnyPattern(tt.name, patterns)\n\t\t\tassert.Equal(t, result, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestParseEnvList(t *testing.T) {\n\ttests := []struct {\n\t\tinput    string\n\t\texpected []string\n\t}{\n\t\t{\"\", []string{}},\n\t\t{\"PATH\", []string{\"PATH\"}},\n\t\t{\"PATH,HOME\", []string{\"PATH\", \"HOME\"}},\n\t\t{\"PATH, HOME , USER\", []string{\"PATH\", \"HOME\", \"USER\"}},\n\t\t{\" , , \", []string{}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input, func(t *testing.T) {\n\t\t\tresult := parseEnvList(tt.input)\n\t\t\tassert.DeepEqual(t, result, tt.expected)\n\t\t})\n\t}\n}\n\nfunc TestGetBlockAndAllowLists(t *testing.T) {\n\tt.Run(\"default config\", func(t *testing.T) {\n\t\tt.Setenv(\"LIMA_SHELLENV_BLOCK\", \"\")\n\t\tt.Setenv(\"LIMA_SHELLENV_ALLOW\", \"\")\n\n\t\tblockList := getBlockList()\n\t\tallowList := getAllowList()\n\n\t\tassert.Assert(t, isUsingDefaultBlockList())\n\t\tassert.DeepEqual(t, blockList, defaultBlockList)\n\t\tassert.Equal(t, len(allowList), 0)\n\t})\n\n\tt.Run(\"custom blocklist\", func(t *testing.T) {\n\t\tt.Setenv(\"LIMA_SHELLENV_BLOCK\", \"PATH,HOME\")\n\n\t\tblockList := getBlockList()\n\t\tassert.Assert(t, !isUsingDefaultBlockList())\n\t\texpected := []string{\"PATH\", \"HOME\"}\n\t\tassert.DeepEqual(t, blockList, expected)\n\t})\n\n\tt.Run(\"additive blocklist\", func(t *testing.T) {\n\t\tt.Setenv(\"LIMA_SHELLENV_BLOCK\", \"+CUSTOM_VAR\")\n\n\t\tblockList := getBlockList()\n\t\tassert.Assert(t, isUsingDefaultBlockList())\n\t\texpected := slices.Concat(GetDefaultBlockList(), []string{\"CUSTOM_VAR\"})\n\t\tassert.DeepEqual(t, blockList, expected)\n\t})\n\n\tt.Run(\"allowlist\", func(t *testing.T) {\n\t\tt.Setenv(\"LIMA_SHELLENV_ALLOW\", \"FOO,BAR\")\n\n\t\tallowList := getAllowList()\n\t\texpected := []string{\"FOO\", \"BAR\"}\n\t\tassert.DeepEqual(t, allowList, expected)\n\t})\n}\n\nfunc TestFilterEnvironment(t *testing.T) {\n\ttestEnv := []string{\n\t\t\"PATH=/usr/bin\",\n\t\t\"HOME=/home/user\",\n\t\t\"USER=testuser\",\n\t\t\"FOO=bar\",\n\t\t\"SSH_AUTH_SOCK=/tmp/ssh\",\n\t\t\"XDG_CONFIG_HOME=/config\",\n\t\t\"BASH_VERSION=5.0\",\n\t\t\"_INTERNAL=secret\",\n\t\t\"CUSTOM_VAR=value\",\n\t}\n\n\tt.Run(\"default blocklist\", func(t *testing.T) {\n\t\tresult := filterEnvironmentWithLists(testEnv, nil, defaultBlockList)\n\n\t\texpected := []string{\"FOO=bar\", \"CUSTOM_VAR=value\"}\n\t\tassert.Assert(t, containsAll(result, expected))\n\n\t\tblockedPrefixes := []string{\n\t\t\t\"PATH=\",\n\t\t\t\"HOME=\",\n\t\t\t\"SSH_AUTH_SOCK=\",\n\t\t\t\"XDG_CONFIG_HOME=\",\n\t\t\t\"BASH_VERSION=\",\n\t\t\t\"_INTERNAL=\",\n\t\t}\n\t\tfor _, prefix := range blockedPrefixes {\n\t\t\tfor _, envVar := range result {\n\t\t\t\tassert.Assert(t, !strings.HasPrefix(envVar, prefix), \"Expected result to not contain variable with prefix %q, but found %q\", prefix, envVar)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"custom blocklist\", func(t *testing.T) {\n\t\tresult := filterEnvironmentWithLists(testEnv, nil, []string{\"FOO\"})\n\n\t\tassert.Assert(t, !slices.Contains(result, \"FOO=bar\"))\n\n\t\texpected := []string{\"PATH=/usr/bin\", \"HOME=/home/user\", \"USER=testuser\"}\n\t\tassert.Assert(t, containsAll(result, expected))\n\t})\n\n\tt.Run(\"allowlist\", func(t *testing.T) {\n\t\tresult := filterEnvironmentWithLists(testEnv, []string{\"FOO\", \"USER\"}, nil)\n\t\texpected := []string{\"FOO=bar\", \"USER=testuser\"}\n\n\t\t// since FOO and USER are included in testEnv, the filtered result should not differ\n\t\t// from what is in the testEnv\n\t\tassert.Equal(t, len(result), len(testEnv))\n\t\tassert.Assert(t, containsAll(result, expected))\n\t})\n\n\tt.Run(\"allowlist takes precedence over blocklist\", func(t *testing.T) {\n\t\tresult := filterEnvironmentWithLists(testEnv, []string{\"FOO\", \"CUSTOM_VAR\"}, []string{\"FOO\", \"USER\"})\n\n\t\texpected := []string{\"FOO=bar\", \"CUSTOM_VAR=value\"}\n\t\tassert.Assert(t, containsAll(result, expected))\n\n\t\tassert.Assert(t, !slices.Contains(result, \"USER=testuser\"))\n\t})\n}\n\nfunc containsAll(slice, items []string) bool {\n\tfor _, item := range items {\n\t\tif !slices.Contains(slice, item) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc TestGetDefaultBlockList(t *testing.T) {\n\tblocklist := GetDefaultBlockList()\n\n\tif &blocklist[0] == &defaultBlockList[0] {\n\t\tt.Error(\"GetDefaultBlockList should return a copy, not the original slice\")\n\t}\n\n\tassert.DeepEqual(t, blocklist, defaultBlockList)\n\n\texpectedItems := []string{\"PATH\", \"HOME\", \"SSH_*\", \"USER\"}\n\tfor _, item := range expectedItems {\n\t\tfound := slices.Contains(blocklist, item)\n\t\tassert.Assert(t, found, \"Expected builtin blocklist to contain %q\", item)\n\t}\n}\n\nfunc TestValidatePattern(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tpattern string\n\t\tvalid   bool\n\t}{\n\t\t{\"simple alphanumeric uppercase\", \"FOO\", true},\n\t\t{\"simple alphanumeric lowercase\", \"foo\", true},\n\t\t{\"mixed case\", \"FooBar\", true},\n\t\t{\"with underscore\", \"FOO_BAR\", true},\n\t\t{\"with numbers\", \"VAR123\", true},\n\t\t{\"with trailing asterisk\", \"FOO*\", true},\n\t\t{\"with multiple asterisks\", \"FOO*BAR*\", true},\n\t\t{\"asterisk at beginning\", \"*FOO\", true},\n\t\t{\"asterisk in middle\", \"FOO*BAR\", true},\n\t\t{\"only asterisk\", \"*\", true},\n\t\t{\"with dash\", \"FOO-BAR\", false},\n\t\t{\"with space\", \"FOO BAR\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := validatePattern(tt.pattern)\n\t\t\tif tt.valid {\n\t\t\t\tassert.NilError(t, err, \"Expected pattern %q to be valid\", tt.pattern)\n\t\t\t} else {\n\t\t\t\tassert.Assert(t, err != nil, \"Expected pattern %q to be invalid\", tt.pattern)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidatePatternErrorMessage(t *testing.T) {\n\terr := validatePattern(\"FOO-BAR\")\n\tassert.Assert(t, err != nil, \"Expected pattern with dash to be invalid\")\n\texpectedMsg := `pattern \"FOO-BAR\" contains invalid character \"-\" at position 3`\n\tassert.Equal(t, err.Error(), expectedMsg)\n}\n"
  },
  {
    "path": "pkg/executil/command.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage executil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n)\n\ntype options struct {\n\tctx context.Context\n}\n\ntype Opt func(*options) error\n\n// WithContext runs the command with CommandContext.\nfunc WithContext(ctx context.Context) Opt {\n\treturn func(o *options) error {\n\t\to.ctx = ctx\n\t\treturn nil\n\t}\n}\n\nfunc RunUTF16leCommand(args []string, opts ...Opt) (string, error) {\n\tvar o options\n\tfor _, f := range opts {\n\t\tif err := f(&o); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tvar cmd *exec.Cmd\n\tctx := o.ctx\n\tif ctx == nil {\n\t\tctx = context.Background()\n\t}\n\tcmd = exec.CommandContext(ctx, args[0], args[1:]...)\n\n\toutString := \"\"\n\tout, err := cmd.CombinedOutput()\n\tif out != nil {\n\t\ts, err := ioutilx.FromUTF16leToString(bytes.NewReader(out))\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to convert output from UTF16 when running command %v, err: %w\", args, err)\n\t\t}\n\t\toutString = s\n\t}\n\treturn outString, err\n}\n"
  },
  {
    "path": "pkg/executil/opts_others.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage executil\n\nimport (\n\t\"syscall\"\n)\n\nvar (\n\tForegroundSysProcAttr = &syscall.SysProcAttr{}\n\tBackgroundSysProcAttr = &syscall.SysProcAttr{Setpgid: true}\n)\n"
  },
  {
    "path": "pkg/executil/opts_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage executil\n\nimport (\n\t\"syscall\"\n)\n\nvar (\n\tForegroundSysProcAttr = &syscall.SysProcAttr{}\n\tBackgroundSysProcAttr = &syscall.SysProcAttr{\n\t\tCreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,\n\t}\n)\n"
  },
  {
    "path": "pkg/fileutils/download.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage fileutils\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/downloader\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n// ErrSkipped is returned when the downloader did not attempt to download the specified file.\nvar ErrSkipped = errors.New(\"skipped to download\")\n\n// DownloadFile downloads a file to the cache, optionally copying it to the destination. Returns path in cache.\nfunc DownloadFile(ctx context.Context, dest string, f limatype.File, decompress bool, description string, expectedArch limatype.Arch) (string, error) {\n\tif f.Arch != expectedArch {\n\t\treturn \"\", fmt.Errorf(\"%w: %q: unsupported arch: %q\", ErrSkipped, f.Location, f.Arch)\n\t}\n\tfields := logrus.Fields{\"location\": f.Location, \"arch\": f.Arch, \"digest\": f.Digest}\n\tlogrus.WithFields(fields).Infof(\"Attempting to download %s\", description)\n\tres, err := downloader.Download(ctx, dest, f.Location,\n\t\tdownloader.WithCache(),\n\t\tdownloader.WithDecompress(decompress),\n\t\tdownloader.WithDescription(fmt.Sprintf(\"%s (%s)\", description, path.Base(f.Location))),\n\t\tdownloader.WithExpectedDigest(f.Digest),\n\t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to download %q: %w\", f.Location, err)\n\t}\n\tlogrus.Debugf(\"res.ValidatedDigest=%v\", res.ValidatedDigest)\n\tswitch res.Status {\n\tcase downloader.StatusDownloaded:\n\t\tlogrus.Infof(\"Downloaded %s from %q\", description, f.Location)\n\tcase downloader.StatusUsedCache:\n\t\tlogrus.Infof(\"Using cache %q\", res.CachePath)\n\tdefault:\n\t\tlogrus.Warnf(\"Unexpected result from downloader.Download(): %+v\", res)\n\t}\n\treturn res.CachePath, nil\n}\n\n// CachedFile checks if a file is in the cache, validating the digest if it is available. Returns path in cache.\nfunc CachedFile(f limatype.File) (string, error) {\n\tres, err := downloader.Cached(f.Location,\n\t\tdownloader.WithCache(),\n\t\tdownloader.WithExpectedDigest(f.Digest))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"cache did not contain %q: %w\", f.Location, err)\n\t}\n\treturn res.CachePath, nil\n}\n\n// Errors compose multiple into a single error.\n// Errors filters out ErrSkipped.\nfunc Errors(errs []error) error {\n\tvar finalErr error\n\tfor _, err := range errs {\n\t\tif errors.Is(err, ErrSkipped) {\n\t\t\tlogrus.Debug(err)\n\t\t} else {\n\t\t\tfinalErr = errors.Join(finalErr, err)\n\t\t}\n\t}\n\tif len(errs) > 0 && finalErr == nil {\n\t\t// errs only contains ErrSkipped\n\t\tfinalErr = fmt.Errorf(\"%v\", errs)\n\t}\n\treturn finalErr\n}\n"
  },
  {
    "path": "pkg/freeport/freeport.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package freeport provides functions to find free localhost ports.\npackage freeport\n\nimport (\n\t\"fmt\"\n\t\"net\"\n)\n\nfunc TCP() (int, error) {\n\tlAddr0, err := net.ResolveTCPAddr(\"tcp4\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tl, err := net.ListenTCP(\"tcp4\", lAddr0)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer l.Close()\n\tlAddr := l.Addr()\n\tlTCPAddr, ok := lAddr.(*net.TCPAddr)\n\tif !ok {\n\t\treturn 0, fmt.Errorf(\"expected *net.TCPAddr, got %v\", lAddr)\n\t}\n\tport := lTCPAddr.Port\n\tif port <= 0 {\n\t\treturn 0, fmt.Errorf(\"unexpected port %d\", port)\n\t}\n\treturn port, nil\n}\n\nfunc UDP() (int, error) {\n\tlAddr0, err := net.ResolveUDPAddr(\"udp4\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tl, err := net.ListenUDP(\"udp4\", lAddr0)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer l.Close()\n\tlAddr := l.LocalAddr()\n\tlUDPAddr, ok := lAddr.(*net.UDPAddr)\n\tif !ok {\n\t\treturn 0, fmt.Errorf(\"expected *net.UDPAddr, got %v\", lAddr)\n\t}\n\tport := lUDPAddr.Port\n\tif port <= 0 {\n\t\treturn 0, fmt.Errorf(\"unexpected port %d\", port)\n\t}\n\treturn port, nil\n}\n"
  },
  {
    "path": "pkg/freeport/freeport_unix.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage freeport\n\nimport \"errors\"\n\nfunc VSock() (int, error) {\n\treturn 0, errors.New(\"freeport.VSock is not implemented for non-Windows hosts\")\n}\n"
  },
  {
    "path": "pkg/freeport/freeport_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage freeport\n\nimport \"github.com/lima-vm/lima/v2/pkg/windows\"\n\nfunc VSock() (int, error) {\n\treturn windows.GetRandomFreeVSockPort(0, 2147483647)\n}\n"
  },
  {
    "path": "pkg/fsutil/fsutil_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage fsutil\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\n// IsNFS checks if the path is on NFS. If the path does not exist yet, it will walk\n// up parent directories until one exists, or it hits '/' or '.'.\n// Any other stat errors will cause IsNFS to fail.\nfunc IsNFS(path string) (bool, error) {\n\tfor len(path) > 1 {\n\t\t_, err := os.Stat(path)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn false, err\n\t\t}\n\t\tpath = filepath.Dir(path)\n\t}\n\n\tvar sf unix.Statfs_t\n\tif err := unix.Statfs(path, &sf); err != nil {\n\t\treturn false, err\n\t}\n\treturn sf.Type == unix.NFS_SUPER_MAGIC, nil\n}\n"
  },
  {
    "path": "pkg/fsutil/fsutil_others.go",
    "content": "//go:build !linux\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage fsutil\n\nfunc IsNFS(string) (bool, error) {\n\treturn false, nil\n}\n"
  },
  {
    "path": "pkg/guestagent/api/client/client.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\nimport (\n\t\"context\"\n\t\"math\"\n\t\"net\"\n\t\"time\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/resolver\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n)\n\ntype GuestAgentClient struct {\n\tcli api.GuestServiceClient\n}\n\nfunc NewGuestAgentClient(dialFn func(ctx context.Context) (net.Conn, error)) (*GuestAgentClient, error) {\n\topts := []grpc.DialOption{\n\t\tgrpc.WithTransportCredentials(NewCredentials()),\n\t\tgrpc.WithInitialWindowSize(512 << 20),\n\t\tgrpc.WithInitialConnWindowSize(512 << 20),\n\t\tgrpc.WithReadBufferSize(8 << 20),\n\t\tgrpc.WithWriteBufferSize(8 << 20),\n\t\tgrpc.WithDefaultCallOptions(\n\t\t\tgrpc.MaxCallRecvMsgSize(math.MaxInt32),\n\t\t\tgrpc.MaxCallSendMsgSize(math.MaxInt32),\n\t\t),\n\t\tgrpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {\n\t\t\treturn dialFn(ctx)\n\t\t}),\n\t}\n\n\tresolver.SetDefaultScheme(\"passthrough\")\n\tclientConn, err := grpc.NewClient(\"\", opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tclient := api.NewGuestServiceClient(clientConn)\n\treturn &GuestAgentClient{\n\t\tcli: client,\n\t}, nil\n}\n\nfunc (c *GuestAgentClient) Info(ctx context.Context) (*api.Info, error) {\n\treturn c.cli.GetInfo(ctx, &emptypb.Empty{})\n}\n\nfunc (c *GuestAgentClient) Events(ctx context.Context, eventCb func(response *api.Event)) error {\n\tevents, err := c.cli.GetEvents(ctx, &emptypb.Empty{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\trecv, err := events.Recv()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\teventCb(recv)\n\t}\n}\n\nfunc (c *GuestAgentClient) Inotify(ctx context.Context) (api.GuestService_PostInotifyClient, error) {\n\tinotify, err := c.cli.PostInotify(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn inotify, nil\n}\n\nfunc (c *GuestAgentClient) Tunnel(ctx context.Context) (api.GuestService_TunnelClient, error) {\n\tstream, err := c.cli.Tunnel(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn stream, nil\n}\n\nfunc (c *GuestAgentClient) SyncTime(ctx context.Context, hostTime time.Time) (*api.TimeSyncResponse, error) {\n\treq := &api.TimeSyncRequest{\n\t\tHostTime: timestamppb.New(hostTime),\n\t}\n\treturn c.cli.SyncTime(ctx, req)\n}\n"
  },
  {
    "path": "pkg/guestagent/api/client/credentials.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"google.golang.org/grpc/credentials\"\n)\n\n// NewCredentials returns a lima credential implementing credentials.TransportCredentials.\nfunc NewCredentials() credentials.TransportCredentials {\n\treturn &secureTC{\n\t\tinfo: credentials.ProtocolInfo{\n\t\t\tSecurityProtocol: \"local\",\n\t\t},\n\t}\n}\n\n// secureTC is the credentials required to establish a lima guest connection.\ntype secureTC struct {\n\tinfo credentials.ProtocolInfo\n}\n\nfunc (c *secureTC) Info() credentials.ProtocolInfo {\n\treturn c.info\n}\n\nfunc (*secureTC) ClientHandshake(_ context.Context, _ string, conn net.Conn) (net.Conn, credentials.AuthInfo, error) {\n\treturn conn, info{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}, nil\n}\n\nfunc (*secureTC) ServerHandshake(conn net.Conn) (net.Conn, credentials.AuthInfo, error) {\n\treturn conn, info{credentials.CommonAuthInfo{SecurityLevel: credentials.PrivacyAndIntegrity}}, nil\n}\n\nfunc (c *secureTC) Clone() credentials.TransportCredentials {\n\treturn &secureTC{info: c.info}\n}\n\nfunc (c *secureTC) OverrideServerName(serverNameOverride string) error {\n\t// SA1019: c.info.ServerName is deprecated:\n\t// Users should use grpc.WithAuthority to override the authority on a channel instead of configuring the credentials.\n\n\t//nolint:staticcheck // c.info.ServerName is used for compatibility reason\n\tc.info.ServerName = serverNameOverride\n\treturn nil\n}\n\ntype info struct {\n\tcredentials.CommonAuthInfo\n}\n\nfunc (info) AuthType() string {\n\treturn \"local\"\n}\n"
  },
  {
    "path": "pkg/guestagent/api/gen.go",
    "content": "//go:generate ../../../hack/gogenerate/protoc.sh guestservice.proto\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage api\n"
  },
  {
    "path": "pkg/guestagent/api/guestservice.pb.desc",
    "content": "\n\b\n\u0012guestservice.proto\u001a\u001bgoogle/protobuf/empty.proto\u001a\u001fgoogle/protobuf/timestamp.proto\"0\n\u0004Info\u0012(\n\u000blocal_ports\u0018\u0001 \u0003(\u000b2\u0007.IPPortR\nlocalPorts\"\u0001\n\u0005Event\u0012.\n\u0004time\u0018\u0001 \u0001(\u000b2\u001a.google.protobuf.TimestampR\u0004time\u00123\n\u0011added_local_ports\u0018\u0002 \u0003(\u000b2\u0007.IPPortR\u000faddedLocalPorts\u00127\n\u0013removed_local_ports\u0018\u0003 \u0003(\u000b2\u0007.IPPortR\u0011removedLocalPorts\u0012\u0016\n\u0006errors\u0018\u0004 \u0003(\tR\u0006errors\"H\n\u0006IPPort\u0012\u001a\n\bprotocol\u0018\u0001 \u0001(\tR\bprotocol\u0012\u000e\n\u0002ip\u0018\u0002 \u0001(\tR\u0002ip\u0012\u0012\n\u0004port\u0018\u0003 \u0001(\u0005R\u0004port\"X\n\u0007Inotify\u0012\u001d\n\nmount_path\u0018\u0001 \u0001(\tR\tmountPath\u0012.\n\u0004time\u0018\u0002 \u0001(\u000b2\u001a.google.protobuf.TimestampR\u0004time\"\u0001\n\rTunnelMessage\u0012\u000e\n\u0002id\u0018\u0001 \u0001(\tR\u0002id\u0012\u001a\n\bprotocol\u0018\u0002 \u0001(\tR\bprotocol\u0012\u0012\n\u0004data\u0018\u0003 \u0001(\fR\u0004data\u0012\u001d\n\nguest_addr\u0018\u0004 \u0001(\tR\tguestAddr\u0012&\n\u000fudp_target_addr\u0018\u0005 \u0001(\tR\rudpTargetAddr\"J\n\u000fTimeSyncRequest\u00127\n\thost_time\u0018\u0001 \u0001(\u000b2\u001a.google.protobuf.TimestampR\bhostTime\"_\n\u0010TimeSyncResponse\u0012\u001a\n\badjusted\u0018\u0001 \u0001(\bR\badjusted\u0012\u0019\n\bdrift_ms\u0018\u0002 \u0001(\u0003R\u0007driftMs\u0012\u0014\n\u0005error\u0018\u0003 \u0001(\tR\u0005error2\u0001\n\fGuestService\u0012(\n\u0007GetInfo\u0012\u0016.google.protobuf.Empty\u001a\u0005.Info\u0012-\n\tGetEvents\u0012\u0016.google.protobuf.Empty\u001a\u0006.Event0\u0001\u00121\n\u000bPostInotify\u0012\b.Inotify\u001a\u0016.google.protobuf.Empty(\u0001\u0012,\n\u0006Tunnel\u0012\u000e.TunnelMessage\u001a\u000e.TunnelMessage(\u00010\u0001\u0012/\n\bSyncTime\u0012\u0010.TimeSyncRequest\u001a\u0011.TimeSyncResponseB/Z-github.com/lima-vm/lima/v2/pkg/guestagent/apib\u0006proto3"
  },
  {
    "path": "pkg/guestagent/api/guestservice.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go [version omitted for reproducibility]\n// \tprotoc        [version omitted for reproducibility]\n// source: guestservice.proto\n\npackage api\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n\ttimestamppb \"google.golang.org/protobuf/types/known/timestamppb\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype Info struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tLocalPorts    []*IPPort              `protobuf:\"bytes,1,rep,name=local_ports,json=localPorts,proto3\" json:\"local_ports,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Info) Reset() {\n\t*x = Info{}\n\tmi := &file_guestservice_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Info) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Info) ProtoMessage() {}\n\nfunc (x *Info) ProtoReflect() protoreflect.Message {\n\tmi := &file_guestservice_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Info.ProtoReflect.Descriptor instead.\nfunc (*Info) Descriptor() ([]byte, []int) {\n\treturn file_guestservice_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *Info) GetLocalPorts() []*IPPort {\n\tif x != nil {\n\t\treturn x.LocalPorts\n\t}\n\treturn nil\n}\n\ntype Event struct {\n\tstate             protoimpl.MessageState `protogen:\"open.v1\"`\n\tTime              *timestamppb.Timestamp `protobuf:\"bytes,1,opt,name=time,proto3\" json:\"time,omitempty\"`\n\tAddedLocalPorts   []*IPPort              `protobuf:\"bytes,2,rep,name=added_local_ports,json=addedLocalPorts,proto3\" json:\"added_local_ports,omitempty\"`\n\tRemovedLocalPorts []*IPPort              `protobuf:\"bytes,3,rep,name=removed_local_ports,json=removedLocalPorts,proto3\" json:\"removed_local_ports,omitempty\"`\n\tErrors            []string               `protobuf:\"bytes,4,rep,name=errors,proto3\" json:\"errors,omitempty\"`\n\tunknownFields     protoimpl.UnknownFields\n\tsizeCache         protoimpl.SizeCache\n}\n\nfunc (x *Event) Reset() {\n\t*x = Event{}\n\tmi := &file_guestservice_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Event) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Event) ProtoMessage() {}\n\nfunc (x *Event) ProtoReflect() protoreflect.Message {\n\tmi := &file_guestservice_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Event.ProtoReflect.Descriptor instead.\nfunc (*Event) Descriptor() ([]byte, []int) {\n\treturn file_guestservice_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *Event) GetTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Time\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetAddedLocalPorts() []*IPPort {\n\tif x != nil {\n\t\treturn x.AddedLocalPorts\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetRemovedLocalPorts() []*IPPort {\n\tif x != nil {\n\t\treturn x.RemovedLocalPorts\n\t}\n\treturn nil\n}\n\nfunc (x *Event) GetErrors() []string {\n\tif x != nil {\n\t\treturn x.Errors\n\t}\n\treturn nil\n}\n\ntype IPPort struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tProtocol      string                 `protobuf:\"bytes,1,opt,name=protocol,proto3\" json:\"protocol,omitempty\"` // tcp, udp\n\tIp            string                 `protobuf:\"bytes,2,opt,name=ip,proto3\" json:\"ip,omitempty\"`\n\tPort          int32                  `protobuf:\"varint,3,opt,name=port,proto3\" json:\"port,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *IPPort) Reset() {\n\t*x = IPPort{}\n\tmi := &file_guestservice_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *IPPort) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*IPPort) ProtoMessage() {}\n\nfunc (x *IPPort) ProtoReflect() protoreflect.Message {\n\tmi := &file_guestservice_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use IPPort.ProtoReflect.Descriptor instead.\nfunc (*IPPort) Descriptor() ([]byte, []int) {\n\treturn file_guestservice_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *IPPort) GetProtocol() string {\n\tif x != nil {\n\t\treturn x.Protocol\n\t}\n\treturn \"\"\n}\n\nfunc (x *IPPort) GetIp() string {\n\tif x != nil {\n\t\treturn x.Ip\n\t}\n\treturn \"\"\n}\n\nfunc (x *IPPort) GetPort() int32 {\n\tif x != nil {\n\t\treturn x.Port\n\t}\n\treturn 0\n}\n\ntype Inotify struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tMountPath     string                 `protobuf:\"bytes,1,opt,name=mount_path,json=mountPath,proto3\" json:\"mount_path,omitempty\"`\n\tTime          *timestamppb.Timestamp `protobuf:\"bytes,2,opt,name=time,proto3\" json:\"time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Inotify) Reset() {\n\t*x = Inotify{}\n\tmi := &file_guestservice_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Inotify) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Inotify) ProtoMessage() {}\n\nfunc (x *Inotify) ProtoReflect() protoreflect.Message {\n\tmi := &file_guestservice_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Inotify.ProtoReflect.Descriptor instead.\nfunc (*Inotify) Descriptor() ([]byte, []int) {\n\treturn file_guestservice_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *Inotify) GetMountPath() string {\n\tif x != nil {\n\t\treturn x.MountPath\n\t}\n\treturn \"\"\n}\n\nfunc (x *Inotify) GetTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.Time\n\t}\n\treturn nil\n}\n\ntype TunnelMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tProtocol      string                 `protobuf:\"bytes,2,opt,name=protocol,proto3\" json:\"protocol,omitempty\"` // tcp, udp\n\tData          []byte                 `protobuf:\"bytes,3,opt,name=data,proto3\" json:\"data,omitempty\"`\n\tGuestAddr     string                 `protobuf:\"bytes,4,opt,name=guest_addr,json=guestAddr,proto3\" json:\"guest_addr,omitempty\"`\n\tUdpTargetAddr string                 `protobuf:\"bytes,5,opt,name=udp_target_addr,json=udpTargetAddr,proto3\" json:\"udp_target_addr,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TunnelMessage) Reset() {\n\t*x = TunnelMessage{}\n\tmi := &file_guestservice_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TunnelMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TunnelMessage) ProtoMessage() {}\n\nfunc (x *TunnelMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_guestservice_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TunnelMessage.ProtoReflect.Descriptor instead.\nfunc (*TunnelMessage) Descriptor() ([]byte, []int) {\n\treturn file_guestservice_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *TunnelMessage) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *TunnelMessage) GetProtocol() string {\n\tif x != nil {\n\t\treturn x.Protocol\n\t}\n\treturn \"\"\n}\n\nfunc (x *TunnelMessage) GetData() []byte {\n\tif x != nil {\n\t\treturn x.Data\n\t}\n\treturn nil\n}\n\nfunc (x *TunnelMessage) GetGuestAddr() string {\n\tif x != nil {\n\t\treturn x.GuestAddr\n\t}\n\treturn \"\"\n}\n\nfunc (x *TunnelMessage) GetUdpTargetAddr() string {\n\tif x != nil {\n\t\treturn x.UdpTargetAddr\n\t}\n\treturn \"\"\n}\n\ntype TimeSyncRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tHostTime      *timestamppb.Timestamp `protobuf:\"bytes,1,opt,name=host_time,json=hostTime,proto3\" json:\"host_time,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TimeSyncRequest) Reset() {\n\t*x = TimeSyncRequest{}\n\tmi := &file_guestservice_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TimeSyncRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TimeSyncRequest) ProtoMessage() {}\n\nfunc (x *TimeSyncRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_guestservice_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TimeSyncRequest.ProtoReflect.Descriptor instead.\nfunc (*TimeSyncRequest) Descriptor() ([]byte, []int) {\n\treturn file_guestservice_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *TimeSyncRequest) GetHostTime() *timestamppb.Timestamp {\n\tif x != nil {\n\t\treturn x.HostTime\n\t}\n\treturn nil\n}\n\ntype TimeSyncResponse struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tAdjusted      bool                   `protobuf:\"varint,1,opt,name=adjusted,proto3\" json:\"adjusted,omitempty\"`\n\tDriftMs       int64                  `protobuf:\"varint,2,opt,name=drift_ms,json=driftMs,proto3\" json:\"drift_ms,omitempty\"`\n\tError         string                 `protobuf:\"bytes,3,opt,name=error,proto3\" json:\"error,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *TimeSyncResponse) Reset() {\n\t*x = TimeSyncResponse{}\n\tmi := &file_guestservice_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *TimeSyncResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*TimeSyncResponse) ProtoMessage() {}\n\nfunc (x *TimeSyncResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_guestservice_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use TimeSyncResponse.ProtoReflect.Descriptor instead.\nfunc (*TimeSyncResponse) Descriptor() ([]byte, []int) {\n\treturn file_guestservice_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *TimeSyncResponse) GetAdjusted() bool {\n\tif x != nil {\n\t\treturn x.Adjusted\n\t}\n\treturn false\n}\n\nfunc (x *TimeSyncResponse) GetDriftMs() int64 {\n\tif x != nil {\n\t\treturn x.DriftMs\n\t}\n\treturn 0\n}\n\nfunc (x *TimeSyncResponse) GetError() string {\n\tif x != nil {\n\t\treturn x.Error\n\t}\n\treturn \"\"\n}\n\nvar File_guestservice_proto protoreflect.FileDescriptor\n\nconst file_guestservice_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x12guestservice.proto\\x1a\\x1bgoogle/protobuf/empty.proto\\x1a\\x1fgoogle/protobuf/timestamp.proto\\\"0\\n\" +\n\t\"\\x04Info\\x12(\\n\" +\n\t\"\\vlocal_ports\\x18\\x01 \\x03(\\v2\\a.IPPortR\\n\" +\n\t\"localPorts\\\"\\xbd\\x01\\n\" +\n\t\"\\x05Event\\x12.\\n\" +\n\t\"\\x04time\\x18\\x01 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\x04time\\x123\\n\" +\n\t\"\\x11added_local_ports\\x18\\x02 \\x03(\\v2\\a.IPPortR\\x0faddedLocalPorts\\x127\\n\" +\n\t\"\\x13removed_local_ports\\x18\\x03 \\x03(\\v2\\a.IPPortR\\x11removedLocalPorts\\x12\\x16\\n\" +\n\t\"\\x06errors\\x18\\x04 \\x03(\\tR\\x06errors\\\"H\\n\" +\n\t\"\\x06IPPort\\x12\\x1a\\n\" +\n\t\"\\bprotocol\\x18\\x01 \\x01(\\tR\\bprotocol\\x12\\x0e\\n\" +\n\t\"\\x02ip\\x18\\x02 \\x01(\\tR\\x02ip\\x12\\x12\\n\" +\n\t\"\\x04port\\x18\\x03 \\x01(\\x05R\\x04port\\\"X\\n\" +\n\t\"\\aInotify\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"mount_path\\x18\\x01 \\x01(\\tR\\tmountPath\\x12.\\n\" +\n\t\"\\x04time\\x18\\x02 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\x04time\\\"\\x96\\x01\\n\" +\n\t\"\\rTunnelMessage\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x1a\\n\" +\n\t\"\\bprotocol\\x18\\x02 \\x01(\\tR\\bprotocol\\x12\\x12\\n\" +\n\t\"\\x04data\\x18\\x03 \\x01(\\fR\\x04data\\x12\\x1d\\n\" +\n\t\"\\n\" +\n\t\"guest_addr\\x18\\x04 \\x01(\\tR\\tguestAddr\\x12&\\n\" +\n\t\"\\x0fudp_target_addr\\x18\\x05 \\x01(\\tR\\rudpTargetAddr\\\"J\\n\" +\n\t\"\\x0fTimeSyncRequest\\x127\\n\" +\n\t\"\\thost_time\\x18\\x01 \\x01(\\v2\\x1a.google.protobuf.TimestampR\\bhostTime\\\"_\\n\" +\n\t\"\\x10TimeSyncResponse\\x12\\x1a\\n\" +\n\t\"\\badjusted\\x18\\x01 \\x01(\\bR\\badjusted\\x12\\x19\\n\" +\n\t\"\\bdrift_ms\\x18\\x02 \\x01(\\x03R\\adriftMs\\x12\\x14\\n\" +\n\t\"\\x05error\\x18\\x03 \\x01(\\tR\\x05error2\\xf9\\x01\\n\" +\n\t\"\\fGuestService\\x12(\\n\" +\n\t\"\\aGetInfo\\x12\\x16.google.protobuf.Empty\\x1a\\x05.Info\\x12-\\n\" +\n\t\"\\tGetEvents\\x12\\x16.google.protobuf.Empty\\x1a\\x06.Event0\\x01\\x121\\n\" +\n\t\"\\vPostInotify\\x12\\b.Inotify\\x1a\\x16.google.protobuf.Empty(\\x01\\x12,\\n\" +\n\t\"\\x06Tunnel\\x12\\x0e.TunnelMessage\\x1a\\x0e.TunnelMessage(\\x010\\x01\\x12/\\n\" +\n\t\"\\bSyncTime\\x12\\x10.TimeSyncRequest\\x1a\\x11.TimeSyncResponseB/Z-github.com/lima-vm/lima/v2/pkg/guestagent/apib\\x06proto3\"\n\nvar (\n\tfile_guestservice_proto_rawDescOnce sync.Once\n\tfile_guestservice_proto_rawDescData []byte\n)\n\nfunc file_guestservice_proto_rawDescGZIP() []byte {\n\tfile_guestservice_proto_rawDescOnce.Do(func() {\n\t\tfile_guestservice_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_guestservice_proto_rawDesc), len(file_guestservice_proto_rawDesc)))\n\t})\n\treturn file_guestservice_proto_rawDescData\n}\n\nvar file_guestservice_proto_msgTypes = make([]protoimpl.MessageInfo, 7)\nvar file_guestservice_proto_goTypes = []any{\n\t(*Info)(nil),                  // 0: Info\n\t(*Event)(nil),                 // 1: Event\n\t(*IPPort)(nil),                // 2: IPPort\n\t(*Inotify)(nil),               // 3: Inotify\n\t(*TunnelMessage)(nil),         // 4: TunnelMessage\n\t(*TimeSyncRequest)(nil),       // 5: TimeSyncRequest\n\t(*TimeSyncResponse)(nil),      // 6: TimeSyncResponse\n\t(*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp\n\t(*emptypb.Empty)(nil),         // 8: google.protobuf.Empty\n}\nvar file_guestservice_proto_depIdxs = []int32{\n\t2,  // 0: Info.local_ports:type_name -> IPPort\n\t7,  // 1: Event.time:type_name -> google.protobuf.Timestamp\n\t2,  // 2: Event.added_local_ports:type_name -> IPPort\n\t2,  // 3: Event.removed_local_ports:type_name -> IPPort\n\t7,  // 4: Inotify.time:type_name -> google.protobuf.Timestamp\n\t7,  // 5: TimeSyncRequest.host_time:type_name -> google.protobuf.Timestamp\n\t8,  // 6: GuestService.GetInfo:input_type -> google.protobuf.Empty\n\t8,  // 7: GuestService.GetEvents:input_type -> google.protobuf.Empty\n\t3,  // 8: GuestService.PostInotify:input_type -> Inotify\n\t4,  // 9: GuestService.Tunnel:input_type -> TunnelMessage\n\t5,  // 10: GuestService.SyncTime:input_type -> TimeSyncRequest\n\t0,  // 11: GuestService.GetInfo:output_type -> Info\n\t1,  // 12: GuestService.GetEvents:output_type -> Event\n\t8,  // 13: GuestService.PostInotify:output_type -> google.protobuf.Empty\n\t4,  // 14: GuestService.Tunnel:output_type -> TunnelMessage\n\t6,  // 15: GuestService.SyncTime:output_type -> TimeSyncResponse\n\t11, // [11:16] is the sub-list for method output_type\n\t6,  // [6:11] is the sub-list for method input_type\n\t6,  // [6:6] is the sub-list for extension type_name\n\t6,  // [6:6] is the sub-list for extension extendee\n\t0,  // [0:6] is the sub-list for field type_name\n}\n\nfunc init() { file_guestservice_proto_init() }\nfunc file_guestservice_proto_init() {\n\tif File_guestservice_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_guestservice_proto_rawDesc), len(file_guestservice_proto_rawDesc)),\n\t\t\tNumEnums:      0,\n\t\t\tNumMessages:   7,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_guestservice_proto_goTypes,\n\t\tDependencyIndexes: file_guestservice_proto_depIdxs,\n\t\tMessageInfos:      file_guestservice_proto_msgTypes,\n\t}.Build()\n\tFile_guestservice_proto = out.File\n\tfile_guestservice_proto_goTypes = nil\n\tfile_guestservice_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "pkg/guestagent/api/guestservice.proto",
    "content": "syntax = \"proto3\";\n\nimport \"google/protobuf/empty.proto\";\nimport \"google/protobuf/timestamp.proto\";\n\noption go_package = \"github.com/lima-vm/lima/v2/pkg/guestagent/api\";\n\nservice GuestService {\n  rpc GetInfo(google.protobuf.Empty) returns (Info);\n  rpc GetEvents(google.protobuf.Empty) returns (stream Event);\n  rpc PostInotify(stream Inotify) returns (google.protobuf.Empty);\n\n  rpc Tunnel(stream TunnelMessage) returns (stream TunnelMessage);\n  rpc SyncTime(TimeSyncRequest) returns (TimeSyncResponse);\n}\n\nmessage Info {\n  repeated IPPort local_ports = 1;\n}\n\nmessage Event {\n  google.protobuf.Timestamp time = 1;\n  repeated IPPort added_local_ports = 2;\n  repeated IPPort removed_local_ports = 3;\n  repeated string errors = 4;\n}\n\nmessage IPPort {\n  string protocol = 1; // tcp, udp\n  string ip = 2;\n  int32 port = 3;\n}\n\nmessage Inotify {\n  string mount_path = 1;\n  google.protobuf.Timestamp time = 2;\n}\n\nmessage TunnelMessage {\n  string id = 1;\n  string protocol = 2; // tcp, udp\n  bytes data = 3;\n  string guest_addr = 4;\n  string udp_target_addr = 5;\n}\n\nmessage TimeSyncRequest {\n  google.protobuf.Timestamp host_time = 1;\n}\n\nmessage TimeSyncResponse {\n  bool adjusted = 1;\n  int64 drift_ms = 2;\n  string error = 3;\n}\n"
  },
  {
    "path": "pkg/guestagent/api/guestservice_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc [version omitted for reproducibility]\n// - protoc             [version omitted for reproducibility]\n// source: guestservice.proto\n\npackage api\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n\temptypb \"google.golang.org/protobuf/types/known/emptypb\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tGuestService_GetInfo_FullMethodName     = \"/GuestService/GetInfo\"\n\tGuestService_GetEvents_FullMethodName   = \"/GuestService/GetEvents\"\n\tGuestService_PostInotify_FullMethodName = \"/GuestService/PostInotify\"\n\tGuestService_Tunnel_FullMethodName      = \"/GuestService/Tunnel\"\n\tGuestService_SyncTime_FullMethodName    = \"/GuestService/SyncTime\"\n)\n\n// GuestServiceClient is the client API for GuestService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype GuestServiceClient interface {\n\tGetInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error)\n\tGetEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Event], error)\n\tPostInotify(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[Inotify, emptypb.Empty], error)\n\tTunnel(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TunnelMessage, TunnelMessage], error)\n\tSyncTime(ctx context.Context, in *TimeSyncRequest, opts ...grpc.CallOption) (*TimeSyncResponse, error)\n}\n\ntype guestServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewGuestServiceClient(cc grpc.ClientConnInterface) GuestServiceClient {\n\treturn &guestServiceClient{cc}\n}\n\nfunc (c *guestServiceClient) GetInfo(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Info, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Info)\n\terr := c.cc.Invoke(ctx, GuestService_GetInfo_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *guestServiceClient) GetEvents(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Event], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &GuestService_ServiceDesc.Streams[0], GuestService_GetEvents_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[emptypb.Empty, Event]{ClientStream: stream}\n\tif err := x.ClientStream.SendMsg(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := x.ClientStream.CloseSend(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype GuestService_GetEventsClient = grpc.ServerStreamingClient[Event]\n\nfunc (c *guestServiceClient) PostInotify(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[Inotify, emptypb.Empty], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &GuestService_ServiceDesc.Streams[1], GuestService_PostInotify_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[Inotify, emptypb.Empty]{ClientStream: stream}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype GuestService_PostInotifyClient = grpc.ClientStreamingClient[Inotify, emptypb.Empty]\n\nfunc (c *guestServiceClient) Tunnel(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[TunnelMessage, TunnelMessage], error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tstream, err := c.cc.NewStream(ctx, &GuestService_ServiceDesc.Streams[2], GuestService_Tunnel_FullMethodName, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tx := &grpc.GenericClientStream[TunnelMessage, TunnelMessage]{ClientStream: stream}\n\treturn x, nil\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype GuestService_TunnelClient = grpc.BidiStreamingClient[TunnelMessage, TunnelMessage]\n\nfunc (c *guestServiceClient) SyncTime(ctx context.Context, in *TimeSyncRequest, opts ...grpc.CallOption) (*TimeSyncResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(TimeSyncResponse)\n\terr := c.cc.Invoke(ctx, GuestService_SyncTime_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// GuestServiceServer is the server API for GuestService service.\n// All implementations must embed UnimplementedGuestServiceServer\n// for forward compatibility.\ntype GuestServiceServer interface {\n\tGetInfo(context.Context, *emptypb.Empty) (*Info, error)\n\tGetEvents(*emptypb.Empty, grpc.ServerStreamingServer[Event]) error\n\tPostInotify(grpc.ClientStreamingServer[Inotify, emptypb.Empty]) error\n\tTunnel(grpc.BidiStreamingServer[TunnelMessage, TunnelMessage]) error\n\tSyncTime(context.Context, *TimeSyncRequest) (*TimeSyncResponse, error)\n\tmustEmbedUnimplementedGuestServiceServer()\n}\n\n// UnimplementedGuestServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedGuestServiceServer struct{}\n\nfunc (UnimplementedGuestServiceServer) GetInfo(context.Context, *emptypb.Empty) (*Info, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method GetInfo not implemented\")\n}\nfunc (UnimplementedGuestServiceServer) GetEvents(*emptypb.Empty, grpc.ServerStreamingServer[Event]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method GetEvents not implemented\")\n}\nfunc (UnimplementedGuestServiceServer) PostInotify(grpc.ClientStreamingServer[Inotify, emptypb.Empty]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method PostInotify not implemented\")\n}\nfunc (UnimplementedGuestServiceServer) Tunnel(grpc.BidiStreamingServer[TunnelMessage, TunnelMessage]) error {\n\treturn status.Errorf(codes.Unimplemented, \"method Tunnel not implemented\")\n}\nfunc (UnimplementedGuestServiceServer) SyncTime(context.Context, *TimeSyncRequest) (*TimeSyncResponse, error) {\n\treturn nil, status.Errorf(codes.Unimplemented, \"method SyncTime not implemented\")\n}\nfunc (UnimplementedGuestServiceServer) mustEmbedUnimplementedGuestServiceServer() {}\nfunc (UnimplementedGuestServiceServer) testEmbeddedByValue()                      {}\n\n// UnsafeGuestServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to GuestServiceServer will\n// result in compilation errors.\ntype UnsafeGuestServiceServer interface {\n\tmustEmbedUnimplementedGuestServiceServer()\n}\n\nfunc RegisterGuestServiceServer(s grpc.ServiceRegistrar, srv GuestServiceServer) {\n\t// If the following call pancis, it indicates UnimplementedGuestServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&GuestService_ServiceDesc, srv)\n}\n\nfunc _GuestService_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(emptypb.Empty)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GuestServiceServer).GetInfo(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: GuestService_GetInfo_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GuestServiceServer).GetInfo(ctx, req.(*emptypb.Empty))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _GuestService_GetEvents_Handler(srv interface{}, stream grpc.ServerStream) error {\n\tm := new(emptypb.Empty)\n\tif err := stream.RecvMsg(m); err != nil {\n\t\treturn err\n\t}\n\treturn srv.(GuestServiceServer).GetEvents(m, &grpc.GenericServerStream[emptypb.Empty, Event]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype GuestService_GetEventsServer = grpc.ServerStreamingServer[Event]\n\nfunc _GuestService_PostInotify_Handler(srv interface{}, stream grpc.ServerStream) error {\n\treturn srv.(GuestServiceServer).PostInotify(&grpc.GenericServerStream[Inotify, emptypb.Empty]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype GuestService_PostInotifyServer = grpc.ClientStreamingServer[Inotify, emptypb.Empty]\n\nfunc _GuestService_Tunnel_Handler(srv interface{}, stream grpc.ServerStream) error {\n\treturn srv.(GuestServiceServer).Tunnel(&grpc.GenericServerStream[TunnelMessage, TunnelMessage]{ServerStream: stream})\n}\n\n// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.\ntype GuestService_TunnelServer = grpc.BidiStreamingServer[TunnelMessage, TunnelMessage]\n\nfunc _GuestService_SyncTime_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(TimeSyncRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(GuestServiceServer).SyncTime(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: GuestService_SyncTime_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(GuestServiceServer).SyncTime(ctx, req.(*TimeSyncRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// GuestService_ServiceDesc is the grpc.ServiceDesc for GuestService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar GuestService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"GuestService\",\n\tHandlerType: (*GuestServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"GetInfo\",\n\t\t\tHandler:    _GuestService_GetInfo_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"SyncTime\",\n\t\t\tHandler:    _GuestService_SyncTime_Handler,\n\t\t},\n\t},\n\tStreams: []grpc.StreamDesc{\n\t\t{\n\t\t\tStreamName:    \"GetEvents\",\n\t\t\tHandler:       _GuestService_GetEvents_Handler,\n\t\t\tServerStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"PostInotify\",\n\t\t\tHandler:       _GuestService_PostInotify_Handler,\n\t\t\tClientStreams: true,\n\t\t},\n\t\t{\n\t\t\tStreamName:    \"Tunnel\",\n\t\t\tHandler:       _GuestService_Tunnel_Handler,\n\t\t\tServerStreams: true,\n\t\t\tClientStreams: true,\n\t\t},\n\t},\n\tMetadata: \"guestservice.proto\",\n}\n"
  },
  {
    "path": "pkg/guestagent/api/ipport.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage api\n\nimport (\n\t\"net\"\n\t\"strconv\"\n)\n\nfunc (x *IPPort) HostString() string {\n\treturn net.JoinHostPort(x.Ip, strconv.Itoa(int(x.Port)))\n}\n"
  },
  {
    "path": "pkg/guestagent/api/server/server.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/keepalive\"\n\t\"google.golang.org/protobuf/types/known/emptypb\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/timesync\"\n\t\"github.com/lima-vm/lima/v2/pkg/portfwdserver\"\n)\n\nfunc StartServer(ctx context.Context, lis net.Listener, guest *GuestServer) error {\n\tserver := grpc.NewServer(\n\t\tgrpc.InitialWindowSize(512<<20),\n\t\tgrpc.InitialConnWindowSize(512<<20),\n\t\tgrpc.ReadBufferSize(8<<20),\n\t\tgrpc.WriteBufferSize(8<<20),\n\t\tgrpc.MaxConcurrentStreams(2048),\n\t\tgrpc.KeepaliveParams(keepalive.ServerParameters{Time: 0, Timeout: 0, MaxConnectionIdle: 0}),\n\t)\n\tapi.RegisterGuestServiceServer(server, guest)\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tlogrus.Debug(\"Stopping the gRPC server\")\n\t\tserver.GracefulStop()\n\t\tlogrus.Debug(\"Closing the listener used by the gRPC server\")\n\t\tlis.Close()\n\t}()\n\terr := server.Serve(lis)\n\t// grpc.Server.Serve() expects to return a non-nil error caused by lis.Accept()\n\tif opErr, ok := err.(*net.OpError); ok && opErr.Err.Error() == \"use of closed network connection\" {\n\t\treturn nil\n\t}\n\treturn err\n}\n\ntype GuestServer struct {\n\tapi.UnimplementedGuestServiceServer\n\tAgent   guestagent.Agent\n\tTunnelS *portfwdserver.TunnelServer\n}\n\nfunc (s *GuestServer) GetInfo(ctx context.Context, _ *emptypb.Empty) (*api.Info, error) {\n\treturn s.Agent.Info(ctx)\n}\n\nfunc (s *GuestServer) GetEvents(_ *emptypb.Empty, stream api.GuestService_GetEventsServer) error {\n\tresponses := make(chan *api.Event)\n\t// expects Events() to close the channel when stream.Context() is done or ticker stops\n\tgo s.Agent.Events(stream.Context(), responses)\n\tfor response := range responses {\n\t\terr := stream.Send(response)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *GuestServer) PostInotify(server api.GuestService_PostInotifyServer) error {\n\tfor {\n\t\trecv, err := server.Recv()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.Agent.HandleInotify(recv)\n\t}\n}\n\nfunc (s *GuestServer) Tunnel(stream api.GuestService_TunnelServer) error {\n\treturn s.TunnelS.Start(stream)\n}\n\nfunc (s *GuestServer) SyncTime(_ context.Context, req *api.TimeSyncRequest) (*api.TimeSyncResponse, error) {\n\thostTime := req.HostTime.AsTime()\n\tnow := time.Now()\n\tdrift := now.Sub(hostTime)\n\n\tresp := &api.TimeSyncResponse{\n\t\tAdjusted: false,\n\t\tDriftMs:  drift.Milliseconds(),\n\t}\n\n\tconst driftThreshold = 100 * time.Millisecond\n\tif drift > driftThreshold || drift < -driftThreshold {\n\t\tif err := timesync.SetSystemTime(hostTime); err != nil {\n\t\t\tlogrus.WithError(err).Warn(\"SyncTime: failed to set system time\")\n\t\t\tresp.Error = err.Error()\n\t\t\treturn resp, nil\n\t\t}\n\t\tresp.Adjusted = true\n\t\tlogrus.Infof(\"SyncTime: system time synchronized with host (drift was %v)\", drift)\n\t} else {\n\t\tlogrus.Debugf(\"SyncTime: drift %v within threshold, no adjustment needed\", drift)\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "pkg/guestagent/fakecloudinit/fakecloudinit_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package fakecloudinit is a fake cloud-init implementation for macOS.\n//\n// TODO: support other OS that does not have cloud-init.\npackage fakecloudinit\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/sethvargo/go-password/password\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/cidata/cloudinittypes\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\nfunc Run(ctx context.Context) error {\n\tconst mnt = \"/Volumes/cidata\"\n\tvar errs []error\n\tif err := enableSSHD(ctx); err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"failed to enable SSHD: %w\", err))\n\t}\n\tif err := processMetaData(ctx, mnt); err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"failed to process meta data: %w\", err))\n\t}\n\tif err := processUserData(ctx, mnt); err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"failed to process user data: %w\", err))\n\t}\n\tif err := runBootScripts(ctx); err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"failed to run boot scripts: %w\", err))\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc enableSSHD(ctx context.Context) error {\n\tcmd := exec.CommandContext(ctx, \"/bin/launchctl\", \"load\", \"-w\", \"/System/Library/LaunchDaemons/ssh.plist\")\n\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute command %v: %w (output=%q)\", cmd.Args, err, output)\n\t}\n\treturn nil\n}\n\nfunc processMetaData(ctx context.Context, mnt string) error {\n\tmetaDataPath := filepath.Join(mnt, \"meta-data\")\n\tmetaDataB, err := os.ReadFile(metaDataPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read meta data file %q: %w\", metaDataPath, err)\n\t}\n\tvar metaData cloudinittypes.MetaData\n\tif err = yaml.Unmarshal(metaDataB, &metaData); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal meta data YAML: %w\", err)\n\t}\n\n\tvar errs []error\n\tlogrus.Infof(\"Instance ID: %q\", metaData.InstanceID)\n\tif metaData.LocalHostname != \"\" {\n\t\tif err = setLocalHostname(ctx, metaData.LocalHostname); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to set local hostname: %w\", err))\n\t\t}\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc setLocalHostname(ctx context.Context, hostname string) error {\n\tcmd := exec.CommandContext(ctx, \"scutil\", \"--set\", \"LocalHostName\", hostname)\n\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute command %v: %w (output=%q)\", cmd.Args, err, output)\n\t}\n\treturn nil\n}\n\nfunc processUserData(ctx context.Context, mnt string) error {\n\tuserDataPath := filepath.Join(mnt, \"user-data\")\n\tuserDataB, err := os.ReadFile(userDataPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read user data file %q: %w\", userDataPath, err)\n\t}\n\tvar userData cloudinittypes.UserData\n\tif err = yaml.Unmarshal(userDataB, &userData); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal user data YAML: %w\", err)\n\t}\n\n\tvar errs []error\n\tif userData.Growpart != nil {\n\t\tlogrus.Warn(\"growpart is not implemented\")\n\t}\n\tif userData.PackageUpdate {\n\t\tlogrus.Warn(\"package_update is not implemented\")\n\t}\n\tif userData.PackageUpgrade {\n\t\tlogrus.Warn(\"package_upgrade is not implemented\")\n\t}\n\tif userData.PackageRebootIfRequired {\n\t\tlogrus.Warn(\"package_reboot_if_required is not implemented\")\n\t}\n\tfor _, m := range userData.Mounts {\n\t\tif err = mountFSTabEntry(m); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to mount fstab entry %v: %w\", m, err))\n\t\t}\n\t}\n\tif userData.Timezone != \"\" {\n\t\tif err = setTimezone(ctx, userData.Timezone); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to set timezone: %w\", err))\n\t\t}\n\t}\n\tfor _, u := range userData.Users {\n\t\tif err := createUser(ctx, &u); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to create user %q: %w\", u.Name, err))\n\t\t}\n\t}\n\tfor _, entry := range userData.WriteFiles {\n\t\tif err := writeFiles(ctx, entry); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to write file for path %q: %w\", entry.Path, err))\n\t\t}\n\t}\n\tif userData.ManageResolvConf && userData.ResolvConf != nil {\n\t\tif err = setResolvConf(ctx, userData.ResolvConf); err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"failed to apply DNS configuration: %w\", err))\n\t\t}\n\t}\n\tif userData.CACerts != nil {\n\t\tlogrus.Warn(\"ca_certs is not implemented\")\n\t}\n\tif len(userData.BootCmd) > 0 {\n\t\tlogrus.Warn(\"bootcmd is not implemented\")\n\t}\n\treturn errors.Join(errs...)\n}\n\n// mountFSTabEntry mounts a filesystem based on the given fstab entry.\n// The format mimics Linux's convention.\n// The entries are not written to /etc/fstab.\nfunc mountFSTabEntry(m []string) error {\n\tif len(m) != 6 {\n\t\treturn fmt.Errorf(\"invalid fstab entry: expected 6 fields, got %d: %v\", len(m), m)\n\t}\n\tsrc, dst, fsType := m[0], m[1], m[2]\n\tswitch fsType {\n\tcase \"virtiofs\":\n\t\treturn mountVirtiofs(src, dst)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported filesystem type %q for fstab entry: %v\", fsType, m)\n\t}\n}\n\n// mountVirtiofs symlinks `/Volumes/My Shared Files/<pseudoTag>` (automatically mounted by macOS) to dir.\n// dir must not exist, or, must be a symlink to the expected source.\nfunc mountVirtiofs(pseudoTag, dir string) error {\n\tif strings.Contains(pseudoTag, string(filepath.Separator)) {\n\t\treturn fmt.Errorf(\"invalid pseudo tag for virtiofs: %q\", pseudoTag)\n\t}\n\tlnSrc := filepath.Join(\"/Volumes/My Shared Files\", pseudoTag)\n\n\t// FIXME: verify that the filesystem of lnSrc is indeed read-only when user-data contains the \"ro\" option.\n\t// unix.Statfs() could be used, but unix.Statfs_t.Flags & unix.MNT_RDONLY seems always 0 for virtiofs.\n\t// `mount -v` does not show \"ro\" flag either.\n\n\tlnExisting, err := os.Readlink(dir)\n\tif err == nil {\n\t\tif lnExisting == lnSrc {\n\t\t\treturn nil // already symlinked\n\t\t}\n\t\treturn fmt.Errorf(\"unexpected symlink target for virtiofs mount source %q: expected %q, got %q\", lnSrc, lnSrc, lnExisting)\n\t}\n\tif !errors.Is(err, os.ErrNotExist) {\n\t\tlogrus.WithError(err).Warnf(\"Failed to read existing symlink for virtiofs mount source %q\", lnSrc)\n\t}\n\tif err := os.Symlink(lnSrc, dir); err != nil {\n\t\treturn fmt.Errorf(\"failed to create symlink from %q to %q: %w\", lnSrc, dir, err)\n\t}\n\treturn nil\n}\n\nfunc setTimezone(ctx context.Context, timezone string) error {\n\tcmd := exec.CommandContext(ctx, \"systemsetup\", \"-settimezone\", timezone)\n\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute command %v: %w (output=%q)\", cmd.Args, err, output)\n\t}\n\treturn nil\n}\n\nfunc generatePassword() (string, error) {\n\tconst pwLen = 16\n\t// Avoid special characters to minimize potential keyboard layout issue in GUI\n\tpw, err := password.Generate(pwLen, pwLen/4, 0, false, false)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to generate password: %w\", err)\n\t}\n\treturn pw, nil\n}\n\nfunc populateHomeDir(ctx context.Context, uid int, homedir string) error {\n\tcmds := [][]string{\n\t\t{\"ditto\", \"--noqtn\", \"/System/Library/User Template/English.lproj\", homedir},\n\t\t{\"chown\", \"-R\", fmt.Sprintf(\"%d:staff\", uid), homedir},\n\t\t{\"chmod\", \"700\", homedir},\n\t}\n\tfor _, args := range cmds {\n\t\tcmd := exec.CommandContext(ctx, args[0], args[1:]...)\n\t\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\t\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute command %v: %w (output=%q)\", cmd.Args, err, output)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc createUser(ctx context.Context, u *cloudinittypes.User) error {\n\thomedir := u.Homedir\n\tif homedir == \"\" {\n\t\treturn fmt.Errorf(\"homedir is required for user %q\", u.Name)\n\t}\n\tif osutil.FileExists(homedir) {\n\t\tlogrus.Debugf(\"homedir %q already exists, skipping user creation for user %q\", homedir, u.Name)\n\t\treturn nil\n\t}\n\tif u.UID == \"\" {\n\t\treturn fmt.Errorf(\"uid is required for user %q\", u.Name)\n\t}\n\tuid, err := strconv.Atoi(u.UID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid uid %q for user %q: %w\", u.UID, u.Name, err)\n\t}\n\tpw, err := generatePassword()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate password for user %q: %w\", u.Name, err)\n\t}\n\targs := []string{\n\t\t\"-addUser\", u.Name,\n\t\t\"-UID\", u.UID,\n\t\t\"-password\", \"-\",\n\t\t\"-home\", homedir,\n\t\t\"-admin\",\n\t}\n\tif u.Gecos != \"\" {\n\t\targs = append(args, \"-fullName\", u.Gecos)\n\t}\n\tif u.Shell != \"\" {\n\t\targs = append(args, \"-shell\", u.Shell)\n\t}\n\tif u.LockPasswd != \"\" {\n\t\tlogrus.Warn(\"lock_passwd field is not implemented\")\n\t}\n\tcmd := exec.CommandContext(ctx, \"/usr/sbin/sysadminctl\", args...)\n\tcmd.Stdin = strings.NewReader(pw)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\tif err = cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute command %v: %w\", cmd.Args, err)\n\t}\n\n\t// sysadminctl does not create the custom home directory\n\tif err = populateHomeDir(ctx, uid, homedir); err != nil {\n\t\treturn fmt.Errorf(\"failed to populate home directory for user %q: %w\", u.Name, err)\n\t}\n\n\tcmd = exec.CommandContext(ctx, \"chmod\", \"700\", homedir)\n\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute command %v: %w (output=%q)\", cmd.Args, err, output)\n\t}\n\n\tpwPath := filepath.Join(homedir, \"password\")\n\tif err = os.WriteFile(pwPath, []byte(pw+\"\\n\"), 0o400); err != nil {\n\t\treturn fmt.Errorf(\"failed to write password file for user %q: %w\", u.Name, err)\n\t}\n\tlogrus.Infof(\"Created user %q. The password is stored in %q\", u.Name, pwPath)\n\n\tdotSSHPath := filepath.Join(homedir, \".ssh\")\n\tif err = os.MkdirAll(dotSSHPath, 0o700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create .ssh directory for user %q: %w\", u.Name, err)\n\t}\n\tauthKeysPath := filepath.Join(dotSSHPath, \"authorized_keys\")\n\tauthKeysContent := strings.Join(u.SSHAuthorizedKeys, \"\\n\")\n\tif err = os.WriteFile(authKeysPath, []byte(authKeysContent), 0o600); err != nil {\n\t\treturn fmt.Errorf(\"failed to write authorized_keys file for user %q: %w\", u.Name, err)\n\t}\n\tfor _, f := range []string{pwPath, dotSSHPath, authKeysPath} {\n\t\tif err = os.Chown(f, uid, -1); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to chown %q for user %q: %w\", f, u.Name, err)\n\t\t}\n\t}\n\tif u.Sudo != \"\" {\n\t\tif err := writeSudoers(u.Name, u.Sudo); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write sudoers file for user %q: %w\", u.Name, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// writeSudoers appends a sudoers entry for the given user.\n// writeSudoers is expected be called only once on creating the user account.\nfunc writeSudoers(userName, sudo string) error {\n\tif strings.Contains(sudo, \"\\n\") {\n\t\treturn errors.New(\"sudo field must not contain newline characters\")\n\t}\n\tif err := os.MkdirAll(\"/etc/sudoers.d\", 0o700); err != nil {\n\t\treturn fmt.Errorf(\"failed to create /etc/sudoers.d directory: %w\", err)\n\t}\n\tsudoersPath := \"/etc/sudoers.d/90-cloud-init-users\"\n\tf, err := os.OpenFile(sudoersPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o400)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open sudoers file %q: %w\", sudoersPath, err)\n\t}\n\tif _, err = fmt.Fprintf(f, \"%s %s\\n\", userName, sudo); err != nil {\n\t\t_ = f.Close()\n\t\treturn fmt.Errorf(\"failed to write to sudoers file %q for user %q: %w\", sudoersPath, userName, err)\n\t}\n\treturn f.Close()\n}\n\nfunc writeFiles(ctx context.Context, entry cloudinittypes.WriteFile) error {\n\tif entry.Path == \"\" {\n\t\treturn errors.New(\"path is required for write_files entry\")\n\t}\n\tperm := os.FileMode(0o644)\n\tif entry.Permissions != \"\" {\n\t\tp, err := strconv.ParseUint(entry.Permissions, 8, 32)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid permissions %q for path %q: %w\", entry.Permissions, entry.Path, err)\n\t\t}\n\t\tperm = os.FileMode(p)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(entry.Path), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to create parent directory for path %q: %w\", entry.Path, err)\n\t}\n\tif err := os.WriteFile(entry.Path, []byte(entry.Content), perm); err != nil {\n\t\treturn fmt.Errorf(\"failed to write file for path %q: %w\", entry.Path, err)\n\t}\n\tif entry.Owner != \"\" {\n\t\tcmd := exec.CommandContext(ctx, \"chown\", entry.Owner, entry.Path)\n\t\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\t\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to execute command %v: %w (output=%q)\", cmd.Args, err, output)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc setResolvConf(ctx context.Context, resolvConf *cloudinittypes.ResolvConf) error {\n\t// FIXME: avoid hardcoding the primary network name\n\tconst primaryNetwork = \"Ethernet\"\n\tcmd := exec.CommandContext(ctx, \"networksetup\", append([]string{\"-setdnsservers\", primaryNetwork}, resolvConf.Nameservers...)...)\n\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to execute command %v: %w (output=%q)\", cmd.Args, err, output)\n\t}\n\treturn nil\n}\n\nfunc runBootScripts(ctx context.Context) error {\n\tdirs := []string{\n\t\t\"/var/lib/cloud/scripts/per-boot\",\n\t}\n\tvar errs []error\n\tfor _, dir := range dirs {\n\t\tdirEntries, err := os.ReadDir(dir)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read boot scripts directory %q: %w\", dir, err)\n\t\t}\n\t\tfor _, entry := range dirEntries {\n\t\t\tif entry.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tscriptPath := filepath.Join(dir, entry.Name())\n\t\t\t// entry.Type().Mode() does not seem to contain permission bits\n\t\t\tentryInfo, err := entry.Info()\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Warnf(\"Skipping boot script %q due to stat error: %v\", scriptPath, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif entryInfo.Mode().Perm()&0o111 == 0 {\n\t\t\t\tlogrus.Warnf(\"Skipping non-executable boot script %q (%v)\", scriptPath, entryInfo.Mode().Perm())\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcmd := exec.CommandContext(ctx, scriptPath)\n\t\t\tcmd.Stdout = os.Stdout\n\t\t\tcmd.Stderr = os.Stderr\n\t\t\tlogrus.Infof(\"Executing command: %v\", cmd.Args)\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to execute command %v: %w\", cmd.Args, err))\n\t\t\t}\n\t\t}\n\t}\n\treturn errors.Join(errs...)\n}\n"
  },
  {
    "path": "pkg/guestagent/guestagent.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage guestagent\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n)\n\ntype Agent interface {\n\tInfo(ctx context.Context) (*api.Info, error)\n\tEvents(ctx context.Context, ch chan *api.Event)\n\tLocalPorts(ctx context.Context) ([]*api.IPPort, error)\n\tHandleInotify(event *api.Inotify)\n\tio.Closer\n}\n"
  },
  {
    "path": "pkg/guestagent/guestagent_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage guestagent\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/kubernetesservice\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/sockets\"\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/ticker\"\n)\n\nfunc New(ctx context.Context, ticker ticker.Ticker, runtimeDir string) (Agent, error) {\n\tsocketsLister, err := sockets.NewLister()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ta := &agent{\n\t\tticker:                   ticker,\n\t\tsocketLister:             socketsLister,\n\t\tkubernetesServiceWatcher: kubernetesservice.NewServiceWatcher(),\n\t\truntimeDir:               runtimeDir,\n\t}\n\n\tgo a.kubernetesServiceWatcher.Start(ctx)\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tlogrus.Debug(\"Closing the agent\")\n\t\tif err := a.Close(); err != nil {\n\t\t\tlogrus.Errorf(\"error on agent.Close(): %v\", err)\n\t\t}\n\t}()\n\n\treturn a, nil\n}\n\nvar _ Agent = (*agent)(nil)\n\ntype agent struct {\n\t// Ticker is like time.Ticker.\n\t// We can't use inotify for /proc/net/tcp, so we need this ticker to\n\t// reload /proc/net/tcp.\n\tticker                   ticker.Ticker\n\tsocketLister             *sockets.Lister\n\tkubernetesServiceWatcher *kubernetesservice.ServiceWatcher\n\truntimeDir               string\n}\n\ntype eventState struct {\n\tPorts []*api.IPPort `json:\"ports,omitempty\"`\n}\n\nfunc comparePorts(old, neww []*api.IPPort) (added, removed []*api.IPPort) {\n\tmRaw := make(map[string]*api.IPPort, len(old))\n\tmStillExist := make(map[string]bool, len(old))\n\n\tfor _, f := range old {\n\t\tk := f.String()\n\t\tmRaw[k] = f\n\t\tmStillExist[k] = false\n\t}\n\tfor _, f := range neww {\n\t\tk := f.String()\n\t\tif _, ok := mRaw[k]; !ok {\n\t\t\tadded = append(added, f)\n\t\t}\n\t\tmStillExist[k] = true\n\t}\n\n\tfor k, stillExist := range mStillExist {\n\t\tif !stillExist {\n\t\t\tif x, ok := mRaw[k]; ok {\n\t\t\t\tremoved = append(removed, x)\n\t\t\t}\n\t\t}\n\t}\n\treturn added, removed\n}\n\nfunc (a *agent) collectEvent(ctx context.Context, st eventState) (*api.Event, eventState) {\n\tvar (\n\t\tev  = &api.Event{}\n\t\terr error\n\t)\n\tnewSt := st\n\tnewSt.Ports, err = a.LocalPorts(ctx)\n\tif err != nil {\n\t\tev.Errors = append(ev.Errors, err.Error())\n\t\tev.Time = timestamppb.Now()\n\t\treturn ev, newSt\n\t}\n\tev.AddedLocalPorts, ev.RemovedLocalPorts = comparePorts(st.Ports, newSt.Ports)\n\tev.Time = timestamppb.Now()\n\treturn ev, newSt\n}\n\nfunc isEventEmpty(ev *api.Event) bool {\n\tempty := &api.Event{}\n\tempty.Time = ev.Time\n\treturn reflect.DeepEqual(empty, ev)\n}\n\nfunc (a *agent) Events(ctx context.Context, ch chan *api.Event) {\n\tdefer close(ch)\n\ttickerCh := a.ticker.Chan()\n\n\tst, err := a.LoadEventState()\n\tif err != nil {\n\t\tlogrus.Errorf(\"failed to load state: %v\", err)\n\t}\n\tdefer func() {\n\t\tif err := a.SaveEventState(st); err != nil {\n\t\t\tlogrus.Errorf(\"failed to save state: %v\", err)\n\t\t}\n\t}()\n\tfor {\n\t\tvar ev *api.Event\n\t\tev, st = a.collectEvent(ctx, st)\n\t\tif !isEventEmpty(ev) {\n\t\t\tch <- ev\n\t\t}\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase _, ok := <-tickerCh:\n\t\t\tif !ok {\n\t\t\t\tlogrus.Debug(\"ticker channel closed\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlogrus.Debug(\"tick!\")\n\t\t}\n\t}\n}\n\nfunc (a *agent) LocalPorts(_ context.Context) ([]*api.IPPort, error) {\n\tvar res []*api.IPPort\n\tsocketsList, err := a.socketLister.List()\n\tif err != nil {\n\t\treturn res, err\n\t}\n\n\tfor _, f := range socketsList {\n\t\tswitch f.Kind {\n\t\tcase sockets.TCP, sockets.TCP6:\n\t\t\tif f.State == sockets.TCPListen {\n\t\t\t\tres = append(res,\n\t\t\t\t\t&api.IPPort{\n\t\t\t\t\t\tIp:       f.IP.String(),\n\t\t\t\t\t\tPort:     int32(f.Port),\n\t\t\t\t\t\tProtocol: \"tcp\",\n\t\t\t\t\t})\n\t\t\t}\n\t\tcase sockets.UDP, sockets.UDP6:\n\t\t\tif f.State == sockets.UDPUnconnected {\n\t\t\t\tres = append(res,\n\t\t\t\t\t&api.IPPort{\n\t\t\t\t\t\tIp:       f.IP.String(),\n\t\t\t\t\t\tPort:     int32(f.Port),\n\t\t\t\t\t\tProtocol: \"udp\",\n\t\t\t\t\t})\n\t\t\t}\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t}\n\n\tkubernetesEntries := a.kubernetesServiceWatcher.GetPorts()\n\tfor _, entry := range kubernetesEntries {\n\t\tfound := false\n\t\tfor _, re := range res {\n\t\t\tif re.Port == int32(entry.Port) {\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\tres = append(res,\n\t\t\t\t&api.IPPort{\n\t\t\t\t\tIp:       entry.IP.String(),\n\t\t\t\t\tPort:     int32(entry.Port),\n\t\t\t\t\tProtocol: string(entry.Protocol),\n\t\t\t\t})\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\nfunc (a *agent) Info(ctx context.Context) (*api.Info, error) {\n\tvar (\n\t\tinfo api.Info\n\t\terr  error\n\t)\n\tinfo.LocalPorts, err = a.LocalPorts(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &info, nil\n}\n\nfunc (a *agent) HandleInotify(event *api.Inotify) {\n\tlocation := event.MountPath\n\tif _, err := os.Stat(location); err == nil {\n\t\tlocal := event.Time.AsTime().Local()\n\t\terr := os.Chtimes(location, local, local)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"error in inotify handle. Event: %s, Error: %s\", event, err)\n\t\t}\n\t}\n}\n\nfunc (a *agent) Close() error {\n\tif a.socketLister != nil {\n\t\tif err := a.socketLister.Close(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\ta.ticker.Stop()\n\treturn nil\n}\n\nconst eventStateFileName = \"event-state.json\"\n\n// LoadEventState loads the event state from a file in JSON format.\n// If the file does not exist, it returns an empty eventState with no error.\n// The saved eventState is expected to be removed on OS restart.\nfunc (a *agent) LoadEventState() (eventState, error) {\n\tlogrus.Debug(\"Loading event state\")\n\tpath := filepath.Join(a.runtimeDir, eventStateFileName)\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn eventState{}, nil\n\t\t}\n\t\treturn eventState{}, err\n\t}\n\tvar st eventState\n\tif err := json.Unmarshal(data, &st); err != nil {\n\t\treturn eventState{}, err\n\t}\n\t// We don't remove the file after loading for debugging purposes.\n\treturn st, nil\n}\n\n// SaveEventState saves the event state to a file in JSON format.\n// It overwrites the file if it already exists.\n// The saved eventState is expected to be removed on OS restart.\nfunc (a *agent) SaveEventState(st eventState) error {\n\tlogrus.Debug(\"Saving event state\")\n\tdata, err := json.Marshal(st)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpath := filepath.Join(a.runtimeDir, eventStateFileName)\n\treturn os.WriteFile(path, data, 0o644)\n}\n"
  },
  {
    "path": "pkg/guestagent/kubernetesservice/kubernetesservice.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage kubernetesservice\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/sirupsen/logrus\"\n)\n\ntype Protocol string\n\nconst (\n\tTCP Protocol = \"tcp\"\n\tUDP Protocol = \"udp\"\n)\n\ntype Entry struct {\n\tProtocol Protocol\n\tIP       net.IP\n\tPort     uint16\n}\n\ntype ServiceWatcher struct {\n\trwMutex sync.RWMutex\n\t// key: namespace/name\n\tserviceSpecs map[string]*serviceSpec\n}\n\nfunc NewServiceWatcher() *ServiceWatcher {\n\treturn &ServiceWatcher{serviceSpecs: make(map[string]*serviceSpec)}\n}\n\nfunc (s *ServiceWatcher) Start(ctx context.Context) {\n\tlogrus.Info(\"Monitoring kubernetes services\")\n\tgo s.loopAttemptToStartKubectl(ctx)\n}\n\nfunc (s *ServiceWatcher) loopAttemptToStartKubectl(ctx context.Context) {\n\tconst retryInterval = 10 * time.Second\n\n\tfor i := 0; ; i++ {\n\t\t// The first iteration does not need to wait for retryInterval.\n\t\tif i > 0 {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase <-time.After(retryInterval):\n\t\t\t}\n\t\t}\n\t\ts.attemptToStartKubectl(ctx)\n\t}\n}\n\nfunc (s *ServiceWatcher) attemptToStartKubectl(ctx context.Context) {\n\tkubectl, err := exec.LookPath(\"kubectl\")\n\tif err != nil {\n\t\tlogrus.WithError(err).Debugf(\"kubectl not available; will retry\")\n\t\treturn\n\t}\n\tkubeconfig := chooseKubeconfig()\n\t// TODO: ensure that kubeconfig points to a local cluster\n\tif err := canGetServices(ctx, kubectl, kubeconfig); err != nil {\n\t\tlogrus.WithError(err).Debugf(\"kubectl auth can-i ... failed; will retry\")\n\t\treturn\n\t}\n\n\tcmd := exec.CommandContext(ctx, kubectl,\n\t\t\"get\", \"--all-namespaces\", \"service\", \"--watch\", \"--output-watch-events\", \"--output\", \"json\")\n\tif kubeconfig != \"\" {\n\t\tcmd.Env = append(os.Environ(), \"KUBECONFIG=\"+kubeconfig)\n\t}\n\tif err := s.startAndStreamKubectl(cmd); err != nil {\n\t\tlogrus.WithError(err).Warn(\"kubectl watch failed; will retry\")\n\t}\n}\n\nfunc chooseKubeconfig() string {\n\tif kubeconfig := os.Getenv(\"KUBECONFIG\"); kubeconfig != \"\" {\n\t\treturn kubeconfig\n\t}\n\tcandidateKubeConfigs := []string{\n\t\t\"/etc/rancher/k3s/k3s.yaml\",\n\t\t\"/root/.kube/config.localhost\", // Created by template:k8s\n\t\t\"/root/.kube/config\",\n\t}\n\tfor _, kc := range candidateKubeConfigs {\n\t\tif _, err := os.Stat(kc); !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn kc\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc canGetServices(ctx context.Context, kubectl, kubeconfig string) error {\n\tcmd := exec.CommandContext(ctx, kubectl, \"auth\", \"can-i\", \"get\", \"service\")\n\tif kubeconfig != \"\" {\n\t\tcmd.Env = append(os.Environ(), \"KUBECONFIG=\"+kubeconfig)\n\t}\n\tvar stdout, stderr bytes.Buffer\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w; stdout=%q, stderr=%q\", cmd.Args, err, stdout.String(), stderr.String())\n\t}\n\tif strings.TrimSpace(stdout.String()) != \"yes\" {\n\t\treturn fmt.Errorf(\"failed to run %v: expected \\\"yes\\\", got %q\", cmd.Args, stdout.String())\n\t}\n\treturn nil\n}\n\nfunc (s *ServiceWatcher) startAndStreamKubectl(cmd *exec.Cmd) error {\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar stderr bytes.Buffer\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w; stderr=%q\", cmd.Args, err, stderr.String())\n\t}\n\n\treadErr := s.readKubectlStream(stdout)\n\twaitErr := cmd.Wait()\n\tif waitErr != nil {\n\t\twaitErr = fmt.Errorf(\"failed to run %v: %w; stderr=%q\", cmd.Args, waitErr, stderr.String())\n\t}\n\treturn errors.Join(readErr, waitErr)\n}\n\n// readKubectlStream reads kubectl JSON watch events from r and updates the internal\n// services map. The stream is newline-delimited JSON objects representing \"WatchEvent\".\nfunc (s *ServiceWatcher) readKubectlStream(r io.Reader) error {\n\tscanner := bufio.NewScanner(r)\n\t// increase buffer in case of large JSON objects\n\tconst maxBuf = 10 * units.MiB\n\tbuf := make([]byte, 0, 64*units.KiB)\n\tscanner.Buffer(buf, maxBuf)\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Bytes()\n\t\tline = bytes.TrimSpace(line)\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar ev struct {\n\t\t\tType   eventType       `json:\"type\"`\n\t\t\tObject json.RawMessage `json:\"object\"`\n\t\t}\n\t\tif err := json.Unmarshal(line, &ev); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unmarshal line %q: %w\", string(line), err)\n\t\t}\n\n\t\tvar svc service\n\t\tif err := json.Unmarshal(ev.Object, &svc); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unmarshal service object: %w (line=%q)\", err, line)\n\t\t}\n\n\t\tkey := svc.Metadata.Namespace + \"/\" + svc.Metadata.Name\n\t\ts.rwMutex.Lock()\n\t\tswitch ev.Type {\n\t\tcase added, modified:\n\t\t\ts.serviceSpecs[key] = &svc.Spec\n\t\tcase deleted:\n\t\t\tdelete(s.serviceSpecs, key)\n\t\tdefault:\n\t\t\t// NOP\n\t\t}\n\t\ts.rwMutex.Unlock()\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\treturn fmt.Errorf(\"failed to scan kubectl event stream: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (s *ServiceWatcher) GetPorts() []Entry {\n\ts.rwMutex.RLock()\n\tdefer s.rwMutex.RUnlock()\n\n\tvar entries []Entry\n\tfor key, spec := range s.serviceSpecs {\n\t\tif spec.Type != serviceTypeNodePort &&\n\t\t\tspec.Type != serviceTypeLoadBalancer {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, portEntry := range spec.Ports {\n\t\t\tswitch portEntry.Protocol {\n\t\t\tcase protocolTCP, protocolUDP:\n\t\t\t\t// NOP\n\t\t\tdefault:\n\t\t\t\tlogrus.Debugf(\"unsupported protocol %s for service %q, skipping\",\n\t\t\t\t\tportEntry.Protocol, key)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar port int32\n\t\t\tswitch spec.Type {\n\t\t\tcase serviceTypeNodePort:\n\t\t\t\tport = portEntry.NodePort\n\t\t\tcase serviceTypeLoadBalancer:\n\t\t\t\tport = portEntry.Port\n\t\t\t}\n\n\t\t\tentries = append(entries, Entry{\n\t\t\t\tProtocol: Protocol(strings.ToLower(string(portEntry.Protocol))),\n\t\t\t\tIP:       net.IPv4zero,\n\t\t\t\tPort:     uint16(port),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn entries\n}\n"
  },
  {
    "path": "pkg/guestagent/kubernetesservice/kubernetesservice_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage kubernetesservice\n\nimport (\n\t\"net\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestGetPorts(t *testing.T) {\n\tserviceWatcher := NewServiceWatcher()\n\n\ttype testCase struct {\n\t\tname    string\n\t\tservice service\n\t\twant    []Entry\n\t}\n\tcases := []testCase{\n\t\t{\n\t\t\tname: \"nodePort service\",\n\t\t\tservice: service{\n\t\t\t\tMetadata: objectMeta{Name: \"nodeport\"},\n\t\t\t\tSpec: serviceSpec{\n\t\t\t\t\tType: serviceTypeNodePort,\n\t\t\t\t\tPorts: []servicePort{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:       \"http\",\n\t\t\t\t\t\t\tProtocol:   protocolTCP,\n\t\t\t\t\t\t\tPort:       80,\n\t\t\t\t\t\t\tTargetPort: 80,\n\t\t\t\t\t\t\tNodePort:   8080,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:       \"dns\",\n\t\t\t\t\t\t\tProtocol:   protocolUDP,\n\t\t\t\t\t\t\tPort:       53,\n\t\t\t\t\t\t\tTargetPort: 53,\n\t\t\t\t\t\t\tNodePort:   5353,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []Entry{{\n\t\t\t\tProtocol: TCP,\n\t\t\t\tIP:       net.IPv4zero,\n\t\t\t\tPort:     8080,\n\t\t\t}, {\n\t\t\t\tProtocol: UDP,\n\t\t\t\tIP:       net.IPv4zero,\n\t\t\t\tPort:     5353,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"loadBalancer service\",\n\t\t\tservice: service{\n\t\t\t\tMetadata: objectMeta{Name: \"loadbalancer\"},\n\t\t\t\tSpec: serviceSpec{\n\t\t\t\t\tType: serviceTypeLoadBalancer,\n\t\t\t\t\tPorts: []servicePort{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:       \"http\",\n\t\t\t\t\t\t\tProtocol:   protocolTCP,\n\t\t\t\t\t\t\tPort:       8081,\n\t\t\t\t\t\t\tTargetPort: 80,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: []Entry{{\n\t\t\t\tProtocol: TCP,\n\t\t\t\tIP:       net.IPv4zero,\n\t\t\t\tPort:     8081,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname: \"clusterIP service\",\n\t\t\tservice: service{\n\t\t\t\tMetadata: objectMeta{Name: \"clusterip\"},\n\t\t\t\tSpec: serviceSpec{\n\t\t\t\t\tType: \"ClusterIP\", // Explicit string or define constant if needed. Using literal as in original implicit check.\n\t\t\t\t\tPorts: []servicePort{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:       \"http\",\n\t\t\t\t\t\t\tProtocol:   protocolTCP,\n\t\t\t\t\t\t\tPort:       80,\n\t\t\t\t\t\t\tTargetPort: 80,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t}\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tconst ns = \"default\"\n\t\t\tservice := c.service\n\t\t\tservice.Metadata.Namespace = ns\n\t\t\tkey := ns + \"/\" + c.service.Metadata.Name\n\n\t\t\tserviceWatcher.rwMutex.Lock()\n\t\t\tserviceWatcher.serviceSpecs[key] = &service.Spec\n\t\t\tserviceWatcher.rwMutex.Unlock()\n\n\t\t\tgot := serviceWatcher.GetPorts()\n\t\t\tassert.DeepEqual(t, got, c.want)\n\n\t\t\tserviceWatcher.rwMutex.Lock()\n\t\t\tdelete(serviceWatcher.serviceSpecs, key)\n\t\t\tserviceWatcher.rwMutex.Unlock()\n\t\t})\n\t}\n}\n\nfunc TestReadKubectlStream(t *testing.T) {\n\tstream := `{\"type\":\"ADDED\",\"object\":{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"creationTimestamp\":\"2025-09-30T13:39:19Z\",\"labels\":{\"component\":\"apiserver\",\"provider\":\"kubernetes\"},\"managedFields\":[{\"apiVersion\":\"v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:labels\":{\".\":{},\"f:component\":{},\"f:provider\":{}}},\"f:spec\":{\"f:clusterIP\":{},\"f:internalTrafficPolicy\":{},\"f:ipFamilyPolicy\":{},\"f:ports\":{\".\":{},\"k:{\\\"port\\\":443,\\\"protocol\\\":\\\"TCP\\\"}\":{\".\":{},\"f:name\":{},\"f:port\":{},\"f:protocol\":{},\"f:targetPort\":{}}},\"f:sessionAffinity\":{},\"f:type\":{}}},\"manager\":\"kube-apiserver\",\"operation\":\"Update\",\"time\":\"2025-09-30T13:39:19Z\"}],\"name\":\"kubernetes\",\"namespace\":\"default\",\"resourceVersion\":\"201\",\"uid\":\"de36e2fb-3883-47f2-860a-c28dfd896bac\"},\"spec\":{\"clusterIP\":\"10.96.0.1\",\"clusterIPs\":[\"10.96.0.1\"],\"internalTrafficPolicy\":\"Cluster\",\"ipFamilies\":[\"IPv4\"],\"ipFamilyPolicy\":\"SingleStack\",\"ports\":[{\"name\":\"https\",\"port\":443,\"protocol\":\"TCP\",\"targetPort\":6443}],\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}}\n{\"type\":\"ADDED\",\"object\":{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{\"prometheus.io/port\":\"9153\",\"prometheus.io/scrape\":\"true\"},\"creationTimestamp\":\"2025-09-30T13:39:19Z\",\"labels\":{\"k8s-app\":\"kube-dns\",\"kubernetes.io/cluster-service\":\"true\",\"kubernetes.io/name\":\"CoreDNS\"},\"managedFields\":[{\"apiVersion\":\"v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:annotations\":{\".\":{},\"f:prometheus.io/port\":{},\"f:prometheus.io/scrape\":{}},\"f:labels\":{\".\":{},\"f:k8s-app\":{},\"f:kubernetes.io/cluster-service\":{},\"f:kubernetes.io/name\":{}}},\"f:spec\":{\"f:clusterIP\":{},\"f:internalTrafficPolicy\":{},\"f:ports\":{\".\":{},\"k:{\\\"port\\\":53,\\\"protocol\\\":\\\"TCP\\\"}\":{\".\":{},\"f:name\":{},\"f:port\":{},\"f:protocol\":{},\"f:targetPort\":{}},\"k:{\\\"port\\\":53,\\\"protocol\\\":\\\"UDP\\\"}\":{\".\":{},\"f:name\":{},\"f:port\":{},\"f:protocol\":{},\"f:targetPort\":{}},\"k:{\\\"port\\\":9153,\\\"protocol\\\":\\\"TCP\\\"}\":{\".\":{},\"f:name\":{},\"f:port\":{},\"f:protocol\":{},\"f:targetPort\":{}}},\"f:selector\":{},\"f:sessionAffinity\":{},\"f:type\":{}}},\"manager\":\"kubeadm\",\"operation\":\"Update\",\"time\":\"2025-09-30T13:39:19Z\"}],\"name\":\"kube-dns\",\"namespace\":\"kube-system\",\"resourceVersion\":\"240\",\"uid\":\"73817952-5f95-4a15-a6f9-64ba220a6933\"},\"spec\":{\"clusterIP\":\"10.96.0.10\",\"clusterIPs\":[\"10.96.0.10\"],\"internalTrafficPolicy\":\"Cluster\",\"ipFamilies\":[\"IPv4\"],\"ipFamilyPolicy\":\"SingleStack\",\"ports\":[{\"name\":\"dns\",\"port\":53,\"protocol\":\"UDP\",\"targetPort\":53},{\"name\":\"dns-tcp\",\"port\":53,\"protocol\":\"TCP\",\"targetPort\":53},{\"name\":\"metrics\",\"port\":9153,\"protocol\":\"TCP\",\"targetPort\":9153}],\"selector\":{\"k8s-app\":\"kube-dns\"},\"sessionAffinity\":\"None\",\"type\":\"ClusterIP\"},\"status\":{\"loadBalancer\":{}}}}\n{\"type\":\"ADDED\",\"object\":{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"creationTimestamp\":\"2025-10-01T09:20:14Z\",\"labels\":{\"app\":\"nginx\"},\"managedFields\":[{\"apiVersion\":\"v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:labels\":{\".\":{},\"f:app\":{}}},\"f:spec\":{\"f:externalTrafficPolicy\":{},\"f:internalTrafficPolicy\":{},\"f:ports\":{\".\":{},\"k:{\\\"port\\\":80,\\\"protocol\\\":\\\"TCP\\\"}\":{\".\":{},\"f:port\":{},\"f:protocol\":{},\"f:targetPort\":{}}},\"f:selector\":{},\"f:sessionAffinity\":{},\"f:type\":{}}},\"manager\":\"kubectl-expose\",\"operation\":\"Update\",\"time\":\"2025-10-01T09:20:14Z\"}],\"name\":\"nginx\",\"namespace\":\"default\",\"resourceVersion\":\"7178\",\"uid\":\"d994b815-78e4-4f42-887c-abd00ff78425\"},\"spec\":{\"clusterIP\":\"10.104.89.81\",\"clusterIPs\":[\"10.104.89.81\"],\"externalTrafficPolicy\":\"Cluster\",\"internalTrafficPolicy\":\"Cluster\",\"ipFamilies\":[\"IPv4\"],\"ipFamilyPolicy\":\"SingleStack\",\"ports\":[{\"nodePort\":30369,\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"nginx\"},\"sessionAffinity\":\"None\",\"type\":\"NodePort\"},\"status\":{\"loadBalancer\":{}}}}\n{\"type\":\"DELETED\",\"object\":{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"creationTimestamp\":\"2025-10-01T09:20:14Z\",\"labels\":{\"app\":\"nginx\"},\"managedFields\":[{\"apiVersion\":\"v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:labels\":{\".\":{},\"f:app\":{}}},\"f:spec\":{\"f:externalTrafficPolicy\":{},\"f:internalTrafficPolicy\":{},\"f:ports\":{\".\":{},\"k:{\\\"port\\\":80,\\\"protocol\\\":\\\"TCP\\\"}\":{\".\":{},\"f:port\":{},\"f:protocol\":{},\"f:targetPort\":{}}},\"f:selector\":{},\"f:sessionAffinity\":{},\"f:type\":{}}},\"manager\":\"kubectl-expose\",\"operation\":\"Update\",\"time\":\"2025-10-01T09:20:14Z\"}],\"name\":\"nginx\",\"namespace\":\"default\",\"resourceVersion\":\"8036\",\"uid\":\"d994b815-78e4-4f42-887c-abd00ff78425\"},\"spec\":{\"clusterIP\":\"10.104.89.81\",\"clusterIPs\":[\"10.104.89.81\"],\"externalTrafficPolicy\":\"Cluster\",\"internalTrafficPolicy\":\"Cluster\",\"ipFamilies\":[\"IPv4\"],\"ipFamilyPolicy\":\"SingleStack\",\"ports\":[{\"nodePort\":30369,\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"nginx\"},\"sessionAffinity\":\"None\",\"type\":\"NodePort\"},\"status\":{\"loadBalancer\":{}}}}\n{\"type\":\"ADDED\",\"object\":{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"creationTimestamp\":\"2025-10-01T09:37:23Z\",\"labels\":{\"app\":\"nginx\"},\"managedFields\":[{\"apiVersion\":\"v1\",\"fieldsType\":\"FieldsV1\",\"fieldsV1\":{\"f:metadata\":{\"f:labels\":{\".\":{},\"f:app\":{}}},\"f:spec\":{\"f:externalTrafficPolicy\":{},\"f:internalTrafficPolicy\":{},\"f:ports\":{\".\":{},\"k:{\\\"port\\\":80,\\\"protocol\\\":\\\"TCP\\\"}\":{\".\":{},\"f:port\":{},\"f:protocol\":{},\"f:targetPort\":{}}},\"f:selector\":{},\"f:sessionAffinity\":{},\"f:type\":{}}},\"manager\":\"kubectl-expose\",\"operation\":\"Update\",\"time\":\"2025-10-01T09:37:23Z\"}],\"name\":\"nginx\",\"namespace\":\"default\",\"resourceVersion\":\"8564\",\"uid\":\"cf112753-1335-4edc-8374-1bfb5aceb41f\"},\"spec\":{\"clusterIP\":\"10.109.71.60\",\"clusterIPs\":[\"10.109.71.60\"],\"externalTrafficPolicy\":\"Cluster\",\"internalTrafficPolicy\":\"Cluster\",\"ipFamilies\":[\"IPv4\"],\"ipFamilyPolicy\":\"SingleStack\",\"ports\":[{\"nodePort\":32762,\"port\":80,\"protocol\":\"TCP\",\"targetPort\":80}],\"selector\":{\"app\":\"nginx\"},\"sessionAffinity\":\"None\",\"type\":\"NodePort\"},\"status\":{\"loadBalancer\":{}}}}\n`\n\n\twatcher := NewServiceWatcher()\n\terr := watcher.readKubectlStream(strings.NewReader(stream))\n\tassert.NilError(t, err)\n\n\tgot := watcher.GetPorts()\n\twant := []Entry{{\n\t\tProtocol: TCP,\n\t\tIP:       net.IPv4zero,\n\t\tPort:     uint16(32762),\n\t}}\n\n\tassert.DeepEqual(t, got, want)\n}\n"
  },
  {
    "path": "pkg/guestagent/kubernetesservice/types.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage kubernetesservice\n\n// The type definitions were taken from https://pkg.go.dev/k8s.io/api@v0.34.3/core/v1\n// to reduce Go module dependencies.\n// SPDX-FileCopyrightText: Copyright 2015 The Kubernetes Authors.\n\ntype service struct {\n\tMetadata objectMeta  `json:\"metadata\"`\n\tSpec     serviceSpec `json:\"spec\"`\n}\n\ntype objectMeta struct {\n\tName      string `json:\"name\"`\n\tNamespace string `json:\"namespace\"`\n}\n\ntype serviceSpec struct {\n\tType  serviceType   `json:\"type\"`\n\tPorts []servicePort `json:\"ports\"`\n}\n\ntype servicePort struct {\n\tName       string      `json:\"name\"`\n\tProtocol   k8sProtocol `json:\"protocol\"`\n\tPort       int32       `json:\"port\"`\n\tTargetPort any         `json:\"targetPort\"` // int or string\n\tNodePort   int32       `json:\"nodePort\"`\n}\n\ntype serviceType string\n\nconst (\n\tserviceTypeNodePort     serviceType = \"NodePort\"\n\tserviceTypeLoadBalancer serviceType = \"LoadBalancer\"\n)\n\ntype k8sProtocol string\n\nconst (\n\tprotocolTCP k8sProtocol = \"TCP\"\n\tprotocolUDP k8sProtocol = \"UDP\"\n)\n\ntype eventType string\n\nconst (\n\tadded    eventType = \"ADDED\"\n\tmodified eventType = \"MODIFIED\"\n\tdeleted  eventType = \"DELETED\"\n)\n"
  },
  {
    "path": "pkg/guestagent/serialport/serialconn_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage serialport\n\nimport (\n\t\"net\"\n\t\"time\"\n)\n\ntype SerialConn struct {\n\tserialDevice string\n\tport         *Port\n}\n\nvar _ net.Conn = (*SerialConn)(nil)\n\nfunc DialSerial(serialDevice string) (*SerialConn, error) {\n\ts, err := openPort(serialDevice)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &SerialConn{port: s, serialDevice: serialDevice}, nil\n}\n\nfunc (c *SerialConn) Read(b []byte) (n int, err error) {\n\treturn c.port.Read(b)\n}\n\nfunc (c *SerialConn) Write(b []byte) (n int, err error) {\n\treturn c.port.Write(b)\n}\n\nfunc (c *SerialConn) Close() error {\n\t// There is no need to close the serial port every time.\n\t// So just do nothing.\n\treturn nil\n}\n\nfunc (c *SerialConn) LocalAddr() net.Addr {\n\treturn &net.UnixAddr{Name: \"virtio-port:\" + c.serialDevice, Net: \"virtio\"}\n}\n\nfunc (c *SerialConn) RemoteAddr() net.Addr {\n\treturn &net.UnixAddr{Name: \"qemu-host\", Net: \"virtio\"}\n}\n\nfunc (c *SerialConn) SetDeadline(_ time.Time) error {\n\treturn nil\n}\n\nfunc (c *SerialConn) SetReadDeadline(_ time.Time) error {\n\treturn nil\n}\n\nfunc (c *SerialConn) SetWriteDeadline(_ time.Time) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/guestagent/serialport/seriallistener_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage serialport\n\nimport (\n\t\"net\"\n\t\"sync\"\n\t\"syscall\"\n\n\t\"golang.org/x/net/netutil\"\n)\n\ntype SerialListener struct {\n\tmu     sync.Mutex\n\tconn   *SerialConn\n\tclosed bool\n}\n\nfunc Listen(serialDevice string) (net.Listener, error) {\n\tc, err := DialSerial(serialDevice)\n\tif err != nil {\n\t\treturn nil, &net.OpError{Op: \"dial\", Net: \"virtio\", Source: c.LocalAddr(), Addr: nil, Err: err}\n\t}\n\n\treturn netutil.LimitListener(&SerialListener{conn: c}, 1), nil\n}\n\nfunc (ln *SerialListener) ok() bool {\n\treturn ln != nil && ln.conn != nil && !ln.closed\n}\n\nfunc (ln *SerialListener) Accept() (net.Conn, error) {\n\tln.mu.Lock()\n\tdefer ln.mu.Unlock()\n\n\tif !ln.ok() {\n\t\treturn nil, syscall.EINVAL\n\t}\n\n\treturn ln.conn, nil\n}\n\nfunc (ln *SerialListener) Close() error {\n\tln.mu.Lock()\n\tdefer ln.mu.Unlock()\n\n\tif !ln.ok() {\n\t\treturn syscall.EINVAL\n\t}\n\n\tif ln.closed {\n\t\treturn nil\n\t}\n\tln.closed = true\n\n\treturn nil\n}\n\nfunc (ln *SerialListener) Addr() net.Addr {\n\tif ln.ok() {\n\t\treturn ln.conn.LocalAddr()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/guestagent/serialport/serialport_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage serialport\n\nimport (\n\t\"os\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc openPort(name string) (p *Port, err error) {\n\tf, err := os.OpenFile(name, unix.O_RDWR|unix.O_NOCTTY|unix.O_NONBLOCK, 0o666)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func() {\n\t\tif err != nil && f != nil {\n\t\t\tf.Close()\n\t\t}\n\t}()\n\n\tfd := f.Fd()\n\tif err = unix.SetNonblock(int(fd), false); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Port{f: f}, nil\n}\n\ntype Port struct {\n\tf *os.File\n}\n\nfunc (p *Port) Read(b []byte) (n int, err error) {\n\treturn p.f.Read(b)\n}\n\nfunc (p *Port) Write(b []byte) (n int, err error) {\n\treturn p.f.Write(b)\n}\n\nfunc (p *Port) Close() (err error) {\n\treturn p.f.Close()\n}\n"
  },
  {
    "path": "pkg/guestagent/sockets/sockets.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sockets\n\nimport (\n\t\"net\"\n)\n\ntype Kind = string\n\nconst (\n\tTCP  Kind = \"tcp\"\n\tTCP6 Kind = \"tcp6\"\n\tUDP  Kind = \"udp\"\n\tUDP6 Kind = \"udp6\"\n\t// TODO: \"udplite\", \"udplite6\".\n)\n\ntype State = byte\n\nconst (\n\tTCPEstablished State = 0x1\n\tTCPListen      State = 0xA\n\tUDPUnconnected State = 0x7\n)\n\ntype Socket struct {\n\tKind  Kind   `json:\"kind\"`\n\tIP    net.IP `json:\"ip\"`\n\tPort  uint16 `json:\"port\"`\n\tState State  `json:\"state\"`\n}\n"
  },
  {
    "path": "pkg/guestagent/sockets/sockets_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sockets\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/mdlayher/netlink\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// buildInetDiagReqV2 builds the inet_diag_req_v2 bytes (56 bytes total).\nfunc buildInetDiagReqV2(family, proto int) []byte {\n\tb := make([]byte, 56) // sizeof(inet_diag_req_v2) == 56\n\t// layout:\n\t// u8 sdiag_family;\n\t// u8 sdiag_protocol;\n\t// u8 idiag_ext;\n\t// u8 pad;\n\t// u32 idiag_states;\n\t// struct inet_diag_sockid { ... }  (48 bytes)\n\tb[0] = byte(family)\n\tb[1] = byte(proto)\n\tb[2] = 0 // ext\n\tb[3] = 0 // pad\n\t// idiag_states -> all states\n\tbinary.NativeEndian.PutUint32(b[4:], 0xFFFFFFFF)\n\t// rest is zero (sockid zero => match all)\n\treturn b\n}\n\nfunc query(conn *netlink.Conn, family, proto int) ([]netlink.Message, error) {\n\treq := buildInetDiagReqV2(family, proto)\n\tmsg := netlink.Message{\n\t\tHeader: netlink.Header{\n\t\t\tType:  unix.SOCK_DIAG_BY_FAMILY,\n\t\t\tFlags: netlink.Request | netlink.Dump,\n\t\t},\n\t\tData: req,\n\t}\n\tmsgs, err := conn.Execute(msg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn msgs, nil\n}\n\nfunc parseMessages(msgs []netlink.Message, proto int) ([]Socket, error) {\n\tif proto != unix.IPPROTO_TCP && proto != unix.IPPROTO_UDP {\n\t\treturn nil, fmt.Errorf(\"unsupported protocol: %d\", proto)\n\t}\n\tvar sockets []Socket\n\tfor _, m := range msgs {\n\t\tdata := m.Data\n\t\t// inet_diag_msg minimum size ~72 bytes (4 + 48 + 20)\n\t\tif len(data) < 72 {\n\t\t\tcontinue\n\t\t}\n\t\tfamily := int(data[0])\n\t\tstate := data[1]\n\t\t// data[2] timer, data[3] retrans (ignored here)\n\t\t// id begins at offset 4:\n\t\t// sport (2B, big-endian), dport (2B, big-endian)\n\t\tsport := binary.BigEndian.Uint16(data[4:6])\n\n\t\tsrc := data[8:24] // 16 bytes\n\n\t\tvar localIP net.IP\n\t\tif family == unix.AF_INET {\n\t\t\t// first 4 bytes are IPv4 in network order\n\t\t\tlocalIP = net.IP(src[0:4])\n\t\t} else {\n\t\t\t// IPv6\n\t\t\tlocalIP = net.IP(src)\n\t\t}\n\n\t\t// proto name + possible 6 suffix\n\t\tpname := \"tcp\"\n\t\tif proto == unix.IPPROTO_UDP {\n\t\t\tpname = \"udp\"\n\t\t}\n\t\tif family == unix.AF_INET6 {\n\t\t\tpname += \"6\"\n\t\t}\n\n\t\tnewSocket := Socket{\n\t\t\tKind:  pname,\n\t\t\tIP:    localIP,\n\t\t\tPort:  sport,\n\t\t\tState: state,\n\t\t}\n\t\tsockets = append(sockets, newSocket)\n\t}\n\treturn sockets, nil\n}\n\nfunc NewLister() (*Lister, error) {\n\tconn, err := netlink.Dial(unix.NETLINK_SOCK_DIAG, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open netlink connection: %w\", err)\n\t}\n\treturn &Lister{conn: conn}, nil\n}\n\ntype Lister struct {\n\tconn *netlink.Conn\n}\n\nfunc (lister *Lister) List() ([]Socket, error) {\n\tprotos := []int{unix.IPPROTO_TCP, unix.IPPROTO_UDP}\n\tfamilies := []int{unix.AF_INET, unix.AF_INET6}\n\n\tvar sockets []Socket\n\tfor _, proto := range protos {\n\t\tfor _, fam := range families {\n\t\t\tmsgs, err := query(lister.conn, fam, proto)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tparsedSockets, err := parseMessages(msgs, proto)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsockets = append(sockets, parsedSockets...)\n\t\t}\n\t}\n\treturn sockets, nil\n}\n\nfunc (lister *Lister) Close() error {\n\treturn lister.conn.Close()\n}\n"
  },
  {
    "path": "pkg/guestagent/sockets/sockets_linux_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sockets\n\nimport (\n\t\"encoding/binary\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/mdlayher/netlink\"\n\t\"golang.org/x/sys/unix\"\n\t\"gotest.tools/v3/assert\"\n)\n\n// helper to construct a minimal inet_diag_msg-like payload.\n// Layout assumptions mirror parseMessages:\n//\n//\tbyte 0: family\n//\tbyte 1: state\n//\tbytes 4-6: sport (big-endian)\n//\tbytes 8-24: source address (16 bytes, IPv4 in first 4 if AF_INET)\nfunc diagMsg(family int, state byte, port uint16, ip net.IP) netlink.Message {\n\tdata := make([]byte, 72) // minimum size accepted\n\tdata[0] = byte(family)\n\tdata[1] = state\n\t// sport\n\tbinary.BigEndian.PutUint16(data[4:6], port)\n\tsrc := data[8:24]\n\tif family == unix.AF_INET {\n\t\tip4 := ip.To4()\n\t\tcopy(src[:4], ip4)\n\t} else {\n\t\tip16 := ip.To16()\n\t\tcopy(src, ip16)\n\t}\n\treturn netlink.Message{Data: data}\n}\n\nfunc TestBuildInetDiagReqV2(t *testing.T) {\n\treq := buildInetDiagReqV2(unix.AF_INET6, unix.IPPROTO_TCP)\n\tassert.Equal(t, len(req), 56, \"unexpected request length\")\n\tif req[0] != byte(unix.AF_INET6) {\n\t\tt.Errorf(\"family byte = %d want %d\", req[0], unix.AF_INET6)\n\t}\n\tif req[1] != byte(unix.IPPROTO_TCP) {\n\t\tt.Errorf(\"proto byte = %d want %d\", req[1], unix.IPPROTO_TCP)\n\t}\n\tstates := binary.LittleEndian.Uint32(req[4:8])\n\tif states != 0xFFFFFFFF {\n\t\tt.Errorf(\"idiag_states = 0x%08X want 0xFFFFFFFF\", states)\n\t}\n}\n\nfunc TestParseMessages_TCPv4(t *testing.T) {\n\tconst port = 8080\n\tip := net.ParseIP(\"127.0.0.1\")\n\tmsg := diagMsg(unix.AF_INET, TCPListen, port, ip)\n\tsocks, err := parseMessages([]netlink.Message{msg}, unix.IPPROTO_TCP)\n\tassert.NilError(t, err, \"parseMessages error\")\n\tassert.Equal(t, len(socks), 1, \"unexpected number of sockets\")\n\ts := socks[0]\n\tif s.Kind != \"tcp\" {\n\t\tt.Errorf(\"Kind = %q want tcp\", s.Kind)\n\t}\n\tif !s.IP.Equal(ip) {\n\t\tt.Errorf(\"IP = %v want %v\", s.IP, ip)\n\t}\n\tif s.Port != port {\n\t\tt.Errorf(\"Port = %d want %d\", s.Port, port)\n\t}\n\tif s.State != TCPListen {\n\t\tt.Errorf(\"State = 0x%X want 0x%X\", s.State, TCPListen)\n\t}\n}\n\nfunc TestParseMessages_UDPv6(t *testing.T) {\n\tconst port = 5353\n\tip := net.ParseIP(\"2001:db8::1\")\n\tmsg := diagMsg(unix.AF_INET6, UDPUnconnected, port, ip)\n\tsocks, err := parseMessages([]netlink.Message{msg}, unix.IPPROTO_UDP)\n\tassert.NilError(t, err, \"parseMessages error\")\n\tassert.Equal(t, len(socks), 1, \"unexpected number of sockets\")\n\ts := socks[0]\n\tif s.Kind != \"udp6\" {\n\t\tt.Errorf(\"Kind = %q want udp6\", s.Kind)\n\t}\n\tif !s.IP.Equal(ip) {\n\t\tt.Errorf(\"IP = %v want %v\", s.IP, ip)\n\t}\n\tif s.Port != port {\n\t\tt.Errorf(\"Port = %d want %d\", s.Port, port)\n\t}\n\tif s.State != UDPUnconnected {\n\t\tt.Errorf(\"State = 0x%X want 0x%X\", s.State, UDPUnconnected)\n\t}\n}\n\nfunc TestParseMessages_ShortDataSkipped(t *testing.T) {\n\tshort := netlink.Message{Data: make([]byte, 10)} // < 72 => ignored\n\tsocks, err := parseMessages([]netlink.Message{short}, unix.IPPROTO_TCP)\n\tassert.NilError(t, err, \"parseMessages error\")\n\tassert.Equal(t, len(socks), 0, \"unexpected number of sockets\")\n}\n\nfunc TestListS_Integration(t *testing.T) {\n\tlister, err := NewLister()\n\tassert.NilError(t, err, \"NewLister error\")\n\tdefer lister.Close()\n\t_, err = lister.List()\n\tif err != nil {\n\t\tt.Skipf(\"skipping: cannot query netlink inet_diag (%v)\", err)\n\t}\n\t// No assertions: presence of error-free call is sufficient.\n}\n"
  },
  {
    "path": "pkg/guestagent/ticker/compound.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ticker\n\nimport (\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc NewCompoundTicker(t1, t2 Ticker) Ticker {\n\treturn &compoundTicker{t1, t2}\n}\n\ntype compoundTicker struct {\n\tt1, t2 Ticker\n}\n\nvar _ Ticker = (*compoundTicker)(nil)\n\nfunc (ticker *compoundTicker) Chan() <-chan time.Time {\n\tch := make(chan time.Time)\n\tgo func() {\n\t\tdefer close(ch)\n\t\tdefer logrus.Debug(\"compoundTicker: exiting\")\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase v, ok := <-ticker.t1.Chan():\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tch <- v\n\t\t\tcase v, ok := <-ticker.t2.Chan():\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tch <- v\n\t\t\t}\n\t\t}\n\t}()\n\treturn ch\n}\n\nfunc (ticker *compoundTicker) Stop() {\n\tticker.t1.Stop()\n\tticker.t2.Stop()\n}\n"
  },
  {
    "path": "pkg/guestagent/ticker/ebpf_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ticker\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cilium/ebpf\"\n\t\"github.com/cilium/ebpf/asm\"\n\t\"github.com/cilium/ebpf/link\"\n\t\"github.com/cilium/ebpf/ringbuf\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc NewEbpfTicker(tracepoints []string) (Ticker, error) {\n\tvar (\n\t\tticker ebpfTicker\n\t\terr    error\n\t)\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tticker.Stop()\n\t\t}\n\t}()\n\tticker.events, err = ebpf.NewMap(&ebpf.MapSpec{\n\t\tName:       \"lima_ticker_events\",\n\t\tType:       ebpf.RingBuf,\n\t\tMaxEntries: 1 << 20,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tticker.prog, err = buildEbpfProg(ticker.events)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, tp := range tracepoints {\n\t\ttpPair := strings.SplitN(tp, \":\", 2)\n\t\ttpLink, err := link.Tracepoint(tpPair[0], tpPair[1], ticker.prog, nil)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tticker.links = append(ticker.links, tpLink)\n\t}\n\n\tticker.reader, err = ringbuf.NewReader(ticker.events)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tticker.ch = make(chan time.Time)\n\tgo func() {\n\t\tdefer close(ticker.ch)\n\t\tfor {\n\t\t\t_, rdErr := ticker.reader.Read()\n\t\t\tif rdErr != nil {\n\t\t\t\tif !errors.Is(rdErr, ringbuf.ErrClosed) {\n\t\t\t\t\tlogrus.WithError(rdErr).Warn(\"ebpfTicker: failed to read ringbuf\")\n\t\t\t\t}\n\t\t\t\tlogrus.Debug(\"ebpfTicker: exiting\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tticker.ch <- time.Now()\n\t\t}\n\t}()\n\n\treturn &ticker, nil\n}\n\nvar _ Ticker = (*ebpfTicker)(nil)\n\ntype ebpfTicker struct {\n\tevents *ebpf.Map\n\tprog   *ebpf.Program\n\tlinks  []link.Link\n\treader *ringbuf.Reader\n\tch     chan time.Time\n}\n\nfunc (ticker *ebpfTicker) Chan() <-chan time.Time {\n\treturn ticker.ch\n}\n\nfunc (ticker *ebpfTicker) Stop() {\n\tif ticker.events != nil {\n\t\t_ = ticker.events.Close()\n\t}\n\tif ticker.prog != nil {\n\t\t_ = ticker.prog.Close()\n\t}\n\tfor _, l := range ticker.links {\n\t\t_ = l.Close()\n\t}\n\tif ticker.reader != nil {\n\t\t_ = ticker.reader.Close()\n\t}\n\t// ticker.ch will be closed in go routine in NewEbpfTicker() to avoid sending on closed channel\n}\n\nfunc buildEbpfProg(events *ebpf.Map) (*ebpf.Program, error) {\n\tinst := asm.Instructions{\n\t\t// ignore events from the guestagent process itself\n\t\tasm.FnGetCurrentPidTgid.Call(),\n\t\tasm.RSh.Imm(asm.R0, 32),\n\t\tasm.JEq.Imm(asm.R0, int32(os.Getpid()), \"ret\"),\n\n\t\t// ringbuf = &map\n\t\tasm.LoadMapPtr(asm.R1, events.FD()),\n\n\t\t// data = FP - 8\n\t\tasm.Mov.Reg(asm.R2, asm.R10),\n\t\tasm.Add.Imm(asm.R2, -8),\n\n\t\t// *data = 1\n\t\tasm.StoreImm(asm.R2, 0, 1, asm.Word),\n\n\t\t// size = 1\n\t\tasm.Mov.Imm(asm.R3, 1),\n\n\t\t// flags = 0\n\t\tasm.Mov.Imm(asm.R4, 0),\n\n\t\t// long bpf_ringbuf_output(void *ringbuf, void *data, u64 size, u64 flags)\n\t\t// https://man7.org/linux/man-pages/man7/bpf-helpers.7.html\n\t\tasm.FnRingbufOutput.Call(),\n\n\t\t// return 0\n\t\tasm.Mov.Imm(asm.R0, 0).WithSymbol(\"ret\"),\n\t\tasm.Return(),\n\t}\n\n\tspec := &ebpf.ProgramSpec{\n\t\tName:         \"lima_ticker\",\n\t\tType:         ebpf.TracePoint,\n\t\tLicense:      \"Apache-2.0\", // No need to be GPL?\n\t\tInstructions: inst,\n\t}\n\n\treturn ebpf.NewProgram(spec)\n}\n"
  },
  {
    "path": "pkg/guestagent/ticker/simple.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ticker\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc NewSimpleTicker(ticker *time.Ticker) Ticker {\n\treturn &simpleTicker{\n\t\tTicker:     ticker,\n\t\tclosableCh: make(chan any),\n\t\texposeCh:   make(chan time.Time),\n\t}\n}\n\nvar _ Ticker = (*simpleTicker)(nil)\n\ntype simpleTicker struct {\n\t*time.Ticker\n\tclosableCh chan any\n\texposeCh   chan time.Time\n\tonce       sync.Once\n}\n\nfunc (ticker *simpleTicker) Chan() <-chan time.Time {\n\t// We cannot directly expose ticker.Ticker.C because it won't be closed on Stop()\n\tticker.once.Do(func() {\n\t\tgo func() {\n\t\t\tdefer close(ticker.exposeCh)\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase v, ok := <-ticker.Ticker.C:\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\t// should not happen as time.Ticker.C is never closed as per docs\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tticker.exposeCh <- v\n\t\t\t\tcase <-ticker.closableCh:\n\t\t\t\t\tlogrus.Debug(\"simpleTicker: exiting\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t\tlogrus.Debug(\"simpleTicker: starting\")\n\t})\n\treturn ticker.exposeCh\n}\n\nfunc (ticker *simpleTicker) Stop() {\n\tticker.Ticker.Stop()\n\t// Since ticker.Ticker.Stop() does not close ticker.Ticker.C,\n\t// we need to close the goroutine created in Chan().\n\tclose(ticker.closableCh)\n}\n"
  },
  {
    "path": "pkg/guestagent/ticker/ticker.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ticker\n\nimport (\n\t\"time\"\n)\n\ntype Ticker interface {\n\t// similar to time.Ticker.C, but must be closed when Stop() is called\n\tChan() <-chan time.Time\n\tStop()\n}\n"
  },
  {
    "path": "pkg/guestagent/timesync/timesync_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage timesync\n\nimport (\n\t\"time\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc SetSystemTime(t time.Time) error {\n\tv := unix.NsecToTimeval(t.UnixNano())\n\treturn unix.Settimeofday(&v)\n}\n"
  },
  {
    "path": "pkg/guestagent/timesync/timesync_others.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n//go:build !linux\n\npackage timesync\n\nimport (\n\t\"errors\"\n\t\"time\"\n)\n\nvar errNotSupported = errors.New(\"timesync: not supported on this platform\")\n\nfunc SetSystemTime(_ time.Time) error {\n\treturn errNotSupported\n}\n"
  },
  {
    "path": "pkg/guestpatch/macos/macos_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage macos\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/apfs\"\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\n// attachImageWithRetry retries `diskutil image attach` because the\n// command occasionally returns \"Resource temporarily unavailable\".\nfunc attachImageWithRetry(ctx context.Context, disk string, retry int) (*asifutil.AttachedDisk, error) {\n\tvar (\n\t\tattached *asifutil.AttachedDisk\n\t\terr      error\n\t)\n\tfor range retry {\n\t\tattached, err = asifutil.DiskutilImageAttachNoMount(ctx, disk)\n\t\tif !errors.Is(err, asifutil.ErrResourceTemporarilyUnavailable) {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(3 * time.Second)\n\t}\n\treturn attached, err\n}\n\n// Patch prepares a macOS guest disk for first boot. It writes the\n// LaunchDaemon plist, init script, and setup markers via a noowners\n// mount, then fixes file ownership by patching APFS inode records\n// directly on the raw disk image. No sudo required.\nfunc Patch(ctx context.Context, disk string) error {\n\tif err := patchWriteGuestFiles(ctx, disk); err != nil {\n\t\treturn err\n\t}\n\treturn patchFixOwnership(ctx, disk)\n}\n\n// patchWriteGuestFiles attaches the disk image, mounts the Data\n// volume with noowners, writes guest files, then detaches.\nfunc patchWriteGuestFiles(ctx context.Context, disk string) error {\n\tattached, err := attachImageWithRetry(ctx, disk, 3)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to attach disk: %w\", err)\n\t}\n\tif attached == nil || attached.Data == \"\" {\n\t\treturn errors.New(\"failed to find data slice in attached disk\")\n\t}\n\tdataDevPath := \"/dev/\" + attached.Data\n\tdefer func() {\n\t\t// Detaching the data slice is enough to detach the whole ASIF.\n\t\tif err := asifutil.DetachASIF(dataDevPath); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"failed to detach %q (%q)\", dataDevPath, disk)\n\t\t}\n\t}()\n\n\tinstDir := filepath.Dir(disk)\n\tmnt := filepath.Join(instDir, filenames.MntDir)\n\tif err := os.MkdirAll(mnt, 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to create mount point %q: %w\", mnt, err)\n\t}\n\tdefer func() {\n\t\tif err := os.Remove(mnt); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"failed to remove mount point %q\", mnt)\n\t\t}\n\t}()\n\treturn writeGuestFiles(ctx, dataDevPath, mnt)\n}\n\n// patchFixOwnership attaches the disk image and patches APFS inode\n// records on the raw container device to set root:wheel ownership.\nfunc patchFixOwnership(ctx context.Context, disk string) error {\n\tattached, err := attachImageWithRetry(ctx, disk, 3)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to attach disk for ownership fix: %w\", err)\n\t}\n\tif attached == nil || attached.Data == \"\" {\n\t\treturn errors.New(\"failed to find data slice in attached disk\")\n\t}\n\tdataDevPath := \"/dev/\" + attached.Data\n\tdefer func() {\n\t\tif err := asifutil.DetachASIF(dataDevPath); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"failed to detach %q (%q)\", dataDevPath, disk)\n\t\t}\n\t}()\n\n\tif attached.Container == \"\" {\n\t\treturn errors.New(\"diskutil did not report an APFS container device\")\n\t}\n\n\t// Patch APFS inode records via the raw container device.\n\t// The noowners mount stores files with uid=99 (nobody);\n\t// LaunchDaemon plists must be owned by root:wheel for launchd\n\t// to load them, so we patch them to UID 0 / GID 0 directly.\n\tcontainerDev := \"/dev/r\" + attached.Container\n\tif err = apfs.Chown(containerDev, apfs.VolRoleData, 0, 0,\n\t\t\"private/var/db/.AppleSetupDone\",\n\t\t\"Library/User Template/.skipbuddy\",\n\t\t\"usr/local/sbin\",\n\t\t\"usr/local/sbin/lima-macos-init.sh\",\n\t\t\"Library/LaunchDaemons/io.lima-vm.lima-macos-init.plist\",\n\t); err != nil {\n\t\treturn fmt.Errorf(\"failed to fix file ownership on disk: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// writeGuestFiles mounts the data volume with noowners and writes the\n// LaunchDaemon plist, init script, and setup markers.\nfunc writeGuestFiles(ctx context.Context, dataSliceDevice, mnt string) error {\n\t// Mount with \"noowners\" so non-root users can write to the volume.\n\tif err := osutil.Mount(ctx, \"apfs\", dataSliceDevice, mnt, []string{\"noowners\"}); err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif err := osutil.Umount(ctx, mnt); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"failed to unmount %q\", mnt)\n\t\t}\n\t}()\n\n\tfilesToTouch := []string{\n\t\tfilepath.Join(mnt, \"private/var/db/.AppleSetupDone\"),\n\t\tfilepath.Join(mnt, \"Library/User Template/.skipbuddy\"),\n\t}\n\tfor _, file := range filesToTouch {\n\t\tif err := osutil.Touch(file); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to touch %q: %w\", file, err)\n\t\t}\n\t}\n\n\tconst initSh = `#!/bin/sh\nset -eux\ndate\nif [ ! -e /Volumes/cidata ]; then\n  echo \"/Volumes/cidata is not mounted\" >&2\n  exit 1\nfi\nexec /Volumes/cidata/lima-guestagent fake-cloud-init\n`\n\tif err := os.MkdirAll(filepath.Join(mnt, \"usr/local/sbin\"), 0o755); err != nil {\n\t\treturn err\n\t}\n\tif err := os.WriteFile(filepath.Join(mnt, \"usr/local/sbin/lima-macos-init.sh\"), []byte(initSh), 0o755); err != nil {\n\t\treturn err\n\t}\n\n\tconst plist = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>io.lima-vm.lima-macos-init</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>/usr/local/sbin/lima-macos-init.sh</string>\n    </array>\n    <key>RunAtLoad</key>\n    <true/>\n    <key>StandardOutPath</key>\n    <string>/dev/tty.virtio</string>\n    <key>StandardErrorPath</key>\n    <string>/dev/tty.virtio</string>\n</dict>\n</plist>\n`\n\tif err := os.WriteFile(filepath.Join(mnt, \"Library/LaunchDaemons/io.lima-vm.lima-macos-init.plist\"), []byte(plist), 0o755); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/hostagent/api/api.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage api\n\ntype Info struct {\n\t// indicate instance is started by launchd or systemd if not empty\n\tAutoStartedIdentifier string `json:\"autoStartedIdentifier,omitempty\"`\n\t// SSHLocalPort is the local port on the host for SSH access to the VM.\n\tSSHLocalPort int `json:\"sshLocalPort,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/hostagent/api/client/client.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage client\n\n// Forked from https://github.com/rootless-containers/rootlesskit/blob/v0.14.2/pkg/api/client/client.go\n// Apache License 2.0\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/api\"\n\t\"github.com/lima-vm/lima/v2/pkg/httpclientutil\"\n)\n\ntype HostAgentClient interface {\n\tHTTPClient() *http.Client\n\tInfo(context.Context) (*api.Info, error)\n}\n\n// NewHostAgentClient creates a client.\n// socketPath is a path to the UNIX socket, without unix:// prefix.\nfunc NewHostAgentClient(socketPath string) (HostAgentClient, error) {\n\thc, err := httpclientutil.NewHTTPClientWithSocketPath(socketPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn NewHostAgentClientWithHTTPClient(hc), nil\n}\n\nfunc NewHostAgentClientWithHTTPClient(hc *http.Client) HostAgentClient {\n\treturn &client{\n\t\tClient:    hc,\n\t\tversion:   \"v1\",\n\t\tdummyHost: \"lima-hostagent\",\n\t}\n}\n\ntype client struct {\n\t*http.Client\n\t// version is always \"v1\"\n\t// TODO(AkihiroSuda): negotiate the version\n\tversion   string\n\tdummyHost string\n}\n\nfunc (c *client) HTTPClient() *http.Client {\n\treturn c.Client\n}\n\nfunc (c *client) Info(ctx context.Context) (*api.Info, error) {\n\tu := fmt.Sprintf(\"http://%s/%s/info\", c.dummyHost, c.version)\n\tresp, err := httpclientutil.Get(ctx, c.HTTPClient(), u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\tvar info api.Info\n\tdec := json.NewDecoder(resp.Body)\n\tif err := dec.Decode(&info); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &info, nil\n}\n"
  },
  {
    "path": "pkg/hostagent/api/server/server.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage server\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent\"\n\t\"github.com/lima-vm/lima/v2/pkg/httputil\"\n)\n\ntype Backend struct {\n\tAgent *hostagent.HostAgent\n}\n\nfunc (b *Backend) onError(w http.ResponseWriter, err error, ec int) {\n\tw.WriteHeader(ec)\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t// err may potentially contain credential info (in a future version),\n\t// but it is safe to return the err to the client, because we do not expose the socket to the internet\n\te := httputil.ErrorJSON{\n\t\tMessage: err.Error(),\n\t}\n\t_ = json.NewEncoder(w).Encode(e)\n}\n\n// GetInfo is the handler for GET /v1/info.\nfunc (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\tctx := r.Context()\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tinfo, err := b.Agent.Info(ctx)\n\tif err != nil {\n\t\tb.onError(w, err, http.StatusInternalServerError)\n\t\treturn\n\t}\n\tm, err := json.Marshal(info)\n\tif err != nil {\n\t\tb.onError(w, err, http.StatusInternalServerError)\n\t\treturn\n\t}\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(http.StatusOK)\n\t_, _ = w.Write(m)\n}\n\nfunc AddRoutes(r *http.ServeMux, b *Backend) {\n\tr.Handle(\"/v1/info\", http.HandlerFunc(b.GetInfo))\n}\n"
  },
  {
    "path": "pkg/hostagent/dns/dns.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// This file has been adapted from https://github.com/norouter/norouter/blob/v0.6.4/pkg/agent/dns/dns.go\n\npackage dns\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/miekg/dns\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Truncate for avoiding \"Parse error\" from `busybox nslookup`\n// https://github.com/lima-vm/lima/issues/380\nconst truncateSize = 512\n\nvar defaultFallbackIPs = []string{\"8.8.8.8\", \"1.1.1.1\"}\n\ntype Network string\n\nconst (\n\tTCP Network = \"tcp\"\n\tUDP Network = \"udp\"\n)\n\ntype HandlerOptions struct {\n\tIPv6            bool\n\tStaticHosts     map[string]string\n\tUpstreamServers []string\n\tTruncateReply   bool\n}\n\ntype ServerOptions struct {\n\tHandlerOptions\n\tAddress string\n\tTCPPort int\n\tUDPPort int\n}\n\ntype Handler struct {\n\ttruncate     bool\n\tclientConfig *dns.ClientConfig\n\tclients      []*dns.Client\n\tipv6         bool\n\tcnameToHost  map[string]string\n\thostToIP     map[string]net.IP\n}\n\ntype Server struct {\n\tudp *dns.Server\n\ttcp *dns.Server\n}\n\nfunc (s *Server) Shutdown() {\n\tif s.udp != nil {\n\t\t_ = s.udp.Shutdown()\n\t}\n\tif s.tcp != nil {\n\t\t_ = s.tcp.Shutdown()\n\t}\n}\n\nfunc newStaticClientConfig(ips []string) (*dns.ClientConfig, error) {\n\tlogrus.Tracef(\"newStaticClientConfig creating config for the following IPs: %v\", ips)\n\tconfig := \"nameserver \" + strings.Join(ips, \"\\nnameserver \") + \"\\n\"\n\treturn dns.ClientConfigFromReader(strings.NewReader(config))\n}\n\nfunc (h *Handler) lookupCnameToHost(cname string) string {\n\tseen := make(map[string]bool)\n\tfor !seen[cname] { // break cyclic definition\n\t\tif _, ok := h.cnameToHost[cname]; ok {\n\t\t\tseen[cname] = true\n\t\t\tcname = h.cnameToHost[cname]\n\t\t\tcontinue\n\t\t}\n\t\tbreak\n\t}\n\treturn cname\n}\n\nfunc NewHandler(opts HandlerOptions) (dns.Handler, error) {\n\tvar cc *dns.ClientConfig\n\tvar err error\n\tif len(opts.UpstreamServers) == 0 {\n\t\tif runtime.GOOS != \"windows\" {\n\t\t\tcc, err = dns.ClientConfigFromFile(\"/etc/resolv.conf\")\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Warnf(\"failed to detect system DNS, falling back to %v\", defaultFallbackIPs)\n\t\t\t\tcc, err = newStaticClientConfig(defaultFallbackIPs)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// For windows, the only fallback addresses are defaultFallbackIPs\n\t\t\t// since there is no /etc/resolv.conf\n\t\t\tcc, err = newStaticClientConfig(defaultFallbackIPs)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif cc, err = newStaticClientConfig(opts.UpstreamServers); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"failed to create a client config from: %v, falling back to %v\", opts.UpstreamServers, defaultFallbackIPs)\n\t\t\tif cc, err = newStaticClientConfig(defaultFallbackIPs); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\tclients := []*dns.Client{\n\t\t{}, // UDP\n\t\t{Net: \"tcp\"},\n\t}\n\th := &Handler{\n\t\ttruncate:     opts.TruncateReply,\n\t\tclientConfig: cc,\n\t\tclients:      clients,\n\t\tipv6:         opts.IPv6,\n\t\tcnameToHost:  make(map[string]string),\n\t\thostToIP:     make(map[string]net.IP),\n\t}\n\tfor host, address := range opts.StaticHosts {\n\t\tcname := dns.CanonicalName(host)\n\t\tif ip := net.ParseIP(address); ip != nil {\n\t\t\th.hostToIP[cname] = ip\n\t\t} else {\n\t\t\th.cnameToHost[cname] = dns.CanonicalName(address)\n\t\t}\n\t}\n\treturn h, nil\n}\n\nfunc (h *Handler) handleQuery(ctx context.Context, w dns.ResponseWriter, req *dns.Msg) {\n\tvar (\n\t\treply   dns.Msg\n\t\thandled bool\n\t)\n\tdefer w.Close()\n\treply.SetReply(req)\n\tlogrus.Tracef(\"handleQuery received DNS query: %v\", req)\n\tfor _, q := range req.Question {\n\t\thdr := dns.RR_Header{\n\t\t\tName:   q.Name,\n\t\t\tRrtype: q.Qtype,\n\t\t\tClass:  q.Qclass,\n\t\t\tTtl:    5,\n\t\t}\n\t\tqtype := q.Qtype\n\t\tswitch q.Qtype {\n\t\tcase dns.TypeAAAA:\n\t\t\tif !h.ipv6 {\n\t\t\t\t// See RFC 2308 section 2.2 which suggests that NODATA is indicated by setting the\n\t\t\t\t// RCODE to NOERROR along with zero entries in the response.\n\t\t\t\treply.SetRcode(req, dns.RcodeSuccess)\n\t\t\t\treply.SetReply(req)\n\t\t\t\thandled = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase dns.TypeA:\n\t\t\tvar err error\n\t\t\tvar addrs []net.IP\n\t\t\tcname := h.lookupCnameToHost(q.Name)\n\t\t\tif _, ok := h.hostToIP[cname]; ok {\n\t\t\t\taddrs = []net.IP{h.hostToIP[cname]}\n\t\t\t} else {\n\t\t\t\taddrs, err = net.DefaultResolver.LookupIP(ctx, \"ip\", cname)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.WithError(err).Debug(\"handleQuery lookup IP failed\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, ip := range addrs {\n\t\t\t\tvar a dns.RR\n\t\t\t\tipv6 := ip.To4() == nil\n\t\t\t\tif qtype == dns.TypeA && !ipv6 {\n\t\t\t\t\thdr.Rrtype = dns.TypeA\n\t\t\t\t\ta = &dns.A{\n\t\t\t\t\t\tHdr: hdr,\n\t\t\t\t\t\tA:   ip.To4(),\n\t\t\t\t\t}\n\t\t\t\t} else if qtype == dns.TypeAAAA && ipv6 {\n\t\t\t\t\thdr.Rrtype = dns.TypeAAAA\n\t\t\t\t\ta = &dns.AAAA{\n\t\t\t\t\t\tHdr:  hdr,\n\t\t\t\t\t\tAAAA: ip.To16(),\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treply.Answer = append(reply.Answer, a)\n\t\t\t\thandled = true\n\t\t\t}\n\t\tcase dns.TypeCNAME:\n\t\t\tcname := h.lookupCnameToHost(q.Name)\n\t\t\tvar err error\n\t\t\tif _, ok := h.hostToIP[cname]; !ok {\n\t\t\t\tcname, err = net.DefaultResolver.LookupCNAME(ctx, cname)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.WithError(err).Debug(\"handleQuery lookup CNAME failed\")\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tif cname != \"\" && cname != q.Name {\n\t\t\t\thdr.Rrtype = dns.TypeCNAME\n\t\t\t\ta := &dns.CNAME{\n\t\t\t\t\tHdr:    hdr,\n\t\t\t\t\tTarget: cname,\n\t\t\t\t}\n\t\t\t\treply.Answer = append(reply.Answer, a)\n\t\t\t\thandled = true\n\t\t\t}\n\t\tcase dns.TypeTXT:\n\t\t\ttxt, err := net.DefaultResolver.LookupTXT(ctx, q.Name)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Debug(\"handleQuery lookup TXT failed\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, s := range txt {\n\t\t\t\ta := &dns.TXT{\n\t\t\t\t\tHdr: hdr,\n\t\t\t\t}\n\t\t\t\t// Per RFC7208 3.3, when a TXT answer has multiple strings, the answer must be treated as\n\t\t\t\t// a single concatenated string. net.LookupTXT is pre-concatenating such answers, which\n\t\t\t\t// means we need to break it back up for this resolver to return a valid response.\n\t\t\t\ta.Txt = chunkify(s, 255)\n\t\t\t\treply.Answer = append(reply.Answer, a)\n\t\t\t\thandled = true\n\t\t\t}\n\t\tcase dns.TypeNS:\n\t\t\tns, err := net.DefaultResolver.LookupNS(ctx, q.Name)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Debug(\"handleQuery lookup NS failed\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, s := range ns {\n\t\t\t\tif s.Host != \"\" {\n\t\t\t\t\ta := &dns.NS{\n\t\t\t\t\t\tHdr: hdr,\n\t\t\t\t\t\tNs:  s.Host,\n\t\t\t\t\t}\n\t\t\t\t\treply.Answer = append(reply.Answer, a)\n\t\t\t\t\thandled = true\n\t\t\t\t}\n\t\t\t}\n\t\tcase dns.TypeMX:\n\t\t\tmx, err := net.DefaultResolver.LookupMX(ctx, q.Name)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Debugf(\"handleQuery lookup MX failed\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, s := range mx {\n\t\t\t\tif s.Host != \"\" {\n\t\t\t\t\ta := &dns.MX{\n\t\t\t\t\t\tHdr:        hdr,\n\t\t\t\t\t\tMx:         s.Host,\n\t\t\t\t\t\tPreference: s.Pref,\n\t\t\t\t\t}\n\t\t\t\t\treply.Answer = append(reply.Answer, a)\n\t\t\t\t\thandled = true\n\t\t\t\t}\n\t\t\t}\n\t\tcase dns.TypeSRV:\n\t\t\t_, addrs, err := net.DefaultResolver.LookupSRV(ctx, \"\", \"\", q.Name)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Debug(\"handleQuery lookup SRV failed\")\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\thdr.Rrtype = dns.TypeSRV\n\t\t\tfor _, addr := range addrs {\n\t\t\t\ta := &dns.SRV{\n\t\t\t\t\tHdr:      hdr,\n\t\t\t\t\tTarget:   addr.Target,\n\t\t\t\t\tPort:     addr.Port,\n\t\t\t\t\tPriority: addr.Priority,\n\t\t\t\t\tWeight:   addr.Weight,\n\t\t\t\t}\n\t\t\t\treply.Answer = append(reply.Answer, a)\n\t\t\t\thandled = true\n\t\t\t}\n\t\t}\n\t}\n\tif handled {\n\t\tif h.truncate {\n\t\t\treply.Truncate(truncateSize)\n\t\t}\n\t\tif err := w.WriteMsg(&reply); err != nil {\n\t\t\tlogrus.WithError(err).Debugf(\"handleQuery failed writing DNS reply\")\n\t\t}\n\n\t\treturn\n\t}\n\th.handleDefault(w, req)\n}\n\nfunc (h *Handler) handleDefault(w dns.ResponseWriter, req *dns.Msg) {\n\tlogrus.Tracef(\"handleDefault for %v\", req)\n\tfor _, client := range h.clients {\n\t\tfor _, srv := range h.clientConfig.Servers {\n\t\t\taddr := net.JoinHostPort(srv, h.clientConfig.Port)\n\t\t\treply, _, err := client.Exchange(req, addr)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Debugf(\"handleDefault failed to perform a synchronous query with upstream [%v]\", addr)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif h.truncate {\n\t\t\t\tlogrus.Tracef(\"handleDefault truncating reply: %v\", reply)\n\t\t\t\treply.Truncate(truncateSize)\n\t\t\t}\n\t\t\tif err = w.WriteMsg(reply); err != nil {\n\t\t\t\tlogrus.WithError(err).Debugf(\"handleDefault failed writing DNS reply to [%v]\", addr)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\tvar reply dns.Msg\n\treply.SetReply(req)\n\tif h.truncate {\n\t\tlogrus.Tracef(\"handleDefault truncating reply: %v\", reply)\n\t\treply.Truncate(truncateSize)\n\t}\n\tif err := w.WriteMsg(&reply); err != nil {\n\t\tlogrus.WithError(err).Debugf(\"handleDefault failed writing DNS reply\")\n\t}\n}\n\nfunc (h *Handler) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {\n\tswitch req.Opcode {\n\tcase dns.OpcodeQuery:\n\t\th.handleQuery(context.Background(), w, req)\n\tdefault:\n\t\th.handleDefault(w, req)\n\t}\n}\n\nfunc Start(opts ServerOptions) (*Server, error) {\n\tserver := &Server{}\n\tif opts.UDPPort > 0 {\n\t\tudpSrv, err := listenAndServe(UDP, opts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tserver.udp = udpSrv\n\t}\n\tif opts.TCPPort > 0 {\n\t\ttcpSrv, err := listenAndServe(TCP, opts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tserver.tcp = tcpSrv\n\t}\n\treturn server, nil\n}\n\nfunc listenAndServe(network Network, opts ServerOptions) (*dns.Server, error) {\n\tvar addr string\n\t// always enable reply truncate for UDP\n\tif network == UDP {\n\t\topts.HandlerOptions.TruncateReply = true\n\t\taddr = net.JoinHostPort(opts.Address, strconv.Itoa(opts.UDPPort))\n\t} else {\n\t\taddr = net.JoinHostPort(opts.Address, strconv.Itoa(opts.TCPPort))\n\t}\n\th, err := NewHandler(opts.HandlerOptions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts := &dns.Server{Net: string(network), Addr: addr, Handler: h}\n\tgo func() {\n\t\tlogrus.Debugf(\"Start %v DNS listening on: %v\", network, addr)\n\t\tif e := s.ListenAndServe(); e != nil {\n\t\t\tpanic(e)\n\t\t}\n\t}()\n\n\treturn s, nil\n}\n\nfunc chunkify(buffer string, limit int) []string {\n\tvar result []string\n\tfor buffer != \"\" {\n\t\tif len(buffer) < limit {\n\t\t\tlimit = len(buffer)\n\t\t}\n\t\tresult = append(result, buffer[:limit])\n\t\tbuffer = buffer[limit:]\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "pkg/hostagent/dns/dns_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage dns\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/foxcpp/go-mockdns\"\n\t\"github.com/miekg/dns\"\n\t\"gotest.tools/v3/assert\"\n\t\"gotest.tools/v3/assert/cmp\"\n)\n\nvar dnsResult *dns.Msg\n\nfunc TestNewHandler(t *testing.T) {\n\tt.Run(\"with upstream servers\", func(t *testing.T) {\n\t\tupstreamServers := []string{\"8.8.4.4\", \"1.1.1.1\", \"9.9.9.9\"}\n\t\topts := HandlerOptions{\n\t\t\tIPv6:            true,\n\t\t\tUpstreamServers: upstreamServers,\n\t\t\tStaticHosts: map[string]string{\n\t\t\t\t\"test.local\":  \"192.168.1.1\",\n\t\t\t\t\"alias.local\": \"test.local\",\n\t\t\t},\n\t\t}\n\t\th, err := NewHandler(opts)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, h != nil)\n\n\t\thandler := h.(*Handler)\n\t\tassert.Equal(t, handler.ipv6, true)\n\t\tassert.Equal(t, len(handler.clients), 2)\n\t\tassert.DeepEqual(t, handler.clientConfig.Servers, upstreamServers)\n\t\tassert.Equal(t, handler.hostToIP[\"test.local.\"].String(), \"192.168.1.1\")\n\t\tassert.Equal(t, handler.cnameToHost[\"alias.local.\"], \"test.local.\")\n\t})\n\n\tt.Run(\"without upstream servers on non-Windows\", func(t *testing.T) {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tt.Skip(\"Skipping on Windows\")\n\t\t}\n\t\topts := HandlerOptions{\n\t\t\tIPv6:        false,\n\t\t\tStaticHosts: map[string]string{},\n\t\t}\n\t\th, err := NewHandler(opts)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, h != nil)\n\n\t\thandler := h.(*Handler)\n\t\tassert.Equal(t, handler.ipv6, false)\n\t\tassert.Assert(t, handler.clientConfig != nil)\n\t})\n\n\tt.Run(\"without upstream servers on Windows\", func(t *testing.T) {\n\t\tif runtime.GOOS != \"windows\" {\n\t\t\tt.Skip(\"Skipping on non-Windows\")\n\t\t}\n\t\topts := HandlerOptions{\n\t\t\tIPv6:        false,\n\t\t\tStaticHosts: map[string]string{},\n\t\t}\n\t\th, err := NewHandler(opts)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, h != nil)\n\n\t\thandler := h.(*Handler)\n\t\tassert.Equal(t, handler.ipv6, false)\n\t\tassert.Assert(t, handler.clientConfig != nil)\n\t\t// Should use default fallback IPs on Windows\n\t\tassert.Assert(t, len(handler.clientConfig.Servers) > 0)\n\t})\n\n\tt.Run(\"with invalid upstream servers fallback\", func(t *testing.T) {\n\t\topts := HandlerOptions{\n\t\t\tIPv6:            true,\n\t\t\tUpstreamServers: []string{}, // empty should trigger default behavior\n\t\t\tStaticHosts:     map[string]string{},\n\t\t}\n\t\th, err := NewHandler(opts)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, h != nil)\n\t})\n\n\tt.Run(\"with static hosts IP and CNAME\", func(t *testing.T) {\n\t\topts := HandlerOptions{\n\t\t\tIPv6:            true,\n\t\t\tUpstreamServers: []string{\"8.8.8.8\"},\n\t\t\tStaticHosts: map[string]string{\n\t\t\t\t\"host1.local\": \"10.0.0.1\",\n\t\t\t\t\"host2.local\": \"10.0.0.2\",\n\t\t\t\t\"cname1\":      \"host1.local\",\n\t\t\t\t\"cname2\":      \"cname1\",\n\t\t\t},\n\t\t}\n\t\th, err := NewHandler(opts)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, h != nil)\n\n\t\thandler := h.(*Handler)\n\t\tassert.Equal(t, handler.hostToIP[\"host1.local.\"].String(), \"10.0.0.1\")\n\t\tassert.Equal(t, handler.hostToIP[\"host2.local.\"].String(), \"10.0.0.2\")\n\t\tassert.Equal(t, handler.cnameToHost[\"cname1.\"], \"host1.local.\")\n\t\tassert.Equal(t, handler.cnameToHost[\"cname2.\"], \"cname1.\")\n\t})\n\n\tt.Run(\"with truncate option\", func(t *testing.T) {\n\t\topts := HandlerOptions{\n\t\t\tIPv6:            false,\n\t\t\tUpstreamServers: []string{\"1.1.1.1\"},\n\t\t\tTruncateReply:   true,\n\t\t\tStaticHosts:     map[string]string{},\n\t\t}\n\t\th, err := NewHandler(opts)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, h != nil)\n\n\t\thandler := h.(*Handler)\n\t\tassert.Equal(t, handler.truncate, true)\n\t})\n}\n\nfunc TestDNSRecords(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\t// \"On Windows, the resolver always uses C library functions, such as GetAddrInfo and DnsQuery.\"\n\t\tt.Skip()\n\t}\n\n\tsrv, err := mockdns.NewServerWithLogger(map[string]mockdns.Zone{\n\t\t\"onerecord.com.\": {\n\t\t\tTXT: []string{\"My txt record\"},\n\t\t},\n\t\t\"multistringrecord.com.\": {\n\t\t\tTXT: []string{\"123456789012345678901234567890123456789012345678901234567890\" +\n\t\t\t\t\"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\" +\n\t\t\t\t\"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\" +\n\t\t\t\t\"12345678901234567890123456789012345678901234567890\"},\n\t\t},\n\t\t\"multiplerecords.com.\": {\n\t\t\tTXT: []string{\"record 1\", \"record 2\"},\n\t\t},\n\t}, log.New(io.Discard, \"mockdns server: \", log.LstdFlags), false)\n\tassert.NilError(t, err)\n\tdefer srv.Close()\n\n\tsrv.PatchNet(net.DefaultResolver)\n\tdefer mockdns.UnpatchNet(net.DefaultResolver)\n\tw := new(TestResponseWriter)\n\toptions := HandlerOptions{\n\t\tIPv6: true,\n\t\tStaticHosts: map[string]string{\n\t\t\t\"MY.DOMAIN.COM\":      \"192.168.0.23\",\n\t\t\t\"host.lima.internal\": \"10.10.0.34\",\n\t\t\t\"my.host\":            \"host.lima.internal\",\n\t\t\t\"default\":            \"my.domain.com\",\n\t\t\t\"cycle1.example.com\": \"cycle2.example.com\",\n\t\t\t\"cycle2.example.com\": \"cycle1.example.com\",\n\t\t\t\"self.example.com\":   \"self.example.com\",\n\t\t},\n\t}\n\n\th, err := NewHandler(options)\n\tassert.NilError(t, err)\n\n\tregexMatch := func(value string, pattern string) cmp.Comparison {\n\t\treturn func() cmp.Result {\n\t\t\tre := regexp.MustCompile(pattern)\n\t\t\tif re.MatchString(value) {\n\t\t\t\treturn cmp.ResultSuccess\n\t\t\t}\n\t\t\treturn cmp.ResultFailure(\n\t\t\t\tfmt.Sprintf(\"%q did not match pattern %q\", value, pattern))\n\t\t}\n\t}\n\tt.Run(\"test TXT records\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\ttestDomain        string\n\t\t\texpectedTXTRecord string\n\t\t}{\n\t\t\t{testDomain: \"onerecord.com\", expectedTXTRecord: `onerecord.com.\\s+5\\s+IN\\s+TXT\\s+\"My txt record\"`},\n\t\t\t{testDomain: \"multistringrecord.com\", expectedTXTRecord: `multistringrecord.com.\\s+5\\s+IN\\s+TXT\\s+\"123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345\" \"67890123456789012345678901234567890\"`},\n\t\t\t{testDomain: \"multiplerecords.com\", expectedTXTRecord: `multiplerecords.com.\\s+5\\s+IN\\s+TXT\\s+\"record 1\"\\nmultiplerecords.com.\\s*5\\s*IN\\s*TXT\\s*\"record 2\"`},\n\t\t}\n\n\t\tfor _, tc := range tests {\n\t\t\treq := new(dns.Msg)\n\t\t\treq.SetQuestion(dns.Fqdn(tc.testDomain), dns.TypeTXT)\n\t\t\th.ServeDNS(w, req)\n\t\t\tassert.Assert(t, regexMatch(dnsResult.String(), tc.expectedTXTRecord))\n\t\t}\n\t})\n\n\tt.Run(\"test A records\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\ttestDomain      string\n\t\t\texpectedARecord string\n\t\t}{\n\t\t\t{testDomain: \"my.domain.com\", expectedARecord: `my.domain.com.\\s+5\\s+IN\\s+A\\s+192.168.0.23`},\n\t\t\t{testDomain: \"host.lima.internal\", expectedARecord: `host.lima.internal.\\s+5\\s+IN\\s+A\\s+10.10.0.34`},\n\t\t}\n\n\t\tfor _, tc := range tests {\n\t\t\treq := new(dns.Msg)\n\t\t\treq.SetQuestion(dns.Fqdn(tc.testDomain), dns.TypeA)\n\t\t\th.ServeDNS(w, req)\n\t\t\tassert.Assert(t, regexMatch(dnsResult.String(), tc.expectedARecord))\n\t\t}\n\t})\n\n\tt.Run(\"test CNAME records\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\ttestDomain    string\n\t\t\texpectedCNAME string\n\t\t}{\n\t\t\t{testDomain: \"my.host\", expectedCNAME: `my.host.\\s+5\\s+IN\\s+CNAME\\s+host.lima.internal.`},\n\t\t\t{testDomain: \"default\", expectedCNAME: `default.\\s+5\\s+IN\\s+CNAME\\s+my.domain.com.`},\n\t\t}\n\n\t\tfor _, tc := range tests {\n\t\t\treq := new(dns.Msg)\n\t\t\treq.SetQuestion(dns.Fqdn(tc.testDomain), dns.TypeCNAME)\n\t\t\th.ServeDNS(w, req)\n\t\t\tassert.Assert(t, regexMatch(dnsResult.String(), tc.expectedCNAME))\n\t\t}\n\t})\n\n\tt.Run(\"test cyclic CNAME records\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\ttestDomain    string\n\t\t\texpectedCNAME string\n\t\t}{\n\t\t\t{testDomain: \"cycle1.example.com\", expectedCNAME: `cycle1.example.com.`},\n\t\t\t{testDomain: \"self.example.com\", expectedCNAME: `self.example.com.`},\n\t\t}\n\n\t\tfor _, tc := range tests {\n\t\t\treq := new(dns.Msg)\n\t\t\treq.SetQuestion(dns.Fqdn(tc.testDomain), dns.TypeCNAME)\n\t\t\th.ServeDNS(w, req)\n\t\t\tassert.Assert(t, regexMatch(dnsResult.String(), tc.expectedCNAME))\n\t\t}\n\t})\n}\n\ntype TestResponseWriter struct{}\n\n// LocalAddr returns the net.Addr of the server\nfunc (r TestResponseWriter) LocalAddr() net.Addr {\n\treturn new(TestAddr)\n}\n\n// RemoteAddr returns the net.Addr of the client that sent the current request.\nfunc (r TestResponseWriter) RemoteAddr() net.Addr {\n\treturn new(TestAddr)\n}\n\n// Network returns the value of the Net field of the Server (e.g., \"tcp\", \"tcp-tls\").\nfunc (r TestResponseWriter) Network() string {\n\treturn \"\"\n}\n\n// WriteMsg writes a reply back to the client.\nfunc (r TestResponseWriter) WriteMsg(newMsg *dns.Msg) error {\n\tdnsResult = newMsg\n\treturn nil\n}\n\n// Write writes a raw buffer back to the client.\nfunc (r TestResponseWriter) Write([]byte) (int, error) {\n\treturn 0, nil\n}\n\n// Close closes the connection.\nfunc (r TestResponseWriter) Close() error {\n\treturn nil\n}\n\n// TsigStatus returns the status of the Tsig.\nfunc (r TestResponseWriter) TsigStatus() error {\n\treturn nil\n}\n\n// TsigTimersOnly sets the tsig timers only boolean.\nfunc (r TestResponseWriter) TsigTimersOnly(bool) {\n}\n\n// Hijack lets the caller take over the connection.\n// After a call to Hijack(), the DNS package will not do anything with the connection.\nfunc (r TestResponseWriter) Hijack() {\n}\n\ntype TestAddr struct{}\n\nfunc (r TestAddr) Network() string {\n\treturn \"\"\n}\n\nfunc (r TestAddr) String() string {\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/hostagent/events/events.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"time\"\n)\n\ntype Status struct {\n\tRunning bool `json:\"running,omitempty\"`\n\t// When Degraded is true, Running must be true as well\n\tDegraded bool `json:\"degraded,omitempty\"`\n\t// When Exiting is true, Running must be false\n\tExiting bool `json:\"exiting,omitempty\"`\n\n\tErrors []string `json:\"errors,omitempty\"`\n\n\tSSHLocalPort int `json:\"sshLocalPort,omitempty\"`\n\n\t// Cloud-init progress information\n\tCloudInitProgress *CloudInitProgress `json:\"cloudInitProgress,omitempty\"`\n\n\t// Port forwarding event\n\tPortForward *PortForwardEvent `json:\"portForward,omitempty\"`\n\n\t// Vsock forwarder event\n\tVsock *VsockEvent `json:\"vsock,omitempty\"`\n}\n\ntype CloudInitProgress struct {\n\t// Current log line from cloud-init\n\tLogLine string `json:\"logLine,omitempty\"`\n\t// Whether cloud-init has completed\n\tCompleted bool `json:\"completed,omitempty\"`\n\t// Whether cloud-init monitoring is active\n\tActive bool `json:\"active,omitempty\"`\n}\n\ntype PortForwardEventType string\n\nconst (\n\tPortForwardEventForwarding    PortForwardEventType = \"forwarding\"\n\tPortForwardEventNotForwarding PortForwardEventType = \"not-forwarding\"\n\tPortForwardEventStopping      PortForwardEventType = \"stopping\"\n\tPortForwardEventFailed        PortForwardEventType = \"failed\"\n)\n\ntype PortForwardEvent struct {\n\tType      PortForwardEventType `json:\"type\"`\n\tProtocol  string               `json:\"protocol,omitempty\"`\n\tGuestAddr string               `json:\"guestAddr,omitempty\"`\n\tHostAddr  string               `json:\"hostAddr,omitempty\"`\n\tError     string               `json:\"error,omitempty\"`\n}\n\ntype VsockEventType string\n\nconst (\n\tVsockEventStarted VsockEventType = \"started\"\n\tVsockEventSkipped VsockEventType = \"skipped\"\n\tVsockEventFailed  VsockEventType = \"failed\"\n)\n\ntype VsockEvent struct {\n\tType      VsockEventType `json:\"type\"`\n\tHostAddr  string         `json:\"hostAddr,omitempty\"`\n\tVsockPort int            `json:\"vsockPort,omitempty\"`\n\tReason    string         `json:\"reason,omitempty\"`\n}\n\ntype Event struct {\n\tTime   time.Time `json:\"time,omitempty\"`\n\tStatus Status    `json:\"status,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/hostagent/events/watcher.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage events\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/nxadm/tail\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/logrusutil\"\n)\n\nfunc Watch(ctx context.Context, haStdoutPath, haStderrPath string, begin time.Time, propagateStderr bool, onEvent func(Event) bool) error {\n\thaStdoutTail, err := tail.TailFile(haStdoutPath,\n\t\ttail.Config{\n\t\t\tFollow:    true,\n\t\t\tReOpen:    true,\n\t\t\tMustExist: false,\n\t\t\tLogger:    logrus.StandardLogger(),\n\t\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = haStdoutTail.Stop()\n\t\t// Do NOT call haStdoutTail.Cleanup(), it prevents the process from ever tailing the file again\n\t}()\n\n\thaStderrTail, err := tail.TailFile(haStderrPath,\n\t\ttail.Config{\n\t\t\tFollow:    true,\n\t\t\tReOpen:    true,\n\t\t\tMustExist: false,\n\t\t\tLogger:    logrus.StandardLogger(),\n\t\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t_ = haStderrTail.Stop()\n\t\t// Do NOT call haStderrTail.Cleanup(), it prevents the process from ever tailing the file again\n\t}()\n\nloop:\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tbreak loop\n\t\tcase line := <-haStdoutTail.Lines:\n\t\t\tif line == nil {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t\tif line.Err != nil {\n\t\t\t\tlogrus.Error(line.Err)\n\t\t\t}\n\t\t\tif line.Text == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar ev Event\n\t\t\tif err := json.Unmarshal([]byte(line.Text), &ev); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmarshal %q as %T: %w\", line.Text, ev, err)\n\t\t\t}\n\t\t\tlogrus.WithField(\"event\", ev).Debugf(\"received an event\")\n\t\t\tif !begin.IsZero() && ev.Time.Before(begin) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif stop := onEvent(ev); stop {\n\t\t\t\treturn nil\n\t\t\t}\n\t\tcase line := <-haStderrTail.Lines:\n\t\t\tif line == nil {\n\t\t\t\tbreak loop\n\t\t\t}\n\t\t\tif line.Err != nil {\n\t\t\t\tlogrus.Error(line.Err)\n\t\t\t}\n\t\t\tif propagateStderr {\n\t\t\t\tlogrusutil.PropagateJSON(logrus.StandardLogger(), []byte(line.Text), \"[hostagent] \", begin)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/hostagent/hostagent.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/lima-vm/sshocker/pkg/ssh\"\n\t\"github.com/sethvargo/go-password/password\"\n\t\"github.com/sirupsen/logrus\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/cidata\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/freeport\"\n\tguestagentapi \"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n\tguestagentclient \"github.com/lima-vm/lima/v2/pkg/guestagent/api/client\"\n\thostagentapi \"github.com/lima-vm/lima/v2/pkg/hostagent/api\"\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/dns\"\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance/hostname\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/portfwd\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/version/versionutil\"\n)\n\ntype HostAgent struct {\n\tinstConfig        *limatype.LimaYAML\n\tsshLocalPort      int\n\tudpDNSLocalPort   int\n\ttcpDNSLocalPort   int\n\tinstDir           string\n\tinstName          string\n\tinstSSHAddress    string\n\tsshConfig         *ssh.SSHConfig\n\tportForwarder     *portForwarder // legacy SSH port forwarder\n\tgrpcPortForwarder *portfwd.Forwarder\n\n\tonClose   []func() error // LIFO\n\tonCloseMu sync.Mutex\n\n\tdriver   driver.Driver\n\tsignalCh chan os.Signal\n\n\teventEnc   *json.Encoder\n\teventEncMu sync.Mutex\n\n\tvSockPort  int\n\tvirtioPort string\n\tiid        string // instance ID, changes on every boot\n\n\tclientMu sync.RWMutex\n\tclient   *guestagentclient.GuestAgentClient\n\n\tguestAgentAliveCh     chan struct{} // closed on establishing the connection\n\tguestAgentAliveChOnce sync.Once\n\n\tshowProgress bool // whether to show cloud-init progress\n\n\tstatusMu      sync.RWMutex\n\tcurrentStatus events.Status\n}\n\ntype options struct {\n\tguestAgentBinary string\n\tnerdctlArchive   string // local path, not URL\n\tshowProgress     bool\n}\n\ntype Opt func(*options) error\n\nfunc WithGuestAgentBinary(s string) Opt {\n\treturn func(o *options) error {\n\t\to.guestAgentBinary = s\n\t\treturn nil\n\t}\n}\n\nfunc WithNerdctlArchive(s string) Opt {\n\treturn func(o *options) error {\n\t\to.nerdctlArchive = s\n\t\treturn nil\n\t}\n}\n\nfunc WithCloudInitProgress(enabled bool) Opt {\n\treturn func(o *options) error {\n\t\to.showProgress = enabled\n\t\treturn nil\n\t}\n}\n\n// New creates the HostAgent.\n//\n// stdout is for emitting JSON lines of Events.\nfunc New(ctx context.Context, instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt) (*HostAgent, error) {\n\tvar o options\n\tfor _, f := range opts {\n\t\tif err := f(&o); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar limaVersion string\n\tlimaVersionFile := filepath.Join(inst.Dir, filenames.LimaVersion)\n\tif b, err := os.ReadFile(limaVersionFile); err == nil {\n\t\tlimaVersion = strings.TrimSpace(string(b))\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\tlogrus.WithError(err).Warnf(\"Failed to read %q\", limaVersionFile)\n\t}\n\n\t// inst.Config is loaded with FillDefault() already, so no need to care about nil pointers.\n\tsshLocalPort, err := determineSSHLocalPort(*inst.Config.SSH.LocalPort, instName, limaVersion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar udpDNSLocalPort, tcpDNSLocalPort int\n\tif *inst.Config.HostResolver.Enabled {\n\t\tudpDNSLocalPort, err = freeport.UDP()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttcpDNSLocalPort, err = freeport.TCP()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, sshLocalPort)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\tsshLocalPort = inst.SSHLocalPort\n\n\tvSockPort := limaDriver.Info().VsockPort\n\tvirtioPort := limaDriver.Info().VirtioPort\n\tnoCloudInit := limaDriver.Info().Features.NoCloudInit\n\trosettaEnabled := limaDriver.Info().Features.RosettaEnabled\n\trosettaBinFmt := limaDriver.Info().Features.RosettaBinFmt\n\n\t// Disable Rosetta in Plain mode\n\tif *inst.Config.Plain {\n\t\trosettaEnabled = false\n\t\trosettaBinFmt = false\n\t}\n\n\tif err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil {\n\t\treturn nil, err\n\t}\n\tiid, err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort, noCloudInit, rosettaEnabled, rosettaBinFmt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsshExe, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsshOpts, err := sshutil.SSHOpts(\n\t\tctx,\n\t\tsshExe,\n\t\tinst.Dir,\n\t\t*inst.Config.User.Name,\n\t\t*inst.Config.SSH.LoadDotSSHPubKeys,\n\t\t*inst.Config.SSH.ForwardAgent,\n\t\t*inst.Config.SSH.ForwardX11,\n\t\t*inst.Config.SSH.ForwardX11Trusted)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err = writeSSHConfigFile(\"ssh\", inst.Name, inst.Dir, inst.SSHAddress, sshLocalPort, sshOpts); err != nil {\n\t\treturn nil, err\n\t}\n\tsshConfig := &ssh.SSHConfig{\n\t\tAdditionalArgs: sshutil.SSHArgsFromOpts(sshOpts),\n\t}\n\n\tignoreTCP := false\n\tignoreUDP := false\n\tfor _, rule := range inst.Config.PortForwards {\n\t\tif rule.Ignore && rule.GuestPortRange[0] == 1 && rule.GuestPortRange[1] == 65535 {\n\t\t\tswitch rule.Proto {\n\t\t\tcase limatype.ProtoTCP:\n\t\t\t\tignoreTCP = true\n\t\t\t\tlogrus.Info(\"TCP port forwarding is disabled (except for SSH)\")\n\t\t\tcase limatype.ProtoUDP:\n\t\t\t\tignoreUDP = true\n\t\t\t\tlogrus.Info(\"UDP port forwarding is disabled\")\n\t\t\tcase limatype.ProtoAny:\n\t\t\t\tignoreTCP = true\n\t\t\t\tignoreUDP = true\n\t\t\t\tlogrus.Info(\"TCP (except for SSH) and UDP port forwarding is disabled\")\n\t\t\t}\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\trules := make([]limatype.PortForward, 0, 3+len(inst.Config.PortForwards))\n\t// Block ports 22 and sshLocalPort on all IPs\n\tfor _, port := range []int{sshGuestPort, sshLocalPort} {\n\t\trule := limatype.PortForward{GuestIP: net.IPv4zero, GuestPort: port, Ignore: true}\n\t\tlimayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Config.User, inst.Param)\n\t\trules = append(rules, rule)\n\t}\n\trules = append(rules, inst.Config.PortForwards...)\n\t// Default forwards for all non-privileged ports from \"127.0.0.1\" and \"::1\"\n\trule := limatype.PortForward{}\n\tlimayaml.FillPortForwardDefaults(&rule, inst.Dir, inst.Config.User, inst.Param)\n\trules = append(rules, rule)\n\n\ta := &HostAgent{\n\t\tinstConfig:        inst.Config,\n\t\tsshLocalPort:      sshLocalPort,\n\t\tudpDNSLocalPort:   udpDNSLocalPort,\n\t\ttcpDNSLocalPort:   tcpDNSLocalPort,\n\t\tinstDir:           inst.Dir,\n\t\tinstName:          instName,\n\t\tinstSSHAddress:    inst.SSHAddress,\n\t\tsshConfig:         sshConfig,\n\t\tdriver:            limaDriver,\n\t\tsignalCh:          signalCh,\n\t\teventEnc:          json.NewEncoder(stdout),\n\t\tvSockPort:         vSockPort,\n\t\tvirtioPort:        virtioPort,\n\t\tiid:               iid,\n\t\tguestAgentAliveCh: make(chan struct{}),\n\t\tshowProgress:      o.showProgress,\n\t}\n\ta.grpcPortForwarder = portfwd.NewPortForwarder(rules, ignoreTCP, ignoreUDP, func(ev *events.PortForwardEvent) {\n\t\ta.emitPortForwardEvent(context.Background(), ev)\n\t})\n\ta.portForwarder = newPortForwarder(sshConfig, a.sshAddressPort, rules, ignoreTCP, inst.VMType, func(ev *events.PortForwardEvent) {\n\t\ta.emitPortForwardEvent(context.Background(), ev)\n\t})\n\n\t// Set up vsock event callback if the driver supports it\n\tif vsockEmitter, ok := limaDriver.Driver.(driver.VsockEventEmitter); ok {\n\t\tvsockEmitter.SetVsockEventCallback(func(ev *events.VsockEvent) {\n\t\t\ta.emitVsockEvent(context.Background(), ev)\n\t\t})\n\t}\n\n\treturn a, nil\n}\n\nfunc writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error {\n\tif instDir == \"\" {\n\t\treturn fmt.Errorf(\"directory is unknown for the instance %q\", instName)\n\t}\n\tb := bytes.NewBufferString(`# This SSH config file can be passed to 'ssh -F'.\n# This file is created by Lima, but not used by Lima itself currently.\n# Modifications to this file will be lost on restarting the Lima instance.\n`)\n\tif runtime.GOOS == \"windows\" {\n\t\t// Remove ControlMaster, ControlPath, and ControlPersist options,\n\t\t// because Cygwin-based SSH clients do not support multiplexing when executing commands.\n\t\t// References:\n\t\t//   https://inbox.sourceware.org/cygwin/c98988a5-7e65-4282-b2a1-bb8e350d5fab@acm.org/T/\n\t\t//   https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible\n\t\t// By removing these options:\n\t\t//   - Avoids execution failures when the control master is not yet available.\n\t\t//   - Prevents error messages such as:\n\t\t//     > mux_client_request_session: read from master failed: Connection reset by peer\n\t\t//     > ControlSocket ....sock already exists, disabling multiplexing\n\t\t// Only remove these options when writing the SSH config file and executing `limactl shell`, since multiplexing seems to work with port forwarding.\n\t\tsshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts)\n\t}\n\tif err := sshutil.Format(b, sshPath, instName, sshutil.FormatConfig,\n\t\tappend(sshOpts,\n\t\t\tfmt.Sprintf(\"Hostname=%s\", instSSHAddress),\n\t\t\tfmt.Sprintf(\"Port=%d\", sshLocalPort),\n\t\t)); err != nil {\n\t\treturn err\n\t}\n\tfileName := filepath.Join(instDir, filenames.SSHConfig)\n\treturn os.WriteFile(fileName, b.Bytes(), 0o600)\n}\n\nfunc determineSSHLocalPort(confLocalPort int, instName, limaVersion string) (int, error) {\n\tif confLocalPort > 0 {\n\t\treturn confLocalPort, nil\n\t}\n\tif confLocalPort < 0 {\n\t\treturn 0, fmt.Errorf(\"invalid ssh local port %d\", confLocalPort)\n\t}\n\tif versionutil.LessThan(limaVersion, \"2.0.0\") && instName == \"default\" {\n\t\t// use hard-coded value for \"default\" instance, for backward compatibility\n\t\treturn 60022, nil\n\t}\n\tsshLocalPort, err := freeport.TCP()\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to find a free port, try setting `ssh.localPort` manually: %w\", err)\n\t}\n\treturn sshLocalPort, nil\n}\n\nfunc (a *HostAgent) emitEvent(_ context.Context, ev events.Event) {\n\ta.eventEncMu.Lock()\n\tdefer a.eventEncMu.Unlock()\n\n\ta.statusMu.Lock()\n\ta.currentStatus = ev.Status\n\ta.statusMu.Unlock()\n\n\tif ev.Time.IsZero() {\n\t\tev.Time = time.Now()\n\t}\n\tif err := a.eventEnc.Encode(ev); err != nil {\n\t\tlogrus.WithField(\"event\", ev).WithError(err).Error(\"failed to emit an event\")\n\t}\n}\n\nfunc (a *HostAgent) emitCloudInitProgressEvent(ctx context.Context, progress *events.CloudInitProgress) {\n\ta.statusMu.RLock()\n\tcurrentStatus := a.currentStatus\n\ta.statusMu.RUnlock()\n\n\tcurrentStatus.CloudInitProgress = progress\n\n\tev := events.Event{Status: currentStatus}\n\ta.emitEvent(ctx, ev)\n}\n\nfunc (a *HostAgent) emitPortForwardEvent(ctx context.Context, pfEvent *events.PortForwardEvent) {\n\ta.statusMu.RLock()\n\tcurrentStatus := a.currentStatus\n\ta.statusMu.RUnlock()\n\n\tcurrentStatus.PortForward = pfEvent\n\n\tev := events.Event{Status: currentStatus}\n\ta.emitEvent(ctx, ev)\n}\n\nfunc (a *HostAgent) emitVsockEvent(ctx context.Context, vsockEvent *events.VsockEvent) {\n\ta.statusMu.RLock()\n\tcurrentStatus := a.currentStatus\n\ta.statusMu.RUnlock()\n\n\tcurrentStatus.Vsock = vsockEvent\n\n\tev := events.Event{Status: currentStatus}\n\ta.emitEvent(ctx, ev)\n}\n\nfunc generatePassword(length int) (string, error) {\n\t// avoid any special symbols, to make it easier to copy/paste\n\treturn password.Generate(length, length/4, 0, false, false)\n}\n\nfunc (a *HostAgent) Run(ctx context.Context) error {\n\tdefer func() {\n\t\texitingEv := events.Event{\n\t\t\tStatus: events.Status{\n\t\t\t\tExiting: true,\n\t\t\t},\n\t\t}\n\t\ta.emitEvent(ctx, exitingEv)\n\t}()\n\tadjustNofileRlimit()\n\n\tif limayaml.FirstUsernetIndex(a.instConfig) == -1 && *a.instConfig.HostResolver.Enabled {\n\t\thosts := a.instConfig.HostResolver.Hosts\n\t\tif hosts == nil {\n\t\t\thosts = make(map[string]string)\n\t\t}\n\t\thosts[\"host.lima.internal\"] = networks.SlirpGateway\n\t\tname := hostname.FromInstName(a.instName) // TODO: support customization\n\t\thosts[name] = networks.SlirpIPAddress\n\t\tsrvOpts := dns.ServerOptions{\n\t\t\tUDPPort: a.udpDNSLocalPort,\n\t\t\tTCPPort: a.tcpDNSLocalPort,\n\t\t\tAddress: \"127.0.0.1\",\n\t\t\tHandlerOptions: dns.HandlerOptions{\n\t\t\t\tIPv6:        *a.instConfig.HostResolver.IPv6,\n\t\t\t\tStaticHosts: hosts,\n\t\t\t},\n\t\t}\n\t\tdnsServer, err := dns.Start(srvOpts)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"cannot start DNS server: %w\", err)\n\t\t}\n\t\tdefer dnsServer.Shutdown()\n\t}\n\n\terrCh, err := a.driver.Start(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := a.driver.AdditionalSetupForSSH(ctx); err != nil {\n\t\treturn err\n\t}\n\n\t// WSL instance SSH address isn't known until after VM start\n\tif a.driver.Info().Features.DynamicSSHAddress {\n\t\tsshAddr, err := a.driver.SSHAddress(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta.instSSHAddress = sshAddr\n\t}\n\n\tif a.instConfig.Video.Display != nil && *a.instConfig.Video.Display == \"vnc\" {\n\t\tvncdisplay, vncoptions, _ := strings.Cut(*a.instConfig.Video.VNC.Display, \",\")\n\t\tvnchost, vncnum, err := net.SplitHostPort(vncdisplay)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tn, err := strconv.Atoi(vncnum)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvncport := strconv.Itoa(5900 + n)\n\t\tvncpwdfile := filepath.Join(a.instDir, filenames.VNCPasswordFile)\n\t\tvncpasswd, err := generatePassword(8)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := a.driver.ChangeDisplayPassword(ctx, vncpasswd); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := os.WriteFile(vncpwdfile, []byte(vncpasswd), 0o600); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif strings.Contains(vncoptions, \"to=\") {\n\t\t\tvncport, err = a.driver.DisplayConnection(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tp, err := strconv.Atoi(vncport)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvncnum = strconv.Itoa(p - 5900)\n\t\t\tvncdisplay = net.JoinHostPort(vnchost, vncnum)\n\t\t}\n\t\tvncfile := filepath.Join(a.instDir, filenames.VNCDisplayFile)\n\t\tif err := os.WriteFile(vncfile, []byte(vncdisplay), 0o600); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvncurl := \"vnc://\" + net.JoinHostPort(vnchost, vncport)\n\t\tlogrus.Infof(\"VNC server running at %s <%s>\", vncdisplay, vncurl)\n\t\tlogrus.Infof(\"VNC Display: `%s`\", vncfile)\n\t\tlogrus.Infof(\"VNC Password: `%s`\", vncpwdfile)\n\t}\n\n\tif a.driver.Info().Features.CanRunGUI {\n\t\tgo func() {\n\t\t\terr = a.startRoutinesAndWait(ctx, errCh)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Error(err)\n\t\t\t}\n\t\t}()\n\t\treturn a.driver.RunGUI()\n\t}\n\treturn a.startRoutinesAndWait(ctx, errCh)\n}\n\nfunc (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error) error {\n\tstBase := events.Status{\n\t\tSSHLocalPort: a.sshLocalPort,\n\t}\n\tstBooting := stBase\n\ta.emitEvent(ctx, events.Event{Status: stBooting})\n\tctxHA, cancelHA := context.WithCancel(ctx)\n\tgo func() {\n\t\tstRunning := stBase\n\t\tif haErr := a.startHostAgentRoutines(ctxHA); haErr != nil {\n\t\t\tstRunning.Degraded = true\n\t\t\tstRunning.Errors = append(stRunning.Errors, haErr.Error())\n\t\t}\n\t\tstRunning.Running = true\n\t\ta.emitEvent(ctx, events.Event{Status: stRunning})\n\t}()\n\t// wait for either the driver to stop or a signal to shut down\n\tselect {\n\tcase driverErr := <-errCh:\n\t\tlogrus.Infof(\"Driver stopped due to error: %q\", driverErr)\n\tcase sig := <-a.signalCh:\n\t\tlogrus.Infof(\"Received %s, shutting down the host agent\", osutil.SignalName(sig))\n\t}\n\t// close the host agent routines before cancelling the context\n\tif closeErr := a.close(); closeErr != nil {\n\t\tlogrus.WithError(closeErr).Warn(\"an error during shutting down the host agent\")\n\t}\n\tcancelHA()\n\treturn a.driver.Stop(ctx)\n}\n\nfunc (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {\n\tinfo := &hostagentapi.Info{\n\t\tAutoStartedIdentifier: autostart.AutoStartedIdentifier(),\n\t\tSSHLocalPort:          a.sshLocalPort,\n\t}\n\treturn info, nil\n}\n\nfunc (a *HostAgent) sshAddressPort() (sshAddress string, sshPort int) {\n\tsshAddress = a.instSSHAddress\n\tsshPort = a.sshLocalPort\n\treturn sshAddress, sshPort\n}\n\nfunc (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {\n\tif *a.instConfig.Plain {\n\t\tmsg := \"Running in plain mode. Mounts, dynamic port forwarding, containerd, etc. will be ignored. Guest agent will not be running.\"\n\t\tfor _, port := range a.instConfig.PortForwards {\n\t\t\tif port.Static {\n\t\t\t\tmsg += \" Static port forwarding is allowed.\" //nolint:modernize // stringsbuilder is not needed\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tlogrus.Info(msg)\n\t}\n\ta.cleanUp(func() error {\n\t\t// Skip ExitMaster when the control socket does not exist.\n\t\t// On Windows, the ControlMaster is used only for SSH port forwarding.\n\t\tif !sshutil.IsControlMasterExisting(a.instDir) {\n\t\t\treturn nil\n\t\t}\n\t\tlogrus.Debugf(\"shutting down the SSH master\")\n\t\tif exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil {\n\t\t\tlogrus.WithError(exitMasterErr).Warn(\"failed to exit SSH master\")\n\t\t}\n\t\treturn nil\n\t})\n\tvar errs []error\n\tif err := a.waitForRequirements(\"essential\", a.essentialRequirements()); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\tif *a.instConfig.SSH.ForwardAgent {\n\t\tfaScript := `#!/bin/bash\nset -eux -o pipefail\nsudo mkdir -p -m 700 /run/host-services\nsudo ln -sf \"${SSH_AUTH_SOCK}\" /run/host-services/ssh-auth.sock\nsudo chown -R \"${USER}\" /run/host-services`\n\t\tfaDesc := \"linking ssh auth socket to static location /run/host-services/ssh-auth.sock\"\n\t\tstdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc)\n\t\tlogrus.Debugf(\"stdout=%q, stderr=%q, err=%v\", stdout, stderr, err)\n\t\tif err != nil {\n\t\t\terrs = append(errs, fmt.Errorf(\"stdout=%q, stderr=%q: %w\", stdout, stderr, err))\n\t\t}\n\t}\n\tif *a.instConfig.MountType == limatype.REVSSHFS && !*a.instConfig.Plain {\n\t\tmounts, err := a.setupMounts(ctx)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t\ta.cleanUp(func() error {\n\t\t\tvar unmountErrs []error\n\t\t\tfor _, m := range mounts {\n\t\t\t\tif unmountErr := m.close(); unmountErr != nil {\n\t\t\t\t\tunmountErrs = append(unmountErrs, unmountErr)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn errors.Join(unmountErrs...)\n\t\t})\n\t}\n\tif len(a.instConfig.AdditionalDisks) > 0 {\n\t\ta.cleanUp(func() error {\n\t\t\tvar unlockErrs []error\n\t\t\tfor _, d := range a.instConfig.AdditionalDisks {\n\t\t\t\tdisk, inspectErr := store.InspectDisk(d.Name, d.FSType)\n\t\t\t\tif inspectErr != nil {\n\t\t\t\t\tunlockErrs = append(unlockErrs, inspectErr)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlogrus.Infof(\"Unmounting disk %q\", disk.Name)\n\t\t\t\tif unlockErr := disk.Unlock(); unlockErr != nil {\n\t\t\t\t\tunlockErrs = append(unlockErrs, unlockErr)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn errors.Join(unlockErrs...)\n\t\t})\n\t}\n\n\tstaticPortForwards := a.separateStaticPortForwards()\n\ta.addStaticPortForwardsFromList(ctx, staticPortForwards)\n\n\thasGuestAgentDaemon := !*a.instConfig.Plain && *a.instConfig.OS == limatype.LINUX\n\tif hasGuestAgentDaemon {\n\t\tgo a.watchGuestAgentEvents(ctx)\n\t\tgo a.startTimeSync(ctx)\n\t\tif a.showProgress {\n\t\t\tcloudInitDone := make(chan struct{})\n\t\t\tgo func() {\n\t\t\t\ttimeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)\n\t\t\t\tdefer cancel()\n\n\t\t\t\ta.watchCloudInitProgress(timeoutCtx)\n\t\t\t\tclose(cloudInitDone)\n\t\t\t}()\n\n\t\t\tgo func() {\n\t\t\t\t<-cloudInitDone\n\t\t\t\tlogrus.Debug(\"Cloud-init monitoring completed, VM is fully ready\")\n\t\t\t}()\n\t\t}\n\t}\n\tif err := a.waitForRequirements(\"optional\", a.optionalRequirements()); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\tif hasGuestAgentDaemon {\n\t\tlogrus.Info(\"Waiting for the guest agent to be running\")\n\t\tselect {\n\t\tcase <-a.guestAgentAliveCh:\n\t\t\t// NOP\n\t\tcase <-time.After(time.Minute):\n\t\t\terrs = append(errs, errors.New(\"guest agent does not seem to be running; port forwards will not work\"))\n\t\t}\n\t}\n\tif err := a.waitForRequirements(\"final\", a.finalRequirements()); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\t// Copy all config files _after_ the requirements are done\n\tfor _, rule := range a.instConfig.CopyToHost {\n\t\tsshAddress, sshPort := a.sshAddressPort()\n\t\tif err := copyToHost(ctx, a.sshConfig, sshAddress, sshPort, rule.HostFile, rule.GuestFile); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\ta.cleanUp(func() error {\n\t\tvar rmErrs []error\n\t\tfor _, rule := range a.instConfig.CopyToHost {\n\t\t\tif rule.DeleteOnStop {\n\t\t\t\tlogrus.Infof(\"Deleting %s\", rule.HostFile)\n\t\t\t\tif err := os.RemoveAll(rule.HostFile); err != nil {\n\t\t\t\t\trmErrs = append(rmErrs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn errors.Join(rmErrs...)\n\t})\n\treturn errors.Join(errs...)\n}\n\n// cleanUp registers a cleanup function to be called when the host agent is stopped.\n// The cleanup functions are called before the context is cancelled, in the reverse order of their registration.\nfunc (a *HostAgent) cleanUp(fn func() error) {\n\ta.onCloseMu.Lock()\n\tdefer a.onCloseMu.Unlock()\n\ta.onClose = append(a.onClose, fn)\n}\n\nfunc (a *HostAgent) close() error {\n\ta.onCloseMu.Lock()\n\tdefer a.onCloseMu.Unlock()\n\tlogrus.Infof(\"Shutting down the host agent\")\n\tvar errs []error\n\tfor i := len(a.onClose) - 1; i >= 0; i-- {\n\t\tf := a.onClose[i]\n\t\tif err := f(); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\treturn errors.Join(errs...)\n}\n\nfunc (a *HostAgent) watchGuestAgentEvents(ctx context.Context) {\n\t// TODO: use vSock (when QEMU for macOS gets support for vSock)\n\n\t// Setup all socket forwards and defer their teardown\n\tif !(a.driver.Info().Features.SkipSocketForwarding) {\n\t\tlogrus.Debugf(\"Forwarding unix sockets\")\n\t\tsshAddress, sshPort := a.sshAddressPort()\n\t\tfor _, rule := range a.instConfig.PortForwards {\n\t\t\tif rule.GuestSocket != \"\" {\n\t\t\t\tlocal := hostAddress(rule, &guestagentapi.IPPort{})\n\t\t\t\t_ = forwardSSH(ctx, a.sshConfig, sshAddress, sshPort, local, rule.GuestSocket, verbForward, rule.Reverse)\n\t\t\t}\n\t\t}\n\t}\n\n\tlocalUnix := filepath.Join(a.instDir, filenames.GuestAgentSock)\n\tremoteUnix := \"/run/lima-guestagent.sock\"\n\n\ta.cleanUp(func() error {\n\t\tlogrus.Debugf(\"Stop forwarding unix sockets\")\n\t\tvar errs []error\n\t\tsshAddress, sshPort := a.sshAddressPort()\n\t\tfor _, rule := range a.instConfig.PortForwards {\n\t\t\tif rule.GuestSocket != \"\" {\n\t\t\t\tlocal := hostAddress(rule, &guestagentapi.IPPort{})\n\t\t\t\t// using ctx.Background() because ctx has already been cancelled\n\t\t\t\tif err := forwardSSH(context.Background(), a.sshConfig, sshAddress, sshPort, local, rule.GuestSocket, verbCancel, rule.Reverse); err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif a.driver.ForwardGuestAgent() {\n\t\t\tif err := forwardSSH(context.Background(), a.sshConfig, sshAddress, sshPort, localUnix, remoteUnix, verbCancel, false); err != nil {\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\t\t}\n\t\treturn errors.Join(errs...)\n\t})\n\n\tgo func() {\n\t\tif a.instConfig.MountInotify != nil && *a.instConfig.MountInotify {\n\t\t\tif a.client == nil || !isGuestAgentSocketAccessible(ctx, a.client) {\n\t\t\t\tif a.driver.ForwardGuestAgent() {\n\t\t\t\t\tsshAddress, sshPort := a.sshAddressPort()\n\t\t\t\t\t_ = forwardSSH(ctx, a.sshConfig, sshAddress, sshPort, localUnix, remoteUnix, verbForward, false)\n\t\t\t\t}\n\t\t\t}\n\t\t\terr := a.startInotify(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Warn(\"failed to start inotify\")\n\t\t\t}\n\t\t}\n\t}()\n\n\t// ensure close before ctx is cancelled\n\ta.cleanUp(a.grpcPortForwarder.Close)\n\n\tfor {\n\t\tif a.client == nil || !isGuestAgentSocketAccessible(ctx, a.client) {\n\t\t\tif a.driver.ForwardGuestAgent() {\n\t\t\t\tsshAddress, sshPort := a.sshAddressPort()\n\t\t\t\t_ = forwardSSH(ctx, a.sshConfig, sshAddress, sshPort, localUnix, remoteUnix, verbForward, false)\n\t\t\t}\n\t\t}\n\t\tclient, err := a.getOrCreateClient(ctx)\n\t\tif err == nil {\n\t\t\tif err := a.processGuestAgentEvents(ctx, client); err != nil {\n\t\t\t\tif !errors.Is(err, context.Canceled) {\n\t\t\t\t\tlogrus.WithError(err).Warn(\"guest agent events closed unexpectedly\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif !strings.Contains(err.Error(), context.Canceled.Error()) {\n\t\t\t\tlogrus.WithError(err).Warn(\"connection to the guest agent was closed unexpectedly\")\n\t\t\t}\n\t\t}\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(10 * time.Second):\n\t\t}\n\t}\n}\n\nfunc (a *HostAgent) addStaticPortForwardsFromList(ctx context.Context, staticPortForwards []limatype.PortForward) {\n\tsshAddress, sshPort := a.sshAddressPort()\n\tfor _, rule := range staticPortForwards {\n\t\tif rule.GuestSocket == \"\" {\n\t\t\tguest := &guestagentapi.IPPort{\n\t\t\t\tIp:       rule.GuestIP.String(),\n\t\t\t\tPort:     int32(rule.GuestPort),\n\t\t\t\tProtocol: rule.Proto,\n\t\t\t}\n\t\t\tlocal, remote := a.portForwarder.forwardingAddresses(guest)\n\t\t\tif local != \"\" {\n\t\t\t\tlogrus.Infof(\"Setting up static TCP forwarding from %s to %s\", remote, local)\n\t\t\t\tif err := forwardTCP(ctx, a.sshConfig, sshAddress, sshPort, local, remote, verbForward); err != nil {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"failed to set up static TCP forwarding %s -> %s\", remote, local)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// separateStaticPortForwards separates static port forwards from a.instConfig.PortForwards,\n// updates a.instConfig.PortForwards to contain only non-static port forwards,\n// and returns the list of static port forwards.\nfunc (a *HostAgent) separateStaticPortForwards() []limatype.PortForward {\n\tstaticPortForwards := make([]limatype.PortForward, 0, len(a.instConfig.PortForwards))\n\tnonStaticPortForwards := make([]limatype.PortForward, 0, len(a.instConfig.PortForwards))\n\n\tfor i := range len(a.instConfig.PortForwards) {\n\t\trule := a.instConfig.PortForwards[i]\n\t\tif rule.Static {\n\t\t\tlogrus.Debugf(\"Found static port forward: guest=%d host=%d\", rule.GuestPort, rule.HostPort)\n\t\t\tstaticPortForwards = append(staticPortForwards, rule)\n\t\t} else {\n\t\t\tlogrus.Debugf(\"Found non-static port forward: guest=%d host=%d\", rule.GuestPort, rule.HostPort)\n\t\t\tnonStaticPortForwards = append(nonStaticPortForwards, rule)\n\t\t}\n\t}\n\n\tlogrus.Debugf(\"Static port forwards: %d, Non-static port forwards: %d\", len(staticPortForwards), len(nonStaticPortForwards))\n\n\ta.instConfig.PortForwards = nonStaticPortForwards\n\treturn staticPortForwards\n}\n\nfunc isGuestAgentSocketAccessible(ctx context.Context, client *guestagentclient.GuestAgentClient) bool {\n\t_, err := client.Info(ctx)\n\treturn err == nil\n}\n\nfunc (a *HostAgent) getOrCreateClient(ctx context.Context) (*guestagentclient.GuestAgentClient, error) {\n\ta.clientMu.Lock()\n\tdefer a.clientMu.Unlock()\n\tif a.client != nil && isGuestAgentSocketAccessible(ctx, a.client) {\n\t\treturn a.client, nil\n\t}\n\tvar err error\n\ta.client, err = guestagentclient.NewGuestAgentClient(a.createConnection)\n\treturn a.client, err\n}\n\nfunc (a *HostAgent) createConnection(ctx context.Context) (net.Conn, error) {\n\tconn, _, err := a.driver.GuestAgentConn(ctx)\n\t// default to forwarded sock\n\tif conn == nil && err == nil {\n\t\tvar d net.Dialer\n\t\tconn, err = d.DialContext(ctx, \"unix\", filepath.Join(a.instDir, filenames.GuestAgentSock))\n\t}\n\treturn conn, err\n}\n\nfunc (a *HostAgent) processGuestAgentEvents(ctx context.Context, client *guestagentclient.GuestAgentClient) error {\n\tinfo, err := client.Info(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.Info(\"Guest agent is running\")\n\ta.guestAgentAliveChOnce.Do(func() {\n\t\tclose(a.guestAgentAliveCh)\n\t})\n\n\tlogrus.Debugf(\"guest agent info: %+v\", info)\n\n\tonEvent := func(ev *guestagentapi.Event) {\n\t\tlogrus.Debugf(\"guest agent event: %+v\", ev)\n\t\tfor _, f := range ev.Errors {\n\t\t\tlogrus.Warnf(\"received error from the guest: %q\", f)\n\t\t}\n\t\t// History of the default value of useSSHFwd:\n\t\t// - v0.1.0:        true  (effectively)\n\t\t// - v1.0.0:        false\n\t\t// - v1.0.1:        true\n\t\t// - v1.1.0-beta.0: false\n\t\tuseSSHFwd := false\n\t\tif envVar := os.Getenv(\"LIMA_SSH_PORT_FORWARDER\"); envVar != \"\" {\n\t\t\tb, err := strconv.ParseBool(envVar)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Warnf(\"invalid LIMA_SSH_PORT_FORWARDER value %q\", envVar)\n\t\t\t} else {\n\t\t\t\tuseSSHFwd = b\n\t\t\t}\n\t\t}\n\t\tif useSSHFwd {\n\t\t\ta.portForwarder.OnEvent(ctx, ev)\n\t\t} else {\n\t\t\tdialContext := portfwd.DialContextToGRPCTunnel(client)\n\t\t\ta.grpcPortForwarder.OnEvent(ctx, dialContext, ev)\n\t\t}\n\t}\n\n\tif err := client.Events(ctx, onEvent); err != nil {\n\t\tif status.Code(err) == codes.Canceled {\n\t\t\treturn context.Canceled\n\t\t}\n\t\treturn err\n\t}\n\treturn io.EOF\n}\n\nconst (\n\tverbForward = \"forward\"\n\tverbCancel  = \"cancel\"\n)\n\nfunc executeSSH(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, command ...string) error {\n\targs := sshConfig.Args()\n\targs = append(args,\n\t\t\"-p\", strconv.Itoa(sshPort),\n\t\tsshAddress,\n\t\t\"--\",\n\t)\n\targs = append(args, command...)\n\tcmd := exec.CommandContext(ctx, sshConfig.Binary(), args...)\n\tif out, err := cmd.Output(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %q: %w\", cmd.Args, string(out), err)\n\t}\n\treturn nil\n}\n\nfunc forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote, verb string, reverse bool) error {\n\targs := sshConfig.Args()\n\targs = append(args,\n\t\t\"-T\",\n\t\t\"-O\", verb,\n\t)\n\tif reverse {\n\t\targs = append(args,\n\t\t\t\"-R\", remote+\":\"+local,\n\t\t)\n\t} else {\n\t\targs = append(args,\n\t\t\t\"-L\", local+\":\"+remote,\n\t\t)\n\t}\n\targs = append(args,\n\t\t\"-N\",\n\t\t\"-f\",\n\t\t\"-p\", strconv.Itoa(sshPort),\n\t\tsshAddress,\n\t\t\"--\",\n\t)\n\tif strings.HasPrefix(local, \"/\") {\n\t\tswitch verb {\n\t\tcase verbForward:\n\t\t\tif reverse {\n\t\t\t\tlogrus.Infof(\"Forwarding %q (host) to %q (guest)\", local, remote)\n\t\t\t\tif err := executeSSH(ctx, sshConfig, sshAddress, sshPort, \"rm\", \"-f\", remote); err != nil {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to clean up %q (guest) before setting up forwarding\", remote)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlogrus.Infof(\"Forwarding %q (guest) to %q (host)\", remote, local)\n\t\t\t\tif err := os.RemoveAll(local); err != nil {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to clean up %q (host) before setting up forwarding\", local)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := os.MkdirAll(filepath.Dir(local), 0o750); err != nil {\n\t\t\t\treturn fmt.Errorf(\"can't create directory for local socket %q: %w\", local, err)\n\t\t\t}\n\t\tcase verbCancel:\n\t\t\tif reverse {\n\t\t\t\tlogrus.Infof(\"Stopping forwarding %q (host) to %q (guest)\", local, remote)\n\t\t\t\tif err := executeSSH(ctx, sshConfig, sshAddress, sshPort, \"rm\", \"-f\", remote); err != nil {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to clean up %q (guest) after stopping forwarding\", remote)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlogrus.Infof(\"Stopping forwarding %q (guest) to %q (host)\", remote, local)\n\t\t\t\tdefer func() {\n\t\t\t\t\tif err := os.RemoveAll(local); err != nil {\n\t\t\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to clean up %q (host) after stopping forwarding\", local)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\t\tdefault:\n\t\t\tpanic(fmt.Errorf(\"invalid verb %q\", verb))\n\t\t}\n\t}\n\tcmd := exec.CommandContext(ctx, sshConfig.Binary(), args...)\n\tlogrus.Debugf(\"Running %q\", cmd)\n\tif out, err := cmd.Output(); err != nil {\n\t\tif verb == verbForward && strings.HasPrefix(local, \"/\") {\n\t\t\tif reverse {\n\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to set up forward from %q (host) to %q (guest)\", local, remote)\n\t\t\t\tif err := executeSSH(ctx, sshConfig, sshAddress, sshPort, \"rm\", \"-f\", remote); err != nil {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to clean up %q (guest) after forwarding failed\", remote)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to set up forward from %q (guest) to %q (host)\", remote, local)\n\t\t\t\tif removeErr := os.RemoveAll(local); removeErr != nil {\n\t\t\t\t\tlogrus.WithError(removeErr).Warnf(\"Failed to clean up %q (host) after forwarding failed\", local)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn fmt.Errorf(\"failed to run %v: %q: %w\", cmd.Args, string(out), err)\n\t}\n\treturn nil\n}\n\nfunc (a *HostAgent) watchCloudInitProgress(ctx context.Context) {\n\texitReason := \"Cloud-init monitoring completed successfully\"\n\tvar cmd *exec.Cmd\n\n\tdefer func() {\n\t\ta.emitCloudInitProgressEvent(context.Background(), &events.CloudInitProgress{\n\t\t\tActive:    false,\n\t\t\tCompleted: true,\n\t\t\tLogLine:   exitReason,\n\t\t})\n\t\tlogrus.Debug(\"Cloud-init progress monitoring completed\")\n\t}()\n\n\tlogrus.Debug(\"Starting cloud-init progress monitoring\")\n\n\ta.emitCloudInitProgressEvent(ctx, &events.CloudInitProgress{\n\t\tActive: true,\n\t})\n\n\tsshAddress, sshPort := a.sshAddressPort()\n\targs := a.sshConfig.Args()\n\targs = append(args,\n\t\t\"-p\", strconv.Itoa(sshPort),\n\t\tsshAddress,\n\t\t\"sh\", \"-c\",\n\t\t`\"if command -v systemctl >/dev/null 2>&1 && systemctl is-enabled -q cloud-init-main.service; then\n\t\t\tsudo journalctl -u cloud-init-main.service -b -S @0 -o cat -f\n\t\telse\n\t\t\tsudo tail -n +$(sudo awk '\n\t\t\t\tBEGIN{b=1; e=1}\n\t\t\t\t/^Cloud-init.* finished/{e=NR}\n\t\t\t\t/.*/{if(NR>e){b=e+1}}\n\t\t\t\tEND{print b}\n\t\t\t' /var/log/cloud-init-output.log) -f /var/log/cloud-init-output.log\n\t\tfi\"`,\n\t)\n\n\tcmd = exec.CommandContext(ctx, a.sshConfig.Binary(), args...)\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\tlogrus.WithError(err).Warn(\"Failed to create stdout pipe for cloud-init monitoring\")\n\t\texitReason = \"Failed to create stdout pipe for cloud-init monitoring\"\n\t\treturn\n\t}\n\n\tif err := cmd.Start(); err != nil {\n\t\tlogrus.WithError(err).Warn(\"Failed to start cloud-init monitoring command\")\n\t\texitReason = \"Failed to start cloud-init monitoring command\"\n\t\treturn\n\t}\n\n\tscanner := bufio.NewScanner(stdout)\n\tcloudInitMainServiceStarted := false\n\tcloudInitFinished := false\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif strings.TrimSpace(line) == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !cloudInitMainServiceStarted {\n\t\t\tif isStartedCloudInitMainService(line) {\n\t\t\t\tlogrus.Debug(\"cloud-init-main.service started detected via log pattern\")\n\t\t\t\tcloudInitMainServiceStarted = true\n\t\t\t} else if !cloudInitFinished {\n\t\t\t\tif isCloudInitFinished(line) {\n\t\t\t\t\tlogrus.Debug(\"Cloud-init completion detected via log pattern\")\n\t\t\t\t\tcloudInitFinished = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else if !cloudInitFinished && isDeactivatedCloudInitMainService(line) {\n\t\t\tlogrus.Debug(\"cloud-init-main.service deactivated detected via log pattern\")\n\t\t\tcloudInitFinished = true\n\t\t}\n\n\t\ta.emitCloudInitProgressEvent(ctx, &events.CloudInitProgress{\n\t\t\tActive:    !cloudInitFinished,\n\t\t\tLogLine:   line,\n\t\t\tCompleted: cloudInitFinished,\n\t\t})\n\n\t\tif cloudInitFinished {\n\t\t\tlogrus.Debug(\"Breaking from cloud-init monitoring loop - completion detected\")\n\t\t\tif cmd.Process != nil {\n\t\t\t\tlogrus.Debug(\"Killing cloud-init monitoring process after completion\")\n\t\t\t\tif err := cmd.Process.Kill(); err != nil {\n\t\t\t\t\tlogrus.WithError(err).Debug(\"Failed to kill cloud-init monitoring process\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err := cmd.Wait(); err != nil {\n\t\tif ctx.Err() == context.DeadlineExceeded {\n\t\t\tlogrus.Warn(\"Cloud-init monitoring timed out after 10 minutes\")\n\t\t\texitReason = \"Cloud-init monitoring timed out after 10 minutes\"\n\t\t\treturn\n\t\t}\n\t\tlogrus.WithError(err).Debug(\"SSH command finished (expected when cloud-init completes)\")\n\t}\n\n\tif !cloudInitFinished {\n\t\tlogrus.Debug(\"Connection dropped, checking for any remaining cloud-init logs\")\n\n\t\tfinalArgs := a.sshConfig.Args()\n\t\tfinalArgs = append(finalArgs,\n\t\t\t\"-p\", strconv.Itoa(sshPort),\n\t\t\tsshAddress,\n\t\t\t\"sudo\", \"tail\", \"-n\", \"20\", \"/var/log/cloud-init-output.log\",\n\t\t)\n\n\t\tfinalCmd := exec.CommandContext(ctx, a.sshConfig.Binary(), finalArgs...)\n\t\tif finalOutput, err := finalCmd.Output(); err == nil {\n\t\t\tfor line := range strings.SplitSeq(string(finalOutput), \"\\n\") {\n\t\t\t\tif strings.TrimSpace(line) != \"\" {\n\t\t\t\t\tif !cloudInitFinished {\n\t\t\t\t\t\tcloudInitFinished = isCloudInitFinished(line)\n\t\t\t\t\t}\n\n\t\t\t\t\ta.emitCloudInitProgressEvent(ctx, &events.CloudInitProgress{\n\t\t\t\t\t\tActive:    !cloudInitFinished,\n\t\t\t\t\t\tLogLine:   line,\n\t\t\t\t\t\tCompleted: cloudInitFinished,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc isCloudInitFinished(line string) bool {\n\tline = strings.ToLower(strings.TrimSpace(line))\n\treturn strings.Contains(line, \"cloud-init\") && strings.Contains(line, \"finished\")\n}\n\nfunc isStartedCloudInitMainService(line string) bool {\n\tline = strings.ToLower(strings.TrimSpace(line))\n\treturn strings.HasPrefix(line, \"started cloud-init-main.service\")\n}\n\nfunc isDeactivatedCloudInitMainService(line string) bool {\n\tline = strings.ToLower(strings.TrimSpace(line))\n\t// Deactivated event lines end with a line reporting consumed CPU time, etc.\n\treturn strings.HasPrefix(line, \"cloud-init-main.service: consumed\")\n}\n\nfunc copyToHost(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote string) error {\n\targs := sshConfig.Args()\n\targs = append(args,\n\t\t\"-p\", strconv.Itoa(sshPort),\n\t\tsshAddress,\n\t\t\"--\",\n\t)\n\targs = append(args,\n\t\t\"sudo\",\n\t\t\"cat\",\n\t\tremote,\n\t)\n\tlogrus.Infof(\"Copying config from %s to %s\", remote, local)\n\tif err := os.MkdirAll(filepath.Dir(local), 0o700); err != nil {\n\t\treturn fmt.Errorf(\"can't create directory for local file %q: %w\", local, err)\n\t}\n\tcmd := exec.CommandContext(ctx, sshConfig.Binary(), args...)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %q: %w\", cmd.Args, string(out), err)\n\t}\n\tif err := os.WriteFile(local, out, 0o600); err != nil {\n\t\treturn fmt.Errorf(\"can't write to local file %q: %w\", local, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/hostagent/hostagent_unix.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"syscall\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Default nofile limit is too low on some system.\n// For example in the macOS standard terminal is 256.\n// It means that there are only ~240 connections available from the host to the vm.\n// That function increases the nofile limit for child processes, especially the ssh process\n//\n// More about limits in go: https://go.dev/issue/46279\nfunc adjustNofileRlimit() {\n\tvar limit syscall.Rlimit\n\tif err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {\n\t\tlogrus.WithError(err).Debug(\"failed to get RLIMIT_NOFILE\")\n\t} else if limit.Cur != limit.Max {\n\t\tlimit.Cur = limit.Max\n\t\terr = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit)\n\t\tif err != nil {\n\t\t\tlogrus.WithError(err).Debugf(\"failed to set RLIMIT_NOFILE (%+v)\", limit)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/hostagent/hostagent_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nfunc adjustNofileRlimit() {}\n"
  },
  {
    "path": "pkg/hostagent/inotify.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/rjeczalik/notify\"\n\t\"github.com/sirupsen/logrus\"\n\t\"google.golang.org/protobuf/types/known/timestamppb\"\n\n\tguestagentapi \"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n)\n\nconst CacheSize = 10000\n\nvar (\n\tinotifyCache   = make(map[string]int64)\n\tmountSymlinks  = make(map[string]string)\n\tmountLocations = make(map[string]string)\n)\n\nfunc (a *HostAgent) startInotify(ctx context.Context) error {\n\tmountWatchCh := make(chan notify.EventInfo, 128)\n\terr := a.setupWatchers(mountWatchCh)\n\tif err != nil {\n\t\treturn err\n\t}\n\tclient, err := a.getOrCreateClient(ctx)\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"failed to create client for inotify\")\n\t}\n\tinotifyClient, err := client.Inotify(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\tcase watchEvent := <-mountWatchCh:\n\t\t\twatchPath := watchEvent.Path()\n\t\t\tstat, err := os.Stat(watchPath)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif filterEvents(watchEvent, stat) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\twatchPath = translateToGuestPath(watchPath, mountSymlinks, mountLocations)\n\n\t\t\tutcTimestamp := timestamppb.New(stat.ModTime().UTC())\n\t\t\tevent := &guestagentapi.Inotify{MountPath: watchPath, Time: utcTimestamp}\n\t\t\terr = inotifyClient.Send(event)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).Warn(\"failed to send inotify\")\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (a *HostAgent) setupWatchers(events chan notify.EventInfo) error {\n\tfor _, m := range a.instConfig.Mounts {\n\t\tif !*m.Writable {\n\t\t\tcontinue\n\t\t}\n\t\tsymlink, err := filepath.EvalSymlinks(m.Location)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif m.Location != symlink {\n\t\t\tmountSymlinks[symlink] = m.Location\n\t\t}\n\t\tif m.MountPoint != nil && m.Location != *m.MountPoint {\n\t\t\tmountLocations[m.Location] = *m.MountPoint\n\t\t}\n\n\t\tlogrus.Infof(\"enable inotify for writable mount: %s\", m.Location)\n\t\terr = notify.Watch(path.Join(m.Location, \"...\"), events, GetNotifyEvent())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc translateToGuestPath(hostPath string, symlinks, locations map[string]string) string {\n\tresult := hostPath\n\n\tfor symlink, original := range symlinks {\n\t\tif strings.HasPrefix(result, symlink) {\n\t\t\tresult = strings.ReplaceAll(result, symlink, original)\n\t\t}\n\t}\n\n\tfor location, mountPoint := range locations {\n\t\tif suffix, ok := strings.CutPrefix(result, location); ok {\n\t\t\treturn mountPoint + suffix\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc filterEvents(event notify.EventInfo, stat os.FileInfo) bool {\n\tcurrTime := stat.ModTime()\n\teventPath := event.Path()\n\tcacheMilli, ok := inotifyCache[eventPath]\n\tif ok {\n\t\t// Ignore repeated events for 10ms to exclude recursive inotify events\n\t\tif currTime.UnixMilli()-cacheMilli < 10 {\n\t\t\treturn true\n\t\t}\n\t}\n\tinotifyCache[eventPath] = currTime.UnixMilli()\n\n\tif len(inotifyCache) >= CacheSize {\n\t\tinotifyCache = make(map[string]int64)\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/hostagent/inotify_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport \"github.com/rjeczalik/notify\"\n\nfunc GetNotifyEvent() notify.Event {\n\treturn notify.Create | notify.Write | notify.FSEventsInodeMetaMod\n}\n"
  },
  {
    "path": "pkg/hostagent/inotify_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport \"github.com/rjeczalik/notify\"\n\nfunc GetNotifyEvent() notify.Event {\n\treturn notify.Create | notify.Write | notify.InAttrib\n}\n"
  },
  {
    "path": "pkg/hostagent/inotify_others.go",
    "content": "//go:build !darwin && !linux\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport \"github.com/rjeczalik/notify\"\n\nfunc GetNotifyEvent() notify.Event {\n\treturn notify.Create | notify.Write\n}\n"
  },
  {
    "path": "pkg/hostagent/inotify_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestTranslateToGuestPath(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\thostPath  string\n\t\tsymlinks  map[string]string\n\t\tlocations map[string]string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"no translation needed - empty maps\",\n\t\t\thostPath:  \"/Users/user/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{},\n\t\t\texpected:  \"/Users/user/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"no translation needed - location equals mountPoint\",\n\t\t\thostPath:  \"/Users/user/project/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user\": \"/Users/user\"},\n\t\t\texpected:  \"/Users/user/project/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"translate location to different mountPoint\",\n\t\t\thostPath:  \"/Users/user/source/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user/source\": \"/mnt/dest\"},\n\t\t\texpected:  \"/mnt/dest/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"translate location to different mountPoint - nested path\",\n\t\t\thostPath:  \"/Users/user/source/subdir/deep/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user/source\": \"/mnt/dest\"},\n\t\t\texpected:  \"/mnt/dest/subdir/deep/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"translate location to different mountPoint - root file\",\n\t\t\thostPath:  \"/Users/user/source/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user/source\": \"/mnt/dest\"},\n\t\t\texpected:  \"/mnt/dest/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"symlink resolution only\",\n\t\t\thostPath:  \"/private/tmp/file.txt\",\n\t\t\tsymlinks:  map[string]string{\"/private/tmp\": \"/tmp\"},\n\t\t\tlocations: map[string]string{},\n\t\t\texpected:  \"/tmp/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"symlink resolution with location translation\",\n\t\t\thostPath:  \"/private/var/folders/source/file.txt\",\n\t\t\tsymlinks:  map[string]string{\"/private/var\": \"/var\"},\n\t\t\tlocations: map[string]string{\"/var/folders/source\": \"/mnt/dest\"},\n\t\t\texpected:  \"/mnt/dest/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"more specific location matches\",\n\t\t\thostPath:  \"/Users/user/source/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user/source\": \"/mnt/source\"},\n\t\t\texpected:  \"/mnt/source/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"less specific location matches when more specific not present\",\n\t\t\thostPath:  \"/Users/user/other/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user\": \"/mnt/home\"},\n\t\t\texpected:  \"/mnt/home/other/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"path not matching any location\",\n\t\t\thostPath:  \"/other/path/file.txt\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user/source\": \"/mnt/dest\"},\n\t\t\texpected:  \"/other/path/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:      \"exact location match - file at mount root\",\n\t\t\thostPath:  \"/Users/user/source\",\n\t\t\tsymlinks:  map[string]string{},\n\t\t\tlocations: map[string]string{\"/Users/user/source\": \"/mnt/dest\"},\n\t\t\texpected:  \"/mnt/dest\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple locations - non-overlapping\",\n\t\t\thostPath: \"/Users/user/project/file.txt\",\n\t\t\tsymlinks: map[string]string{},\n\t\t\tlocations: map[string]string{\n\t\t\t\t\"/Users/user/project\": \"/mnt/project\",\n\t\t\t\t\"/Users/user/other\":   \"/mnt/other\",\n\t\t\t},\n\t\t\texpected: \"/mnt/project/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple symlinks\",\n\t\t\thostPath: \"/private/var/tmp/file.txt\",\n\t\t\tsymlinks: map[string]string{\n\t\t\t\t\"/private/var\": \"/var\",\n\t\t\t\t\"/private/tmp\": \"/tmp\",\n\t\t\t},\n\t\t\tlocations: map[string]string{},\n\t\t\texpected:  \"/var/tmp/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple locations and symlinks combined\",\n\t\t\thostPath: \"/private/var/folders/source/file.txt\",\n\t\t\tsymlinks: map[string]string{\n\t\t\t\t\"/private/var\": \"/var\",\n\t\t\t},\n\t\t\tlocations: map[string]string{\n\t\t\t\t\"/var/folders/source\": \"/mnt/dest1\",\n\t\t\t\t\"/tmp/test\":           \"/mnt/dest2\",\n\t\t\t},\n\t\t\texpected: \"/mnt/dest1/file.txt\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := translateToGuestPath(tc.hostPath, tc.symlinks, tc.locations)\n\t\t\tassert.Equal(t, result, tc.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/hostagent/mount.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/lima-vm/sshocker/pkg/reversesshfs\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n)\n\ntype mount struct {\n\tclose func() error\n}\n\nfunc (a *HostAgent) setupMounts(ctx context.Context) ([]*mount, error) {\n\tvar (\n\t\tres  []*mount\n\t\terrs []error\n\t)\n\tfor _, f := range a.instConfig.Mounts {\n\t\tm, err := a.setupMount(ctx, f)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tres = append(res, m)\n\t}\n\treturn res, errors.Join(errs...)\n}\n\nfunc (a *HostAgent) setupMount(ctx context.Context, m limatype.Mount) (*mount, error) {\n\tif err := os.MkdirAll(m.Location, 0o755); err != nil {\n\t\treturn nil, err\n\t}\n\t// NOTE: allow_other requires \"user_allow_other\" in /etc/fuse.conf\n\tsshfsOptions := \"allow_other\"\n\tif !*m.SSHFS.Cache {\n\t\tsshfsOptions += \",cache=no\"\n\t}\n\tif *m.SSHFS.FollowSymlinks {\n\t\tsshfsOptions += \",follow_symlinks\"\n\t}\n\tlogrus.Infof(\"Mounting %q on %q\", m.Location, *m.MountPoint)\n\n\tresolvedLocation := m.Location\n\tif runtime.GOOS == \"windows\" {\n\t\tvar err error\n\t\tresolvedLocation, err = ioutilx.WindowsSubsystemPath(ctx, m.Location)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tsshAddress, sshPort := a.sshAddressPort()\n\t// Create a copy of sshConfig to avoid\n\t// modifying HostAgent's sshConfig in case of Windows\n\tsshConfig := *a.sshConfig\n\trsf := &reversesshfs.ReverseSSHFS{\n\t\tDriver:              *m.SSHFS.SFTPDriver,\n\t\tSSHConfig:           &sshConfig,\n\t\tLocalPath:           resolvedLocation,\n\t\tHost:                sshAddress,\n\t\tPort:                sshPort,\n\t\tRemotePath:          *m.MountPoint,\n\t\tReadonly:            !(*m.Writable),\n\t\tSSHFSAdditionalArgs: []string{\"-o\", sshfsOptions},\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\t// cygwin/msys2 doesn't support full feature set over mux socket, this has at least 2 side effects:\n\t\t// 1. unnecessary pollutes output with error on errors encountered (ssh will try to tolerate them with fallbacks);\n\t\t// 2. these errors still imply additional coms over mux socket, which resulted sftp-server to fail more often statistically during test runs.\n\t\t// It is reasonable to disable this on Windows if required feature is not fully operational.\n\t\trsf.SSHConfig.Persist = false\n\n\t\t// HostAgent's `sshConfig` already has some ControlMaster related args in `AdditionalArgs`,\n\t\t// so it is necessary to remove them to avoid overrides above `Persist=false`.\n\t\trsf.SSHConfig.AdditionalArgs = sshutil.DisableControlMasterOptsFromSSHArgs(rsf.SSHConfig.AdditionalArgs)\n\t}\n\tif err := rsf.Prepare(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prepare reverse sshfs for %q on %q: %w\", resolvedLocation, *m.MountPoint, err)\n\t}\n\tif err := rsf.Start(); err != nil {\n\t\tlogrus.WithError(err).Warnf(\"failed to mount reverse sshfs for %q on %q, retrying with `-o nonempty`\", resolvedLocation, *m.MountPoint)\n\t\t// NOTE: nonempty is not supported for libfuse3: https://github.com/canonical/multipass/issues/1381\n\t\trsf.SSHFSAdditionalArgs = []string{\"-o\", \"nonempty\"}\n\t\tif err := rsf.Start(); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to mount reverse sshfs for %q on %q: %w\", resolvedLocation, *m.MountPoint, err)\n\t\t}\n\t}\n\n\tres := &mount{\n\t\tclose: func() error {\n\t\t\tlogrus.Infof(\"Unmounting %q\", resolvedLocation)\n\t\t\tif err := rsf.Close(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmount reverse sshfs for %q on %q: %w\", resolvedLocation, *m.MountPoint, err)\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\treturn res, nil\n}\n"
  },
  {
    "path": "pkg/hostagent/port.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"context\"\n\t\"net\"\n\n\t\"github.com/lima-vm/sshocker/pkg/ssh\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n)\n\ntype portForwarder struct {\n\tsshConfig      *ssh.SSHConfig\n\tsshAddressPort func() (string, int)\n\trules          []limatype.PortForward\n\tignore         bool\n\tvmType         limatype.VMType\n\tonEvent        func(*events.PortForwardEvent)\n}\n\nconst sshGuestPort = 22\n\nvar IPv4loopback1 = limayaml.IPv4loopback1\n\nfunc newPortForwarder(sshConfig *ssh.SSHConfig, sshAddressPort func() (string, int), rules []limatype.PortForward, ignore bool, vmType limatype.VMType, onEvent func(*events.PortForwardEvent)) *portForwarder {\n\treturn &portForwarder{\n\t\tsshConfig:      sshConfig,\n\t\tsshAddressPort: sshAddressPort,\n\t\trules:          rules,\n\t\tignore:         ignore,\n\t\tvmType:         vmType,\n\t\tonEvent:        onEvent,\n\t}\n}\n\nfunc (pf *portForwarder) emitEvent(ev *events.PortForwardEvent) {\n\tif pf.onEvent != nil {\n\t\tpf.onEvent(ev)\n\t}\n}\n\nfunc hostAddress(rule limatype.PortForward, guest *api.IPPort) string {\n\tif rule.HostSocket != \"\" {\n\t\treturn rule.HostSocket\n\t}\n\thost := &api.IPPort{Ip: rule.HostIP.String()}\n\tif guest.Port == 0 {\n\t\t// guest is a socket\n\t\thost.Port = int32(rule.HostPort)\n\t} else {\n\t\thost.Port = guest.Port + int32(rule.HostPortRange[0]-rule.GuestPortRange[0])\n\t}\n\treturn host.HostString()\n}\n\nfunc (pf *portForwarder) forwardingAddresses(guest *api.IPPort) (hostAddr, guestAddr string) {\n\tguestIP := net.ParseIP(guest.Ip)\n\tfor _, rule := range pf.rules {\n\t\tif rule.GuestSocket != \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tswitch rule.Proto {\n\t\tcase limatype.ProtoTCP, limatype.ProtoAny:\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tif guest.Port < int32(rule.GuestPortRange[0]) || guest.Port > int32(rule.GuestPortRange[1]) {\n\t\t\tcontinue\n\t\t}\n\t\tswitch {\n\t\tcase guestIP.IsUnspecified():\n\t\tcase guestIP.Equal(rule.GuestIP):\n\t\tcase guestIP.Equal(net.IPv6loopback) && rule.GuestIP.Equal(IPv4loopback1):\n\t\tcase rule.GuestIP.IsUnspecified() && !*rule.GuestIPMustBeZero:\n\t\t\t// When GuestIPMustBeZero is true, then 0.0.0.0 must be an exact match, which is already\n\t\t\t// handled above by the guest.IP.IsUnspecified() condition.\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tif rule.Ignore {\n\t\t\tif guestIP.IsUnspecified() && !rule.GuestIP.IsUnspecified() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\treturn hostAddress(rule, guest), guest.HostString()\n\t}\n\treturn \"\", guest.HostString()\n}\n\nfunc (pf *portForwarder) OnEvent(ctx context.Context, ev *api.Event) {\n\tsshAddress, sshPort := pf.sshAddressPort()\n\tfor _, f := range ev.RemovedLocalPorts {\n\t\tif f.Protocol != \"tcp\" {\n\t\t\tcontinue\n\t\t}\n\t\tlocal, remote := pf.forwardingAddresses(f)\n\t\tif local == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Infof(\"Stopping forwarding TCP from %s to %s\", remote, local)\n\t\tpf.emitEvent(&events.PortForwardEvent{\n\t\t\tType:      events.PortForwardEventStopping,\n\t\t\tProtocol:  \"tcp\",\n\t\t\tGuestAddr: remote,\n\t\t\tHostAddr:  local,\n\t\t})\n\t\tif err := forwardTCP(ctx, pf.sshConfig, sshAddress, sshPort, local, remote, verbCancel); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"failed to stop forwarding tcp port %d\", f.Port)\n\t\t}\n\t}\n\tfor _, f := range ev.AddedLocalPorts {\n\t\tif f.Protocol != \"tcp\" {\n\t\t\tcontinue\n\t\t}\n\t\tlocal, remote := pf.forwardingAddresses(f)\n\t\tif local == \"\" {\n\t\t\tif !pf.ignore {\n\t\t\t\tlogrus.Infof(\"Not forwarding TCP %s\", remote)\n\t\t\t\tpf.emitEvent(&events.PortForwardEvent{\n\t\t\t\t\tType:      events.PortForwardEventNotForwarding,\n\t\t\t\t\tProtocol:  \"tcp\",\n\t\t\t\t\tGuestAddr: remote,\n\t\t\t\t})\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Infof(\"Forwarding TCP from %s to %s\", remote, local)\n\t\tpf.emitEvent(&events.PortForwardEvent{\n\t\t\tType:      events.PortForwardEventForwarding,\n\t\t\tProtocol:  \"tcp\",\n\t\t\tGuestAddr: remote,\n\t\t\tHostAddr:  local,\n\t\t})\n\t\tif err := forwardTCP(ctx, pf.sshConfig, sshAddress, sshPort, local, remote, verbForward); err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"failed to set up forwarding tcp port %d (negligible if already forwarded)\", f.Port)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/hostagent/port_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/sshocker/pkg/ssh\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/bicopy\"\n\t\"github.com/lima-vm/lima/v2/pkg/portfwd\"\n)\n\n// forwardTCP is not thread-safe.\nfunc forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote, verb string) error {\n\tif strings.HasPrefix(local, \"/\") {\n\t\treturn forwardSSH(ctx, sshConfig, sshAddress, sshPort, local, remote, verb, false)\n\t}\n\tlocalIPStr, localPortStr, err := net.SplitHostPort(local)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlocalIP := net.ParseIP(localIPStr)\n\tlocalPort, err := strconv.Atoi(localPortStr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !localIP.Equal(IPv4loopback1) || localPort >= 1024 {\n\t\treturn forwardSSH(ctx, sshConfig, sshAddress, sshPort, local, remote, verb, false)\n\t}\n\n\t// on macOS, listening on 127.0.0.1:80 requires root while 0.0.0.0:80 does not require root.\n\t// https://twitter.com/_AkihiroSuda_/status/1403403845842075648\n\t//\n\t// We use \"pseudoloopback\" forwarder that listens on 0.0.0.0:80 but rejects connections from non-loopback src IP.\n\tlogrus.Debugf(\"using pseudoloopback port forwarder for %q\", local)\n\n\tif verb == verbCancel {\n\t\tplf, ok := pseudoLoopbackForwarders[local]\n\t\tif ok {\n\t\t\tlocalUnix := plf.unixAddr.Name\n\t\t\t_ = plf.Close()\n\t\t\tdelete(pseudoLoopbackForwarders, local)\n\t\t\tif err := forwardSSH(ctx, sshConfig, sshAddress, sshPort, localUnix, remote, verb, false); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tlogrus.Warnf(\"forwarding for %q seems already cancelled?\", local)\n\t\t}\n\t\treturn nil\n\t}\n\n\tlocalUnixDir, err := os.MkdirTemp(\"/tmp\", fmt.Sprintf(\"lima-psl-%s-%d-\", localIP, localPort))\n\tif err != nil {\n\t\treturn err\n\t}\n\tlocalUnix := filepath.Join(localUnixDir, \"sock\")\n\tlogrus.Debugf(\"forwarding %q to %q\", localUnix, remote)\n\tif err := forwardSSH(ctx, sshConfig, sshAddress, sshPort, localUnix, remote, verb, false); err != nil {\n\t\treturn err\n\t}\n\tplf, err := newPseudoLoopbackForwarder(localPort, localUnix)\n\tif err != nil {\n\t\tif cancelErr := forwardSSH(ctx, sshConfig, sshAddress, sshPort, localUnix, remote, verbCancel, false); cancelErr != nil {\n\t\t\tlogrus.WithError(cancelErr).Warnf(\"failed to cancel forwarding %q to %q\", localUnix, remote)\n\t\t}\n\t\treturn err\n\t}\n\tplf.onClose = func() error {\n\t\treturn os.RemoveAll(localUnixDir)\n\t}\n\tpseudoLoopbackForwarders[local] = plf\n\tgo func() {\n\t\tif plfErr := plf.Serve(); plfErr != nil {\n\t\t\tlogrus.WithError(plfErr).Warning(\"pseudoloopback forwarder crashed\")\n\t\t}\n\t}()\n\treturn nil\n}\n\nvar pseudoLoopbackForwarders = make(map[string]*pseudoLoopbackForwarder)\n\ntype pseudoLoopbackForwarder struct {\n\tln       *net.TCPListener\n\tunixAddr *net.UnixAddr\n\tonClose  func() error\n}\n\nfunc newPseudoLoopbackForwarder(localPort int, unixSock string) (*pseudoLoopbackForwarder, error) {\n\tunixAddr, err := net.ResolveUnixAddr(\"unix\", unixSock)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// use \"tcp\" network to listen on both \"tcp4\" and \"tcp6\"\n\tlnAddr, err := net.ResolveTCPAddr(\"tcp\", fmt.Sprintf(\"0.0.0.0:%d\", localPort))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tln, err := net.ListenTCP(\"tcp\", lnAddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplf := &pseudoLoopbackForwarder{\n\t\tln:       ln,\n\t\tunixAddr: unixAddr,\n\t}\n\n\treturn plf, nil\n}\n\nfunc (plf *pseudoLoopbackForwarder) Serve() error {\n\tdefer plf.ln.Close()\n\tfor {\n\t\tac, err := plf.ln.AcceptTCP()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tremoteAddr := ac.RemoteAddr().String() // ip:port\n\t\tremoteAddrIP, _, err := net.SplitHostPort(remoteAddr)\n\t\tif err != nil {\n\t\t\tlogrus.WithError(err).Debugf(\"pseudoloopback forwarder: rejecting non-loopback remoteAddr %q (unparsable)\", remoteAddr)\n\t\t\tac.Close()\n\t\t\tcontinue\n\t\t}\n\t\tif !portfwd.IsLoopback(remoteAddrIP) {\n\t\t\tlogrus.WithError(err).Debugf(\"pseudoloopback forwarder: rejecting non-loopback remoteAddr %q\", remoteAddr)\n\t\t\tac.Close()\n\t\t\tcontinue\n\t\t}\n\t\tgo func(ac *net.TCPConn) {\n\t\t\tif fErr := plf.forward(ac); fErr != nil {\n\t\t\t\tlogrus.Error(fErr)\n\t\t\t}\n\t\t}(ac)\n\t}\n}\n\nfunc (plf *pseudoLoopbackForwarder) forward(ac *net.TCPConn) error {\n\tdefer ac.Close()\n\tunixConn, err := net.DialUnix(\"unix\", nil, plf.unixAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer unixConn.Close()\n\tbicopy.Bicopy(ac, unixConn, nil)\n\treturn nil\n}\n\nfunc (plf *pseudoLoopbackForwarder) Close() error {\n\t_ = plf.ln.Close()\n\treturn plf.onClose()\n}\n"
  },
  {
    "path": "pkg/hostagent/port_others.go",
    "content": "//go:build !darwin && !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"context\"\n\n\t\"github.com/lima-vm/sshocker/pkg/ssh\"\n)\n\nfunc forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote, verb string) error {\n\treturn forwardSSH(ctx, sshConfig, sshAddress, sshPort, local, remote, verb, false)\n}\n"
  },
  {
    "path": "pkg/hostagent/port_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"context\"\n\n\t\"github.com/lima-vm/sshocker/pkg/ssh\"\n)\n\nfunc forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote, verb string) error {\n\treturn forwardSSH(ctx, sshConfig, sshAddress, sshPort, local, remote, verb, false)\n}\n"
  },
  {
    "path": "pkg/hostagent/requirements.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lima-vm/sshocker/pkg/ssh\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n)\n\nfunc (a *HostAgent) waitForRequirements(label string, requirements []requirement) error {\n\tconst (\n\t\tretries       = 200\n\t\tsleepDuration = 3 * time.Second\n\t)\n\tvar errs []error\n\n\tfor i, req := range requirements {\n\t\tlogrus.Infof(\"Waiting for the %s requirement %d of %d: %q\", label, i+1, len(requirements), req.description)\n\tretryLoop:\n\t\tfor j := range retries {\n\t\t\terr := a.waitForRequirement(req)\n\t\t\tif err == nil {\n\t\t\t\tlogrus.Infof(\"The %s requirement %d of %d is satisfied\", label, i+1, len(requirements))\n\t\t\t\tbreak retryLoop\n\t\t\t}\n\t\t\tif req.fatal {\n\t\t\t\tlogrus.Infof(\"No further %s requirements will be checked\", label)\n\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to satisfy the %s requirement %d of %d %q: %s; skipping further checks: %w\", label, i+1, len(requirements), req.description, req.debugHint, err))\n\t\t\t\treturn errors.Join(errs...)\n\t\t\t}\n\t\t\tif j == retries-1 {\n\t\t\t\terrs = append(errs, fmt.Errorf(\"failed to satisfy the %s requirement %d of %d %q: %s: %w\", label, i+1, len(requirements), req.description, req.debugHint, err))\n\t\t\t\tbreak retryLoop\n\t\t\t}\n\t\t\ttime.Sleep(sleepDuration)\n\t\t}\n\t}\n\treturn errors.Join(errs...)\n}\n\n// prefixExportParam will modify a script to be executed by ssh.ExecuteScript so that it exports\n// all the variables from /mnt/lima-cidata/param.env before invoking the actual interpreter.\n//\n//   - The script is executed in user mode, so needs to read the file using `sudo`.\n//\n//   - `sudo cat param.env | while …; do export …; done` does not work because the piping\n//     creates a subshell, and the exported variables are not visible to the parent process.\n//\n//   - The `<<<\"$string\"` redirection is not available on alpine-lima, where /bin/bash is\n//     just a wrapper around busybox ash.\n//\n// A script that will start with `#!/usr/bin/env ruby` will be modified to look like this:\n//\n//\twhile read -r line; do\n//\t    [ -n \"$line\" ] && export \"$line\"\n//\tdone<<EOF\n//\t$(sudo cat /mnt/lima-cidata/param.env)\n//\tEOF\n//\t/usr/bin/env ruby\n//\n// ssh.ExecuteScript will strip the `#!` prefix from the first line and invoke the\n// rest of the line as the command. The full script is then passed via STDIN. We use\n// \"$(printf '…')\" to be able to use \\n as newline escapes, to fit everything on a\n// single line:\n//\n//\t#!/bin/bash -c \"$(printf 'while … done<<EOF\\n$(sudo …)\\nEOF\\n/usr/bin/env ruby')\"\n//\t#!/usr/bin/env ruby\n//\t…\n//\n// An earlier implementation used $'…' for quoting, but that isn't supported if the\n// user switched the default shell to fish.\nfunc prefixExportParam(script string, guestOS *limatype.OS) (string, error) {\n\tinterpreter, err := ssh.ParseScriptInterpreter(script)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// TODO we should have a symbolic constant for `/mnt/lima-cidata`\n\tcidata := \"/mnt/lima-cidata\"\n\tsudo := \"sudo\"\n\tif guestOS != nil && *guestOS == limatype.DARWIN {\n\t\tcidata = \"/Volumes/cidata\"\n\t\t// On macOS, /Volumes/cidata is not mounted as \"root access only\".\n\t\t// FIXME: The cidata does not need to be root-only on Linux, either?\n\t\tsudo = \"\"\n\t}\n\texportParam := `param_env=\"$(` + sudo + ` cat ` + cidata + `/param.env)\"; while read -r line; do [ -n \"$line\" ] && export \"$line\"; done<<EOF\\n${param_env}\\nEOF\\n`\n\n\t// double up all '%' characters so we can pass them through unchanged in the format string of printf\n\tinterpreter = strings.ReplaceAll(interpreter, \"%\", \"%%\")\n\texportParam = strings.ReplaceAll(exportParam, \"%\", \"%%\")\n\t// strings will be interpolated into single-quoted strings, so protect any existing single quotes\n\tinterpreter = strings.ReplaceAll(interpreter, \"'\", `'\"'\"'`)\n\texportParam = strings.ReplaceAll(exportParam, \"'\", `'\"'\"'`)\n\treturn fmt.Sprintf(\"#!/bin/bash -c \\\"$(printf '%s%s')\\\"\\n%s\", exportParam, interpreter, script), nil\n}\n\nfunc (a *HostAgent) bashAvailable() bool {\n\treturn *a.instConfig.OS != limatype.FREEBSD\n}\n\nfunc (a *HostAgent) waitForRequirement(r requirement) error {\n\tlogrus.Debugf(\"executing script %q\", r.description)\n\tscript := r.script\n\tif a.bashAvailable() {\n\t\tvar err error\n\t\t// FIXME: prefixExportParam depends on bash\n\t\tscript, err = prefixExportParam(r.script, a.instConfig.OS)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tsshConfig := a.sshConfig\n\tif r.noMaster || runtime.GOOS == \"windows\" {\n\t\t// Remove ControlMaster, ControlPath, and ControlPersist options,\n\t\t// because Cygwin-based SSH clients do not support multiplexing when executing commands.\n\t\t// References:\n\t\t//   https://inbox.sourceware.org/cygwin/c98988a5-7e65-4282-b2a1-bb8e350d5fab@acm.org/T/\n\t\t//   https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible\n\t\t// By removing these options:\n\t\t//   - Avoids execution failures when the control master is not yet available.\n\t\t//   - Prevents error messages such as:\n\t\t//     > mux_client_request_session: read from master failed: Connection reset by peer\n\t\t//     > ControlSocket ....sock already exists, disabling multiplexing\n\t\t//     > mm_send_fd: sendmsg(2): Connection reset by peer\\\\r\\\\nmux_client_request_session: send fds failed\\\\r\\\\n\n\t\tsshConfig = &ssh.SSHConfig{\n\t\t\tConfigFile:     sshConfig.ConfigFile,\n\t\t\tPersist:        false,\n\t\t\tAdditionalArgs: sshutil.DisableControlMasterOptsFromSSHArgs(sshConfig.AdditionalArgs),\n\t\t}\n\t}\n\tstdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, sshConfig, script, r.description)\n\tlogrus.Debugf(\"stdout=%q, stderr=%q, err=%v\", stdout, stderr, err)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"stdout=%q, stderr=%q: %w\", stdout, stderr, err)\n\t}\n\treturn nil\n}\n\ntype requirement struct {\n\tdescription string\n\tscript      string\n\tdebugHint   string\n\tfatal       bool\n\tnoMaster    bool\n}\n\nfunc (a *HostAgent) essentialRequirements() []requirement {\n\treq := make([]requirement, 0)\n\treq = append(req,\n\t\trequirement{\n\t\t\tdescription: \"ssh\",\n\t\t\tscript: `#!/bin/sh\ntrue\n`,\n\t\t\tdebugHint: `Failed to SSH into the guest.\nMake sure that the YAML field \"ssh.localPort\" is not used by other processes on the host.\nIf any private key under ~/.ssh is protected with a passphrase, you need to have ssh-agent to be running.\n`,\n\t\t\tnoMaster: true,\n\t\t})\n\tstartControlMasterReq := requirement{\n\t\tdescription: \"Explicitly start ssh ControlMaster\",\n\t\tscript: `#!/bin/sh\ntrue\n`,\n\t\tdebugHint: `The persistent ssh ControlMaster should be started immediately.`,\n\t}\n\tif *a.instConfig.Plain || *a.instConfig.OS != limatype.LINUX {\n\t\treq = append(req, startControlMasterReq)\n\t\treturn req\n\t}\n\treq = append(req,\n\t\trequirement{\n\t\t\tdescription: \"user session is ready for ssh\",\n\t\t\tscript: fmt.Sprintf(`#!/bin/sh\nset -eux\n[ \"$(cat /run/lima-ssh-ready 2>/dev/null)\" = \"%s\" ]\n`, a.iid),\n\t\t\tdebugHint: `The boot sequence will terminate any existing user session after updating\n/etc/environment to make sure the session includes the new values.\nTerminating the session will break the persistent SSH tunnel, so\nit must not be created until the session reset is done.\n`,\n\t\t\tnoMaster: true,\n\t\t})\n\n\tif *a.instConfig.MountType == limatype.REVSSHFS && len(a.instConfig.Mounts) > 0 {\n\t\treq = append(req, requirement{\n\t\t\tdescription: \"sshfs binary to be installed\",\n\t\t\tscript: `#!/bin/sh\nset -eux\ncommand -v sshfs\n`,\n\t\t\tdebugHint: `The sshfs binary was not installed in the guest.\nMake sure that you are using an officially supported image.\nAlso see \"/var/log/cloud-init-output.log\" in the guest.\nA possible workaround is to run \"apt-get install sshfs\" in the guest.\n`,\n\t\t})\n\t\treq = append(req, requirement{\n\t\t\tdescription: \"fuse to \\\"allow_other\\\" as user\",\n\t\t\tscript: `#!/bin/sh\nset -eux\nsudo grep -q ^user_allow_other /etc/fuse*.conf\n`,\n\t\t\tdebugHint: `Append \"user_allow_other\" to /etc/fuse.conf (/etc/fuse3.conf) in the guest`,\n\t\t})\n\t} else {\n\t\treq = append(req, startControlMasterReq)\n\t}\n\treturn req\n}\n\nfunc (a *HostAgent) optionalRequirements() []requirement {\n\treq := make([]requirement, 0)\n\tisLinuxGuest := a.instConfig.OS == nil || *a.instConfig.OS == limatype.LINUX\n\tif isLinuxGuest && (*a.instConfig.Containerd.System || *a.instConfig.Containerd.User) && !*a.instConfig.Plain {\n\t\treq = append(req,\n\t\t\trequirement{\n\t\t\t\tdescription: \"systemd must be available\",\n\t\t\t\tfatal:       true,\n\t\t\t\tscript: `#!/bin/bash\nset -eux -o pipefail\nif ! command -v systemctl 2>&1 >/dev/null; then\n    echo >&2 \"systemd is not available on this OS\"\n    exit 1\nfi\n`,\n\t\t\t\tdebugHint: `systemd is required to run containerd, but does not seem to be available.\nMake sure that you use an image that supports systemd. If you do not want to run\ncontainerd, please make sure that both 'container.system' and 'containerd.user'\nare set to 'false' in the config file.\n`,\n\t\t\t},\n\t\t\trequirement{\n\t\t\t\tdescription: \"containerd binaries to be installed\",\n\t\t\t\tscript: `#!/bin/sh\nset -eux\ncommand -v nerdctl || test -x ` + *a.instConfig.GuestInstallPrefix + `/bin/nerdctl\n`,\n\t\t\t\tdebugHint: `The nerdctl binary was not installed in the guest.\nMake sure that you are using an officially supported image.\nAlso see \"/var/log/cloud-init-output.log\" in the guest.\n`,\n\t\t\t})\n\t}\n\tfor _, probe := range a.instConfig.Probes {\n\t\tif probe.Mode == limatype.ProbeModeReadiness {\n\t\t\treq = append(req, requirement{\n\t\t\t\tdescription: probe.Description,\n\t\t\t\tscript:      *probe.Script,\n\t\t\t\tdebugHint:   probe.Hint,\n\t\t\t})\n\t\t}\n\t}\n\treturn req\n}\n\nfunc (a *HostAgent) finalRequirements() []requirement {\n\treq := make([]requirement, 0)\n\tlogLocation := \"/var/log/cloud-init-output.log in the guest\"\n\tif *a.instConfig.OS == limatype.DARWIN {\n\t\tlogLocation = \"serialv.log in the host\"\n\t}\n\treq = append(req,\n\t\trequirement{\n\t\t\tdescription: \"boot scripts must have finished\",\n\t\t\tscript: fmt.Sprintf(`#!/bin/sh\nset -eux\nBOOT_DONE=/run/lima-boot-done\nUNAME=\"$(uname)\"\nif [ \"$UNAME\" = \"Darwin\" ] || [ \"$UNAME\" = \"FreeBSD\" ]; then\n\tBOOT_DONE=/var/run/lima-boot-done\nfi\n[ \"$(cat \"$BOOT_DONE\" 2>/dev/null)\" = \"%s\" ]\n`, a.iid),\n\t\t\tdebugHint: `All boot scripts, provisioning scripts, and readiness probes must\nfinish before the instance is considered \"ready\".\nCheck \"` + logLocation + `\" to see where the process is blocked!\n`,\n\t\t})\n\treturn req\n}\n"
  },
  {
    "path": "pkg/hostagent/timesync.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostagent\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst (\n\ttimeSyncInterval     = 10 * time.Second\n\ttimeSyncStartupDelay = 5 * time.Second\n)\n\nfunc (a *HostAgent) startTimeSync(ctx context.Context) {\n\tselect {\n\tcase <-a.guestAgentAliveCh:\n\t\tlogrus.Info(\"Time sync: guest agent is alive, starting time synchronization\")\n\tcase <-ctx.Done():\n\t\treturn\n\t}\n\n\tselect {\n\tcase <-time.After(timeSyncStartupDelay):\n\tcase <-ctx.Done():\n\t\treturn\n\t}\n\n\tticker := time.NewTicker(timeSyncInterval)\n\tdefer ticker.Stop()\n\n\ta.syncTimeOnce(ctx)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlogrus.Debug(\"Time sync: context cancelled, stopping\")\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\ta.syncTimeOnce(ctx)\n\t\t}\n\t}\n}\n\nfunc (a *HostAgent) syncTimeOnce(ctx context.Context) {\n\tclient, err := a.getOrCreateClient(ctx)\n\tif err != nil {\n\t\tlogrus.WithError(err).Debug(\"Time sync: failed to get client\")\n\t\treturn\n\t}\n\n\thostTime := time.Now()\n\tresp, err := client.SyncTime(ctx, hostTime)\n\tif err != nil {\n\t\tlogrus.WithError(err).Debug(\"Time sync: RPC failed\")\n\t\treturn\n\t}\n\n\tif resp.Error != \"\" {\n\t\tlogrus.Warnf(\"Time sync: guest failed to set time: %s (drift was %dms)\", resp.Error, resp.DriftMs)\n\t\treturn\n\t}\n\n\tif resp.Adjusted {\n\t\tlogrus.Infof(\"Time sync: guest clock adjusted (was %dms off)\", resp.DriftMs)\n\t} else {\n\t\tlogrus.Debugf(\"Time sync: drift %dms within threshold\", resp.DriftMs)\n\t}\n}\n"
  },
  {
    "path": "pkg/httpclientutil/httpclientutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage httpclientutil\n\n// Forked from https://github.com/rootless-containers/rootlesskit/blob/v0.14.2/pkg/api/client/client.go\n// Apache License 2.0\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/httputil\"\n)\n\n// Get calls HTTP GET and verifies that the status code is 2XX .\nfunc Get(ctx context.Context, c *http.Client, url string) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := Successful(resp); err != nil {\n\t\tresp.Body.Close()\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc Head(ctx context.Context, c *http.Client, url string) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"HEAD\", url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := Successful(resp); err != nil {\n\t\tresp.Body.Close()\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc Post(ctx context.Context, c *http.Client, url string, body io.Reader) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := c.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := Successful(resp); err != nil {\n\t\tresp.Body.Close()\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc readAtMost(r io.Reader, maxBytes int) ([]byte, error) {\n\tlr := &io.LimitedReader{\n\t\tR: r,\n\t\tN: int64(maxBytes),\n\t}\n\tb, err := io.ReadAll(lr)\n\tif err != nil {\n\t\treturn b, err\n\t}\n\tif lr.N == 0 {\n\t\treturn b, fmt.Errorf(\"expected at most %d bytes, got more\", maxBytes)\n\t}\n\treturn b, nil\n}\n\n// HTTPStatusErrorBodyMaxLength specifies the maximum length of HTTPStatusError.Body.\nconst HTTPStatusErrorBodyMaxLength = 64 * 1024\n\n// HTTPStatusError is created from non-2XX HTTP response.\ntype HTTPStatusError struct {\n\t// StatusCode is non-2XX status code\n\tStatusCode int\n\t// Body is at most HTTPStatusErrorBodyMaxLength\n\tBody string\n}\n\n// Error implements error.\n// If e.Body is a marshalled string of httputil.ErrorJSON, Error returns ErrorJSON.Message .\n// Otherwise Error returns a human-readable string that contains e.StatusCode and e.Body.\nfunc (e *HTTPStatusError) Error() string {\n\tif e.Body != \"\" && len(e.Body) < HTTPStatusErrorBodyMaxLength {\n\t\tvar ej httputil.ErrorJSON\n\t\tif json.Unmarshal([]byte(e.Body), &ej) == nil {\n\t\t\treturn ej.Message\n\t\t}\n\t}\n\treturn fmt.Sprintf(\"unexpected HTTP status %s, body=%q\", http.StatusText(e.StatusCode), e.Body)\n}\n\nfunc Successful(resp *http.Response) error {\n\tif resp == nil {\n\t\treturn errors.New(\"nil response\")\n\t}\n\tif resp.StatusCode/100 != 2 {\n\t\tb, _ := readAtMost(resp.Body, HTTPStatusErrorBodyMaxLength)\n\t\treturn &HTTPStatusError{\n\t\t\tStatusCode: resp.StatusCode,\n\t\t\tBody:       string(b),\n\t\t}\n\t}\n\treturn nil\n}\n\n// NewHTTPClientWithDialFn creates a client.\n// conn is a raw net.Conn instance.\nfunc NewHTTPClientWithDialFn(dialFn func(ctx context.Context) (net.Conn, error)) (*http.Client, error) {\n\thc := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\treturn dialFn(ctx)\n\t\t\t},\n\t\t},\n\t}\n\treturn hc, nil\n}\n"
  },
  {
    "path": "pkg/httpclientutil/httpclientutil_others.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage httpclientutil\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n)\n\n// NewHTTPClientWithSocketPath creates a client.\n// socketPath is a path to the UNIX socket, without unix:// prefix.\nfunc NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) {\n\tif _, err := os.Stat(socketPath); err != nil {\n\t\treturn nil, err\n\t}\n\thc := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\tvar d net.Dialer\n\t\t\t\treturn d.DialContext(ctx, \"unix\", socketPath)\n\t\t\t},\n\t\t},\n\t}\n\treturn hc, nil\n}\n"
  },
  {
    "path": "pkg/httpclientutil/httpclientutil_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage httpclientutil\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n)\n\n// NewHTTPClientWithSocketPath creates a client.\n// socketPath is a path to the UNIX socket, without unix:// prefix.\nfunc NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) {\n\t// Use Lstat on windows, see: https://github.com/adrg/xdg/pull/14\n\tif _, err := os.Lstat(socketPath); err != nil {\n\t\treturn nil, err\n\t}\n\thc := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\tvar d net.Dialer\n\t\t\t\treturn d.DialContext(ctx, \"unix\", socketPath)\n\t\t\t},\n\t\t},\n\t}\n\treturn hc, nil\n}\n"
  },
  {
    "path": "pkg/httputil/httputil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage httputil\n\n// ErrorJSON is returned with \"application/json\" content type and non-2XX status code.\ntype ErrorJSON struct {\n\tMessage string `json:\"message\"`\n}\n"
  },
  {
    "path": "pkg/identifiers/validate.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://github.com/containerd/containerd/blob/v2.1.1/pkg/identifiers/validate.go\n// SPDX-FileCopyrightText: Copyright The containerd Authors\n// LICENSE: https://github.com/containerd/containerd/blob/v2.1.1/LICENSE\n// NOTICE: https://github.com/containerd/containerd/blob/v2.1.1/NOTICE\n\n/*\n   Copyright The containerd Authors.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n*/\n\n// Package identifiers provides common validation for identifiers and keys\n// across Lima (originally from containerd).\n//\n// Identifiers in Lima must be a alphanumeric, allowing limited\n// underscores, dashes and dots.\n//\n// While the character set may be expanded in the future, identifiers\n// are guaranteed to be safely used as filesystem path components.\npackage identifiers\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n)\n\nconst (\n\tmaxLength  = 76\n\talphanum   = `[A-Za-z0-9]+`\n\tseparators = `[._-]`\n)\n\n// identifierRe defines the pattern for valid identifiers.\nvar identifierRe = regexp.MustCompile(reAnchor(alphanum + reGroup(separators+reGroup(alphanum)) + \"*\"))\n\n// Validate returns nil if the string s is a valid identifier.\n//\n// Identifiers are similar to the domain name rules according to RFC 1035, section 2.3.1. However\n// rules in this package are relaxed to allow numerals to follow period (\".\") and mixed case is\n// allowed.\n//\n// In general identifiers that pass this validation should be safe for use as filesystem path components.\nfunc Validate(s string) error {\n\tif s == \"\" {\n\t\treturn errors.New(\"identifier must not be empty\")\n\t}\n\n\tif len(s) > maxLength {\n\t\treturn fmt.Errorf(\"identifier %q greater than maximum length (%d characters)\", s, maxLength)\n\t}\n\n\tif !identifierRe.MatchString(s) {\n\t\treturn fmt.Errorf(\"identifier %q must match %v\", s, identifierRe)\n\t}\n\treturn nil\n}\n\nfunc reGroup(s string) string {\n\treturn `(?:` + s + `)`\n}\n\nfunc reAnchor(s string) string {\n\treturn `^` + s + `$`\n}\n"
  },
  {
    "path": "pkg/identifiers/validate_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://github.com/containerd/containerd/blob/v2.1.1/pkg/identifiers/validate_test.go\n// SPDX-FileCopyrightText: Copyright The containerd Authors\n// LICENSE: https://github.com/containerd/containerd/blob/v2.1.1/LICENSE\n// NOTICE: https://github.com/containerd/containerd/blob/v2.1.1/NOTICE\n\n/*\n   Copyright The containerd Authors.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n*/\n\npackage identifiers\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestValidIdentifiers(t *testing.T) {\n\tfor _, input := range []string{\n\t\t\"default\",\n\t\t\"Default\",\n\t\tt.Name(),\n\t\t\"default-default\",\n\t\t\"containerd.io\",\n\t\t\"foo.boo\",\n\t\t\"swarmkit.docker.io\",\n\t\t\"0912341234\",\n\t\t\"task.0.0123456789\",\n\t\t\"container.system-75-f19a.00\",\n\t\t\"underscores_are_allowed\",\n\t\tstrings.Repeat(\"a\", maxLength),\n\t} {\n\t\tt.Run(input, func(t *testing.T) {\n\t\t\tassert.NilError(t, Validate(input))\n\t\t})\n\t}\n}\n\nfunc TestInvalidIdentifiers(t *testing.T) {\n\tfor _, input := range []string{\n\t\t\"\",\n\t\t\".foo..foo\",\n\t\t\"foo/foo\",\n\t\t\"foo/..\",\n\t\t\"foo..foo\",\n\t\t\"foo.-boo\",\n\t\t\"-foo.boo\",\n\t\t\"foo.boo-\",\n\t\t\"but__only_tasteful_underscores\",\n\t\t\"zn--e9.org\", // or something like it!\n\t\t\"default--default\",\n\t\tstrings.Repeat(\"a\", maxLength+1),\n\t} {\n\t\tt.Run(input, func(t *testing.T) {\n\t\t\tassert.ErrorContains(t, Validate(input), \"\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/imgutil/manager.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage imgutil\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n)\n\n// ImageDiskManager defines the common operations for disk image utilities.\ntype ImageDiskManager interface {\n\t// CreateDisk creates a new disk image with the specified size.\n\tCreateDisk(ctx context.Context, disk string, size int64) error\n\n\t// ResizeDisk resizes an existing disk image to the specified size.\n\tResizeDisk(ctx context.Context, disk string, size int64) error\n\n\t// Convert converts a disk image to the specified format.\n\tConvert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error\n\n\t// MakeSparse makes a file sparse, starting from the specified offset.\n\tMakeSparse(ctx context.Context, f *os.File, offset int64) error\n}\n"
  },
  {
    "path": "pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage asifutil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/plist\"\n)\n\n// NewASIF creates a new ASIF image file at the specified path with the given size.\nfunc NewASIF(path string, size int64) error {\n\tcreateArgs := []string{\"image\", \"create\", \"blank\", \"--fs\", \"none\", \"--format\", \"ASIF\", \"--size\", strconv.FormatInt(size, 10), path}\n\tif err := exec.CommandContext(context.Background(), \"diskutil\", createArgs...).Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to create ASIF image %q: %w\", path, err)\n\t}\n\tif _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {\n\t\tif _, err2 := os.Stat(path + \".asif\"); !errors.Is(err2, os.ErrNotExist) {\n\t\t\t// diskutil may create the file with .asif suffix\n\t\t\tif err3 := os.Rename(path+\".asif\", path); err3 != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to rename ASIF image from %q to %q: %w\", path+\".asif\", path, err3)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// NewAttachedASIF creates a new ASIF image file at the specified path with the given size\n// and attaches it, returning the attached device path and an open file handle.\n// The caller is responsible for detaching the ASIF image device when done.\nfunc NewAttachedASIF(path string, size int64) (string, *os.File, error) {\n\tif err := NewASIF(path, size); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tattachArgs := []string{\"image\", \"attach\", \"--noMount\", path}\n\tout, err := exec.CommandContext(context.Background(), \"diskutil\", attachArgs...).Output()\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"failed to attach ASIF image %q: %w\", path, err)\n\t}\n\tdevicePath := strings.TrimSpace(string(out))\n\tf, err := os.OpenFile(devicePath, os.O_RDWR, 0o644)\n\tif err != nil {\n\t\t_ = DetachASIF(devicePath)\n\t\treturn \"\", nil, fmt.Errorf(\"failed to open ASIF device %q: %w\", devicePath, err)\n\t}\n\treturn devicePath, f, err\n}\n\n// DetachASIF detaches the ASIF image device at the specified path.\nfunc DetachASIF(devicePath string) error {\n\tif output, err := exec.CommandContext(context.Background(), \"hdiutil\", \"detach\", devicePath).CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to detach ASIF image %q: %w: %s\", devicePath, err, output)\n\t}\n\treturn nil\n}\n\n// ResizeASIF resizes the ASIF image at the specified path to the given size.\nfunc ResizeASIF(path string, size int64) error {\n\tresizeArgs := []string{\"image\", \"resize\", \"--size\", fmt.Sprintf(\"%d\", size), path}\n\tif output, err := exec.CommandContext(context.Background(), \"diskutil\", resizeArgs...).CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to resize ASIF image %q: %w: %s\", path, err, output)\n\t}\n\treturn nil\n}\n\ntype AttachedDisk struct {\n\tDisk      string // whole disk device, e.g. \"disk4\" (GUID_partition_scheme)\n\tContainer string // APFS container partition, e.g. \"disk4s2\" (Apple_APFS)\n\tData      string // data volume device, e.g. \"disk7s5\"\n}\n\n// parseDiskutilImageAttachOutput parses the output of `diskutil image attach -plist -nomount <disk>`\n// and returns the attached disk information.\nfunc parseDiskutilImageAttachOutput(xmlStr string) (*AttachedDisk, error) {\n\tvar p plist.Plist\n\tif err := xml.Unmarshal([]byte(xmlStr), &p); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal xml: %w\", err)\n\t}\n\n\tif p.Value.Dict == nil {\n\t\treturn nil, errors.New(\"unexpected plist format: missing root dict\")\n\t}\n\n\tseVal, ok := p.Value.Dict[\"system-entities\"]\n\tif !ok || len(seVal.Array) == 0 {\n\t\treturn nil, errors.New(\"unexpected plist format: missing system-entities array\")\n\t}\n\n\tresult := &AttachedDisk{}\n\tfor _, devEnt := range seVal.Array {\n\t\tdevDict := devEnt.Dict\n\t\tif devDict == nil {\n\t\t\tcontinue\n\t\t}\n\t\tdevEntry, hasDevEntry := devDict[\"dev-entry\"]\n\t\tif !hasDevEntry || devEntry.String == nil || *devEntry.String == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif hint, ok := devDict[\"content-hint\"]; ok && hint.String != nil {\n\t\t\tswitch *hint.String {\n\t\t\tcase \"GUID_partition_scheme\":\n\t\t\t\tresult.Disk = *devEntry.String\n\t\t\tcase \"Apple_APFS\":\n\t\t\t\tresult.Container = *devEntry.String\n\t\t\t}\n\t\t}\n\t\tif role, ok := devDict[\"role\"]; ok && role.String != nil && *role.String == \"Data\" {\n\t\t\tresult.Data = *devEntry.String\n\t\t}\n\t}\n\tif result.Data == \"\" {\n\t\treturn nil, errors.New(\"no data device found in diskutil output\")\n\t}\n\n\treturn result, nil\n}\n\nvar ErrResourceTemporarilyUnavailable = errors.New(\"resource temporarily unavailable\")\n\n// DiskutilImageAttachNoMount executes `diskutil image attach -plist -nomount <disk>`.\nfunc DiskutilImageAttachNoMount(ctx context.Context, disk string) (*AttachedDisk, error) {\n\tcmd := exec.CommandContext(ctx, \"diskutil\", \"image\", \"attach\", \"-plist\", \"-nomount\", disk)\n\t// Enforce English output for parsing stderr\n\tcmd.Env = append(os.Environ(), \"LANG=C\", \"LC_ALL=C\")\n\tvar stdout, stderr bytes.Buffer\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\terrToWrap := err\n\t\tif strings.Contains(stderr.String(), \"Resource temporarily unavailable\") {\n\t\t\terrToWrap = ErrResourceTemporarilyUnavailable\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to execute %v: %w (stderr: %q)\", cmd.Args, errToWrap, stderr.String())\n\t}\n\treturn parseDiskutilImageAttachOutput(stdout.String())\n}\n"
  },
  {
    "path": "pkg/imgutil/nativeimgutil/asifutil/asif_darwin_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage asifutil\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestParseDiskutilImageAttachOutput(t *testing.T) {\n\tconst input = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>system-entities</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>GUID_partition_scheme</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk4</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>107374182400</integer>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_ISC</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk4s1</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>524288000</integer>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Container</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk5</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>524288000</integer>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk5s1</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Preboot</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>524288000</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>iSCPreboot</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk5s2</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>XART</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>524288000</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>xART</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk5s3</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Hardware</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>524288000</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Hardware</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk5s4</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Recovery</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>524288000</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Recovery</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk4s2</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Container</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk7</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk7s1</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>System</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t\t<key>volume-group</key>\n\t\t\t<string>4B262719-EFC8-4B4D-8831-8AD57E005B59</string>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Macintosh HD</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk7s2</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Preboot</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Preboot</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk7s3</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Recovery</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Recovery</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk7s4</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Update</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Update</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk7s5</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Data</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t\t<key>volume-group</key>\n\t\t\t<string>4B262719-EFC8-4B4D-8831-8AD57E005B59</string>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Data</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk7s6</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>VM</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>101481185280</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>VM</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Recovery</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk4s3</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>5368668160</integer>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Container</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk6</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>5368668160</integer>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk6s1</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Recovery</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>5368668160</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Recovery</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>content-hint</key>\n\t\t\t<string>Apple_APFS_Volume</string>\n\t\t\t<key>dev-entry</key>\n\t\t\t<string>disk6s2</string>\n\t\t\t<key>filesystem-name</key>\n\t\t\t<string>APFS</string>\n\t\t\t<key>filesystem-type</key>\n\t\t\t<string>apfs</string>\n\t\t\t<key>role</key>\n\t\t\t<string>Update</string>\n\t\t\t<key>size</key>\n\t\t\t<integer>5368668160</integer>\n\t\t\t<key>volume-name</key>\n\t\t\t<string>Update</string>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n`\n\texpected := &AttachedDisk{\n\t\tDisk:      \"disk4\",\n\t\tContainer: \"disk4s2\",\n\t\tData:      \"disk7s5\",\n\t}\n\tres, err := parseDiskutilImageAttachOutput(input)\n\tassert.NilError(t, err)\n\tassert.DeepEqual(t, res, expected)\n}\n"
  },
  {
    "path": "pkg/imgutil/nativeimgutil/asifutil/asif_others.go",
    "content": "//go:build !darwin\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage asifutil\n\nimport (\n\t\"errors\"\n\t\"os\"\n)\n\nvar ErrASIFNotSupported = errors.New(\"ASIF is only supported on macOS\")\n\nfunc NewASIF(_ string, _ int64) error {\n\treturn ErrASIFNotSupported\n}\n\nfunc NewAttachedASIF(_ string, _ int64) (string, *os.File, error) {\n\treturn \"\", nil, ErrASIFNotSupported\n}\n\nfunc DetachASIF(_ string) error {\n\treturn ErrASIFNotSupported\n}\n\nfunc ResizeASIF(_ string, _ int64) error {\n\treturn ErrASIFNotSupported\n}\n"
  },
  {
    "path": "pkg/imgutil/nativeimgutil/fuzz_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage nativeimgutil\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc FuzzConvertToRaw(f *testing.F) {\n\tf.Fuzz(func(t *testing.T, imgData []byte, withBacking bool, size int64) {\n\t\tsrcPath := filepath.Join(t.TempDir(), \"src.img\")\n\t\tdestPath := filepath.Join(t.TempDir(), \"dest.img\")\n\t\terr := os.WriteFile(srcPath, imgData, 0o600)\n\t\tassert.NilError(t, err)\n\t\t_ = convertTo(raw.Type, srcPath, destPath, &size, withBacking)\n\t})\n}\n"
  },
  {
    "path": "pkg/imgutil/nativeimgutil/nativeimgutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package nativeimgutil provides image utilities that do not depend on `qemu-img` binary.\npackage nativeimgutil\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"math\"\n\t\"math/rand/v2\"\n\t\"os\"\n\t\"path/filepath\"\n\n\tcontainerdfs \"github.com/containerd/continuity/fs\"\n\t\"github.com/docker/go-units\"\n\t\"github.com/lima-vm/go-qcow2reader\"\n\t\"github.com/lima-vm/go-qcow2reader/convert\"\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"github.com/lima-vm/go-qcow2reader/image/asif\"\n\t\"github.com/lima-vm/go-qcow2reader/image/qcow2\"\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/progressbar\"\n)\n\n// Disk image size must be aligned to sector size. Qemu block layer is rounding\n// up the size to 512 bytes. Apple virtualization framework reject disks not\n// aligned to 512 bytes.\nconst sectorSize = 512\n\n// NativeImageUtil is the native implementation of the imgutil.ImageDiskManager.\ntype NativeImageUtil struct{}\n\n// roundUp rounds size up to sectorSize.\nfunc roundUp(size int64) int64 {\n\tsectors := (size + sectorSize - 1) / sectorSize\n\treturn sectors * sectorSize\n}\n\n// convertTo converts a source disk into a raw or ASIF disk.\n// source and dest may be same.\n// convertTo is a NOP if source == dest, and no resizing is needed.\nfunc convertTo(destType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {\n\tsrcF, err := os.Open(source)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer srcF.Close()\n\tsrcImg, err := qcow2reader.Open(srcF)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to detect the format of %q: %w\", source, err)\n\t}\n\tif size != nil && *size < srcImg.Size() {\n\t\treturn fmt.Errorf(\"specified size %d is smaller than the original image size (%d) of %q\", *size, srcImg.Size(), source)\n\t}\n\tlogrus.Infof(\"Converting %q (%s) to a %s disk %q\", source, srcImg.Type(), destType, dest)\n\tswitch t := srcImg.Type(); t {\n\tcase raw.Type:\n\t\tif destType == raw.Type {\n\t\t\tif err = srcF.Close(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn convertRawToRaw(source, dest, size)\n\t\t}\n\tcase qcow2.Type:\n\t\tif !allowSourceWithBackingFile {\n\t\t\tq, ok := srcImg.(*qcow2.Qcow2)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"unexpected qcow2 image %T\", srcImg)\n\t\t\t}\n\t\t\tif q.BackingFile != \"\" {\n\t\t\t\treturn fmt.Errorf(\"qcow2 image %q has an unexpected backing file: %q\", source, q.BackingFile)\n\t\t\t}\n\t\t}\n\tcase asif.Type:\n\t\tif destType == asif.Type {\n\t\t\treturn convertASIFToASIF(source, dest, size)\n\t\t}\n\t\treturn fmt.Errorf(\"conversion from ASIF to %q is not supported\", destType)\n\tdefault:\n\t\tlogrus.Warnf(\"image %q has an unexpected format: %q\", source, t)\n\t}\n\tif err = srcImg.Readable(); err != nil {\n\t\treturn fmt.Errorf(\"image %q is not readable: %w\", source, err)\n\t}\n\n\t// Create a tmp file because source and dest can be same.\n\tvar (\n\t\tdestTmpF       *os.File\n\t\tdestTmp        string\n\t\tattachedDevice string\n\t)\n\tswitch destType {\n\tcase raw.Type:\n\t\tdestTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+\".lima-*.tmp\")\n\t\tdestTmp = destTmpF.Name()\n\tcase asif.Type:\n\t\t// destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file.\n\t\trandomBase := fmt.Sprintf(\"%s.lima-%d.tmp.asif\", filepath.Base(dest), rand.UintN(math.MaxUint))\n\t\tdestTmp = filepath.Join(filepath.Dir(dest), randomBase)\n\t\t// Since qcow2 image is smaller than expected size, we need to specify expected size to avoid resize later.\n\t\t// Resizing ASIF image is not supported by qemu-img which recognizes ASIF format as raw.\n\t\tvar newSize int64\n\t\tif size != nil {\n\t\t\tnewSize = *size\n\t\t} else {\n\t\t\tnewSize = srcImg.Size()\n\t\t}\n\t\tattachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, newSize)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported target image type: %q\", destType)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.RemoveAll(destTmp)\n\tdefer destTmpF.Close()\n\n\t// Truncating before copy eliminates the seeks during copy and provide a\n\t// hint to the file system that may minimize allocations and fragmentation\n\t// of the file.\n\tif err := makeSparse(destTmpF, srcImg.Size()); err != nil {\n\t\treturn err\n\t}\n\n\t// Copy\n\tbar, err := progressbar.New(srcImg.Size())\n\tif err != nil {\n\t\treturn err\n\t}\n\tbar.Start()\n\terr = convert.Convert(destTmpF, srcImg, convert.Options{Progress: bar})\n\tbar.Finish()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to convert image: %w\", err)\n\t}\n\n\t// Resize\n\tif size != nil {\n\t\tlogrus.Infof(\"Expanding to %s\", units.BytesSize(float64(*size)))\n\t\tif err = makeSparse(destTmpF, *size); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err = destTmpF.Close(); err != nil {\n\t\treturn err\n\t}\n\t// Detach ASIF device\n\tif destType == asif.Type {\n\t\terr := asifutil.DetachASIF(attachedDevice)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to detach ASIF image %q: %w\", attachedDevice, err)\n\t\t}\n\t}\n\n\t// Rename destTmp into dest\n\tif err = os.RemoveAll(dest); err != nil {\n\t\treturn err\n\t}\n\treturn os.Rename(destTmp, dest)\n}\n\nfunc convertRawToRaw(source, dest string, size *int64) error {\n\tif source != dest {\n\t\t// continuity attempts clonefile\n\t\tif err := containerdfs.CopyFile(dest, source); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to copy %q into %q: %w\", source, dest, err)\n\t\t}\n\t\tif err := os.Chmod(dest, 0o644); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set permissions on %q: %w\", dest, err)\n\t\t}\n\t}\n\tif size != nil {\n\t\tlogrus.Infof(\"Expanding to %s\", units.BytesSize(float64(*size)))\n\t\tdestF, err := os.OpenFile(dest, os.O_RDWR, 0o644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err = makeSparse(destF, *size); err != nil {\n\t\t\t_ = destF.Close()\n\t\t\treturn err\n\t\t}\n\t\treturn destF.Close()\n\t}\n\treturn nil\n}\n\nfunc convertASIFToASIF(source, dest string, size *int64) error {\n\tif source != dest {\n\t\tif err := containerdfs.CopyFile(dest, source); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to copy %q into %q: %w\", source, dest, err)\n\t\t}\n\t\tif err := os.Chmod(dest, 0o644); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set permissions on %q: %w\", dest, err)\n\t\t}\n\t}\n\tif size != nil {\n\t\tlogrus.Infof(\"Resizing to %s\", units.BytesSize(float64(*size)))\n\t\tif err := asifutil.ResizeASIF(dest, *size); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to resize ASIF image %q: %w\", dest, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc makeSparse(f *os.File, offset int64) error {\n\tif _, err := f.Seek(offset, io.SeekStart); err != nil {\n\t\treturn err\n\t}\n\treturn f.Truncate(offset)\n}\n\n// CreateDisk creates a new disk image with the specified size.\nfunc (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64) error {\n\tif _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {\n\t\treturn err\n\t}\n\tf, err := os.Create(disk)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\troundedSize := roundUp(size)\n\treturn f.Truncate(roundedSize)\n}\n\n// Convert converts a disk image to the specified format.\n// Currently supported formats are raw.Type and asif.Type.\nfunc (n *NativeImageUtil) Convert(_ context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {\n\treturn convertTo(imageType, source, dest, size, allowSourceWithBackingFile)\n}\n\n// ResizeDisk resizes an existing disk image to the specified size.\nfunc (n *NativeImageUtil) ResizeDisk(_ context.Context, disk string, size int64) error {\n\troundedSize := roundUp(size)\n\treturn os.Truncate(disk, roundedSize)\n}\n\n// MakeSparse makes a file sparse, starting from the specified offset.\nfunc (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64) error {\n\treturn makeSparse(f, offset)\n}\n"
  },
  {
    "path": "pkg/imgutil/nativeimgutil/nativeimgutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage nativeimgutil\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestRoundUp(t *testing.T) {\n\ttests := []struct {\n\t\tSize    int64\n\t\tRounded int64\n\t}{\n\t\t{0, 0},\n\t\t{1, 512},\n\t\t{511, 512},\n\t\t{512, 512},\n\t\t{123456789, 123457024},\n\t}\n\tfor _, tc := range tests {\n\t\tif roundUp(tc.Size) != tc.Rounded {\n\t\t\tt.Errorf(\"expected %d, got %d\", tc.Rounded, tc.Size)\n\t\t}\n\t}\n}\n\nfunc createImg(ctx context.Context, name, format, size string) error {\n\treturn exec.CommandContext(ctx, \"qemu-img\", \"create\", name, \"-f\", format, size).Run()\n}\n\nfunc TestConvertToRaw(t *testing.T) {\n\t_, err := exec.LookPath(\"qemu-img\")\n\tif err != nil {\n\t\tt.Skipf(\"qemu-img does not seem installed: %v\", err)\n\t}\n\ttmpDir := t.TempDir()\n\tctx := t.Context()\n\n\tqcowImage, err := os.Create(filepath.Join(tmpDir, \"qcow.img\"))\n\tassert.NilError(t, err)\n\tdefer qcowImage.Close()\n\terr = createImg(ctx, qcowImage.Name(), \"qcow2\", \"1M\")\n\tassert.NilError(t, err)\n\n\trawImage, err := os.Create(filepath.Join(tmpDir, \"raw.img\"))\n\tassert.NilError(t, err)\n\tdefer rawImage.Close()\n\terr = createImg(ctx, rawImage.Name(), \"raw\", \"1M\")\n\tassert.NilError(t, err)\n\n\trawImageExtended, err := os.Create(filepath.Join(tmpDir, \"raw_extended.img\"))\n\tassert.NilError(t, err)\n\tdefer rawImageExtended.Close()\n\terr = createImg(ctx, rawImageExtended.Name(), \"raw\", \"2M\")\n\tassert.NilError(t, err)\n\n\tt.Run(\"qcow without backing file\", func(t *testing.T) {\n\t\tresultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), \"_\"), \"/\", \"_\"))\n\n\t\terr = convertTo(raw.Type, qcowImage.Name(), resultImage, nil, false)\n\t\tassert.NilError(t, err)\n\t\tassertFileEquals(t, rawImage.Name(), resultImage)\n\t})\n\n\tt.Run(\"qcow with backing file\", func(t *testing.T) {\n\t\tresultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), \"_\"), \"/\", \"_\"))\n\n\t\terr = convertTo(raw.Type, qcowImage.Name(), resultImage, nil, true)\n\t\tassert.NilError(t, err)\n\t\tassertFileEquals(t, rawImage.Name(), resultImage)\n\t})\n\n\tt.Run(\"qcow with extra size\", func(t *testing.T) {\n\t\tresultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), \"_\"), \"/\", \"_\"))\n\n\t\tsize := int64(2_097_152) // 2mb\n\t\terr = convertTo(raw.Type, qcowImage.Name(), resultImage, &size, false)\n\t\tassert.NilError(t, err)\n\t\tassertFileEquals(t, rawImageExtended.Name(), resultImage)\n\t})\n\n\tt.Run(\"raw\", func(t *testing.T) {\n\t\tresultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), \"_\"), \"/\", \"_\"))\n\n\t\terr = convertTo(raw.Type, rawImage.Name(), resultImage, nil, false)\n\t\tassert.NilError(t, err)\n\t\tassertFileEquals(t, rawImage.Name(), resultImage)\n\t})\n}\n\nfunc assertFileEquals(t *testing.T, expected, actual string) {\n\texpectedContent, err := os.ReadFile(expected)\n\tassert.NilError(t, err)\n\tactualContent, err := os.ReadFile(actual)\n\tassert.NilError(t, err)\n\tassert.DeepEqual(t, expectedContent, actualContent)\n}\n"
  },
  {
    "path": "pkg/imgutil/proxyimgutil/proxyimgutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage proxyimgutil\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/qemuimgutil\"\n)\n\n// ImageDiskManager is a proxy implementation of imgutil.ImageDiskManager that uses both QEMU and native image utilities.\ntype ImageDiskManager struct {\n\tqemu   imgutil.ImageDiskManager\n\tnative imgutil.ImageDiskManager\n}\n\n// NewDiskUtil returns a new instance of ImageDiskManager that uses both QEMU and native image utilities.\nfunc NewDiskUtil(_ context.Context) imgutil.ImageDiskManager {\n\treturn &ImageDiskManager{\n\t\tqemu:   &qemuimgutil.QemuImageUtil{DefaultFormat: qemuimgutil.QemuImgFormat},\n\t\tnative: &nativeimgutil.NativeImageUtil{},\n\t}\n}\n\n// CreateDisk creates a new disk image with the specified size.\nfunc (p *ImageDiskManager) CreateDisk(ctx context.Context, disk string, size int64) error {\n\terr := p.qemu.CreateDisk(ctx, disk, size)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, exec.ErrNotFound) {\n\t\treturn p.native.CreateDisk(ctx, disk, size)\n\t}\n\treturn err\n}\n\n// ResizeDisk resizes an existing disk image to the specified size.\nfunc (p *ImageDiskManager) ResizeDisk(ctx context.Context, disk string, size int64) error {\n\terr := p.qemu.ResizeDisk(ctx, disk, size)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, exec.ErrNotFound) {\n\t\treturn p.native.ResizeDisk(ctx, disk, size)\n\t}\n\treturn err\n}\n\n// Convert converts a disk image to the specified format.\n// Currently supported formats are raw.Type and asif.Type.\nfunc (p *ImageDiskManager) Convert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {\n\tif imageType == raw.Type {\n\t\terr := p.qemu.Convert(ctx, imageType, source, dest, size, allowSourceWithBackingFile)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tif errors.Is(err, exec.ErrNotFound) {\n\t\t\treturn p.native.Convert(ctx, imageType, source, dest, size, allowSourceWithBackingFile)\n\t\t}\n\t\treturn err\n\t}\n\treturn p.native.Convert(ctx, imageType, source, dest, size, allowSourceWithBackingFile)\n}\n\nfunc (p *ImageDiskManager) MakeSparse(ctx context.Context, f *os.File, offset int64) error {\n\terr := p.qemu.MakeSparse(ctx, f, offset)\n\tif err == nil {\n\t\treturn nil\n\t}\n\tif errors.Is(err, exec.ErrNotFound) {\n\t\treturn p.native.MakeSparse(ctx, f, offset)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "pkg/instance/ansible.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\nfunc runAnsibleProvision(ctx context.Context, inst *limatype.Instance) error {\n\tfor _, f := range inst.Config.Provision {\n\t\tif f.Mode == limatype.ProvisionModeAnsible {\n\t\t\tlogrus.Infof(\"Waiting for ansible playbook %q\", f.Playbook)\n\t\t\tif err := runAnsiblePlaybook(ctx, inst, f.Playbook); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc runAnsiblePlaybook(ctx context.Context, inst *limatype.Instance, playbook string) error {\n\tinventory, err := createAnsibleInventory(inst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.Debugf(\"ansible-playbook -i %q %q\", inventory, playbook)\n\targs := []string{\"-i\", inventory, playbook}\n\tcmd := exec.CommandContext(ctx, \"ansible-playbook\", args...)\n\tcmd.Env = getAnsibleEnvironment(inst)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n}\n\nfunc createAnsibleInventory(inst *limatype.Instance) (string, error) {\n\tvars := map[string]any{\n\t\t\"ansible_connection\":      \"ssh\",\n\t\t\"ansible_host\":            inst.Hostname,\n\t\t\"ansible_ssh_common_args\": \"-F \" + inst.SSHConfigFile,\n\t}\n\thosts := map[string]any{\n\t\tinst.Name: vars,\n\t}\n\tgroup := \"lima\"\n\tdata := map[string]any{\n\t\tgroup: map[string]any{\n\t\t\t\"hosts\": hosts,\n\t\t},\n\t}\n\tbytes, err := yaml.Marshal(data)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tinventory := filepath.Join(inst.Dir, filenames.AnsibleInventoryYAML)\n\treturn inventory, os.WriteFile(inventory, bytes, 0o644)\n}\n\nfunc getAnsibleEnvironment(inst *limatype.Instance) []string {\n\tenv := os.Environ()\n\tfor key, val := range inst.Config.Param {\n\t\tenv = append(env, fmt.Sprintf(\"PARAM_%s=%s\", key, val))\n\t}\n\treturn env\n}\n"
  },
  {
    "path": "pkg/instance/clone.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\tcontinuityfs \"github.com/containerd/continuity/fs\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc CloneOrRename(ctx context.Context, oldInst *limatype.Instance, newInstName string, rename bool) (*limatype.Instance, error) {\n\tverb := \"clone\"\n\tif rename {\n\t\tverb = \"rename\"\n\t}\n\tif newInstName == \"\" {\n\t\treturn nil, errors.New(\"got empty instName\")\n\t}\n\tif oldInst.Name == newInstName {\n\t\treturn nil, fmt.Errorf(\"new instance name %q must be different from %q\", newInstName, oldInst.Name)\n\t}\n\tif oldInst.Status == limatype.StatusRunning {\n\t\treturn nil, errors.New(\"cannot \" + verb + \" a running instance\")\n\t}\n\n\tnewInstDir, err := dirnames.InstanceDir(newInstName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, err = os.Stat(newInstDir); !errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil, fmt.Errorf(\"instance %q already exists\", newInstName)\n\t}\n\n\t// the full path of the socket name must be less than UNIX_PATH_MAX chars.\n\tmaxSockName := filepath.Join(newInstDir, filenames.LongestSock)\n\tif len(maxSockName) >= osutil.UnixPathMax {\n\t\treturn nil, fmt.Errorf(\"instance name %q too long: %q must be less than UNIX_PATH_MAX=%d characters, but is %d\",\n\t\t\tnewInstName, maxSockName, osutil.UnixPathMax, len(maxSockName))\n\t}\n\n\tif err = os.Mkdir(newInstDir, 0o700); err != nil {\n\t\treturn nil, err\n\t}\n\n\twalkDirFn := func(path string, d fs.DirEntry, err error) error {\n\t\tbase := filepath.Base(path)\n\t\tif slices.Contains(filenames.SkipOnClone, base) {\n\t\t\treturn nil\n\t\t}\n\t\tfor _, ext := range filenames.TmpFileSuffixes {\n\t\t\tif strings.HasSuffix(path, ext) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpathRel, err := filepath.Rel(oldInst.Dir, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdst := filepath.Join(newInstDir, pathRel)\n\t\tif d.IsDir() {\n\t\t\treturn os.MkdirAll(dst, d.Type().Perm())\n\t\t}\n\t\t// NullifyOnClone contains VzIdentifier.\n\t\t// VzIdentifier file must not be just removed here, as pkg/limayaml depends on\n\t\t// the existence of VzIdentifier for resolving the VM type.\n\t\tif slices.Contains(filenames.NullifyOnClone, base) {\n\t\t\treturn os.WriteFile(dst, nil, 0o666)\n\t\t}\n\t\tif rename {\n\t\t\treturn os.Rename(path, dst)\n\t\t}\n\t\t// CopyFile attempts copy-on-write when supported by the filesystem\n\t\treturn continuityfs.CopyFile(dst, path)\n\t}\n\n\tif err = filepath.WalkDir(oldInst.Dir, walkDirFn); err != nil {\n\t\treturn nil, err\n\t}\n\tif rename {\n\t\tif err = os.RemoveAll(oldInst.Dir); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn store.Inspect(ctx, newInstName)\n}\n"
  },
  {
    "path": "pkg/instance/create.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/cidata\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n)\n\nfunc Create(ctx context.Context, instName string, instConfig []byte, saveBrokenYAML bool) (*limatype.Instance, error) {\n\tif instName == \"\" {\n\t\treturn nil, errors.New(\"got empty instName\")\n\t}\n\tif len(instConfig) == 0 {\n\t\treturn nil, errors.New(\"got empty instConfig\")\n\t}\n\n\tinstDir, err := dirnames.InstanceDir(instName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// the full path of the socket name must be less than UNIX_PATH_MAX chars.\n\tmaxSockName := filepath.Join(instDir, filenames.LongestSock)\n\tif len(maxSockName) >= osutil.UnixPathMax {\n\t\treturn nil, fmt.Errorf(\"instance name %q too long: %q must be less than UNIX_PATH_MAX=%d characters, but is %d\",\n\t\t\tinstName, maxSockName, osutil.UnixPathMax, len(maxSockName))\n\t}\n\tif _, err := os.Stat(instDir); !errors.Is(err, os.ErrNotExist) {\n\t\treturn nil, fmt.Errorf(\"instance %q already exists (%q)\", instName, instDir)\n\t}\n\t// limayaml.Load() needs to pass the store file path to limayaml.FillDefault() to calculate default MAC addresses\n\tfilePath := filepath.Join(instDir, filenames.LimaYAML)\n\tloadedInstConfig, err := limayaml.LoadWithWarnings(ctx, instConfig, filePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := driverutil.ResolveVMType(ctx, loadedInstConfig, filePath); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to resolve vm for %q: %w\", filePath, err)\n\t}\n\tif err := limayaml.Validate(loadedInstConfig, true); err != nil {\n\t\tif !saveBrokenYAML {\n\t\t\treturn nil, err\n\t\t}\n\t\trejectedYAML := \"lima.REJECTED.yaml\"\n\t\tif writeErr := os.WriteFile(rejectedYAML, instConfig, 0o644); writeErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"the YAML is invalid, attempted to save the buffer as %q but failed: %w: %w\", rejectedYAML, writeErr, err)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"the YAML is invalid, saved the buffer as %q: %w\", rejectedYAML, err)\n\t}\n\tif err := os.MkdirAll(instDir, 0o700); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := os.WriteFile(filePath, instConfig, 0o644); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := cidata.GenerateCloudConfig(ctx, instDir, instName, loadedInstConfig); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := os.WriteFile(filepath.Join(instDir, filenames.LimaVersion), []byte(version.Version), 0o444); err != nil {\n\t\treturn nil, err\n\t}\n\n\tinst, err := store.Inspect(ctx, instName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\n\tif err := limaDriver.Create(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn inst, nil\n}\n"
  },
  {
    "path": "pkg/instance/delete.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nfunc Delete(ctx context.Context, inst *limatype.Instance, force bool) error {\n\tif inst.Protected {\n\t\treturn errors.New(\"instance is protected to prohibit accidental removal (Hint: use `limactl unprotect`)\")\n\t}\n\tif !force && inst.Status != limatype.StatusStopped {\n\t\treturn fmt.Errorf(\"expected status %q, got %q\", limatype.StatusStopped, inst.Status)\n\t}\n\n\tStopForcibly(inst)\n\n\tif len(inst.Errors) == 0 {\n\t\tif err := unregister(ctx, inst); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unregister %q: %w\", inst.Dir, err)\n\t\t}\n\t}\n\tif err := os.RemoveAll(inst.Dir); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove %q: %w\", inst.Dir, err)\n\t}\n\n\treturn nil\n}\n\nfunc unregister(ctx context.Context, inst *limatype.Instance) error {\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\n\treturn limaDriver.Delete(ctx)\n}\n"
  },
  {
    "path": "pkg/instance/hostname/hostname.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostname\n\nimport \"strings\"\n\n// FromInstName generates a hostname from an instance name by prefixing\n// it with \"lima-\" and replacing all dots and underscores with dashes.\n// E.g. \"my_example.com\" becomes \"lima-my-example-com\".\nfunc FromInstName(instName string) string {\n\ts := strings.ReplaceAll(instName, \".\", \"-\")\n\ts = strings.ReplaceAll(s, \"_\", \"-\")\n\treturn \"lima-\" + s\n}\n"
  },
  {
    "path": "pkg/instance/hostname/hostname_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage hostname_test\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/instance/hostname\"\n)\n\nfunc TestFromInstName(t *testing.T) {\n\tassert.Equal(t, hostname.FromInstName(\"default\"), \"lima-default\")\n\tassert.Equal(t, hostname.FromInstName(\"ubuntu-24.04\"), \"lima-ubuntu-24-04\")\n\tassert.Equal(t, hostname.FromInstName(\"foo_bar.baz\"), \"lima-foo-bar-baz\")\n}\n"
  },
  {
    "path": "pkg/instance/restart.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/reconcile\"\n)\n\nconst (\n\tlaunchHostAgentForeground = false\n)\n\nfunc Restart(ctx context.Context, inst *limatype.Instance, showProgress bool) error {\n\tif err := StopGracefully(ctx, inst, true); err != nil {\n\t\treturn err\n\t}\n\n\t// Network reconciliation will be performed by the process launched by the autostart manager\n\tif registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t} else if !registered {\n\t\tif err := reconcile.Reconcile(ctx, inst.Name); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := Start(ctx, inst, launchHostAgentForeground, showProgress); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc RestartForcibly(ctx context.Context, inst *limatype.Instance, showProgress bool) error {\n\tlogrus.Info(\"Restarting the instance forcibly\")\n\tStopForcibly(inst)\n\n\tif registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\treturn fmt.Errorf(\"failed to check if the autostart entry for instance %q is registered: %w\", inst.Name, err)\n\t} else if !registered {\n\t\tif err := reconcile.Reconcile(ctx, inst.Name); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := Start(ctx, inst, launchHostAgentForeground, showProgress); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/instance/start.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/lima-vm/go-qcow2reader\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\t\"github.com/lima-vm/lima/v2/pkg/cacheutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/executil\"\n\t\"github.com/lima-vm/lima/v2/pkg/fileutils\"\n\thostagentevents \"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/registry\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n\t\"github.com/lima-vm/lima/v2/pkg/usrlocal\"\n)\n\n// DefaultWatchHostAgentEventsTimeout is the duration to wait for the instance\n// to be running before timing out.\nconst DefaultWatchHostAgentEventsTimeout = 10 * time.Minute\n\ntype Prepared struct {\n\tDriver              driver.Driver\n\tGuestAgent          string\n\tNerdctlArchiveCache string\n}\n\n// Prepare ensures the disk, the nerdctl archive, etc.\nfunc Prepare(ctx context.Context, inst *limatype.Instance, guestAgent string) (*Prepared, error) {\n\tvar needsGuestAgent bool\n\tswitch *inst.Config.OS {\n\tcase limatype.DARWIN:\n\t\t// macOS guests always need the guest agent for running fake-cloud-init\n\t\tneedsGuestAgent = true\n\tcase limatype.FREEBSD:\n\t\t// guest agent is not implemented for FreeBSD yet\n\t\tneedsGuestAgent = false\n\tdefault:\n\t\tneedsGuestAgent = !*inst.Config.Plain\n\t}\n\tif needsGuestAgent && guestAgent == \"\" {\n\t\tvar err error\n\t\tguestAgent, err = usrlocal.GuestAgentBinary(*inst.Config.OS, *inst.Config.Arch)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\n\tif err := limaDriver.Validate(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := limaDriver.Create(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Migrate legacy disk layout (diffdisk → disk, ISO basedisk → iso)\n\tif err := driverutil.MigrateDiskLayout(inst.Dir); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcreated := limayaml.IsExistingInstanceDir(inst.Dir)\n\n\timagePath := filepath.Join(inst.Dir, filenames.Image)\n\tdisk := filepath.Join(inst.Dir, filenames.Disk)\n\tkernel := filepath.Join(inst.Dir, filenames.Kernel)\n\tkernelCmdline := filepath.Join(inst.Dir, filenames.KernelCmdline)\n\tinitrd := filepath.Join(inst.Dir, filenames.Initrd)\n\tif !osutil.FileExists(imagePath) && !osutil.FileExists(disk) {\n\t\tvar ensuredImage bool\n\t\terrs := make([]error, len(inst.Config.Images))\n\t\tfor i, f := range inst.Config.Images {\n\t\t\tif _, err := fileutils.DownloadFile(ctx, imagePath, f.File, true, \"the image\", *inst.Config.Arch); err != nil {\n\t\t\t\terrs[i] = err\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif f.Kernel != nil {\n\t\t\t\t// ensure decompress kernel because vz expects it to be decompressed\n\t\t\t\tif _, err := fileutils.DownloadFile(ctx, kernel, f.Kernel.File, true, \"the kernel\", *inst.Config.Arch); err != nil {\n\t\t\t\t\terrs[i] = err\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif f.Kernel.Cmdline != \"\" {\n\t\t\t\t\tif err := os.WriteFile(kernelCmdline, []byte(f.Kernel.Cmdline), 0o644); err != nil {\n\t\t\t\t\t\terrs[i] = err\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif f.Initrd != nil {\n\t\t\t\t// vz does not need initrd to be decompressed\n\t\t\t\tif _, err := fileutils.DownloadFile(ctx, initrd, *f.Initrd, false, \"the initrd\", *inst.Config.Arch); err != nil {\n\t\t\t\t\terrs[i] = err\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tensuredImage = true\n\t\t\tbreak\n\t\t}\n\t\tif !ensuredImage {\n\t\t\treturn nil, fileutils.Errors(errs)\n\t\t}\n\t}\n\n\tif err := limaDriver.CreateDisk(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Ensure disk size matches the configured value\n\tif err := prepareDisk(ctx, inst); err != nil {\n\t\treturn nil, err\n\t}\n\n\tnerdctlArchiveCache, err := cacheutil.EnsureNerdctlArchiveCache(ctx, inst.Config, created)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Prepared{\n\t\tDriver:              limaDriver,\n\t\tGuestAgent:          guestAgent,\n\t\tNerdctlArchiveCache: nerdctlArchiveCache,\n\t}, nil\n}\n\n// StartWithPaths starts the hostagent in the background, which in turn will start the instance.\n// StartWithPaths will listen to hostagent events and log them to STDOUT until either the instance\n// is running, or has failed to start.\n//\n// The launchHostAgentForeground argument makes the hostagent run in the foreground.\n// The function will continue to listen and log hostagent events until the instance is\n// shut down again.\n//\n// The showProgress argument tells the hostagent to show provision script progress by tailing cloud-init logs.\n//\n// The limactl argument allows the caller to specify the full path of the limactl executable.\n// The guestAgent argument allows the caller to specify the full path of the guest agent executable.\n// Inside limactl this function is only called by Start, which passes empty strings for both\n// limactl and guestAgent, in which case the location of the current executable is used for\n// limactl and the guest agent is located from the corresponding <prefix>/share/lima directory.\n//\n// StartWithPaths calls Prepare by itself, so you do not need to call Prepare manually before calling Start.\nfunc StartWithPaths(ctx context.Context, inst *limatype.Instance, launchHostAgentForeground, showProgress bool, limactl, guestAgent string) error {\n\thaPIDPath := filepath.Join(inst.Dir, filenames.HostAgentPID)\n\tif _, err := os.Stat(haPIDPath); !errors.Is(err, os.ErrNotExist) {\n\t\treturn fmt.Errorf(\"instance %q seems running (hint: remove %q if the instance is not actually running)\", inst.Name, haPIDPath)\n\t}\n\tlogrus.Infof(\"Starting the instance %q with %s VM driver %q\", inst.Name, registry.CheckInternalOrExternal(inst.VMType), inst.VMType)\n\n\thaSockPath := filepath.Join(inst.Dir, filenames.HostAgentSock)\n\n\tprepared, err := Prepare(ctx, inst, guestAgent)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif limactl == \"\" {\n\t\tlimactl, err = os.Executable()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\thaStdoutPath := filepath.Join(inst.Dir, filenames.HostAgentStdoutLog)\n\thaStderrPath := filepath.Join(inst.Dir, filenames.HostAgentStderrLog)\n\n\tbegin := time.Now() // used for logrus propagation\n\tvar haCmd *exec.Cmd\n\tif isRegisteredToAutoStart, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\treturn fmt.Errorf(\"failed to check autostart registration: %w\", err)\n\t} else if !isRegisteredToAutoStart || launchHostAgentForeground {\n\t\tif err := os.RemoveAll(haStdoutPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := os.RemoveAll(haStderrPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t\thaStdoutW, err := os.Create(haStdoutPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// no defer haStdoutW.Close()\n\t\thaStderrW, err := os.Create(haStderrPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// no defer haStderrW.Close()\n\n\t\tvar args []string\n\t\tif logrus.GetLevel() >= logrus.DebugLevel {\n\t\t\targs = append(args, \"--debug\")\n\t\t}\n\t\targs = append(args,\n\t\t\t\"hostagent\",\n\t\t\t\"--pidfile\", haPIDPath,\n\t\t\t\"--socket\", haSockPath)\n\t\tif prepared.Driver.Info().Features.CanRunGUI {\n\t\t\targs = append(args, \"--run-gui\")\n\t\t}\n\t\tif prepared.GuestAgent != \"\" {\n\t\t\targs = append(args, \"--guestagent\", prepared.GuestAgent)\n\t\t}\n\t\tif prepared.NerdctlArchiveCache != \"\" {\n\t\t\targs = append(args, \"--nerdctl-archive\", prepared.NerdctlArchiveCache)\n\t\t}\n\t\tif showProgress {\n\t\t\targs = append(args, \"--progress\")\n\t\t}\n\t\targs = append(args, inst.Name)\n\t\thaCmd = exec.CommandContext(ctx, limactl, args...)\n\n\t\thaCmd.SysProcAttr = executil.BackgroundSysProcAttr\n\n\t\thaCmd.Stdout = haStdoutW\n\t\thaCmd.Stderr = haStderrW\n\n\t\tif launchHostAgentForeground {\n\t\t\tif isRegisteredToAutoStart {\n\t\t\t\tlogrus.Warn(\"The instance is registered to start at login, but the --foreground option was given, so starting the instance directly\")\n\t\t\t}\n\t\t\thaCmd.SysProcAttr = executil.ForegroundSysProcAttr\n\t\t\tif err := execHostAgentForeground(limactl, haCmd); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if err := haCmd.Start(); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if err = autostart.RequestStart(ctx, inst); err != nil {\n\t\treturn fmt.Errorf(\"failed to request start via autostart manager: %w\", err)\n\t}\n\n\tif err := waitHostAgentStart(ctx, haPIDPath, haStderrPath); err != nil {\n\t\treturn err\n\t}\n\n\twatchErrCh := make(chan error)\n\tgo func() {\n\t\twatchErrCh <- watchHostAgentEvents(ctx, inst, haStdoutPath, haStderrPath, begin, showProgress)\n\t\tclose(watchErrCh)\n\t}()\n\twaitErrCh := make(chan error)\n\tif haCmd != nil {\n\t\tgo func() {\n\t\t\twaitErrCh <- haCmd.Wait()\n\t\t\tclose(waitErrCh)\n\t\t}()\n\t} else {\n\t\tdefer close(waitErrCh)\n\t}\n\n\tselect {\n\tcase watchErr := <-watchErrCh:\n\t\t// watchErr can be nil\n\t\treturn watchErr\n\t\t// leave the hostagent process running\n\tcase waitErr := <-waitErrCh:\n\t\t// waitErr should not be nil\n\t\treturn fmt.Errorf(\"host agent process has exited: %w\", waitErr)\n\t}\n}\n\nfunc Start(ctx context.Context, inst *limatype.Instance, launchHostAgentForeground, showProgress bool) error {\n\treturn StartWithPaths(ctx, inst, launchHostAgentForeground, showProgress, \"\", \"\")\n}\n\nfunc waitHostAgentStart(_ context.Context, haPIDPath, haStderrPath string) error {\n\tbegin := time.Now()\n\tdeadlineDuration := 5 * time.Second\n\tdeadline := begin.Add(deadlineDuration)\n\tfor {\n\t\tif _, err := os.Stat(haPIDPath); !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil\n\t\t}\n\t\tif time.Now().After(deadline) {\n\t\t\treturn fmt.Errorf(\"hostagent (%q) did not start up in %v (hint: see %q)\", haPIDPath, deadlineDuration, haStderrPath)\n\t\t}\n\t}\n}\n\nfunc watchHostAgentEvents(ctx context.Context, inst *limatype.Instance, haStdoutPath, haStderrPath string, begin time.Time, showProgress bool) error {\n\tctx, cancel := context.WithTimeout(ctx, watchHostAgentTimeout(ctx))\n\tdefer cancel()\n\n\tvar (\n\t\tprintedSSHLocalPort  bool\n\t\treceivedRunningEvent bool\n\t\tcloudInitCompleted   bool\n\t\terr                  error\n\t)\n\n\tonEvent := func(ev hostagentevents.Event) bool {\n\t\tif !printedSSHLocalPort && ev.Status.SSHLocalPort != 0 {\n\t\t\tlogrus.Infof(\"SSH Local Port: %d\", ev.Status.SSHLocalPort)\n\t\t\tprintedSSHLocalPort = true\n\n\t\t\t// Update the instance's SSH port\n\t\t\tinst.SSHLocalPort = ev.Status.SSHLocalPort\n\t\t}\n\n\t\tif showProgress && ev.Status.CloudInitProgress != nil {\n\t\t\tprogress := ev.Status.CloudInitProgress\n\t\t\tif progress.Active && progress.LogLine == \"\" {\n\t\t\t\tlogrus.Infof(\"Cloud-init provisioning started...\")\n\t\t\t}\n\n\t\t\tif progress.LogLine != \"\" {\n\t\t\t\tlogrus.Infof(\"[cloud-init] %s\", progress.LogLine)\n\t\t\t}\n\n\t\t\tif progress.Completed {\n\t\t\t\tcloudInitCompleted = true\n\t\t\t\tlogrus.Infof(\"Cloud-init progress monitoring done.\")\n\t\t\t}\n\t\t}\n\t\tif len(ev.Status.Errors) > 0 {\n\t\t\tlogrus.Errorf(\"%+v\", ev.Status.Errors)\n\t\t}\n\t\tif ev.Status.Exiting {\n\t\t\terr = fmt.Errorf(\"exiting, status=%+v (hint: see %q)\", ev.Status, haStderrPath)\n\t\t\treturn true\n\t\t} else if ev.Status.Running {\n\t\t\treceivedRunningEvent = true\n\t\t\tif ev.Status.Degraded {\n\t\t\t\tlogrus.Warnf(\"DEGRADED. The VM seems running, but file sharing and port forwarding may not work. (hint: see %q)\", haStderrPath)\n\t\t\t\terr = fmt.Errorf(\"degraded, status=%+v\", ev.Status)\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tif xerr := runAnsibleProvision(ctx, inst); xerr != nil {\n\t\t\t\terr = xerr\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tif showProgress && !cloudInitCompleted {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif !isLaunchingShell(ctx) {\n\t\t\t\tif *inst.Config.Plain {\n\t\t\t\t\tlogrus.Infof(\"READY. Run `ssh -F %q %s` to open the shell.\", inst.SSHConfigFile, inst.Hostname)\n\t\t\t\t} else {\n\t\t\t\t\tlogrus.Infof(\"READY. Run `%s` to open the shell.\", LimactlShellCmd(inst.Name))\n\t\t\t\t}\n\t\t\t}\n\t\t\t_ = ShowMessage(inst)\n\t\t\terr = nil\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tif xerr := hostagentevents.Watch(ctx, haStdoutPath, haStderrPath, begin, true, onEvent); xerr != nil {\n\t\treturn xerr\n\t}\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !receivedRunningEvent {\n\t\treturn errors.New(\"did not receive an event with the \\\"running\\\" status\")\n\t}\n\n\treturn nil\n}\n\ntype watchHostAgentEventsTimeoutKey = struct{}\n\n// WithWatchHostAgentTimeout sets the value of the timeout to use for\n// watchHostAgentEvents in the given Context.\nfunc WithWatchHostAgentTimeout(ctx context.Context, timeout time.Duration) context.Context {\n\treturn context.WithValue(ctx, watchHostAgentEventsTimeoutKey{}, timeout)\n}\n\ntype launchingShellKey = struct{}\n\n// WithLaunchingShell marks the context as launching a shell after start,\n// suppressing the \"READY. Run ... to open the shell\" message.\nfunc WithLaunchingShell(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, launchingShellKey{}, true)\n}\n\n// IsLaunchingShell returns whether the launching shell flag is set in the context.\nfunc isLaunchingShell(ctx context.Context) bool {\n\tv, _ := ctx.Value(launchingShellKey{}).(bool)\n\treturn v\n}\n\n// watchHostAgentTimeout returns the value of the timeout to use for\n// watchHostAgentEvents contained in the given Context, or its default value.\nfunc watchHostAgentTimeout(ctx context.Context) time.Duration {\n\tif timeout, ok := ctx.Value(watchHostAgentEventsTimeoutKey{}).(time.Duration); ok {\n\t\treturn timeout\n\t}\n\treturn DefaultWatchHostAgentEventsTimeout\n}\n\nfunc LimactlShellCmd(instName string) string {\n\tshellCmd := fmt.Sprintf(\"limactl shell %s\", instName)\n\tif instName == \"default\" {\n\t\tshellCmd = \"lima\"\n\t}\n\treturn shellCmd\n}\n\nfunc ShowMessage(inst *limatype.Instance) error {\n\tif inst.Message == \"\" {\n\t\treturn nil\n\t}\n\tt, err := template.New(\"message\").Parse(inst.Message)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdata, err := store.AddGlobalFields(inst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar b bytes.Buffer\n\tif err := t.Execute(&b, data); err != nil {\n\t\treturn err\n\t}\n\tscanner := bufio.NewScanner(&b)\n\tlogrus.Infof(\"Message from the instance %q:\", inst.Name)\n\tfor scanner.Scan() {\n\t\t// Avoid prepending logrus \"INFO\" header, for ease of copy pasting\n\t\tfmt.Fprintln(logrus.StandardLogger().Out, scanner.Text())\n\t}\n\treturn scanner.Err()\n}\n\n// prepareDisk resizes the VM disk if its size differs from the configured size.\n// Returns nil if the disk does not yet exist (instance not yet initialized).\nfunc prepareDisk(ctx context.Context, inst *limatype.Instance) error {\n\tdisk := filepath.Join(inst.Dir, filenames.Disk)\n\n\t_, err := os.Stat(disk)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\n\tf, err := os.Open(disk)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\timg, err := qcow2reader.Open(f)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdiskSize := img.Size()\n\n\tif inst.Disk == diskSize {\n\t\treturn nil\n\t}\n\n\tlogrus.Infof(\"Resize instance %s's disk from %s to %s\", inst.Name, units.BytesSize(float64(diskSize)), units.BytesSize(float64(inst.Disk)))\n\n\tif inst.Disk < diskSize {\n\t\tinst.Disk = diskSize\n\t\treturn errors.New(\"disk shrinking is not supported\")\n\t}\n\n\tdiskUtil := proxyimgutil.NewDiskUtil(ctx)\n\n\treturn diskUtil.ResizeDisk(ctx, disk, inst.Disk)\n}\n"
  },
  {
    "path": "pkg/instance/start_unix.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\nfunc execHostAgentForeground(limactl string, haCmd *exec.Cmd) error {\n\thaStdoutW, ok := haCmd.Stdout.(*os.File)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected haCmd.Stdout to be *os.File, got %T\", haCmd.Stdout)\n\t}\n\thaStderrW, ok := haCmd.Stderr.(*os.File)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected haCmd.Stderr to be *os.File, got %T\", haCmd.Stderr)\n\t}\n\tlogrus.Info(\"Running the host agent in the foreground\")\n\tif isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) {\n\t\t// Write message to standard log files to avoid confusing users\n\t\tmessage := \"This log file is not used because `limactl start` was launched in the terminal with the `--foreground` option.\"\n\t\tif _, err := haStdoutW.WriteString(message); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := haStderrW.WriteString(message); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tif err := osutil.Dup2(int(haStdoutW.Fd()), syscall.Stdout); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := osutil.Dup2(int(haStderrW.Fd()), syscall.Stderr); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn syscall.Exec(limactl, haCmd.Args, haCmd.Environ())\n}\n"
  },
  {
    "path": "pkg/instance/start_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"errors\"\n\t\"os/exec\"\n)\n\nfunc execHostAgentForeground(_ string, _ *exec.Cmd) error {\n\treturn errors.New(\"`limactl start --foreground` is not supported on Windows\")\n}\n"
  },
  {
    "path": "pkg/instance/stop.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage instance\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/autostart\"\n\thostagentevents \"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc StopGracefully(ctx context.Context, inst *limatype.Instance, isRestart bool) error {\n\tif inst.Status != limatype.StatusRunning {\n\t\tif isRestart {\n\t\t\tlogrus.Warn(\"The instance is not running, continuing with the restart\")\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"expected status %q, got %q (maybe use `limactl stop -f`?)\", limatype.StatusRunning, inst.Status)\n\t}\n\n\tbegin := time.Now() // used for logrus propagation\n\tif requested, err := autostart.RequestStop(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {\n\t\treturn fmt.Errorf(\"failed to request stop via autostart manager: %w\", err)\n\t} else if !requested {\n\t\tlogrus.Infof(\"Sending SIGINT to hostagent process %d\", inst.HostAgentPID)\n\t\tif err := osutil.SysKill(inst.HostAgentPID, osutil.SigInt); err != nil {\n\t\t\tlogrus.Error(err)\n\t\t}\n\t}\n\n\tlogrus.Info(\"Waiting for the host agent and the driver processes to shut down\")\n\terr := waitForHostAgentTermination(ctx, inst, begin)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogrus.Info(\"Waiting for the instance to shut down\")\n\treturn waitForInstanceShutdown(ctx, inst)\n}\n\nfunc waitForHostAgentTermination(ctx context.Context, inst *limatype.Instance, begin time.Time) error {\n\tctx, cancel := context.WithTimeout(ctx, 3*time.Minute+10*time.Second)\n\tdefer cancel()\n\n\tvar receivedExitingEvent bool\n\tonEvent := func(ev hostagentevents.Event) bool {\n\t\tif len(ev.Status.Errors) > 0 {\n\t\t\tlogrus.Errorf(\"%+v\", ev.Status.Errors)\n\t\t}\n\t\tif ev.Status.Exiting {\n\t\t\treceivedExitingEvent = true\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\thaStdoutPath := filepath.Join(inst.Dir, filenames.HostAgentStdoutLog)\n\thaStderrPath := filepath.Join(inst.Dir, filenames.HostAgentStderrLog)\n\n\tif err := hostagentevents.Watch(ctx, haStdoutPath, haStderrPath, begin, true, onEvent); err != nil {\n\t\treturn err\n\t}\n\n\tif !receivedExitingEvent {\n\t\treturn errors.New(\"did not receive an event with the \\\"exiting\\\" status\")\n\t}\n\n\treturn nil\n}\n\nfunc waitForInstanceShutdown(ctx context.Context, inst *limatype.Instance) error {\n\tctx, cancel := context.WithTimeout(ctx, 3*time.Minute)\n\tdefer cancel()\n\n\tticker := time.NewTicker(500 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\tupdatedInst, err := store.Inspect(ctx, inst.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to inspect instance status: %w\", err)\n\t\t\t}\n\n\t\t\tif updatedInst.Status == limatype.StatusStopped {\n\t\t\t\tlogrus.Infof(\"The instance %s has shut down\", updatedInst.Name)\n\t\t\t\treturn nil\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\treturn errors.New(\"timed out waiting for instance to shut down after 3 minutes\")\n\t\t}\n\t}\n}\n\nfunc StopForcibly(inst *limatype.Instance) {\n\tif inst.DriverPID > 0 {\n\t\tlogrus.Infof(\"Sending SIGKILL to the %s driver process %d\", inst.VMType, inst.DriverPID)\n\t\tif err := osutil.SysKill(inst.DriverPID, osutil.SigKill); err != nil {\n\t\t\tlogrus.Error(err)\n\t\t}\n\t} else {\n\t\tlogrus.Infof(\"The %s driver process seems already stopped\", inst.VMType)\n\t}\n\n\tfor _, d := range inst.AdditionalDisks {\n\t\tdiskName := d.Name\n\t\tdisk, err := store.InspectDisk(diskName, d.FSType)\n\t\tif err != nil {\n\t\t\tlogrus.Warnf(\"Disk %q does not exist\", diskName)\n\t\t\tcontinue\n\t\t}\n\t\tif err := disk.Unlock(); err != nil {\n\t\t\tlogrus.Warnf(\"Failed to unlock disk %q. To use, run `limactl disk unlock %v`\", diskName, diskName)\n\t\t}\n\t}\n\n\tif inst.HostAgentPID > 0 {\n\t\tlogrus.Infof(\"Sending SIGKILL to the host agent process %d\", inst.HostAgentPID)\n\t\tif err := osutil.SysKill(inst.HostAgentPID, osutil.SigKill); err != nil {\n\t\t\tlogrus.Error(err)\n\t\t}\n\t} else {\n\t\tlogrus.Info(\"The host agent process seems already stopped\")\n\t}\n\n\tglobPatterns := strings.ReplaceAll(strings.Join(filenames.TmpFileSuffixes, \" \"), \".\", \"*.\")\n\tlogrus.Infof(\"Removing %s under %q\", globPatterns, inst.Dir)\n\n\tfi, err := os.ReadDir(inst.Dir)\n\tif err != nil {\n\t\tlogrus.Error(err)\n\t\treturn\n\t}\n\tfor _, f := range fi {\n\t\tpath := filepath.Join(inst.Dir, f.Name())\n\t\tfor _, suffix := range filenames.TmpFileSuffixes {\n\t\t\tif strings.HasSuffix(path, suffix) {\n\t\t\t\tlogrus.Infof(\"Removing %q\", path)\n\t\t\t\tif err := os.Remove(path); err != nil {\n\t\t\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\t\t\tlogrus.Debug(err.Error())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlogrus.Error(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/ioutilx/ioutilx.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ioutilx\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/text/encoding/unicode\"\n\t\"golang.org/x/text/transform\"\n)\n\n// ReadAtMaximum reads n at maximum.\nfunc ReadAtMaximum(r io.Reader, n int64) ([]byte, error) {\n\tlr := &io.LimitedReader{\n\t\tR: r,\n\t\tN: n,\n\t}\n\tb, err := io.ReadAll(lr)\n\tif err != nil {\n\t\tif errors.Is(err, io.EOF) && lr.N <= 0 {\n\t\t\terr = fmt.Errorf(\"exceeded the limit (%d bytes): %w\", n, err)\n\t\t}\n\t}\n\treturn b, err\n}\n\n// FromUTF16le returns an io.Reader for UTF16le data.\n// Windows uses little endian by default, use unicode.UseBOM policy to retrieve BOM from the text,\n// and unicode.LittleEndian as a fallback.\nfunc FromUTF16le(r io.Reader) io.Reader {\n\to := transform.NewReader(r, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder())\n\treturn o\n}\n\n// FromUTF16leToString reads from Unicode 16 LE encoded data from an io.Reader and returns a string.\nfunc FromUTF16leToString(r io.Reader) (string, error) {\n\tout, err := io.ReadAll(FromUTF16le(r))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(out), nil\n}\n\nfunc WindowsSubsystemPath(ctx context.Context, orig string) (string, error) {\n\tout, err := exec.CommandContext(ctx, \"cygpath\", filepath.ToSlash(orig)).CombinedOutput()\n\tif err != nil {\n\t\tlogrus.WithError(err).Errorf(\"failed to convert path to mingw, maybe not using Git ssh?\")\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(out)), nil\n}\n\nfunc WindowsSubsystemPathForLinux(ctx context.Context, orig, distro string) (string, error) {\n\tout, err := exec.CommandContext(ctx, \"wsl\", \"-d\", distro, \"--exec\", \"wslpath\", filepath.ToSlash(orig)).CombinedOutput()\n\tif err != nil {\n\t\tlogrus.WithError(err).Errorf(\"failed to convert path to mingw, maybe wsl command is not operational?\")\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(out)), nil\n}\n"
  },
  {
    "path": "pkg/iso9660util/fuzz_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage iso9660util\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc FuzzIsISO9660(f *testing.F) {\n\tf.Fuzz(func(t *testing.T, fileContents []byte) {\n\t\timageFile := filepath.Join(t.TempDir(), \"fuzz.iso\")\n\t\terr := os.WriteFile(imageFile, fileContents, 0o600)\n\t\tassert.NilError(t, err)\n\t\t_, _ = IsISO9660(imageFile)\n\t})\n}\n"
  },
  {
    "path": "pkg/iso9660util/iso9660util.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage iso9660util\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\n\t\"github.com/diskfs/go-diskfs/backend/file\"\n\t\"github.com/diskfs/go-diskfs/filesystem\"\n\t\"github.com/diskfs/go-diskfs/filesystem/iso9660\"\n\t\"github.com/sirupsen/logrus\"\n)\n\ntype Entry struct {\n\tPath   string\n\tReader io.Reader\n}\n\ntype WriteOptions struct {\n\tJoliet bool\n}\n\ntype WriteOpt func(*WriteOptions)\n\nfunc WithJoliet() WriteOpt {\n\treturn func(opts *WriteOptions) {\n\t\topts.Joliet = true\n\t}\n}\n\nfunc Write(isoPath, label string, layout []Entry, opts ...WriteOpt) error {\n\toptions := &WriteOptions{}\n\tfor _, opt := range opts {\n\t\topt(options)\n\t}\n\tif options.Joliet {\n\t\treturn writeJoliet(isoPath, label, layout)\n\t}\n\n\tif err := os.RemoveAll(isoPath); err != nil {\n\t\treturn err\n\t}\n\n\tisoFile, err := os.Create(isoPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbackendFile := file.New(isoFile, false)\n\tdefer isoFile.Close()\n\n\tworkdir, err := os.MkdirTemp(\"\", \"diskfs_iso\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.Debugf(\"Creating iso file %s\", isoFile.Name())\n\tlogrus.Debugf(\"Using %s as workspace\", workdir)\n\tfs, err := iso9660.Create(backendFile, 0, 0, 0, workdir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, f := range layout {\n\t\tif _, err := WriteFile(fs, f.Path, f.Reader); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tfinalizeOptions := iso9660.FinalizeOptions{\n\t\tRockRidge:        true,\n\t\tVolumeIdentifier: label,\n\t}\n\tif err := fs.Finalize(finalizeOptions); err != nil {\n\t\treturn err\n\t}\n\n\treturn isoFile.Close()\n}\n\nfunc WriteFile(fs filesystem.FileSystem, pathStr string, r io.Reader) (int64, error) {\n\tif dir := path.Dir(pathStr); dir != \"\" && dir != \"/\" {\n\t\tif err := fs.Mkdir(dir); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\tf, err := fs.OpenFile(pathStr, os.O_CREATE|os.O_RDWR)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer f.Close()\n\treturn io.Copy(f, r)\n}\n\nfunc IsISO9660(imagePath string) (bool, error) {\n\timageFile, err := os.Open(imagePath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer imageFile.Close()\n\tbackendFile := file.New(imageFile, true)\n\n\tfileInfo, err := imageFile.Stat()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t_, err = iso9660.Read(backendFile, fileInfo.Size(), 0, 0)\n\treturn err == nil, nil\n}\n"
  },
  {
    "path": "pkg/iso9660util/joliet.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage iso9660util\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc writeJoliet(isoPath, label string, layout []Entry) error {\n\tif err := os.RemoveAll(isoPath); err != nil {\n\t\treturn err\n\t}\n\ttmpDir, err := os.MkdirTemp(\"\", \"joliet\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.RemoveAll(tmpDir)\n\tfor _, entry := range layout {\n\t\tpath := filepath.Join(tmpDir, entry.Path)\n\t\tif err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf, err := os.Create(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif _, err := io.Copy(f, entry.Reader); err != nil {\n\t\t\tf.Close()\n\t\t\treturn err\n\t\t}\n\t\tf.Close()\n\t}\n\n\tctx := context.TODO()\n\tcmdSlice, err := writeJolietCommand(isoPath, label, tmpDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd := exec.CommandContext(ctx, cmdSlice[0], cmdSlice[1:]...)\n\tlogrus.Debugf(\"Executing %v\", cmd.Args)\n\tb, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w (output=%q)\", cmd.Args, err, string(b))\n\t}\n\treturn nil\n}\n\nfunc writeJolietCommand(isoPath, label, workDir string) ([]string, error) {\n\tif runtime.GOOS == \"darwin\" {\n\t\treturn []string{\"hdiutil\", \"makehybrid\", \"-o\", isoPath, \"-iso\", \"-joliet\", \"-default-volume-name\", label, workDir}, nil\n\t}\n\tcandidates := []string{\"xorrisofs\", \"genisoimage\", \"mkisofs\"}\n\tfor _, cmd := range candidates {\n\t\tif cmdAbs, err := exec.LookPath(cmd); err == nil {\n\t\t\treturn []string{cmdAbs, \"-o\", isoPath, \"--norock\", \"-J\", \"-V\", label, workDir}, nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"none of %v is available\", candidates)\n}\n"
  },
  {
    "path": "pkg/jsonschemautil/jsonschemautil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage jsonschemautil\n\nimport (\n\t\"os\"\n\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/santhosh-tekuri/jsonschema/v6\"\n)\n\nfunc Validate(schemafile, instancefile string) error {\n\tcompiler := jsonschema.NewCompiler()\n\tschema, err := compiler.Compile(schemafile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tinstance, err := os.ReadFile(instancefile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar y any\n\terr = yaml.Unmarshal(instance, &y)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn schema.Validate(y)\n}\n"
  },
  {
    "path": "pkg/jsonschemautil/jsonschemautil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage jsonschemautil\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestValidateValidInstance(t *testing.T) {\n\tschema := \"testdata/schema.json\"\n\tinstance := \"testdata/valid.yaml\"\n\terr := Validate(schema, instance)\n\tassert.NilError(t, err)\n}\n\nfunc TestValidateInvalidInstance(t *testing.T) {\n\tschema := \"testdata/schema.json\"\n\tinstance := \"testdata/invalid.yaml\"\n\terr := Validate(schema, instance)\n\tassert.ErrorContains(t, err, \"jsonschema validation failed\")\n}\n"
  },
  {
    "path": "pkg/jsonschemautil/testdata/invalid.yaml",
    "content": "name: John Doe\nage: \"30\"\n"
  },
  {
    "path": "pkg/jsonschemautil/testdata/schema.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\"\n    },\n    \"age\": {\n      \"type\": \"integer\"\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/jsonschemautil/testdata/valid.yaml",
    "content": "name: John Doe\nage: 30\n"
  },
  {
    "path": "pkg/limactlutil/limactlutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limactlutil\n\nimport (\n\t\"bytes\"\n\t\"cmp\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n// Path returns the path to the `limactl` executable.\nfunc Path() (string, error) {\n\tlimactl := cmp.Or(os.Getenv(\"LIMACTL\"), \"limactl\")\n\treturn exec.LookPath(limactl)\n}\n\n// Inspect runs `limactl list --json INST` and parses the output.\nfunc Inspect(ctx context.Context, limactl, instName string) (*limatype.Instance, error) {\n\tvar stdout, stderr bytes.Buffer\n\tcmd := exec.CommandContext(ctx, limactl, \"list\", \"--json\", instName)\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run %v: stdout=%q, stderr=%q: %w\", cmd.Args, stdout.String(), stderr.String(), err)\n\t}\n\tvar inst limatype.Instance\n\tif err := json.Unmarshal(stdout.Bytes(), &inst); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &inst, nil\n}\n"
  },
  {
    "path": "pkg/limainfo/limainfo.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limainfo\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/envutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/plugins\"\n\t\"github.com/lima-vm/lima/v2/pkg/registry\"\n\t\"github.com/lima-vm/lima/v2/pkg/templatestore\"\n\t\"github.com/lima-vm/lima/v2/pkg/usrlocal\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n)\n\ntype LimaInfo struct {\n\tVersion         string                       `json:\"version\"`\n\tTemplates       []templatestore.Template     `json:\"templates\"`\n\tDefaultTemplate *limatype.LimaYAML           `json:\"defaultTemplate\"`\n\tLimaHome        string                       `json:\"limaHome\"`\n\tVMTypes         []string                     `json:\"vmTypes\"`       // since Lima v0.14.2\n\tVMTypesEx       map[string]DriverExt         `json:\"vmTypesEx\"`     // since Lima v2.0.0\n\tGuestAgents     map[limatype.Arch]GuestAgent `json:\"guestAgents\"`   // since Lima v1.1.0\n\tShellEnvBlock   []string                     `json:\"shellEnvBlock\"` // since Lima v2.0.0\n\tHostOS          string                       `json:\"hostOS\"`        // since Lima v2.0.0\n\tHostArch        string                       `json:\"hostArch\"`      // since Lima v2.0.0\n\tIdentityFile    string                       `json:\"identityFile\"`  // since Lima v2.0.0\n\tPlugins         []plugins.Plugin             `json:\"plugins\"`       // since Lima v2.0.0\n\tLibexecPaths    []string                     `json:\"libexecPaths\"`  // since Lima v2.0.0\n\tSharePaths      []string                     `json:\"sharePaths\"`    // since Lima v2.0.0\n}\n\ntype DriverExt struct {\n\tLocation string `json:\"location,omitempty\"` // since Lima v2.0.0\n}\n\ntype GuestAgent struct {\n\tLocation string `json:\"location\"` // since Lima v1.1.0\n}\n\n// New returns a LimaInfo object with the Lima version, a list of all Templates and their location,\n// the DefaultTemplate corresponding to template:default with all defaults filled in, the\n// LimaHome location, a list of all supported VMTypes, and a map of GuestAgents for each architecture.\nfunc New(ctx context.Context) (*LimaInfo, error) {\n\tb, err := templatestore.Read(templatestore.Default)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ty, err := limayaml.Load(ctx, b, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treg := registry.List()\n\tif len(reg) == 0 {\n\t\treturn nil, errors.New(\"no VM types found; ensure that the drivers are properly registered\")\n\t}\n\tvmTypesEx := make(map[string]DriverExt)\n\tvar vmTypes []string\n\tfor name, path := range reg {\n\t\tvmTypesEx[name] = DriverExt{\n\t\t\tLocation: path,\n\t\t}\n\t\tvmTypes = append(vmTypes, name)\n\t}\n\n\tinfo := &LimaInfo{\n\t\tVersion:         version.Version,\n\t\tDefaultTemplate: y,\n\t\tVMTypes:         vmTypes,\n\t\tVMTypesEx:       vmTypesEx,\n\t\tGuestAgents:     make(map[limatype.Arch]GuestAgent),\n\t\tShellEnvBlock:   envutil.GetDefaultBlockList(),\n\t\tHostOS:          runtime.GOOS,\n\t\tHostArch:        limatype.NewArch(runtime.GOARCH),\n\t}\n\tinfo.Templates, err = templatestore.Templates()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinfo.LimaHome, err = dirnames.LimaDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tconfigDir, err := dirnames.LimaConfigDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinfo.LibexecPaths, err = usrlocal.LibexecLima()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinfo.SharePaths, err = usrlocal.ShareLima()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinfo.IdentityFile = filepath.Join(configDir, filenames.UserPrivateKey)\n\tfor _, arch := range limatype.ArchTypes {\n\t\tfor _, os := range limatype.OSTypes {\n\t\t\tbin, err := usrlocal.GuestAgentBinary(os, arch)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\tlogrus.WithError(err).Debugf(\"Failed to resolve the guest agent binary for %q-%q\", os, arch)\n\t\t\t\t} else {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Failed to resolve the guest agent binary for %q-%q\", os, arch)\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tkey := arch\n\t\t\t// For the historical reason, the key does not have \"Linux-\" prefix\n\t\t\tif os != limatype.LINUX {\n\t\t\t\tkey = os + \"-\" + arch\n\t\t\t}\n\t\t\tinfo.GuestAgents[key] = GuestAgent{\n\t\t\t\tLocation: bin,\n\t\t\t}\n\t\t}\n\t}\n\n\tinfo.Plugins, err = plugins.Discover()\n\tif err != nil {\n\t\t// Don't fail the entire info command if plugin discovery fails.\n\t\tlogrus.WithError(err).Warn(\"Failed to discover plugins\")\n\t}\n\n\treturn info, nil\n}\n"
  },
  {
    "path": "pkg/limatmpl/abs.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/localpathutil\"\n)\n\n// UseAbsLocators will replace all relative template locators with absolute ones, so this template\n// can be stored anywhere and still reference the same base templates and files.\nfunc (tmpl *Template) UseAbsLocators() error {\n\terr := tmpl.useAbsLocators()\n\treturn tmpl.ClearOnError(err)\n}\n\nfunc (tmpl *Template) useAbsLocators() error {\n\tif err := tmpl.Unmarshal(); err != nil {\n\t\treturn err\n\t}\n\tbasePath, err := basePath(tmpl.Locator)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor i, baseLocator := range tmpl.Config.Base {\n\t\tabsLocator, err := absPath(baseLocator.URL, basePath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif i == 0 {\n\t\t\t// base can be either a single string (URL), or a single locator object, or a list whose first element can be either a string or an object\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.base | select(type == \\\"!!str\\\")) |= %q\\n\", absLocator)\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.base | select(type == \\\"!!map\\\") | .url) |= %q\\n\", absLocator)\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.base | select(type == \\\"!!seq\\\" and (.[0] | type) == \\\"!!str\\\") | .[0]) |= %q\\n\", absLocator)\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.base | select(type == \\\"!!seq\\\" and (.[0] | type) == \\\"!!map\\\") | .[0].url) |= %q\\n\", absLocator)\n\t\t} else {\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.base[%d] | select(type == \\\"!!str\\\")) |= %q\\n\", i, absLocator)\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.base[%d] | select(type == \\\"!!map\\\") | .url) |= %q\\n\", i, absLocator)\n\t\t}\n\t}\n\tfor i, p := range tmpl.Config.Probes {\n\t\tif p.File != nil {\n\t\t\tabsLocator, err := absPath(p.File.URL, basePath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.probes[%d].file | select(type == \\\"!!str\\\")) = %q\\n\", i, absLocator)\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.probes[%d].file | select(type == \\\"!!map\\\") | .url) = %q\\n\", i, absLocator)\n\t\t}\n\t}\n\tfor i, p := range tmpl.Config.Provision {\n\t\tif p.File != nil {\n\t\t\tabsLocator, err := absPath(p.File.URL, basePath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.provision[%d].file | select(type == \\\"!!str\\\")) = %q\\n\", i, absLocator)\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| ($a.provision[%d].file | select(type == \\\"!!map\\\") | .url) = %q\\n\", i, absLocator)\n\t\t}\n\t}\n\treturn tmpl.evalExpr()\n}\n\n// withVolume adds the volume name of the current working directory to a path without volume name.\n// On Windows filepath.Abs() only returns a \"rooted\" name, but does not add the volume name.\n// withVolume also normalizes all path separators to the platform native one.\nfunc withVolume(path string) (string, error) {\n\tif runtime.GOOS == \"windows\" && filepath.VolumeName(path) == \"\" {\n\t\troot, err := filepath.Abs(\"/\")\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tpath = filepath.VolumeName(root) + path\n\t}\n\treturn filepath.Clean(path), nil\n}\n\n// basePath returns the locator in absolute format, but without the filename part.\nfunc basePath(locator string) (string, error) {\n\tu, err := url.Parse(locator)\n\t// Single-letter schemes will be drive names on Windows, e.g. \"c:/foo\"\n\tif err == nil && len(u.Scheme) > 1 {\n\t\t// path.Dir(\"\") returns \".\", which must be removed for url.JoinPath() to do the right thing later\n\t\tif u.Opaque != \"\" {\n\t\t\treturn u.Scheme + \":\" + strings.TrimSuffix(path.Dir(u.Opaque), \".\"), nil\n\t\t}\n\t\treturn u.Scheme + \"://\" + strings.TrimSuffix(path.Dir(path.Join(u.Host, u.Path)), \".\"), nil\n\t}\n\tbase, err := filepath.Abs(filepath.Dir(locator))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn withVolume(base)\n}\n\n// absPath either returns the locator directly, or combines it with the basePath if the locator is a relative path.\nfunc absPath(locator, basePath string) (string, error) {\n\tif locator == \"\" {\n\t\treturn \"\", errors.New(\"locator is empty\")\n\t}\n\tu, err := url.Parse(locator)\n\tif err == nil && len(u.Scheme) > 1 {\n\t\treturn locator, nil\n\t}\n\t// Don't expand relative path to absolute. Tilde paths however are absolute paths already.\n\tif localpathutil.IsTildePath(locator) {\n\t\tlocator, err = localpathutil.Expand(locator)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\t// Check for rooted locator; filepath.IsAbs() returns false on Windows when the volume name is missing\n\tvolumeLen := len(filepath.VolumeName(locator))\n\tif locator[volumeLen] != '/' && locator[volumeLen] != filepath.Separator {\n\t\tswitch {\n\t\tcase basePath == \"\":\n\t\t\treturn \"\", errors.New(\"basePath is empty\")\n\t\tcase basePath == \"-\":\n\t\t\treturn \"\", errors.New(\"can't use relative paths when reading template from STDIN\")\n\t\tcase strings.Contains(locator, \"../\"):\n\t\t\treturn \"\", fmt.Errorf(\"relative locator path %q must not contain '../' segments\", locator)\n\t\tcase volumeLen != 0:\n\t\t\treturn \"\", fmt.Errorf(\"relative locator path %q must not include a volume name\", locator)\n\t\t}\n\t\tu, err = url.Parse(basePath)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif len(u.Scheme) > 1 {\n\t\t\t// Treat empty \"template:\" URL as opaque\n\t\t\tif u.Opaque != \"\" || (u.Scheme == \"template\" && u.Host == \"\") {\n\t\t\t\treturn u.Scheme + \":\" + path.Join(u.Opaque, locator), nil\n\t\t\t}\n\t\t\treturn u.JoinPath(locator).String(), nil\n\t\t}\n\t\tlocator = filepath.Join(basePath, locator)\n\t}\n\treturn withVolume(locator)\n}\n"
  },
  {
    "path": "pkg/limatmpl/abs_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\ntype useAbsLocatorsTestCase struct {\n\tdescription string\n\tlocator     string\n\ttemplate    string\n\texpected    string\n}\n\nvar useAbsLocatorsTestCases = []useAbsLocatorsTestCase{\n\t{\n\t\t\"Template without base or script file\",\n\t\t\"template:foo\",\n\t\t`arch: aarch64`,\n\t\t`arch: aarch64`,\n\t},\n\t{\n\t\t\"Single string base template\",\n\t\t\"template:foo\",\n\t\t`base: bar.yaml`,\n\t\t`base: template:bar.yaml`,\n\t},\n\t{\n\t\t\"Legacy template:// base template\",\n\t\t\"template://foo\",\n\t\t`base: bar.yaml`,\n\t\t`base: template:bar.yaml`,\n\t},\n\t{\n\t\t\"Flow style array of one base template\",\n\t\t\"template:foo\",\n\t\t`base: [{url: bar.yaml, digest: deadbeef}]`,\n\t\t// not sure why the quotes around the URL were added; maybe because we don't copy the style from the source\n\t\t`base: [{url: 'template:bar.yaml', digest: deadbeef}]`,\n\t},\n\t{\n\t\t\"Flow style array of sequence of two base URLs\",\n\t\t\"template:foo\",\n\t\t`base: [bar.yaml, baz.yaml]`,\n\t\t`base: ['template:bar.yaml', 'template:baz.yaml']`,\n\t},\n\t{\n\t\t\"Flow style array of sequence of two base locator objects\",\n\t\t\"template:foo\",\n\t\t`base: [{url: bar.yaml, digest: deadbeef}, {url: baz.yaml, digest: decafbad}]`,\n\t\t`base: [{url: 'template:bar.yaml', digest: deadbeef}, {url: 'template:baz.yaml', digest: decafbad}]`,\n\t},\n\t{\n\t\t\"Block style array of one base template\",\n\t\t\"template:foo\",\n\t\t`\nbase:\n- bar.yaml\n`,\n\t\t`\nbase:\n- template:bar.yaml`,\n\t},\n\t{\n\t\t\"Block style of four base templates\",\n\t\t\"template:foo\",\n\t\t`\nbase:\n- bar.yaml\n- template:my\n- https://example.com/my.yaml\n- baz.yaml\n`,\n\t\t`\nbase:\n- template:bar.yaml\n- template:my\n- https://example.com/my.yaml\n- template:baz.yaml\n`,\n\t},\n\t{\n\t\t\"Provisioning and probe scripts\",\n\t\t\"template:experimental/foo\",\n\t\t`\nprovision:\n- mode: user\n  file: userscript.sh\n- mode: system\n  file:\n    url: systemscript.sh\n    digest: abc123\nprobes:\n- file: probe.sh\n- file:\n    url: probe.sh\n    digest: digest\n`,\n\t\t`\nprovision:\n- mode: user\n  file: template:experimental/userscript.sh\n- mode: system\n  file:\n    url: template:experimental/systemscript.sh\n    digest: abc123\nprobes:\n- file: template:experimental/probe.sh\n- file:\n    url: template:experimental/probe.sh\n    digest: digest\n`,\n\t},\n}\n\nfunc TestUseAbsLocators(t *testing.T) {\n\tfor _, tc := range useAbsLocatorsTestCases {\n\t\tt.Run(tc.description, func(t *testing.T) { RunUseAbsLocatorTest(t, tc) })\n\t}\n}\n\nfunc RunUseAbsLocatorTest(t *testing.T, tc useAbsLocatorsTestCase) {\n\ttmpl := &Template{\n\t\tBytes:   []byte(strings.TrimSpace(tc.template)),\n\t\tLocator: tc.locator,\n\t}\n\terr := tmpl.UseAbsLocators()\n\tassert.NilError(t, err, tc.description)\n\n\tactual := strings.TrimSpace(string(tmpl.Bytes))\n\texpected := strings.TrimSpace(tc.expected)\n\tassert.Equal(t, actual, expected, tc.description)\n}\n\nfunc TestBasePath(t *testing.T) {\n\t// On Windows the root will be something like \"C:\\\"\n\troot, err := filepath.Abs(\"/\")\n\tassert.NilError(t, err)\n\tvolume := filepath.VolumeName(root)\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"/foo\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, root)\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"/foo/bar\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, filepath.Clean(volume+\"/foo\"))\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"template:foo\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"template:\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"template:foo/bar\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"template:foo\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"http://host/foo\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"http://host\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"http://host/foo/bar\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"http://host/foo\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"file:///foo\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"file:///\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := basePath(\"file:///foo/bar\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"file:///foo\")\n\t})\n}\n\nfunc TestAbsPath(t *testing.T) {\n\troot, err := filepath.Abs(\"/\")\n\tassert.NilError(t, err)\n\tvolume := filepath.VolumeName(root)\n\n\tt.Run(\"If the locator is already an absolute path, it is returned unchanged\", func(t *testing.T) {\n\t\tactual, err := absPath(volume+\"/foo\", volume+\"/root\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, filepath.Clean(volume+\"/foo\"))\n\t})\n\n\tt.Run(\"If the locator is a rooted path without volume name, then the volume will be added\", func(t *testing.T) {\n\t\tactual, err := absPath(\"/foo\", volume+\"/root\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, filepath.Clean(volume+\"/foo\"))\n\t})\n\n\tt.Run(\"If the locator starts with ~/, then it will be expanded to an absolute path\", func(t *testing.T) {\n\t\tactual, err := absPath(\"~/foo\", volume+\"/root\")\n\t\tassert.NilError(t, err)\n\t\thomeDir, err := os.UserHomeDir()\n\t\tassert.NilError(t, err)\n\t\t// homeDir already includes the volume\n\t\tassert.Equal(t, actual, filepath.Join(homeDir, \"foo\"))\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"template:foo\", volume+\"/root\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"template:foo\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"http://host/foo\", volume+\"/root\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"http://host/foo\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"file:///foo\", volume+\"/root\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"file:///foo\")\n\t})\n\n\tt.Run(\"Can't have relative path when reading from STDIN\", func(t *testing.T) {\n\t\t_, err = absPath(\"foo\", \"-\")\n\t\tassert.ErrorContains(t, err, \"STDIN\")\n\t})\n\n\tt.Run(\"Relative paths must be underneath the basePath\", func(t *testing.T) {\n\t\t_, err = absPath(\"../foo\", volume+\"/root\")\n\t\tassert.ErrorContains(t, err, \"'../'\")\n\t})\n\n\tt.Run(\"locator must not be empty\", func(t *testing.T) {\n\t\t_, err = absPath(\"\", \"foo\")\n\t\tassert.ErrorContains(t, err, \"locator is empty\")\n\t})\n\n\tt.Run(\"basePath must not be empty\", func(t *testing.T) {\n\t\t_, err = absPath(\"foo\", \"\")\n\t\tassert.ErrorContains(t, err, \"basePath is empty\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\t_, err = absPath(\"./foo\", \"\")\n\t\tassert.ErrorContains(t, err, \"empty\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"./foo\", volume+\"/root\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, filepath.Clean(volume+\"/root/foo\"))\n\t})\n\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Run(\"Relative locators must not include volume names\", func(t *testing.T) {\n\t\t\t_, err := absPath(volume+\"foo\", volume+\"/root\")\n\t\t\tassert.ErrorContains(t, err, \"volume\")\n\t\t})\n\t}\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"foo\", \"template:\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"template:foo\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"bar\", \"template:foo\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"template:foo/bar\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"foo\", \"http://host\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"http://host/foo\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"bar\", \"http://host/foo\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"http://host/foo/bar\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"foo\", \"file:///\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"file:///foo\")\n\t})\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tactual, err := absPath(\"bar\", \"file:///foo\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, actual, \"file:///foo/bar\")\n\t})\n}\n"
  },
  {
    "path": "pkg/limatmpl/embed.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"unicode\"\n\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/version/versionutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\n// Embed will recursively resolve all \"base\" dependencies and update the\n// template with the merged result. It also inlines all external provisioning\n// and probe scripts.\nfunc (tmpl *Template) Embed(ctx context.Context, embedAll, defaultBase bool) error {\n\tif err := tmpl.UseAbsLocators(); err != nil {\n\t\treturn err\n\t}\n\tseen := make(map[string]bool)\n\terr := tmpl.embedAllBases(ctx, embedAll, defaultBase, seen)\n\t// additionalDisks, mounts, and networks may combine entries based on a shared key\n\t// This must be done after **all** base templates have been merged, so that wildcard keys can match\n\t// against all earlier list entries, and not just against the direct parent template.\n\tif err == nil {\n\t\terr = tmpl.combineListEntries()\n\t}\n\treturn tmpl.ClearOnError(err)\n}\n\nfunc (tmpl *Template) embedAllBases(ctx context.Context, embedAll, defaultBase bool, seen map[string]bool) error {\n\tlogrus.Debugf(\"Embedding templates into %q\", tmpl.Locator)\n\tif defaultBase {\n\t\tconfigDir, err := dirnames.LimaConfigDir()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefaultBaseFilename := filepath.Join(configDir, filenames.Base)\n\t\tif _, err := os.Stat(defaultBaseFilename); err == nil {\n\t\t\t// turn string into single element list\n\t\t\t// empty concatenation works around bug https://github.com/mikefarah/yq/issues/2269\n\t\t\ttmpl.expr.WriteString(\"| ($a.base | select(type == \\\"!!str\\\")) |= [\\\"\\\" + .]\\n\")\n\t\t\ttmpl.expr.WriteString(\"| ($a.base | select(type == \\\"!!map\\\")) |= [[] + .]\\n\")\n\t\t\t// prepend base template at the beginning of the list\n\t\t\tfmt.Fprintf(&tmpl.expr, \"| $a.base = [%q, $a.base[]]\\n\", defaultBaseFilename)\n\t\t\tif err := tmpl.evalExpr(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\tfor {\n\t\tif err := tmpl.Unmarshal(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(tmpl.Config.Base) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tbaseLocator := tmpl.Config.Base[0]\n\t\tif baseLocator.Digest != nil {\n\t\t\treturn fmt.Errorf(\"base %q in %q has specified a digest; digest support is not yet implemented\", baseLocator.URL, tmpl.Locator)\n\t\t}\n\t\tisTemplate, _ := SeemsTemplateURL(baseLocator.URL)\n\t\tif isTemplate && !embedAll {\n\t\t\t// Once we skip a template: URL we can no longer embed any other base template\n\t\t\tfor i := 1; i < len(tmpl.Config.Base); i++ {\n\t\t\t\tisTemplate, _ = SeemsTemplateURL(tmpl.Config.Base[i].URL)\n\t\t\t\tif !isTemplate {\n\t\t\t\t\treturn fmt.Errorf(\"cannot embed template %q after not embedding %q\", tmpl.Config.Base[i].URL, baseLocator.URL)\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak\n\t\t\t// TODO should we track embedding of template: URLs so we can warn if we embed a non-template: URL afterwards?\n\t\t}\n\n\t\tif seen[baseLocator.URL] {\n\t\t\treturn fmt.Errorf(\"base template loop detected: template %q already included\", baseLocator.URL)\n\t\t}\n\t\tseen[baseLocator.URL] = true\n\n\t\t// remove base[0] from template before merging\n\t\tif err := tmpl.embedBase(ctx, baseLocator, embedAll, seen); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := tmpl.embedAllScripts(ctx, embedAll); err != nil {\n\t\treturn err\n\t}\n\tif len(tmpl.Bytes) > yBytesLimit {\n\t\treturn fmt.Errorf(\"template %q embedding exceeded the size limit (%d bytes)\", tmpl.Locator, yBytesLimit)\n\t}\n\treturn nil\n}\n\nfunc (tmpl *Template) embedBase(ctx context.Context, baseLocator limatype.LocatorWithDigest, embedAll bool, seen map[string]bool) error {\n\tlogrus.Debugf(\"Embedding base %q in template %q\", baseLocator.URL, tmpl.Locator)\n\tif err := tmpl.Unmarshal(); err != nil {\n\t\treturn err\n\t}\n\tbase, err := Read(ctx, \"\", baseLocator.URL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := base.UseAbsLocators(); err != nil {\n\t\treturn err\n\t}\n\tif err := base.embedAllBases(ctx, embedAll, false, seen); err != nil {\n\t\treturn err\n\t}\n\tif err := tmpl.merge(base); err != nil {\n\t\treturn err\n\t}\n\tif len(tmpl.Bytes) > yBytesLimit {\n\t\treturn fmt.Errorf(\"template %q embedding exceeded the size limit (%d bytes)\", tmpl.Locator, yBytesLimit)\n\t}\n\treturn nil\n}\n\n// evalExprImpl evaluates tmpl.expr against one or more documents.\n// Called by evalExpr() and embedAllScripts() for single documents and merge() for 2 documents.\nfunc (tmpl *Template) evalExprImpl(prefix string, b []byte) error {\n\tvar err error\n\texpr := prefix + tmpl.expr.String() + \"| $a\"\n\ttmpl.Bytes, err = yqutil.EvaluateExpression(expr, b)\n\t// Make sure the YAML ends with just a single newline\n\ttmpl.Bytes = append(bytes.TrimRight(tmpl.Bytes, \"\\n\"), '\\n')\n\ttmpl.Config = nil\n\ttmpl.expr.Reset()\n\treturn tmpl.ClearOnError(err)\n}\n\n// evalExpr evaluates tmpl.expr against the tmpl.Bytes document.\nfunc (tmpl *Template) evalExpr() error {\n\tvar err error\n\tif tmpl.expr.Len() > 0 {\n\t\t// There is just a single document; $a and $b are the same\n\t\tsingleDocument := \"select(document_index == 0) as $a | $a as $b\\n\"\n\t\terr = tmpl.evalExprImpl(singleDocument, tmpl.Bytes)\n\t}\n\treturn err\n}\n\n// merge merges the base template into tmpl.\nfunc (tmpl *Template) merge(base *Template) error {\n\tif err := tmpl.mergeBase(base); err != nil {\n\t\treturn tmpl.ClearOnError(err)\n\t}\n\tdocuments := fmt.Sprintf(\"%s\\n---\\n%s\", string(tmpl.Bytes), string(base.Bytes))\n\treturn tmpl.evalExprImpl(mergeDocuments, []byte(documents))\n}\n\n// mergeBase generates a yq script to merge the template with a base.\n// Most of the merging is done generically by the mergeDocuments script below.\n// Only thing left is to compare minimum version numbers and keep the highest version.\nfunc (tmpl *Template) mergeBase(base *Template) error {\n\tif err := tmpl.Unmarshal(); err != nil {\n\t\treturn err\n\t}\n\tif err := base.Unmarshal(); err != nil {\n\t\treturn err\n\t}\n\tif tmpl.Config.MinimumLimaVersion != nil && base.Config.MinimumLimaVersion != nil {\n\t\tif versionutil.GreaterThan(*base.Config.MinimumLimaVersion, *tmpl.Config.MinimumLimaVersion) {\n\t\t\tconst minimumLimaVersion = \"minimumLimaVersion\"\n\t\t\ttmpl.copyField(minimumLimaVersion, minimumLimaVersion)\n\t\t}\n\t}\n\tvar tmplOpts limatype.QEMUOpts\n\tif err := limayaml.Convert(tmpl.Config.VMOpts[limatype.QEMU], &tmplOpts, \"vmOpts.qemu\"); err != nil {\n\t\treturn err\n\t}\n\tvar baseOpts limatype.QEMUOpts\n\tif err := limayaml.Convert(base.Config.VMOpts[limatype.QEMU], &baseOpts, \"vmOpts.qemu\"); err != nil {\n\t\treturn err\n\t}\n\tif tmplOpts.MinimumVersion != nil && baseOpts.MinimumVersion != nil {\n\t\ttmplVersion := *semver.New(*tmplOpts.MinimumVersion)\n\t\tbaseVersion := *semver.New(*baseOpts.MinimumVersion)\n\t\tif tmplVersion.LessThan(baseVersion) {\n\t\t\tconst minimumQEMUVersion = \"vmOpts.qemu.minimumVersion\"\n\t\t\ttmpl.copyField(minimumQEMUVersion, minimumQEMUVersion)\n\t\t}\n\t}\n\treturn nil\n}\n\n// mergeDocuments copies over settings from the base that don't yet exist\n// in the template, and to append lists from the base to template lists.\n// Both head and line comments are copied over as well.\n//\n// It also handles these special cases:\n// * dns lists are not merged and only copied when the template doesn't have any dns entries at all.\n// * probes and provision scripts are appended in reverse order.\n// * mountTypesUnsupported have duplicate values removed.\n// * base is removed from the template.\nconst mergeDocuments = `\n  select(document_index == 0) as $a\n| select(document_index == 1) as $b\n\n# $c will be mutilated to implement our own \"merge only new fields\" logic.\n| $b as $c\n\n# Delete the base that is being merged right now\n| $a | select(.base | tag == \"!!seq\") | del(.base[0])\n| $a | select(.base | (tag == \"!!seq\" and length == 0)) | del(.base)\n| $a | select(.base | tag == \"!!str\") | del(.base)\n\n# If $a.base is a list, then $b.base must be a list as well\n# (note $b, not $c, because we merge lists from $b)\n| $b | select((.base | tag == \"!!str\") and ($a.base | tag == \"!!seq\")) | .base = [ \"\" + .base ]\n\n# Delete base DNS entries if the template list is not empty.\n| $a | select(.dns) | del($b.dns, $c.dns)\n\n# Mark all new list fields with a custom tag. This is needed to avoid appending\n# newly copied lists to themselves again when we merge lists.\n| $c | .. | select(tag == \"!!seq\") tag = \"!!tag\"\n\n# Delete all nodes in $c that are in $a and not a map. This is necessary because\n# the yq \"*n\" operator (merge only new fields) does not copy all comments across.\n| $c | delpaths([$a | .. | select(tag != \"!!map\") | path])\n\n# Merging with null returns null; use an empty map if $c has only comments\n| $a * ($c // {}) as $a\n\n# Find all elements that are existing lists. This will not match newly\n# copied lists because they have a custom !!tag instead of !!seq.\n# Append the elements from the same path in $b.\n# Exception: base templates, provision scripts and probes are prepended instead.\n| $a | (.. | select(tag == \"!!seq\" and (path[0] | test(\"^(base|provision|probes)$\") | not))) |=\n   (. + (path[] as $p ireduce ($b; .[$p])))\n| $a | (.. | select(tag == \"!!seq\" and (path[0] | test(\"^(base|provision|probes)$\")))) |=\n   ((path[] as $p ireduce ($b; .[$p])) + .)\n\n# Copy head and line comments for existing lists that do not already have comments.\n# New lists and existing maps already have their comments updated by the $a * $c merge.\n| $a | (.. | select(tag == \"!!seq\" and (key | head_comment == \"\")) | key) head_comment |=\n   (((path[] as $p ireduce ($b; .[$p])) | key | head_comment) // \"\")\n| $a | (.. | select(tag == \"!!seq\" and (key | line_comment == \"\")) | key) line_comment |=\n   (((path[] as $p ireduce ($b; .[$p])) | key | line_comment) // \"\")\n\n# Make sure mountTypesUnsupported elements are unique.\n| $a | (select(.mountTypesUnsupported) | .mountTypesUnsupported) |= unique\n\n# Remove the custom tags again so they do not clutter up the YAML output.\n| $a | .. | select(tag == \"!!tag\") tag = \"\"\n`\n\n// listFields returns dst and src fields like \"list[idx].field\".\nfunc listFields(list string, dstIdx, srcIdx int, field string) (dst, src string) {\n\tdst = fmt.Sprintf(\"%s[%d]\", list, dstIdx)\n\tsrc = fmt.Sprintf(\"%s[%d]\", list, srcIdx)\n\tif field != \"\" {\n\t\tdst += \".\" + field\n\t\tsrc += \".\" + field\n\t}\n\treturn dst, src\n}\n\n// copyField copies value and comments from $b.src to $a.dst.\nfunc (tmpl *Template) copyField(dst, src string) {\n\tfmt.Fprintf(&tmpl.expr, \"| ($a.%s) = $b.%s\\n\", dst, src)\n\t// The head_comment is on the key and not the value, so needs to be copied explicitly.\n\t// Surprisingly the line_comment seems to be copied with the value already even though it is also on the key.\n\tfmt.Fprintf(&tmpl.expr, \"| ($a.%s | key) head_comment = ($b.%s | key | head_comment)\\n\", dst, src)\n}\n\n// copyListEntryField copies $b.list[srcIdx].field to $a.list[dstIdx].field (including comments).\n// Note: field must not be \"\" and must not be a list field itself either.\nfunc (tmpl *Template) copyListEntryField(list string, dstIdx, srcIdx int, field string) {\n\ttmpl.copyField(listFields(list, dstIdx, srcIdx, field))\n}\n\ntype commentType string\n\nconst (\n\theadComment commentType = \"head\"\n\tlineComment commentType = \"line\"\n)\n\n// copyComment copies a non-empty head or line comment from $b.src to $a.dst, but only if $a.dst already exists.\nfunc (tmpl *Template) copyComment(dst, src string, commentType commentType, isMapElement bool) {\n\tonKey := \"\"\n\tif isMapElement {\n\t\tonKey = \" | key\" // For map elements the comments are on the keys and not the values.\n\t}\n\t// The expression is careful not to create a null $a.dst entry if $b.src has no comments and $a.dst didn't already exist.\n\t// e.g.: `| $a | (select(.foo) | .foo | key | select(head_comment == \"\" and ($b.bar | key | head_comment != \"\"))) head_comment |= ($b.bar | key | head_comment)`\n\tfmt.Fprintf(&tmpl.expr, \"| $a | (select(.%s) | .%s%s | select(%s_comment == \\\"\\\" and ($b.%s%s | %s_comment != \\\"\\\"))) %s_comment |= ($b.%s%s | %s_comment)\\n\",\n\t\tdst, dst, onKey, commentType, src, onKey, commentType, commentType, src, onKey, commentType)\n}\n\n// copyComments copies all non-empty comments from $b.src to $a.dst.\nfunc (tmpl *Template) copyComments(dst, src string, isMapElement bool) {\n\tfor _, commentType := range []commentType{headComment, lineComment} {\n\t\ttmpl.copyComment(dst, src, commentType, isMapElement)\n\t}\n}\n\n// copyListEntryComments copies all non-empty comments from $b.list[srcIdx].field to $a.list[dstIdx].field.\nfunc (tmpl *Template) copyListEntryComments(list string, dstIdx, srcIdx int, field string) {\n\tdst, src := listFields(list, dstIdx, srcIdx, field)\n\tisMapElement := field != \"\"\n\ttmpl.copyComments(dst, src, isMapElement)\n}\n\nfunc (tmpl *Template) deleteListEntry(list string, idx int) {\n\tfmt.Fprintf(&tmpl.expr, \"| del($a.%s[%d], $b.%s[%d])\\n\", list, idx, list, idx)\n}\n\n// upgradeListEntryStringToMapField turns list[idx] from a string to a {field: list[idx]} map.\nfunc (tmpl *Template) upgradeListEntryStringToMapField(list string, idx int, field string) {\n\t// TODO the head_comment on the string becomes duplicated as a foot_comment on the new field; could be a yq bug?\n\tfmt.Fprintf(&tmpl.expr, \"| ($a.%s[%d] | select(type == \\\"!!str\\\")) |= {\\\"%s\\\": .}\\n\", list, idx, field)\n}\n\n// combineListEntries combines entries based on a shared unique key.\n// If two entries share the same key, then any missing fields in the earlier entry are\n// filled in from the latter one. The latter one is then deleted.\n//\n// Notes:\n// * The field order is not maintained when entries with a matching key are merged.\n// * The unique keys (and mount locations) are assumed to not be subject to Go templating.\n// * A wildcard key '*' matches all prior list entries.\nfunc (tmpl *Template) combineListEntries() error {\n\tif err := tmpl.Unmarshal(); err != nil {\n\t\treturn err\n\t}\n\n\ttmpl.combineAdditionalDisks()\n\ttmpl.combineMounts()\n\ttmpl.combineNetworks()\n\n\treturn tmpl.evalExpr()\n}\n\n// TODO: Maybe instead of hard-coding all the yaml names of LimaYAML struct fields we should\n// TODO: retrieve them using reflection from the Go type tags to avoid possible typos.\n\n// combineAdditionalDisks combines additionalDisks entries. The shared key is the disk name.\nfunc (tmpl *Template) combineAdditionalDisks() {\n\tconst additionalDisks = \"additionalDisks\"\n\n\tdiskIdx := make(map[string]int, len(tmpl.Config.AdditionalDisks))\n\tfor src := 0; src < len(tmpl.Config.AdditionalDisks); {\n\t\tdisk := tmpl.Config.AdditionalDisks[src]\n\t\tvar from, to int\n\t\tif disk.Name == \"*\" {\n\t\t\t// copy to **all** previous entries\n\t\t\tfrom = 0\n\t\t\tto = src - 1\n\t\t} else {\n\t\t\tif i, ok := diskIdx[disk.Name]; ok {\n\t\t\t\t// copy to previous disk with the same diskIdx\n\t\t\t\tfrom = i\n\t\t\t\tto = i\n\t\t\t} else {\n\t\t\t\t// record disk index and continue with the next entry\n\t\t\t\tif disk.Name != \"\" {\n\t\t\t\t\tdiskIdx[disk.Name] = src\n\t\t\t\t}\n\t\t\t\tsrc++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tfor dst := from; dst <= to; dst++ {\n\t\t\t// upgrade additionalDisks[dst] from \"disk\" name string to {\"name\": \"disk\"} map so we can add fields\n\t\t\tupgradeDiskToMap := sync.OnceFunc(func() {\n\t\t\t\ttmpl.upgradeListEntryStringToMapField(additionalDisks, dst, \"name\")\n\t\t\t})\n\n\t\t\tdest := &tmpl.Config.AdditionalDisks[dst]\n\t\t\tif dest.Format == nil && disk.Format != nil {\n\t\t\t\tupgradeDiskToMap()\n\t\t\t\ttmpl.copyListEntryField(additionalDisks, dst, src, \"format\")\n\t\t\t\tdest.Format = disk.Format\n\t\t\t}\n\t\t\t// TODO: Does it make sense to merge \"fsType\" and \"fsArgs\" independently of each other?\n\t\t\tif dest.FSType == nil && disk.FSType != nil {\n\t\t\t\tupgradeDiskToMap()\n\t\t\t\ttmpl.copyListEntryField(additionalDisks, dst, src, \"fsType\")\n\t\t\t\tdest.FSType = disk.FSType\n\t\t\t}\n\t\t\t// \"fsArgs\" are inherited all-or-nothing; they are not appended\n\t\t\tif len(dest.FSArgs) == 0 && len(disk.FSArgs) != 0 {\n\t\t\t\tupgradeDiskToMap()\n\t\t\t\ttmpl.copyListEntryField(additionalDisks, dst, src, \"fsArgs\")\n\t\t\t\tdest.FSArgs = disk.FSArgs\n\t\t\t}\n\t\t\t// TODO: Is there a good reason not to copy comments from wildcard entries?\n\t\t\tif disk.Name != \"*\" {\n\t\t\t\ttmpl.copyListEntryComments(additionalDisks, dst, src, \"\")\n\t\t\t}\n\t\t}\n\t\ttmpl.Config.AdditionalDisks = slices.Delete(tmpl.Config.AdditionalDisks, src, src+1)\n\t\ttmpl.deleteListEntry(additionalDisks, src)\n\t}\n}\n\n// combineMounts combines mounts entries. The shared key is the mount point.\nfunc (tmpl *Template) combineMounts() {\n\tconst mounts = \"mounts\"\n\n\tmountPointIdx := make(map[string]int, len(tmpl.Config.Mounts))\n\tfor src := 0; src < len(tmpl.Config.Mounts); {\n\t\tmount := tmpl.Config.Mounts[src]\n\t\t// mountPoint (an optional field) defaults to location (a required field)\n\t\tmountPoint := mount.Location\n\t\tif mount.MountPoint != nil {\n\t\t\tmountPoint = *mount.MountPoint\n\t\t}\n\t\tvar from, to int\n\t\tif mountPoint == \"*\" {\n\t\t\tfrom = 0\n\t\t\tto = src - 1\n\t\t} else {\n\t\t\tif i, ok := mountPointIdx[mountPoint]; ok {\n\t\t\t\tfrom = i\n\t\t\t\tto = i\n\t\t\t} else {\n\t\t\t\tif mountPoint != \"\" {\n\t\t\t\t\tmountPointIdx[mountPoint] = src\n\t\t\t\t}\n\t\t\t\tsrc++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tfor dst := from; dst <= to; dst++ {\n\t\t\tdest := &tmpl.Config.Mounts[dst]\n\t\t\t// MountPoint\n\t\t\tif dest.MountPoint == nil && mount.MountPoint != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"mountPoint\")\n\t\t\t\tdest.MountPoint = mount.MountPoint\n\t\t\t}\n\t\t\t// Writable\n\t\t\tif dest.Writable == nil && mount.Writable != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"writable\")\n\t\t\t\tdest.Writable = mount.Writable\n\t\t\t}\n\t\t\t// SSHFS\n\t\t\tif dest.SSHFS.Cache == nil && mount.SSHFS.Cache != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"sshfs.cache\")\n\t\t\t\tdest.SSHFS.Cache = mount.SSHFS.Cache\n\t\t\t}\n\t\t\tif dest.SSHFS.FollowSymlinks == nil && mount.SSHFS.FollowSymlinks != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"sshfs.followSymlinks\")\n\t\t\t\tdest.SSHFS.FollowSymlinks = mount.SSHFS.FollowSymlinks\n\t\t\t}\n\t\t\tif dest.SSHFS.SFTPDriver == nil && mount.SSHFS.SFTPDriver != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"sshfs.sftpDriver\")\n\t\t\t\tdest.SSHFS.SFTPDriver = mount.SSHFS.SFTPDriver\n\t\t\t}\n\t\t\t// NineP\n\t\t\tif dest.NineP.SecurityModel == nil && mount.NineP.SecurityModel != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"9p.securityModel\")\n\t\t\t\tdest.NineP.SecurityModel = mount.NineP.SecurityModel\n\t\t\t}\n\t\t\tif dest.NineP.ProtocolVersion == nil && mount.NineP.ProtocolVersion != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"9p.protocolVersion\")\n\t\t\t\tdest.NineP.ProtocolVersion = mount.NineP.ProtocolVersion\n\t\t\t}\n\t\t\tif dest.NineP.Msize == nil && mount.NineP.Msize != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"9p.msize\")\n\t\t\t\tdest.NineP.Msize = mount.NineP.Msize\n\t\t\t}\n\t\t\tif dest.NineP.Cache == nil && mount.NineP.Cache != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"9p.cache\")\n\t\t\t\tdest.NineP.Cache = mount.NineP.Cache\n\t\t\t}\n\t\t\t// Virtiofs\n\t\t\tif dest.Virtiofs.QueueSize == nil && mount.Virtiofs.QueueSize != nil {\n\t\t\t\ttmpl.copyListEntryField(mounts, dst, src, \"virtiofs.queueSize\")\n\t\t\t\tdest.Virtiofs.QueueSize = mount.Virtiofs.QueueSize\n\t\t\t}\n\t\t\tif mountPoint != \"*\" {\n\t\t\t\ttmpl.copyListEntryComments(mounts, dst, src, \"\")\n\t\t\t\ttmpl.copyListEntryComments(mounts, dst, src, \"sshfs\")\n\t\t\t\ttmpl.copyListEntryComments(mounts, dst, src, \"9p\")\n\t\t\t\ttmpl.copyListEntryComments(mounts, dst, src, \"virtiofs\")\n\t\t\t}\n\t\t}\n\t\ttmpl.Config.Mounts = slices.Delete(tmpl.Config.Mounts, src, src+1)\n\t\ttmpl.deleteListEntry(mounts, src)\n\t}\n}\n\n// combineNetworks combines networks entries. The shared key is the interface name.\nfunc (tmpl *Template) combineNetworks() {\n\tconst networks = \"networks\"\n\n\tinterfaceIdx := make(map[string]int, len(tmpl.Config.Networks))\n\tfor src := 0; src < len(tmpl.Config.Networks); {\n\t\tnw := tmpl.Config.Networks[src]\n\t\tvar from, to int\n\t\tif nw.Interface == \"*\" {\n\t\t\tfrom = 0\n\t\t\tto = src - 1\n\t\t} else {\n\t\t\tif i, ok := interfaceIdx[nw.Interface]; ok {\n\t\t\t\tfrom = i\n\t\t\t\tto = i\n\t\t\t} else {\n\t\t\t\tif nw.Interface != \"\" {\n\t\t\t\t\tinterfaceIdx[nw.Interface] = src\n\t\t\t\t}\n\t\t\t\tsrc++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tfor dst := from; dst <= to; dst++ {\n\t\t\tdest := &tmpl.Config.Networks[dst]\n\t\t\t// Lima and Socket are mutually exclusive. Only copy base values if both are still unset.\n\t\t\tif dest.Lima == \"\" && dest.Socket == \"\" {\n\t\t\t\tif nw.Lima != \"\" {\n\t\t\t\t\ttmpl.copyListEntryField(networks, dst, src, \"lima\")\n\t\t\t\t\tdest.Lima = nw.Lima\n\t\t\t\t}\n\t\t\t\tif nw.Socket != \"\" {\n\t\t\t\t\ttmpl.copyListEntryField(networks, dst, src, \"socket\")\n\t\t\t\t\tdest.Socket = nw.Socket\n\t\t\t\t}\n\t\t\t}\n\t\t\tif dest.MACAddress == \"\" && nw.MACAddress != \"\" {\n\t\t\t\ttmpl.copyListEntryField(networks, dst, src, \"macAddress\")\n\t\t\t\tdest.MACAddress = nw.MACAddress\n\t\t\t}\n\t\t\tif dest.VZNAT == nil && nw.VZNAT != nil {\n\t\t\t\ttmpl.copyListEntryField(networks, dst, src, \"vzNAT\")\n\t\t\t\tdest.VZNAT = nw.VZNAT\n\t\t\t}\n\t\t\tif dest.Metric == nil && nw.Metric != nil {\n\t\t\t\ttmpl.copyListEntryField(networks, dst, src, \"metric\")\n\t\t\t\tdest.Metric = nw.Metric\n\t\t\t}\n\t\t\tif nw.Interface != \"*\" {\n\t\t\t\ttmpl.copyListEntryComments(networks, dst, src, \"\")\n\t\t\t}\n\t\t}\n\t\ttmpl.Config.Networks = slices.Delete(tmpl.Config.Networks, src, src+1)\n\t\ttmpl.deleteListEntry(networks, src)\n\t}\n}\n\n// yamlfmt will fail with a buffer overflow while trying to retain line breaks if the line\n// is longer than 64K. We will encode all text files that have a line that comes close.\n// maxLineLength is a constant; it is only a variable for the benefit of the unit tests.\nvar maxLineLength = 65000\n\n// encodeScriptReason returns the reason why a script needs to be base64 encoded or the empty string if it doesn't.\nfunc encodeScriptReason(script string) string {\n\tstart := 0\n\tline := 1\n\tfor i, r := range script {\n\t\tif !(unicode.IsPrint(r) || r == '\\n') {\n\t\t\treturn fmt.Sprintf(\"unprintable character %q at offset %d\", r, i)\n\t\t}\n\t\t// maxLineLength includes final newline\n\t\tif i-start >= maxLineLength {\n\t\t\treturn fmt.Sprintf(\"line %d (offset %d) is longer than %d characters\", line, start, maxLineLength)\n\t\t}\n\t\tif r == '\\n' {\n\t\t\tline++\n\t\t\tstart = i + 1\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// Break base64 strings into shorter chunks. Technically we could use maxLineLength here,\n// but shorter lines look better.\nconst base64ChunkLength = 76\n\n// binaryString returns a base64 encoded version of the binary string, broken into chunks\n// of at most base64ChunkLength characters per line.\nfunc binaryString(s string) string {\n\tencoded := base64.StdEncoding.EncodeToString([]byte(s))\n\tif len(encoded) <= base64ChunkLength {\n\t\treturn encoded\n\t}\n\n\t// Estimate capacity: encoded length + number of newlines\n\tlineCount := (len(encoded) + base64ChunkLength - 1) / base64ChunkLength\n\tbuilder := strings.Builder{}\n\tbuilder.Grow(len(encoded) + lineCount)\n\n\tfor i := 0; i < len(encoded); i += base64ChunkLength {\n\t\tend := min(i+base64ChunkLength, len(encoded))\n\t\tbuilder.WriteString(encoded[i:end])\n\t\tbuilder.WriteByte('\\n')\n\t}\n\n\treturn builder.String()\n}\n\n// updateScript replaces a \"file\" property with the actual script and then renames the field to newName (\"script\" or \"content\").\nfunc (tmpl *Template) updateScript(field string, idx int, newName, script, file string) {\n\ttag := \"\"\n\tif reason := encodeScriptReason(script); reason != \"\" {\n\t\tlogrus.Infof(\"File %q is being base64 encoded: %s\", file, reason)\n\t\tscript = binaryString(script)\n\t\ttag = \"!!binary\"\n\t}\n\tentry := fmt.Sprintf(\"$a.%s[%d].file\", field, idx)\n\t// Assign script to the \"file\" field and then rename it to \"script\" or \"content\".\n\tfmt.Fprintf(&tmpl.expr, \"| (%s) = %q | (%s) tag = %q | (%s | key) = %q\\n\",\n\t\tentry, script, entry, tag, entry, newName)\n}\n\n// embedAllScripts replaces all \"provision\" and \"probes\" file references with the actual script.\nfunc (tmpl *Template) embedAllScripts(ctx context.Context, embedAll bool) error {\n\tif err := tmpl.Unmarshal(); err != nil {\n\t\treturn err\n\t}\n\tfor i, p := range tmpl.Config.Probes {\n\t\tif p.File == nil {\n\t\t\tcontinue\n\t\t}\n\t\t// Don't overwrite existing script. This should throw an error during validation.\n\t\tif p.Script == nil || *p.Script != \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tisTemplate, _ := SeemsTemplateURL(p.File.URL)\n\t\tif embedAll || !isTemplate {\n\t\t\tscriptTmpl, err := Read(ctx, \"\", p.File.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttmpl.updateScript(\"probes\", i, \"script\", string(scriptTmpl.Bytes), p.File.URL)\n\t\t}\n\t}\n\tfor i, p := range tmpl.Config.Provision {\n\t\tif p.File == nil {\n\t\t\tcontinue\n\t\t}\n\t\tnewName := \"script\"\n\t\tswitch p.Mode {\n\t\tcase limatype.ProvisionModeData:\n\t\t\tnewName = \"content\"\n\t\t\tif p.Content != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase limatype.ProvisionModeYQ:\n\t\t\tnewName = \"expression\"\n\t\t\tif p.Expression != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\tdefault:\n\t\t\tif p.Script != nil && *p.Script != \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tisTemplate, _ := SeemsTemplateURL(p.File.URL)\n\t\tif embedAll || !isTemplate {\n\t\t\tscriptTmpl, err := Read(ctx, \"\", p.File.URL)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\ttmpl.updateScript(\"provision\", i, newName, string(scriptTmpl.Bytes), p.File.URL)\n\t\t}\n\t}\n\treturn tmpl.evalExpr()\n}\n"
  },
  {
    "path": "pkg/limatmpl/embed_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"gotest.tools/v3/assert\"\n\t\"gotest.tools/v3/assert/cmp\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n)\n\ntype embedTestCase struct {\n\tdescription string\n\ttemplate    string\n\tbase        string\n\texpected    string\n}\n\n// Notes:\n// * When the template starts with \"#\" then the comparison will be textual instead of structural.\n//   This is required to verify comment handling.\n// * If the description starts with \"TODO\" then the test is expected to fail (until it is fixed).\n// * If the description starts with \"ERROR\" then the test is expected to fail with an error containing the expected string.\n// * base is split on \"---\\n\" and stored as base0.yaml, base1.yaml, ... in the same dir as the template.\n// * If any base template starts with \"#!\" then the extension will be .sh instead of .yaml.\n// * The template is automatically prefixed with \"base: base0.yaml\" unless base0 is a script.\n// * All line comments will be separated by 2 spaces from the value on output.\n// * Merge order of additionalDisks, mounts, and networks depends on the logic in the\n//   combineListEntries() functions and will not follow the order of the base template(s).\n\nvar embedTestCases = []embedTestCase{\n\t{\n\t\t\"Empty template\",\n\t\t\"\",\n\t\t\"vmType: qemu\",\n\t\t\"vmType: qemu\",\n\t},\n\t{\n\t\t\"Base doesn't override existing values\",\n\t\t\"vmType: vz\",\n\t\t\"{arch: aarch64, vmType: qemu}\",\n\t\t\"{arch: aarch64, vmType: vz}\",\n\t},\n\t{\n\t\t\"Comments are copied over as well\",\n\t\t`#\n# VM Type is QEMU\nvmType: qemu # QEMU\n`,\n\t\t`\n# Arch is x86_64\narch: x86_64 # X86\n`,\n\t\t`\n# VM Type is QEMU\nvmType: qemu  # QEMU\n# Arch is x86_64\narch: x86_64  # X86\n`,\n\t},\n\t{\n\t\t\"mountTypesUnsupported are concatenated and duplicates removed\",\n\t\t\"mountTypesUnsupported: [9p,reverse-sshfs]\",\n\t\t\"mountTypesUnsupported: [9p,virtiofs]\",\n\t\t\"mountTypesUnsupported: [9p,reverse-sshfs,virtiofs]\",\n\t},\n\t{\n\t\t\"minimumLimaVersion (including comments) is updated when the base version is higher\",\n\t\t`#\n# Works with Lima 0.8.0 and later\nminimumLimaVersion: 0.8.0 # needs 0.8.0\n`,\n\t\t`\n# Requires at least 1.0.2\nminimumLimaVersion: 1.0.2    # or later\n`,\n\t\t`\n# Requires at least 1.0.2\nminimumLimaVersion: 1.0.2  # or later\n`,\n\t},\n\t{\n\t\t\"vmOpts.qmu.minimumVersion is updated when the base version is higher\",\n\t\t\"vmOpts: {qemu: {minimumVersion: 8.2.1}}\",\n\t\t\"vmOpts: {qemu: {minimumVersion: 9.1.0}}\",\n\t\t\"vmOpts: {qemu: {minimumVersion: 9.1.0}}\",\n\t},\n\t{\n\t\t\"dns list is not appended, but the highest priority one is picked\",\n\t\t\"dns: [1.1.1.1]\",\n\t\t\"dns: [8.8.8.8, 1.2.3.4]\",\n\t\t\"dns: [1.1.1.1]\",\n\t},\n\t{\n\t\t\"Update comments on existing maps and lists that don't have comments yet\",\n\t\t`#\nadditionalDisks:\n- name: disk1 # One\n`,\n\t\t`\n# Mount additional disks\nadditionalDisks: # comment\n# This is disk2\n- name: disk2 # Two\n`,\n\t\t`\n# Mount additional disks\nadditionalDisks:  # comment\n- name: disk1  # One\n# This is disk2\n- name: disk2  # Two\n`,\n\t},\n\t{\n\t\t\"probes and provision scripts are prepended instead of appended\",\n\t\t\"probes: [{script: 1}]\\nprovision: [{script: One}]\",\n\t\t\"probes: [{script: 2}]\\nprovision: [{script: Two}]\",\n\t\t\"probes: [{script: 2},{script: 1}]\\nprovision: [{script: Two},{script: One}]\",\n\t},\n\t{\n\t\t\"additionalDisks append, but merge fields on shared name\",\n\t\t\"additionalDisks: [{name: disk1}]\",\n\t\t\"additionalDisks: [{name: disk2},{name: disk1, format: true}]\",\n\t\t\"additionalDisks: [{name: disk1, format: true},{name: disk2}]\",\n\t},\n\t{\n\t\t// This test fails because there are 2 spurious newlines in the merged output\n\t\t\"TODO mounts append, but merge fields on shared mountPoint\",\n\t\t`#\n# My mounts\nmounts:\n- location: loc1  # mountPoint loc1\n- location: loc1\n  mountPoint: loc2\n`,\n\t\t`\nmounts:\n# will update mountPoint loc2\n- location: loc1\n  mountPoint: loc2\n  writable: true\n  # SSHFS\n  sshfs:  # ssh\n    followSymlinks: true\n# will create new mountPoint loc3\n- location: loc1\n  mountPoint: loc3\n  writable: true\n`,\n\t\t`\n# My mounts\nmounts:\n- location: loc1  # mountPoint loc1\n# will update mountPoint loc2\n- location: loc1\n  mountPoint: loc2\n  writable: true\n  # SSHFS\n  sshfs:  # ssh\n    followSymlinks: true\n# will create new mountPoint loc3\n- location: loc1\n  mountPoint: loc3\n  writable: true\n`,\n\t},\n\t{\n\t\t// This entry can be deleted when the previous one no longer fails\n\t\t\"mounts append, but merge fields on shared mountPoint (no comments version)\",\n\t\t`mounts: [{location: loc1}, {location: loc1, mountPoint: loc2}]`,\n\t\t`mounts: [{location: loc1, mountPoint: loc2, writable: true, sshfs: {followSymlinks: true}}, {location: loc1, mountPoint: loc3, writable: true}]`,\n\t\t`mounts: [{location: loc1}, {location: loc1, mountPoint: loc2, writable: true, sshfs: {followSymlinks: true}}, {location: loc1, mountPoint: loc3, writable: true}]`,\n\t},\n\t{\n\t\t\"template: URLs are not embedded when embedAll is false\",\n\t\t// also tests file.url format\n\t\t``,\n\t\t`\nbase: template:default\nprovision:\n- file:\n    url: template:provision.sh\nprobes:\n- file:\n    url: template:probe.sh\n`,\n\t\t`\nbase: template:default\nprovision:\n- file: template:provision.sh\nprobes:\n- file: template:probe.sh\n`,\n\t},\n\t{\n\t\t\"ERROR Each template must only be embedded once\",\n\t\t`#\narch: aarch64\n`,\n\t\t`\nbase: base0.yaml\n# failure would mean this test loops forever, not that it fails the test\nvmType: qemu\n`,\n\t\t`base template loop detected`,\n\t},\n\t{\n\t\t\"ERROR All bases following template: bases must be template: URLs too when embedAll is false\",\n\t\t``,\n\t\t`base: [template:default, base1.yaml]`,\n\t\t\"after not embedding\",\n\t},\n\t{\n\t\t\"ERROR All bases following template: bases must be template: URLs too when embedAll is false\",\n\t\t``,\n\t\t`\nbase: [base1.yaml, base2.yaml]\n---\nbase: template:default\n---\nbase: baseX.yaml`,\n\t\t\"after not embedding\",\n\t},\n\t{\n\t\t\"Bases are embedded depth-first\",\n\t\t`#`,\n\t\t`\nbase: [base1.yaml, {url: base2.yaml}] # also test file.url format\nadditionalDisks: [disk0]\n---\nbase: base3.yaml\nadditionalDisks: [disk1]\n---\nadditionalDisks: [disk2]\n---\nadditionalDisks: [disk3]\n`,\n\t\t`\nadditionalDisks: [disk0, disk1, disk3, disk2]\n`,\n\t},\n\t{\n\t\t\"additionalDisks with name '*' are merged with all previous entries\",\n\t\t`\nadditionalDisks:\n- name: disk1\n- name: disk2\n- name: disk3\n  format: false\n`,\n\t\t`\nadditionalDisks:\n- name: disk4\n- name: \"*\"\n  format: true # will apply to disk1, disk2, and disk4\n- name: disk5\n`,\n\t\t`\nadditionalDisks:\n- name: disk1\n  format: true\n- name: disk2\n  format: true\n- name: disk3\n  format: false\n- name: disk4\n  format: true\n- name: disk5\n`,\n\t},\n\t{\n\t\t// This test fails because the yq commands don't handle comments properly; may need to be fixed in yq\n\t\t\"TODO additionalDisks will be upgraded from string to map\",\n\t\t`#\nadditionalDisks:\n# my head comment\n- mine # my line comment\n`,\n\t\t`\n# head comment\nadditionalDisks: # line comment\n- name: \"*\"\n  format: true # formatting is good for you\n`,\n\t\t`\n# head comment\nadditionalDisks:  # line comment\n# my head comment\n- name: mine  # my line comment\n  format: true  # formatting is good for you\n`,\n\t},\n\t{\n\t\t// This entry can be deleted when the previous one no longer fails\n\t\t\"additionalDisks will be upgraded from string to map (no comments version)\",\n\t\t`additionalDisks: [mine]`,\n\t\t`additionalDisks: [{name: \"*\", format: true}]`,\n\t\t`additionalDisks: [{name: mine, format: true}]`,\n\t},\n\t{\n\t\t\"networks without interface name are not merged\",\n\t\t`\nnetworks:\n- interface: lima1\n`,\n\t\t`\nnetworks:\n- interface: lima2\n# The metric will not be merged with anything\n- metric: 250\n- interface: lima1\n  metric: 100     # will be set on the first entry\n- interface: '*'  # wildcard\n  metric: 123     # will be set on the first entry\n`,\n\t\t`\nnetworks:\n- interface: lima1\n  metric: 100  # will be set on the first entry\n- interface: lima2\n  metric: 123  # will be set on the first entry\n# The metric will not be merged with anything\n- metric: 250\n`,\n\t},\n\t{\n\t\t\"Scripts are embedded with comments moved\",\n\t\t`#\n# Hi There!\nprovision:\n# This script will be merged from an external file\n- file: base1.sh # This comment will move to the \"script\" key\n# This is just a data file\n- mode: data\n  file: base1.sh # This comment will move to the \"content\" key\n  path: /tmp/data\n- mode: yq\n  file: base1.sh # This comment will move to the \"expression\" key\n  path: /tmp/yq\n`,\n\t\t`\n# base0.yaml is ignored\n---\n#!/usr/bin/env bash\necho \"This is base1.sh\"\n`,\n\t\t// TODO: the empty line after the `path` is unexpected\n\t\t`\n# Hi There!\nprovision:\n# This script will be merged from an external file\n- script: |-  # This comment will move to the \"script\" key\n    #!/usr/bin/env bash\n    echo \"This is base1.sh\"\n# This is just a data file\n- mode: data\n  content: |-  # This comment will move to the \"content\" key\n    #!/usr/bin/env bash\n    echo \"This is base1.sh\"\n  path: /tmp/data\n- mode: yq\n  expression: |-  # This comment will move to the \"expression\" key\n    #!/usr/bin/env bash\n    echo \"This is base1.sh\"\n  path: /tmp/yq\n\n# base0.yaml is ignored\n`,\n\t},\n\t{\n\t\t\"Script files are embedded even when no base property exists\",\n\t\t\"provision: [{file: base0.sh}]\",\n\t\t\"#! my script\",\n\t\t`provision: [{script: \"#! my script\"}]`,\n\t},\n\t{\n\t\t\"ERROR base digest is not yet implemented\",\n\t\t\"\",\n\t\t\"base: [{url: base.yaml, digest: deafbad}]\",\n\t\t\"not yet implemented\",\n\t},\n\t{\n\t\t\"Image URLs will be converted into a template\",\n\t\t\"\",\n\t\t\"base: https://example.com/lima-linux-riscv64.img\",\n\t\t\"{arch: riscv64, os: Linux, images: [{location: https://example.com/lima-linux-riscv64.img, arch: riscv64}]}\",\n\t},\n\t{\n\t\t\"Binary files are base64 encoded\",\n\t\t`#\nprovision:\n- mode: data\n  file: base1.sh # This comment will move to the \"content\" key\n  path: /tmp/data\n`,\n\t\t// base1.sh is binary because it contains an audible bell character '\\a'\n\t\t\"# base0.yaml is ignored\\n---\\n#!\\a123456789012345678901234567890123456789012345678901234567890\",\n\t\t`\nprovision:\n- mode: data\n  content: !!binary |  # This comment will move to the \"content\" key\n    IyEHMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\n    NTY3ODkw\n  path: /tmp/data\n\n# base0.yaml is ignored\n`,\n\t},\n}\n\nfunc TestEmbed(t *testing.T) {\n\tfocus := os.Getenv(\"TEST_FOCUS\")\n\tfor _, tc := range embedTestCases {\n\t\tif focus != \"\" {\n\t\t\tif !strings.Contains(tc.description, focus) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t\t}\n\t\tt.Run(tc.description, func(t *testing.T) { RunEmbedTest(t, tc) })\n\t}\n\tlogrus.SetLevel(logrus.InfoLevel)\n}\n\nfunc RunEmbedTest(t *testing.T, tc embedTestCase) {\n\ttodo := strings.HasPrefix(tc.description, \"TODO\")\n\texpectError := strings.HasPrefix(tc.description, \"ERROR\")\n\tstringCompare := strings.HasPrefix(tc.template, \"#\")\n\n\t// Normalize testcase data\n\ttc.template = strings.TrimSpace(strings.TrimPrefix(tc.template, \"#\"))\n\ttc.base = strings.TrimSpace(tc.base)\n\ttc.expected = strings.TrimSpace(tc.expected)\n\n\t// Change to temp directory so all template and script names don't include a slash.\n\tt.Chdir(t.TempDir())\n\n\tfor i, base := range strings.Split(tc.base, \"---\\n\") {\n\t\textension := \".yaml\"\n\t\tif strings.HasPrefix(base, \"#!\") {\n\t\t\textension = \".sh\"\n\t\t}\n\t\tbaseFilename := fmt.Sprintf(\"base%d%s\", i, extension)\n\t\terr := os.WriteFile(baseFilename, []byte(base), 0o600)\n\t\tassert.NilError(t, err, tc.description)\n\t}\n\ttmpl := &Template{\n\t\tBytes:   fmt.Appendf(nil, \"base: base0.yaml\\n%s\", tc.template),\n\t\tLocator: \"tmpl.yaml\",\n\t}\n\t// Don't include `base` property if base0 is a script\n\tif strings.HasPrefix(tc.base, \"#!\") {\n\t\ttmpl.Bytes = []byte(tc.template)\n\t}\n\terr := tmpl.Embed(t.Context(), false, false)\n\tif expectError {\n\t\tassert.ErrorContains(t, err, tc.expected, tc.description)\n\t\treturn\n\t}\n\tassert.NilError(t, err, tc.description)\n\n\tif stringCompare {\n\t\tactual := strings.TrimSpace(string(tmpl.Bytes))\n\t\tif todo {\n\t\t\tassert.Assert(t, actual != tc.expected, tc.description)\n\t\t} else {\n\t\t\tassert.Equal(t, actual, tc.expected, tc.description)\n\t\t}\n\t\treturn\n\t}\n\n\terr = tmpl.Unmarshal()\n\tassert.NilError(t, err, tc.description)\n\n\tvar expected limatype.LimaYAML\n\terr = limayaml.Unmarshal([]byte(tc.expected), &expected, \"expected\")\n\tassert.NilError(t, err, tc.description)\n\n\tif todo {\n\t\t// using reflect.DeepEqual because cmp.DeepEqual can't easily be negated\n\t\tassert.Assert(t, !reflect.DeepEqual(tmpl.Config, &expected), tc.description)\n\t} else {\n\t\tassert.Assert(t, cmp.DeepEqual(tmpl.Config, &expected), tc.description)\n\t}\n}\n\nfunc TestEncodeScriptReason(t *testing.T) {\n\tmaxLineLength = 8\n\tt.Run(\"regular script\", func(t *testing.T) {\n\t\treason := encodeScriptReason(\"0123456\\n\")\n\t\tassert.Equal(t, reason, \"\")\n\t})\n\tt.Run(\"binary script\", func(t *testing.T) {\n\t\treason := encodeScriptReason(\"abc\\a123\")\n\t\tassert.Equal(t, reason, \"unprintable character '\\\\a' at offset 3\")\n\t})\n\tt.Run(\"contains a tab character\", func(t *testing.T) {\n\t\t// newline character is included in character count\n\t\treason := encodeScriptReason(\"foo\\tbar\")\n\t\tassert.Equal(t, reason, \"unprintable character '\\\\t' at offset 3\")\n\t})\n\tt.Run(\"long line\", func(t *testing.T) {\n\t\t// newline character is included in character count\n\t\treason := encodeScriptReason(\"line 1\\nline 2\\n01234567\\n\")\n\t\tassert.Equal(t, reason, \"line 3 (offset 14) is longer than 8 characters\")\n\t})\n}\n"
  },
  {
    "path": "pkg/limatmpl/github.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n)\n\nconst defaultFilename = \".lima.yaml\"\n\n// transformGitHubURL transforms a github: URL to a raw.githubusercontent.com URL.\n// Input format: ORG/REPO[/PATH][@BRANCH]\n//\n// If REPO is missing, it will be set the same as ORG.\n// If BRANCH is missing, it will be queried from GitHub.\n// If PATH filename has no extension, it will get .yaml.\n// If PATH is just a directory (trailing slash), it will be set to .lima.yaml\n// IF FILE is .lima.yaml and contents looks like a symlink, it will be replaced by the symlink target.\nfunc transformGitHubURL(ctx context.Context, input string) (string, error) {\n\tinput, origBranch, _ := strings.Cut(input, \"@\")\n\n\tparts := strings.Split(input, \"/\")\n\tfor len(parts) < 2 {\n\t\tparts = append(parts, \"\")\n\t}\n\torg := parts[0]\n\tif org == \"\" {\n\t\treturn \"\", fmt.Errorf(\"github: URL must contain at least an ORG, got %q\", input)\n\t}\n\t// If REPO is omitted (github:ORG or github:ORG//PATH), default it to ORG\n\trepo := cmp.Or(parts[1], org)\n\tfilePath := strings.Join(parts[2:], \"/\")\n\n\tif filePath == \"\" {\n\t\tfilePath = defaultFilename\n\t} else {\n\t\t// If path ends with / then it's a directory, so append .lima\n\t\tif strings.HasSuffix(filePath, \"/\") {\n\t\t\tfilePath += defaultFilename\n\t\t}\n\n\t\t// If the filename (excluding first char for hidden files) has no extension, add .yaml\n\t\tfilename := path.Base(filePath)\n\t\tif !strings.Contains(filename[1:], \".\") {\n\t\t\tfilePath += \".yaml\"\n\t\t}\n\t}\n\n\t// Query default branch if no branch was specified\n\tbranch := origBranch\n\tif branch == \"\" {\n\t\tvar err error\n\t\tbranch, err = getGitHubDefaultBranch(ctx, org, repo)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to get default branch for %s/%s: %w\", org, repo, err)\n\t\t}\n\t}\n\n\t// If filename has a .yaml/.yml extension, check if it's a symlink/redirect to another file\n\text := strings.ToLower(path.Ext(filePath))\n\tif ext == \".yaml\" || ext == \".yml\" {\n\t\treturn resolveGitHubSymlink(ctx, org, repo, branch, filePath, origBranch)\n\t}\n\treturn githubUserContentURL(org, repo, branch, filePath), nil\n}\n\nfunc githubUserContentURL(org, repo, branch, filePath string) string {\n\treturn fmt.Sprintf(\"https://raw.githubusercontent.com/%s/%s/%s/%s\", org, repo, branch, filePath)\n}\n\nfunc getGitHubUserContent(ctx context.Context, org, repo, branch, filePath string) (*http.Response, error) {\n\turl := githubUserContentURL(org, repo, branch, filePath)\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\treq.Header.Set(\"User-Agent\", \"lima\")\n\treturn http.DefaultClient.Do(req)\n}\n\n// getGitHubDefaultBranch queries the GitHub API to get the default branch for a repository.\nfunc getGitHubDefaultBranch(ctx context.Context, org, repo string) (string, error) {\n\tapiURL := fmt.Sprintf(\"https://api.github.com/repos/%s/%s\", org, repo)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, http.NoBody)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"User-Agent\", \"lima\")\n\treq.Header.Set(\"Accept\", \"application/vnd.github.v3+json\")\n\n\t// Check for GitHub token in environment for authenticated requests (higher rate limit)\n\ttoken := cmp.Or(os.Getenv(\"GH_TOKEN\"), os.Getenv(\"GITHUB_TOKEN\"))\n\tif token != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"token \"+token)\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to query GitHub API: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read GitHub API response: %w\", err)\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"GitHub API returned status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar repoData struct {\n\t\tDefaultBranch string `json:\"default_branch\"`\n\t}\n\tif err := json.Unmarshal(body, &repoData); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse GitHub API response: %w\", err)\n\t}\n\tif repoData.DefaultBranch == \"\" {\n\t\treturn \"\", fmt.Errorf(\"repository %s/%s has no default branch\", org, repo)\n\t}\n\treturn repoData.DefaultBranch, nil\n}\n\n// resolveGitHubSymlink checks if a file at the given path is a symlink/redirect to another file.\n// If the file contains a single line without newline, space, or colon then it's treated as a path to the actual file.\n// Returns a URL to the redirect path if found, or a URL for original path otherwise.\nfunc resolveGitHubSymlink(ctx context.Context, org, repo, branch, filePath, origBranch string) (string, error) {\n\tresp, err := getGitHubUserContent(ctx, org, repo, branch, filePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch file: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// Special rule for branch/tag propagation for github:ORG// requests.\n\tif resp.StatusCode == http.StatusNotFound && repo == org {\n\t\tdefaultBranch, err := getGitHubDefaultBranch(ctx, org, repo)\n\t\tif err == nil {\n\t\t\treturn resolveGitHubRedirect(ctx, org, repo, defaultBranch, filePath, branch)\n\t\t}\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"file %q not found or inaccessible: status %d\", resp.Request.URL, resp.StatusCode)\n\t}\n\n\t// Read first 1KB to check the file content\n\tbuf := make([]byte, 1024)\n\tn, err := resp.Body.Read(buf)\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\treturn \"\", fmt.Errorf(\"failed to read %q content: %w\", resp.Request.URL, err)\n\t}\n\tcontent := string(buf[:n])\n\n\t// Symlink can also be a github: redirect if we are in a github:ORG// repo.\n\tif repo == org && strings.HasPrefix(content, \"github:\") {\n\t\treturn validateGitHubRedirect(content, org, origBranch, resp.Request.URL.String())\n\t}\n\n\t// A symlink must be a single line (without trailing newline), no spaces, no colons\n\tif !(content == \"\" || strings.ContainsAny(content, \"\\n :\")) {\n\t\t// symlink is relative to the directory of filePath\n\t\tfilePath = path.Join(path.Dir(filePath), content)\n\t}\n\treturn githubUserContentURL(org, repo, branch, filePath), nil\n}\n\n// resolveGitHubRedirect checks if a file at the given path is a github: URL to another file within the same repo.\n// Returns the URL, or an error if the file doesn't exist, or doesn't start with github:ORG.\nfunc resolveGitHubRedirect(ctx context.Context, org, repo, defaultBranch, filePath, origBranch string) (string, error) {\n\t// Refetch the filepath from the defaultBranch\n\tresp, err := getGitHubUserContent(ctx, org, repo, defaultBranch, filePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch file: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"file %q not found or inaccessible: status %d\", resp.Request.URL, resp.StatusCode)\n\t}\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read %q content: %w\", resp.Request.URL, err)\n\t}\n\treturn validateGitHubRedirect(string(body), org, origBranch, resp.Request.URL.String())\n}\n\nfunc validateGitHubRedirect(body, org, origBranch, url string) (string, error) {\n\tredirect, _, _ := strings.Cut(body, \"\\n\")\n\tredirect = strings.TrimSpace(redirect)\n\n\tif !strings.HasPrefix(redirect, \"github:\"+org+\"/\") {\n\t\treturn \"\", fmt.Errorf(`redirect %q is not a \"github:%s\" URL (from %q)`, redirect, org, url)\n\t}\n\tif strings.ContainsRune(redirect, '@') {\n\t\treturn \"\", fmt.Errorf(\"redirect %q must not include a branch/tag/sha (from %q)\", redirect, url)\n\t}\n\t// If the origBranch is empty, then we need to look up the default branch in the redirect\n\tif origBranch != \"\" {\n\t\tredirect += \"@\" + origBranch\n\t}\n\treturn redirect, nil\n}\n"
  },
  {
    "path": "pkg/limatmpl/locator.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/identifiers\"\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/plugins\"\n\t\"github.com/lima-vm/lima/v2/pkg/templatestore\"\n)\n\nconst yBytesLimit = 4 * 1024 * 1024 // 4MiB\n\nfunc Read(ctx context.Context, name, locator string) (*Template, error) {\n\ttmpl := &Template{\n\t\tName:    name,\n\t\tLocator: locator,\n\t}\n\n\tlocator, err := TransformCustomURL(ctx, locator)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif imageTemplate(tmpl, locator) {\n\t\treturn tmpl, nil\n\t}\n\n\tisTemplateURL, templateName := SeemsTemplateURL(locator)\n\tswitch {\n\tcase isTemplateURL:\n\t\tlogrus.Debugf(\"interpreting argument %q as a template name %q\", locator, templateName)\n\t\tif tmpl.Name == \"\" {\n\t\t\t// e.g., templateName = \"deprecated/centos-7.yaml\" , tmpl.Name = \"centos-7\"\n\t\t\ttmpl.Name, err = InstNameFromYAMLPath(templateName)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\ttmpl.Bytes, err = templatestore.Read(templateName)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase SeemsHTTPURL(locator):\n\t\tif tmpl.Name == \"\" {\n\t\t\ttmpl.Name, err = InstNameFromURL(locator)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tlogrus.Debugf(\"interpreting argument %q as a http url for instance %q\", locator, tmpl.Name)\n\t\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, locator, http.NoBody)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\ttmpl.Bytes, err = ioutilx.ReadAtMaximum(resp.Body, yBytesLimit)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase SeemsFileURL(locator):\n\t\tif tmpl.Name == \"\" {\n\t\t\ttmpl.Name, err = InstNameFromURL(locator)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tlogrus.Debugf(\"interpreting argument %q as a file URL for instance %q\", locator, tmpl.Name)\n\t\tfilePath := strings.TrimPrefix(locator, \"file://\")\n\t\tif !filepath.IsAbs(filePath) {\n\t\t\treturn nil, fmt.Errorf(\"file URL %q is not an absolute path\", locator)\n\t\t}\n\t\tr, err := os.Open(filePath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer r.Close()\n\t\ttmpl.Bytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\tcase locator == \"-\":\n\t\ttmpl.Bytes, err = io.ReadAll(os.Stdin)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unexpected error reading stdin: %w\", err)\n\t\t}\n\tdefault:\n\t\tif tmpl.Name == \"\" {\n\t\t\ttmpl.Name, err = InstNameFromYAMLPath(locator)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tlogrus.Debugf(\"interpreting argument %q as a file path for instance %q\", locator, tmpl.Name)\n\t\tif locator, err = filepath.Abs(locator); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttmpl.Locator = locator\n\t\tr, err := os.Open(locator)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer r.Close()\n\t\ttmpl.Bytes, err = ioutilx.ReadAtMaximum(r, yBytesLimit)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\t// The only reason not to call tmpl.UseAbsLocators() here is that `limactl tmpl copy --verbatim …`\n\t// should create an unmodified copy of the template.\n\treturn tmpl, nil\n}\n\n// Locators with an image file format extension, optionally followed by a compression method.\n// This regex is also used to remove the file format suffix from the instance name.\nvar imageURLRegex = regexp.MustCompile(`\\.(img|qcow2|raw|iso|ipsw)(\\.(gz|xz|bz2|zstd))?$`)\n\n// Image architecture will be guessed based on the presence of arch keywords.\nvar archKeywords = map[string]limatype.Arch{\n\t\"aarch64\": limatype.AARCH64,\n\t\"amd64\":   limatype.X8664,\n\t\"arm64\":   limatype.AARCH64,\n\t\"armhf\":   limatype.ARMV7L,\n\t\"armv7l\":  limatype.ARMV7L,\n\t\"ppc64el\": limatype.PPC64LE,\n\t\"ppc64le\": limatype.PPC64LE,\n\t\"riscv64\": limatype.RISCV64,\n\t\"s390x\":   limatype.S390X,\n\t\"x86_64\":  limatype.X8664,\n}\n\n// OS will be guessed based on the presence of OS keywords.\nvar osKeywords = map[string]limatype.OS{\n\t\"FreeBSD\": limatype.FREEBSD,\n\t\"macOS\":   limatype.DARWIN,\n\t\".ipsw\":   limatype.DARWIN,\n}\n\n// These generic tags will be stripped from an image name before turning it into an instance name.\nvar genericTags = []string{\n\t\"base\",         // Fedora, Rocky\n\t\"basic\",        // FreeBSD\n\t\"cloud\",        // Fedora, openSUSE\n\t\"cloudimg\",     // Ubuntu, Arch\n\t\"cloudinit\",    // Alpine\n\t\"current\",      // FreeBSD\n\t\"daily\",        // Debian\n\t\"default\",      // Gentoo\n\t\"generic\",      // Fedora\n\t\"genericcloud\", // CentOS, Debian, Rocky, Alma\n\t\"kvm\",          // Oracle\n\t\"latest\",       // Gentoo, CentOS, Rocky, Alma\n\t\"linux\",        // Arch\n\t\"minimal\",      // openSUSE\n\t\"openstack\",    // Gentoo\n\t\"release\",      // FreeBSD\n\t\"restore\",      // macOS\n\t\"server\",       // Ubuntu\n\t\"std\",          // Alpine-Lima\n\t\"stream\",       // CentOS\n\t\"uefi\",         // Alpine\n\t\"vm\",           // openSUSE\n}\n\n// imageTemplate checks if the locator specifies an image URL.\n// It will create a minimal template with the image URL and arch derived from the image name\n// and also set the default instance name to the image name, but stripped of generic tags.\nfunc imageTemplate(tmpl *Template, locator string) bool {\n\tif !imageURLRegex.MatchString(locator) {\n\t\treturn false\n\t}\n\n\tvar imageOS limatype.OS\n\tfor keyword, os := range osKeywords {\n\t\tpattern := fmt.Sprintf(`(?i)\\b%s\\b`, keyword)\n\t\tif regexp.MustCompile(pattern).MatchString(locator) {\n\t\t\timageOS = os\n\t\t\tbreak\n\t\t}\n\t}\n\tif imageOS == \"\" {\n\t\timageOS = limatype.LINUX\n\t\tlogrus.Debugf(\"cannot determine image OS from URL %q; assuming %q\", locator, imageOS)\n\t}\n\n\tvar imageArch limatype.Arch\n\tfor keyword, arch := range archKeywords {\n\t\tpattern := fmt.Sprintf(`\\b%s\\b`, keyword)\n\t\tif regexp.MustCompile(pattern).MatchString(locator) {\n\t\t\timageArch = arch\n\t\t\tbreak\n\t\t}\n\t}\n\tif imageArch == \"\" {\n\t\tif imageOS == limatype.DARWIN {\n\t\t\timageArch = limatype.AARCH64\n\t\t\t// Other architectures were never supported for macOS guests\n\t\t} else {\n\t\t\timageArch = limatype.NewArch(runtime.GOARCH)\n\t\t\tlogrus.Warnf(\"cannot determine image arch from URL %q; assuming %q\", locator, imageArch)\n\t\t}\n\t}\n\n\ttemplate := `os: %q\narch: %q\nimages:\n- location: %q\n  arch: %q\n`\n\ttmpl.Bytes = fmt.Appendf(nil, template, imageOS, imageArch, locator, imageArch)\n\tif tmpl.Name == \"\" {\n\t\ttmpl.Name = InstNameFromImageURL(locator, imageArch)\n\t}\n\treturn true\n}\n\nfunc InstNameFromImageURL(locator, imageArch string) string {\n\t// We intentionally call both path.Base and filepath.Base in case we are running on Windows.\n\tname := strings.ToLower(filepath.Base(path.Base(locator)))\n\t// Remove file format and compression file types.\n\tname = imageURLRegex.ReplaceAllString(name, \"\")\n\t// The Alpine \"nocloud_\" prefix does not fit the genericTags pattern.\n\tname = strings.TrimPrefix(name, \"nocloud_\")\n\tfor _, tag := range genericTags {\n\t\tre := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\\b`, tag))\n\t\tname = re.ReplaceAllString(name, \"\")\n\t}\n\t// The \"UniversalMac\" prefix does not fit the genericTags pattern and also should be normalized to \"macos\".\n\t// \"UniversalMac_15.6.1_24G90_Restore.ipsw\"\n\tname = strings.Replace(name, \"universalmac_\", \"macos-\", 1)\n\t// ARM64 FreeBSD images have both \"arm64\" and \"aarch64\" in their names.\n\t// \"FreeBSD-16.0-CURRENT-arm64-aarch64-BASIC-CLOUDINIT-ufs.qcow2.xz\"\n\tname = strings.Replace(name, \"arm64-aarch64\", \"arm64\", 1)\n\t// Remove imageArch as well if it is the native arch.\n\tif limatype.IsNativeArch(imageArch) {\n\t\tre := regexp.MustCompile(fmt.Sprintf(`[-_.]%s\\b`, imageArch))\n\t\tname = re.ReplaceAllString(name, \"\")\n\t}\n\t// Remove timestamps from name: 8 digit date, optionally followed by\n\t// a delimiter and one or more digits before a word boundary.\n\tname = regexp.MustCompile(`[-_.]20\\d{6}([-_.]\\d+)?\\b`).ReplaceAllString(name, \"\")\n\t// Normalize archlinux name\n\tname = regexp.MustCompile(`^arch\\b`).ReplaceAllString(name, \"archlinux\")\n\t// Remove redundant major version, e.g. \"rocky-8-8.10\" becomes \"rocky-8.10\".\n\t// Unfortunately regexp doesn't support back references, so we have to\n\t// check manually if both numbers are the same.\n\tre := regexp.MustCompile(`-(\\d+)-(\\d+)\\.`)\n\tname = re.ReplaceAllStringFunc(name, func(match string) string {\n\t\tsubmatch := re.FindStringSubmatch(match)\n\t\tif submatch[1] == submatch[2] {\n\t\t\t// Replace -X-X. with -X.\n\t\t\treturn \"-\" + submatch[1] + \".\"\n\t\t}\n\t\treturn match\n\t})\n\t// Normalize \"macos-15.6.1_24g90\" to \"macos-15.6.1\"\n\tname = regexp.MustCompile(`^(macos-[\\d.]+)[-_].*$`).ReplaceAllString(name, \"$1\")\n\treturn name\n}\n\n// SeemsTemplateURL returns true if the arg is a URL using the template scheme.\n// When it returns true, it also returns the template name.\nfunc SeemsTemplateURL(arg string) (isTemplate bool, templateName string) {\n\tu, err := url.Parse(arg)\n\tif err != nil {\n\t\treturn false, \"\"\n\t}\n\tif u.Scheme == \"template\" {\n\t\tif u.Opaque == \"\" {\n\t\t\treturn true, path.Join(u.Host, u.Path)\n\t\t}\n\t\treturn true, u.Opaque\n\t}\n\treturn false, \"\"\n}\n\n// SeemsHTTPURL returns true if the arg is a URL using the http or https scheme.\nfunc SeemsHTTPURL(arg string) bool {\n\tu, err := url.Parse(arg)\n\tif err != nil {\n\t\treturn false\n\t}\n\tif u.Scheme != \"http\" && u.Scheme != \"https\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// SeemsFileURL returns true if the arg is a URL using the file scheme.\nfunc SeemsFileURL(arg string) bool {\n\tu, err := url.Parse(arg)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn u.Scheme == \"file\"\n}\n\nfunc InstNameFromURL(urlStr string) (string, error) {\n\tu, err := url.Parse(urlStr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn InstNameFromYAMLPath(path.Base(u.Path))\n}\n\nfunc InstNameFromYAMLPath(yamlPath string) (string, error) {\n\ts := strings.ToLower(filepath.Base(yamlPath))\n\ts = strings.TrimSuffix(strings.TrimSuffix(s, \".yml\"), \".yaml\")\n\t// \".\" is allowed in instance names, but replaced to \"-\" for hostnames.\n\t// e.g., yaml: \"ubuntu-24.04.yaml\" , instance name: \"ubuntu-24.04\", hostname: \"lima-ubuntu-24-04\"\n\tif err := identifiers.Validate(s); err != nil {\n\t\treturn \"\", fmt.Errorf(\"filename %q is invalid: %w\", yamlPath, err)\n\t}\n\treturn s, nil\n}\n\nfunc transformCustomURL(ctx context.Context, locator string) (string, error) {\n\tu, err := url.Parse(locator)\n\tif err != nil || len(u.Scheme) <= 1 {\n\t\treturn locator, nil\n\t}\n\n\tif u.Scheme == \"template\" {\n\t\tif u.Opaque != \"\" {\n\t\t\treturn locator, nil\n\t\t}\n\t\t// Fix malformed \"template:\" URLs.\n\t\tnewLocator := \"template:\" + path.Join(u.Host, u.Path)\n\t\tlogrus.Warnf(\"Template locator %q should be written %q since Lima v2.0\", locator, newLocator)\n\t\treturn newLocator, nil\n\t}\n\n\tif u.Scheme == \"github\" {\n\t\treturn transformGitHubURL(ctx, u.Opaque)\n\t}\n\n\tplugin, err := plugins.Find(\"url-\" + u.Scheme)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif plugin == nil {\n\t\treturn locator, nil\n\t}\n\n\tcurrentPath := os.Getenv(\"PATH\")\n\tdefer os.Setenv(\"PATH\", currentPath)\n\terr = plugins.UpdatePath()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := exec.CommandContext(ctx, plugin.Path, strings.TrimPrefix(u.String(), u.Scheme+\":\"))\n\tcmd.Env = os.Environ()\n\n\tstdout, err := cmd.Output()\n\tif err != nil {\n\t\tvar exitErr *exec.ExitError\n\t\tif errors.As(err, &exitErr) {\n\t\t\tstderrMsg := string(exitErr.Stderr)\n\t\t\tif stderrMsg != \"\" {\n\t\t\t\treturn \"\", fmt.Errorf(\"command %q failed: %s\", cmd.String(), strings.TrimSpace(stderrMsg))\n\t\t\t}\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"command %q failed: %w\", cmd.String(), err)\n\t}\n\treturn strings.TrimSpace(string(stdout)), nil\n}\n\nfunc TransformCustomURL(ctx context.Context, locator string) (string, error) {\n\tseen := make(map[string]bool)\n\torigLocator := locator\n\tgithubSchemeDetected := false\n\n\tfor !seen[locator] {\n\t\tseen[locator] = true\n\t\tif strings.HasPrefix(locator, \"github:\") {\n\t\t\tgithubSchemeDetected = true\n\t\t}\n\t\tnewLocator, err := transformCustomURL(ctx, locator)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif newLocator == locator {\n\t\t\tif githubSchemeDetected {\n\t\t\t\tlogrus.Warn(\"The github: scheme is still EXPERIMENTAL\")\n\t\t\t}\n\t\t\treturn newLocator, nil\n\t\t}\n\t\tlogrus.Debugf(\"Locator %q replaced with %q\", locator, newLocator)\n\t\tlocator = newLocator\n\t}\n\treturn \"\", fmt.Errorf(\"custom locator %q has a redirect loop\", origLocator)\n}\n"
  },
  {
    "path": "pkg/limatmpl/locator_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl_test\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatmpl\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nfunc TestInstNameFromImageURL(t *testing.T) {\n\tt.Run(\"strips image format and compression method\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"linux.iso.bz2\", \"unknown\")\n\t\tassert.Equal(t, name, \"linux\")\n\t})\n\tt.Run(\"removes generic tags\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"linux-linux_cloudimg.base-x86_64.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"linux-x86_64\")\n\t})\n\tt.Run(\"removes Alpine `nocloud_` prefix\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"nocloud_linux-x86_64.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"linux-x86_64\")\n\t})\n\tt.Run(\"removes date tag\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"linux-20250101.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"linux\")\n\t})\n\tt.Run(\"removes date tag including time\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"linux-20250101-2000.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"linux\")\n\t})\n\tt.Run(\"removes date tag including zero time\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"linux-20250101.0.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"linux\")\n\t})\n\tt.Run(\"replace arch with archlinux\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"arch-aarch64.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"archlinux-aarch64\")\n\t})\n\tt.Run(\"don't replace arch in the middle of the name\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"my-arch-aarch64.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"my-arch-aarch64\")\n\t})\n\tt.Run(\"removes native arch\", func(t *testing.T) {\n\t\tarch := limatype.NewArch(runtime.GOARCH)\n\t\timage := fmt.Sprintf(\"linux_cloudimg.base-%s.qcow2.gz\", arch)\n\t\tname := limatmpl.InstNameFromImageURL(image, arch)\n\t\tassert.Equal(t, name, \"linux\")\n\t})\n\tt.Run(\"removes redundant major version\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"rocky-8-8.10.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"rocky-8.10\")\n\t})\n\tt.Run(\"don't remove non-redundant major version\", func(t *testing.T) {\n\t\tname := limatmpl.InstNameFromImageURL(\"rocky-8-9.10.raw\", \"unknown\")\n\t\tassert.Equal(t, name, \"rocky-8-9.10\")\n\t})\n}\n\nfunc TestReadImageURLRespectsName(t *testing.T) {\n\timageURL := \"https://download.freebsd.org/releases/VM-IMAGES/15.0-RELEASE/aarch64/Latest/FreeBSD-15.0-RELEASE-arm64-aarch64-BASIC-CLOUDINIT-zfs.raw.xz\"\n\tt.Run(\"--name flag overrides image-derived name\", func(t *testing.T) {\n\t\ttmpl, err := limatmpl.Read(t.Context(), \"freebsd\", imageURL)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, tmpl.Name, \"freebsd\")\n\t})\n\tt.Run(\"name is derived from image URL when --name is empty\", func(t *testing.T) {\n\t\ttmpl, err := limatmpl.Read(t.Context(), \"\", imageURL)\n\t\tassert.NilError(t, err)\n\t\tassert.Assert(t, tmpl.Name != \"\")\n\t\tassert.Assert(t, tmpl.Name != \"freebsd\")\n\t})\n}\n\nfunc TestSeemsTemplateURL(t *testing.T) {\n\targ := \"template:foo/bar\"\n\tt.Run(arg, func(t *testing.T) {\n\t\tis, name := limatmpl.SeemsTemplateURL(arg)\n\t\tassert.Equal(t, is, true)\n\t\tassert.Equal(t, name, \"foo/bar\")\n\t})\n\tnotTemplateURLs := []string{\n\t\t\"file:///foo\",\n\t\t\"http://foo\",\n\t\t\"https://foo\",\n\t\t\"foo\",\n\t}\n\tfor _, arg := range notTemplateURLs {\n\t\tt.Run(arg, func(t *testing.T) {\n\t\t\tis, _ := limatmpl.SeemsTemplateURL(arg)\n\t\t\tassert.Equal(t, is, false)\n\t\t})\n\t}\n}\n\nfunc TestSeemsHTTPURL(t *testing.T) {\n\thttpURLs := []string{\n\t\t\"http://foo/\",\n\t\t\"https://foo/\",\n\t}\n\tfor _, arg := range httpURLs {\n\t\tt.Run(arg, func(t *testing.T) {\n\t\t\tassert.Equal(t, limatmpl.SeemsHTTPURL(arg), true)\n\t\t})\n\t}\n\tnotHTTPURLs := []string{\n\t\t\"file:///foo\",\n\t\t\"template:foo\",\n\t\t\"foo\",\n\t}\n\tfor _, arg := range notHTTPURLs {\n\t\tt.Run(arg, func(t *testing.T) {\n\t\t\tassert.Equal(t, limatmpl.SeemsHTTPURL(arg), false)\n\t\t})\n\t}\n}\n\nfunc TestSeemsFileURL(t *testing.T) {\n\targ := \"file:///foo\"\n\tt.Run(arg, func(t *testing.T) {\n\t\tassert.Equal(t, limatmpl.SeemsFileURL(arg), true)\n\t})\n\tnotFileURLs := []string{\n\t\t\"http://foo\",\n\t\t\"https://foo\",\n\t\t\"template:foo\",\n\t\t\"foo\",\n\t}\n\tfor _, arg := range notFileURLs {\n\t\tt.Run(arg, func(t *testing.T) {\n\t\t\tassert.Equal(t, limatmpl.SeemsFileURL(arg), false)\n\t\t})\n\t}\n}\n\n// TestRead tests that OS and architecture are correctly inferred from the image URL.\nfunc TestRead(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tlocator      string\n\t\texpectedName string\n\t\texpectedOS   limatype.OS\n\t\texpectedArch limatype.Arch\n\t}{\n\t\t{\n\t\t\tlocator:      \"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\",\n\t\t\texpectedName: \"freebsd-15.0-amd64-zfs\",\n\t\t\texpectedOS:   limatype.FREEBSD,\n\t\t\texpectedArch: limatype.X8664,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"https://download.freebsd.org/ftp/snapshots/VM-IMAGES/16.0-CURRENT/aarch64/Latest/FreeBSD-16.0-CURRENT-arm64-aarch64-BASIC-CLOUDINIT-ufs.qcow2.xz\",\n\t\t\texpectedName: \"freebsd-16.0-arm64-ufs\",\n\t\t\texpectedOS:   limatype.FREEBSD,\n\t\t\texpectedArch: limatype.AARCH64,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"https://updates.cdn-apple.com/2025SummerFCS/fullrestores/093-10809/CFD6DD38-DAF0-40DA-854F-31AAD1294C6F/UniversalMac_15.6.1_24G90_Restore.ipsw\",\n\t\t\texpectedName: \"macos-15.6.1\",\n\t\t\texpectedOS:   limatype.DARWIN,\n\t\t\texpectedArch: limatype.AARCH64,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-60229/6D5DBEA5-75A0-4BEF-ACC9-5ACF9B8DF6B7/UniversalMac_26.3_25D125_Restore.ipsw\",\n\t\t\texpectedName: \"macos-26.3\",\n\t\t\texpectedOS:   limatype.DARWIN,\n\t\t\texpectedArch: limatype.AARCH64,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"file:///somewhere/macOS-26.ipsw\",\n\t\t\texpectedName: \"macos-26\",\n\t\t\texpectedOS:   limatype.DARWIN,\n\t\t\texpectedArch: limatype.AARCH64,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"file:///somewhere/my-custom-macos.img\",\n\t\t\texpectedName: \"my-custom-macos\",\n\t\t\texpectedOS:   limatype.DARWIN,\n\t\t\texpectedArch: limatype.AARCH64,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"file:///somewhere/something.ipsw\",\n\t\t\texpectedName: \"something\",\n\t\t\texpectedOS:   limatype.DARWIN,\n\t\t\texpectedArch: limatype.AARCH64,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"https://cloud-images.ubuntu.com/releases/noble/release-20260209/ubuntu-24.04-server-cloudimg-amd64.img\",\n\t\t\texpectedName: \"ubuntu-24.04-amd64\",\n\t\t\texpectedOS:   limatype.LINUX,\n\t\t\texpectedArch: limatype.X8664,\n\t\t},\n\t\t{\n\t\t\tlocator:      \"https://cloud-images.ubuntu.com/releases/noble/release-20260209/ubuntu-24.04-server-cloudimg-arm64.img\",\n\t\t\texpectedName: \"ubuntu-24.04-arm64\",\n\t\t\texpectedOS:   limatype.LINUX,\n\t\t\texpectedArch: limatype.AARCH64,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.locator, func(t *testing.T) {\n\t\t\ttmpl, err := limatmpl.Read(t.Context(), \"\", tt.locator)\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, tmpl.Name, tt.expectedName)\n\t\t\terr = tmpl.Unmarshal()\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Assert(t, tmpl.Config.OS != nil, \"os must be set\")\n\t\t\tassert.Equal(t, *tmpl.Config.OS, tt.expectedOS)\n\t\t\tassert.Assert(t, tmpl.Config.Arch != nil, \"arch must be set\")\n\t\t\tassert.Equal(t, *tmpl.Config.Arch, tt.expectedArch)\n\t\t\tassert.Assert(t, len(tmpl.Config.Images) == 1, \"expected exactly one image entry\")\n\t\t\tassert.Equal(t, tmpl.Config.Images[0].File.Location, tt.locator)\n\t\t\tassert.Equal(t, tmpl.Config.Images[0].File.Arch, tt.expectedArch)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/limatmpl/template.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatmpl\n\nimport (\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n)\n\ntype Template struct {\n\tLocator string // template locator (absolute path or URL)\n\tBytes   []byte // file contents\n\n\t// The following fields are only used when the template represents a YAML config file.\n\tName   string // instance name, may be inferred from locator\n\tConfig *limatype.LimaYAML\n\n\texpr strings.Builder // yq expression to update template\n}\n\nfunc (tmpl *Template) ClearOnError(err error) error {\n\tif err != nil {\n\t\ttmpl.Bytes = nil\n\t\ttmpl.Config = nil\n\t\ttmpl.expr.Reset()\n\t}\n\treturn err\n}\n\n// Unmarshal makes sure the tmpl.Config field is set. Any operation that modified\n// tmpl.Bytes is expected to set tmpl.Config back to nil.\nfunc (tmpl *Template) Unmarshal() error {\n\tif tmpl.Config == nil {\n\t\ttmpl.Config = &limatype.LimaYAML{}\n\t\tif err := limayaml.Unmarshal(tmpl.Bytes, tmpl.Config, tmpl.Locator); err != nil {\n\t\t\ttmpl.Config = nil\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/limatype/dirnames/dirnames.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage dirnames\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/identifiers\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\n// DotLima is a directory that appears under the home directory.\nconst DotLima = \".lima\"\n\n// LimaDir returns the absolute path of `~/.lima` (or $LIMA_HOME, if set).\n//\n// NOTE: We do not use `~/Library/Application Support/Lima` on macOS.\n// We use `~/.lima` so that we can have enough space for the length of the socket path,\n// which can be only 104 characters on macOS.\nfunc LimaDir() (string, error) {\n\tdir := os.Getenv(\"LIMA_HOME\")\n\tif dir == \"\" {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tdir = filepath.Join(homeDir, DotLima)\n\t}\n\tif _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {\n\t\treturn dir, nil\n\t}\n\trealdir, err := filepath.EvalSymlinks(dir)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"cannot evaluate symlinks in %q: %w\", dir, err)\n\t}\n\treturn realdir, nil\n}\n\n// LimaConfigDir returns the path of the config directory, $LIMA_HOME/_config.\nfunc LimaConfigDir() (string, error) {\n\tlimaDir, err := LimaDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(limaDir, filenames.ConfigDir), nil\n}\n\n// LimaNetworksDir returns the path of the networks log directory, $LIMA_HOME/_networks.\nfunc LimaNetworksDir() (string, error) {\n\tlimaDir, err := LimaDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(limaDir, filenames.NetworksDir), nil\n}\n\n// LimaDisksDir returns the path of the disks directory, $LIMA_HOME/_disks.\nfunc LimaDisksDir() (string, error) {\n\tlimaDir, err := LimaDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(limaDir, filenames.DisksDir), nil\n}\n\n// LimaTemplatesDir returns the path of the templates directory, $LIMA_HOME/_templates.\nfunc LimaTemplatesDir() (string, error) {\n\tlimaDir, err := LimaDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(limaDir, filenames.TemplatesDir), nil\n}\n\n// InstanceDir returns the instance dir.\n// InstanceDir does not check whether the instance exists.\nfunc InstanceDir(name string) (string, error) {\n\tif err := ValidateInstName(name); err != nil {\n\t\treturn \"\", err\n\t}\n\tlimaDir, err := LimaDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdir := filepath.Join(limaDir, name)\n\treturn dir, nil\n}\n\n// ValidateInstName checks if the name is a valid instance name. For this it needs to\n// be a valid identifier, and not end in .yml or .yaml (case insensitively).\nfunc ValidateInstName(name string) error {\n\tif err := identifiers.Validate(name); err != nil {\n\t\treturn fmt.Errorf(\"instance name %q is not a valid identifier: %w\", name, err)\n\t}\n\tlower := strings.ToLower(name)\n\tif strings.HasSuffix(lower, \".yml\") || strings.HasSuffix(lower, \".yaml\") {\n\t\treturn fmt.Errorf(\"instance name %q must not end with .yml or .yaml suffix\", name)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/limatype/filenames/filenames.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package filenames defines the names of the files that appear under an instance dir\n// or inside the config directory.\n//\n// See https://lima-vm.io/docs/dev/internals/\npackage filenames\n\n// Instance names starting with an underscore are reserved for lima internal usage\n\nconst (\n\tCacheDir     = \"_cache\" // not yet implemented\n\tConfigDir    = \"_config\"\n\tDisksDir     = \"_disks\"     // disks are stored here\n\tNetworksDir  = \"_networks\"  // network log files are stored here\n\tTemplatesDir = \"_templates\" // user templates are stored here\n)\n\n// Filenames used inside the ConfigDir\n\nconst (\n\tUserPrivateKey = \"user\"\n\tUserPublicKey  = UserPrivateKey + \".pub\"\n\tNetworksConfig = \"networks.yaml\"\n\tDefault        = \"default.yaml\"\n\tOverride       = \"override.yaml\"\n\tBase           = \"base.yaml\"\n)\n\n// Filenames that may appear under an instance directory\n\nconst (\n\tLimaYAML                = \"lima.yaml\"\n\tLimaVersion             = \"lima-version\" // Lima version used to create instance\n\tCIDataISO               = \"cidata.iso\"\n\tCIDataISODir            = \"cidata\"\n\tCloudConfig             = \"cloud-config.yaml\"\n\tImage                   = \"image\"      // downloaded VM image; renamed to Disk or ISO during setup\n\tImageIPSW               = \"image.ipsw\" // hardlink to Image for macOS guests\n\tDisk                    = \"disk\"       // VM disk (or symlink to DiffDiskLegacy for migrated instances)\n\tISO                     = \"iso\"        // optional CDROM image (or symlink to BaseDiskLegacy for migrated instances)\n\tBaseDiskLegacy          = \"basedisk\"   // legacy name for Image; may remain as qcow2 backing file\n\tDiffDiskLegacy          = \"diffdisk\"   // legacy name for Disk\n\tKernel                  = \"kernel\"\n\tKernelCmdline           = \"kernel.cmdline\"\n\tInitrd                  = \"initrd\"\n\tQMPSock                 = \"qmp.sock\"\n\tSerialLog               = \"serial.log\" // default serial (ttyS0, but ttyAMA0 on qemu-system-{arm,aarch64})\n\tSerialSock              = \"serial.sock\"\n\tSerialPCILog            = \"serialp.log\" // pci serial (ttyS0 on qemu-system-{arm,aarch64})\n\tSerialPCISock           = \"serialp.sock\"\n\tSerialVirtioLog         = \"serialv.log\" // virtio serial\n\tSerialVirtioSock        = \"serialv.sock\"\n\tSSHSock                 = \"ssh.sock\"\n\tSSHConfig               = \"ssh.config\"\n\tVhostSock               = \"virtiofsd-%d.sock\"\n\tVNCDisplayFile          = \"vncdisplay\"\n\tVNCPasswordFile         = \"vncpassword\"\n\tGuestAgentSock          = \"ga.sock\"\n\tVirtioPort              = \"io.lima-vm.guest_agent.0\"\n\tHostAgentPID            = \"ha.pid\"\n\tHostAgentSock           = \"ha.sock\"\n\tHostAgentStdoutLog      = \"ha.stdout.log\"\n\tHostAgentStderrLog      = \"ha.stderr.log\"\n\tExternalDriverStderrLog = \"driver.stderr.log\"\n\tVzIdentifier            = \"vz-identifier\"\n\tVzHwModel               = \"vz-hwmodel\"       // macOS guests only\n\tVzAux                   = \"vz-aux\"           // macOS guests only\n\tVzEfi                   = \"vz-efi\"           // efi variable store\n\tQemuEfiCodeFD           = \"qemu-efi-code.fd\" // efi code; not always created\n\tQemuEfiFullFD           = \"qemu-efi-full.fd\" // concatenated efi vars and code; not always created\n\tAnsibleInventoryYAML    = \"ansible-inventory.yaml\"\n\n\t// SocketDir is the default location for forwarded sockets with a relative paths in HostSocket.\n\tSocketDir = \"sock\"\n\tMntDir    = \"mnt\" // mount point (macOS guests only)\n\n\tProtected = \"protected\" // empty file; used by `limactl protect`\n)\n\n// Filenames used under a disk directory\n\nconst (\n\tDataDisk = \"datadisk\"\n\tInUseBy  = \"in_use_by\"\n)\n\n// LongestSock is the longest socket name.\n// On macOS, the full path of the socket (excluding the NUL terminator) must be less than 104 characters.\n// See unix(4).\n//\n// On Linux, the full path must be less than 108 characters.\n//\n// ssh appends 16 bytes of random characters when it first creates the socket:\n// https://github.com/openssh/openssh-portable/blob/V_8_7_P1/mux.c#L1271-L1285\nconst LongestSock = SSHSock + \".1234567890123456\"\n\nfunc PIDFile(name string) string {\n\treturn name + \".pid\"\n}\n\n// SkipOnClone files should be skipped on cloning an instance.\nvar SkipOnClone = []string{\n\tProtected,\n}\n\n// NullifyOnClone files should be nullified on cloning an instance.\n// FIXME: this list should be provided by the VM driver.\nvar NullifyOnClone = []string{\n\tVzIdentifier,\n}\n\n// TmpFileSuffixes is the list of the tmp file suffixes.\nvar TmpFileSuffixes = []string{\".pid\", \".sock\", \".tmp\"}\n"
  },
  {
    "path": "pkg/limatype/lima_instance.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatype\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\ntype Status = string\n\nconst (\n\tStatusUnknown       Status = \"\"\n\tStatusUninitialized Status = \"Uninitialized\"\n\tStatusInstalling    Status = \"Installing\"\n\tStatusBroken        Status = \"Broken\"\n\tStatusStopped       Status = \"Stopped\"\n\tStatusRunning       Status = \"Running\"\n)\n\ntype Instance struct {\n\tName string `json:\"name\"`\n\t// Hostname, not HostName (corresponds to SSH's naming convention)\n\tHostname              string            `json:\"hostname\"`\n\tStatus                Status            `json:\"status\"`\n\tDir                   string            `json:\"dir\"`\n\tVMType                VMType            `json:\"vmType\"`\n\tArch                  Arch              `json:\"arch\"`\n\tCPUs                  int               `json:\"cpus,omitempty\"`\n\tMemory                int64             `json:\"memory,omitempty\"` // bytes\n\tDisk                  int64             `json:\"disk,omitempty\"`   // bytes\n\tMessage               string            `json:\"message,omitempty\"`\n\tAdditionalDisks       []Disk            `json:\"additionalDisks,omitempty\"`\n\tNetworks              []Network         `json:\"network,omitempty\"`\n\tSSHLocalPort          int               `json:\"sshLocalPort,omitempty\"`\n\tSSHConfigFile         string            `json:\"sshConfigFile,omitempty\"`\n\tHostAgentPID          int               `json:\"hostAgentPID,omitempty\"`\n\tDriverPID             int               `json:\"driverPID,omitempty\"`\n\tErrors                []error           `json:\"errors,omitempty\"`\n\tConfig                *LimaYAML         `json:\"config,omitempty\"`\n\tSSHAddress            string            `json:\"sshAddress,omitempty\"`\n\tProtected             bool              `json:\"protected\"`\n\tLimaVersion           string            `json:\"limaVersion\"`\n\tParam                 map[string]string `json:\"param,omitempty\"`\n\tAutoStartedIdentifier string            `json:\"autoStartedIdentifier,omitempty\"`\n}\n\n// Protect protects the instance to prohibit accidental removal.\n// Protect does not return an error even when the instance is already protected.\nfunc (inst *Instance) Protect() error {\n\tprotected := filepath.Join(inst.Dir, filenames.Protected)\n\t// TODO: Do an equivalent of `chmod +a \"everyone deny delete,delete_child,file_inherit,directory_inherit\"`\n\t// https://github.com/lima-vm/lima/issues/1595\n\tif err := os.WriteFile(protected, nil, 0o400); err != nil {\n\t\treturn err\n\t}\n\tinst.Protected = true\n\treturn nil\n}\n\n// Unprotect unprotects the instance.\n// Unprotect does not return an error even when the instance is already unprotected.\nfunc (inst *Instance) Unprotect() error {\n\tprotected := filepath.Join(inst.Dir, filenames.Protected)\n\tif err := os.RemoveAll(protected); err != nil {\n\t\treturn err\n\t}\n\tinst.Protected = false\n\treturn nil\n}\n\nfunc (inst *Instance) MarshalJSON() ([]byte, error) {\n\ttype Alias Instance\n\terrorsAsStrings := make([]string, len(inst.Errors))\n\tfor i, err := range inst.Errors {\n\t\tif err != nil {\n\t\t\terrorsAsStrings[i] = err.Error()\n\t\t}\n\t}\n\treturn json.Marshal(&struct {\n\t\t*Alias\n\t\tErrors []string `json:\"errors,omitempty\"`\n\t}{\n\t\tAlias:  (*Alias)(inst),\n\t\tErrors: errorsAsStrings,\n\t})\n}\n\nfunc (inst *Instance) UnmarshalJSON(data []byte) error {\n\ttype Alias Instance\n\taux := &struct {\n\t\t*Alias\n\t\tErrors []string `json:\"errors,omitempty\"`\n\t}{\n\t\tAlias: (*Alias)(inst),\n\t}\n\tif err := json.Unmarshal(data, &aux); err != nil {\n\t\treturn err\n\t}\n\tinst.Errors = nil\n\tfor _, msg := range aux.Errors {\n\t\tinst.Errors = append(inst.Errors, errors.New(msg))\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/limatype/lima_yaml.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limatype\n\nimport (\n\t\"net\"\n\t\"runtime\"\n\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"github.com/opencontainers/go-digest\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/cpu\"\n)\n\ntype LimaYAML struct {\n\tBase               BaseTemplates `yaml:\"base,omitempty\" json:\"base,omitempty\"`\n\tMinimumLimaVersion *string       `yaml:\"minimumLimaVersion,omitempty\" json:\"minimumLimaVersion,omitempty\" jsonschema:\"nullable\"`\n\tVMType             *VMType       `yaml:\"vmType,omitempty\" json:\"vmType,omitempty\" jsonschema:\"nullable\"`\n\tVMOpts             VMOpts        `yaml:\"vmOpts,omitempty\" json:\"vmOpts,omitempty\"`\n\tOS                 *OS           `yaml:\"os,omitempty\" json:\"os,omitempty\" jsonschema:\"nullable\"`\n\tArch               *Arch         `yaml:\"arch,omitempty\" json:\"arch,omitempty\" jsonschema:\"nullable\"`\n\tImages             []Image       `yaml:\"images,omitempty\" json:\"images,omitempty\" jsonschema:\"nullable\"`\n\t// Deprecated: Use vmOpts.qemu.cpuType instead.\n\tCPUType               CPUType       `yaml:\"cpuType,omitempty\" json:\"cpuType,omitempty\" jsonschema:\"nullable\"`\n\tCPUs                  *int          `yaml:\"cpus,omitempty\" json:\"cpus,omitempty\" jsonschema:\"nullable\"`\n\tMemory                *string       `yaml:\"memory,omitempty\" json:\"memory,omitempty\" jsonschema:\"nullable\"` // go-units.RAMInBytes\n\tDisk                  *string       `yaml:\"disk,omitempty\" json:\"disk,omitempty\" jsonschema:\"nullable\"`     // go-units.RAMInBytes\n\tAdditionalDisks       []Disk        `yaml:\"additionalDisks,omitempty\" json:\"additionalDisks,omitempty\" jsonschema:\"nullable\"`\n\tMounts                []Mount       `yaml:\"mounts,omitempty\" json:\"mounts,omitempty\"`\n\tMountTypesUnsupported []string      `yaml:\"mountTypesUnsupported,omitempty\" json:\"mountTypesUnsupported,omitempty\" jsonschema:\"nullable\"`\n\tMountType             *MountType    `yaml:\"mountType,omitempty\" json:\"mountType,omitempty\" jsonschema:\"nullable\"`\n\tMountInotify          *bool         `yaml:\"mountInotify,omitempty\" json:\"mountInotify,omitempty\" jsonschema:\"nullable\"`\n\tSSH                   SSH           `yaml:\"ssh,omitempty\" json:\"ssh,omitempty\"` // REQUIRED (FIXME)\n\tFirmware              Firmware      `yaml:\"firmware,omitempty\" json:\"firmware,omitempty\"`\n\tAudio                 Audio         `yaml:\"audio,omitempty\" json:\"audio,omitempty\"`\n\tVideo                 Video         `yaml:\"video,omitempty\" json:\"video,omitempty\"`\n\tProvision             []Provision   `yaml:\"provision,omitempty\" json:\"provision,omitempty\"`\n\tUpgradePackages       *bool         `yaml:\"upgradePackages,omitempty\" json:\"upgradePackages,omitempty\" jsonschema:\"nullable\"`\n\tContainerd            Containerd    `yaml:\"containerd,omitempty\" json:\"containerd,omitempty\"`\n\tGuestInstallPrefix    *string       `yaml:\"guestInstallPrefix,omitempty\" json:\"guestInstallPrefix,omitempty\" jsonschema:\"nullable\"`\n\tProbes                []Probe       `yaml:\"probes,omitempty\" json:\"probes,omitempty\"`\n\tPortForwards          []PortForward `yaml:\"portForwards,omitempty\" json:\"portForwards,omitempty\"`\n\tCopyToHost            []CopyToHost  `yaml:\"copyToHost,omitempty\" json:\"copyToHost,omitempty\"`\n\tMessage               string        `yaml:\"message,omitempty\" json:\"message,omitempty\"`\n\tNetworks              []Network     `yaml:\"networks,omitempty\" json:\"networks,omitempty\" jsonschema:\"nullable\"`\n\t// `network` was deprecated in Lima v0.7.0, removed in Lima v0.14.0. Use `networks` instead.\n\tEnv          map[string]string `yaml:\"env,omitempty\" json:\"env,omitempty\"`\n\tParam        map[string]string `yaml:\"param,omitempty\" json:\"param,omitempty\"`\n\tDNS          []net.IP          `yaml:\"dns,omitempty\" json:\"dns,omitempty\"`\n\tHostResolver HostResolver      `yaml:\"hostResolver,omitempty\" json:\"hostResolver,omitempty\"`\n\t// `useHostResolver` was deprecated in Lima v0.8.1, removed in Lima v0.14.0. Use `hostResolver.enabled` instead.\n\tPropagateProxyEnv *bool          `yaml:\"propagateProxyEnv,omitempty\" json:\"propagateProxyEnv,omitempty\" jsonschema:\"nullable\"`\n\tCACertificates    CACertificates `yaml:\"caCerts,omitempty\" json:\"caCerts,omitempty\"`\n\t// Deprecated: Use vmOpts.vz.rosetta instead.\n\tRosetta              Rosetta `yaml:\"rosetta,omitempty\" json:\"rosetta,omitempty\"`\n\tPlain                *bool   `yaml:\"plain,omitempty\" json:\"plain,omitempty\" jsonschema:\"nullable\"`\n\tTimeZone             *string `yaml:\"timezone,omitempty\" json:\"timezone,omitempty\" jsonschema:\"nullable\"`\n\tNestedVirtualization *bool   `yaml:\"nestedVirtualization,omitempty\" json:\"nestedVirtualization,omitempty\" jsonschema:\"nullable\"`\n\tUser                 User    `yaml:\"user,omitempty\" json:\"user,omitempty\"`\n}\n\ntype BaseTemplates []LocatorWithDigest\n\ntype LocatorWithDigest struct {\n\tURL    string  `yaml:\"url\" json:\"url\"`\n\tDigest *string `yaml:\"digest,omitempty\" json:\"digest,omitempty\"` // TODO currently unused\n}\n\ntype (\n\tOS        = string\n\tArch      = string\n\tMountType = string\n\tVMType    = string\n)\n\ntype CPUType = map[Arch]string\n\nconst (\n\tLINUX   OS = \"Linux\"\n\tDARWIN  OS = \"Darwin\"\n\tFREEBSD OS = \"FreeBSD\"\n\n\tX8664   Arch = \"x86_64\"\n\tAARCH64 Arch = \"aarch64\"\n\tARMV7L  Arch = \"armv7l\"\n\tPPC64LE Arch = \"ppc64le\"\n\tRISCV64 Arch = \"riscv64\"\n\tS390X   Arch = \"s390x\"\n\n\tREVSSHFS MountType = \"reverse-sshfs\"\n\tNINEP    MountType = \"9p\"\n\tVIRTIOFS MountType = \"virtiofs\"\n\tWSLMount MountType = \"wsl2\"\n\n\tQEMU VMType = \"qemu\"\n\tVZ   VMType = \"vz\"\n\tWSL2 VMType = \"wsl2\"\n)\n\nvar (\n\tOSTypes    = []OS{LINUX, DARWIN, FREEBSD}\n\tArchTypes  = []Arch{X8664, AARCH64, ARMV7L, PPC64LE, RISCV64, S390X}\n\tMountTypes = []MountType{REVSSHFS, NINEP, VIRTIOFS, WSLMount}\n\tVMTypes    = []VMType{QEMU, VZ, WSL2}\n)\n\ntype User struct {\n\tName    *string `yaml:\"name,omitempty\" json:\"name,omitempty\" jsonschema:\"nullable\"`\n\tComment *string `yaml:\"comment,omitempty\" json:\"comment,omitempty\" jsonschema:\"nullable\"`\n\tHome    *string `yaml:\"home,omitempty\" json:\"home,omitempty\" jsonschema:\"nullable\"`\n\tShell   *string `yaml:\"shell,omitempty\" json:\"shell,omitempty\" jsonschema:\"nullable\"`\n\tUID     *uint32 `yaml:\"uid,omitempty\" json:\"uid,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype VMOpts map[VMType]any\n\ntype QEMUOpts struct {\n\tMinimumVersion *string `yaml:\"minimumVersion,omitempty\" json:\"minimumVersion,omitempty\" jsonschema:\"nullable\"`\n\tCPUType        CPUType `yaml:\"cpuType,omitempty\" json:\"cpuType,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype VZOpts struct {\n\tRosetta         Rosetta     `yaml:\"rosetta,omitempty\" json:\"rosetta,omitempty\"`\n\tDiskImageFormat *image.Type `yaml:\"diskImageFormat,omitempty\" json:\"diskImageFormat,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype Rosetta struct {\n\tEnabled *bool `yaml:\"enabled,omitempty\" json:\"enabled,omitempty\" jsonschema:\"nullable\"`\n\tBinFmt  *bool `yaml:\"binfmt,omitempty\" json:\"binfmt,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype File struct {\n\tLocation string        `yaml:\"location\" json:\"location\"` // REQUIRED\n\tArch     Arch          `yaml:\"arch,omitempty\" json:\"arch,omitempty\"`\n\tDigest   digest.Digest `yaml:\"digest,omitempty\" json:\"digest,omitempty\"`\n}\n\ntype FileWithVMType struct {\n\tFile   `yaml:\",inline\"`\n\tVMType VMType `yaml:\"vmType,omitempty\" json:\"vmType,omitempty\"`\n}\n\ntype Kernel struct {\n\tFile    `yaml:\",inline\"`\n\tCmdline string `yaml:\"cmdline,omitempty\" json:\"cmdline,omitempty\"`\n}\n\ntype Image struct {\n\tFile   `yaml:\",inline\"`\n\tKernel *Kernel `yaml:\"kernel,omitempty\" json:\"kernel,omitempty\"`\n\tInitrd *File   `yaml:\"initrd,omitempty\" json:\"initrd,omitempty\"`\n}\n\ntype Disk struct {\n\tName   string   `yaml:\"name\" json:\"name\"` // REQUIRED\n\tFormat *bool    `yaml:\"format,omitempty\" json:\"format,omitempty\"`\n\tFSType *string  `yaml:\"fsType,omitempty\" json:\"fsType,omitempty\"`\n\tFSArgs []string `yaml:\"fsArgs,omitempty\" json:\"fsArgs,omitempty\"`\n}\n\ntype Mount struct {\n\tLocation   string   `yaml:\"location\" json:\"location\"` // REQUIRED\n\tMountPoint *string  `yaml:\"mountPoint,omitempty\" json:\"mountPoint,omitempty\" jsonschema:\"nullable\"`\n\tWritable   *bool    `yaml:\"writable,omitempty\" json:\"writable,omitempty\" jsonschema:\"nullable\"`\n\tSSHFS      SSHFS    `yaml:\"sshfs,omitempty\" json:\"sshfs,omitempty\"`\n\tNineP      NineP    `yaml:\"9p,omitempty\" json:\"9p,omitempty\"`\n\tVirtiofs   Virtiofs `yaml:\"virtiofs,omitempty\" json:\"virtiofs,omitempty\"`\n}\n\ntype SFTPDriver = string\n\nconst (\n\tSFTPDriverBuiltin           = \"builtin\"\n\tSFTPDriverOpenSSHSFTPServer = \"openssh-sftp-server\"\n)\n\ntype SSHFS struct {\n\tCache          *bool       `yaml:\"cache,omitempty\" json:\"cache,omitempty\" jsonschema:\"nullable\"`\n\tFollowSymlinks *bool       `yaml:\"followSymlinks,omitempty\" json:\"followSymlinks,omitempty\" jsonschema:\"nullable\"`\n\tSFTPDriver     *SFTPDriver `yaml:\"sftpDriver,omitempty\" json:\"sftpDriver,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype NineP struct {\n\tSecurityModel   *string `yaml:\"securityModel,omitempty\" json:\"securityModel,omitempty\" jsonschema:\"nullable\"`\n\tProtocolVersion *string `yaml:\"protocolVersion,omitempty\" json:\"protocolVersion,omitempty\" jsonschema:\"nullable\"`\n\tMsize           *string `yaml:\"msize,omitempty\" json:\"msize,omitempty\" jsonschema:\"nullable\"`\n\tCache           *string `yaml:\"cache,omitempty\" json:\"cache,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype Virtiofs struct {\n\tQueueSize *int `yaml:\"queueSize,omitempty\" json:\"queueSize,omitempty\"`\n}\n\ntype SSH struct {\n\tLocalPort *int `yaml:\"localPort,omitempty\" json:\"localPort,omitempty\" jsonschema:\"nullable\"`\n\n\t// LoadDotSSHPubKeys loads ~/.ssh/*.pub in addition to $LIMA_HOME/_config/user.pub .\n\tLoadDotSSHPubKeys *bool `yaml:\"loadDotSSHPubKeys,omitempty\" json:\"loadDotSSHPubKeys,omitempty\" jsonschema:\"nullable\"` // default: false\n\tForwardAgent      *bool `yaml:\"forwardAgent,omitempty\" json:\"forwardAgent,omitempty\" jsonschema:\"nullable\"`           // default: false\n\tForwardX11        *bool `yaml:\"forwardX11,omitempty\" json:\"forwardX11,omitempty\" jsonschema:\"nullable\"`               // default: false\n\tForwardX11Trusted *bool `yaml:\"forwardX11Trusted,omitempty\" json:\"forwardX11Trusted,omitempty\" jsonschema:\"nullable\"` // default: false\n\n\tOverVsock *bool `yaml:\"overVsock,omitempty\" json:\"overVsock,omitempty\" jsonschema:\"nullable\"` // default: depends on VMType\n}\n\ntype Firmware struct {\n\t// LegacyBIOS disables UEFI if set.\n\t// LegacyBIOS is ignored for aarch64.\n\tLegacyBIOS *bool `yaml:\"legacyBIOS,omitempty\" json:\"legacyBIOS,omitempty\" jsonschema:\"nullable\"`\n\n\t// Images specify UEFI images (edk2-aarch64-code.fd.gz).\n\t// Defaults to built-in UEFI.\n\tImages []FileWithVMType `yaml:\"images,omitempty\" json:\"images,omitempty\"`\n}\n\ntype Audio struct {\n\t// Device is a QEMU audiodev string\n\tDevice *string `yaml:\"device,omitempty\" json:\"device,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype VNCOptions struct {\n\tDisplay *string `yaml:\"display,omitempty\" json:\"display,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype Video struct {\n\t// Display is a QEMU display string\n\tDisplay *string    `yaml:\"display,omitempty\" json:\"display,omitempty\" jsonschema:\"nullable\"`\n\tVNC     VNCOptions `yaml:\"vnc,omitempty\" json:\"vnc,omitempty\"`\n}\n\ntype ProvisionMode = string\n\nconst (\n\tProvisionModeSystem     ProvisionMode = \"system\"\n\tProvisionModeUser       ProvisionMode = \"user\"\n\tProvisionModeBoot       ProvisionMode = \"boot\"\n\tProvisionModeDependency ProvisionMode = \"dependency\"\n\tProvisionModeAnsible    ProvisionMode = \"ansible\" // DEPRECATED\n\tProvisionModeData       ProvisionMode = \"data\"\n\tProvisionModeYQ         ProvisionMode = \"yq\"\n)\n\ntype Provision struct {\n\tMode                            ProvisionMode      `yaml:\"mode,omitempty\" json:\"mode,omitempty\" jsonschema:\"default=system\"`\n\tSkipDefaultDependencyResolution *bool              `yaml:\"skipDefaultDependencyResolution,omitempty\" json:\"skipDefaultDependencyResolution,omitempty\"`\n\tScript                          *string            `yaml:\"script,omitempty\" json:\"script,omitempty\"`\n\tFile                            *LocatorWithDigest `yaml:\"file,omitempty\" json:\"file,omitempty\" jsonschema:\"nullable\"`\n\tPlaybook                        string             `yaml:\"playbook,omitempty\" json:\"playbook,omitempty\"` // DEPRECATED\n\t// All ProvisionData fields must be nil unless Mode is ProvisionModeData\n\tProvisionData `yaml:\",inline\"` // Flatten fields for \"strict\" YAML mode\n\t// ProvisionModeYQ borrows Owner, Path, and Permissions from ProvisionData\n\tExpression *string `yaml:\"expression,omitempty\" json:\"expression,omitempty\" jsonschema:\"nullable\"`\n\tFormat     *string `yaml:\"format,omitempty\" json:\"format,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype ProvisionData struct {\n\tContent     *string `yaml:\"content,omitempty\" json:\"content,omitempty\" jsonschema:\"nullable\"`\n\tOverwrite   *bool   `yaml:\"overwrite,omitempty\" json:\"overwrite,omitempty\" jsonschema:\"nullable\"`\n\tOwner       *string `yaml:\"owner,omitempty\" json:\"owner,omitempty\"` // any owner string supported by `chown`, defaults to \"root:root\" on Linux\n\tPath        *string `yaml:\"path,omitempty\" json:\"path,omitempty\"`\n\tPermissions *string `yaml:\"permissions,omitempty\" json:\"permissions,omitempty\"`\n}\n\ntype Containerd struct {\n\tSystem   *bool  `yaml:\"system,omitempty\" json:\"system,omitempty\" jsonschema:\"nullable\"` // default: false\n\tUser     *bool  `yaml:\"user,omitempty\" json:\"user,omitempty\" jsonschema:\"nullable\"`     // default: true\n\tArchives []File `yaml:\"archives,omitempty\" json:\"archives,omitempty\"`                   // default: see defaultContainerdArchives\n}\n\ntype ProbeMode = string\n\nconst (\n\tProbeModeReadiness ProbeMode = \"readiness\"\n)\n\ntype Probe struct {\n\tMode        ProbeMode          `yaml:\"mode,omitempty\" json:\"mode,omitempty\" jsonschema:\"default=readiness\"`\n\tDescription string             `yaml:\"description,omitempty\" json:\"description,omitempty\"`\n\tScript      *string            `yaml:\"script,omitempty\" json:\"script,omitempty\"`\n\tFile        *LocatorWithDigest `yaml:\"file,omitempty\" json:\"file,omitempty\" jsonschema:\"nullable\"`\n\tHint        string             `yaml:\"hint,omitempty\" json:\"hint,omitempty\"`\n}\n\ntype Proto = string\n\nconst (\n\tProtoTCP Proto = \"tcp\"\n\tProtoUDP Proto = \"udp\"\n\tProtoAny Proto = \"any\"\n)\n\ntype PortForward struct {\n\tGuestIPMustBeZero *bool  `yaml:\"guestIPMustBeZero,omitempty\" json:\"guestIPMustBeZero,omitempty\"`\n\tGuestIP           net.IP `yaml:\"guestIP,omitempty\" json:\"guestIP,omitempty\"`\n\tGuestPort         int    `yaml:\"guestPort,omitempty\" json:\"guestPort,omitempty\"`\n\tGuestPortRange    [2]int `yaml:\"guestPortRange,omitempty\" json:\"guestPortRange,omitempty\"`\n\tGuestSocket       string `yaml:\"guestSocket,omitempty\" json:\"guestSocket,omitempty\"`\n\tHostIP            net.IP `yaml:\"hostIP,omitempty\" json:\"hostIP,omitempty\"`\n\tHostPort          int    `yaml:\"hostPort,omitempty\" json:\"hostPort,omitempty\"`\n\tHostPortRange     [2]int `yaml:\"hostPortRange,omitempty\" json:\"hostPortRange,omitempty\"`\n\tHostSocket        string `yaml:\"hostSocket,omitempty\" json:\"hostSocket,omitempty\"`\n\tProto             Proto  `yaml:\"proto,omitempty\" json:\"proto,omitempty\"`\n\tReverse           bool   `yaml:\"reverse,omitempty\" json:\"reverse,omitempty\"`\n\tIgnore            bool   `yaml:\"ignore,omitempty\" json:\"ignore,omitempty\"`\n\tStatic            bool   `yaml:\"static,omitempty\" json:\"static,omitempty\"`\n}\n\ntype CopyToHost struct {\n\tGuestFile    string `yaml:\"guest,omitempty\" json:\"guest,omitempty\"`\n\tHostFile     string `yaml:\"host,omitempty\" json:\"host,omitempty\"`\n\tDeleteOnStop bool   `yaml:\"deleteOnStop,omitempty\" json:\"deleteOnStop,omitempty\"`\n}\n\ntype Network struct {\n\t// `Lima` and `Socket` are mutually exclusive; exactly one is required\n\tLima string `yaml:\"lima,omitempty\" json:\"lima,omitempty\"`\n\t// Socket is a QEMU-compatible socket\n\tSocket string `yaml:\"socket,omitempty\" json:\"socket,omitempty\"`\n\t// VZNAT uses VZNATNetworkDeviceAttachment. Needs VZ. No root privilege is required.\n\tVZNAT *bool `yaml:\"vzNAT,omitempty\" json:\"vzNAT,omitempty\"`\n\n\tMACAddress string  `yaml:\"macAddress,omitempty\" json:\"macAddress,omitempty\"`\n\tInterface  string  `yaml:\"interface,omitempty\" json:\"interface,omitempty\"`\n\tMetric     *uint32 `yaml:\"metric,omitempty\" json:\"metric,omitempty\"`\n}\n\ntype HostResolver struct {\n\tEnabled *bool             `yaml:\"enabled,omitempty\" json:\"enabled,omitempty\" jsonschema:\"nullable\"`\n\tIPv6    *bool             `yaml:\"ipv6,omitempty\" json:\"ipv6,omitempty\" jsonschema:\"nullable\"`\n\tHosts   map[string]string `yaml:\"hosts,omitempty\" json:\"hosts,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype CACertificates struct {\n\tRemoveDefaults *bool    `yaml:\"removeDefaults,omitempty\" json:\"removeDefaults,omitempty\" jsonschema:\"nullable\"` // default: false\n\tFiles          []string `yaml:\"files,omitempty\" json:\"files,omitempty\" jsonschema:\"nullable\"`\n\tCerts          []string `yaml:\"certs,omitempty\" json:\"certs,omitempty\" jsonschema:\"nullable\"`\n}\n\ntype PreConfiguredDriverPayload struct {\n\tConfig   LimaYAML `json:\"config\"`\n\tFilePath string   `json:\"filePath\"`\n}\n\nfunc NewOS(osname string) OS {\n\tswitch osname {\n\tcase \"linux\":\n\t\treturn LINUX\n\tcase \"darwin\":\n\t\treturn DARWIN\n\tdefault:\n\t\tlogrus.Warnf(\"Unknown os: %s\", osname)\n\t\treturn osname\n\t}\n}\n\nfunc Goarm() int {\n\tif runtime.GOOS != \"linux\" {\n\t\treturn 0\n\t}\n\tif runtime.GOARCH != \"arm\" {\n\t\treturn 0\n\t}\n\tif cpu.ARM.HasVFPv3 {\n\t\treturn 7\n\t}\n\tif cpu.ARM.HasVFP {\n\t\treturn 6\n\t}\n\treturn 5 // default\n}\n\nfunc NewArch(arch string) Arch {\n\tswitch arch {\n\tcase \"amd64\":\n\t\treturn X8664\n\tcase \"arm64\":\n\t\treturn AARCH64\n\tcase \"arm\":\n\t\tarm := Goarm()\n\t\tif arm == 7 {\n\t\t\treturn ARMV7L\n\t\t}\n\t\tlogrus.Warnf(\"Unknown arm: %d\", arm)\n\t\treturn arch\n\tcase \"ppc64le\":\n\t\treturn PPC64LE\n\tcase \"riscv64\":\n\t\treturn RISCV64\n\tcase \"s390x\":\n\t\treturn S390X\n\tdefault:\n\t\tlogrus.Warnf(\"Unknown arch: %s\", arch)\n\t\treturn arch\n\t}\n}\n\nfunc IsNativeArch(arch Arch) bool {\n\tnativeX8664 := arch == X8664 && runtime.GOARCH == \"amd64\"\n\tnativeAARCH64 := arch == AARCH64 && runtime.GOARCH == \"arm64\"\n\tnativeARMV7L := arch == ARMV7L && runtime.GOARCH == \"arm\" && Goarm() == 7\n\tnativePPC64LE := arch == PPC64LE && runtime.GOARCH == \"ppc64le\"\n\tnativeRISCV64 := arch == RISCV64 && runtime.GOARCH == \"riscv64\"\n\tnativeS390X := arch == S390X && runtime.GOARCH == \"s390x\"\n\treturn nativeX8664 || nativeAARCH64 || nativeARMV7L || nativePPC64LE || nativeRISCV64 || nativeS390X\n}\n\nfunc DefaultDriver() VMType {\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\treturn VZ\n\tcase \"windows\":\n\t\treturn WSL2\n\tdefault:\n\t\treturn QEMU\n\t}\n}\n\nfunc DefaultNonNativeArchDriver() VMType {\n\treturn QEMU\n}\n"
  },
  {
    "path": "pkg/limayaml/containerd.yaml",
    "content": "# nerdctl version must be >= 2.1.6 since Lima v2.0.0.\n# Port forwarding in rootful mode no longer works with older versions of nerdctl.\narchives:\n- location: https://github.com/containerd/nerdctl/releases/download/v2.2.1/nerdctl-full-2.2.1-linux-amd64.tar.gz\n  arch: x86_64\n  digest: sha256:cf4720a290f098f1a66d34a1b2e1d3736c9014fceca737861fb7a069c66c01c2\n- location: https://github.com/containerd/nerdctl/releases/download/v2.2.1/nerdctl-full-2.2.1-linux-arm64.tar.gz\n  arch: aarch64\n  digest: sha256:2c4b97312acd41c4dfe80db6e82592367b3862b5db4c51ce67a6d79bf6ee00ee\n# No arm-v7\n# No riscv64\n"
  },
  {
    "path": "pkg/limayaml/defaults.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/pbnjay/memory\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/instance/hostname\"\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/localpathutil\"\n\t. \"github.com/lima-vm/lima/v2/pkg/must\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n)\n\nconst (\n\t// Default9pSecurityModel is \"none\" for supporting symlinks\n\t// https://gitlab.com/qemu-project/qemu/-/issues/173\n\tDefault9pSecurityModel   string = \"none\"\n\tDefault9pProtocolVersion string = \"9p2000.L\"\n\tDefault9pMsize           string = \"128KiB\"\n\tDefault9pCacheForRO      string = \"fscache\"\n\tDefault9pCacheForRW      string = \"mmap\"\n\n\tDefaultVirtiofsQueueSize int = 1024\n)\n\nvar (\n\tIPv4loopback1 = net.IPv4(127, 0, 0, 1)\n\n\tuserHomeDir = Must(os.UserHomeDir())\n\tcurrentUser = Must(user.Current())\n)\n\n//go:embed containerd.yaml\nvar defaultContainerdYAML []byte\n\ntype ContainerdYAML struct {\n\tArchives []limatype.File\n}\n\nfunc defaultContainerdArchives() []limatype.File {\n\tvar containerd ContainerdYAML\n\terr := yaml.UnmarshalWithOptions(defaultContainerdYAML, &containerd, yaml.Strict())\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to unmarshal as YAML: %w\", err))\n\t}\n\treturn containerd.Archives\n}\n\n// FirstUsernetIndex gets the index of first usernet network under l.Network[]. Returns -1 if no usernet network found.\nfunc FirstUsernetIndex(l *limatype.LimaYAML) int {\n\treturn slices.IndexFunc(l.Networks, func(network limatype.Network) bool { return networks.IsUsernet(network.Lima) })\n}\n\nfunc MACAddress(uniqueID string) string {\n\tsha := sha256.Sum256([]byte(osutil.MachineID() + uniqueID))\n\t// \"5\" is the magic number in the Lima ecosystem.\n\t// (Visit https://en.wiktionary.org/wiki/lima and Command-F \"five\")\n\t//\n\t// But the second hex number is changed to 2 to satisfy the convention for\n\t// local MAC addresses (https://en.wikipedia.org/wiki/MAC_address#Ranges_of_group_and_locally_administered_addresses)\n\t//\n\t// See also https://gitlab.com/wireshark/wireshark/-/blob/release-4.0/manuf to confirm the uniqueness of this prefix.\n\thw := append(net.HardwareAddr{0x52, 0x55, 0x55}, sha[0:3]...)\n\treturn hw.String()\n}\n\n// MountTag generates a stable mount tag from location and mountPoint.\n// Both paths are hashed to handle the same location mounted to multiple mount points.\nfunc MountTag(location, mountPoint string) string {\n\tsha := sha256.Sum256([]byte(location + \"\\x00\" + mountPoint))\n\treturn fmt.Sprintf(\"lima-%x\", sha[0:8])\n}\n\nfunc defaultCPUs() int {\n\tconst x = 4\n\tif hostCPUs := runtime.NumCPU(); hostCPUs < x {\n\t\treturn hostCPUs\n\t}\n\treturn x\n}\n\nfunc defaultMemory() uint64 {\n\tconst x uint64 = 4 * 1024 * 1024 * 1024\n\tif halfOfHostMemory := memory.TotalMemory() / 2; halfOfHostMemory < x {\n\t\treturn halfOfHostMemory\n\t}\n\treturn x\n}\n\nfunc defaultMemoryAsString() string {\n\treturn units.BytesSize(float64(defaultMemory()))\n}\n\nfunc defaultDiskSizeAsString() string {\n\t// currently just hardcoded\n\treturn \"100GiB\"\n}\n\nfunc defaultGuestInstallPrefix() string {\n\treturn \"/usr/local\"\n}\n\n// FillDefault updates undefined fields in y with defaults from d (or built-in default), and overwrites with values from o.\n// Both d and o may be empty.\n//\n// Maps (`Env`) are being merged: first populated from d, overwritten by y, and again overwritten by o.\n// Slices (e.g. `Mounts`, `Provision`) are appended, starting with o, followed by y, and finally d. This\n// makes sure o takes priority over y over d, in cases it matters (e.g. `PortForwards`, where the first\n// matching rule terminates the search).\n//\n// Exceptions:\n//   - Mounts are appended in d, y, o order, but \"merged\" when the Location matches a previous entry;\n//     the highest priority Writable setting wins.\n//   - Networks are appended in d, y, o order\n//   - DNS are picked from the highest priority where DNS is not empty.\n//   - CACertificates Files and Certs are uniquely appended in d, y, o order\nfunc FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath string, warn bool) {\n\tinstDir := filepath.Dir(filePath)\n\n\texistingLimaVersion := ExistingLimaVersion(instDir)\n\n\t// OS has to be resolved before User\n\tif y.OS == nil {\n\t\ty.OS = d.OS\n\t}\n\tif o.OS != nil {\n\t\ty.OS = o.OS\n\t}\n\ty.OS = ptr.Of(ResolveOS(y.OS))\n\n\tif y.User.Name == nil {\n\t\ty.User.Name = d.User.Name\n\t}\n\tif y.User.Comment == nil {\n\t\ty.User.Comment = d.User.Comment\n\t}\n\tif y.User.Home == nil {\n\t\ty.User.Home = d.User.Home\n\t}\n\tif y.User.Shell == nil {\n\t\ty.User.Shell = d.User.Shell\n\t}\n\tif y.User.UID == nil {\n\t\ty.User.UID = d.User.UID\n\t}\n\tif o.User.Name != nil {\n\t\ty.User.Name = o.User.Name\n\t}\n\tif o.User.Comment != nil {\n\t\ty.User.Comment = o.User.Comment\n\t}\n\tif o.User.Home != nil {\n\t\ty.User.Home = o.User.Home\n\t}\n\tif o.User.Shell != nil {\n\t\ty.User.Shell = o.User.Shell\n\t}\n\tif o.User.UID != nil {\n\t\ty.User.UID = o.User.UID\n\t}\n\tif y.User.Name == nil {\n\t\ty.User.Name = ptr.Of(osutil.LimaUser(ctx, existingLimaVersion, warn, y.OS).Username)\n\t\twarn = false\n\t}\n\tif y.User.Comment == nil {\n\t\ty.User.Comment = ptr.Of(osutil.LimaUser(ctx, existingLimaVersion, warn, y.OS).Name)\n\t\twarn = false\n\t}\n\tif y.User.Home == nil {\n\t\ty.User.Home = ptr.Of(osutil.LimaUser(ctx, existingLimaVersion, warn, y.OS).HomeDir)\n\t\twarn = false\n\t}\n\tif y.User.Shell == nil {\n\t\tswitch *y.OS {\n\t\tcase limatype.FREEBSD:\n\t\t\ty.User.Shell = ptr.Of(\"/bin/sh\")\n\t\tcase limatype.DARWIN:\n\t\t\ty.User.Shell = ptr.Of(\"/bin/zsh\")\n\t\tdefault:\n\t\t\ty.User.Shell = ptr.Of(\"/bin/bash\")\n\t\t}\n\t}\n\tif y.User.UID == nil {\n\t\tuidString := osutil.LimaUser(ctx, existingLimaVersion, warn, y.OS).Uid\n\t\tif uid, err := strconv.ParseUint(uidString, 10, 32); err == nil {\n\t\t\ty.User.UID = ptr.Of(uint32(uid))\n\t\t} else {\n\t\t\t// This should never happen; LimaUser() makes sure that .Uid is numeric\n\t\t\tlogrus.WithError(err).Warnf(\"Can't parse `user.uid` %q\", uidString)\n\t\t\ty.User.UID = ptr.Of(uint32(1000))\n\t\t}\n\t\t// warn = false\n\t}\n\tif out, err := executeGuestTemplate(*y.User.Home, instDir, y.User, y.Param); err == nil {\n\t\ty.User.Home = ptr.Of(out.String())\n\t} else {\n\t\tlogrus.WithError(err).Warnf(\"Couldn't process `user.home` value %q as a template\", *y.User.Home)\n\t}\n\n\tif y.VMType == nil {\n\t\ty.VMType = d.VMType\n\t}\n\tif o.VMType != nil {\n\t\ty.VMType = o.VMType\n\t}\n\n\tif y.Arch == nil {\n\t\ty.Arch = d.Arch\n\t}\n\tif o.Arch != nil {\n\t\ty.Arch = o.Arch\n\t}\n\ty.Arch = ptr.Of(ResolveArch(y.Arch))\n\n\ty.Images = slices.Concat(o.Images, y.Images, d.Images)\n\tfor i := range y.Images {\n\t\timg := &y.Images[i]\n\t\tif img.Arch == \"\" {\n\t\t\timg.Arch = *y.Arch\n\t\t}\n\t\tif img.Kernel != nil && img.Kernel.Arch == \"\" {\n\t\t\timg.Kernel.Arch = img.Arch\n\t\t}\n\t\tif img.Initrd != nil && img.Initrd.Arch == \"\" {\n\t\t\timg.Initrd.Arch = img.Arch\n\t\t}\n\t}\n\n\tif y.CPUs == nil {\n\t\ty.CPUs = d.CPUs\n\t}\n\tif o.CPUs != nil {\n\t\ty.CPUs = o.CPUs\n\t}\n\tif y.CPUs == nil || *y.CPUs == 0 {\n\t\ty.CPUs = ptr.Of(defaultCPUs())\n\t}\n\n\tif y.Memory == nil {\n\t\ty.Memory = d.Memory\n\t}\n\tif o.Memory != nil {\n\t\ty.Memory = o.Memory\n\t}\n\tif y.Memory == nil || *y.Memory == \"\" {\n\t\ty.Memory = ptr.Of(defaultMemoryAsString())\n\t}\n\n\tif y.Disk == nil {\n\t\ty.Disk = d.Disk\n\t}\n\tif o.Disk != nil {\n\t\ty.Disk = o.Disk\n\t}\n\tif y.Disk == nil || *y.Disk == \"\" {\n\t\ty.Disk = ptr.Of(defaultDiskSizeAsString())\n\t}\n\n\ty.AdditionalDisks = slices.Concat(o.AdditionalDisks, y.AdditionalDisks, d.AdditionalDisks)\n\n\tif y.Audio.Device == nil {\n\t\ty.Audio.Device = d.Audio.Device\n\t}\n\tif o.Audio.Device != nil {\n\t\ty.Audio.Device = o.Audio.Device\n\t}\n\tif y.Audio.Device == nil {\n\t\ty.Audio.Device = ptr.Of(\"\")\n\t}\n\n\tif y.Video.Display == nil {\n\t\ty.Video.Display = d.Video.Display\n\t}\n\tif o.Video.Display != nil {\n\t\ty.Video.Display = o.Video.Display\n\t}\n\tif y.Video.Display == nil || *y.Video.Display == \"\" {\n\t\ty.Video.Display = ptr.Of(\"none\")\n\t}\n\n\tif y.Video.VNC.Display == nil {\n\t\ty.Video.VNC.Display = d.Video.VNC.Display\n\t}\n\tif o.Video.VNC.Display != nil {\n\t\ty.Video.VNC.Display = o.Video.VNC.Display\n\t}\n\n\tif y.Firmware.LegacyBIOS == nil {\n\t\ty.Firmware.LegacyBIOS = d.Firmware.LegacyBIOS\n\t}\n\tif o.Firmware.LegacyBIOS != nil {\n\t\ty.Firmware.LegacyBIOS = o.Firmware.LegacyBIOS\n\t}\n\tif y.Firmware.LegacyBIOS == nil {\n\t\ty.Firmware.LegacyBIOS = ptr.Of(false)\n\t}\n\n\ty.Firmware.Images = slices.Concat(o.Firmware.Images, y.Firmware.Images, d.Firmware.Images)\n\tfor i := range y.Firmware.Images {\n\t\tf := &y.Firmware.Images[i]\n\t\tif f.Arch == \"\" {\n\t\t\tf.Arch = *y.Arch\n\t\t}\n\t}\n\n\tif y.TimeZone == nil {\n\t\ty.TimeZone = d.TimeZone\n\t}\n\tif o.TimeZone != nil {\n\t\ty.TimeZone = o.TimeZone\n\t}\n\tif y.TimeZone == nil {\n\t\ty.TimeZone = ptr.Of(hostTimeZone())\n\t}\n\n\tif y.SSH.LocalPort == nil {\n\t\ty.SSH.LocalPort = d.SSH.LocalPort\n\t}\n\tif o.SSH.LocalPort != nil {\n\t\ty.SSH.LocalPort = o.SSH.LocalPort\n\t}\n\tif y.SSH.LocalPort == nil {\n\t\t// y.SSH.LocalPort value is not filled here (filled by the hostagent)\n\t\ty.SSH.LocalPort = ptr.Of(0)\n\t}\n\tif y.SSH.LoadDotSSHPubKeys == nil {\n\t\ty.SSH.LoadDotSSHPubKeys = d.SSH.LoadDotSSHPubKeys\n\t}\n\tif o.SSH.LoadDotSSHPubKeys != nil {\n\t\ty.SSH.LoadDotSSHPubKeys = o.SSH.LoadDotSSHPubKeys\n\t}\n\tif y.SSH.LoadDotSSHPubKeys == nil {\n\t\ty.SSH.LoadDotSSHPubKeys = ptr.Of(false) // was true before Lima v1.0\n\t}\n\n\tif y.SSH.ForwardAgent == nil {\n\t\ty.SSH.ForwardAgent = d.SSH.ForwardAgent\n\t}\n\tif o.SSH.ForwardAgent != nil {\n\t\ty.SSH.ForwardAgent = o.SSH.ForwardAgent\n\t}\n\tif y.SSH.ForwardAgent == nil {\n\t\ty.SSH.ForwardAgent = ptr.Of(false)\n\t}\n\n\tif y.SSH.ForwardX11 == nil {\n\t\ty.SSH.ForwardX11 = d.SSH.ForwardX11\n\t}\n\tif o.SSH.ForwardX11 != nil {\n\t\ty.SSH.ForwardX11 = o.SSH.ForwardX11\n\t}\n\tif y.SSH.ForwardX11 == nil {\n\t\ty.SSH.ForwardX11 = ptr.Of(false)\n\t}\n\n\tif y.SSH.ForwardX11Trusted == nil {\n\t\ty.SSH.ForwardX11Trusted = d.SSH.ForwardX11Trusted\n\t}\n\tif o.SSH.ForwardX11Trusted != nil {\n\t\ty.SSH.ForwardX11Trusted = o.SSH.ForwardX11Trusted\n\t}\n\tif y.SSH.ForwardX11Trusted == nil {\n\t\ty.SSH.ForwardX11Trusted = ptr.Of(false)\n\t}\n\n\tif y.SSH.OverVsock == nil {\n\t\ty.SSH.OverVsock = d.SSH.OverVsock\n\t}\n\tif o.SSH.OverVsock != nil {\n\t\ty.SSH.OverVsock = o.SSH.OverVsock\n\t}\n\t// y.SSH.OverVsock default value depends on the driver; filled in driver-specific FillDefault()\n\n\t// The deprecated environment variable LIMA_SSH_OVER_VSOCK takes precedence over .ssh.overVsock\n\tif envVar := os.Getenv(\"LIMA_SSH_OVER_VSOCK\"); envVar != \"\" {\n\t\tlogrus.Warn(\"The environment variable LIMA_SSH_OVER_VSOCK is deprecated in favor of the YAML field .ssh.overVsock\")\n\t\tb, err := strconv.ParseBool(envVar)\n\t\tif err != nil {\n\t\t\tlogrus.WithError(err).Warnf(\"invalid LIMA_SSH_OVER_VSOCK value %q\", envVar)\n\t\t} else {\n\t\t\tlogrus.Debugf(\"Overriding ssh.overVsock from %v to %v via LIMA_SSH_OVER_VSOCK\", y.SSH.OverVsock, &b)\n\t\t\ty.SSH.OverVsock = ptr.Of(b)\n\t\t}\n\t}\n\n\thosts := make(map[string]string)\n\t// Values can be either names or IP addresses. Name values are canonicalized in the hostResolver.\n\tmaps.Copy(hosts, d.HostResolver.Hosts)\n\tmaps.Copy(hosts, y.HostResolver.Hosts)\n\tmaps.Copy(hosts, o.HostResolver.Hosts)\n\ty.HostResolver.Hosts = hosts\n\n\ty.Provision = slices.Concat(o.Provision, y.Provision, d.Provision)\n\tfor i := range y.Provision {\n\t\tprovision := &y.Provision[i]\n\t\tif provision.Mode == \"\" {\n\t\t\tprovision.Mode = limatype.ProvisionModeSystem\n\t\t}\n\t\tif provision.Mode == limatype.ProvisionModeDependency && provision.SkipDefaultDependencyResolution == nil {\n\t\t\tprovision.SkipDefaultDependencyResolution = ptr.Of(false)\n\t\t}\n\t\tif provision.Mode == limatype.ProvisionModeData {\n\t\t\tif provision.Content == nil {\n\t\t\t\tprovision.Content = ptr.Of(\"\")\n\t\t\t} else {\n\t\t\t\tif out, err := executeGuestTemplate(*provision.Content, instDir, y.User, y.Param); err == nil {\n\t\t\t\t\tprovision.Content = ptr.Of(out.String())\n\t\t\t\t} else {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process data content %q as a template\", *provision.Content)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif provision.Overwrite == nil {\n\t\t\t\tprovision.Overwrite = ptr.Of(true)\n\t\t\t}\n\t\t}\n\t\tif provision.Mode == limatype.ProvisionModeYQ {\n\t\t\tif provision.Expression != nil {\n\t\t\t\tif out, err := executeGuestTemplate(*provision.Expression, instDir, y.User, y.Param); err == nil {\n\t\t\t\t\tprovision.Expression = ptr.Of(out.String())\n\t\t\t\t} else {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process expression %q as a template\", *provision.Expression)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif provision.Format == nil {\n\t\t\t\tprovision.Format = ptr.Of(\"auto\")\n\t\t\t}\n\t\t}\n\t\tif provision.Mode == limatype.ProvisionModeData || provision.Mode == limatype.ProvisionModeYQ {\n\t\t\tif provision.Owner == nil {\n\t\t\t\tswitch *y.OS {\n\t\t\t\tcase limatype.DARWIN, limatype.FREEBSD:\n\t\t\t\t\tprovision.Owner = ptr.Of(\"root:wheel\")\n\t\t\t\tdefault:\n\t\t\t\t\tprovision.Owner = ptr.Of(\"root:root\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif out, err := executeGuestTemplate(*provision.Owner, instDir, y.User, y.Param); err == nil {\n\t\t\t\t\tprovision.Owner = ptr.Of(out.String())\n\t\t\t\t} else {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process owner %q as a template\", *provision.Owner)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Path is required; validation will throw an error when it is nil\n\t\t\tif provision.Path != nil {\n\t\t\t\tif out, err := executeGuestTemplate(*provision.Path, instDir, y.User, y.Param); err == nil {\n\t\t\t\t\tprovision.Path = ptr.Of(out.String())\n\t\t\t\t} else {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process path %q as a template\", *provision.Path)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif provision.Permissions == nil {\n\t\t\t\tprovision.Permissions = ptr.Of(\"644\")\n\t\t\t}\n\t\t}\n\t\tif provision.Script == nil {\n\t\t\tprovision.Script = ptr.Of(\"\")\n\t\t}\n\t\tif *provision.Script != \"\" {\n\t\t\tif out, err := executeGuestTemplate(*provision.Script, instDir, y.User, y.Param); err == nil {\n\t\t\t\t*provision.Script = out.String()\n\t\t\t} else {\n\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process provisioning script %q as a template\", *provision.Script)\n\t\t\t}\n\t\t}\n\t}\n\n\tif y.GuestInstallPrefix == nil {\n\t\ty.GuestInstallPrefix = d.GuestInstallPrefix\n\t}\n\tif o.GuestInstallPrefix != nil {\n\t\ty.GuestInstallPrefix = o.GuestInstallPrefix\n\t}\n\tif y.GuestInstallPrefix == nil {\n\t\ty.GuestInstallPrefix = ptr.Of(defaultGuestInstallPrefix())\n\t}\n\n\tif y.UpgradePackages == nil {\n\t\ty.UpgradePackages = d.UpgradePackages\n\t}\n\tif o.UpgradePackages != nil {\n\t\ty.UpgradePackages = o.UpgradePackages\n\t}\n\tif y.UpgradePackages == nil {\n\t\ty.UpgradePackages = ptr.Of(false)\n\t}\n\n\tif y.Containerd.System == nil {\n\t\ty.Containerd.System = d.Containerd.System\n\t}\n\tif o.Containerd.System != nil {\n\t\ty.Containerd.System = o.Containerd.System\n\t}\n\tif y.Containerd.System == nil {\n\t\ty.Containerd.System = ptr.Of(false)\n\t}\n\tif y.Containerd.User == nil {\n\t\ty.Containerd.User = d.Containerd.User\n\t}\n\tif o.Containerd.User != nil {\n\t\ty.Containerd.User = o.Containerd.User\n\t}\n\tif y.Containerd.User == nil {\n\t\tswitch *y.Arch {\n\t\tcase limatype.X8664, limatype.AARCH64:\n\t\t\ty.Containerd.User = ptr.Of(true)\n\t\tdefault:\n\t\t\ty.Containerd.User = ptr.Of(false)\n\t\t}\n\t}\n\n\ty.Containerd.Archives = slices.Concat(o.Containerd.Archives, y.Containerd.Archives, d.Containerd.Archives)\n\tif len(y.Containerd.Archives) == 0 {\n\t\ty.Containerd.Archives = defaultContainerdArchives()\n\t}\n\tfor i := range y.Containerd.Archives {\n\t\tf := &y.Containerd.Archives[i]\n\t\tif f.Arch == \"\" {\n\t\t\tf.Arch = *y.Arch\n\t\t}\n\t}\n\n\ty.Probes = slices.Concat(o.Probes, y.Probes, d.Probes)\n\tfor i := range y.Probes {\n\t\tprobe := &y.Probes[i]\n\t\tif probe.Mode == \"\" {\n\t\t\tprobe.Mode = limatype.ProbeModeReadiness\n\t\t}\n\t\tif probe.Description == \"\" {\n\t\t\tprobe.Description = fmt.Sprintf(\"user probe %d/%d\", i+1, len(y.Probes))\n\t\t}\n\t\tif probe.Script == nil {\n\t\t\tprobe.Script = ptr.Of(\"\")\n\t\t}\n\t\tif out, err := executeGuestTemplate(*probe.Script, instDir, y.User, y.Param); err == nil {\n\t\t\tprobe.Script = ptr.Of(out.String())\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process probing script %q as a template\", *probe.Script)\n\t\t}\n\t}\n\n\ty.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, d.PortForwards)\n\tfor i := range y.PortForwards {\n\t\tFillPortForwardDefaults(&y.PortForwards[i], instDir, y.User, y.Param)\n\t\t// After defaults processing the singular HostPort and GuestPort values should not be used again.\n\t}\n\n\ty.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, d.CopyToHost)\n\tfor i := range y.CopyToHost {\n\t\tFillCopyToHostDefaults(&y.CopyToHost[i], instDir, y.User, y.Param)\n\t}\n\n\tif y.HostResolver.Enabled == nil {\n\t\ty.HostResolver.Enabled = d.HostResolver.Enabled\n\t}\n\tif o.HostResolver.Enabled != nil {\n\t\ty.HostResolver.Enabled = o.HostResolver.Enabled\n\t}\n\tif y.HostResolver.Enabled == nil {\n\t\ty.HostResolver.Enabled = ptr.Of(true)\n\t}\n\n\tif y.HostResolver.IPv6 == nil {\n\t\ty.HostResolver.IPv6 = d.HostResolver.IPv6\n\t}\n\tif o.HostResolver.IPv6 != nil {\n\t\ty.HostResolver.IPv6 = o.HostResolver.IPv6\n\t}\n\tif y.HostResolver.IPv6 == nil {\n\t\ty.HostResolver.IPv6 = ptr.Of(false)\n\t}\n\n\tif y.PropagateProxyEnv == nil {\n\t\ty.PropagateProxyEnv = d.PropagateProxyEnv\n\t}\n\tif o.PropagateProxyEnv != nil {\n\t\ty.PropagateProxyEnv = o.PropagateProxyEnv\n\t}\n\tif y.PropagateProxyEnv == nil {\n\t\ty.PropagateProxyEnv = ptr.Of(true)\n\t}\n\n\tnetworks := make([]limatype.Network, 0, len(d.Networks)+len(y.Networks)+len(o.Networks))\n\tiface := make(map[string]int)\n\tfor _, nw := range slices.Concat(d.Networks, y.Networks, o.Networks) {\n\t\tif i, ok := iface[nw.Interface]; ok {\n\t\t\tif nw.Socket != \"\" {\n\t\t\t\tnetworks[i].Socket = nw.Socket\n\t\t\t\tnetworks[i].Lima = \"\"\n\t\t\t}\n\t\t\tif nw.Lima != \"\" {\n\t\t\t\tif nw.Socket != \"\" {\n\t\t\t\t\t// We can't return an error, so just log it, and prefer `lima` over `socket`\n\t\t\t\t\tlogrus.Errorf(\"Network %q has both socket=%q and lima=%q fields; ignoring socket\",\n\t\t\t\t\t\tnw.Interface, nw.Socket, nw.Lima)\n\t\t\t\t}\n\t\t\t\tnetworks[i].Lima = nw.Lima\n\t\t\t\tnetworks[i].Socket = \"\"\n\t\t\t}\n\t\t\tif nw.MACAddress != \"\" {\n\t\t\t\tnetworks[i].MACAddress = nw.MACAddress\n\t\t\t}\n\t\t\tif nw.Metric != nil {\n\t\t\t\tnetworks[i].Metric = nw.Metric\n\t\t\t}\n\t\t} else {\n\t\t\t// unnamed network definitions are not combined/overwritten\n\t\t\tif nw.Interface != \"\" {\n\t\t\t\tiface[nw.Interface] = len(networks)\n\t\t\t}\n\t\t\tnetworks = append(networks, nw)\n\t\t}\n\t}\n\ty.Networks = networks\n\tfor i := range y.Networks {\n\t\tnw := &y.Networks[i]\n\t\tif nw.MACAddress == \"\" {\n\t\t\t// every interface in every limayaml file must get its own unique MAC address\n\t\t\tnw.MACAddress = MACAddress(fmt.Sprintf(\"%s#%d\", filePath, i))\n\t\t}\n\t\tif nw.Interface == \"\" {\n\t\t\tnw.Interface = \"lima\" + strconv.Itoa(i)\n\t\t}\n\t\tif nw.Metric == nil {\n\t\t\tnw.Metric = ptr.Of(uint32(100))\n\t\t}\n\t}\n\n\ty.MountTypesUnsupported = slices.Concat(o.MountTypesUnsupported, y.MountTypesUnsupported, d.MountTypesUnsupported)\n\n\t// MountType has to be resolved before resolving Mounts\n\tif y.MountType == nil {\n\t\ty.MountType = d.MountType\n\t}\n\tif o.MountType != nil {\n\t\ty.MountType = o.MountType\n\t}\n\n\tif y.MountInotify == nil {\n\t\ty.MountInotify = d.MountInotify\n\t}\n\tif o.MountInotify != nil {\n\t\ty.MountInotify = o.MountInotify\n\t}\n\tif y.MountInotify == nil {\n\t\ty.MountInotify = ptr.Of(false)\n\t}\n\n\t// Combine all mounts; highest priority entry determines writable status.\n\t// Only works for exact matches; does not normalize case or resolve symlinks.\n\tmounts := make([]limatype.Mount, 0, len(d.Mounts)+len(y.Mounts)+len(o.Mounts))\n\tmountPoint := make(map[string]int)\n\tfor _, mount := range slices.Concat(d.Mounts, y.Mounts, o.Mounts) {\n\t\tif out, err := executeHostTemplate(mount.Location, instDir, y.Param); err == nil {\n\t\t\tmount.Location = filepath.Clean(out.String())\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process mount location %q as a template\", mount.Location)\n\t\t}\n\t\t// Expand a path that begins with `~`. Relative paths are not modified, and rejected by Validate() later.\n\t\tif localpathutil.IsTildePath(mount.Location) {\n\t\t\tif location, err := localpathutil.Expand(mount.Location); err == nil {\n\t\t\t\tmount.Location = location\n\t\t\t} else {\n\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't expand location %q\", mount.Location)\n\t\t\t}\n\t\t}\n\t\tif mount.MountPoint == nil {\n\t\t\tmountLocation := mount.Location\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\tvar err error\n\t\t\t\tmountLocation, err = ioutilx.WindowsSubsystemPath(ctx, mountLocation)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't convert location %q into mount target\", mount.Location)\n\t\t\t\t}\n\t\t\t}\n\t\t\tmount.MountPoint = ptr.Of(mountLocation)\n\t\t} else {\n\t\t\tif out, err := executeGuestTemplate(*mount.MountPoint, instDir, y.User, y.Param); err == nil {\n\t\t\t\tmount.MountPoint = ptr.Of(out.String())\n\t\t\t} else {\n\t\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process mount point %q as a template\", *mount.MountPoint)\n\t\t\t}\n\t\t}\n\t\tif i, ok := mountPoint[*mount.MountPoint]; ok {\n\t\t\tif mount.SSHFS.Cache != nil {\n\t\t\t\tmounts[i].SSHFS.Cache = mount.SSHFS.Cache\n\t\t\t}\n\t\t\tif mount.SSHFS.FollowSymlinks != nil {\n\t\t\t\tmounts[i].SSHFS.FollowSymlinks = mount.SSHFS.FollowSymlinks\n\t\t\t}\n\t\t\tif mount.SSHFS.SFTPDriver != nil {\n\t\t\t\tmounts[i].SSHFS.SFTPDriver = mount.SSHFS.SFTPDriver\n\t\t\t}\n\t\t\tif mount.NineP.SecurityModel != nil {\n\t\t\t\tmounts[i].NineP.SecurityModel = mount.NineP.SecurityModel\n\t\t\t}\n\t\t\tif mount.NineP.ProtocolVersion != nil {\n\t\t\t\tmounts[i].NineP.ProtocolVersion = mount.NineP.ProtocolVersion\n\t\t\t}\n\t\t\tif mount.NineP.Msize != nil {\n\t\t\t\tmounts[i].NineP.Msize = mount.NineP.Msize\n\t\t\t}\n\t\t\tif mount.NineP.Cache != nil {\n\t\t\t\tmounts[i].NineP.Cache = mount.NineP.Cache\n\t\t\t}\n\t\t\tif mount.Virtiofs.QueueSize != nil {\n\t\t\t\tmounts[i].Virtiofs.QueueSize = mount.Virtiofs.QueueSize\n\t\t\t}\n\t\t\tif mount.Writable != nil {\n\t\t\t\tmounts[i].Writable = mount.Writable\n\t\t\t}\n\t\t\tif mount.MountPoint != nil {\n\t\t\t\tmounts[i].MountPoint = mount.MountPoint\n\t\t\t}\n\t\t} else {\n\t\t\tmountPoint[*mount.MountPoint] = len(mounts)\n\t\t\tmounts = append(mounts, mount)\n\t\t}\n\t}\n\ty.Mounts = mounts\n\n\tfor i := range y.Mounts {\n\t\tmount := &y.Mounts[i]\n\t\tif mount.SSHFS.Cache == nil {\n\t\t\tmount.SSHFS.Cache = ptr.Of(true)\n\t\t}\n\t\tif mount.SSHFS.FollowSymlinks == nil {\n\t\t\tmount.SSHFS.FollowSymlinks = ptr.Of(false)\n\t\t}\n\t\tif mount.SSHFS.SFTPDriver == nil {\n\t\t\tmount.SSHFS.SFTPDriver = ptr.Of(\"\")\n\t\t}\n\t\tif mount.NineP.SecurityModel == nil {\n\t\t\tmounts[i].NineP.SecurityModel = ptr.Of(Default9pSecurityModel)\n\t\t}\n\t\tif mount.NineP.ProtocolVersion == nil {\n\t\t\tmounts[i].NineP.ProtocolVersion = ptr.Of(Default9pProtocolVersion)\n\t\t}\n\t\tif mount.NineP.Msize == nil {\n\t\t\tmounts[i].NineP.Msize = ptr.Of(Default9pMsize)\n\t\t}\n\t\tif mount.Writable == nil {\n\t\t\tmount.Writable = ptr.Of(false)\n\t\t}\n\t\tif mount.NineP.Cache == nil {\n\t\t\tif *mount.Writable {\n\t\t\t\tmounts[i].NineP.Cache = ptr.Of(Default9pCacheForRW)\n\t\t\t} else {\n\t\t\t\tmounts[i].NineP.Cache = ptr.Of(Default9pCacheForRO)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Note: DNS lists are not combined; highest priority setting is picked\n\tif len(y.DNS) == 0 {\n\t\ty.DNS = d.DNS\n\t}\n\tif len(o.DNS) > 0 {\n\t\ty.DNS = o.DNS\n\t}\n\n\tenv := make(map[string]string)\n\tmaps.Copy(env, d.Env)\n\tmaps.Copy(env, y.Env)\n\tmaps.Copy(env, o.Env)\n\ty.Env = env\n\n\tparam := make(map[string]string)\n\tmaps.Copy(param, d.Param)\n\tmaps.Copy(param, y.Param)\n\tmaps.Copy(param, o.Param)\n\ty.Param = param\n\n\tvmOpts := make(limatype.VMOpts)\n\tmaps.Copy(vmOpts, d.VMOpts)\n\tmaps.Copy(vmOpts, y.VMOpts)\n\tmaps.Copy(vmOpts, o.VMOpts)\n\ty.VMOpts = vmOpts\n\n\tif y.CACertificates.RemoveDefaults == nil {\n\t\ty.CACertificates.RemoveDefaults = d.CACertificates.RemoveDefaults\n\t}\n\tif o.CACertificates.RemoveDefaults != nil {\n\t\ty.CACertificates.RemoveDefaults = o.CACertificates.RemoveDefaults\n\t}\n\tif y.CACertificates.RemoveDefaults == nil {\n\t\ty.CACertificates.RemoveDefaults = ptr.Of(false)\n\t}\n\n\ty.CACertificates.Files = unique(slices.Concat(d.CACertificates.Files, y.CACertificates.Files, o.CACertificates.Files))\n\ty.CACertificates.Certs = unique(slices.Concat(d.CACertificates.Certs, y.CACertificates.Certs, o.CACertificates.Certs))\n\n\tif y.NestedVirtualization == nil {\n\t\ty.NestedVirtualization = d.NestedVirtualization\n\t}\n\tif o.NestedVirtualization != nil {\n\t\ty.NestedVirtualization = o.NestedVirtualization\n\t}\n\tif y.NestedVirtualization == nil {\n\t\ty.NestedVirtualization = ptr.Of(false)\n\t}\n\n\tif y.Plain == nil {\n\t\ty.Plain = d.Plain\n\t}\n\tif o.Plain != nil {\n\t\ty.Plain = o.Plain\n\t}\n\tif y.Plain == nil {\n\t\ty.Plain = ptr.Of(false)\n\t}\n\n\tfixUpForPlainMode(y)\n}\n\n// ExistingLimaVersion returns empty if the instance was created with Lima prior to v0.20.\nfunc ExistingLimaVersion(instDir string) string {\n\tif !IsExistingInstanceDir(instDir) {\n\t\treturn version.Version\n\t}\n\n\tlimaVersionFile := filepath.Join(instDir, filenames.LimaVersion)\n\tif b, err := os.ReadFile(limaVersionFile); err == nil {\n\t\treturn strings.TrimSpace(string(b))\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\tlogrus.WithError(err).Warnf(\"Failed to read %q\", limaVersionFile)\n\t}\n\n\treturn version.Version\n}\n\nfunc fixUpForPlainMode(y *limatype.LimaYAML) {\n\tif !*y.Plain {\n\t\treturn\n\t}\n\tdeleteNonStaticPortForwards(&y.PortForwards)\n\ty.Mounts = nil\n\ty.Containerd.System = ptr.Of(false)\n\ty.Containerd.User = ptr.Of(false)\n\ty.TimeZone = ptr.Of(\"\")\n}\n\n// deleteNonStaticPortForwards removes all non-static port forwarding rules in case of Plain mode.\nfunc deleteNonStaticPortForwards(portForwards *[]limatype.PortForward) {\n\tstaticPortForwards := make([]limatype.PortForward, 0, len(*portForwards))\n\tfor _, rule := range *portForwards {\n\t\tif rule.Static {\n\t\t\tstaticPortForwards = append(staticPortForwards, rule)\n\t\t}\n\t}\n\t*portForwards = staticPortForwards\n}\n\nfunc executeGuestTemplate(format, instDir string, user limatype.User, param map[string]string) (bytes.Buffer, error) {\n\ttmpl, err := template.New(\"\").Parse(format)\n\tif err == nil {\n\t\tname := filepath.Base(instDir)\n\t\tdata := map[string]any{\n\t\t\t\"Name\":     name,\n\t\t\t\"Hostname\": hostname.FromInstName(name), // TODO: support customization\n\t\t\t\"UID\":      *user.UID,\n\t\t\t\"User\":     *user.Name,\n\t\t\t\"Home\":     *user.Home,\n\t\t\t\"Param\":    param,\n\t\t}\n\t\tvar out bytes.Buffer\n\t\terr = tmpl.Execute(&out, data)\n\t\tif err == nil {\n\t\t\treturn out, nil\n\t\t}\n\t}\n\treturn bytes.Buffer{}, err\n}\n\nfunc executeHostTemplate(format, instDir string, param map[string]string) (bytes.Buffer, error) {\n\ttmpl, err := template.New(\"\").Parse(format)\n\tif err == nil {\n\t\tlimaHome, _ := dirnames.LimaDir()\n\t\tglobalTempDir := \"/tmp\"\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\t// On Windows the global temp directory \"%SystemRoot%\\Temp\" (normally \"C:\\Windows\\Temp\")\n\t\t\t// is not writable by regular users.\n\t\t\tglobalTempDir = os.TempDir()\n\t\t}\n\t\tdata := map[string]any{\n\t\t\t\"Dir\":  instDir,\n\t\t\t\"Name\": filepath.Base(instDir),\n\t\t\t// TODO: add hostname fields for the host and the guest\n\t\t\t\"UID\":           currentUser.Uid,\n\t\t\t\"User\":          currentUser.Username,\n\t\t\t\"Home\":          userHomeDir,\n\t\t\t\"Param\":         param,\n\t\t\t\"GlobalTempDir\": globalTempDir,\n\t\t\t\"TempDir\":       os.TempDir(),\n\n\t\t\t\"Instance\": filepath.Base(instDir), // DEPRECATED, use `{{.Name}}`\n\t\t\t\"LimaHome\": limaHome,               // DEPRECATED, use `{{.Dir}}` instead of `{{.LimaHome}}/{{.Instance}}`\n\t\t}\n\t\tvar out bytes.Buffer\n\t\terr = tmpl.Execute(&out, data)\n\t\tif err == nil {\n\t\t\treturn out, nil\n\t\t}\n\t}\n\treturn bytes.Buffer{}, err\n}\n\nfunc FillPortForwardDefaults(rule *limatype.PortForward, instDir string, user limatype.User, param map[string]string) {\n\tif rule.Proto == \"\" {\n\t\trule.Proto = limatype.ProtoAny\n\t}\n\tif rule.GuestIP == nil {\n\t\tif rule.GuestIPMustBeZero != nil && *rule.GuestIPMustBeZero {\n\t\t\trule.GuestIP = net.IPv4zero\n\t\t} else {\n\t\t\trule.GuestIP = IPv4loopback1\n\t\t}\n\t}\n\tif rule.GuestIPMustBeZero == nil {\n\t\trule.GuestIPMustBeZero = ptr.Of(rule.GuestIP.Equal(net.IPv4zero))\n\t}\n\tif rule.HostIP == nil {\n\t\trule.HostIP = IPv4loopback1\n\t}\n\tif rule.GuestPortRange[0] == 0 && rule.GuestPortRange[1] == 0 {\n\t\tif rule.GuestPort == 0 {\n\t\t\trule.GuestPortRange[0] = 1\n\t\t\trule.GuestPortRange[1] = 65535\n\t\t} else {\n\t\t\trule.GuestPortRange[0] = rule.GuestPort\n\t\t\trule.GuestPortRange[1] = rule.GuestPort\n\t\t}\n\t}\n\tif rule.GuestSocket != \"\" {\n\t\tif out, err := executeGuestTemplate(rule.GuestSocket, instDir, user, param); err == nil {\n\t\t\trule.GuestSocket = out.String()\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process guestSocket %q as a template\", rule.GuestSocket)\n\t\t}\n\t}\n\tif rule.HostSocket != \"\" {\n\t\tif out, err := executeHostTemplate(rule.HostSocket, instDir, param); err == nil {\n\t\t\trule.HostSocket = out.String()\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process hostSocket %q as a template\", rule.HostSocket)\n\t\t}\n\t\tif !filepath.IsAbs(rule.HostSocket) {\n\t\t\trule.HostSocket = filepath.Join(instDir, filenames.SocketDir, rule.HostSocket)\n\t\t}\n\t} else if rule.HostPortRange[0] == 0 && rule.HostPortRange[1] == 0 {\n\t\tif rule.HostPort == 0 {\n\t\t\trule.HostPortRange = rule.GuestPortRange\n\t\t} else {\n\t\t\trule.HostPortRange[0] = rule.HostPort\n\t\t\trule.HostPortRange[1] = rule.HostPort\n\t\t}\n\t}\n}\n\nfunc FillCopyToHostDefaults(rule *limatype.CopyToHost, instDir string, user limatype.User, param map[string]string) {\n\tif rule.GuestFile != \"\" {\n\t\tif out, err := executeGuestTemplate(rule.GuestFile, instDir, user, param); err == nil {\n\t\t\trule.GuestFile = out.String()\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process guest %q as a template\", rule.GuestFile)\n\t\t}\n\t}\n\tif rule.HostFile != \"\" {\n\t\tif out, err := executeHostTemplate(rule.HostFile, instDir, param); err == nil {\n\t\t\trule.HostFile = out.String()\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warnf(\"Couldn't process host %q as a template\", rule.HostFile)\n\t\t}\n\t}\n}\n\nfunc IsExistingInstanceDir(dir string) bool {\n\t// existence of \"lima.yaml\" does not signify existence of the instance,\n\t// because the file is created during the initialization of the instance.\n\tfor _, f := range []string{\n\t\tfilenames.HostAgentStdoutLog, filenames.HostAgentStderrLog,\n\t\tfilenames.VzIdentifier, filenames.Image, filenames.Disk, filenames.BaseDiskLegacy, filenames.DiffDiskLegacy, filenames.CIDataISO,\n\t} {\n\t\tfile := filepath.Join(dir, f)\n\t\tif _, err := os.Lstat(file); !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc ResolveOS(s *string) limatype.OS {\n\tif s == nil || *s == \"\" || *s == \"default\" {\n\t\treturn limatype.NewOS(\"linux\")\n\t}\n\treturn *s\n}\n\nfunc ResolveArch(s *string) limatype.Arch {\n\tif s == nil || *s == \"\" || *s == \"default\" {\n\t\treturn limatype.NewArch(runtime.GOARCH)\n\t}\n\treturn *s\n}\n\nfunc unique(s []string) []string {\n\tkeys := make(map[string]bool)\n\tlist := []string{}\n\tfor _, entry := range s {\n\t\tif _, found := keys[entry]; !found {\n\t\t\tkeys[entry] = true\n\t\t\tlist = append(list, entry)\n\t\t}\n\t}\n\treturn list\n}\n"
  },
  {
    "path": "pkg/limayaml/defaults_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/google/go-cmp/cmp\"\n\t\"github.com/google/go-cmp/cmp/cmpopts\"\n\t\"github.com/sirupsen/logrus\"\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n)\n\nfunc TestFillDefault(t *testing.T) {\n\tlogrus.SetLevel(logrus.DebugLevel)\n\tvar d, y, o limatype.LimaYAML\n\n\topts := []cmp.Option{\n\t\t// Consider nil slices and empty slices to be identical\n\t\tcmpopts.EquateEmpty(),\n\t}\n\n\tvar arch limatype.Arch\n\tswitch runtime.GOARCH {\n\tcase \"amd64\":\n\t\tarch = limatype.X8664\n\tcase \"arm64\":\n\t\tarch = limatype.AARCH64\n\tcase \"arm\":\n\t\tif runtime.GOOS != \"linux\" {\n\t\t\tt.Skipf(\"unsupported GOOS: %s\", runtime.GOOS)\n\t\t}\n\t\tif arm := limatype.Goarm(); arm < 7 {\n\t\t\tt.Skipf(\"unsupported GOARM: %d\", arm)\n\t\t}\n\t\tarch = limatype.ARMV7L\n\tcase \"ppc64le\":\n\t\tarch = limatype.PPC64LE\n\tcase \"riscv64\":\n\t\tarch = limatype.RISCV64\n\tcase \"s390x\":\n\t\tarch = limatype.S390X\n\tdefault:\n\t\tt.Skipf(\"unknown GOARCH: %s\", runtime.GOARCH)\n\t}\n\n\thostHome, err := os.UserHomeDir()\n\tassert.NilError(t, err)\n\tlimaHome, err := dirnames.LimaDir()\n\tassert.NilError(t, err)\n\tuser := osutil.LimaUser(t.Context(), \"0.0.0\", false, nil)\n\tuser.HomeDir = fmt.Sprintf(\"/home/%s.guest\", user.Username)\n\tuid, err := strconv.ParseUint(user.Uid, 10, 32)\n\tassert.NilError(t, err)\n\n\tinstName := \"instance\"\n\tinstDir := filepath.Join(limaHome, instName)\n\tfilePath := filepath.Join(instDir, filenames.LimaYAML)\n\n\t// Builtin default values\n\tbuiltin := limatype.LimaYAML{\n\t\tOS:                 ptr.Of(limatype.LINUX),\n\t\tArch:               ptr.Of(arch),\n\t\tCPUs:               ptr.Of(defaultCPUs()),\n\t\tMemory:             ptr.Of(defaultMemoryAsString()),\n\t\tDisk:               ptr.Of(defaultDiskSizeAsString()),\n\t\tGuestInstallPrefix: ptr.Of(defaultGuestInstallPrefix()),\n\t\tUpgradePackages:    ptr.Of(false),\n\t\tContainerd: limatype.Containerd{\n\t\t\tSystem:   ptr.Of(false),\n\t\t\tUser:     ptr.Of(true),\n\t\t\tArchives: defaultContainerdArchives(),\n\t\t},\n\t\tSSH: limatype.SSH{\n\t\t\tLocalPort:         ptr.Of(0),\n\t\t\tLoadDotSSHPubKeys: ptr.Of(false),\n\t\t\tForwardAgent:      ptr.Of(false),\n\t\t\tForwardX11:        ptr.Of(false),\n\t\t\tForwardX11Trusted: ptr.Of(false),\n\t\t},\n\t\tTimeZone: ptr.Of(hostTimeZone()),\n\t\tFirmware: limatype.Firmware{\n\t\t\tLegacyBIOS: ptr.Of(false),\n\t\t},\n\t\tAudio: limatype.Audio{\n\t\t\tDevice: ptr.Of(\"\"),\n\t\t},\n\t\tVideo: limatype.Video{\n\t\t\tDisplay: ptr.Of(\"none\"),\n\t\t},\n\t\tHostResolver: limatype.HostResolver{\n\t\t\tEnabled: ptr.Of(true),\n\t\t\tIPv6:    ptr.Of(false),\n\t\t},\n\t\tPropagateProxyEnv: ptr.Of(true),\n\t\tCACertificates: limatype.CACertificates{\n\t\t\tRemoveDefaults: ptr.Of(false),\n\t\t},\n\t\tNestedVirtualization: ptr.Of(false),\n\t\tPlain:                ptr.Of(false),\n\t\tUser: limatype.User{\n\t\t\tName:    ptr.Of(user.Username),\n\t\t\tComment: ptr.Of(user.Name),\n\t\t\tHome:    ptr.Of(user.HomeDir),\n\t\t\tShell:   ptr.Of(\"/bin/bash\"),\n\t\t\tUID:     ptr.Of(uint32(uid)),\n\t\t},\n\t}\n\n\tdefaultPortForward := limatype.PortForward{\n\t\tGuestIP:           IPv4loopback1,\n\t\tGuestIPMustBeZero: ptr.Of(false),\n\t\tGuestPortRange:    [2]int{1, 65535},\n\t\tHostIP:            IPv4loopback1,\n\t\tHostPortRange:     [2]int{1, 65535},\n\t\tProto:             limatype.ProtoAny,\n\t\tReverse:           false,\n\t}\n\n\t// ------------------------------------------------------------------------------------\n\t// Builtin defaults are set when y is (mostly) empty\n\n\t// All these slices and maps are empty in \"builtin\". Add minimal entries here to see that\n\t// their values are retained and defaults for their fields are applied correctly.\n\ty = limatype.LimaYAML{\n\t\tHostResolver: limatype.HostResolver{\n\t\t\tHosts: map[string]string{\n\t\t\t\t\"MY.Host\": \"host.lima.internal\",\n\t\t\t},\n\t\t},\n\t\tMounts: []limatype.Mount{\n\t\t\t//nolint:usetesting // We need the OS temp directory name here; it is not used to create temp files for testing\n\t\t\t{Location: filepath.Clean(os.TempDir())},\n\t\t\t{Location: filepath.Clean(\"{{.Dir}}/{{.Param.ONE}}\"), MountPoint: ptr.Of(\"/mnt/{{.Param.ONE}}\")},\n\t\t},\n\t\tMountType: ptr.Of(limatype.NINEP),\n\t\tProvision: []limatype.Provision{\n\t\t\t{Script: ptr.Of(\"#!/bin/true # {{.Param.ONE}}\")},\n\t\t},\n\t\tProbes: []limatype.Probe{\n\t\t\t{Script: ptr.Of(\"#!/bin/false # {{.Param.ONE}}\")},\n\t\t},\n\t\tNetworks: []limatype.Network{\n\t\t\t{Lima: \"shared\"},\n\t\t},\n\t\tDNS: []net.IP{\n\t\t\tnet.ParseIP(\"1.0.1.0\"),\n\t\t},\n\t\tPortForwards: []limatype.PortForward{\n\t\t\t{},\n\t\t\t{GuestPort: 80},\n\t\t\t{GuestPort: 8080, HostPort: 8888},\n\t\t\t{\n\t\t\t\tGuestSocket: \"{{.Home}} | {{.UID}} | {{.User}} | {{.Param.ONE}}\",\n\t\t\t\tHostSocket:  \"{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}} | {{.Param.ONE}}\",\n\t\t\t},\n\t\t},\n\t\tCopyToHost: []limatype.CopyToHost{\n\t\t\t{\n\t\t\t\tGuestFile: \"{{.Home}} | {{.UID}} | {{.User}} | {{.Param.ONE}}\",\n\t\t\t\tHostFile:  \"{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}} | {{.Param.ONE}}\",\n\t\t\t},\n\t\t},\n\t\tEnv: map[string]string{\n\t\t\t\"ONE\": \"Eins\",\n\t\t},\n\t\tParam: map[string]string{\n\t\t\t\"ONE\": \"Eins\",\n\t\t},\n\t\tCACertificates: limatype.CACertificates{\n\t\t\tFiles: []string{\"ca.crt\"},\n\t\t\tCerts: []string{\n\t\t\t\t\"-----BEGIN CERTIFICATE-----\\nYOUR-ORGS-TRUSTED-CA-CERT\\n-----END CERTIFICATE-----\\n\",\n\t\t\t},\n\t\t},\n\t\tTimeZone: ptr.Of(\"Antarctica/Troll\"),\n\t}\n\n\texpect := builtin\n\t// VMType should remain nil when not explicitly set (will be resolved by ValidateVMType later)\n\texpect.VMType = nil\n\texpect.HostResolver.Hosts = map[string]string{\n\t\t\"MY.Host\": \"host.lima.internal\",\n\t}\n\n\texpect.Mounts = slices.Clone(y.Mounts)\n\texpect.Mounts[0].MountPoint = ptr.Of(expect.Mounts[0].Location)\n\tif runtime.GOOS == \"windows\" {\n\t\tmountLocation, err := ioutilx.WindowsSubsystemPath(t.Context(), expect.Mounts[0].Location)\n\t\tif err == nil {\n\t\t\texpect.Mounts[0].MountPoint = ptr.Of(mountLocation)\n\t\t}\n\t}\n\texpect.Mounts[0].Writable = ptr.Of(false)\n\texpect.Mounts[0].SSHFS.Cache = ptr.Of(true)\n\texpect.Mounts[0].SSHFS.FollowSymlinks = ptr.Of(false)\n\texpect.Mounts[0].SSHFS.SFTPDriver = ptr.Of(\"\")\n\texpect.Mounts[0].NineP.SecurityModel = ptr.Of(Default9pSecurityModel)\n\texpect.Mounts[0].NineP.ProtocolVersion = ptr.Of(Default9pProtocolVersion)\n\texpect.Mounts[0].NineP.Msize = ptr.Of(Default9pMsize)\n\texpect.Mounts[0].NineP.Cache = ptr.Of(Default9pCacheForRO)\n\texpect.Mounts[0].Virtiofs.QueueSize = nil\n\t// Only missing Mounts field is Writable, and the default value is also the null value: false\n\texpect.Mounts[1].Location = filepath.Join(instDir, y.Param[\"ONE\"])\n\texpect.Mounts[1].MountPoint = ptr.Of(path.Join(\"/mnt\", y.Param[\"ONE\"]))\n\texpect.Mounts[1].Writable = ptr.Of(false)\n\texpect.Mounts[1].SSHFS.Cache = ptr.Of(true)\n\texpect.Mounts[1].SSHFS.FollowSymlinks = ptr.Of(false)\n\texpect.Mounts[1].SSHFS.SFTPDriver = ptr.Of(\"\")\n\texpect.Mounts[1].NineP.SecurityModel = ptr.Of(Default9pSecurityModel)\n\texpect.Mounts[1].NineP.ProtocolVersion = ptr.Of(Default9pProtocolVersion)\n\texpect.Mounts[1].NineP.Msize = ptr.Of(Default9pMsize)\n\texpect.Mounts[1].NineP.Cache = ptr.Of(Default9pCacheForRO)\n\texpect.Mounts[1].Virtiofs.QueueSize = nil\n\n\texpect.MountType = ptr.Of(limatype.NINEP)\n\n\texpect.MountInotify = ptr.Of(false)\n\n\texpect.Provision = slices.Clone(y.Provision)\n\texpect.Provision[0].Mode = limatype.ProvisionModeSystem\n\texpect.Provision[0].Script = ptr.Of(\"#!/bin/true # Eins\")\n\n\texpect.Probes = slices.Clone(y.Probes)\n\texpect.Probes[0].Mode = limatype.ProbeModeReadiness\n\texpect.Probes[0].Description = \"user probe 1/1\"\n\texpect.Probes[0].Script = ptr.Of(\"#!/bin/false # Eins\")\n\n\texpect.Networks = slices.Clone(y.Networks)\n\texpect.Networks[0].MACAddress = MACAddress(fmt.Sprintf(\"%s#%d\", filePath, 0))\n\texpect.Networks[0].Interface = \"lima0\"\n\texpect.Networks[0].Metric = ptr.Of(uint32(100))\n\n\texpect.DNS = slices.Clone(y.DNS)\n\texpect.PortForwards = []limatype.PortForward{\n\t\tdefaultPortForward,\n\t\tdefaultPortForward,\n\t\tdefaultPortForward,\n\t\tdefaultPortForward,\n\t}\n\texpect.CopyToHost = []limatype.CopyToHost{\n\t\t{},\n\t}\n\n\t// Setting GuestPort and HostPort for DeepEqual(), but they are not supposed to be used\n\t// after FillDefault() has been called and the ...PortRange fields have been set.\n\texpect.PortForwards[1].GuestPort = 80\n\texpect.PortForwards[1].GuestPortRange = [2]int{80, 80}\n\texpect.PortForwards[1].HostPortRange = expect.PortForwards[1].GuestPortRange\n\n\texpect.PortForwards[2].GuestPort = 8080\n\texpect.PortForwards[2].GuestPortRange = [2]int{8080, 8080}\n\texpect.PortForwards[2].HostPort = 8888\n\texpect.PortForwards[2].HostPortRange = [2]int{8888, 8888}\n\n\texpect.PortForwards[3].HostPortRange = [2]int{0, 0}\n\texpect.PortForwards[3].GuestSocket = fmt.Sprintf(\"%s | %s | %s | %s\", user.HomeDir, user.Uid, user.Username, y.Param[\"ONE\"])\n\texpect.PortForwards[3].HostSocket = fmt.Sprintf(\"%s | %s | %s | %s | %s | %s\", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param[\"ONE\"])\n\n\texpect.CopyToHost[0].GuestFile = fmt.Sprintf(\"%s | %s | %s | %s\", user.HomeDir, user.Uid, user.Username, y.Param[\"ONE\"])\n\texpect.CopyToHost[0].HostFile = fmt.Sprintf(\"%s | %s | %s | %s | %s | %s\", hostHome, instDir, instName, currentUser.Uid, currentUser.Username, y.Param[\"ONE\"])\n\n\texpect.Env = y.Env\n\n\texpect.Param = y.Param\n\n\texpect.CACertificates = limatype.CACertificates{\n\t\tRemoveDefaults: ptr.Of(false),\n\t\tFiles:          []string{\"ca.crt\"},\n\t\tCerts: []string{\n\t\t\t\"-----BEGIN CERTIFICATE-----\\nYOUR-ORGS-TRUSTED-CA-CERT\\n-----END CERTIFICATE-----\\n\",\n\t\t},\n\t}\n\n\texpect.TimeZone = y.TimeZone\n\t// Set firmware expectations to match what FillDefault actually does\n\t// FillDefault uses the builtin default values, which include LegacyBIOS: ptr.Of(false)\n\texpect.Firmware = limatype.Firmware{\n\t\tLegacyBIOS: ptr.Of(false), // This matches what FillDefault actually sets\n\t\tImages:     nil,\n\t}\n\n\texpect.NestedVirtualization = ptr.Of(false)\n\n\tFillDefault(t.Context(), &y, &limatype.LimaYAML{}, &limatype.LimaYAML{}, filePath, false)\n\tassert.DeepEqual(t, &y, &expect, opts...)\n\n\tfilledDefaults := y\n\n\t// ------------------------------------------------------------------------------------\n\t// User-provided defaults should override any builtin defaults\n\n\t// Choose values that are different from the \"builtin\" defaults\n\n\t// Calling filepath.Abs() to add a drive letter on Windows\n\tvarLog, _ := filepath.Abs(\"/var/log\")\n\td = limatype.LimaYAML{\n\t\t// Remove driver-specific VMType from defaults test\n\t\tOS:     ptr.Of(\"unknown\"),\n\t\tArch:   ptr.Of(\"unknown\"),\n\t\tCPUs:   ptr.Of(7),\n\t\tMemory: ptr.Of(\"5GiB\"),\n\t\tDisk:   ptr.Of(\"105GiB\"),\n\t\tAdditionalDisks: []limatype.Disk{\n\t\t\t{Name: \"data\"},\n\t\t},\n\t\tGuestInstallPrefix: ptr.Of(\"/opt\"),\n\t\tUpgradePackages:    ptr.Of(true),\n\t\tContainerd: limatype.Containerd{\n\t\t\tSystem: ptr.Of(true),\n\t\t\tUser:   ptr.Of(false),\n\t\t\tArchives: []limatype.File{\n\t\t\t\t{Location: \"/tmp/nerdctl.tgz\"},\n\t\t\t},\n\t\t},\n\t\tSSH: limatype.SSH{\n\t\t\tLocalPort:         ptr.Of(888),\n\t\t\tLoadDotSSHPubKeys: ptr.Of(false),\n\t\t\tForwardAgent:      ptr.Of(true),\n\t\t\tForwardX11:        ptr.Of(false),\n\t\t\tForwardX11Trusted: ptr.Of(false),\n\t\t},\n\t\tTimeZone: ptr.Of(\"Zulu\"),\n\t\tFirmware: limatype.Firmware{\n\t\t\tLegacyBIOS: ptr.Of(true),\n\t\t\t// Remove driver-specific firmware images from defaults\n\t\t},\n\t\tAudio: limatype.Audio{\n\t\t\tDevice: ptr.Of(\"coreaudio\"),\n\t\t},\n\t\tVideo: limatype.Video{\n\t\t\tDisplay: ptr.Of(\"cocoa\"),\n\t\t\t// Remove driver-specific VNC configuration\n\t\t},\n\t\tHostResolver: limatype.HostResolver{\n\t\t\tEnabled: ptr.Of(false),\n\t\t\tIPv6:    ptr.Of(true),\n\t\t\tHosts: map[string]string{\n\t\t\t\t\"default\": \"localhost\",\n\t\t\t},\n\t\t},\n\t\tPropagateProxyEnv: ptr.Of(false),\n\n\t\tMounts: []limatype.Mount{\n\t\t\t{\n\t\t\t\tLocation: varLog,\n\t\t\t\tWritable: ptr.Of(false),\n\t\t\t},\n\t\t},\n\t\tProvision: []limatype.Provision{\n\t\t\t{\n\t\t\t\tScript: ptr.Of(\"#!/bin/true\"),\n\t\t\t\tMode:   limatype.ProvisionModeUser,\n\t\t\t},\n\t\t},\n\t\tProbes: []limatype.Probe{\n\t\t\t{\n\t\t\t\tScript:      ptr.Of(\"#!/bin/false\"),\n\t\t\t\tMode:        limatype.ProbeModeReadiness,\n\t\t\t\tDescription: \"User Probe\",\n\t\t\t},\n\t\t},\n\t\tNetworks: []limatype.Network{\n\t\t\t{\n\t\t\t\tMACAddress: \"11:22:33:44:55:66\",\n\t\t\t\tInterface:  \"def0\",\n\t\t\t\tMetric:     ptr.Of(uint32(50)),\n\t\t\t},\n\t\t},\n\t\tDNS: []net.IP{\n\t\t\tnet.ParseIP(\"1.1.1.1\"),\n\t\t},\n\t\tPortForwards: []limatype.PortForward{{\n\t\t\tGuestIP:           IPv4loopback1,\n\t\t\tGuestIPMustBeZero: ptr.Of(false),\n\t\t\tGuestPort:         80,\n\t\t\tGuestPortRange:    [2]int{80, 80},\n\t\t\tHostIP:            IPv4loopback1,\n\t\t\tHostPort:          80,\n\t\t\tHostPortRange:     [2]int{80, 80},\n\t\t\tProto:             limatype.ProtoTCP,\n\t\t}},\n\t\tCopyToHost: []limatype.CopyToHost{{}},\n\t\tEnv: map[string]string{\n\t\t\t\"ONE\": \"one\",\n\t\t\t\"TWO\": \"two\",\n\t\t},\n\t\tParam: map[string]string{\n\t\t\t\"ONE\": \"one\",\n\t\t\t\"TWO\": \"two\",\n\t\t},\n\t\tCACertificates: limatype.CACertificates{\n\t\t\tRemoveDefaults: ptr.Of(true),\n\t\t\tCerts: []string{\n\t\t\t\t\"-----BEGIN CERTIFICATE-----\\nYOUR-ORGS-TRUSTED-CA-CERT\\n-----END CERTIFICATE-----\\n\",\n\t\t\t},\n\t\t},\n\t\tNestedVirtualization: ptr.Of(true),\n\t\tUser: limatype.User{\n\t\t\tName:    ptr.Of(\"xxx\"),\n\t\t\tComment: ptr.Of(\"Foo Bar\"),\n\t\t\tHome:    ptr.Of(\"/tmp\"),\n\t\t\tShell:   ptr.Of(\"/bin/tcsh\"),\n\t\t\tUID:     ptr.Of(uint32(8080)),\n\t\t},\n\t\tVMOpts: limatype.VMOpts{\n\t\t\t\"qemu\": map[string]any{\n\t\t\t\t\"minimumVersion\": \"9.1.0\",\n\t\t\t},\n\t\t},\n\t}\n\n\texpect = d\n\t// VMType should remain nil when not explicitly set\n\texpect.VMType = nil\n\t// Also verify that archive arch is filled in\n\texpect.Containerd.Archives = slices.Clone(d.Containerd.Archives)\n\texpect.Containerd.Archives[0].Arch = *d.Arch\n\texpect.Mounts = slices.Clone(d.Mounts)\n\texpect.Mounts[0].MountPoint = ptr.Of(expect.Mounts[0].Location)\n\tif runtime.GOOS == \"windows\" {\n\t\tmountLocation, err := ioutilx.WindowsSubsystemPath(t.Context(), expect.Mounts[0].Location)\n\t\tif err == nil {\n\t\t\texpect.Mounts[0].MountPoint = ptr.Of(mountLocation)\n\t\t}\n\t}\n\texpect.Mounts[0].SSHFS.Cache = ptr.Of(true)\n\texpect.Mounts[0].SSHFS.FollowSymlinks = ptr.Of(false)\n\texpect.Mounts[0].SSHFS.SFTPDriver = ptr.Of(\"\")\n\texpect.Mounts[0].NineP.SecurityModel = ptr.Of(Default9pSecurityModel)\n\texpect.Mounts[0].NineP.ProtocolVersion = ptr.Of(Default9pProtocolVersion)\n\texpect.Mounts[0].NineP.Msize = ptr.Of(Default9pMsize)\n\texpect.Mounts[0].NineP.Cache = ptr.Of(Default9pCacheForRO)\n\texpect.Mounts[0].Virtiofs.QueueSize = nil\n\texpect.HostResolver.Hosts = map[string]string{\n\t\t\"default\": d.HostResolver.Hosts[\"default\"],\n\t}\n\t// Remove driver-specific mount type from defaults test\n\texpect.MountType = nil\n\texpect.MountInotify = ptr.Of(false)\n\texpect.CACertificates.RemoveDefaults = ptr.Of(true)\n\texpect.CACertificates.Certs = []string{\n\t\t\"-----BEGIN CERTIFICATE-----\\nYOUR-ORGS-TRUSTED-CA-CERT\\n-----END CERTIFICATE-----\\n\",\n\t}\n\n\texpect.Plain = ptr.Of(false)\n\n\ty = limatype.LimaYAML{}\n\tFillDefault(t.Context(), &y, &d, &limatype.LimaYAML{}, filePath, false)\n\tassert.DeepEqual(t, &y, &expect, opts...)\n\n\tdExpected := expect\n\n\t// ------------------------------------------------------------------------------------\n\t// User-provided defaults should not override user-provided config values\n\n\ty = filledDefaults\n\ty.DNS = []net.IP{net.ParseIP(\"8.8.8.8\")}\n\ty.AdditionalDisks = []limatype.Disk{{Name: \"overridden\"}}\n\ty.User.Home = ptr.Of(\"/root\")\n\ty.VMOpts = limatype.VMOpts{\n\t\t\"vz\": map[string]any{\n\t\t\t\"diskImageFormat\": \"raw\",\n\t\t},\n\t}\n\n\texpect = y\n\n\texpect.Provision = slices.Concat(y.Provision, dExpected.Provision)\n\texpect.Probes = slices.Concat(y.Probes, dExpected.Probes)\n\texpect.PortForwards = slices.Concat(y.PortForwards, dExpected.PortForwards)\n\texpect.CopyToHost = slices.Concat(y.CopyToHost, dExpected.CopyToHost)\n\texpect.Containerd.Archives = slices.Concat(y.Containerd.Archives, dExpected.Containerd.Archives)\n\texpect.Containerd.Archives[2].Arch = *expect.Arch\n\texpect.AdditionalDisks = slices.Concat(y.AdditionalDisks, dExpected.AdditionalDisks)\n\texpect.Firmware.Images = slices.Concat(y.Firmware.Images, dExpected.Firmware.Images)\n\n\t// Mounts and Networks start with lowest priority first, so higher priority entries can overwrite\n\texpect.Mounts = slices.Concat(dExpected.Mounts, y.Mounts)\n\texpect.Networks = slices.Concat(dExpected.Networks, y.Networks)\n\n\texpect.HostResolver.Hosts[\"default\"] = dExpected.HostResolver.Hosts[\"default\"]\n\n\t// dExpected.DNS will be ignored, and not appended to y.DNS\n\n\t// \"TWO\" does not exist in filledDefaults.Env, so is set from dExpected.Env\n\texpect.Env[\"TWO\"] = dExpected.Env[\"TWO\"]\n\n\texpect.Param[\"TWO\"] = dExpected.Param[\"TWO\"]\n\n\texpect.VMOpts = limatype.VMOpts{\n\t\t\"qemu\": dExpected.VMOpts[\"qemu\"],\n\t\t\"vz\":   y.VMOpts[\"vz\"],\n\t}\n\n\tt.Logf(\"d.vmType=%v, y.vmType=%v, expect.vmType=%v\", d.VMType, y.VMType, expect.VMType)\n\n\tFillDefault(t.Context(), &y, &d, &limatype.LimaYAML{}, filePath, false)\n\tassert.DeepEqual(t, &y, &expect, opts...)\n\n\t// ------------------------------------------------------------------------------------\n\t// User-provided overrides should override user-provided config settings\n\n\to = limatype.LimaYAML{\n\t\t// Remove driver-specific VMType from override test\n\t\tOS:     ptr.Of(limatype.LINUX),\n\t\tArch:   ptr.Of(arch),\n\t\tCPUs:   ptr.Of(12),\n\t\tMemory: ptr.Of(\"7GiB\"),\n\t\tDisk:   ptr.Of(\"117GiB\"),\n\t\tAdditionalDisks: []limatype.Disk{\n\t\t\t{Name: \"test\"},\n\t\t},\n\t\tGuestInstallPrefix: ptr.Of(\"/usr\"),\n\t\tUpgradePackages:    ptr.Of(true),\n\t\tContainerd: limatype.Containerd{\n\t\t\tSystem: ptr.Of(true),\n\t\t\tUser:   ptr.Of(false),\n\t\t\tArchives: []limatype.File{\n\t\t\t\t{\n\t\t\t\t\tArch:     arch,\n\t\t\t\t\tLocation: \"/tmp/nerdctl.tgz\",\n\t\t\t\t\tDigest:   \"$DIGEST\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tSSH: limatype.SSH{\n\t\t\tLocalPort:         ptr.Of(4433),\n\t\t\tLoadDotSSHPubKeys: ptr.Of(true),\n\t\t\tForwardAgent:      ptr.Of(true),\n\t\t\tForwardX11:        ptr.Of(false),\n\t\t\tForwardX11Trusted: ptr.Of(false),\n\t\t},\n\t\tTimeZone: ptr.Of(\"Universal\"),\n\t\tFirmware: limatype.Firmware{\n\t\t\tLegacyBIOS: ptr.Of(true),\n\t\t},\n\t\tAudio: limatype.Audio{\n\t\t\tDevice: ptr.Of(\"coreaudio\"),\n\t\t},\n\t\tVideo: limatype.Video{\n\t\t\tDisplay: ptr.Of(\"cocoa\"),\n\t\t\t// Remove driver-specific VNC configuration\n\t\t},\n\t\tHostResolver: limatype.HostResolver{\n\t\t\tEnabled: ptr.Of(false),\n\t\t\tIPv6:    ptr.Of(false),\n\t\t\tHosts: map[string]string{\n\t\t\t\t\"override.\": \"underflow\",\n\t\t\t},\n\t\t},\n\t\tPropagateProxyEnv: ptr.Of(false),\n\n\t\tMounts: []limatype.Mount{\n\t\t\t{\n\t\t\t\tLocation: varLog,\n\t\t\t\tWritable: ptr.Of(true),\n\t\t\t\tSSHFS: limatype.SSHFS{\n\t\t\t\t\tCache:          ptr.Of(false),\n\t\t\t\t\tFollowSymlinks: ptr.Of(true),\n\t\t\t\t},\n\t\t\t\tNineP: limatype.NineP{\n\t\t\t\t\tSecurityModel:   ptr.Of(\"mapped-file\"),\n\t\t\t\t\tProtocolVersion: ptr.Of(\"9p2000\"),\n\t\t\t\t\tMsize:           ptr.Of(\"8KiB\"),\n\t\t\t\t\tCache:           ptr.Of(\"none\"),\n\t\t\t\t},\n\t\t\t\tVirtiofs: limatype.Virtiofs{\n\t\t\t\t\tQueueSize: ptr.Of(2048),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMountInotify: ptr.Of(true),\n\t\tProvision: []limatype.Provision{\n\t\t\t{\n\t\t\t\tScript: ptr.Of(\"#!/bin/true\"),\n\t\t\t\tMode:   limatype.ProvisionModeSystem,\n\t\t\t},\n\t\t},\n\t\tProbes: []limatype.Probe{\n\t\t\t{\n\t\t\t\tScript:      ptr.Of(\"#!/bin/false\"),\n\t\t\t\tMode:        limatype.ProbeModeReadiness,\n\t\t\t\tDescription: \"Another Probe\",\n\t\t\t},\n\t\t},\n\t\tNetworks: []limatype.Network{\n\t\t\t{\n\t\t\t\tLima:       \"shared\",\n\t\t\t\tMACAddress: \"10:20:30:40:50:60\",\n\t\t\t\tInterface:  \"def1\",\n\t\t\t\tMetric:     ptr.Of(uint32(25)),\n\t\t\t},\n\t\t\t{\n\t\t\t\tLima:      \"bridged\",\n\t\t\t\tInterface: \"def0\",\n\t\t\t},\n\t\t},\n\t\tDNS: []net.IP{\n\t\t\tnet.ParseIP(\"2.2.2.2\"),\n\t\t},\n\t\tPortForwards: []limatype.PortForward{{\n\t\t\tGuestIP:           IPv4loopback1,\n\t\t\tGuestIPMustBeZero: ptr.Of(false),\n\t\t\tGuestPort:         88,\n\t\t\tGuestPortRange:    [2]int{88, 88},\n\t\t\tHostIP:            IPv4loopback1,\n\t\t\tHostPort:          8080,\n\t\t\tHostPortRange:     [2]int{8080, 8080},\n\t\t\tProto:             limatype.ProtoTCP,\n\t\t}},\n\t\tCopyToHost: []limatype.CopyToHost{{}},\n\t\tEnv: map[string]string{\n\t\t\t\"TWO\":   \"deux\",\n\t\t\t\"THREE\": \"trois\",\n\t\t},\n\t\tParam: map[string]string{\n\t\t\t\"TWO\":   \"deux\",\n\t\t\t\"THREE\": \"trois\",\n\t\t},\n\t\tCACertificates: limatype.CACertificates{\n\t\t\tRemoveDefaults: ptr.Of(true),\n\t\t},\n\t\tNestedVirtualization: ptr.Of(false),\n\t\tUser: limatype.User{\n\t\t\tName:    ptr.Of(\"foo\"),\n\t\t\tComment: ptr.Of(\"foo bar baz\"),\n\t\t\tHome:    ptr.Of(\"/override\"),\n\t\t\tShell:   ptr.Of(\"/bin/sh\"),\n\t\t\tUID:     ptr.Of(uint32(1122)),\n\t\t},\n\t\tVMOpts: limatype.VMOpts{\n\t\t\t\"vz\": map[string]any{\n\t\t\t\t\"rosetta\": map[string]any{\n\t\t\t\t\t\"enabled\": true,\n\t\t\t\t\t\"binfmt\":  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\ty = filledDefaults\n\n\texpect = o\n\n\texpect.Provision = slices.Concat(o.Provision, y.Provision, dExpected.Provision)\n\texpect.Probes = slices.Concat(o.Probes, y.Probes, dExpected.Probes)\n\texpect.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, dExpected.PortForwards)\n\texpect.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, dExpected.CopyToHost)\n\texpect.Containerd.Archives = slices.Concat(o.Containerd.Archives, y.Containerd.Archives, dExpected.Containerd.Archives)\n\texpect.Containerd.Archives[3].Arch = *expect.Arch\n\texpect.AdditionalDisks = slices.Concat(o.AdditionalDisks, y.AdditionalDisks, dExpected.AdditionalDisks)\n\texpect.Firmware.Images = slices.Concat(o.Firmware.Images, y.Firmware.Images, dExpected.Firmware.Images)\n\n\texpect.HostResolver.Hosts[\"default\"] = dExpected.HostResolver.Hosts[\"default\"]\n\texpect.HostResolver.Hosts[\"MY.Host\"] = dExpected.HostResolver.Hosts[\"host.lima.internal\"]\n\n\t// o.Mounts just makes dExpected.Mounts[0] writable because the Location matches\n\texpect.Mounts = slices.Concat(dExpected.Mounts, y.Mounts)\n\texpect.Mounts[0].Writable = ptr.Of(true)\n\texpect.Mounts[0].SSHFS.Cache = ptr.Of(false)\n\texpect.Mounts[0].SSHFS.FollowSymlinks = ptr.Of(true)\n\texpect.Mounts[0].NineP.SecurityModel = ptr.Of(\"mapped-file\")\n\texpect.Mounts[0].NineP.ProtocolVersion = ptr.Of(\"9p2000\")\n\texpect.Mounts[0].NineP.Msize = ptr.Of(\"8KiB\")\n\texpect.Mounts[0].NineP.Cache = ptr.Of(\"none\")\n\texpect.Mounts[0].Virtiofs.QueueSize = ptr.Of(2048)\n\n\texpect.MountType = ptr.Of(limatype.NINEP)\n\texpect.MountInotify = ptr.Of(true)\n\n\t// o.Networks[1] is overriding the dExpected.Networks[0].Lima entry for the \"def0\" interface\n\texpect.Networks = slices.Concat(dExpected.Networks, y.Networks, []limatype.Network{o.Networks[0]})\n\texpect.Networks[0].Lima = o.Networks[1].Lima\n\n\t// Only highest prio DNS are retained\n\texpect.DNS = slices.Clone(o.DNS)\n\n\t// ONE remains from filledDefaults.Env; the rest are set from o\n\texpect.Env[\"ONE\"] = y.Env[\"ONE\"]\n\n\texpect.Param[\"ONE\"] = y.Param[\"ONE\"]\n\n\texpect.CACertificates.RemoveDefaults = ptr.Of(true)\n\texpect.CACertificates.Files = []string{\"ca.crt\"}\n\texpect.CACertificates.Certs = []string{\n\t\t\"-----BEGIN CERTIFICATE-----\\nYOUR-ORGS-TRUSTED-CA-CERT\\n-----END CERTIFICATE-----\\n\",\n\t}\n\n\texpect.Plain = ptr.Of(false)\n\n\texpect.NestedVirtualization = ptr.Of(false)\n\n\texpect.VMOpts = limatype.VMOpts{\n\t\t\"qemu\": dExpected.VMOpts[\"qemu\"],\n\t\t\"vz\": map[string]any{\n\t\t\t\"rosetta\": map[string]any{\n\t\t\t\t\"enabled\": true,\n\t\t\t\t\"binfmt\":  true,\n\t\t\t},\n\t\t},\n\t}\n\n\tFillDefault(t.Context(), &y, &d, &o, filePath, false)\n\tassert.DeepEqual(t, &y, &expect, opts...)\n}\n\nfunc TestContainerdDefault(t *testing.T) {\n\tarchives := defaultContainerdArchives()\n\tassert.Assert(t, len(archives) > 0)\n}\n\nfunc TestStaticPortForwarding(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tconfig   limatype.LimaYAML\n\t\texpected []limatype.PortForward\n\t}{\n\t\t{\n\t\t\tname: \"plain mode with static port forwards\",\n\t\t\tconfig: limatype.LimaYAML{\n\t\t\t\tPlain: ptr.Of(true),\n\t\t\t\tPortForwards: []limatype.PortForward{\n\t\t\t\t\t{\n\t\t\t\t\t\tGuestPort: 8080,\n\t\t\t\t\t\tHostPort:  8080,\n\t\t\t\t\t\tStatic:    true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tGuestPort: 9000,\n\t\t\t\t\t\tHostPort:  9000,\n\t\t\t\t\t\tStatic:    false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tGuestPort: 8081,\n\t\t\t\t\t\tHostPort:  8081,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []limatype.PortForward{\n\t\t\t\t{\n\t\t\t\t\tGuestPort: 8080,\n\t\t\t\t\tHostPort:  8080,\n\t\t\t\t\tStatic:    true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"non-plain mode with static port forwards\",\n\t\t\tconfig: limatype.LimaYAML{\n\t\t\t\tPlain: ptr.Of(false),\n\t\t\t\tPortForwards: []limatype.PortForward{\n\t\t\t\t\t{\n\t\t\t\t\t\tGuestPort: 8080,\n\t\t\t\t\t\tHostPort:  8080,\n\t\t\t\t\t\tStatic:    true,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tGuestPort: 9000,\n\t\t\t\t\t\tHostPort:  9000,\n\t\t\t\t\t\tStatic:    false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []limatype.PortForward{\n\t\t\t\t{\n\t\t\t\t\tGuestPort: 8080,\n\t\t\t\t\tHostPort:  8080,\n\t\t\t\t\tStatic:    true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGuestPort: 9000,\n\t\t\t\t\tHostPort:  9000,\n\t\t\t\t\tStatic:    false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"plain mode with no static port forwards\",\n\t\t\tconfig: limatype.LimaYAML{\n\t\t\t\tPlain: ptr.Of(true),\n\t\t\t\tPortForwards: []limatype.PortForward{\n\t\t\t\t\t{\n\t\t\t\t\t\tGuestPort: 8080,\n\t\t\t\t\t\tHostPort:  8080,\n\t\t\t\t\t\tStatic:    false,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tGuestPort: 9000,\n\t\t\t\t\t\tHostPort:  9000,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []limatype.PortForward{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfixUpForPlainMode(&tt.config)\n\n\t\t\tif *tt.config.Plain {\n\t\t\t\tfor _, pf := range tt.config.PortForwards {\n\t\t\t\t\tif !pf.Static {\n\t\t\t\t\t\tt.Errorf(\"Non-static port forward found in plain mode: %+v\", pf)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Equal(t, len(tt.config.PortForwards), len(tt.expected),\n\t\t\t\t\"Expected %d port forwards, got %d\", len(tt.expected), len(tt.config.PortForwards))\n\n\t\t\tfor i, expected := range tt.expected {\n\t\t\t\tif i >= len(tt.config.PortForwards) {\n\t\t\t\t\tt.Errorf(\"Missing port forward at index %d\", i)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tactual := tt.config.PortForwards[i]\n\t\t\t\tassert.Equal(t, expected.Static, actual.Static,\n\t\t\t\t\t\"Port forward %d: expected Static=%v, got %v\", i, expected.Static, actual.Static)\n\t\t\t\tassert.Equal(t, expected.GuestPort, actual.GuestPort,\n\t\t\t\t\t\"Port forward %d: expected GuestPort=%d, got %d\", i, expected.GuestPort, actual.GuestPort)\n\t\t\t\tassert.Equal(t, expected.HostPort, actual.HostPort,\n\t\t\t\t\t\"Port forward %d: expected HostPort=%d, got %d\", i, expected.HostPort, actual.HostPort)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMountTag(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tlocation   string\n\t\tmountPoint string\n\t}{\n\t\t{\"home directory\", \"/Users/testuser\", \"/Users/testuser\"},\n\t\t{\"nested path\", \"/Users/testuser/Development/project\", \"/mnt/project\"},\n\t\t{\"root path\", \"/\", \"/mnt/root\"},\n\t\t{\"tmp directory\", \"/tmp/lima\", \"/tmp/lima\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttag := MountTag(tt.location, tt.mountPoint)\n\t\t\tassert.Assert(t, len(tag) > 5 && tag[:5] == \"lima-\")\n\t\t\tassert.Assert(t, len(tag) <= 36)\n\t\t\tassert.Equal(t, tag, MountTag(tt.location, tt.mountPoint))\n\t\t})\n\t}\n}\n\nfunc TestMountTagUniqueness(t *testing.T) {\n\tmounts := []struct{ location, mountPoint string }{\n\t\t{\"/Users/user1\", \"/Users/user1\"},\n\t\t{\"/Users/user2\", \"/Users/user2\"},\n\t\t{\"/home/user1\", \"/home/user1\"},\n\t\t{\"/mnt/data\", \"/mnt/data\"},\n\t\t{\"/tmp/lima\", \"/tmp/lima\"},\n\t}\n\ttags := make(map[string]bool)\n\tfor _, m := range mounts {\n\t\ttag := MountTag(m.location, m.mountPoint)\n\t\tassert.Assert(t, !tags[tag], \"tag collision: %s\", tag)\n\t\ttags[tag] = true\n\t}\n}\n\nfunc TestMountTagDuplicateLocation(t *testing.T) {\n\tlocation := \"/Users/testuser\"\n\ttag1 := MountTag(location, \"/home/user\")\n\ttag2 := MountTag(location, \"/mnt/home-writable\")\n\tassert.Assert(t, tag1 != tag2)\n}\n"
  },
  {
    "path": "pkg/limayaml/defaults_unix.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc hostTimeZone() string {\n\tif tzBytes, err := os.ReadFile(\"/etc/timezone\"); err == nil {\n\t\tif tz := strings.TrimSpace(string(tzBytes)); tz != \"\" {\n\t\t\tif _, err := time.LoadLocation(tz); err != nil {\n\t\t\t\tlogrus.Warnf(\"invalid timezone found in /etc/timezone: %v\", err)\n\t\t\t} else {\n\t\t\t\treturn tz\n\t\t\t}\n\t\t}\n\t}\n\n\tif zoneinfoFile, err := filepath.EvalSymlinks(\"/etc/localtime\"); err == nil {\n\t\tif tz, err := extractTZFromPath(zoneinfoFile); err != nil {\n\t\t\tlogrus.Warnf(\"failed to extract timezone from %s: %v\", zoneinfoFile, err)\n\t\t} else {\n\t\t\treturn tz\n\t\t}\n\t}\n\n\tlogrus.Warn(\"unable to determine host timezone, falling back to default value\")\n\treturn \"\"\n}\n\nfunc extractTZFromPath(zoneinfoFile string) (string, error) {\n\tif zoneinfoFile == \"\" {\n\t\treturn \"\", errors.New(\"invalid zoneinfo file path\")\n\t}\n\n\tif _, err := os.Stat(zoneinfoFile); os.IsNotExist(err) {\n\t\treturn \"\", fmt.Errorf(\"zoneinfo file does not exist: %s\", zoneinfoFile)\n\t}\n\n\tfor dir := filepath.Dir(zoneinfoFile); dir != filepath.Dir(dir); dir = filepath.Dir(dir) {\n\t\tif _, err := os.Stat(filepath.Join(dir, \"Etc\", \"UTC\")); err == nil {\n\t\t\treturn filepath.Rel(dir, zoneinfoFile)\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"timezone base directory not found\")\n}\n"
  },
  {
    "path": "pkg/limayaml/defaults_unix_test.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestExtractTimezoneFromPath(t *testing.T) {\n\ttmpDir := t.TempDir()\n\n\t// Create test timezone directory structure\n\tassert.NilError(t, os.MkdirAll(filepath.Join(tmpDir, \"Etc\"), 0o755))\n\tassert.NilError(t, os.WriteFile(filepath.Join(tmpDir, \"Etc\", \"UTC\"), []byte{}, 0o644))\n\tassert.NilError(t, os.WriteFile(filepath.Join(tmpDir, \"UTC\"), []byte{}, 0o644))\n\tassert.NilError(t, os.MkdirAll(filepath.Join(tmpDir, \"Antarctica\"), 0o755))\n\tassert.NilError(t, os.WriteFile(filepath.Join(tmpDir, \"Antarctica\", \"Troll\"), []byte{}, 0o644))\n\n\ttests := []struct {\n\t\tname    string\n\t\tpath    string\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\t\"valid_timezone\",\n\t\t\tfilepath.Join(tmpDir, \"Antarctica\", \"Troll\"),\n\t\t\t\"Antarctica/Troll\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"root_level_zone\",\n\t\t\tfilepath.Join(tmpDir, \"UTC\"),\n\t\t\t\"UTC\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"outside_zoneinfo\",\n\t\t\t\"/tmp/somefile\",\n\t\t\t\"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"empty_path\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"nonexistent_file\",\n\t\t\tfilepath.Join(tmpDir, \"Invalid\", \"Zone\"),\n\t\t\t\"\",\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := extractTZFromPath(tt.path)\n\t\t\tif tt.wantErr {\n\t\t\t\tassert.Assert(t, err != nil, \"expected error but got none\")\n\t\t\t} else {\n\t\t\t\tassert.NilError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/limayaml/defaults_windows.go",
    "content": "//go:build windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nfunc hostTimeZone() string {\n\t// WSL2 will automatically set the timezone\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/limayaml/limayaml_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nfunc dumpJSON(t *testing.T, d any) string {\n\tb, err := json.Marshal(d)\n\tassert.NilError(t, err)\n\treturn string(b)\n}\n\nconst emptyYAML = \"{}\\n\"\n\nfunc TestEmptyYAML(t *testing.T) {\n\tvar y limatype.LimaYAML\n\tt.Log(dumpJSON(t, y))\n\tb, err := Marshal(&y, false)\n\tassert.NilError(t, err)\n\tassert.Equal(t, string(b), emptyYAML)\n}\n\nconst defaultYAML = \"{}\\n\"\n\nfunc TestDefaultYAML(t *testing.T) {\n\tcontent, err := os.ReadFile(\"default.yaml\")\n\tassert.NilError(t, err)\n\t// if this is the unresolved symlink as a file, then make sure to resolve it\n\tif runtime.GOOS == \"windows\" && bytes.HasPrefix(content, []byte{'.', '.'}) {\n\t\tf, err := filepath.Rel(\".\", string(content))\n\t\tassert.NilError(t, err)\n\t\tcontent, err = os.ReadFile(f)\n\t\tassert.NilError(t, err)\n\t}\n\n\tvar y limatype.LimaYAML\n\terr = Unmarshal(content, &y, \"\")\n\tassert.NilError(t, err)\n\ty.Images = nil                // remove default images\n\ty.Mounts = nil                // remove default mounts\n\ty.Base = nil                  // remove default base templates\n\ty.VMOpts = nil                // remove default vmopts mapping\n\ty.MinimumLimaVersion = nil    // remove minimum Lima version\n\ty.MountTypesUnsupported = nil // remove default workaround for kernel 6.9-6.11\n\tt.Log(dumpJSON(t, y))\n\tb, err := Marshal(&y, false)\n\tassert.NilError(t, err)\n\tassert.Equal(t, string(b), defaultYAML)\n}\n"
  },
  {
    "path": "pkg/limayaml/load.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\n// Load loads the yaml and fulfills unspecified fields with the default values.\n//\n// Load does not validate. Use Validate for validation.\nfunc Load(ctx context.Context, b []byte, filePath string) (*limatype.LimaYAML, error) {\n\treturn load(ctx, b, filePath, false)\n}\n\n// LoadWithWarnings will call FillDefaults with warnings enabled (e.g. when\n// the username is not valid on Linux and must be replaced by \"Lima\").\n// It is called when creating or editing an instance.\nfunc LoadWithWarnings(ctx context.Context, b []byte, filePath string) (*limatype.LimaYAML, error) {\n\treturn load(ctx, b, filePath, true)\n}\n\nfunc load(ctx context.Context, b []byte, filePath string, warn bool) (*limatype.LimaYAML, error) {\n\tvar y, d, o limatype.LimaYAML\n\n\tif err := Unmarshal(b, &y, fmt.Sprintf(\"main file %q\", filePath)); err != nil {\n\t\treturn nil, err\n\t}\n\tconfigDir, err := dirnames.LimaConfigDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefaultPath := filepath.Join(configDir, filenames.Default)\n\tbytes, err := os.ReadFile(defaultPath)\n\tif err == nil {\n\t\tlogrus.Debugf(\"Mixing %q into %q\", defaultPath, filePath)\n\t\tif err := Unmarshal(bytes, &d, fmt.Sprintf(\"default file %q\", defaultPath)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn nil, err\n\t}\n\n\toverridePath := filepath.Join(configDir, filenames.Override)\n\tbytes, err = os.ReadFile(overridePath)\n\tif err == nil {\n\t\tlogrus.Debugf(\"Mixing %q into %q\", overridePath, filePath)\n\t\tif err := Unmarshal(bytes, &o, fmt.Sprintf(\"override file %q\", overridePath)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\treturn nil, err\n\t}\n\n\t// It should be called before the `y` parameter is passed to FillDefault() that execute template.\n\tif err := validateParamIsUsed(&y); err != nil {\n\t\treturn nil, err\n\t}\n\n\tFillDefault(ctx, &y, &d, &o, filePath, warn)\n\n\treturn &y, nil\n}\n"
  },
  {
    "path": "pkg/limayaml/load_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestLoadEmpty(t *testing.T) {\n\t_, err := Load(t.Context(), []byte{}, \"empty.yaml\")\n\tassert.NilError(t, err)\n}\n\nfunc TestLoadError(t *testing.T) {\n\t// missing a \"script:\" line\n\ts := `\nprovision:\n- mode: system\n  script: |\n    #!/bin/sh\n    echo one\n- mode: system\n    #!/bin/sh\n    echo two\n- mode: system\n  script: |\n    #!/bin/sh\n    echo three\n`\n\t_, err := Load(t.Context(), []byte(s), \"error.yaml\")\n\tassert.ErrorContains(t, err, \"map key-value is pre-defined\")\n}\n\nfunc TestLoadDiskString(t *testing.T) {\n\ts := `\nadditionalDisks:\n- name\n`\n\ty, err := Load(t.Context(), []byte(s), \"disk.yaml\")\n\tassert.NilError(t, err)\n\tassert.Equal(t, len(y.AdditionalDisks), 1)\n\tassert.Equal(t, y.AdditionalDisks[0].Name, \"name\")\n\tassert.Assert(t, y.AdditionalDisks[0].Format == nil)\n\tassert.Assert(t, y.AdditionalDisks[0].FSType == nil)\n\tassert.Assert(t, y.AdditionalDisks[0].FSArgs == nil)\n}\n\nfunc TestLoadDiskStruct(t *testing.T) {\n\ts := `\nadditionalDisks:\n- name: \"name\"\n  format: false\n  fsType: \"xfs\"\n  fsArgs: [\"-i\",\"size=512\"]\n`\n\ty, err := Load(t.Context(), []byte(s), \"disk.yaml\")\n\tassert.NilError(t, err)\n\tassert.Assert(t, len(y.AdditionalDisks) == 1)\n\tassert.Equal(t, y.AdditionalDisks[0].Name, \"name\")\n\tassert.Assert(t, y.AdditionalDisks[0].Format != nil)\n\tassert.Equal(t, *y.AdditionalDisks[0].Format, false)\n\tassert.Assert(t, y.AdditionalDisks[0].FSType != nil)\n\tassert.Equal(t, *y.AdditionalDisks[0].FSType, \"xfs\")\n\tassert.Assert(t, len(y.AdditionalDisks[0].FSArgs) == 2)\n\tassert.Equal(t, y.AdditionalDisks[0].FSArgs[0], \"-i\")\n\tassert.Equal(t, y.AdditionalDisks[0].FSArgs[1], \"size=512\")\n}\n"
  },
  {
    "path": "pkg/limayaml/marshal.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/yqutil\"\n)\n\nconst (\n\tdocumentStart = \"---\\n\"\n\tdocumentEnd   = \"...\\n\"\n)\n\n// Marshal the struct as a YAML document, optionally as a stream.\nfunc Marshal(y *limatype.LimaYAML, stream bool) ([]byte, error) {\n\tb, err := yaml.Marshal(y)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif stream {\n\t\tdoc := documentStart + string(b) + documentEnd\n\t\tb = []byte(doc)\n\t}\n\treturn b, nil\n}\n\nfunc unmarshalDisk(dst *limatype.Disk, b []byte) error {\n\tvar s string\n\tif err := yaml.Unmarshal(b, &s); err == nil {\n\t\t*dst = limatype.Disk{Name: s}\n\t\treturn nil\n\t}\n\treturn yaml.Unmarshal(b, dst)\n}\n\n// unmarshalBaseTemplates unmarshalls `base` which is either a string or a list of Locators.\nfunc unmarshalBaseTemplates(dst *limatype.BaseTemplates, b []byte) error {\n\tvar s string\n\tif err := yaml.Unmarshal(b, &s); err == nil {\n\t\t*dst = limatype.BaseTemplates{limatype.LocatorWithDigest{URL: s}}\n\t\treturn nil\n\t}\n\treturn yaml.UnmarshalWithOptions(b, dst, yaml.CustomUnmarshaler[limatype.LocatorWithDigest](unmarshalLocatorWithDigest))\n}\n\n// unmarshalLocator unmarshalls a locator which is either a string or a Locator struct.\nfunc unmarshalLocatorWithDigest(dst *limatype.LocatorWithDigest, b []byte) error {\n\tvar s string\n\tif err := yaml.Unmarshal(b, &s); err == nil {\n\t\t*dst = limatype.LocatorWithDigest{URL: s}\n\t\treturn nil\n\t}\n\treturn yaml.Unmarshal(b, dst)\n}\n\nfunc Unmarshal(data []byte, y *limatype.LimaYAML, comment string) error {\n\topts := []yaml.DecodeOption{\n\t\tyaml.CustomUnmarshaler[limatype.BaseTemplates](unmarshalBaseTemplates),\n\t\tyaml.CustomUnmarshaler[limatype.Disk](unmarshalDisk),\n\t\tyaml.CustomUnmarshaler[limatype.LocatorWithDigest](unmarshalLocatorWithDigest),\n\t}\n\tif err := yaml.UnmarshalWithOptions(data, y, opts...); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal YAML (%s): %w\", comment, err)\n\t}\n\t// The go-yaml library doesn't catch all markup errors, unfortunately\n\t// make sure to get a \"second opinion\", using the same library as \"yq\"\n\tif err := yqutil.ValidateContent(data); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal YAML (%s): %w\", comment, err)\n\t}\n\t// Finally log a warning if the YAML file violates the \"strict\" rules\n\topts = append(opts, yaml.Strict())\n\tvar ignore limatype.LimaYAML\n\tif err := yaml.UnmarshalWithOptions(data, &ignore, opts...); err != nil {\n\t\tlogrus.WithField(\"comment\", comment).WithError(err).Warn(\"Non-strict YAML detected; please check for typos\")\n\t}\n\treturn nil\n}\n\n// Convert converts from x to y, using YAML.\n// If x is nil, then y is left unmodified.\nfunc Convert(x, y any, comment string) error {\n\tif x == nil {\n\t\treturn nil\n\t}\n\tb, err := yaml.Marshal(x)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = yaml.Unmarshal(b, y)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal YAML (%s): %w\", comment, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/limayaml/marshal_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"text/template\"\n\n\t\"github.com/goccy/go-yaml\"\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n)\n\nfunc dumpYAML(t *testing.T, d any) string {\n\tb, err := yaml.Marshal(d)\n\tassert.NilError(t, err)\n\treturn string(b)\n}\n\nfunc TestMarshalEmpty(t *testing.T) {\n\t_, err := Marshal(&limatype.LimaYAML{}, false)\n\tassert.NilError(t, err)\n}\n\nfunc TestMarshalTilde(t *testing.T) {\n\ty := limatype.LimaYAML{\n\t\tMounts: []limatype.Mount{\n\t\t\t{Location: \"~\", Writable: ptr.Of(false)},\n\t\t\t{Location: \"/tmp/lima\", Writable: ptr.Of(true)},\n\t\t\t{Location: \"null\"},\n\t\t},\n\t}\n\tb, err := Marshal(&y, true)\n\tassert.NilError(t, err)\n\t// yaml will load ~ (or null) as null\n\t// make sure that it is always quoted\n\tassert.Equal(t, string(b), `---\nmounts:\n- location: \"~\"\n  writable: false\n- location: /tmp/lima\n  writable: true\n- location: \"null\"\n...\n`)\n}\n\ntype Opts struct {\n\tFoo int\n\tBar string\n}\n\nvar (\n\topts = Opts{Foo: 1, Bar: \"two\"}\n\ttext = `{\"foo\":1,\"bar\":\"two\"}`\n\tcode any\n)\n\nfunc TestConvert(t *testing.T) {\n\terr := yaml.Unmarshal([]byte(text), &code)\n\tassert.NilError(t, err)\n\to := opts\n\tvar a any\n\terr = Convert(o, &a, \"\")\n\tassert.NilError(t, err)\n\tassert.DeepEqual(t, a, code)\n\terr = Convert(a, &o, \"\")\n\tassert.NilError(t, err)\n\tassert.Equal(t, o, opts)\n}\n\nfunc TestVMOpts(t *testing.T) {\n\ttext := `\nvmType: null\n`\n\tvar y limatype.LimaYAML\n\terr := Unmarshal([]byte(text), &y, \"lima.yaml\")\n\tassert.NilError(t, err)\n\tvar o limatype.VMOpts\n\terr = Convert(y.VMOpts, &o, \"vmOpts\")\n\tassert.NilError(t, err)\n\tt.Log(dumpYAML(t, o))\n}\n\nfunc TestQEMUOpts(t *testing.T) {\n\ttext := `\nvmType: \"qemu\"\nvmOpts:\n  qemu:\n    minimumVersion: null\n    cpuType:\n`\n\tvar y limatype.LimaYAML\n\terr := Unmarshal([]byte(text), &y, \"lima.yaml\")\n\tassert.NilError(t, err)\n\tvar o limatype.QEMUOpts\n\terr = Convert(y.VMOpts[limatype.QEMU], &o, \"vmOpts.qemu\")\n\tassert.NilError(t, err)\n\tt.Log(dumpYAML(t, o))\n}\n\nfunc TestVZOpts(t *testing.T) {\n\ttext := `\nvmType: \"vz\"\nvmOpts:\n  vz:\n    diskImageFormat: null\n    rosetta:\n      enabled: null\n      binfmt: null\n`\n\tvar y limatype.LimaYAML\n\terr := Unmarshal([]byte(text), &y, \"lima.yaml\")\n\tassert.NilError(t, err)\n\tvar o limatype.VZOpts\n\terr = Convert(y.VMOpts[limatype.VZ], &o, \"vmOpts.vz\")\n\tassert.NilError(t, err)\n\tt.Log(dumpYAML(t, o))\n}\n\nfunc TestVMOptsNull(t *testing.T) {\n\ttext := `\nvmOpts: null\n`\n\tvar y limatype.LimaYAML\n\terr := Unmarshal([]byte(text), &y, \"lima.yaml\")\n\tassert.NilError(t, err)\n\tvar o limatype.VMOpts\n\terr = Convert(y.VMOpts, &o, \"vmOpts\")\n\tassert.NilError(t, err)\n\tvar oq limatype.QEMUOpts\n\terr = Convert(y.VMOpts[limatype.QEMU], &oq, \"vmOpts.qemu\")\n\tassert.NilError(t, err)\n\tvar ov limatype.VZOpts\n\terr = Convert(y.VMOpts[limatype.VZ], &ov, \"vmOpts.vz\")\n\tassert.NilError(t, err)\n}\n\ntype FormatData struct {\n\tlimatype.Instance `yaml:\",inline\"`\n}\n\nfunc TestVZOptsRosettaMessage(t *testing.T) {\n\ttext := `\nvmType: \"vz\"\nvmOpts:\n  vz:\n    diskImageFormat: \"raw\"\n    rosetta:\n      enabled: true\n      binfmt: false\n\nmessage: |\n  {{- if .Instance.Config.VMOpts.vz.rosetta.enabled}}\n  Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon.\n  {{- end}}\n`\n\twant := `vmType: vz\nvmOpts:\n  vz:\n    diskImageFormat: raw\n    rosetta:\n      binfmt: false\n      enabled: true\nmessage: |` + \"\\n  \\n\" + // goccy/go-yaml indents blank lines in block scalars\n\t\t`  Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon.\n`\n\tvar y limatype.LimaYAML\n\terr := Unmarshal([]byte(text), &y, \"lima.yaml\")\n\tassert.NilError(t, err)\n\ttmpl, err := template.New(\"format\").Parse(y.Message)\n\tassert.NilError(t, err)\n\tinst := limatype.Instance{Config: &y}\n\tvar message strings.Builder\n\tdata := FormatData{Instance: inst}\n\terr = tmpl.Execute(&message, data)\n\tassert.NilError(t, err)\n\ty.Message = message.String()\n\tb, err := Marshal(&y, false)\n\tassert.NilError(t, err)\n\tassert.Equal(t, string(b), want)\n}\n"
  },
  {
    "path": "pkg/limayaml/validate.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/identifiers\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/localpathutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n\t\"github.com/lima-vm/lima/v2/pkg/version/versionutil\"\n)\n\nfunc Validate(y *limatype.LimaYAML, warn bool) error {\n\tvar errs error\n\n\tif len(y.Base) > 0 {\n\t\terrs = errors.Join(errs, errors.New(\"field `base` must be empty for YAML validation\"))\n\t}\n\n\tif y.MinimumLimaVersion != nil {\n\t\tif _, err := versionutil.Parse(*y.MinimumLimaVersion); err != nil {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `minimumLimaVersion` must be a semvar value, got %q: %w\", *y.MinimumLimaVersion, err))\n\t\t}\n\t\t// Unparsable version.Version (like commit hashes or \"<unknown>\") is treated as \"latest/greatest\"\n\t\t// and will pass all version comparisons, allowing development builds to work.\n\t\tif !versionutil.GreaterEqual(version.Version, *y.MinimumLimaVersion) {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"template requires Lima version %q; this is only %q\", *y.MinimumLimaVersion, version.Version))\n\t\t}\n\t}\n\n\tswitch *y.OS {\n\tcase limatype.LINUX, limatype.DARWIN, limatype.FREEBSD:\n\tdefault:\n\t\terrs = errors.Join(errs, fmt.Errorf(\"field `os` must be one of %q; got %q\", limatype.OSTypes, *y.OS))\n\t}\n\tif !slices.Contains(limatype.ArchTypes, *y.Arch) {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"field `arch` must be one of %v; got %q\", limatype.ArchTypes, *y.Arch))\n\t}\n\n\tif len(y.Images) == 0 {\n\t\terrs = errors.Join(errs, errors.New(\"field `images` must be set\"))\n\t}\n\tfor i, f := range y.Images {\n\t\terr := validateFileObject(f.File, fmt.Sprintf(\"images[%d]\", i))\n\t\tif err != nil {\n\t\t\terrs = errors.Join(errs, err)\n\t\t}\n\t\tif f.Kernel != nil {\n\t\t\terr := validateFileObject(f.Kernel.File, fmt.Sprintf(\"images[%d].kernel\", i))\n\t\t\tif err != nil {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t}\n\t\t\tif f.Kernel.Arch != f.Arch {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"images[%d].kernel has unexpected architecture %q, must be %q\", i, f.Kernel.Arch, f.Arch))\n\t\t\t}\n\t\t}\n\t\tif f.Initrd != nil {\n\t\t\terr := validateFileObject(*f.Initrd, fmt.Sprintf(\"images[%d].initrd\", i))\n\t\t\tif err != nil {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t}\n\t\t\tif f.Initrd.Arch != f.Arch {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"images[%d].initrd has unexpected architecture %q, must be %q\", i, f.Initrd.Arch, f.Arch))\n\t\t\t}\n\t\t}\n\t}\n\n\tif *y.CPUs == 0 {\n\t\terrs = errors.Join(errs, errors.New(\"field `cpus` must be set\"))\n\t}\n\n\tif _, err := units.RAMInBytes(*y.Memory); err != nil {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"field `memory` has an invalid value: %w\", err))\n\t}\n\n\tif _, err := units.RAMInBytes(*y.Disk); err != nil {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"field `disk` has an invalid value: %w\", err))\n\t}\n\n\tfor i, disk := range y.AdditionalDisks {\n\t\tif err := identifiers.Validate(disk.Name); err != nil {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `additionalDisks[%d].name is invalid`: %w\", i, err))\n\t\t}\n\t}\n\n\tfor i, f := range y.Mounts {\n\t\tif !filepath.IsAbs(f.Location) && !strings.HasPrefix(f.Location, \"~\") {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mounts[%d].location` must be an absolute path, got %q\",\n\t\t\t\ti, f.Location))\n\t\t}\n\t\t// f.Location has already been expanded in FillDefaults(), but that function cannot return errors.\n\t\tloc, err := localpathutil.Expand(f.Location)\n\t\tif err != nil {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mounts[%d].location` refers to an unexpandable path: %q: %w\", i, f.Location, err))\n\t\t}\n\t\tst, err := os.Stat(loc)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mounts[%d].location` refers to an inaccessible path: %q: %w\", i, f.Location, err))\n\t\t\t}\n\t\t\tif warn {\n\t\t\t\tlogrus.Warnf(\"field `mounts[%d].location` refers to a non-existent directory: %q:\", i, f.Location)\n\t\t\t}\n\t\t} else if !st.IsDir() {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mounts[%d].location` refers to a non-directory path: %q: %w\", i, f.Location, err))\n\t\t}\n\n\t\tswitch *f.MountPoint {\n\t\tcase \"/\", \"/bin\", \"/dev\", \"/etc\", \"/home\", \"/opt\", \"/sbin\", \"/tmp\", \"/usr\", \"/var\":\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mounts[%d].mountPoint` must not be a system path such as /etc or /usr\", i))\n\t\t// home directory defined in \"cidata.iso:/user-data\"\n\t\tcase *y.User.Home:\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mounts[%d].mountPoint` is the reserved internal home directory %q\", i, *y.User.Home))\n\t\t}\n\t\t// There is no tilde-expansion for guest filenames\n\t\tif strings.HasPrefix(*f.MountPoint, \"~\") {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mounts[%d].mountPoint` must not start with \\\"~\\\"\", i))\n\t\t}\n\n\t\tif _, err := units.RAMInBytes(*f.NineP.Msize); err != nil {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `msize` has an invalid value: %w\", err))\n\t\t}\n\t}\n\n\tif *y.SSH.LocalPort != 0 {\n\t\tif err := validatePort(\"ssh.localPort\", *y.SSH.LocalPort); err != nil {\n\t\t\terrs = errors.Join(errs, err)\n\t\t}\n\t}\n\n\tif y.MountType != nil {\n\t\tswitch *y.MountType {\n\t\tcase limatype.REVSSHFS, limatype.NINEP, limatype.VIRTIOFS, limatype.WSLMount:\n\t\tdefault:\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mountType` must be %q or %q or %q, or %q, got %q\", limatype.REVSSHFS, limatype.NINEP, limatype.VIRTIOFS, limatype.WSLMount, *y.MountType))\n\t\t}\n\n\t\tif slices.Contains(y.MountTypesUnsupported, *y.MountType) {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `mountType` must not be one of %v (`mountTypesUnsupported`), got %q\", y.MountTypesUnsupported, *y.MountType))\n\t\t}\n\t}\n\n\tif warn && runtime.GOOS != \"linux\" {\n\t\tfor i, mount := range y.Mounts {\n\t\t\tif mount.Virtiofs.QueueSize != nil {\n\t\t\t\tlogrus.Warnf(\"field mounts[%d].virtiofs.queueSize is only supported on Linux\", i)\n\t\t\t}\n\t\t}\n\t}\n\n\t// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.\n\n\tfor i, p := range y.Provision {\n\t\tif p.File != nil {\n\t\t\tif p.File.URL != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].file.url` must be empty during validation (script should already be embedded)\", i))\n\t\t\t}\n\t\t\tif p.File.Digest != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].file.digest` support is not yet implemented\", i))\n\t\t\t}\n\t\t}\n\t\tswitch p.Mode {\n\t\tcase limatype.ProvisionModeSystem, limatype.ProvisionModeUser, limatype.ProvisionModeBoot, limatype.ProvisionModeData, limatype.ProvisionModeDependency, limatype.ProvisionModeAnsible, limatype.ProvisionModeYQ:\n\t\tdefault:\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].mode` must one of %q, %q, %q, %q, %q, %q, or %q\",\n\t\t\t\ti, limatype.ProvisionModeSystem, limatype.ProvisionModeUser, limatype.ProvisionModeBoot, limatype.ProvisionModeData, limatype.ProvisionModeDependency, limatype.ProvisionModeAnsible, limatype.ProvisionModeYQ))\n\t\t}\n\t\tif p.Mode != limatype.ProvisionModeDependency && p.SkipDefaultDependencyResolution != nil {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].mode` cannot set skipDefaultDependencyResolution, only valid on scripts of type %q\",\n\t\t\t\ti, limatype.ProvisionModeDependency))\n\t\t}\n\n\t\t// This can lead to fatal Panic if p.Path is nil, better to return an error here\n\t\tswitch p.Mode {\n\t\tcase limatype.ProvisionModeData, limatype.ProvisionModeYQ:\n\t\t\tif p.Path == nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].path` must not be empty when mode is %q\", i, p.Mode))\n\t\t\t\treturn errs\n\t\t\t}\n\t\t\tif !path.IsAbs(*p.Path) {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].path` must be an absolute path\", i))\n\t\t\t}\n\t\t\tif p.Mode == limatype.ProvisionModeData && p.Content == nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].content` must not be empty when mode is %q\", i, p.Mode))\n\t\t\t}\n\t\t\tif p.Mode == limatype.ProvisionModeYQ && p.Expression == nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].expression` must not be empty when mode is %q\", i, p.Mode))\n\t\t\t}\n\t\t\t// FillDefaults makes sure that p.Permissions is not nil\n\t\t\tif _, err := strconv.ParseInt(*p.Permissions, 8, 64); err != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].permissions` must be an octal number: %w\", i, err))\n\t\t\t}\n\t\tdefault:\n\t\t\tif (p.Script == nil || *p.Script == \"\") && p.Mode != limatype.ProvisionModeAnsible {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].script` must not be empty\", i))\n\t\t\t}\n\t\t\tif p.Content != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].content` can only be set when mode is %q\", i, limatype.ProvisionModeData))\n\t\t\t}\n\t\t\tif p.Overwrite != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].overwrite` can only be set when mode is %q\", i, limatype.ProvisionModeData))\n\t\t\t}\n\t\t\tif p.Owner != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].owner` can only be set when mode is %q\", i, limatype.ProvisionModeData))\n\t\t\t}\n\t\t\tif p.Path != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].path` can only be set when mode is %q, or %q\", i, limatype.ProvisionModeData, limatype.ProvisionModeYQ))\n\t\t\t}\n\t\t\tif p.Permissions != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].permissions` can only be set when mode is %q, or %q\", i, limatype.ProvisionModeData, limatype.ProvisionModeYQ))\n\t\t\t}\n\t\t\tif p.Format != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].format` can only be set when mode is %q\", i, limatype.ProvisionModeYQ))\n\t\t\t}\n\t\t}\n\t\tif p.Playbook != \"\" {\n\t\t\tif p.Mode != limatype.ProvisionModeAnsible {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].playbook can only be set when mode is %q\", i, limatype.ProvisionModeAnsible))\n\t\t\t}\n\t\t\tif p.Script != nil && *p.Script != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].script must be empty if playbook is set\", i))\n\t\t\t}\n\t\t\tplaybook := p.Playbook\n\t\t\tif _, err := os.Stat(playbook); err != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `provision[%d].playbook` refers to an inaccessible path: %q: %w\", i, playbook, err))\n\t\t\t}\n\t\t\tlogrus.Warnf(\"provision mode %q is deprecated, use `ansible-playbook %q` instead\", limatype.ProvisionModeAnsible, playbook)\n\t\t}\n\t\tif p.Script != nil {\n\t\t\tif strings.Contains(*p.Script, \"LIMA_CIDATA\") {\n\t\t\t\tlogrus.Warn(\"provisioning scripts should not reference the LIMA_CIDATA variables\")\n\t\t\t}\n\t\t}\n\t}\n\tneedsContainerdArchives := (y.Containerd.User != nil && *y.Containerd.User) || (y.Containerd.System != nil && *y.Containerd.System)\n\tif needsContainerdArchives {\n\t\tif len(y.Containerd.Archives) == 0 {\n\t\t\terrs = errors.Join(errs, errors.New(\"field `containerd.archives` must be provided\"))\n\t\t}\n\t\tfor i, f := range y.Containerd.Archives {\n\t\t\terr := validateFileObject(f, fmt.Sprintf(\"containerd.archives[%d]\", i))\n\t\t\tif err != nil {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t}\n\t\t}\n\t}\n\tfor i, p := range y.Probes {\n\t\tif p.File != nil {\n\t\t\tif p.File.URL != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `probe[%d].file.url` must be empty during validation (script should already be embedded)\", i))\n\t\t\t}\n\t\t\tif p.File.Digest != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `probe[%d].file.digest` support is not yet implemented\", i))\n\t\t\t}\n\t\t}\n\t\tif p.Script != nil && !strings.HasPrefix(*p.Script, \"#!\") {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `probe[%d].script` must start with a '#!' line\", i))\n\t\t}\n\t\tswitch p.Mode {\n\t\tcase limatype.ProbeModeReadiness:\n\t\tdefault:\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `probe[%d].mode` can only be %q\", i, limatype.ProbeModeReadiness))\n\t\t}\n\t}\n\tfor i, rule := range y.PortForwards {\n\t\tfield := fmt.Sprintf(\"portForwards[%d]\", i)\n\t\tif *rule.GuestIPMustBeZero && !rule.GuestIP.Equal(net.IPv4zero) {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.guestIPMustBeZero` can only be true when field `%s.guestIP` is 0.0.0.0\", field, field))\n\t\t}\n\t\tif rule.GuestPort != 0 {\n\t\t\tif rule.GuestSocket != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.guestPort` must be 0 when field `%s.guestSocket` is set\", field, field))\n\t\t\t}\n\t\t\tif rule.GuestPort != rule.GuestPortRange[0] {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.guestPort` must match field `%s.guestPortRange[0]`\", field, field))\n\t\t\t}\n\t\t\t// redundant validation to make sure the error contains the correct field name\n\t\t\tif err := validatePort(field+\".guestPort\", rule.GuestPort); err != nil {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t}\n\t\t}\n\t\tif rule.HostPort != 0 {\n\t\t\tif rule.HostSocket != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.hostPort` must be 0 when field `%s.hostSocket` is set\", field, field))\n\t\t\t}\n\t\t\tif rule.HostPort != rule.HostPortRange[0] {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.hostPort` must match field `%s.hostPortRange[0]`\", field, field))\n\t\t\t}\n\t\t\t// redundant validation to make sure the error contains the correct field name\n\t\t\tif err := validatePort(field+\".hostPort\", rule.HostPort); err != nil {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t}\n\t\t}\n\t\tfor j := range 2 {\n\t\t\tif err := validatePort(fmt.Sprintf(\"%s.guestPortRange[%d]\", field, j), rule.GuestPortRange[j]); err != nil {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t}\n\t\t\tif rule.HostSocket == \"\" {\n\t\t\t\tif err := validatePort(fmt.Sprintf(\"%s.hostPortRange[%d]\", field, j), rule.HostPortRange[j]); err != nil {\n\t\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif rule.GuestPortRange[0] > rule.GuestPortRange[1] {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.guestPortRange[1]` must be greater than or equal to field `%s.guestPortRange[0]`\", field, field))\n\t\t}\n\t\tif rule.HostPortRange[0] > rule.HostPortRange[1] {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.hostPortRange[1]` must be greater than or equal to field `%s.hostPortRange[0]`\", field, field))\n\t\t}\n\t\tif rule.GuestSocket != \"\" {\n\t\t\tif !path.IsAbs(rule.GuestSocket) {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.guestSocket` must be an absolute path, but is %q\", field, rule.GuestSocket))\n\t\t\t}\n\t\t\tif rule.HostSocket == \"\" && rule.HostPortRange[1]-rule.HostPortRange[0] > 0 {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.guestSocket` can only be mapped to a single port or socket. not a range\", field))\n\t\t\t}\n\t\t}\n\t\tif rule.HostSocket != \"\" {\n\t\t\tif !filepath.IsAbs(rule.HostSocket) {\n\t\t\t\t// should be unreachable because FillDefault() will prepend the instance directory to relative names\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.hostSocket` must be an absolute path, but is %q\", field, rule.HostSocket))\n\t\t\t}\n\t\t\tif rule.GuestSocket == \"\" && rule.GuestPortRange[1]-rule.GuestPortRange[0] > 0 {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.hostSocket` can only be mapped from a single port or socket. not a range\", field))\n\t\t\t}\n\t\t} else if rule.GuestPortRange[1]-rule.GuestPortRange[0] != rule.HostPortRange[1]-rule.HostPortRange[0] {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.hostPortRange` must specify the same number of ports as field `%s.guestPortRange`\", field, field))\n\t\t}\n\n\t\tif len(rule.HostSocket) >= osutil.UnixPathMax {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.hostSocket` must be less than UNIX_PATH_MAX=%d characters, but is %d\",\n\t\t\t\tfield, osutil.UnixPathMax, len(rule.HostSocket)))\n\t\t}\n\t\tswitch rule.Proto {\n\t\tcase limatype.ProtoTCP, limatype.ProtoUDP, limatype.ProtoAny:\n\t\tdefault:\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.proto` must be %q, %q, or %q\", field, limatype.ProtoTCP, limatype.ProtoUDP, limatype.ProtoAny))\n\t\t}\n\t\tif rule.Reverse && rule.GuestSocket == \"\" {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.reverse` must be %t\", field, false))\n\t\t}\n\t\tif rule.Reverse && rule.HostSocket == \"\" {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.reverse` must be %t\", field, false))\n\t\t}\n\t\t// Not validating that the various GuestPortRanges and HostPortRanges are not overlapping. Rules will be\n\t\t// processed sequentially and the first matching rule for a guest port determines forwarding behavior.\n\t}\n\tfor i, rule := range y.CopyToHost {\n\t\tfield := fmt.Sprintf(\"CopyToHost[%d]\", i)\n\t\tif rule.GuestFile != \"\" {\n\t\t\tif !path.IsAbs(rule.GuestFile) {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.guest` must be an absolute path, but is %q\", field, rule.GuestFile))\n\t\t\t}\n\t\t}\n\t\tif rule.HostFile != \"\" {\n\t\t\tif !filepath.IsAbs(rule.HostFile) {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.host` must be an absolute path, but is %q\", field, rule.HostFile))\n\t\t\t}\n\t\t}\n\t}\n\n\tif y.HostResolver.Enabled != nil && *y.HostResolver.Enabled && len(y.DNS) > 0 {\n\t\terrs = errors.Join(errs, errors.New(\"field `dns` must be empty when field `HostResolver.Enabled` is true\"))\n\t}\n\n\terr := validateNetwork(y)\n\tif err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\tif warn {\n\t\twarnExperimental(y)\n\t}\n\n\t// Validate Param settings\n\t// Names must start with a letter, followed by any number of letters, digits, or underscores\n\tvalidParamName := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`)\n\tfor param, value := range y.Param {\n\t\tif !validParamName.MatchString(param) {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"param %q name does not match regex %q\", param, validParamName.String()))\n\t\t}\n\t\tfor _, r := range value {\n\t\t\tif !unicode.IsPrint(r) && r != '\\t' && r != ' ' {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"param %q value contains unprintable character %q\", param, r))\n\t\t\t}\n\t\t}\n\t}\n\tif y.Plain != nil && *y.Plain {\n\t\tconst portRangeWarnThreshold = 10\n\t\tfor i, rule := range y.PortForwards {\n\t\t\tguestRange := rule.GuestPortRange[1] - rule.GuestPortRange[0] + 1\n\t\t\thostRange := rule.HostPortRange[1] - rule.HostPortRange[0] + 1\n\t\t\tif guestRange > portRangeWarnThreshold || hostRange > portRangeWarnThreshold {\n\t\t\t\tlogrus.Warnf(\"[plain mode] portForwards[%d] covers a range of more than %d ports (guest: %d, host: %d). All ports will be forwarded unconditionally, which may be inefficient.\", i, portRangeWarnThreshold, guestRange, hostRange)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errs\n}\n\nfunc validateFileObject(f limatype.File, fieldName string) error {\n\tvar errs error\n\tif !strings.Contains(f.Location, \"://\") {\n\t\tif _, err := localpathutil.Expand(f.Location); err != nil {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.location` refers to an invalid local file path: %q: %w\", fieldName, f.Location, err))\n\t\t}\n\t\t// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)\n\t}\n\tif !slices.Contains(limatype.ArchTypes, f.Arch) {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"field `arch` must be one of %v; got %q\", limatype.ArchTypes, f.Arch))\n\t}\n\tif f.Digest != \"\" {\n\t\tif err := f.Digest.Validate(); err != nil {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.digest` is invalid: %s: %w\", fieldName, f.Digest.String(), err))\n\t\t}\n\t}\n\treturn errs\n}\n\nfunc validateNetwork(y *limatype.LimaYAML) error {\n\tvar errs error\n\tinterfaceName := make(map[string]int)\n\tfor i, nw := range y.Networks {\n\t\tfield := fmt.Sprintf(\"networks[%d]\", i)\n\t\tswitch {\n\t\tcase nw.Lima != \"\":\n\t\t\tnwCfg, err := networks.LoadConfig()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif nwCfg.Check(nw.Lima) != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.lima` references network %q which is not defined in networks.yaml\", field, nw.Lima))\n\t\t\t}\n\t\t\tusernet, err := nwCfg.Usernet(nw.Lima)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !usernet && runtime.GOOS != \"darwin\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.lima` is only supported on macOS right now\", field))\n\t\t\t}\n\t\t\tif nw.Socket != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.lima` and field `%s.socket` are mutually exclusive\", field, field))\n\t\t\t}\n\t\t\tif nw.VZNAT != nil && *nw.VZNAT {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.lima` and field `%s.vzNAT` are mutually exclusive\", field, field))\n\t\t\t}\n\t\tcase nw.Socket != \"\":\n\t\t\tif nw.VZNAT != nil && *nw.VZNAT {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.socket` and field `%s.vzNAT` are mutually exclusive\", field, field))\n\t\t\t}\n\t\t\tif fi, err := os.Stat(nw.Socket); err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\t\t\terrs = errors.Join(errs, err)\n\t\t\t} else if err == nil && fi.Mode()&os.ModeSocket == 0 {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.socket` %q points to a non-socket file\", field, nw.Socket))\n\t\t\t}\n\t\tcase nw.VZNAT != nil && *nw.VZNAT:\n\t\t\tif nw.Lima != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.vzNAT` and field `%s.lima` are mutually exclusive\", field, field))\n\t\t\t}\n\t\t\tif nw.Socket != \"\" {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.vzNAT` and field `%s.socket` are mutually exclusive\", field, field))\n\t\t\t}\n\t\tdefault:\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.lima` or  field `%s.socket must be set\", field, field))\n\t\t}\n\t\tif nw.MACAddress != \"\" {\n\t\t\thw, err := net.ParseMAC(nw.MACAddress)\n\t\t\tif err != nil {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `vmnet.mac` invalid: %w\", err))\n\t\t\t}\n\t\t\tif len(hw) != 6 {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.macAddress` must be a 48 bit (6 bytes) MAC address; actual length of %q is %d bytes\", field, nw.MACAddress, len(hw)))\n\t\t\t}\n\t\t}\n\t\t// FillDefault() will make sure that nw.Interface is not the empty string\n\t\tif len(nw.Interface) >= 16 {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.interface` must be less than 16 bytes, but is %d bytes: %q\", field, len(nw.Interface), nw.Interface))\n\t\t}\n\t\tif strings.ContainsAny(nw.Interface, \" \\t\\n/\") {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.interface` must not contain whitespace or slashes\", field))\n\t\t}\n\t\tif nw.Interface == networks.SlirpNICName {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.interface` must not be set to %q because it is reserved for slirp\", field, networks.SlirpNICName))\n\t\t}\n\t\tif prev, ok := interfaceName[nw.Interface]; ok {\n\t\t\terrs = errors.Join(errs, fmt.Errorf(\"field `%s.interface` value %q has already been used by field `networks[%d].interface`\", field, nw.Interface, prev))\n\t\t}\n\t\tinterfaceName[nw.Interface] = i\n\t}\n\n\treturn errs\n}\n\n// validateParamIsUsed checks if the keys in the `param` field are used in any script, probe, copyToHost, or portForward.\n// It should be called before the `y` parameter is passed to FillDefault() that execute template.\nfunc validateParamIsUsed(y *limatype.LimaYAML) error {\n\tfor key := range y.Param {\n\t\tre, err := regexp.Compile(`{{[^}]*\\.Param\\.` + key + `[^}]*}}|\\bPARAM_` + key + `\\b`)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"field to compile regexp for key %q: %w\", key, err)\n\t\t}\n\t\tkeyIsUsed := false\n\t\tfor _, p := range y.Provision {\n\t\t\tfor _, ptr := range []*string{p.Script, p.Content, p.Expression, p.Owner, p.Path, p.Permissions} {\n\t\t\t\tif ptr != nil && re.MatchString(*ptr) {\n\t\t\t\t\tkeyIsUsed = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif p.Playbook != \"\" {\n\t\t\t\tplaybook, err := os.ReadFile(p.Playbook)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif re.Match(playbook) {\n\t\t\t\t\tkeyIsUsed = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor _, p := range y.Probes {\n\t\t\tif p.Script != nil && re.MatchString(*p.Script) {\n\t\t\t\tkeyIsUsed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor _, p := range y.CopyToHost {\n\t\t\tif re.MatchString(p.GuestFile) || re.MatchString(p.HostFile) {\n\t\t\t\tkeyIsUsed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor _, p := range y.PortForwards {\n\t\t\tif re.MatchString(p.GuestSocket) || re.MatchString(p.HostSocket) {\n\t\t\t\tkeyIsUsed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor _, p := range y.Mounts {\n\t\t\tif re.MatchString(p.Location) {\n\t\t\t\tkeyIsUsed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif p.MountPoint != nil && re.MatchString(*p.MountPoint) {\n\t\t\t\tkeyIsUsed = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif !keyIsUsed {\n\t\t\treturn fmt.Errorf(\"field `param` key %q is not used in any provision, probe, copyToHost, or portForward\", key)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc validatePort(field string, port int) error {\n\tswitch {\n\tcase port < 0:\n\t\treturn fmt.Errorf(\"field `%s` must be > 0\", field)\n\tcase port == 0:\n\t\treturn fmt.Errorf(\"field `%s` must be set\", field)\n\tcase port == 22:\n\t\treturn fmt.Errorf(\"field `%s` must not be 22\", field)\n\tcase port > 65535:\n\t\treturn fmt.Errorf(\"field `%s` must be < 65536\", field)\n\t}\n\treturn nil\n}\n\nfunc warnExperimental(y *limatype.LimaYAML) {\n\tif *y.MountType == limatype.VIRTIOFS && runtime.GOOS == \"linux\" {\n\t\tlogrus.Warn(\"`mountType: virtiofs` on Linux is experimental\")\n\t}\n\tswitch *y.Arch {\n\tcase limatype.RISCV64, limatype.ARMV7L, limatype.S390X, limatype.PPC64LE:\n\t\tlogrus.Warnf(\"`arch: %s ` is experimental\", *y.Arch)\n\t}\n\tif y.Video.Display != nil && strings.Contains(*y.Video.Display, \"vnc\") {\n\t\tlogrus.Warn(\"`video.display: vnc` is experimental\")\n\t}\n\tif y.Audio.Device != nil && *y.Audio.Device != \"\" {\n\t\tlogrus.Warn(\"`audio.device` is experimental\")\n\t}\n\tif y.MountInotify != nil && *y.MountInotify {\n\t\tlogrus.Warn(\"`mountInotify` is experimental\")\n\t}\n}\n\n// ValidateAgainstLatestConfig validates the values between the latest YAML and the updated(New) YAML.\n// This validates configuration rules that disallow certain changes, such as shrinking the disk.\nfunc ValidateAgainstLatestConfig(ctx context.Context, yNew, yLatest []byte) error {\n\tvar n limatype.LimaYAML\n\tvar errs error\n\n\t// Load the latest YAML and fill in defaults\n\tl, err := LoadWithWarnings(ctx, yLatest, \"\")\n\tif err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\tif err := driverutil.ResolveVMType(ctx, l, \"\"); err != nil {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"failed to resolve vm for %q: %w\", \"\", err))\n\t}\n\tif err := Unmarshal(yNew, &n, \"Unmarshal new YAML bytes\"); err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\t// Handle editing the template without a disk value\n\tif n.Disk == nil || l.Disk == nil {\n\t\treturn errs\n\t}\n\n\t// Disk value must be provided, as it is required when creating an instance.\n\tnDisk, err := units.RAMInBytes(*n.Disk)\n\tif err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\tlDisk, err := units.RAMInBytes(*l.Disk)\n\tif err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\t// Reject shrinking disk\n\tif nDisk < lDisk {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"field `disk`: shrinking the disk (%v --> %v) is not supported\", *l.Disk, *n.Disk))\n\t}\n\n\treturn errs\n}\n"
  },
  {
    "path": "pkg/limayaml/validate_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/version\"\n)\n\nfunc TestValidateEmpty(t *testing.T) {\n\ty, err := Load(t.Context(), []byte{}, \"empty.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tassert.Error(t, err, \"field `images` must be set\")\n}\n\nfunc TestValidateMinimumLimaVersion(t *testing.T) {\n\timages := `images: [{\"location\": \"/\"}]`\n\n\ttests := []struct {\n\t\tname               string\n\t\tcurrentVersion     string\n\t\tminimumLimaVersion string\n\t\twantErr            string\n\t}{\n\t\t{\n\t\t\tname:               \"minimumLimaVersion less than current version\",\n\t\t\tcurrentVersion:     \"1.1.1-114-g5bf5e513\",\n\t\t\tminimumLimaVersion: \"1.1.0\",\n\t\t\twantErr:            \"\",\n\t\t},\n\t\t{\n\t\t\tname:               \"minimumLimaVersion greater than current version\",\n\t\t\tcurrentVersion:     \"1.1.1-114-g5bf5e513\",\n\t\t\tminimumLimaVersion: \"1.1.2\",\n\t\t\twantErr:            `template requires Lima version \"1.1.2\"; this is only \"1.1.1-114-g5bf5e513\"`,\n\t\t},\n\t\t{\n\t\t\tname:               \"invalid current version\",\n\t\t\tcurrentVersion:     \"<unknown>\",\n\t\t\tminimumLimaVersion: \"0.8.0\",\n\t\t\twantErr:            \"\", // Unparsable versions are treated as \"latest\"\n\t\t},\n\t\t{\n\t\t\tname:               \"invalid minimumLimaVersion\",\n\t\t\tcurrentVersion:     \"1.1.1-114-g5bf5e513\",\n\t\t\tminimumLimaVersion: \"invalid\",\n\t\t\twantErr:            \"field `minimumLimaVersion` must be a semvar value, got \\\"invalid\\\": invalid is not in dotted-tri format\", // Only parse error, no comparison error\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\toldVersion := version.Version\n\t\t\tversion.Version = tt.currentVersion\n\t\t\tt.Cleanup(func() { version.Version = oldVersion })\n\n\t\t\ty, err := Load(t.Context(), []byte(\"minimumLimaVersion: \"+tt.minimumLimaVersion+\"\\n\"+images), \"lima.yaml\")\n\t\t\tassert.NilError(t, err)\n\n\t\t\terr = Validate(y, false)\n\t\t\tif tt.wantErr == \"\" {\n\t\t\t\tassert.NilError(t, err)\n\t\t\t} else {\n\t\t\t\tassert.Error(t, err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateDigest(t *testing.T) {\n\timages := `images: [{\"location\": \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-amd64.img\",digest: \"69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03\"}]`\n\tvalidProbe := `probes: [{\"script\": \"#!foo\"}]`\n\ty, err := Load(t.Context(), []byte(validProbe+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tassert.Error(t, err, \"field `images[0].digest` is invalid: 69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03: invalid checksum digest format\")\n\n\timages2 := `images: [{\"location\": \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-amd64.img\",digest: \"sha001:69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03\"}]`\n\ty2, err := Load(t.Context(), []byte(validProbe+\"\\n\"+images2), \"lima.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y2, false)\n\tassert.Error(t, err, \"field `images[0].digest` is invalid: sha001:69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03: unsupported digest algorithm\")\n}\n\nfunc TestValidateProbes(t *testing.T) {\n\timages := `images: [{\"location\": \"/\"}]`\n\tvalidProbe := `probes: [{\"script\": \"#!foo\"}]`\n\ty, err := Load(t.Context(), []byte(validProbe+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.NilError(t, err)\n\n\tinvalidProbe := `probes: [{\"script\": \"foo\"}]`\n\ty, err = Load(t.Context(), []byte(invalidProbe+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.Error(t, err, \"field `probe[0].script` must start with a '#!' line\")\n\n\tinvalidProbe = `probes: [{file: {digest: decafbad}}]`\n\ty, err = Load(t.Context(), []byte(invalidProbe+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.Error(t, err, \"field `probe[0].file.digest` support is not yet implemented\\n\"+\n\t\t\"field `probe[0].script` must start with a '#!' line\")\n}\n\nfunc TestValidateProvisionMode(t *testing.T) {\n\timages := `images: [{location: /}]`\n\tprovisionBoot := `provision: [{mode: boot, script: \"touch /tmp/param-$PARAM_BOOT\"}]`\n\ty, err := Load(t.Context(), []byte(provisionBoot+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.NilError(t, err)\n\n\tprovisionUser := `provision: [{mode: user, script: \"touch /tmp/param-$PARAM_USER\"}]`\n\ty, err = Load(t.Context(), []byte(provisionUser+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.NilError(t, err)\n\n\tprovisionDependency := `provision: [{mode: ansible, script: \"touch /tmp/param-$PARAM_DEPENDENCY\"}]`\n\ty, err = Load(t.Context(), []byte(provisionDependency+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.NilError(t, err)\n\n\tprovisionInvalid := `provision: [{mode: invalid}]`\n\ty, err = Load(t.Context(), []byte(provisionInvalid+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.Error(t, err, \"field `provision[0].mode` must one of \\\"system\\\", \\\"user\\\", \\\"boot\\\", \\\"data\\\", \\\"dependency\\\", \\\"ansible\\\", or \\\"yq\\\"\\n\"+\n\t\t\"field `provision[0].script` must not be empty\")\n}\n\nfunc TestValidateProvisionData(t *testing.T) {\n\timages := `images: [{location: /}]`\n\tvalidData := `provision: [{mode: data, path: /tmp, content: hello}]`\n\ty, err := Load(t.Context(), []byte(validData+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.NilError(t, err)\n\n\tinvalidData := `provision: [{mode: data, content: hello}]`\n\ty, err = Load(t.Context(), []byte(invalidData+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.Error(t, err, \"field `provision[0].path` must not be empty when mode is \\\"data\\\"\")\n\n\tinvalidData = `provision: [{mode: data, path: /tmp, content: hello, permissions: 9}]`\n\ty, err = Load(t.Context(), []byte(invalidData+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.ErrorContains(t, err, \"provision[0].permissions` must be an octal number\")\n}\n\nfunc TestValidateProvisionYQ(t *testing.T) {\n\timages := `images: [{location: /}]`\n\tparam := `param: {\"cdi\": \"true\"}`\n\t// Valid\n\tvalidYQProvision := `provision: [{mode: yq, expression: \".features.cdi={{.Param.cdi}}\", path: /tmp}]`\n\ty, err := Load(t.Context(), []byte(param+\"\\n\"+validYQProvision+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tassert.NilError(t, err)\n\n\t// Missing path\n\tinvalidYQProvision := `provision: [{mode: yq, expression: \".features.cdi={{.Param.cdi}}\"}]`\n\ty, err = Load(t.Context(), []byte(param+\"\\n\"+invalidYQProvision+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tassert.ErrorContains(t, err, \"field `provision[0].path` must not be empty when mode is \\\"yq\\\"\")\n\n\t// non-absolute path\n\tinvalidYQProvision = `provision: [{mode: yq, expression: \".features.cdi={{.Param.cdi}}\", path: tmp}]`\n\ty, err = Load(t.Context(), []byte(param+\"\\n\"+invalidYQProvision+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tassert.ErrorContains(t, err, \"field `provision[0].path` must be an absolute path\")\n\n\t// Missing expression\n\tinvalidYQProvision = `provision: [{mode: yq, path: \"/{{.Param.cdi}}\"}]`\n\ty, err = Load(t.Context(), []byte(param+\"\\n\"+invalidYQProvision+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tassert.ErrorContains(t, err, \"field `provision[0].expression` must not be empty when mode is \\\"yq\\\"\")\n\n\t// Invalid permissions\n\tinvalidYQProvision = `provision: [{mode: yq, expression: \".features.cdi={{.Param.cdi}}\", path: /tmp, permissions: 9}]`\n\ty, err = Load(t.Context(), []byte(param+\"\\n\"+invalidYQProvision+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tassert.ErrorContains(t, err, \"provision[0].permissions` must be an octal number\")\n}\n\nfunc TestValidateAdditionalDisks(t *testing.T) {\n\timages := `images: [{\"location\": \"/\"}]`\n\n\tvalidDisks := `\nadditionalDisks:\n  - name: \"disk1\"\n  - name: \"disk2\"\n`\n\ty, err := Load(t.Context(), []byte(validDisks+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.NilError(t, err)\n\n\tinvalidDisks := `\nadditionalDisks:\n  - name: \"\"\n`\n\ty, err = Load(t.Context(), []byte(invalidDisks+\"\\n\"+images), \"lima.yaml\")\n\tassert.NilError(t, err)\n\n\terr = Validate(y, false)\n\tassert.Error(t, err, \"field `additionalDisks[0].name is invalid`: identifier must not be empty\")\n}\n\nfunc TestValidateParamName(t *testing.T) {\n\timages := `images: [{\"location\": \"/\"}]`\n\tvalidProvision := `provision: [{\"script\": \"echo $PARAM_name $PARAM_NAME $PARAM_Name_123\"}]`\n\tvalidParam := []string{\n\t\t`param: {\"name\": \"value\"}`,\n\t\t`param: {\"NAME\": \"value\"}`,\n\t\t`param: {\"Name_123\": \"value\"}`,\n\t}\n\tfor _, param := range validParam {\n\t\ty, err := Load(t.Context(), []byte(param+\"\\n\"+validProvision+\"\\n\"+images), \"lima.yaml\")\n\t\tassert.NilError(t, err)\n\n\t\terr = Validate(y, false)\n\t\tassert.NilError(t, err)\n\t}\n\n\tinvalidProvision := `provision: [{\"script\": \"echo $PARAM__Name $PARAM_3Name $PARAM_Last.Name\"}]`\n\tinvalidParam := []string{\n\t\t`param: {\"_Name\": \"value\"}`,\n\t\t`param: {\"3Name\": \"value\"}`,\n\t\t`param: {\"Last.Name\": \"value\"}`,\n\t}\n\tfor _, param := range invalidParam {\n\t\ty, err := Load(t.Context(), []byte(param+\"\\n\"+invalidProvision+\"\\n\"+images), \"lima.yaml\")\n\t\tassert.NilError(t, err)\n\n\t\terr = Validate(y, false)\n\t\tassert.ErrorContains(t, err, \"name does not match regex\")\n\t}\n}\n\nfunc TestValidateParamValue(t *testing.T) {\n\timages := `images: [{\"location\": \"/\"}]`\n\tprovision := `provision: [{\"script\": \"echo $PARAM_name\"}]`\n\tvalidParam := []string{\n\t\t`param: {\"name\": \"\"}`,\n\t\t`param: {\"name\": \"foo bar\"}`,\n\t\t`param: {\"name\": \"foo\\tbar\"}`,\n\t\t`param: {\"name\": \"Symbols ½ and emoji → 👀\"}`,\n\t}\n\tfor _, param := range validParam {\n\t\ty, err := Load(t.Context(), []byte(param+\"\\n\"+provision+\"\\n\"+images), \"lima.yaml\")\n\t\tassert.NilError(t, err)\n\n\t\terr = Validate(y, false)\n\t\tassert.NilError(t, err)\n\t}\n\n\tinvalidParam := []string{\n\t\t`param: {\"name\": \"The end.\\n\"}`,\n\t\t`param: {\"name\": \"\\r\"}`,\n\t}\n\tfor _, param := range invalidParam {\n\t\ty, err := Load(t.Context(), []byte(param+\"\\n\"+provision+\"\\n\"+images), \"lima.yaml\")\n\t\tassert.NilError(t, err)\n\n\t\terr = Validate(y, false)\n\t\tassert.ErrorContains(t, err, \"value contains unprintable character\")\n\t}\n}\n\nfunc TestValidateParamIsUsed(t *testing.T) {\n\tparamYaml := `param:\n  name: value`\n\t_, err := Load(t.Context(), []byte(paramYaml), \"paramIsNotUsed.yaml\")\n\tassert.Error(t, err, \"field `param` key \\\"name\\\" is not used in any provision, probe, copyToHost, or portForward\")\n\n\tfieldsUsingParam := []string{\n\t\t`mounts: [{\"location\": \"/tmp/{{ .Param.name }}\"}]`,\n\t\t`mounts: [{\"location\": \"/tmp\", mountPoint: \"/tmp/{{ .Param.name }}\"}]`,\n\t\t`provision: [{\"script\": \"echo {{ .Param.name }}\"}]`,\n\t\t`provision: [{\"script\": \"echo $PARAM_name\"}]`,\n\t\t`probes: [{\"script\": \"echo {{ .Param.name }}\"}]`,\n\t\t`probes: [{\"script\": \"echo $PARAM_name\"}]`,\n\t\t`copyToHost: [{\"guest\": \"/tmp/{{ .Param.name }}\", \"host\": \"/tmp\"}]`,\n\t\t`copyToHost: [{\"guest\": \"/tmp\", \"host\": \"/tmp/{{ .Param.name }}\"}]`,\n\t\t`portForwards: [{\"guestSocket\": \"/tmp/{{ .Param.name }}\", \"hostSocket\": \"/tmp\"}]`,\n\t\t`portForwards: [{\"guestSocket\": \"/tmp\", \"hostSocket\": \"/tmp/{{ .Param.name }}\"}]`,\n\t}\n\tfor _, fieldUsingParam := range fieldsUsingParam {\n\t\t_, err = Load(t.Context(), []byte(fieldUsingParam+\"\\n\"+paramYaml), \"paramIsUsed.yaml\")\n\t\t//\n\t\tassert.NilError(t, err)\n\t}\n\n\t// use \"{{if eq .Param.rootful \\\"true\\\"}}…{{else}}…{{end}}\" in provision, probe, copyToHost, and portForward\n\trootfulYaml := `param:\n  rootful: true`\n\tfieldsUsingIfParamRootfulTrue := []string{\n\t\t`mounts: [{\"location\": \"/tmp/{{if eq .Param.rootful \\\"true\\\"}}rootful{{else}}rootless{{end}}\", \"mountPoint\": \"/tmp\"}]`,\n\t\t`mounts: [{\"location\": \"/tmp\", \"mountPoint\": \"/tmp/{{if eq .Param.rootful \\\"true\\\"}}rootful{{else}}rootless{{end}}\"}]`,\n\t\t`provision: [{\"script\": \"echo {{if eq .Param.rootful \\\"true\\\"}}rootful{{else}}rootless{{end}}\"}]`,\n\t\t`probes: [{\"script\": \"echo {{if eq .Param.rootful \\\"true\\\"}}rootful{{else}}rootless{{end}}\"}]`,\n\t\t`copyToHost: [{\"guest\": \"/tmp/{{if eq .Param.rootful \\\"true\\\"}}rootful{{else}}rootless{{end}}\", \"host\": \"/tmp\"}]`,\n\t\t`copyToHost: [{\"guest\": \"/tmp\", \"host\": \"/tmp/{{if eq .Param.rootful \\\"true\\\"}}rootful{{else}}rootless{{end}}\"}]`,\n\t\t`portForwards: [{\"guestSocket\": \"{{if eq .Param.rootful \\\"true\\\"}}/var/run{{else}}/run/user/{{.UID}}{{end}}/docker.sock\", \"hostSocket\": \"{{.Dir}}/sock/docker.sock\"}]`,\n\t\t`portForwards: [{\"guestSocket\": \"/var/run/docker.sock\", \"hostSocket\": \"{{.Dir}}/sock/docker-{{if eq .Param.rootful \\\"true\\\"}}rootful{{else}}rootless{{end}}.sock\"}]`,\n\t}\n\tfor _, fieldUsingIfParamRootfulTrue := range fieldsUsingIfParamRootfulTrue {\n\t\t_, err = Load(t.Context(), []byte(fieldUsingIfParamRootfulTrue+\"\\n\"+rootfulYaml), \"paramIsUsed.yaml\")\n\t\t//\n\t\tassert.NilError(t, err)\n\t}\n\n\t// use rootFul instead of rootful\n\trootFulYaml := `param:\n  rootFul: true`\n\tfor _, fieldUsingIfParamRootfulTrue := range fieldsUsingIfParamRootfulTrue {\n\t\t_, err = Load(t.Context(), []byte(fieldUsingIfParamRootfulTrue+\"\\n\"+rootFulYaml), \"paramIsUsed.yaml\")\n\t\t//\n\t\tassert.Error(t, err, \"field `param` key \\\"rootFul\\\" is not used in any provision, probe, copyToHost, or portForward\")\n\t}\n}\n\nfunc TestValidateMultipleErrors(t *testing.T) {\n\tyamlWithMultipleErrors := `\nos: windows\narch: unsupported_arch\nportForwards:\n  - guestPort: 22\n    hostPort: 2222\n  - guestPort: 8080\n    hostPort: 65536\nprovision:\n  - mode: invalid_mode\n    script: echo test\n  - mode: data\n    content: test\n`\n\n\ty, err := Load(t.Context(), []byte(yamlWithMultipleErrors), \"multiple-errors.yaml\")\n\tassert.NilError(t, err)\n\terr = Validate(y, false)\n\tt.Logf(\"Validation errors: %v\", err)\n\n\tassert.Error(t, err, \"field `os` must be one of [\\\"Linux\\\" \\\"Darwin\\\" \\\"FreeBSD\\\"]; got \\\"windows\\\"\\n\"+\n\t\t\"field `arch` must be one of [x86_64 aarch64 armv7l ppc64le riscv64 s390x]; got \\\"unsupported_arch\\\"\\n\"+\n\t\t\"field `images` must be set\\n\"+\n\t\t\"field `provision[0].mode` must one of \\\"system\\\", \\\"user\\\", \\\"boot\\\", \\\"data\\\", \\\"dependency\\\", \\\"ansible\\\", or \\\"yq\\\"\\n\"+\n\t\t\"field `provision[1].path` must not be empty when mode is \\\"data\\\"\")\n}\n\nfunc TestValidateAgainstLatestConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tyNew    string\n\t\tyLatest string\n\t\twantErr string\n\t}{\n\t\t{\n\t\t\tname:    \"Valid disk size unchanged\",\n\t\t\tyNew:    `disk: 100GiB`,\n\t\t\tyLatest: `disk: 100GiB`,\n\t\t\twantErr: fmt.Sprintf(\"failed to resolve vm for \\\"\\\": vmType %q is not a registered driver\", limatype.DefaultDriver()),\n\t\t},\n\t\t{\n\t\t\tname:    \"Valid disk size increased\",\n\t\t\tyNew:    `disk: 200GiB`,\n\t\t\tyLatest: `disk: 100GiB`,\n\t\t\twantErr: fmt.Sprintf(\"failed to resolve vm for \\\"\\\": vmType %q is not a registered driver\", limatype.DefaultDriver()),\n\t\t},\n\t\t{\n\t\t\tname:    \"No disk field in both YAMLs\",\n\t\t\tyNew:    ``,\n\t\t\tyLatest: ``,\n\t\t\twantErr: fmt.Sprintf(\"failed to resolve vm for \\\"\\\": vmType %q is not a registered driver\", limatype.DefaultDriver()),\n\t\t},\n\t\t{\n\t\t\tname:    \"No disk field in new YAMLs\",\n\t\t\tyNew:    ``,\n\t\t\tyLatest: `disk: 100GiB`,\n\t\t\twantErr: fmt.Sprintf(\"failed to resolve vm for \\\"\\\": vmType %q is not a registered driver\", limatype.DefaultDriver()),\n\t\t},\n\t\t{\n\t\t\tname:    \"No disk field in latest YAMLs\",\n\t\t\tyNew:    `disk: 100GiB`,\n\t\t\tyLatest: ``,\n\t\t\twantErr: fmt.Sprintf(\"failed to resolve vm for \\\"\\\": vmType %q is not a registered driver\", limatype.DefaultDriver()),\n\t\t},\n\t\t{\n\t\t\tname:    \"Disk size shrunk\",\n\t\t\tyNew:    `disk: 50GiB`,\n\t\t\tyLatest: `disk: 100GiB`,\n\t\t\twantErr: fmt.Sprintf(\"failed to resolve vm for \\\"\\\": vmType %q is not a registered driver\\n\", limatype.DefaultDriver()) +\n\t\t\t\t\"field `disk`: shrinking the disk (100GiB --> 50GiB) is not supported\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := ValidateAgainstLatestConfig(t.Context(), []byte(tt.yNew), []byte(tt.yLatest))\n\t\t\tassert.Error(t, err, tt.wantErr)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/limayaml/validate_unix_test.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage limayaml\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestValidateMounts(t *testing.T) {\n\tyBase := `images: [{\"location\": \"/dummy\"}]`\n\ttests := []struct {\n\t\tname          string\n\t\tmounts        string\n\t\tskipOnWindows bool\n\t\twantErr       string\n\t}{\n\t\t{\n\t\t\tname:    \"Valid\",\n\t\t\tmounts:  `mounts: [{location: \"/foo\", writable: false}, {location: \"~/foo\", writable: true}]`,\n\t\t\twantErr: \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"Invalid (relative)\",\n\t\t\tmounts: `mounts: [{location: \".\", writable: false}]`,\n\t\t\twantErr: func() string {\n\t\t\t\treturn \"must be an absolute path\"\n\t\t\t}(),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ty, err := Load(t.Context(), []byte(yBase+\"\\n\"+tt.mounts), \"lima.yaml\")\n\t\t\tassert.NilError(t, err)\n\t\t\terr = Validate(y, false)\n\t\t\tif tt.wantErr != \"\" {\n\t\t\t\tassert.ErrorContains(t, err, tt.wantErr)\n\t\t\t} else {\n\t\t\t\tassert.NilError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/localpathutil/localpathutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage localpathutil\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// IsTildePath returns true if the path is \"~\" or starts with \"~/\".\n// This means Expand() can expand it with the home directory.\nfunc IsTildePath(path string) bool {\n\treturn path == \"~\" || strings.HasPrefix(path, \"~/\")\n}\n\n// Expand expands a path like \"~\", \"~/\", \"~/foo\".\n// Paths like \"~foo/bar\" are unsupported.\n//\n// FIXME: is there an existing library for this?\nfunc Expand(orig string) (string, error) {\n\ts := orig\n\tif s == \"\" {\n\t\treturn \"\", errors.New(\"empty path\")\n\t}\n\n\tif strings.HasPrefix(s, \"~\") {\n\t\tif IsTildePath(s) {\n\t\t\thomeDir, err := os.UserHomeDir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\ts = strings.Replace(s, \"~\", homeDir, 1)\n\t\t} else {\n\t\t\t// Paths like \"~foo/bar\" are unsupported.\n\t\t\treturn \"\", fmt.Errorf(\"unexpandable path %q\", orig)\n\t\t}\n\t}\n\treturn filepath.Abs(s)\n}\n"
  },
  {
    "path": "pkg/lockutil/lockutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage lockutil\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nconst parallel = 20\n\nfunc TestWithDirLock(t *testing.T) {\n\tdir := t.TempDir()\n\tlog := filepath.Join(dir, \"log\")\n\n\terrc := make(chan error, 10)\n\tfor i := range parallel {\n\t\tgo func() {\n\t\t\terr := WithDirLock(dir, func() error {\n\t\t\t\tif _, err := os.Stat(log); err == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tlogFile, err := os.OpenFile(log, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o640)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tdefer logFile.Close()\n\t\t\t\tif _, err := fmt.Fprintf(logFile, \"writer %d\\n\", i); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn logFile.Close()\n\t\t\t})\n\t\t\terrc <- err\n\t\t}()\n\t}\n\n\tfor range parallel {\n\t\terr := <-errc\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}\n\n\tdata, err := os.ReadFile(log)\n\tassert.NilError(t, err)\n\tlines := strings.Split(strings.Trim(string(data), \"\\n\"), \"\\n\")\n\tassert.Equal(t, len(lines), 1, \"unexpected number of writers\")\n}\n"
  },
  {
    "path": "pkg/lockutil/lockutil_unix.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://github.com/containerd/nerdctl/blob/v0.13.0/pkg/lockutil/lockutil_unix.go\n/*\n   Copyright The containerd Authors.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n*/\n\npackage lockutil\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc WithDirLock(dir string, fn func() error) error {\n\tdirFile, err := os.Open(dir)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer dirFile.Close()\n\tif err := Flock(dirFile, unix.LOCK_EX); err != nil {\n\t\treturn fmt.Errorf(\"failed to lock %q: %w\", dir, err)\n\t}\n\tdefer func() {\n\t\tif err := Flock(dirFile, unix.LOCK_UN); err != nil {\n\t\t\tlogrus.WithError(err).Errorf(\"failed to unlock %q\", dir)\n\t\t}\n\t}()\n\treturn fn()\n}\n\nfunc Flock(f *os.File, flags int) error {\n\tfd := int(f.Fd())\n\tfor {\n\t\terr := unix.Flock(fd, flags)\n\t\tif err == nil || err != unix.EINTR {\n\t\t\treturn err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/lockutil/lockutil_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://github.com/containerd/nerdctl/blob/v0.13.0/pkg/lockutil/lockutil_windows.go\n/*\n   Copyright The containerd Authors.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n*/\n\npackage lockutil\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"syscall\"\n\t\"unsafe\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// LockFile modified from https://github.com/boltdb/bolt/blob/v1.3.1/bolt_windows.go using MIT.\nvar (\n\tmodkernel32      = syscall.NewLazyDLL(\"kernel32.dll\")\n\tprocLockFileEx   = modkernel32.NewProc(\"LockFileEx\")\n\tprocUnlockFileEx = modkernel32.NewProc(\"UnlockFileEx\")\n)\n\n// LOCKFILE_EXCLUSIVE_LOCK from https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx\nconst flagLockfileExclusiveLock = 0x00000002\n\nfunc WithDirLock(dir string, fn func() error) error {\n\tdirFile, err := os.OpenFile(dir+\".lock\", os.O_CREATE, 0o644)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer dirFile.Close()\n\tif err := lockFileEx(\n\t\tsyscall.Handle(dirFile.Fd()), // hFile\n\t\tflagLockfileExclusiveLock,    // dwFlags\n\t\t0,                            // dwReserved\n\t\t1,                            // nNumberOfBytesToLockLow\n\t\t0,                            // nNumberOfBytesToLockHigh\n\t\t&syscall.Overlapped{},        // lpOverlapped\n\t); err != nil {\n\t\treturn fmt.Errorf(\"failed to lock %q: %w\", dir, err)\n\t}\n\n\tdefer func() {\n\t\tif err := unlockFileEx(\n\t\t\tsyscall.Handle(dirFile.Fd()), // hFile\n\t\t\t0,                            // dwReserved\n\t\t\t1,                            // nNumberOfBytesToLockLow\n\t\t\t0,                            // nNumberOfBytesToLockHigh\n\t\t\t&syscall.Overlapped{},        // lpOverlapped\n\t\t); err != nil {\n\t\t\tlogrus.WithError(err).Errorf(\"failed to unlock %q\", dir)\n\t\t}\n\t}()\n\treturn fn()\n}\n\nfunc lockFileEx(h syscall.Handle, flags, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {\n\tr, _, err := procLockFileEx.Call(uintptr(h), uintptr(flags), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)))\n\tif r == 0 {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc unlockFileEx(h syscall.Handle, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {\n\tr, _, err := procUnlockFileEx.Call(uintptr(h), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)), 0)\n\tif r == 0 {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/logrusutil/logrusutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage logrusutil\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst epsilon = 1 * time.Second\n\n// PropagateJSON propagates JSONFormatter lines.\n//\n// PanicLevel and FatalLevel are converted to ErrorLevel.\nfunc PropagateJSON(logger *logrus.Logger, jsonLine []byte, header string, begin time.Time) {\n\tif strings.TrimSpace(string(jsonLine)) == \"\" {\n\t\treturn\n\t}\n\n\tvar (\n\t\tentry  *logrus.Entry\n\t\tfields logrus.Fields\n\t\tlv     logrus.Level\n\t\tj      JSON\n\t\terr    error\n\t)\n\tentry = logrus.NewEntry(logger)\n\n\tif err := json.Unmarshal(jsonLine, &j); err != nil {\n\t\tgoto fallback\n\t}\n\tif !j.Time.IsZero() && !begin.IsZero() && begin.After(j.Time.Add(epsilon)) {\n\t\treturn\n\t}\n\tlv, err = logrus.ParseLevel(j.Level)\n\tif err != nil {\n\t\tgoto fallback\n\t}\n\tentry = entry.WithTime(j.Time)\n\t// Unmarshal jsonLine once more to capture all the \"extra\" fields that have been added by\n\t// WithError() and WithField(). The regular fields \"level\", \"msg\", and \"time\" are already\n\t// unmarshalled into j and are handled specially. They must not be added again.\n\tif err := json.Unmarshal(jsonLine, &fields); err == nil {\n\t\tdelete(fields, \"level\")\n\t\tdelete(fields, \"msg\")\n\t\tdelete(fields, \"time\")\n\t\tentry = entry.WithFields(fields)\n\t}\n\t// Don't exit on Fatal or Panic entries\n\tif lv <= logrus.FatalLevel {\n\t\tentry = entry.WithField(\"level\", lv)\n\t\tlv = logrus.ErrorLevel\n\t}\n\tentry.Log(lv, header+j.Msg)\n\treturn\n\nfallback:\n\tentry.Info(header + string(jsonLine))\n}\n\n// JSON is the type used in logrus.JSONFormatter.\ntype JSON struct {\n\tLevel string    `json:\"level\"`\n\tMsg   string    `json:\"msg\"`\n\tTime  time.Time `json:\"time\"`\n}\n"
  },
  {
    "path": "pkg/logrusutil/logrusutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage logrusutil\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestPropagateJSON(t *testing.T) {\n\tloggerWithoutTS := func(output *bytes.Buffer) *logrus.Logger {\n\t\tlogger := logrus.New()\n\t\tlogger.SetOutput(output)\n\t\tlogger.SetLevel(logrus.TraceLevel)\n\t\tlogger.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true})\n\t\treturn logger\n\t}\n\n\tt.Run(\"trace level\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"trace\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=trace msg=header\\n\", actual.String())\n\t})\n\tt.Run(\"debug level\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"debug\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=debug msg=header\\n\", actual.String())\n\t})\n\tt.Run(\"info level\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"info\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=info msg=header\\n\", actual.String())\n\t})\n\tt.Run(\"error level\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"error\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=error msg=header\\n\", actual.String())\n\t})\n\tt.Run(\"warning level\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"warning\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=warning msg=header\\n\", actual.String())\n\t})\n\tt.Run(\"panic level\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"panic\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=error msg=header fields.level=panic\\n\", actual.String())\n\t})\n\tt.Run(\"fatal level\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"fatal\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=error msg=header fields.level=fatal\\n\", actual.String())\n\t})\n\tt.Run(\"SetLevel\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tlogger.SetLevel(logrus.ErrorLevel)\n\t\tjsonLine := []byte(`{\"level\": \"warning\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"\", actual.String())\n\t})\n\tt.Run(\"extra fields\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"warning\", \"error\": \"oops\", \"extra\": \"field\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"level=warning msg=header error=oops extra=field\\n\", actual.String())\n\t})\n\tt.Run(\"timestamp\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tlogger.SetFormatter(&logrus.TextFormatter{DisableTimestamp: false})\n\t\tjsonLine := []byte(`{\"level\": \"warning\", \"time\": \"2024-03-06T00:20:53-08:00\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"time=\\\"2024-03-06T00:20:53-08:00\\\" level=warning msg=header\\n\", actual.String())\n\t})\n\tt.Run(\"empty json line\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte{}\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, \"\", actual.String())\n\t})\n\tt.Run(\"unmarshal failed\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`\"`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, `level=info msg=\"header\\\"\"\n`, actual.String())\n\t})\n\tt.Run(\"begin time after time in jsonLine\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"info\", \"time\": \"2023-12-01T00:00:00.0000+00:00\"}`)\n\t\tbegin := time.Date(2023, time.December, 15, 0, 0, 0, 0, time.UTC)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", begin)\n\n\t\tassert.Equal(t, \"\", actual.String())\n\t})\n\tt.Run(\"parse level failed\", func(t *testing.T) {\n\t\tactual := &bytes.Buffer{}\n\t\tlogger := loggerWithoutTS(actual)\n\t\tjsonLine := []byte(`{\"level\": \"info\", \"level\": \"unknown level\"}`)\n\n\t\tPropagateJSON(logger, jsonLine, \"header\", time.Time{})\n\n\t\tassert.Equal(t, `level=info msg=\"header{\\\"level\\\": \\\"info\\\", \\\"level\\\": \\\"unknown level\\\"}\"\n`, actual.String())\n\t})\n}\n"
  },
  {
    "path": "pkg/mcp/msi/filesystem.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Portion of AI prompt texts from:\n// - https://github.com/google-gemini/gemini-cli/blob/v0.1.12/docs/tools/file-system.md\n//\n// SPDX-FileCopyrightText: Copyright 2025 Google LLC\n\npackage msi\n\nimport (\n\t\"io/fs\"\n\t\"time\"\n\n\t\"github.com/modelcontextprotocol/go-sdk/mcp\"\n)\n\nvar ListDirectory = &mcp.Tool{\n\tName:        \"list_directory\",\n\tDescription: `Lists the names of files and subdirectories directly within a specified directory path.`,\n}\n\ntype ListDirectoryParams struct {\n\tPath string `json:\"path\" jsonschema:\"The absolute path to the directory to list.\"`\n}\n\n// ListDirectoryResultEntry is similar to [io/fs.FileInfo].\ntype ListDirectoryResultEntry struct {\n\tName    string       `json:\"name\" jsonschema:\"base name of the file\"`\n\tSize    *int64       `json:\"size,omitempty\" jsonschema:\"length in bytes for regular files; system-dependent for others\"`\n\tMode    *fs.FileMode `json:\"mode,omitempty\" jsonschema:\"file mode bits\"`\n\tModTime *time.Time   `json:\"time,omitempty\" jsonschema:\"modification time\"`\n\tIsDir   *bool        `json:\"is_dir,omitempty\" jsonschema:\"true for a directory\"`\n}\n\ntype ListDirectoryResult struct {\n\tEntries []ListDirectoryResultEntry `json:\"entries\" jsonschema:\"The directory content entries.\"`\n}\n\nvar ReadFile = &mcp.Tool{\n\tName:        \"read_file\",\n\tDescription: `Reads and returns the content of a specified file.`,\n}\n\ntype ReadFileResult struct {\n\tContent string `json:\"content\" jsonschema:\"The content of the file.\"`\n}\n\ntype ReadFileParams struct {\n\tPath string `json:\"path\" jsonschema:\"The absolute path to the file to read.\"`\n\t// TODO: Offset *int   `json:\"offset,omitempty\" jsonschema:\"For text files, the 0-based line number to start reading from. Requires limit to be set.\"`\n\t// TODO: Limit  *int   `json:\"limit,omitempty\" jsonschema:\"For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible.\"`\n}\n\nvar WriteFile = &mcp.Tool{\n\tName:        \"write_file\",\n\tDescription: `Writes content to a specified file. If the file exists, it will be overwritten. If the file doesn't exist, it (and any necessary parent directories) will be created.`,\n}\n\ntype WriteFileResult struct {\n\t// Empty for now\n}\n\ntype WriteFileParams struct {\n\tPath    string `json:\"path\" jsonschema:\"The absolute path to the file to write to.\"`\n\tContent string `json:\"content\" jsonschema:\"The content to write into the file.\"`\n}\n\nvar Glob = &mcp.Tool{\n\tName:        \"glob\",\n\tDescription: `Finds files matching specific glob patterns (e.g., src/**/*.ts, *.md)`, // Not sorted by mod time, unlike Gemini\n}\n\ntype GlobParams struct {\n\tPattern string  `json:\"pattern\" jsonschema:\"The glob pattern to match against (e.g., '*.py', 'src/**/*.js').\"`\n\tPath    *string `json:\"path,omitempty\" jsonschema:\"The absolute path to the directory to search within. If omitted, searches the tool's root directory.\"`\n\t// TODO: CaseSensitive bool    `json:\"case_sensitive,omitempty\" jsonschema:\": Whether the search should be case-sensitive. Defaults to false.\"`\n}\n\ntype GlobResult struct {\n\tMatches []string `json:\"matches\" jsonschema:\"A list of absolute file paths that match the provided glob pattern.\"`\n}\n\nvar SearchFileContent = &mcp.Tool{\n\tName:        \"search_file_content\",\n\tDescription: `Searches for a regular expression pattern within the content of files in a specified directory. Internally calls 'git grep -n --no-index'.`,\n}\n\ntype SearchFileContentParams struct {\n\tPattern string  `json:\"pattern\" jsonschema:\"The regular expression (regex) to search for (e.g., 'function\\\\s+myFunction').\"`\n\tPath    *string `json:\"path,omitempty\" jsonschema:\"The absolute path to the directory to search within. Defaults to the current working directory.\"`\n\tInclude *string `json:\"include,omitempty\" jsonschema:\"A glob pattern to filter which files are searched (e.g., '*.js', 'src/**/*.{ts,tsx}'). If omitted, searches most files (respecting common ignores).\"`\n}\n\ntype SearchFileContentResult struct {\n\tGitGrepOutput string `json:\"git_grep_output\" jsonschema:\"The raw output from the 'git grep -n --no-index' command, containing matching lines with filenames and line numbers.\"`\n}\n\n// TODO: implement Replace\n"
  },
  {
    "path": "pkg/mcp/msi/msi.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package msi provides the \"MCP Sandbox Interface\" (tentative)\n// that should be reusable for other projects too.\n//\n// MCP Sandbox Interface defines MCP (Model Context Protocol) tools\n// that can be used for reading, writing, and executing local files\n// with an appropriate sandboxing technology. The sandboxing technology\n// can be more secure and/or efficient than the default tools provided\n// by an AI agent.\n//\n// MCP Sandbox Interface was inspired by Gemini CLI's built-in tools.\n// https://github.com/google-gemini/gemini-cli/tree/v0.1.12/docs/tools\n//\n// Notable differences from Gemini CLI's built-in tools:\n//   - the output format is JSON, not a plain text\n//   - the output of [SearchFileContent] always corresponds to `git grep -n --no-index`\n//   - [RunShellCommandParams].Command is a string slice, not a string\n//   - [RunShellCommandParams].Directory is an absolute path, not a relative path\n//   - [RunShellCommandParams].Directory must not be empty\n//\n// Eventually, this package may be split to a separate repository.\npackage msi\n"
  },
  {
    "path": "pkg/mcp/msi/shell.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Portion of AI prompt texts from:\n// - https://github.com/google-gemini/gemini-cli/blob/v0.1.12/docs/tools/shell.md\n//\n// SPDX-FileCopyrightText: Copyright 2025 Google LLC\n\npackage msi\n\nimport \"github.com/modelcontextprotocol/go-sdk/mcp\"\n\nvar RunShellCommand = &mcp.Tool{\n\tName:        \"run_shell_command\",\n\tDescription: `Executes a given shell command.`,\n}\n\ntype RunShellCommandParams struct {\n\tCommand     []string `json:\"command\" jsonschema:\"The exact shell command to execute. Defined as a string slice, unlike Gemini's run_shell_command that defines it as a single string.\"`\n\tDescription string   `json:\"description,omitempty\" jsonschema:\"A brief description of the command's purpose, which will be potentially shown to the user.\"`\n\tDirectory   string   `json:\"directory\" jsonschema:\"The absolute directory in which to execute the command. Unlike Gemini's run_shell_command, this must not be a relative path, and must not be empty.\"`\n}\n\ntype RunShellCommandResult struct {\n\tStdout   string `json:\"stdout\" jsonschema:\"Output from the standard output stream.\"`\n\tStderr   string `json:\"stderr\" jsonschema:\"Output from the standard error stream.\"`\n\tError    string `json:\"error,omitempty\" jsonschema:\"Any error message reported by the subprocess.\"`\n\tExitCode *int   `json:\"exit_code,omitempty\" jsonschema:\"Exit code of the command.\"`\n}\n"
  },
  {
    "path": "pkg/mcp/toolset/filesystem.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage toolset\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/modelcontextprotocol/go-sdk/mcp\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/mcp/msi\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n)\n\nfunc (ts *ToolSet) ListDirectory(ctx context.Context,\n\t_ *mcp.CallToolRequest, args msi.ListDirectoryParams,\n) (*mcp.CallToolResult, *msi.ListDirectoryResult, error) {\n\tif ts.inst == nil {\n\t\treturn nil, nil, errors.New(\"instance not registered\")\n\t}\n\tguestPath, err := ts.TranslateHostPath(args.Path)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tguestEnts, err := ts.sftp.ReadDirContext(ctx, guestPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tres := &msi.ListDirectoryResult{\n\t\tEntries: make([]msi.ListDirectoryResultEntry, len(guestEnts)),\n\t}\n\tfor i, f := range guestEnts {\n\t\tres.Entries[i].Name = f.Name()\n\t\tres.Entries[i].Size = ptr.Of(f.Size())\n\t\tres.Entries[i].Mode = ptr.Of(f.Mode())\n\t\tres.Entries[i].ModTime = ptr.Of(f.ModTime())\n\t\tres.Entries[i].IsDir = ptr.Of(f.IsDir())\n\t}\n\treturn &mcp.CallToolResult{\n\t\tStructuredContent: res,\n\t}, res, nil\n}\n\nfunc (ts *ToolSet) ReadFile(_ context.Context,\n\t_ *mcp.CallToolRequest, args msi.ReadFileParams,\n) (*mcp.CallToolResult, *msi.ReadFileResult, error) {\n\tif ts.inst == nil {\n\t\treturn nil, nil, errors.New(\"instance not registered\")\n\t}\n\tguestPath, err := ts.TranslateHostPath(args.Path)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tf, err := ts.sftp.Open(guestPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdefer f.Close()\n\tconst limitBytes = 32 * 1024 * 1024\n\tlr := io.LimitReader(f, limitBytes)\n\tb, err := io.ReadAll(lr)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tres := &msi.ReadFileResult{\n\t\tContent: string(b),\n\t}\n\treturn &mcp.CallToolResult{\n\t\t// Gemini:\n\t\t// For text files: The file content, potentially prefixed with a truncation message\n\t\t// (e.g., [File content truncated: showing lines 1-100 of 500 total lines...]\\nActual file content...).\n\t\tStructuredContent: res,\n\t}, res, nil\n}\n\nfunc (ts *ToolSet) WriteFile(_ context.Context,\n\t_ *mcp.CallToolRequest, args msi.WriteFileParams,\n) (*mcp.CallToolResult, *msi.WriteFileResult, error) {\n\tif ts.inst == nil {\n\t\treturn nil, nil, errors.New(\"instance not registered\")\n\t}\n\tguestPath, err := ts.TranslateHostPath(args.Path)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdir := filepath.Dir(guestPath)\n\terr = ts.sftp.MkdirAll(dir)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tf, err := ts.sftp.Create(guestPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tdefer f.Close()\n\t_, err = f.Write([]byte(args.Content))\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tres := &msi.WriteFileResult{}\n\treturn &mcp.CallToolResult{\n\t\t// Gemini:\n\t\t// A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt`\n\t\t// or `Successfully created and wrote to new file: /path/to/new/file.txt.`\n\t\tStructuredContent: res,\n\t}, res, nil\n}\n\nfunc (ts *ToolSet) Glob(_ context.Context,\n\t_ *mcp.CallToolRequest, args msi.GlobParams,\n) (*mcp.CallToolResult, *msi.GlobResult, error) {\n\tif ts.inst == nil {\n\t\treturn nil, nil, errors.New(\"instance not registered\")\n\t}\n\tpathStr, err := os.Getwd()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif args.Path != nil && *args.Path != \"\" {\n\t\tpathStr = *args.Path\n\t}\n\tguestPath, err := ts.TranslateHostPath(pathStr)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tpattern := path.Join(guestPath, args.Pattern)\n\tmatches, err := ts.sftp.Glob(pattern)\n\tif matches == nil {\n\t\tmatches = []string{}\n\t}\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tres := &msi.GlobResult{\n\t\tMatches: matches,\n\t}\n\treturn &mcp.CallToolResult{\n\t\t// Gemini:\n\t\t// A message like: Found 5 file(s) matching \"*.ts\" within src, sorted by modification time (newest first):\\nsrc/file1.ts\\nsrc/subdir/file2.ts...\n\t\tStructuredContent: res,\n\t}, res, nil\n}\n\nfunc (ts *ToolSet) SearchFileContent(ctx context.Context,\n\treq *mcp.CallToolRequest, args msi.SearchFileContentParams,\n) (*mcp.CallToolResult, *msi.SearchFileContentResult, error) {\n\tif ts.inst == nil {\n\t\treturn nil, nil, errors.New(\"instance not registered\")\n\t}\n\tpathStr, err := os.Getwd()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif args.Path != nil && *args.Path != \"\" {\n\t\tpathStr = *args.Path\n\t}\n\tguestPath, err := ts.TranslateHostPath(pathStr)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif args.Include != nil && *args.Include != \"\" {\n\t\tguestPath = path.Join(guestPath, *args.Include)\n\t}\n\tcmdToolRes, cmdRes, err := ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{\n\t\tCommand:   []string{\"git\", \"grep\", \"-n\", \"--no-index\", args.Pattern, guestPath},\n\t\tDirectory: pathStr, // Directory must be always set\n\t})\n\tif err != nil {\n\t\treturn cmdToolRes, nil, err\n\t}\n\tres := &msi.SearchFileContentResult{\n\t\tGitGrepOutput: cmdRes.Stdout,\n\t}\n\treturn &mcp.CallToolResult{\n\t\t// Gemini:\n\t\t// A message like: Found 10 matching lines for regex \"function\\\\s+myFunction\" in directory src:\\nsrc/file1.js:10:function myFunction() {...}\\nsrc/subdir/file2.ts:45:    function myFunction(param) {...}...\n\t\tStructuredContent: res,\n\t}, res, nil\n}\n"
  },
  {
    "path": "pkg/mcp/toolset/shell.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage toolset\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\n\t\"github.com/modelcontextprotocol/go-sdk/mcp\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/mcp/msi\"\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n)\n\nfunc (ts *ToolSet) RunShellCommand(ctx context.Context,\n\t_ *mcp.CallToolRequest, args msi.RunShellCommandParams,\n) (*mcp.CallToolResult, *msi.RunShellCommandResult, error) {\n\tif ts.inst == nil {\n\t\treturn nil, nil, errors.New(\"instance not registered\")\n\t}\n\tguestPath, err := ts.TranslateHostPath(args.Directory)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tcmd := exec.CommandContext(ctx, ts.limactl,\n\t\tappend([]string{\"shell\", \"--workdir=\" + guestPath, ts.inst.Name},\n\t\t\targs.Command...)...)\n\tvar stdout, stderr bytes.Buffer\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tcmdErr := cmd.Run()\n\tres := &msi.RunShellCommandResult{\n\t\tStdout: stdout.String(),\n\t\tStderr: stderr.String(),\n\t}\n\tif cmdErr == nil {\n\t\tres.ExitCode = ptr.Of(0)\n\t} else {\n\t\tres.Error = cmdErr.Error()\n\t\tif st := cmd.ProcessState; st != nil {\n\t\t\tres.ExitCode = ptr.Of(st.ExitCode())\n\t\t}\n\t}\n\treturn &mcp.CallToolResult{\n\t\tStructuredContent: res,\n\t}, res, nil\n}\n"
  },
  {
    "path": "pkg/mcp/toolset/toolset.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage toolset\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/modelcontextprotocol/go-sdk/mcp\"\n\t\"github.com/pkg/sftp\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/instance/hostname\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/mcp/msi\"\n\t\"github.com/lima-vm/lima/v2/pkg/sshutil\"\n)\n\nfunc New(limactl string) (*ToolSet, error) {\n\tts := &ToolSet{\n\t\tlimactl: limactl,\n\t}\n\treturn ts, nil\n}\n\ntype ToolSet struct {\n\tlimactl string // needed for `limactl shell --workdir`\n\n\t// Set on RegisterInstance()\n\tinst    *limatype.Instance\n\tsftp    *sftp.Client\n\tsftpCmd *exec.Cmd\n}\n\nfunc newSFTPClient(ctx context.Context, inst *limatype.Instance) (*sftp.Client, *exec.Cmd, error) {\n\tsshExe, err := sshutil.NewSSHExe()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\targs := slices.Concat(sshExe.Args, []string{\"-F\", filepath.Join(inst.Dir, filenames.SSHConfig), hostname.FromInstName(inst.Name), \"-s\", \"sftp\"})\n\tcmd := exec.CommandContext(ctx, sshExe.Exe, args...)\n\tcmd.Stderr = os.Stderr\n\tw, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tr, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err = cmd.Start(); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tclient, err := sftp.NewClientPipe(r, w)\n\tif err != nil {\n\t\tif cmd != nil && cmd.Process != nil {\n\t\t\t_ = cmd.Process.Kill()\n\t\t}\n\t}\n\treturn client, cmd, err\n}\n\nfunc (ts *ToolSet) RegisterInstance(ctx context.Context, inst *limatype.Instance) error {\n\tif inst.Status != limatype.StatusRunning {\n\t\treturn fmt.Errorf(\"expected status of instance %q to be %q, got %q\",\n\t\t\tinst.Name, limatype.StatusRunning, inst.Status)\n\t}\n\tif len(inst.Config.Mounts) == 0 {\n\t\tlogrus.Warnf(\"instance %q has no mount\", inst.Name)\n\t}\n\tts.inst = inst\n\tvar err error\n\tts.sftp, ts.sftpCmd, err = newSFTPClient(ctx, inst)\n\treturn err\n}\n\nfunc (ts *ToolSet) RegisterServer(server *mcp.Server) error {\n\tmcp.AddTool(server, msi.ListDirectory, ts.ListDirectory)\n\tmcp.AddTool(server, msi.ReadFile, ts.ReadFile)\n\tmcp.AddTool(server, msi.WriteFile, ts.WriteFile)\n\tmcp.AddTool(server, msi.Glob, ts.Glob)\n\tmcp.AddTool(server, msi.SearchFileContent, ts.SearchFileContent)\n\tmcp.AddTool(server, msi.RunShellCommand, ts.RunShellCommand)\n\treturn nil\n}\n\nfunc (ts *ToolSet) Close() error {\n\tvar err error\n\tif ts.sftp != nil {\n\t\terr = errors.Join(err, ts.sftp.Close())\n\t}\n\tif ts.sftpCmd != nil && ts.sftpCmd.Process != nil {\n\t\terr = errors.Join(err, ts.sftpCmd.Process.Kill())\n\t}\n\treturn err\n}\n\nfunc (ts *ToolSet) TranslateHostPath(hostPath string) (string, error) {\n\tif hostPath == \"\" {\n\t\treturn \"\", errors.New(\"path is empty\")\n\t}\n\tif !filepath.IsAbs(hostPath) {\n\t\treturn \"\", fmt.Errorf(\"expected an absolute path, got a relative path: %q\", hostPath)\n\t}\n\t// TODO: make sure that hostPath is mounted\n\treturn hostPath, nil\n}\n"
  },
  {
    "path": "pkg/must/must.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage must\n\nfunc Must[T any](obj T, err error) T {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn obj\n}\n"
  },
  {
    "path": "pkg/networks/commands.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\nconst (\n\tSocketVMNet = \"socket_vmnet\"\n)\n\n// Commands in `sudoers` cannot use quotes, so all arguments are printed via \"%s\"\n// and not \"%q\". cfg.Paths.* entries must not include any whitespace!\n\nfunc (c *Config) Check(name string) error {\n\tif _, ok := c.Networks[name]; ok {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"network %q is not defined\", name)\n}\n\n// Usernet returns true if the mode of given network is ModeUserV2.\nfunc (c *Config) Usernet(name string) (bool, error) {\n\tif nw, ok := c.Networks[name]; ok {\n\t\treturn nw.Mode == ModeUserV2, nil\n\t}\n\treturn false, fmt.Errorf(\"network %q is not defined\", name)\n}\n\n// DaemonPath returns the daemon path.\nfunc (c *Config) DaemonPath(daemon string) (string, error) {\n\tswitch daemon {\n\tcase SocketVMNet:\n\t\treturn c.Paths.SocketVMNet, nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"unknown daemon type %q\", daemon)\n\t}\n}\n\n// IsDaemonInstalled checks whether the daemon is installed.\nfunc (c *Config) IsDaemonInstalled(daemon string) (bool, error) {\n\tp, err := c.DaemonPath(daemon)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif p == \"\" {\n\t\treturn false, nil\n\t}\n\tif _, err := exec.LookPath(p); err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, err\n\t}\n\treturn true, nil\n}\n\n// Sock returns a socket_vmnet socket.\nfunc (c *Config) Sock(name string) string {\n\treturn filepath.Join(c.Paths.VarRun, fmt.Sprintf(\"socket_vmnet.%s\", name))\n}\n\nfunc (c *Config) PIDFile(name, daemon string) string {\n\treturn filepath.Join(c.Paths.VarRun, fmt.Sprintf(\"%s_%s.pid\", name, daemon))\n}\n\nfunc (c *Config) LogFile(name, daemon, stream string) string {\n\tnetworksDir, _ := dirnames.LimaNetworksDir()\n\treturn filepath.Join(networksDir, fmt.Sprintf(\"%s_%s.%s.log\", name, daemon, stream))\n}\n\nfunc (c *Config) User(daemon string) (osutil.User, error) {\n\tif ok, _ := c.IsDaemonInstalled(daemon); !ok {\n\t\tdaemonPath, _ := c.DaemonPath(daemon)\n\t\treturn osutil.User{}, fmt.Errorf(\"daemon %q (path=%q) is not available\", daemon, daemonPath)\n\t}\n\t//nolint:gocritic // singleCaseSwitch: should rewrite switch statement to if statement\n\tswitch daemon {\n\tcase SocketVMNet:\n\t\treturn osutil.LookupUser(\"root\")\n\t}\n\treturn osutil.User{}, fmt.Errorf(\"daemon %q not defined\", daemon)\n}\n\nfunc (c *Config) MkdirCmd() string {\n\treturn fmt.Sprintf(\"/bin/mkdir -m 775 -p %s\", c.Paths.VarRun)\n}\n\nfunc (c *Config) StartCmd(name, daemon string) string {\n\tif ok, _ := c.IsDaemonInstalled(daemon); !ok {\n\t\tpanic(fmt.Errorf(\"daemon %q is not available\", daemon))\n\t}\n\tvar cmd string\n\tswitch daemon {\n\tcase SocketVMNet:\n\t\tnw := c.Networks[name]\n\t\tif c.Paths.SocketVMNet == \"\" {\n\t\t\tpanic(\"c.Paths.SocketVMNet is empty\")\n\t\t}\n\t\tcmd = fmt.Sprintf(\"%s --pidfile=%s --socket-group=%s --vmnet-mode=%s\",\n\t\t\tc.Paths.SocketVMNet, c.PIDFile(name, SocketVMNet), c.Group, nw.Mode)\n\t\tswitch nw.Mode {\n\t\tcase ModeBridged:\n\t\t\tcmd += fmt.Sprintf(\" --vmnet-interface=%s\", nw.Interface)\n\t\tcase ModeHost, ModeShared:\n\t\t\tcmd += fmt.Sprintf(\" --vmnet-gateway=%s --vmnet-dhcp-end=%s --vmnet-mask=%s\",\n\t\t\t\tnw.Gateway, nw.DHCPEnd, nw.NetMask)\n\t\t}\n\t\tcmd += \" \" + c.Sock(name)\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected daemon %q\", daemon))\n\t}\n\treturn cmd\n}\n\nfunc (c *Config) StopCmd(name, daemon string) string {\n\treturn fmt.Sprintf(\"/usr/bin/pkill -F %s\", c.PIDFile(name, daemon))\n}\n"
  },
  {
    "path": "pkg/networks/commands_darwin_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestSock(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\n\tsock := config.Sock(\"foo\")\n\tassert.Equal(t, sock, \"/private/var/run/lima/socket_vmnet.foo\")\n}\n\nfunc TestPIDFile(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\n\tpidFile := config.PIDFile(\"name\", \"daemon\")\n\tassert.Equal(t, pidFile, \"/private/var/run/lima/name_daemon.pid\")\n}\n"
  },
  {
    "path": "pkg/networks/commands_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n)\n\nfunc TestCheck(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\n\tfor _, name := range []string{\"bridged\", \"shared\", \"host\"} {\n\t\terr = config.Check(name)\n\t\tassert.NilError(t, err)\n\t}\n\terr = config.Check(\"unknown\")\n\tassert.ErrorContains(t, err, \"not defined\")\n}\n\nfunc TestLogFile(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\n\tlogFile := config.LogFile(\"name\", \"daemon\", \"stream\")\n\tnetworksDir, err := dirnames.LimaNetworksDir()\n\tassert.NilError(t, err)\n\tassert.Equal(t, logFile, filepath.Join(networksDir, \"name_daemon.stream.log\"))\n}\n\nfunc TestUser(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\tif runtime.GOOS != \"darwin\" && config.Group == \"everyone\" {\n\t\t// The \"everyone\" group is a specific macOS feature to include non-local accounts.\n\t\tconfig.Group = \"staff\"\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\t// unimplemented\n\t\tt.Skip()\n\t}\n\n\tt.Run(\"socket_vmnet\", func(t *testing.T) {\n\t\tif ok, _ := config.IsDaemonInstalled(SocketVMNet); !ok {\n\t\t\tt.Skip(\"socket_vmnet is not installed\")\n\t\t}\n\t\tuser, err := config.User(SocketVMNet)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, user.User, \"root\")\n\t\tif runtime.GOOS == \"darwin\" {\n\t\t\tassert.Equal(t, user.Group, \"wheel\")\n\t\t} else {\n\t\t\tassert.Equal(t, user.Group, \"root\")\n\t\t}\n\t\tassert.Equal(t, user.Uid, uint32(0))\n\t\tassert.Equal(t, user.Gid, uint32(0))\n\t})\n}\n\nfunc TestMkdirCmd(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\n\tcmd := config.MkdirCmd()\n\tassert.Equal(t, cmd, \"/bin/mkdir -m 775 -p /private/var/run/lima\")\n}\n\nfunc TestStartCmd(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\n\tvarRunDir := filepath.Join(\"/\", \"private\", \"var\", \"run\", \"lima\")\n\n\tt.Run(\"socket_vmnet\", func(t *testing.T) {\n\t\tif ok, _ := config.IsDaemonInstalled(SocketVMNet); !ok {\n\t\t\tt.Skip(\"socket_vmnet is not installed\")\n\t\t}\n\n\t\tcmd := config.StartCmd(\"shared\", SocketVMNet)\n\t\tassert.Equal(t, cmd, \"/opt/socket_vmnet/bin/socket_vmnet --pidfile=\"+filepath.Join(varRunDir, \"shared_socket_vmnet.pid\")+\" --socket-group=everyone --vmnet-mode=shared \"+\n\t\t\t\"--vmnet-gateway=192.168.105.1 --vmnet-dhcp-end=192.168.105.254 --vmnet-mask=255.255.255.0 \"+filepath.Join(varRunDir, \"socket_vmnet.shared\"))\n\n\t\tcmd = config.StartCmd(\"bridged\", SocketVMNet)\n\t\tassert.Equal(t, cmd, \"/opt/socket_vmnet/bin/socket_vmnet --pidfile=\"+filepath.Join(varRunDir, \"bridged_socket_vmnet.pid\")+\" --socket-group=everyone --vmnet-mode=bridged \"+\n\t\t\t\"--vmnet-interface=en0 \"+filepath.Join(varRunDir, \"socket_vmnet.bridged\"))\n\t})\n}\n\nfunc TestStopCmd(t *testing.T) {\n\tconfig, err := DefaultConfig()\n\tassert.NilError(t, err)\n\n\tvarRunDir := filepath.Join(\"/\", \"private\", \"var\", \"run\", \"lima\")\n\n\tcmd := config.StopCmd(\"name\", \"daemon\")\n\tassert.Equal(t, cmd, \"/usr/bin/pkill -F \"+filepath.Join(varRunDir, \"name_daemon.pid\"))\n}\n"
  },
  {
    "path": "pkg/networks/config.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport (\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/textutil\"\n)\n\n//go:embed networks.TEMPLATE.yaml\nvar defaultConfigTemplate string\n\ntype defaultConfigTemplateArgs struct {\n\tSocketVMNet string // \"/opt/socket_vmnet/bin/socket_vmnet\"\n}\n\nfunc defaultConfigBytes() ([]byte, error) {\n\tvar args defaultConfigTemplateArgs\n\tcandidates := []string{\n\t\t\"/opt/socket_vmnet/bin/socket_vmnet\", // the hard-coded path before v0.14\n\t\t\"socket_vmnet\",\n\t\t\"/usr/local/opt/socket_vmnet/bin/socket_vmnet\",    // Homebrew (Intel)\n\t\t\"/opt/homebrew/opt/socket_vmnet/bin/socket_vmnet\", // Homebrew (ARM)\n\t}\n\tfor _, candidate := range candidates {\n\t\tif p, err := exec.LookPath(candidate); err == nil {\n\t\t\trealP, evalErr := filepath.EvalSymlinks(p)\n\t\t\tif evalErr != nil {\n\t\t\t\treturn nil, evalErr\n\t\t\t}\n\t\t\targs.SocketVMNet = realP\n\t\t\tbreak\n\t\t} else if errors.Is(err, exec.ErrNotFound) || errors.Is(err, os.ErrNotExist) {\n\t\t\tlogrus.WithError(err).Debugf(\"Failed to look up socket_vmnet path %q\", candidate)\n\t\t} else {\n\t\t\tlogrus.WithError(err).Warnf(\"Failed to look up socket_vmnet path %q\", candidate)\n\t\t}\n\t}\n\tif args.SocketVMNet == \"\" {\n\t\targs.SocketVMNet = candidates[0] // the hard-coded path before v0.14\n\t}\n\treturn textutil.ExecuteTemplate(defaultConfigTemplate, args)\n}\n\nfunc fillDefaults(cfg Config) (Config, error) {\n\tusernetFound := false\n\tif cfg.Networks == nil {\n\t\tcfg.Networks = make(map[string]Network)\n\t}\n\tfor nw := range cfg.Networks {\n\t\tif cfg.Networks[nw].Mode == ModeUserV2 && cfg.Networks[nw].Gateway != nil {\n\t\t\tusernetFound = true\n\t\t}\n\t}\n\tif !usernetFound {\n\t\tdefaultCfg, err := DefaultConfig()\n\t\tif err != nil {\n\t\t\treturn cfg, err\n\t\t}\n\t\tcfg.Networks[ModeUserV2] = defaultCfg.Networks[ModeUserV2]\n\t}\n\treturn cfg, nil\n}\n\nfunc DefaultConfig() (Config, error) {\n\tvar cfg Config\n\tb, err := defaultConfigBytes()\n\tif err != nil {\n\t\treturn cfg, err\n\t}\n\terr = yaml.UnmarshalWithOptions(b, &cfg, yaml.Strict())\n\tif err != nil {\n\t\treturn cfg, err\n\t}\n\treturn cfg, nil\n}\n\nvar cache struct {\n\tsync.Once\n\tcfg Config\n\terr error\n}\n\nfunc ConfigFile() (string, error) {\n\tcfgDir, err := dirnames.LimaConfigDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(cfgDir, filenames.NetworksConfig), nil\n}\n\n// loadCache loads the _config/networks.yaml file into the cache.\nfunc loadCache() {\n\tcache.Do(func() {\n\t\tvar cfgFile string\n\t\tcfgFile, cache.err = ConfigFile()\n\t\tif cache.err != nil {\n\t\t\treturn\n\t\t}\n\t\t_, cache.err = os.Stat(cfgFile)\n\t\tif cache.err != nil {\n\t\t\tif !errors.Is(cache.err, os.ErrNotExist) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcfgDir := filepath.Dir(cfgFile)\n\t\t\tcache.err = os.MkdirAll(cfgDir, 0o755)\n\t\t\tif cache.err != nil {\n\t\t\t\tcache.err = fmt.Errorf(\"could not create %q directory: %w\", cfgDir, cache.err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvar b []byte\n\t\t\tb, cache.err = defaultConfigBytes()\n\t\t\tif cache.err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcache.err = os.WriteFile(cfgFile, b, 0o644)\n\t\t\tif cache.err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tvar b []byte\n\t\tb, cache.err = os.ReadFile(cfgFile)\n\t\tif cache.err != nil {\n\t\t\treturn\n\t\t}\n\t\tcache.err = yaml.Unmarshal(b, &cache.cfg)\n\t\tif cache.err != nil {\n\t\t\tcache.err = fmt.Errorf(\"cannot parse %q: %w\", cfgFile, cache.err)\n\t\t\treturn\n\t\t}\n\t\tvar strictCfg Config\n\t\tif strictErr := yaml.UnmarshalWithOptions(b, &strictCfg, yaml.Strict()); strictErr != nil {\n\t\t\t// Allow non-existing YAML fields, as a cfg created with Lima < v0.22 contains `vdeSwitch` and `vdeVMNet`.\n\t\t\t// These fields were removed in Lima v0.22.\n\t\t\tlogrus.WithError(strictErr).Warn(\"Non-strict YAML is deprecated and will be unsupported in a future version of Lima: \" + cfgFile)\n\t\t}\n\t\tcache.cfg, cache.err = fillDefaults(cache.cfg)\n\t\tif cache.err != nil {\n\t\t\tcache.err = fmt.Errorf(\"cannot fill default %q: %w\", cfgFile, cache.err)\n\t\t}\n\t})\n}\n\n// LoadConfig returns the network cfg from the _config/networks.yaml file.\nfunc LoadConfig() (Config, error) {\n\tloadCache()\n\treturn cache.cfg, cache.err\n}\n\n// Sock returns a socket_vmnet socket.\nfunc Sock(name string) (string, error) {\n\tloadCache()\n\tif cache.err != nil {\n\t\treturn \"\", cache.err\n\t}\n\tif err := cache.cfg.Check(name); err != nil {\n\t\treturn \"\", err\n\t}\n\tif cache.cfg.Paths.SocketVMNet == \"\" {\n\t\treturn \"\", errors.New(\"socketVMNet is not set\")\n\t}\n\treturn cache.cfg.Sock(name), nil\n}\n\n// IsUsernet returns true if the given network name is a usernet network.\n// It return false if the cache cannot be loaded or the network is not defined.\nfunc IsUsernet(name string) bool {\n\tloadCache()\n\tif cache.err != nil {\n\t\treturn false\n\t}\n\tisUsernet, err := cache.cfg.Usernet(name)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn isUsernet\n}\n"
  },
  {
    "path": "pkg/networks/config_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestFillDefault(t *testing.T) {\n\tcfg, err := fillDefaults(Config{})\n\tassert.NilError(t, err)\n\n\tuserNet := cfg.Networks[ModeUserV2]\n\tassert.Equal(t, userNet.Mode, ModeUserV2)\n\tassert.Equal(t, userNet.Interface, \"\")\n\tassert.DeepEqual(t, userNet.NetMask, net.ParseIP(\"255.255.255.0\"))\n\tassert.DeepEqual(t, userNet.Gateway, net.ParseIP(\"192.168.104.1\"))\n\tassert.DeepEqual(t, userNet.DHCPEnd, net.IP{})\n}\n\nfunc TestFillDefaultWithV2(t *testing.T) {\n\tcfg := Config{Networks: map[string]Network{\n\t\t\"user-v2\": {Mode: ModeUserV2},\n\t}}\n\tcfg, err := fillDefaults(cfg)\n\tassert.NilError(t, err)\n\n\tuserNet := cfg.Networks[ModeUserV2]\n\tassert.Equal(t, userNet.Mode, ModeUserV2)\n\tassert.Equal(t, userNet.Interface, \"\")\n\tassert.DeepEqual(t, userNet.NetMask, net.ParseIP(\"255.255.255.0\"))\n\tassert.DeepEqual(t, userNet.Gateway, net.ParseIP(\"192.168.104.1\"))\n\tassert.DeepEqual(t, userNet.DHCPEnd, net.IP{})\n}\n\nfunc TestFillDefaultWithV2AndGateway(t *testing.T) {\n\tcfg := Config{Networks: map[string]Network{\n\t\t\"user-v2\": {Mode: ModeUserV2, Gateway: net.ParseIP(\"192.168.105.1\")},\n\t}}\n\tcfg, err := fillDefaults(cfg)\n\tassert.NilError(t, err)\n\n\tuserNet := cfg.Networks[ModeUserV2]\n\tassert.Equal(t, userNet.Mode, ModeUserV2)\n\tassert.Equal(t, userNet.Interface, \"\")\n\tassert.DeepEqual(t, userNet.NetMask, net.IP{})\n\tassert.DeepEqual(t, userNet.Gateway, net.ParseIP(\"192.168.105.1\"))\n\tassert.DeepEqual(t, userNet.DHCPEnd, net.IP{})\n}\n"
  },
  {
    "path": "pkg/networks/const.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nconst (\n\tSlirpNICName = \"eth0\"\n\t// CIDR is intentionally hardcoded to 192.168.5.0/24, as each of QEMU has its own independent slirp network.\n\tSlirpNetwork   = \"192.168.5.0/24\"\n\tSlirpGateway   = \"192.168.5.2\"\n\tSlirpIPAddress = \"192.168.5.15\"\n)\n"
  },
  {
    "path": "pkg/networks/networks.TEMPLATE.yaml",
    "content": "# Path to socket_vmnet executable. Because socket_vmnet is invoked via sudo it should be\n# installed where only root can modify/replace it. This means also none of the\n# parent directories can be writable by the user.\n#\n# The varRun directory also must not be writable by the user because it will\n# include the socket_vmnet pid file. Those will be terminated via sudo, so replacing\n# the pid file would allow killing of arbitrary privileged processes. varRun\n# however MUST be writable by the daemon user.\n#\n# None of the paths segments may be symlinks, why it has to be /private/var\n# instead of /var etc.\npaths:\n# socketVMNet requires Lima >= 0.12 .\n  socketVMNet: \"{{.SocketVMNet}}\"\n  varRun: /private/var/run/lima\n  sudoers: /private/etc/sudoers.d/lima\n\ngroup: everyone\n\nnetworks:\n  user-v2:\n    mode: user-v2\n    gateway: 192.168.104.1\n    netmask: 255.255.255.0\n  shared:\n    mode: shared\n    gateway: 192.168.105.1\n    dhcpEnd: 192.168.105.254\n    netmask: 255.255.255.0\n  bridged:\n    mode: bridged\n    interface: en0\n    # bridged mode doesn't have a gateway; dhcp is managed by outside network\n  host:\n    mode: host\n    gateway: 192.168.106.1\n    dhcpEnd: 192.168.106.254\n    netmask: 255.255.255.0\n"
  },
  {
    "path": "pkg/networks/networks.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport \"net\"\n\ntype Config struct {\n\tPaths    Paths              `yaml:\"paths\" json:\"paths\"`\n\tGroup    string             `yaml:\"group,omitempty\" json:\"group,omitempty\"` // default: \"everyone\"\n\tNetworks map[string]Network `yaml:\"networks\" json:\"networks\"`\n}\n\ntype Paths struct {\n\tSocketVMNet string `yaml:\"socketVMNet\" json:\"socketVMNet\"`\n\tVarRun      string `yaml:\"varRun\" json:\"varRun\"`\n\tSudoers     string `yaml:\"sudoers,omitempty\" json:\"sudoers,omitempty\"`\n}\n\nconst (\n\tModeUserV2  = \"user-v2\"\n\tModeHost    = \"host\"\n\tModeShared  = \"shared\"\n\tModeBridged = \"bridged\"\n)\n\nvar Modes = []string{\n\tModeUserV2,\n\tModeHost,\n\tModeShared,\n\tModeBridged,\n}\n\ntype Network struct {\n\tMode      string `yaml:\"mode\" json:\"mode\"`                               // \"user-v2\", \"host\", \"shared\", or \"bridged\"\n\tInterface string `yaml:\"interface,omitempty\" json:\"interface,omitempty\"` // only used by \"bridged\" networks\n\tGateway   net.IP `yaml:\"gateway,omitempty\" json:\"gateway,omitempty\"`     // only used by \"user-v2\", \"host\" and \"shared\" networks\n\tDHCPEnd   net.IP `yaml:\"dhcpEnd,omitempty\" json:\"dhcpEnd,omitempty\"`     // default: same as Gateway, last byte is 254\n\tNetMask   net.IP `yaml:\"netmask,omitempty\" json:\"netmask,omitempty\"`     // default: 255.255.255.0\n}\n"
  },
  {
    "path": "pkg/networks/reconcile/reconcile.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage reconcile\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\nfunc Reconcile(ctx context.Context, newInst string) error {\n\tcfg, err := networks.LoadConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tinstances, err := store.Instances()\n\tif err != nil {\n\t\treturn err\n\t}\n\tactiveNetwork := make(map[string]bool, 3)\n\tfor _, instName := range instances {\n\t\tinstance, err := store.Inspect(ctx, instName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// newInst is about to be started, so its networks should be running\n\t\tif instance.Status != limatype.StatusRunning && instName != newInst {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, nw := range instance.Networks {\n\t\t\tif nw.Lima == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := cfg.Networks[nw.Lima]; !ok {\n\t\t\t\tlogrus.Errorf(\"network %q (used by instance %q) is missing from networks.yaml\", nw.Lima, instName)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tactiveNetwork[nw.Lima] = true\n\t\t}\n\t}\n\tfor name := range cfg.Networks {\n\t\tvar err error\n\t\tif activeNetwork[name] {\n\t\t\terr = startNetwork(ctx, &cfg, name)\n\t\t} else {\n\t\t\terr = stopNetwork(ctx, &cfg, name)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc sudo(ctx context.Context, user, group, command string) error {\n\targs := []string{\"--user\", user, \"--group\", group, \"--non-interactive\"}\n\targs = append(args, strings.Split(command, \" \")...)\n\tvar stdout, stderr bytes.Buffer\n\tcmd := exec.CommandContext(ctx, \"sudo\", args...)\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tlogrus.Debugf(\"Running: %v\", cmd.Args)\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: stdout=%q, stderr=%q: %w\",\n\t\t\tcmd.Args, stdout.String(), stderr.String(), err)\n\t}\n\treturn nil\n}\n\nfunc makeVarRun(ctx context.Context, cfg *networks.Config) error {\n\terr := sudo(ctx, \"root\", \"wheel\", cfg.MkdirCmd())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check that VarRun is daemon-group writable. If we don't report it here, the error would only be visible\n\t// in the vde_switch daemon log. This has not been checked by networks.Validate() because only the VarRun\n\t// directory itself needs to be daemon-group writable, any parents just need to be daemon-group executable.\n\tfi, err := os.Stat(cfg.Paths.VarRun)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstat, ok := osutil.SysStat(fi)\n\tif !ok {\n\t\t// should never happen\n\t\treturn fmt.Errorf(\"could not retrieve stat buffer for %q\", cfg.Paths.VarRun)\n\t}\n\tdaemon, err := osutil.LookupUser(\"daemon\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif fi.Mode()&0o20 == 0 || stat.Gid != daemon.Gid {\n\t\treturn fmt.Errorf(\"%q doesn't seem to be writable by the daemon (gid:%d) group\",\n\t\t\tcfg.Paths.VarRun, daemon.Gid)\n\t}\n\treturn nil\n}\n\nfunc startDaemon(ctx context.Context, cfg *networks.Config, name, daemon string) error {\n\tif err := makeVarRun(ctx, cfg); err != nil {\n\t\treturn err\n\t}\n\tnetworksDir, err := dirnames.LimaNetworksDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := os.MkdirAll(networksDir, 0o755); err != nil {\n\t\treturn err\n\t}\n\tuser, err := cfg.User(daemon)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\targs := []string{\"--user\", user.User, \"--group\", user.Group, \"--non-interactive\"}\n\targs = append(args, strings.Split(cfg.StartCmd(name, daemon), \" \")...)\n\tcmd := exec.CommandContext(ctx, \"sudo\", args...)\n\t// set directory to a path the daemon user has read access to because vde_switch calls getcwd() which\n\t// can fail when called from directories like ~/Downloads, which has 700 permissions\n\tcmd.Dir = cfg.Paths.VarRun\n\n\tstdoutPath := cfg.LogFile(name, daemon, \"stdout\")\n\tstderrPath := cfg.LogFile(name, daemon, \"stderr\")\n\tif err := os.RemoveAll(stdoutPath); err != nil {\n\t\treturn err\n\t}\n\tif err := os.RemoveAll(stderrPath); err != nil {\n\t\treturn err\n\t}\n\n\tcmd.Stdout, err = os.Create(stdoutPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcmd.Stderr, err = os.Create(stderrPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogrus.Debugf(\"Starting %q daemon for %q network: %v\", daemon, name, cmd.Args)\n\tif err := cmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w (Hint: check %q, %q)\", cmd.Args, err, stdoutPath, stderrPath)\n\t}\n\treturn nil\n}\n\nvar validation struct {\n\tsync.Once\n\terr error\n}\n\nfunc validateConfig(ctx context.Context, cfg *networks.Config) error {\n\tvalidation.Do(func() {\n\t\t// make sure all cfg.Paths.* are secure\n\t\tvalidation.err = cfg.Validate()\n\t\tif validation.err == nil {\n\t\t\tvalidation.err = cfg.VerifySudoAccess(ctx, cfg.Paths.Sudoers)\n\t\t}\n\t})\n\treturn validation.err\n}\n\nfunc startNetwork(ctx context.Context, cfg *networks.Config, name string) error {\n\tlogrus.Debugf(\"Make sure %q network is running\", name)\n\n\t// Handle usernet first without sudo requirements\n\tisUsernet, err := cfg.Usernet(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif isUsernet {\n\t\tif err := usernet.Start(ctx, name); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to start usernet %q: %w\", name, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn nil\n\t}\n\n\tif err := validateConfig(ctx, cfg); err != nil {\n\t\treturn err\n\t}\n\tvar daemons []string\n\tok, err := cfg.IsDaemonInstalled(networks.SocketVMNet)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif ok {\n\t\tdaemons = append(daemons, networks.SocketVMNet)\n\t} else {\n\t\treturn fmt.Errorf(\"daemon %q needs to be installed\", networks.SocketVMNet)\n\t}\n\tfor _, daemon := range daemons {\n\t\tpid, _ := store.ReadPIDFile(cfg.PIDFile(name, daemon))\n\t\tif pid == 0 {\n\t\t\tlogrus.Infof(\"Starting %s daemon for %q network\", daemon, name)\n\t\t\tif err := startDaemon(ctx, cfg, name, daemon); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc stopNetwork(ctx context.Context, cfg *networks.Config, name string) error {\n\tlogrus.Debugf(\"Make sure %q network is stopped\", name)\n\t// Handle usernet first without sudo requirements\n\tisUsernet, err := cfg.Usernet(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif isUsernet {\n\t\tif err := usernet.Stop(ctx, name); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to stop usernet %q: %w\", name, err)\n\t\t}\n\t\treturn nil\n\t}\n\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn nil\n\t}\n\n\t// Don't call validateConfig() until we actually need to stop a daemon because\n\t// stopNetwork() may be called even when the daemons are not installed.\n\tfor _, daemon := range []string{networks.SocketVMNet} {\n\t\tif ok, _ := cfg.IsDaemonInstalled(daemon); !ok {\n\t\t\tcontinue\n\t\t}\n\t\tpid, _ := store.ReadPIDFile(cfg.PIDFile(name, daemon))\n\t\tif pid != 0 {\n\t\t\tlogrus.Infof(\"Stopping %s daemon for %q network\", daemon, name)\n\t\t\tif err := validateConfig(ctx, cfg); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tuser, err := cfg.User(daemon)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = sudo(ctx, user.User, user.Group, cfg.StopCmd(name, daemon))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t// wait for daemons to terminate (up to 5s) before stopping, otherwise the sockets may not get deleted which\n\t\t// will cause subsequent start commands to fail.\n\t\tstartWaiting := time.Now()\n\t\tfor {\n\t\t\tif pid, _ := store.ReadPIDFile(cfg.PIDFile(name, daemon)); pid == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif time.Since(startWaiting) > 5*time.Second {\n\t\t\t\tlogrus.Infof(\"%q daemon for %q network still running after 5 seconds\", daemon, name)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/networks/sudoers.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc Sudoers() (string, error) {\n\tcfg, err := LoadConfig()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar sb strings.Builder\n\tfmt.Fprintf(&sb, \"%%%s ALL=(root:wheel) NOPASSWD:NOSETENV: %s\\n\", cfg.Group, cfg.MkdirCmd())\n\n\t// names must be in stable order to be able to check if sudoers file needs updating\n\tnames := make([]string, 0, len(cfg.Networks))\n\tfor name, nw := range cfg.Networks {\n\t\tif nw.Mode == ModeUserV2 {\n\t\t\tcontinue // no sudo needed\n\t\t}\n\t\tnames = append(names, name)\n\t}\n\tslices.Sort(names)\n\n\tfor _, name := range names {\n\t\tsb.WriteRune('\\n')\n\t\tfmt.Fprintf(&sb, \"# Manage %q network daemons\\n\", name)\n\t\tfor _, daemon := range []string{SocketVMNet} {\n\t\t\tif ok, err := cfg.IsDaemonInstalled(daemon); err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t} else if !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tuser, err := cfg.User(daemon)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tsb.WriteRune('\\n')\n\t\t\tfmt.Fprintf(&sb, \"%%%s ALL=(%s:%s) NOPASSWD:NOSETENV: \\\\\\n\", cfg.Group, user.User, user.Group)\n\t\t\tfmt.Fprintf(&sb, \"    %s, \\\\\\n\", cfg.StartCmd(name, daemon))\n\t\t\tfmt.Fprintf(&sb, \"    %s\\n\", cfg.StopCmd(name, daemon))\n\t\t}\n\t}\n\treturn sb.String(), nil\n}\n\nfunc (c *Config) passwordLessSudo(ctx context.Context) error {\n\t// Flush cached sudo password\n\tcmd := exec.CommandContext(ctx, \"sudo\", \"-k\")\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %w\", cmd.Args, err)\n\t}\n\t// Verify that user/groups for both daemons work without a password, e.g.\n\t// %admin ALL = (ALL:ALL) NOPASSWD: ALL\n\tfor _, daemon := range []string{SocketVMNet} {\n\t\tif ok, err := c.IsDaemonInstalled(daemon); err != nil {\n\t\t\treturn err\n\t\t} else if !ok {\n\t\t\tcontinue\n\t\t}\n\t\tuser, err := c.User(daemon)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd = exec.CommandContext(ctx, \"sudo\", \"--user\", user.User, \"--group\", user.Group, \"--non-interactive\", \"true\")\n\t\tif err := cmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to run %v: %w\", cmd.Args, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Config) VerifySudoAccess(ctx context.Context, sudoersFile string) error {\n\tif sudoersFile == \"\" {\n\t\terr := c.passwordLessSudo(ctx)\n\t\tif err == nil {\n\t\t\tlogrus.Debug(\"sudo doesn't seem to require a password\")\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"passwordLessSudo error: %w\", err)\n\t}\n\thint := fmt.Sprintf(\"run `%s sudoers >etc_sudoers.d_lima && sudo install -o root etc_sudoers.d_lima %q`)\",\n\t\tos.Args[0], sudoersFile)\n\tb, err := os.ReadFile(sudoersFile)\n\tif err != nil {\n\t\t// Default networks.yaml specifies /etc/sudoers.d/lima file. Don't throw an error when the\n\t\t// file doesn't exist, as long as password-less sudo still works.\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\terr = c.passwordLessSudo(ctx)\n\t\t\tif err == nil {\n\t\t\t\tlogrus.Debugf(\"%q does not exist, but sudo doesn't seem to require a password\", sudoersFile)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tlogrus.Debugf(\"%q does not exist; passwordLessSudo error: %s\", sudoersFile, err)\n\t\t}\n\t\treturn fmt.Errorf(\"can't read %q: %w: (Hint: %s)\", sudoersFile, err, hint)\n\t}\n\tsudoers, err := Sudoers()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif string(b) != sudoers {\n\t\t// Happens on upgrading socket_vmnet with Homebrew\n\t\treturn fmt.Errorf(\"sudoers file %q is out of sync and must be regenerated (Hint: %s)\", sudoersFile, hint)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/networks/usernet/client.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usernet\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\tgvproxyclient \"github.com/containers/gvisor-tap-vsock/pkg/client\"\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/httpclientutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks/usernet/dnshosts\"\n)\n\ntype Client struct {\n\tDirectory string\n\n\tclient   *http.Client\n\tdelegate *gvproxyclient.Client\n\tbase     string\n\tsubnet   net.IP\n}\n\nfunc (c *Client) ConfigureDriver(ctx context.Context, inst *limatype.Instance, sshLocalPort int) error {\n\tmacAddress := limayaml.MACAddress(inst.Dir)\n\tipAddress, err := c.ResolveIPAddress(ctx, macAddress)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif sshLocalPort != 0 {\n\t\terr = c.ResolveAndForwardSSH(ipAddress, sshLocalPort)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\thosts := inst.Config.HostResolver.Hosts\n\tif hosts == nil {\n\t\thosts = make(map[string]string)\n\t}\n\thosts[fmt.Sprintf(\"%s.internal\", inst.Hostname)] = ipAddress\n\terr = c.AddDNSHosts(hosts)\n\treturn err\n}\n\nfunc (c *Client) UnExposeSSH(sshPort int) error {\n\treturn c.delegate.Unexpose(&types.UnexposeRequest{\n\t\tLocal:    fmt.Sprintf(\"127.0.0.1:%d\", sshPort),\n\t\tProtocol: \"tcp\",\n\t})\n}\n\nfunc (c *Client) AddDNSHosts(hosts map[string]string) error {\n\thosts[\"host.lima.internal\"] = GatewayIP(c.subnet)\n\tzones := dnshosts.ExtractZones(hosts)\n\tfor _, zone := range zones {\n\t\terr := c.delegate.AddDNS(&zone)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Client) ResolveAndForwardSSH(ipAddr string, sshPort int) error {\n\terr := c.delegate.Expose(&types.ExposeRequest{\n\t\tLocal:    fmt.Sprintf(\"127.0.0.1:%d\", sshPort),\n\t\tRemote:   fmt.Sprintf(\"%s:22\", ipAddr),\n\t\tProtocol: \"tcp\",\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (c *Client) ResolveIPAddress(ctx context.Context, vmMacAddr string) (string, error) {\n\tresolveIPAddressTimeout := 2 * time.Minute\n\tresolveIPAddressTimeoutEnv := os.Getenv(\"LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT\")\n\tif resolveIPAddressTimeoutEnv != \"\" {\n\t\tif parsedTimeout, err := strconv.Atoi(resolveIPAddressTimeoutEnv); err == nil {\n\t\t\tresolveIPAddressTimeout = time.Duration(parsedTimeout) * time.Minute\n\t\t}\n\t}\n\tctx, cancel := context.WithTimeout(ctx, resolveIPAddressTimeout)\n\tdefer cancel()\n\tticker := time.NewTicker(500 * time.Millisecond)\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", errors.New(\"usernet unable to resolve IP for SSH forwarding\")\n\t\tcase <-ticker.C:\n\t\t\tleases, err := c.Leases(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\tfor ipAddr, leaseAddr := range leases {\n\t\t\t\tif vmMacAddr == leaseAddr {\n\t\t\t\t\treturn ipAddr, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *Client) Leases(ctx context.Context) (map[string]string, error) {\n\tu := fmt.Sprintf(\"%s%s\", c.base, \"/services/dhcp/leases\")\n\tres, err := httpclientutil.Get(ctx, c.client, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\tdec := json.NewDecoder(res.Body)\n\tvar leases map[string]string\n\tif err := dec.Decode(&leases); err != nil {\n\t\treturn nil, err\n\t}\n\treturn leases, nil\n}\n\n// WaitOpeningSSHPort Wait until the guest ssh server is available.\nfunc (c *Client) WaitOpeningSSHPort(ctx context.Context, inst *limatype.Instance) error {\n\t// This timeout is based on the maximum wait time for the first essential requirement.\n\ttimeoutSeconds := 600\n\tctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)\n\tdefer cancel()\n\tmacAddress := limayaml.MACAddress(inst.Dir)\n\tipAddr, err := c.ResolveIPAddress(ctx, macAddress)\n\tif err != nil {\n\t\treturn err\n\t}\n\t// -1 avoids both sides timing out simultaneously.\n\tu := fmt.Sprintf(\"%s/extension/wait_port?ip=%s&port=22&timeout=%d\", c.base, ipAddr, timeoutSeconds-1)\n\tres, err := httpclientutil.Get(ctx, c.client, u)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tif res.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"failed to wait for SSH port\")\n\t}\n\treturn nil\n}\n\nfunc NewClientByName(nwName string) *Client {\n\tendpointSock, err := Sock(nwName, EndpointSock)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tsubnet, err := Subnet(nwName)\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn NewClient(endpointSock, subnet)\n}\n\nfunc NewClient(endpointSock string, subnet net.IP) *Client {\n\treturn create(endpointSock, subnet, \"http://lima\")\n}\n\nfunc create(sock string, subnet net.IP, base string) *Client {\n\tclient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\t\tvar d net.Dialer\n\t\t\t\treturn d.DialContext(ctx, \"unix\", sock)\n\t\t\t},\n\t\t},\n\t}\n\tdelegate := gvproxyclient.New(client, \"http://lima\")\n\treturn &Client{\n\t\tclient:   client,\n\t\tdelegate: delegate,\n\t\tbase:     base,\n\t\tsubnet:   subnet,\n\t}\n}\n"
  },
  {
    "path": "pkg/networks/usernet/config.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usernet\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"path/filepath\"\n\n\t\"github.com/apparentlymart/go-cidr/cidr\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\ntype SockType = string\n\nconst (\n\tFDSock       = \"fd\"\n\tQEMUSock     = \"qemu\"\n\tEndpointSock = \"ep\"\n)\n\n// Sock returns a usernet socket based on name and sockType.\nfunc Sock(name string, sockType SockType) (string, error) {\n\tdir, err := dirnames.LimaNetworksDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn SockWithDirectory(filepath.Join(dir, name), name, sockType)\n}\n\n// SockWithDirectory return a usernet socket based on dir, name and sockType.\nfunc SockWithDirectory(dir, name string, sockType SockType) (string, error) {\n\tif name == \"\" {\n\t\tname = \"default\"\n\t}\n\tsockPath := filepath.Join(dir, fmt.Sprintf(\"%s_%s.sock\", name, sockType))\n\tif len(sockPath) >= osutil.UnixPathMax {\n\t\treturn \"\", fmt.Errorf(\"usernet socket path %q too long: must be less than UNIX_PATH_MAX=%d characters, but is %d\",\n\t\t\tsockPath, osutil.UnixPathMax, len(sockPath))\n\t}\n\treturn sockPath, nil\n}\n\n// PIDFile returns a path for usernet PID file.\nfunc PIDFile(name string) (string, error) {\n\tdir, err := dirnames.LimaNetworksDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn filepath.Join(dir, name, fmt.Sprintf(\"usernet_%s.pid\", name)), nil\n}\n\n// SubnetCIDR returns a subnet in form of net.IPNet for the given network name.\nfunc SubnetCIDR(name string) (*net.IPNet, error) {\n\tcfg, err := networks.LoadConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = cfg.Check(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, ipNet, err := netmaskToCidr(cfg.Networks[name].Gateway, cfg.Networks[name].NetMask)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ipNet, err\n}\n\n// Subnet returns a subnet net.IP for the given network name.\nfunc Subnet(name string) (net.IP, error) {\n\tcfg, err := networks.LoadConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\terr = cfg.Check(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, ipNet, err := netmaskToCidr(cfg.Networks[name].Gateway, cfg.Networks[name].NetMask)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn ipNet.IP, err\n}\n\n// GatewayIP returns the 2nd IP for the given subnet.\nfunc GatewayIP(subnet net.IP) string {\n\treturn cidr.Inc(cidr.Inc(subnet)).String()\n}\n\n// DNSIP returns the 3rd IP for the given subnet.\nfunc DNSIP(subnet net.IP) string {\n\treturn cidr.Inc(cidr.Inc(cidr.Inc(subnet))).String()\n}\n\n// Leases returns a leases file based on network name.\nfunc Leases(name string) (string, error) {\n\tdir, err := dirnames.LimaNetworksDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tsockPath := filepath.Join(filepath.Join(dir, name), \"leases.json\")\n\tif len(sockPath) >= osutil.UnixPathMax {\n\t\treturn \"\", fmt.Errorf(\"usernet leases path %q too long: must be less than UNIX_PATH_MAX=%d characters, but is %d\",\n\t\t\tsockPath, osutil.UnixPathMax, len(sockPath))\n\t}\n\treturn sockPath, nil\n}\n\nfunc netmaskToCidr(baseIP, netMask net.IP) (net.IP, *net.IPNet, error) {\n\tsize, _ := net.IPMask(netMask.To4()).Size()\n\treturn net.ParseCIDR(fmt.Sprintf(\"%s/%d\", baseIP.String(), size))\n}\n"
  },
  {
    "path": "pkg/networks/usernet/config_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usernet\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/networks\"\n)\n\nfunc TestUsernetConfig(t *testing.T) {\n\tt.Run(\"verify dns ip\", func(t *testing.T) {\n\t\tsubnet, _, err := net.ParseCIDR(networks.SlirpNetwork)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, DNSIP(subnet), \"192.168.5.3\")\n\t})\n\n\tt.Run(\"verify gateway ip\", func(t *testing.T) {\n\t\tsubnet, _, err := net.ParseCIDR(networks.SlirpNetwork)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, GatewayIP(subnet), \"192.168.5.2\")\n\t})\n\n\tt.Run(\"verify subnet via config ip\", func(t *testing.T) {\n\t\tsubnet, err := Subnet(\"user-v2\")\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, subnet.String(), \"192.168.104.0\")\n\t})\n}\n"
  },
  {
    "path": "pkg/networks/usernet/dnshosts/dnshosts.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://raw.githubusercontent.com/abiosoft/colima/v0.5.5/daemon/process/gvproxy/dnshosts_test.go\n/*\n\tMIT License\n\n\tCopyright (c) 2021 Abiola Ibrahim\n\n\tPermission is hereby granted, free of charge, to any person obtaining a copy\n\tof this software and associated documentation files (the \"Software\"), to deal\n\tin the Software without restriction, including without limitation the rights\n\tto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n\tcopies of the Software, and to permit persons to whom the Software is\n\tfurnished to do so, subject to the following conditions:\n\n\tThe above copyright notice and this permission notice shall be included in all\n\tcopies or substantial portions of the Software.\n\n\tTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n\tIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n\tFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n\tAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n\tLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n\tOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n\tSOFTWARE.\n*/\n\npackage dnshosts\n\nimport (\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n)\n\nfunc ExtractZones(hosts hostMap) []types.Zone {\n\tlist := make(map[string]types.Zone)\n\n\tfor host := range hosts {\n\t\th := zoneHost(host)\n\n\t\tzone := types.Zone{Name: h.name()}\n\t\tif existingZone, ok := list[h.name()]; ok {\n\t\t\tzone = existingZone\n\t\t}\n\n\t\tif h.recordName() == \"\" {\n\t\t\tif zone.DefaultIP == nil {\n\t\t\t\tzone.DefaultIP = hosts.hostIP(host)\n\t\t\t}\n\t\t} else {\n\t\t\tzone.Records = append(zone.Records, types.Record{\n\t\t\t\tName: h.recordName(),\n\t\t\t\tIP:   hosts.hostIP(host),\n\t\t\t})\n\t\t}\n\n\t\tlist[h.name()] = zone\n\t}\n\n\tzones := make([]types.Zone, 0, len(list))\n\tfor _, zone := range list {\n\t\tzones = append(zones, zone)\n\t}\n\treturn zones\n}\n\ntype hostMap map[string]string\n\nfunc (z hostMap) hostIP(host string) net.IP {\n\tfor {\n\t\t// check if host entry exists\n\t\th, ok := z[host]\n\t\tif !ok || h == \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\t// if it's a valid ip, return\n\t\tif ip := net.ParseIP(h); ip != nil {\n\t\t\treturn ip\n\t\t}\n\n\t\t// otherwise, a string i.e. another host\n\t\t// loop through the process again.\n\t\thost = h\n\t}\n}\n\ntype zoneHost string\n\nfunc (z zoneHost) name() string {\n\ti := z.dotIndex()\n\tif i < 0 {\n\t\treturn string(z)\n\t}\n\treturn string(z)[i+1:] + \".\"\n}\n\nfunc (z zoneHost) recordName() string {\n\ti := z.dotIndex()\n\tif i < 0 {\n\t\treturn \"\"\n\t}\n\treturn string(z)[:i]\n}\n\nfunc (z zoneHost) dotIndex() int {\n\treturn strings.LastIndex(string(z), \".\")\n}\n"
  },
  {
    "path": "pkg/networks/usernet/dnshosts/dnshosts_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// From https://raw.githubusercontent.com/abiosoft/colima/v0.5.5/daemon/process/gvproxy/dnshosts_test.go\n/*\n\tMIT License\n\n\tCopyright (c) 2021 Abiola Ibrahim\n\n\tPermission is hereby granted, free of charge, to any person obtaining a copy\n\tof this software and associated documentation files (the \"Software\"), to deal\n\tin the Software without restriction, including without limitation the rights\n\tto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n\tcopies of the Software, and to permit persons to whom the Software is\n\tfurnished to do so, subject to the following conditions:\n\n\tThe above copyright notice and this permission notice shall be included in all\n\tcopies or substantial portions of the Software.\n\n\tTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n\tIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n\tFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n\tAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n\tLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n\tOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n\tSOFTWARE.\n*/\n\npackage dnshosts\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n)\n\nfunc Test_hostsMapIP(t *testing.T) {\n\thosts := hostMap{}\n\thosts[\"sample\"] = \"1.1.1.1\"\n\thosts[\"another.sample\"] = \"1.2.2.1\"\n\thosts[\"google.com\"] = \"8.8.8.8\"\n\thosts[\"google.ae\"] = \"google.com\"\n\thosts[\"google.ie\"] = \"google.ae\"\n\n\ttests := []struct {\n\t\thost string\n\t\twant net.IP\n\t}{\n\t\t{host: \"sample\", want: net.ParseIP(\"1.1.1.1\")},\n\t\t{host: \"another.sample\", want: net.ParseIP(\"1.2.2.1\")},\n\t\t{host: \"google.com\", want: net.ParseIP(\"8.8.8.8\")},\n\t\t{host: \"google.ae\", want: net.ParseIP(\"8.8.8.8\")},\n\t\t{host: \"google.ie\", want: net.ParseIP(\"8.8.8.8\")},\n\t\t{host: \"google.sample\", want: nil},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tgot := hosts.hostIP(tt.host)\n\t\t\tif !got.Equal(tt.want) {\n\t\t\t\tt.Errorf(\"hostsMapIP() = %v, want %v\", got, tt.want)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_zoneHost(t *testing.T) {\n\ttype val struct {\n\t\tname       string\n\t\trecordName string\n\t}\n\ttests := []struct {\n\t\thost zoneHost\n\t\twant val\n\t}{\n\t\t{}, // test for empty value as well\n\t\t{host: \"sample\", want: val{name: \"sample\"}},\n\t\t{host: \"another.sample\", want: val{name: \"sample.\", recordName: \"another\"}},\n\t\t{host: \"another.sample.com\", want: val{name: \"com.\", recordName: \"another.sample\"}},\n\t\t{host: \"a.c\", want: val{name: \"c.\", recordName: \"a\"}},\n\t\t{host: \"a.b.c.d\", want: val{name: \"d.\", recordName: \"a.b.c\"}},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tgot := val{\n\t\t\t\tname:       tt.host.name(),\n\t\t\t\trecordName: tt.host.recordName(),\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"host = %+v, want %+v\", got, tt.want)\n\t\t\t\treturn\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExtractZones(t *testing.T) {\n\tequalZones := func(za, zb []types.Zone) bool {\n\t\tequal := func(a, b types.Zone) bool {\n\t\t\tif a.Name != b.Name {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif !a.DefaultIP.Equal(b.DefaultIP) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tfor i := range a.Records {\n\t\t\t\ta, b := a.Records[i], b.Records[i]\n\t\t\t\tif !a.IP.Equal(b.IP) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tif a.Name != b.Name {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t}\n\n\t\tfor _, a := range za {\n\t\t\tib := slices.IndexFunc(zb, func(z types.Zone) bool { return z.Name == a.Name })\n\t\t\tif ib < 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tb := zb[ib]\n\t\t\tif !equal(a, b) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\thosts := hostMap{\n\t\t\"google.com\":           \"8.8.4.4\",\n\t\t\"local.google.com\":     \"8.8.8.8\",\n\t\t\"google.ae\":            \"google.com\",\n\t\t\"localhost\":            \"127.0.0.1\",\n\t\t\"host.lima.internal\":   \"192.168.5.2\",\n\t\t\"host.docker.internal\": \"host.lima.internal\",\n\t}\n\n\ttests := []struct {\n\t\twantZones []types.Zone\n\t}{\n\t\t{\n\t\t\twantZones: []types.Zone{\n\t\t\t\t{\n\t\t\t\t\tName: \"ae.\",\n\t\t\t\t\tRecords: []types.Record{\n\t\t\t\t\t\t{Name: \"google\", IP: net.ParseIP(\"8.8.4.4\")},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"com.\",\n\t\t\t\t\tRecords: []types.Record{\n\t\t\t\t\t\t{Name: \"google\", IP: net.ParseIP(\"8.8.4.4\")},\n\t\t\t\t\t\t{Name: \"local.google\", IP: net.ParseIP(\"8.8.8.8\")},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"internal.\",\n\t\t\t\t\tRecords: []types.Record{\n\t\t\t\t\t\t{Name: \"host.docker\", IP: net.ParseIP(\"192.168.5.2\")},\n\t\t\t\t\t\t{Name: \"host.lima\", IP: net.ParseIP(\"192.168.5.2\")},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:      \"localhost\",\n\t\t\t\t\tDefaultIP: net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tgotZones := ExtractZones(hosts)\n\t\t\tfor _, zone := range gotZones {\n\t\t\t\tslices.SortFunc(zone.Records, func(a, b types.Record) int { return strings.Compare(a.Name, b.Name) })\n\t\t\t}\n\t\t\tslices.SortFunc(gotZones, func(a, b types.Zone) int { return strings.Compare(a.Name, b.Name) })\n\n\t\t\tif !equalZones(gotZones, tt.wantZones) {\n\t\t\t\tt.Errorf(\"extractZones() = %+v, want %+v\", gotZones, tt.wantZones)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/networks/usernet/gvproxy.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usernet\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/balajiv113/fd\"\n\t\"github.com/containers/gvisor-tap-vsock/pkg/transport\"\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n\t\"github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\ntype GVisorNetstackOpts struct {\n\tMTU int\n\n\tQemuSocket string\n\tFdSocket   string\n\tEndpoint   string\n\n\tSubnet string\n\n\tAsync bool\n\n\tDefaultLeases map[string]string\n}\n\nvar opts *GVisorNetstackOpts\n\nconst gatewayMacAddr = \"5a:94:ef:e4:0c:dd\"\n\nfunc StartGVisorNetstack(ctx context.Context, gVisorOpts *GVisorNetstackOpts) error {\n\topts = gVisorOpts\n\n\tip, ipNet, err := net.ParseCIDR(opts.Subnet)\n\tif err != nil {\n\t\treturn err\n\t}\n\tgatewayIP := GatewayIP(ip)\n\n\tleases := map[string]string{}\n\tif opts.DefaultLeases != nil {\n\t\tfor k, v := range opts.DefaultLeases {\n\t\t\tif ipNet.Contains(net.ParseIP(k)) {\n\t\t\t\tleases[k] = v\n\t\t\t}\n\t\t}\n\t}\n\tleases[gatewayIP] = gatewayMacAddr\n\n\t// The way gvisor-tap-vsock implemented slirp is different from tradition SLIRP,\n\t// - GatewayIP handling all request, also answers DNS queries\n\t// - based on NAT configuration, gateway forwards and translates calls to host\n\t// Comparing this with QEMU SLIRP,\n\t// - DNS is equivalent to GatewayIP\n\t// - GatewayIP is equivalent to NAT configuration\n\tconfig := types.Configuration{\n\t\tDebug:             false,\n\t\tMTU:               opts.MTU,\n\t\tSubnet:            opts.Subnet,\n\t\tGatewayIP:         gatewayIP,\n\t\tGatewayMacAddress: gatewayMacAddr,\n\t\tDHCPStaticLeases:  leases,\n\t\tForwards:          map[string]string{},\n\t\tDNS:               []types.Zone{},\n\t\tDNSSearchDomains:  searchDomains(),\n\t\tNAT: map[string]string{\n\t\t\tgatewayIP: \"127.0.0.1\",\n\t\t},\n\t\tGatewayVirtualIPs: []string{gatewayIP},\n\t}\n\n\tgroupErrs, ctx := errgroup.WithContext(ctx)\n\terr = run(ctx, groupErrs, &config)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif opts.Async {\n\t\treturn err\n\t}\n\treturn groupErrs.Wait()\n}\n\nfunc run(ctx context.Context, g *errgroup.Group, configuration *types.Configuration) error {\n\tvn, err := virtualnetwork.New(configuration)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tln, err := transport.Listen(fmt.Sprintf(\"unix://%s\", opts.Endpoint))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thttpServe(ctx, g, ln, muxWithExtension(vn))\n\n\tif opts.QemuSocket != \"\" {\n\t\terr = listenQEMU(ctx, vn)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif opts.FdSocket != \"\" {\n\t\terr = listenFD(ctx, vn)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc listenQEMU(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {\n\tvar lc net.ListenConfig\n\tlistener, err := lc.Listen(ctx, \"unix\", opts.QemuSocket)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tdefer listener.Close()\n\t\t<-ctx.Done()\n\t}()\n\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := listener.Accept()\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlogrus.Error(\"QEMU accept failed\", err)\n\t\t\t}\n\n\t\t\tgo func() {\n\t\t\t\terr = vn.AcceptQemu(ctx, conn)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Error(\"QEMU connection closed with error\", err)\n\t\t\t\t}\n\t\t\t\tconn.Close()\n\t\t\t}()\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc listenFD(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {\n\tvar lc net.ListenConfig\n\tlistener, err := lc.Listen(ctx, \"unix\", opts.FdSocket)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tdefer listener.Close()\n\t\t<-ctx.Done()\n\t}()\n\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := listener.Accept()\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlogrus.Error(\"FD accept failed\", err)\n\t\t\t\tcontinue // since conn is nil\n\t\t\t}\n\n\t\t\tfiles, err := fd.Get(conn.(*net.UnixConn), 1, []string{\"client\"})\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Error(\"Failed to get FD via socket\", err)\n\t\t\t}\n\n\t\t\tif len(files) != 1 {\n\t\t\t\tlogrus.Error(\"Invalid number of fd in response\", err)\n\t\t\t}\n\t\t\tfileConn, err := net.FileConn(files[0])\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Error(\"Error in FD Socket\", err)\n\t\t\t}\n\t\t\tfiles[0].Close()\n\n\t\t\tgo func() {\n\t\t\t\terr = vn.AcceptBess(ctx, &UDPFileConn{Conn: fileConn})\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Error(\"FD connection closed with error\", err)\n\t\t\t\t}\n\t\t\t\tfileConn.Close()\n\t\t\t}()\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http.Handler) {\n\ts := &http.Server{\n\t\tHandler:      mux,\n\t\tReadTimeout:  10 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t}\n\tg.Go(func() error {\n\t\t<-ctx.Done()\n\t\treturn s.Close()\n\t})\n\tg.Go(func() error {\n\t\terr := s.Serve(ln)\n\t\tif err != nil {\n\t\t\tif err == http.ErrServerClosed {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc muxWithExtension(n *virtualnetwork.VirtualNetwork) *http.ServeMux {\n\tm := n.Mux()\n\tm.HandleFunc(\"/extension/wait_port\", func(w http.ResponseWriter, r *http.Request) {\n\t\tip := r.URL.Query().Get(\"ip\")\n\t\tif net.ParseIP(ip) == nil {\n\t\t\tmsg := fmt.Sprintf(\"invalid ip address: %s\", ip)\n\t\t\thttp.Error(w, msg, http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tport16, err := strconv.ParseUint(r.URL.Query().Get(\"port\"), 10, 16)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tport := uint16(port16)\n\t\taddr := fmt.Sprintf(\"%s:%d\", ip, port)\n\n\t\ttimeoutSeconds := 10\n\t\tif timeoutString := r.URL.Query().Get(\"timeout\"); timeoutString != \"\" {\n\t\t\ttimeout16, err := strconv.ParseUint(timeoutString, 10, 16)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttimeoutSeconds = int(timeout16)\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second)\n\t\tdefer cancel()\n\t\t// Wait until the port is available.\n\t\tfor {\n\t\t\tconn, err := n.DialContextTCP(ctx, addr)\n\t\t\tif err == nil {\n\t\t\t\tconn.Close()\n\t\t\t\tlogrus.Debugf(\"Port is available on %s\", addr)\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tmsg := fmt.Sprintf(\"timed out waiting for port to become available on %s\", addr)\n\t\t\t\tlogrus.Warn(msg)\n\t\t\t\thttp.Error(w, msg, http.StatusRequestTimeout)\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\t\t\tlogrus.Debugf(\"Waiting for port to become available on %s\", addr)\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}\n\t})\n\treturn m\n}\n\nfunc searchDomains() []string {\n\tif runtime.GOOS != \"windows\" {\n\t\treturn resolveSearchDomain(\"/etc/resolv.conf\")\n\t}\n\treturn nil\n}\n\nfunc resolveSearchDomain(file string) []string {\n\tf, err := os.Open(file)\n\tif err != nil {\n\t\tlogrus.Errorf(\"open file error: %v\", err)\n\t\treturn nil\n\t}\n\tdefer f.Close()\n\tsc := bufio.NewScanner(f)\n\tsearchPrefix := \"search \"\n\tfor sc.Scan() {\n\t\tif after, ok := strings.CutPrefix(sc.Text(), searchPrefix); ok {\n\t\t\tsearchDomains := strings.Split(after, \" \")\n\t\t\tlogrus.Debugf(\"Using search domains: %v\", searchDomains)\n\t\t\treturn searchDomains\n\t\t}\n\t}\n\tif err := sc.Err(); err != nil {\n\t\tlogrus.Errorf(\"scan file error: %v\", err)\n\t\treturn nil\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/networks/usernet/gvproxy_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usernet\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestSearchDomain(t *testing.T) {\n\tt.Run(\"search domain\", func(t *testing.T) {\n\t\tresolvFile := path.Join(t.TempDir(), \"resolv.conf\")\n\t\tcreateResolveFile(t, resolvFile, `\nsearch test.com lima.net\nnameserver 192.168.0.100\nnameserver 8.8.8.8`)\n\n\t\tdns := resolveSearchDomain(resolvFile)\n\t\tassert.DeepEqual(t, dns, []string{\"test.com\", \"lima.net\"})\n\t})\n\n\tt.Run(\"empty search domain\", func(t *testing.T) {\n\t\tresolvFile := path.Join(t.TempDir(), \"resolv.conf\")\n\t\tcreateResolveFile(t, resolvFile, `\nnameserver 192.168.0.100\nnameserver 8.8.8.8`)\n\n\t\tdns := resolveSearchDomain(resolvFile)\n\t\tvar expected []string\n\t\tassert.DeepEqual(t, dns, expected)\n\t})\n}\n\nfunc createResolveFile(t *testing.T, file, content string) {\n\tf, err := os.Create(file)\n\tassert.NilError(t, err)\n\tt.Cleanup(func() { _ = f.Close() })\n\twriter := bufio.NewWriter(f)\n\t_, err = writer.WriteString(content)\n\tassert.NilError(t, err)\n\terr = writer.Flush()\n\tassert.NilError(t, err)\n}\n"
  },
  {
    "path": "pkg/networks/usernet/recoincile.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usernet\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/executil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/lockutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/store\"\n)\n\n// Start starts a instance a usernet network with the given name.\n// The name parameter must point to a valid network configuration name under <LIMA_HOME>/_config/networks.yaml with `mode: user-v2`.\nfunc Start(ctx context.Context, name string) error {\n\tlogrus.Debugf(\"Make sure usernet network is started\")\n\tnetworksDir, err := dirnames.LimaNetworksDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\t// usernet files contents are stored under {LIMA_HOME}/_networks/user-v2/<pid, fdsock, endpointsock, logs>\n\tusernetDir := path.Join(networksDir, name)\n\tif err := os.MkdirAll(usernetDir, 0o755); err != nil {\n\t\treturn err\n\t}\n\n\tpidFile, err := PIDFile(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpid, _ := store.ReadPIDFile(pidFile)\n\tif pid == 0 {\n\t\tqemuSock, err := Sock(name, QEMUSock)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfdSock, err := Sock(name, FDSock)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tendpointSock, err := Sock(name, EndpointSock)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsubnet, err := SubnetCIDR(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tleases, err := readLeases(name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\terr = lockutil.WithDirLock(usernetDir, func() error {\n\t\t\tself, err := os.Executable()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tleasesString := mapToCliString(leases)\n\t\t\targs := []string{\n\t\t\t\t\"usernet\", \"-p\", pidFile,\n\t\t\t\t\"-e\", endpointSock,\n\t\t\t\t\"--listen-qemu\", qemuSock,\n\t\t\t\t\"--listen\", fdSock,\n\t\t\t\t\"--subnet\", subnet.String(),\n\t\t\t}\n\t\t\tif leasesString != \"\" {\n\t\t\t\targs = append(args, \"--leases\", leasesString)\n\t\t\t}\n\t\t\tcmd := exec.CommandContext(ctx, self, args...)\n\t\t\tcmd.SysProcAttr = executil.BackgroundSysProcAttr\n\n\t\t\tstdoutPath := filepath.Join(usernetDir, fmt.Sprintf(\"%s.%s.%s.log\", \"usernet\", name, \"stdout\"))\n\t\t\tstderrPath := filepath.Join(usernetDir, fmt.Sprintf(\"%s.%s.%s.log\", \"usernet\", name, \"stderr\"))\n\t\t\tif err := os.RemoveAll(stdoutPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := os.RemoveAll(stderrPath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tcmd.Stdout, err = os.Create(stdoutPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcmd.Stderr, err = os.Create(stderrPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tlogrus.Debugf(\"Starting usernet network: %v\", cmd.Args)\n\t\t\tif err := cmd.Start(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to run %v: %w (Hint: check %s/usernet.*.log)\", cmd.Args, err, usernetDir)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor {\n\t\t\tif _, err := os.Stat(fdSock); !errors.Is(err, os.ErrNotExist) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t}\n\t}\n\treturn nil\n}\n\n// Stop stops running instance a usernet network with the given name.\n// The name parameter must point to a valid network configuration name under <LIMA_HOME>/_config/networks.yaml with `mode: user-v2`.\nfunc Stop(ctx context.Context, name string) error {\n\tlogrus.Debugf(\"Make sure usernet network is stopped\")\n\tpidFile, err := PIDFile(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tpid, _ := store.ReadPIDFile(pidFile)\n\n\tif pid != 0 {\n\t\tlogrus.Debugf(\"Stopping usernet daemon\")\n\n\t\terr = writeLeases(ctx, name)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := osutil.SysKill(pid, osutil.SigInt); err != nil {\n\t\t\tlogrus.Error(err)\n\t\t\treturn fmt.Errorf(\"failed to kill process with pid %d: %w\", pid, err)\n\t\t}\n\t}\n\n\t// wait for daemons to terminate (up to 5s) before stopping, otherwise the sockets may not get deleted which\n\t// will cause subsequent start commands to fail.\n\tstartWaiting := time.Now()\n\tfor {\n\t\tif pid, _ := store.ReadPIDFile(pidFile); pid == 0 {\n\t\t\tbreak\n\t\t}\n\t\tif time.Since(startWaiting) > 5*time.Second {\n\t\t\tlogrus.Infof(\"usernet network still running after 5 seconds. Attempting to forcibly kill\")\n\t\t\tif err := osutil.SysKill(pid, osutil.SigKill); err != nil {\n\t\t\t\tlogrus.Error(err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}\n\treturn nil\n}\n\nfunc mapToCliString(m map[string]string) string {\n\tvar strArr []string\n\tfor key, value := range m {\n\t\tstrArr = append(strArr, fmt.Sprintf(\"%s=%s\", key, value))\n\t}\n\treturn strings.Join(strArr, \",\")\n}\n\nfunc readLeases(name string) (map[string]string, error) {\n\tleasesFile, err := Leases(name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar leases map[string]string\n\tif _, err := os.Stat(leasesFile); errors.Is(err, os.ErrNotExist) {\n\t\treturn leases, nil\n\t}\n\tfile, err := os.Open(leasesFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdecoder := json.NewDecoder(file)\n\terr = decoder.Decode(&leases)\n\treturn leases, err\n}\n\nfunc writeLeases(ctx context.Context, nwName string) error {\n\tclient := NewClientByName(nwName)\n\tleases, err := client.Leases(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\tleasesFile, err := Leases(nwName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfile, err := os.Create(leasesFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tencoder := json.NewEncoder(file)\n\terr = encoder.Encode(leases)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/networks/usernet/udpfileconn.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usernet\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"time\"\n)\n\ntype UDPFileConn struct {\n\tnet.Conn\n}\n\nfunc (conn *UDPFileConn) Read(b []byte) (n int, err error) {\n\t// Check if the connection has been closed\n\tif err := conn.SetReadDeadline(time.Time{}); err != nil {\n\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\treturn 0, errors.New(\"UDPFileConn connection closed\")\n\t\t}\n\t}\n\treturn conn.Conn.Read(b)\n}\n"
  },
  {
    "path": "pkg/networks/validate.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage networks\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\nfunc (c *Config) Validate() error {\n\t// validate all paths.* values\n\tpaths := reflect.ValueOf(&c.Paths).Elem()\n\tpathsMap := make(map[string]string, paths.NumField())\n\tvar socketVMNetNotFound bool\n\tfor i := range paths.NumField() {\n\t\t// extract YAML name from struct tag; strip options like \"omitempty\"\n\t\tname := paths.Type().Field(i).Tag.Get(\"yaml\")\n\t\tif i := strings.IndexRune(name, ','); i > -1 {\n\t\t\tname = name[:i]\n\t\t}\n\t\tpath := paths.Field(i).Interface().(string)\n\t\tpathsMap[name] = path\n\t\t// varPath will be created securely, but any existing parent directories must already be secure\n\t\tif name == \"varRun\" {\n\t\t\tpath = findBaseDirectory(path)\n\t\t}\n\t\terr := validatePath(path, name == \"varRun\")\n\t\tif err != nil {\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\tswitch name {\n\t\t\t\t// sudoers file does not need to exist; otherwise `limactl sudoers` couldn't bootstrap\n\t\t\t\tcase \"sudoers\":\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"socketVMNet\":\n\t\t\t\t\tsocketVMNetNotFound = true\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"networks.yaml field `paths.%s` error: %w\", name, err)\n\t\t}\n\t}\n\tif socketVMNetNotFound {\n\t\treturn fmt.Errorf(\"networks.yaml: %q (`paths.socketVMNet`) has to be installed\", pathsMap[\"socketVMNet\"])\n\t}\n\t// TODO(jandubois): validate network definitions\n\treturn nil\n}\n\n// findBaseDirectory removes non-existing directories from the end of the path.\nfunc findBaseDirectory(path string) string {\n\tif _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {\n\t\tif path != \"/\" {\n\t\t\treturn findBaseDirectory(filepath.Dir(path))\n\t\t}\n\t}\n\treturn path\n}\n\nfunc validatePath(path string, allowDaemonGroupWritable bool) error {\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\tif path[0] != '/' {\n\t\treturn fmt.Errorf(\"path %q is not an absolute path\", path)\n\t}\n\tif strings.ContainsRune(path, ' ') {\n\t\treturn fmt.Errorf(\"path %q contains whitespace\", path)\n\t}\n\tfi, err := os.Lstat(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfile := \"file\"\n\tif fi.Mode().IsDir() {\n\t\tfile = \"dir\"\n\t}\n\t// TODO: should we allow symlinks when both the link and the target are secure?\n\t// E.g. on macOS /var is a symlink to /private/var, /etc to /private/etc\n\tif (fi.Mode() & fs.ModeSymlink) != 0 {\n\t\treturn fmt.Errorf(\"%s %q is a symlink\", file, path)\n\t}\n\tstat, ok := osutil.SysStat(fi)\n\tif !ok {\n\t\t// should never happen\n\t\treturn fmt.Errorf(\"could not retrieve stat buffer for %q\", path)\n\t}\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn errors.New(\"vmnet code must not be called on non-Darwin\") // TODO: move to *_darwin.go\n\t}\n\t// TODO: cache looked up UIDs/GIDs\n\troot, err := osutil.LookupUser(\"root\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tif stat.Uid != root.Uid {\n\t\treturn fmt.Errorf(`%s %q is not owned by %q (uid: %d), but by uid %d`, file, path, root.User, root.Uid, stat.Uid)\n\t}\n\tif allowDaemonGroupWritable {\n\t\tdaemon, err := osutil.LookupUser(\"daemon\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fi.Mode()&0o20 != 0 && stat.Gid != root.Gid && stat.Gid != daemon.Gid {\n\t\t\treturn fmt.Errorf(`%s %q is group-writable and group is neither %q (gid: %d) nor %q (gid: %d), but is gid: %d`,\n\t\t\t\tfile, path, root.User, root.Gid, daemon.User, daemon.Gid, stat.Gid)\n\t\t}\n\t\tif fi.Mode().IsDir() && fi.Mode()&1 == 0 && (fi.Mode()&0o010 == 0 || stat.Gid != daemon.Gid) {\n\t\t\treturn fmt.Errorf(`%s %q is not executable by the %q (gid: %d)\" group`, file, path, daemon.User, daemon.Gid)\n\t\t}\n\t} else if fi.Mode()&0o20 != 0 && stat.Gid != root.Gid {\n\t\treturn fmt.Errorf(`%s %q is group-writable and group is not %q (gid: %d), but is gid: %d`,\n\t\t\tfile, path, root.User, root.Gid, stat.Gid)\n\t}\n\tif fi.Mode()&0o02 != 0 {\n\t\treturn fmt.Errorf(\"%s %q is world-writable\", file, path)\n\t}\n\tif path != \"/\" {\n\t\treturn validatePath(filepath.Dir(path), allowDaemonGroupWritable)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/osutil/dns_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/sysprof\"\n)\n\nfunc DNSAddresses() ([]string, error) {\n\tnwData, err := sysprof.NetworkData()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar addresses []string\n\t// Return DNS addresses from the first interface that has an IPv4 address.\n\t// The networks are in service order already.\n\tfor _, nw := range nwData {\n\t\tif len(nw.IPv4.Addresses) > 0 {\n\t\t\taddresses = nw.DNS.ServerAddresses\n\t\t\tbreak\n\t\t}\n\t}\n\treturn addresses, nil\n}\n\nfunc proxyURL(proxy string, port any) string {\n\tif strings.Contains(proxy, \"://\") {\n\t\tif portNumber, ok := port.(float64); ok && portNumber != 0 {\n\t\t\tproxy = fmt.Sprintf(\"%s:%.0f\", proxy, portNumber)\n\t\t} else if portString, ok := port.(string); ok && portString != \"\" {\n\t\t\tproxy = fmt.Sprintf(\"%s:%s\", proxy, portString)\n\t\t}\n\t} else {\n\t\tif portNumber, ok := port.(float64); ok && portNumber != 0 {\n\t\t\tproxy = net.JoinHostPort(proxy, fmt.Sprintf(\"%.0f\", portNumber))\n\t\t} else if portString, ok := port.(string); ok && portString != \"\" {\n\t\t\tproxy = net.JoinHostPort(proxy, portString)\n\t\t}\n\t\tproxy = \"http://\" + proxy\n\t}\n\treturn proxy\n}\n\nfunc ProxySettings() (map[string]string, error) {\n\tnwData, err := sysprof.NetworkData()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tenv := make(map[string]string)\n\tif len(nwData) > 0 {\n\t\t// Return proxy settings from the first interface that has an IPv4 address.\n\t\t// The networks are in service order already.\n\t\tvar proxies sysprof.Proxies\n\t\tfor _, nw := range nwData {\n\t\t\tif len(nw.IPv4.Addresses) > 0 {\n\t\t\t\tproxies = nw.Proxies\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\t// Proxies with a username are not going to work because the password is stored in a keychain.\n\t\t// If users are fine with exposing the username/password, they can set the proxy to\n\t\t// \"http://username:password@proxyhost.com\" in the system settings (or in lima.yaml).\n\t\tif proxies.FTPEnable == \"yes\" && proxies.FTPUser == \"\" {\n\t\t\tenv[\"ftp_proxy\"] = proxyURL(proxies.FTPProxy, proxies.FTPPort)\n\t\t}\n\t\tif proxies.HTTPEnable == \"yes\" && proxies.HTTPUser == \"\" {\n\t\t\tenv[\"http_proxy\"] = proxyURL(proxies.HTTPProxy, proxies.HTTPPort)\n\t\t}\n\t\tif proxies.HTTPSEnable == \"yes\" && proxies.HTTPSUser == \"\" {\n\t\t\tenv[\"https_proxy\"] = proxyURL(proxies.HTTPSProxy, proxies.HTTPSPort)\n\t\t}\n\t\t// Not setting up \"no_proxy\" variable; the values from the proxies.ExceptionList are\n\t\t// not understood by most applications checking \"no_proxy\". The default value would\n\t\t// be \"*.local,169.254/16\". Users can always specify env.no_proxy in lima.yaml.\n\t}\n\treturn env, nil\n}\n"
  },
  {
    "path": "pkg/osutil/dns_others.go",
    "content": "//go:build !darwin\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nfunc DNSAddresses() ([]string, error) {\n\t// TODO: parse /etc/resolv.conf?\n\treturn []string{}, nil\n}\n\nfunc ProxySettings() (map[string]string, error) {\n\treturn make(map[string]string), nil\n}\n"
  },
  {
    "path": "pkg/osutil/exit.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n)\n\n// HandleExitError calls os.Exit immediately without printing an error, only if the error is an *exec.ExitError (non-nil).\n//\n// The function does not call os.Exit if the error is of any other type, even if it wraps an *exec.ExitError,\n// so that the caller can print the error message.\nfunc HandleExitError(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\t// Do not use errors.As, because we want to match only *exec.ExitError, not wrapped ones.\n\t// https://github.com/lima-vm/lima/pull/4168\n\tif exitErr, ok := err.(*exec.ExitError); ok {\n\t\tos.Exit(exitErr.ExitCode()) //nolint:revive // it's intentional to call os.Exit in this function\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "pkg/osutil/file.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"errors\"\n\t\"os\"\n)\n\n// FileExists reports whether path exists and is accessible.\n// It returns true for any non-ErrNotExist stat result, including permission errors.\nfunc FileExists(path string) bool {\n\t_, err := os.Stat(path)\n\treturn !errors.Is(err, os.ErrNotExist)\n}\n\n// Touch touches a file.\nfunc Touch(path string) error {\n\tf, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o644)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn f.Close()\n}\n"
  },
  {
    "path": "pkg/osutil/machineid.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/plist\"\n)\n\nvar MachineID = sync.OnceValue(func() string {\n\tx, err := machineID(context.Background())\n\tif err == nil && x != \"\" {\n\t\treturn x\n\t}\n\tlogrus.WithError(err).Debug(\"failed to get machine ID, falling back to use hostname instead\")\n\thostname, err := os.Hostname()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to get hostname: %w\", err))\n\t}\n\treturn hostname\n})\n\nfunc machineID(ctx context.Context) (string, error) {\n\tif runtime.GOOS == \"darwin\" {\n\t\tioPlatformExpertDeviceCmd := exec.CommandContext(ctx, \"/usr/sbin/ioreg\", \"-a\", \"-d2\", \"-c\", \"IOPlatformExpertDevice\")\n\t\tioPlatformExpertDevice, err := ioPlatformExpertDeviceCmd.CombinedOutput()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn parseIOPlatformUUIDFromIOPlatformExpertDevice(bytes.NewReader(ioPlatformExpertDevice))\n\t}\n\n\tcandidates := []string{\n\t\t\"/etc/machine-id\",\n\t\t\"/var/lib/dbus/machine-id\",\n\t\t// We don't use \"/sys/class/dmi/id/product_uuid\"\n\t}\n\tfor _, f := range candidates {\n\t\tb, err := os.ReadFile(f)\n\t\tif err == nil {\n\t\t\treturn strings.TrimSpace(string(b)), nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"no machine-id found, tried %v\", candidates)\n}\n\nfunc parseIOPlatformUUIDFromIOPlatformExpertDevice(r io.Reader) (string, error) {\n\tvar p plist.Plist\n\tdec := xml.NewDecoder(r)\n\tif err := dec.Decode(&p); err != nil {\n\t\treturn \"\", err\n\t}\n\tif p.Value.Dict == nil {\n\t\treturn \"\", errors.New(\"invalid plist: top-level value is not a dict\")\n\t}\n\tioRegistryEntryChildren, ok := p.Value.Dict[\"IORegistryEntryChildren\"]\n\tif !ok || ioRegistryEntryChildren.Array == nil || len(ioRegistryEntryChildren.Array) == 0 {\n\t\treturn \"\", errors.New(\"invalid plist: IORegistryEntryChildren not found or empty\")\n\t}\n\tfor _, child := range ioRegistryEntryChildren.Array {\n\t\tif child.Dict == nil {\n\t\t\tcontinue\n\t\t}\n\t\tioPlatformUUID, ok := child.Dict[\"IOPlatformUUID\"]\n\t\tif !ok || ioPlatformUUID.String == nil {\n\t\t\tcontinue\n\t\t}\n\t\treturn *ioPlatformUUID.String, nil\n\t}\n\n\treturn \"\", errors.New(\"invalid plist: IOPlatformUUID not found in any child of IORegistryEntryChildren\")\n}\n"
  },
  {
    "path": "pkg/osutil/machineid_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestMachineID(t *testing.T) {\n\tt.Log(MachineID())\n}\n\nfunc TestParseIOPlatformUUIDFromIOPlatformExpertDevice(t *testing.T) {\n\tioPlatformExpertDevice := `\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n        <key>IOObjectClass</key>\n        <string>IORegistryEntry</string>\n        <key>IORegistryEntryChildren</key>\n        <array>\n                <dict>\n                        <key>foo</key>\n                        <string>foo value</string>\n                        <key>IOPlatformUUID</key>\n                        <string>1A008DA1-06E0-49AB-8EC9-88E9C85F67FB</string>\n                        <key>bar</key>\n                        <string>bar value</string>\n                </dict>\n        </array>\n        <key>IORegistryEntryName</key>\n        <string>Root</string>\n</dict>\n</plist>\n`\n\tgot, err := parseIOPlatformUUIDFromIOPlatformExpertDevice(strings.NewReader(ioPlatformExpertDevice))\n\tassert.NilError(t, err)\n\tassert.Equal(t, \"1A008DA1-06E0-49AB-8EC9-88E9C85F67FB\", got)\n}\n"
  },
  {
    "path": "pkg/osutil/mount_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Mount mounts the device.\n// Root privileges is not necessary.\nfunc Mount(ctx context.Context, fs, dev, mnt string, options []string) error {\n\targs := []string{\"-t\", fs}\n\tif len(options) > 0 {\n\t\targs = append(args, \"-o\", strings.Join(options, \",\"))\n\t}\n\targs = append(args, dev, mnt)\n\tcmd := exec.CommandContext(ctx, \"mount\", args...)\n\tlogrus.Debugf(\"Executing command: %v\", cmd.Args)\n\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to mount %q on %q: %w (output=%q)\", dev, mnt, err, output)\n\t}\n\treturn nil\n}\n\nfunc Umount(ctx context.Context, mnt string) error {\n\tcmd := exec.CommandContext(ctx, \"umount\", mnt)\n\tlogrus.Debugf(\"Executing command: %v\", cmd.Args)\n\tif output, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmount %q: %w (output=%q)\", mnt, err, output)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/osutil/osutil_linux.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"io/fs\"\n\t\"syscall\"\n)\n\n// UnixPathMax is the value of UNIX_PATH_MAX.\nconst UnixPathMax = 108\n\n// Stat is a selection of syscall.Stat_t.\ntype Stat struct {\n\tUid uint32\n\tGid uint32\n}\n\nfunc SysStat(fi fs.FileInfo) (Stat, bool) {\n\tstat, ok := fi.Sys().(*syscall.Stat_t)\n\treturn Stat{Uid: stat.Uid, Gid: stat.Gid}, ok\n}\n\n// SigInt is the value of SIGINT.\nconst SigInt = Signal(syscall.SIGINT)\n\n// SigKill is the value of SIGKILL.\nconst SigKill = Signal(syscall.SIGKILL)\n\ntype Signal syscall.Signal\n\nfunc SysKill(pid int, sig Signal) error {\n\treturn syscall.Kill(pid, syscall.Signal(sig))\n}\n"
  },
  {
    "path": "pkg/osutil/osutil_others.go",
    "content": "//go:build !linux && !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"io/fs\"\n\t\"syscall\"\n)\n\n// UnixPathMax is the value of UNIX_PATH_MAX.\nconst UnixPathMax = 104\n\n// Stat is a selection of syscall.Stat_t.\ntype Stat struct {\n\tUid uint32\n\tGid uint32\n}\n\nfunc SysStat(fi fs.FileInfo) (Stat, bool) {\n\tstat, ok := fi.Sys().(*syscall.Stat_t)\n\treturn Stat{Uid: stat.Uid, Gid: stat.Gid}, ok\n}\n\n// SigInt is the value of SIGINT.\nconst SigInt = Signal(syscall.SIGINT)\n\n// SigKill is the value of SIGKILL.\nconst SigKill = Signal(syscall.SIGKILL)\n\ntype Signal syscall.Signal\n\nfunc SysKill(pid int, sig Signal) error {\n\treturn syscall.Kill(pid, syscall.Signal(sig))\n}\n"
  },
  {
    "path": "pkg/osutil/osutil_unix.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc Dup2(oldfd, newfd int) (err error) {\n\treturn unix.Dup2(oldfd, newfd)\n}\n\nfunc SignalName(sig os.Signal) string {\n\treturn unix.SignalName(sig.(syscall.Signal))\n}\n\nfunc Sysctl(ctx context.Context, name string) (string, error) {\n\tvar stderrBuf bytes.Buffer\n\tcmd := exec.CommandContext(ctx, \"sysctl\", \"-n\", name)\n\tcmd.Stderr = &stderrBuf\n\tstdout, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to run %v: %w (stdout=%q, stderr=%q)\", cmd.Args, err,\n\t\t\tstring(stdout), stderrBuf.String())\n\t}\n\treturn strings.TrimSuffix(string(stdout), \"\\n\"), nil\n}\n\nfunc IsEACCES(err error) bool {\n\treturn errors.Is(err, unix.EACCES)\n}\n"
  },
  {
    "path": "pkg/osutil/osutil_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"syscall\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\n// UnixPathMax is the value of UNIX_PATH_MAX.\nconst UnixPathMax = 108\n\n// Stat is a selection of syscall.Stat_t.\ntype Stat struct {\n\tUid uint32\n\tGid uint32\n}\n\nfunc SysStat(_ fs.FileInfo) (Stat, bool) {\n\treturn Stat{Uid: 0, Gid: 0}, false\n}\n\n// SigInt is the value of SIGINT.\nconst SigInt = Signal(2)\n\n// SigKill is the value of SIGKILL.\nconst SigKill = Signal(9)\n\ntype Signal int\n\nfunc SysKill(pid int, _ Signal) error {\n\treturn windows.GenerateConsoleCtrlEvent(syscall.CTRL_BREAK_EVENT, uint32(pid))\n}\n\nfunc Dup2(_ int, _ syscall.Handle) error {\n\treturn errors.New(\"unimplemented\")\n}\n\nfunc SignalName(sig os.Signal) string {\n\tswitch sig {\n\tcase syscall.SIGINT:\n\t\treturn \"SIGINT\"\n\tcase syscall.SIGTERM:\n\t\treturn \"SIGTERM\"\n\tdefault:\n\t\treturn fmt.Sprintf(\"Signal(%d)\", sig)\n\t}\n}\n\nfunc Sysctl(_ context.Context, _ string) (string, error) {\n\treturn \"\", errors.New(\"sysctl: unimplemented on Windows\")\n}\n\nfunc IsEACCES(err error) bool {\n\treturn errors.Is(err, syscall.ERROR_ACCESS_DENIED) || errors.Is(err, syscall.WSAEACCES)\n}\n"
  },
  {
    "path": "pkg/osutil/osversion_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/coreos/go-semver/semver\"\n)\n\n// ProductVersion returns the macOS product version like \"12.3.1\".\nvar ProductVersion = sync.OnceValues(func() (*semver.Version, error) {\n\tcmd := exec.Command(\"sw_vers\", \"-productVersion\")\n\t// output is like \"12.3.1\\n\"\n\tb, err := cmd.Output()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute %v: %w\", cmd.Args, err)\n\t}\n\tverTrimmed := strings.TrimSpace(string(b))\n\t// macOS 12.4 returns just \"12.4\\n\"\n\tfor strings.Count(verTrimmed, \".\") < 2 {\n\t\tverTrimmed += \".0\"\n\t}\n\tverSem, err := semver.NewVersion(verTrimmed)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse macOS version %q: %w\", verTrimmed, err)\n\t}\n\treturn verSem, nil\n})\n"
  },
  {
    "path": "pkg/osutil/osversion_others.go",
    "content": "//go:build !darwin\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"errors\"\n\n\t\"github.com/coreos/go-semver/semver\"\n)\n\n// ProductVersion returns the OS product version, not the kernel version.\nfunc ProductVersion() (*semver.Version, error) {\n\treturn nil, errors.New(\"not implemented\")\n}\n"
  },
  {
    "path": "pkg/osutil/rosetta_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc IsBeingRosettaTranslated() bool {\n\tret, err := unix.SysctlUint32(\"sysctl.proc_translated\")\n\tif err != nil {\n\t\tconst fallback = false\n\t\tif errors.Is(err, unix.ENOENT) {\n\t\t\treturn false\n\t\t}\n\n\t\terr = fmt.Errorf(`failed to read sysctl \"sysctl.proc_translated\": %w`, err)\n\t\tlogrus.WithError(err).Warnf(\"failed to detect whether running under rosetta, assuming %v\", fallback)\n\t\treturn fallback\n\t}\n\n\treturn ret != 0\n}\n"
  },
  {
    "path": "pkg/osutil/rosetta_others.go",
    "content": "//go:build !darwin\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nfunc IsBeingRosettaTranslated() bool {\n\treturn false\n}\n"
  },
  {
    "path": "pkg/osutil/user.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"os/user\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t. \"github.com/lima-vm/lima/v2/pkg/must\"\n\t\"github.com/lima-vm/lima/v2/pkg/version/versionutil\"\n)\n\ntype User struct {\n\tUser  string\n\tUid   uint32\n\tGroup string\n\tGid   uint32\n\tName  string // or Comment\n\tHome  string\n}\n\ntype Group struct {\n\tName string\n\tGid  uint32\n}\n\nvar (\n\tusers  map[string]User\n\tgroups map[string]Group\n)\n\n// regexUsername matches user and group names to be valid for `useradd`.\n// `useradd` allows names with a trailing '$', but it feels prudent to map those\n// names to the fallback user as well, so the regex does not allow them.\nvar regexUsername = regexp.MustCompile(\"^[a-z_][a-z0-9_-]*$\")\n\nfunc LookupUser(name string) (User, error) {\n\tif users == nil {\n\t\tusers = make(map[string]User)\n\t}\n\tif _, ok := users[name]; !ok {\n\t\tu, err := user.Lookup(name)\n\t\tif err != nil {\n\t\t\treturn User{}, err\n\t\t}\n\t\tg, err := user.LookupGroupId(u.Gid)\n\t\tif err != nil {\n\t\t\treturn User{}, err\n\t\t}\n\t\tuid, err := parseUidGid(u.Uid)\n\t\tif err != nil {\n\t\t\treturn User{}, err\n\t\t}\n\t\tgid, err := parseUidGid(u.Gid)\n\t\tif err != nil {\n\t\t\treturn User{}, err\n\t\t}\n\t\tusers[name] = User{User: u.Username, Uid: uid, Group: g.Name, Gid: gid, Name: u.Name, Home: u.HomeDir}\n\t}\n\treturn users[name], nil\n}\n\nfunc LookupGroup(name string) (Group, error) {\n\tif groups == nil {\n\t\tgroups = make(map[string]Group)\n\t}\n\tif _, ok := groups[name]; !ok {\n\t\tg, err := user.LookupGroup(name)\n\t\tif err != nil {\n\t\t\treturn Group{}, err\n\t\t}\n\t\tgid, err := parseUidGid(g.Gid)\n\t\tif err != nil {\n\t\t\treturn Group{}, err\n\t\t}\n\t\tgroups[name] = Group{Name: g.Name, Gid: gid}\n\t}\n\treturn groups[name], nil\n}\n\nconst (\n\tfallbackUser = \"lima\"\n\tfallbackUid  = 1000\n\tfallbackGid  = 1000\n)\n\nvar currentUser = Must(user.Current())\n\nvar (\n\tonce     = new(sync.Once)\n\tlimaUser *user.User\n\twarnings []string\n)\n\nfunc LimaUser(ctx context.Context, limaVersion string, warn bool, guestOS *limatype.OS) *user.User {\n\tonce.Do(func() {\n\t\tlimaUser = currentUser\n\t\tif !regexUsername.MatchString(limaUser.Username) {\n\t\t\twarning := fmt.Sprintf(\"local username %q is not a valid Linux username (must match %q); using %q instead\",\n\t\t\t\tlimaUser.Username, regexUsername.String(), fallbackUser)\n\t\t\twarnings = append(warnings, warning)\n\t\t\tlimaUser.Username = fallbackUser\n\t\t}\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tidu, err := call(ctx, []string{\"id\", \"-u\"})\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Debug(err)\n\t\t\t}\n\t\t\tuid, err := parseUidGid(idu)\n\t\t\tif err != nil {\n\t\t\t\tuid = fallbackUid\n\t\t\t}\n\t\t\tif _, err := parseUidGid(limaUser.Uid); err != nil {\n\t\t\t\twarning := fmt.Sprintf(\"local uid %q is not a valid Linux uid (must be integer); using %d uid instead\",\n\t\t\t\t\tlimaUser.Uid, uid)\n\t\t\t\twarnings = append(warnings, warning)\n\t\t\t\tlimaUser.Uid = formatUidGid(uid)\n\t\t\t}\n\t\t\tidg, err := call(ctx, []string{\"id\", \"-g\"})\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Debug(err)\n\t\t\t}\n\t\t\tgid, err := parseUidGid(idg)\n\t\t\tif err != nil {\n\t\t\t\tgid = fallbackGid\n\t\t\t}\n\t\t\tif _, err := parseUidGid(limaUser.Gid); err != nil {\n\t\t\t\twarning := fmt.Sprintf(\"local gid %q is not a valid Linux gid (must be integer); using %d gid instead\",\n\t\t\t\t\tlimaUser.Gid, gid)\n\t\t\t\twarnings = append(warnings, warning)\n\t\t\t\tlimaUser.Gid = formatUidGid(gid)\n\t\t\t}\n\t\t}\n\t})\n\tif warn {\n\t\tfor _, warning := range warnings {\n\t\t\tlogrus.Warn(warning)\n\t\t}\n\t}\n\t// Make sure we return a pointer to a COPY of limaUser\n\tu := *limaUser\n\tlimaVersionUnknown := limaVersion == \"\" || limaVersion == \"<unknown>\"\n\tif guestOS != nil && *guestOS == limatype.DARWIN {\n\t\tu.HomeDir = \"/Users/{{.User}}.guest\"\n\t} else if limaVersionUnknown || versionutil.GreaterEqual(limaVersion, \"2.1.0\") {\n\t\tu.HomeDir = \"/home/{{.User}}.guest\"\n\t\t// boot script symlinks \"/home/{{.User}}.linux\" to \"{{.User}}.guest\" for compatibility\n\t} else {\n\t\tu.HomeDir = \"/home/{{.User}}.linux\"\n\t\t// boot script symlinks \"/home/{{.User}}.guest\" to \"{{.User}}.linux\" for compatibility\n\t}\n\tif versionutil.GreaterEqual(limaVersion, \"1.0.0\") {\n\t\tif u.Username == \"admin\" {\n\t\t\tif warn {\n\t\t\t\tlogrus.Warnf(\"local username %q is reserved; using %q instead\", u.Username, fallbackUser)\n\t\t\t}\n\t\t\tu.Username = fallbackUser\n\t\t}\n\t}\n\treturn &u\n}\n\nfunc call(ctx context.Context, args []string) (string, error) {\n\tcmd := exec.CommandContext(ctx, args[0], args[1:]...)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimSpace(string(out)), nil\n}\n\n// parseUidGid converts string value to Linux uid or gid.\nfunc parseUidGid(uidOrGid string) (uint32, error) {\n\tres, err := strconv.ParseUint(uidOrGid, 10, 32)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint32(res), nil\n}\n\n// formatUidGid converts uid or gid to string value.\nfunc formatUidGid(uidOrGid uint32) string {\n\treturn strconv.FormatUint(uint64(uidOrGid), 10)\n}\n"
  },
  {
    "path": "pkg/osutil/user_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage osutil\n\nimport (\n\t\"path\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nconst limaVersion = \"1.0.0\"\n\n// \"admin\" is a reserved username in 1.0.0\nfunc TestLimaUserAdminNew(t *testing.T) {\n\tcurrentUser.Username = \"admin\"\n\tonce = new(sync.Once)\n\tuser := LimaUser(t.Context(), limaVersion, false, nil)\n\tassert.Equal(t, user.Username, fallbackUser)\n}\n\n// \"admin\" is allowed in older instances\nfunc TestLimaUserAdminOld(t *testing.T) {\n\tcurrentUser.Username = \"admin\"\n\tonce = new(sync.Once)\n\tuser := LimaUser(t.Context(), \"0.23.0\", false, nil)\n\tassert.Equal(t, user.Username, \"admin\")\n}\n\nfunc TestLimaUserInvalid(t *testing.T) {\n\tcurrentUser.Username = \"use@example.com\"\n\tonce = new(sync.Once)\n\tuser := LimaUser(t.Context(), limaVersion, false, nil)\n\tassert.Equal(t, user.Username, fallbackUser)\n}\n\nfunc TestLimaUserUid(t *testing.T) {\n\tcurrentUser.Username = fallbackUser\n\tonce = new(sync.Once)\n\tuser := LimaUser(t.Context(), limaVersion, false, nil)\n\t_, err := strconv.Atoi(user.Uid)\n\tassert.NilError(t, err)\n}\n\nfunc TestLimaUserGid(t *testing.T) {\n\tcurrentUser.Username = fallbackUser\n\tonce = new(sync.Once)\n\tuser := LimaUser(t.Context(), limaVersion, false, nil)\n\t_, err := strconv.Atoi(user.Gid)\n\tassert.NilError(t, err)\n}\n\nfunc TestLimaHomeDir(t *testing.T) {\n\tcurrentUser.Username = fallbackUser\n\tonce = new(sync.Once)\n\tuser := LimaUser(t.Context(), limaVersion, false, nil)\n\t// check for absolute unix path (/home)\n\tassert.Assert(t, path.IsAbs(user.HomeDir), user.HomeDir)\n}\n"
  },
  {
    "path": "pkg/plist/plist.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package plist provides a parser for XML-formatted plist documents.\n// Binary plist is not supported.\npackage plist\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Plist represents a plist document.\ntype Plist struct {\n\tValue Value\n}\n\n// Value represents a value.\ntype Value struct {\n\tArray Array\n\tDict  Dict\n\n\tString *string\n\tData   []byte\n\tDate   *time.Time\n\n\tBoolean *bool\n\tReal    *float64\n\tInteger *int64\n}\n\n// Array represents an array.\ntype Array []Value\n\n// Dict represents a dict.\ntype Dict map[string]Value\n\nfunc (p *Plist) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {\n\tfor {\n\t\ttok, err := dec.Token()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tswitch t := tok.(type) {\n\t\tcase xml.StartElement:\n\t\t\tvar v Value\n\t\t\tif err := dec.DecodeElement(&v, &t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tp.Value = v\n\t\tcase xml.EndElement:\n\t\t\tif t.Name.Local == start.Name.Local {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (v *Value) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {\n\tswitch start.Name.Local {\n\tcase \"array\":\n\t\tvar arr Array\n\t\tif err := dec.DecodeElement(&arr, &start); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tv.Array = arr\n\t\treturn nil\n\tcase \"dict\":\n\t\tvar sub Dict\n\t\tif err := dec.DecodeElement(&sub, &start); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tv.Dict = sub\n\t\treturn nil\n\tcase \"string\":\n\t\tvar txt string\n\t\tif err := dec.DecodeElement(&txt, &start); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tv.String = &txt\n\t\treturn nil\n\tcase \"data\":\n\t\tvar txt string\n\t\tif err := dec.DecodeElement(&txt, &start); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// remove all whitespace/newlines from base64 text\n\t\tb64 := strings.Join(strings.Fields(txt), \"\")\n\t\tdb, err := base64.StdEncoding.DecodeString(b64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid base64 data: %w\", err)\n\t\t}\n\t\tv.Data = db\n\t\treturn nil\n\tcase \"date\":\n\t\tvar txt string\n\t\tif err := dec.DecodeElement(&txt, &start); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tt, err := time.Parse(time.RFC3339, strings.TrimSpace(txt))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid date value: %w\", err)\n\t\t}\n\t\tv.Date = &t\n\t\treturn nil\n\tcase \"true\":\n\t\tb := true\n\t\tv.Boolean = &b\n\t\t// consume tokens until matching end element\n\t\treturn dec.Skip()\n\tcase \"false\":\n\t\tb := false\n\t\tv.Boolean = &b\n\t\t// consume tokens until matching end element\n\t\treturn dec.Skip()\n\tcase \"real\":\n\t\tvar txt string\n\t\tif err := dec.DecodeElement(&txt, &start); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf, err := strconv.ParseFloat(strings.TrimSpace(txt), 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid real value: %w\", err)\n\t\t}\n\t\tv.Real = &f\n\t\treturn nil\n\tcase \"integer\":\n\t\tvar txt string\n\t\tif err := dec.DecodeElement(&txt, &start); err != nil {\n\t\t\treturn err\n\t\t}\n\t\ti, err := strconv.ParseInt(strings.TrimSpace(txt), 10, 64)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid integer value: %w\", err)\n\t\t}\n\t\tv.Integer = &i\n\t\treturn nil\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported plist type: %s\", start.Name.Local)\n\t}\n}\n\nfunc (a *Array) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {\n\tvar vals []Value\n\tfor {\n\t\ttok, err := dec.Token()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\t*a = vals\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tswitch t := tok.(type) {\n\t\tcase xml.StartElement:\n\t\t\tvar v Value\n\t\t\tif err := dec.DecodeElement(&v, &t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvals = append(vals, v)\n\t\tcase xml.EndElement:\n\t\t\tif t.Name.Local == start.Name.Local {\n\t\t\t\t*a = vals\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (d *Dict) UnmarshalXML(dec *xml.Decoder, start xml.StartElement) error {\n\t*d = make(map[string]Value)\n\n\tfor {\n\t\ttok, err := dec.Token()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tswitch t := tok.(type) {\n\t\tcase xml.StartElement:\n\t\t\tif t.Name.Local != \"key\" {\n\t\t\t\treturn fmt.Errorf(\"expected <key> element, got <%s>\", t.Name.Local)\n\t\t\t}\n\t\t\tvar key string\n\t\t\tif err := dec.DecodeElement(&key, &t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tvar vs xml.StartElement\n\t\t\tfor {\n\t\t\t\tvt, err := dec.Token()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif se, ok := vt.(xml.StartElement); ok {\n\t\t\t\t\tvs = se\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tvar v Value\n\t\t\tif err := dec.DecodeElement(&v, &vs); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t(*d)[key] = v\n\t\tcase xml.EndElement:\n\t\t\tif t.Name.Local == start.Name.Local {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/plist/plist_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage plist\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/ptr\"\n)\n\nfunc TestUnmarshalPlist(t *testing.T) {\n\tconst input = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>arrKey</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>strKey</key>\n\t\t\t<string>strVal</string>\n\t\t\t<key> strKey with\n\t\t\tspaces</key>\n\t\t\t<string> strVal\n\t\t\twith space</string>\n\t\t\t<key>intKey</key>\n\t\t\t<integer>42</integer>\n\t\t\t<key> intKey with\n\t\t\tspaces</key>\n\t\t\t<integer>  43</integer>\n\t\t\t<key>intKeyZero</key>\n\t\t\t<integer>0</integer>\n\t\t\t<key>dataKey</key>\n\t\t\t<data>\n\t\t\taGVs\n\t\t\tbG8=\n\t\t\t</data>\n\t\t\t<key>dateKey</key>\n\t\t\t<date>2020-01-02T03:04:05Z</date>\n\t\t\t<key>trueKey</key>\n\t\t\t<true/>\n\t\t\t<key>falseKey</key>\n\t\t\t<false/>\n\t\t\t<key>realKey</key>\n\t\t\t<real>3.14</real>\n\t\t\t<key>realExpKey</key>\n\t\t\t<real>1e-6</real>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n`\n\texpected := Plist{\n\t\tValue: Value{\n\t\t\tDict: map[string]Value{\n\t\t\t\t\"arrKey\": {\n\t\t\t\t\tArray: Array{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDict: map[string]Value{\n\t\t\t\t\t\t\t\t\"strKey\":                     {String: ptr.Of(\"strVal\")},\n\t\t\t\t\t\t\t\t\" strKey with\\n\\t\\t\\tspaces\": {String: ptr.Of(\" strVal\\n\\t\\t\\twith space\")},\n\t\t\t\t\t\t\t\t\"intKey\":                     {Integer: ptr.Of(int64(42))},\n\t\t\t\t\t\t\t\t\" intKey with\\n\\t\\t\\tspaces\": {Integer: ptr.Of(int64(43))},\n\t\t\t\t\t\t\t\t\"intKeyZero\":                 {Integer: ptr.Of(int64(0))},\n\t\t\t\t\t\t\t\t\"dataKey\":                    {Data: []byte(\"hello\")},\n\t\t\t\t\t\t\t\t\"dateKey\":                    {Date: ptr.Of(time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC))},\n\t\t\t\t\t\t\t\t\"trueKey\":                    {Boolean: ptr.Of(true)},\n\t\t\t\t\t\t\t\t\"falseKey\":                   {Boolean: ptr.Of(false)},\n\t\t\t\t\t\t\t\t\"realKey\":                    {Real: ptr.Of(3.14)},\n\t\t\t\t\t\t\t\t\"realExpKey\":                 {Real: ptr.Of(1e-6)},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tplutilExpected := map[string]string{\n\t\t\"arrKey.0.strKey\":                     \"strVal\",\n\t\t\"arrKey.0. strKey with\\n\\t\\t\\tspaces\": \" strVal\\n\\t\\t\\twith space\",\n\t\t\"arrKey.0.intKey\":                     \"42\",\n\t\t\"arrKey.0. intKey with\\n\\t\\t\\tspaces\": \"43\",\n\t\t\"arrKey.0.intKeyZero\":                 \"0\",\n\t\t\"arrKey.0.dataKey\":                    \"aGVsbG8=\",\n\t\t\"arrKey.0.dateKey\":                    \"2020-01-02T03:04:05Z\",\n\t\t\"arrKey.0.trueKey\":                    \"true\",\n\t\t\"arrKey.0.falseKey\":                   \"false\",\n\t\t\"arrKey.0.realKey\":                    \"3.140000\",\n\t\t\"arrKey.0.realExpKey\":                 \"0.000001\",\n\t}\n\n\tvar p Plist\n\terr := xml.Unmarshal([]byte(input), &p)\n\tassert.NilError(t, err)\n\tassert.DeepEqual(t, p, expected)\n\n\tif runtime.GOOS == \"darwin\" {\n\t\tctx := t.Context()\n\t\ttd := t.TempDir()\n\t\ttestFilePath := filepath.Join(td, \"test.plist\")\n\t\terr := os.WriteFile(testFilePath, []byte(input), 0o644)\n\t\tassert.NilError(t, err)\n\t\tfor plutilExpectedKey, plutilExpectedValue := range plutilExpected {\n\t\t\tplutilCmd := exec.CommandContext(ctx, \"plutil\", \"-extract\", plutilExpectedKey, \"raw\", \"-n\", \"-o\", \"-\", testFilePath)\n\t\t\tvar stderr bytes.Buffer\n\t\t\tplutilCmd.Stderr = &stderr\n\t\t\tplutilOutput, err := plutilCmd.Output()\n\t\t\tassert.NilError(t, err, fmt.Sprintf(\"failed to run %v (stderr=%q)\", plutilCmd.Args, stderr.String()))\n\t\t\tplutilOutputStr := string(plutilOutput)\n\t\t\tassert.Equal(t, plutilExpectedValue, plutilOutputStr, \"plutil output mismatch for key %q\", plutilExpectedKey)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/plugins/plugins.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage plugins\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/usrlocal\"\n)\n\nconst defaultPathExt = \".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL\"\n\ntype Plugin struct {\n\tName        string `json:\"name\"`\n\tPath        string `json:\"path\"`\n\tDescription string `json:\"description,omitempty\"`\n}\n\nvar Discover = sync.OnceValues(func() ([]Plugin, error) {\n\tvar plugins []Plugin\n\tseen := make(map[string]bool)\n\n\tfor _, dir := range getPluginDirectories() {\n\t\tfor _, plugin := range scanDirectory(dir) {\n\t\t\tif !seen[plugin.Name] {\n\t\t\t\tplugins = append(plugins, plugin)\n\t\t\t\tseen[plugin.Name] = true\n\t\t\t}\n\t\t}\n\t}\n\n\tslices.SortFunc(plugins,\n\t\tfunc(i, j Plugin) int {\n\t\t\treturn cmp.Compare(i.Name, j.Name)\n\t\t})\n\n\treturn plugins, nil\n})\n\nvar getPluginDirectories = sync.OnceValue(func() []string {\n\tdirs := usrlocal.SelfDirs()\n\n\tpathEnv := os.Getenv(\"PATH\")\n\tif pathEnv != \"\" {\n\t\tpathDirs := filepath.SplitList(pathEnv)\n\t\tdirs = append(dirs, pathDirs...)\n\t}\n\n\tlibexecDirs, err := usrlocal.LibexecLima()\n\tif err == nil {\n\t\tdirs = append(dirs, libexecDirs...)\n\t}\n\n\treturn dirs\n})\n\n// isWindowsExecutableExt checks if the given extension is a valid Windows executable extension\n// according to PATHEXT environment variable.\nfunc isWindowsExecutableExt(ext string) bool {\n\tif runtime.GOOS != \"windows\" {\n\t\treturn false\n\t}\n\n\tpathExt := cmp.Or(os.Getenv(\"PATHEXT\"), defaultPathExt)\n\textensions := strings.Split(strings.ToUpper(pathExt), \";\")\n\treturn slices.Contains(extensions, strings.ToUpper(ext))\n}\n\nfunc isExecutable(path string) bool {\n\tinfo, err := os.Stat(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tif !info.Mode().IsRegular() {\n\t\treturn false\n\t}\n\n\tif runtime.GOOS != \"windows\" {\n\t\treturn info.Mode()&0o111 != 0\n\t}\n\n\treturn isWindowsExecutableExt(filepath.Ext(path))\n}\n\nfunc scanDirectory(dir string) []Plugin {\n\tvar plugins []Plugin\n\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\tlogrus.Debugf(\"Plugin discovery: failed to scan directory %s: %v\", dir, err)\n\n\t\treturn plugins\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := entry.Name()\n\t\tif !strings.HasPrefix(name, \"limactl-\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tpluginName := strings.TrimPrefix(name, \"limactl-\")\n\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\text := filepath.Ext(pluginName)\n\t\t\tif isWindowsExecutableExt(ext) {\n\t\t\t\tpluginName = strings.TrimSuffix(pluginName, ext)\n\t\t\t}\n\t\t}\n\n\t\tfullPath := filepath.Join(dir, name)\n\n\t\tif !isExecutable(fullPath) {\n\t\t\tcontinue\n\t\t}\n\n\t\tplugin := Plugin{\n\t\t\tName: pluginName,\n\t\t\tPath: fullPath,\n\t\t}\n\n\t\tif desc := extractDescFromScript(fullPath); desc != \"\" {\n\t\t\tplugin.Description = desc\n\t\t}\n\n\t\tplugins = append(plugins, plugin)\n\t}\n\n\treturn plugins\n}\n\nfunc (plugin *Plugin) Run(ctx context.Context, args []string) {\n\tif err := UpdatePath(); err != nil {\n\t\tlogrus.Warnf(\"failed to update PATH environment: %v\", err)\n\t\t// PATH update failure shouldn't prevent plugin execution\n\t}\n\n\tcmd := exec.CommandContext(ctx, plugin.Path, args...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.Env = os.Environ()\n\n\terr := cmd.Run()\n\tosutil.HandleExitError(err)\n\tif err == nil {\n\t\tos.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function\n\t}\n\tlogrus.Fatalf(\"external command %q failed: %v\", plugin.Path, err)\n}\n\nvar descRegex = regexp.MustCompile(`<limactl-desc>(.*?)</limactl-desc>`)\n\nfunc extractDescFromScript(path string) string {\n\tcontent, err := os.ReadFile(path)\n\tif err != nil {\n\t\tlogrus.Debugf(\"Failed to read plugin script %s: %v\", path, err)\n\t\treturn \"\"\n\t}\n\n\tif !strings.HasPrefix(string(content), \"#!\") {\n\t\tlogrus.Debugf(\"Plugin %s: not a script file, skipping description extraction\", path)\n\t\treturn \"\"\n\t}\n\n\tmatches := descRegex.FindStringSubmatch(string(content))\n\tif len(matches) < 2 {\n\t\tlogrus.Debugf(\"Plugin %s: no <limactl-desc> found in script\", filepath.Base(path))\n\t\treturn \"\"\n\t}\n\n\tdesc := strings.Trim(matches[1], \" \")\n\tlogrus.Debugf(\"Plugin %s: extracted description: %q\", filepath.Base(path), desc)\n\treturn desc\n}\n\n// Find locates a plugin by name and returns a pointer to a copy.\nfunc Find(name string) (*Plugin, error) {\n\tallPlugins, err := Discover()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, plugin := range allPlugins {\n\t\tif name == plugin.Name {\n\t\t\tpluginCopy := plugin\n\t\t\treturn &pluginCopy, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc UpdatePath() error {\n\tpluginDirs := getPluginDirectories()\n\tnewPath := strings.Join(pluginDirs, string(filepath.ListSeparator))\n\treturn os.Setenv(\"PATH\", newPath)\n}\n"
  },
  {
    "path": "pkg/portfwd/client.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/containers/gvisor-tap-vsock/pkg/services/forwarder\"\n\t\"github.com/inetaf/tcpproxy\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n\tguestagentclient \"github.com/lima-vm/lima/v2/pkg/guestagent/api/client\"\n)\n\nfunc HandleTCPConnection(_ context.Context, dialContext func(ctx context.Context, network string, addr string) (net.Conn, error), conn net.Conn, guestAddr string) {\n\tproxy := tcpproxy.DialProxy{Addr: guestAddr, DialContext: dialContext}\n\tproxy.HandleConn(conn)\n\tlogrus.Debugf(\"tcp proxy for guestAddr: %s closed\", guestAddr)\n}\n\nfunc HandleUDPConnection(ctx context.Context, dialContext func(ctx context.Context, network string, addr string) (net.Conn, error), conn net.PacketConn, guestAddr string) {\n\tproxy, err := forwarder.NewUDPProxy(conn, func() (net.Conn, error) {\n\t\treturn dialContext(ctx, \"udp\", guestAddr)\n\t})\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"error in udp tunnel proxy\")\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\terr := proxy.Close()\n\t\tif err != nil {\n\t\t\tlogrus.WithError(err).Error(\"error in closing udp tunnel proxy\")\n\t\t}\n\t}()\n\tproxy.Run()\n\tlogrus.Debugf(\"udp proxy for guestAddr: %s closed\", guestAddr)\n}\n\nfunc DialContextToGRPCTunnel(client *guestagentclient.GuestAgentClient) func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t// gvisor-tap-vsock's UDPProxy demultiplexes client connections internally based on their source address.\n\t// It calls this dialer function only when it receives a datagram from a new, unrecognized client.\n\t// For each new client, we must return a new net.Conn, which in our case is a new gRPC stream.\n\t// The atomic counter ensures that each stream has a unique ID to distinguish them on the server side.\n\tvar connectionCounter atomic.Uint32\n\treturn func(_ context.Context, network, addr string) (net.Conn, error) {\n\t\t// Passed context.Context is used for timeout on initiate connection, not for the lifetime of the connection.\n\t\t// We use context.Background() here to avoid unexpected cancellation.\n\t\tstream, err := client.Tunnel(context.Background())\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not open tunnel for addr: %s error:%w\", addr, err)\n\t\t}\n\t\t// Handshake message to start tunnel\n\t\tid := fmt.Sprintf(\"%s-%s-%d\", network, addr, connectionCounter.Add(1))\n\t\tif err := stream.Send(&api.TunnelMessage{Id: id, Protocol: network, GuestAddr: addr}); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not start tunnel for id: %s addr: %s error:%w\", id, addr, err)\n\t\t}\n\t\trw := &GrpcClientRW{stream: stream, id: id, addr: addr, protocol: network}\n\t\treturn rw, nil\n\t}\n}\n\ntype GrpcClientRW struct {\n\tid   string\n\taddr string\n\n\tprotocol string\n\tstream   api.GuestService_TunnelClient\n}\n\nvar _ net.Conn = (*GrpcClientRW)(nil)\n\nfunc (g *GrpcClientRW) Write(p []byte) (n int, err error) {\n\terr = g.stream.Send(&api.TunnelMessage{\n\t\tId:        g.id,\n\t\tGuestAddr: g.addr,\n\t\tData:      p,\n\t\tProtocol:  g.protocol,\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(p), nil\n}\n\nfunc (g *GrpcClientRW) Read(p []byte) (n int, err error) {\n\tin, err := g.stream.Recv()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tcopy(p, in.Data)\n\treturn len(in.Data), nil\n}\n\nfunc (g *GrpcClientRW) Close() error {\n\tlogrus.Debugf(\"closing GrpcClientRW for id: %s\", g.id)\n\treturn g.stream.CloseSend()\n}\n\nfunc (g *GrpcClientRW) LocalAddr() net.Addr {\n\treturn &net.UnixAddr{Name: \"grpc\", Net: \"unixpacket\"}\n}\n\nfunc (g *GrpcClientRW) RemoteAddr() net.Addr {\n\treturn &net.UnixAddr{Name: \"grpc\", Net: \"unixpacket\"}\n}\n\nfunc (g *GrpcClientRW) SetDeadline(_ time.Time) error {\n\treturn nil\n}\n\nfunc (g *GrpcClientRW) SetReadDeadline(_ time.Time) error {\n\treturn nil\n}\n\nfunc (g *GrpcClientRW) SetWriteDeadline(_ time.Time) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/portfwd/control_others.go",
    "content": "//go:build !windows\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwd\n\nimport (\n\t\"syscall\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc Control(_, _ string, c syscall.RawConn) (err error) {\n\tcontrolErr := c.Control(func(fd uintptr) {\n\t\terr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t})\n\tif controlErr != nil {\n\t\terr = controlErr\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "pkg/portfwd/control_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwd\n\nimport (\n\t\"syscall\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc Control(_, _ string, c syscall.RawConn) (err error) {\n\tcontrolErr := c.Control(func(fd uintptr) {\n\t\terr = windows.SetsockoptInt(windows.Handle(int(fd)), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t})\n\tif controlErr != nil {\n\t\terr = controlErr\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "pkg/portfwd/forward.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwd\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n\t\"github.com/lima-vm/lima/v2/pkg/hostagent/events\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n)\n\nvar IPv4loopback1 = limayaml.IPv4loopback1\n\ntype Forwarder struct {\n\trules             []limatype.PortForward\n\tignoreTCP         bool\n\tignoreUDP         bool\n\tclosableListeners *ClosableListeners\n\tonEvent           func(*events.PortForwardEvent)\n}\n\nfunc NewPortForwarder(rules []limatype.PortForward, ignoreTCP, ignoreUDP bool, onEvent func(*events.PortForwardEvent)) *Forwarder {\n\treturn &Forwarder{\n\t\trules:             rules,\n\t\tignoreTCP:         ignoreTCP,\n\t\tignoreUDP:         ignoreUDP,\n\t\tclosableListeners: NewClosableListener(),\n\t\tonEvent:           onEvent,\n\t}\n}\n\nfunc (fw *Forwarder) emitEvent(ev *events.PortForwardEvent) {\n\tif fw.onEvent != nil {\n\t\tfw.onEvent(ev)\n\t}\n}\n\nfunc (fw *Forwarder) Close() error {\n\treturn fw.closableListeners.Close()\n}\n\nfunc (fw *Forwarder) OnEvent(ctx context.Context, dialContext func(ctx context.Context, network string, addr string) (net.Conn, error), ev *api.Event) {\n\tfor _, f := range ev.AddedLocalPorts {\n\t\t// Before forwarding, check if any static rule matches this port otherwise it will be forwarded twice and cause a port conflict\n\t\tif fw.isPortStaticallyForwarded(f) {\n\t\t\tcontinue\n\t\t}\n\t\tlocal, remote := fw.forwardingAddresses(f)\n\t\tif local == \"\" {\n\t\t\tif !fw.ignoreTCP && f.Protocol == \"tcp\" {\n\t\t\t\tlogrus.Infof(\"Not forwarding TCP %s\", remote)\n\t\t\t\tfw.emitEvent(&events.PortForwardEvent{\n\t\t\t\t\tType:      events.PortForwardEventNotForwarding,\n\t\t\t\t\tProtocol:  f.Protocol,\n\t\t\t\t\tGuestAddr: remote,\n\t\t\t\t})\n\t\t\t}\n\t\t\tif !fw.ignoreUDP && f.Protocol == \"udp\" {\n\t\t\t\tlogrus.Infof(\"Not forwarding UDP %s\", remote)\n\t\t\t\tfw.emitEvent(&events.PortForwardEvent{\n\t\t\t\t\tType:      events.PortForwardEventNotForwarding,\n\t\t\t\t\tProtocol:  f.Protocol,\n\t\t\t\t\tGuestAddr: remote,\n\t\t\t\t})\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Infof(\"Forwarding %s from %s to %s\", strings.ToUpper(f.Protocol), remote, local)\n\t\tfw.emitEvent(&events.PortForwardEvent{\n\t\t\tType:      events.PortForwardEventForwarding,\n\t\t\tProtocol:  f.Protocol,\n\t\t\tGuestAddr: remote,\n\t\t\tHostAddr:  local,\n\t\t})\n\t\tfw.closableListeners.Forward(ctx, dialContext, f.Protocol, local, remote)\n\t}\n\tfor _, f := range ev.RemovedLocalPorts {\n\t\tlocal, remote := fw.forwardingAddresses(f)\n\t\tif local == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tfw.emitEvent(&events.PortForwardEvent{\n\t\t\tType:      events.PortForwardEventStopping,\n\t\t\tProtocol:  f.Protocol,\n\t\t\tGuestAddr: remote,\n\t\t\tHostAddr:  local,\n\t\t})\n\t\tfw.closableListeners.Remove(ctx, f.Protocol, local, remote)\n\t\tlogrus.Debugf(\"Port forwarding closed proto:%s host:%s guest:%s\", f.Protocol, local, remote)\n\t}\n}\n\nfunc (fw *Forwarder) forwardingAddresses(guest *api.IPPort) (hostAddr, guestAddr string) {\n\tguestIP := net.ParseIP(guest.Ip)\n\tfor _, rule := range fw.rules {\n\t\tif rule.GuestSocket != \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif rule.Proto != limatype.ProtoAny && rule.Proto != guest.Protocol {\n\t\t\tcontinue\n\t\t}\n\t\tif guest.Port < int32(rule.GuestPortRange[0]) || guest.Port > int32(rule.GuestPortRange[1]) {\n\t\t\tcontinue\n\t\t}\n\t\tswitch {\n\t\tcase guestIP.IsUnspecified():\n\t\tcase guestIP.Equal(rule.GuestIP):\n\t\tcase guestIP.Equal(net.IPv6loopback) && rule.GuestIP.Equal(IPv4loopback1):\n\t\tcase rule.GuestIP.IsUnspecified() && !*rule.GuestIPMustBeZero:\n\t\t\t// When GuestIPMustBeZero is true, then 0.0.0.0 must be an exact match, which is already\n\t\t\t// handled above by the guestIP.IsUnspecified() condition.\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\tif rule.Ignore {\n\t\t\tif guestIP.IsUnspecified() && !rule.GuestIP.IsUnspecified() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\treturn hostAddress(rule, guest), guest.HostString()\n\t}\n\treturn \"\", guest.HostString()\n}\n\nfunc (fw *Forwarder) isPortStaticallyForwarded(guest *api.IPPort) bool {\n\tfor _, rule := range fw.rules {\n\t\tif !rule.Static {\n\t\t\tcontinue\n\t\t}\n\t\tif guest.Port >= int32(rule.GuestPortRange[0]) && guest.Port <= int32(rule.GuestPortRange[1]) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc hostAddress(rule limatype.PortForward, guest *api.IPPort) string {\n\tif rule.HostSocket != \"\" {\n\t\treturn rule.HostSocket\n\t}\n\thost := &api.IPPort{Ip: rule.HostIP.String()}\n\tif guest.Port == 0 {\n\t\t// guest is a socket\n\t\thost.Port = int32(rule.HostPort)\n\t} else {\n\t\thost.Port = guest.Port + int32(rule.HostPortRange[0]-rule.GuestPortRange[0])\n\t}\n\treturn host.HostString()\n}\n"
  },
  {
    "path": "pkg/portfwd/listener.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\ntype ClosableListeners struct {\n\tlistenConfig   net.ListenConfig\n\tlisteners      map[string]net.Listener\n\tudpListeners   map[string]net.PacketConn\n\tlistenersRW    sync.Mutex\n\tudpListenersRW sync.Mutex\n}\n\nfunc NewClosableListener() *ClosableListeners {\n\tlistenConfig := net.ListenConfig{\n\t\tControl: Control,\n\t}\n\n\treturn &ClosableListeners{\n\t\tlisteners:    make(map[string]net.Listener),\n\t\tudpListeners: make(map[string]net.PacketConn),\n\t\tlistenConfig: listenConfig,\n\t}\n}\n\nfunc (p *ClosableListeners) Close() error {\n\tp.listenersRW.Lock()\n\tdefer p.listenersRW.Unlock()\n\tvar errs []error\n\tfor _, listener := range p.listeners {\n\t\tif err := listener.Close(); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\tclear(p.listeners)\n\tp.udpListenersRW.Lock()\n\tdefer p.udpListenersRW.Unlock()\n\tfor _, listener := range p.udpListeners {\n\t\tif err := listener.Close(); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\tclear(p.udpListeners)\n\treturn errors.Join(errs...)\n}\n\nfunc (p *ClosableListeners) Forward(ctx context.Context, dialContext func(ctx context.Context, network string, addr string) (net.Conn, error),\n\tprotocol string, hostAddress string, guestAddress string,\n) {\n\tswitch protocol {\n\tcase \"tcp\", \"tcp6\":\n\t\tgo p.forwardTCP(ctx, dialContext, hostAddress, guestAddress)\n\tcase \"udp\", \"udp6\":\n\t\tgo p.forwardUDP(ctx, dialContext, hostAddress, guestAddress)\n\t}\n}\n\nfunc (p *ClosableListeners) Remove(_ context.Context, protocol, hostAddress, guestAddress string) {\n\tlogrus.Debugf(\"removing listener for hostAddress: %s, guestAddress: %s\", hostAddress, guestAddress)\n\tkey := key(protocol, hostAddress, guestAddress)\n\tswitch protocol {\n\tcase \"tcp\", \"tcp6\":\n\t\tp.listenersRW.Lock()\n\t\tdefer p.listenersRW.Unlock()\n\t\tlistener, ok := p.listeners[key]\n\t\tif ok {\n\t\t\tlistener.Close()\n\t\t\tdelete(p.listeners, key)\n\t\t}\n\tcase \"udp\", \"udp6\":\n\t\tp.udpListenersRW.Lock()\n\t\tdefer p.udpListenersRW.Unlock()\n\t\tlistener, ok := p.udpListeners[key]\n\t\tif ok {\n\t\t\tlistener.Close()\n\t\t\tdelete(p.udpListeners, key)\n\t\t}\n\t}\n}\n\nfunc (p *ClosableListeners) forwardTCP(ctx context.Context, dialContext func(ctx context.Context, network string, addr string) (net.Conn, error), hostAddress, guestAddress string) {\n\tkey := key(\"tcp\", hostAddress, guestAddress)\n\n\tp.listenersRW.Lock()\n\t_, ok := p.listeners[key]\n\tif ok {\n\t\tp.listenersRW.Unlock()\n\t\treturn\n\t}\n\ttcpLis, err := Listen(ctx, p.listenConfig, hostAddress)\n\tif err != nil {\n\t\tlogListenError(err, \"tcp\", hostAddress)\n\t\tp.listenersRW.Unlock()\n\t\treturn\n\t}\n\tdefer p.Remove(ctx, \"tcp\", hostAddress, guestAddress)\n\tp.listeners[key] = tcpLis\n\tp.listenersRW.Unlock()\n\tfor {\n\t\tconn, err := tcpLis.Accept()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlogrus.Errorf(\"failed to accept TCP connection: %v\", err)\n\t\t\tif strings.Contains(err.Error(), \"pseudoloopback\") {\n\t\t\t\t// don't stop forwarding because the forwarder has rejected a non-local address\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tgo HandleTCPConnection(ctx, dialContext, conn, guestAddress)\n\t}\n}\n\nfunc (p *ClosableListeners) forwardUDP(ctx context.Context, dialContext func(ctx context.Context, network string, addr string) (net.Conn, error), hostAddress, guestAddress string) {\n\tkey := key(\"udp\", hostAddress, guestAddress)\n\tdefer p.Remove(ctx, \"udp\", hostAddress, guestAddress)\n\n\tp.udpListenersRW.Lock()\n\t_, ok := p.udpListeners[key]\n\tif ok {\n\t\tp.udpListenersRW.Unlock()\n\t\treturn\n\t}\n\n\tudpConn, err := ListenPacket(ctx, p.listenConfig, hostAddress)\n\tif err != nil {\n\t\tlogListenError(err, \"udp\", hostAddress)\n\t\tp.udpListenersRW.Unlock()\n\t\treturn\n\t}\n\tp.udpListeners[key] = udpConn\n\tp.udpListenersRW.Unlock()\n\n\tHandleUDPConnection(ctx, dialContext, udpConn, guestAddress)\n}\n\nfunc key(protocol, hostAddress, guestAddress string) string {\n\treturn fmt.Sprintf(\"%s-%s-%s\", protocol, hostAddress, guestAddress)\n}\n\nfunc prepareUnixSocket(hostSocket string) error {\n\tif err := os.RemoveAll(hostSocket); err != nil {\n\t\treturn fmt.Errorf(\"can't clean up %q: %w\", hostSocket, err)\n\t}\n\tif err := os.MkdirAll(filepath.Dir(hostSocket), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"can't create directory for local socket %q: %w\", hostSocket, err)\n\t}\n\treturn nil\n}\n\nfunc logListenError(err error, proto, hostAddress string) {\n\tvar negligibleReason string\n\tif proto != \"unix\" {\n\t\tif _, portStr, err := net.SplitHostPort(hostAddress); err == nil {\n\t\t\tswitch proto {\n\t\t\tcase \"tcp\":\n\t\t\t\t//nolint:gocritic // singleCaseSwitch: should rewrite switch statement to if statement\n\t\t\t\tswitch portStr {\n\t\t\t\tcase \"53\":\n\t\t\t\t\tnegligibleReason = \"DNS port (often conflicts with the system resolver)\"\n\t\t\t\t}\n\t\t\tcase \"udp\":\n\t\t\t\tswitch portStr {\n\t\t\t\tcase \"53\":\n\t\t\t\t\tnegligibleReason = \"DNS port (often conflicts with the system resolver)\"\n\t\t\t\tcase \"323\":\n\t\t\t\t\tnegligibleReason = \"NTP port (often conflicts with the system NTP server)\"\n\t\t\t\tcase \"5353\":\n\t\t\t\t\tnegligibleReason = \"mDNS port (often conflicts with the system mDNS responder)\"\n\t\t\t\tcase \"5355\":\n\t\t\t\t\tnegligibleReason = \"LLMNR port (often conflicts with the system LLMNR responder)\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif osutil.IsEACCES(err) {\n\t\t\tnegligibleReason = \"privileged port\"\n\t\t}\n\t}\n\n\tif negligibleReason != \"\" {\n\t\tlogrus.WithError(err).WithField(\"negligible-reason\", negligibleReason).Debugf(\"failed to listen %s: %s\", proto, hostAddress)\n\t\tlogrus.Infof(\"Not forwarding %s %s\", strings.ToUpper(proto), hostAddress)\n\t\treturn\n\t}\n\n\tlogrus.WithError(err).Warnf(\"failed to listen %s: %s\", proto, hostAddress)\n}\n"
  },
  {
    "path": "pkg/portfwd/listener_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) {\n\tif filepath.IsAbs(hostAddress) {\n\t\t// Handle Unix domain sockets\n\t\tif err := prepareUnixSocket(hostAddress); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar lc net.ListenConfig\n\t\tunixLis, err := lc.Listen(ctx, \"unix\", hostAddress)\n\t\tif err != nil {\n\t\t\tlogListenError(err, \"unix\", hostAddress)\n\t\t\treturn nil, err\n\t\t}\n\t\treturn unixLis, nil\n\t}\n\tlocalIPStr, localPortStr, _ := net.SplitHostPort(hostAddress)\n\tlocalIP := net.ParseIP(localIPStr)\n\tlocalPort, _ := strconv.Atoi(localPortStr)\n\n\tif !localIP.Equal(IPv4loopback1) || localPort >= 1024 {\n\t\ttcpLis, err := listenConfig.Listen(ctx, \"tcp\", hostAddress)\n\t\tif err != nil {\n\t\t\tlogListenError(err, \"tcp\", hostAddress)\n\t\t\treturn nil, err\n\t\t}\n\t\treturn tcpLis, nil\n\t}\n\thostAddressPseudo := net.JoinHostPort(\"0.0.0.0\", localPortStr)\n\ttcpLis, err := listenConfig.Listen(ctx, \"tcp\", hostAddressPseudo)\n\tif err != nil {\n\t\tlogListenError(err, \"tcp\", hostAddressPseudo)\n\t\treturn nil, err\n\t}\n\treturn &pseudoLoopbackListener{tcpLis}, nil\n}\n\nfunc ListenPacket(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.PacketConn, error) {\n\tlocalIPStr, localPortStr, _ := net.SplitHostPort(hostAddress)\n\tlocalIP := net.ParseIP(localIPStr)\n\tlocalPort, _ := strconv.Atoi(localPortStr)\n\n\tif !localIP.Equal(IPv4loopback1) || localPort >= 1024 {\n\t\tudpConn, err := listenConfig.ListenPacket(ctx, \"udp\", hostAddress)\n\t\tif err != nil {\n\t\t\tlogListenError(err, \"udp\", hostAddress)\n\t\t\treturn nil, err\n\t\t}\n\t\treturn udpConn, nil\n\t}\n\thostAddressPseudo := net.JoinHostPort(\"0.0.0.0\", localPortStr)\n\tudpConn, err := listenConfig.ListenPacket(ctx, \"udp\", hostAddressPseudo)\n\tif err != nil {\n\t\tlogListenError(err, \"udp\", hostAddressPseudo)\n\t\treturn nil, err\n\t}\n\treturn &pseudoLoopbackPacketConn{udpConn}, nil\n}\n\ntype pseudoLoopbackListener struct {\n\tnet.Listener\n}\n\nfunc (p pseudoLoopbackListener) Accept() (net.Conn, error) {\n\tconn, err := p.Listener.Accept()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tremoteAddr := conn.RemoteAddr().String() // ip:port\n\tremoteAddrIP, _, err := net.SplitHostPort(remoteAddr)\n\tif err != nil {\n\t\tlogrus.WithError(err).Debugf(\"pseudoloopback forwarder: rejecting non-loopback remoteAddr %q (unparsable)\", remoteAddr)\n\t\tconn.Close()\n\t\treturn nil, err\n\t}\n\tif !IsLoopback(remoteAddrIP) {\n\t\terr := fmt.Errorf(\"pseudoloopback forwarder: rejecting non-loopback remoteAddr %q\", remoteAddr)\n\t\tlogrus.Debug(err)\n\t\tconn.Close()\n\t\treturn nil, err\n\t}\n\tlogrus.Infof(\"pseudoloopback forwarder: accepting connection from %q\", remoteAddr)\n\treturn conn, nil\n}\n\ntype pseudoLoopbackPacketConn struct {\n\tnet.PacketConn\n}\n\nfunc (pk *pseudoLoopbackPacketConn) ReadFrom(bytes []byte) (n int, addr net.Addr, err error) {\n\tn, remoteAddr, err := pk.PacketConn.ReadFrom(bytes)\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\n\tremoteAddrIP, _, err := net.SplitHostPort(remoteAddr.String())\n\tif err != nil {\n\t\treturn 0, nil, err\n\t}\n\tif !IsLoopback(remoteAddrIP) {\n\t\treturn 0, nil, fmt.Errorf(\"pseudoloopback forwarder: rejecting non-loopback remoteAddr %q\", remoteAddr)\n\t}\n\treturn n, remoteAddr, nil\n}\n\nfunc (pk *pseudoLoopbackPacketConn) WriteTo(bytes []byte, remoteAddr net.Addr) (n int, err error) {\n\tremoteAddrIP, _, err := net.SplitHostPort(remoteAddr.String())\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif !IsLoopback(remoteAddrIP) {\n\t\treturn 0, fmt.Errorf(\"pseudoloopback forwarder: rejecting non-loopback remoteAddr %q\", remoteAddr)\n\t}\n\treturn pk.PacketConn.WriteTo(bytes, remoteAddr)\n}\n\nfunc IsLoopback(addr string) bool {\n\treturn net.ParseIP(addr).IsLoopback()\n}\n"
  },
  {
    "path": "pkg/portfwd/listener_others.go",
    "content": "//go:build !darwin\n\n// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwd\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"path/filepath\"\n)\n\nfunc Listen(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.Listener, error) {\n\tif filepath.IsAbs(hostAddress) {\n\t\t// Handle Unix domain sockets\n\t\tif err := prepareUnixSocket(hostAddress); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar lc net.ListenConfig\n\t\tunixLis, err := lc.Listen(ctx, \"unix\", hostAddress)\n\t\tif err != nil {\n\t\t\tlogListenError(err, \"unix\", hostAddress)\n\t\t\treturn nil, err\n\t\t}\n\t\treturn unixLis, nil\n\t}\n\treturn listenConfig.Listen(ctx, \"tcp\", hostAddress)\n}\n\nfunc ListenPacket(ctx context.Context, listenConfig net.ListenConfig, hostAddress string) (net.PacketConn, error) {\n\treturn listenConfig.ListenPacket(ctx, \"udp\", hostAddress)\n}\n"
  },
  {
    "path": "pkg/portfwdserver/server.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage portfwdserver\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/inetaf/tcpproxy\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/guestagent/api\"\n)\n\ntype TunnelServer struct{}\n\nfunc NewTunnelServer() *TunnelServer {\n\treturn &TunnelServer{}\n}\n\nfunc (s *TunnelServer) Start(stream api.GuestService_TunnelServer) error {\n\tctx := stream.Context()\n\t// Receive the handshake message to start tunnel\n\tin, err := stream.Recv()\n\tif err != nil {\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\n\t// We simply forward data form GRPC stream to net.Conn for both tcp and udp. So simple proxy is sufficient\n\tvar dialer net.Dialer\n\tconn, err := dialer.DialContext(ctx, in.Protocol, in.GuestAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\trw := &GRPCServerRW{stream: stream, id: in.Id, closeCh: make(chan any, 1)}\n\tgo func() {\n\t\t<-ctx.Done()\n\t\trw.Close()\n\t}()\n\n\tproxy := tcpproxy.DialProxy{DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {\n\t\treturn conn, nil\n\t}}\n\tgo proxy.HandleConn(rw)\n\n\t// The stream will be closed when this function returns.\n\t// Wait here until rw.Close(), rw.CloseRead(), or rw.CloseWrite() is called.\n\t// We can't close rw.closeCh since the calling order of Close* methods is not guaranteed.\n\t<-rw.closeCh\n\tlogrus.Debugf(\"closed GRPCServerRW for id: %s\", in.Id)\n\n\treturn nil\n}\n\ntype GRPCServerRW struct {\n\tid      string\n\tstream  api.GuestService_TunnelServer\n\tcloseCh chan any\n}\n\nvar _ net.Conn = (*GRPCServerRW)(nil)\n\nfunc (g *GRPCServerRW) Write(p []byte) (n int, err error) {\n\terr = g.stream.Send(&api.TunnelMessage{Id: g.id, Data: p})\n\treturn len(p), err\n}\n\nfunc (g *GRPCServerRW) Read(p []byte) (n int, err error) {\n\tin, err := g.stream.Recv()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tcopy(p, in.Data)\n\treturn len(in.Data), nil\n}\n\nfunc (g *GRPCServerRW) Close() error {\n\tlogrus.Debugf(\"closing GRPCServerRW for id: %s\", g.id)\n\tg.closeCh <- struct{}{}\n\treturn nil\n}\n\n// By adding CloseRead and CloseWrite methods, GRPCServerRW can work with\n// other than containers/gvisor-tap-vsock/pkg/tcpproxy, e.g., inetaf/tcpproxy, bicopy.Bicopy.\n\nfunc (g *GRPCServerRW) CloseRead() error {\n\tlogrus.Debugf(\"closing read GRPCServerRW for id: %s\", g.id)\n\tg.closeCh <- struct{}{}\n\treturn nil\n}\n\nfunc (g *GRPCServerRW) CloseWrite() error {\n\tlogrus.Debugf(\"closing write GRPCServerRW for id: %s\", g.id)\n\tg.closeCh <- struct{}{}\n\treturn nil\n}\n\nfunc (g *GRPCServerRW) LocalAddr() net.Addr {\n\treturn &net.UnixAddr{Name: \"grpc\", Net: \"unixpacket\"}\n}\n\nfunc (g *GRPCServerRW) RemoteAddr() net.Addr {\n\treturn &net.UnixAddr{Name: \"grpc\", Net: \"unixpacket\"}\n}\n\nfunc (g *GRPCServerRW) SetDeadline(_ time.Time) error {\n\treturn nil\n}\n\nfunc (g *GRPCServerRW) SetReadDeadline(_ time.Time) error {\n\treturn nil\n}\n\nfunc (g *GRPCServerRW) SetWriteDeadline(_ time.Time) error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/progressbar/progressbar.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage progressbar\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/cheggaaa/pb/v3\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// ProgressBar adapts pb.ProgressBar to go-qcow2reader.convert.Updater interface.\ntype ProgressBar struct {\n\t*pb.ProgressBar\n}\n\nfunc (b *ProgressBar) Update(n int64) {\n\tb.Add64(n)\n}\n\nfunc New(size int64) (*ProgressBar, error) {\n\tbar := &ProgressBar{pb.New64(size)}\n\n\tbar.Set(pb.Bytes, true)\n\n\tif showProgress() {\n\t\tbar.SetTemplateString(`{{counters . }} {{bar . | green }} {{percent .}} {{speed . \"%s/s\"}}`)\n\t\tbar.SetRefreshRate(200 * time.Millisecond)\n\t} else {\n\t\tbar.Set(pb.Static, true)\n\t}\n\n\tbar.SetWidth(80)\n\tif err := bar.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn bar, nil\n}\n\nfunc showProgress() bool {\n\t// Progress supports only text format fow now.\n\tif _, ok := logrus.StandardLogger().Formatter.(*logrus.TextFormatter); !ok {\n\t\treturn false\n\t}\n\n\t// Both logrus and pb use stderr by default.\n\tlogFd := os.Stderr.Fd()\n\treturn isatty.IsTerminal(logFd) || isatty.IsCygwinTerminal(logFd)\n}\n"
  },
  {
    "path": "pkg/ptr/ptr.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// Package ptr holds utilities for taking pointer references to values.\npackage ptr\n\n// Of returns pointer to value.\nfunc Of[T any](value T) *T {\n\treturn &value\n}\n"
  },
  {
    "path": "pkg/ptr/ptr_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage ptr\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestOf(t *testing.T) {\n\tassert.DeepEqual(t, bool(true), *Of(true))\n\tassert.DeepEqual(t, int(10), *Of(10))\n\tassert.DeepEqual(t, string(\"\"), *Of(\"\"))\n\tassert.DeepEqual(t, string(\"value\"), *Of(\"value\"))\n}\n"
  },
  {
    "path": "pkg/qemuimgutil/qemuimgutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage qemuimgutil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\n\t\"github.com/lima-vm/go-qcow2reader/image\"\n\t\"github.com/lima-vm/go-qcow2reader/image/raw\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst QemuImgFormat = \"qcow2\"\n\n// QemuImageUtil is the QEMU implementation of the imgutil Interface.\ntype QemuImageUtil struct {\n\tDefaultFormat string // Default disk format, e.g., \"qcow2\"\n}\n\n// Info corresponds to the output of `qemu-img info --output=json FILE`.\ntype Info struct {\n\tFilename              string              `json:\"filename,omitempty\"`                // since QEMU 1.3\n\tFormat                string              `json:\"format,omitempty\"`                  // since QEMU 1.3\n\tVSize                 int64               `json:\"virtual-size,omitempty\"`            // since QEMU 1.3\n\tActualSize            int64               `json:\"actual-size,omitempty\"`             // since QEMU 1.3\n\tDirtyFlag             bool                `json:\"dirty-flag,omitempty\"`              // since QEMU 5.2\n\tClusterSize           int                 `json:\"cluster-size,omitempty\"`            // since QEMU 1.3\n\tBackingFilename       string              `json:\"backing-filename,omitempty\"`        // since QEMU 1.3\n\tFullBackingFilename   string              `json:\"full-backing-filename,omitempty\"`   // since QEMU 1.3\n\tBackingFilenameFormat string              `json:\"backing-filename-format,omitempty\"` // since QEMU 1.3\n\tFormatSpecific        *InfoFormatSpecific `json:\"format-specific,omitempty\"`         // since QEMU 1.7\n\tChildren              []InfoChild         `json:\"children,omitempty\"`                // since QEMU 8.0\n}\n\ntype InfoChild struct {\n\tName string `json:\"name,omitempty\"` // since QEMU 8.0\n\tInfo Info   `json:\"info,omitempty\"` // since QEMU 8.0\n}\n\ntype InfoFormatSpecific struct {\n\tType string          `json:\"type,omitempty\"` // since QEMU 1.7\n\tData json.RawMessage `json:\"data,omitempty\"` // since QEMU 1.7\n}\n\nfunc resizeDisk(ctx context.Context, disk, format string, size int64) error {\n\targs := []string{\"resize\", \"-f\", format, disk, strconv.FormatInt(size, 10)}\n\tcmd := exec.CommandContext(ctx, \"qemu-img\", args...)\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %q: %w\", cmd.Args, string(out), err)\n\t}\n\treturn nil\n}\n\nfunc (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {\n\tif sp.Type != \"qcow2\" {\n\t\treturn nil\n\t}\n\tvar x InfoFormatSpecificDataQcow2\n\tif err := json.Unmarshal(sp.Data, &x); err != nil {\n\t\tpanic(err)\n\t}\n\treturn &x\n}\n\nfunc (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk {\n\tif sp.Type != \"vmdk\" {\n\t\treturn nil\n\t}\n\tvar x InfoFormatSpecificDataVmdk\n\tif err := json.Unmarshal(sp.Data, &x); err != nil {\n\t\tpanic(err)\n\t}\n\treturn &x\n}\n\ntype InfoFormatSpecificDataQcow2 struct {\n\tCompat          string `json:\"compat,omitempty\"`           // since QEMU 1.7\n\tLazyRefcounts   bool   `json:\"lazy-refcounts,omitempty\"`   // since QEMU 1.7\n\tCorrupt         bool   `json:\"corrupt,omitempty\"`          // since QEMU 2.2\n\tRefcountBits    int    `json:\"refcount-bits,omitempty\"`    // since QEMU 2.3\n\tCompressionType string `json:\"compression-type,omitempty\"` // since QEMU 5.1\n\tExtendedL2      bool   `json:\"extended-l2,omitempty\"`      // since QEMU 5.2\n}\n\ntype InfoFormatSpecificDataVmdk struct {\n\tCreateType string                             `json:\"create-type,omitempty\"` // since QEMU 1.7\n\tCID        int                                `json:\"cid,omitempty\"`         // since QEMU 1.7\n\tParentCID  int                                `json:\"parent-cid,omitempty\"`  // since QEMU 1.7\n\tExtents    []InfoFormatSpecificDataVmdkExtent `json:\"extents,omitempty\"`     // since QEMU 1.7\n}\n\ntype InfoFormatSpecificDataVmdkExtent struct {\n\tFilename    string `json:\"filename,omitempty\"`     // since QEMU 1.7\n\tFormat      string `json:\"format,omitempty\"`       // since QEMU 1.7\n\tVSize       int64  `json:\"virtual-size,omitempty\"` // since QEMU 1.7\n\tClusterSize int    `json:\"cluster-size,omitempty\"` // since QEMU 1.7\n}\n\nfunc convertToRaw(ctx context.Context, source, dest string) error {\n\tif source != dest {\n\t\treturn execQemuImgConvert(ctx, source, dest)\n\t}\n\n\t// If source == dest, we need to use a temporary file to avoid file locking issues\n\n\tinfo, err := getInfo(ctx, source)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get info for source disk %q: %w\", source, err)\n\t}\n\tif info.Format == \"raw\" {\n\t\treturn nil\n\t}\n\n\ttempFile := dest + \".lima-qemu-convert.tmp\"\n\tdefer os.Remove(tempFile)\n\n\tif err := execQemuImgConvert(ctx, source, tempFile); err != nil {\n\t\treturn err\n\t}\n\n\treturn os.Rename(tempFile, dest)\n}\n\nfunc execQemuImgConvert(ctx context.Context, source, dest string) error {\n\tvar stdout, stderr bytes.Buffer\n\tcmd := exec.CommandContext(ctx, \"qemu-img\", \"convert\", \"-O\", \"raw\", source, dest)\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: stdout=%q, stderr=%q: %w\",\n\t\t\tcmd.Args, stdout.String(), stderr.String(), err)\n\t}\n\treturn nil\n}\n\nfunc parseInfo(b []byte) (*Info, error) {\n\tvar imgInfo Info\n\tif err := json.Unmarshal(b, &imgInfo); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &imgInfo, nil\n}\n\nfunc getInfo(ctx context.Context, f string) (*Info, error) {\n\tvar stdout, stderr bytes.Buffer\n\tcmd := exec.CommandContext(ctx, \"qemu-img\", \"info\", \"--output=json\", \"--force-share\", f)\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to run %v: stdout=%q, stderr=%q: %w\",\n\t\t\tcmd.Args, stdout.String(), stderr.String(), err)\n\t}\n\treturn parseInfo(stdout.Bytes())\n}\n\n// CreateDisk creates a new disk image with the specified size.\nfunc (q *QemuImageUtil) CreateDisk(ctx context.Context, disk string, size int64) error {\n\tif _, err := os.Stat(disk); err == nil || !errors.Is(err, fs.ErrNotExist) {\n\t\t// disk already exists\n\t\treturn err\n\t}\n\n\targs := []string{\"create\", \"-f\", q.DefaultFormat, disk, strconv.FormatInt(size, 10)}\n\tcmd := exec.CommandContext(ctx, \"qemu-img\", args...)\n\tif out, err := cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"failed to run %v: %q: %w\", cmd.Args, string(out), err)\n\t}\n\treturn nil\n}\n\n// ResizeDisk resizes an existing disk image to the specified size.\nfunc (q *QemuImageUtil) ResizeDisk(ctx context.Context, disk string, size int64) error {\n\tinfo, err := getInfo(ctx, disk)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get info for disk %q: %w\", disk, err)\n\t}\n\treturn resizeDisk(ctx, disk, info.Format, size)\n}\n\n// MakeSparse is a stub implementation as the qemu package doesn't provide this functionality.\nfunc (q *QemuImageUtil) MakeSparse(_ context.Context, _ *os.File, _ int64) error {\n\treturn nil\n}\n\n// GetInfo retrieves the information of a disk image.\nfunc GetInfo(ctx context.Context, path string) (*Info, error) {\n\tqemuInfo, err := getInfo(ctx, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn qemuInfo, nil\n}\n\n// Convert converts a disk image to raw format.\n// Currently only raw.Type is supported.\nfunc (q *QemuImageUtil) Convert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error {\n\tif imageType != raw.Type {\n\t\treturn fmt.Errorf(\"QemuImageUtil.Convert only supports raw.Type, got %q\", imageType)\n\t}\n\tif !allowSourceWithBackingFile {\n\t\tinfo, err := getInfo(ctx, source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get info for source disk %q: %w\", source, err)\n\t\t}\n\t\tif info.BackingFilename != \"\" || info.FullBackingFilename != \"\" {\n\t\t\treturn fmt.Errorf(\"qcow2 image %q has an unexpected backing file: %q\", source, info.BackingFilename)\n\t\t}\n\t}\n\n\tif err := convertToRaw(ctx, source, dest); err != nil {\n\t\treturn err\n\t}\n\n\tif size != nil {\n\t\tdestInfo, err := getInfo(ctx, dest)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get info for converted disk %q: %w\", dest, err)\n\t\t}\n\n\t\tif *size > destInfo.VSize {\n\t\t\treturn resizeDisk(ctx, dest, \"raw\", *size)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// AcceptableAsBaseDisk checks if a disk image is acceptable as a base disk.\nfunc AcceptableAsBaseDisk(info *Info) error {\n\tswitch info.Format {\n\tcase \"qcow2\", \"raw\":\n\t\t// NOP\n\tdefault:\n\t\tlogrus.WithField(\"filename\", info.Filename).\n\t\t\tWarnf(\"Unsupported image format %q. The image may not boot, or may have an extra privilege to access the host filesystem. Use with caution.\", info.Format)\n\t}\n\tif info.BackingFilename != \"\" {\n\t\treturn fmt.Errorf(\"base disk (%q) must not have a backing file (%q)\", info.Filename, info.BackingFilename)\n\t}\n\tif info.FullBackingFilename != \"\" {\n\t\treturn fmt.Errorf(\"base disk (%q) must not have a backing file (%q)\", info.Filename, info.FullBackingFilename)\n\t}\n\tif info.FormatSpecific != nil {\n\t\tif vmdk := info.FormatSpecific.Vmdk(); vmdk != nil {\n\t\t\tfor _, e := range vmdk.Extents {\n\t\t\t\tif e.Filename != info.Filename {\n\t\t\t\t\treturn fmt.Errorf(\"base disk (%q) must not have an extent file (%q)\", info.Filename, e.Filename)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t// info.Children is set since QEMU 8.0\n\tswitch len(info.Children) {\n\tcase 0:\n\t// NOP\n\tcase 1:\n\t\tif info.Filename != info.Children[0].Info.Filename {\n\t\t\treturn fmt.Errorf(\"base disk (%q) child must not have a different filename (%q)\", info.Filename, info.Children[0].Info.Filename)\n\t\t}\n\t\tif len(info.Children[0].Info.Children) > 0 {\n\t\t\treturn fmt.Errorf(\"base disk (%q) child must not have children of its own\", info.Filename)\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"base disk (%q) must not have multiple children: %+v\", info.Filename, info.Children)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/qemuimgutil/qemuimgutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage qemuimgutil\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestParseInfo(t *testing.T) {\n\tt.Run(\"qcow2\", func(t *testing.T) {\n\t\t// qemu-img create -f qcow2 foo.qcow2 4G\n\t\t// (QEMU 8.0)\n\t\tconst s = `{\n    \"children\": [\n        {\n            \"name\": \"file\",\n            \"info\": {\n                \"children\": [\n                ],\n                \"virtual-size\": 197120,\n                \"filename\": \"foo.qcow2\",\n                \"format\": \"file\",\n                \"actual-size\": 200704,\n                \"format-specific\": {\n                    \"type\": \"file\",\n                    \"data\": {\n                    }\n                },\n                \"dirty-flag\": false\n            }\n        }\n    ],\n    \"virtual-size\": 4294967296,\n    \"filename\": \"foo.qcow2\",\n    \"cluster-size\": 65536,\n    \"format\": \"qcow2\",\n    \"actual-size\": 200704,\n    \"format-specific\": {\n        \"type\": \"qcow2\",\n        \"data\": {\n            \"compat\": \"1.1\",\n            \"compression-type\": \"zlib\",\n            \"lazy-refcounts\": false,\n            \"refcount-bits\": 16,\n            \"corrupt\": false,\n            \"extended-l2\": false\n        }\n    },\n    \"dirty-flag\": false\n}`\n\n\t\tinfo, err := parseInfo([]byte(s))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, 1, len(info.Children))\n\t\tassert.Check(t, info.FormatSpecific != nil)\n\t\tqcow2 := info.FormatSpecific.Qcow2()\n\t\tassert.Check(t, qcow2 != nil)\n\t\tassert.Equal(t, qcow2.Compat, \"1.1\")\n\n\t\tt.Run(\"diff\", func(t *testing.T) {\n\t\t\t// qemu-img create -f qcow2 -F qcow2 -b foo.qcow2 bar.qcow2\n\t\t\t// (QEMU 8.0)\n\t\t\tconst s = `{\n    \"children\": [\n        {\n            \"name\": \"file\",\n            \"info\": {\n                \"children\": [\n                ],\n                \"virtual-size\": 197120,\n                \"filename\": \"bar.qcow2\",\n                \"format\": \"file\",\n                \"actual-size\": 200704,\n                \"format-specific\": {\n                    \"type\": \"file\",\n                    \"data\": {\n                    }\n                },\n                \"dirty-flag\": false\n            }\n        }\n    ],\n    \"backing-filename-format\": \"qcow2\",\n    \"virtual-size\": 4294967296,\n    \"filename\": \"bar.qcow2\",\n    \"cluster-size\": 65536,\n    \"format\": \"qcow2\",\n    \"actual-size\": 200704,\n    \"format-specific\": {\n        \"type\": \"qcow2\",\n        \"data\": {\n            \"compat\": \"1.1\",\n            \"compression-type\": \"zlib\",\n            \"lazy-refcounts\": false,\n            \"refcount-bits\": 16,\n            \"corrupt\": false,\n            \"extended-l2\": false\n        }\n    },\n    \"full-backing-filename\": \"foo.qcow2\",\n    \"backing-filename\": \"foo.qcow2\",\n    \"dirty-flag\": false\n}`\n\t\t\tinfo, err := parseInfo([]byte(s))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, 1, len(info.Children))\n\t\t\tassert.Equal(t, \"foo.qcow2\", info.BackingFilename)\n\t\t\tassert.Equal(t, \"bar.qcow2\", info.Filename)\n\t\t\tassert.Check(t, info.FormatSpecific != nil)\n\t\t\tqcow2 := info.FormatSpecific.Qcow2()\n\t\t\tassert.Check(t, qcow2 != nil)\n\t\t\tassert.Equal(t, qcow2.Compat, \"1.1\")\n\t\t})\n\t})\n\tt.Run(\"vmdk\", func(t *testing.T) {\n\t\tt.Run(\"twoGbMaxExtentSparse\", func(t *testing.T) {\n\t\t\t// qemu-img create -f vmdk foo.vmdk 4G -o subformat=twoGbMaxExtentSparse\n\t\t\t// (QEMU 8.0)\n\t\t\tconst s = `{\n    \"children\": [\n        {\n            \"name\": \"extents.1\",\n            \"info\": {\n                \"children\": [\n                ],\n                \"virtual-size\": 327680,\n                \"filename\": \"foo-s002.vmdk\",\n                \"format\": \"file\",\n                \"actual-size\": 327680,\n                \"format-specific\": {\n                    \"type\": \"file\",\n                    \"data\": {\n                    }\n                },\n                \"dirty-flag\": false\n            }\n        },\n        {\n            \"name\": \"extents.0\",\n            \"info\": {\n                \"children\": [\n                ],\n                \"virtual-size\": 327680,\n                \"filename\": \"foo-s001.vmdk\",\n                \"format\": \"file\",\n                \"actual-size\": 327680,\n                \"format-specific\": {\n                    \"type\": \"file\",\n                    \"data\": {\n                    }\n                },\n                \"dirty-flag\": false\n            }\n        },\n        {\n            \"name\": \"file\",\n            \"info\": {\n                \"children\": [\n                ],\n                \"virtual-size\": 512,\n                \"filename\": \"foo.vmdk\",\n                \"format\": \"file\",\n                \"actual-size\": 4096,\n                \"format-specific\": {\n                    \"type\": \"file\",\n                    \"data\": {\n                    }\n                },\n                \"dirty-flag\": false\n            }\n        }\n    ],\n    \"virtual-size\": 4294967296,\n    \"filename\": \"foo.vmdk\",\n    \"cluster-size\": 65536,\n    \"format\": \"vmdk\",\n    \"actual-size\": 659456,\n    \"format-specific\": {\n        \"type\": \"vmdk\",\n        \"data\": {\n            \"cid\": 918420663,\n            \"parent-cid\": 4294967295,\n            \"create-type\": \"twoGbMaxExtentSparse\",\n            \"extents\": [\n                {\n                    \"virtual-size\": 2147483648,\n                    \"filename\": \"foo-s001.vmdk\",\n                    \"cluster-size\": 65536,\n                    \"format\": \"SPARSE\"\n                },\n                {\n                    \"virtual-size\": 2147483648,\n                    \"filename\": \"foo-s002.vmdk\",\n                    \"cluster-size\": 65536,\n                    \"format\": \"SPARSE\"\n                }\n            ]\n        }\n    },\n    \"dirty-flag\": false\n}`\n\t\t\tinfo, err := parseInfo([]byte(s))\n\t\t\tassert.NilError(t, err)\n\t\t\tassert.Equal(t, 3, len(info.Children))\n\t\t\tassert.Equal(t, \"foo.vmdk\", info.Filename)\n\t\t\tassert.Check(t, info.FormatSpecific != nil)\n\t\t\tvmdk := info.FormatSpecific.Vmdk()\n\t\t\tassert.Check(t, vmdk != nil)\n\t\t\tassert.Equal(t, vmdk.CreateType, \"twoGbMaxExtentSparse\")\n\t\t})\n\t})\n}\n"
  },
  {
    "path": "pkg/reflectutil/reflectutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\n// This file has been adapted from https://github.com/containerd/nerdctl/blob/v1.0.0/pkg/reflectutil/reflectutil.go\n/*\n   Copyright The containerd Authors.\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n       http://www.apache.org/licenses/LICENSE-2.0\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n*/\n\npackage reflectutil\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\nfunc UnknownNonEmptyFields(structOrStructPtr any, knownNames ...string) []string {\n\tvar unknown []string\n\tknownNamesMap := make(map[string]struct{}, len(knownNames))\n\tfor _, name := range knownNames {\n\t\tknownNamesMap[name] = struct{}{}\n\t}\n\torigVal := reflect.ValueOf(structOrStructPtr)\n\tvar val reflect.Value\n\tswitch kind := origVal.Kind(); kind {\n\tcase reflect.Ptr:\n\t\tval = origVal.Elem()\n\tcase reflect.Struct:\n\t\tval = origVal\n\tdefault:\n\t\tpanic(fmt.Errorf(\"expected Ptr or Struct, got %+v\", kind))\n\t}\n\tfor i := range val.NumField() {\n\t\tiField := val.Field(i)\n\t\tif isEmpty(iField) {\n\t\t\tcontinue\n\t\t}\n\t\tiName := val.Type().Field(i).Name\n\t\tif _, ok := knownNamesMap[iName]; !ok {\n\t\t\tunknown = append(unknown, iName)\n\t\t}\n\t}\n\treturn unknown\n}\n\nfunc isEmpty(v reflect.Value) bool {\n\t// NOTE: IsZero returns false for zero-length map and slice\n\tif v.IsZero() {\n\t\treturn true\n\t}\n\tswitch v.Kind() {\n\tcase reflect.Map, reflect.Slice:\n\t\treturn v.Len() == 0\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/registry/registry.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage registry\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/driver/external/client\"\n\t\"github.com/lima-vm/lima/v2/pkg/usrlocal\"\n)\n\nconst (\n\tInternal = \"internal\"\n\tExternal = \"external\"\n)\n\ntype ExternalDriver struct {\n\tName         string\n\tInstanceName string\n\tCommand      *exec.Cmd\n\tSocketPath   string\n\tClient       *client.DriverClient // Client is the gRPC client for the external driver\n\tPath         string\n\tCtx          context.Context\n\tLogger       *logrus.Logger\n\tCancelFunc   context.CancelFunc\n}\n\nvar (\n\tinternalDrivers = make(map[string]driver.Driver)\n\tExternalDrivers = make(map[string]*ExternalDriver)\n)\n\nfunc List() map[string]string {\n\tif err := discoverDrivers(); err != nil {\n\t\tlogrus.Warnf(\"Error discovering drivers: %v\", err)\n\t}\n\n\tvmTypes := make(map[string]string)\n\tfor name := range internalDrivers {\n\t\tvmTypes[name] = Internal\n\t}\n\tfor name, d := range ExternalDrivers {\n\t\tvmTypes[name] = d.Path\n\t}\n\n\treturn vmTypes\n}\n\nfunc CheckInternalOrExternal(name string) string {\n\textDriver, _, exists := Get(name)\n\tif !exists {\n\t\treturn \"\"\n\t}\n\tif extDriver != nil {\n\t\treturn External\n\t}\n\n\treturn Internal\n}\n\nfunc Get(name string) (*ExternalDriver, driver.Driver, bool) {\n\tinternalDriver, exists := internalDrivers[name]\n\tif !exists {\n\t\tif err := discoverDrivers(); err != nil {\n\t\t\tlogrus.Warnf(\"Error discovering drivers: %v\", err)\n\t\t}\n\t\texternalDriver, exists := ExternalDrivers[name]\n\t\tif exists {\n\t\t\treturn externalDriver, nil, exists\n\t\t}\n\t}\n\treturn nil, internalDriver, exists\n}\n\nfunc registerExternalDriver(name, path string) {\n\tif _, exists := ExternalDrivers[name]; exists {\n\t\treturn\n\t}\n\n\tif _, exists := internalDrivers[name]; exists {\n\t\tlogrus.Debugf(\"Driver %q is already registered as an internal driver, skipping external registration\", name)\n\t\treturn\n\t}\n\n\tExternalDrivers[name] = &ExternalDriver{\n\t\tName:   name,\n\t\tPath:   path,\n\t\tLogger: logrus.New(),\n\t}\n}\n\nfunc discoverDrivers() error {\n\tif driverPaths := os.Getenv(\"LIMA_DRIVERS_PATH\"); driverPaths != \"\" {\n\t\tpaths := filepath.SplitList(driverPaths)\n\t\tfor _, path := range paths {\n\t\t\tif path == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tinfo, err := os.Stat(path)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Warnf(\"Error accessing external driver path %q: %v\", path, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif info.IsDir() {\n\t\t\t\tif err := discoverDriversInDir(path); err != nil {\n\t\t\t\t\tlogrus.Warnf(\"Error discovering external drivers in %q: %v\", path, err)\n\t\t\t\t}\n\t\t\t} else if isExecutable(info.Mode()) {\n\t\t\t\tregisterDriverFile(path)\n\t\t\t}\n\t\t}\n\t}\n\n\tstdDriverDirs, err := usrlocal.LibexecLima()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogrus.Debugf(\"Discovering external drivers in %v\", stdDriverDirs)\n\tfor _, dir := range stdDriverDirs {\n\t\tif _, err := os.Stat(dir); err == nil {\n\t\t\tif err := discoverDriversInDir(dir); err != nil {\n\t\t\t\tlogrus.Warnf(\"Error discovering external drivers in %q: %v\", dir, err)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc discoverDriversInDir(dir string) error {\n\tentries, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read driver directory %q: %w\", dir, err)\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tinfo, err := entry.Info()\n\t\tif err != nil {\n\t\t\tlogrus.Warnf(\"Failed to get info for %q: %v\", entry.Name(), err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif !isExecutable(info.Mode()) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdriverPath := filepath.Join(dir, entry.Name())\n\t\tregisterDriverFile(driverPath)\n\t}\n\n\treturn nil\n}\n\nfunc registerDriverFile(path string) {\n\tbase := filepath.Base(path)\n\tname := \"\"\n\tif runtime.GOOS == \"windows\" {\n\t\tif strings.HasPrefix(base, \"lima-driver-\") && strings.HasSuffix(base, \".exe\") {\n\t\t\tname = strings.TrimSuffix(strings.TrimPrefix(base, \"lima-driver-\"), \".exe\")\n\t\t}\n\t} else {\n\t\tif strings.HasPrefix(base, \"lima-driver-\") && !strings.HasSuffix(base, \".exe\") {\n\t\t\tname = strings.TrimPrefix(base, \"lima-driver-\")\n\t\t}\n\t}\n\tif name == \"\" {\n\t\treturn\n\t}\n\tregisterExternalDriver(name, path)\n}\n\nfunc isExecutable(mode os.FileMode) bool {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn true\n\t}\n\treturn mode&0o111 != 0\n}\n\nfunc Register(driver driver.Driver) {\n\tname := driver.Info().Name\n\tif _, exists := internalDrivers[name]; exists {\n\t\treturn\n\t}\n\tinternalDrivers[name] = driver\n}\n"
  },
  {
    "path": "pkg/registry/registry_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage registry\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driver\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\ntype mockDriver struct {\n\tName string\n}\n\nfunc newMockDriver(name string) *mockDriver {\n\treturn &mockDriver{Name: name}\n}\n\nvar _ driver.Driver = (*mockDriver)(nil)\n\nfunc (m *mockDriver) Validate(_ context.Context) error                           { return nil }\nfunc (m *mockDriver) Create(_ context.Context) error                             { return nil }\nfunc (m *mockDriver) Delete(_ context.Context) error                             { return nil }\nfunc (m *mockDriver) CreateDisk(_ context.Context) error                         { return nil }\nfunc (m *mockDriver) Start(_ context.Context) (chan error, error)                { return nil, nil }\nfunc (m *mockDriver) Stop(_ context.Context) error                               { return nil }\nfunc (m *mockDriver) RunGUI() error                                              { return nil }\nfunc (m *mockDriver) ChangeDisplayPassword(_ context.Context, _ string) error    { return nil }\nfunc (m *mockDriver) DisplayConnection(_ context.Context) (string, error)        { return \"\", nil }\nfunc (m *mockDriver) CreateSnapshot(_ context.Context, _ string) error           { return nil }\nfunc (m *mockDriver) ApplySnapshot(_ context.Context, _ string) error            { return nil }\nfunc (m *mockDriver) DeleteSnapshot(_ context.Context, _ string) error           { return nil }\nfunc (m *mockDriver) ListSnapshots(_ context.Context) (string, error)            { return \"\", nil }\nfunc (m *mockDriver) Register(_ context.Context) error                           { return nil }\nfunc (m *mockDriver) Unregister(_ context.Context) error                         { return nil }\nfunc (m *mockDriver) ForwardGuestAgent() bool                                    { return false }\nfunc (m *mockDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) { return nil, \"\", nil }\nfunc (m *mockDriver) Info() driver.Info {\n\treturn driver.Info{Name: m.Name}\n}\nfunc (m *mockDriver) Configure(_ *limatype.Instance) *driver.ConfiguredDriver            { return nil }\nfunc (m *mockDriver) FillConfig(_ context.Context, _ *limatype.LimaYAML, _ string) error { return nil }\nfunc (m *mockDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string       { return \"\" }\nfunc (m *mockDriver) SSHAddress(_ context.Context) (string, error)                       { return \"\", nil }\nfunc (m *mockDriver) BootScripts() (map[string][]byte, error)                            { return nil, nil }\nfunc (m *mockDriver) AdditionalSetupForSSH(_ context.Context) error                      { return nil }\n\nfunc TestRegister(t *testing.T) {\n\tBackupRegistry(t)\n\n\tmockDrv := newMockDriver(\"test-driver\")\n\tmockDrv2 := newMockDriver(\"test-driver-2\")\n\tRegister(mockDrv)\n\tRegister(mockDrv2)\n\n\tassert.Equal(t, len(internalDrivers), 2)\n\tassert.Equal(t, internalDrivers[\"test-driver\"], mockDrv)\n\tassert.Equal(t, internalDrivers[\"test-driver-2\"], mockDrv2)\n\n\t// Test registering duplicate driver (should not overwrite)\n\tmockDrv3 := newMockDriver(\"test-driver\")\n\tRegister(mockDrv3)\n\n\tassert.Equal(t, len(internalDrivers), 2)\n\tassert.Equal(t, internalDrivers[\"test-driver\"], mockDrv)\n\n\tdriverType := CheckInternalOrExternal(\"test-driver\")\n\tassert.Equal(t, driverType, Internal)\n\n\textDriver, intDriver, exists := Get(\"test-driver\")\n\tassert.Equal(t, exists, true)\n\tassert.Assert(t, extDriver == nil)\n\tassert.Assert(t, intDriver != nil)\n\tassert.Equal(t, intDriver.Info().Name, \"test-driver\")\n\n\tvmTypes := List()\n\tassert.Equal(t, vmTypes[\"test-driver-2\"], Internal)\n}\n\nfunc TestDiscoverDriversInDir(t *testing.T) {\n\tBackupRegistry(t)\n\n\ttempDir := t.TempDir()\n\n\tvar driverPath string\n\tdriverName := \"mockext\"\n\tif runtime.GOOS == \"windows\" {\n\t\tdriverPath = filepath.Join(tempDir, \"lima-driver-\"+driverName+\".exe\")\n\t} else {\n\t\tdriverPath = filepath.Join(tempDir, \"lima-driver-\"+driverName)\n\t}\n\n\terr := os.WriteFile(driverPath, []byte(\"\"), 0o755)\n\tassert.NilError(t, err)\n\n\terr = discoverDriversInDir(tempDir)\n\tassert.NilError(t, err)\n\n\tassert.Equal(t, len(ExternalDrivers), 1)\n\textDriver := ExternalDrivers[driverName]\n\tassert.Assert(t, extDriver != nil)\n\tassert.Equal(t, extDriver.Name, driverName)\n\tassert.Equal(t, extDriver.Path, driverPath)\n\n\tdriverType := CheckInternalOrExternal(driverName)\n\tassert.Equal(t, driverType, External)\n\n\textDriver, intDriver, exists := Get(driverName)\n\tassert.Equal(t, exists, true)\n\tassert.Assert(t, extDriver != nil)\n\tassert.Assert(t, intDriver == nil)\n\tassert.Equal(t, extDriver.Name, driverName)\n\n\tvmTypes := List()\n\tassert.Equal(t, vmTypes[driverName], driverPath)\n}\n\nfunc TestRegisterDriverFile(t *testing.T) {\n\tBackupRegistry(t)\n\n\ttests := []struct {\n\t\tname         string\n\t\tfilename     string\n\t\texpectDriver bool\n\t\texpectedName string\n\t}{\n\t\t{\n\t\t\tname:         \"valid driver file\",\n\t\t\tfilename:     \"lima-driver-test\",\n\t\t\texpectDriver: runtime.GOOS != \"windows\",\n\t\t\texpectedName: \"test\",\n\t\t},\n\t\t{\n\t\t\tname:         \"valid driver file with extension on Windows\",\n\t\t\tfilename:     \"lima-driver-windows.exe\",\n\t\t\texpectDriver: runtime.GOOS == \"windows\",\n\t\t\texpectedName: \"windows\",\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid filename - no prefix\",\n\t\t\tfilename:     \"not-a-driver\",\n\t\t\texpectDriver: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"invalid filename - wrong prefix\",\n\t\t\tfilename:     \"driver-lima-test\",\n\t\t\texpectDriver: false,\n\t\t},\n\t\t{\n\t\t\tname:         \"empty name after prefix\",\n\t\t\tfilename:     \"lima-driver-\",\n\t\t\texpectDriver: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tExternalDrivers = make(map[string]*ExternalDriver)\n\t\t\tregisterDriverFile(filepath.Join(\"/test/path\", tt.filename))\n\n\t\t\tif tt.expectDriver {\n\t\t\t\tassert.Equal(t, len(ExternalDrivers), 1)\n\t\t\t\textDriver := ExternalDrivers[tt.expectedName]\n\t\t\t\tassert.Assert(t, extDriver != nil)\n\t\t\t\tassert.Equal(t, extDriver.Name, tt.expectedName)\n\t\t\t\tassert.Equal(t, extDriver.Path, filepath.Join(\"/test/path\", tt.filename))\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, len(ExternalDrivers), 0)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\tBackupRegistry(t)\n\n\tmockDrv := newMockDriver(\"internal-test\")\n\tRegister(mockDrv)\n\n\textDriver, intDriver, exists := Get(\"internal-test\")\n\tassert.Equal(t, exists, true)\n\tassert.Assert(t, extDriver == nil)\n\tassert.Equal(t, intDriver, mockDrv)\n\n\tregisterExternalDriver(\"external-test\", \"/path/to/external\")\n\n\textDriver, intDriver, exists = Get(\"external-test\")\n\tassert.Equal(t, exists, true)\n\tassert.Assert(t, extDriver != nil)\n\tassert.Assert(t, intDriver == nil)\n\tassert.Equal(t, extDriver.Name, \"external-test\")\n\n\textDriver, intDriver, exists = Get(\"non-existent\")\n\tassert.Equal(t, exists, false)\n\tassert.Assert(t, extDriver == nil)\n\tassert.Assert(t, intDriver == nil)\n}\n\nfunc TestList(t *testing.T) {\n\tBackupRegistry(t)\n\n\tvmTypes := List()\n\tassert.Equal(t, len(vmTypes), 0)\n\n\tmockDrv := newMockDriver(\"internal-test\")\n\tRegister(mockDrv)\n\n\tvmTypes = List()\n\tassert.Equal(t, len(vmTypes), 1)\n\tassert.Equal(t, vmTypes[\"internal-test\"], Internal)\n\n\tregisterExternalDriver(\"external-test\", \"/path/to/external\")\n\n\tvmTypes = List()\n\tassert.Equal(t, len(vmTypes), 2)\n\tassert.Equal(t, vmTypes[\"internal-test\"], Internal)\n\tassert.Equal(t, vmTypes[\"external-test\"], \"/path/to/external\")\n}\n\nfunc BackupRegistry(t *testing.T) {\n\toriginalExternalDrivers := ExternalDrivers\n\toriginalInternalDrivers := internalDrivers\n\tt.Cleanup(func() {\n\t\tExternalDrivers = originalExternalDrivers\n\t\tinternalDrivers = originalInternalDrivers\n\t})\n\n\tinternalDrivers = make(map[string]driver.Driver)\n\tExternalDrivers = make(map[string]*ExternalDriver)\n}\n"
  },
  {
    "path": "pkg/snapshot/snapshot.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage snapshot\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nfunc Del(ctx context.Context, inst *limatype.Instance, tag string) error {\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\n\treturn limaDriver.DeleteSnapshot(ctx, tag)\n}\n\nfunc Save(ctx context.Context, inst *limatype.Instance, tag string) error {\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\treturn limaDriver.CreateSnapshot(ctx, tag)\n}\n\nfunc Load(ctx context.Context, inst *limatype.Instance, tag string) error {\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\treturn limaDriver.ApplySnapshot(ctx, tag)\n}\n\nfunc List(ctx context.Context, inst *limatype.Instance) (string, error) {\n\tlimaDriver, err := driverutil.CreateConfiguredDriver(inst, 0)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create driver instance: %w\", err)\n\t}\n\n\treturn limaDriver.ListSnapshots(ctx)\n}\n"
  },
  {
    "path": "pkg/sshutil/format.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sshutil\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/instance/hostname\"\n)\n\n// FormatT specifies the format type.\ntype FormatT = string\n\nconst (\n\t// FormatCmd prints the full ssh command line.\n\t//\n\t//\tssh -o IdentityFile=\"/Users/example/.lima/_config/user\" -o User=example -o Hostname=127.0.0.1 -o Port=60022 lima-default\n\tFormatCmd = FormatT(\"cmd\")\n\n\t// FormatArgs is similar to FormatCmd but omits \"ssh\" and the destination address.\n\t//\n\t//\t-o IdentityFile=\"/Users/example/.lima/_config/user\" -o User=example -o Hostname=127.0.0.1 -o Port=60022\n\tFormatArgs = FormatT(\"args\")\n\n\t// FormatOptions prints the ssh option key value pairs.\n\t//\n\t//\tIdentityFile=\"/Users/example/.lima/_config/user\"\n\t//\tUser=example\n\t//\tHostname=127.0.0.1\n\t//\tPort=60022\n\tFormatOptions = FormatT(\"options\")\n\n\t// FormatConfig uses the ~/.ssh/config format\n\t//\n\t//\tHost lima-default\n\t//\t  IdentityFile \"/Users/example/.lima/_config/user \"\n\t//\t  User example\n\t//\t  Hostname 127.0.0.1\n\t//\t  Port 60022\n\tFormatConfig = FormatT(\"config\")\n\n\t// TODO: consider supporting \"url\" format (ssh://USER@HOSTNAME:PORT).\n\t//\n\t// TODO: consider supporting \"json\" format.\n\t// It is unclear whether we can just map ssh \"config\" into JSON, as \"config\" has duplicated keys.\n\t// (JSON supports duplicated keys too, but not all JSON implementations expect JSON with duplicated keys).\n)\n\n// Formats is the list of the supported formats.\nvar Formats = []FormatT{FormatCmd, FormatArgs, FormatOptions, FormatConfig}\n\nfunc quoteOption(o string) string {\n\t// make sure the shell doesn't swallow quotes in option values\n\tif strings.ContainsRune(o, '\"') {\n\t\to = \"'\" + o + \"'\"\n\t}\n\treturn o\n}\n\n// Format formats the ssh options.\nfunc Format(w io.Writer, sshPath, instName string, format FormatT, opts []string) error {\n\tfakeHostname := hostname.FromInstName(instName) // TODO: support customization\n\tswitch format {\n\tcase FormatCmd:\n\t\targs := []string{sshPath}\n\t\tfor _, o := range opts {\n\t\t\targs = append(args, \"-o\", quoteOption(o))\n\t\t}\n\t\targs = append(args, fakeHostname)\n\t\t// the args are similar to `limactl shell` but not exactly same. (e.g., lacks -t)\n\t\tfmt.Fprintln(w, strings.Join(args, \" \")) // no need to use shellescape.QuoteCommand\n\tcase FormatArgs:\n\t\tvar args []string\n\t\tfor _, o := range opts {\n\t\t\targs = append(args, \"-o\", quoteOption(o))\n\t\t}\n\t\tfmt.Fprintln(w, strings.Join(args, \" \")) // no need to use shellescape.QuoteCommand\n\tcase FormatOptions:\n\t\tfor _, o := range opts {\n\t\t\tfmt.Fprintln(w, o)\n\t\t}\n\tcase FormatConfig:\n\t\tfmt.Fprintf(w, \"Host %s\\n\", fakeHostname)\n\t\tfor _, o := range opts {\n\t\t\tkv := strings.SplitN(o, \"=\", 2)\n\t\t\tif len(kv) != 2 {\n\t\t\t\treturn fmt.Errorf(\"unexpected option %q\", o)\n\t\t\t}\n\t\t\tfmt.Fprintf(w, \"  %s %s\\n\", kv[0], kv[1])\n\t\t}\n\tdefault:\n\t\treturn fmt.Errorf(\"unknown format: %q\", format)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/sshutil/sshutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sshutil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/mattn/go-shellwords\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/cpu\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/ioutilx\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/lockutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/osutil\"\n)\n\n// Environment variable that allows configuring the command (alias) to execute\n// in place of the 'ssh' executable.\nconst EnvShellSSH = \"SSH\"\n\ntype SSHExe struct {\n\tExe  string\n\tArgs []string\n}\n\nfunc NewSSHExe() (SSHExe, error) {\n\tvar sshExe SSHExe\n\n\tif sshShell := os.Getenv(EnvShellSSH); sshShell != \"\" {\n\t\tsshShellFields, err := shellwords.Parse(sshShell)\n\t\tswitch {\n\t\tcase err != nil:\n\t\t\tlogrus.WithError(err).Warnf(\"Failed to split %s variable into shell tokens. \"+\n\t\t\t\t\"Falling back to 'ssh' command\", EnvShellSSH)\n\t\tcase len(sshShellFields) > 0:\n\t\t\tsshExe.Exe = sshShellFields[0]\n\t\t\tif len(sshShellFields) > 1 {\n\t\t\t\tsshExe.Args = sshShellFields[1:]\n\t\t\t}\n\t\t\treturn sshExe, nil\n\t\t}\n\t}\n\n\texecutable, err := exec.LookPath(\"ssh\")\n\tif err != nil {\n\t\treturn SSHExe{}, err\n\t}\n\tsshExe.Exe = executable\n\n\treturn sshExe, nil\n}\n\ntype PubKey struct {\n\tFilename string\n\tContent  string\n}\n\nfunc readPublicKey(f string) (PubKey, error) {\n\tentry := PubKey{\n\t\tFilename: f,\n\t}\n\tcontent, err := os.ReadFile(f)\n\tif err == nil {\n\t\tentry.Content = strings.TrimSpace(string(content))\n\t} else {\n\t\terr = fmt.Errorf(\"failed to read ssh public key %q: %w\", f, err)\n\t}\n\treturn entry, err\n}\n\n// DefaultPubKeys returns the public key from $LIMA_HOME/_config/user.pub.\n// The key will be created if it does not yet exist.\n//\n// When loadDotSSH is true, ~/.ssh/*.pub will be appended to make the VM accessible without specifying\n// an identity explicitly.\nfunc DefaultPubKeys(ctx context.Context, loadDotSSH bool) ([]PubKey, error) {\n\t// Read $LIMA_HOME/_config/user.pub\n\tconfigDir, err := dirnames.LimaConfigDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t_, err = os.Stat(filepath.Join(configDir, filenames.UserPrivateKey))\n\tif err != nil {\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := os.MkdirAll(configDir, 0o700); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not create %q directory: %w\", configDir, err)\n\t\t}\n\t\tif err := lockutil.WithDirLock(configDir, func() error {\n\t\t\t// no passphrase, no user@host comment\n\t\t\tprivPath := filepath.Join(configDir, filenames.UserPrivateKey)\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\tprivPath, err = ioutilx.WindowsSubsystemPath(ctx, privPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tkeygenCmd := exec.CommandContext(ctx, \"ssh-keygen\", \"-t\", \"ed25519\", \"-q\", \"-N\", \"\",\n\t\t\t\t\"-C\", \"lima\", \"-f\", privPath)\n\t\t\tlogrus.Debugf(\"executing %v\", keygenCmd.Args)\n\t\t\tif out, err := keygenCmd.CombinedOutput(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to run %v: %q: %w\", keygenCmd.Args, string(out), err)\n\t\t\t}\n\t\t\treturn nil\n\t\t}); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tentry, err := readPublicKey(filepath.Join(configDir, filenames.UserPublicKey))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := []PubKey{entry}\n\n\tif !loadDotSSH {\n\t\treturn res, nil\n\t}\n\n\t// Append all of ~/.ssh/*.pub\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfiles, err := filepath.Glob(filepath.Join(homeDir, \".ssh/*.pub\"))\n\tif err != nil {\n\t\tpanic(err) // Only possible error is ErrBadPattern, so this should be unreachable.\n\t}\n\tfor _, f := range files {\n\t\tif !strings.HasSuffix(f, \".pub\") {\n\t\t\tpanic(fmt.Errorf(\"unexpected ssh public key filename %q\", f))\n\t\t}\n\t\tentry, err := readPublicKey(f)\n\t\tif err == nil {\n\t\t\tif !detectValidPublicKey(entry.Content) {\n\t\t\t\tlogrus.Warnf(\"public key %q doesn't seem to be in ssh format\", entry.Filename)\n\t\t\t} else {\n\t\t\t\tres = append(res, entry)\n\t\t\t}\n\t\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn res, nil\n}\n\ntype openSSHInfo struct {\n\t// Version is set to the version of OpenSSH, or semver.New(\"0.0.0\") if the version cannot be determined.\n\tVersion semver.Version\n\n\t// Some distributions omit this feature by default, for example, Alpine, NixOS.\n\tGSSAPISupported bool\n}\n\nvar sshInfo struct {\n\tsync.Once\n\t// aesAccelerated is set to true when AES acceleration is available.\n\t// Available on almost all modern Intel/AMD processors.\n\taesAccelerated bool\n\n\t// OpenSSH executable information for the version and supported options.\n\topenSSH openSSHInfo\n}\n\n// CommonOpts returns ssh option key-value pairs like {\"IdentityFile=/path/to/id_foo\"}.\n// The result may contain different values with the same key.\n//\n// The result always contains the IdentityFile option.\n// The result never contains the Port option.\nfunc CommonOpts(ctx context.Context, sshExe SSHExe, useDotSSH bool) ([]string, error) {\n\tconfigDir, err := dirnames.LimaConfigDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tprivateKeyPath := filepath.Join(configDir, filenames.UserPrivateKey)\n\t_, err = os.Stat(privateKeyPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar opts []string\n\tidf, err := identityFileEntry(ctx, privateKeyPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\topts = []string{idf}\n\n\t// Append all private keys corresponding to ~/.ssh/*.pub to keep old instances working\n\t// that had been created before lima started using an internal identity.\n\tif useDotSSH {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfiles, err := filepath.Glob(filepath.Join(homeDir, \".ssh/*.pub\"))\n\t\tif err != nil {\n\t\t\tpanic(err) // Only possible error is ErrBadPattern, so this should be unreachable.\n\t\t}\n\t\tfor _, f := range files {\n\t\t\tif !strings.HasSuffix(f, \".pub\") {\n\t\t\t\tpanic(fmt.Errorf(\"unexpected ssh public key filename %q\", f))\n\t\t\t}\n\t\t\tprivateKeyPath := strings.TrimSuffix(f, \".pub\")\n\t\t\t_, err = os.Stat(privateKeyPath)\n\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t// Skip .pub files without a matching private key. This is reasonably common,\n\t\t\t\t// due to major projects like Vault recommending the ${name}-cert.pub format\n\t\t\t\t// for SSH certificate files.\n\t\t\t\t//\n\t\t\t\t// e.g. https://www.vaultproject.io/docs/secrets/ssh/signed-ssh-certificates\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\t// Fail on permission-related and other path errors\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tidf, err = identityFileEntry(ctx, privateKeyPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\topts = append(opts, idf)\n\t\t}\n\t}\n\n\topts = append(opts,\n\t\t\"StrictHostKeyChecking=no\",\n\t\t\"UserKnownHostsFile=/dev/null\",\n\t\t\"NoHostAuthenticationForLocalhost=yes\",\n\t\t\"PreferredAuthentications=publickey\",\n\t\t\"Compression=no\",\n\t\t\"BatchMode=yes\",\n\t\t\"IdentitiesOnly=yes\",\n\t)\n\n\tsshInfo.Do(func() {\n\t\tsshInfo.aesAccelerated = detectAESAcceleration()\n\t\tsshInfo.openSSH = detectOpenSSHInfo(ctx, sshExe)\n\t})\n\n\tif sshInfo.openSSH.GSSAPISupported {\n\t\topts = append(opts, \"GSSAPIAuthentication=no\")\n\t}\n\n\t// Only OpenSSH version 8.1 and later support adding ciphers to the front of the default set\n\tif !sshInfo.openSSH.Version.LessThan(*semver.New(\"8.1.0\")) {\n\t\t// By default, `ssh` choose chacha20-poly1305@openssh.com, even when AES accelerator is available.\n\t\t// (OpenSSH_8.1p1, macOS 11.6, MacBookPro 2020, Core i7-1068NG7)\n\t\t//\n\t\t// We prioritize AES algorithms when AES accelerator is available.\n\t\tif sshInfo.aesAccelerated {\n\t\t\tlogrus.Debugf(\"AES accelerator seems available, prioritizing aes128-gcm@openssh.com and aes256-gcm@openssh.com\")\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\topts = append(opts, \"Ciphers=^aes128-gcm@openssh.com,aes256-gcm@openssh.com\")\n\t\t\t} else {\n\t\t\t\topts = append(opts, \"Ciphers=\\\"^aes128-gcm@openssh.com,aes256-gcm@openssh.com\\\"\")\n\t\t\t}\n\t\t} else {\n\t\t\tlogrus.Debugf(\"AES accelerator does not seem available, prioritizing chacha20-poly1305@openssh.com\")\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\topts = append(opts, \"Ciphers=^chacha20-poly1305@openssh.com\")\n\t\t\t} else {\n\t\t\t\topts = append(opts, \"Ciphers=\\\"^chacha20-poly1305@openssh.com\\\"\")\n\t\t\t}\n\t\t}\n\t}\n\treturn opts, nil\n}\n\nfunc identityFileEntry(ctx context.Context, privateKeyPath string) (string, error) {\n\tif runtime.GOOS == \"windows\" {\n\t\tprivateKeyPath, err := ioutilx.WindowsSubsystemPath(ctx, privateKeyPath)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn fmt.Sprintf(`IdentityFile='%s'`, privateKeyPath), nil\n\t}\n\treturn fmt.Sprintf(`IdentityFile=\"%s\"`, privateKeyPath), nil\n}\n\n// DisableControlMasterOptsFromSSHArgs returns ssh args that disable ControlMaster, ControlPath, and ControlPersist.\nfunc DisableControlMasterOptsFromSSHArgs(sshArgs []string) []string {\n\targsForOverridingConfigFile := []string{\n\t\t\"-o\", \"ControlMaster=no\",\n\t\t\"-o\", \"ControlPath=none\",\n\t\t\"-o\", \"ControlPersist=no\",\n\t}\n\treturn slices.Concat(argsForOverridingConfigFile, removeOptsFromSSHArgs(sshArgs, \"ControlMaster\", \"ControlPath\", \"ControlPersist\"))\n}\n\nfunc removeOptsFromSSHArgs(sshArgs []string, removeOpts ...string) []string {\n\tres := make([]string, 0, len(sshArgs))\n\tisOpt := false\n\tfor _, arg := range sshArgs {\n\t\tif isOpt {\n\t\t\tisOpt = false\n\t\t\tif !slices.ContainsFunc(removeOpts, func(opt string) bool {\n\t\t\t\treturn strings.HasPrefix(arg, opt)\n\t\t\t}) {\n\t\t\t\tres = append(res, \"-o\", arg)\n\t\t\t}\n\t\t} else if arg == \"-o\" {\n\t\t\tisOpt = true\n\t\t} else {\n\t\t\tres = append(res, arg)\n\t\t}\n\t}\n\treturn res\n}\n\n// IsControlMasterExisting returns true if the control socket file exists.\nfunc IsControlMasterExisting(instDir string) bool {\n\tcontrolSock := filepath.Join(instDir, filenames.SSHSock)\n\t_, err := os.Stat(controlSock)\n\treturn err == nil\n}\n\n// SSHOpts adds the following options to CommonOptions: User, ControlMaster, ControlPath, ControlPersist.\nfunc SSHOpts(ctx context.Context, sshExe SSHExe, instDir, username string, useDotSSH, forwardAgent, forwardX11, forwardX11Trusted bool) ([]string, error) {\n\tcontrolSock := filepath.Join(instDir, filenames.SSHSock)\n\tif len(controlSock) >= osutil.UnixPathMax {\n\t\treturn nil, fmt.Errorf(\"socket path %q is too long: >= UNIX_PATH_MAX=%d\", controlSock, osutil.UnixPathMax)\n\t}\n\topts, err := CommonOpts(ctx, sshExe, useDotSSH)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcontrolPath := fmt.Sprintf(`ControlPath=\"%s\"`, controlSock)\n\tif runtime.GOOS == \"windows\" {\n\t\tcontrolSock, err = ioutilx.WindowsSubsystemPath(ctx, controlSock)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcontrolPath = fmt.Sprintf(`ControlPath='%s'`, controlSock)\n\t}\n\topts = append(opts,\n\t\tfmt.Sprintf(\"User=%s\", username), // guest and host have the same username, but we should specify the username explicitly (#85)\n\t\t\"ControlMaster=auto\",\n\t\tcontrolPath,\n\t\t\"ControlPersist=yes\",\n\t)\n\tif forwardAgent {\n\t\topts = append(opts, \"ForwardAgent=yes\")\n\t}\n\tif forwardX11 {\n\t\topts = append(opts, \"ForwardX11=yes\")\n\t}\n\tif forwardX11Trusted {\n\t\topts = append(opts, \"ForwardX11Trusted=yes\")\n\t}\n\treturn opts, nil\n}\n\n// SSHArgsFromOpts returns ssh args from opts.\n// The result always contains {\"-F\", \"/dev/null} in addition to {\"-o\", \"KEY=VALUE\", ...}.\nfunc SSHArgsFromOpts(opts []string) []string {\n\targs := []string{\"-F\", \"/dev/null\"}\n\tfor _, o := range opts {\n\t\targs = append(args, \"-o\", o)\n\t}\n\treturn args\n}\n\n// SSHOptsRemovingControlPath removes ControlMaster, ControlPath, and ControlPersist options from SSH options.\nfunc SSHOptsRemovingControlPath(opts []string) []string {\n\t// Create a copy of opts to avoid modifying the original slice, since slices.DeleteFunc modifies the slice in place.\n\tcopiedOpts := slices.Clone(opts)\n\treturn slices.DeleteFunc(copiedOpts, func(s string) bool {\n\t\treturn strings.HasPrefix(s, \"ControlMaster\") || strings.HasPrefix(s, \"ControlPath\") || strings.HasPrefix(s, \"ControlPersist\")\n\t})\n}\n\nfunc ParseOpenSSHVersion(version []byte) *semver.Version {\n\tregex := regexp.MustCompile(`(?m)^OpenSSH_(\\d+\\.\\d+)(?:p(\\d+))?\\b`)\n\tmatches := regex.FindSubmatch(version)\n\tif len(matches) == 3 {\n\t\tif len(matches[2]) == 0 {\n\t\t\tmatches[2] = []byte(\"0\")\n\t\t}\n\t\treturn semver.New(fmt.Sprintf(\"%s.%s\", matches[1], matches[2]))\n\t}\n\treturn &semver.Version{}\n}\n\nfunc parseOpenSSHGSSAPISupported(version string) bool {\n\treturn !strings.Contains(version, `Unsupported option \"gssapiauthentication\"`)\n}\n\n// sshExecutable beyond path also records size and mtime, in the case of ssh upgrades.\ntype sshExecutable struct {\n\tPath    string\n\tSize    int64\n\tModTime time.Time\n}\n\nvar (\n\t// openSSHInfos caches the parsed version and supported options of each ssh executable, if it is needed again.\n\topenSSHInfos   = map[sshExecutable]*openSSHInfo{}\n\topenSSHInfosRW sync.RWMutex\n)\n\nfunc detectOpenSSHInfo(ctx context.Context, sshExe SSHExe) openSSHInfo {\n\tvar (\n\t\tinfo   openSSHInfo\n\t\texe    sshExecutable\n\t\tstderr bytes.Buffer\n\t)\n\t// Note: For SSH wrappers like \"kitten ssh\", os.Stat will check the wrapper\n\t// executable (kitten) instead of the underlying ssh binary. This means\n\t// cache invalidation won't work properly - ssh upgrades won't be detected\n\t// since kitten's size/mtime won't change. This is probably acceptable.\n\tif st, err := os.Stat(sshExe.Exe); err == nil {\n\t\texe = sshExecutable{Path: sshExe.Exe, Size: st.Size(), ModTime: st.ModTime()}\n\t\topenSSHInfosRW.RLock()\n\t\tinfo := openSSHInfos[exe]\n\t\topenSSHInfosRW.RUnlock()\n\t\tif info != nil {\n\t\t\treturn *info\n\t\t}\n\t}\n\tsshArgs := append([]string{}, sshExe.Args...)\n\t// -V should be last\n\tsshArgs = append(sshArgs, \"-o\", \"GSSAPIAuthentication=no\", \"-V\")\n\tcmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...)\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\tlogrus.Warnf(\"failed to run %v: stderr=%q\", cmd.Args, stderr.String())\n\t} else {\n\t\tinfo = openSSHInfo{\n\t\t\tVersion:         *ParseOpenSSHVersion(stderr.Bytes()),\n\t\t\tGSSAPISupported: parseOpenSSHGSSAPISupported(stderr.String()),\n\t\t}\n\t\tlogrus.Debugf(\"OpenSSH version %s detected, is GSSAPI supported: %t\", info.Version, info.GSSAPISupported)\n\t\topenSSHInfosRW.Lock()\n\t\topenSSHInfos[exe] = &info\n\t\topenSSHInfosRW.Unlock()\n\t}\n\treturn info\n}\n\nfunc DetectOpenSSHVersion(ctx context.Context, sshExe SSHExe) semver.Version {\n\treturn detectOpenSSHInfo(ctx, sshExe).Version\n}\n\n// detectValidPublicKey returns whether content represent a public key.\n// OpenSSH public key format have the structure of '<algorithm> <key> <comment>'.\n// By checking 'algorithm' with signature format identifier in 'key' part,\n// this function may report false positive but provide better compatibility.\nfunc detectValidPublicKey(content string) bool {\n\tif strings.ContainsRune(content, '\\n') {\n\t\treturn false\n\t}\n\tspaced := strings.SplitN(content, \" \", 3)\n\tif len(spaced) < 2 {\n\t\treturn false\n\t}\n\talgo, base64Key := spaced[0], spaced[1]\n\tdecodedKey, err := base64.StdEncoding.DecodeString(base64Key)\n\tif err != nil || len(decodedKey) < 4 {\n\t\treturn false\n\t}\n\tsigLength := binary.BigEndian.Uint32(decodedKey)\n\tif uint32(len(decodedKey)) < sigLength {\n\t\treturn false\n\t}\n\tsigFormat := string(decodedKey[4 : 4+sigLength])\n\treturn algo == sigFormat\n}\n\nfunc detectAESAcceleration() bool {\n\tif !cpu.Initialized {\n\t\tif runtime.GOOS == \"linux\" && runtime.GOARCH == \"arm64\" {\n\t\t\t// cpu.Initialized seems to always be false, even when the cpu.ARM64 struct is filled out\n\t\t\t// it is only being set by readARM64Registers, but not by readHWCAP or readLinuxProcCPUInfo\n\t\t\treturn cpu.ARM64.HasAES\n\t\t}\n\t\tif runtime.GOOS == \"darwin\" && runtime.GOARCH == \"arm64\" {\n\t\t\t// golang.org/x/sys/cpu supports darwin/amd64, linux/amd64, and linux/arm64,\n\t\t\t// but apparently lacks support for darwin/arm64: https://github.com/golang/sys/blob/v0.5.0/cpu/cpu_arm64.go#L43-L60\n\t\t\t//\n\t\t\t// According to https://gist.github.com/voluntas/fd279c7b4e71f9950cfd4a5ab90b722b ,\n\t\t\t// aes-128-gcm is faster than chacha20-poly1305 on Apple M1.\n\t\t\t//\n\t\t\t// So we return `true` here.\n\t\t\t//\n\t\t\t// This workaround will not be needed when https://go-review.googlesource.com/c/sys/+/332729 is merged.\n\t\t\tlogrus.Debug(\"Failed to detect CPU features. Assuming that AES acceleration is available on this Apple silicon.\")\n\t\t\treturn true\n\t\t}\n\t\tlogrus.Warn(\"Failed to detect CPU features. Assuming that AES acceleration is not available.\")\n\t\treturn false\n\t}\n\treturn cpu.ARM.HasAES || cpu.ARM64.HasAES || cpu.PPC64.IsPOWER8 || cpu.S390X.HasAES || cpu.X86.HasAES\n}\n"
  },
  {
    "path": "pkg/sshutil/sshutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sshutil\n\nimport (\n\t\"testing\"\n\n\t\"github.com/coreos/go-semver/semver\"\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestDefaultPubKeys(t *testing.T) {\n\tkeys, _ := DefaultPubKeys(t.Context(), true)\n\tt.Logf(\"found %d public keys\", len(keys))\n\tfor _, key := range keys {\n\t\tt.Logf(\"%s: %q\", key.Filename, key.Content)\n\t}\n}\n\nfunc TestParseOpenSSHVersion(t *testing.T) {\n\tassert.Check(t, ParseOpenSSHVersion([]byte(\"OpenSSH_8.4p1 Ubuntu\")).Equal(\n\t\tsemver.Version{Major: 8, Minor: 4, Patch: 1, PreRelease: \"\", Metadata: \"\"}))\n\n\tassert.Check(t, ParseOpenSSHVersion([]byte(\"OpenSSH_7.6p1 Ubuntu\")).LessThan(*semver.New(\"8.0.0\")))\n\n\t// macOS 10.15\n\tassert.Check(t, ParseOpenSSHVersion([]byte(\"OpenSSH_8.1p1, LibreSSL 2.7.3\")).Equal(*semver.New(\"8.1.1\")))\n\n\t// OpenBSD 5.8\n\tassert.Check(t, ParseOpenSSHVersion([]byte(\"OpenSSH_7.0, LibreSSL\")).Equal(*semver.New(\"7.0.0\")))\n\n\t// NixOS 25.05\n\tassert.Check(t, ParseOpenSSHVersion([]byte(`command-line line 0: Unsupported option \"gssapiauthentication\"\nOpenSSH_10.0p2, OpenSSL 3.4.1 11 Feb 2025`)).Equal(*semver.New(\"10.0.2\")))\n}\n\nfunc TestParseOpenSSHGSSAPISupported(t *testing.T) {\n\tassert.Check(t, parseOpenSSHGSSAPISupported(\"OpenSSH_8.4p1 Ubuntu\"))\n\tassert.Check(t, !parseOpenSSHGSSAPISupported(`command-line line 0: Unsupported option \"gssapiauthentication\"\nOpenSSH_10.0p2, OpenSSL 3.4.1 11 Feb 2025`))\n}\n\nfunc Test_detectValidPublicKey(t *testing.T) {\n\tassert.Check(t, detectValidPublicKey(\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAACQDf2IooTVPDBw== 64bit\"))\n\tassert.Check(t, detectValidPublicKey(\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAACQDf2IooTVPDBw==\"))\n\tassert.Check(t, detectValidPublicKey(\"ssh-dss AAAAB3NzaC1kc3MAAACBAP/yAytaYzqXq01uTd5+1RC=\" /* truncate */))\n\tassert.Check(t, detectValidPublicKey(\"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY=\" /* truncate */))\n\tassert.Check(t, detectValidPublicKey(\"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICs1tSO/jx8oc4O=\" /* truncate */))\n\n\tassert.Check(t, !detectValidPublicKey(\"wrong-algo AAAAB3NzaC1kc3MAAACBAP/yAytaYzqXq01uTd5+1RC=\"))\n\tassert.Check(t, !detectValidPublicKey(\"huge-length AAAD6A==\"))\n\tassert.Check(t, !detectValidPublicKey(\"arbitrary content\"))\n\tassert.Check(t, !detectValidPublicKey(\"\"))\n}\n\nfunc Test_DisableControlMasterOptsFromSSHArgs(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsshArgs []string\n\t\twant    []string\n\t}{\n\t\t{\n\t\t\tname: \"no ControlMaster options\",\n\t\t\tsshArgs: []string{\n\t\t\t\t\"-o\", \"StrictHostKeyChecking=no\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"ControlPath=none\", \"-o\", \"ControlPersist=no\",\n\t\t\t\t\"-o\", \"StrictHostKeyChecking=no\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ControlMaster=yes\",\n\t\t\tsshArgs: []string{\n\t\t\t\t\"-o\", \"ControlMaster=yes\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"ControlPath=none\", \"-o\", \"ControlPersist=no\",\n\t\t\t\t\"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ControlMaster=auto\",\n\t\t\tsshArgs: []string{\n\t\t\t\t\"-o\", \"ControlMaster=auto\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"ControlPath=none\", \"-o\", \"ControlPersist=no\",\n\t\t\t\t\"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ControlMaster=auto with ControlPath\",\n\t\t\tsshArgs: []string{\n\t\t\t\t\"-o\", \"ControlMaster=auto\", \"-o\", \"ControlPath=/tmp/ssh-%r@%h:%p\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"ControlPath=none\", \"-o\", \"ControlPersist=no\",\n\t\t\t\t\"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ControlPath only\",\n\t\t\tsshArgs: []string{\n\t\t\t\t\"-o\", \"ControlPath=/tmp/ssh-%r@%h:%p\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"ControlPath=none\", \"-o\", \"ControlPersist=no\",\n\t\t\t\t\"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ControlMaster=no\",\n\t\t\tsshArgs: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"ControlPath=none\", \"-o\", \"ControlPersist=no\",\n\t\t\t\t\"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ControlMaster=auto with other options\",\n\t\t\tsshArgs: []string{\n\t\t\t\t\"-o\", \"ControlMaster=auto\", \"-o\", \"StrictHostKeyChecking=no\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t\twant: []string{\n\t\t\t\t\"-o\", \"ControlMaster=no\", \"-o\", \"ControlPath=none\", \"-o\", \"ControlPersist=no\",\n\t\t\t\t\"-o\", \"StrictHostKeyChecking=no\", \"-o\", \"UserKnownHostsFile=/dev/null\",\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tassert.DeepEqual(t, DisableControlMasterOptsFromSSHArgs(tt.sshArgs), tt.want)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/store/disk.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage store\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/lima-vm/go-qcow2reader\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\ntype Disk struct {\n\tName        string `json:\"name\"`\n\tSize        int64  `json:\"size\"`\n\tFormat      string `json:\"format\"`\n\tDir         string `json:\"dir\"`\n\tInstance    string `json:\"instance\"`\n\tInstanceDir string `json:\"instanceDir\"`\n\tMountPoint  string `json:\"mountPoint\"`\n\n\t// if the Disk is in use and the FSType is specified\n\tFSType *string `json:\"fsType,omitempty\"`\n}\n\nfunc InspectDisk(diskName string, fsType *string) (*Disk, error) {\n\tdisk := &Disk{\n\t\tName:   diskName,\n\t\tFSType: fsType,\n\t}\n\n\tdiskDir, err := DiskDir(diskName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdisk.Dir = diskDir\n\tdataDisk := filepath.Join(diskDir, filenames.DataDisk)\n\tif _, err := os.Stat(dataDisk); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdisk.Size, disk.Format, err = inspectDisk(dataDisk)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinstDir, err := os.Readlink(filepath.Join(diskDir, filenames.InUseBy))\n\tif err != nil {\n\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tdisk.Instance = filepath.Base(instDir)\n\t\tdisk.InstanceDir = instDir\n\t}\n\n\tif disk.FSType != nil && *disk.FSType == \"swap\" {\n\t\tdisk.MountPoint = \"[SWAP]\" // only used for log message\n\t} else {\n\t\tdisk.MountPoint = fmt.Sprintf(\"/mnt/lima-%s\", diskName)\n\t}\n\n\treturn disk, nil\n}\n\n// inspectDisk attempts to inspect the disk size and format with qcow2reader.\nfunc inspectDisk(fName string) (size int64, format string, _ error) {\n\tf, err := os.Open(fName)\n\tif err != nil {\n\t\treturn -1, \"\", err\n\t}\n\tdefer f.Close()\n\timg, err := qcow2reader.Open(f)\n\tif err != nil {\n\t\treturn -1, \"\", err\n\t}\n\tsz := img.Size()\n\tif sz < 0 {\n\t\treturn -1, \"\", fmt.Errorf(\"cannot determine size of %q\", fName)\n\t}\n\n\treturn sz, string(img.Type()), nil\n}\n\nfunc (d *Disk) Lock(instanceDir string) error {\n\tinUseBy := filepath.Join(d.Dir, filenames.InUseBy)\n\treturn os.Symlink(instanceDir, inUseBy)\n}\n\nfunc (d *Disk) Unlock() error {\n\tinUseBy := filepath.Join(d.Dir, filenames.InUseBy)\n\treturn os.Remove(inUseBy)\n}\n\nfunc (d *Disk) LockForInstance(instanceDir string) error {\n\tif d.Instance != \"\" {\n\t\tif d.InstanceDir != instanceDir {\n\t\t\treturn fmt.Errorf(\"in use by instance %q\", d.Instance)\n\t\t}\n\t\tif err := d.Unlock(); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unlock for reuse in the same instance: %w\", err)\n\t\t}\n\t}\n\treturn d.Lock(instanceDir)\n}\n"
  },
  {
    "path": "pkg/store/disk_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage store\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\nfunc TestDiskLockForInstance(t *testing.T) {\n\tt.Run(\"unlocked disk\", func(t *testing.T) {\n\t\tdiskDir := t.TempDir()\n\t\tinstanceDir := t.TempDir()\n\t\tdisk := &Disk{Name: \"testdisk\", Dir: diskDir}\n\n\t\tassert.NilError(t, disk.LockForInstance(instanceDir))\n\t\ttarget, err := os.Readlink(filepath.Join(diskDir, filenames.InUseBy))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, target, instanceDir)\n\t})\n\n\tt.Run(\"locked by same instance (stale lock)\", func(t *testing.T) {\n\t\tdiskDir := t.TempDir()\n\t\tinstanceDir := t.TempDir()\n\t\tdisk := &Disk{\n\t\t\tName:        \"testdisk\",\n\t\t\tDir:         diskDir,\n\t\t\tInstance:    filepath.Base(instanceDir),\n\t\t\tInstanceDir: instanceDir,\n\t\t}\n\t\t// Simulate stale lock from previous run\n\t\tassert.NilError(t, os.Symlink(instanceDir, filepath.Join(diskDir, filenames.InUseBy)))\n\n\t\t// LockForInstance should auto-unlock and re-lock\n\t\tassert.NilError(t, disk.LockForInstance(instanceDir))\n\t\ttarget, err := os.Readlink(filepath.Join(diskDir, filenames.InUseBy))\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, target, instanceDir)\n\t})\n\n\tt.Run(\"locked by different instance\", func(t *testing.T) {\n\t\tdiskDir := t.TempDir()\n\t\totherDir := t.TempDir()\n\t\tdisk := &Disk{\n\t\t\tName:        \"testdisk\",\n\t\t\tDir:         diskDir,\n\t\t\tInstance:    filepath.Base(otherDir),\n\t\t\tInstanceDir: otherDir,\n\t\t}\n\t\tassert.NilError(t, os.Symlink(otherDir, filepath.Join(diskDir, filenames.InUseBy)))\n\n\t\tnewInstanceDir := t.TempDir()\n\t\terr := disk.LockForInstance(newInstanceDir)\n\t\tassert.ErrorContains(t, err, \"in use by instance\")\n\t})\n}\n"
  },
  {
    "path": "pkg/store/fuzz_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage store\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n)\n\nfunc FuzzLoadYAMLByFilePath(f *testing.F) {\n\tf.Fuzz(func(t *testing.T, fileContents []byte) {\n\t\tlocalFile := filepath.Join(t.TempDir(), \"yaml_file.yml\")\n\t\terr := os.WriteFile(localFile, fileContents, 0o600)\n\t\tassert.NilError(t, err)\n\t\t_, _ = LoadYAMLByFilePath(t.Context(), localFile)\n\t})\n}\n\nfunc FuzzInspect(f *testing.F) {\n\tf.Fuzz(func(t *testing.T, yml, limaVersion []byte) {\n\t\tlimaDir := t.TempDir()\n\t\tt.Setenv(\"LIMA_HOME\", limaDir)\n\t\terr := os.MkdirAll(filepath.Join(limaDir, \"fuzz-instance\"), 0o700)\n\t\tassert.NilError(t, err)\n\t\tymlFile := filepath.Join(limaDir, \"fuzz-instance\", filenames.LimaYAML)\n\t\tlimaVersionFile := filepath.Join(limaDir, filenames.LimaVersion)\n\t\terr = os.WriteFile(ymlFile, yml, 0o600)\n\t\tassert.NilError(t, err)\n\t\terr = os.WriteFile(limaVersionFile, limaVersion, 0o600)\n\t\tassert.NilError(t, err)\n\t\t_, _ = Inspect(t.Context(), \"fuzz-instance\")\n\t})\n}\n"
  },
  {
    "path": "pkg/store/instance.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage store\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"syscall\"\n\t\"text/tabwriter\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/docker/go-units\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\thostagentclient \"github.com/lima-vm/lima/v2/pkg/hostagent/api/client\"\n\t\"github.com/lima-vm/lima/v2/pkg/instance/hostname\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/textutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/version/versionutil\"\n)\n\n// Inspect returns err only when the instance does not exist (os.ErrNotExist).\n// Other errors are returned as *Instance.Errors.\nfunc Inspect(ctx context.Context, instName string) (*limatype.Instance, error) {\n\tinst := &limatype.Instance{\n\t\tName: instName,\n\t\t// TODO: support customizing hostname\n\t\tHostname: hostname.FromInstName(instName),\n\t\tStatus:   limatype.StatusUnknown,\n\t}\n\t// InstanceDir validates the instName but does not check whether the instance exists\n\tinstDir, err := dirnames.InstanceDir(instName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Make sure inst.Dir is set, even when YAML validation fails\n\tinst.Dir = instDir\n\tyamlPath := filepath.Join(instDir, filenames.LimaYAML)\n\ty, err := LoadYAMLByFilePath(ctx, yamlPath)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, err\n\t\t}\n\t\tinst.Errors = append(inst.Errors, err)\n\t\treturn inst, nil\n\t}\n\tinst.Config = y\n\tinst.Arch = *y.Arch\n\tinst.VMType = *y.VMType\n\tinst.SSHAddress = \"127.0.0.1\"\n\tinst.SSHLocalPort = *y.SSH.LocalPort // maybe 0\n\tinst.SSHConfigFile = filepath.Join(instDir, filenames.SSHConfig)\n\tinst.HostAgentPID, err = ReadPIDFile(filepath.Join(instDir, filenames.HostAgentPID))\n\tif err != nil {\n\t\tinst.Status = limatype.StatusBroken\n\t\tinst.Errors = append(inst.Errors, err)\n\t}\n\n\tif inst.HostAgentPID != 0 {\n\t\thaSock := filepath.Join(instDir, filenames.HostAgentSock)\n\t\thaClient, err := hostagentclient.NewHostAgentClient(haSock)\n\t\tif err != nil {\n\t\t\tinst.Status = limatype.StatusBroken\n\t\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"failed to connect to %q: %w\", haSock, err))\n\t\t} else {\n\t\t\tctx, cancel := context.WithTimeout(ctx, 3*time.Second)\n\t\t\tdefer cancel()\n\t\t\tinfo, err := haClient.Info(ctx)\n\t\t\tif err != nil {\n\t\t\t\tinst.Status = limatype.StatusBroken\n\t\t\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"failed to get Info from %q: %w\", haSock, err))\n\t\t\t} else {\n\t\t\t\tinst.SSHLocalPort = info.SSHLocalPort\n\t\t\t\tinst.AutoStartedIdentifier = info.AutoStartedIdentifier\n\t\t\t}\n\t\t}\n\t}\n\tif inst.SSHLocalPort == 0 {\n\t\tsshConfigPath := filepath.Join(instDir, filenames.SSHConfig)\n\t\tif port, err := sshPortFromConfig(sshConfigPath); err == nil {\n\t\t\tinst.SSHLocalPort = port\n\t\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"failed to extract SSH local port from %q: %w\", sshConfigPath, err))\n\t\t}\n\t}\n\n\tinst.CPUs = *y.CPUs\n\tmemory, err := units.RAMInBytes(*y.Memory)\n\tif err == nil {\n\t\tinst.Memory = memory\n\t}\n\tdisk, err := units.RAMInBytes(*y.Disk)\n\tif err == nil {\n\t\tinst.Disk = disk\n\t}\n\tinst.AdditionalDisks = y.AdditionalDisks\n\tinst.Networks = y.Networks\n\n\t// 0 out values since not configurable on WSL2\n\tif inst.VMType == limatype.WSL2 {\n\t\tinst.Memory = 0\n\t\tinst.CPUs = 0\n\t\tinst.Disk = 0\n\t}\n\n\tprotected := filepath.Join(instDir, filenames.Protected)\n\tif _, err := os.Lstat(protected); !errors.Is(err, os.ErrNotExist) {\n\t\tinst.Protected = true\n\t}\n\n\tinspectStatus(ctx, instDir, inst, y)\n\n\ttmpl, err := template.New(\"format\").Parse(y.Message)\n\tif err != nil {\n\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"message %q is not a valid template: %w\", y.Message, err))\n\t\tinst.Status = limatype.StatusBroken\n\t} else {\n\t\tdata, err := AddGlobalFields(inst)\n\t\tif err != nil {\n\t\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"cannot add global fields to instance data: %w\", err))\n\t\t\tinst.Status = limatype.StatusBroken\n\t\t} else {\n\t\t\tdata.Param = y.Param\n\t\t\tvar message strings.Builder\n\t\t\terr = tmpl.Execute(&message, data)\n\t\t\tif err != nil {\n\t\t\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"cannot execute template %q: %w\", y.Message, err))\n\t\t\t\tinst.Status = limatype.StatusBroken\n\t\t\t} else {\n\t\t\t\tinst.Message = message.String()\n\t\t\t}\n\t\t}\n\t}\n\n\tlimaVersionFile := filepath.Join(instDir, filenames.LimaVersion)\n\tif version, err := os.ReadFile(limaVersionFile); err == nil {\n\t\tinst.LimaVersion = strings.TrimSpace(string(version))\n\t\tif _, err = versionutil.Parse(inst.LimaVersion); err != nil {\n\t\t\tlogrus.Warnf(\"treating lima version %q from %q as very latest release\", inst.LimaVersion, limaVersionFile)\n\t\t}\n\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\tinst.Errors = append(inst.Errors, err)\n\t}\n\tinst.Param = y.Param\n\treturn inst, nil\n}\n\nfunc inspectStatus(ctx context.Context, instDir string, inst *limatype.Instance, y *limatype.LimaYAML) {\n\tstatus, err := driverutil.InspectStatus(ctx, inst)\n\tif err != nil {\n\t\tinst.Status = limatype.StatusBroken\n\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"failed to inspect status: %w\", err))\n\t\treturn\n\t}\n\n\tif status == \"\" {\n\t\tinspectStatusWithPIDFiles(instDir, inst, y)\n\t\treturn\n\t}\n\n\tinst.Status = status\n}\n\nfunc inspectStatusWithPIDFiles(instDir string, inst *limatype.Instance, y *limatype.LimaYAML) {\n\tvar err error\n\tinst.DriverPID, err = ReadPIDFile(filepath.Join(instDir, filenames.PIDFile(*y.VMType)))\n\tif err != nil {\n\t\tinst.Status = limatype.StatusBroken\n\t\tinst.Errors = append(inst.Errors, err)\n\t}\n\n\tif inst.Status == limatype.StatusUnknown {\n\t\tswitch {\n\t\tcase inst.HostAgentPID > 0 && inst.DriverPID > 0:\n\t\t\tinst.Status = limatype.StatusRunning\n\t\tcase inst.HostAgentPID == 0 && inst.DriverPID == 0:\n\t\t\tinst.Status = limatype.StatusStopped\n\t\tcase inst.HostAgentPID > 0 && inst.DriverPID == 0:\n\t\t\tinst.Errors = append(inst.Errors, errors.New(\"host agent is running but driver is not\"))\n\t\t\tinst.Status = limatype.StatusBroken\n\t\tdefault:\n\t\t\tinst.Errors = append(inst.Errors, fmt.Errorf(\"%s driver is running but host agent is not\", inst.VMType))\n\t\t\tinst.Status = limatype.StatusBroken\n\t\t}\n\t}\n}\n\n// ReadPIDFile returns 0 if the PID file does not exist or the process has already terminated\n// (in which case the PID file will be removed).\nfunc ReadPIDFile(path string) (int, error) {\n\tb, err := os.ReadFile(path)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn 0, nil\n\t\t}\n\t\treturn 0, err\n\t}\n\tpid, err := strconv.Atoi(strings.TrimSpace(string(b)))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tproc, err := os.FindProcess(pid)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\t// os.FindProcess will only return running processes on Windows, exit early\n\tif runtime.GOOS == \"windows\" {\n\t\treturn pid, nil\n\t}\n\terr = proc.Signal(syscall.Signal(0))\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrProcessDone) {\n\t\t\t_ = os.Remove(path)\n\t\t\treturn 0, nil\n\t\t}\n\t\t// We may not have permission to send the signal (e.g. to network daemon running as root).\n\t\t// But if we get a permissions error, it means the process is still running.\n\t\tif !errors.Is(err, os.ErrPermission) {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\treturn pid, nil\n}\n\n// sshPortFromConfig extracts the SSH port from an ssh config file.\nfunc sshPortFromConfig(configPath string) (int, error) {\n\tb, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tfor line := range strings.SplitSeq(string(b), \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tif port, ok := strings.CutPrefix(line, \"Port \"); ok {\n\t\t\treturn strconv.Atoi(port)\n\t\t}\n\t}\n\treturn 0, fmt.Errorf(\"port not found in %q\", configPath)\n}\n\ntype FormatData struct {\n\tlimatype.Instance `yaml:\",inline\"`\n\n\t// Using these host attributes is deprecated; they will be removed in Lima 3.0\n\t// The values are available from `limactl info` as hostOS, hostArch, limaHome, and identifyFile.\n\tHostOS       string `json:\"HostOS\" yaml:\"HostOS\" lima:\"deprecated\"`\n\tHostArch     string `json:\"HostArch\" yaml:\"HostArch\" lima:\"deprecated\"`\n\tLimaHome     string `json:\"LimaHome\" yaml:\"LimaHome\" lima:\"deprecated\"`\n\tIdentityFile string `json:\"IdentityFile\" yaml:\"IdentityFile\" lima:\"deprecated\"`\n}\n\nvar FormatHelp = \"\\n\" +\n\t\"These functions are available to go templates:\\n\\n\" +\n\ttextutil.IndentString(2,\n\t\tstrings.Join(textutil.FuncHelp, \"\\n\")+\"\\n\")\n\nfunc AddGlobalFields(inst *limatype.Instance) (FormatData, error) {\n\tvar data FormatData\n\tdata.Instance = *inst\n\t// Add HostOS\n\tdata.HostOS = runtime.GOOS\n\t// Add HostArch\n\tdata.HostArch = limatype.NewArch(runtime.GOARCH)\n\t// Add IdentityFile\n\tconfigDir, err := dirnames.LimaConfigDir()\n\tif err != nil {\n\t\treturn FormatData{}, err\n\t}\n\tdata.IdentityFile = filepath.Join(configDir, filenames.UserPrivateKey)\n\t// Add LimaHome\n\tdata.LimaHome, err = dirnames.LimaDir()\n\tif err != nil {\n\t\treturn FormatData{}, err\n\t}\n\treturn data, nil\n}\n\ntype PrintOptions struct {\n\tAllFields     bool\n\tTerminalWidth int\n}\n\n// PrintInstances prints instances in a requested format to a given io.Writer.\n// Supported formats are \"json\", \"yaml\", \"table\", or a go template.\nfunc PrintInstances(w io.Writer, instances []*limatype.Instance, format string, options *PrintOptions) error {\n\tswitch format {\n\tcase \"json\":\n\t\tformat = \"{{json .}}\"\n\tcase \"yaml\":\n\t\tformat = \"{{yaml .}}\"\n\tcase \"table\":\n\t\ttypes := map[string]int{}\n\t\tarchs := map[string]int{}\n\t\tfor _, instance := range instances {\n\t\t\ttypes[instance.VMType]++\n\t\t\tarchs[instance.Arch]++\n\t\t}\n\t\tall := options != nil && options.AllFields\n\t\twidth := 0\n\t\tif options != nil {\n\t\t\twidth = options.TerminalWidth\n\t\t}\n\t\tcolumnWidth := 8\n\t\thideType := false\n\t\thideArch := false\n\t\thideDir := false\n\n\t\tcolumns := 1 // NAME\n\t\tcolumns += 2 // STATUS\n\t\tcolumns += 2 // SSH\n\t\t// can we still fit the remaining columns (7)\n\t\tif width == 0 || (columns+7)*columnWidth > width && !all {\n\t\t\thideType = len(types) == 1\n\t\t}\n\t\tif !hideType {\n\t\t\tcolumns++ // VMTYPE\n\t\t}\n\t\t// only hide arch if it is the same as the host arch\n\t\tgoarch := limatype.NewArch(runtime.GOARCH)\n\t\t// can we still fit the remaining columns (6)\n\t\tif width == 0 || (columns+6)*columnWidth > width && !all {\n\t\t\thideArch = len(archs) == 1 && instances[0].Arch == goarch\n\t\t}\n\t\tif !hideArch {\n\t\t\tcolumns++ // ARCH\n\t\t}\n\t\tcolumns++ // CPUS\n\t\tcolumns++ // MEMORY\n\t\tcolumns++ // DISK\n\t\t// can we still fit the remaining columns (2)\n\t\tif width != 0 && (columns+2)*columnWidth > width && !all {\n\t\t\thideDir = true\n\t\t}\n\t\tif !hideDir {\n\t\t\tcolumns += 2 // DIR\n\t\t}\n\t\t_ = columns\n\n\t\tw := tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)\n\t\tfmt.Fprint(w, \"NAME\\tSTATUS\\tSSH\")\n\t\tif !hideType {\n\t\t\tfmt.Fprint(w, \"\\tVMTYPE\")\n\t\t}\n\t\tif !hideArch {\n\t\t\tfmt.Fprint(w, \"\\tARCH\")\n\t\t}\n\t\tfmt.Fprint(w, \"\\tCPUS\\tMEMORY\\tDISK\")\n\t\tif !hideDir {\n\t\t\tfmt.Fprint(w, \"\\tDIR\")\n\t\t}\n\t\tfmt.Fprintln(w)\n\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, instance := range instances {\n\t\t\tdir := instance.Dir\n\t\t\tif strings.HasPrefix(dir, homeDir) {\n\t\t\t\tdir = strings.Replace(dir, homeDir, \"~\", 1)\n\t\t\t}\n\t\t\tfmt.Fprintf(w, \"%s\\t%s\\t%s\",\n\t\t\t\tinstance.Name,\n\t\t\t\tinstance.Status,\n\t\t\t\tfmt.Sprintf(\"%s:%d\", instance.SSHAddress, instance.SSHLocalPort),\n\t\t\t)\n\t\t\tif !hideType {\n\t\t\t\tfmt.Fprintf(w, \"\\t%s\",\n\t\t\t\t\tinstance.VMType,\n\t\t\t\t)\n\t\t\t}\n\t\t\tif !hideArch {\n\t\t\t\tfmt.Fprintf(w, \"\\t%s\",\n\t\t\t\t\tinstance.Arch,\n\t\t\t\t)\n\t\t\t}\n\t\t\tfmt.Fprintf(w, \"\\t%d\\t%s\\t%s\",\n\t\t\t\tinstance.CPUs,\n\t\t\t\tunits.BytesSize(float64(instance.Memory)),\n\t\t\t\tunits.BytesSize(float64(instance.Disk)),\n\t\t\t)\n\t\t\tif !hideDir {\n\t\t\t\tfmt.Fprintf(w, \"\\t%s\",\n\t\t\t\t\tdir,\n\t\t\t\t)\n\t\t\t}\n\t\t\tfmt.Fprint(w, \"\\n\")\n\t\t}\n\t\treturn w.Flush()\n\tdefault:\n\t\t// NOP\n\t}\n\ttmpl, err := template.New(\"format\").Funcs(textutil.TemplateFuncMap).Parse(format)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid go template: %w\", err)\n\t}\n\tfor _, instance := range instances {\n\t\tdata, err := AddGlobalFields(instance)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdata.Message = strings.TrimSuffix(instance.Message, \"\\n\")\n\t\terr = tmpl.Execute(w, data)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfmt.Fprintln(w)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/store/instance_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage store\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\nconst separator = string(filepath.Separator)\n\nvar (\n\tvmtype = limatype.QEMU\n\tgoarch = limatype.NewArch(runtime.GOARCH)\n\tspace  = strings.Repeat(\" \", len(goarch)-4)\n)\n\nvar instance = limatype.Instance{\n\tName:       \"foo\",\n\tStatus:     limatype.StatusStopped,\n\tVMType:     vmtype,\n\tArch:       goarch,\n\tDir:        \"dir\",\n\tSSHAddress: \"127.0.0.1\",\n}\n\nvar table = \"NAME    STATUS     SSH            CPUS    MEMORY    DISK    DIR\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    0       0B        0B      dir\\n\"\n\nvar tableEmu = \"NAME    STATUS     SSH            ARCH       CPUS    MEMORY    DISK    DIR\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    unknown    0       0B        0B      dir\\n\"\n\nvar tableHome = \"NAME    STATUS     SSH            CPUS    MEMORY    DISK    DIR\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    0       0B        0B      ~\" + separator + \"dir\\n\"\n\nvar tableAll = \"NAME    STATUS     SSH            VMTYPE    ARCH\" + space + \"    CPUS    MEMORY    DISK    DIR\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    \" + vmtype + \"      \" + goarch + \"    0       0B        0B      dir\\n\"\n\n// for width 60, everything is hidden\nvar table60 = \"NAME    STATUS     SSH            CPUS    MEMORY    DISK\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    0       0B        0B\\n\"\n\n// for width 80, identical is hidden (type/arch)\nvar table80i = \"NAME    STATUS     SSH            CPUS    MEMORY    DISK    DIR\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    0       0B        0B      dir\\n\"\n\n// for width 80, different arch is still shown (not dir)\nvar table80d = \"NAME    STATUS     SSH            ARCH       CPUS    MEMORY    DISK\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    unknown    0       0B        0B\\n\"\n\n// for width 100, nothing is hidden\nvar table100 = \"NAME    STATUS     SSH            VMTYPE    ARCH\" + space + \"    CPUS    MEMORY    DISK    DIR\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    \" + vmtype + \"      \" + goarch + \"    0       0B        0B      dir\\n\"\n\n// for width 80, directory is hidden (if not identical)\nvar tableTwo = \"NAME    STATUS     SSH            VMTYPE    ARCH       CPUS    MEMORY    DISK\\n\" +\n\t\"foo     Stopped    127.0.0.1:0    qemu      x86_64     0       0B        0B\\n\" +\n\t\"bar     Stopped    127.0.0.1:0    vz        aarch64    0       0B        0B\\n\"\n\nfunc TestSSHPortFromConfig(t *testing.T) {\n\tt.Run(\"valid\", func(t *testing.T) {\n\t\tf := filepath.Join(t.TempDir(), \"ssh.config\")\n\t\tcontent := \"Host lima-default\\n  Hostname 127.0.0.1\\n  Port 58786\\n  User foo\\n\"\n\t\tassert.NilError(t, os.WriteFile(f, []byte(content), 0o644))\n\t\tport, err := sshPortFromConfig(f)\n\t\tassert.NilError(t, err)\n\t\tassert.Equal(t, 58786, port)\n\t})\n\tt.Run(\"missing file\", func(t *testing.T) {\n\t\t_, err := sshPortFromConfig(filepath.Join(t.TempDir(), \"nonexistent\"))\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t})\n\tt.Run(\"no port line\", func(t *testing.T) {\n\t\tf := filepath.Join(t.TempDir(), \"ssh.config\")\n\t\tassert.NilError(t, os.WriteFile(f, []byte(\"Host lima-default\\n  Hostname 127.0.0.1\\n\"), 0o644))\n\t\t_, err := sshPortFromConfig(f)\n\t\tassert.ErrorContains(t, err, \"port not found\")\n\t})\n\tt.Run(\"invalid port\", func(t *testing.T) {\n\t\tf := filepath.Join(t.TempDir(), \"ssh.config\")\n\t\tassert.NilError(t, os.WriteFile(f, []byte(\"Host lima-default\\n  Port abc\\n\"), 0o644))\n\t\t_, err := sshPortFromConfig(f)\n\t\tassert.ErrorContains(t, err, \"invalid syntax\")\n\t})\n}\n\nfunc TestPrintInstanceTable(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstances := []*limatype.Instance{&instance}\n\terr := PrintInstances(&buf, instances, \"table\", nil)\n\tassert.NilError(t, err)\n\tassert.Equal(t, table, buf.String())\n}\n\nfunc TestPrintInstanceTableEmu(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstance1 := instance\n\tinstance1.Arch = \"unknown\"\n\tinstances := []*limatype.Instance{&instance1}\n\terr := PrintInstances(&buf, instances, \"table\", nil)\n\tassert.NilError(t, err)\n\tassert.Equal(t, tableEmu, buf.String())\n}\n\nfunc TestPrintInstanceTableHome(t *testing.T) {\n\tvar buf bytes.Buffer\n\thomeDir, err := os.UserHomeDir()\n\tassert.NilError(t, err)\n\tinstance1 := instance\n\tinstance1.Dir = filepath.Join(homeDir, \"dir\")\n\tinstances := []*limatype.Instance{&instance1}\n\terr = PrintInstances(&buf, instances, \"table\", nil)\n\tassert.NilError(t, err)\n\tassert.Equal(t, tableHome, buf.String())\n}\n\nfunc TestPrintInstanceTable60(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstances := []*limatype.Instance{&instance}\n\toptions := PrintOptions{TerminalWidth: 60}\n\terr := PrintInstances(&buf, instances, \"table\", &options)\n\tassert.NilError(t, err)\n\tassert.Equal(t, table60, buf.String())\n}\n\nfunc TestPrintInstanceTable80SameArch(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstances := []*limatype.Instance{&instance}\n\toptions := PrintOptions{TerminalWidth: 80}\n\terr := PrintInstances(&buf, instances, \"table\", &options)\n\tassert.NilError(t, err)\n\tassert.Equal(t, table80i, buf.String())\n}\n\nfunc TestPrintInstanceTable80DiffArch(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstance1 := instance\n\tinstance1.Arch = limatype.NewArch(\"unknown\")\n\tinstances := []*limatype.Instance{&instance1}\n\toptions := PrintOptions{TerminalWidth: 80}\n\terr := PrintInstances(&buf, instances, \"table\", &options)\n\tassert.NilError(t, err)\n\tassert.Equal(t, table80d, buf.String())\n}\n\nfunc TestPrintInstanceTable100(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstances := []*limatype.Instance{&instance}\n\toptions := PrintOptions{TerminalWidth: 100}\n\terr := PrintInstances(&buf, instances, \"table\", &options)\n\tassert.NilError(t, err)\n\tassert.Equal(t, table100, buf.String())\n}\n\nfunc TestPrintInstanceTableAll(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstances := []*limatype.Instance{&instance}\n\toptions := PrintOptions{TerminalWidth: 40, AllFields: true}\n\terr := PrintInstances(&buf, instances, \"table\", &options)\n\tassert.NilError(t, err)\n\tassert.Equal(t, tableAll, buf.String())\n}\n\nfunc TestPrintInstanceTableTwo(t *testing.T) {\n\tvar buf bytes.Buffer\n\tinstance1 := instance\n\tinstance1.Name = \"foo\"\n\tinstance1.VMType = limatype.QEMU\n\tinstance1.Arch = limatype.X8664\n\tinstance2 := instance\n\tinstance2.Name = \"bar\"\n\tinstance2.VMType = limatype.VZ\n\tinstance2.Arch = limatype.AARCH64\n\tinstances := []*limatype.Instance{&instance1, &instance2}\n\toptions := PrintOptions{TerminalWidth: 80}\n\terr := PrintInstances(&buf, instances, \"table\", &options)\n\tassert.NilError(t, err)\n\tassert.Equal(t, tableTwo, buf.String())\n}\n"
  },
  {
    "path": "pkg/store/store.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage store\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/driverutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/identifiers\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/filenames\"\n\t\"github.com/lima-vm/lima/v2/pkg/limayaml\"\n)\n\n// Directory returns the LimaDir.\nfunc Directory() string {\n\tlimaDir, err := dirnames.LimaDir()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn limaDir\n}\n\n// Validate checks the LimaDir.\nfunc Validate() error {\n\tlimaDir, err := dirnames.LimaDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\tnames, err := Instances()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, name := range names {\n\t\t// Each instance directory needs to have limayaml\n\t\tinstDir := filepath.Join(limaDir, name)\n\t\tyamlPath := filepath.Join(instDir, filenames.LimaYAML)\n\t\tif _, err := os.Stat(yamlPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Instances returns the names of the instances under LimaDir.\nfunc Instances() ([]string, error) {\n\tlimaDir, err := dirnames.LimaDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlimaDirList, err := os.ReadDir(limaDir)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tvar names []string\n\tfor _, f := range limaDirList {\n\t\tif strings.HasPrefix(f.Name(), \".\") || strings.HasPrefix(f.Name(), \"_\") {\n\t\t\tcontinue\n\t\t}\n\t\tif !f.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tnames = append(names, f.Name())\n\t}\n\treturn names, nil\n}\n\nfunc Disks() ([]string, error) {\n\tlimaDiskDir, err := dirnames.LimaDisksDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlimaDiskDirList, err := os.ReadDir(limaDiskDir)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\tvar names []string\n\tfor _, f := range limaDiskDirList {\n\t\tnames = append(names, f.Name())\n\t}\n\treturn names, nil\n}\n\nfunc DiskDir(name string) (string, error) {\n\tif err := identifiers.Validate(name); err != nil {\n\t\treturn \"\", err\n\t}\n\tlimaDisksDir, err := dirnames.LimaDisksDir()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdir := filepath.Join(limaDisksDir, name)\n\treturn dir, nil\n}\n\n// LoadYAMLByFilePath loads and validates the yaml.\nfunc LoadYAMLByFilePath(ctx context.Context, filePath string) (*limatype.LimaYAML, error) {\n\t// We need to use the absolute path because it may be used to determine hostSocket locations.\n\tabsPath, err := filepath.Abs(filePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tyContent, err := os.ReadFile(absPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ty, err := limayaml.Load(ctx, yContent, absPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := driverutil.ResolveVMType(ctx, y, filePath); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to resolve vm for %q: %w\", filePath, err)\n\t}\n\tif err := limayaml.Validate(y, false); err != nil {\n\t\treturn nil, err\n\t}\n\treturn y, nil\n}\n"
  },
  {
    "path": "pkg/store/store_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage store\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n)\n\nfunc TestValidateInstName(t *testing.T) {\n\tinstNames := []string{\n\t\t\"default\",\n\t\t\"Ubuntu-20.04\",\n\t\t\"example.com\",\n\t\t\"under_score\",\n\t\t\"1-2_3.4\",\n\t\t\"yml\",\n\t\t\"yaml\",\n\t\t\"foo.yaml.com\",\n\t}\n\tfor _, arg := range instNames {\n\t\tt.Run(arg, func(t *testing.T) {\n\t\t\terr := dirnames.ValidateInstName(arg)\n\t\t\tassert.NilError(t, err)\n\t\t})\n\t}\n\tinvalidIdentifiers := []string{\n\t\t\"\",\n\t\t\"my/instance\",\n\t\t\"my\\\\instance\",\n\t\t\"c:default\",\n\t\t\"dot.\",\n\t\t\".dot\",\n\t\t\"dot..dot\",\n\t\t\"underscore_\",\n\t\t\"_underscore\",\n\t\t\"underscore__underscore\",\n\t\t\"dash-\",\n\t\t\"-dash\",\n\t\t\"dash--dash\",\n\t}\n\tfor _, arg := range invalidIdentifiers {\n\t\tt.Run(arg, func(t *testing.T) {\n\t\t\terr := dirnames.ValidateInstName(arg)\n\t\t\tassert.ErrorContains(t, err, \"not a valid identifier\")\n\t\t})\n\t}\n\tyamlNames := []string{\n\t\t\"default.yaml\",\n\t\t\"MY.YAML\",\n\t\t\"My.YmL\",\n\t}\n\tfor _, arg := range yamlNames {\n\t\tt.Run(arg, func(t *testing.T) {\n\t\t\terr := dirnames.ValidateInstName(arg)\n\t\t\tassert.ErrorContains(t, err, \"must not end with .y\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/sysprof/network_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sysprof\n\ntype SPNetworkDataType struct {\n\tSPNetworkDataType []NetworkDataType `json:\"SPNetworkDataType\"`\n}\n\ntype NetworkDataType struct {\n\tDNS       DNS     `json:\"DNS\"`\n\tInterface string  `json:\"interface\"`\n\tIPv4      IPv4    `json:\"IPv4,omitempty\"`\n\tProxies   Proxies `json:\"Proxies\"`\n}\n\ntype DNS struct {\n\tServerAddresses []string `json:\"ServerAddresses\"`\n}\n\ntype IPv4 struct {\n\tAddresses []string `json:\"Addresses,omitempty\"`\n}\n\ntype Proxies struct {\n\tExceptionList []string `json:\"ExceptionList\"` // default: [\"*.local\", \"169.254/16\"]\n\tFTPEnable     string   `json:\"FTPEnable\"`\n\tFTPPort       any      `json:\"FTPPort\"`\n\tFTPProxy      string   `json:\"FTPProxy\"`\n\tFTPUser       string   `json:\"FTPUser\"`\n\tHTTPEnable    string   `json:\"HTTPEnable\"`\n\tHTTPPort      any      `json:\"HTTPPort\"`\n\tHTTPProxy     string   `json:\"HTTPProxy\"`\n\tHTTPUser      string   `json:\"HTTPUser\"`\n\tHTTPSEnable   string   `json:\"HTTPSEnable\"`\n\tHTTPSPort     any      `json:\"HTTPSPort\"`\n\tHTTPSProxy    string   `json:\"HTTPSProxy\"`\n\tHTTPSUser     string   `json:\"HTTPSUser\"`\n}\n"
  },
  {
    "path": "pkg/sysprof/sysprof_darwin.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage sysprof\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nvar NetworkData = sync.OnceValues(func() ([]NetworkDataType, error) {\n\tb, err := SystemProfiler(context.Background(), \"SPNetworkDataType\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar networkData SPNetworkDataType\n\tif err := json.Unmarshal(b, &networkData); err != nil {\n\t\treturn nil, err\n\t}\n\treturn networkData.SPNetworkDataType, nil\n})\n\nfunc SystemProfiler(ctx context.Context, dataType string) ([]byte, error) {\n\texe, err := exec.LookPath(\"system_profiler\")\n\tif err != nil {\n\t\t// $PATH may lack /usr/sbin\n\t\texe = \"/usr/sbin/system_profiler\"\n\t}\n\tvar stdout, stderr bytes.Buffer\n\tcmd := exec.CommandContext(ctx, exe, dataType, \"-json\")\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\tif strings.HasPrefix(stderr.String(), \"Usage: system_profiler\") {\n\t\t\tlogrus.Warn(\"Can't fetch system_profiler data; maybe OS is older than macOS Catalina 10.15\")\n\t\t\treturn []byte(\"{}\"), nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to run %v: stdout=%q, stderr=%q: %w\",\n\t\t\tcmd.Args, stdout.String(), stderr.String(), err)\n\t}\n\treturn stdout.Bytes(), nil\n}\n"
  },
  {
    "path": "pkg/templatestore/templatestore.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage templatestore\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/limatype/dirnames\"\n\t\"github.com/lima-vm/lima/v2/pkg/usrlocal\"\n)\n\ntype Template struct {\n\tName     string `json:\"name\"`\n\tLocation string `json:\"location\"`\n}\n\nfunc templatesPaths() ([]string, error) {\n\tif tmplPath := os.Getenv(\"LIMA_TEMPLATES_PATH\"); tmplPath != \"\" {\n\t\treturn strings.Split(tmplPath, string(filepath.ListSeparator)), nil\n\t}\n\tlimaTemplatesDir, err := dirnames.LimaTemplatesDir()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tshareLimaDirs, err := usrlocal.ShareLima()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := []string{limaTemplatesDir}\n\tfor _, shareLimaDir := range shareLimaDirs {\n\t\tres = append(res, filepath.Join(shareLimaDir, \"templates\"))\n\t}\n\treturn res, nil\n}\n\n// Read searches for template `name` in all template directories and returns the\n// contents of the first one found. Template names cannot contain the substring \"..\"\n// to make sure they don't reference files outside the template directories. We are\n// not using securejoin.SecureJoin because the actual template may be a symlink to a\n// directory elsewhere (e.g. when installed by Homebrew).\nfunc Read(name string) ([]byte, error) {\n\tdoubleDot := \"..\"\n\tif strings.Contains(name, doubleDot) {\n\t\treturn nil, fmt.Errorf(\"template name %q must not contain %q\", name, doubleDot)\n\t}\n\tpaths, err := templatesPaths()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\text := filepath.Ext(name)\n\t// Append .yaml extension if name doesn't have an extension, or if it starts with a digit.\n\t// So \"docker.sh\" would remain unchanged but \"ubuntu-24.04\" becomes \"ubuntu-24.04.yaml\".\n\tif len(ext) < 2 || unicode.IsDigit(rune(ext[1])) {\n\t\tname += \".yaml\"\n\t}\n\tfor _, templatesDir := range paths {\n\t\t// Normalize filePath for error messages because template names always use forward slashes\n\t\tfilePath := filepath.Clean(filepath.Join(templatesDir, name))\n\t\tif b, err := os.ReadFile(filePath); !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn b, err\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"template %q not found\", name)\n}\n\nconst Default = \"default\"\n\n// Templates returns a list of Template structures containing the Name and Location for each template.\n// It searches all template directories, but only the first template of a given name is recorded.\n// Only non-hidden files with a \".yaml\" file extension are considered templates.\n// The final result is sorted alphabetically by template name.\nfunc Templates() ([]Template, error) {\n\tpaths, err := templatesPaths()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttemplates := make(map[string]string)\n\tfor _, templatesDir := range paths {\n\t\tif _, err := os.Stat(templatesDir); os.IsNotExist(err) {\n\t\t\tcontinue\n\t\t}\n\t\twalkDirFn := func(p string, _ fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tbase := filepath.Base(p)\n\t\t\tif strings.HasPrefix(base, \".\") || !strings.HasSuffix(base, \".yaml\") {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\t// Name is like \"default\", \"debian\", \"deprecated/centos-7\", ...\n\t\t\tname := strings.TrimSuffix(strings.TrimPrefix(p, templatesDir+\"/\"), \".yaml\")\n\t\t\tif _, ok := templates[name]; !ok {\n\t\t\t\ttemplates[name] = p\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\tif err = filepath.WalkDir(templatesDir, walkDirFn); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tvar res []Template\n\tfor name, loc := range templates {\n\t\tres = append(res, Template{Name: name, Location: loc})\n\t}\n\tslices.SortFunc(res, func(i, j Template) int { return cmp.Compare(i.Name, j.Name) })\n\treturn res, nil\n}\n"
  },
  {
    "path": "pkg/textutil/textutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage textutil\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/template\"\n\n\t\"github.com/goccy/go-yaml\"\n)\n\n// ExecuteTemplate executes a text/template template.\nfunc ExecuteTemplate(tmpl string, args any) ([]byte, error) {\n\tx, err := template.New(\"\").Parse(tmpl)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar b bytes.Buffer\n\tif err := x.Execute(&b, args); err != nil {\n\t\treturn nil, err\n\t}\n\treturn b.Bytes(), nil\n}\n\n// PrefixString adds prefix to beginning of each line.\nfunc PrefixString(prefix, text string) string {\n\tlines := strings.Split(text, \"\\n\")\n\tfor i, line := range lines {\n\t\tif line != \"\" {\n\t\t\tlines[i] = prefix + line\n\t\t}\n\t}\n\treturn strings.Join(lines, \"\\n\")\n}\n\n// IndentString add spaces to beginning of each line.\nfunc IndentString(size int, text string) string {\n\tprefix := strings.Repeat(\" \", size)\n\treturn PrefixString(prefix, text)\n}\n\n// MissingString returns message if the text is empty.\nfunc MissingString(message, text string) string {\n\tif text == \"\" {\n\t\treturn message\n\t}\n\treturn text\n}\n\n// TemplateFuncMap is a text/template FuncMap.\nvar TemplateFuncMap = template.FuncMap{\n\t\"json\": func(v any) string {\n\t\tvar b bytes.Buffer\n\t\tenc := json.NewEncoder(&b)\n\t\tenc.SetEscapeHTML(false)\n\t\tif err := enc.Encode(v); err != nil {\n\t\t\tpanic(fmt.Errorf(\"failed to marshal as JSON: %+v: %w\", v, err))\n\t\t}\n\t\treturn strings.TrimSuffix(b.String(), \"\\n\")\n\t},\n\t\"yaml\": func(v any) string {\n\t\tvar b bytes.Buffer\n\t\tenc := yaml.NewEncoder(&b)\n\t\tif err := enc.Encode(v); err != nil {\n\t\t\tpanic(fmt.Errorf(\"failed to marshal as YAML: %+v: %w\", v, err))\n\t\t}\n\t\treturn \"---\\n\" + strings.TrimSuffix(b.String(), \"\\n\")\n\t},\n\t\"indent\": func(a ...any) (string, error) {\n\t\tif len(a) == 0 {\n\t\t\treturn \"\", errors.New(\"function takes at least one string argument\")\n\t\t}\n\t\tif len(a) > 2 {\n\t\t\treturn \"\", errors.New(\"function takes at most 2 arguments\")\n\t\t}\n\t\tvar ok bool\n\t\tsize := 2\n\t\tif len(a) > 1 {\n\t\t\tif size, ok = a[0].(int); !ok {\n\t\t\t\treturn \"\", errors.New(\"optional first argument must be an integer\")\n\t\t\t}\n\t\t}\n\t\ttext := \"\"\n\t\tif text, ok = a[len(a)-1].(string); !ok {\n\t\t\treturn \"\", errors.New(\"last argument must be a string\")\n\t\t}\n\t\treturn IndentString(size, text), nil\n\t},\n\t\"missing\": func(a ...any) (string, error) {\n\t\tif len(a) == 0 {\n\t\t\treturn \"\", errors.New(\"function takes at least one string argument\")\n\t\t}\n\t\tif len(a) > 2 {\n\t\t\treturn \"\", errors.New(\"function takes at most 2 arguments\")\n\t\t}\n\t\tvar ok bool\n\t\tmessage := \"<missing>\"\n\t\tif len(a) > 1 {\n\t\t\tif message, ok = a[0].(string); !ok {\n\t\t\t\treturn \"\", errors.New(\"optional first argument must be a string\")\n\t\t\t}\n\t\t}\n\t\ttext := \"\"\n\t\tif text, ok = a[len(a)-1].(string); !ok {\n\t\t\treturn \"\", errors.New(\"last argument must be a string\")\n\t\t}\n\t\treturn MissingString(message, text), nil\n\t},\n}\n\n// FuncHelp is help for TemplateFuncMap.\nvar FuncHelp = []string{\n\t\"indent <size>: add spaces to beginning of each line\",\n\t\"missing <message>: return message if the text is empty\",\n}\n"
  },
  {
    "path": "pkg/textutil/textutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage textutil\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"text/template\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestPrefixString(t *testing.T) {\n\tassert.Equal(t, \"\", PrefixString(\"- \", \"\"))\n\tassert.Equal(t, \"\\n\", PrefixString(\"- \", \"\\n\"))\n\tassert.Equal(t, \"- foo\", PrefixString(\"- \", \"foo\"))\n\tassert.Equal(t, \"- foo\\n- bar\\n\", PrefixString(\"- \", \"foo\\nbar\\n\"))\n\tassert.Equal(t, \"- foo\\n\\n- bar\\n\", PrefixString(\"- \", \"foo\\n\\nbar\\n\"))\n}\n\nfunc TestIndentString(t *testing.T) {\n\tassert.Equal(t, \"  foo\", IndentString(2, \"foo\"))\n\tassert.Equal(t, \"  foo\\n  bar\\n\", IndentString(2, \"foo\\nbar\\n\"))\n}\n\nfunc TestMissingString(t *testing.T) {\n\tassert.Equal(t, \"no\", MissingString(\"no\", \"\"))\n\tassert.Equal(t, \"msg\", MissingString(\"no\", \"msg\"))\n}\n\nfunc TestTemplateFuncs(t *testing.T) {\n\ttype X struct {\n\t\tFoo     int    `json:\"foo\" yaml:\"foo\"`\n\t\tBar     string `json:\"bar\" yaml:\"bar\"`\n\t\tMessage string `json:\"message,omitempty\" yaml:\"message,omitempty\"`\n\t\tMissing string `json:\"missing,omitempty\" yaml:\"missing,omitempty\"`\n\t}\n\tx := X{Foo: 42, Bar: \"hello\", Message: \"One\\nTwo\\nThree\", Missing: \"\"}\n\n\ttestCases := map[string]string{\n\t\t\"{{json .}}\": `{\"foo\":42,\"bar\":\"hello\",\"message\":\"One\\nTwo\\nThree\"}`,\n\t\t\"{{yaml .}}\": `---\nfoo: 42\nbar: hello\nmessage: |-\n  One\n  Two\n  Three`,\n\t\t`{{.Bar}}{{\"\\n\"}}{{.Message | missing \"<no message>\" | indent 2}}`: \"hello\\n  One\\n  Two\\n  Three\",\n\t\t`{{.Message | indent}}`:  \"  One\\n  Two\\n  Three\",\n\t\t`{{.Missing | missing}}`: \"<missing>\",\n\t}\n\n\tfor format, expected := range testCases {\n\t\ttmpl, err := template.New(\"format\").Funcs(TemplateFuncMap).Parse(format)\n\t\tassert.NilError(t, err)\n\t\tvar b bytes.Buffer\n\t\tassert.NilError(t, tmpl.Execute(&b, x))\n\t\tassert.Equal(t, expected, b.String())\n\t}\n}\n"
  },
  {
    "path": "pkg/uiutil/uiutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage uiutil\n\nimport (\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/AlecAivazis/survey/v2\"\n\t\"github.com/AlecAivazis/survey/v2/terminal\"\n\t\"github.com/mattn/go-isatty\"\n)\n\nvar InterruptErr = terminal.InterruptErr\n\n// Confirm is a regular text input that accept yes/no answers.\nfunc Confirm(message string, defaultParam bool) (bool, error) {\n\tvar ans bool\n\tprompt := &survey.Confirm{\n\t\tMessage: message,\n\t\tDefault: defaultParam,\n\t}\n\tif err := survey.AskOne(prompt, &ans); err != nil {\n\t\treturn false, err\n\t}\n\treturn ans, nil\n}\n\n// Select is a prompt that presents a list of various options\n// to the user for them to select using the arrow keys and enter.\nfunc Select(message string, options []string) (int, error) {\n\tvar ans int\n\tprompt := &survey.Select{\n\t\tMessage: message,\n\t\tOptions: options,\n\t}\n\tif err := survey.AskOne(prompt, &ans); err != nil {\n\t\treturn -1, err\n\t}\n\treturn ans, nil\n}\n\n// InputIsTTY returns true if reader is coming from stdin, and stdin is a terminal device,\n// not a regular file, stream, or pipe etc.\nfunc InputIsTTY(reader io.Reader) bool {\n\treturn reader == os.Stdin && (isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()))\n}\n\n// OutputIsTTY returns true if writer is going to stdout, and stdout is a terminal device,\n// not a regular file, stream, or pipe etc.\nfunc OutputIsTTY(writer io.Writer) bool {\n\t// This setting is needed so we can write integration tests for the TTY output.\n\t// It is probably not useful otherwise.\n\tif os.Getenv(\"_LIMA_OUTPUT_IS_TTY\") != \"\" {\n\t\treturn true\n\t}\n\treturn writer == os.Stdout && (isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()))\n}\n"
  },
  {
    "path": "pkg/usrlocal/usrlocal.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage usrlocal\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/lima-vm/lima/v2/pkg/debugutil\"\n\t\"github.com/lima-vm/lima/v2/pkg/limatype\"\n)\n\n// ExecutableViaArgs0 returns the absolute path to the executable used to start this process.\n// It will also append the file extension on Windows, if necessary.\n// This function is different from os.Executable(), which will use /proc/self/exe on Linux\n// and therefore will resolve any symlink used to locate the executable. This function will\n// return the symlink instead because we want to be able to locate ../share/lima relative\n// to the location of the symlink, and not the actual executable. This is important when\n// using Homebrew.\nvar ExecutableViaArgs0 = sync.OnceValues(func() (string, error) {\n\tif os.Args[0] == \"\" {\n\t\treturn \"\", errors.New(\"os.Args[0] has not been set\")\n\t}\n\texecutable, err := exec.LookPath(os.Args[0])\n\tif err == nil {\n\t\texecutable, err = filepath.Abs(executable)\n\t}\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"os.Args[0] is invalid: %w\", err)\n\t}\n\n\treturn executable, nil\n})\n\n// SelfDirs returns a list of directory paths where the current executable might be located.\n// It checks both os.Args[0] and os.Executable() methods and returns directories containing\n// the executable, resolving symlinks as needed.\nfunc SelfDirs() []string {\n\tvar selfPaths []string\n\n\tselfViaArgs0, err := ExecutableViaArgs0()\n\tif err != nil {\n\t\tlogrus.WithError(err).Warn(\"failed to find executable from os.Args[0]\")\n\t} else {\n\t\tselfPaths = append(selfPaths, filepath.Dir(selfViaArgs0))\n\t}\n\n\tselfViaOS, err := os.Executable()\n\tif err != nil {\n\t\tlogrus.WithError(err).Warn(\"failed to find os.Executable()\")\n\t} else {\n\t\tselfFinalPathViaOS, err := filepath.EvalSymlinks(selfViaOS)\n\t\tif err != nil {\n\t\t\tlogrus.WithError(err).Warn(\"failed to resolve symlinks\")\n\t\t\tselfFinalPathViaOS = selfViaOS // fallback to the original path\n\t\t}\n\n\t\tselfDir := filepath.Dir(selfFinalPathViaOS)\n\t\tif len(selfPaths) == 0 || selfDir != selfPaths[0] {\n\t\t\tselfPaths = append(selfPaths, selfDir)\n\t\t}\n\t}\n\n\treturn selfPaths\n}\n\nfunc delveDebugExe() string {\n\texe, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\texeBase := filepath.Base(exe)\n\tif strings.HasPrefix(exeBase, \"__debug_bin\") {\n\t\treturn exe\n\t}\n\treturn \"\"\n}\n\nfunc delveWorkspace() string {\n\tself := delveDebugExe()\n\tif self == \"\" {\n\t\treturn \"\"\n\t}\n\t// https://github.com/lima-vm/lima/pull/2651/commits/644c11373cb79aaebd8520706f7d51bd3ee5fbe4\n\t// launched by `~/go/bin/dlv dap`\n\t// - self: ${workspaceFolder}/cmd/limactl/__debug_bin_XXXXXX\n\treturn filepath.Dir(filepath.Dir(filepath.Dir(self)))\n}\n\n// ShareLima returns the <PREFIX>/share/lima directories.\nfunc ShareLima() ([]string, error) {\n\tvar candidates []string\n\tselfDirs := SelfDirs()\n\tfor _, selfDir := range selfDirs {\n\t\t// selfDir:  /usr/local/bin\n\t\t// prefix: /usr/local\n\t\t// candidate: /usr/local/share/lima\n\t\tprefix := filepath.Dir(selfDir)\n\t\tcandidate := filepath.Join(prefix, \"share\", \"lima\")\n\t\tif ents, err := os.ReadDir(candidate); err == nil && len(ents) > 0 {\n\t\t\tcandidates = append(candidates, candidate)\n\t\t}\n\t}\n\tif debugutil.Debug {\n\t\tif workspace := delveWorkspace(); workspace != \"\" {\n\t\t\t// https://github.com/lima-vm/lima/pull/2651/commits/644c11373cb79aaebd8520706f7d51bd3ee5fbe4\n\t\t\t// launched by `~/go/bin/dlv dap`\n\t\t\t// - self: ${workspaceFolder}/cmd/limactl/__debug_bin_XXXXXX\n\t\t\t// - agent: ${workspaceFolder}/_output/share/lima/lima-guestagent.Linux-x86_64\n\t\t\t// - dir:  ${workspaceFolder}/_output/share/lima\n\t\t\tcandidate := filepath.Join(workspace, \"_output\", \"share\", \"lima\")\n\t\t\tif ents, err := os.ReadDir(candidate); err == nil && len(ents) > 0 {\n\t\t\t\tcandidates = append(candidates, candidate)\n\t\t\t}\n\t\t}\n\t}\n\treturn candidates, nil\n}\n\n// GuestAgentBinary returns the absolute path of the guest agent binary, possibly with \".gz\" suffix.\nfunc GuestAgentBinary(ostype limatype.OS, arch limatype.Arch) (string, error) {\n\tif ostype == \"\" {\n\t\treturn \"\", errors.New(\"os must be set\")\n\t}\n\tif arch == \"\" {\n\t\treturn \"\", errors.New(\"arch must be set\")\n\t}\n\tshareLimaDirs, err := ShareLima()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, dir := range shareLimaDirs {\n\t\tuncomp := filepath.Join(dir, \"lima-guestagent.\"+ostype+\"-\"+arch)\n\t\tcomp := uncomp + \".gz\"\n\t\tvar res string\n\t\tres, err = chooseGABinary([]string{comp, uncomp})\n\t\tif err != nil {\n\t\t\tlogrus.Debug(err)\n\t\t\tcontinue\n\t\t}\n\t\treturn res, nil\n\t}\n\tif err == nil {\n\t\t// caller expects err to be comparable to fs.ErrNotExist\n\t\terr = fs.ErrNotExist\n\t}\n\treturn \"\", fmt.Errorf(\"guest agent binary could not be found for %s-%s: %w (Hint: try installing `lima-additional-guestagents` package)\", ostype, arch, err)\n}\n\nfunc chooseGABinary(candidates []string) (string, error) {\n\tvar entries []string\n\tfor _, f := range candidates {\n\t\tif _, err := os.Stat(f); err != nil {\n\t\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tlogrus.WithError(err).Warnf(\"failed to stat %q\", f)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tentries = append(entries, f)\n\t}\n\tswitch len(entries) {\n\tcase 0:\n\t\treturn \"\", fmt.Errorf(\"%w: attempted %v\", fs.ErrNotExist, candidates)\n\tcase 1:\n\t\treturn entries[0], nil\n\tdefault:\n\t\tlogrus.Warnf(\"multiple files found, choosing %q from %v; consider removing the other ones\",\n\t\t\tentries[0], candidates)\n\t\treturn entries[0], nil\n\t}\n}\n\n// LibexecLima returns the <PREFIX>/libexec/lima directories.\n// For Homebrew compatibility, it also checks <PREFIX>/lib/lima.\nfunc LibexecLima() ([]string, error) {\n\tvar candidates []string\n\tselfDirs := SelfDirs()\n\tfor _, selfDir := range selfDirs {\n\t\t// selfDir:  /usr/local/bin\n\t\t// prefix: /usr/local\n\t\t// candidate: /usr/local/libexec/lima\n\t\tprefix := filepath.Dir(selfDir)\n\t\tcandidate := filepath.Join(prefix, \"libexec\", \"lima\")\n\t\tif ents, err := os.ReadDir(candidate); err == nil && len(ents) > 0 {\n\t\t\tcandidates = append(candidates, candidate)\n\t\t}\n\t\t// selfDir: /opt/homebrew/bin\n\t\t// prefix: /opt/homebrew\n\t\t// candidate: /opt/homebrew/lib/lima\n\t\t//\n\t\t// Note that there is no /opt/homebrew/libexec directory,\n\t\t// as Homebrew reserves libexec for private use.\n\t\t// https://github.com/lima-vm/lima/issues/4295#issuecomment-3490680651\n\t\tcandidate = filepath.Join(prefix, \"lib\", \"lima\")\n\t\tif ents, err := os.ReadDir(candidate); err == nil && len(ents) > 0 {\n\t\t\tcandidates = append(candidates, candidate)\n\t\t}\n\t}\n\tif debugutil.Debug {\n\t\tif workspace := delveWorkspace(); workspace != \"\" {\n\t\t\tcandidate := filepath.Join(workspace, \"_output\", \"libexec\", \"lima\")\n\t\t\tif ents, err := os.ReadDir(candidate); err == nil && len(ents) > 0 {\n\t\t\t\tcandidates = append(candidates, candidate)\n\t\t\t}\n\t\t}\n\t}\n\treturn candidates, nil\n}\n"
  },
  {
    "path": "pkg/version/version.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage version\n\n// Version is filled on compilation time.\nvar Version = \"<unknown>\"\n"
  },
  {
    "path": "pkg/version/versionutil/versionutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage versionutil\n\nimport (\n\t\"strings\"\n\n\t\"github.com/coreos/go-semver/semver\"\n)\n\n// Parse parses a Lima version string by removing the leading \"v\" character and\n// stripping everything from the first \"-\" forward (which are `git describe` artifacts and\n// not semver pre-release markers) or cutting \".m\" suffix from exact version.\n// So \"v0.19.1-16-gf3dc6ed.m\" will be parsed as \"0.19.1\".\nfunc Parse(version string) (*semver.Version, error) {\n\tversion = strings.TrimPrefix(version, \"v\")\n\tversion, _, _ = strings.Cut(version, \"-\")\n\tversion = strings.TrimSuffix(version, \".m\")\n\treturn semver.NewVersion(version)\n}\n\nfunc compare(limaVersion, oldVersion string) int {\n\tif limaVersion == \"\" {\n\t\tif oldVersion == \"\" {\n\t\t\treturn 0\n\t\t}\n\t\treturn -1\n\t}\n\tversion, err := Parse(limaVersion)\n\tif err != nil {\n\t\treturn 1\n\t}\n\t// Handle Unparsable oldVersion gracefully - treat as 0.0.0 so Unparsable limaVersion is always greater\n\toldVer, err := semver.NewVersion(oldVersion)\n\tif err != nil {\n\t\treturn 1\n\t}\n\tcmp := version.Compare(*oldVer)\n\tif cmp == 0 && strings.Contains(limaVersion, \"-\") {\n\t\tcmp = 1\n\t}\n\treturn cmp\n}\n\n// GreaterThan returns true if the Lima version used to create an instance is greater\n// than a specific older version. Always returns false if the Lima version is the empty string.\n// Unparsable lima versions (like SHA1 commit ids) are treated as the latest version and return true.\n// limaVersion is a `github describe` string, not a semantic version. So \"0.19.1-16-gf3dc6ed.m\"\n// will be considered greater than \"0.19.1\".\nfunc GreaterThan(limaVersion, oldVersion string) bool {\n\treturn compare(limaVersion, oldVersion) > 0\n}\n\n// GreaterEqual return true if limaVersion >= oldVersion.\nfunc GreaterEqual(limaVersion, oldVersion string) bool {\n\treturn compare(limaVersion, oldVersion) >= 0\n}\n\n// LessThan returns true if limaVersion < oldVersion.\nfunc LessThan(limaVersion, oldVersion string) bool {\n\treturn !GreaterEqual(limaVersion, oldVersion)\n}\n"
  },
  {
    "path": "pkg/version/versionutil/versionutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage versionutil\n\nimport (\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestGreaterThan(t *testing.T) {\n\tassert.Equal(t, GreaterThan(\"\", \"0.1.0\"), false)\n\tassert.Equal(t, GreaterThan(\"0.0.1\", \"0.1.0\"), false)\n\tassert.Equal(t, GreaterThan(\"0.1.0\", \"0.1.0\"), false)\n\tassert.Equal(t, GreaterThan(\"0.1.0-2\", \"0.1.0\"), true)\n\tassert.Equal(t, GreaterThan(\"0.2.0\", \"0.1.0\"), true)\n\tassert.Equal(t, GreaterThan(\"abacab\", \"0.1.0\"), true)\n}\n\nfunc TestGreaterEqual(t *testing.T) {\n\tassert.Equal(t, GreaterEqual(\"\", \"\"), true)\n\tassert.Equal(t, GreaterEqual(\"\", \"0.1.0\"), false)\n\tassert.Equal(t, GreaterEqual(\"0.0.1\", \"0.1.0\"), false)\n\tassert.Equal(t, GreaterEqual(\"0.1.0\", \"0.1.0\"), true)\n\tassert.Equal(t, GreaterEqual(\"0.1.0-2\", \"0.1.0\"), true)\n\tassert.Equal(t, GreaterEqual(\"0.2.0\", \"0.1.0\"), true)\n\tassert.Equal(t, GreaterEqual(\"abacab\", \"0.1.0\"), true)\n}\n\nfunc TestParse(t *testing.T) {\n\tv1, err1 := Parse(\"v0.19.1-16-gf3dc6ed.m\")\n\tassert.NilError(t, err1)\n\tassert.Equal(t, v1.Major, int64(0))\n\tassert.Equal(t, v1.Minor, int64(19))\n\tassert.Equal(t, v1.Patch, int64(1))\n\n\tv2, err2 := Parse(\"v0.19.1.m\")\n\tassert.NilError(t, err2)\n\tassert.Equal(t, v2.Major, int64(0))\n\tassert.Equal(t, v2.Minor, int64(19))\n\tassert.Equal(t, v2.Patch, int64(1))\n}\n\nfunc TestCompareWithUnparsableOldVersion(t *testing.T) {\n\tassert.Equal(t, GreaterThan(\"1.0.0\", \"invalid-version\"), true)\n\tassert.Equal(t, GreaterThan(\"0.1.0\", \"<unknown>\"), true)\n\tassert.Equal(t, GreaterThan(\"0.0.1\", \"commit-hash-abc123\"), true)\n\n\tassert.Equal(t, GreaterEqual(\"1.0.0\", \"invalid-version\"), true)\n\tassert.Equal(t, GreaterEqual(\"0.1.0\", \"<unknown>\"), true)\n\tassert.Equal(t, GreaterEqual(\"0.0.1\", \"commit-hash-abc123\"), true)\n\n\tassert.Equal(t, GreaterThan(\"invalid-lima-version\", \"1.0.0\"), true)\n\tassert.Equal(t, GreaterEqual(\"invalid-lima-version\", \"1.0.0\"), true)\n\n\tassert.Equal(t, GreaterThan(\"invalid-lima-version\", \"invalid-old-version\"), true)\n\tassert.Equal(t, GreaterEqual(\"invalid-lima-version\", \"invalid-old-version\"), true)\n}\n"
  },
  {
    "path": "pkg/windows/process_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage windows\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os/exec\"\n)\n\ntype CommandLineJSON []struct {\n\tCommandLine string\n}\n\n// GetProcessCommandLine returns a slice of string containing all commandlines for a given process name.\nfunc GetProcessCommandLine(ctx context.Context, name string) ([]string, error) {\n\tout, err := exec.CommandContext(ctx,\n\t\t\"powershell.exe\",\n\t\t\"-nologo\",\n\t\t\"-noprofile\",\n\t\tfmt.Sprintf(\n\t\t\t`Get-CimInstance Win32_Process -Filter \"name = '%s'\" | Select CommandLine | ConvertTo-Json`,\n\t\t\tname,\n\t\t),\n\t).CombinedOutput()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar outJSON CommandLineJSON\n\tif err = json.Unmarshal(out, &outJSON); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal %q as %T: %w\", out, outJSON, err)\n\t}\n\n\tvar ret []string\n\tfor _, s := range outJSON {\n\t\tret = append(ret, s.CommandLine)\n\t}\n\n\treturn ret, nil\n}\n"
  },
  {
    "path": "pkg/windows/registry_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage windows\n\nimport (\n\t\"fmt\"\n\t\"math/rand/v2\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"golang.org/x/sys/windows/registry\"\n)\n\nconst (\n\tguestCommunicationsPrefix = `SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Virtualization\\GuestCommunicationServices`\n\tmagicVSOCKSuffix          = \"-facb-11e6-bd58-64006a7986d3\"\n\twslDistroInfoPrefix       = `SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Lxss`\n)\n\n// AddVSockRegistryKey makes a vsock server running on the host accessible in guests.\nfunc AddVSockRegistryKey(port int) error {\n\trootKey, err := getGuestCommunicationServicesKey(true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rootKey.Close()\n\n\tused, err := getUsedPorts(rootKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif slices.Contains(used, port) {\n\t\treturn fmt.Errorf(\"port %q in use\", port)\n\t}\n\n\tvsockKeyPath := fmt.Sprintf(`%x%s`, port, magicVSOCKSuffix)\n\tvSockKey, _, err := registry.CreateKey(\n\t\trootKey,\n\t\tvsockKeyPath,\n\t\tregistry.ALL_ACCESS,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\n\t\t\t\"failed to create new key (%s%s): %w\",\n\t\t\tguestCommunicationsPrefix,\n\t\t\tvsockKeyPath,\n\t\t\terr,\n\t\t)\n\t}\n\tdefer vSockKey.Close()\n\n\treturn nil\n}\n\n// RemoveVSockRegistryKey removes entries created by AddVSockRegistryKey.\nfunc RemoveVSockRegistryKey(port int) error {\n\trootKey, err := getGuestCommunicationServicesKey(true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rootKey.Close()\n\n\tvsockKeyPath := fmt.Sprintf(`%x%s`, port, magicVSOCKSuffix)\n\tif err := registry.DeleteKey(rootKey, vsockKeyPath); err != nil {\n\t\treturn fmt.Errorf(\n\t\t\t\"failed to create new key (%s%s): %w\",\n\t\t\tguestCommunicationsPrefix,\n\t\t\tvsockKeyPath,\n\t\t\terr,\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// IsVSockPortFree determines if a VSock port has been registered already.\nfunc IsVSockPortFree(port int) (bool, error) {\n\trootKey, err := getGuestCommunicationServicesKey(false)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer rootKey.Close()\n\n\tused, err := getUsedPorts(rootKey)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif slices.Contains(used, port) {\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\n// GetDistroID returns a DistroId GUID corresponding to a Lima instance name.\nfunc GetDistroID(name string) (string, error) {\n\trootKey, err := registry.OpenKey(\n\t\tregistry.CURRENT_USER,\n\t\twslDistroInfoPrefix,\n\t\tregistry.READ,\n\t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\n\t\t\t\"failed to open Lxss key (%s): %w\",\n\t\t\twslDistroInfoPrefix,\n\t\t\terr,\n\t\t)\n\t}\n\tdefer rootKey.Close()\n\n\tkeys, err := rootKey.ReadSubKeyNames(-1)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read subkey names for %s: %w\", wslDistroInfoPrefix, err)\n\t}\n\n\tvar out string\n\tfor _, k := range keys {\n\t\tsubKey, err := registry.OpenKey(\n\t\t\tregistry.CURRENT_USER,\n\t\t\tfmt.Sprintf(`%s\\%s`, wslDistroInfoPrefix, k),\n\t\t\tregistry.READ,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read subkey %q for key %q: %w\", k, wslDistroInfoPrefix, err)\n\t\t}\n\t\tdn, _, err := subKey.GetStringValue(\"DistributionName\")\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to read 'DistributionName' value for subkey %q of %q: %w\", k, wslDistroInfoPrefix, err)\n\t\t}\n\t\tif dn == name {\n\t\t\tout = k\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif out == \"\" {\n\t\treturn \"\", fmt.Errorf(\"failed to find matching DistroID for %q\", name)\n\t}\n\n\treturn out, nil\n}\n\n// GetRandomFreeVSockPort gets a list of all registered VSock ports and returns a non-registered port.\nfunc GetRandomFreeVSockPort(minPort, maxPort int) (int, error) {\n\trootKey, err := getGuestCommunicationServicesKey(false)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer rootKey.Close()\n\n\tused, err := getUsedPorts(rootKey)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\ttype pair struct{ v, offset int }\n\ttree := make([]pair, 1, len(used)+1)\n\ttree[0] = pair{0, minPort}\n\n\tslices.Sort(used)\n\tfor i, v := range used {\n\t\tif tree[len(tree)-1].v+tree[len(tree)-1].offset == v {\n\t\t\ttree[len(tree)-1].offset++\n\t\t} else {\n\t\t\ttree = append(tree, pair{v - minPort - i, minPort + i + 1})\n\t\t}\n\t}\n\n\tv := rand.IntN(maxPort - minPort + 1 - len(used))\n\n\tfor len(tree) > 1 {\n\t\tm := len(tree) / 2\n\t\tif v < tree[m].v {\n\t\t\ttree = tree[:m]\n\t\t} else {\n\t\t\ttree = tree[m:]\n\t\t}\n\t}\n\n\treturn tree[0].offset + v, nil\n}\n\n// getGuestCommunicationServicesKey returns the HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Virtualization\\GuestCommunicationServices\n// registry key for use in other operations.\n//\n// allowWrite is configurable because setting it to true requires Administrator access.\nfunc getGuestCommunicationServicesKey(allowWrite bool) (registry.Key, error) {\n\tvar registryPermissions uint32 = registry.READ\n\tif allowWrite {\n\t\tregistryPermissions = registry.WRITE | registry.READ\n\t}\n\trootKey, err := registry.OpenKey(\n\t\tregistry.LOCAL_MACHINE,\n\t\tguestCommunicationsPrefix,\n\t\tregistryPermissions,\n\t)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\n\t\t\t\"failed to open GuestCommunicationServices key (%s): %w\",\n\t\t\tguestCommunicationsPrefix,\n\t\t\terr,\n\t\t)\n\t}\n\n\treturn rootKey, nil\n}\n\nfunc getUsedPorts(key registry.Key) ([]int, error) {\n\tkeys, err := key.ReadSubKeyNames(-1)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read subkey names for %s: %w\", guestCommunicationsPrefix, err)\n\t}\n\n\tout := []int{}\n\tfor _, k := range keys {\n\t\tsplit := strings.Split(k, magicVSOCKSuffix)\n\t\tif len(split) == 2 {\n\t\t\ti, err := strconv.Atoi(split[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed convert %q to int: %w\", split[0], err)\n\t\t\t}\n\t\t\tout = append(out, i)\n\t\t}\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "pkg/windows/wsl_util_windows.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage windows\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// vmIDRegex is a regular expression to extract the VM ID from the command line of wslhost.exe.\nvar vmIDRegex = regexp.MustCompile(`--vm-id\\s\\{(?P<vmID>.{36})\\}`)\n\n// GetInstanceVMID returns the VM ID of a running WSL instance.\nfunc GetInstanceVMID(ctx context.Context, instanceName string) (string, error) {\n\tdistroID, err := GetDistroID(instanceName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmdLines, err := GetProcessCommandLine(ctx, \"wslhost.exe\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvmID := \"\"\n\tfor _, cmdLine := range cmdLines {\n\t\tif strings.Contains(cmdLine, distroID) {\n\t\t\tif matches := vmIDRegex.FindStringSubmatch(cmdLine); matches != nil {\n\t\t\t\tvmID = matches[vmIDRegex.SubexpIndex(\"vmID\")]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif vmID == \"\" {\n\t\treturn \"\", fmt.Errorf(\"failed to find VM ID for instance %q\", instanceName)\n\t}\n\n\treturn vmID, nil\n}\n"
  },
  {
    "path": "pkg/yqutil/fuzz_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage yqutil\n\nimport (\n\t\"testing\"\n)\n\nfunc FuzzEvaluateExpression(f *testing.F) {\n\tf.Fuzz(func(_ *testing.T, expression string, content []byte) {\n\t\t_, _ = EvaluateExpression(expression, content)\n\t})\n}\n"
  },
  {
    "path": "pkg/yqutil/yqutil.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage yqutil\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/google/yamlfmt\"\n\t\"github.com/google/yamlfmt/formatters/basic\"\n\t\"github.com/mikefarah/yq/v4/pkg/yqlib\"\n\t\"github.com/sirupsen/logrus\"\n\tlogging \"gopkg.in/op/go-logging.v1\"\n)\n\n// ValidateContent decodes the content yaml, to check it for syntax errors.\nfunc ValidateContent(content []byte) error {\n\tmemory := logging.NewMemoryBackend(0)\n\tbackend := logging.AddModuleLevel(memory)\n\tlogging.SetBackend(backend)\n\tyqlib.InitExpressionParser()\n\n\tdecoder := yqlib.NewYamlDecoder(yqlib.ConfiguredYamlPreferences)\n\n\treader := bytes.NewReader(content)\n\terr := decoder.Init(reader)\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = decoder.Decode()\n\tif errors.Is(err, io.EOF) {\n\t\treturn nil\n\t}\n\treturn err\n}\n\n// EvaluateExpressionWithEncoder evaluates the yq expression and returns the yq result using a custom encoder.\nfunc EvaluateExpressionWithEncoder(expression, content string, encoder yqlib.Encoder) (string, error) {\n\tif expression == \"\" {\n\t\treturn content, nil\n\t}\n\tlogrus.Debugf(\"Evaluating yq expression: %q\", expression)\n\tmemory := logging.NewMemoryBackend(0)\n\tbackend := logging.AddModuleLevel(memory)\n\tlogging.SetBackend(backend)\n\tyqlib.InitExpressionParser()\n\n\t// Disable access to environment variables and file loading functions\n\tyqlib.ConfiguredSecurityPreferences.DisableEnvOps = true\n\tyqlib.ConfiguredSecurityPreferences.DisableFileOps = true\n\n\tdecoder := yqlib.NewYamlDecoder(yqlib.ConfiguredYamlPreferences)\n\tout, err := yqlib.NewStringEvaluator().EvaluateAll(expression, content, encoder, decoder)\n\tif err != nil {\n\t\tlogger := logrus.StandardLogger()\n\t\tfor node := memory.Head(); node != nil; node = node.Next() {\n\t\t\tentry := logrus.NewEntry(logger).WithTime(node.Record.Time)\n\t\t\tprefix := fmt.Sprintf(\"[%s] \", node.Record.Module)\n\t\t\tmessage := prefix + node.Record.Message()\n\t\t\tswitch node.Record.Level {\n\t\t\tcase logging.CRITICAL:\n\t\t\t\tentry.Fatal(message)\n\t\t\tcase logging.ERROR:\n\t\t\t\tentry.Error(message)\n\t\t\tcase logging.WARNING:\n\t\t\t\tentry.Warn(message)\n\t\t\tcase logging.NOTICE:\n\t\t\t\tentry.Info(message)\n\t\t\tcase logging.INFO:\n\t\t\t\tentry.Info(message)\n\t\t\tcase logging.DEBUG:\n\t\t\t\tentry.Debug(message)\n\t\t\t}\n\t\t}\n\t\treturn \"\", err\n\t}\n\treturn out, nil\n}\n\n// EvaluateExpressionPlain evaluates the yq expression and returns the yq result.\nfunc EvaluateExpressionPlain(expression, content string, colorsEnabled bool) (string, error) {\n\tencoderPrefs := yqlib.ConfiguredYamlPreferences.Copy()\n\tencoderPrefs.Indent = 2\n\tencoderPrefs.ColorsEnabled = colorsEnabled\n\tencoder := yqlib.NewYamlEncoder(encoderPrefs)\n\treturn EvaluateExpressionWithEncoder(expression, content, encoder)\n}\n\n// EvaluateExpression evaluates the yq expression and returns the output formatted with yamlfmt.\nfunc EvaluateExpression(expression string, content []byte) ([]byte, error) {\n\tif expression == \"\" {\n\t\treturn content, nil\n\t}\n\tformatter, err := yamlfmtBasicFormatter()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// `ApplyFeatures()` is being called directly before passing content to `yqlib`.\n\t// This results in `ApplyFeatures()` being called twice with `FeatureApplyBefore`:\n\t// once here and once inside `formatter.Format`.\n\t// Currently, calling `ApplyFeatures()` with `FeatureApplyBefore` twice is not an issue,\n\t// but future changes to `yamlfmt` might cause problems if it is called twice.\n\t_, contentModified, err := formatter.Features.ApplyFeatures(context.Background(), content, yamlfmt.FeatureApplyBefore)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout, err := EvaluateExpressionPlain(expression, string(contentModified), false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn formatter.Format([]byte(out))\n}\n\nfunc Join(yqExprs []string) string {\n\treturn strings.Join(yqExprs, \" | \")\n}\n\nfunc yamlfmtBasicFormatter() (*basic.BasicFormatter, error) {\n\tfactory := basic.BasicFormatterFactory{}\n\tconfig := map[string]any{\n\t\t\"indentless_arrays\":         true,\n\t\t\"line_ending\":               \"lf\", // prefer LF even on Windows\n\t\t\"pad_line_comments\":         2,\n\t\t\"retain_line_breaks\":        true,\n\t\t\"retain_line_breaks_single\": false,\n\t}\n\n\tformatter, err := factory.NewFormatter(config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbasicFormatter, ok := formatter.(*basic.BasicFormatter)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unexpected formatter type: %T\", formatter)\n\t}\n\treturn basicFormatter, nil\n}\n"
  },
  {
    "path": "pkg/yqutil/yqutil_test.go",
    "content": "// SPDX-FileCopyrightText: Copyright The Lima Authors\n// SPDX-License-Identifier: Apache-2.0\n\npackage yqutil\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gotest.tools/v3/assert\"\n)\n\nfunc TestValidateContent(t *testing.T) {\n\tcontent := `\n# comment\nfoo: bar\n`\n\terr := ValidateContent([]byte(content))\n\tassert.NilError(t, err)\n}\n\nfunc TestValidateContentError(t *testing.T) {\n\tcontent := `\n- foo: bar\n  foo\n  bar\n`\n\terr := ValidateContent([]byte(content))\n\tassert.ErrorContains(t, err, \"could not find expected\")\n}\n\nfunc TestEvaluateExpressionEmpty(t *testing.T) {\n\texpression := \"\"\n\tcontent := `\nfoo: bar\n`\n\texpected := content\n\tout, err := EvaluateExpression(expression, []byte(content))\n\tassert.NilError(t, err)\n\tassert.Equal(t, expected, string(out))\n}\n\nfunc TestEvaluateExpressionSimple(t *testing.T) {\n\texpression := `.cpus = 2 | .memory = \"2GiB\"`\n\tcontent := `\n# CPUs\ncpus: null\n\n# Memory size\nmemory: null\n`\n\t// Note: yq currently removes empty lines, but not comments\n\texpected := `\n# CPUs\ncpus: 2\n\n# Memory size\nmemory: 2GiB\n`\n\tout, err := EvaluateExpression(expression, []byte(content))\n\tassert.NilError(t, err)\n\tassert.Equal(t, expected, string(out))\n}\n\nfunc TestEvaluateExpressionComplex(t *testing.T) {\n\texpression := `.mounts += {\"location\": \"foo\", \"mountPoint\": \"bar\"}`\n\tcontent := `\n# Expose host directories to the guest, the mount point might be accessible from all UIDs in the guest\n# 🟢 Builtin default: null (Mount nothing)\n# 🔵 This file: Mount the home as read-only\nmounts:\n- location: \"~\"\n  # Configure the mountPoint inside the guest.\n  # 🟢 Builtin default: value of location\n  mountPoint: null\n`\n\t// Note: yq will use canonical yaml, with indented sequences\n\t// Note: yq will not explicitly quote strings, when not needed\n\t// Note: yamlfmt will fix indentation of sequences\n\texpected := `\n# Expose host directories to the guest, the mount point might be accessible from all UIDs in the guest\n# 🟢 Builtin default: null (Mount nothing)\n# 🔵 This file: Mount the home as read-only\nmounts:\n- location: \"~\"\n  # Configure the mountPoint inside the guest.\n  # 🟢 Builtin default: value of location\n  mountPoint: null\n- location: foo\n  mountPoint: bar\n`\n\tout, err := EvaluateExpression(expression, []byte(content))\n\tassert.NilError(t, err)\n\tassert.Equal(t, expected, string(out))\n}\n\nfunc TestEvaluateExpressionError(t *testing.T) {\n\texpression := `arch: aarch64`\n\t_, err := EvaluateExpression(expression, []byte(\"\"))\n\tassert.ErrorContains(t, err, \"invalid input text\")\n}\n\nfunc TestEvaluateMergeExpression(t *testing.T) {\n\texpression := `select(di == 0) * select(di == 1)`\n\tcontent := `\nyolo: true\nfoo:\n  bar: 1\n  baz: 2\n---\nfoo:\n  bar: 3\n  fomo: false\n`\n\texpected := `\nyolo: true\nfoo:\n  bar: 3\n  baz: 2\n  fomo: false\n`\n\tout, err := EvaluateExpression(expression, []byte(strings.TrimSpace(content)))\n\tassert.NilError(t, err)\n\tassert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out)))\n}\n\nfunc TestJoin(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    []string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"multiple values\",\n\t\t\tinput:    []string{\"foo\", \"bar\", \"baz\"},\n\t\t\texpected: \"foo | bar | baz\",\n\t\t},\n\t\t{\n\t\t\tname:     \"one value\",\n\t\t\tinput:    []string{\"foo\"},\n\t\t\texpected: \"foo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty values\",\n\t\t\tinput:    []string{},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"nil values\",\n\t\t\tinput:    nil,\n\t\t\texpected: \"\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tactual := Join(test.input)\n\t\t\tassert.Equal(t, test.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "templates/README.md",
    "content": "## Quick usage\n\nTo create and start a new instance from the template `fedora`:\n```bash\n# Note: In Lima 1.x, template URLs included leading slashes, e.g. `template://fedora`.\nlimactl create template:fedora\nlimactl start fedora\n```\nor\n```bash\nlimactl start template:fedora\n# For the second time onward, just run `limactl start fedora`\n```\n\nTo open a shell:\n```bash\nlimactl shell fedora\n```\nor\n```bash\nexport LIMA_INSTANCE=fedora\nlima\n```\n\n## Template list\n\n⭐ = [\"Tier 1\"](#tier)\n\n☆ = [\"Tier 2\"](#tier)\n\n### Default\n\n- [`default`](./default.yaml) (⭐Ubuntu, with containerd/nerdctl)\n\n### Linux distributions\n\n- [`almalinux-8`](./almalinux-8.yaml): AlmaLinux 8\n- [`almalinux-9`](./almalinux-9.yaml): AlmaLinux 9\n- [`almalinux-10`](./almalinux-10.yaml), `almalinux`: AlmaLinux 10\n- [`almalinux-kitten-10`](./almalinux-kitten-10.yaml), `almalinux-kitten`: AlmaLinux Kitten 10\n- [`alpine`](./alpine.yaml): ☆Alpine Linux\n- [`alpine-iso`](./alpine-iso.yaml): ☆Alpine Linux (ISO9660 image). Compatible with the `alpine` template used in Lima prior to v1.0.\n- [`archlinux`](./archlinux.yaml): ☆Arch Linux\n- [`centos-stream-9`](./centos-stream-9.yaml), `centos-stream`: CentOS Stream 9\n- [`centos-stream-10`](./centos-stream-10.yaml): CentOS Stream 10\n- [`debian-11`](./debian-11.yaml): Debian GNU/Linux 11 (Bullseye)\n- [`debian-12`](./debian-12.yaml): Debian GNU/Linux 12 (Bookworm)\n- [`debian-13`](./debian-13.yaml), `debian`: ⭐Debian GNU/Linux 13 (Trixie)\n- [`fedora-41`](./fedora-41.yaml): Fedora 41\n- [`fedora-42`](./fedora-42.yaml): Fedora 42\n- [`fedora-43`](./fedora-43.yaml), `fedora`: ⭐Fedora 43\n- [`opensuse-leap-15`](./opensuse-leap-15.yaml): openSUSE Leap 15\n- [`opensuse-leap-16`](./opensuse-leap-16.yaml), `opensuse-leap`, `opensuse`: ⭐openSUSE Leap 16\n- [`oraclelinux-8`](./oraclelinux-8.yaml): Oracle Linux 8\n- [`oraclelinux-9`](./oraclelinux-9.yaml): Oracle Linux 9\n- [`oraclelinux-10`](./oraclelinux-10.yaml), `oraclelinux`: Oracle Linux 10\n- [`rocky-8`](./rocky-8.yaml): Rocky Linux 8\n- [`rocky-9`](./rocky-9.yaml): Rocky Linux 9\n- [`rocky-10`](./rocky-10.yaml), `rocky`: Rocky Linux 10\n- [`ubuntu-20.04`](./ubuntu-20.04.yaml): Ubuntu 20.04 LTS (Focal Fossa)\n- [`ubuntu-22.04`](./ubuntu-22.04.yaml): Ubuntu 22.04 LTS (Jammy Jellyfish)\n- [`ubuntu-24.04`](./ubuntu-24.04.yaml), `ubuntu-lts`: Ubuntu 24.04 LTS (Noble Numbat)\n- [`ubuntu-24.10`](./ubuntu-24.10.yaml): Ubuntu 24.10 (Oracular Oriole)\n- [`ubuntu-25.04`](./ubuntu-25.04.yaml): Ubuntu 25.04 (Plucky Puffin)\n- [`ubuntu-25.10`](./ubuntu-25.10.yaml), `ubuntu`: Ubuntu 25.10 (Questing Quokka)\n  - Same as `default` but comment lines are omitted from the YAML\n- [`experimental/ubuntu-next`](./experimental/ubuntu-next.yaml): Ubuntu vNext\n- [`experimental/gentoo`](./experimental/gentoo.yaml): Gentoo\n- [`experimental/opensuse-tumbleweed`](./experimental/opensuse-tumbleweed.yaml): openSUSE Tumbleweed\n- [`experimental/debian-sid`](./experimental/debian-sid.yaml): Debian Sid\n- [`experimental/fedora-rawhide`](./experimental/fedora-rawhide.yaml): Fedora Rawhide\n\n### Non-Linux\n\nNOTE: support for non-Linux OSes is [experimental](https://lima-vm.io/docs/releases/experimental/).\n\n- [`macos-15`](./macos-15.yaml): [macOS](https://lima-vm.io/docs/usage/guests/macos/) 15 (Sequoia)\n- [`macos-26`](./macos-26.yaml), `macos`: [macOS](https://lima-vm.io/docs/usage/guests/macos/) 26 (Tahoe)\n- [`freebsd-15`](./freebsd-15.yaml), `freebsd`: [FreeBSD](https://lima-vm.io/docs/usage/guests/freebsd/) 15\n- [`experimental/freebsd-current`](./experimental/freebsd-current.yaml): [FreeBSD](https://lima-vm.io/docs/usage/guests/freebsd/) CURRENT\n\n### Alternative package managers\n\n- [`linuxbrew`](./linuxbrew.yaml), `homebrew-linux`: [Homebrew](https://brew.sh) on Linux (Ubuntu)\n- [`homebrew-macos`](./homebrew-macos.yaml): [Homebrew](https://brew.sh) on macOS\n\n### Containers\n\n#### Container engines\n\n- [`apptainer`](./apptainer.yaml): [Apptainer](https://lima-vm.io/docs/examples/containers/apptainer/)\n- [`apptainer-rootful`](./apptainer-rootful.yaml): [Apptainer](https://lima-vm.io/docs/examples/containers/apptainer/) (rootful)\n- [`docker`](./docker.yaml): ⭐[Docker](https://lima-vm.io/docs/examples/containers/docker/)\n- [`docker-rootful`](./docker-rootful.yaml): [Docker](https://lima-vm.io/docs/examples/containers/docker/) (rootful)\n- [`podman`](./podman.yaml): [Podman](https://lima-vm.io/docs/examples/containers/podman/)\n- [`podman-rootful`](./podman-rootful.yaml): [Podman](https://lima-vm.io/docs/examples/containers/podman/) (rootful)\n- LXD is installed in the default Ubuntu template, so there is no `lxd`\n\n#### Container image builders\n\n- [`buildkit`](./buildkit.yaml): BuildKit\n\n#### Container orchestration\n\n- [`faasd`](./faasd.yaml): [Faasd](https://docs.openfaas.com/deployment/edge/)\n- [`k0s`](./k0s.yaml): [k0s](https://k0sproject.io/) Zero Friction Kubernetes\n- [`k3s`](./k3s.yaml): Kubernetes via k3s\n- [`k8s`](./k8s.yaml): ⭐[Kubernetes](https://lima-vm.io/docs/examples/containers/kubernetes/) via kubeadm\n- [`experimental/rke2`](./experimental/rke2.yaml): RKE2\n- [`experimental/u7s`](./experimental/u7s.yaml): [Usernetes](https://github.com/rootless-containers/usernetes): Rootless Kubernetes\n\n### AI agents\n\nSee <https://lima-vm.io/docs/examples/ai/>.\n\n### Optional feature enablers\n\n- [`experimental/vnc`](./experimental/vnc.yaml): use vnc display and xorg server\n- [`experimental/alsa`](./experimental/alsa.yaml): use alsa and default audio device\n\n### Lost+found\n\n<details>\n<p>\n\n- `centos`: Removed in Lima v0.8.0, as CentOS 8 reached [EOL](https://www.centos.org/centos-linux-eol/).\n  Replaced by [`almalinux`](./almalinux.yaml), [`centos-stream`](./centos-stream.yaml), [`oraclelinux`](./oraclelinux.yaml),\n  and [`rocky`](./rocky.yaml).\n- `singularity`: Moved to [`apptainer-rootful`](./apptainer-rootful.yaml) in Lima v0.13.0, as Singularity was renamed to Apptainer.\n- `experimental/apptainer`: Moved to [`apptainer`](./apptainer.yaml) in Lima v0.13.0.\n- `experimental/{almalinux,centos-stream-9,oraclelinux,rocky}-9`: Moved to [`almalinux-9`](./almalinux-9.yaml), [`centos-stream-9`](./centos-stream-9.yaml),\n  [`oraclelinux-9`](./oraclelinux-9.yaml), and [`rocky-9`](./rocky-9.yaml) in Lima v0.13.0.\n- `nomad`: Removed in Lima v0.17.1, as Nomad is [no longer free software](https://github.com/hashicorp/nomad/commit/b3e30b1dfa185d9437a25830522da47b91f78816)\n- `centos-stream-8`: Remove in Lima v0.23.0, as CentOS Stream 8 reached [EOL](https://blog.centos.org/2023/04/end-dates-are-coming-for-centos-stream-8-and-centos-linux-7/).\n- `deprecated/centos-7`: Remove in Lima v0.23.0, as CentOS 7 reached [EOL](https://blog.centos.org/2023/04/end-dates-are-coming-for-centos-stream-8-and-centos-linux-7/).\n- `experimental/vz`: Merged into the default template in Lima v1.0. See also <https://lima-vm.io/docs/config/vmtype/>.\n- `experimental/armv7l`: Merged into the `default` template in Lima v1.0. Use `limactl create --arch=armv7l template:default`.\n- `experimental/riscv64`: Merged into the `default` template in Lima v1.0. Use `limactl create --arch=riscv64 template:default`.\n- `vmnet`: Removed in Lima v1.0. Use `limactl create --network=lima:shared template:default` instead. See also <https://lima-vm.io/docs/config/network/>.\n- `experimental/net-user-v2`: Removed in Lima v1.0. Use `limactl create --network=lima:user-v2 template:default` instead. See also <https://lima-vm.io/docs/config/network/>.\n- `experimental/9p`: Removed in Lima v1.0. Use `limactl create --vm-type=qemu --mount-type=9p template:default` instead. See also <https://lima-vm.io/docs/config/mount/>.\n- `experimental/virtiofs-linux`: Removed in Lima v1.0. Use `limactl create --mount-type=virtiofs-linux template:default` instead. See also <https://lima-vm.io/docs/config/mount/>.\n\n</p>\n</details>\n\n## Tier\n\n- \"Tier 1\" (marked with ⭐): Good stability. Regularly tested on the CI.\n- \"Tier 2\" (marked with ☆): Moderate stability. Regularly tested on the CI.\n\nOther templates are tested only occasionally and manually.\n"
  },
  {
    "path": "templates/_default/mounts.yaml",
    "content": "mounts:\n- location: \"~\"\n\n# # /tmp/lima is no longer mounted by default since Lima v2.0.\n# - location: \"{{.GlobalTempDir}}/lima\"\n#  mountPoint: /tmp/lima\n#  writable: true\n"
  },
  {
    "path": "templates/_images/almalinux-10.yaml",
    "content": "images:\n- location: \"https://repo.almalinux.org/almalinux/10.1/cloud/x86_64/images/AlmaLinux-10-GenericCloud-10.1-20251125.0.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:8729862a9e2d93c478c2334b33d01d7ee21934a795a9b68550cc1c63ffd0efae\"\n- location: \"https://repo.almalinux.org/almalinux/10.1/cloud/aarch64/images/AlmaLinux-10-GenericCloud-10.1-20251125.0.aarch64.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:6aebd793c098d40e5ca119aee7531bb49c3b1bbaccedf8ab6c92cd7299ce1c6e\"\n- location: \"https://repo.almalinux.org/almalinux/10.1/cloud/s390x/images/AlmaLinux-10-GenericCloud-10.1-20251125.0.s390x.qcow2\"\n  arch: \"s390x\"\n  digest: \"sha256:8e42c18ada02c8db72f51ed90a4a4db8ef7503b64dfd6431d4d7e62295ca6819\"\n- location: \"https://repo.almalinux.org/almalinux/10.1/cloud/ppc64le/images/AlmaLinux-10-GenericCloud-10.1-20251125.0.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha256:ec4fb138e6543ab51d4184369dc49bb9a779d2fec98c37a9bf42fc24f9389888\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://repo.almalinux.org/almalinux/10/cloud/x86_64/images/AlmaLinux-10-GenericCloud-latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://repo.almalinux.org/almalinux/10/cloud/aarch64/images/AlmaLinux-10-GenericCloud-latest.aarch64.qcow2\n  arch: aarch64\n\n- location: https://repo.almalinux.org/almalinux/10/cloud/s390x/images/AlmaLinux-10-GenericCloud-latest.s390x.qcow2\n  arch: s390x\n\n- location: https://repo.almalinux.org/almalinux/10/cloud/ppc64le/images/AlmaLinux-10-GenericCloud-latest.ppc64le.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/almalinux-8.yaml",
    "content": "# NOTE: EL8-based distros are known not to work on M1 chips: https://github.com/lima-vm/lima/issues/841\n# EL9-based distros are known to work.\nimages:\n- location: https://repo.almalinux.org/almalinux/8.10/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.10-20240819.x86_64.qcow2\n  arch: x86_64\n  digest: sha256:669bd580dcef5491d4dfd5724d252cce7cde1b2b33a3ca951e688d71386875e3\n\n- location: https://repo.almalinux.org/almalinux/8.10/cloud/aarch64/images/AlmaLinux-8-GenericCloud-8.10-20240819.aarch64.qcow2\n  arch: aarch64\n  digest: sha256:cec6736cbc562d06895e218b6f022621343c553bfa79192ca491381b4636c7b8\n\n- location: https://repo.almalinux.org/almalinux/8.10/cloud/s390x/images/AlmaLinux-8-GenericCloud-8.10-20240819.s390x.qcow2\n  arch: s390x\n  digest: sha256:7f8866a4247ad57c81f5d2c5a0fa64940691f9df1e858a1510d34a0de008eb16\n\n- location: https://repo.almalinux.org/almalinux/8.10/cloud/ppc64le/images/AlmaLinux-8-GenericCloud-8.10-20240819.ppc64le.qcow2\n  arch: ppc64le\n  digest: sha256:01acccc74009ebe8ddb13ea6a313479b85a372cb5b898f9090a12a2bd2d62520\n\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://repo.almalinux.org/almalinux/8/cloud/aarch64/images/AlmaLinux-8-GenericCloud-latest.aarch64.qcow2\n  arch: aarch64\n\n- location: https://repo.almalinux.org/almalinux/8/cloud/s390x/images/AlmaLinux-8-GenericCloud-latest.s390x.qcow2\n  arch: s390x\n\n- location: https://repo.almalinux.org/almalinux/8/cloud/ppc64le/images/AlmaLinux-8-GenericCloud-latest.ppc64le.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n\ncpuType:\n  # Workaround for \"vmx_write_mem: mmu_gva_to_gpa XXXXXXXXXXXXXXXX failed\" on Intel Mac\n  # https://bugs.launchpad.net/qemu/+bug/1838390\n  x86_64: Haswell-v4\n"
  },
  {
    "path": "templates/_images/almalinux-9.yaml",
    "content": "images:\n- location: \"https://repo.almalinux.org/almalinux/9.7/cloud/x86_64/images/AlmaLinux-9-GenericCloud-9.7-20251118.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:5ff9c048859046f41db4a33b1f1a96675711288078aac66b47d0be023af270d1\"\n- location: \"https://repo.almalinux.org/almalinux/9.7/cloud/aarch64/images/AlmaLinux-9-GenericCloud-9.7-20251118.aarch64.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:c6c09af3b5be62e0ca82ccfafe0b1de9be90890fa8fddbd6118fa8b76b36de2d\"\n- location: \"https://repo.almalinux.org/almalinux/9.7/cloud/s390x/images/AlmaLinux-9-GenericCloud-9.7-20251118.s390x.qcow2\"\n  arch: \"s390x\"\n  digest: \"sha256:fdd52f3fae45fe47fe258bf77f63c4b29c9b8870b74166490bdaefe9a6717685\"\n- location: \"https://repo.almalinux.org/almalinux/9.7/cloud/ppc64le/images/AlmaLinux-9-GenericCloud-9.7-20251118.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha256:f16abd0ff12ba54f43c6a5675546cc72bc8f4a76bf7852e645ccf54cca1e8827\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://repo.almalinux.org/almalinux/9/cloud/aarch64/images/AlmaLinux-9-GenericCloud-latest.aarch64.qcow2\n  arch: aarch64\n\n- location: https://repo.almalinux.org/almalinux/9/cloud/s390x/images/AlmaLinux-9-GenericCloud-latest.s390x.qcow2\n  arch: s390x\n\n- location: https://repo.almalinux.org/almalinux/9/cloud/ppc64le/images/AlmaLinux-9-GenericCloud-latest.ppc64le.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/almalinux-kitten-10.yaml",
    "content": "images:\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/x86_64/images/AlmaLinux-Kitten-GenericCloud-10-20251215.0.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:897a47031eb7cc240915afebe60f0485535b38cc77a9d72d4c7a5626a74bb246\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/aarch64/images/AlmaLinux-Kitten-GenericCloud-10-20251215.0.aarch64.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:98c9c64488ae243b8165db1a6af9fd54f2b2dce9335e7940ec15a34adee968db\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/s390x/images/AlmaLinux-Kitten-GenericCloud-10-20241227.0.s390x.qcow2\"\n  arch: \"s390x\"\n  digest: \"sha256:b4174bd71f59346bc35c32b3743d13e0a06458f203cc3736580cc9f8d28f6725\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/ppc64le/images/AlmaLinux-Kitten-GenericCloud-10-20251215.0.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha256:5ea5dc019a2dcb37edff42b39fa6597f6e9f72d7dbc579564b0badeaf1770727\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/riscv64/images/AlmaLinux-Kitten-GenericCloud-10-20260302.0.riscv64.qcow2\"\n  arch: \"riscv64\"\n  digest: \"sha256:857a43a08016731b2c84483ebac862a0ad327394fd6687f0f04c3b7c547b00d2\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/x86_64/images/AlmaLinux-Kitten-GenericCloud-10-latest.x86_64.qcow2\"\n  arch: \"x86_64\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/aarch64/images/AlmaLinux-Kitten-GenericCloud-10-latest.aarch64.qcow2\"\n  arch: \"aarch64\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/s390x/images/AlmaLinux-Kitten-GenericCloud-10-latest.s390x.qcow2\"\n  arch: \"s390x\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/ppc64le/images/AlmaLinux-Kitten-GenericCloud-10-latest.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n- location: \"https://kitten.repo.almalinux.org/10-kitten/cloud/riscv64/images/AlmaLinux-Kitten-GenericCloud-10-latest.riscv64.qcow2\"\n  arch: \"riscv64\"\nmountTypesUnsupported: [\"9p\"]\n"
  },
  {
    "path": "templates/_images/alpine-iso.yaml",
    "content": "# Using the Alpine 3.22 aarch64 image with vmType=vz requires macOS Ventura 13.3 or later.\nimages:\n- location: https://github.com/lima-vm/alpine-lima/releases/download/v0.2.47/alpine-lima-std-3.23.0-x86_64.iso\n  arch: x86_64\n  digest: sha512:c71e21dfb152642dd79af281497f86e7f690998997f787307978d83594e5e47addbc61e7d8ee405b0afc4230688de9eeb98fa44d6e74654e8d9d8b70151fb8da\n\n- location: https://github.com/lima-vm/alpine-lima/releases/download/v0.2.47/alpine-lima-std-3.23.0-aarch64.iso\n  arch: aarch64\n  digest: sha512:44659e71c1e277361bc10ecc813fba799d0371c2bc291db811e05e43429fd31aaa7ebe185331d02dccadccddfe9d54184376cdceeb1c444b58e3e9a4e690ce33\n"
  },
  {
    "path": "templates/_images/alpine.yaml",
    "content": "images:\n- location: \"https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/cloud/nocloud_alpine-3.23.3-x86_64-uefi-cloudinit-r0.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha512:9b61666394dcbeb65d3b3ab0864f5ab6c992c32094622333c5b37b83021fd359cc41eb3fa00abf5cdbe25ee2cef600577a7b23eb6f1a103cc32aff862ef88862\"\n- location: \"https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/cloud/nocloud_alpine-3.23.3-aarch64-uefi-cloudinit-r0.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha512:7a3cdfaefb0cbf3bb6824cd6ae80d6a3e0b0e367609e5fc50c5f714374d31e8d70dd607094a4e9cfe2e6ead7537781782469fe93bcea79611fbce4ddeacc92e1\"\n"
  },
  {
    "path": "templates/_images/archlinux.yaml",
    "content": "images:\n# Try to use yyyyMMdd.REV image if available. Note that yyyyMMdd.REV will be removed after several months.\n\n- location: \"https://geo.mirror.pkgbuild.com/images/v20260315.500742/Arch-Linux-x86_64-cloudimg-20260315.500742.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:74403af7ca9ec85766f0143d33b5b7a6717413a6a0fb9c276d403e21f057cd72\"\n- location: https://github.com/mcginty/arch-boxes-arm/releases/download/v20220323/Arch-Linux-aarch64-cloudimg-20220323.0.qcow2\n  arch: aarch64\n  digest: sha512:27524910bf41cb9b3223c8749c6e67fd2f2fdb8b70d40648708e64d6b03c0b4a01b3c5e72d51fefd3e0c3f58487dbb400a79ca378cde2da341a3a19873612be8\n\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2\n  arch: x86_64\n"
  },
  {
    "path": "templates/_images/centos-stream-10.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-20260309.0.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:3306948108e387cea0e4967fe4241b36ce6475e89a88bc057c7ce54998c6bf43\"\n- location: \"https://cloud.centos.org/centos/10-stream/aarch64/images/CentOS-Stream-GenericCloud-10-20260309.0.aarch64.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:aa8f51021f717afc094d67be68d32b3bd8c3934978f8f398cb99092f85c7304c\"\n- location: \"https://cloud.centos.org/centos/10-stream/s390x/images/CentOS-Stream-GenericCloud-10-20260309.0.s390x.qcow2\"\n  arch: \"s390x\"\n  digest: \"sha256:a1f1ddc6133de5878f99e43f9c660e2b6f166b1331a8bba1e990d5e5fdabda33\"\n- location: \"https://cloud.centos.org/centos/10-stream/ppc64le/images/CentOS-Stream-GenericCloud-10-20260309.0.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha256:98901665a7d74c6726fb2a3f10cd13371ed292d067f51cb11d0cdd5f0cfb2935\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://cloud.centos.org/centos/10-stream/aarch64/images/CentOS-Stream-GenericCloud-10-latest.aarch64.qcow2\n  arch: aarch64\n\n- location: https://cloud.centos.org/centos/10-stream/s390x/images/CentOS-Stream-GenericCloud-10-latest.s390x.qcow2\n  arch: s390x\n\n- location: https://cloud.centos.org/centos/10-stream/ppc64le/images/CentOS-Stream-GenericCloud-10-latest.ppc64le.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n\nfirmware:\n  # CentOS Stream 10 still requires legacyBIOS\n  # https://issues.redhat.com/browse/CS-2672\n  legacyBIOS: true\n"
  },
  {
    "path": "templates/_images/centos-stream-9.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-20260309.0.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:988b081695a9f5249bb76a891d5144fde8dede7c6722397db4e160f3c15400d9\"\n- location: \"https://cloud.centos.org/centos/9-stream/aarch64/images/CentOS-Stream-GenericCloud-9-20260309.0.aarch64.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:018c6186fc738fc1dd409cad4a576d2e92bb49bd3f251cf174316d4ceab1a9ad\"\n- location: \"https://cloud.centos.org/centos/9-stream/s390x/images/CentOS-Stream-GenericCloud-9-20260309.0.s390x.qcow2\"\n  arch: \"s390x\"\n  digest: \"sha256:6496fc69f518fe4524a571e66a9b7533f8f7579482c62bdb49a02e7ab328f3c2\"\n- location: \"https://cloud.centos.org/centos/9-stream/ppc64le/images/CentOS-Stream-GenericCloud-9-20260309.0.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha256:5c524c42f5cba1e5d5b82eca26e8333f140bcd36707249f77bff2d34b7c06b7e\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://cloud.centos.org/centos/9-stream/aarch64/images/CentOS-Stream-GenericCloud-9-latest.aarch64.qcow2\n  arch: aarch64\n\n- location: https://cloud.centos.org/centos/9-stream/s390x/images/CentOS-Stream-GenericCloud-9-latest.s390x.qcow2\n  arch: s390x\n\n- location: https://cloud.centos.org/centos/9-stream/ppc64le/images/CentOS-Stream-GenericCloud-9-latest.ppc64le.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n\nfirmware:\n  # CentOS Stream 9 still requires legacyBIOS, while AlmaLinux 9 and Rocky Linux 9 do not.\n  legacyBIOS: true\n"
  },
  {
    "path": "templates/_images/debian-11.yaml",
    "content": "# DEPRECATED: Debian 11 (Bullseye) is deprecated. Use debian-12 (Bookworm) or debian-13 (Trixie) instead.\nimages:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud.debian.org/images/cloud/bullseye/20260224-2398/debian-11-genericcloud-amd64-20260224-2398.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha512:72ea2b99c912b1c01aae342945d61065de797d39a4f7c6cf252fe80a83cb54eae36ec8afe7b6a2eb77317a481a17ebf13e26939dba673e90f7cc551f93108866\"\n- location: \"https://cloud.debian.org/images/cloud/bullseye/20260224-2398/debian-11-genericcloud-arm64-20260224-2398.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha512:39551bc6b64b44a64933ee5073748fecd0f6ac7e6cbbcf33d3bb2a5aefc687beaf6cb2ebc4bdc004f440a8f2a2746fb750707f461f85d388bdc65b21b92b624b\"\n- location: \"https://cloud.debian.org/images/cloud/bullseye/20250703-2162/debian-11-genericcloud-ppc64el-20250703-2162.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha512:71f0039d3ae46504d189f009f91fb9fa9873153d250969b56a4656d75caec17356705237d7926153e2c10d9251dc4909ec34a2afa7256a9359df279cbde8d679\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-amd64.qcow2\n  arch: x86_64\n\n- location: https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-arm64.qcow2\n  arch: aarch64\n\n- location: https://cloud.debian.org/images/cloud/bullseye/latest/debian-11-genericcloud-ppc64el.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n\n# debian-11 seems incompatible with vz\n# https://github.com/lima-vm/lima/issues/2855\nvmType: qemu\n"
  },
  {
    "path": "templates/_images/debian-12.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud.debian.org/images/cloud/bookworm/20260316-2418/debian-12-genericcloud-amd64-20260316-2418.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha512:9c9363e1b0fa5ba57974b98b108f5170a56e021e91e7ddc4d8f094909edecf8374f77b1eb1ce277e57a63128fe13071f6b5d2b16aa6aab68a0760bcbf627a52d\"\n- location: \"https://cloud.debian.org/images/cloud/bookworm/20260316-2418/debian-12-genericcloud-arm64-20260316-2418.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha512:26b8edf24cc8464f334b1dbce6fcc8c789d14258d07ad3c226fdaa7068b665d78fa9888a12557f4e21e3b76ae3eaec069d8183cbec6b5bc145c02513025cd706\"\n- location: \"https://cloud.debian.org/images/cloud/bookworm/20250703-2162/debian-12-genericcloud-ppc64el-20250703-2162.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha512:fcf25e5d0e6a76e93c3abaecb8109a931380af29658586f51e30baec0f699a2f17de8a14de57acec80b13ab125fd165140893d814800144505a4982492256f19\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2\n  arch: x86_64\n\n- location: https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-arm64.qcow2\n  arch: aarch64\n\n- location: https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-ppc64el.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/debian-13.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud.debian.org/images/cloud/trixie/20260316-2418/debian-13-genericcloud-amd64-20260316-2418.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha512:0e5edfbe49b0cca779a4a7dc9738f34c92e3ff481ee1f7d5c4e93e180654fe275eb8c96397224c6ca04a2910eaaed27489f431573ebe4cb5412ef257888b2b18\"\n- location: \"https://cloud.debian.org/images/cloud/trixie/20260316-2418/debian-13-genericcloud-arm64-20260316-2418.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha512:400d1651c2e6e5467736553cc67c91f06fdb6b4316d60ea45aa6a6e5dd91499048f7240628bc95c2a3d3d74bb3ed1f7e0df7d3698d1e3960e6eff2fbbee6b149\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud.debian.org/images/cloud/trixie/daily/latest/debian-13-genericcloud-amd64-daily.qcow2\n  arch: x86_64\n\n- location: https://cloud.debian.org/images/cloud/trixie/daily/latest/debian-13-genericcloud-arm64-daily.qcow2\n  arch: aarch64\n\n- location: https://cloud.debian.org/images/cloud/trixie/daily/latest/debian-13-genericcloud-ppc64el-daily.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/fedora-41.yaml",
    "content": "images:\n- location: https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-41-1.4.x86_64.qcow2\n  arch: x86_64\n  digest: sha256:6205ae0c524b4d1816dbd3573ce29b5c44ed26c9fbc874fbe48c41c89dd0bac2\n\n- location: https://download.fedoraproject.org/pub/fedora/linux/releases/41/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-41-1.4.aarch64.qcow2\n  arch: aarch64\n  digest: sha256:085883b42c7e3b980e366a1fe006cd0ff15877f7e6e984426f3c6c67c7cc2faa\n\n- location: https://download.fedoraproject.org/pub/alt/risc-v/release/41/Cloud/riscv64/images/Fedora-Cloud-Base-Generic-41.20250224-1026a2d0e311.riscv64.qcow2\n  arch: riscv64\n  digest: sha256:6a8272a858d7f1498f49ce362b34f0b9b959885f63285158947e045abfeece40\n\n# 9p is broken in Linux v6.9, v6.10, and v6.11 (used by Fedora 41).\n# The issue was fixed in Linux v6.12-rc5 (https://github.com/torvalds/linux/commit/be2ca38).\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/fedora-42.yaml",
    "content": "images:\n- location: https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-42-1.1.x86_64.qcow2\n  arch: x86_64\n  digest: sha256:e401a4db2e5e04d1967b6729774faa96da629bcf3ba90b67d8d9cce9906bec0f\n\n- location: https://download.fedoraproject.org/pub/fedora/linux/releases/42/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-42-1.1.aarch64.qcow2\n  arch: aarch64\n  digest: sha256:e10658419a8d50231037dc781c3155aa94180a8c7a74e5cac2a6b09eaa9342b7\n\n- location: https://download.fedoraproject.org/pub/alt/risc-v/release/42/Cloud/riscv64/images/Fedora-Cloud-Base-Generic-42.20250414-8635a3a5bfcd.riscv64.qcow2\n  arch: riscv64\n  digest: sha256:537c67710f4f1c9112fecaafafc293b649acd1d35b46619b97b5a5a0132241b0\n\n# # NOTE: Intel Mac with macOS prior to 15.5 requires setting vmType to qemu\n# # https://github.com/lima-vm/lima/issues/3334\n# vmType: qemu\n"
  },
  {
    "path": "templates/_images/fedora-43.yaml",
    "content": "images:\n- location: \"https://download.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/x86_64/images/Fedora-Cloud-Base-Generic-43-1.6.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:846574c8a97cd2d8dc1f231062d73107cc85cbbbda56335e264a46e3a6c8ab2f\"\n\n- location: \"https://download.fedoraproject.org/pub/fedora/linux/releases/43/Cloud/aarch64/images/Fedora-Cloud-Base-Generic-43-1.6.aarch64.qcow2\"\n  arch: aarch64\n  digest: \"sha256:66031aea9ec61e6d0d5bba12b9454e80ca94e8a79c913d37ded4c60311705b8b\"\n\n# No RISC-V release yet for Fedora 43: https://download.fedoraproject.org/pub/alt/risc-v/release/\n\nssh:\n  # ssh.overVsock does not work with Fedora 43 due to a SELinux policy issue\n  # https://github.com/lima-vm/lima/issues/4334#issuecomment-3561294333\n  # avc:  denied  { getattr } for  pid=1355 comm=\"sshd-auth\" scontext=system_u:system_r:sshd_auth_t:s0-s0:c0.c1023 tcontext=system_u:system_r:sshd_t:s0-s0:c0.c1023 tclass=vsock_socket permissive=1\n  overVsock: false\n\n# # NOTE: Intel Mac with macOS prior to 15.5 requires setting vmType to qemu\n# # https://github.com/lima-vm/lima/issues/3334\n# vmType: qemu\n"
  },
  {
    "path": "templates/_images/freebsd-15.yaml",
    "content": "# The FreeBSD-Archive URLs are used for permanence. They don't seem to support HTTPS though.\nimages:\n- 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\"\n  digest: sha256:45febaec571bc9f5917ecd7833d8cff3f7cb049c1d13ad99ce3067a6c9a17b25\n  arch: x86_64\n- 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\"\n  digest: sha256:1490d8f5071a146e75154cf7177570a59c2964d81224b90a162ceaa6f459aabf\n  arch: aarch64\nos: FreeBSD\n# Apparently does not boot with vz\nvmType: qemu\n"
  },
  {
    "path": "templates/_images/macos-15.yaml",
    "content": "# Apple Software License Agreements: https://www.apple.com/legal/sla/\n# (macOS users should have already accepted the agreements on their host machines)\n\nimages:\n- location: \"https://updates.cdn-apple.com/2025SummerFCS/fullrestores/093-10809/CFD6DD38-DAF0-40DA-854F-31AAD1294C6F/UniversalMac_15.6.1_24G90_Restore.ipsw\"\n  arch: \"aarch64\"\n  digest: \"sha256:3d87686b691ac765eb6a6b3082b2334e2af9710096a00432dd519af89ff2ea78\"\nos: Darwin\narch: aarch64\n# vz is the only supported VM type for macOS guests\nvmType: vz\n"
  },
  {
    "path": "templates/_images/macos-26.yaml",
    "content": "# Apple Software License Agreements: https://www.apple.com/legal/sla/\n# (macOS users should have already accepted the agreements on their host machines)\n\nimages:\n- location: \"https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-88313/2E098049-1731-4415-A206-546D09301973/UniversalMac_26.3.1_25D2128_Restore.ipsw\"\n  arch: \"aarch64\"\n  digest: \"sha256:4ff171ba3fa6db4758a2a7cedc8f24d44f4e3de8051a84400984872619abd6bc\"\nos: Darwin\narch: aarch64\n# vz is the only supported VM type for macOS guests\nvmType: vz\n"
  },
  {
    "path": "templates/_images/opensuse-leap-15.yaml",
    "content": "images:\n# Hint: run `limactl prune` to invalidate the Current cache\n\n- location: https://download.opensuse.org/distribution/leap/15.6/appliances/openSUSE-Leap-15.6-Minimal-VM.x86_64-Cloud.qcow2\n  arch: x86_64\n\n- location: https://download.opensuse.org/distribution/leap/15.6/appliances/openSUSE-Leap-15.6-Minimal-VM.aarch64-Cloud.qcow2\n  arch: aarch64\n\n# Hint: to allow 9p and virtiofs, replace the `kernel-default-base` package with `kernel-default` and reboot the VM.\n# https://github.com/lima-vm/lima/issues/3055\nmountType: reverse-sshfs\n\nmountTypesUnsupported: [9p, virtiofs]\n"
  },
  {
    "path": "templates/_images/opensuse-leap-16.yaml",
    "content": "images:\n# Hint: run `limactl prune` to invalidate the Current cache\n\n- location: https://download.opensuse.org/distribution/leap/16.0/appliances/Leap-16.0-Minimal-VM.x86_64-Cloud.qcow2\n  arch: x86_64\n\n- location: https://download.opensuse.org/distribution/leap/16.0/appliances/Leap-16.0-Minimal-VM.aarch64-Cloud.qcow2\n  arch: aarch64\n\n- location: https://download.opensuse.org/distribution/leap/16.0/appliances/Leap-16.0-Minimal-VM.s390x-s390x-Cloud.qcow2\n  arch: s390x\n"
  },
  {
    "path": "templates/_images/oraclelinux-10.yaml",
    "content": "# Oracle image license: https://www.oracle.com/downloads/licenses/oracle-linux-license.html\n# Image source: https://yum.oracle.com/oracle-linux-templates.html\n\nimages:\n- location: \"https://yum.oracle.com/templates/OracleLinux/OL10/u1/x86_64/OL10U1_x86_64-kvm-b270.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:65077d1363f107cd750cdea26c73868c2128b5ed778ee93f0873aa2999228765\"\n- location: \"https://yum.oracle.com/templates/OracleLinux/OL10/u1/aarch64/OL10U1_aarch64-kvm-cloud-b154.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:7a4033cc90e22facd85c34641393f646c38e5a5f5c091b0c809f3c7d13b6b81b\"\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/oraclelinux-8.yaml",
    "content": "# Oracle image license: https://www.oracle.com/downloads/licenses/oracle-linux-license.html\n# Image source: https://yum.oracle.com/oracle-linux-templates.html\n\n# Unlike AlmaLinux 8 and Rocky Linux 8, Oracle Linux 8 is known to work on Apple M1.\n\nimages:\n- location: \"https://yum.oracle.com/templates/OracleLinux/OL8/u10/x86_64/OL8U10_x86_64-kvm-b271.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:23c72a22201b80c98195212e205c2ec0e2a641dfd5f37374dfe6e4f0639ef311\"\n- location: \"https://yum.oracle.com/templates/OracleLinux/OL8/u10/aarch64/OL8U10_aarch64-kvm-cloud-b162.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:7894656f52b81a32ca3e8de31b69f40fc149b5630a9fff592bab6cc708a837af\"\nmountTypesUnsupported: [9p]\n\nfirmware:\n  # Oracle Linux 8 still requires legacyBIOS, while AlmaLinux 8 and Rocky Linux 8 do not.\n  legacyBIOS: true\n\ncpuType:\n  # Workaround for vmx_write_mem: mmu_gva_to_gpa XXXXXXXXXXXXXXXX failed on Intel Mac\n  # https://bugs.launchpad.net/qemu/+bug/1838390\n  x86_64: Haswell-v4\n"
  },
  {
    "path": "templates/_images/oraclelinux-9.yaml",
    "content": "# Oracle image license: https://www.oracle.com/downloads/licenses/oracle-linux-license.html\n# Image source: https://yum.oracle.com/oracle-linux-templates.html\n\nimages:\n- location: \"https://yum.oracle.com/templates/OracleLinux/OL9/u7/x86_64/OL9U7_x86_64-kvm-b269.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:88c75cf913a66227e9ce74b0087ecac4cce1883f3e5649082e982d0d00310f1c\"\n- location: \"https://yum.oracle.com/templates/OracleLinux/OL9/u7/aarch64/OL9U7_aarch64-kvm-cloud-b158.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:725e2ef14b2de72b5b09c84ca0119e5de06cc0c68a68aa677dccdd2fcaeaff9f\"\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/rocky-10.yaml",
    "content": "images:\n- location: \"https://dl.rockylinux.org/pub/rocky/10.0/images/x86_64/Rocky-10-GenericCloud-Base-10.0-20250609.1.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:20e771c654724e002c32fb92a05fdfdd7ac878c192f50e2fc21f53e8f098b8f9\"\n- location: \"https://dl.rockylinux.org/pub/rocky/10.0/images/aarch64/Rocky-10-GenericCloud-Base-10.0-20250609.1.aarch64.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:326264421955473a3576feff35076b7a7ef4bf2a14b5f6d238b7ec65c0426fbc\"\n- location: \"https://dl.rockylinux.org/pub/rocky/10.0/images/ppc64le/Rocky-10-GenericCloud-Base-10.0-20250609.1.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha256:aba0ecaf13afccc90e30388eb61d89071bac26818f06e815c6d764f5ccd9bef4\"\n- location: \"https://dl.rockylinux.org/pub/rocky/10.0/images/s390x/Rocky-10-GenericCloud-Base-10.0-20250609.1.s390x.qcow2\"\n  arch: \"s390x\"\n  digest: \"sha256:ecaf7c23f64f4c229a851cd9e263d3b31b4a877e9a01a420d27d20e341c3e681\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://dl.rockylinux.org/pub/rocky/10/images/x86_64/Rocky-10-GenericCloud-Base.latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://dl.rockylinux.org/pub/rocky/10/images/aarch64/Rocky-10-GenericCloud-Base.latest.aarch64.qcow2\n  arch: aarch64\n\n- location: https://dl.rockylinux.org/pub/rocky/10/images/ppc64le/Rocky-10-GenericCloud-Base.latest.ppc64le.qcow2\n  arch: ppc64le\n\n- location: https://dl.rockylinux.org/pub/rocky/10/images/s390x/Rocky-10-GenericCloud-Base.latest.s390x.qcow2\n  arch: s390x\n\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/rocky-8.yaml",
    "content": "# NOTE: EL8-based distros are known not to work on M1 chips: https://github.com/lima-vm/lima/issues/841\n# EL9-based distros are known to work.\n\nimages:\n- location: https://dl.rockylinux.org/pub/rocky/8.10/images/x86_64/Rocky-8-GenericCloud-Base-8.10-20240528.0.x86_64.qcow2\n  arch: x86_64\n  digest: sha256:e56066c58606191e96184de9a9183a3af33c59bcbd8740d8b10ca054a7a89c14\n\n- location: https://dl.rockylinux.org/pub/rocky/8.10/images/aarch64/Rocky-8-GenericCloud-Base-8.10-20240528.0.aarch64.qcow2\n  arch: aarch64\n  digest: sha256:946b5b9845aa5e3ed98f1bc6ee9873201712a2aef01b87731aed16857e0ca13f\n\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-GenericCloud.latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://dl.rockylinux.org/pub/rocky/8/images/aarch64/Rocky-8-GenericCloud.latest.aarch64.qcow2\n  arch: aarch64\n\nmountTypesUnsupported: [9p]\n\ncpuType:\n  # Workaround for vmx_write_mem: mmu_gva_to_gpa XXXXXXXXXXXXXXXX failed on Intel Mac\n  # https://bugs.launchpad.net/qemu/+bug/1838390\n  x86_64: Haswell-v4\n"
  },
  {
    "path": "templates/_images/rocky-9.yaml",
    "content": "images:\n- location: \"https://dl.rockylinux.org/pub/rocky/9.6/images/x86_64/Rocky-9-GenericCloud-Base-9.6-20250531.0.x86_64.qcow2\"\n  arch: \"x86_64\"\n  digest: \"sha256:2c72815bb83cadccbede4704780e9b52033722db8a45c3fb02130aa380690a3d\"\n- location: \"https://dl.rockylinux.org/pub/rocky/9.6/images/aarch64/Rocky-9-GenericCloud-Base-9.6-20250531.0.aarch64.qcow2\"\n  arch: \"aarch64\"\n  digest: \"sha256:3776b2c17cc011c28e2ab440c49dfba8d2be214d8b85df2b5edc97ebdeb30e4a\"\n- location: \"https://dl.rockylinux.org/pub/rocky/9.6/images/ppc64le/Rocky-9-GenericCloud-Base-9.6-20250531.0.ppc64le.qcow2\"\n  arch: \"ppc64le\"\n  digest: \"sha256:03dcfa25cef7b372506b34a5c30892b1d88ad48bc2670493519ab03088febedd\"\n- location: \"https://dl.rockylinux.org/pub/rocky/9.6/images/s390x/Rocky-9-GenericCloud-Base-9.6-20250531.0.s390x.qcow2\"\n  arch: \"s390x\"\n  digest: \"sha256:f4aa994c02fd831c0f83f4f2f7d95e410a1ca3df3902472059e6fbe8fab883fa\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2\n  arch: x86_64\n\n- location: https://dl.rockylinux.org/pub/rocky/9/images/aarch64/Rocky-9-GenericCloud.latest.aarch64.qcow2\n  arch: aarch64\n\n- location: https://dl.rockylinux.org/pub/rocky/9/images/ppc64le/Rocky-9-GenericCloud.latest.ppc64le.qcow2\n  arch: ppc64le\n\n- location: https://dl.rockylinux.org/pub/rocky/9/images/s390x/Rocky-9-GenericCloud.latest.s390x.qcow2\n  arch: s390x\n\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/ubuntu-20.04.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud-images.ubuntu.com/releases/focal/release-20250624/ubuntu-20.04-server-cloudimg-amd64.img\"\n  arch: \"x86_64\"\n  digest: \"sha256:18f2977d77dfea1b74aee14533bd21c34f789139e949c57023b7364894b7e5e9\"\n- location: \"https://cloud-images.ubuntu.com/releases/focal/release-20250624/ubuntu-20.04-server-cloudimg-arm64.img\"\n  arch: \"aarch64\"\n  digest: \"sha256:d4603cd783e2578f838c63f7c3a8dae6b554bd6358870f2c8eb182aa51ade465\"\n- location: \"https://cloud-images.ubuntu.com/releases/focal/release-20250624/ubuntu-20.04-server-cloudimg-armhf.img\"\n  arch: \"armv7l\"\n  digest: \"sha256:5dba684cb4d143b0f7105c21449155cbf9b8c77505d09a96c7ff104ea76bc943\"\n- location: \"https://cloud-images.ubuntu.com/releases/focal/release-20250624/ubuntu-20.04-server-cloudimg-s390x.img\"\n  arch: \"s390x\"\n  digest: \"sha256:48e42f4dc1e4f539b0a1a78078a6ffb4fa48d9ea4bb8a7086d5e33af9452122a\"\n- location: \"https://cloud-images.ubuntu.com/releases/focal/release-20250624/ubuntu-20.04-server-cloudimg-ppc64el.img\"\n  arch: \"ppc64le\"\n  digest: \"sha256:47c32128a4c96985e2a44a8b8539c560ab06cd26e1276fe73502fc4ff8849e18\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.img\n  arch: x86_64\n\n- location: https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-arm64.img\n  arch: aarch64\n\n- location: https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-armhf.img\n  arch: armv7l\n\n- location: https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-s390x.img\n  arch: s390x\n\n- location: https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-ppc64el.img\n  arch: ppc64le\n"
  },
  {
    "path": "templates/_images/ubuntu-22.04.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-amd64.img\"\n  arch: \"x86_64\"\n  digest: \"sha256:e66ef756881b5e682c496112201382abd76291797a7395bf81fd1bd0888f5b6f\"\n- location: \"https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-arm64.img\"\n  arch: \"aarch64\"\n  digest: \"sha256:0bdc35735c490ed1cf04f93b104fdf19eff7e5f4777d4dfdf777bc374ac32db8\"\n- location: \"https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-riscv64.img\"\n  arch: \"riscv64\"\n  digest: \"sha256:a0ecbcdcebc0a6c7855babd348071293a675a11513cff9eea63f75b829125c58\"\n- location: \"https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-armhf.img\"\n  arch: \"armv7l\"\n  digest: \"sha256:5a0c7b132ff344daadc9cd4d4bbd11d432eef6f343b1e65855e37b9a2d1a3db5\"\n- location: \"https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-s390x.img\"\n  arch: \"s390x\"\n  digest: \"sha256:c5a30253941e784c93f975f2c755f9ceb813b7c818088c400b083be921bc2aea\"\n- location: \"https://cloud-images.ubuntu.com/releases/jammy/release-20260227/ubuntu-22.04-server-cloudimg-ppc64el.img\"\n  arch: \"ppc64le\"\n  digest: \"sha256:c194954b0f5c64b9fc49bb64b24261a5765708f6275c0aa2cf9a4c7a98223d5e\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img\n  arch: x86_64\n\n- location: https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-arm64.img\n  arch: aarch64\n\n- location: https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-riscv64.img\n  arch: riscv64\n\n- location: https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-armhf.img\n  arch: armv7l\n\n- location: https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-s390x.img\n  arch: s390x\n\n- location: https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-ppc64el.img\n  arch: ppc64le\n"
  },
  {
    "path": "templates/_images/ubuntu-24.04.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud-images.ubuntu.com/releases/noble/release-20260225/ubuntu-24.04-server-cloudimg-amd64.img\"\n  arch: \"x86_64\"\n  digest: \"sha256:7aa6d9f5e8a3a55c7445b138d31a73d1187871211b2b7da9da2e1a6cbf169b21\"\n- location: \"https://cloud-images.ubuntu.com/releases/noble/release-20260225/ubuntu-24.04-server-cloudimg-arm64.img\"\n  arch: \"aarch64\"\n  digest: \"sha256:99e1d482b958e6bfd0183a4c48ce6dc334e09a3e29a4560f6f5ff85593d09d1d\"\n- location: \"https://cloud-images.ubuntu.com/releases/noble/release-20260225/ubuntu-24.04-server-cloudimg-riscv64.img\"\n  arch: \"riscv64\"\n  digest: \"sha256:4e2561ac874eef1ce3e013133bbd88d2379564546faf2a75fa5b467acc742723\"\n- location: \"https://cloud-images.ubuntu.com/releases/noble/release-20260225/ubuntu-24.04-server-cloudimg-armhf.img\"\n  arch: \"armv7l\"\n  digest: \"sha256:813d945c7ea5e6419fc96217d55e36352d60f1505c6f9e71560237fa5688bc41\"\n- location: \"https://cloud-images.ubuntu.com/releases/noble/release-20260225/ubuntu-24.04-server-cloudimg-s390x.img\"\n  arch: \"s390x\"\n  digest: \"sha256:056b356d1afae04ec92b58f93976b10299a0a672e786ebee1c5794937ddb3649\"\n- location: \"https://cloud-images.ubuntu.com/releases/noble/release-20260225/ubuntu-24.04-server-cloudimg-ppc64el.img\"\n  arch: \"ppc64le\"\n  digest: \"sha256:291872476e51216ca54b95184ae648fab2d8d579338a076d1785e83b1c273aa6\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-amd64.img\n  arch: x86_64\n\n- location: https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-arm64.img\n  arch: aarch64\n\n- location: https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-riscv64.img\n  arch: riscv64\n\n- location: https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-armhf.img\n  arch: armv7l\n\n- location: https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-s390x.img\n  arch: s390x\n\n- location: https://cloud-images.ubuntu.com/releases/noble/release/ubuntu-24.04-server-cloudimg-ppc64el.img\n  arch: ppc64le\n"
  },
  {
    "path": "templates/_images/ubuntu-24.10.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-amd64.img\"\n  arch: \"x86_64\"\n  digest: \"sha256:69f31d3208895e5f646e345fbc95190e5e311ecd1359a4d6ee2c0b6483ceca03\"\n- location: \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-arm64.img\"\n  arch: \"aarch64\"\n  digest: \"sha256:3d1d134d66318f982d32f02aec00fe879bfeb0338147b4038a25d1f9cddb527f\"\n- location: \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-riscv64.img\"\n  arch: \"riscv64\"\n  digest: \"sha256:0803a88c98fe7f511f379ce378751ce4f435e24da756540583b5dc269116d83a\"\n- location: \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-armhf.img\"\n  arch: \"armv7l\"\n  digest: \"sha256:818741e7c423248fdc18d503beea094acad9ed1710ba14e5df3fae54f2a76b72\"\n- location: \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-s390x.img\"\n  arch: \"s390x\"\n  digest: \"sha256:ae5bd8d899b6b9908c76b0416a3e82e899654c6048d0eea8e18159a51e01b774\"\n- location: \"https://cloud-images.ubuntu.com/releases/oracular/release-20250701/ubuntu-24.10-server-cloudimg-ppc64el.img\"\n  arch: \"ppc64le\"\n  digest: \"sha256:e78b9b8df911926de80f4fbeb9465256c77ac1da7c1b058d236b603fc4eb755f\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud-images.ubuntu.com/releases/oracular/release/ubuntu-24.10-server-cloudimg-amd64.img\n  arch: x86_64\n\n- location: https://cloud-images.ubuntu.com/releases/oracular/release/ubuntu-24.10-server-cloudimg-arm64.img\n  arch: aarch64\n\n- location: https://cloud-images.ubuntu.com/releases/oracular/release/ubuntu-24.10-server-cloudimg-riscv64.img\n  arch: riscv64\n\n- location: https://cloud-images.ubuntu.com/releases/oracular/release/ubuntu-24.10-server-cloudimg-armhf.img\n  arch: armv7l\n\n- location: https://cloud-images.ubuntu.com/releases/oracular/release/ubuntu-24.10-server-cloudimg-s390x.img\n  arch: s390x\n\n- location: https://cloud-images.ubuntu.com/releases/oracular/release/ubuntu-24.10-server-cloudimg-ppc64el.img\n  arch: ppc64le\n\n# 9p is broken in Linux v6.9, v6.10, and v6.11 (used by Ubuntu 24.10).\n# The issue was fixed in Linux v6.12-rc5 (https://github.com/torvalds/linux/commit/be2ca38).\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/_images/ubuntu-25.04.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud-images.ubuntu.com/releases/plucky/release-20260114/ubuntu-25.04-server-cloudimg-amd64.img\"\n  arch: \"x86_64\"\n  digest: \"sha256:534cbf0c44e86862535502f853829cefb771d19991892a31d14827d985829612\"\n- location: \"https://cloud-images.ubuntu.com/releases/plucky/release-20260114/ubuntu-25.04-server-cloudimg-arm64.img\"\n  arch: \"aarch64\"\n  digest: \"sha256:202945537a70805a41901e767d0ecb8b2badd824f9441b43880b98b63f963d63\"\n- location: \"https://cloud-images.ubuntu.com/releases/plucky/release-20260114/ubuntu-25.04-server-cloudimg-riscv64.img\"\n  arch: \"riscv64\"\n  digest: \"sha256:7a35a907290669cedb76090522b2d2f3882293a74eaafabb644e53a83d0257f7\"\n- location: \"https://cloud-images.ubuntu.com/releases/plucky/release-20260114/ubuntu-25.04-server-cloudimg-armhf.img\"\n  arch: \"armv7l\"\n  digest: \"sha256:17570e4fb1217383908d46fc3d2dfd5cd452fdd571979bdeb4d34b2bd5282d72\"\n- location: \"https://cloud-images.ubuntu.com/releases/plucky/release-20260114/ubuntu-25.04-server-cloudimg-s390x.img\"\n  arch: \"s390x\"\n  digest: \"sha256:89b9f739d983ee86eaab29fbfb3d8a99c8d656aeaaa421a30044ca8eaa8302d7\"\n- location: \"https://cloud-images.ubuntu.com/releases/plucky/release-20260114/ubuntu-25.04-server-cloudimg-ppc64el.img\"\n  arch: \"ppc64le\"\n  digest: \"sha256:e5780e30b9d35d5392fb3dd4f67a3fb10b27ea602017d62116166de4382b4d57\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud-images.ubuntu.com/releases/plucky/release/ubuntu-25.04-server-cloudimg-amd64.img\n  arch: x86_64\n\n- location: https://cloud-images.ubuntu.com/releases/plucky/release/ubuntu-25.04-server-cloudimg-arm64.img\n  arch: aarch64\n\n- location: https://cloud-images.ubuntu.com/releases/plucky/release/ubuntu-25.04-server-cloudimg-riscv64.img\n  arch: riscv64\n\n- location: https://cloud-images.ubuntu.com/releases/plucky/release/ubuntu-25.04-server-cloudimg-armhf.img\n  arch: armv7l\n\n- location: https://cloud-images.ubuntu.com/releases/plucky/release/ubuntu-25.04-server-cloudimg-s390x.img\n  arch: s390x\n\n- location: https://cloud-images.ubuntu.com/releases/plucky/release/ubuntu-25.04-server-cloudimg-ppc64el.img\n  arch: ppc64le\n\n# # NOTE: Intel Mac with macOS prior to 15.5 requires setting vmType to qemu\n# # https://github.com/lima-vm/lima/issues/3334\n# vmType: qemu\n"
  },
  {
    "path": "templates/_images/ubuntu-25.10.yaml",
    "content": "images:\n# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.\n\n- location: \"https://cloud-images.ubuntu.com/releases/questing/release-20260226/ubuntu-25.10-server-cloudimg-amd64.img\"\n  arch: \"x86_64\"\n  digest: \"sha256:3a4f0a9c628e23ac0db996c91ba2609658668601f93836cbd3d32cbd594dac05\"\n- location: \"https://cloud-images.ubuntu.com/releases/questing/release-20260226/ubuntu-25.10-server-cloudimg-arm64.img\"\n  arch: \"aarch64\"\n  digest: \"sha256:ee462342e2f4827f9e6008bc17776ad14086c8b8971e4ac24e54bc2b92c139b8\"\n- location: \"https://cloud-images.ubuntu.com/releases/questing/release-20260226/ubuntu-25.10-server-cloudimg-riscv64.img\"\n  arch: \"riscv64\"\n  digest: \"sha256:378361044e88aecbc9eca5e74386e6adb20b54d1cd6fa63ef92eeb97c6d28798\"\n- location: \"https://cloud-images.ubuntu.com/releases/questing/release-20260226/ubuntu-25.10-server-cloudimg-armhf.img\"\n  arch: \"armv7l\"\n  digest: \"sha256:c26e3d702e091a5b6ce762e647c64379a155f7d1f76e70ddc7c32932af449ea2\"\n- location: \"https://cloud-images.ubuntu.com/releases/questing/release-20260226/ubuntu-25.10-server-cloudimg-s390x.img\"\n  arch: \"s390x\"\n  digest: \"sha256:26715d7584b684ec9008eaffd8022429067502b55505fe004f49c1f766dc1196\"\n- location: \"https://cloud-images.ubuntu.com/releases/questing/release-20260226/ubuntu-25.10-server-cloudimg-ppc64el.img\"\n  arch: \"ppc64le\"\n  digest: \"sha256:489f652f40dc3228d89261a67d5a756ea6c3efdcaf77aeaf7ad180dd0d1de924\"\n# Fallback to the latest release image.\n# Hint: run `limactl prune` to invalidate the cache\n\n- location: https://cloud-images.ubuntu.com/releases/questing/release/ubuntu-25.10-server-cloudimg-amd64.img\n  arch: x86_64\n\n- location: https://cloud-images.ubuntu.com/releases/questing/release/ubuntu-25.10-server-cloudimg-arm64.img\n  arch: aarch64\n\n- location: https://cloud-images.ubuntu.com/releases/questing/release/ubuntu-25.10-server-cloudimg-riscv64.img\n  arch: riscv64\n\n- location: https://cloud-images.ubuntu.com/releases/questing/release/ubuntu-25.10-server-cloudimg-armhf.img\n  arch: armv7l\n\n- location: https://cloud-images.ubuntu.com/releases/questing/release/ubuntu-25.10-server-cloudimg-s390x.img\n  arch: s390x\n\n- location: https://cloud-images.ubuntu.com/releases/questing/release/ubuntu-25.10-server-cloudimg-ppc64el.img\n  arch: ppc64le\n\n# # NOTE: Intel Mac with macOS prior to 15.5 requires setting vmType to qemu\n# # https://github.com/lima-vm/lima/issues/3334\n# vmType: qemu\n"
  },
  {
    "path": "templates/almalinux-10.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/almalinux-10\n- template:_default/mounts\n"
  },
  {
    "path": "templates/almalinux-8.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/almalinux-8\n- template:_default/mounts\n"
  },
  {
    "path": "templates/almalinux-9.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/almalinux-9\n- template:_default/mounts\n"
  },
  {
    "path": "templates/almalinux-kitten-10.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/almalinux-kitten-10\n- template:_default/mounts\n"
  },
  {
    "path": "templates/alpine-iso.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/alpine-iso\n- template:_default/mounts\n\n# The built-in containerd installer does not support Alpine currently.\n# Use a provisioning script to install containerd, buildkit, and nerdctl.\n#\n# NOTE: Starting with Lima v2.0, nerdctl version must be v2.1.6 or later to\n# enable port forwarding in rootful mode.\n# Available in apk since Alpine 3.22.\ncontainerd:\n  system: false\n  user: false\n"
  },
  {
    "path": "templates/alpine.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/alpine\n- template:_default/mounts\n\n# The built-in containerd installer does not support Alpine currently.\n# Use a provisioning script to install containerd, buildkit, and nerdctl.\n#\n# NOTE: Starting with Lima v2.0, nerdctl version must be v2.1.6 or later to\n# enable port forwarding in rootful mode.\n# Available in apk since Alpine 3.22.\ncontainerd:\n  system: false\n  user: false\n"
  },
  {
    "path": "templates/apptainer-rootful.yaml",
    "content": "# A template to use Apptainer instead of containerd & nerdctl\n# $ limactl start ./apptainer-rootful.yaml\n# $ limactl shell apptainer-rootful apptainer run -u -B $HOME:$HOME docker://alpine\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-lts\n- template:_default/mounts\n\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v apptainer >/dev/null 2>&1 && exit 0\n    # add the \"Official PPA for Apptainer\"\n    add-apt-repository -y ppa:apptainer/ppa\n    apt-get update\n    apt-get install -y apptainer-suid\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v apptainer >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"apptainer is not installed yet\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\n"
  },
  {
    "path": "templates/apptainer.yaml",
    "content": "# A template to use Apptainer instead of containerd & nerdctl\n# $ limactl start ./apptainer.yaml\n# $ limactl shell apptainer apptainer run -u -B $HOME:$HOME docker://alpine\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-lts\n- template:_default/mounts\n\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v apptainer >/dev/null 2>&1 && exit 0\n    # Workaround for https://github.com/apptainer/apptainer/issues/2027\n    echo \"kernel.apparmor_restrict_unprivileged_userns = 0\" >/etc/sysctl.d/99-userns.conf\n    sysctl --system\n    # add the \"Official PPA for Apptainer\"\n    add-apt-repository -y ppa:apptainer/ppa\n    apt-get update\n    apt-get install -y apptainer\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v apptainer >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"apptainer is not installed yet\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\n"
  },
  {
    "path": "templates/archlinux.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/archlinux\n- template:_default/mounts\n"
  },
  {
    "path": "templates/buildkit.yaml",
    "content": "# A template to use BuildKit\n# $ limactl start ./buildkit.yaml\n\n# To run `buildkit` on the host (assumes buildctl is installed):\n# $ export BUILDKIT_HOST=$(limactl list buildkit --format 'unix://{{.Dir}}/sock/buildkitd.sock')\n# $ buildctl debug workers\nmessage: |\n  To run `buildkit` on the host (assumes buildctl is installed), run the following commands:\n  -------\n  export BUILDKIT_HOST=\"unix://{{.Dir}}/sock/buildkitd.sock\"\n  buildctl debug workers\n  -------\n\nminimumLimaVersion: 2.0.0\n\nbase: template:_images/ubuntu-lts\n\ncontainerd:\n  system: false\n  user: true\n\nportForwards:\n- guestSocket: \"/run/user/{{.UID}}/buildkit-default/buildkitd.sock\"\n  hostSocket: \"{{.Dir}}/sock/buildkitd.sock\"\n"
  },
  {
    "path": "templates/centos-stream-10.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/centos-stream-10\n- template:_default/mounts\n"
  },
  {
    "path": "templates/centos-stream-9.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/centos-stream-9\n- template:_default/mounts\n"
  },
  {
    "path": "templates/debian-11.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/debian-11\n- template:_default/mounts\n"
  },
  {
    "path": "templates/debian-12.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/debian-12\n- template:_default/mounts\n"
  },
  {
    "path": "templates/debian-13.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/debian-13\n- template:_default/mounts\n"
  },
  {
    "path": "templates/default.yaml",
    "content": "# ===================================================================== #\n# BASIC CONFIGURATION\n# ===================================================================== #\n\n# Default values in this YAML file are specified by `null` instead of Lima's \"builtin default\" values,\n# so they can be overridden by the $LIMA_HOME/_config/default.yaml mechanism documented at the end of this file.\n\n# VM type: \"qemu\", \"vz\" (on macOS 13 and later), or \"default\".\n# The vmType can be specified only on creating the instance.\n# The vmType of existing instances cannot be changed.\n# 🟢 Builtin default: \"vz\" (on macOS 13.5 and later), \"qemu\" (on others)\nvmType: null\n\n# Arch: \"default\", \"x86_64\", \"aarch64\".\n# 🟢 Builtin default: \"default\" (corresponds to the host architecture)\narch: null\n\n\n# OpenStack-compatible disk image.\n# Each image has a `location` URL for the disk image, an `arch` setting, and an optional `digest`.\n# To specify a custom kernel and initial RAM disk, nest `kernel` and `initrd` under an image entry:\n#   - location: \"https://example.com/image.qcow2\"\n#     # kernel specifies a custom kernel to boot.\n#     kernel:\n#       location: \"https://example.com/vmlinuz\"\n#       digest: \"sha256:...\"\n#       cmdline: \"console=ttyS0 root=/dev/sda1\"\n#     # initrd specifies a custom initial RAM disk.\n#     initrd:\n#       location: \"https://example.com/initrd.img\"\n#       digest: \"sha256:...\"\n# 🟢 Builtin default: none (must be specified)\n# 🔵 This file: Ubuntu images (inherited via the `base` mechanism later in this file)\nimages: []\n\n# CPUs\n# 🟢 Builtin default: min(4, host CPU cores)\ncpus: null\n\n# Memory size\n# 🟢 Builtin default: min(\"4GiB\", half of host memory)\nmemory: null\n\n# Disk size\n# 🟢 Builtin default: \"100GiB\"\ndisk: null\n\n# Expose host directories to the guest, the mount point might be accessible from all UIDs in the guest\n# \"location\" can use these template variables: {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, {{.User}}, {{.Param.Key}},\n# {{.GlobalTempDir}}, and {{.TempDir}}. The global temp dir is always \"/tmp\" on Unix.\n# \"mountPoint\" can use these template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n# 🟢 Builtin default: [] (Mount nothing)\n# 🔵 This file: Mount the home as read-only (inherited via the `base` mechanism later in this file)\n#    Until Lima v1.2, /tmp/lima was mounted too as writable.\nmounts: []\n# - location: \"~\"\n#  # Configure the mountPoint inside the guest.\n#  # 🟢 Builtin default: value of location\n#  mountPoint: null\n#  # Setting `writable` to true is discouraged when mountType is set to \"reverse-sshfs\".\n#  # 🟢 Builtin default: false\n#  writable: null\n#  sshfs:\n#    # Enabling the SSHFS cache will increase performance of the mounted filesystem, at\n#    # the cost of potentially not reflecting changes made on the host in a timely manner.\n#    # Warning: It looks like PHP filesystem access does not work correctly when\n#    # the cache is disabled.\n#    # 🟢 Builtin default: true\n#    cache: null\n#    # SSHFS has an optional flag called 'follow_symlinks'. This allows mounts\n#    # to be properly resolved in the guest os and allow for access to the\n#    # contents of the symlink. As a result, symlinked files & folders on the Host\n#    # system will look and feel like regular files directories in the Guest OS.\n#    # 🟢 Builtin default: false\n#    followSymlinks: null\n#    # SFTP driver, \"builtin\" or \"openssh-sftp-server\". \"openssh-sftp-server\" is recommended.\n#    # 🟢 Builtin default: \"openssh-sftp-server\" if OpenSSH SFTP Server binary is found, otherwise \"builtin\"\n#    sftpDriver: null\n#  9p:\n#    # Supported security models are \"passthrough\", \"mapped-xattr\", \"mapped-file\" and \"none\".\n#    # \"mapped-xattr\" and \"mapped-file\" are useful for persistent chown but incompatible with symlinks.\n#    # 🟢 Builtin default: \"none\" (since Lima v0.13)\n#    securityModel: null\n#    # Select 9P protocol version. Valid options are: \"9p2000\" (legacy), \"9p2000.u\", \"9p2000.L\".\n#    # 🟢 Builtin default: \"9p2000.L\"\n#    protocolVersion: null\n#    # The number of bytes to use for 9p packet payload, where 4KiB is the absolute minimum.\n#    # 🟢 Builtin default: \"128KiB\"\n#    msize: null\n#    # Specifies a caching policy. Valid options are: \"none\", \"loose\", \"fscache\" and \"mmap\".\n#    # Try choosing \"mmap\" or \"none\" if you see a stability issue with the default \"fscache\".\n#    # See https://www.kernel.org/doc/Documentation/filesystems/9p.txt\n#    # 🟢 Builtin default: \"fscache\" for non-writable mounts, \"mmap\" for writable mounts\n#    cache: null\n\n# List of mount types not supported by the kernel of this distro.\n# Also used to resolve the default mount type when not explicitly specified.\n#\n# NOTE: 9p is broken in Linux v6.9, v6.10, and v6.11.\n# The issue was fixed in Linux v6.12-rc5 (https://github.com/torvalds/linux/commit/be2ca38).\n#\n# 🟢 Builtin default: []\nmountTypesUnsupported: null\n\n# Mount type for above mounts, such as \"reverse-sshfs\" (from sshocker), \"9p\" (QEMU’s virtio-9p-pci, aka virtfs),\n# or \"virtiofs\" (experimental on Linux; needs `vmType: vz` on macOS).\n# 🟢 Builtin default: \"default\" (resolved to be \"9p\" for QEMU since Lima v1.0 on non-Windows, \"virtiofs\" for vz)\nmountType: null\n\n# Enable inotify support for mounted directories (EXPERIMENTAL)\n# 🟢 Builtin default: Disabled by default\nmountInotify: null\n\n# ===================================================================== #\n# ADVANCED CONFIGURATION\n# ===================================================================== #\n\n# Lima disks to attach to the instance. The disks will be accessible from inside the\n# instance, labeled by name. (e.g. if the disk is named \"data\", it will be labeled\n# \"lima-data\" inside the instance). The disk will be mounted inside the instance at\n# `/mnt/lima-${VOLUME}`.\n# 🟢 Builtin default: []\nadditionalDisks:\n# disks should either be a list of disk name strings, for example:\n# - \"data\"\n# or a list of disk objects with extra parameters, for example:\n# - name: \"data\"\n#   format: true\n#   fsType: \"ext4\"\n\nssh:\n  # A localhost port of the host. Forwarded to port 22 of the guest.\n  # 🟢 Builtin default: 0 (automatically assigned to a free port)\n  localPort: null\n  # Load ~/.ssh/*.pub in addition to $LIMA_HOME/_config/user.pub .\n  # This option is useful when you want to use other SSH-based\n  # applications such as rsync with the Lima instance.\n  # If you have an insecure key under ~/.ssh, do not use this option.\n  # 🟢 Builtin default: false (since Lima v1.0)\n  loadDotSSHPubKeys: null\n  # Forward ssh agent into the instance.\n  # The ssh agent socket can be mounted in a container at the path `/run/host-services/ssh-auth.sock`.\n  # Set the environment variable `SSH_AUTH_SOCK` value to the path above.\n  # The socket is accessible by the non-root user inside the Lima instance.\n  # 🟢 Builtin default: false\n  forwardAgent: null\n  # Forward X11 into the instance\n  # 🟢 Builtin default: false\n  forwardX11: null\n  # Trust forwarded X11 clients\n  # 🟢 Builtin default: false\n  forwardX11Trusted: null\n  # Enable SSH over vsock.\n  # 🟢 Builtin default: true for vz with Linux guests, false otherwise\n  overVsock: null\n\ncaCerts:\n  # If set to `true`, this will remove all the default trusted CA certificates that\n  # are normally shipped with the OS.\n  # 🟢 Builtin default: false\n  removeDefaults: null\n\n  # A list of trusted CA certificate files. The files will be read and passed to cloud-init.\n  files:\n  # - examples/hello.crt\n\n  # A list of trusted CA certificates. These are directly passed to cloud-init.\n  certs:\n  # - |\n  #   -----BEGIN CERTIFICATE-----\n  #   YOUR-ORGS-TRUSTED-CA-CERT-HERE\n  #   -----END CERTIFICATE-----\n  # - |\n  #   -----BEGIN CERTIFICATE-----\n  #   YOUR-ORGS-TRUSTED-CA-CERT-HERE\n  #   -----END CERTIFICATE-----\n\n# Upgrade the instance on boot\n# Reboot after upgrade if required\n# 🟢 Builtin default: false\nupgradePackages: null\n\ncontainerd:\n  # Enable system-wide (aka rootful)  containerd and its dependencies (BuildKit, Stargz Snapshotter)\n  # Note that `nerdctl.lima` only works in rootless mode; you have to use `lima sudo nerdctl ...`\n  # to use rootful containerd with nerdctl.\n  # 🟢 Builtin default: false\n  system: null\n  # Enable user-scoped (aka rootless) containerd and its dependencies\n  # 🟢 Builtin default: true (for x86_64 and aarch64)\n  user: null\n#  # Override containerd archive\n#  # 🟢 Builtin default: hard-coded URL with hard-coded digest (see the output of `limactl info | jq .defaultTemplate.containerd.archives`)\n#  archives:\n#  - location: \"~/Downloads/nerdctl-full-X.Y.Z-linux-amd64.tar.gz\"\n#    arch: \"x86_64\"\n#    digest: \"sha256:...\"\n\n# Provisioning scripts need to be idempotent because they might be called\n# multiple times, e.g. when the host VM is being restarted.\n# The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n#\n# EXPERIMENTAL Alternatively the script can be provided using the \"file\" property. This file is read when the instance\n# is created and then stored under the \"script\" property. When \"file\" is specified \"script\" must be empty.\n# The \"file\" property can either be a string (URL), or an object with a \"url\" and \"digest\" properties.\n# The \"digest\" property is currently unused.\n# Relative script files will be resolved relative to the location of the template file.\n# 🟢 Builtin default: []\n# provision:\n# # `system` is executed with root privileges\n# - mode: system\n#   script: |\n#     #!/bin/bash\n#     set -eux -o pipefail\n#     export DEBIAN_FRONTEND=noninteractive\n#     apt-get install -y vim\n# # `user` is executed without root privileges\n# - mode: user\n#   file:\n#     url: user-provisioning.sh\n#     digest: deadbeef\n# # `boot` is executed directly by /bin/sh as part of cloud-init-local.service's early boot process,\n# # which is why there is no hash-bang specified in the example\n# # See cloud-init docs for more info https://docs.cloud-init.io/en/latest/reference/examples.html#run-commands-on-first-boot\n# - mode: boot\n#   script: |\n#     systemctl disable NetworkManager-wait-online.service\n# # `dependency` is executed before the regular dependency resolution workflow in\n# # pkg/cidata/cidata.TEMPLATE.d/boot.Linux/30-install-packages.sh\n# # If skipDefaultDependencyResolution is set on at least one `dependency` mode provisioning script, the regular\n# # dependency resolution workflow in pkg/cidata/cidata.TEMPLATE.d/boot.Linux/30-install-packages.sh will be skipped.\n# - mode: dependency\n#   skipDefaultDependencyResolution: false\n#   script: |\n#     #!/bin/bash\n#     dnf config-manager --add-repo ...\n#     dnf install ...\n# # `ansible` is executed after other scripts are complete\n# # It requires `ansible-playbook` command to be installed.\n# # Environment variables such as ANSIBLE_CONFIG can be used, to control the behavior of the playbook execution.\n# # See ansible docs, and `ansible-config`, for more info https://docs.ansible.com/ansible/latest/playbook_guide/\n# # DEPRECATED The ansible mode is deprecated, and should not be used. Instead call ansible-playbook directly,\n# # either from the host after the instance is started or from the instance by running ansible locally instead.\n# - mode: ansible\n#   playbook: playbook.yaml\n# # `data` is a file that is written to the guest filesystem and not executed at all.\n# # The file is written after the boot scripts, but before any other provisioning scripts are run.\n# # Note that reverse-sshfs mounts are not established at this time; other mount types are already mounted.\n# # The `path` and `content` properties are required. The `file` property can be used the same way as with\n# # other provisioning scripts, in which case `content` must be empty. The `owner` defaults to \"root:root\";\n# # the permissions default to 644. The `overwrite` property defaults to `true`, in which case the file will\n# # be overwritten on every boot.\n# # `path`, `contents`, and `owner` are evaluated as guest templates (see above).\n# - mode: data\n#   path: /etc/conf.d/example\n#   content: |\n#     FOO=bar\n#   owner: \"root:root\"\n#   permissions: 644\n#   overwrite: false\n# # Create or edit a file in the guest filesystem by using `yq`.\n# # The file specified by `path` will be updated by `expression`.\n# # An empty file of the required `format` will be created if it does not yet exist.\n# # `format` defaults to \"auto\" and will be detected by file extension of `path`.\n# # If the extension is not recognized by `yq` then `format` must be set to a\n# # value from this list:\n# #   \"auto\", \"csv\", \"ini\", \"json\", \"props\", \"tsv\", \"toml\", \"xml\", \"yaml\"\n# # See https://github.com/mikefarah/yq for more info.\n# # Any missing directories will be created as needed.\n# # The file permissions will be set to the specified value.\n# # The file and directory creation will be performed as the specified owner.\n# # If the existing file is not writable by the specified owner, the operation will fail.\n# # `path` and `expression` are required.\n# # `owner` and `permissions` are optional. Defaults to \"root:root\" (on Linux) and 644.\n# - mode: yq\n#   path: \"{{.Home}}/.config/docker/daemon.json\"\n#   expression: \".features.containerd-snapshotter = {{.Param.containerdSnapshotter}}\"\n#   format: auto\n#   owner: \"{{.User}}\"\n#   permissions: 644\n#\n# Q. In what order are provision scripts executed?\n# A. All provisions are processed per boot by each module in stages of cloud-init as follows:\n#   1. cloud-init 'init' stage\n#     bootcmd:\n#     - `mode: boot` scripts are executed\n#\n#   2. cloud-init 'config' stage\n#     write_files:\n#     - `00-lima.boot.sh` is created, but not executed\n#\n#   3. cloud-init 'final' stage\n#     scripts_per_boot:\n#     - `00-lima.boot.sh` is executed; remaining provisions are processed in the following order:\n#       - LIMA pre-defined boot scripts:\n#         - `boot.Linux/{00-..., ..., 25-...}`\n#         - `boot.Linux/30-install-packages.sh`\n#           - `mode: dependency` scripts are executed\n#         - `boot.Linux/{35-..., ...}`\n#       - `mode: data` files are copied\n#       - `mode: yq` files are processed\n#       - `mode: system` scripts are executed\n#       - `mode: user` scripts are executed\n\n# Probe scripts to check readiness.\n# The scripts run in user mode. They must start with a '#!' line.\n# The scripts can use the following template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n# EXPERIMENTAL Alternatively the script can be provided using the \"file\" property. This file is read when the instance\n# is created and then stored under the \"script\" property. When \"file\" is specified \"script\" must be empty.\n# The \"file\" property can either be a string (URL), or an object with a \"url\" and \"digest\" properties.\n# The \"digest\" property is currently unused.\n# Relative script files will be resolved relative to the location of the template file.\n# 🟢 Builtin default: []\n# probes:\n# # Only `readiness` probes are supported right now.\n# - mode: readiness\n#   description: vim to be installed\n#   script: |\n#      #!/bin/bash\n#      set -eux -o pipefail\n#      if ! timeout 30s bash -c \"until command -v vim; do sleep 3; done\"; then\n#        echo >&2 \"vim is not installed yet\"\n#        exit 1\n#      fi\n#   hint: |\n#     vim was not installed in the guest. Make sure the package system is working correctly.\n#     Also see \"/var/log/cloud-init-output.log\" in the guest.\n\n# ===================================================================== #\n# FURTHER ADVANCED CONFIGURATION\n# ===================================================================== #\n\n# A template should specify the minimum Lima version required to parse this template correctly.\n# It should not be set if the minimum version is less than 1.0.0\n# 🟢 Builtin default: not set\n# 🔵 This file: \"1.1.0\" to use the `base` templating mechanism\nminimumLimaVersion: 2.0.0\n\n# EXPERIMENTAL\n# Default settings can be imported from base templates. These will be merged in when the instance\n# is created, and the combined template is stored in the instance directory.\n# This setting can be either a single string (URL), or a list of locators.\n# A locator is again either a string (URL), or an object with \"url\" and \"digest\" properties, e.g.\n# base: [{url: ./base.yaml, digest: decafbad}, …]\n# The \"digest\" property is currently unused.\n# Any relative base template name will be resolved relative to the location of the main template.\n# 🟢 Builtin default: no base template\n# 🔵 This file: Ubuntu images and default mount points\nbase:\n- template:_images/ubuntu\n- template:_default/mounts\n\n# User to be used inside the VM\nuser:\n  # User name. An explicitly specified username is not validated by Lima.\n  # 🟢 Builtin default: same as the host username, if it is a valid Linux username, otherwise \"lima\"\n  name: null\n  # Full name or display name of the user.\n  # 🟢 Builtin default: user information from the host\n  comment: null\n  # Numeric user id. It is not currently possible to specify a group id.\n  # 🟢 Builtin default: same as the host user id of the current user (NOT a lookup of the specified \"username\").\n  uid: null\n  # Home directory inside the VM, NOT the mounted home directory of the host.\n  # It can use the following template variables: {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n  # 🟢 Builtin default: \"/home/{{.User}}.guest\" (Also accessible as \"/home/{{.User}}.linux\")\n  home: null\n  # Shell. Needs to be an absolute path.\n  # 🟢 Builtin default: \"/bin/bash\"\n  shell: null\n\nvmOpts:\n  qemu:\n    # Minimum version of QEMU required to create an instance of this template.\n    # Will be ignored if the vmType is not \"qemu\"\n    # 🟢 Builtin default: not set\n    minimumVersion: null\n    # Specify desired QEMU CPU type for each arch.\n    # You can see what options are available for host emulation with: `qemu-system-$(arch) -cpu help`.\n    # Setting of instructions is supported like this: \"qemu64,+ssse3\".\n    # 🟢 Builtin default: hard-coded arch map with type (see the output of `limactl info | jq .defaultTemplate.cpuType`)\n    cpuType:\n    #   aarch64: \"max\" # (or \"host\" when running on aarch64 host)\n    #   armv7l:  \"max\" # (or \"host\" when running on armv7l host)\n    #   riscv64: \"max\" # (or \"host\" when running on riscv64 host)\n    #   x86_64:  \"max\" # (or \"host\" when running on x86_64 host; additional options are appended on Intel Mac)\n  vz:\n    # Specify the disk image format: \"raw\" or \"asif\".\n    # Currently only applies to the primary disk image.\n    # \"asif\" requires macOS 26+, and does not support converting back to \"raw\".\n    # 🟢 Builtin default: \"raw\"\n    diskImageFormat: null\n    rosetta:\n      # Enable Rosetta inside the VM; needs `vmType: vz`\n      # Hint: try `softwareupdate --install-rosetta` if Lima gets stuck at `Installing rosetta...`\n      # 🟢 Builtin default: false\n      enabled: null\n      # Register rosetta to /proc/sys/fs/binfmt_misc\n      # 🟢 Builtin default: false\n      binfmt: null\n\n# OS: \"Linux\".\n# 🟢 Builtin default: \"Linux\"\nos: null\n\n# DEPRECATED: Use vmOpts.qemu.cpuType instead. See the vmOpts.qemu.cpuType section above for configuration.\ncpuType:\n\n# DEPRECATED: Use vmOpts.vz.rosetta instead. See the vmOpts.qemu.cpuType section above for configuration.\nrosetta:\n  enabled: null\n  binfmt: null\n\n# Specify the timezone name (as used by the zoneinfo database). Specify the empty string\n# to not set a timezone in the instance.\n# 🟢 Builtin default: use name from /etc/timezone or deduce from symlink target of /etc/localtime\ntimezone: null\n\nfirmware:\n  # Use legacy BIOS instead of UEFI. Ignored for aarch64 and vz.\n  # 🟢 Builtin default: false\n  legacyBIOS: null\n#  # Override UEFI images\n#  # 🟢 Builtin default: uses VM's default UEFI, except for qemu + aarch64.\n#  # See <https://lists.gnu.org/archive/html/qemu-devel/2023-12/msg01694.html>\n#  images:\n#  - location: \"~/Downloads/edk2-aarch64-code.fd.gz\"\n#    arch: \"aarch64\"\n#    digest: \"sha256:...\"\n#    vmType: \"qemu\"\n\naudio:\n  # EXPERIMENTAL\n  # QEMU audiodev, e.g., \"none\", \"coreaudio\", \"pa\", \"alsa\", \"oss\".\n  # VZ driver, use \"vz\" as device name\n  # Choosing \"none\" will mute the audio output, and not play any sound.\n  # Choosing \"default\" will pick a suitable choice of: coreaudio, pa, dsound, oss.\n  # As of QEMU v6.2 the default is to create a disconnected sound device\n  # that is still visible in the guest but not connected to the host.\n  # 🟢 Builtin default: \"\"\n  device: null\n\nvideo:\n  # QEMU display, e.g., \"none\", \"cocoa\", \"sdl\", \"gtk\", \"vnc\", \"default\".\n  # Choosing \"none\" will hide the video output, and not show any window.\n  # Choosing \"vnc\" will use a network server, and not show any window.\n  # Choosing \"default\" will pick the first available of: gtk, sdl, cocoa.\n  # As of QEMU v6.2, enabling anything but none or vnc is known to have negative impact\n  # on performance on macOS hosts: https://gitlab.com/qemu-project/qemu/-/issues/334\n  # 🟢 Builtin default: \"none\"\n  display: null\n  # VNC (Virtual Network Computing) is a platform-independent graphical\n  # desktop-sharing system that uses the Remote Frame Buffer protocol (RFB)\n  vnc:\n    # VNC display, e.g.,\"to=L\", \"host:d\", \"unix:path\", \"none\"\n    # By convention the TCP port is 5900+d, connections from any host.\n    # 🟢 Builtin default: \"127.0.0.1:0,to=9\"\n    display: null\n\n# The instance can get routable IP addresses from the vmnet framework using\n# https://github.com/lima-vm/socket_vmnet.\n# 🟢 Builtin default: []\nnetworks:\n# Lima can manage daemons for networks defined in $LIMA_HOME/_config/networks.yaml\n# automatically. The socket_vmnet binary must be installed into\n# secure locations only alterable by the \"root\" user.\n# - lima: shared\n#   # MAC address of the instance; lima will pick one based on the instance name,\n#   # so DHCP assigned ip addresses should remain constant over instance restarts.\n#   macAddress: \"\"\n#   # Interface name, defaults to \"lima0\", \"lima1\", etc.\n#   interface: \"\"\n#   # Interface metric, lowest metric becomes the preferred route.\n#   # Defaults to 100. Builtin SLIRP network uses 200.\n#   metric: 100\n#\n# Lima can also connect to \"unmanaged\" networks addressed by \"socket\". This\n# means that the daemons will not be controlled by Lima, but must be started\n# before the instance.  The interface type (host, shared, or bridged) is\n# configured in socket_vmnet and not in lima.\n# - socket: \"/var/run/socket_vmnet\"\n\n\n# The \"vzNAT\" IP address is accessible from the host, but not from other guests.\n# Needs `vmType: vz`\n# - vzNAT: true\n\n# Port forwarding rules. Forwarding between ports 22 and ssh.localPort cannot be overridden.\n# Rules are checked sequentially until the first one matches.\n# portForwards:\n# - guestPort: 443\n#   hostIP: \"0.0.0.0\" # overrides the default value \"127.0.0.1\"; allows privileged port forwarding\n# # default: hostPort: 443 (same as guestPort)\n# # default: guestIP: \"127.0.0.1\" (also matches bind addresses \"0.0.0.0\", \"::\", and \"::1\")\n# # default: proto: \"tcp\" (other options: \"udp, \"any\")\n#\n# - guestPortRange: [4000, 4999]\n#   hostIP:  \"0.0.0.0\" # overrides the default value \"127.0.0.1\"\n# # default: hostPortRange: [4000, 4999] (must specify same number of ports as guestPortRange)\n#\n# - guestPort: 80\n#   hostPort: 8080 # overrides the default value 80\n#\n# - guestIP: \"127.0.0.2\" # overrides the default value \"127.0.0.1\"\n#   hostIP: \"127.0.0.2\" # overrides the default value \"127.0.0.1\"\n# # default: guestPortRange: [1, 65535]\n# # default: hostPortRange: [1, 65535]\n#\n# - guestIP: 0.0.0.0 # otherwise defaults to 127.0.0.1\n#   proto: any       # tcp and udp\n#   ignore: true     # don't forward these ports (guestPortRange, in this case 1-65535)\n#\n# - guestPort: 7443\n#   guestIP: \"0.0.0.0\"        # Will match *any* interface\n#   guestIPMustBeZero: false  # 0.0.0.0 matches any bound interface, not just 0.0.0.0 itself\n#   hostIP: \"0.0.0.0\"         # Forwards to 0.0.0.0, exposing it externally\n#\n# - guestSocket: \"/run/user/{{.UID}}/my.sock\"\n#   hostSocket: mysocket\n# # default: reverse: false\n# # \"guestSocket\" can include these template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n# # \"hostSocket\" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n# # \"reverse\" can only be used for unix sockets right now, not for tcp sockets.\n# # Put sockets into \"{{.Dir}}/sock\" to avoid collision with Lima internal sockets!\n# # Sockets can also be forwarded to ports and vice versa, but not to/from a range of ports.\n# # Forwarding requires the lima user to have rw access to the \"guestsocket\",\n# # and the local user rwx access to the directory of the \"hostsocket\".\n#\n# # Lima internally appends this fallback rule at the end:\n# - guestIP: \"127.0.0.1\"\n#   proto: \"any\"\n#   guestPortRange: [1, 65535]\n#   hostIP: \"127.0.0.1\"\n#   hostPortRange: [1, 65535]\n# # Any port still not matched by a rule will not be forwarded (ignored)\n\n# Copy files from the guest to the host. Copied after provisioning scripts have been completed.\n# copyToHost:\n# - guest: \"/etc/myconfig.cfg\"\n#   host: \"{{.Dir}}/copied-from-guest/myconfig\"\n# # deleteOnStop: false\n# # \"guest\" can include these template variables: {{.Home}}, {{.Name}}, {{.Hostname}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n# # \"host\" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, {{.User}}, and {{.Param.Key}}.\n# # \"deleteOnStop\" will delete the file from the host when the instance is stopped.\n\n# Message. Information to be shown to the user, given as a Go template for the instance.\n# The same template variables as for listing instances can be used, for example {{.Dir}}.\n# You can view the complete list of variables using `limactl list --list-fields` command.\n# It also includes {{.HostOS}} and {{.HostArch}} vars, for the runtime GOOS and GOARCH.\n# 🟢 Builtin default: \"\"\n# message: |\n#   This will be shown to the user.\n\n# Extra environment variables that will be loaded into the VM at start up.\n# These variables are consumed by internal init scripts, and also added\n# to /etc/environment.\n# If you set any of \"ftp_proxy\", \"http_proxy\", \"https_proxy\", or \"no_proxy\", then\n# Lima will automatically set an uppercase variant to the same value as well.\n# 🟢 Builtin default: {}\n# env:\n#   KEY: value\n\n# Defines variables used for customizing the functionality.\n# Key names must start with an uppercase or lowercase letter followed by\n# any number of letters, digits, and underscores.\n# Values must not contain non-printable characters except for spaces and tabs.\n# These variables can be referenced as {{.Param.Key}} in lima.yaml.\n# In provisioning scripts and probes they are also available as predefined\n# environment variables, prefixed with \"PARAM_\" (so `Key` → `$PARAM_Key`).\n# param:\n#   Key: value\n\n# Lima will override the proxy environment variables with values from the current process\n# environment (the environment in effect when you run `limactl start`). It will automatically\n# replace the strings \"localhost\" and \"127.0.0.1\" with the host gateway address from inside\n# the VM, so it stays routable. Use of the process environment can be disabled by setting\n# propagateProxyEnv to false.\n# 🟢 Builtin default: true\npropagateProxyEnv: null\n\n# The host agent implements a DNS server that looks up host names on the host\n# using the local system resolver. This means changing VPN and network settings\n# are reflected automatically into the guest, including conditional forward,\n# and mDNS lookup. By default, only IPv4 addresses will be returned. IPv6 addresses\n# can only work when using a vmnet network interface and the host has working\n# IPv6 configured as well.\nhostResolver:\n  # 🟢 Builtin default: true\n  enabled: null\n  # 🟢 Builtin default: false\n  ipv6: null\n  # Static names can be defined here as an alternative to adding them to the hosts /etc/hosts.\n  # Values can be either other hostnames, or IP addresses. The host.lima.internal name is\n  # predefined to specify the gateway address to the host.\n  # 🟢 Builtin default: {}\n  hosts:\n  #   guest.name: 127.1.1.1\n  #   host.name: host.lima.internal\n\n# If hostResolver.enabled is false, then the following rules apply for configuring dns:\n# Explicitly set DNS addresses for qemu user-mode networking. By default, qemu picks *one*\n# nameserver from the host config and forwards all queries to this server. On macOS\n# Lima adds the nameservers configured for the first host interface in service order,\n# that has an IPv4 address, to the list. In case this still doesn't work (e.g. VPN\n# setups), the servers can be specified here explicitly. If nameservers are specified\n# here, then the configuration from network preferences will be ignored.\n# 🟢 Builtin default: []\n# dns:\n# - 1.1.1.1\n# - 1.0.0.1\n\n# Prefix to use for installing guest agent, and containerd with dependencies (if configured)\n# 🟢 Builtin default: /usr/local\nguestInstallPrefix: null\n\n# When the \"plain\" mode is enabled:\n# - the YAML properties for mounts, port forwarding, containerd, etc. will be ignored\n# - guest agent will not be running\n# - dependency packages like sshfs will not be installed into the VM\n# User-specified provisioning scripts will be still executed.\n# 🟢 Builtin default: false\nplain: null\n\n# When the \"nestedVirtualization\" feature is enabled:\n# - Allows running a VM inside the guest VM.\n# - The guest VM must configure QEMU with the `-cpu host` parameters to run a nested VM:\n#   qemu-system-aarch64 -accel kvm -cpu host -M virt\n# - Without specifying `-cpu host`, nested virtualization may fail with the error:\n#   qemu-system-aarch64: kvm_init_vcpu: kvm_arch_init_vcpu failed (0): Invalid argument\n# - Only supported on Apple M3 or later with `vmType: vz` or `vmType: krunkit`.\n# 🟢 Builtin default: false\nnestedVirtualization: null\n\n# ===================================================================== #\n# GLOBAL DEFAULTS AND OVERRIDES\n# ===================================================================== #\n\n# The builtin defaults can be changed globally by creating a $LIMA_HOME/_config/default.yaml\n# file. It will be used by ALL instances under the same $LIMA_HOME, and it\n# will be applied on each `limactl start`, so can affect instance restarts.\n\n# A similar mechanism is $LIMA_HOME/_config/override.yaml, which will take\n# precedence even over the settings in an instances lima.yaml file.\n# It also applies to ALL instances under the same $LIMA_HOME, and is applied\n# on each restart. It can be used to globally override settings, e.g. make\n# the mount of the home directory writable.\n\n# EXPERIMENTAL\n# A third mechanism is $LIMA_HOME/_config/base.yaml. It is similar to\n# `default.yaml` but will be merged during the `base` template embedding\n# when the instance is created, and not on every start. It becomes part\n# of the lima.yaml file.\n\n# On each instance start the config settings are determined: If a value is\n# not set in `lima.yaml`, then the `default.yaml` is used. If that file\n# doesn't exist, or the value is not defined in the file, then the builtin\n# default is used. If `override.yaml` exists and defines the value, then\n# it overrides whatever has been chosen so far.\n\n# For slices (e.g. `mounts`, `provision`) and maps (`env`) the entries are\n# combined instead of replacing each other. Slices are produced from override\n# settings, followed by lima.yaml, followed by default.yaml (but NOT from\n# builtin defaults). Maps are produced starting with default.yaml values,\n# overwriting with lima.yaml ones, overwriting with override.yaml.\n\n# Exceptions:\n# - `dns` will use the list from the highest priority file; they are not\n#   combined. If override.yaml defines a list of `dns` entries, then the\n#   settings in default.yaml and lima.yaml are ignored.\n#\n# - `mounts` will update the `writable` setting when 2 entries have the\n#   same `location` value. For this reason they are processed in the opposite\n#   order: starting with default, followed by lima, and then override.\n#\n# -`networks` will replace lower priority entries with the same `interface`\n#   name with higher priority definitions. This does not apply if the\n#  `interface` field is empty. `networks` are therefore also processed\n#  in lowest to highest priority order.\n\n# ===================================================================== #\n# END OF TEMPLATE\n# ===================================================================== #\n"
  },
  {
    "path": "templates/docker-rootful.yaml",
    "content": "# A template to use Docker (rootful) instead of containerd & nerdctl\n# $ limactl start ./docker-rootful.yaml\n# $ limactl shell docker-roootful docker run -it -v $HOME:$HOME --rm alpine\n\n# To run `docker` on the host (assumes docker-cli is installed):\n# $ export DOCKER_HOST=$(limactl list docker-rootful --format 'unix://{{.Dir}}/sock/docker.sock')\n# $ docker ...\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-lts\n- template:_default/mounts\n\n# containerd is managed by Docker, not by Lima, so the values are set to false here.\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  # This script defines the host.docker.internal hostname when hostResolver is disabled.\n  # It is also needed for lima 0.8.2 and earlier, which does not support hostResolver.hosts.\n  # Names defined in /etc/hosts inside the VM are not resolved inside containers when\n  # using the hostResolver; use hostResolver.hosts instead (requires lima 0.8.3 or later).\n  script: |\n    #!/bin/sh\n    sed -i 's/host.lima.internal.*/host.lima.internal host.docker.internal/' /etc/hosts\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v docker >/dev/null 2>&1 && exit 0\n    export DEBIAN_FRONTEND=noninteractive\n    curl -fsSL https://get.docker.com | sh\n- mode: yq\n  path: /etc/systemd/system/docker.socket.d/override.conf\n  format: ini\n  expression: .Socket.SocketUser=\"{{.User}}\"\n- mode: yq\n  path: \"/etc/docker/daemon.json\"\n  expression: |\n    .features.cdi = true |\n    .features.containerd-snapshotter = {{.Param.containerdSnapshotter}}\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v docker >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"docker is not installed yet\"\n      exit 1\n    fi\n    if ! timeout 30s bash -c \"until pgrep dockerd; do sleep 3; done\"; then\n      echo >&2 \"dockerd is not running\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\nhostResolver:\n  # hostResolver.hosts requires lima 0.8.3 or later. Names defined here will also\n  # resolve inside containers, and not just inside the VM itself.\n  hosts:\n    host.docker.internal: host.lima.internal\nportForwards:\n- guestSocket: \"/var/run/docker.sock\"\n  hostSocket: \"{{.Dir}}/sock/docker.sock\"\nmessage: |\n  To run `docker` on the host (assumes docker-cli is installed), run the following commands:\n  ------\n  docker context create lima-{{.Name}} --docker \"host=unix://{{.Dir}}/sock/docker.sock\"\n  docker context use lima-{{.Name}}\n  docker run hello-world\n  ------\n  {{- if .Instance.Config.VMOpts.vz.rosetta.enabled}}\n  Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon.\n  You can use Rosetta AOT Caching with the CDI spec:\n  - To run a container, add `--device=lima-vm.io/rosetta=cached` to your `docker run` command:\n    ------\n    docker run --platform=linux/amd64 --device=lima-vm.io/rosetta=cached ...\n    ------\n  - To build an image, add `# syntax=docker/dockerfile:1-labs` at the top of your Dockerfile,\n    and use `--device=lima-vm.io/rosetta=cached` in the `RUN` command:\n    ------\n    # syntax=docker/dockerfile:1-labs\n    FROM ...\n    ...\n    RUN --device=lima-vm.io/rosetta=cached <your amd64 command>\n    ------\n  See: https://lima-vm.io/docs/config/multi-arch/#rosetta-aot-caching\n  {{- end}}\nparam:\n  containerdSnapshotter: true\n"
  },
  {
    "path": "templates/docker.yaml",
    "content": "# A template to use Docker instead of containerd & nerdctl\n# $ limactl start ./docker.yaml\n# $ limactl shell docker docker run -it -v $HOME:$HOME --rm alpine\n\n# To run `docker` on the host (assumes docker-cli is installed):\n# $ export DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')\n# $ docker ...\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-lts\n- template:_default/mounts\n\n# containerd is managed by Docker, not by Lima, so the values are set to false here.\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  # This script defines the host.docker.internal hostname when hostResolver is disabled.\n  # It is also needed for lima 0.8.2 and earlier, which does not support hostResolver.hosts.\n  # Names defined in /etc/hosts inside the VM are not resolved inside containers when\n  # using the hostResolver; use hostResolver.hosts instead (requires lima 0.8.3 or later).\n  script: |\n    #!/bin/sh\n    sed -i 's/host.lima.internal.*/host.lima.internal host.docker.internal/' /etc/hosts\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v docker >/dev/null 2>&1 && exit 0\n    export DEBIAN_FRONTEND=noninteractive\n    curl -fsSL https://get.docker.com | sh\n    # NOTE: you may remove the lines below, if you prefer to use rootful docker, not rootless\n    systemctl disable --now docker\n    apt-get install -y uidmap dbus-user-session\n- mode: yq\n  path: \"{{.Home}}/.config/docker/daemon.json\"\n  expression: |\n    .features.cdi = true |\n    .features.containerd-snapshotter = {{.Param.containerdSnapshotter}}\n  owner: \"{{.User}}\"\n- mode: user\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    systemctl --user start dbus\n    dockerd-rootless-setuptool.sh install\n    docker context use rootless\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v docker >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"docker is not installed yet\"\n      exit 1\n    fi\n    if ! timeout 30s bash -c \"until pgrep rootlesskit; do sleep 3; done\"; then\n      echo >&2 \"rootlesskit (used by rootless docker) is not running\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\nhostResolver:\n  # hostResolver.hosts requires lima 0.8.3 or later. Names defined here will also\n  # resolve inside containers, and not just inside the VM itself.\n  hosts:\n    host.docker.internal: host.lima.internal\nportForwards:\n- guestSocket: \"/run/user/{{.UID}}/docker.sock\"\n  hostSocket: \"{{.Dir}}/sock/docker.sock\"\nmessage: |\n  To run `docker` on the host (assumes docker-cli is installed), run the following commands:\n  ------\n  docker context create lima-{{.Name}} --docker \"host=unix://{{.Dir}}/sock/docker.sock\"\n  docker context use lima-{{.Name}}\n  docker run hello-world\n  ------\n  {{- if .Instance.Config.VMOpts.vz.rosetta.enabled}}\n  Rosetta is enabled in this VM, so you can run x86_64 containers on Apple Silicon.\n  You can use Rosetta AOT Caching with the CDI spec:\n  - To run a container, add `--device=lima-vm.io/rosetta=cached` to your `docker run` command:\n    ------\n    docker run --platform=linux/amd64 --device=lima-vm.io/rosetta=cached ...\n    ------\n  - To build an image, add `# syntax=docker/dockerfile:1-labs` at the top of your Dockerfile,\n    and use `--device=lima-vm.io/rosetta=cached` in the `RUN` command:\n    ------\n    # syntax=docker/dockerfile:1-labs\n    FROM ...\n    ...\n    RUN --device=lima-vm.io/rosetta=cached <your amd64 command>\n    ------\n  See: https://lima-vm.io/docs/config/multi-arch/#rosetta-aot-caching\n  {{- end}}\nparam:\n  containerdSnapshotter: true\n"
  },
  {
    "path": "templates/experimental/alsa.yaml",
    "content": "# A template to run ubuntu using device: default\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-lts\n- template:_default/mounts\n\nvmType: qemu\naudio:\n  device: default\n\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    test -e /lib/modules/$(uname -r)/kernel/sound/pci/hda/snd-hda-intel.ko* && exit 0\n    apt-get install -y linux-modules-extra-$(uname -r)\n    modprobe snd-hda-intel\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v aplay >/dev/null 2>&1 && exit 0\n    apt-get install -y --no-install-recommends alsa-utils\nprobes:\n- description: \"alsa to be installed\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v aplay >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"alsa is not installed yet\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\nmessage: |\n  To get a list of all available audio devices:\n  $ sudo aplay -L\n  To test the audio device, use something like:\n  $ sudo speaker-test -c2 -twav\n"
  },
  {
    "path": "templates/experimental/debian-sid.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase: template:_default/mounts\n\nimages:\n- location: https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-genericcloud-amd64-daily.qcow2\n  arch: x86_64\n\n- location: https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-genericcloud-arm64-daily.qcow2\n  arch: aarch64\n\n- location: https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-genericcloud-riscv64-daily.qcow2\n  arch: riscv64\n\n- location: https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-genericcloud-ppc64el-daily.qcow2\n  arch: ppc64le\n\nmountTypesUnsupported: [9p]\n"
  },
  {
    "path": "templates/experimental/fedora-rawhide.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- fedora-rawhide:_images/fedora-rawhide\n- template:_default/mounts\n"
  },
  {
    "path": "templates/experimental/freebsd-16.yaml",
    "content": "minimumLimaVersion: 2.1.0\nimages:\n- location: \"https://download.freebsd.org/ftp/snapshots/VM-IMAGES/16.0-CURRENT/amd64/Latest/FreeBSD-16.0-CURRENT-amd64-BASIC-CLOUDINIT-zfs.raw.xz\"\n  arch: x86_64\n- location: \"https://download.freebsd.org/ftp/snapshots/VM-IMAGES/16.0-CURRENT/aarch64/Latest/FreeBSD-16.0-CURRENT-arm64-aarch64-BASIC-CLOUDINIT-zfs.raw.xz\"\n  arch: aarch64\nos: FreeBSD\nvmType: qemu\nmountType: 9p\nbase: template:_default/mounts\n"
  },
  {
    "path": "templates/experimental/gentoo.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase: template:_default/mounts\n\nimages:\n- location: https://distfiles.gentoo.org/experimental/amd64/openstack/gentoo-openstack-amd64-default-latest.qcow2\n  arch: x86_64\n\nvmType: qemu\n\nmountType: 9p\n\n# The built-in containerd installer does not support Gentoo currently.\ncontainerd:\n  system: false\n  user: false\n"
  },
  {
    "path": "templates/experimental/opensuse-tumbleweed.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase: template:_default/mounts\n\nimages:\n# Hint: run `limactl prune` to invalidate the \"Current\" cache\n- location: https://download.opensuse.org/tumbleweed/appliances/openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2\n  arch: x86_64\n\n- location: https://download.opensuse.org/ports/aarch64/tumbleweed/appliances/openSUSE-Tumbleweed-Minimal-VM.aarch64-Cloud.qcow2\n  arch: aarch64\n"
  },
  {
    "path": "templates/experimental/rke2.yaml",
    "content": "# Deploy kubernetes via rke2 (which installs a bundled containerd).\n# $ limactl start ./rke2.yaml\n# $ limactl shell rke2 sudo kubectl\n#\n# It can be accessed from the host by exporting the kubeconfig file;\n# the ports are already forwarded automatically by lima:\n#\n# $ export KUBECONFIG=$(limactl list rke2 --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\n# $ kubectl get no\n# NAME            STATUS   ROLES                       AGE   VERSION\n# lima-rke2       Ready    control-plane,etcd,master   68s   v1.27.3+rke2r1\n#\n# For more details of RKE2, please refer to https://docs.rke2.io/\n\nminimumLimaVersion: 2.0.0\n\nbase: template:_images/ubuntu-lts\n\n# Mounts are disabled in this template, but can be enabled optionally.\nmounts: []\n\n# containerd is managed by rke2, not by Lima, so the values are set to false here.\ncontainerd:\n  system: false\n  user: false\n\nprovision:\n- mode: system\n  script: |\n    #!/bin/sh\n    if [ ! -d /var/lib/rancher/rke2 ]; then\n      curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=v1.34 sh -\n    fi\n    env | grep \"http_proxy\\|https_proxy\\|no_proxy\" > /etc/default/rke2-server\n    sed -i \"s/http_proxy/CONTAINERD_HTTP_PROXY/g\" /etc/default/rke2-server\n    sed -i \"s/https_proxy/CONTAINERD_HTTPS_PROXY/g\" /etc/default/rke2-server\n    sed -i \"s/no_proxy/CONTAINERD_NO_PROXY/g\" /etc/default/rke2-server\n\n    systemctl start rke2-server.service\n\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until test -f /etc/rancher/rke2/rke2.yaml; do sleep 3; done\"; then\n            echo >&2 \"rke2 is not running yet\"\n            exit 1\n    fi\n  hint: |\n    The rke2 kubeconfig file has not yet been created.\n    Run \"limactl shell rke2 sudo journalctl -u rke2-server\" to check the log.\n    If that is still empty, check the bottom of the log at \"/var/log/cloud-init-output.log\".\ncopyToHost:\n- guest: \"/etc/rancher/rke2/rke2.yaml\"\n  host: \"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  deleteOnStop: true\nmessage: |\n  To run `kubectl` on the host (assumes kubectl is installed), run the following commands:\n  ------\n  export KUBECONFIG=\"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  kubectl ...\n  ------\n"
  },
  {
    "path": "templates/experimental/u7s.yaml",
    "content": "# Deploy kubernetes via usernetes.\n# $ limactl start ./u7s.yaml\n# $ limactl shell u7s kubectl\n\n# It can be accessed from the host by exporting the kubeconfig file;\n# the ports are already forwarded automatically by lima:\n#\n# $ export KUBECONFIG=$(limactl list u7s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\n# $ kubectl get no\n# NAME           STATUS   ROLES           AGE   VERSION\n# u7s-lima-u7s   Ready    control-plane   33s   v1.28.0\n\nminimumLimaVersion: 2.0.0\n\nbase: template:_images/ubuntu-lts\n\n# Mounts are disabled in this template, but can be enabled optionally.\nmounts: []\n# containerd is managed by Docker, not by Lima, so the values are set to false here.\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v kubectl >/dev/null 2>&1 && exit 0\n    version=$(curl -L -s https://dl.k8s.io/release/stable.txt)\n    case $(uname -m) in\n      x86_64)   arch=amd64;;\n      aarch64)  arch=arm64;;\n    esac\n    curl -L \"https://dl.k8s.io/release/$version/bin/linux/$arch/kubectl\" -o /usr/local/bin/kubectl\n    chmod 755 /usr/local/bin/kubectl\n    kubectl version --client\n- mode: user\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    test -d ~/usernetes && exit 0\n    cd ~\n    git clone --branch=gen2-v20251218.0 https://github.com/rootless-containers/usernetes\n- mode: user\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    cd ~/usernetes/init-host\n    sudo ./init-host.root.sh\n    ./init-host.rootless.sh\n- mode: user\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    test -e ~/usernetes/kubeconfig && exit 0\n    cd ~/usernetes\n    export KUBECONFIG=\"$(pwd)/kubeconfig\"\n    make up\n    sleep 5\n    make kubeadm-init\n    # Installing a Pod network add-on\n    make install-flannel\n    # Control plane node isolation\n    make kubeconfig\n    kubectl taint nodes --all node-role.kubernetes.io/control-plane-\n    # Symlink the kubeconfig file to the default location for kubectl\n    mkdir -p ~/.kube && ln -sf $KUBECONFIG ~/.kube/config\n    # Replace the server address with localhost, so that it works from the host.\n    # The original kubeconfig is kept unmodified, so that `kubeadm token create --print-join-command`\n    # can still print the reachable address.\n    sed -e \"/server:/ s|https://.*:\\([0-9]*\\)$|https://127.0.0.1:\\1|\" $KUBECONFIG >kubeconfig.localhost\nprobes:\n- description: \"kubectl to be installed\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v kubectl >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"kubectl is not installed yet\"\n      exit 1\n    fi\n  hint: |\n    See \"/var/log/cloud-init-output.log\" in the guest\n- description: \"kubeadm to be completed\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 300s bash -c \"until test -f ~/usernetes/kubeconfig; do sleep 3; done\"; then\n      echo >&2 \"k8s is not running yet\"\n      exit 1\n    fi\n  hint: |\n    The k8s kubeconfig file has not yet been created.\n- description: \"kubernetes cluster to be running\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 300s bash -c \"until kubectl version >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"kubernetes cluster is not up and running yet\"\n      exit 1\n    fi\n- description: \"coredns deployment to be running\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    kubectl wait -n kube-system --timeout=180s --for=condition=available deploy coredns\ncopyToHost:\n- guest: \"{{.Home}}/usernetes/kubeconfig.localhost\"\n  host: \"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  deleteOnStop: true\nmessage: |\n  To run `kubectl` on the host (assumes kubectl is installed), run the following commands:\n  ------\n  export KUBECONFIG=\"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  kubectl ...\n  ------\n"
  },
  {
    "path": "templates/experimental/ubuntu-26.04.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase: template:_default/mounts\n\nimages:\n- location: https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-amd64.img\n  arch: x86_64\n- location: https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-arm64.img\n  arch: aarch64\n- location: https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-armhf.img\n  arch: armv7l\n- location: https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-riscv64.img\n  arch: riscv64\n- location: https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-s390x.img\n  arch: s390x\n- location: https://cloud-images.ubuntu.com/resolute/current/resolute-server-cloudimg-ppc64el.img\n  arch: ppc64le\n"
  },
  {
    "path": "templates/experimental/vnc.yaml",
    "content": "# A template to run ubuntu using display: vnc\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-lts\n- template:_default/mounts\n\nvmType: \"qemu\"\nvideo:\n  display: \"vnc\"\n\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v Xorg >/dev/null 2>&1 && exit 0\n    export DEBIAN_FRONTEND=noninteractive\n    # x-terminal-emulator x-session-manager x-window-manager\n    apt-get install -y xorg xterm openbox hsetroot tint2 slim\n    printf \"auto_login yes\\ndefault_user {{.User}}\\n\" >>/etc/slim.conf\n    # configure some nice lima green, set up panel and apps\n    printf \"hsetroot -solid \\\"#32CD32\\\" &\\ntint2 &\\n\" >>/etc/xdg/openbox/autostart\n    sed -i 's/Clearlooks/Clearlooks-Olive/' /etc/xdg/openbox/rc.xml # go for green\n    apt-get install -y --no-install-recommends dillo xfe # x-www-browser +explorer\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    systemctl set-default graphical.target\n    systemctl isolate graphical.target\nprobes:\n- description: \"Xorg to be installed\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v Xorg >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"Xorg is not installed yet\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\nmessage: |\n  Use a VNC viewer or noVNC, to connect to the display:\n\n  * VNC Display:    see <file://{{.Dir}}/vncdisplay>\n  * VNC Password:   see <file://{{.Dir}}/vncpassword>\n"
  },
  {
    "path": "templates/experimental/wsl2.yaml",
    "content": "# This template requires Lima v0.18.0 or later and only works on Windows versions\n# that support WSL2 (Windows 10 Build >= 19041, all Windows 11).\nvmType: wsl2\n\nimages:\n# Source: https://github.com/runfinch/finch-core/blob/main/rootfs/Dockerfile (image based on fedora)\n- location: \"https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1771357941.tar.gz\"\n  arch: \"x86_64\"\n  digest: \"sha256:423d1a0f1cabeaea6801995c90ed896dccc091180068626430f19fd87853fdf3\"\n\nmountType: wsl2\n\n# Use system because of an error when setting up RootlessKit (see https://github.com/microsoft/WSL/issues/8842)\n# There are possible workarounds, just not implemented yet.\ncontainerd:\n  system: true\n  user: false\n"
  },
  {
    "path": "templates/faasd.yaml",
    "content": "# Deploy faasd (which installs a bundled containerd).\n#\n# It can be accessed from the host by authenticating with faas-cli;\n# the ports are already forwarded automatically by lima.\n\nmessage: |\n  # Get the faas-cli from one of following sources:\n  # package manager:\n  brew install faas-cli\n  #\n  # script:\n  curl -sLS https://cli.openfaas.com | sh\n  #\n  # You can now log into your gateway:\n  ------\n  export OPENFAAS_URL=http://localhost:8080\n  limactl shell faasd sudo cat /var/lib/faasd/secrets/basic-auth-password | faas-cli login -u admin --password-stdin\n  ------\n  #\n  # Once logged in, you can deploy your first function\n  ------\n  faas-cli store deploy NodeInfo\n  ------\n\nminimumLimaVersion: 2.0.0\n\nbase: template:_images/ubuntu-lts\n\n# Mounts are disabled in this template, but can be enabled optionally.\nmounts: []\n\n# containerd is installed by the faasd installer script, not by Lima, so the values are set to false here.\ncontainerd:\n  system: false\n  user: false\n\nprovision:\n- mode: user\n  script: |\n    #!/bin/sh\n    curl -sfL https://raw.githubusercontent.com/openfaas/faasd/master/hack/install.sh | bash -s -\n\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c 'while [[ \"$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8080/healthz)\" != \"200\" ]]; do sleep 5; done'; then\n            echo >&2 \"faasd is not running yet\"\n            exit 1\n    fi\n  hint: |\n    The faasd service is not yet running.\n    Run \"limactl shell faasd sudo journalctl -u faasd\" to check the log.\n    If that is still empty, check the bottom of the log at \"/var/log/cloud-init-output.log\".\n"
  },
  {
    "path": "templates/fedora-41.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/fedora-41\n- template:_default/mounts\n"
  },
  {
    "path": "templates/fedora-42.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/fedora-42\n- template:_default/mounts\n"
  },
  {
    "path": "templates/fedora-43.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/fedora-43\n- template:_default/mounts\n"
  },
  {
    "path": "templates/freebsd-15.yaml",
    "content": "minimumLimaVersion: 2.1.0\n\nbase:\n- template:_images/freebsd-15\n# - template:_default/mounts\n\n# Mounts are not functional in freebsd-15.\n# Seems to be working in experimental/freebsd-16.\n"
  },
  {
    "path": "templates/homebrew-macos.yaml",
    "content": "# Homebrew on macOS.\n#\n# Useful for sandboxing `brew` in an isolated environment.\n\nminimumLimaVersion: 2.1.0\n\nbase:\n- template:_images/macos\n# Remove this line to disable the mounts\n- template:_default/mounts\n\n# The macOS installer seems to require the display device to be present.\nvideo:\n  display: \"default\"\n\nmessage: |\n  The user password for GUI session is stored in the `~/password` file in the guest.\n  Consider changing it after the first login.\n\nprovision:\n- mode: data\n  path: /usr/local/bin/lima-sudo-askpass.sh\n  permissions: 755\n  content: |\n    #!/bin/sh\n    set -eu\n    # This script is expected to be used only for the initial setup.\n    # This script stops working after the user changes the password.\n    cat \"$HOME/password\"\n- mode: user\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    [ -e /opt/homebrew ] && exit 0\n    curl -o homebrew-install.sh -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh\n    SUDO_ASKPASS=/usr/local/bin/lima-sudo-askpass.sh NONINTERACTIVE=1 /bin/bash homebrew-install.sh\n    rm -f homebrew-install.sh\n    # No /etc/profile.d on macOS\n    echo >> ~/.zprofile\n    echo 'eval \"$(/opt/homebrew/bin/brew shellenv zsh)\"' >> ~/.zprofile\n    echo >> ~/.bash_profile\n    echo 'eval \"$(/opt/homebrew/bin/brew shellenv bash)\"' >> ~/.bash_profile\n"
  },
  {
    "path": "templates/k0s.yaml",
    "content": "# Deploy kubernetes via k0s (which installs a bundled containerd).\n# $ limactl start ./k0s.yaml\n# $ limactl shell k0s kubectl\n#\n# It can be accessed from the host by exporting the kubeconfig file;\n# the ports are already forwarded automatically by lima:\n#\n# $ export KUBECONFIG=$(limactl list k0s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\n# $ kubectl get no\n# NAME       STATUS   ROLES                  AGE   VERSION\n#  lima-k0s   Ready    control-plane   2m48s   v1.33.2+k0s\n\nminimumLimaVersion: 2.0.0\n\nbase: template:_images/ubuntu-lts\n\n# Mounts are disabled in this template, but can be enabled optionally.\nmounts: []\n\n# containerd is managed by k0s, not by Lima, so the values are set to false here.\ncontainerd:\n  system: false\n  user: false\n\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v k0s >/dev/null 2>&1 && exit 0\n\n    # install k0s prerequisites\n    curl -sfL https://get.k0s.sh | sh\n\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n\n    #  start k0s as a single node cluster\n    if ! systemctl status k0scontroller >/dev/null 2>&1; then\n      k0s install controller --single\n    fi\n\n    systemctl start k0scontroller\n\nprobes:\n- description: \"k0s to be running\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until sudo test -f /var/lib/k0s/pki/admin.conf; do sleep 3; done\"; then\n      echo >&2 \"k0s kubeconfig file has not yet been created\"\n      exit 1\n    fi\n  hint: |\n    The k0s control plane is not ready yet.\n    Run \"limactl shell k0s sudo journalctl -u k0scontroller\" to debug.\n\ncopyToHost:\n- guest: \"/var/lib/k0s/pki/admin.conf\"\n  host: \"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  deleteOnStop: true\nmessage: |\n  To run `kubectl` on the host (assumes kubectl is installed), run the following commands:\n  ------\n  export KUBECONFIG=\"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  kubectl ...\n  ------\n"
  },
  {
    "path": "templates/k3s.yaml",
    "content": "# Deploy kubernetes via k3s (which installs a bundled containerd).\n# $ limactl start ./k3s.yaml\n# $ limactl shell k3s kubectl\n#\n# It can be accessed from the host by exporting the kubeconfig file;\n# the ports are already forwarded automatically by lima:\n#\n# $ export KUBECONFIG=$(limactl list k3s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\n# $ kubectl get no\n# NAME       STATUS   ROLES                  AGE   VERSION\n# lima-k3s   Ready    control-plane,master   69s   v1.21.1+k3s1\n\n# A multi-node cluster can be created by starting multiple instances of this template\n# connected via the `lima:user-v2` network.\n#\n# $ limactl start --name k3s-0 --network lima:user-v2 template:k3s\n# $ printf \"https://lima-%s.internal:6443\\n\" k3s-0\n# (The url for the start command printed here)\n# $ limactl shell k3s-0 sudo cat /var/lib/rancher/k3s/server/node-token\n# (The token for the start command printed here)\n#\n# $ limactl start --name k3s-1 --network lima:user-v2 template:k3s \\\n#                 --set '.param.url=\"<URL_FROM_ABOVE>\" | .param.token=\"<TOKEN_FROM_ABOVE>\"'\n\nminimumLimaVersion: 2.0.0\n\nbase: template:_images/ubuntu-lts\n\n# Mounts are disabled in this template, but can be enabled optionally.\nmounts: []\n# containerd is managed by k3s, not by Lima, so the values are set to false here.\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  script: |\n    #!/bin/sh\n    if [ ! -d /var/lib/rancher/k3s ]; then\n    {{if not ( and .Param.url .Param.token )}}\n            curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\"server --write-kubeconfig-mode 644\" sh -\n    {{else}}\n            curl -sfL https://get.k3s.io | K3S_URL={{.Param.url}} K3S_TOKEN={{.Param.token}} sh -\n    {{end}}\n    fi\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    {{if not ( and .Param.url .Param.token )}}\n    if ! timeout 30s bash -c \"until test -f /etc/rancher/k3s/k3s.yaml; do sleep 3; done\"; then\n            echo >&2 \"k3s is not running yet\"\n            exit 1\n    fi\n    {{else}}\n    # create an empty file so that the \"copyToHost\" does not fail\n    sudo mkdir -p /etc/rancher/k3s && sudo touch /etc/rancher/k3s/k3s.yaml\n    {{end}}\n  hint: |\n    The k3s kubeconfig file has not yet been created.\n    Run \"limactl shell k3s sudo journalctl -u k3s\" to check the log.\n    If that is still empty, check the bottom of the log at \"/var/log/cloud-init-output.log\".\ncopyToHost:\n- guest: \"/etc/rancher/k3s/k3s.yaml\"\n  host: \"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  deleteOnStop: true\nmessage: |\n  {{- if not ( and .Param.url .Param.token ) -}}\n  To run `kubectl` on the host (assumes kubectl is installed), run the following commands:\n  ------\n  export KUBECONFIG=\"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  kubectl ...\n  ------\n  {{end}}\nparam:\n  url: \"\"\n  token: \"\"\n"
  },
  {
    "path": "templates/k8s.yaml",
    "content": "# Deploy kubernetes via kubeadm.\n# $ limactl start ./k8s.yaml\n# $ limactl shell k8s kubectl\n\n# It can be accessed from the host by exporting the kubeconfig file;\n# the ports are already forwarded automatically by lima:\n#\n# $ export KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\n# $ kubectl get no\n# NAME       STATUS   ROLES                  AGE   VERSION\n# lima-k8s   Ready    control-plane,master   44s   v1.22.3\n\n# A multi-node cluster can be created by starting multiple instances of this template\n# connected via the `lima:user-v2` network.\n#\n# $ limactl start --name k8s-0 --network lima:user-v2 template:k8s\n# $ limactl shell k8s-0 sudo kubeadm token create --print-join-command\n# (The parameters for the start command printed here)\n#\n# $ limactl start --name k8s-1 --network lima:user-v2 template:k8s \\\n#                 --set '.param.url=\"https://<ADDRESS_FROM_ABOVE>\" | .param.token=\"<TOKEN_FROM_ABOVE>\" | \\\n#                        .param.discoveryTokenCaCertHash=\"<DISCOVERY_TOKEN_CA_CERT_HASH_FROM_ABOVE>\"'\n\nminimumLimaVersion: 2.0.0\n\nbase: template:_images/ubuntu-lts\n\n# Mounts are disabled in this template, but can be enabled optionally.\nmounts: []\ncontainerd:\n  system: true\n  user: false\nprovision:\n# See <https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/>\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v kubeadm >/dev/null 2>&1 && exit 0\n    # Install and configure prerequisites\n    cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf\n    overlay\n    br_netfilter\n    EOF\n    modprobe overlay\n    modprobe br_netfilter\n    cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes-cri.conf\n    net.bridge.bridge-nf-call-iptables  = 1\n    net.ipv4.ip_forward                 = 1\n    net.bridge.bridge-nf-call-ip6tables = 1\n    EOF\n    sysctl --system\n    # Installing kubeadm, kubelet and kubectl\n    export DEBIAN_FRONTEND=noninteractive\n    apt-get update\n    apt-get install -y apt-transport-https ca-certificates curl\n    VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt | sed -e 's/v//' | cut -d'.' -f1-2)\n    echo \"deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${VERSION}/deb/ /\" | sudo tee /etc/apt/sources.list.d/kubernetes.list\n    curl -fsSL https://pkgs.k8s.io/core:/stable:/v${VERSION}/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg\n    apt-get update\n    apt-get install -y kubelet kubeadm kubectl && apt-mark hold kubelet kubeadm kubectl\n    systemctl enable --now kubelet\n# See <https://kubernetes.io/docs/setup/production-environment/container-runtimes/>\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    [ -e /etc/containerd/conf.d/k8s.toml ] && exit 0\n    mkdir -p /etc/containerd/conf.d\n    # Configuring the systemd cgroup driver\n    # Overriding the sandbox (pause) image\n    cat <<EOF >/etc/containerd/conf.d/k8s.toml\n    version = 2\n    [plugins]\n      [plugins.\"io.containerd.grpc.v1.cri\"]\n        sandbox_image = \"$(kubeadm config images list | grep pause | sort -r | head -n1)\"\n        [plugins.\"io.containerd.grpc.v1.cri\".containerd]\n          [plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes]\n            [plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc]\n              runtime_type = \"io.containerd.runc.v2\"\n              [plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc.options]\n                SystemdCgroup = true\n      [plugins.\"io.containerd.cri.v1.runtime\".cni]\n        bin_dirs = [\"/usr/local/libexec/cni\",\"/opt/cni/bin\"]\n    EOF\n    systemctl restart containerd\n# See <https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/>\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    test -e /etc/kubernetes/admin.conf && exit 0\n    export KUBECONFIG=/etc/kubernetes/admin.conf\n    {{if not ( and .Param.url .Param.token )}}\n    systemctl stop kubelet\n    kubeadm config images list\n    kubeadm config images pull --cri-socket=unix:///run/containerd/containerd.sock\n    systemctl start kubelet\n    # Initializing your control-plane node\n    cat <<EOF >kubeadm-config.yaml\n    kind: InitConfiguration\n    apiVersion: kubeadm.k8s.io/v1beta4\n    nodeRegistration:\n      criSocket: unix:///run/containerd/containerd.sock\n    ---\n    kind: ClusterConfiguration\n    apiVersion: kubeadm.k8s.io/v1beta4\n    apiServer:\n      certSANs: # --apiserver-cert-extra-sans\n      - \"127.0.0.1\"\n    networking:\n      podSubnet: \"10.244.0.0/16\" # --pod-network-cidr\n    ---\n    kind: KubeletConfiguration\n    apiVersion: kubelet.config.k8s.io/v1beta1\n    cgroupDriver: systemd\n    EOF\n    kubeadm init --config kubeadm-config.yaml\n    {{else}}\n    cat <<EOF >kubeadm-config.yaml\n    kind: JoinConfiguration\n    apiVersion: kubeadm.k8s.io/v1beta4\n    nodeRegistration:\n      criSocket: unix:///run/containerd/containerd.sock\n    EOF\n    kubeadm join --config kubeadm-config.yaml {{.Param.url}} --token {{.Param.token}} \\\n        --discovery-token-ca-cert-hash {{.Param.discoveryTokenCaCertHash}}\n    {{end}}\n\n    {{if not ( and .Param.url .Param.token )}}\n    # Installing a Pod network add-on\n    kubectl apply -f https://github.com/flannel-io/flannel/releases/download/v0.27.4/kube-flannel.yml\n    # Control plane node isolation\n    kubectl taint nodes --all node-role.kubernetes.io/control-plane-\n    # Symlink the kubeconfig file to the default location for kubectl\n    mkdir -p /root/.kube && ln -sf $KUBECONFIG /root/.kube/config\n    # Replace the server address with localhost, so that it works from the host.\n    # The original kubeconfig is kept unmodified, so that `kubeadm token create --print-join-command`\n    # can still print the reachable address.\n    sed -e \"/server:/ s|https://.*:\\([0-9]*\\)$|https://127.0.0.1:\\1|\" $KUBECONFIG >/root/.kube/config.localhost\n    {{end}}\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    export KUBECONFIG=/etc/kubernetes/admin.conf\n    mkdir -p {{.Home}}/.kube\n    cp -f $KUBECONFIG {{.Home}}/.kube/config\n    chown -R {{.User}} {{.Home}}/.kube\nprobes:\n- description: \"kubeadm to be installed\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v kubeadm >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"kubeadm is not installed yet\"\n      exit 1\n    fi\n  hint: |\n    See \"/var/log/cloud-init-output.log\" in the guest\n- description: \"kubernetes images to be pulled\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    {{if not ( and .Param.url .Param.token )}}\n    if ! timeout 30s bash -c \"images=\\\"$(kubeadm config images list)\\\"; until for image in \\$images; do sudo ctr -n k8s.io image inspect \\$image >/dev/null; done; do sleep 3; done\"; then\n      echo >&2 \"k8s images are not pulled yet\"\n      exit 1\n    fi\n    {{end}}\n- description: \"kubeadm to be completed\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    {{if not ( and .Param.url .Param.token )}}\n    if ! timeout 300s bash -c \"until test -f /etc/kubernetes/admin.conf; do sleep 3; done\"; then\n      echo >&2 \"k8s is not running yet\"\n      exit 1\n    fi\n    {{else}}\n    # create an empty file so that the \"copyToHost\" does not fail\n    sudo mkdir -p /root/.kube && sudo touch /root/.kube/config.localhost\n    {{end}}\n  hint: |\n    The k8s kubeconfig file has not yet been created.\n- description: \"kubernetes cluster to be running\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    {{if not ( and .Param.url .Param.token )}}\n    if ! timeout 300s bash -c \"until kubectl version >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"kubernetes cluster is not up and running yet\"\n      exit 1\n    fi\n    {{end}}\n- description: \"coredns deployment to be running\"\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    {{if not ( and .Param.url .Param.token )}}\n    kubectl wait -n kube-system --timeout=180s --for=condition=available deploy coredns\n    {{end}}\ncopyToHost:\n- guest: \"/root/.kube/config.localhost\"\n  host: \"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  deleteOnStop: true\nmessage: |\n  {{- if not ( and .Param.url .Param.token )}}\n  To run `kubectl` on the host (assumes kubectl is installed), run the following commands:\n  ------\n  export KUBECONFIG=\"{{.Dir}}/copied-from-guest/kubeconfig.yaml\"\n  kubectl ...\n  ------\n  {{end -}}\nparam:\n  url: \"\"\n  token: \"\"\n  discoveryTokenCaCertHash: \"\"\n"
  },
  {
    "path": "templates/linuxbrew.yaml",
    "content": "# Homebrew on Linux.\n#\n# Useful for sandboxing `brew` in an isolated environment.\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-lts\n# Remove this line to disable the mounts\n- template:_default/mounts\n\n# containerd should be installed via Homebrew when Homebrew supports it\ncontainerd:\n  system: false\n  user: false\n\nprovision:\n- mode: data\n  path: /etc/profile.d/99-linuxbrew.sh\n  # nofile is increased due to https://github.com/Homebrew/brew/issues/9120\n  content: |\n    ulimit -n 65536\n    [ -e /home/linuxbrew/.linuxbrew/bin/brew ] && eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v gcc >/dev/null 2>&1 && exit 0\n    export DEBIAN_FRONTEND=noninteractive\n    apt-get update\n    apt-get install -y build-essential\n- mode: user\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    [ -e /home/linuxbrew ] && exit 0\n    /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n"
  },
  {
    "path": "templates/macos-15.yaml",
    "content": "minimumLimaVersion: 2.1.0\n\nbase:\n- template:_images/macos-15\n- template:_default/mounts\n\n# The installer seems to require the display device to be present.\nvideo:\n  display: \"default\"\n\nmessage: |\n  The user password for GUI session is stored in the `~/password` file in the guest.\n  Consider changing it after the first login.\n"
  },
  {
    "path": "templates/macos-26.yaml",
    "content": "minimumLimaVersion: 2.1.0\n\nbase:\n- template:_images/macos-26\n- template:_default/mounts\n\n# The installer seems to require the display device to be present.\nvideo:\n  display: \"default\"\n\nmessage: |\n  The user password for GUI session is stored in the `~/password` file in the guest.\n  Consider changing it after the first login.\n"
  },
  {
    "path": "templates/opensuse-leap-15.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/opensuse-leap-15\n- template:_default/mounts\n"
  },
  {
    "path": "templates/opensuse-leap-16.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/opensuse-leap-16\n- template:_default/mounts\n"
  },
  {
    "path": "templates/oraclelinux-10.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/oraclelinux-10\n- template:_default/mounts\n"
  },
  {
    "path": "templates/oraclelinux-8.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/oraclelinux-8\n- template:_default/mounts\n"
  },
  {
    "path": "templates/oraclelinux-9.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/oraclelinux-9\n- template:_default/mounts\n"
  },
  {
    "path": "templates/podman-rootful.yaml",
    "content": "# A template to use Podman instead of containerd & nerdctl\n# $ limactl start ./podman-rootful.yaml\n# $ limactl shell podman-rootful sudo podman run -it -v $HOME:$HOME --rm docker.io/library/alpine\n\n# To run `podman` on the host (assumes podman-remote is installed):\n# $ export CONTAINER_HOST=$(limactl list podman-rootful --format 'unix://{{.Dir}}/sock/podman.sock')\n# $ podman --remote ...\n\n# To run `docker` on the host (assumes docker-cli is installed):\n# $ export DOCKER_HOST=$(limactl list podman-rootful --format 'unix://{{.Dir}}/sock/podman.sock')\n# $ docker ...\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/fedora\n- template:_default/mounts\n\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v podman >/dev/null 2>&1 && test -e /etc/lima-podman && exit 0\n    if [ ! -e /etc/systemd/system/podman.socket.d/override.conf ]; then\n      mkdir -p /etc/systemd/system/podman.socket.d\n      cat <<-EOF >/etc/systemd/system/podman.socket.d/override.conf\n      [Socket]\n      SocketUser={{.User}}\n    EOF\n    fi\n    if [ ! -e /etc/tmpfiles.d/podman.conf ]; then\n      mkdir -p /etc/tmpfiles.d\n      echo \"d /run/podman 0700 {{.User}} -\" > /etc/tmpfiles.d/podman.conf\n    fi\n    dnf -y install --best podman && touch /etc/lima-podman\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    systemctl --system enable --now podman.socket\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v podman >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"podman is not installed yet\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\nportForwards:\n- guestSocket: \"/run/podman/podman.sock\"\n  hostSocket: \"{{.Dir}}/sock/podman.sock\"\nmessage: |\n  To run `podman` on the host (assumes podman-remote is installed), run the following commands:\n  ------\n  podman system connection add lima-{{.Name}} \"unix://{{.Dir}}/sock/podman.sock\"\n  podman system connection default lima-{{.Name}}\n  podman{{if eq .HostOS \"linux\"}} --remote{{end}} run quay.io/podman/hello\n  ------\n"
  },
  {
    "path": "templates/podman.yaml",
    "content": "# A template to use Podman instead of containerd & nerdctl\n# $ limactl start ./podman.yaml\n# $ limactl shell podman podman run -it -v $HOME:$HOME --rm docker.io/library/alpine\n\n# To run `podman` on the host (assumes podman-remote is installed):\n# $ export CONTAINER_HOST=$(limactl list podman --format 'unix://{{.Dir}}/sock/podman.sock')\n# $ podman --remote ...\n\n# To run `docker` on the host (assumes docker-cli is installed):\n# $ export DOCKER_HOST=$(limactl list podman --format 'unix://{{.Dir}}/sock/podman.sock')\n# $ docker ...\n\nminimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/fedora\n- template:_default/mounts\n\ncontainerd:\n  system: false\n  user: false\nprovision:\n- mode: system\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    command -v podman >/dev/null 2>&1 && test -e /etc/lima-podman && exit 0\n    dnf -y install --best podman && touch /etc/lima-podman\n- mode: user\n  script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    systemctl --user enable --now podman.socket\nprobes:\n- script: |\n    #!/bin/bash\n    set -eux -o pipefail\n    if ! timeout 30s bash -c \"until command -v podman >/dev/null 2>&1; do sleep 3; done\"; then\n      echo >&2 \"podman is not installed yet\"\n      exit 1\n    fi\n  hint: See \"/var/log/cloud-init-output.log\" in the guest\nportForwards:\n- guestSocket: \"/run/user/{{.UID}}/podman/podman.sock\"\n  hostSocket: \"{{.Dir}}/sock/podman.sock\"\nmessage: |\n  To run `podman` on the host (assumes podman-remote is installed), run the following commands:\n  ------\n  podman system connection add lima-{{.Name}} \"unix://{{.Dir}}/sock/podman.sock\"\n  podman system connection default lima-{{.Name}}\n  podman{{if eq .HostOS \"linux\"}} --remote{{end}} run quay.io/podman/hello\n  ------\n"
  },
  {
    "path": "templates/rocky-10.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/rocky-10\n- template:_default/mounts\n"
  },
  {
    "path": "templates/rocky-8.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/rocky-8\n- template:_default/mounts\n"
  },
  {
    "path": "templates/rocky-9.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/rocky-9\n- template:_default/mounts\n"
  },
  {
    "path": "templates/ubuntu-20.04.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-20.04\n- template:_default/mounts\n"
  },
  {
    "path": "templates/ubuntu-22.04.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-22.04\n- template:_default/mounts\n"
  },
  {
    "path": "templates/ubuntu-24.04.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-24.04\n- template:_default/mounts\n"
  },
  {
    "path": "templates/ubuntu-24.10.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-24.10\n- template:_default/mounts\n"
  },
  {
    "path": "templates/ubuntu-25.04.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-25.04\n- template:_default/mounts\n"
  },
  {
    "path": "templates/ubuntu-25.10.yaml",
    "content": "minimumLimaVersion: 2.0.0\n\nbase:\n- template:_images/ubuntu-25.10\n- template:_default/mounts\n"
  },
  {
    "path": "vz.entitlements",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.network.server</key>\n\t<true/>\n\t<key>com.apple.security.network.client</key>\n\t<true/>\n\t<key>com.apple.security.virtualization</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "website/.gitignore",
    "content": "/public\nresources/\nnode_modules/\n.hugo_build.lock\n"
  },
  {
    "path": "website/.nvmrc",
    "content": "lts/*\n"
  },
  {
    "path": "website/Makefile",
    "content": "# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n# See also: package.json\nNPM ?= npm\n\nbuild serve: _output/docsy node_modules/.bin/hugo\n\t$(NPM) run $@\n\n_output/docsy:\n\t$(MAKE) -C .. docsy\n\nnode_modules/.bin/hugo:\n\t$(NPM) install\n\nclean:\n\t$(NPM) run $@\n\trm -Rf _output\n"
  },
  {
    "path": "website/README.md",
    "content": "# The source of the Lima website (https://lima-vm.io)\n\nThis directory is the [Netlify base directory](https://docs.netlify.com/configure-builds/overview/) of [https://lima-vm.io](https://lima-vm.io/) .\n"
  },
  {
    "path": "website/assets/scss/_variables_project.scss",
    "content": "/*\n\nAdd styles or override variables from the theme here.\n\n*/\n\n$display1-weight: 500 !default;\n$display2-weight: 100 !default;\n\n$primary: rgb(255, 255, 255) !default;\n$secondary: rgb(66.274512%, 81.176472%, 21.960784%) !default;\n$dark: black !default;\n$td-sidebar-tree-root-color: #222 !default;\n\n.nav-shadow {\n  box-shadow: 0 2px 2px -2px rgba(0, 0, 0, .2);\n}\n\n.nav-link {\n  text-shadow: none !important;\n}\n\n.cncf-logo {\n  width: 20rem;\n  max-width: 80%;\n}\n\n.join-community {\n  color: $dark !important;\n  background: $secondary !important;\n\n  .td-arrow-down:before {\n    border-color: $secondary transparent transparent transparent !important;\n  }\n}\n\n.td-search__input {\n  color: var(--bs-body-color) !important;\n  border: 1px solid var(--bs-gray-400) !important;\n}\n\n.td-search__input:focus {\n  color: inherit !important;\n  border-color: var(--bs-body-bg) !important;\n  box-shadow: 0 0 0 2px var(--bs-primary) !important;\n}\n\n.td-search__input::placeholder {\n  color: var(--bs-gray) !important;\n}\n\n.td-search__icon {\n  color: $secondary !important;\n}\n"
  },
  {
    "path": "website/config.yaml",
    "content": "# THIS IS A TEST CONFIG ONLY!\n# FOR THE CONFIGURATION OF YOUR SITE USE hugo.yaml.\n#\n# As of Docsy 0.7.0, Hugo 0.110.0 or later must be used.\n#\n# The sole purpose of this config file is to detect Hugo-module builds that use\n# an older version of Hugo.\n#\n# DO NOT add any config parameters to this file. You can safely delete this file\n# if your project is using the required Hugo version.\n\nmodule:\n  hugoVersion:\n    extended: true\n    min: 0.110.0\n"
  },
  {
    "path": "website/content/en/_index.html",
    "content": "+++\ntitle = \"Lima\"\nlinkTitle = \"Lima\"\n\n+++\n\n{{< blocks/cover title=\"\" image_anchor=\"top\" height=\"min\" color=\"white\" >}}\n<div class=\"mx-auto\">\n  <a class=\"btn btn-lg btn-secondary mr-3 mb-4\" href=\"{{< relref \"/docs\" >}}\">\n  Learn More <i class=\"fas fa-arrow-alt-circle-right ml-2\"></i>\n  </a>\n</div>\n{{< /blocks/cover >}}\n\n{{% blocks/lead color=\"secondary\" %}}\n<p>\n  <strong>What is Lima?</strong>\n</p>\n\n<p>\n  Lima launches Linux virtual machines with automatic file sharing and port forwarding (similar to WSL2).\n</p>\n{{% /blocks/lead %}}\n{{< blocks/section color=\"dark\" type=\"row\" >}}\n\n  {{< blocks/helpfullinks >}}\n\n{{< /blocks/section >}}\n\n{{< blocks/adopters >}}\n\n{{< blocks/cncf >}}\n"
  },
  {
    "path": "website/content/en/docs/_index.md",
    "content": "---\ntitle: \"Lima: Linux Machines\"\nlinkTitle: Documentation\nmenu: {main: {weight: 20}}\nweight: 20\n---\n{{% fixlinks %}}\nLima launches Linux virtual machines with automatic file sharing and port forwarding (similar to WSL2).\n\n✅ Automatic file sharing\n\n✅ Automatic port forwarding\n\n✅ Built-in support for [containerd](https://containerd.io) ([Other container engines can be used too]({{< ref \"/docs/examples/containers\" >}}))\n\n✅ Intel on Intel\n\n✅ [ARM on Intel]({{< ref \"/docs/config/multi-arch\" >}})\n\n✅ ARM on ARM\n\n✅ [Intel on ARM]({{< ref \"/docs/config/multi-arch\" >}})\n\n✅ Various guest Linux distributions: [AlmaLinux](./templates/almalinux.yaml), [Alpine](./templates/alpine.yaml), [Arch Linux](./templates/archlinux.yaml), [Debian](./templates/debian.yaml), [Fedora](./templates/fedora.yaml), [openSUSE](./templates/opensuse.yaml), [Oracle Linux](./templates/oraclelinux.yaml), [Rocky](./templates/rocky.yaml), [Ubuntu](./templates/ubuntu.yaml) (default), ...\n\n✅ Experimental support for non-Linux guests: [macOS]({{< ref \"/docs/usage/guests/macos\" >}}), [FreeBSD]({{< ref \"/docs/usage/guests/freebsd\" >}})\n\nRelated project: [sshocker (ssh with file sharing and port forwarding)](https://github.com/lima-vm/sshocker)\n\nThis project is unrelated to [The Lima driver project (driver for ARM Mali GPUs)](https://gitlab.freedesktop.org/lima).\n\n## Project history\n\nLima began in May 2021, aiming at promoting [containerd](https://containerd.io) including [nerdctl (contaiNERD ctl)](https://github.com/containerd/nerdctl)\nto Mac users.\nLater the project scope was expanded to support other containers\nand non-container applications as well.\nLima also supports non-macOS hosts (Linux, NetBSD, etc.).\n\nLima joined the [Cloud Native Computing Foundation (CNCF)](https://cncf.io)\nin September 2022 as a Sandbox project.\nThe project was promoted to the Incubating level in October 2025.\n\n{{% /fixlinks %}}\n"
  },
  {
    "path": "website/content/en/docs/community/_index.md",
    "content": "---\ntitle: Community\nweight: 400\n---\n\nThe GitHub repo of Lima can be found at <https://github.com/lima-vm/lima>.\n\n## Communication channels\n\n- [GitHub Discussions](https://github.com/lima-vm/lima/discussions)\n\n- `#lima` channel in the CNCF Slack\n  - New account: <https://slack.cncf.io/>\n  - Login: <https://cloud-native.slack.com/>\n\n- Zoom meetings (tentatively monthly)\n  - Meeting notes & agenda proposals: https://github.com/lima-vm/lima/discussions/categories/meetings\n  - Calendar: https://zoom-lfx.platform.linuxfoundation.org/meetings/lima\n\n- See <https://github.com/lima-vm/.github/blob/main/SECURITY.md> for how to report security issues.\n\n## Social media accounts\n\nFollow us for project updates, release announcements, and community news:\n\n- https://x.com/@TheLimaProject\n- https://mastodon.social/@TheLimaProject\n\n## Projects using Lima\n\n### Container environments\n- [Rancher Desktop](https://rancherdesktop.io/): Kubernetes and container management to the desktop\n- [Colima](https://github.com/abiosoft/colima): Docker (and Kubernetes) on macOS with minimal setup\n- [Finch](https://github.com/runfinch/finch): Finch is a command line client for local container development\n- [Podman Desktop](https://podman-desktop.io/): Podman Desktop GUI has a plug-in for Lima virtual machines\n\n### GUI\n- [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.\n- [lima-gui](https://github.com/afbjorklund/lima-gui): Qt GUI for Lima\n"
  },
  {
    "path": "website/content/en/docs/community/contributing.md",
    "content": "---\ntitle: Contributing\nweight: 20\n---\n\n## Reporting issues\n\nBugs and feature requests can be submitted via <https://github.com/lima-vm/lima/issues>.\n\nFor asking questions, use [GitHub Discussions](https://github.com/lima-vm/lima/discussions) or [Slack (`#lima`)](https://slack.cncf.io).\n\nFor reporting vulnerabilities, see <https://github.com/lima-vm/.github/blob/main/SECURITY.md>.\n\n## Contributing code\n\n### Getting Involved\n\nWe welcome new contributors! Here are some ways to get started and engage with the Lima community:\n\n#### Introduce Yourself\n\n- Join our [community communication channels](https://lima-vm.io/docs/community/#communication-channels) (Slack, GitHub Discussions, Zoom meetings) and say hello! Let us know your interests and how you’d like to help. Also share how your [organization](https://github.com/lima-vm/lima/discussions/2390) is involved with Lima.\n\n#### Learn Where Work Is Needed\n\n- Check the [Lima Roadmap](https://lima-vm.io/docs/community/roadmap/), related [issues](https://github.com/lima-vm/lima/issues), and [discussions](https://github.com/lima-vm/lima/discussions) to see ongoing and planned work.\n- Read through the [documentation](https://lima-vm.io/docs/) to understand the project’s goals and architecture.\n\n#### Find Open Issues\n\n- Browse [GitHub Issues](https://github.com/lima-vm/lima/issues) labeled as [`good first issue`](https://github.com/lima-vm/lima/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) for tasks that are great for new contributors.\n- If you’re unsure where to start, ask in the community channels or open a new discussion.\n\nWe’re glad to have you here, your contributions make Lima better!\n\n### Developer Certificate of Origin\n\nEvery commit must be signed off with the `Signed-off-by: REAL NAME <email@example.com>` line.\n\nUse the `git commit -s` command to add the Signed-off-by line.\n\nSee also <https://github.com/cncf/foundation/blob/main/policies-guidance/dco-guidelines.md>.\n\n### Licensing\n\nLima is licensed under the terms of [Apache License, Version 2.0](https://github.com/lima-vm/lima/blob/master/LICENSE).\n\nSee also <https://github.com/cncf/foundation/blob/main/policies-guidance/allowed-third-party-license-policy.md> for third-party dependencies.\n\n### Sending pull requests\n\nPull requests can be submitted to <https://github.com/lima-vm/lima/pulls>.\n\nIt is highly suggested to add [tests](../../dev/testing/) for every non-trivial pull requests.\nA test can be implemented as a unit test rather than an integration test when it is possible,\nto avoid slowing the integration test CI.\n\nFor tips on squashing commits and rebasing before submitting your pull request, see [Git Tips](../dev/git.md).\n\n### Merging pull requests\n\n[Committers](../governance) can merge pull requests.\n[Reviewers](../governance) can approve, but cannot merge, pull requests.\n\nA Committer shouldn't merge their own pull requests without approval by at least one other Maintainer (Committer or Reviewer).\n\nThis rule does not apply to trivial pull requests such as fixing typos, CI failures,\nand updating image references in templates (e.g., <https://github.com/lima-vm/lima/pull/2318>).\n"
  },
  {
    "path": "website/content/en/docs/community/governance.md",
    "content": "---\ntitle: Governance\nweight: 10\n---\n\n<!-- The governance model is similar to https://github.com/containerd/project/blob/main/GOVERNANCE.md but simplified -->\n\n## Code of Conduct\nLima follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).\n\n## Maintainership\nLima is governed by Maintainers who are elected from active contributors.\n\nAs a [Cloud Native Computing Foundation](https://cncf.io/) project, Lima will keep its [vendor-neutrality](https://contribute.cncf.io/maintainers/community/vendor-neutrality/).\n\n### Roles\nMaintainers consist of two roles:\n\n- **Committer** (Full maintainership): Committers have full write accesses to repos under <https://github.com/lima-vm>.\n  Committers' commits should still be made via GitHub pull requests (except for urgent security fixes), and should not be pushed directly.\n  Committers must enable 2FA for their GitHub accounts.\n  Committers are also recognized as Maintainers in <https://github.com/cncf/foundation/blob/main/project-maintainers.csv>.\n\n- **Reviewer** (Limited maintainership): Reviewers may moderate GitHub issues and pull requests (such as adding labels and cleaning up spams), and make posts on [social accounts](./_index.md#social-accounts),\n  but they do not have any access to merge pull requests nor push commits.\n  A Reviewer is considered as a candidate to become a Committer.\n  Reviewers are not recognized as Maintainers in <https://github.com/cncf/foundation/blob/main/project-maintainers.csv>.\n\nAccess control is enforced via GitHub team membership:\n* All committers are members of the `@lima-vm/committers` team and have the `Maintain` role on all repos.\n* All reviewers are members of the `@lima-vm/reviewers` team and have the `Triage` role on all repos.\n* The `@lima-vm/maintainers` team includes both committers and reviewers, but doesn't grant any extra access.\n\nSee also the [Contributing](../contributing) page.\n\n### Current maintainers\n\n<!-- When updating the affiliation, update https://github.com/cncf/foundation/blob/main/project-maintainers.csv too -->\n\n| Name               | Affiliation  | Role      | GitHub ID (not X ID)                             | GPG fingerprint                                                                           |\n|--------------------|--------------|-----------|--------------------------------------------------|-------------------------------------------------------------------------------------------|\n| Akihiro Suda       | NTT          | Committer | [@AkihiroSuda](https://github.com/AkihiroSuda)   | [C020 EA87 6CE4 E06C 7AB9  5AEF 4952 4C6F 9F63 8F1A](https://github.com/AkihiroSuda.gpg)  |\n| Jan Dubois         | SUSE         | Committer | [@jandubois](https://github.com/jandubois)       | [DBF6 DA01 BD81 2D63 3B77  300F A2CA E583 3B6A D416](https://github.com/jandubois.gpg)    |\n| Anders F Björklund | Individual   | Committer | [@afbjorklund](https://github.com/afbjorklund)   | [5981 D2E8 4E4B 9197 95B3  2174 DC05 CAD2 E73B 0C92](https://github.com/afbjorklund.gpg)  |\n| Balaji Vijayakumar | Thoughtworks | Committer | [@balajiv113](https://github.com/balajiv113)     | [80E1 01FE 5C89 FCF6 6171  72C8 377C 6A63 934B 8E6E](https://github.com/balajiv113.gpg)   |\n| Norio Nomura       | Individual   | Committer | [@norio-nomura](https://github.com/norio-nomura) | [0010 36FA 2504 DBFF 37BA  2EF8 D4A7 318E B7F7 138D](https://github.com/norio-nomura.gpg) |\n| Oleksandr Redko    | Individual   | Reviewer  | [@alexandear](https://github.com/alexandear)     | [50F8 9811 D8D8 3E79 3E7E  0680 A947 E3F1 1A61 2A57](https://github.com/alexandear.gpg)   |\n| Nir Soffer         | IBM          | Reviewer  | [@nirs](https://github.com/nirs)                 | [6F81 B717 51A1 4171 4C09  AF19 4C67 29D7 B2DD 8AFF](https://github.com/nirs.gpg)         |\n| Ansuman Sahoo      | Individual   | Reviewer  | [@unsuman](https://github.com/unsuman)           | [0AA6 D4D0 E084 D8B9 36B7  68D8 FF58 56B9 C216 D800](https://github.com/unsuman.gpg)      |\n\n### Emeritus maintainers\n(No emeritus maintainers yet)\n\n### Addition and promotion of Maintainers\nAn active contributor to the project can be invited as a Reviewer,\nand can be eventually promoted to a Committer after 2 months at least.\n\nA contributor who have made significant contributions in quality and in quantity\ncan be also directly invited as a Committer.\n\nA proposal to add or promote a Maintainer must be approved by 2/3 of the Committers who vote within 7 days.\nVoting needs 2 approvals at least. The proposer can vote too.\n\nA proposal should happen as a GitHub pull request to the Maintainer list above.\nIt is highly suggested to reach out to the Committers before submitting a pull request to check the will of the Committers.\n\n### Removal and demotion of Maintainers\nA Maintainer who do not show significant activities for 6 months, or, who have been violating the Code of Conduct,\nmay be demoted or removed from the project.\n\nA proposal to demote or remove a Maintainer must be approved by 2/3 of the Committers (excluding the person in question) who vote within 14 days.\nVoting needs 2 approvals at least. The proposer can vote too.\n\nA proposal may happen as a GitHub pull request, or, as a private discussion in the case of removal of a harmful Maintainer.\nIt is highly suggested to reach out to the Committers before submitting a pull request to check the will of the Committers.\n\n### Other decisions\nAny decision that is not documented here can be made by the Committers.\nWhen a dispute happens across the Committers, it will be resolved through a majority vote within the Committers.\nA tie should be considered as a failed vote.\n\n## Release process\n\nEligibility to be a release manager:\n- MUST be an active Committer\n- MUST have the GPG fingerprint listed in the maintainer list above\n- MUST upload the GPG public key to `https://github.com/USERNAME.gpg`\n- MUST protect the GPG key with a passphrase or a hardware token.\n\nRelease steps:\n- Open an issue to propose making a new release, e.g. <https://github.com/lima-vm/lima/issues/2296>.\n  The proposal should be public, with an exception for vulnerability fixes.\n  If this is the first time for you to take a role of release management,\n  you SHOULD make a beta (or alpha, RC) release as an exercise before releasing GA.\n- Make sure that all the merged PRs are associated with the correct [Milestone](https://github.com/lima-vm/lima/milestones).\n- Run `git tag --sign vX.Y.Z-beta.W` .\n- Run `git push UPSTREAM vX.Y.Z-beta.W` .\n- Wait for the `Release` action on GitHub Actions to complete. A draft release will appear in https://github.com/lima-vm/lima/releases .\n- Download `SHA256SUMS` from the draft release, and confirm that it corresponds to the hashes printed in the build logs on the `Release` action.\n- Sign `SHA256SUMS` with `gpg --detach-sign -a SHA256SUMS` to produce `SHA256SUMS.asc`, and upload it to the draft release.\n- Add release notes in the draft release, to explain the changes and show appreciation to the contributors.\n  Make sure to fulfill the `Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE])` line with your name.\n  e.g., `Release manager: Akihiro Suda (@AkihiroSuda)` .\n- Click the `Set as a pre-release` checkbox if this release is a beta (or alpha, RC).\n- Click the `Publish release` button.\n- Close the [Milestone](https://github.com/lima-vm/lima/milestones).\n"
  },
  {
    "path": "website/content/en/docs/community/roadmap.md",
    "content": "---\ntitle: Roadmap\nweight: 50\n---\n\nInstead of using a static text file, Lima uses the `roadmap` label on GitHub issues to designate features or bug fixes that we plan to implement.\n\nIssues are tagged with the `roadmap` label when at least one maintainer or contributor has declared intent to work on or help with the implementation.\n\nThere are no commitments or timelines attached to the label, and the label may be removed again from an abandoned issue at any time.\n\nNon-roadmap issues are kept open (as long as they fit the scope of the project) in case a volunteer one day appears and offers to work on them.\n\nTo find the items currently planned for Lima you can filter on [open issues with the `roadmap` label]( https://github.com/lima-vm/lima/issues?q=is%3Aissue+is%3Aopen+label%3Aroadmap).\n"
  },
  {
    "path": "website/content/en/docs/community/subprojects.md",
    "content": "---\ntitle: Subprojects\nweight: 90\n---\n\nSome portions of Lima are useful for other projects too and split out to separate repos:\n\n<!-- sorted by importance to end users -->\n- <https://github.com/lima-vm/socket_vmnet>: vmnet.framework support for unmodified rootless QEMU\n- <https://github.com/lima-vm/lima-actions>: run Lima on GitHub Actions\n- <https://github.com/lima-vm/go-qcow2reader>: qcow2 reader for Go\n- <https://github.com/lima-vm/sshocker>: ssh + reverse sshfs + port forwarder, in Docker-like CLI (predecessor of Lima)\n- <https://github.com/lima-vm/alpine-lima>: Create an alpine based image for lima\n\nSee also <https://github.com/lima-vm> for other subprojects.\n\nThe maintainership of the subprojects corresponds to the [maintainership](../governance) of Lima itself.\n"
  },
  {
    "path": "website/content/en/docs/config/_index.md",
    "content": "---\ntitle: Configuration guide\nweight: 5\n---\n\nFor all the configuration items, see <https://github.com/lima-vm/lima/blob/master/templates/default.yaml>.\n\nThe current default spec:\n- OS: Ubuntu\n- CPU: 4 cores\n- Memory: 4 GiB\n- Disk: 100 GiB\n- Mounts: `~` (read-only), `/tmp/lima` (writable; removed in Lima v2.0)\n- SSH: 127.0.0.1:<Random port>\n"
  },
  {
    "path": "website/content/en/docs/config/ai/_index.md",
    "content": "---\ntitle: AI\nweight: 90\n---\n\nThere are two kinds of scenario to use Lima with AI:\n\n- [AI agents inside Lima](./inside): running an AI agent **inside** a VM\n- [AI agents outside Lima](./outside): calling Lima's MCP tools from an AI agent running **outside** a VM\n\n<!-- TODO: add a comparison table -->\n"
  },
  {
    "path": "website/content/en/docs/config/ai/inside/_index.md",
    "content": "---\ntitle: AI agents inside Lima\nweight: 10\n---\n\nLima is useful for running AI agents (e.g., Claude Code, Codex, Gemini)\ninside a VM, so as to prevent agents from directly reading, writing, or executing the host files.\n\nSee also:\n- [Examples » AI](../../../examples/ai.md).\n- [Config » GPU](../../gpu.md)\n"
  },
  {
    "path": "website/content/en/docs/config/ai/outside/_index.md",
    "content": "---\ntitle: AI agents outside Lima (MCP)\nweight: 10\n---\n\nStarting with Lima v2.0, Lima provides Model Context Protocol (MCP) tools\nfor reading, writing, and executing local files using a VM sandbox.\n"
  },
  {
    "path": "website/content/en/docs/config/ai/outside/gemini.md",
    "content": "---\ntitle: Gemini\nweight: 20\n\n---\n\n| ⚡ Requirement | Lima >= 2.0 |\n|---------------|-------------|\n\nThis page describes how to use Lima as an sandbox for [Google Gemini CLI](https://github.com/google-gemini/gemini-cli).\n\n## Prerequisite\nIn addition to Gemini and Lima, make sure that `limactl mcp` plugin is installed:\n\n```console\n$ limactl mcp -v\nlimactl-mcp version 2.0.0-alpha.1\n```\n\nThe `limactl mcp` plugin is bundled in Lima since v2.0, however, it may not be installed\ndepending on the method of the [installation](../../../installation/).\n\n## Configuration\n1. Run the default Lima instance, with a mount of your project directory:\n```bash\nlimactl start --mount-only \"$(pwd):w\" default\n```\n\nDrop the `:w` suffix if you do not want to allow writing to the mounted directory.\n\n2. Create `.gemini/extensions/lima/gemini-extension.json` as follows:\n```json\n{\n  \"name\": \"lima\",\n  \"version\": \"2.0.0\",\n  \"mcpServers\": {\n    \"lima\": {\n      \"command\": \"limactl\",\n      \"args\": [\n        \"mcp\",\n        \"serve\",\n        \"default\"\n      ]\n    }\n  }\n}\n```\n\n3. Modify `.gemini/settings.json` so as to disable Gemini CLI's [built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools)\nexcept ones that do not relate to local command execution and file I/O:\n```json\n{\n  \"coreTools\": [\"WebFetchTool\", \"WebSearchTool\", \"MemoryTool\"]\n}\n```\n\n## Usage\nJust run `gemini` in your project directory.\n\nGemini automatically recognizes the MCP tools provided by Lima.\n"
  },
  {
    "path": "website/content/en/docs/config/disk.md",
    "content": "---\ntitle: Disks\n---\n\nThis guide explains how to increase the disk size of a Lima VM when you've run out of space, as well as how to edit the disk size using the `limactl` CLI.\n\n## Resize Disk Using limactl\n\nStarting with v1.1, Lima supports editing the disk size of an existing instance using the `--disk` flag with the `limactl edit` command.\nThis is the recommended and simplest way to resize your VM disk.\n\n```sh\nlimactl edit <vm-name> --disk <new-size>\n```\n\nExample for 20GB:\n\n```sh\nlimactl edit default --disk 20\n```\n\n> **Note:**\n> - Increasing disk size is supported, but shrinking disks is not recommended.\n> - The instance may need to be stopped before editing disk size.\n"
  },
  {
    "path": "website/content/en/docs/config/environment-variables.md",
    "content": "---\ntitle: Environment Variables\nweight: 80\n---\n\n## Environment Variables\n\nThis page documents the environment variables used in Lima.\n\n### `LIMA_HOME`\n\n- **Description**: Specifies the Lima home directory.\n- **Default**: `~/.lima`\n- **Usage**:\n  ```sh\n  export LIMA_HOME=~/.lima-custom\n  lima\n  ```\n\n### `LIMA_INSTANCE`\n\n- **Description**: Specifies the name of the Lima instance to use.\n- **Default**: `default`\n- **Usage**:\n  ```sh\n  export LIMA_INSTANCE=my-instance\n  lima uname -a\n  ```\n\n### `LIMA_SHELL`\n\n- **Description**: Specifies the shell interpreter to use inside the Lima instance.\n- **Default**: User's shell configured inside the instance\n- **Usage**:\n  ```sh\n  export LIMA_SHELL=/bin/bash\n  lima\n  ```\n\n### `LIMA_TEMPLATES_PATH`\n\n- **Description**: Specifies the directories used to resolve `template:` URLs.\n- **Default**: `$LIMA_HOME/_templates:/usr/local/share/lima/templates`\n- **Usage**:\n  ```sh\n  export LIMA_TEMPLATES_PATH=\"$HOME/.config/lima/templates:/usr/local/share/lima/templates\"\n  limactl create --name my-vm template:my-distro\n  ```\n\n### `LIMA_WORKDIR`\n\n- **Description**: Specifies the initial working directory inside the Lima instance.\n- **Default**: Current directory from the host\n- **Usage**:\n  ```sh\n  export LIMA_WORKDIR=/home/user/project\n  lima\n  ```\n\n### `LIMA_SHELLENV_ALLOW`\n\n- **Description**: Specifies a comma-separated list of environment variable patterns to allow when propagating environment variables to the Lima instance with `--preserve-env`. When set, **only** variables matching these patterns will be passed through, completely overriding the default block list behavior. This feature only applies to Lima v2.0.0 or later.\n- **Default**: unset (when using `--preserve-env`, all variables are propagated except those matching the block list patterns)\n- **Usage**:\n  ```sh\n  export LIMA_SHELLENV_ALLOW=\"FPATH,XAUTHORITY,CUSTOM_*\"\n  limactl shell --preserve-env default\n  ```\n- **Behavior**:\n  - **Without `--preserve-env`**: No environment variables are propagated (regardless of this setting)\n  - **With `--preserve-env` and `LIMA_SHELLENV_ALLOW` unset**: All variables are propagated except those in the block list\n  - **With `--preserve-env` and `LIMA_SHELLENV_ALLOW` set**: Only variables matching the allow patterns are propagated (block list is ignored)\n- **Note**: Patterns support wildcards using `*` at the end (e.g., `CUSTOM_*` matches `CUSTOM_VAR`, `CUSTOM_PATH`, etc.).\n\n### `LIMA_SHELLENV_BLOCK`\n\n- **Description**: Specifies a comma-separated list of environment variable patterns to block when propagating environment variables to the Lima instance with `--preserve-env`. Can either replace the default block list or extend it by prefixing with `+`. This feature only applies to Lima v2.0.0 or later.\n- **Default**: A predefined list of system and shell-specific variables that should not be propagated:\n  - Shell variables: `BASH*`, `SHELL`, `SHLVL`, `ZSH*`, `ZDOTDIR`, `FPATH`\n  - System paths: `PATH`, `PWD`, `OLDPWD`, `TMPDIR`\n  - User/system info: `HOME`, `USER`, `LOGNAME`, `UID`, `GID`, `EUID`, `GROUP`, `HOSTNAME`\n  - Display/terminal: `DISPLAY`, `TERM`, `TERMINFO`, `XAUTHORITY`, `XDG_*`\n  - SSH/security: `SSH_*`\n  - Dynamic linker: `DYLD_*`, `LD_*`\n  - Internal variables: `_*` (variables starting with underscore)\n\n  See [`GetDefaultBlockList()`](https://github.com/lima-vm/lima/blob/master/pkg/envutil/envutil.go#L133) for the complete list.\n- **Usage**:\n  ```sh\n  # Replace default block list entirely (not recommended)\n  export LIMA_SHELLENV_BLOCK=\"SECRET_*,PRIVATE_*\"\n\n  # Extend default block list (recommended)\n  export LIMA_SHELLENV_BLOCK=\"+SECRET_*,PRIVATE_*\"\n  limactl shell --preserve-env default\n  ```\n- **Note**: Patterns support wildcards using `*` at the end (e.g., `SSH_*` matches `SSH_AUTH_SOCK`, `SSH_AGENT_PID`). Use the `+` prefix to add to the default block list rather than replacing it entirely. This variable only affects the `--preserve-env` flag behavior.\n\n### `LIMACTL`\n\n- **Description**: Specifies the path to the `limactl` binary.\n- **Default**: `limactl` in `$PATH`\n- **Usage**:\n  ```sh\n  export LIMACTL=/usr/local/bin/limactl\n  lima\n  ```\n\n### `LIMA_SSH_OVER_VSOCK`\n- **Description**: Specifies to use vsock for SSH connection instead of port forwarding.\n- **Default**: `true` (since v2.0.0)\n- **Usage**:\n  ```sh\n  export LIMA_SSH_OVER_VSOCK=true\n  ```\n- **Note**: This variable is effective only if the VM is VZ based and systemd is v256 or later (e.g. Ubuntu 24.10+).\n- **Deprecated**: This variable is deprecated in favor of the YAML field `.ssh.overVsock` (since v2.0.2).\n\n### `LIMA_SSH_PORT_FORWARDER`\n\n- **Description**: Specifies to use the SSH port forwarder (slow) instead of gRPC (fast, previously unstable)\n- **Default**: `false` (since v1.1.0)\n- **Usage**:\n  ```sh\n  export LIMA_SSH_PORT_FORWARDER=false\n  ```\n- **The history of the default value**:\n  | Version | Default value       |\n  |---------|---------------------|\n  | v0.1.0  | `true`, effectively |\n  | v1.0.0  | `false`             |\n  | v1.0.1  | `true`              |\n  | v1.1.0  | `false`             |\n\n### `LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT`\n\n- **Description**: Specifies the timeout duration for resolving the IP address in usernet.\n- **Default**: 2 minutes\n- **Usage**:\n  ```sh\n  export LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT=5\n  ```\n\n### `_LIMA_QEMU_UEFI_IN_BIOS`\n\n- **Description**: Commands QEMU to load x86_64 UEFI images using `-bios` instead of `pflash` drives.\n- **Default**: `false` on Unix like hosts and `true` on Windows hosts\n- **Usage**:\n  ```sh\n  export _LIMA_QEMU_UEFI_IN_BIOS=true\n  ```\n- **Note**: It is expected that this variable will be set to `false` by default in future\n  when QEMU supports `pflash` UEFI for accelerated guests on Windows.\n\n### `_LIMA_WINDOWS_EXTRA_PATH`\n\n- **Description**: Additional directories which will be added to PATH by `limactl.exe` process to search for tools.\n  It is useful, when there is a need to prevent collisions between binaries available in active shell and ones\n  used by `limactl.exe` - injecting them only for the running process w/o altering PATH observed by user shell.\n  Is is Windows specific and does nothing for other platforms.\n- **Default**: unset\n- **Usage**:\n  ```bat\n  set _LIMA_WINDOWS_EXTRA_PATH=C:\\Program Files\\Git\\usr\\bin\n  ```\n- **Note**: It is an experimental setting and has no guarantees being ever promoted to stable. It may be removed\n  or changed at any stage of project development.\n\n### `QEMU_SYSTEM_AARCH64`\n\n- **Description**: Path to the `qemu-system-aarch64` binary.\n- **Default**: `qemu-system-aarch64` found in `$PATH`\n- **Usage**:\n  ```sh\n  export QEMU_SYSTEM_AARCH64=/usr/local/bin/qemu-system-aarch64\n  ```\n\n### `QEMU_SYSTEM_ARM`\n\n- **Description**: Path to the `qemu-system-arm` binary.\n- **Default**: `qemu-system-arm` found in `$PATH`\n- **Usage**:\n  ```sh\n  export QEMU_SYSTEM_ARM=/usr/local/bin/qemu-system-arm\n  ```\n\n### `QEMU_SYSTEM_PPC64`\n\n- **Description**: Path to the `qemu-system-ppc64` binary.\n- **Default**: `qemu-system-ppc64` found in `$PATH`\n- **Usage**:\n  ```sh\n  export QEMU_SYSTEM_PPC64=/usr/local/bin/qemu-system-ppc64\n  ```\n\n### `QEMU_SYSTEM_RISCV64`\n\n- **Description**: Path to the `qemu-system-riscv64` binary.\n- **Default**: `qemu-system-riscv64` found in `$PATH`\n- **Usage**:\n  ```sh\n  export QEMU_SYSTEM_RISCV64=/usr/local/bin/qemu-system-riscv64\n  ```\n\n### `QEMU_SYSTEM_S390X`\n\n- **Description**: Path to the `qemu-system-s390x` binary.\n- **Default**: `qemu-system-s390x` found in `$PATH`\n- **Usage**:\n  ```sh\n  export QEMU_SYSTEM_S390X=/usr/local/bin/qemu-system-s390x\n  ```\n\n### `QEMU_SYSTEM_X86_64`\n\n- **Description**: Path to the `qemu-system-x86_64` binary.\n- **Default**: `qemu-system-x86_64` found in `$PATH`\n- **Usage**:\n  ```sh\n  export QEMU_SYSTEM_X86_64=/usr/local/bin/qemu-system-x86_64\n  ```\n"
  },
  {
    "path": "website/content/en/docs/config/gpu.md",
    "content": "---\ntitle: GPU acceleration\nweight: 90\n---\n\nLima VM supports GPU acceleration for the following VM types:\n- [krunkit](./vmtype/krunkit.md).\n\n{{% alert title=\"Note\" color=success %}}\n\"Lima\" in this web site refers to [the Lima VM project](https://lima-vm.io).\n\nThe Lima VM project is unrelated to [the Lima driver project (driver for ARM Mali GPUs)](https://gitlab.freedesktop.org/lima),\nwhich appears as \"Lima\" in the documentations of [Mesa 3D](https://docs.mesa3d.org/drivers/lima.html), etc.\n{{% /alert %}}\n"
  },
  {
    "path": "website/content/en/docs/config/mount.md",
    "content": "---\ntitle: Filesystem mounts\nweight: 50\n---\n\nLima supports several methods for mounting the host filesystem into the guest.\n\nThe default mount type is shown in the following table:\n\n| Lima Version | Default                                                       |\n| ------------ | ------------------------------------------------------------- |\n| < 0.10       | reverse-sshfs + Builtin SFTP server                           |\n| >= 0.10      | reverse-sshfs + OpenSSH SFTP server                           |\n| >= 0.17      | reverse-sshfs + OpenSSH SFTP server for QEMU, virtiofs for VZ |\n| >= 1.0       | 9p for QEMU (on non-Windows), virtiofs for VZ                 |\n\n## Mount types\n\n### reverse-sshfs\nThe \"reverse-sshfs\" mount type exposes the host filesystem by running an SFTP server on the host.\nWhile the host works as an SFTP server, the host does not open any TCP port,\nas the host initiates an SSH connection into the guest and let the guest connect to the SFTP server via the stdin.\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --mount-type=reverse-sshfs\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nmountType: \"reverse-sshfs\"\nmounts:\n- location: \"~\"\n  sshfs:\n    # Enabling the SSHFS cache will increase performance of the mounted filesystem, at\n    # the cost of potentially not reflecting changes made on the host in a timely manner.\n    # Warning: It looks like PHP filesystem access does not work correctly when\n    # the cache is disabled.\n    # 🟢 Builtin default: true\n    cache: null\n    # SSHFS has an optional flag called 'follow_symlinks'. This allows mounts\n    # to be properly resolved in the guest os and allow for access to the\n    # contents of the symlink. As a result, symlinked files & folders on the Host\n    # system will look and feel like regular files directories in the Guest OS.\n    # 🟢 Builtin default: false\n    followSymlinks: null\n    # SFTP driver, \"builtin\" or \"openssh-sftp-server\". \"openssh-sftp-server\" is recommended.\n    # 🟢 Builtin default: \"openssh-sftp-server\" if OpenSSH SFTP Server binary is found, otherwise \"builtin\"\n    sftpDriver: null\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nThe default value of `sftpDriver` has been set to \"openssh-sftp-server\" since Lima v0.10, when an OpenSSH SFTP Server binary\nsuch as `/usr/libexec/sftp-server` is detected on the host.\nLima prior to v0.10 had used \"builtin\" as the SFTP driver.\n\n#### Caveats\n- A mount is disabled when the SSH connection was shut down.\n- A compromised `sshfs` process in the guest may have access to unexposed host directories.\n\n### 9p\n\nThe \"9p\" mount type is implemented by using QEMU's virtio-9p-pci devices.\nvirtio-9p-pci is also known as \"virtfs\", but note that this is unrelated to [virtio-fs](https://virtio-fs.gitlab.io/).\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=qemu --mount-type=9p\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nvmType: \"qemu\"\nmountType: \"9p\"\nmounts:\n- location: \"~\"\n  9p:\n    # Supported security models are \"passthrough\", \"mapped-xattr\", \"mapped-file\" and \"none\".\n    # \"mapped-xattr\" and \"mapped-file\" are useful for persistent chown but incompatible with symlinks.\n    # 🟢 Builtin default: \"none\" (since Lima v0.13)\n    securityModel: null\n    # Select 9P protocol version. Valid options are: \"9p2000\" (legacy), \"9p2000.u\", \"9p2000.L\".\n    # 🟢 Builtin default: \"9p2000.L\"\n    protocolVersion: null\n    # The number of bytes to use for 9p packet payload, where 4KiB is the absolute minimum.\n    # 🟢 Builtin default: \"128KiB\"\n    msize: null\n    # Specifies a caching policy. Valid options are: \"none\", \"loose\", \"fscache\" and \"mmap\".\n    # Try choosing \"mmap\" or \"none\" if you see a stability issue with the default \"fscache\".\n    # See https://www.kernel.org/doc/Documentation/filesystems/9p.txt\n    # 🟢 Builtin default: \"fscache\" for non-writable mounts, \"mmap\" for writable mounts\n    cache: null\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nThe \"9p\" mount type requires Lima v0.10.0 or later.\n\n#### Caveats\n- The \"9p\" mount type is known to be incompatible with CentOS, Rocky Linux, and AlmaLinux as their kernel do not support `CONFIG_NET_9P_VIRTIO`.\n\n### virtiofs\n> **Warning**\n> \"virtiofs\" mode is experimental on Linux hosts\n\n| ⚡ Requirement | Lima >= 0.14, macOS >= 13.0 | Lima >= 0.17.0, Linux, QEMU 4.2.0+, virtiofsd (Rust version) |\n|-------------------|-----------------------------| ------------------------------------------------------------ |\n\nThe \"virtiofs\" mount type is implemented via the virtio-fs device by using apple Virtualization.Framework shared directory on macOS and virtiofsd on Linux.\nLinux guest kernel must enable the CONFIG_VIRTIO_FS support for this support.\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=vz --mount-type=virtiofs\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nvmType: \"vz\"  # only for macOS; Linux uses 'qemu'\nmountType: \"virtiofs\"\nmounts:\n- location: \"~\"\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\n#### Caveats\n- For macOS, the \"virtiofs\" mount type is supported only on macOS 13 or above with `vmType: vz` config. See also [`vmtype`](../vmtype/).\n- For Linux, the \"virtiofs\" mount type requires the [Rust version of virtiofsd](https://gitlab.com/virtio-fs/virtiofsd).\n  Using the version from QEMU (usually packaged as `qemu-virtiofsd`) will *not* work, as it requires root access to run.\n\n### wsl2\n> **Warning**\n> \"wsl2\" mode is experimental\n\n| ⚡ Requirement | Lima >= 0.18 + (Windows >= 10 Build 19041 OR Windows 11) |\n| ----------------- | -------------------------------------------------------- |\n\nThe \"wsl2\" mount type relies on using WSL2's native disk sharing, where the root disk is available by default at `/mnt/$DISK_LETTER` (e.g. `/mnt/c/`).\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=wsl2 --mount-type=wsl2\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nvmType: \"wsl2\"\nmountType: \"wsl2\"\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\n#### Caveats\n- WSL2 file permissions may not work exactly as expected when accessing files that are natively on the Windows disk ([more info](https://github.com/MicrosoftDocs/WSL/blob/mattw-wsl2-explainer/WSL/file-permissions.md))\n- WSL2's disk sharing system uses a 9P protocol server, making the performance similar to [Lima's 9p](#9p) mode ([more info](https://github.com/MicrosoftDocs/WSL/blob/mattw-wsl2-explainer/WSL/wsl2-architecture.md#wsl-2-architectural-flow))\n\n## Mount Inotify\n> **Warning**\n> \"mountInotify\" is experimental\n\n| ⚡ Requirement | Lima >= 0.21.0 |\n| ----------------- |----------------|\n\nThe `mountInotify` support enables inotify support for all different mountTypes like 9p, virtiofs etc.\n\nWhen mountInotify is enabled,\n- hostagent will listen and send inotify events from host machine to guest.\n- Guest will modify the file to trigger inotify on guest side\n\nThis support will be enabled only for writable mounts because only for writable mount guest will be able to trigger inotify\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --mount-inotify\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nmountInotify: true\nmounts:\n  - location: \"~\"\n    writable: true\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\n#### Caveats\n- For `mountType: 9p`, Inotify events are not triggered for nested files from the listening directory.\n- Inotify events are not triggered when files are removed from host\n"
  },
  {
    "path": "website/content/en/docs/config/multi-arch.md",
    "content": "---\ntitle: Intel-on-ARM and ARM-on-Intel\nweight: 20\n---\n\nLima supports several modes for running Intel-on-ARM and ARM-on-Intel:\n- [Slow mode](#slow-mode)\n- [Fast mode](#fast-mode)\n- [Fast mode 2](#fast-mode-2)\n\n## [Slow mode: Intel VM on ARM Host / ARM VM on Intel Host](#slow-mode)\n\n| ⚡ Requirement | QEMU, lima-additional-guestagents |\n|---------------|-----------------------------------|\n\nLima can run a VM with a foreign architecture, using [QEMU](./vmtype/qemu.md).\n\nFor [port forwarding](./port.md), the [`lima-additional-guestagents`](../installation/) package has to be installed on the host.\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=qemu --arch=x86_64 --plain\n```\n\nSee the YAML tab for the explanation of the corresponding CLI flags.\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\n# Starting with Lima v2.0, `vmType` has to be expclitly set to \"qemu\" on non-Linux hosts.\nvmType: \"qemu\"\n\n# Supported architectures:\n#   Non-experimental: aarch64, x86_64\n#   Experimental:     armv7l, ppc64le, riscv64, s390x\n#\n# containerd is not automatically installed for the experimental architectures.\narch: \"x86_64\"\n\n# A slow host may need `plain` to disable mounts, port forwards, and containerd, so as to avoid timeout.\nplain: true\n\nbase:\n- template:_images/ubuntu\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nRunning a VM with a foreign architecture is extremely slow.\nConsider using [Fast mode](#fast-mode) or [Fast mode 2](#fast-mode-2) whenever possible.\n\n## [Fast mode: Intel containers on ARM VM on ARM Host / ARM containers on Intel VM on Intel Host](#fast-mode)\n\nThis mode uses QEMU User Mode Emulation.\nQEMU User Mode Emulation is significantly faster than QEMU System Mode Emulation, but it often sacrifices compatibility.\n\nSet up:\n```bash\nlima sudo systemctl start containerd\nlima sudo nerdctl run --privileged --rm tonistiigi/binfmt:qemu-v10.0.4-56@sha256:30cc9a4d03765acac9be2ed0afc23af1ad018aed2c28ea4be8c2eb9afe03fbd1 --install all\n```\n\nRun containers:\n```console\n$ lima nerdctl run --platform=amd64 --rm alpine uname -m\nx86_64\n\n$ lima nerdctl run --platform=arm64 --rm alpine uname -m\naarch64\n```\n\nBuild and push container images:\n```console\n$ lima nerdctl build --platform=amd64,arm64 -t example.com/foo:latest .\n$ lima nerdctl push --all-platforms example.com/foo:latest\n```\n\nSee also https://github.com/containerd/nerdctl/blob/main/docs/multi-platform.md\n\n## [Fast mode 2 (Rosetta): Intel containers on ARM VM on ARM Host](#fast-mode-2)\n\n| ⚡ Requirement | Lima >= 0.14, macOS >= 13.0, ARM |\n|-------------------|----------------------------------|\n\n[Rosetta](https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta) is known to be much faster than QEMU User Mode Emulation.\nRosetta is available for [VZ](../vmtype/#vz) instances on ARM hosts.\n\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=vz --rosetta\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nvmType: \"vz\"\nrosetta:\n  # Enable Rosetta for Linux.\n  # Hint: try `softwareupdate --install-rosetta` if Lima gets stuck at `Installing rosetta...`\n  enabled: true\n  # Register rosetta to /proc/sys/fs/binfmt_misc\n  binfmt: true\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\n### [Enable Rosetta AOT Caching with CDI spec](#rosetta-aot-caching)\n| ⚡ Requirement | Lima >= 2.0, macOS >= 14.0, ARM |\n|-------------------|----------------------------------|\n\nRosetta AOT Caching speeds up containers by saving translated binaries, so they don't need to be translated again.\nLearn more: [WWDC2023 video](https://developer.apple.com/videos/play/wwdc2023/10007/?time=721)\n\n**How to use Rosetta AOT Caching:**\n\n- **Run a container:**\n  Add `--device=lima-vm.io/rosetta=cached` to your `docker run` command:\n  ```bash\n  docker run --platform=linux/amd64 --device=lima-vm.io/rosetta=cached ...\n  ```\n\n- **Build an image:**\n  Add `# syntax=docker/dockerfile:1-labs` at the top of your Dockerfile to enable the `--device` option.\n  Use `--device=lima-vm.io/rosetta=cached` in your `RUN` command:\n  ```Dockerfile\n  # syntax=docker/dockerfile:1-labs\n  FROM ...\n  ...\n  RUN --device=lima-vm.io/rosetta=cached <your amd64 command>\n  ```\n\n- **Check if caching works:**\n  Look for cache files in the VM:\n  ```bash\n  limactl shell {{.Name}} ls -la /var/cache/rosettad\n  docker run --platform linux/amd64 --device=lima-vm.io/rosetta=cached ubuntu echo hello\n  limactl shell {{.Name}} ls -la /var/cache/rosettad\n  # You should see *.aotcache files here\n  ```\n\n- **Check if Docker recognizes the CDI device:**\n  Look for CDI info in the output of `docker info`:\n  ```console\n  docker info\n  ...\n  CDI spec directories:\n    /etc/cdi\n    /var/run/cdi\n  Discovered Devices:\n    cdi: lima-vm.io/rosetta=cached\n  ```\n\n- **Learn more about CDI:**\n  [CDI spec documentation](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md)\n"
  },
  {
    "path": "website/content/en/docs/config/network/_index.md",
    "content": "---\ntitle: Network\nweight: 30\n---\n\nSee the following flowchart to choose the best network for you:\n```mermaid\nflowchart\nconnect_to_vm_via{\"Connect to the VM via\"} -- \"localhost\" --> default[\"Default\"]\n  connect_to_vm_via -- \"IP\" --> connect_from{\"Connect to the VM IP from\"}\n  connect_from -- \"Host\" --> vm{\"VM type\"}\n  vm -- \"vz\" --> vzNAT[\"vzNAT (see the VMNet page)\"]\n  vm -- \"qemu\" --> shared[\"socket_vmnet (shared)\"]\n  connect_from -- \"Other VMs\" --> userV2[\"user-v2\"]\n  connect_from -- \"Other hosts\" --> bridged[\"socket_vmnet (bridged)\"]\n```\n"
  },
  {
    "path": "website/content/en/docs/config/network/user-v2.md",
    "content": "---\ntitle: user-v2 network\nweight: 32\n---\n\n| ⚡ Requirement | Lima >= 0.16.0 |\n|-------------------|----------------|\n\nuser-v2 network provides a user-mode networking similar to the [default user-mode network](#user-mode-network--1921685024-) and also provides support for `vm -> vm` communication.\n\nTo enable this network mode, define a network with `mode: user-v2` in networks.yaml\n\nBy default, the below network configuration is already applied (Since v0.18).\n\n```yaml\n...\nnetworks:\n  user-v2:\n    mode: user-v2\n    gateway: 192.168.104.1\n    netmask: 255.255.255.0\n...\n```\n\nInstances can then reference these networks from their `lima.yaml` file:\n\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --network=lima:user-v2\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nnetworks:\n   - lima: user-v2\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nAn instance's IP address is resolvable from another instance as `lima-<NAME>.internal.` (e.g., `lima-default.internal.`).\n\n> **Note**\n>\n> Enabling user-v2 network will disable the [default user-mode network]({{< ref \"/docs/config/network/user\" >}}).\n\n## Accessing VMs from the host\n\nBy default, the `lima-<NAME>.internal` hostnames are only resolvable from within the guest VMs.\nTo access the guest network from the host, use `limactl tunnel` to create a SOCKS proxy:\n\n```bash\nlimactl tunnel <INSTANCE>\n```\n\nThe command will output the proxy address (the port is randomly assigned):\n```console\n$ limactl tunnel default\nSet `ALL_PROXY=socks5h://127.0.0.1:<PORT>`, etc.\nThe instance can be connected from the host as <http://lima-default.internal> via a web browser.\n```\n\nYou can then access any VM on the user-v2 network from the host using the SOCKS proxy:\n\n{{< tabpane text=true >}}\n{{% tab header=\"curl\" %}}\n```bash\ncurl --proxy socks5h://127.0.0.1:<PORT> http://lima-default.internal\n```\n{{% /tab %}}\n{{% tab header=\"Environment variable\" %}}\n```bash\nexport ALL_PROXY=socks5h://127.0.0.1:<PORT>\ncurl http://lima-default.internal\n```\n{{% /tab %}}\n{{% tab header=\"Web browser\" %}}\nConfigure your browser to use the SOCKS5 proxy at `127.0.0.1:<PORT>`,\nthen navigate to `http://lima-default.internal`.\n{{% /tab %}}\n{{< /tabpane >}}\n\n> **Note**\n>\n> `limactl tunnel` is experimental.\n"
  },
  {
    "path": "website/content/en/docs/config/network/user.md",
    "content": "---\ntitle: Default user-mode network\nweight: 30\n\n---\n\nBy default Lima only enables the user-mode networking aka \"slirp\".\n\nThe subnet is hard-coded to `192.168.5.0/24`.\nUse [`user-v2`]({{< ref \"/docs/config/network/user-v2\" >}}) network to customize the subnet.\n\n## Guest IP (192.168.5.15)\n\nThe guest IP address is set to `192.168.5.15`.\n\nThis IP address is not accessible from the host by design.\n\nUse [VMNet]({{< ref \"/docs/config/network/vmnet\" >}}) to allow accessing the guest IP from the host and other guests.\n\n## Host IP (192.168.5.2)\n\nThe loopback addresses of the host is `192.168.5.2` and is accessible from the guest as `host.lima.internal`.\n\n## DNS (192.168.5.3)\n\nIf `hostResolver.enabled` in `lima.yaml` is true, then the hostagent is going to run a DNS server over tcp and udp - each on a separate randomly selected free port. This server does a local lookup using the native host resolver, so it will deal correctly with VPN configurations and split-DNS setups, as well as mDNS, local `/etc/hosts` etc. For this the hostagent has to be compiled with `CGO_ENABLED=1` as default Go resolver is [broken](https://github.com/golang/go/issues/12524).\n\nThese tcp and udp ports are then forwarded via iptables rules to `192.168.5.3:53`, overriding the DNS provided by QEMU via slirp.\n\nCurrently following request types are supported:\n\n- A\n- AAAA\n- CNAME\n- TXT\n- NS\n- MX\n- SRV\n\nFor all other queries hostagent will redirect the query to the nameservers specified in `/etc/resolv.conf` (or, if that fails - to `8.8.8.8` and `1.1.1.1`).\n\nDNS over tcp is rarely used. It is usually only used either when user explicitly requires it, or when request+response can't fit into a single UDP packet (most likely in case of DNSSEC), or in the case of certain management operations such as domain transfers. Neither DNSSEC nor management operations are currently supported by a hostagent, but on the off chance that the response may contain an unusually long list of records - hostagent will also listen for the tcp traffic.\n\nDuring initial cloud-init bootstrap, `iptables` may not yet be installed. In that case the repo server is determined using the slirp DNS. After `iptables` has been installed, the forwarding rule is applied, switching over to the hostagent DNS.\n\nIf `hostResolver.enabled` is false, then DNS servers can be configured manually in `lima.yaml` via the `dns` setting. If that list is empty, then Lima will either use the slirp DNS (on Linux), or the nameservers from the first host interface in service order that has an assigned IPv4 address (on macOS).\n"
  },
  {
    "path": "website/content/en/docs/config/network/vmnet.md",
    "content": "---\ntitle: VMNet networks\nweight: 33\n\n---\n\n| ⚡ Requirement    | macOS |\n|-------------------|-------|\n\nVMNet assigns a \"real\" IP address that is reachable from the host.\n\nThe configuration steps are different for each network type:\n- [vzNAT](#vzNAT)\n- [socket_vmnet](#socket_vmnet)\n\n## vzNAT\n\n| ⚡ Requirement | Lima >= 0.14, macOS >= 13.0 |\n|-------------------|-----------------------------|\n\nFor [VZ]({{< ref \"/docs/config/vmtype#vz\" >}}) instances, the \"vzNAT\" network can be configured as follows:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=vz --network=vzNAT\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nnetworks:\n- vzNAT: true\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nThe range of the IP address is not specifiable.\n\nThe \"vzNAT\" network does not need the `socket_vmnet` binary and the `sudoers` file.\n\nThe \"vzNAT\" network also has a significant advantage in the throughput. See the [benchmark result](../port.md#benchmarks).\n\n## socket_vmnet\n### Managed (192.168.105.0/24)\n\n[`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) can be used for adding another guest IP that is accessible from the host and other guests,\nwithout depending on vz.\nIt must be installed according to the instruction provided on https://github.com/lima-vm/socket_vmnet.\n\nNote that installation using Homebrew is not secure and not recommended by the Lima project.\nHomebrew installation will only work with Lima if password-less `sudo` is enabled for the current user.\nThe `limactl sudoers` command requires that `socket_vmnet` is installed into a secure path only\nwritable by `root` and will reject `socket_vmnet` installed by Homebrew into a user-writable location.\n\n```bash\n# Install socket_vmnet as root from source to /opt/socket_vmnet\n# using instructions on https://github.com/lima-vm/socket_vmnet\n# This assumes that Xcode Command Line Tools are already installed\ngit clone https://github.com/lima-vm/socket_vmnet\ncd socket_vmnet\n# Change \"v1.2.2\" to the actual latest release in https://github.com/lima-vm/socket_vmnet/releases\ngit checkout v1.2.2\nmake\nsudo make PREFIX=/opt/socket_vmnet install.bin\n\n# Set up the sudoers file for launching socket_vmnet from Lima\nlimactl sudoers >etc_sudoers.d_lima\nless etc_sudoers.d_lima  # verify that the file looks correct\nsudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima\nrm etc_sudoers.d_lima\n```\n\n> **Note**\n>\n> Lima before v0.12 used `vde_vmnet` for managing the networks.\n> `vde_vmnet` is no longer supported.\n>\n> Lima v0.14.0 and later used to also accept `socket_vmnet` installations if they were\n> owned by the `admin` user. Starting with v1.0.0 only `root` ownership is acceptable.\n\nThe networks are defined in `$LIMA_HOME/_config/networks.yaml`. If this file doesn't already exist, it will be created with these default\nsettings:\n\n<details>\n<summary>Default</summary>\n\n<p>\n\n```yaml\n# Path to socket_vmnet executable. Because socket_vmnet is invoked via sudo it should be\n# installed where only root can modify/replace it. This means also none of the\n# parent directories should be writable by the user.\n#\n# The varRun directory also must not be writable by the user because it will\n# include the socket_vmnet pid file. Those will be terminated via sudo, so replacing\n# the pid file would allow killing of arbitrary privileged processes. varRun\n# however MUST be writable by the daemon user.\n#\n# None of the paths segments may be symlinks, why it has to be /private/var\n# instead of /var etc.\npaths:\n# socketVMNet requires Lima >= 0.12 .\n  socketVMNet: /opt/socket_vmnet/bin/socket_vmnet\n  varRun: /private/var/run/lima\n  sudoers: /private/etc/sudoers.d/lima\n\ngroup: everyone\n\nnetworks:\n  shared:\n    mode: shared\n    gateway: 192.168.105.1\n    dhcpEnd: 192.168.105.254\n    netmask: 255.255.255.0\n  bridged:\n    mode: bridged\n    interface: en0\n    # bridged mode doesn't have a gateway; dhcp is managed by outside network\n  host:\n    mode: host\n    gateway: 192.168.106.1\n    dhcpEnd: 192.168.106.254\n    netmask: 255.255.255.0\n```\n\n</p>\n\n</details>\n\nInstances can then reference these networks:\n\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --network=lima:shared\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nnetworks:\n  # Lima can manage the socket_vmnet daemon for networks defined in $LIMA_HOME/_config/networks.yaml automatically.\n  # The socket_vmnet binary must be installed into a secure location only alterable by the \"root\" user.\n  # - lima: shared\n  #   # MAC address of the instance; lima will pick one based on the instance name,\n  #   # so DHCP assigned ip addresses should remain constant over instance restarts.\n  #   macAddress: \"\"\n  #   # Interface name, defaults to \"lima0\", \"lima1\", etc.\n  #   interface: \"\"\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nThe network daemon is started automatically when the first instance referencing them is started,\nand will stop automatically once the last instance has stopped. Daemon logs will be stored in the\n`$LIMA_HOME/_networks` directory.\n\nSince the commands to start and stop the `socket_vmnet` daemon requires root, the user either must\nhave password-less `sudo` enabled, or add the required commands to a `sudoers` file. This can\nbe done via:\n\n```shell\nlimactl sudoers >etc_sudoers.d_lima\nless etc_sudoers.d_lima  # verify that the file looks correct\nsudo install -o root etc_sudoers.d_lima /etc/sudoers.d/lima\nrm etc_sudoers.d_lima\n```\n\nThe IP address is automatically assigned by macOS's bootpd.\nIf the IP address is not assigned, try the following commands:\n```bash\nsudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /usr/libexec/bootpd\nsudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblock /usr/libexec/bootpd\n```\n\n### Unmanaged\nLima can also connect to \"unmanaged\" networks addressed by \"socket\". This\nmeans that the daemons will not be controlled by Lima, but must be started\nbefore the instance.  The interface type (host, shared, or bridged) is\nconfigured in `socket_vmnet` and not in lima.\n\n```yaml\nnetworks:\n  - socket: \"/var/run/socket_vmnet\"\n```\n"
  },
  {
    "path": "website/content/en/docs/config/plugin/_index.md",
    "content": "---\ntitle: Plug-in\nweight: 90\n---\n"
  },
  {
    "path": "website/content/en/docs/config/plugin/cli.md",
    "content": "---\ntitle: CLI plugins\nweight: 2\n---\n\n> **Warning**\n> Support for CLI plugins is experimental\n\n | ⚡ Requirement | Lima >= 2.0 |\n |----------------|-------------|\n\nLima supports a plugin-like command aliasing system similar to `git`, `kubectl`, and `docker`. When you run a `limactl` command that doesn't exist, Lima will automatically look for an external program named `limactl-<command>` in your system's PATH and additional directories.\n\n## Plugin Discovery\n\nLima discovers plugins by scanning for executables named `limactl-<plugin-name>` in the following locations:\n\n1. **Directory containing the `limactl` binary** (including symlink support)\n2. **All directories in your `$PATH` environment variable**\n3. **`<PREFIX>/libexec/lima`** - For plugins installed by package managers or distribution packages\n\nPlugin discovery respects symlinks, ensuring that even if `limactl` is installed via Homebrew and points to a symlink, all plugins are correctly discovered.\n\n## Plugin Information\n\nAvailable plugins are automatically displayed in:\n\n- **`limactl --help`** - Shows all discovered plugins with descriptions in an \"Available Plugins (Experimental)\" section\n```bash\nAvailable Plugins (Experimental):\n  ps                  Sample limactl-ps alias that shows running instances\n  sh\n```\n\n\n- **`limactl info`** - Includes plugin information in the JSON output\n```json\n{\n   \"plugins\": [\n      {\n         \"name\": \"ps\",\n         \"path\": \"/opt/homebrew/bin/limactl-ps\"\n      },\n      {\n         \"name\": \"sh\",\n         \"path\": \"/opt/homebrew/bin/limactl-sh\"\n      }\n   ]\n}\n\n```\n\n### Plugin Descriptions\n\nLima extracts plugin descriptions from script comments using the `<limactl-desc>` format. Include a description comment in your plugin script:\n\n```bash\n#!/bin/sh\n# <limactl-desc>Docker wrapper that connects to Docker daemon running in Lima instance</limactl-desc>\nset -eu\n\n# Rest of your script...\n```\n\n**Format Requirements:**\n- Only files beginning with a shebang (`#!`) are treated as scripts, and their `<limactl-desc>` lines will be extracted as the plugin description i.e Must contain exactly `<limactl-desc>Description text</limactl-desc>`\n- The description text should be concise and descriptive\n\n**Limitations:**\n- Binary executables cannot have descriptions extracted and will appear in the help output without a description\n- If no `<limactl-desc>` comment is found in a script, the plugin will appear in the help output without a description\n\n## Creating Custom Aliases\n\nTo create a custom alias, create an executable script with the name `limactl-<alias>` and place it somewhere in your PATH.\n\n**Example: Creating a `ps` alias for listing instances**\n\n1. Create a script called `limactl-ps`:\n   ```bash\n   #!/bin/sh\n   # Show instances in a compact format\n   limactl list --format table \"$@\"\n   ```\n\n2. Make it executable and place it in your PATH:\n   ```bash\n   chmod +x limactl-ps\n   sudo mv limactl-ps /usr/local/bin/\n   ```\n\n3. Now you can use it:\n   ```bash\n   limactl ps                    # Shows instances in table format\n   limactl ps --quiet            # Shows only instance names\n   ```\n\n**Example: Creating an `sh` alias**\n\n```bash\n#!/bin/sh\n# limactl-sh - Connect to an instance shell\nlimactl shell \"$@\"\n```\n\nAfter creating this alias:\n```bash\nlimactl sh default           # Equivalent to: limactl shell default\nlimactl sh myinstance bash   # Equivalent to: limactl shell myinstance bash\n```\n\n## How It Works\n\n1. When you run `limactl <unknown-command>`, Lima first tries to find a built-in command\n3. If found, Lima executes the external program and passes all remaining arguments to it\n4. If not found, Lima shows the standard \"unknown command\" error\n\nThis system allows you to:\n- Create personal shortcuts and aliases\n- Extend Lima's functionality without modifying the core application\n- Share custom commands with your team by distributing scripts\n- Package plugins with Lima distributions in the `libexec/lima` directory\n\n## Package Installation\n\nDistribution packages and package managers can install plugins in `<PREFIX>/libexec/lima/` where `<PREFIX>` is typically `/usr/local` or `/opt/homebrew`. This allows plugins to be:\n- Managed by the package manager\n- Isolated from user's `$PATH`\n- Automatically discovered by Lima\n\n## Experimental Status\n\n**Experimental Feature**: The CLI plugin system is currently experimental and may change in future versions. Breaking changes to the plugin API or discovery mechanism may occur without notice.\n"
  },
  {
    "path": "website/content/en/docs/config/plugin/url.md",
    "content": "---\ntitle: URL handler plugins\nweight: 3\n---\n\n> **Warning**\n> Support for URL handler plugins is experimental\n\n| ⚡ Requirement | Lima >= 2.0 |\n|----------------|-------------|\n\nLima's template locator supports custom URL schemes through plugins. A plugin named `limactl-url-<scheme>` handles URLs that begin with `<scheme>:`. This lets you create short, memorable template locators for your own workflows.\n\n## How it works\n\nWhen Lima encounters a URL with an unrecognized scheme (e.g. `dev:webapp`), it:\n\n1. Searches for an executable named `limactl-url-dev` using the standard [plugin discovery](../cli/#plugin-discovery) mechanism\n2. Calls the plugin with the part after the colon as its sole argument (in this case, `webapp`)\n3. Reads the plugin's stdout, which must be either a URL (with any supported scheme) or a local file path\n\nThe plugin's output can itself use a custom scheme. Lima resolves the chain until it reaches a final `https:` URL or local file path. It detects and rejects redirect loops.\n\nIf the plugin exits with a non-zero status, Lima reports the error. Any stderr output from the plugin is included in the error message.\n\nYou can use `limactl template url` to see what a custom URL resolves to without fetching the template:\n\n```console\n$ limactl template url dev:webapp\nhttps://github.example.com/raw/infra/lima-templates/master/webapp.yaml\n```\n\n## Creating a URL handler\n\nCreate an executable script named `limactl-url-<scheme>` and place it in your `PATH`.\n\n### Returning a URL\n\nThe simplest handler maps a short name to a full URL. Here two schemes point at the same repository but select different branches:\n\n**`limactl-url-dev`:**\n```bash\n#!/bin/sh\necho \"https://github.example.com/raw/infra/lima-templates/master/$1.yaml\"\n```\n\n**`limactl-url-prod`:**\n```bash\n#!/bin/sh\necho \"https://github.example.com/raw/infra/lima-templates/v1.8.3/$1.yaml\"\n```\n\n```console\n$ limactl start dev:webapp    # uses master branch\n$ limactl start prod:webapp   # uses pinned release\n```\n\nA handler can also generate a pre-signed URL. This `s3:` handler serves templates from a private S3 bucket:\n\n**`limactl-url-s3`:**\n```bash\n#!/bin/sh\naws s3 presign \"s3://my-lima-templates/$1.yaml\"\n```\n\n```console\n$ limactl start s3:webapp\n```\n\n### Returning a file path\n\nA handler can return a local file path instead of a URL. This `instance:` handler retrieves the saved configuration of an existing instance:\n\n**`limactl-url-instance`:**\n```bash\n#!/bin/sh\necho \"${LIMA_HOME:-$HOME/.lima}/$1/lima.yaml\"\n```\n\nThis lets you create a new instance using the same template as an existing one:\n\n```console\n$ limactl create --name another instance:default\n```\n\n### Generating YAML on the fly\n\nA handler can generate a template file at runtime and return its path. This is useful when image URLs contain dynamic components like dates or version numbers that cannot be expressed in static YAML.\n\n**`limactl-url-nightly`:**\n```bash\n#!/bin/sh\nset -eu\nCACHE_DIR=\"${XDG_CACHE_HOME:-$HOME/.cache}/lima/limactl-url-nightly\"\nmkdir -p \"${CACHE_DIR}\"\n\nBUILD=$(curl -fsSL \"https://builds.example.com/latest-id\")\nFILE=\"${CACHE_DIR}/nightly.yaml\"\ncat <<EOF >\"${FILE}\"\nimages:\n- location: \"https://builds.example.com/${BUILD}/image-amd64.qcow2\"\n  arch: \"x86_64\"\n- location: \"https://builds.example.com/${BUILD}/image-arm64.qcow2\"\n  arch: \"aarch64\"\nEOF\necho \"$FILE\"\n```\n\nThe generated file stays in the cache directory. It will be overwritten on the next invocation or cleaned up with the rest of the cache.\n\nA template can reference this handler via the `base:` field:\n\n```yaml\nbase:\n- nightly:images\n- template:_default/mounts\n```\n\n## Composing schemes\n\nHandlers can call `limactl template url` to resolve other schemes, including [`github:`](../../templates/github/). This lets a handler build on existing schemes rather than constructing raw URLs itself.\n\n### Track the latest release\n\nThis handler resolves a `github:` URL and then replaces the branch with the latest semver tag:\n\n**`limactl-url-latest`:**\n```bash\n#!/bin/sh\n# Resolve the github: scheme to an https://raw.githubusercontent.com URL\nurl=$(limactl template url \"github:$1\")\n# Extract \"org/repo\" from the URL (fields 4-5 of the path)\nrepo=$(echo \"$url\" | cut -d'/' -f4-5)\n# Find the latest semver release tag (e.g. \"v2.1.0\"), ignoring pre-releases\ntag=$(gh release list --repo \"$repo\" --json tagName \\\n         --jq 'map(select(.tagName | test(\"^v[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\"))) | .[0].tagName')\n# Replace the branch/tag segment in the URL with the release tag\necho \"$url\" | sed -E \"s|(https://raw\\.githubusercontent\\.com/[^/]+/[^/]+/)[^/]+/|\\1$tag/|\"\n```\n\n```console\n$ limactl template url latest:lima-vm/lima/templates/default\nhttps://raw.githubusercontent.com/lima-vm/lima/v2.1.0/templates/default.yaml\n```\n\n### Search multiple repos\n\nThis handler tries several repositories in order and returns the first match:\n\n**`limactl-url-my`:**\n```bash\n#!/bin/sh\ntemplate=$1\nfor repo in \\\n    \"github:my-org/templates/%s@main\" \\\n    \"github:lima-vm/lima/templates/%s@master\"; do\n    url=$(limactl template url \"$(printf \"$repo\" \"$template\")\")\n    if curl --head --silent --fail \"$url\" >/dev/null; then\n        echo \"$url\"\n        exit\n    fi\ndone\necho \"Template $template not found\" >&2\nexit 1\n```\n\n```console\n$ limactl start my:custom-distro   # checks my-org/templates first, then lima-vm/lima\n```\n"
  },
  {
    "path": "website/content/en/docs/config/plugin/vm.md",
    "content": "---\ntitle: VM driver plugins\nweight: 1\n---\n\nSee [Virtual Machine Drivers](../../dev/drivers.md).\n<!-- TODO: migrate the most of the content from ../../dev/drivers.md to here -->\n"
  },
  {
    "path": "website/content/en/docs/config/port.md",
    "content": "---\ntitle: Port Forwarding\nweight: 31\n---\n\nLima supports automatic port-forwarding of localhost ports from guest to host.\n\n## Port forwarding types\n\nLima supports two port forwarders: SSH and GRPC.\n\nThe default port forwarder is shown in the following table.\n\n| Version | Default |\n| --------| ------- |\n| v0.1.0  | SSH     |\n| v1.0.0  | GRPC    |\n| v1.0.1  | SSH     |\n| v1.1.0  | GRPC    |\n\nThe default was once changed to GRPC in Lima v1.0, but it was reverted to SSH in v1.0.1 due to stability reasons.\nThe default was further reverted to GRPC in Lima v1.1, as the stability issues were resolved.\n\n### Using SSH\n\nSSH based port forwarding was previously the default mode.\n\nTo explicitly use SSH forwarding use the below command\n\n```bash\nLIMA_SSH_PORT_FORWARDER=true limactl start\n```\n\n#### Advantages\n\n- Outperforms GRPC when VSOCK is available\n\n#### Caveats\n\n- Doesn't support UDP based port forwarding\n- Spawns child process on host for running SSH master.\n\n#### SSH over AF_VSOCK\n\n| ⚡ Requirement | Lima >= 2.0 |\n|---------------|-------------|\n\nIf VM is VZ based and systemd is v256 or later (e.g. Ubuntu 24.10+), Lima uses AF_VSOCK for communication between host and guest.\nSSH based port forwarding is much faster when using AF_VSOCK compared to traditional virtual network based port forwarding.\n\nTo disable this feature, set `.ssh.overVsock` to `false`:\n\n```bash\nlimactl start --set '.ssh.overVsock=false'\n```\n\n### Using GRPC\n\n| ⚡ Requirement | Lima >= 1.0 |\n|---------------|-------------|\n\nIn this model, lima uses existing GRPC communication (Host <-> Guest) to tunnel port forwarding requests.\nFor each port forwarding request, a GRPC tunnel is created and this will be used for transmitting data\n\nTo enable this feature, set `LIMA_SSH_PORT_FORWARDER` to `false`:\n\n```bash\nLIMA_SSH_PORT_FORWARDER=false limactl start\n```\n\n#### Advantages\n\n- Supports both TCP and UDP based port forwarding\n- Performs faster compared to SSH based forwarding, when VSOCK is not available\n- No additional child process for port forwarding\n\n\n## Accessing ports by IP address\n\nTo access a guest's ports by its IP address, connect the guest to the `vzNAT` or the `lima:shared` network.\n\nThe `vzNAT` network is extremely faster and easier to use, however, `vzNAT` is only available for [VZ](./vmtype/vz.md) guests.\n\n```bash\nlimactl start --network vzNAT\nlima ip addr show lima0\n```\n\nSee [Config » Network » VMNet networks](./network/vmnet.md) for the further information.\n\n## Benchmarks\n\n<!-- When updating the benchmark result, make sure to update the benchmarking environment too -->\n\n| By localhost | SSH (w/o VSOCK) | GRPC           | SSH (w/ VSOCK)  |\n|--------------|-----------------|----------------|-----------------|\n| TCP          | 4.06 Gbits/sec  | 5.37 Gbits/sec | 6.32 Gbits/sec  |\n| TCP Reverse  | 3.84 Gbits/sec  | 7.11 Gbits/sec | 7.47 Gbits/sec  |\n\n| By IP address | lima:shared    | vzNAT          |\n|---------------|----------------|----------------|\n| TCP           | 3.46 Gbits/sec | 59.2 Gbits/sec |\n| TCP Reverse   | 2.35 Gbits/sec | 130  Gbits/sec |\n\nThe benchmarks detail above are obtained using the following commands\n\n```\nHost -> limactl start vz\n\nVZ Guest -> iperf3 -s\n\nHost -> iperf3 -c 127.0.0.1 //Benchmark for TCP (average of \"sender\" and \"receiver\")\nHost -> iperf3 -c 127.0.0.1 -R //Benchmark for TCP Reverse (same as above)\n```\n\nThe benchmark result, especially the throughput of vzNAT, highly depends on the performance of the hardware.\n\n<details>\n<summary>Benchmarking environment</summary>\n<p>\n\n- Lima version: 2.0.0-alpha.2\n- Guest: Ubuntu 25.04\n  - OpenSSH 9.9p1\n  - iperf 3.18\n- Host: macOS 26.0.1\n  - OpenSSH 10.0p2\n  - iperf 3.19.1 (Homebrew)\n- Hardware: MacBook Pro 2024 (M4 Max, 128 GiB)\n\n</p>\n</details>\n"
  },
  {
    "path": "website/content/en/docs/config/vmtype/_index.md",
    "content": "---\ntitle: VM types\nweight: 10\n---\n\nLima supports several VM drivers for running guest machines:\n\nThe vmType can be specified only on creating the instance.\nThe vmType of existing instances cannot be changed.\n\n> **💡 For developers**: See [Virtual Machine Drivers](../../dev/drivers) for technical details about driver architecture and creating custom drivers.\n\n\nSee the following flowchart to choose the best vmType for you:\n```mermaid\nflowchart\n  host{\"Host OS\"} -- \"Windows\" --> wsl2[\"WSL2\"]\n  host -- \"Linux\" --> qemu[\"QEMU\"]\n  host -- \"macOS\" --> intel_on_arm{\"Need to run <br> Intel binaries <br> on ARM?\"}\n  intel_on_arm -- \"Yes\" --> just_elf{\"Just need to <br> run Intel userspace (fast), <br> or entire Intel VM (slow)?\"}\n  just_elf -- \"Userspace (fast)\" --> vz\n  just_elf -- \"VM (slow)\" --> qemu\n  intel_on_arm --  \"No\" --> vz[\"VZ\"]\n```\n\nThe default vmType is QEMU in Lima prior to v1.0.\nStarting with Lima v1.0, Lima will use VZ by default on macOS (>= 13.5) for new instances,\nunless the config is incompatible with VZ. (e.g., non-native arch is specified)\n"
  },
  {
    "path": "website/content/en/docs/config/vmtype/krunkit.md",
    "content": "---\ntitle: Krunkit\nweight: 4\n---\n\n> **Warning**\n> \"krunkit\" is experimental\n\n| ⚡ Requirement | Lima >= 2.0, macOS >= 14 (Sonoma+), Apple Silicon (arm64) |\n| ------------- | ----------------------------------------------------------- |\n\nKrunkit runs super‑light VMs on macOS/ARM64 with a focus on GPU access. It builds on [libkrun](https://github.com/containers/libkrun), a library that embeds a VMM so apps can launch processes in a hardware‑isolated VM (HVF on macOS, KVM on Linux). The standout feature is GPU support in the guest via Mesa’s Venus Vulkan driver ([venus](https://docs.mesa3d.org/drivers/venus.html)), enabling Vulkan workloads inside the VM. See the project: [containers/krunkit](https://github.com/containers/krunkit).\n\n## Install krunkit (host)\n```bash\nbrew tap slp/krunkit\nbrew install krunkit\n```\nFor reference: https://github.com/slp/homebrew-krun\n\n\n## Using the driver with Lima\n\nThe `krunkit` driver is usually bundled with Lima as an external driver:\n\n```console\n$ limactl info | jq .vmTypesEx\n{\n  \"krunkit\": {\n    \"location\": \"/opt/homebrew/Cellar/lima/2.0.1/libexec/lima/lima-driver-krunkit\"\n  },\n  \"qemu\": {\n    \"location\": \"internal\"\n  },\n  \"vz\": {\n    \"location\": \"internal\"\n  }\n}\n```\n\nIf the driver is not installed, build and install the driver as follows:\n\n```bash\ngit clone https://github.com/lima-vm/lima\ncd lima\n# Replace vX.Y.Z with the actual version\ngit checkout vX.Y.Z\nmake ADDITIONAL_DRIVERS=krunkit additional-drivers\n# Replace /usr/local with the actual installation prefix\ncp -a _output/libexec/lima/lima-driver-krunkit /usr/local/libexec/lima/\n```\n\nSee also [Developers' guide » Virtual Machine Drivers](../../dev/drivers.md).\n\n## Quick start\n\nYou can run AI models either:\n- With containers (fast to get started; any distro works), or\n- Without containers (choose Fedora; build `llama.cpp` from source).\n\nBefore running, install a small model on the host so examples can run quickly. We’ll use `Qwen3‑1.7B GGUF`:\n\n```bash\nmkdir -p models\ncurl -LO --output-dir models 'https://huggingface.co/Qwen/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q8_0.gguf'\n```\n\n### 1) Run models using containers (easiest)\n\nStart a krunkit VM with the default Lima template:\n\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=krunkit\nlimactl shell default\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nThen inside the VM:\n\n```bash\nnerdctl run --rm -ti \\\n  --device /dev/dri \\\n  -v $(pwd)/models:/models \\\n  quay.io/slopezpa/fedora-vgpu-llama\n```\nFor reference: https://sinrega.org/2024-03-06-enabling-containers-gpu-macos/\n\nOnce inside the container:\n\n```bash\nllama-cli -m /models/Qwen3-1.7B-Q8_0.gguf -b 512 -ngl 99 -p \"Introduce yourself\"\n```\n\nYou can now chat with the model.\n\n### 2) Run models without containers (hard way)\n\nThis path builds and installs dependencies (which can take some time. For faster builds, allocate more CPUs and memory to the VM. See [`options`](../../reference/limactl_start/#options)). Use Fedora as the image.\n\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=krunkit template:fedora\nlimactl shell fedora\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nvmType: krunkit\n\nbase:\n- template:_images/fedora\n- template:_default/mounts\n\nmountType: virtiofs\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nOnce inside the VM, install GPU/Vulkan support:\n\n<p>\n<details>\n<summary>Click to expand script</summary>\n\n```bash\n#!/bin/bash\n# SPDX-FileCopyrightText: Copyright The Lima Authors\n# SPDX-License-Identifier: Apache-2.0\n\nset -eu -o pipefail\n\n# Install required packages\ndnf install -y dnf-plugins-core dnf-plugin-versionlock llvm18-libs\n\n# Install Vulkan and Mesa base packages\ndnf install -y \\\n  mesa-vulkan-drivers \\\n  vulkan-loader-devel \\\n  vulkan-headers \\\n  vulkan-tools \\\n  vulkan-loader \\\n  glslc\n\n# Enable COPR repo with patched Mesa for Venus support\ndnf copr enable -y slp/mesa-libkrun-vulkan\n\n# Downgrade to patched Mesa version from COPR\nREPOID=\"copr:copr.fedorainfracloud.org:slp:mesa-libkrun-vulkan\"\nMESA_VERSION=$(dnf repoquery -q --available --repoid=\"$REPOID\" --latest-limit=1 --qf '%{evr}' mesa-vulkan-drivers 2>/dev/null)\ndnf downgrade -y \"mesa-vulkan-drivers-${MESA_VERSION}\"\n\n# Lock Mesa version to prevent automatic upgrades\ndnf versionlock add \"mesa-vulkan-drivers-${MESA_VERSION}\"\n\n# Clean up\ndnf clean all\n\necho \"Installing llama.cpp with Vulkan support...\"\n# Build and install llama.cpp with Vulkan support\ndnf install -y git cmake clang curl-devel glslc vulkan-devel virglrenderer\n(\n  cd ~\n  git clone https://github.com/ggml-org/llama.cpp\n  (\n    cd llama.cpp\n    cmake -B build -DGGML_VULKAN=ON -DGGML_CCACHE=OFF -DGGML_NATIVE=OFF -DCMAKE_INSTALL_PREFIX=/usr\n    cmake --build build --config Release -j8\n    cmake --install build\n  )\n  rm -fr llama.cpp\n)\n\necho \"Successfully installed llama.cpp with Vulkan support. Use 'llama-cli' app with .gguf models.\"\n```\n\n</details>\n</p>\n\nThe script will prompt to build and install `llama.cpp` with Venus support from source.\n\nAfter installation, run:\n\n```bash\nllama-cli -m models/Qwen3-1.7B-Q8_0.gguf -b 512 -ngl 99 -p \"Introduce yourself\"\n```\n\nand enjoy chatting with the AI model.\n\n💡 **Tip:** If the model takes too long to load or experiences performance issues, try reducing the `-ngl` (number of GPU layers) value.\n\n## Notes and caveats\n- macOS Ventura or later on Apple Silicon is required.\n- To verify GPU/Vulkan in the guest container or VM, use tools like `vulkaninfo --summary`.\n- AI models on containers can run on any Linux distribution but without containers Fedora is required.\n- For more information about usage of `llama-cli`. See [llama.cpp](https://github.com/ggml-org/llama.cpp) `README.md`.\n- Driver architecture details: see [Virtual Machine Drivers](../../dev/drivers).\n"
  },
  {
    "path": "website/content/en/docs/config/vmtype/qemu.md",
    "content": "---\ntitle: QEMU\nweight: 1\n---\n\n\"qemu\" option makes use of QEMU to run guest operating system.\n\n\"qemu\" is the default driver for Linux hosts.\n\nRecommended QEMU version:\n- v8.2.1 or later (macOS)\n- v6.2.0 or later (Linux)\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=qemu\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nvmType: \"qemu\"\n\nbase:\n- template:_images/ubuntu\n- template:_default/mounts\n```\n{{% /tab %}}\n{{< /tabpane >}}\n"
  },
  {
    "path": "website/content/en/docs/config/vmtype/vz.md",
    "content": "---\ntitle: VZ\nweight: 2\n---\n\n| ⚡ Requirement | Lima >= 0.14, macOS >= 13.0 |\n|-------------------|-----------------------------|\n\n\"vz\" option makes use of native virtualization support provided by macOS Virtualization.Framework.\n\n\"vz\" has been the default driver for macOS hosts since Lima v1.0.\n\nAn example configuration (no need to be specified manually):\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=vz\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nvmType: \"vz\"\n\nbase:\n- template:_images/ubuntu\n- template:_default/mounts\n```\n{{% /tab %}}\n{{< /tabpane >}}\n### Caveats\n- \"vz\" option is only supported on macOS 13 or above\n- Virtualization.framework doesn't support running \"intel guest on arm\" and vice versa\n\n### Known Issues\n- \"vz\" doesn't support `legacyBIOS: true` option, so guest machine like `centos-stream` and `oraclelinux-8` will not work on Intel Mac.\n- When running lima using \"vz\", `${LIMA_HOME}/<INSTANCE>/serial.log` will not contain kernel boot logs\n- On Intel Mac with macOS prior to 13.5, Linux kernel v6.2 (used by Ubuntu 23.04, Fedora 38, etc.) is known to be unbootable on vz.\n  kernel v6.3 and later should boot, as long as it is booted via GRUB.\n  https://github.com/lima-vm/lima/issues/1577#issuecomment-1565625668\n  The issue is fixed in macOS 13.5.\n"
  },
  {
    "path": "website/content/en/docs/config/vmtype/wsl2.md",
    "content": "---\ntitle: WSL2\nweight: 3\n---\n\n> **Warning**\n> \"wsl2\" mode is experimental\n\n| ⚡ Requirement | Lima >= 0.18 + (Windows >= 10 Build 19041 OR Windows 11) |\n| ----------------- | -------------------------------------------------------- |\n\n\"wsl2\" option makes use of native virtualization support provided by Windows' `wsl.exe` ([more info](https://learn.microsoft.com/en-us/windows/wsl/about)).\n\nAn example configuration:\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --vm-type=wsl2 --mount-type=wsl2 --containerd=system\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\n# Example to run Fedora using vmType: wsl2\nvmType: wsl2\nimages:\n# Source: https://github.com/runfinch/finch-core/blob/main/rootfs/Dockerfile\n- location: \"https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1771357941.tar.gz\"\n  arch: \"x86_64\"\n  digest: \"sha256:423d1a0f1cabeaea6801995c90ed896dccc091180068626430f19fd87853fdf3\"\nmountType: wsl2\ncontainerd:\n  system: true\n  user: false\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\n### Caveats\n- \"wsl2\" option is only supported on newer versions of Windows (roughly anything since 2019)\n\n### Known Issues\n- \"wsl2\" currently doesn't support many of Lima's options. See [this file](https://github.com/lima-vm/lima/blob/master/pkg/wsl2/wsl_driver_windows.go#L19) for the latest supported options.\n- When running lima using \"wsl2\", `${LIMA_HOME}/<INSTANCE>/serial.log` will not contain kernel boot logs\n- WSL2 requires a `tar` formatted rootfs archive instead of a VM image\n- Windows doesn't ship with ssh.exe, gzip.exe, etc. which are used by Lima at various points. The easiest way around this is to run `winget install -e --id Git.MinGit` (winget is now built in to Windows as well), and add the resulting `C:\\Program Files\\Git\\usr\\bin\\` directory to your path.\n"
  },
  {
    "path": "website/content/en/docs/dev/_index.md",
    "content": "---\ntitle: Developers' guide\nweight: 500\n---\n\nThis section provides technological resources for developers of Lima.\n\nSee also [`Community`](../community) » [`Contributing`](../community/contributing)\nfor how to contribute to the project.\n"
  },
  {
    "path": "website/content/en/docs/dev/drivers.md",
    "content": "---\ntitle: Virtual Machine Drivers\nweight: 15\n---\n\n | ⚡ Requirement | Lima >= 2.0 |\n |----------------|-------------|\n\nLima supports two types of drivers: **internal** and **external**. This architecture allows for extensibility and platform-specific implementations. Drivers are unware whether they are internal or external.\n\n> **💡 See also**: [VM Types](../../config/vmtype) for user configuration of different virtualization backends.\n\n\n## Internal vs External Drivers\n\n**Internal Drivers** are compiled directly into the `limactl` binary and are registered automatically at startup by passing the driver object into `registry.Register()` function and importing the package in the main limactl code using Go's blank import `_`. For example:\n- **qemu:** [registration file](https://github.com/lima-vm/lima/blob/master/pkg/driver/qemu/register.go) & [import file](https://github.com/lima-vm/lima/blob/master/cmd/limactl/main_qemu.go)\n\nBuild tags control which drivers are compiled as internal vs external (e.g., `external_qemu`, `external_vz`, `external_wsl2`).\n\n**External Drivers** are separate executables that communicate with Lima via gRPC. They are discovered at runtime from configured directories.\n\n> **⚠️ Note**: External drivers are experimental and the API may change in future releases.\n\n## Building Drivers as External\n\nYou can build existing internal drivers as external drivers using the `ADDITIONAL_DRIVERS` Makefile variable:\n\n```bash\n# Build QEMU as external driver\nmake ADDITIONAL_DRIVERS=qemu limactl additional-drivers\n\n# Build multiple drivers as external\nmake ADDITIONAL_DRIVERS=\"qemu vz wsl2\" limactl additional-drivers\n```\n\nThis creates external driver binaries in `_output/libexec/lima/` with the naming pattern `lima-driver-<name>` (or `lima-driver-<name>.exe` on Windows).\n\n## Driver Discovery\n\nLima discovers external drivers from these locations:\n\n1. **Custom directories**: Set path to the external driver's directory via `LIMA_DRIVERS_PATH` environment variable\n2. **Standard directory**: `<LIMA-PREFIX>/libexec/lima/`, where `<LIMA_PREFIX>` is the location path where the Lima binary is present\n\nThe discovery process is handled by [`pkg/registry/registry.go`.](https://github.com/lima-vm/lima/blob/master/pkg/registry/registry.go)\n\n## Creating Custom External Drivers\n\nTo create a new external driver:\n\n1. **Implement the interface**: Your driver must implement the [`driver.Driver`](https://pkg.go.dev/github.com/lima-vm/lima/v2/pkg/driver#Driver) interface:\n\n```go\ntype Driver interface {\n\tLifecycle\n\tGUI\n\tSnapshotManager\n\tGuestAgent\n\n\tInfo() Info\n\tConfigure(inst *limatype.Instance) *ConfiguredDriver\n\tFillConfig(ctx context.Context, cfg *limatype.LimaYAML, filePath string) error\n\tSSHAddress(ctx context.Context) (string, error)\n}\n```\n\n2. **Create main.go**: Use [`server.Serve()`](https://pkg.go.dev/github.com/lima-vm/lima/v2/pkg/driver/external/server#Serve) to expose your driver:\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"github.com/lima-vm/lima/v2/pkg/driver/external/server\"\n)\n\nfunc main() {\n    driver := &MyDriver{}\n    server.Serve(context.Background(), driver)\n}\n```\n\n3. **Build and deploy**:\n   - Build your driver: `go build -o lima-driver-mydriver main.go`\n   - Place the binary in a directory accessible via `LIMA_DRIVERS_PATH`\n   - Ensure the binary is executable\n\n4. **Use the driver**: Explicitly specify the driver when creating instances:\n\n```bash\nlimactl create myinstance --vm-type=mydriver template:default\n```\n\n## Examples\n\nSee existing external driver implementations:\n- [`cmd/lima-driver-qemu/main.go`](https://github.com/lima-vm/lima/blob/master/cmd/lima-driver-qemu/main.go)\n- [`cmd/lima-driver-vz/main_darwin.go`](https://github.com/lima-vm/lima/blob/master/cmd/lima-driver-vz/main_darwin.go)\n- [`cmd/lima-driver-wsl2/main_windows.go`](https://github.com/lima-vm/lima/blob/master/cmd/lima-driver-wsl2/main_windows.go)\n"
  },
  {
    "path": "website/content/en/docs/dev/git.md",
    "content": "---\ntitle: Git tips\nweight: 30\n---\n\n## Squashing Commits\n\nTo combine multiple commits into one (recommended unless your PR covers multiple topics):\n\n```bash\n# Adjust the number based on how many commits you want to squash\ngit rebase -i HEAD~3\n```\n\nIn the interactive editor that appears:\n1. Keep the first commit as `pick`\n2. Change subsequent commits from `pick` to `fixup` (short form`f`). You may also choose `squash` (`s`), however, `fixup` is recommended to keep the commit message clean.\n3. Save and close the editor to proceed\n\nExample:\n```\npick aaaaaaa First commit message\npick bbbbbbb Second commit message\npick ccccccc Fix typo\n```\n\nTo:\n```\npick aaaaaaa First commit message\nf bbbbbbb Second commit message\nf ccccccc Fix typo\n```\n\n## Rebasing onto Upstream Master\n\nTo update your branch with the latest changes from upstream:\n\n```bash\ngit remote add upstream https://github.com/lima-vm/lima.git  # Only needed once\ngit fetch upstream\ngit rebase upstream/master\n```\n\n## Troubleshooting\n\nIf you encounter issues during rebase:\n\n```bash\ngit rebase --abort  # Cancel the rebase and return to original state\ngit status          # Check current state\n```\n\nFor merge conflicts during rebase:\n1. Resolve the conflicts in the files\n2. `git add` the resolved files\n3. `git rebase --continue`\n"
  },
  {
    "path": "website/content/en/docs/dev/internals.md",
    "content": "---\ntitle: Internal data structure\nweight: 10\n---\n\n## Lima home directory (`${LIMA_HOME}`)\n\nDefaults to `~/.lima`.\n\nNote that we intentionally avoid using `~/Library/Application Support/Lima` on macOS.\n\nWe use `~/.lima` so that we can have enough space for the length of the socket path,\nwhich must be less than 104 characters on macOS.\n\nUnix: The directory can not be located on an NFS file system, it needs to be local.\n\n### Config directory (`${LIMA_HOME}/_config`)\n\nThe config directory contains global lima settings that apply to all instances.\n\nUser identity:\n\nLima creates a default identity and uses its public key as the authorized key\nto access all lima instances. In addition, lima will also configure all public\nkeys from `~/.ssh/*.pub` as well, so the user can use the ssh endpoint without\nhaving to specify an identity explicitly.\n- `user`: private key\n- `user.pub`: public key\n\n### Instance directory (`${LIMA_HOME}/<INSTANCE>`)\n\nAn instance directory contains the following files:\n\nMetadata:\n- `lima-version`: the Lima version used to create this instance\n- `lima.yaml`: the YAML\n- `protected`: empty file, used by `limactl protect`\n\ncloud-init:\n- `cloud-config.yaml`: cloud-init configuration, for reference only.\n- `cidata.iso`: cloud-init ISO9660 image. See [`cidata.iso`](#cidataiso).\n\nAnsible:\n- `ansible-inventory.yaml`: the Ansible node inventory. See [ansible](#ansible).\n\ndisk:\n- `image`: the downloaded VM image; renamed to `disk` or `iso` during setup\n- `image.ipsw`: hardlink to `image`, created for running `VZMacOSInstaller` that requires the image file to have the `.ipsw` suffix\n- `disk`: the VM disk (can be a symlink to legacy `diffdisk`)\n- `iso`: optional CDROM image for ISO-based installations (can be a symlink to legacy `basedisk`)\n- `basedisk`: legacy name for the downloaded image (pre-v2.1 instances; may remain as a qcow2 backing file)\n- `diffdisk`: legacy name for `disk` (pre-v2.1 instances)\n\ndisk mount:\n- `mnt`: the mount point directory for the `disk`, used for macOS guests\n\nkernel:\n- `kernel`: the kernel\n- `kernel.cmdline`: the kernel cmdline\n- `initrd`: the initrd\n\nQEMU:\n- `qemu.pid`: QEMU PID\n- `qmp.sock`: QMP socket\n- `qemu-efi-code.fd`: QEMU UEFI code (not always present)\n\nVZ:\n- `vz.pid`: VZ PID\n- `vz-identifier`: Unique machine identifier file for a VM\n- `vz-hwmodel`: Hardware model information for a Mac VM\n- `vz-aux`: Auxiliary storage for a Mac VM\n- `vz-efi`: EFIVariable store file for a VM\n\nSerial:\n- `serial.log`: default serial log (QEMU only), for debugging\n- `serial.sock`: default serial socket (QEMU only), for debugging (Usage: `socat -,echo=0,icanon=0 unix-connect:serial.sock`)\n- `serialp.log`: PCI serial log (QEMU (ARM) only), for debugging\n- `serialp.sock`: PCI serial socket (QEMU (ARM) only), for debugging (Usage: `socat -,echo=0,icanon=0 unix-connect:serialp.sock`)\n- `serialv.log`: virtio serial log, for debugging\n- `serialv.sock`: virtio serial socket (QEMU only), for debugging (Usage: `socat -,echo=0,icanon=0 unix-connect:serialv.sock`)\n\nSSH:\n- `ssh.sock`: SSH control master socket\n- `ssh.config`: SSH config file for `ssh -F`. Not consumed by Lima itself.\n\nVNC:\n- `vncdisplay`: VNC display host/port\n- `vncpassword`: VNC display password\n\nGuest agent:\n\nEach drivers use their own mode of communication\n- `qemu`: uses virtio-port `io.lima-vm.guest_agent.0`\n- `vz`: uses vsock port 2222\n- `wsl2`: uses free random vsock port\nThe fallback is to use port forward over ssh port\n- `ga.sock`: Forwarded to `/run/lima-guestagent.sock` in the guest, via SSH\n\nHost agent:\n- `ha.pid`: hostagent PID\n- `ha.sock`: hostagent REST API\n- `ha.stdout.log`: hostagent stdout (JSON lines, see `pkg/hostagent/events.Event`)\n- `ha.stderr.log`: hostagent stderr (human-readable messages)\n\n## Disk directory (`${LIMA_HOME}/_disk/<DISK>`)\n\nA disk directory contains the following files:\n\ndata disk:\n- `datadisk`: the qcow2 or raw disk that is attached to an instance\n\nlock:\n- `in_use_by`: symlink to the instance directory that is using the disk\n\nWhen using `vmType: vz` (Virtualization.framework), on boot, any qcow2 (default) formatted disks that are specified in `additionalDisks` will be converted to RAW since [Virtualization.framework only supports mounting RAW disks](https://developer.apple.com/documentation/virtualization/vzdiskimagestoragedeviceattachment). This conversion enables additional disks to work with both Virtualization.framework and QEMU, but it has some consequences when it comes to interacting with the disks. Most importantly, a regular macOS default `cp` command will copy the _entire_ virtual disk size, instead of just the _used/allocated_ portion. The easiest way to copy only the used data is by adding the `-c` option to cp: `cp -c old_path new_path`. `cp -c` uses clonefile(2) to create a copy-on-write clone of the disk, and should return instantly.\n\n`ls` will also only show the full/virtual size of the disks. To see the allocated space, `du -h disk_path` or `qemu-img info disk_path` can be used instead. See [#1405](https://github.com/lima-vm/lima/pull/1405) for more details.\n\n## Templates directory (`${LIMA_HOME}/_templates`)\n\nThe templates directory can store additional template files that can be referenced with the `template:` schema.\n\nIf the template directory exists (and `$LIMA_TEMPLATES_PATH` is not set), then this directory will be searched before the `/usr/local/share/lima/templates` default directory that contains all the templates bundled with Lima itself.\n\n## Lima cache directory (`~/Library/Caches/lima`)\n\nCurrently hard-coded to `~/Library/Caches/lima` on macOS.\n\nUses `$XDG_CACHE_HOME/lima`, normally `$HOME/.cache/lima`, on Linux.\n\nUses `%LocalAppData%\\lima`, `C:\\Users\\<USERNAME>\\AppData\\Local\\lima`, on Windows.\n\n### Download cache (`~/Library/Caches/lima/download/by-url-sha256/<SHA256_OF_URL>`)\n\nThe directory contains the following files:\n\n- `url`: raw url text, without \"\\n\"\n- `data`: data\n- `<ALGO>.digest`: digest of the data, in OCI format.\n   e.g., file name `sha256.digest`, with content `sha256:5ba3d476707d510fe3ca3928e9cda5d0b4ce527d42b343404c92d563f82ba967`\n\n\n## Ansible\nThe instance directory contains an inventory file, that might be used with Ansible playbooks and commands.\nSee [Building Ansible inventories](https://docs.ansible.com/ansible/latest/inventory_guide/) about dynamic inventories.\n\n## `cidata.iso`\n`cidata.iso` contains the following files:\n\n- `user-data`: [Cloud-init user-data](https://docs.cloud-init.io/en/latest/explanation/format.html)\n- `meta-data`: [Cloud-init meta-data](https://docs.cloud-init.io/en/latest/explanation/instancedata.html)\n- `network-config`: [Cloud-init Networking Config Version 2](https://docs.cloud-init.io/en/latest/reference/network-config-format-v2.html)\n- `lima.env`: The `LIMA_CIDATA_*` environment variables (see below) available during `boot.sh` processing\n- `param.env`: The `PARAM_*` environment variables corresponding to the `param` settings from `lima.yaml`\n- `lima-guestagent`: Lima guest agent binary\n- `nerdctl-full.tgz`: [`nerdctl-full-<VERSION>-<OS>-<ARCH>.tar.gz`](https://github.com/containerd/nerdctl/releases)\n- `boot.sh`: Boot script\n- `boot.<OS>/*`: Boot script modules\n- `boot.essential.<OS>/*`: Essential boot script modules, executed in plain mode too.\n- `util/*`: Utility command scripts, executed in the boot script modules\n- `provision.data/*`: Custom provision files (data)\n- `provision.dependency/*`: Custom provision scripts (dependency)\n- `provision.system/*`: Custom provision scripts (system)\n- `provision.user/*`: Custom provision scripts (user)\n- `provision.yq/*`: Custom provision scripts (yq)\n- `etc_environment`: Environment variables to be added to `/etc/environment` (also loaded during `boot.sh`)\n\nMax file name length = 30\n\n### Volume label\nThe volume label is \"cidata\", as defined by [cloud-init NoCloud](https://docs.cloud-init.io/en/latest/reference/datasources/nocloud.html).\n\n### Environment variables\n- `LIMA_CIDATA_DEBUG`: the value of the `--debug` flag of the `limactl start` command.\n- `LIMA_CIDATA_IID`: the instance ID, regenerated on every boot.\n- `LIMA_CIDATA_NAME`: the lima instance name\n- `LIMA_CIDATA_MNT`: the mount point of the disk. `/mnt/lima-cidata`.\n- `LIMA_CIDATA_USER`: the username string\n- `LIMA_CIDATA_UID`: the numeric UID\n- `LIMA_CIDATA_COMMENT`: the full name or comment string\n- `LIMA_CIDATA_HOME`: the guest home directory\n- `LIMA_CIDATA_SHELL`: the guest login shell\n- `LIMA_CIDATA_HOSTHOME_MOUNTPOINT`: the mount point of the host home directory, or empty if not mounted\n- `LIMA_CIDATA_MOUNTS`: the number of the Lima mounts\n- `LIMA_CIDATA_MOUNTS_%d_MOUNTPOINT`: the N-th mount point of Lima mounts (N=0, 1, ...)\n- `LIMA_CIDATA_MOUNTTYPE`: the type of the Lima mounts (\"reverse-sshfs\", \"9p\", ...)\n- `LIMA_CIDATA_DATAFILE_%08d_OVERWRITE`: set to \"true\" if the datafile should be overwritten if it already exists.\n- `LIMA_CIDATA_DATAFILE_%08d_OWNER`: set to the owner of the datafile.\n- `LIMA_CIDATA_DATAFILE_%08d_PATH`: set to the path the datafile should be copied to.\n- `LIMA_CIDATA_DATAFILE_%08d_PERMISSIONS`: set to the file permissions (in octal) for the datafile.\n- `LIMA_CIDATA_CONTAINERD_USER`: set to \"1\" if rootless containerd to be set up\n- `LIMA_CIDATA_CONTAINERD_SYSTEM`: set to \"1\" if system-wide containerd to be set up\n- `LIMA_CIDATA_CONTAINERD_ARCHIVE`: the name of the containerd archive. `nerdctl-full.tgz`\n- `LIMA_CIDATA_SLIRP_GATEWAY`: set to the IP address of the host on the SLIRP network. `192.168.5.2`.\n- `LIMA_CIDATA_SLIRP_DNS`: set to the IP address of the DNS on the SLIRP network. `192.168.5.3`.\n- `LIMA_CIDATA_SLIRP_IP_ADDRESS`: set to the IP address of the guest on the SLIRP network. `192.168.5.15`.\n- `LIMA_CIDATA_UDP_DNS_LOCAL_PORT`: set to the udp port number of the hostagent dns server (or 0 when not enabled).\n- `LIMA_CIDATA_TCP_DNS_LOCAL_PORT`: set to the tcp port number of the hostagent dns server (or 0 when not enabled).\n\n# VM lifecycle\n\n![](/images/internals/lima-sequence-diagram.png)\n\n(based on Lima 0.8.3)\n"
  },
  {
    "path": "website/content/en/docs/dev/testing/_index.md",
    "content": "---\ntitle: Testing\nweight: 20\n---\n\n## Unit tests\n\nThe unit tests are written in Go and can be executed with the following commands:\n\n```bash\ngo test -v ./...\n```\n\nThe unit tests do not execute actual virtual machines.\n\n## Integration tests\n\nThe integration tests incurs actual execution of virtual machines.\n\nThe integration tests are written in [BATS (Bash Automated Testing System)](https://github.com/bats-core/bats-core).\n\nRun the following commands to run the BATS tests:\n\n```bash\ngit submodule update --init --recursive\nmake bats\n```\n\nThe BATS tests are located under [`hack/bats/tests`](https://github.com/lima-vm/lima/tree/master/hack/bats/tests).\n\n### Extra tests\nThere are also extra tests ([`hack/bats/extras`](https://github.com/lima-vm/lima/tree/master/hack/bats/extras)) that are not automatically\ninvoked from `make bats`.\n\nRun the following command to run the extra BATS tests:\n\n```bash\n./hack/bats/lib/bats-core/bin/bats ./hack/bats/extras\n```\n\n## Template-specific tests\n\nTests that are specific to template files are written in bash and partially in Perl.\n\nUse [`hack/test-templates.sh`](https://github.com/lima-vm/lima/blob/master/hack/test-templates.sh)\nto execute tests, with a virtual machine template file, e.g.,:\n\n```bash\n./hack/test-templates.sh ./templates/default.yaml\n./hack/test-templates.sh ./templates/fedora.yaml\n./hack/test-templates.sh ./hack/test-templates/test-misc.yaml\n```\n\n## CI\n\n[`.github/workflows/test.yml`](https://github.com/lima-vm/lima/blob/master/.github/workflows/test.yml)\nexecutes the tests on the GitHub Actions with the [\"Tier 1\"](../../templates/) templates.\n\nMost tests are executed on Linux runners, as macOS runners are slow and flaky.\n\nThe tests about macOS-specific features (e.g., vz and vmnet) are still executed on macOS runners.\n\nCurrently, the Intel version of macOS is used, as the ARM version of macOS on GitHub Actions still\ndo not support nested virtualization.\n"
  },
  {
    "path": "website/content/en/docs/dev/testing/bats-style.md",
    "content": "---\ntitle: BATS Style Guide\nweight: 25\n---\n\nLima uses [BATS](https://bats-core.readthedocs.io/) with the\n[bats-support](https://github.com/bats-core/bats-support),\n[bats-assert](https://github.com/bats-core/bats-assert), and\n[bats-file](https://github.com/bats-core/bats-file) helper libraries.\n\nAll tests run with `errexit` enabled (via `BATS_RUN_ERREXIT=1` in\n`helpers/load.bash`), so any failing command aborts the test immediately.\n\n## When to use `run`\n\nUse `run` only when you need to capture output or assert a non-zero exit code.\nDo not use it just to check that a command succeeds.\n\n### Command should succeed, output does not matter\n\nCall the command directly. `errexit` handles the failure case.\n\n```bash\n# Good\nlimactl shell \"$INSTANCE\" -- mkdir -p /tmp/foo\n\n# Bad — unnecessary run and status check\nrun limactl shell \"$INSTANCE\" -- mkdir -p /tmp/foo\n[[ $status == 0 ]]\n\n# Bad — unnecessary run and assert_success\nrun limactl shell \"$INSTANCE\" -- mkdir -p /tmp/foo\nassert_success\n```\n\n### Command should succeed, and you need its output\n\nUse `run -0` to assert success and capture `$output`/`$lines`, then use\n`assert_output` or `assert_line` to verify the output.\n\n```bash\n# Good\nrun -0 limactl shell \"$INSTANCE\" -- cat /tmp/hello.txt\nassert_output \"hello\"\n\n# Bad — manual status and output checks\nrun limactl shell \"$INSTANCE\" -- cat /tmp/hello.txt\n[[ $status == 0 ]]\n[[ $output == \"hello\" ]]\n```\n\n### Command should fail with a specific exit code\n\nUse `run -N` where `N` is the expected exit code.\n\n```bash\nrun -1 limactl yq -n foo\nassert_output --partial \"invalid input\"\n```\n\n## Checking stderr (log messages)\n\nUse `run_e` (a wrapper for `run --separate-stderr`) when you need to check\nboth stdout and stderr. The helpers `assert_fatal`, `assert_warning`,\n`assert_info`, `assert_error`, and `assert_debug` match Lima's structured\nlog output in stderr.\n\n```bash\nrun_e -1 limactl ls foo foobar bar\nassert_warning 'No instance matching foobar found.'\nassert_fatal 'unmatched instances'\n```\n\n## Checking files and directories\n\nUse bats-file assertions instead of test expressions.\n\n```bash\n# Good\nassert_file_exists \"$LIMA_HOME/$INSTANCE/protected\"\nassert_dir_exists \"$BATS_TEST_TMPDIR/foo/bar\"\n\n# Bad — raw test expressions give poor failure messages\n[[ -f \"$LIMA_HOME/$INSTANCE/protected\" ]]\n[[ -d \"$BATS_TEST_TMPDIR/foo/bar\" ]]\n```\n\n## Test lifecycle\n\nDefine `local_setup_file`, `local_teardown_file`, `local_setup`, and\n`local_teardown` instead of overriding `setup_file`, `setup`, etc. directly.\nThe base implementations in `helpers/load.bash` call these `local_` variants\nautomatically.\n\nSet `INSTANCE` at file scope to have `setup_file` create (or reuse) a Lima\ninstance and `teardown_file` delete it.\n\n## Summary\n\n| Goal | Pattern |\n|---|---|\n| Command must succeed, ignore output | `limactl ...` (bare command) |\n| Command must succeed, check output | `run -0 cmd; assert_output ...` |\n| Command must fail | `run -N cmd; assert_output ...` |\n| Command must fail, check stderr | `run_e -N cmd; assert_fatal ...` |\n| Command must succeed, check stderr | `run_e -0 cmd; assert_info ...` |\n| File or directory exists | `assert_file_exists` / `assert_dir_exists` |\n"
  },
  {
    "path": "website/content/en/docs/examples/_index.md",
    "content": "---\ntitle: Examples\nweight: 3\n---\n\n## Running Linux commands\n```bash\nlima uname -a\n```\n\n## Accessing host files\n\nBy default, the VM has read-only accesses to `/Users/<USERNAME>`.\n\nTo allow writing to `/Users/<USERNAME>`:\n```bash\nlimactl edit --mount-writable\n```\n\n{{% alert title=\"Hint\" color=success %}}\nLima prior to v2.0 mounts `/tmp/lima` too in read-write mode.\n\nThis directory is no longer mounted by default since Lima v2.0.\nTo mount `/tmp/lima` in Lima v2.0 and later, set `--mount /tmp/lima:w`.\nThe `:w` suffix indicates read-write mode.\n{{% /alert %}}\n\n## Running containers\n{{< tabpane text=true >}}\n\n{{% tab header=\"containerd\" %}}\n```bash\nnerdctl.lima run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n{{% /tab %}}\n\n{{% tab header=\"Docker\" %}}\n```bash\nlimactl start template:docker\nexport DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')\ndocker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n{{% /tab %}}\n\n{{% tab header=\"Podman\" %}}\n```bash\nlimactl start template:podman\nexport DOCKER_HOST=$(limactl list podman --format 'unix://{{.Dir}}/sock/podman.sock')\ndocker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n{{% /tab %}}\n\n{{% tab header=\"Kubernetes\" %}}\n```bash\nlimactl start template:k8s\nexport KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\nkubectl create deployment nginx --image nginx:alpine\nkubectl create service nodeport nginx --node-port=31080 --tcp=80:80\n```\n{{% /tab %}}\n\n{{< /tabpane >}}\n\n- <http://127.0.0.1:8080> is accessible from the host, as well as from the VM.\n\n- See more [examples](./containers/).\n\n## Advanced configuration\n\n```bash\nlimactl start \\\n  --name=default \\\n  --cpus=4 \\\n  --memory=8 \\\n  --vm-type=vz \\\n  --rosetta \\\n  --mount-writable \\\n  --network=vzNAT \\\n  template:fedora\n```\n\n- `--name=default`: Set the instance name to \"default\"\n- `--cpus=4`: Set the number of the CPUs to 4\n- `--memory=8`: Set the amount of the memory to 8 GiB\n- `--vm-type=vz`: Use Apple's Virtualization.framework (vz) to enable Rosetta, virtiofs, and vzNAT\n- `--rosetta`: Allow running Intel (AMD) binaries on ARM\n- `--mount-writable`: Make the home mount (`/Users/<USERNAME>`) writable\n- `--network=vzNAT`: Make the VM reachable from the host by its IP address\n- `template:fedora`: Use Fedora\n"
  },
  {
    "path": "website/content/en/docs/examples/ai.md",
    "content": "---\ntitle: AI agents\nweight: 5\n---\n\nLima is useful for running AI agents inside a VM, so as to prevent agents\nfrom directly reading, writing, or executing the host files.\n\nFor running AI agents, it is highly recommended to only mount your project directory (current directory)\ninto the VM:\n\n{{< tabpane text=true >}}\n{{% tab header=\"Lima v2.0+\" %}}\n```bash\nlimactl start --mount-only .:w\n```\n\nDrop `:w` for read-only mode.\n{{% /tab %}}\n{{% tab header=\"Lima v1.x\" %}}\n```bash\nlimactl start --set \".mounts=[{\\\"location\\\":\\\"$(pwd)\\\", \\\"writable\\\":true}]\"\n```\n\nSet `writable` to `false` for read-only mode.\n{{% /tab %}}\n{{< /tabpane >}}\n\n<!--\nAI agents are sorted alphabetically.\n\nNode.js is installed via snap, not apt, as AI agents often require a very recent release of Node.js.\n-->\n{{< tabpane text=true >}}\n{{% tab header=\"Aider\" %}}\n```\nlima sudo apt install -y pipx\nlima pipx install aider-install\nlima sh -c 'echo \"export PATH=$PATH:$HOME/.local/bin\" >>~/.bash_profile'\nlima aider-install\nlima aider\n```\n\nFollow the guide shown in the first session for authentication.\n\nAlternatively, you can set environmental variables via:\n<!-- TODO: change ${USER}.linux to ${USER}.guest after releasing v2.1 GA -->\n```\nlima vi \"/home/${USER}.linux/.bash_profile\"\n```\n\nSee also <https://github.com/Aider-AI/aider>.\n{{% /tab %}}\n{{% tab header=\"Claude Code\" %}}\n```\nlima sudo snap install node --classic\nlima sudo npm install -g @anthropic-ai/claude-code\nlima claude\n```\n\nFollow the guide shown in the first session for authentication.\n\nAlternatively, you can set `export ANTHROPIC_API_KEY...` via:\n```\nlima vi \"/home/${USER}.linux/.bash_profile\"\n```\n\nSee also <https://github.com/anthropics/claude-code>.\n{{% /tab %}}\n{{% tab header=\"Codex\" %}}\n```\nlima sudo snap install node --classic\nlima sudo npm install -g @openai/codex\nlima codex\n```\n\nFollow the guide shown in the first session for authentication.\n\nAlternatively, you can set `export OPENAI_API_KEY...` via:\n```\nlima vi \"/home/${USER}.linux/.bash_profile\"\n```\n\nSee also <https://github.com/openai/codex>.\n{{% /tab %}}\n{{% tab header=\"Gemini\" %}}\n```\nlima sudo snap install node --classic\nlima sudo npm install -g @google/gemini-cli\nlima gemini\n```\n\nFollow the guide shown in the first session for authentication.\n\nAlternatively, you can set `export GEMINI_API_KEY...` via:\n```\nlima vi \"/home/${USER}.linux/.bash_profile\"\n```\n\nSee also <https://github.com/google-gemini/gemini-cli>.\n{{% /tab %}}\n{{% tab header=\"GitHub Copilot\" %}}\n```\nlima sudo snap install node --classic\nlima sudo npm install -g @github/copilot\nlima copilot\n```\n\nType `/login` in the first session for authentication.\n\nAlternatively, you can set `export GH_TOKEN=...` via:\n```\nlima vi \"/home/${USER}.linux/.bash_profile\"\n```\n\nSee also <https://github.com/github/copilot-cli>.\n{{% /tab %}}\n{{% tab header=\"OpenCode\" %}}\n```\nlima sudo snap install node --classic\nlima sudo npm install -g opencode-ai\nlima opencode\n```\n\nType `/connect` in the first session for authentication.\nUnlike other agents, this step is not necessary for OpenCode.\n\nSee also <https://github.com/anomalyco/opencode>.\n{{% /tab %}}\n\n{{< /tabpane >}}\n\n\n# Syncing Working Directory\n\n | ⚡ Requirement | Lima >= 2.1 |\n |----------------|-------------|\n\nThe `--sync` flag for [`limactl shell`](../reference/limactl_shell) enables bidirectional synchronization of your host working directory with the guest VM. This is particularly useful when running AI agents (like Claude, Copilot, or Gemini) inside VMs to prevent them from accidentally modifying or breaking files on your host system.\n\n### Comparison with `mount`\n\n| Feature | Mounts (`--mount`/`--mount-only`) | Sync (`--sync`) |\n|---|---|---|\n| Purpose | Make host directories visible inside guest (bidirectional if write mode is enabled) | Temporary bidirectional sync of a working directory (guest changes merged back on accept) |\n| Live updates | Yes | No |\n| Safety | Lower (AI agents can access host files directly) | Higher (changes are reviewed before being applied to host) |\n| Requires rsync | No | Yes |\n\n### Usecase - Running AI Code Assistants Safely\n\n1. Create an isolated instance for AI agents which must be started without host mounts for `--sync` to work:\n\n```bash\nlimactl start --mount-none template:default\n```\n\n2. Navigate to your project\n\n```bash\ncd ~/my-project\n```\n\n3. Run an AI agent that modifies code:\n\n```bash\nlimactl shell --sync . default claude \"Add error handling to all functions\"\n```\n\nOr simply shell into the instance and make changes:\n```bash\nlimactl shell --sync . default\n```\n\n4. After running commands, you'll see an interactive prompt:\n\n```\n⚠️ Accept the changes?\n→ Yes\n  No\n  View the changed contents\n```\n\n- **Yes**: Syncs changes back to your host and cleans up guest directory\n- **No**: Discards changes and cleans up guest directory\n- **View the changed contents**: Shows a diff of changes made by the agent\n\n### Requirements\n\n- **rsync** must be installed on both host and guest\n- The host working directory must be at least 4 levels deep (e.g., `/Users/username/projects/myproject`)\n- The instance must not have any host mounts configured (use `--mount-none` when creating)\n\n## See also\n\n- [Config » AI](../config/ai/)\n- [Config » GPU](../config/gpu.md)\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/_index.md",
    "content": "---\ntitle: Containers\nweight: 3\n---\n\nLima was designed to facilitate running containers inside a virtual machine, with automatic\n[filesystem sharing](../../config/mount.md) and  [port forwarding](../../config/port.md).\n\nThe original motivation of Lima was to promote [containerd](./containerd) for macOS users, however,\nthe current version of Lima supports other container engines too, and does not depend on macOS hosts.\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/apptainer/_index.md",
    "content": "---\ntitle: Apptainer\nweight: 90\n---\n\n{{< tabpane text=true >}}\n{{% tab header=\"Rootless\" %}}\n```bash\nlimactl start template:apptainer\nlimactl shell apptainer apptainer run -u -B $HOME:$HOME docker://alpine\n```\n{{% /tab %}}\n{{% tab header=\"Rootful\" %}}\n```bash\nlimactl start template:apptainer-rootful\nlimactl shell apptainer-rootful apptainer run -u -B $HOME:$HOME docker://alpine\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nSee also <https://apptainer.org/docs/user/latest/>.\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/containerd/_index.md",
    "content": "---\ntitle: containerd (Default)\nweight: 1\n---\n\nLima comes with the built-in integration for [containerd](https://containerd.io) and\n[nerdctl](https://github.com/containerd/nerdctl) (contaiNERD CTL):\n\n{{< tabpane text=true >}}\n{{% tab header=\"Rootless\" %}}\n```bash\nlima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n\nor\n\n```bash\nnerdctl.lima run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n\n- If you have installed Lima by [`make install`](../../../installation/source.md), the `nerdctl.lima` command is also available as `nerdctl`.\n- If you have installed Lima by [`brew install lima`](../../../installation/_index.md), you may make an alias (or a symlink) by yourself:\n  `alias nerdctl=nerdctl.lima`\n{{% /tab %}}\n{{% tab header=\"Rootful\" %}}\n```bash\nlimactl start --containerd=system\nlima sudo nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nThe usage of the `nerdctl` command is similar to the `docker` command. See the [Command Reference](https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md).\n\n## Disabling containerd\n\nTo disable containerd, start an instance with `--containerd=none`:\n\n```bash\nlimactl start --containerd=none\n```\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/containerd/advanced/_index.md",
    "content": "---\ntitle: Advanced\nweight: 1\n---\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/containerd/advanced/bypass4netns.md",
    "content": "---\ntitle: Accelerating rootless networking with bypass4netns\nlinkTitle: bypass4netns\nweight: 2\n---\n\n[bypass4netns](https://github.com/rootless-containers/bypass4netns) is an experimental accelerator for rootless networking.\n\nOn macOS hosts, it is highly recommended to use the [vzNAT](../../../../config/network/vmnet.md#vznat) networking in conjunction\nto reduce the overhead of Lima's user-mode networking:\n\n```bash\nlimactl start --network vzNAT\n```\n\nTo enable bypass4netns, the daemon process (`bypass4netnsd`) has to be installed in the VM as follows:\n<!-- TODO: install by default -->\n```bash\nlima containerd-rootless-setuptool.sh install-bypass4netnsd\n```\n\nThen run a container with an annotation `nerdctl/bypass4netns=true`:\n\n```bash\n# 192.168.64.1 is the IP address of the \"bridge100\" interface on the macOS host\nlima nerdctl run --annotation nerdctl/bypass4netns=true alpine \\\n  sh -euc 'apk add iperf3 && iperf3 -c 192.168.64.1'\n```\n\nBenchmark result:\n\n| Mode                          | Throughput     |\n|-------------------------------|----------------|\n| Rootless without bypass4netns | 2.30 Gbits/sec |\n| Rootless with bypass4netns    | 86.0 Gbits/sec |\n| Rootful                       | 90.3 Gbits/sec |\n\n<details>\n<summary>Benchmarking environment</summary>\n<p>\n\n- Lima version: 2.0.0-alpha.2\n  - nerdctl 2.1.6\n  - containerd 2.1.4\n  - bypass4netns 0.4.2\n- Container: Alpine Linux 3.22.2\n  - iperf 3.19.1-r0 (apk)\n- Guest: Ubuntu 25.04\n- Host: macOS 26.0.1\n  - iperf 3.19.1 (Homebrew)\n- Hardware: MacBook Pro 2024 (M4 Max, 128 GiB)\n\n</p>\n</details>\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/containerd/advanced/gomodjail.md",
    "content": "---\ntitle: Enhanced supply chain security with gomodjail\nlinkTitle: gomodjail\nweight: 1\n---\n\n[gomodjail](https://github.com/AkihiroSuda/gomodjail) is an experimental library sandbox for Go modules.\n\ngomodjail imposes syscall restrictions on a specific set of Go modules, so as to mitigate their potential vulnerabilities and supply chain attack vectors.\nA restricted module is hindered to access files and execute commands.\n\ngomodjail can be enabled for nerdctl by using the `nerdctl.gomodjail` binary.\n\n```bash\nlima nerdctl.gomodjail ...\n```\n\nFor the gomodjail policy applied to `nerdctl.gomodjail`, see <https://github.com/containerd/nerdctl/blob/main/go.mod>.\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/containerd/advanced/stargz.md",
    "content": "---\ntitle: Accelerating start-up time with eStargz\nlinkTitle: eStargz\nweight: 3\n---\n\n[eStargz](https://github.com/containerd/stargz-snapshotter) is an OCI-compatible container image format\nthat reduces start-up latency using lazy-pulling technique.\n\nThe support for eStargz is available by default in Lima.\n\n{{% alert title=\"Hint\" color=success %}}\nARM Mac users need to run `limactl start` with `--rosetta` to allow [running AMD64 binaries](../../../../config/multi-arch.md).\nThis is not an architectural limitation of eStargz, however, Rosetta is needed because the example Python image below\nis currently [only available for AMD64](https://github.com/containerd/stargz-snapshotter/issues/2143).\n{{% /alert %}}\n\nWithout eStargz:\n\n```console\n$ time lima nerdctl run --platform=amd64 ghcr.io/stargz-containers/python:3.13-org python3 -c 'print(\"hi\")'\n[...]\nhi\n\nreal\t0m23.767s\nuser\t0m0.025s\nsys\t0m0.020s\n```\n\nWith eStargz:\n\n```console\n$ time lima nerdctl --snapshotter=stargz run --platform=amd64 ghcr.io/stargz-containers/python:3.13-esgz python3 -c 'print(\"hi\")'\n[...]\nhi\n\nreal\t0m13.365s\nuser\t0m0.026s\nsys\t0m0.021s\n```\n\nExamples of eStargz images can be found at\n<https://github.com/containerd/stargz-snapshotter/blob/main/docs/pre-converted-images.md>.\n\nSee also:\n- https://github.com/containerd/stargz-snapshotter\n- https://github.com/containerd/nerdctl/blob/main/docs/stargz.md\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/docker/_index.md",
    "content": "---\ntitle: Docker\nweight: 2\n---\n\n{{< tabpane text=true >}}\n{{% tab header=\"Rootless\" %}}\n```bash\nlimactl start template:docker\nexport DOCKER_HOST=$(limactl list docker --format 'unix://{{.Dir}}/sock/docker.sock')\ndocker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n{{% /tab %}}\n{{% tab header=\"Rootful\" %}}\n```bash\nlimactl start template:docker-rootful\nexport DOCKER_HOST=$(limactl list docker-rootful --format 'unix://{{.Dir}}/sock/docker.sock')\ndocker run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine\n```\n{{% /tab %}}\n{{< /tabpane >}}\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/kubernetes/_index.md",
    "content": "---\ntitle: Kubernetes\nweight: 4\n---\n\n## Single-node\n\n{{< tabpane text=true >}}\n{{% tab header=\"kubeadm\" %}}\n```bash\nlimactl start template:k8s\nexport KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\nkubectl create deployment nginx --image nginx:alpine\nkubectl create service nodeport nginx --node-port=31080 --tcp=80:80\n```\n\nModify [`templates/k8s.yaml`](https://github.com/lima-vm/lima/blob/master/templates/k8s.yaml) to change\nthe kubeadm configuration.\n\nSee also <https://kubernetes.io/docs/reference/setup-tools/kubeadm/>.\n{{% /tab %}}\n{{% tab header=\"k3s\" %}}\n```bash\nlimactl start template:k3s\nexport KUBECONFIG=$(limactl list k3s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\nkubectl create deployment nginx --image nginx:alpine\nkubectl create service nodeport nginx --node-port=31080 --tcp=80:80\n```\n\nSee also <https://docs.k3s.io>.\n{{% /tab %}}\n{{% tab header=\"k0s\" %}}\n```bash\nlimactl start template:k0s\nexport KUBECONFIG=$(limactl list k0s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\nkubectl create deployment nginx --image nginx:alpine\nkubectl create service nodeport nginx --node-port=31080 --tcp=80:80\n```\n\nSee also <https://docs.k0sproject.io/>.\n{{% /tab %}}\n{{% tab header=\"RKE2\" %}}\n```bash\nlimactl start template:experimental/rke2\nexport KUBECONFIG=$(limactl list rke2 --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\nkubectl create deployment nginx --image nginx:alpine\nkubectl create service nodeport nginx --node-port=31080 --tcp=80:80\n```\n\nSee also <https://docs.rke2.io/>.\n{{% /tab %}}\n{{% tab header=\"Usernetes\" %}}\n```bash\nlimactl start template:experimental/u7s\nexport KUBECONFIG=$(limactl list u7s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')\nkubectl create deployment nginx --image nginx:alpine\n# NodePorts are not available by default in the case of Usernetes\nkubectl port-forward deployments/nginx 8080:80\n```\n\nSee also <https://github.com/rootless-containers/usernetes>.\n{{% /tab %}}\n{{< /tabpane >}}\n\n## Multi-node\n\nA multi-node cluster can be created by creating multiple VMs connected via the [`lima:user-v2`](../../../config/network/user-v2.md) network.\n\nThe following templates are designed to support multi-node mode:\n- `k8s` (since Lima v2.0)\n- `k3s` (since Lima v2.1)\n\n{{< tabpane text=true >}}\n{{% tab header=\"Lima v2.0\" %}}\n```bash\nlimactl start --name k8s-0 --network lima:user-v2 template:k8s\nlimactl shell k8s-0 sudo kubeadm token create --print-join-command\n# (The join command printed here)\n```\n\n```bash\nlimactl start --name k8s-1 --network lima:user-v2 template:k8s\nlimactl shell k8s-1 sudo bash -euxc \"kubeadm reset --force ; ip link delete cni0 ; ip link delete flannel.1 ; rm -rf /var/lib/cni /etc/cni\"\nlimactl shell k8s-1 sudo <JOIN_COMMAND_FROM_ABOVE>\n```\n{{% /tab %}}\n{{% tab header=\"Lima v2.1 (k8s)\" %}}\n```bash\nlimactl start --name k8s-0 --network lima:user-v2 template:k8s\nlimactl shell k8s-0 sudo kubeadm token create --print-join-command\n# (The parameters for the start command printed here)\n```\n\n```bash\nlimactl start --name k8s-1 --network lima:user-v2 template:k8s \\\n              --set '.param.url=\"https://<ADDRESS_FROM_ABOVE>\" | .param.token=\"<TOKEN_FROM_ABOVE>\" | \\\n                     .param.discoveryTokenCaCertHash=\"<DISCOVERY_TOKEN_CA_CERT_HASH_FROM_ABOVE>\"'\n```\n{{% /tab %}}\n{{% tab header=\"Lima v2.1 (k3s)\" %}}\n```bash\nlimactl start --name k3s-0 --network lima:user-v2 template:k3s\nprintf \"https://lima-%s.internal:6443\\n\" k3s-0\n# (The url for the start command printed here)\nlimactl shell k3s-0 sudo cat /var/lib/rancher/k3s/server/node-token\n# (The token for the start command printed here)\n```\n\n```bash\nlimactl start --name k3s-1 --network lima:user-v2 template:k3s \\\n              --set '.param.url=\"<URL_FROM_ABOVE>\" | .param.token=\"<TOKEN_FROM_ABOVE>\"'\n```\n{{% /tab %}}\n{{< /tabpane >}}\n"
  },
  {
    "path": "website/content/en/docs/examples/containers/podman/_index.md",
    "content": "---\ntitle: Podman\nweight: 3\n---\n\n{{< tabpane text=true >}}\n{{% tab header=\"Rootless\" %}}\nTo use `podman` command in the VM:\n```bash\nlimactl start template:podman\nlimactl shell podman podman run -d --name nginx -p 127.0.0.1:8080:80 docker.io/library/nginx:alpine\n```\n\nTo use `podman` command on the host:\n```bash\nexport CONTAINER_HOST=$(limactl list podman --format 'unix://{{.Dir}}/sock/podman.sock')\npodman --remote run -d --name nginx -p 127.0.0.1:8080:80 docker.io/library/nginx:alpine\n```\n\nTo use `docker` command on the host:\n```bash\nexport DOCKER_HOST=$(limactl list podman --format 'unix://{{.Dir}}/sock/podman.sock')\ndocker run -d --name nginx -p 127.0.0.1:8080:80 docker.io/library/nginx:alpine\n```\n{{% /tab %}}\n{{% tab header=\"Rootful\" %}}\nTo use `podman` command in the VM:\n```bash\nlimactl start template:podman-rootful\nlimactl shell podman-rootful sudo podman run -d --name nginx -p 127.0.0.1:8080:80 docker.io/library/nginx:alpine\n```\n\nTo use `podman` command on the host:\n```bash\nexport CONTAINER_HOST=$(limactl list podman-rootful --format 'unix://{{.Dir}}/sock/podman.sock')\npodman --remote run -d --name nginx -p 127.0.0.1:8080:80 docker.io/library/nginx:alpine\n```\n\nTo use `docker` command on the host:\n```bash\nexport DOCKER_HOST=$(limactl list podman-rootful --format 'unix://{{.Dir}}/sock/podman.sock')\ndocker run -d --name nginx -p 127.0.0.1:8080:80 docker.io/library/nginx:alpine\n```\n{{% /tab %}}\n{{< /tabpane >}}\n"
  },
  {
    "path": "website/content/en/docs/examples/gha.md",
    "content": "---\ntitle: GitHub Actions\nweight: 10\n---\n\n## Running Lima on GitHub Actions\n\nOn GitHub Actions, Lima is useful for:\n- Running commands on non-Ubuntu operating systems (e.g., Fedora for testing SELinux)\n- Emulating multiple hosts\n\nWhile these tasks can be partially accomplished with containers like Docker, those containers still rely on the Ubuntu host's kernel and cannot utilize features missing in Ubuntu, such as SELinux.\n\nIn contrast, Lima runs virtual machines that do not depend on the Ubuntu host's kernel.\n\nThe following GitHub Actions workflow illustrates how to run multiple instances of Fedora using Lima.\nThe instances are connected by the `user-v2` network.\n\n```yaml\nname: Fedora\n\non:\n  workflow_dispatch:\n  pull_request:\n\njobs:\n  fedora:\n    runs-on: ubuntu-24.04\n    steps:\n    - name: Check out code\n      uses: actions/checkout@v4\n\n    - name: \"Set up Lima\"\n      uses: lima-vm/lima-actions/setup@v1\n      id: lima-actions-setup\n\n    - name: \"Cache ~/.cache/lima\"\n      uses: actions/cache@v4\n      with:\n        path: ~/.cache/lima\n        key: lima-${{ steps.lima-actions-setup.outputs.version }}\n\n    - name: \"Start an instance of Fedora\"\n      run: |\n        set -eux\n        limactl start --name=default --cpus=1 --memory=1 --network=lima:user-v2 template:fedora\n        lima sudo dnf install -y httpd\n        lima sudo systemctl enable --now httpd\n\n    - name: \"Start another instance of Fedora\"\n      run: |\n        set -eux\n        limactl start --name=another --cpus=1 --memory=1 --network=lima:user-v2 template:fedora\n        limactl shell another curl http://lima-default.internal\n```\n\nSee also <https://github.com/lima-vm/lima-actions>.\n\n### Plain mode\n\nThe `--plain` mode is useful when you want the VM instance to be as close as possible to a physical host:\n\n```yaml\n    - name: \"Start Fedora\"\n      # --plain is set to disable file sharing, port forwarding, built-in containerd, etc.\n      run: limactl start --plain --name=default --cpus=1 --memory=1 --network=lima:user-v2 template:fedora\n\n    - name: \"Initialize Fedora\"\n      # plain old rsync and ssh are used for the initialization of the guest,\n      # so that people who are not familiar with Lima can understand the initialization steps.\n      run: |\n        set -eux -o pipefail\n        # Sync the current directory to /tmp/repo in the guest\n        rsync -a -e ssh . lima-default:/tmp/repo\n        # Install packages\n        ssh lima-default sudo dnf install -y httpd\n```\n\n### Full examples\nKubernetes:\n- [kind, for running tests with SELinux using Fedora](https://github.com/kubernetes-sigs/kind/blob/v0.30.0/.github/workflows/vm.yaml#L46-L71)\n- [Usernetes, for running tests with multiple nodes](https://github.com/rootless-containers/usernetes/blob/gen2-v20250828.0/.github/workflows/reusable-multi-node.yaml#L52-L61)\n\nContainer engines:\n- [Docker (Moby), for running tests with cgroup v1 using Oracle Linux 8 ](https://github.com/moby/moby/blob/master/.github/workflows/.vm.yml)\n- [nerdctl, for running tests with cgroup v1 using AlmaLinux 8](https://github.com/containerd/nerdctl/blob/v2.1.6/.github/workflows/job-test-in-lima.yml)\n\nContainer runtimes:\n- [runc, for running tests with SELinux using Fedora](https://github.com/opencontainers/runc/blob/v1.3.2/.github/workflows/test.yml#L182-L202)\n- [opencontainers/selinux, for running tests with SELinux using AlmaLinux, CentOS Stream, Fedora, and openSUSE](https://github.com/opencontainers/selinux/blob/v1.12.0/.github/workflows/validate.yml#L106-L133)\n- [youki, for running tests with cgroup v1 using AlmaLinux 8](https://github.com/youki-dev/youki/blob/v0.5.5/.github/workflows/e2e.yaml#L206-L227)\n\nOthers:\n- [uutils coreutils, for running tests with SELinux using Fedora](https://github.com/uutils/coreutils/blob/0.2.2/.github/workflows/GnuTests.yml#L190-L225)\n"
  },
  {
    "path": "website/content/en/docs/examples/vscode.md",
    "content": "---\ntitle: Visual Studio Code\nweight: 9\n---\n\n## Securing Visual Studio Code with Lima\n\nLima helps securing the development environment by running it inside a VM.\nNotably, this prevents AI agents, such as [GitHub Copilot in VS Code](https://code.visualstudio.com/docs/copilot/overview), from directly executing untrusted commands on the host.\n\n1. Start a Lima instance. If you use GitHub Copilot, consider disabling mounts by passing the `--mount-none` flag to prevent Copilot from accessing host files:\n\n```bash\nlimactl start --mount-none\n```\n\n2. Add the following line to `~/.ssh/config`:\n\n```\nInclude ~/.lima/*/ssh.config\n```\n\n3. Open the Remote Explorer in the Visual Studio Code sidebar and select `lima-<INSTANCE>` from the SSH remote list.\n\n![](/images/vscode-remote-explorer.png)\n\n{{% alert title=\"Hint\" color=success %}}\nIf the Remote Explorer is missing in the sidebar, install the following extensions:\n- [Remote Explorer](https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-explorer)\n- [Remote - SSH](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh)\n\nSee also the [documentation](https://code.visualstudio.com/docs/remote/ssh) of Visual Studio Code for further troubleshooting.\n{{% /alert %}}\n\n4. Set up the workspace by clicking `Clone Git Repository...` on the `Welcome` screen, or copy the project directory with `limactl cp`:\n\n```bash\nlimactl cp -r DIR default:~/\n```\n"
  },
  {
    "path": "website/content/en/docs/faq/_index.md",
    "content": "---\ntitle: FAQs\nweight: 6\n---\n<!-- doctoc: https://github.com/thlorenz/doctoc -->\n\n<!-- START doctoc generated TOC please keep comment here to allow auto update -->\n<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->\n\n\n- [Generic](#generic)\n  - [\"How does Lima work?\"](#how-does-lima-work)\n  - [\"What's my login password?\"](#whats-my-login-password)\n  - [\"Does Lima work on ARM Mac?\"](#does-lima-work-on-arm-mac)\n  - [\"Can I run non-Ubuntu guests?\"](#can-i-run-non-ubuntu-guests)\n  - [\"Can I run other container engines such as Docker and Podman? What about Kubernetes?\"](#can-i-run-other-container-engines-such-as-docker-and-podman-what-about-kubernetes)\n  - [\"Can I run Lima with a remote Linux machine?\"](#can-i-run-lima-with-a-remote-linux-machine)\n  - [\"Advantages compared to Docker for Mac?\"](#advantages-compared-to-docker-for-mac)\n- [Configuration](#configuration)\n  - [\"Is it possible to disable mounts, port forwarding, containerd, etc. ?\"](#is-it-possible-to-disable-mounts-port-forwarding-containerd-etc-)\n- [QEMU](#qemu)\n  - [\"QEMU crashes with `HV_ERROR`\"](#qemu-crashes-with-hv_error)\n  - [\"QEMU is slow\"](#qemu-is-slow)\n  - [error \"killed -9\"](#error-killed--9)\n  - [\"QEMU crashes with `vmx_write_mem: mmu_gva_to_gpa XXXXXXXXXXXXXXXX failed`\"](#qemu-crashes-with-vmx_write_mem-mmu_gva_to_gpa-xxxxxxxxxxxxxxxx-failed)\n- [VZ](#vz)\n  - [\"Lima gets stuck at `Installing rosetta...`\"](#lima-gets-stuck-at-installing-rosetta)\n- [Networking](#networking)\n  - [\"Cannot access the guest IP 192.168.5.15 from the host\"](#cannot-access-the-guest-ip-192168515-from-the-host)\n  - [\"Ping shows duplicate packets and massive response times\"](#ping-shows-duplicate-packets-and-massive-response-times)\n  - [\"IP address is not assigned for vmnet networks\"](#ip-address-is-not-assigned-for-vmnet-networks)\n- [Filesystem sharing](#filesystem-sharing)\n  - [\"Filesystem is slow\"](#filesystem-is-slow)\n  - [\"Filesystem is not writable\"](#filesystem-is-not-writable)\n  - [\"Filesystem is unmounted after upgrading Lima to v1.0\"](#filesystem-is-unmounted-after-upgrading-lima-to-v10)\n- [External projects](#external-projects)\n  - [\"I am using Rancher Desktop. How to deal with the underlying Lima?\"](#i-am-using-rancher-desktop-how-to-deal-with-the-underlying-lima)\n- [\"Hints for debugging other problems?\"](#hints-for-debugging-other-problems)\n\n<!-- END doctoc generated TOC please keep comment here to allow auto update -->\n\n### Generic\n#### \"How does Lima work?\"\n\n- Hypervisor: [QEMU (default on Linux), or Virtualization.framework (default on macOS)](../config/vmtype/)\n- Filesystem sharing: [Reverse SSHFS, virtio-9p-pci aka virtfs (default for QEMU), or virtiofs (default for Virtualization.framework)](../config/mount/)\n- Port forwarding: [`ssh -L`](../config/port), automated by watching `/proc/net/tcp` and `iptables` events in the guest\n\n#### \"What's my login password?\"\nFor Linux and FreeBSD guests, the password is disabled and locked by default.\nYou have to use `limactl shell <INSTANCE>` (or `lima`) instead of the video console to open a shell.\n\n{{% fixlinks %}}\nSee also [`Usage » SSH`]({{< ref \"/docs/usage/ssh\" >}}) for SSH with publickey authentication.\n{{% /fixlinks %}}\n\nFor macOS guests, the password is randomly generated and stored as `~/password` in the guest.\n\n#### \"Does Lima work on ARM Mac?\"\nYes\n\n#### \"Can I run non-Ubuntu guests?\"\nAlmaLinux, Alpine, Arch Linux, Debian, Fedora, openSUSE, Oracle Linux, and Rocky are also known to work.\n{{% fixlinks %}}\nSee [`./templates/`](./templates/).\n{{% /fixlinks %}}\n\nStarting with Lima v2.1, non-Linux guests such as [macOS guests](../usage/guests/macos.md) are experimentally supported too.\n\nAn image for Linux guests has to satisfy the following requirements:\n- systemd or OpenRC\n- cloud-init\n- The following binaries to be preinstalled:\n    - `sudo`\n- The following binaries to be preinstalled, or installable via the package manager:\n    - `sshfs`\n    - `newuidmap` and `newgidmap`\n- `apt-get`, `dnf`, `apk`, `pacman`, or `zypper` (if you want to contribute support for another package manager, run `git grep apt-get` to find out where to modify)\n\n#### \"Can I run other container engines such as Docker and Podman? What about Kubernetes?\"\n{{% fixlinks %}}\nYes, any container engine should work with Lima.\n\nContainer runtime templates:\n- [`./templates/docker.yaml`](./templates/docker.yaml): Docker\n- [`./templates/podman.yaml`](./templates/podman.yaml): Podman\n- [`./templates/apptainer.yaml`](./templates/apptainer.yaml): Apptainer\n\nContainer image builder templates:\n- [`./templates/buildkit.yaml`](./templates/buildkit.yaml): BuildKit\n\nContainer orchestrator templates:\n- [`./templates/k3s.yaml`](./templates/k3s.yaml): Kubernetes (k3s)\n- [`./templates/k8s.yaml`](./templates/k8s.yaml): Kubernetes (kubeadm)\n\nThe default Ubuntu image also contains LXD. Run `lima sudo lxc init` to set up LXD.\n\nSee also third party containerd projects based on Lima:\n- [Rancher Desktop](https://rancherdesktop.io/): Kubernetes and container management to the desktop\n- [Colima](https://github.com/abiosoft/colima): Docker (and Kubernetes) on macOS with minimal setup\n\nOr third party \"[containers](https://github.com/containers)\" projects compatible with Lima:\n- [Podman Desktop](https://podman-desktop.io/): Containers and Kubernetes for application developers\n\n{{% /fixlinks %}}\n\n#### \"Can I run Lima with a remote Linux machine?\"\nLima itself does not support connecting to a remote Linux machine, but [sshocker](https://github.com/lima-vm/sshocker),\nthe predecessor or Lima, provides similar features for remote Linux machines.\n\ne.g., run `sshocker -v /Users/foo:/home/foo/mnt -p 8080:80 <USER>@<HOST>` to expose `/Users/foo` to the remote machine as `/home/foo/mnt`,\nand forward `localhost:8080` to the port 80 of the remote machine.\n\n#### \"Advantages compared to Docker for Mac?\"\nLima is free software (Apache License 2.0), while Docker for Mac is not.\n\n### Configuration\n#### \"Is it possible to disable mounts, port forwarding, containerd, etc. ?\"\n\nYes, since Lima v0.18:\n\n{{< tabpane text=true >}}\n{{% tab header=\"CLI\" %}}\n```bash\nlimactl start --plain\n```\n{{% /tab %}}\n{{% tab header=\"YAML\" %}}\n```yaml\nplain: true\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\n\nWhen the \"plain\" mode is enabled:\n- the YAML properties for mounts, port forwarding, containerd, etc. will be ignored\n- guest agent will not be running\n- dependency packages like sshfs will not be installed into the VM\n\nUser-specified provisioning scripts will be still executed.\n\n### QEMU\n#### \"QEMU crashes with `HV_ERROR`\"\nIf you have installed QEMU v6.0.0 or later on macOS 11 via homebrew, your QEMU binary should have been already automatically signed to enable HVF acceleration.\n\nHowever, if you see `HV_ERROR`, you might need to sign the binary manually.\n\n```bash\ncat >entitlements.xml <<EOF\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>com.apple.security.hypervisor</key>\n    <true/>\n</dict>\n</plist>\nEOF\n\ncodesign -s - --entitlements entitlements.xml --force /usr/local/bin/qemu-system-x86_64\n```\n\nNote: **Only** on macOS versions **before** 10.15.7 you might need to add this entitlement in addition:\n\n```xml\n    <key>com.apple.vm.hypervisor</key>\n    <true/>\n```\n\n#### \"QEMU is slow\"\n{{% fixlinks %}}\n- Make sure that HVF is enabled with `com.apple.security.hypervisor` entitlement. See [\"QEMU crashes with `HV_ERROR`\"](#qemu-crashes-with-hv_error).\n- Emulating non-native machines is slow by design. See [`Configuration guide » Intel-on-ARM and ARM-on-Intel`]({{< ref \"/docs/config/multi-arch\" >}}) for a workaround.\n{{% /fixlinks %}}\n\n#### error \"killed -9\"\n- make sure qemu is codesigned, See [\"QEMU crashes with `HV_ERROR`\"](#qemu-crashes-with-hv_error).\n- if you are on macOS 10.15.7 or 11.0 or later make sure the entitlement `com.apple.vm.hypervisor` is **not** added. It only works on older macOS versions. You can clear the codesigning with `codesign --remove-signature /usr/local/bin/qemu-system-x86_64` and [start over](../installation/).\n\n#### \"QEMU crashes with `vmx_write_mem: mmu_gva_to_gpa XXXXXXXXXXXXXXXX failed`\"\nThis error is known to happen when running an image of RHEL8-compatible distribution such as Rocky Linux 8.x on Intel Mac.\nA workaround is to set environment variable `QEMU_SYSTEM_X86_64=\"qemu-system-x86_64 -cpu Haswell-v4\"`.\n\n<https://bugs.launchpad.net/qemu/+bug/1838390>\n\n### VZ\n#### \"Lima gets stuck at `Installing rosetta...`\"\n\nTry `softwareupdate --install-rosetta` from a terminal.\n\n### Networking\n#### \"Cannot access the guest IP 192.168.5.15 from the host\"\n{{% fixlinks %}}\nThe default guest IP 192.168.5.15 is not accessible from the host and other guests.\n\nTo add another IP address that is accessible from the host and other virtual machines, enable [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) (since Lima v0.12).\n\nSee [`Configuration guide » Network`]({{< ref \"/docs/config/network\" >}}).\n{{% /fixlinks %}}\n\n#### \"Ping shows duplicate packets and massive response times\"\n\nLima uses QEMU's SLIRP networking which does not support `ping` out of the box:\n\n```console\n$ ping google.com\nPING google.com (172.217.165.14): 56 data bytes\n64 bytes from 172.217.165.14: seq=0 ttl=42 time=2395159.646 ms\n64 bytes from 172.217.165.14: seq=0 ttl=42 time=2396160.798 ms (DUP!)\n```\n\nFor more details, see [Documentation/Networking](https://wiki.qemu.org/Documentation/Networking#User_Networking_.28SLIRP.29).\n\n#### \"IP address is not assigned for vmnet networks\"\nTry the following commands:\n```bash\nsudo /usr/libexec/ApplicationFirewall/socketfilterfw --add /usr/libexec/bootpd\nsudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblock /usr/libexec/bootpd\n```\n\n### Filesystem sharing\n#### \"Filesystem is slow\"\n{{% fixlinks %}}\nTry virtiofs. See [`Configuration guide » Filesystem mounts`]({{< ref \"/docs/config/mount\" >}})\n{{% /fixlinks %}}\n\n#### \"Filesystem is not writable\"\nThe home directory is mounted as read-only by default.\nTo enable writing, specify `writable: true` in the YAML:\n\n```yaml\nmounts:\n- location: \"~\"\n  writable: true\n```\n\nRun `limactl edit <INSTANCE>` to open the YAML editor for an existing instance.\n\n#### \"Filesystem is unmounted after upgrading Lima to v1.0\"\n\nLima v1.0 changed the default mount type for QEMU from `reverse-sshfs` to `9p`.\n\nThe `9p` mount type is known to be incompatible with the following guest operating systems:\n- AlmaLinux, CentOS Stream, Oracle Linux, and RockyLinux\n- Debian GNU/Linux\n- openSUSE\n\nA new instance of these OS still use `reverse-sshfs` by default.\nHowever, an existing instance created with a previous version of Lima may potentially need\nrunning the following command (usually not needed):\n\n```\nlimactl edit --mount-type=reverse-sshfs <NAME>\n```\n\nUbuntu users are not affected by this issue.\n\n### External projects\n#### \"I am using Rancher Desktop. How to deal with the underlying Lima?\"\n\nRancher Desktop includes the `rdctl` tool (installed in `~/.rd/bin/rdctl`) that provides shell access via `rdctl shell`.\n\nIt is not recommended to directly interact with the Rancher Desktop VM via `limactl`.\n\nIf you need to create an `override.yaml` file, its location should be:\n\n* macOS: `$HOME/Library/Application Support/rancher-desktop/lima/_config/override.yaml`\n* Linux: `$HOME/.local/share/rancher-desktop/lima/_config/override.yaml`\n\n### \"Hints for debugging other problems?\"\n- Inspect logs:\n    - `limactl --debug start`\n    - `$HOME/.lima/<INSTANCE>/serial.log`\n    - `/var/log/cloud-init-output.log` (inside the guest)\n    - `/var/log/cloud-init.log` (inside the guest)\n- Make sure that you aren't mixing up tabs and spaces in the YAML.\n"
  },
  {
    "path": "website/content/en/docs/faq/colima.md",
    "content": "---\ntitle: Colima (third-party project)\nweight: 0\n---\n\n## \"How does Lima relate to Colima?\"\n\n[Colima](https://github.com/abiosoft/colima) is a third-party project\nthat wraps Lima to provide an alternative user experience for launching containers.\n\nThe key difference is that Colima launches Docker by default,\nwhile Lima launches containerd by default.\n\n| Container            | Lima                            | Colima                              |\n|----------------------|---------------------------------|-------------------------------------|\n| containerd           | `limactl start`                 | `colima start --runtime=containerd` |\n| Docker               | `limactl start template:docker` | `colima start`                      |\n| Podman               | `limactl start template:podman` | -                                   |\n| Kubernetes (k3s)     | `limactl start template:k3s`    | `colima start --kubernetes`         |\n| Kubernetes (kubeadm) | `limactl start template:k8s`    | -                                   |\n\nThe `colima` CLI is similar to the `limactl` CLI, but there are subtle differences:\n\n| Configuration      | Lima                                       | Colima                            |\n|--------------------|--------------------------------------------|-----------------------------------|\n| CPUs               | `limactl start --cpus=4`                   | `colima start --cpu=4`            |\n| Reverse SSHFS      | `limactl start --mount-type=reverse-sshfs` | `colima start --mount-type=sshfs` |\n| Rosetta            | `limactl start --rosetta`                  | `colima start --vz-rosetta`       |\n| Access to VM by IP | `limactl start --network=lima:shared`      | `colima start --network-address`  |\n"
  },
  {
    "path": "website/content/en/docs/installation/_index.md",
    "content": "---\ntitle: Installation\nweight: 1\n---\nSupported host OS:\n- macOS (the latest version is recommended)\n- Linux\n- NetBSD (untested)\n- DragonFlyBSD (untested)\n- Windows (untested)\n\nPrerequisite:\n- QEMU (Required, only if [QEMU]({{< ref \"/docs/config/vmtype#qemu\" >}}) driver is used)\n\n{{< tabpane text=true >}}\n\n{{% tab header=\"Homebrew\" %}}\n```bash\nbrew install lima\n```\n\nHint: specify `--HEAD` to install the HEAD (master) version.\nThe HEAD version provides early access to the latest features and improvements before they are officially released.\n\nHomebrew formula is available [here](https://github.com/Homebrew/homebrew-core/blob/master/Formula/l/lima.rb).\nSupports macOS and Linux.\n{{% /tab %}}\n\n{{% tab header=\"MacPorts\" %}}\n```bash\nsudo port install lima\n```\n\nPort: <https://ports.macports.org/port/lima/>\n{{% /tab %}}\n\n{{% tab header=\"Nix\" %}}\n```bash\nnix-env -i lima\n```\n\nNix file: <https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/li/lima/package.nix>\n{{% /tab %}}\n\n{{% tab header=\"Binary\" %}}\nDownload the binary archive of Lima from <https://github.com/lima-vm/lima/releases>,\nand extract it under `/usr/local` (or somewhere else).\n\n```bash\nVERSION=$(curl -fsSL https://api.github.com/repos/lima-vm/lima/releases/latest | jq -r .tag_name)\ncurl -fsSL \"https://github.com/lima-vm/lima/releases/download/${VERSION}/lima-${VERSION:1}-$(uname -s)-$(uname -m).tar.gz\" | tar Cxzvm /usr/local\n\n# For Lima v1.1 onward\ncurl -fsSL \"https://github.com/lima-vm/lima/releases/download/${VERSION}/lima-additional-guestagents-${VERSION:1}-$(uname -s)-$(uname -m).tar.gz\" | tar Cxzvm /usr/local\n```\n{{% /tab %}}\n{{< /tabpane >}}\n"
  },
  {
    "path": "website/content/en/docs/installation/source.md",
    "content": "---\ntitle: Source Installation\nweight: 30\n---\n\n## Installing from Source\n\nIf you prefer to build Lima from source, follow these steps:\n\n### Prerequisites\nEnsure you have the following dependencies installed:\n- `git`\n- `go`\n- `make`\n\n### Build and Install\nRun the following commands:\n\n```bash\ngit clone https://github.com/lima-vm/lima\ncd lima\nmake\nsudo make install\n```\n\n> **Note:** `sudo make install` is required unless you have write permissions for `/usr/local`. Otherwise, installation may fail.\n\n### Alternative Installation (Without Sudo)\nIf you prefer installing Lima in your home directory, configure the `PREFIX` and `PATH` as follows:\n\n```bash\nmake PREFIX=$HOME/.local install\nexport PATH=$HOME/.local/bin:$PATH\n```\n\n### Building External Drivers\n\n> **⚠️ Building drivers as external mode is experimental**\n\nLima supports building drivers as external executables. For detailed information on creating and building external drivers, see the [Virtual Machine Drivers](../../dev/drivers) guide.\n\n## Packaging Lima for Distribution\nAfter building Lima from source, you may want to package it for installation on other machines:\n\n1. The package for the core component and the native guest agent:\n```bash\nmake clean native\ncd _output\ntar czf lima-package.tar.gz *\n```\n\n2. The package for the additional guest agents:\n```\nmake clean additional-guestagents\ncd _output\ntar czf lima-additional-guestagents-package.tar.gz *\n```\n\nThese packages can then be transferred and installed on the target system.\n"
  },
  {
    "path": "website/content/en/docs/reference/_index.md",
    "content": "---\ntitle: Command Reference\nweight: 999\n---\n"
  },
  {
    "path": "website/content/en/docs/releases/_index.md",
    "content": "---\ntitle: Releases\nweight: 300\n---\n\nSee <https://github.com/lima-vm/lima/releases> for the latest release.\n\n## Lifecycle\n\n| Release | Begin      | End                              |\n|---------|------------|----------------------------------|\n| v2.x    | 2025-11-06 | Undecided  (v3 Begin + 3 months) |\n| v1.x    | 2024-11-06 | 2026-02-06 (v2 Begin + 3 months) |\n| v0.x    | 2021-05-14 | 2024-11-06 (v1 Begin + 0 days)   |\n\nSince Lima v1.x, each major release receives security updates and critical bug fixes until three months after the next major release.\n"
  },
  {
    "path": "website/content/en/docs/releases/breaking.md",
    "content": "---\ntitle: Breaking changes\nweight: 20\n---\n\n## [v2.0.0](https://github.com/lima-vm/lima/releases/tag/v2.0.0)\n- `/tmp/lima` is no longer mounted by default.\n- SSH port is no longer hard-coded to 60022 for the \"default\" instance.\n- Port forwarding with `sudo nerdctl run -p` no longer works with nerdctl prior to v2.1.6.\n- The default of `guestIPMustBeZero` was changed from `false` to `true` when `guestIP` is `0.0.0.0`.\n\n## [v1.1.0](https://github.com/lima-vm/lima/releases/tag/v1.1.0)\n- The `lima-additional-guestagent` package was split from the main `lima` package.\n\n## [v1.0.0](https://github.com/lima-vm/lima/releases/tag/v1.0.0)\n- The default [VM type](../config/vmtype/_index.md) was changed from `qemu` to `vz` on macOS hosts with the support for `vz`.\n- The default [mount type](../config/mount.md) was changed from `reverse-sshfs` to `virtiofs` for `vz`, `9p` for `qemu`.\n- `socket_vmnet` binary has to be strictly owned by root.\n- The default value of `ssh.loadDotSSHPubKeys` was changed from `true` to `false`.\n- Several templates were removed or renamed.\n\n## [v0.22.0](https://github.com/lima-vm/lima/releases/tag/v0.22.0)\n- Support for `vde_vmnet` was dropped in favor of `socket_vmnet`.\n\n## [v0.3.0](https://github.com/lima-vm/lima/releases/tag/v0.3.0)\n- `limactl start` no longer starts a VM in the foreground.\n\nSee also <https://github.com/lima-vm/lima/releases>.\n"
  },
  {
    "path": "website/content/en/docs/releases/deprecated.md",
    "content": "---\ntitle: Deprecated features\nweight: 10\n---\n\nThe following features are deprecated:\n\n- `limactl show-ssh` command: deprecated in v0.18.0 (Use `ssh -F ~/.lima/default/ssh.config lima-default` instead)\n- Ansible provisioning mode: deprecated in Lima v1.1.0 (Use `ansible-playbook playbook.yaml` after the start instead)\n- `limactl --yes` flag: deprecated in Lima v2.0.0 (Use `limactl (clone|rename|edit|shell) --start` instead)\n- Environment variable `LIMA_SSH_OVER_VSOCK`: deprecated in Lima v2.0.2 (Use the YAML property `.ssh.overVsock`)\n- YAML property `cpuType`: deprecated in Lima v2.0.0 (Use `vmOpts.qemu.cpuType` instead)\n- YAML property `rosetta`: deprecated in Lima v2.0.0 (Use `vmOpts.vz.rosetta` instead)\n\n## Removed features\n- YAML property `network`: deprecated in [Lima v0.7.0](https://github.com/lima-vm/lima/commit/07e68230e70b21108d2db3ca5e0efd0e43842fbd)\n  and removed in Lima v0.14.0, in favor of `networks`\n- YAML property `useHostResolver`: deprecated in [Lima v0.8.1](https://github.com/lima-vm/lima/commit/eaeee31b0496174363c55da732c855ae21e9ad97)\n  and removed in Lima v0.14.0,in favor of `hostResolver.enabled`\n- VDE support, including VNL and `vde_vmnet`: deprecated in [Lima v0.12.0](https://github.com/lima-vm/lima/pull/851/commits/b5e0d5abd0fb2f74b7ddf8faea7a855b5a14ceda)\n  and removed in Lima v0.22.0, in favor of `socket_vmnet`\n- CentOS 7 guest support: deprecated in Lima v0.9.2 and removed in Lima v0.23.0, in favor of CentOS Stream, AlmaLinux, and Rocky Linux.\n\n## Undeprecated features\n- Loading non-strict YAMLs (i.e., YAMLs with unknown properties): once deprecated in Lima v0.12.0, but undeprecated in Lima v1.0.4\n"
  },
  {
    "path": "website/content/en/docs/releases/experimental.md",
    "content": "---\ntitle: Experimental features\nweight: 10\n---\n\n\nThe following features are experimental and subject to change:\n\n- `mountType: virtiofs` on Linux\n- `vmType: wsl2` and relevant configurations (`mountType: wsl2`)\n- `arch`: `riscv64`, `armv7l`, `s390x`, and `ppc64le`\n- `video.display: vnc` and relevant configuration (`video.vnc.display`)\n- `audio.device`\n- `mountInotify: true`\n- `External drivers`: building and using drivers as separate executables (see [Virtual Machine Drivers](../dev/drivers))\n- [`vmType: krunkit`](../config/vmtype/krunkit.md)\n- [`github` URL scheme](../templates/github.md): referencing templates on GitHub with `github:` URLs\n- [macOS guests](../usage/guests/macos.md)\n- [FreeBSD guests](../usage/guests/freebsd.md)\n\nThe following commands are experimental and subject to change:\n\n- `limactl snapshot *`\n- `limactl tunnel`\n- `limactl template *`\n- `limactl mcp *`\n\n## Graduated\n\nThe following features were experimental in the past:\n\n### Until v1.0\n\n- `mountType: 9p`\n- `vmType: vz` and relevant configurations (`mountType: virtiofs`, `rosetta`, `[]networks.vzNAT`)\n- `mode: user-v2` in `networks.yml` and relevant configuration in `lima.yaml`\n"
  },
  {
    "path": "website/content/en/docs/security/_index.md",
    "content": "---\ntitle: Security\nweight: 350\n---\n\n## Security model\n\nSee <https://github.com/cncf/tag-security/blob/main/community/assessments/projects/lima/self-assessment.md>.\n\n## Reporting vulnerabilities\n\nSee <https://github.com/lima-vm/.github/blob/main/SECURITY.md>.\n\n## Past vulnerabilities\n\nSee <https://github.com/lima-vm/lima/security>.\n"
  },
  {
    "path": "website/content/en/docs/talks/_index.md",
    "content": "---\ntitle: Talks\nweight: 450\n---\n\n## 2022\n### KubeCon + CloudNativeCon Europe 2022\n\n[Akihiro Suda](https://github.com/AkihiroSuda) and [Jan Dubois](https://github.com/jandubois) presented \"Running containerd and k3s on macOS\" about Lima.\n\n> It has been very hard to use Mac for developing containerized apps. A typical way is to use Docker for Mac, but it is not FLOSS. Another option is to install Docker and/or Kubernetes into VirtualBox, often via minikube, but it doesn't propagate localhost ports, and VirtualBox also doesn't support the ARM architecture. This session will show how to run containerd and k3s on macOS, using Lima and Rancher Desktop. Lima wraps QEMU in a simple CLI, with neat features for container users, such as filesystem sharing and automatic localhost port forwarding, as well as DNS and proxy propagation for enterprise networks. Rancher Desktop wraps Lima with k3s integration and GUI.\n\nRead the [slides](https://static.sched.com/hosted_files/kccnceu2022/5f/lima.pdf) or watch the [video](https://www.youtube.com/watch?v=g5GCsbjkzRM).\n\n### CNCF TAG-Runtime Meeting 2022-10-06\n\n[Akihiro Suda](https://github.com/AkihiroSuda) presented Lima in [the CNCF TAG-Runtime Meeting](https://github.com/cncf/tag-runtime).\n\nRead the [slides](https://www.slideshare.net/AkihiroSuda/cncf-tagruntime-20221006-limapdf).\n\n## 2023\n### DevOps Toolkit 2023-02-09\n\n[Anders Björklund](https://github.com/afbjorklund) presented Lima in The DevOps Toolkit Series.\n\nWatch the [video](https://www.youtube.com/watch?v=GDInFocQJTU).\n\n### KubeCon + CloudNativeCon Europe 2023\n\n[Akihiro Suda](https://github.com/AkihiroSuda) presented Lima in the CNCF project pavilion.\n\nRead the [slides](https://www.slideshare.net/AkihiroSuda/kubeconeu2023-lima-pavilion).\n\n### KubeCon + CloudNativeCon North America 2023\n\n[Akihiro Suda](https://github.com/AkihiroSuda) presented Lima in the CNCF project pavilion.\n\nRead the [slides](https://github.com/AkihiroSuda/AkihiroSuda/blob/master/slides/2023/20231107%20%5BKubeCon%20NA%20Pavilion%5D%20Lima.pdf).\n\n## 2024\n### KubeCon + CloudNativeCon Europe 2024\n\n[Akihiro Suda](https://github.com/AkihiroSuda) presented Lima in the CNCF project pavilion.\n\nRead the [slides](https://github.com/AkihiroSuda/AkihiroSuda/blob/master/slides/2024/20240321%20%5BKubeCon%20EU%20Pavilion%5D%20Lima.pdf).\n\n### Open Source Summit Europe 2024\n\n[Harsh Thakur](https://github.com/RealHarshThakur) and [Kunal Verma](https://github.com/verma-kunal) presented\na talk [\"Container Development Client for Reproducible Artifacts\"](https://osseu2024.sched.com/event/1ej5z/container-development-client-for-reproducible-artifacts-harsh-thakur-civo-kunal-verma-kubesimplify).\n\n> The container landscape is undergoing a transformation with innovative snapshotters and image formats. But how do we leverage these advancements to empower developers with more cost-effective solutions? Open source projects like Colima and Finch are paving the way for this new wave of container tooling. While they didn't fully meet our specific needs, they provided valuable building blocks for our solution. In our pursuit of achieving truly reproducible builds, we explored Nix and Nix-snapshotter. Combining the power of Nix, a robust package manager, with Nix-snapshotter's efficient caching capabilities within containerd, and the cross-platform functionality of Lima for managing Linux VMs, we've unlocked a new level of consistency and reproducibility for developers' software builds. This project empowers developers with unprecedented flexibility and control over their containerized environments. By embracing these innovations, we can expect reduced CI resource consumption, faster build times, and simplified security and compliance.\n\nWatch the [video (13:40-)](https://www.youtube.com/watch?v=O4qIoMSv674&t=820s).\n\n### KubeCon + CloudNativeCon North America 2024\n\n[Akihiro Suda](https://github.com/AkihiroSuda) presented Lima in the CNCF project pavilion.\n\nRead the [slides](https://github.com/AkihiroSuda/AkihiroSuda/blob/master/slides/2024/20241115%20%5BKubeCon%20NA%20Pavilion%5D%20Lima.pdf).\n\nHe also mentioned Lima in the presentation \"What’s Going on in the Containerd Neighborhood?\" in the context of case study of nerdctl.\n\nRead the [slides](https://static.sched.com/hosted_files/kccncna2024/74/KCCNC-SLC-24-Containerd-session.pdf) or watch the [video](https://www.youtube.com/watch?v=kCNhgNXVdxw).\n\n## 2025\n### KubeCon + CloudNativeCon Europe 2025\n\n[Akihiro Suda](https://github.com/AkihiroSuda) presented Lima in the CNCF project pavilion.\n\nRead the [slides](https://github.com/AkihiroSuda/AkihiroSuda/blob/master/slides/2025/20250402%20%5BKubeCon%20EU%20Pavilion%5D%20Lima.pdf).\n\n### EuroBSDCon 2025\n\n[Leonardo Taccari](https://github.com/iamleot) presented Lima in the NetBSD devsummit at EuroBSDCon 2025.\n\nRead the [slides](https://www.NetBSD.org/gallery/presentations/leot/eurobsdcon2025-devsummit-lima/lima.pdf).\n\n### KubeCon + CloudNativeCon North America 2025\n\n[Akihiro Suda](https://github.com/AkihiroSuda) presented Lima in the CNCF project pavilion.\n\nRead the [slides](https://github.com/AkihiroSuda/AkihiroSuda/blob/master/slides/2025/20251113%20%5BKubeCon%20NA%20Pavilion%5D%20Lima.pdf).\n"
  },
  {
    "path": "website/content/en/docs/templates/_index.md",
    "content": "---\ntitle: Templates\nweight: 4\n---\n\n{{% readtemplates \"/templates/README.md\" %}}\n"
  },
  {
    "path": "website/content/en/docs/templates/github.md",
    "content": "---\ntitle: GitHub template URLs\nweight: 20\n---\n\n| ⚡ Requirement | Lima >= 2.0 |\n|----------------|-------------|\n\nLima provides a special `github:` URL scheme to reference templates from a GitHub repo, as an alternative to using the `https:` scheme with a \"raw\" URL.\n\nFor example the `templates/fedora.yaml` template in the `lima-vm/lima` repo could be referenced as\n\n```\nhttps://raw.githubusercontent.com/lima-vm/lima/refs/heads/master/templates/fedora.yaml\n```\n\nUsing the `github:` scheme this becomes:\n\n```\ngithub:lima-vm/lima/templates/fedora\n```\n\n**⚠️ Note**: `github:` URLs are experimental and the exact semantics may change in future releases.\n\n## General rules\n\n**File extension:**\n\nA `github:` URL without file extension will automatically get a `.yaml` suffix. So the Fedora URL above is the same as\n\n```\ngithub:lima-vm/lima/templates/fedora.yaml\n```\n\n**File name:**\n\nThe default filename for `github:` URLs is `.lima.yaml`. These URLs all reference the same file:\n\n```\ngithub:lima-vm/lima/.lima.yaml\ngithub:lima-vm/lima/.lima\ngithub:lima-vm/lima/\ngithub:lima-vm/lima\n```\n\n**Branch/tag/commit:**\n\nYou can append `@TAG` to a `github:` URL to specify a branch, a tag, or a commit id. For example:\n\n```\ngithub:lima-vm/lima/templates/fedora@v2.0.0\n```\n\nLima looks up the default branch of the repo when no `@TAG` is specified. This uses a GitHub API call.\n\n**Note:** Frequent use of `github:` URLs may require setting `GITHUB_TOKEN` or `GH_TOKEN` to a personal access token to avoid GitHub rate limits.\n\n## Testing URL resolution\n\nYou can use the `limactl template url` command to see which `https:` URL is generated from a `github:` URL. For example:\n\n```console\n❯ limactl template url github:lima-vm/lima/templates/docker\nWARN[0000] The github: scheme is still EXPERIMENTAL\nhttps://raw.githubusercontent.com/lima-vm/lima/master/templates/docker.yaml\n```\n\nYou'll get an error if the template does not exist:\n\n```console\n❯ limactl template url github:lima-vm/lima\nFATA[0000] file \"https://raw.githubusercontent.com/lima-vm/lima/master/.lima.yaml\" not found or inaccessible: status 404\n```\n\n## Symbolic links\n\nLima will check if the template file referenced by the `github:` URL is a symlink (or a text file whose content has no spaces, newlines, or colons). In that case it will treat the content as a relative path and return the address of that target file.\n\nFor example the `fedora` template is a symlink to `fedora-43.yaml`:\n\n```console\n❯ limactl tmpl url github:lima-vm/lima/templates/fedora\nhttps://raw.githubusercontent.com/lima-vm/lima/master/templates/fedora-43.yaml\n```\n\n## Org repositories\n\nAn \"org repo\" has identical org and repo names (e.g. `lima-vm/lima-vm`). For these repos, the repo name can be omitted:\n\n```\ngithub:lima-vm/lima-vm/.lima.yaml\ngithub:lima-vm//.lima.yaml\ngithub:lima-vm\n```\n\nOrg repos support two additional features that enable shorter URLs, even when the main project lives in a different repo (like `lima-vm/lima` instead of `lima-vm/lima-vm`).\n\n**Redirects:**\n\nIn an org repo a template file can not only be a symlink, but also a text file containing a `github:` URL. The URL must point to the same GitHub org and must NOT include a `@TAG`. It will be used to replace the original URL.\n\nFor example assume the `lima-vm` projects wants to support this URL:\n\n```\ngithub:lima-vm//fedora\n```\n\nThen it would have to create a `lima-vm/lima-vm` repo with a `fedora.yaml` file (in the default branch) that contains:\n\n```\ngithub:lima-vm/lima/templates/fedora\n```\n\n**Tag propagation:**\n\nOrg repo redirects work with tags. For example:\n\n```\ngithub:lima-vm//fedora@v1.2.1\n```\n\nLima resolves this through the following steps:\n\n1. Tries to load `fedora.yaml` from tag `v1.2.1` in the `lima-vm/lima-vm` repo\n2. Tag doesn't exist, so falls back to the default branch (`master`)\n3. Loads `fedora.yaml@master`, which contains the redirect: `github:lima-vm/lima/templates/fedora`\n4. Applies the original tag to the redirect URL\n\nFinal resolved URL:\n\n```\ngithub:lima-vm/lima/templates/fedora.yaml@v1.2.1\n```\n\nLima will error if the fallback file doesn't exist or isn't a valid `github:` redirect.\n"
  },
  {
    "path": "website/content/en/docs/usage/_index.md",
    "content": "---\ntitle: Usage\nweight: 2\n---\n\n## Starting a Linux instance\n\nRun `limactl start <INSTANCE>` to create and start the first instance.\nThe `<INSTANCE>` name defaults to \"default\".\n\n```console\n$ limactl start\n? Creating an instance \"default\"  [Use arrows to move, type to filter]\n> Proceed with the current configuration\n  Open an editor to review or modify the current configuration\n  Choose another template (docker, podman, archlinux, fedora, ...)\n  Exit\n...\nINFO[0029] READY. Run `lima` to open the shell.\n```\n\nChoose `Proceed with the current configuration`, and wait until \"READY\" to be printed on the host terminal.\n\nFor automation,  `--tty=false` flag can be used for disabling the interactive user interface.\n\n### Customization\nTo create an instance \"default\" from a template \"docker\":\n```bash\nlimactl create --name=default template:docker\nlimactl start default\n```\n\nSee also the command reference:\n- [`limactl create`](../reference/limactl_create/)\n- [`limactl start`](../reference/limactl_start/)\n- [`limactl edit`](../reference/limactl_edit/)\n\n### Executing Linux commands\nRun `limactl shell <INSTANCE> <COMMAND>` to launch `<COMMAND>` on the VM:\n```bash\nlimactl shell default uname -a\n```\n\nSee also the command reference:\n- [`limactl shell`](../reference/limactl_shell/)\n\nFor the \"default\" instance, this command can be shortened as `lima <COMMAND>`.\n```bash\nlima uname -a\n```\nThe `lima` command also accepts the instance name as the environment variable `$LIMA_INSTANCE`.\n\n### Home directory\n\nThe host home directory is mounted as read-only on the following path by default:\n- `/Users/${USER}` (on macOS hosts)\n- `/home/${USER}`  (on other hosts)\n\nTo make the host mount writable, run `limactl start` with `--mount-writable`.\nTo disable the mount, `limactl start` with `--mount-none` or `--plain`.\n\nThe guest home directory exists independently on the following path:\n- `/Users/${USER}.guest` (on macOS guests)\n- `/home/${USER}.guest`  (on other guests, since Lima v2.1)\n- `/home/${USER}.linux`  (prior to Lima v2.1)\n\n### Shell completion\n- To enable bash completion, add `source <(limactl completion bash)` to `~/.bash_profile`.\n- To enable zsh completion, see `limactl completion zsh --help`\n"
  },
  {
    "path": "website/content/en/docs/usage/guests/_index.md",
    "content": "---\ntitle: Guest OS\nweight: 10\n---\n"
  },
  {
    "path": "website/content/en/docs/usage/guests/freebsd.md",
    "content": "---\ntitle: FreeBSD\nweight: 3\n---\n\n| ⚡ Requirement | Lima >= 2.1 |\n|-------------------|---------|\n\nRunning FreeBSD guests is experimentally supported since Lima v2.1.\n\n{{< tabpane text=true >}}\n{{% tab header=\"FreeBSD 15\" %}}\n```\nlimactl start --mount-none template:freebsd-15\n```\n{{% /tab %}}\n{{% tab header=\"FreeBSD 16 (CURRENT)\" %}}\n```\nlimactl start template:experimental/freebsd-16\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nPrerequisites:\n- QEMU\n- xorriso (on non-macOS hosts)\n\n## Difference from Linux guests\n- Several features are not implemented yet. See [Caveats](#caveats) below.\n\n## Caveats\n- No support for automatic port forwarding.  Use `ssh -L` to manually set up port forwarding.\n- No support for installing custom `caCerts`\n- And more\n\n### FreeBSD prior to 16\n- No support for mounting host directories.\n  Use `limactl cp` or `limactl shell --sync` to share files with the host.\n"
  },
  {
    "path": "website/content/en/docs/usage/guests/linux.md",
    "content": "---\ntitle: Linux\nweight: 1\n---\n\nLinux is the default guest operating system.\n\n## See also\n- [Templates](../../templates/_index.md)\n"
  },
  {
    "path": "website/content/en/docs/usage/guests/macos.md",
    "content": "---\ntitle: macOS\nweight: 2\n---\n\n| ⚡ Requirement | Lima >= 2.1, macOS, ARM  |\n|-------------------|-----------------------------|\n\nRunning macOS guests is experimentally supported since Lima v2.1.\n\n{{< tabpane text=true >}}\n{{% tab header=\"macOS only\" %}}\n```bash\nlimactl start template:macos\n```\n{{% /tab %}}\n{{% tab header=\"With Homebrew\" %}}\n```bash\nlimactl start template:homebrew-macos\n```\n{{% /tab %}}\n{{< /tabpane >}}\n\nThe user password is randomly generated and stored in the `~/password` file in the VM.\nConsider changing it after the first login.\n\n```bash\nlimactl shell macos cat /Users/${USER}.guest/password\n```\n\n## Difference from Linux guests\n- Password login is enabled\n- Password-less sudo is disabled, except for `/sbin/shutdown -h now`\n- Several features are not implemented yet. See [Caveats](#caveats) below.\n\n## Caveats\n- No support for turning off the video display.\n- No support for automatic port forwarding.\n  Use `ssh -L` to manually set up port forwarding, or,\n  use the [`vzNAT`](../../config/network/vmnet.md#vznat) network to access the guest by its IP.\n- No support for installing custom `caCerts`\n"
  },
  {
    "path": "website/content/en/docs/usage/ssh.md",
    "content": "---\ntitle: SSH\nweight: 3\n---\n\nInstead of the `limactl shell` command, SSH can be used too:\n\n```console\n$ limactl ls --format='{{.SSHConfigFile}}' default\n/Users/example/.lima/default/ssh.config\n\n$ ssh -F /Users/example/.lima/default/ssh.config lima-default\n```\n\nThis is useful for interoperability with other software that expects the SSH connectivity.\n\n## Using SSH without additional options\n\nAdd the following line to your `~/.ssh/config`:\n\n```\nInclude ~/.lima/*/ssh.config\n```\n\nThen you can connect directly without specifying `-F`:\n```bash\nssh lima-default\n```\n\nThis configuration is notably useful for the Remote Development mode of [Visual Studio Code](../examples/vscode.md).\n\n## Using SSH without a config file\n\nIf your SSH client does not support a config file, try specifying an equivalent of the following command:\n\n```bash\nssh -p <PORT> -i ~/.lima/_config/user -o NoHostAuthenticationForLocalhost=yes 127.0.0.1\n```\n\nThe port number can be inspected as follows:\n```bash\nlimactl list --format '{{ .SSHLocalPort }}' default\n```\n\nSee also `.lima/default/ssh.config`.\n"
  },
  {
    "path": "website/content/en/search.md",
    "content": "---\ntitle: Search Results\nlayout: search\n---\n"
  },
  {
    "path": "website/data/adopters.yaml",
    "content": "- name: Rancher Desktop\n  link: https://rancherdesktop.io\n  image: rancherdesktop.svg\n  width: 200\n- name: Colima\n  link: https://github.com/abiosoft/colima\n  image: colima.svg\n  width: 100\n- name: Finch\n  link: https://github.com/runfinch/finch\n  image: finch.svg\n  width: 100\n- name: Podman Desktop\n  link: https://podman-desktop.io\n  image: podman-desktop.svg\n  width: 200\n"
  },
  {
    "path": "website/data/helpfullinks.yaml",
    "content": "- header: Install Lima\n  icon: fa-solid fa-code\n  link: /docs/installation\n  label: Get Lima Here\n- header: Try Lima\n  icon: fa-solid fa-chalkboard\n  link: /docs/usage\n  label: Get started with Lima\n- header: Contributions welcome\n  icon: fa-brands fa-github\n  link: /docs/community\n  label: Join the Lima community\n"
  },
  {
    "path": "website/go.mod",
    "content": "module github.com/lima-vm/lima/website\n\ngo 1.23.0\n\nrequire (\n\tgithub.com/google/docsy v0.11.0\n)\n\nrequire (\n\tgithub.com/FortAwesome/Font-Awesome v0.0.0-20240716171331-37eff7fa00de // indirect\n\tgithub.com/google/docsy/dependencies v0.7.2 // indirect\n\tgithub.com/twbs/bootstrap v5.3.6+incompatible // indirect\n)\n"
  },
  {
    "path": "website/go.sum",
    "content": "github.com/FortAwesome/Font-Awesome v0.0.0-20230327165841-0698449d50f2/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo=\ngithub.com/FortAwesome/Font-Awesome v0.0.0-20240716171331-37eff7fa00de h1:JvHOfdSqvArF+7cffH9oWU8oLhn6YFYI60Pms8M/6tI=\ngithub.com/FortAwesome/Font-Awesome v0.0.0-20240716171331-37eff7fa00de/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo=\ngithub.com/google/docsy v0.11.0 h1:QnV40cc28QwS++kP9qINtrIv4hlASruhC/K3FqkHAmM=\ngithub.com/google/docsy v0.11.0/go.mod h1:hGGW0OjNuG5ZbH5JRtALY3yvN8ybbEP/v2iaK4bwOUI=\ngithub.com/google/docsy/dependencies v0.7.2 h1:+t5ufoADQAj4XneFphz4A+UU0ICAxmNaRHVWtMYXPSI=\ngithub.com/google/docsy/dependencies v0.7.2/go.mod h1:gihhs5gmgeO+wuoay4FwOzob+jYJVyQbNaQOh788lD4=\ngithub.com/twbs/bootstrap v5.2.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=\ngithub.com/twbs/bootstrap v5.3.3+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=\ngithub.com/twbs/bootstrap v5.3.6+incompatible h1:efmXVyq839m5QQ0+JBUdQQ1TrmoBqvQ5kRhUueKsH+4=\ngithub.com/twbs/bootstrap v5.3.6+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0=\n"
  },
  {
    "path": "website/hugo.toml",
    "content": "baseURL = \"/\"\ntitle = \"Lima\"\n\n# Language settings\ncontentDir = \"content/en\"\ndefaultContentLanguage = \"en\"\ndefaultContentLanguageInSubdir = false\n# Useful when translating.\nenableMissingTranslationPlaceholders = true\n\nenableRobotsTXT = true\n\n# Will give values to .Lastmod etc.\nenableGitInfo = true\n\n# Comment out to enable taxonomies in Docsy\n# disableKinds = [\"taxonomy\", \"taxonomyTerm\"]\n\n# You can add your own taxonomies\n[taxonomies]\ntag = \"tags\"\ncategory = \"categories\"\n\n[params.taxonomy]\n# set taxonomyCloud = [] to hide taxonomy clouds\ntaxonomyCloud = [\"tags\", \"categories\"]\n\n# If used, must have same length as taxonomyCloud\ntaxonomyCloudTitle = [\"Tag Cloud\", \"Categories\"]\n\n# set taxonomyPageHeader = [] to hide taxonomies on the page headers\ntaxonomyPageHeader = [\"tags\", \"categories\"]\n\n\n# Highlighting config\npygmentsCodeFences = true\npygmentsUseClasses = false\n# Use the new Chroma Go highlighter in Hugo.\npygmentsUseClassic = false\n#pygmentsOptions = \"linenos=table\"\n# See https://help.farbox.com/pygments.html\npygmentsStyle = \"tango\"\n\n# Configure how URLs look like per section.\n[permalinks]\nblog = \"/:section/:year/:month/:day/:slug/\"\n\n# Image processing configuration.\n[imaging]\nresampleFilter = \"CatmullRom\"\nquality = 75\nanchor = \"smart\"\n\n# Language configuration\n\n[languages]\n[languages.en]\nlanguageName =\"English\"\n# Weight used for sorting.\nweight = 1\n[languages.en.params]\ntitle = \"Lima\"\n\n[markup]\n  [markup.goldmark]\n    [markup.goldmark.parser.attribute]\n      block = true\n    [markup.goldmark.renderer]\n      unsafe = true\n  [markup.highlight]\n    # See a complete list of available styles at https://xyproto.github.io/splash/docs/all.html\n    style = \"tango\"\n    # Uncomment if you want your chosen highlight style used for code blocks without a specified language\n    # guessSyntax = \"true\"\n\n# Everything below this are Site Params\n\n# Comment out if you don't want the \"print entire section\" link enabled.\n[outputs]\nsection = [\"HTML\", \"print\", \"RSS\"]\n\n[params]\ncopyright = \"Lima Authors\"\n\n# First one is picked as the Twitter card image if not set on page.\n# images = [\"images/project-illustration.png\"]\n\n# Menu title if your navbar has a versions selector to access old versions of your site.\n# This menu appears only if you have at least one [params.versions] set.\nversion_menu = \"Releases\"\n\n# Repository configuration (URLs for in-page links to opening issues and suggesting changes)\ngithub_repo = \"https://github.com/lima-vm/lima\"\n\n# An optional link to a related project repo. For example, the sibling repository where your product code lives.\ngithub_project_repo = \"https://github.com/lima-vm/lima\"\n\n# Flag used in the \"version-banner\" partial to decide whether to display a\n# banner on every page indicating that this is an archived version of the docs.\n# Set this flag to \"true\" if you want to display the banner.\narchived_version = false\n\n# The version number for the version of the docs represented in this doc set.\n# Used in the \"version-banner\" partial to display a version number for the\n# current doc set.\nversion = \"master\"\n\n# A link to latest version of the docs. Used in the \"version-banner\" partial to\n# point people to the main doc site.\n#url_latest_version = \"https://lima-vm.io\"\n\n# Specify a value here if your content directory is not in your repo's root directory\ngithub_subdir = \"website\"\n\n# Uncomment this if your GitHub repo does not have \"main\" as the default branch,\n# or specify a new value if you want to reference another branch in your GitHub links\ngithub_branch= \"master\"\n\n# Enable Lunr.js offline search\nofflineSearch = true\n\n# Enable syntax highlighting and copy buttons on code blocks with Prism\nprism_syntax_highlighting = false\n\n# User interface configuration\n[params.ui]\n#  Set to true to disable breadcrumb navigation.\nbreadcrumb_disable = false\n# Set to true to disable the About link in the site footer\nfooter_about_disable = false\n# Set to false if you don't want to display a logo (/assets/icons/logo.svg) in the top navbar\nnavbar_logo = true\n# Set to true if you don't want the top navbar to be translucent when over a `block/cover`, like on the homepage.\nnavbar_translucent_over_cover_disable = false\n# Enable to show the side bar menu in its compact state.\nsidebar_menu_compact = false\n# Set to true to hide the sidebar search box (the top nav search box will still be displayed if search is enabled)\nsidebar_search_disable = true\nsidebar_menu_foldable = true\nul_show = 1\n\n# Adds a H2 section titled \"Feedback\" to the bottom of each doc. The responses are sent to Google Analytics as events.\n# This feature depends on [services.googleAnalytics] and will be disabled if \"services.googleAnalytics.id\" is not set.\n# If you want this feature, but occasionally need to remove the \"Feedback\" section from a single page,\n# add \"hide_feedback: true\" to the page's front matter.\n[params.ui.feedback]\nenable = false\n\n[params.ui.readingtime]\nenable = false\n\n[params.links]\n# TODO: unify developer and user links\n[[params.links.developer]]\n  name = \"GitHub\"\n  url = \"https://github.com/lima-vm/lima\"\n  icon = \"fab fa-github\"\n  desc = \"Development takes place here!\"\n[[params.links.developer]]\n  name = \"Slack\"\n  url = \"https://slack.cncf.io/\"\n  icon = \"fab fa-slack\"\n  desc = \"Chat with other project developers\"\n[[params.links.developer]]\n  name = \"X (Twitter)\"\n  url = \"https://x.com/@TheLimaProject\"\n  icon = \"fab fa-x-twitter\"\n  desc = \"Follow for updates\"\n[[params.links.developer]]\n  name = \"Mastodon\"\n  url = \"https://mastodon.social/@TheLimaProject\"\n  icon = \"fab fa-mastodon\"\n  desc = \"Follow for updates\"\n[[params.links.user]]\n  name = \"Discussion\"\n  url = \"https://github.com/lima-vm/lima/discussions\"\n  icon = \"fab fa-github\"\n  desc = \"Discussion and help from your fellow users\"\n[[params.links.user]]\n  name = \"Slack\"\n  url = \"https://slack.cncf.io/\"\n  icon = \"fab fa-slack\"\n  desc = \"Chat with other project users\"\n[[params.links.user]]\n  name = \"X (Twitter)\"\n  url = \"https://x.com/@TheLimaProject\"\n  icon = \"fab fa-x-twitter\"\n  desc = \"Follow for updates\"\n[[params.links.user]]\n  name = \"Mastodon\"\n  url = \"https://mastodon.social/@TheLimaProject\"\n  icon = \"fab fa-mastodon\"\n  desc = \"Follow for updates\"\n\n[params.mermaid]\n  enable = true\n\n[[menus.main]]\n  name = \"GitHub\"\n  url = \"https://github.com/lima-vm/lima\"\n  pre = '<i class=\"fab fa-github\"></i>'\n[[menus.main]]\n  name = \"Community\"\n  url = \"/docs/community\"\n[[menus.main]]\n  name = \"Releases\"\n  url = \"https://github.com/lima-vm/lima/releases\"\n\n[module]\n  # uncomment line below for temporary local development of module\n  # replacements = \"github.com/google/docsy -> ../../docsy\"\n  [module.hugoVersion]\n    extended = true\n    min = \"0.110.0\"\n  [[module.imports]]\n    path = \"github.com/google/docsy\"\n    disable = false\n  [[module.imports]]\n    path = \"github.com/google/docsy/dependencies\"\n    disable = false\n  # Import command reference from website _output folder\n  [[module.imports]]\n    path=\"../_output\"\n  [[module.imports.mounts]]\n    source=\"docsy\"\n    target=\"content/docs/Reference\"\n    language=\"en\"\n  [[module.imports.mounts]]\n    source=\"docsy-mcp\"\n    target=\"content/docs/config/ai/outside\"\n    language=\"en\"\n  # Import templates README.md from templates folder\n  [[module.imports]]\n    path=\"../..\"\n  [[module.imports.mounts]]\n    source=\"templates\"\n    target=\"static/templates\"\n    language=\"en\"\n    includeFiles = \"README.md\"\n"
  },
  {
    "path": "website/layouts/404.html",
    "content": "{{ define \"main\" -}}\n<div class=\"td-content\">\n  <h1>Not found</h1>\n  <p>Oops! This page doesn't exist. Try going back to the <a href=\"{{ \"\" | relURL }}\">home page</a>.</p>\n</div>\n{{- end }}\n"
  },
  {
    "path": "website/layouts/partials/footer.html",
    "content": "{{ $links := .Site.Params.links -}}\n<footer class=\"td-footer row d-print-none\">\n  <div class=\"container-fluid\">\n    <div class=\"row mx-md-2\">\n      <div class=\"col-6 col-sm-4 text-xs-center order-sm-2\">\n        {{ with $links }}\n        {{ with index . \"user\" }}\n        {{ end }}\n        {{ end }}\n      </div>\n      <div class=\"col-6 col-sm-4 text-end text-xs-center order-sm-3\">\n        {{ with $links }}\n        {{ with index . \"developer\" }}\n        {{ template \"footer-links-block\"  . }}\n        {{ end }}\n        {{ end }}\n      </div>\n      <div class=\"td-footer__copyright-etc col-12 col-sm-4 text-center py-2 order-sm-2\">\n        {{ with .Site.Params.copyright -}}\n        <span>&copy; {{ now.Year }} {{ . }} {{ T \"footer_all_rights_reserved\" }}</span>\n        {{- end }}\n        {{ with .Site.Params.privacy_policy -}}\n        <span class=\"ms-1\"><a href=\"{{ . }}\" target=\"_blank\" rel=\"noopener\">{{ T \"footer_privacy_policy\" }}</a></span>\n        {{- end }}\n        {{ if not .Site.Params.ui.footer_about_disable -}}\n        {{ with .Site.GetPage \"about\" -}}\n        <p class=\"td-footer__about mt-2\"><a href=\"{{ .RelPermalink }}\">{{ .Title }}</a></p>\n        {{- end -}}\n        {{ end }}\n      </div>\n    </div>\n    <div class=\"row text-center text-white small\">\n      <div id=\"cncf-footer\" class=\"col-12 text-center py-2 order-sm-2\">\n        <a class=\"text-secondary\" href=\"https://www.linuxfoundation.org/terms\" target=\"_blank\" rel=\"noopener\">Terms</a> |\n        <a class=\"text-secondary\" href=\"https://www.linuxfoundation.org/privacy\" target=\"_blank\" rel=\"noopener\">Privacy</a> |\n        <a class=\"text-secondary\" href=\"https://www.linuxfoundation.org/trademark-usage\" target=\"_blank\" rel=\"noopener\">Trademarks</a> |\n        <a class=\"text-secondary\" href=\"https://github.com/lima-vm/lima/blob/master/LICENSE\" target=\"_blank\" rel=\"noopener\">License</a>\n      </div>\n    </div>\n  </div>\n</footer>\n\n{{- define \"footer-links-block\" }}\n<ul class=\"td-footer__links-list\">\n  {{ range . }}\n  <li class=\"td-footer__links-item\" data-bs-toggle=\"tooltip\" title=\"{{ .name }}\" aria-label=\"{{ .name }}\">\n    <a target=\"_blank\" rel=\"noopener\" href=\"{{ .url }}\" aria-label=\"{{ .name }}\">\n      <i class=\"{{ .icon }}\"></i>\n    </a>\n  </li>\n  {{ end }}\n</ul>\n{{ end -}}\n"
  },
  {
    "path": "website/layouts/partials/navbar.html",
    "content": "{{ $cover := and\n(.HasShortcode \"blocks/cover\")\n(not .Site.Params.ui.navbar_translucent_over_cover_disable)\n-}}\n{{ $baseURL := urls.Parse $.Site.Params.Baseurl -}}\n\n<nav class=\"td-navbar navbar-light js-navbar-scroll\n            {{- if $cover }} td-navbar-cover {{else}} nav-shadow {{- end }}\">\n  <div class=\"container-fluid flex-column flex-md-row\">\n    <a class=\"navbar-brand\" {{- if $cover }} style=\"display: none\" {{- end }} href=\"{{ .Site.Home.RelPermalink }}\">\n      {{- /**/ -}}\n      <span class=\"navbar-brand__logo navbar-logo\">\n      {{- if ne .Site.Params.ui.navbar_logo false -}}\n        {{ with resources.Get \"icons/logo.svg\" -}}\n          {{ ( . | minify).Content | safeHTML -}}\n        {{ end -}}\n      {{ end -}}\n    </span>\n      {{- /**/ -}}\n      {{- /**/ -}}\n    </a>\n    <div class=\"td-navbar-nav-scroll ms-md-auto\" id=\"main_navbar\">\n      <ul class=\"navbar-nav\">\n        {{ $p := . -}}\n        {{ range .Site.Menus.main -}}\n        <li class=\"nav-item\">\n          {{ $active := or ($p.IsMenuCurrent \"main\" .) ($p.HasMenuCurrent \"main\" .) -}}\n          {{ $href := \"\" -}}\n          {{ with .Page -}}\n          {{ $active = or $active ( $.IsDescendant .) -}}\n          {{ $href = .RelPermalink -}}\n          {{ else -}}\n          {{ $href = .URL | relLangURL -}}\n          {{ end -}}\n          {{ $isExternal := ne $baseURL.Host (urls.Parse .URL).Host -}}\n          <a {{/**/ -}}\n          class=\"nav-link {{- if $active }} active {{- end }}\" {{/**/ -}}\n          href=\"{{ $href }}\"\n          {{- if $isExternal }} target=\"_blank\" rel=\"noopener\" {{- end -}}\n          >\n          {{- .Pre -}}\n          <span>{{ .Name }}</span>\n          {{- .Post -}}\n          </a>\n        </li>\n        {{ end -}}\n        {{ if .Site.Params.versions -}}\n        <li class=\"nav-item dropdown d-none d-lg-block\">\n          {{ partial \"navbar-version-selector.html\" . -}}\n        </li>\n        {{ end -}}\n        {{ if (gt (len .Site.Home.Translations) 0) -}}\n        <li class=\"nav-item dropdown d-none d-lg-block\">\n          {{ partial \"navbar-lang-selector.html\" . -}}\n        </li>\n        {{ end -}}\n      </ul>\n    </div>\n    <div class=\"d-none d-lg-block\">\n      {{ partial \"search-input.html\" . }}\n    </div>\n  </div>\n</nav>\n"
  },
  {
    "path": "website/layouts/shortcodes/blocks/adopters.html",
    "content": "{{ $usedBy := site.Data.adopters -}}\n\n<div class=\"col\">\n  <h2 class=\"text-center pb-3 mt-3\">Adopters</h2>\n  <p class=\"text-center\">\n    {{ range $usedBy -}}\n    {{ $img := printf \"images/users/%s\" .image | relURL -}}\n    <a href=\"{{ .link }}\" aria-label=\"{{ .name }}\" target=\"_blank\" rel=\"noopener\">\n      <img class=\"mx-3\" width=\"{{ .width }}\" src=\"{{ $img }}\" alt=\"{{ .name }} logo\">\n    </a>\n    {{- end }}\n  </p>\n</div>\n"
  },
  {
    "path": "website/layouts/shortcodes/blocks/cncf.html",
    "content": "<section class=\"text-center\">\n  <h4>{{ .Site.Title }} is a <a class=\"text-secondary\" href=\"https://cncf.io\">CNCF</a> incubating project</h4>\n\n  <img class=\"cncf-logo\"\n       src=\"/images/cncf-color-bg.svg\" alt=\"Cloud Native Computing Foundation logo\" />\n</section>\n"
  },
  {
    "path": "website/layouts/shortcodes/blocks/cover.html",
    "content": "{{ $col_id := .Get \"color\" | default \"dark\" }}\n{{/* Height can be one of: auto, min, med, max, full. */}}\n{{ $height := .Get \"height\" | default \"max\"  }}\n<section\n  class=\"row td-cover-block td-cover-block--height-{{ $height }} js-td-cover td-overlay td-overlay--light -bg-{{ $col_id }}\">\n  <div class=\"container td-overlay__inner\">\n    <div class=\"row\">\n      <div class=\"col-lg-6\">\n        <div class=\"d-flex h-100 flex-column justify-content-center align-items-center\">\n          <h1 class=\"display-1 mt-0 pb-2\">\n            <img alt=\"Lima\" width=\"150\" src=\"images/logo.svg\" />\n          </h1>\n          <p class=\"display-2 mb-0\">Linux Machines</p>\n        </div>\n      </div>\n      <div class=\"col-lg-6\">\n        <img src=\"/images/demo.gif\" alt=\"demo\" class=\"img-fluid\" />\n      </div>\n      <div class=\"col-12 pt-3\">\n        <div class=\"pt-3 lead text-center\">\n          {{ .Inner }}\n        </div>\n      </div>\n    </div>\n  </div>\n</section>\n"
  },
  {
    "path": "website/layouts/shortcodes/blocks/helpfullinks.html",
    "content": "{{ $usedBy := site.Data.helpfullinks -}}\n\n{{ range $usedBy -}}\n  <div class=\"col-lg-4 mb-5 mb-lg-0 text-center\">\n    <a class=\"text-primary\" href=\"{{ .link }}\">\n      <div class=\"mb-4 h1\">\n        <i class=\"{{ .icon }}\"></i>\n      </div>\n      <h4 class=\"h3\">{{ .header }}</h4>\n    </a>\n    <a class=\"mr-3 mb-4 text-secondary\" href=\"{{ .link }}\">\n      {{ .label }} <i class=\"fas fa-arrow-alt-circle-right ml-2\"></i>\n    </a>\n  </div>\n{{- end }}\n"
  },
  {
    "path": "website/layouts/shortcodes/fixlinks.html",
    "content": "{{ $gh_repo := (.Site.Params.github_repo) -}}\n\n{{- replace (.Inner | safeHTML) \"(./\" (printf \"(%s/tree/%s/\" $gh_repo $.Site.Params.version) -}}\n"
  },
  {
    "path": "website/layouts/shortcodes/readtemplates.html",
    "content": "{{ $gh_repo := (.Site.Params.github_repo) -}}\n{{ $gh_docs := (printf \"%s/tree/%s/docs/\" $gh_repo $.Site.Params.version) -}}\n{{ $gh_templates := (printf \"%s/tree/%s/templates/\" $gh_repo $.Site.Params.version) -}}\n\n{{/* Store ordinal, to be retrieved by parent element */}}\n{{ if ge hugo.Version \"0.93.0\" }}\n\n{{ .Page.Store.Add \"Ordinal\" 1 }}\n{{ end }}\n\n{{/* Handle the \"file\" named parameter or a single unnamed parameter as the file\npath */}}\n{{ if .IsNamedParams }}\n{{ $.Scratch.Set \"fparameter\" ( .Get \"file\" ) }}\n{{ else }}\n{{ $.Scratch.Set \"fparameter\" ( .Get 0 ) }}\n{{ end }}\n\n\n{{/* If the first character is \"/\", the path is absolute from the site's\n`baseURL`. Otherwise, construct an absolute path using the current directory */}}\n\n{{ if eq (.Scratch.Get \"fparameter\" | printf \"%.1s\") \"/\" }}\n{{ $.Scratch.Set \"filepath\" ($.Scratch.Get \"fparameter\") }}\n{{ else }}\n{{ $.Scratch.Set \"filepath\" \"/\" }}\n{{ $.Scratch.Add \"filepath\" $.Page.File.Dir }}\n{{ $.Scratch.Add \"filepath\" ($.Scratch.Get \"fparameter\") }}\n{{ end }}\n\n\n{{/* If the file exists, read it and highlight it if it's code.\nThrow a compile error or print an error on the page if the file is not found */}}\n\n{{ if fileExists ($.Scratch.Get \"filepath\") }}\n{{ if eq (.Get \"code\") \"true\" }}\n{{- highlight ($.Scratch.Get \"filepath\" | readFile | htmlUnescape |\nsafeHTML ) (.Get \"lang\") \"\" -}}\n{{ else }}\n{{ $file_content := ($.Scratch.Get \"filepath\" | os.ReadFile) }}\n{{ $file_content = replace $file_content \"../docs/\" $gh_docs }}\n{{- replace $file_content \"./\" $gh_templates -}}\n{{ end }}\n{{ else if eq (.Get \"draft\") \"true\" }}\n\n<p style=\"color: #D74848\"><b><i>The file <code>{{ $.Scratch.Get \"filepath\" }}</code> was not found.</i></b></p>\n\n{{ else }}{{- errorf \"Shortcode %q: file %q not found at %s\" .Name ($.Scratch.Get \"filepath\") .Position -}}{{ end }}\n"
  },
  {
    "path": "website/netlify.toml",
    "content": "# Hugo build configuration for Netlify\n# (https://gohugo.io/hosting-and-deployment/hosting-on-netlify/#configure-hugo-version-in-netlify)\n\n[build]\ncommand = \"make -C .. docsy && npm run build:preview\"\npublish = \"public\"\n\n[context.production]\ncommand = \"make -C .. docsy && npm run build:production\"\n\n[[redirects]]\nfrom = \"/community\"\nto = \"/docs/community\"\nstatus = 301\nforce = true\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"lima-site\",\n  \"version\": \"1.0\",\n  \"description\": \"Lima website that uses Docsy theme for technical documentation.\",\n  \"repository\": \"github:lima-vm/lima\",\n  \"homepage\": \"https://lima-vm.io\",\n  \"author\": \"Lima\",\n  \"license\": \"Apache-2.0\",\n  \"bugs\": \"https://github.com/lima-vm/lima/issues\",\n  \"spelling\": \"cSpell:ignore HTMLTEST precheck postbuild -\",\n  \"scripts\": {\n    \"_build\": \"npm run _hugo-dev\",\n    \"_check:links\": \"echo IMPLEMENTATION PENDING for check-links; echo\",\n    \"_hugo\": \"hugo --cleanDestinationDir\",\n    \"_hugo-dev\": \"npm run _hugo -- -e dev -DFE\",\n    \"_serve\": \"npm run _hugo-dev -- --minify serve\",\n    \"build:preview\": \"npm run _hugo-dev -- --minify --baseURL \\\"${DEPLOY_PRIME_URL:-/}\\\"\",\n    \"build:production\": \"npm run _hugo -- --minify\",\n    \"build\": \"npm run _build\",\n    \"check:links:all\": \"HTMLTEST_ARGS= npm run _check:links\",\n    \"check:links\": \"npm run _check:links\",\n    \"clean\": \"rm -Rf public/* resources\",\n    \"make:public\": \"git init -b main public\",\n    \"precheck:links:all\": \"npm run build\",\n    \"precheck:links\": \"npm run build\",\n    \"postbuild:preview\": \"npm run _check:links\",\n    \"postbuild:production\": \"npm run _check:links\",\n    \"serve\": \"npm run _serve\",\n    \"test\": \"npm run check:links\",\n    \"update:pkg:dep\": \"npm install --save-dev autoprefixer@latest postcss-cli@latest\",\n    \"update:pkg:hugo\": \"npm install --save-dev --save-exact hugo-extended@latest\"\n  },\n  \"devDependencies\": {\n    \"autoprefixer\": \"^10.4.21\",\n    \"hugo-extended\": \"0.133.1\",\n    \"postcss-cli\": \"^11.0.1\"\n  }\n}\n"
  },
  {
    "path": "website/static/images/internals/lima-sequence-diagram.puml",
    "content": "@startuml\n'https://plantuml.com/sequence-diagram\n\nautonumber\n\nactor User\nparticipant limactl\nparticipant \"lima hostagent\"\nparticipant qemu\nparticipant \"guest os\"\nparticipant \"lima guestagent\"\nparticipant nerdctl\n\n== start ==\n\nUser -> limactl: start\nactivate limactl\nlimactl -> \"lima hostagent\": launch host agent\nactivate \"lima hostagent\"\ndeactivate limactl\n\"lima hostagent\" -> \"lima hostagent\": find free ports\n\"lima hostagent\" -> \"lima hostagent\": generate cloud-init data & CD-ROM image\n\"lima hostagent\" -> \"lima hostagent\": generate QEMU command line\n\"lima hostagent\" -> \"lima hostagent\": set up port forwarding\n\"lima hostagent\" -> qemu: start\nactivate qemu\nqemu -> \"guest os\": boot\nactivate \"guest os\"\n\"guest os\" -> \"guest os\": run setup commands from CD-ROM\n\"guest os\" -> \"lima guestagent\": start\nactivate \"lima guestagent\"\n\"lima hostagent\" -> \"guest os\": set up sshfs mounts\n\"lima hostagent\" -> \"guest os\": set up socket forwarding with SSH\nloop periodically\n\"lima guestagent\" -> \"guest os\": read /proc/net/tcp* and iptables\nend loop\nloop periodically\n\"lima hostagent\" -> \"lima guestagent\": connect to guest agent (query info)\n\"lima guestagent\" -> \"lima hostagent\": IP/port pairs (from /proc/net/tcp* and iptables)\n\"lima hostagent\" -> \"lima guestagent\": subscribe to port add/remove events\n\"lima guestagent\" -> \"lima hostagent\": (ongoing) return port add/remove events\nend loop\n\"lima hostagent\" -> \"guest os\": query for requirements/boot status/readiness probes\n\n== command execution ==\n\nUser -> limactl: shell \"command\"\nactivate limactl\nlimactl -> \"guest os\": ssh \"command\"\n\"guest os\" -> limactl: command output\nlimactl -> User: command output\ndeactivate limactl\n\n== nerdctl run ==\n\nUser -> limactl: \"nerdctl run\"\nactivate limactl\nlimactl -> \"guest os\": ssh \"nerdctl run\"\n\"guest os\" -> nerdctl: run container\nactivate nerdctl\nloop periodically\n\"lima guestagent\" -> \"guest os\": read /proc/net/tcp* and iptables\nend loop\nloop periodically\n\"lima hostagent\" -> \"lima guestagent\": query events\n\"lima guestagent\" -> \"lima hostagent\": return port add/remove events\n\"lima hostagent\" -> \"lima hostagent\": set up new port forwarding\nend loop\nnerdctl -> nerdctl: container exits\nnerdctl -> limactl: container output\ndeactivate nerdctl\nlimactl -> User: container output\ndeactivate limactl\nloop periodically\n\"lima guestagent\" -> \"guest os\": read /proc/net/tcp* and iptables\nend loop\nloop periodically\n\"lima hostagent\" -> \"lima guestagent\": query events\n\"lima guestagent\" -> \"lima hostagent\": return port add/remove events\n\"lima hostagent\" -> \"lima hostagent\": remove old port forwarding\nend loop\n\n== stop ==\n\nUser -> limactl: stop\nactivate limactl\nlimactl -> \"lima hostagent\": SIGINT\n\"lima hostagent\" -> qemu: ACPI shutdown\nqemu -> \"guest os\": ACPI shutdown\n\"guest os\" -> \"lima guestagent\": SIGTERM\ndeactivate \"lima guestagent\"\ndeactivate \"guest os\"\n\"lima hostagent\" -> \"lima hostagent\": wait up to 3 minutes for shutdown\ndeactivate qemu\n\"lima hostagent\" -> limactl: emit exit event\ndeactivate \"lima hostagent\"\nlimactl -> User: stopped\ndeactivate limactl\n@enduml\n"
  }
]