diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 10d229af76..bddbf5b13f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -65,6 +65,8 @@ updates: - dependency-type: all cooldown: default-days: 7 + exclude: + - Homebrew/actions/* - package-ecosystem: uv directories: - "/Library/Homebrew/formula-analytics/" diff --git a/.github/workflows/actionlint.yml b/.github/workflows/actionlint.yml index 3a66ff4eaf..7ef744e488 100644 --- a/.github/workflows/actionlint.yml +++ b/.github/workflows/actionlint.yml @@ -34,12 +34,12 @@ jobs: steps: - name: Set up Homebrew id: setup-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - - uses: Homebrew/actions/cache-homebrew-prefix@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + - uses: Homebrew/actions/cache-homebrew-prefix@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 # Don't cache the prefix for the formulae.brew.sh repository as it won't download its own JSON API files if: github.repository != 'Homebrew/formulae.brew.sh' with: diff --git a/.github/workflows/autogenerated-files.yml b/.github/workflows/autogenerated-files.yml index d6552f5fe2..7a0f2d0684 100644 --- a/.github/workflows/autogenerated-files.yml +++ b/.github/workflows/autogenerated-files.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false diff --git a/.github/workflows/check-prs.yml b/.github/workflows/check-prs.yml index bcf940e995..6c345a687e 100644 --- a/.github/workflows/check-prs.yml +++ b/.github/workflows/check-prs.yml @@ -37,7 +37,6 @@ jobs: pull-requests: write env: GH_TOKEN: ${{ github.token }} - PR_AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} PR_NUMBER: ${{ github.event.pull_request.number }} PR_TEMPLATE_URL: ${{ github.server_url }}/${{ github.repository }}/blob/main/.github/PULL_REQUEST_TEMPLATE.md steps: @@ -79,13 +78,6 @@ jobs: - name: Check pull request template id: template run: | - case "${PR_AUTHOR_ASSOCIATION}" in - OWNER | MEMBER | COLLABORATOR | CONTRIBUTOR) - echo "complete_template=true" >>"${GITHUB_OUTPUT:?}" - exit 0 - ;; - esac - # homebrew-core and homebrew-cask allow condensed PR bodies from recognised bump tools. if [[ "${GITHUB_REPOSITORY}" == "Homebrew/homebrew-core" || "${GITHUB_REPOSITORY}" == "Homebrew/homebrew-cask" ]] @@ -161,7 +153,7 @@ jobs: --raw-field body="$( cat < - Thanks for your pull request. This has been closed because it appears to use an incomplete or outdated pull request template. + Thanks for your pull request. This has been closed because it appears to be missing the pull request template, perhaps because this was written by an AI not a human. We require humans to read and fill in these templates. Please edit this pull request to fill in the current [pull request template](${PR_TEMPLATE_URL:?}). This workflow will reopen this pull request automatically once the template is complete. **Do not open a new pull request for this.** COMMENT diff --git a/.github/workflows/commit-style.yml b/.github/workflows/commit-style.yml index 4b85e48b8b..d6c5cde805 100644 --- a/.github/workflows/commit-style.yml +++ b/.github/workflows/commit-style.yml @@ -3,6 +3,7 @@ name: Commit Style on: pull_request: branches: ["**"] + merge_group: permissions: {} @@ -12,10 +13,9 @@ jobs: permissions: contents: read pull-requests: read - statuses: write steps: - name: Check commit format - uses: Homebrew/actions/check-commit-format@9af03b7ae3f9e2ae5c174f659d5c3909f7e7dbac # 2026.07.29.1 + if: github.event_name == 'pull_request' + uses: Homebrew/actions/check-commit-format@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: - token: ${{ secrets.GITHUB_TOKEN }} check_package_commit_format: false diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 783b5d57bf..e27182c3b4 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: true cask: true @@ -25,7 +25,7 @@ jobs: - run: brew install-bundler-gems --groups=all # install Homebrew formulae we might need - - uses: Homebrew/actions/cache-homebrew-prefix@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + - uses: Homebrew/actions/cache-homebrew-prefix@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: install: shellcheck shfmt gh gnu-tar subversion curl workflow-key: copilot-setup-steps diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 077a5b9ab2..cfc581a31d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -178,7 +178,7 @@ jobs: echo "push=$(jq --raw-output "${filter}" <<<"${PUSH}")" >>"${GITHUB_OUTPUT}" - name: Log in to GitHub Packages (github-actions[bot]) - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: github-actions[bot] @@ -206,7 +206,7 @@ jobs: - name: Log in to GitHub Packages (BrewTestBot) if: fromJSON(steps.attributes.outputs.push) - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: BrewTestBot @@ -265,7 +265,7 @@ jobs: merge-multiple: true - name: Log in to GitHub Packages (BrewTestBot) - uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ghcr.io username: BrewTestBot diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 21a3aacab1..ddd0507a5e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false @@ -54,7 +54,7 @@ jobs: run: vale docs/ - name: Setup Ruby - uses: Homebrew/actions/setup-ruby@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-ruby@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: bundler-cache: true portable-ruby: true @@ -128,7 +128,7 @@ jobs: issues: write # for Homebrew/actions/create-or-update-issue steps: - name: Open, update, or close deploy issue - uses: Homebrew/actions/create-or-update-issue@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/create-or-update-issue@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ github.token }} repository: ${{ github.repository }} diff --git a/.github/workflows/doctor.yml b/.github/workflows/doctor.yml index 1ee34c2c4a..9f3af59ec1 100644 --- a/.github/workflows/doctor.yml +++ b/.github/workflows/doctor.yml @@ -28,7 +28,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false @@ -54,7 +54,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml index 35fbf20d40..791e026b45 100644 --- a/.github/workflows/licenses.yml +++ b/.github/workflows/licenses.yml @@ -36,7 +36,7 @@ jobs: persist-credentials: false - name: Set up Homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 - name: Install git-pkgs run: brew install git-pkgs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63000b1cca..643d3beb2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,13 +77,13 @@ jobs: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - name: Install Pandoc - uses: Homebrew/actions/cache-homebrew-prefix@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/cache-homebrew-prefix@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: install: pandoc workflow-key: release @@ -212,7 +212,7 @@ jobs: fi - name: Generate build provenance - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1 with: subject-path: Homebrew-${{ steps.homebrew-version.outputs.version }}.pkg diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 4578745eb3..d70d7065ab 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -24,19 +24,19 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - name: Configure Git user - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: username: BrewTestBot - name: Set up commit signing if: github.actor != 'dependabot[bot]' - uses: Homebrew/actions/setup-commit-signing@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-commit-signing@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: signing_key: ${{ secrets.BREWTESTBOT_SSH_SIGNING_KEY }} @@ -80,7 +80,7 @@ jobs: - name: Push commits if: steps.update.outputs.committed == 'true' && github.actor != 'dependabot[bot]' - uses: Homebrew/actions/git-try-push@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-try-push@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }} directory: ${{ steps.set-up-homebrew.outputs.repository-path }} @@ -106,7 +106,7 @@ jobs: issues: write steps: - name: Open, update, or close schema issue - uses: Homebrew/actions/create-or-update-issue@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/create-or-update-issue@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: title: Failed to update SBOM schema body: > diff --git a/.github/workflows/sorbet.yml b/.github/workflows/sorbet.yml index 6d5a9d78a1..ef636aee85 100644 --- a/.github/workflows/sorbet.yml +++ b/.github/workflows/sorbet.yml @@ -30,20 +30,20 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - name: Configure Git user if: github.event_name != 'pull_request' - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: username: BrewTestBot - name: Set up commit signing if: github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' - uses: Homebrew/actions/setup-commit-signing@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-commit-signing@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: signing_key: ${{ secrets.BREWTESTBOT_SSH_SIGNING_KEY }} @@ -99,7 +99,7 @@ jobs: - name: Push commits if: steps.commit.outputs.committed == 'true' && github.actor != 'dependabot[bot]' - uses: Homebrew/actions/git-try-push@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-try-push@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }} directory: ${{ steps.set-up-homebrew.outputs.repository-path }} @@ -125,7 +125,7 @@ jobs: issues: write steps: - name: Open, update, or close Sorbet issue - uses: Homebrew/actions/create-or-update-issue@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/create-or-update-issue@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: title: Failed to update RBI files body: > diff --git a/.github/workflows/spdx.yml b/.github/workflows/spdx.yml index 5a98c37d42..d1f655553d 100644 --- a/.github/workflows/spdx.yml +++ b/.github/workflows/spdx.yml @@ -24,19 +24,19 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - name: Configure Git user - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: username: BrewTestBot - name: Set up commit signing if: github.actor != 'dependabot[bot]' - uses: Homebrew/actions/setup-commit-signing@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-commit-signing@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: signing_key: ${{ secrets.BREWTESTBOT_SSH_SIGNING_KEY }} @@ -74,7 +74,7 @@ jobs: - name: Push commits if: steps.update.outputs.committed == 'true' && github.actor != 'dependabot[bot]' - uses: Homebrew/actions/git-try-push@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-try-push@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }} directory: ${{ steps.set-up-homebrew.outputs.repository-path }} @@ -100,7 +100,7 @@ jobs: issues: write steps: - name: Open, update, or close SPDX issue - uses: Homebrew/actions/create-or-update-issue@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/create-or-update-issue@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: title: Failed to update SPDX license data body: > diff --git a/.github/workflows/sponsors-maintainers-man-completions.yml b/.github/workflows/sponsors-maintainers-man-completions.yml index 64f04ab7d4..7acd69b997 100644 --- a/.github/workflows/sponsors-maintainers-man-completions.yml +++ b/.github/workflows/sponsors-maintainers-man-completions.yml @@ -33,18 +33,18 @@ jobs: steps: - name: Setup Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - name: Configure Git user - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: username: BrewTestBot - name: Set up commit signing - uses: Homebrew/actions/setup-commit-signing@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-commit-signing@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: signing_key: ${{ secrets.BREWTESTBOT_SSH_SIGNING_KEY }} @@ -124,7 +124,7 @@ jobs: - name: Push commits if: steps.update.outputs.committed == 'true' - uses: Homebrew/actions/git-try-push@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-try-push@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ secrets.HOMEBREW_GITHUB_PUBLIC_REPO_TOKEN }} directory: ${{ steps.set-up-homebrew.outputs.repository-path }} @@ -149,7 +149,7 @@ jobs: issues: write steps: - name: Open, update, or close sponsors, maintainers, manpage and completions issue - uses: Homebrew/actions/create-or-update-issue@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/create-or-update-issue@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: title: Failed to update sponsors, maintainers, manpage and completions body: > diff --git a/.github/workflows/stale-issues-and-prs.yml b/.github/workflows/stale-issues-and-prs.yml index 6ec2190076..e00ed0e363 100644 --- a/.github/workflows/stale-issues-and-prs.yml +++ b/.github/workflows/stale-issues-and-prs.yml @@ -39,7 +39,7 @@ jobs: pull-requests: write steps: - name: Mark/Close Stale Issues and Pull Requests - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 + uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 21 @@ -69,7 +69,7 @@ jobs: pull-requests: write steps: - name: Mark/Close Stale `bump-formula-pr` and `bump-cask-pr` Pull Requests - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 + uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 2 diff --git a/.github/workflows/sync-default-branches.yml b/.github/workflows/sync-default-branches.yml index 4b59b4fc39..4cd807859f 100644 --- a/.github/workflows/sync-default-branches.yml +++ b/.github/workflows/sync-default-branches.yml @@ -26,7 +26,7 @@ jobs: contents: write steps: - name: Configure Git user - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: username: github-actions[bot] diff --git a/.github/workflows/tap-new-autobump.yml b/.github/workflows/tap-new-autobump.yml new file mode 100644 index 0000000000..a26e7ef978 --- /dev/null +++ b/.github/workflows/tap-new-autobump.yml @@ -0,0 +1,42 @@ +# brew tap-new uses this file to generate an autobump workflow for the new tap +name: tap-new autobump template + +on: + push: + branches: + - TAP_NEW_BRANCH + paths: + - .github/workflows/autobump.yml + schedule: + # this will be changed later and randomised by brew tap-new + - cron: "1 1 1 1 1" + +permissions: {} + +defaults: + run: + shell: bash -xeuo pipefail {0} + +jobs: + autobump: + if: github.repository == '' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Set up Homebrew + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 + + - name: Set up git + id: git_setup + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 + + - name: Bump formulae + env: + HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_GIT_COMMITTER_NAME: ${{ steps.git_setup.outputs.name }} + HOMEBREW_GIT_COMMITTER_EMAIL: ${{ steps.git_setup.outputs.email }} + TAP_NAME: ${{ steps.set-up-homebrew.outputs.tap-name }} + run: brew bump --no-fork --open-pr --formulae --bump-synced --tap="$TAP_NAME" diff --git a/.github/workflows/tap-new-publish.yml b/.github/workflows/tap-new-publish.yml index 5c156398a3..1d42b5300b 100644 --- a/.github/workflows/tap-new-publish.yml +++ b/.github/workflows/tap-new-publish.yml @@ -1,3 +1,4 @@ +# brew tap-new uses this file to generate a bottle publish workflow for the new tap name: tap-new publish template on: @@ -9,6 +10,12 @@ on: head_sha: description: Expected pull request head commit SHA (optional) +permissions: {} + +defaults: + run: + shell: bash -xeuo pipefail {0} + jobs: pr-pull: if: github.repository == '' @@ -24,16 +31,19 @@ jobs: pull-requests: write steps: - name: Set up Homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ secrets.GITHUB_TOKEN }} - name: Set up git - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + id: git_setup + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 - name: Pull bottles env: HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_GIT_COMMITTER_NAME: ${{ steps.git_setup.outputs.name }} + HOMEBREW_GIT_COMMITTER_EMAIL: ${{ steps.git_setup.outputs.email }} # tap-new-github-packages-start HOMEBREW_GITHUB_PACKAGES_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_GITHUB_PACKAGES_USER: ${{ github.repository_owner }} @@ -49,6 +59,6 @@ jobs: fi - name: Push commits - uses: Homebrew/actions/git-try-push@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-try-push@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: branch: TAP_NEW_BRANCH diff --git a/.github/workflows/tap-new-tests.yml b/.github/workflows/tap-new-tests.yml index a404933507..bd46b4bafe 100644 --- a/.github/workflows/tap-new-tests.yml +++ b/.github/workflows/tap-new-tests.yml @@ -1,14 +1,21 @@ +# brew tap-new uses this file to generate a PR test workflow for the new tap name: tap-new tests template on: workflow_dispatch: +permissions: {} + +defaults: + run: + shell: bash -xeuo pipefail {0} + jobs: test-bot: if: github.repository == '' strategy: matrix: - os: [ macos-15-intel, macos-26 ] + os: [ macos-26 ] include: - os: ubuntu-latest container: @@ -27,7 +34,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7d80e98816..4a357a284d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false @@ -50,7 +50,7 @@ jobs: run: brew install-bundler-gems --groups=style,typecheck - name: Install actionlint, shellcheck and shfmt - uses: Homebrew/actions/cache-homebrew-prefix@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/cache-homebrew-prefix@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: install: actionlint shellcheck shfmt workflow-key: tests-syntax @@ -74,7 +74,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: true cask: true @@ -112,7 +112,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: true cask: false @@ -134,7 +134,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: true cask: true @@ -156,13 +156,13 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - name: Configure Git user - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: username: BrewTestBot @@ -197,7 +197,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false @@ -239,7 +239,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: # We only test needs_homebrew_core tests on macOS because # homebrew/core is not available by default on GitHub-hosted Ubuntu @@ -271,14 +271,14 @@ jobs: - name: Install brew tests --online dependencies if: matrix.name == 'tests (online)' - uses: Homebrew/actions/cache-homebrew-prefix@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/cache-homebrew-prefix@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: install: curl subversion workflow-key: tests-tests-online - name: Install brew tests macOS dependencies if: runner.os != 'Linux' - uses: Homebrew/actions/cache-homebrew-prefix@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/cache-homebrew-prefix@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: install: subversion gnupg workflow-key: tests-tests-macos @@ -298,7 +298,6 @@ jobs: run: brew tests ${{ matrix.test-flags }} env: HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HOMEBREW_SANDBOX_LINUX_LANDLOCK: 1 # These cannot be queried at the macOS level on GitHub Actions. HOMEBREW_LANGUAGES: en-GB CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -359,7 +358,6 @@ jobs: - name: test-bot (macOS arm64) runs-on: macos-26 env: - HOMEBREW_SANDBOX_LINUX_LANDLOCK: 1 HOMEBREW_TEST_BOT_ANALYTICS: 1 steps: - name: Install Homebrew and Homebrew's dependencies @@ -399,12 +397,12 @@ jobs: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: true cask: false - - uses: Homebrew/actions/cache-homebrew-prefix@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + - uses: Homebrew/actions/cache-homebrew-prefix@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: install: gnu-tar workflow-key: test-bot @@ -444,7 +442,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false @@ -496,7 +494,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 - name: Cache Homebrew Bundler RubyGems id: cache diff --git a/.github/workflows/vendor-gems.yml b/.github/workflows/vendor-gems.yml index 0e21f4601e..b396c20b19 100644 --- a/.github/workflows/vendor-gems.yml +++ b/.github/workflows/vendor-gems.yml @@ -36,14 +36,14 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false - name: Configure Git user if: github.event_name != 'pull_request' - uses: Homebrew/actions/git-user-config@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-user-config@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: username: github-actions[bot] @@ -96,7 +96,7 @@ jobs: - name: Push to pull request if: github.event_name != 'pull_request' - uses: Homebrew/actions/git-try-push@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/git-try-push@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: token: ${{ github.token }} directory: ${{ steps.set-up-homebrew.outputs.repository-path }} diff --git a/.github/workflows/vendor-version.yml b/.github/workflows/vendor-version.yml index dab8f03881..cd2a405cf9 100644 --- a/.github/workflows/vendor-version.yml +++ b/.github/workflows/vendor-version.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@df4b09108a1de9d6f995fe68f302b3f68bd6d2ef # 2026.07.20.1 + uses: Homebrew/actions/setup-homebrew@fd832223f9f99ebf0244dd20658680e5d4aca049 # 2026.08.03.2 with: core: false cask: false diff --git a/.gitignore b/.gitignore index cb9acc9367..1df912cddb 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ # Ignore dependencies we don't wish to vendor **/vendor/bundle/ruby/*/gems/ast-*/ +**/vendor/bundle/ruby/*/gems/base64-*/ **/vendor/bundle/ruby/*/gems/benchmark-*/ **/vendor/bundle/ruby/*/gems/bigdecimal-*/ **/vendor/bundle/ruby/*/gems/bootsnap-*/ diff --git a/Dockerfile b/Dockerfile index 42eb0a8518..a0634c5699 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ && retry apt-get update --error-on=any \ && apt-get install -y --no-install-recommends \ acl \ - bubblewrap \ bzip2 \ ca-certificates \ curl \ diff --git a/Library/.rubocop.yml b/Library/.rubocop.yml index 556c40c561..b4460e741e 100644 --- a/Library/.rubocop.yml +++ b/Library/.rubocop.yml @@ -71,6 +71,14 @@ Homebrew/CompactBlank: # `blank?` is not necessarily available here: - "Homebrew/extend/enumerable.rb" +Homebrew/NoBase64: + Description: "Use `String#unpack1` or `Array#pack` instead of the `base64` gem." + Include: + - "/**/{Formula,Casks}/**/*.rb" + - "**/{Formula,Casks}/**/*.rb" + Exclude: + - "Homebrew/test/**/*.rb" + Homebrew/NoFileutilsRmrf: Include: - "/**/{Formula,Casks}/**/*.rb" diff --git a/Library/Homebrew/Gemfile b/Library/Homebrew/Gemfile index 19135206b1..ccc7b2efed 100644 --- a/Library/Homebrew/Gemfile +++ b/Library/Homebrew/Gemfile @@ -79,7 +79,6 @@ group :audit, :bump_unversioned_casks, :livecheck, optional: true do end # vendored gems (no group) -gem "base64" gem "concurrent-ruby" gem "patchelf" gem "plist" diff --git a/Library/Homebrew/Gemfile.lock b/Library/Homebrew/Gemfile.lock index f9853ff612..bf08a84903 100644 --- a/Library/Homebrew/Gemfile.lock +++ b/Library/Homebrew/Gemfile.lock @@ -7,7 +7,7 @@ GEM bigdecimal (4.1.2) bindata (2.5.1) concurrent-ruby (1.3.8) - csv (3.3.5) + csv (3.3.6) diff-lcs (1.6.2) drb (2.2.3) elftools (1.3.1) @@ -36,7 +36,7 @@ GEM parser (3.3.12.0) ast (~> 2.4.1) racc - patchelf (1.5.2) + patchelf (1.6.2) elftools (>= 1.3) logger (~> 1) plist (3.7.2) @@ -46,7 +46,7 @@ GEM rbi (0.4.1) prism (~> 1.0) rbs (>= 4.0.1) - rbs (4.0.3) + rbs (4.1.2) logger prism (>= 1.6.0) tsort @@ -115,20 +115,20 @@ GEM rubydex (0.3.0-arm64-darwin) rubydex (0.3.0-x86_64-darwin) rubydex (0.3.0-x86_64-linux) - simplecov (1.0.2) + simplecov (1.0.3) simplecov-cobertura (4.0.0) rexml simplecov (~> 1.0) simpleidn (0.2.3) - sorbet (0.6.13359) - sorbet-static (= 0.6.13359) - sorbet-runtime (0.6.13359) - sorbet-static (0.6.13359-aarch64-linux) - sorbet-static (0.6.13359-universal-darwin) - sorbet-static (0.6.13359-x86_64-linux) - sorbet-static-and-runtime (0.6.13359) - sorbet (= 0.6.13359) - sorbet-runtime (= 0.6.13359) + sorbet (0.6.13365) + sorbet-static (= 0.6.13365) + sorbet-runtime (0.6.13365) + sorbet-static (0.6.13365-aarch64-linux) + sorbet-static (0.6.13365-universal-darwin) + sorbet-static (0.6.13365-x86_64-linux) + sorbet-static-and-runtime (0.6.13365) + sorbet (= 0.6.13365) + sorbet-runtime (= 0.6.13365) spoom (1.8.6) erubi (>= 1.10.0) prism (>= 0.28.0) @@ -170,7 +170,6 @@ PLATFORMS x86_64-linux-gnu DEPENDENCIES - base64 concurrent-ruby csv json_schemer @@ -216,7 +215,7 @@ CHECKSUMS bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd bindata (2.5.1) sha256=53186a1ec2da943d4cb413583d680644eb810aacbf8902497aac8f191fad9e58 concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 - csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373 elftools (1.3.1) sha256=9d46fca49a7d5e821b9c80127b19d59e8ce578bfaa17b99c8ff259222686aaf8 @@ -234,13 +233,13 @@ CHECKSUMS parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 parallel_tests (5.7.0) sha256=3f1762c46ca2c223b8af8ef877217f9d76974e191bfa934f2580b58bcf1d005c parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828 - patchelf (1.5.2) sha256=7efd746a52344be18c44b2fafeb80517c2c12e18dc24ac65d328b7c0609bc386 + patchelf (1.6.2) sha256=b3867281dd5f4089bc656311e5de774515e01f97aa44a27a1536ec1ad4a77bda plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rbi (0.4.1) sha256=66611ca331b0b47d98607a7afda12ab44e0a98297d5393d4b93b846b9786d44d - rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14 + rbs (4.1.2) sha256=050eb1d8b508f1233bed929c0f2c7052302f7adf295230d9cb314e9024078f48 regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb require-hooks (0.4.1) sha256=3a4cd3ed0131b9ec5c2e68f64cb0c5ba56daec6fc363d56e8eef74816c374d7d rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 @@ -267,15 +266,15 @@ CHECKSUMS rubydex (0.3.0-arm64-darwin) sha256=19f685610216e4e7f488722ac90dfbc33113a384dd16db309cf939606f4c32dd rubydex (0.3.0-x86_64-darwin) sha256=59d20c98b0bf2226f5db5bee14cf57075aa93aa0c3fa0176f496a217691ff7b6 rubydex (0.3.0-x86_64-linux) sha256=dfe4026591f226b4a1b53f82f86dc14fad2e7c221ee8a5be99a0a46bf2d58b04 - simplecov (1.0.2) sha256=c6459434efe4b948b46477cc2df2faa73ab365f83a33c7c17f81262f4f7f1244 + simplecov (1.0.3) sha256=38ef0514f16ae7562f0d0f4df02610071115103d301b6de7dacbcc000082e39b simplecov-cobertura (4.0.0) sha256=e4fb3159b1ecea545b44f5452a8611305323e78ad23eae8aed35924d072e01ea simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29 - sorbet (0.6.13359) sha256=772eabba1cadbfdba4cd8416d342e4ad5f195b8e57bf08467cf1fb103fc5a415 - sorbet-runtime (0.6.13359) sha256=5edcd198c678d69f07ec2758b9d0f277bce2e322efd7d5bd902a34c1056a2e5a - sorbet-static (0.6.13359-aarch64-linux) sha256=d6ffbc62c2189d6d02ae2d7b054e78565f404a5d254b34c876dd2adade621f63 - sorbet-static (0.6.13359-universal-darwin) sha256=ebad32899f8cf929589a1932f48670fb4e522ea75a9f4cdfa1395475377282f5 - sorbet-static (0.6.13359-x86_64-linux) sha256=08bb9cc0db2d0a6f9e181af0259c54455bfa66577b1581a4f11e928ad58190bd - sorbet-static-and-runtime (0.6.13359) sha256=cea6c6c1e930becdaadf6bbe732c119441ef206bf16c74e8bc11d11975dcb181 + sorbet (0.6.13365) sha256=3a642fe7afb031ad670c2e6161b3d5492dd67705cbcafb132c9e87ded7ccf676 + sorbet-runtime (0.6.13365) sha256=0657cddfd2319c9695a0b6e13aa7a7c7b093149e5589448c03b911adf1e243cb + sorbet-static (0.6.13365-aarch64-linux) sha256=14b7bf3a227ee102c119763158618bc6b677994cebfda2705b94bca56e4c25e1 + sorbet-static (0.6.13365-universal-darwin) sha256=0f6033dde8a0dc7b7e72ada3ce73e1e3097843be16e473fb8bb2b8fb6199212b + sorbet-static (0.6.13365-x86_64-linux) sha256=f94daadcae55f2e0f7797f8fdc8a250c89ec62e11f2d41905ed6fb1e60ff708e + sorbet-static-and-runtime (0.6.13365) sha256=9768c114686f7cac0a6f2e222db4213887d294df6059802242ba6e4e08adbad9 spoom (1.8.6) sha256=b59ea53b8e258b044c08ef48ef819dd6f12aa386c47a745aa9d8e7fb1ae34a29 stackprof (0.2.28) sha256=4ec2ace02f386012b40ca20ef80c030ad711831f59511da12e83b34efb0f9a04 tapioca (0.19.2) sha256=938731b07811aee8d23871b1aee8861d464fbaf2cfffbf79a62b0c869a5120ec diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index 1ac5c863cf..0bffe3a4e1 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -17,6 +17,7 @@ module Homebrew require "api/internal" require "api/formula_struct" require "api/cask_struct" + require "api/packages_index" extend Utils::Output::Mixin @@ -176,7 +177,10 @@ module Homebrew end # Skip on insecure downloads: their pinned 1970 mtime would make the # source fingerprint ambiguous (and they always re-download anyway). - write_jws_payload_cache(target, json_data, source_stat:) if source_stat && !insecure_download + if source_stat && !insecure_download + write_jws_payload_cache(target, json_data, source_stat:) + write_jws_payload_index_cache(target, json_data, parsed: data, source_stat:) + end [data, !skip_download] else [json_data, !skip_download] @@ -226,7 +230,7 @@ module Homebrew ENV["HOMEBREW_API_UPDATED"] = "1" begin - download_queue.fetch + download_queue.fetch(heading: "Downloading Homebrew API data") ensure download_queue.shutdown end @@ -238,23 +242,26 @@ module Homebrew Homebrew::API::Internal.write_cask_names end - sig { params(names: T::Array[String], type: String, regenerate: T::Boolean).returns(T::Boolean) } - def self.write_names_file!(names, type, regenerate:) + sig { params(type: String, regenerate: T::Boolean, names: T.proc.returns(T::Array[String])).returns(T::Boolean) } + def self.write_names_file!(type, regenerate:, &names) names_path = HOMEBREW_CACHE_API/"#{type}_names.txt" if !names_path.exist? || regenerate names_path.unlink if names_path.exist? - names_path.write(names.sort.join("\n")) + names_path.write(yield.sort.join("\n")) return true end false end - sig { params(aliases: T::Hash[String, String], type: String, regenerate: T::Boolean).returns(T::Boolean) } - def self.write_aliases_file!(aliases, type, regenerate:) + sig { + params(type: String, regenerate: T::Boolean, + aliases: T.proc.returns(T::Hash[String, String])).returns(T::Boolean) + } + def self.write_aliases_file!(type, regenerate:, &aliases) aliases_path = HOMEBREW_CACHE_API/"#{type}_aliases.txt" if !aliases_path.exist? || regenerate - aliases_text = aliases.map do |alias_name, real_name| + aliases_text = yield.map do |alias_name, real_name| "#{alias_name}|#{real_name}" end aliases_path.unlink if aliases_path.exist? @@ -267,12 +274,12 @@ module Homebrew sig { params( - formulae: T::Hash[String, T::Hash[String, T.untyped]], regenerate: T::Boolean, source: Pathname, + formulae: T.proc.returns(T::Hash[String, T::Hash[String, T.untyped]]), ).returns(T::Boolean) } - def self.write_executables_file!(formulae, regenerate:, source:) + def self.write_executables_file!(regenerate:, source:, &formulae) executables_path = HOMEBREW_CACHE_API/"internal/executables.txt" # The file is derived only from the API data in `source`, so it stays # current until that file next changes or is revalidated. @@ -283,7 +290,7 @@ module Homebrew end return false if !regenerate && executables_mtime && source_mtime && source_mtime <= executables_mtime - executables_lines = formulae.filter_map do |name, hash| + executables_lines = yield.filter_map do |name, hash| executables = T.cast(hash["executables"], T.nilable(T::Array[String])) next if executables.blank? @@ -415,15 +422,48 @@ module Homebrew } end + # Returns the verified raw payload bytes, and the envelope stat they were + # validated against, for an internal packages endpoint served entirely + # from a fresh cached envelope's sidecar. Returns nil when a download, + # revalidation or envelope parse is needed instead. + sig { + params(endpoint: String, stale_seconds: T.nilable(Integer)) + .returns(T.nilable([String, File::Stat])) + } + def self.cached_internal_packages_payload(endpoint, stale_seconds:) + target = HOMEBREW_CACHE_API/endpoint + return unless jws_payload_cacheable?(target) + return if !target.exist? || target.empty? + return unless skip_download?(target:, stale_seconds:) + + source_stat = target.stat + payload = cached_jws_payload_string(target, source_stat:) + return if payload.nil? + + [payload, source_stat] + rescue SystemCallError + nil + end + # Loads the signed payload of a `.jws.json` file from the sidecar cache # written after a previous verification, if it still matches the file. # The signature is verified on every load; only re-parsing the much # larger envelope is skipped. sig { params(target: Pathname).returns(T.nilable(T.any(T::Array[T.untyped], T::Hash[String, T.untyped]))) } private_class_method def self.cached_jws_payload(target) + payload = cached_jws_payload_string(target, source_stat: target.stat) + return if payload.nil? + + JSON.parse(payload, freeze: true) + rescue SystemCallError, JSON::ParserError + nil + end + + sig { params(target: Pathname, source_stat: File::Stat).returns(T.nilable(String)) } + private_class_method def self.cached_jws_payload_string(target, source_stat:) return unless jws_payload_cacheable?(target) - expected_fingerprint = jws_source_fingerprint(target.stat) + expected_fingerprint = jws_source_fingerprint(source_stat) jws_payload_cache_path(target).open("rb") do |file| header_line = file.gets @@ -442,12 +482,29 @@ module Homebrew payload = file.read.force_encoding(Encoding::UTF_8) next unless verify_jws_signature(protected_b64, signature_b64, payload).nil? - JSON.parse(payload, freeze: true) + payload end rescue SystemCallError, ArgumentError, JSON::ParserError nil end + # Writes the packages byte-offset index beside the payload sidecar so + # later loads can parse only the entries they need. + sig { + params(target: Pathname, json_data: T.any(T::Array[T.untyped], T::Hash[String, T.untyped]), + parsed: T.any(String, T::Array[T.untyped], T::Hash[String, T.untyped]), + source_stat: File::Stat).void + } + private_class_method def self.write_jws_payload_index_cache(target, json_data, parsed:, source_stat:) + return unless jws_payload_cacheable?(target) + return unless json_data.is_a?(Hash) + + payload = json_data["payload"] + return if !payload.is_a?(String) || !parsed.is_a?(Hash) + + PackagesIndex.write!(target, payload:, parsed:, source_stat:) + end + sig { params(target: Pathname, json_data: T.any(T::Array[T.untyped], T::Hash[String, T.untyped]), source_stat: File::Stat).void @@ -504,12 +561,12 @@ module Homebrew sig { returns(T::Array[String]) } def self.formula_names - Homebrew::API::Internal.formula_hashes.keys + Homebrew::API::Internal.formula_names end sig { params(name: String).returns(T::Boolean) } def self.formula_name?(name) - Homebrew::API::Internal.formula_hashes.key?(name) + Homebrew::API::Internal.formula_name?(name) end sig { returns(T::Hash[String, String]) } @@ -529,12 +586,12 @@ module Homebrew sig { returns(T::Array[String]) } def self.cask_tokens - Homebrew::API::Internal.cask_hashes.keys + Homebrew::API::Internal.cask_names end sig { params(token: String).returns(T::Boolean) } def self.cask_token?(token) - Homebrew::API::Internal.cask_hashes.key?(token) + Homebrew::API::Internal.cask_name?(token) end sig { returns(T::Hash[String, String]) } diff --git a/Library/Homebrew/api/cask.rb b/Library/Homebrew/api/cask.rb index 5942480aa1..53d58d3b91 100644 --- a/Library/Homebrew/api/cask.rb +++ b/Library/Homebrew/api/cask.rb @@ -148,7 +148,7 @@ module Homebrew def self.write_names(regenerate: false) download_and_cache_data! unless cache.key?("casks") - Homebrew::API.write_names_file!(all_casks.keys, "cask", regenerate:) + Homebrew::API.write_names_file!("cask", regenerate:) { all_casks.keys } end end end diff --git a/Library/Homebrew/api/cask/cask_struct_generator.rb b/Library/Homebrew/api/cask/cask_struct_generator.rb index 1dbb38b3aa..c9657d381f 100644 --- a/Library/Homebrew/api/cask/cask_struct_generator.rb +++ b/Library/Homebrew/api/cask/cask_struct_generator.rb @@ -145,7 +145,7 @@ module Homebrew sig { params(artifacts: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Array[CaskStruct::ArtifactArgs]) } def process_artifacts(artifacts) artifacts.map do |artifact| - key = T.must(artifact.keys.first) + key = T.must(artifact.keys.find { |artifact_key| artifact_key != :target }) # Pass an empty block to artifacts like postflight that can't be loaded from the API, # but need to be set to something. diff --git a/Library/Homebrew/api/formula.rb b/Library/Homebrew/api/formula.rb index 3e97afb1f8..69f6c9998f 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -192,9 +192,9 @@ module Homebrew def self.write_names_and_aliases(regenerate: false) download_and_cache_data! unless cache.key?("formulae") - Homebrew::API.write_names_file!(all_formulae.keys, "formula", regenerate:) - Homebrew::API.write_aliases_file!(all_aliases, "formula", regenerate:) - Homebrew::API.write_executables_file!(all_formulae, regenerate:, source: cached_json_file_path) + Homebrew::API.write_names_file!("formula", regenerate:) { all_formulae.keys } + Homebrew::API.write_aliases_file!("formula", regenerate:) { all_aliases } + Homebrew::API.write_executables_file!(regenerate:, source: cached_json_file_path) { all_formulae } end end end diff --git a/Library/Homebrew/api/internal.rb b/Library/Homebrew/api/internal.rb index 9102894558..dd023a4c0d 100644 --- a/Library/Homebrew/api/internal.rb +++ b/Library/Homebrew/api/internal.rb @@ -3,6 +3,7 @@ require "cachable" require "api" +require "api/packages_index" module Homebrew module API @@ -34,7 +35,7 @@ module Homebrew def self.formula_struct(name) return cache["formula_structs"][name] if cache.key?("formula_structs") && cache["formula_structs"].key?(name) - hash = formula_hashes[name] + hash = formula_hash(name) raise "No formula found for #{name}" unless hash struct = Homebrew::API::FormulaStruct.deserialize(hash, bottle_tag: effective_tag) @@ -49,7 +50,7 @@ module Homebrew def self.cask_struct(name) return cache["cask_structs"][name] if cache.key?("cask_structs") && cache["cask_structs"].key?(name) - hash = cask_hashes[name] + hash = cask_hash(name) raise "No cask found for #{name}" unless hash struct = Homebrew::API::CaskStruct.deserialize(hash) @@ -67,12 +68,13 @@ module Homebrew sig { params(download_queue: DownloadQueueType, stale_seconds: T.nilable(Integer), enqueue: T::Boolean) - .returns([T::Hash[String, T.untyped], T::Boolean]) + .returns([T.any(T::Hash[String, T.untyped], Homebrew::API::PackagesIndex), T::Boolean]) } def self.fetch_packages_api!(download_queue: nil, stale_seconds: nil, enqueue: false) old_failed = Homebrew.failed? json_contents, updated = begin - Homebrew::API.fetch_json_api_file(packages_endpoint, stale_seconds:, download_queue:, enqueue:) + cached_packages_index(stale_seconds:, enqueue:) || + Homebrew::API.fetch_json_api_file(packages_endpoint, stale_seconds:, download_queue:, enqueue:) rescue ErrorDuringExecution => e raise if e.stderr.exclude?("HTTP status: 404") || effective_tag == fallback_tag @@ -81,7 +83,32 @@ module Homebrew retry end - [T.cast(json_contents, T::Hash[String, T.untyped]), updated] + [T.cast(json_contents, T.any(T::Hash[String, T.untyped], Homebrew::API::PackagesIndex)), updated] + end + + # Serves a fresh cached packages payload through its byte-offset index + # so only the entries that get used are parsed, building the index + # after a full parse when it is missing or stale. + sig { + params(stale_seconds: T.nilable(Integer), enqueue: T::Boolean) + .returns(T.nilable([T.any(T::Hash[String, T.untyped], Homebrew::API::PackagesIndex), T::Boolean])) + } + private_class_method def self.cached_packages_index(stale_seconds:, enqueue:) + return if enqueue + + cached = Homebrew::API.cached_internal_packages_payload(packages_endpoint, stale_seconds:) + return if cached.nil? + + payload, source_stat = cached + target = cached_packages_json_file_path + index = Homebrew::API::PackagesIndex.load(target, payload:, source_stat:) + return [index, false] if index + + parsed = JSON.parse(payload, freeze: true) + return unless parsed.is_a?(Hash) + + Homebrew::API::PackagesIndex.write!(target, payload:, parsed:, source_stat:) + [parsed, false] end sig { returns(T::Boolean) } @@ -89,6 +116,19 @@ module Homebrew json_contents, updated = fetch_packages_api! cache["formula_structs"] = {} cache["cask_structs"] = {} + if json_contents.is_a?(Homebrew::API::PackagesIndex) + cache["packages_index"] = json_contents + else + cache_parsed_packages!(json_contents) + end + + updated + end + private_class_method :download_and_cache_data! + + sig { params(json_contents: T::Hash[String, T.untyped]).void } + private_class_method def self.cache_parsed_packages!(json_contents) + cache.delete("packages_index") cache["formula_aliases"] = json_contents["formula_aliases"] cache["formula_renames"] = json_contents["formula_renames"] cache["cask_renames"] = json_contents["cask_renames"] @@ -98,122 +138,192 @@ module Homebrew cache["cask_tap_migrations"] = json_contents["cask_tap_migrations"] cache["formula_hashes"] = json_contents["formulae"] cache["cask_hashes"] = json_contents["casks"] - - updated end - private_class_method :download_and_cache_data! + + # Replaces a cached index with fully parsed payload data, for callers + # that need every entry or when index validation fails. + sig { void } + private_class_method def self.materialize_packages_index! + index = cache.delete("packages_index") + return unless index.is_a?(Homebrew::API::PackagesIndex) + + parsed = JSON.parse(index.payload, freeze: true) + return unless parsed.is_a?(Hash) + + cache_parsed_packages!(parsed) + Homebrew::API::PackagesIndex.write!(cached_packages_json_file_path, payload: index.payload, parsed:, + source_stat: index.source_stat) + end + + sig { returns(T::Boolean) } + private_class_method def self.data_loaded? + cache.key?("formula_hashes") || cache.key?("packages_index") + end + + sig { void } + private_class_method def self.ensure_formula_data! + return if data_loaded? + + updated = download_and_cache_data! + write_formula_names_and_aliases(regenerate: updated) + end + + sig { void } + private_class_method def self.ensure_cask_data! + return if data_loaded? + + updated = download_and_cache_data! + write_cask_names(regenerate: updated) + end + + sig { params(key: String).returns(T.untyped) } + private_class_method def self.packages_value(key) + return cache[key] if cache.key?(key) + + cache[key] = cache["packages_index"].top_level_value(key) + rescue Homebrew::API::PackagesIndex::Invalid + materialize_packages_index! + cache[key] + end sig { params(regenerate: T::Boolean).void } def self.write_formula_names_and_aliases(regenerate: false) - download_and_cache_data! unless cache.key?("formula_hashes") + download_and_cache_data! unless data_loaded? - Homebrew::API.write_names_file!(formula_hashes.keys, "formula", regenerate:) - Homebrew::API.write_aliases_file!(formula_aliases, "formula", regenerate:) - Homebrew::API.write_executables_file!(formula_hashes, regenerate:, source: cached_packages_json_file_path) + Homebrew::API.write_names_file!("formula", regenerate:) { formula_names } + Homebrew::API.write_aliases_file!("formula", regenerate:) { formula_aliases } + Homebrew::API.write_executables_file!(regenerate:, source: cached_packages_json_file_path) { formula_hashes } end sig { params(regenerate: T::Boolean).void } def self.write_cask_names(regenerate: false) - download_and_cache_data! unless cache.key?("cask_hashes") + download_and_cache_data! unless data_loaded? - Homebrew::API.write_names_file!(cask_hashes.keys, "cask", regenerate:) + Homebrew::API.write_names_file!("cask", regenerate:) { cask_names } end - # Whether formula hashes are already loaded, so callers can use them - # opportunistically without triggering a download and full JSON parse. + # Whether internal packages API data is already loaded, as full hashes + # or a byte-offset index, so callers can use it opportunistically + # without triggering a download and full JSON parse. sig { returns(T::Boolean) } def self.formula_hashes_cached? - cache.key?("formula_hashes") + data_loaded? end sig { returns(T::Hash[String, T::Hash[String, T.untyped]]) } def self.formula_hashes - unless cache.key?("formula_hashes") - updated = download_and_cache_data! - write_formula_names_and_aliases(regenerate: updated) - end + ensure_formula_data! + materialize_packages_index! unless cache.key?("formula_hashes") cache["formula_hashes"] end + sig { params(name: String).returns(T.nilable(T::Hash[String, T.untyped])) } + def self.formula_hash(name) + ensure_formula_data! + return cache["formula_hashes"][name] if cache.key?("formula_hashes") + + begin + cache["packages_index"].formula_hash(name) + rescue Homebrew::API::PackagesIndex::Invalid + materialize_packages_index! + cache["formula_hashes"][name] + end + end + + sig { returns(T::Array[String]) } + def self.formula_names + ensure_formula_data! + return cache["formula_hashes"].keys if cache.key?("formula_hashes") + + cache["packages_index"].formula_names + end + + sig { params(name: String).returns(T::Boolean) } + def self.formula_name?(name) + ensure_formula_data! + return cache["formula_hashes"].key?(name) if cache.key?("formula_hashes") + + cache["packages_index"].formula_name?(name) + end + sig { returns(T::Hash[String, String]) } def self.formula_aliases - unless cache.key?("formula_aliases") - updated = download_and_cache_data! - write_formula_names_and_aliases(regenerate: updated) - end - - cache["formula_aliases"] + ensure_formula_data! + packages_value("formula_aliases") end sig { returns(T::Hash[String, String]) } def self.formula_renames - unless cache.key?("formula_renames") - updated = download_and_cache_data! - write_formula_names_and_aliases(regenerate: updated) - end - - cache["formula_renames"] + ensure_formula_data! + packages_value("formula_renames") end sig { returns(T::Hash[String, String]) } def self.formula_tap_migrations - unless cache.key?("formula_tap_migrations") - updated = download_and_cache_data! - write_formula_names_and_aliases(regenerate: updated) - end - - cache["formula_tap_migrations"] + ensure_formula_data! + packages_value("formula_tap_migrations") end sig { returns(String) } def self.formula_tap_git_head - unless cache.key?("formula_tap_git_head") - updated = download_and_cache_data! - write_formula_names_and_aliases(regenerate: updated) - end - - cache["formula_tap_git_head"] + ensure_formula_data! + packages_value("formula_tap_git_head") end sig { returns(T::Hash[String, T::Hash[String, T.untyped]]) } def self.cask_hashes - unless cache.key?("cask_hashes") - updated = download_and_cache_data! - write_cask_names(regenerate: updated) - end + ensure_cask_data! + materialize_packages_index! unless cache.key?("cask_hashes") cache["cask_hashes"] end + sig { params(name: String).returns(T.nilable(T::Hash[String, T.untyped])) } + def self.cask_hash(name) + ensure_cask_data! + return cache["cask_hashes"][name] if cache.key?("cask_hashes") + + begin + cache["packages_index"].cask_hash(name) + rescue Homebrew::API::PackagesIndex::Invalid + materialize_packages_index! + cache["cask_hashes"][name] + end + end + + sig { returns(T::Array[String]) } + def self.cask_names + ensure_cask_data! + return cache["cask_hashes"].keys if cache.key?("cask_hashes") + + cache["packages_index"].cask_names + end + + sig { params(name: String).returns(T::Boolean) } + def self.cask_name?(name) + ensure_cask_data! + return cache["cask_hashes"].key?(name) if cache.key?("cask_hashes") + + cache["packages_index"].cask_name?(name) + end + sig { returns(T::Hash[String, String]) } def self.cask_renames - unless cache.key?("cask_renames") - updated = download_and_cache_data! - write_cask_names(regenerate: updated) - end - - cache["cask_renames"] + ensure_cask_data! + packages_value("cask_renames") end sig { returns(T::Hash[String, String]) } def self.cask_tap_migrations - unless cache.key?("cask_tap_migrations") - updated = download_and_cache_data! - write_cask_names(regenerate: updated) - end - - cache["cask_tap_migrations"] + ensure_cask_data! + packages_value("cask_tap_migrations") end sig { returns(String) } def self.cask_tap_git_head - unless cache.key?("cask_tap_git_head") - updated = download_and_cache_data! - write_cask_names(regenerate: updated) - end - - cache["cask_tap_git_head"] + ensure_cask_data! + packages_value("cask_tap_git_head") end end end diff --git a/Library/Homebrew/api/packages_index.rb b/Library/Homebrew/api/packages_index.rb new file mode 100644 index 0000000000..e2c37ac3a4 --- /dev/null +++ b/Library/Homebrew/api/packages_index.rb @@ -0,0 +1,299 @@ +# typed: strict +# frozen_string_literal: true + +module Homebrew + module API + # Byte-offset index into a signature-verified internal packages JWS + # payload, so commands can parse only the entries they need instead of + # the whole multi-megabyte document. + # + # The index is derived, unverified cache data guarded in layers: the + # payload bytes it points into are signature-verified on every run, + # loading requires the recorded top-level spans to tile that payload + # exactly (so the formulae and casks section spans are provably the + # real top-level values) and every lookup revalidates that its offsets + # sit at the expected `"":` key inside the requested section's + # span and that the slice parses. A forged or stale index therefore + # cannot inject unverified content or remap a name to another entry, + # even a matching key in the other section; it fails validation and + # callers fall back to a full parse when {Invalid} is raised. + class PackagesIndex + FORMAT_VERSION = 1 + SECTION_KEYS = %w[formulae casks].freeze + # Bounds index building when payload bytes stop round-tripping through + # `JSON.generate`; giving up just means no index is written. + MAX_FALSE_MATCH_RETRIES = 100 + + # Raised when index contents do not match the verified payload. + class Invalid < RuntimeError; end + + sig { params(target: Pathname).returns(Pathname) } + def self.path_for(target) + Pathname("#{target}.payload.index") + end + + sig { params(stat: File::Stat).returns(T::Hash[String, Integer]) } + def self.source_fingerprint(stat) + { + "source_size" => stat.size, + "source_mtime_ns" => (stat.mtime.to_r * 1_000_000_000).to_i, + } + end + + sig { params(target: Pathname, payload: String, source_stat: File::Stat).returns(T.nilable(PackagesIndex)) } + def self.load(target, payload:, source_stat:) + data = JSON.parse(path_for(target).read(encoding: Encoding::UTF_8)) + return unless data.is_a?(Hash) + return if data["version"] != FORMAT_VERSION + return if source_fingerprint(source_stat).any? { |key, value| data[key] != value } + return if data["payload_bytesize"] != payload.bytesize + + top_level = data["top_level"] + sections = data.slice(*SECTION_KEYS) + return unless top_level.is_a?(Hash) + return unless sections.values.all?(Hash) + return unless top_level_spans_tile_payload?(payload, top_level) + + new(payload:, source_stat:, top_level:, sections:) + rescue SystemCallError, JSON::ParserError + nil + end + + # The recorded top-level spans must reconstruct the payload's + # top-level object exactly: starting at the opening brace, each span + # is immediately preceded by its own comma-separated JSON key and the + # last ends at the closing brace. This proves every span, including + # the section spans entry lookups are bounded by, is the real + # top-level value for its key rather than an arbitrary or inflated + # byte range. + sig { params(payload: String, top_level: T::Hash[String, T.untyped]).returns(T::Boolean) } + private_class_method def self.top_level_spans_tile_payload?(payload, top_level) + return false if payload.byteslice(0, 1) != "{" + + spans = top_level.map do |key, location| + offset, bytesize = location + return false if !offset.is_a?(Integer) || !bytesize.is_a?(Integer) || bytesize.negative? + + [key.to_s, offset, bytesize] + end + spans.sort_by! { |_, offset, _| offset } + + position = 1 + spans.each_with_index do |(key, offset, bytesize), index| + key_bytes = "#{key.to_json}:" + key_bytes = ",#{key_bytes}" if index.positive? + return false if payload.byteslice(position, key_bytes.bytesize) != key_bytes + return false if position + key_bytes.bytesize != offset + + position = offset + bytesize + end + + position + 1 == payload.bytesize && payload.byteslice(position, 1) == "}" + end + + # Builds and persists an index for a freshly verified and parsed + # payload. Failing to build or write one only costs the fast path. + sig { + params(target: Pathname, payload: String, parsed: T::Hash[String, T.untyped], + source_stat: File::Stat).void + } + def self.write!(target, payload:, parsed:, source_stat:) + # Never write to a user-owned cache as root, matching `skip_download?`. + return if Homebrew.running_as_root_but_not_owned_by_root? + return if (data = build(payload:, parsed:)).nil? + + data = { + "version" => FORMAT_VERSION, + **source_fingerprint(source_stat), + "payload_bytesize" => payload.bytesize, + **data, + } + index_path = path_for(target) + temporary_path = Pathname("#{index_path}.tmp") + begin + temporary_path.write(JSON.generate(data)) + File.rename(temporary_path, index_path) + ensure + temporary_path.unlink if temporary_path.exist? + end + rescue SystemCallError + nil + end + + # Locates every top-level value and every formula and cask entry in the + # payload bytes. Offsets are found by searching for each JSON key in + # document order and validating that the following bytes byte-match the + # entry's `JSON.generate` round trip, so every recorded offset provably + # reproduces the canonical parse. + sig { + params(payload: String, parsed: T::Hash[String, T.untyped]) + .returns(T.nilable(T::Hash[String, T::Hash[String, [Integer, Integer]]])) + } + def self.build(payload:, parsed:) + data = T.let({ "top_level" => {} }, T::Hash[String, T::Hash[String, [Integer, Integer]]]) + SECTION_KEYS.each { |section| data[section] = {} } + retries = 0 + position = 0 + + parsed.each do |key, value| + location = locate(payload, key, value, position) + return nil if location.nil? + + value_start, value_bytesize = location + T.must(data["top_level"])[key] = [value_start, value_bytesize] + + if SECTION_KEYS.include?(key) && value.is_a?(Hash) + entry_position = value_start + value.each do |name, entry| + entry_location = T.let(nil, T.nilable([Integer, Integer])) + loop do + entry_location = locate(payload, name, entry, entry_position) + break unless entry_location.nil? + + retries += 1 + return nil if retries > MAX_FALSE_MATCH_RETRIES + + next_position = payload.byteindex("#{name.to_json}:", entry_position) + return nil if next_position.nil? + + entry_position = next_position + 1 + end + + entry_start, entry_bytesize = entry_location + T.must(data[key])[name] = [entry_start, entry_bytesize] + entry_position = entry_start + entry_bytesize + end + end + + position = value_start + value_bytesize + end + + data + end + + # Finds `"":` at or after `position`, returning the value's + # byte offset and length only when the payload bytes match the value's + # canonical serialisation exactly. + sig { + params(payload: String, key: String, value: T.untyped, position: Integer) + .returns(T.nilable([Integer, Integer])) + } + private_class_method def self.locate(payload, key, value, position) + key_bytes = "#{key.to_json}:" + key_position = payload.byteindex(key_bytes, position) + return if key_position.nil? + + value_bytes = JSON.generate(value) + value_start = key_position + key_bytes.bytesize + return if payload.byteslice(value_start, value_bytes.bytesize) != value_bytes + + [value_start, value_bytes.bytesize] + end + + sig { returns(String) } + attr_reader :payload + + sig { returns(File::Stat) } + attr_reader :source_stat + + sig { + params(payload: String, source_stat: File::Stat, top_level: T::Hash[String, T.untyped], + sections: T::Hash[String, T::Hash[String, T.untyped]]).void + } + def initialize(payload:, source_stat:, top_level:, sections:) + @payload = payload + @source_stat = source_stat + @top_level = top_level + @sections = sections + end + + sig { params(name: String).returns(T.nilable(T::Hash[String, T.untyped])) } + def formula_hash(name) + entry_value("formulae", name) + end + + sig { params(name: String).returns(T.nilable(T::Hash[String, T.untyped])) } + def cask_hash(name) + entry_value("casks", name) + end + + sig { returns(T::Array[String]) } + def formula_names + @sections.fetch("formulae", {}).keys + end + + sig { returns(T::Array[String]) } + def cask_names + @sections.fetch("casks", {}).keys + end + + sig { params(name: String).returns(T::Boolean) } + def formula_name?(name) + @sections.fetch("formulae", {}).key?(name) + end + + sig { params(name: String).returns(T::Boolean) } + def cask_name?(name) + @sections.fetch("casks", {}).key?(name) + end + + sig { params(key: String).returns(T.untyped) } + def top_level_value(key) + return if SECTION_KEYS.include?(key) + + location = @top_level[key] + return if location.nil? + + slice_value(key, location) + end + + private + + sig { params(section: String, name: String).returns(T.nilable(T::Hash[String, T.untyped])) } + def entry_value(section, name) + location = @sections.fetch(section, {})[name] + return if location.nil? + + section_location = @top_level[section] + raise Invalid, "no #{section} span for the #{name} index entry" unless section_location.is_a?(Array) + + value = slice_value(name, location, within: section_location) + raise Invalid, "#{section} index entry for #{name} is not a hash" unless value.is_a?(Hash) + + value + end + + # Revalidates a recorded location against the verified payload bytes: + # it must be preceded by the expected JSON key, sit inside the given + # load-validated span and parse cleanly. + sig { params(name: String, location: T.untyped, within: T.untyped).returns(T.untyped) } + def slice_value(name, location, within: nil) + offset, bytesize = location + key_bytes = "#{name.to_json}:" + key_offset = offset - key_bytes.bytesize if offset.is_a?(Integer) + if !offset.is_a?(Integer) || !bytesize.is_a?(Integer) || + key_offset.nil? || key_offset.negative? || (offset + bytesize) > payload.bytesize || + payload.byteslice(key_offset, key_bytes.bytesize) != key_bytes || + outside_span?(key_offset, offset + bytesize, within) + raise Invalid, "index location for #{name} does not match the payload" + end + + begin + JSON.parse(T.must(payload.byteslice(offset, bytesize)), freeze: true) + rescue JSON::ParserError + raise Invalid, "index slice for #{name} does not parse" + end + end + + sig { params(start_offset: Integer, end_offset: Integer, within: T.untyped).returns(T::Boolean) } + def outside_span?(start_offset, end_offset, within) + return false if within.nil? + + within_offset, within_bytesize = within + return true if !within_offset.is_a?(Integer) || !within_bytesize.is_a?(Integer) + + start_offset < within_offset || end_offset > within_offset + within_bytesize + end + end + end +end diff --git a/Library/Homebrew/bottle.rb b/Library/Homebrew/bottle.rb index 7464a9d233..8c30d3ae2d 100644 --- a/Library/Homebrew/bottle.rb +++ b/Library/Homebrew/bottle.rb @@ -179,7 +179,7 @@ class Bottle sig { returns(T::Boolean) } def skip_relocation? attrs = tab_attributes - tab = Tab.new(attrs) unless attrs.empty? + tab = Tab.new(**attrs.transform_keys(&:to_sym)) unless attrs.empty? @spec.skip_relocation?(tag: @tag, tab:) end diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index 4097b38d62..80101b8589 100644 --- a/Library/Homebrew/brew.sh +++ b/Library/Homebrew/brew.sh @@ -241,13 +241,6 @@ then export HOMEBREW_COLOR="1" fi -# This is set by Homebrew's self-hosted runner environment. -# shellcheck disable=SC2154 -if [[ -n "${HOMEBREW_LINUX}" && -n "${GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED}" ]] -then - export HOMEBREW_SANDBOX_LINUX_LANDLOCK="1" -fi - setup-locale ##### diff --git a/Library/Homebrew/bundle/subcommand/cleanup.rb b/Library/Homebrew/bundle/subcommand/cleanup.rb index 443240c2e5..489959ce6b 100644 --- a/Library/Homebrew/bundle/subcommand/cleanup.rb +++ b/Library/Homebrew/bundle/subcommand/cleanup.rb @@ -23,13 +23,16 @@ module Homebrew This workflow is useful for maintainers or testers who regularly install lots of formulae. + When cleanup is performed, Homebrew's global trust store is reset to the trust values declared by the `Brewfile`, removing trust entries not declared there. + Unless `--force` is passed, this prompts before removing anything and returns a 1 exit code if the prompt is declined or cannot be shown. EOS named_args :none switch "--install", description: "Run `install` before cleaning up dependencies." switch "-f", "--force", - description: "Actually perform cleanup operations." + description: "Actually perform cleanup operations and reset Homebrew's global trust store " \ + "to the `Brewfile` values." switch "--all", description: "Clean up all supported dependencies." switch "--formula", "--formulae", "--brews", diff --git a/Library/Homebrew/cask/artifact/abstract_artifact.rb b/Library/Homebrew/cask/artifact/abstract_artifact.rb index b5176887de..52363afb37 100644 --- a/Library/Homebrew/cask/artifact/abstract_artifact.rb +++ b/Library/Homebrew/cask/artifact/abstract_artifact.rb @@ -203,7 +203,6 @@ module Cask sig { returns(T.nilable(Sandbox)) } def cask_sandbox - Sandbox.ensure_sandbox_installed! return unless Sandbox.available? Sandbox.new.tap do |sandbox| diff --git a/Library/Homebrew/cask/artifact/symlinked.rb b/Library/Homebrew/cask/artifact/symlinked.rb index 67781e6909..991cc6e93d 100644 --- a/Library/Homebrew/cask/artifact/symlinked.rb +++ b/Library/Homebrew/cask/artifact/symlinked.rb @@ -79,6 +79,9 @@ module Cask (target.realpath == source.realpath || target.realpath.to_s.start_with?("#{cask.caskroom_path}/")) opoo "#{message}; overwriting." Utils.gain_permissions_remove(target, command:) + elsif target_links_to_source? + ohai "#{self.class.english_name} '#{source.basename}' is already linked to '#{target}'" + return elsif (formula = conflicting_formula) opoo "#{message} from formula #{formula}; skipping link." return @@ -113,6 +116,14 @@ module Cask sudo: !target.dirname.writable? end + sig { returns(T::Boolean) } + def target_links_to_source? + target.symlink? && target.realpath == source.realpath + rescue => e + odebug "Error checking whether #{target} links to #{source}: #{e}" + false + end + # Check if the target file is a symlink that originates from a formula # with the same name as this cask, indicating a potential conflict sig { returns(T.nilable(String)) } diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index d48a17d258..df85c24e90 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -686,6 +686,55 @@ module Cask end end + sig { void } + def audit_artifact_case + return if (url = cask.url).nil? + return unless online? + + odebug "Auditing artifact case" + + extract_artifacts(include_manual_installers: true) do |artifacts, tmpdir| + artifacts.each do |artifact| + source = case artifact + when Artifact::Pkg, Artifact::Installer + artifact.path + else + artifact.source + end + + source = if source.to_s.start_with?("#{cask.appdir}/") + Pathname(source.to_s.delete_prefix("#{cask.appdir}/")) + elsif source.absolute? + source.relative_path_from(cask.staged_path) + else + source + end + + components = source.each_filename.to_a + current = tmpdir + on_disk = [] + components.each do |component| + break unless current.directory? + + children = current.children.map { |child| child.basename.to_s } + match = children.find { |name| name == component } || + children.find { |name| name.casecmp?(component) } + break if match.nil? + + on_disk << match + current /= match + end + + next if on_disk.length != components.length + next if on_disk == components + + add_error "Artifact #{source} does not match the case of the extracted " \ + "#{File.join(on_disk)}; this fails on case-sensitive filesystems.", + location: url.location + end + end + end + sig { void } def audit_rosetta return if (url = cask.url).nil? diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index edffe9b8e1..0df140a958 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -121,11 +121,14 @@ module Cask @default_config = T.let(config || Config.new, Config) - @config = T.let(if config_path.exist? - Config.from_json(File.read(config_path), ignore_invalid_keys: true) - else - @default_config - end, Config) + @config = T.let( + if config_path.exist? + Config.from_json(File.read(config_path), ignore_invalid_keys: true) + else + @default_config + end, + Config, + ) refresh end @@ -613,7 +616,7 @@ module Cask if dsl!.on_system_blocks_exist? begin OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag| - next if bottle_tag.linux? && dsl!.os.nil? && !dsl!.sha256_set_for_linux? + next if bottle_tag.linux? && dsl!.os.nil? && !dsl!.sha256_set_for_linux? && !dsl!.on_linux_blocks_exist? macos_requirements = [depends_on.macos, depends_on.maximum_macos].compact next if bottle_tag.macos? && diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index bb2466e694..258e7a9930 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -456,7 +456,9 @@ module Cask sig { params(config: T.nilable(Config)).returns(Cask) } def load_from_internal_api(config:) cask_struct = Homebrew::API::Internal.cask_struct(token) - api_source = Homebrew::API::Internal.cask_hashes.fetch(token) + api_source = Homebrew::API::Internal.cask_hash(token) + raise KeyError, "key not found: #{token.inspect}" if api_source.nil? + tap_git_head = Homebrew::API::Internal.cask_tap_git_head load_from_struct(config:, cask_struct:, api_source:, tap_git_head:, internal_api: true) diff --git a/Library/Homebrew/cask/dsl.rb b/Library/Homebrew/cask/dsl.rb index 8b2b516bee..76cff2be9c 100644 --- a/Library/Homebrew/cask/dsl.rb +++ b/Library/Homebrew/cask/dsl.rb @@ -128,6 +128,7 @@ module Cask :no_autobump_message, :on_system_blocks_exist?, :on_os_blocks_exist?, + :on_linux_blocks_exist?, :on_system_block_min_os, :depends_on_set_in_block?, *ORDINARY_ARTIFACT_CLASSES.map(&:dsl_key), @@ -197,6 +198,7 @@ module Cask @auto_updates_set_in_block = T.let(false, T::Boolean) @autobump = T.let(true, T::Boolean) @called_in_on_system_block = T.let(false, T::Boolean) + @called_in_on_os_block = T.let(false, T::Boolean) @cask = cask @caveats = T.let(DSL::Caveats.new(cask), DSL::Caveats) @conflicts_with = T.let(nil, T.nilable(DSL::ConflictsWith)) @@ -231,6 +233,7 @@ module Cask @no_autobump_message = T.let(nil, T.nilable(T.any(String, Symbol))) @on_system_blocks_exist = T.let(false, T::Boolean) @on_os_blocks_exist = T.let(false, T::Boolean) + @on_linux_blocks_exist = T.let(false, T::Boolean) @on_system_block_min_os = T.let(nil, T.nilable(MacOSVersion)) @os = T.let(nil, T.nilable(String)) @os_set_in_block = T.let(false, T::Boolean) @@ -264,6 +267,9 @@ module Cask sig { returns(T::Boolean) } def on_os_blocks_exist? = @on_os_blocks_exist + sig { returns(T::Boolean) } + def on_linux_blocks_exist? = @on_linux_blocks_exist + sig { returns(T::Boolean) } def sha256_set_for_linux? = @sha256_set_for_linux @@ -570,13 +576,16 @@ module Cask # running OS, matching `sha256` inside an `on_macos`/`on_linux` block; # `depends_on` governs whether the cask is usable there. A checksum # declared for the running OS but missing the running architecture - # still raises. + # still raises on the real system but is nil under simulation so + # API variations can be generated for the missing architecture. running_os_checksums = if OnSystem.os_condition_met?(:linux) [x86_64_linux, arm64_linux] else [arm, x86_64] end - raise CaskInvalidError.new(cask, "invalid 'sha256' value: nil") if running_os_checksums.any?(&:present?) + if running_os_checksums.any?(&:present?) && !Homebrew::SimulateSystem.simulating? + raise CaskInvalidError.new(cask, "invalid 'sha256' value: nil") + end nil else @@ -653,7 +662,10 @@ module Cask return @depends_on if kwargs.empty? begin - @depends_on.load(kwargs, set_in_block: @called_in_on_system_block) + # Only OS blocks scope a dependency to one OS: `on_arm`/`on_intel` + # blocks are evaluated on every OS, so a macOS dependency inside one + # applies everywhere and marks the cask macOS-only. + @depends_on.load(kwargs, set_in_block: @called_in_on_system_block, os_scoped: @called_in_on_os_block) rescue RuntimeError => e raise CaskInvalidError.new(cask, e) end diff --git a/Library/Homebrew/cask/dsl/depends_on.rb b/Library/Homebrew/cask/dsl/depends_on.rb index 86aac01c9c..6f4e444ec5 100644 --- a/Library/Homebrew/cask/dsl/depends_on.rb +++ b/Library/Homebrew/cask/dsl/depends_on.rb @@ -50,6 +50,7 @@ module Cask @macos = T.let(nil, T.nilable(MacOSRequirement)) @maximum_macos = T.let(nil, T.nilable(MacOSRequirement)) @linux = T.let(nil, T.nilable(LinuxRequirement)) + @macos_required = T.let(false, T::Boolean) @macos_bare_set_top_level = T.let(false, T::Boolean) @macos_version_set_top_level = T.let(false, T::Boolean) @maximum_macos_set_top_level = T.let(false, T::Boolean) @@ -70,9 +71,10 @@ module Cask params( pairs: T::Hash[Symbol, T.any(String, Symbol, T::Array[T.any(String, Symbol)])], set_in_block: T::Boolean, + os_scoped: T::Boolean, ).void } - def load(pairs, set_in_block: false) + def load(pairs, set_in_block: false, os_scoped: false) pairs.each do |key, value| raise "invalid depends_on key: '#{key.inspect}'" unless VALID_KEYS.include?(key) @@ -83,7 +85,7 @@ module Cask else send(:"#{key}=", *value) end - record_os_requirement(key, set_in_block:) + record_os_requirement(key, set_in_block:, os_scoped:) next if key != :macos next if value != :any next unless previous_macos&.version_specified? @@ -153,26 +155,24 @@ module Cask def present? = !empty? sig { returns(T::Boolean) } - def requires_macos? - @macos_bare_set_top_level || @macos_version_set_top_level || @maximum_macos_set_top_level - end + def requires_macos? = @macos_required sig { returns(T::Boolean) } def requires_linux? = @linux_set_top_level - sig { params(key: Symbol, set_in_block: T::Boolean).void } - def record_os_requirement(key, set_in_block:) + sig { params(key: Symbol, set_in_block: T::Boolean, os_scoped: T::Boolean).void } + def record_os_requirement(key, set_in_block:, os_scoped:) case key when :macos macos = @macos raise "invalid 'depends_on macos' value" unless macos - record_macos_requirement(macos, set_in_block:) + record_macos_requirement(macos, set_in_block:, os_scoped:) when :maximum_macos maximum_macos = @maximum_macos raise "invalid 'depends_on maximum_macos' value" unless maximum_macos - record_macos_requirement(maximum_macos, set_in_block:) + record_macos_requirement(maximum_macos, set_in_block:, os_scoped:) when :linux return if set_in_block raise "`depends_on :linux` cannot be combined with `depends_on macos:`" if requires_macos? @@ -181,8 +181,13 @@ module Cask end end - sig { params(requirement: MacOSRequirement, set_in_block: T::Boolean).void } - def record_macos_requirement(requirement, set_in_block:) + sig { params(requirement: MacOSRequirement, set_in_block: T::Boolean, os_scoped: T::Boolean).void } + def record_macos_requirement(requirement, set_in_block:, os_scoped:) + # `on_arm`/`on_intel` blocks are evaluated on every OS, so a macOS + # dependency inside one applies everywhere; only an OS block scopes a + # dependency to macOS alone. + @macos_required = true unless os_scoped + return if set_in_block raise "`depends_on :linux` cannot be combined with `depends_on macos:`" if requires_linux? diff --git a/Library/Homebrew/cask/reinstall.rb b/Library/Homebrew/cask/reinstall.rb index 8766abf192..958789d4d0 100644 --- a/Library/Homebrew/cask/reinstall.rb +++ b/Library/Homebrew/cask/reinstall.rb @@ -57,9 +57,9 @@ module Cask unless skip_prefetch Homebrew::Install.enqueue_cask_installers(cask_installers, download_queue:) - oh1 "Fetching downloads for: #{casks.map { |cask| Formatter.identifier(cask.full_name) }.to_sentence}", - truncate: false - download_queue.fetch + download_queue.fetch( + heading: Homebrew::Install.combined_fetch_downloads_heading(cask_names: casks.map(&:full_name)), + ) end ensure download_queue.shutdown if created_download_queue diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index 18117a8c79..76d7896b75 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -14,12 +14,16 @@ module Cask sig { returns(T.nilable(T::Array[T.untyped])) } attr_accessor :uninstall_artifacts - sig { params(attributes: T.any(T::Hash[String, T.untyped], T::Hash[Symbol, T.untyped])).void } - def initialize(attributes = {}) - @uninstall_flight_blocks = T.let(nil, T.nilable(T::Boolean)) - @uninstall_artifacts = T.let(nil, T.nilable(T::Array[T.untyped])) + sig { + params(uninstall_flight_blocks: T.nilable(T::Boolean), + uninstall_artifacts: T.nilable(T::Array[T.untyped]), + rest: T.untyped).void + } + def initialize(uninstall_flight_blocks: nil, uninstall_artifacts: nil, **rest) + @uninstall_flight_blocks = uninstall_flight_blocks + @uninstall_artifacts = uninstall_artifacts - super + super(**rest) end # Instantiates a {Tab} for a new installation of a cask. diff --git a/Library/Homebrew/cask/upgrade.rb b/Library/Homebrew/cask/upgrade.rb index aa653734c3..15037d92c3 100644 --- a/Library/Homebrew/cask/upgrade.rb +++ b/Library/Homebrew/cask/upgrade.rb @@ -241,13 +241,13 @@ module Cask end fetchable_casks = upgradable_casks.map(&:last) - fetchable_casks_sentence = fetchable_casks.map { |cask| Formatter.identifier(cask.full_name) }.to_sentence Homebrew::Install.enqueue_cask_installers(fetchable_cask_installers, download_queue: prefetch_download_queue) - if fetchable_casks.any? - oh1 "Fetching downloads for: #{fetchable_casks_sentence}", truncate: false - prefetch_download_queue.fetch - end + prefetch_download_queue.fetch( + heading: Homebrew::Install.combined_fetch_downloads_heading( + cask_names: fetchable_casks.map(&:full_name), + ), + ) ensure prefetch_download_queue.shutdown if created_download_queue end diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb index 5e21d89119..be7759c1e4 100644 --- a/Library/Homebrew/cleanup.rb +++ b/Library/Homebrew/cleanup.rb @@ -570,10 +570,15 @@ module Homebrew api_internal = cache/"api/internal" api_package_files = if scrub? && api_internal.directory? current_api_package_basename = Homebrew::API::Internal.cached_packages_json_file_path.basename.to_s - # Keep only the current OS's envelope and its `.payload` sidecar and - # scrub the rest, including orphaned sidecars and temp files. + # Keep only the current OS's envelope and its `.payload` and + # `.payload.index` sidecars and scrub the rest, including orphaned + # sidecars and temp files. # Keep in sync with the previous-OS-version removal in cmd/update.sh. - kept_basenames = [current_api_package_basename, "#{current_api_package_basename}.payload"] + kept_basenames = [ + current_api_package_basename, + "#{current_api_package_basename}.payload", + "#{current_api_package_basename}.payload.index", + ] api_internal.glob("packages.*.jws.json*").reject do |path| kept_basenames.include?(path.basename.to_s) end diff --git a/Library/Homebrew/cmd/fetch.rb b/Library/Homebrew/cmd/fetch.rb index 74c059fb85..8e31bf1f20 100644 --- a/Library/Homebrew/cmd/fetch.rb +++ b/Library/Homebrew/cmd/fetch.rb @@ -246,7 +246,7 @@ module Homebrew names = api_fetch_names( regex: HOMEBREW_DEFAULT_TAP_FORMULA_REGEX, capture: :name, - hashes: Homebrew::API::Internal.formula_hashes, + named: ->(name) { Homebrew::API::Internal.formula_name?(name) }, aliases: Homebrew::API::Internal.formula_aliases, renames: Homebrew::API::Internal.formula_renames, ) @@ -286,7 +286,7 @@ module Homebrew tokens = api_fetch_names( regex: HOMEBREW_DEFAULT_TAP_CASK_REGEX, capture: :token, - hashes: Homebrew::API::Internal.cask_hashes, + named: ->(token) { Homebrew::API::Internal.cask_name?(token) }, aliases: {}, renames: Homebrew::API::Internal.cask_renames, ) @@ -325,12 +325,12 @@ module Homebrew params( regex: Regexp, capture: Symbol, - hashes: T::Hash[String, T::Hash[String, T.untyped]], + named: T.proc.params(name: String).returns(T::Boolean), aliases: T::Hash[String, String], renames: T::Hash[String, String], ).returns(T.nilable(T::Array[String])) } - def api_fetch_names(regex:, capture:, hashes:, aliases:, renames:) + def api_fetch_names(regex:, capture:, named:, aliases:, renames:) requested_names = args.named.downcased_unique_named names = T.let(requested_names.filter_map do |requested_name| name = requested_name[regex, capture] @@ -339,7 +339,7 @@ module Homebrew name = name.downcase name = aliases.fetch(name, name) name = renames.fetch(name, name) - next unless hashes.key?(name) + next unless named.call(name) name end, T::Array[String]) diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index 89b7df8590..b7ba04d4ab 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -276,9 +276,6 @@ module Homebrew return if formulae.any? && installed_formulae.empty? && casks.empty? - Install.perform_preinstall_checks_once - Install.check_cc_argv(args.cc) - formulae_installer = Install.formula_installers( installed_formulae, installed_on_request: !args.as_dependency?, @@ -305,14 +302,20 @@ module Homebrew ) shared_download_queue = T.let(nil, T.nilable(Homebrew::DownloadQueue)) - if !ask && !args.dry_run? && formulae_installer.any? + if !args.dry_run? && formulae_installer.any? shared_download_queue = Homebrew::DownloadQueue.new(pour: true) + # Start bottle manifest (and, once downloads are confirmed, bottle) + # transfers before the local-only work below. formulae_installer = Install.prelude_fetch_formulae(formulae_installer, - download_queue: shared_download_queue) + download_queue: shared_download_queue, + metadata_only: ask) end - dependants = begin - Upgrade.dependants( + begin + Install.perform_preinstall_checks_once + Install.check_cc_argv(args.cc) + + dependants = Upgrade.dependants( installed_formulae, flags: args.flags_only, ask: ask, @@ -328,30 +331,32 @@ module Homebrew verbose: args.verbose?, dry_run: args.dry_run?, ) - # Ensure the early download queue is shut down on interrupts. + + # Main block: if asking the user is enabled, show dry-run information. + if ask + shared_download_queue&.fetch(only: Resource::BottleManifest, + heading: "Downloading bottle manifests", allow_failures: true) + Install.ask_formulae( + formulae_installer, + dependants, + flags: args.flags_only, + force_bottle: args.force_bottle?, + build_from_source_formulae: args.build_from_source_formulae, + interactive: args.interactive?, + keep_tmp: args.keep_tmp?, + debug_symbols: args.debug_symbols?, + force: args.force?, + debug: args.debug?, + quiet: args.quiet?, + verbose: args.verbose?, + ) + end + # Ensure the early download queue is shut down on interrupts and declined prompts. rescue Exception # rubocop:disable Lint/RescueException shared_download_queue&.shutdown raise end - # Main block: if asking the user is enabled, show dry-run information. - if ask - Install.ask_formulae( - formulae_installer, - dependants, - flags: args.flags_only, - force_bottle: args.force_bottle?, - build_from_source_formulae: args.build_from_source_formulae, - interactive: args.interactive?, - keep_tmp: args.keep_tmp?, - debug_symbols: args.debug_symbols?, - force: args.force?, - debug: args.debug?, - quiet: args.quiet?, - verbose: args.verbose?, - ) - end - if !args.dry_run? && (formulae_installer.any? || fetch_casks.any?) download_queue = T.let(shared_download_queue || Homebrew::DownloadQueue.new(pour: true), Homebrew::DownloadQueue) @@ -360,10 +365,6 @@ module Homebrew Cask::Upgrade.show_upgrade_summary( upgrade_casks.map { |cask| "#{cask.full_name} #{cask.installed_version} -> #{cask.version}" }, ) - Install.show_combined_fetch_downloads_heading( - formula_names: formulae_installer.map { |fi| fi.formula.name }, - cask_names: fetch_casks.map(&:full_name), - ) formulae_installer = Install.enqueue_formulae(formulae_installer, download_queue:) @@ -386,7 +387,10 @@ module Homebrew Install.enqueue_cask_installers(fetch_cask_installers, download_queue:) end - download_queue.fetch + download_queue.fetch(heading: Install.combined_fetch_downloads_heading( + formula_names: formulae_installer.map { |fi| fi.formula.name }, + cask_names: fetch_casks.map(&:full_name), + )) ensure download_queue.shutdown end diff --git a/Library/Homebrew/cmd/readall.rb b/Library/Homebrew/cmd/readall.rb index c34974aa75..a42f6b2fc8 100644 --- a/Library/Homebrew/cmd/readall.rb +++ b/Library/Homebrew/cmd/readall.rb @@ -38,7 +38,7 @@ module Homebrew Homebrew.with_no_api_env do if args.syntax? && args.no_named? scan_files = "#{HOMEBREW_LIBRARY_PATH}/**/*.rb" - ruby_files = Dir.glob(scan_files).grep_v(%r{/(vendor)/}) + ruby_files = Dir.glob(scan_files).grep_v(%r{/(vendor)/}).map { Pathname(it) } Homebrew.failed = true unless Readall.valid_ruby_syntax?(ruby_files) end diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index ebb6694d8f..a5700a8a3d 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -244,11 +244,6 @@ module Homebrew shared_download_queue ||= Homebrew::DownloadQueue.new(pour: true) download_queue = shared_download_queue begin - Install.show_combined_fetch_downloads_heading( - formula_names: formulae_installers.map { |fi| fi.formula.name }, - cask_names: casks.map(&:full_name), - ) - valid_formula_installers = Install.enqueue_formulae(formulae_installers, download_queue:) @@ -268,7 +263,10 @@ module Homebrew ) end Install.enqueue_cask_installers(fetch_cask_installers, download_queue:) - download_queue.fetch + download_queue.fetch(heading: Install.combined_fetch_downloads_heading( + formula_names: valid_formula_installers.map { |fi| fi.formula.name }, + cask_names: casks.map(&:full_name), + )) casks_prefetched = true valid_formula_installers ensure diff --git a/Library/Homebrew/cmd/setup-sandbox.rb b/Library/Homebrew/cmd/setup-sandbox.rb deleted file mode 100644 index ace0d09277..0000000000 --- a/Library/Homebrew/cmd/setup-sandbox.rb +++ /dev/null @@ -1,20 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "abstract_command" -require "shell_command" - -module Homebrew - module Cmd - class SetupSandbox < AbstractCommand - include ShellCommand - - cmd_args do - description <<~EOS - Run any necessary commands to setup the Homebrew sandbox. - Must be run with `sudo`. Currently a no-op on non-Linux. - EOS - end - end - end -end diff --git a/Library/Homebrew/cmd/setup-sandbox.sh b/Library/Homebrew/cmd/setup-sandbox.sh deleted file mode 100644 index 85601fe8ec..0000000000 --- a/Library/Homebrew/cmd/setup-sandbox.sh +++ /dev/null @@ -1,67 +0,0 @@ -# Documentation defined in Library/Homebrew/cmd/setup-sandbox.rb - -# This Bubblewrap installation mirrors the package manager approaches in -# https://github.com/Homebrew/install and the Homebrew formula fallback in -# `ensure_sandbox_installed!` in Library/Homebrew/extend/os/linux/sandbox.rb. - -# `sudo` strips `GITHUB_ACTIONS`, so also detect the runner via `/proc/1/cgroup` -# like `check-run-command-as-root` in Library/Homebrew/brew.sh does. -homebrew-on-github-actions() { - [[ -n "${GITHUB_ACTIONS}" ]] && return 0 - grep -q "actions_job" /proc/1/cgroup &>/dev/null -} - -homebrew-setup-sandbox() { - # The sandbox sysctls and Bubblewrap are Linux-only. - [[ -z "${HOMEBREW_LINUX}" ]] && return 0 - - if homebrew-on-github-actions && ! command -v bwrap &>/dev/null - then - if command -v apt-get &>/dev/null - then - apt-get install --yes bubblewrap - elif command -v dnf &>/dev/null - then - dnf install --assumeyes bubblewrap - elif command -v yum &>/dev/null - then - yum install --assumeyes bubblewrap - elif command -v pacman &>/dev/null - then - pacman --sync --noconfirm bubblewrap - elif command -v apk &>/dev/null - then - apk add bubblewrap - fi - fi - - # These settings mirror SANDBOX_SYSCTL_SETTINGS in - # Library/Homebrew/extend/os/linux/sandbox.rb; keep both in sync. - local proc_sys_root="${HOMEBREW_PROC_SYS:-/proc/sys}" - local sysctl_value - local unprivileged_userns_clone_sysctl="${proc_sys_root}/kernel/unprivileged_userns_clone" - if [[ -e "${unprivileged_userns_clone_sysctl}" ]] && - sysctl_value="$(sysctl -n "kernel.unprivileged_userns_clone")" && - [[ "${sysctl_value}" != "1" ]] && - [[ -w "${unprivileged_userns_clone_sysctl}" ]] - then - sysctl -w kernel.unprivileged_userns_clone=1 || true - fi - local max_user_namespaces_sysctl="${proc_sys_root}/user/max_user_namespaces" - if [[ -e "${max_user_namespaces_sysctl}" ]] && - sysctl_value="$(sysctl -n "user.max_user_namespaces")" && - [[ "${sysctl_value}" -lt 28633 ]] && - [[ -w "${max_user_namespaces_sysctl}" ]] - then - sysctl -w user.max_user_namespaces=28633 || true - fi - - local apparmor_restrict_unprivileged_userns_sysctl="${proc_sys_root}/kernel/apparmor_restrict_unprivileged_userns" - if [[ -e "${apparmor_restrict_unprivileged_userns_sysctl}" ]] && - sysctl_value="$(sysctl -n "kernel.apparmor_restrict_unprivileged_userns")" && - [[ "${sysctl_value}" != "0" ]] && - [[ -w "${apparmor_restrict_unprivileged_userns_sysctl}" ]] - then - sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true - fi -} diff --git a/Library/Homebrew/cmd/update.sh b/Library/Homebrew/cmd/update.sh index 13b2b51df8..32a44651ee 100644 --- a/Library/Homebrew/cmd/update.sh +++ b/Library/Homebrew/cmd/update.sh @@ -398,6 +398,21 @@ EOWARN trap - SIGINT } +api_curl_download() { + local json_url="$1" + local cache_path="$2" + shift 2 + + curl \ + "${CURL_DISABLE_CURLRC_ARGS[@]}" \ + --fail --compressed --silent \ + --speed-limit "${HOMEBREW_CURL_SPEED_LIMIT}" --speed-time "${HOMEBREW_CURL_SPEED_TIME}" \ + --location --remote-time --output "${cache_path}" \ + "$@" \ + --user-agent "${HOMEBREW_USER_AGENT_CURL}" \ + "${json_url}" +} + fetch_api_file() { local filename="$1" local update_failed_file="$2" @@ -427,7 +442,8 @@ fetch_api_file() { echo "Checking if we need to fetch ${filename}..." fi - local arg json_url last_json_url + local arg curl_exit_code json_url last_json_url + local -a time_cond while read -r json_url do time_cond=() @@ -435,15 +451,15 @@ fetch_api_file() { do time_cond+=("${arg}") done < <(api_time_cond_args "${cache_path}") - curl \ - "${CURL_DISABLE_CURLRC_ARGS[@]}" \ - --fail --compressed --silent \ - --speed-limit "${HOMEBREW_CURL_SPEED_LIMIT}" --speed-time "${HOMEBREW_CURL_SPEED_TIME}" \ - --location --remote-time --output "${cache_path}" \ - "${time_cond[@]}" \ - --user-agent "${HOMEBREW_USER_AGENT_CURL}" \ - "${json_url}" + api_curl_download "${json_url}" "${cache_path}" "${time_cond[@]}" curl_exit_code=$? + # A conditional request can fail with a receive error (curl exit code 56) when + # an unconditional request for the same URL succeeds, so retry exactly once. + if [[ ${curl_exit_code} -eq 56 ]] && [[ ${#time_cond[@]} -gt 0 ]] + then + api_curl_download "${json_url}" "${cache_path}" + curl_exit_code=$? + fi last_json_url="${json_url}" [[ ${curl_exit_code} -eq 0 ]] && break done < <(api_urls "${filename}") @@ -1051,13 +1067,16 @@ EOS rm -f "${HOMEBREW_CACHE}"/api/internal/formula.*.jws.json rm -f "${HOMEBREW_CACHE}"/api/internal/cask.*.jws.json - # Remove API files (and their `.payload` sidecars) from previous OS - # versions. Keep in sync with `cache_files` in Library/Homebrew/cleanup.rb. + # Remove API files (and their `.payload` and `.payload.index` sidecars) + # from previous OS versions, keeping the current OS's so `brew + # update-report`'s API data load stays or becomes prewarmed. Keep in + # sync with `cache_files` in Library/Homebrew/cleanup.rb. for f in "${HOMEBREW_CACHE}"/api/internal/packages.*.jws.json* do case "${f}" in "${HOMEBREW_CACHE}/api/internal/packages.$(bottle_tag).jws.json") ;; "${HOMEBREW_CACHE}/api/internal/packages.$(bottle_tag).jws.json.payload") ;; + "${HOMEBREW_CACHE}/api/internal/packages.$(bottle_tag).jws.json.payload.index") ;; *) rm -f "${f}" ;; esac done diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index 4162be29d8..41261c0f78 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -263,21 +263,19 @@ module Homebrew begin formulae_prefetched = upgrade_outdated_formulae!( formulae, - prefetch_only: true, - download_queue: shared_download_queue, - prefetch_names: prefetched_formulae_names, - prefetch_upgrades: prefetched_formulae_upgrades, - show_upgrade_summary: false, - show_downloads_heading: false, + prefetch_only: true, + download_queue: shared_download_queue, + prefetch_names: prefetched_formulae_names, + prefetch_upgrades: prefetched_formulae_upgrades, + show_upgrade_summary: false, ) prefetched_casks = prefetch_outdated_casks!( casks, - download_queue: shared_download_queue, - prefetch_names: prefetched_cask_names, - prefetch_upgrades: prefetched_cask_upgrades, - prefetch_casks: prefetched_cask_upgrade_casks, - prefetch_errors: prefetched_cask_errors, - show_downloads_heading: false, + download_queue: shared_download_queue, + prefetch_names: prefetched_cask_names, + prefetch_upgrades: prefetched_cask_upgrades, + prefetch_casks: prefetched_cask_upgrade_casks, + prefetch_errors: prefetched_cask_errors, ) unless ask Cask::Upgrade.show_upgrade_summary( @@ -285,11 +283,10 @@ module Homebrew dry_run: args.dry_run?, ) end - Install.show_combined_fetch_downloads_heading( + shared_download_queue.fetch(heading: Install.combined_fetch_downloads_heading( formula_names: prefetched_formulae_names, cask_names: prefetched_cask_names, - ) - shared_download_queue.fetch + )) if shared_download_queue.fetch_failed formulae_prefetched = false prefetched_casks = false @@ -414,12 +411,6 @@ module Homebrew Install.perform_preinstall_checks_once - if formulae_to_install.any? do |formula| - formula.bottle&.github_packages_manifest_resource&.downloaded_and_valid? == false - end - oh1 "Downloading bottle manifests" - end - formulae_installer = Upgrade.formula_installers( formulae_to_install, flags: args.flags_only, @@ -602,15 +593,14 @@ module Homebrew sig { params( - formulae: T::Array[Formula], - prefetch_only: T::Boolean, - use_prefetched: T::Boolean, - dry_run: T::Boolean, - download_queue: T.nilable(Homebrew::DownloadQueue), - prefetch_names: T.nilable(T::Array[String]), - prefetch_upgrades: T.nilable(T::Array[String]), - show_upgrade_summary: T::Boolean, - show_downloads_heading: T::Boolean, + formulae: T::Array[Formula], + prefetch_only: T::Boolean, + use_prefetched: T::Boolean, + dry_run: T::Boolean, + download_queue: T.nilable(Homebrew::DownloadQueue), + prefetch_names: T.nilable(T::Array[String]), + prefetch_upgrades: T.nilable(T::Array[String]), + show_upgrade_summary: T::Boolean, ).returns(T::Boolean) } def upgrade_outdated_formulae!(formulae, prefetch_only: false, use_prefetched: false, @@ -618,8 +608,7 @@ module Homebrew download_queue: nil, prefetch_names: nil, prefetch_upgrades: nil, - show_upgrade_summary: true, - show_downloads_heading: true) + show_upgrade_summary: true) return false if args.cask? use_prefetched_context = use_prefetched && @prefetched_formulae_upgrade_context @@ -634,11 +623,6 @@ module Homebrew prefetch_download_queue = download_queue || Homebrew.default_download_queue valid_formula_installers = Install.enqueue_formulae(context.formulae_installer, download_queue: prefetch_download_queue) - if show_downloads_heading - Install.show_combined_fetch_downloads_heading( - formula_names: valid_formula_installers.map { |fi| fi.formula.name }, - ) - end prefetch_names&.replace(valid_formula_installers.map { |fi| fi.formula.name }) prefetch_upgrades&.replace(formula_upgrade_descriptions(valid_formula_installers.map(&:formula))) @prefetched_formulae_upgrade_context = FormulaeUpgradeContext.new( @@ -719,13 +703,11 @@ module Homebrew prefetch_names: T.nilable(T::Array[String]), prefetch_upgrades: T.nilable(T::Array[String]), prefetch_casks: T.nilable(T::Array[Cask::Cask]), - prefetch_errors: T.nilable(T::Array[StandardError]), - show_downloads_heading: T::Boolean) + prefetch_errors: T.nilable(T::Array[StandardError])) .returns(T::Boolean) } def prefetch_outdated_casks!(casks, download_queue:, prefetch_names: nil, - prefetch_upgrades: nil, prefetch_casks: nil, prefetch_errors: nil, - show_downloads_heading: true) + prefetch_upgrades: nil, prefetch_casks: nil, prefetch_errors: nil) return false if args.formula? casks = minimum_version_casks(casks, quiet: true) @@ -783,8 +765,6 @@ module Homebrew prefetch_upgrades&.replace( outdated_casks.map { |cask| "#{cask.full_name} #{cask.installed_version} -> #{cask.version}" }, ) - Install.show_combined_fetch_downloads_heading(cask_names:) if show_downloads_heading - true rescue => e ofail e diff --git a/Library/Homebrew/debrew.rb b/Library/Homebrew/debrew.rb index 7d97389fcc..e151272358 100644 --- a/Library/Homebrew/debrew.rb +++ b/Library/Homebrew/debrew.rb @@ -93,24 +93,14 @@ module Debrew sig { type_parameters(:U) - .params(_block: T.proc.returns(T.type_parameter(:U))) - .returns(T.nilable(T.type_parameter(:U))) + .params(block: T.proc.returns(T.type_parameter(:U))) + .returns(T.type_parameter(:U)) } - def self.debrew(&_block) + def self.debrew(&block) @mutex = Mutex.new - Ignorable.hook_raise - - begin - yield - rescue SystemExit - raise - rescue Ignorable::ExceptionMixin => e - e.ignore if debug(e) == :ignore # execution jumps back to where the exception was thrown - nil - ensure - Ignorable.unhook_raise - @mutex = nil - end + Ignorable.hook_raise(on_ignorable: ->(e) { e.is_a?(SystemExit) ? :raise : debug(e) }, &block) + ensure + @mutex = nil end sig { params(exception: Exception).returns(Symbol) } diff --git a/Library/Homebrew/dependency_collector.rb b/Library/Homebrew/dependency_collector.rb index a6dd80ef72..dfa27217e7 100644 --- a/Library/Homebrew/dependency_collector.rb +++ b/Library/Homebrew/dependency_collector.rb @@ -99,15 +99,11 @@ class DependencyCollector sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) } def glibc_dep_if_needed(related_formula_names); end - sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) } - def bubblewrap_dep_if_needed(related_formula_names); end - # Names implicitly added to any formula's deps right now, reusing the same checks # `Formula#add_global_deps_to_spec` uses to inject them onto a real formula. sig { returns(T::Set[String]) } def implicit_dependency_names [ - bubblewrap_dep_if_needed(Set.new), gcc_dep_if_needed(Set.new), glibc_dep_if_needed(Set.new), ].compact.to_set(&:name) diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb index cb591b18b5..84736f9043 100644 --- a/Library/Homebrew/dev-cmd/audit.rb +++ b/Library/Homebrew/dev-cmd/audit.rb @@ -263,6 +263,10 @@ module Homebrew path = cask.sourcefile_path errors = os_arch_combinations.flat_map do |os, arch| + # Linux-only casks have no stanza values for macOS, so audit them + # under Linux instead. + os = :linux if os != :linux && !cask.supports_macos? + SimulateSystem.with(os:, arch:) do odebug "Auditing Cask #{cask} on os #{os} and arch #{arch}" diff --git a/Library/Homebrew/dev-cmd/bump.rb b/Library/Homebrew/dev-cmd/bump.rb index 10772e6939..0fbaf564c8 100644 --- a/Library/Homebrew/dev-cmd/bump.rb +++ b/Library/Homebrew/dev-cmd/bump.rb @@ -48,6 +48,7 @@ module Homebrew const :resource_versions, T::Array[ResourceVersionInfo], default: [] const :repology_latest, T.any(String, Version) const :newer_than_upstream, T::Hash[Symbol, T::Boolean], default: {} + const :cooldown_skipped_versions, T::Hash[Symbol, Version], default: {} const :duplicate_pull_requests, T.nilable(T.any(T::Array[String], String)) const :maybe_duplicate_pull_requests, T.nilable(T.any(T::Array[String], String)) end @@ -230,6 +231,7 @@ module Homebrew deprecated = {} current_versions = {} new_versions = {} + cooldown_skipped_versions = {} repology_latest = repositories.present? ? Repology.latest_version(repositories) : "not found" repology_latest_is_a_version = repology_latest.is_a?(Version) @@ -271,7 +273,8 @@ module Homebrew deprecated[version_key] = loaded_formula_or_cask.deprecated? formula_or_cask_has_livecheck = loaded_formula_or_cask.livecheck_defined? - livecheck_latest = livecheck_result(loaded_formula_or_cask, current_version_value) + livecheck_latest, cooldown_skipped = livecheck_result(loaded_formula_or_cask, current_version_value) + cooldown_skipped_versions[version_key] = cooldown_skipped if cooldown_skipped livecheck_latest_is_a_version = livecheck_latest.is_a?(Version) new_version_value = if (livecheck_latest_is_a_version && @@ -307,6 +310,7 @@ module Homebrew single_arch = arch_options[0] current_versions = { general: current_versions[single_arch] } new_versions = { general: new_versions[single_arch] } + cooldown_skipped_versions = { general: cooldown_skipped_versions[single_arch] }.compact else if current_versions[:arm].present? && current_versions[:arm] == current_versions[:intel] current_versions = { general: current_versions[:arm] } @@ -314,6 +318,10 @@ module Homebrew if new_versions[:arm].present? && new_versions[:arm] == new_versions[:intel] new_versions = { general: new_versions[:arm] } end + if cooldown_skipped_versions[:arm].present? && + cooldown_skipped_versions[:arm] == cooldown_skipped_versions[:intel] + cooldown_skipped_versions = { general: cooldown_skipped_versions[:arm] } + end end current_version = BumpVersionParser.new(general: current_versions[:general], @@ -386,6 +394,7 @@ module Homebrew resource_versions:, repology_latest:, newer_than_upstream:, + cooldown_skipped_versions:, duplicate_pull_requests:, maybe_duplicate_pull_requests:, ) @@ -410,6 +419,7 @@ module Homebrew new_version = version_info.new_version repology_latest = version_info.repology_latest newer_than_upstream = version_info.newer_than_upstream + cooldown_skipped_version = version_info.cooldown_skipped_versions.values.max duplicate_pull_requests = version_info.duplicate_pull_requests maybe_duplicate_pull_requests = version_info.maybe_duplicate_pull_requests @@ -418,7 +428,11 @@ module Homebrew title_name = ambiguous_cask ? "#{name} (cask)" : name title = if (repology_latest == current_version.general || !repology_latest.is_a?(Version)) && versions_equal - "#{title_name} #{Tty.green}is up to date!#{Tty.reset}" + if cooldown_skipped_version + "#{title_name} #{Tty.yellow}has a new version in release cooldown#{Tty.reset}" + else + "#{title_name} #{Tty.green}is up to date!#{Tty.reset}" + end else title_name end @@ -446,11 +460,18 @@ module Homebrew end throttled = formula_or_cask.livecheck.throttle || formula_or_cask.livecheck.throttle_days + latest_versions = if cooldown_skipped_version + cooldown_days = Utils.pluralize("day", Homebrew::RELEASE_COOLDOWN_DAYS, include_count: true) + "#{cooldown_skipped_version} (released less than #{cooldown_days} ago)" + else + "#{new_versions}#{" (throttled)" if throttled}" + end ohai title puts <<~EOS Current #{version_info.version_name} #{current_versions} - Latest livecheck version: #{new_versions}#{" (throttled)" if throttled} + Latest livecheck version: #{latest_versions} EOS + puts "Bump-ready version: #{new_versions}" if cooldown_skipped_version puts <<~EOS unless skip_repology?(formula_or_cask) Latest Repology version: #{repology_latest} EOS @@ -851,11 +872,13 @@ module Homebrew end end + # Returns the new version (or a message string) and the newest upstream + # version skipped due to the release cooldown, if any. sig { params( formula_or_cask: T.any(Formula, Cask::Cask), current: T.nilable(T.any(Version, Cask::DSL::Version)), - ).returns(T.any(Version, String)) + ).returns([T.any(Version, String), T.nilable(Version)]) } def livecheck_result(formula_or_cask, current) name = Livecheck.package_or_resource_name(formula_or_cask) @@ -886,9 +909,9 @@ module Homebrew skip_status = skip_info[:status] skip_messages = skip_info[:messages] skip_message = skip_messages.join("; ") if skip_messages.present? - return "error: #{skip_message}" if skip_status == "error" && skip_message + return "error: #{skip_message}", nil if skip_status == "error" && skip_message - return "skipped - #{skip_message || skip_status}" + return "skipped - #{skip_message || skip_status}", nil end version_info = Livecheck.latest_version( @@ -896,17 +919,20 @@ module Homebrew referenced_formula_or_cask:, json: true, full_name: false, verbose: true, debug: false ) - return "unable to get versions" if version_info.blank? + return "unable to get versions", nil if version_info.blank? if !version_info.key?(:latest_throttled) - version_with_cooldown(version_info, current) || Version.new(version_info[:latest]) + latest = Version.new(version_info[:latest]) + cooldown_version = version_with_cooldown(version_info, current) + cooldown_skipped = (latest if cooldown_version && cooldown_version < latest) + [cooldown_version || latest, cooldown_skipped] elsif version_info[:latest_throttled].nil? - "unable to get throttled versions" + ["unable to get throttled versions", nil] else - Version.new(version_info[:latest_throttled]) + [Version.new(version_info[:latest_throttled]), nil] end rescue => e - "error: #{e}" + ["error: #{e}", nil] end sig { diff --git a/Library/Homebrew/dev-cmd/contributions.rb b/Library/Homebrew/dev-cmd/contributions.rb index 1b198cd7de..b402a52779 100644 --- a/Library/Homebrew/dev-cmd/contributions.rb +++ b/Library/Homebrew/dev-cmd/contributions.rb @@ -40,7 +40,8 @@ module Homebrew EOS comma_array "--user=", description: "Specify a comma-separated list of GitHub usernames or email addresses to find " \ - "contributions from. Omitting this flag searches Homebrew maintainers. " \ + "contributions from. Omitting this flag searches Homebrew maintainers and " \ + "requires access to the `Homebrew/maintainers` team. " \ "With `--maintainer-report-csv`, only matching quarter-end Maintainers are included." comma_array "--repositories", description: "Specify a comma-separated list of repositories to search. " \ diff --git a/Library/Homebrew/dev-cmd/tap-new.rb b/Library/Homebrew/dev-cmd/tap-new.rb index 8f467f6d2d..5a011ba511 100644 --- a/Library/Homebrew/dev-cmd/tap-new.rb +++ b/Library/Homebrew/dev-cmd/tap-new.rb @@ -90,19 +90,21 @@ module Homebrew publish_yml = render_workflow_template( "tap-new-publish.yml", branch:, github_packages: args.github_packages? ) + autobump_yml = render_workflow_template( + "tap-new-autobump.yml", branch:, github_packages: args.github_packages? + ) (tap.path/".github/workflows").mkpath write_path(tap, ".github/dependabot.yml", dependabot_yml) write_path(tap, ".github/workflows/tests.yml", tests_yml) write_path(tap, ".github/workflows/publish.yml", publish_yml) + write_path(tap, ".github/workflows/autobump.yml", autobump_yml) unless args.no_git? cd tap.path do |path| Utils::Git.set_name_email! Utils::Git.setup_gpg! - # Would be nice to use --initial-branch here but it's not available in - # older versions of Git that we support. - safe_system "git", "-c", "init.defaultBranch=#{branch}", "init" + safe_system "git", "init", "--initial-branch=#{branch}" args = [] git_owner = File.stat(File.join(path, ".git")).uid @@ -150,6 +152,7 @@ module Homebrew workflow = (HOMEBREW_LIBRARY_PATH.parent.parent/".github/workflows"/filename).read workflow.sub!("name: tap-new tests template", "name: brew test-bot") workflow.sub!("name: tap-new publish template", "name: brew pr-pull") + workflow.sub!("name: tap-new autobump template", "name: brew bump") if filename == "tap-new-tests.yml" workflow.sub!("on:\n workflow_dispatch:\n", <<~YAML) on: @@ -159,6 +162,14 @@ module Homebrew pull_request: YAML end + # Pick a random 5 minute block in which to execute the autobump action to avoid peak GitHub loads + hour = Random.rand(24) + minute = Random.rand(12) * 5 + workflow.gsub!("this will be changed later and randomised by brew tap-new") do + "Every day at #{hour}:#{minute} UTC" + end + workflow.gsub!("\"1 1 1 1 1\"") { "#{minute} #{hour} * * *" } + workflow.sub!(" if: github.repository == ''\n", "") workflow.gsub!("TAP_NEW_BRANCH") { branch } workflow.gsub!("TAP_NEW_ROOT_URL_ARGUMENT") { root_url ? " --root-url=#{root_url}" : "" } diff --git a/Library/Homebrew/dev-cmd/test.rb b/Library/Homebrew/dev-cmd/test.rb index 019ea00e86..906864d1d6 100644 --- a/Library/Homebrew/dev-cmd/test.rb +++ b/Library/Homebrew/dev-cmd/test.rb @@ -82,7 +82,6 @@ module Homebrew exec_args << "--HEAD" if f.head? - Sandbox.ensure_sandbox_installed! if Sandbox.available? sandbox = Sandbox.new f.logs.mkpath diff --git a/Library/Homebrew/download_queue.rb b/Library/Homebrew/download_queue.rb index cd0ab83ec9..c0987742c7 100644 --- a/Library/Homebrew/download_queue.rb +++ b/Library/Homebrew/download_queue.rb @@ -100,30 +100,74 @@ module Homebrew end end - sig { void } - def fetch + # Waits for and reports queued downloads. With `only:`, limits that to + # downloadables of the given class, leaving the rest enqueued and + # unreported for a later fetch, e.g. so dependency resolution can wait + # on bottle manifests without reporting in-flight bottles before their + # downloads heading has been printed. A `heading:` is printed only when + # there is something to report, so every report gets a heading and empty + # fetches stay silent. With `allow_failures:`, failures are still + # reported with a ✘ line but neither raise nor mark the fetch or run + # as failed, for metadata prefetches such as the bottle manifest of a + # version whose bottle has not been published yet, where dependency + # resolution just falls back to a full install; known-bad cached files + # from checksum mismatches are still removed. + sig { + params(only: T.nilable(T::Class[Downloadable]), heading: T.nilable(String), + allow_failures: T::Boolean).void + } + def fetch(only: nil, heading: nil, allow_failures: false) @fetch_failed = false @deferred_failure_messages = [] context_before_fetch = Context.current - return if downloads.empty? + fetchable_downloads = if only + downloads.select { |downloadable, _| downloadable.is_a?(only) } + else + downloads + end + return if fetchable_downloads.empty? + + if heading + if tty + oh1 heading, truncate: false + $stdout.flush + else + # Keep the heading off parsed stdout (e.g. `brew info --json | jq`) + # and on the same stream as the non-TTY report lines below. + $stderr.puts oh1_title(heading, truncate: false) + end + end if concurrency == 1 - downloads.each do |downloadable, promise| + fetchable_downloads.each do |downloadable, promise| promise.wait! rescue CancelledDownloadError next rescue ChecksumMismatchError => e + if allow_failures + report_tolerated_failure(downloadable) + # Remove the known-bad download so it cannot be reused. + unlink_cached_download(downloadable) + next + end + @fetch_failed = true ofail "#{downloadable.download_queue_type} reports different checksum: #{e.expected}" + rescue + raise unless allow_failures + + report_tolerated_failure(downloadable) end else - message_length_max = downloads.keys.map { |download| download.download_queue_message.length }.max || 0 - remaining_downloads = downloads.dup.to_a + message_length_max = fetchable_downloads.keys.map do |download| + download.download_queue_message.length + end.max || 0 + remaining_downloads = fetchable_downloads.dup.to_a previous_pending_line_count = 0 max_lines = [concurrency, Tty.height].min resolution = Concurrent::Event.new - downloads.each_value { |future| future.on_resolution! { resolution.set } } + fetchable_downloads.each_value { |future| future.on_resolution! { resolution.set } } begin stdout_print_and_flush_if_tty Tty.hide_cursor @@ -141,7 +185,11 @@ module Homebrew $stderr.puts "#{status} #{message}" end - if future.rejected? + if future.rejected? && allow_failures + # Remove known-bad downloads so they cannot be reused, while + # staying non-fatal for tolerated metadata prefetches. + unlink_cached_download(downloadable) if exception.is_a?(ChecksumMismatchError) + elsif future.rejected? if exception.is_a?(ChecksumMismatchError) @fetch_failed = true actual = Digest::SHA256.file(downloadable.cached_download).hexdigest @@ -152,8 +200,7 @@ module Homebrew puts "#{expected_message} #{actual}" end elsif exception.is_a?(CannotInstallFormulaError) - cached_download = downloadable.cached_download - cached_download.unlink if cached_download&.exist? + unlink_cached_download(downloadable) raise exception elsif bottle_manifest_error?(downloadable, exception) # Fatal: unlike a missing blob (which then fails to stage), a @@ -250,9 +297,15 @@ module Homebrew # aborts the fetch above. Context.current = context_before_fetch if context_before_fetch - downloads.clear - @downloads_by_location.clear - @symlink_targets.clear + if only + # Keep unfetched downloads (and their location dedup entries) queued + # for the next fetch. + fetchable_downloads.each_key { |downloadable| downloads.delete(downloadable) } + else + downloads.clear + @downloads_by_location.clear + @symlink_targets.clear + end end sig { returns(T::Boolean) } @@ -366,6 +419,24 @@ module Homebrew tty && !@dumb_tty end + sig { params(downloadable: Downloadable).void } + def unlink_cached_download(downloadable) + cached_download = downloadable.cached_download + cached_download.unlink if cached_download.exist? + end + + # Matches the parallel-mode ✘ report for failures the serial path + # tolerates instead of raising. + sig { params(downloadable: Downloadable).void } + def report_tolerated_failure(downloadable) + status = if tty + "#{Tty.red}✘#{Tty.reset}" + else + "✘" + end + $stderr.puts "#{status} #{downloadable.download_queue_message}" + end + sig { params(future: Concurrent::Promises::Future).returns(T.nilable(String)) } def status_from_future(future) case future.state @@ -488,6 +559,14 @@ module Homebrew @default_download_queue ||= T.let(DownloadQueue.new, T.nilable(DownloadQueue)) end + sig { void } + def self.reset_default_download_queue + # Skip `shutdown` for a leaked RSpec double, which cannot receive + # messages outside the per-example rspec-mocks lifecycle. + @default_download_queue.shutdown if @default_download_queue.is_a?(DownloadQueue) + @default_download_queue = nil + end + sig { void } def self.shutdown_default_download_queue @default_download_queue&.shutdown diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb index 7404d357d7..6318167726 100644 --- a/Library/Homebrew/env_config.rb +++ b/Library/Homebrew/env_config.rb @@ -658,7 +658,7 @@ module Homebrew default: true, }, HOMEBREW_SANDBOX_LINUX: { - description: "The `bwrap`(1) sandbox is the default for formula installation and testing " \ + description: "The Landlock sandbox is the default for formula installation and testing " \ "on Linux unless `$HOMEBREW_NO_SANDBOX_LINUX` is set.", boolean: :set, disabled_by: :HOMEBREW_NO_SANDBOX_LINUX, diff --git a/Library/Homebrew/extend/os/linux/dependency_collector.rb b/Library/Homebrew/extend/os/linux/dependency_collector.rb index ba8494d2b3..7a69985472 100644 --- a/Library/Homebrew/extend/os/linux/dependency_collector.rb +++ b/Library/Homebrew/extend/os/linux/dependency_collector.rb @@ -2,22 +2,10 @@ # frozen_string_literal: true require "os/linux/glibc" -require "sandbox" module OS module Linux module DependencyCollector - sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) } - def bubblewrap_dep_if_needed(related_formula_names) - return unless bubblewrap_dependency_needed? - return if building_global_dep_tree? - return if related_formula_names.include?(BUBBLEWRAP) - return if global_dep_tree[BUBBLEWRAP]&.intersect?(related_formula_names) - return unless formula_for(BUBBLEWRAP) - - Dependency.new(BUBBLEWRAP, [:implicit]) - end - sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) } def gcc_dep_if_needed(related_formula_names) # gcc is required for libgcc_s.so.1 if glibc or gcc are too old @@ -50,31 +38,18 @@ module OS GLIBC = "glibc" GCC = OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA - BUBBLEWRAP = "bubblewrap" - private_constant :GLIBC, :GCC, :BUBBLEWRAP + private_constant :GLIBC, :GCC sig { void } def init_global_dep_tree_if_needed! return if building_global_dep_tree? - - sandbox_tree_needed = bubblewrap_dependency_needed? - build_formulae_tree_needed = ::DevelopmentTools.needs_build_formulae? - return if !sandbox_tree_needed && !build_formulae_tree_needed - return if (!sandbox_tree_needed || global_dep_tree.key?(BUBBLEWRAP)) && - (!build_formulae_tree_needed || (global_dep_tree.key?(GLIBC) && global_dep_tree.key?(GCC))) + return unless ::DevelopmentTools.needs_build_formulae? + return if global_dep_tree.key?(GLIBC) && global_dep_tree.key?(GCC) building_global_dep_tree! - if sandbox_tree_needed - include_build = OS.not_tier_one_configuration? || build_formulae_tree_needed - global_dep_tree[BUBBLEWRAP] = Set.new(global_deps_for(BUBBLEWRAP, include_build:)) - end - if build_formulae_tree_needed - global_dep_tree[GLIBC] = Set.new(global_deps_for(GLIBC)) - # gcc depends on glibc - global_dep_tree[GCC] = Set.new([*global_deps_for(GCC), GLIBC, *@@global_dep_tree[GLIBC]]) - # bubblewrap depends on gcc - global_dep_tree[BUBBLEWRAP]&.merge([GCC, *@@global_dep_tree[GCC]]) - end + global_dep_tree[GLIBC] = Set.new(global_deps_for(GLIBC)) + # gcc depends on glibc + global_dep_tree[GCC] = Set.new([*global_deps_for(GCC), GLIBC, *@@global_dep_tree[GLIBC]]) built_global_dep_tree! end @@ -86,27 +61,17 @@ module OS nil end - sig { returns(T::Boolean) } - def bubblewrap_dependency_needed? - return false unless ::Homebrew::EnvConfig.sandbox_linux? - return false if ENV["HOMEBREW_TESTS"] - return false if OS::Linux::Sandbox.landlock? - - ::Sandbox.executable.blank? - end - - sig { params(name: String, include_build: T::Boolean).returns(T::Array[String]) } - def global_deps_for(name, include_build: true) + sig { params(name: String).returns(T::Array[String]) } + def global_deps_for(name) @global_deps_for ||= T.let({}, T.nilable(T::Hash[String, T::Array[String]])) # Always strip out glibc and gcc from all parts of dependency tree when # we're calculating their dependency trees. Other parts of Homebrew will # catch any circular dependencies. - @global_deps_for["#{name}|#{include_build}"] ||= if (formula = formula_for(name)) + @global_deps_for[name] ||= if (formula = formula_for(name)) formula.deps.filter_map do |dep| next if dep.test? && !dep.build? - next if dep.build? && !include_build - [dep.name, *global_deps_for(dep.name, include_build:)].compact + [dep.name, *global_deps_for(dep.name)].compact end.flatten.uniq else [] diff --git a/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb b/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb index f16216bf83..d76e3f233a 100644 --- a/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb +++ b/Library/Homebrew/extend/os/linux/dev-cmd/tests.rb @@ -17,20 +17,8 @@ module OS return unless Homebrew::EnvConfig.sandbox_linux? require "sandbox" + return if !::Sandbox.available? && GitHub::Actions.env_set? - if OS::Linux::Sandbox.landlock? - unless ::Sandbox.available? - return if GitHub::Actions.env_set? - - ::Sandbox.ensure_sandbox_available! - end - - ::Sandbox.configure! - elsif GitHub::Actions.env_set? - ::Sandbox.configure! - else - ::Sandbox.ensure_sandbox_installed!(install_from_tests: true) - end ::Sandbox.ensure_sandbox_available! end diff --git a/Library/Homebrew/extend/os/linux/diagnostic.rb b/Library/Homebrew/extend/os/linux/diagnostic.rb index 14fb04c23b..c5e401d5c4 100644 --- a/Library/Homebrew/extend/os/linux/diagnostic.rb +++ b/Library/Homebrew/extend/os/linux/diagnostic.rb @@ -196,64 +196,24 @@ module OS def check_linux_sandbox return unless Homebrew::EnvConfig.sandbox_linux? - inside_docker = OS::Linux.inside_docker? - return if inside_docker && !GitHub::Actions.env_set? + return if OS::Linux.inside_docker? && !GitHub::Actions.env_set? state = ::Sandbox.state return if state == :available - reason = ::Sandbox.failure_reason || "The Linux sandbox is not available." - state = :landlock if OS::Linux::Sandbox.landlock? - reason_append = case state - when :setuid - "\n\nHomebrew's Linux sandbox requires a rootless `bwrap` executable." - when :unavailable - "\n\nHomebrew's Linux sandbox requires rootless Bubblewrap and unprivileged user namespaces." + fix = if state == :missing_fiddle + "Run Homebrew with its vendored Ruby, which includes Fiddle." else - "" - end - reason += reason_append - - fix_lines = case state - when :missing - missing_lines = [ - reason, - "", - "Install Bubblewrap and ensure a rootless `bwrap` executable is available on `PATH`.", - ] - if (install_command = ::Sandbox.sandbox_install_command) - missing_lines.push("", "On this system, install it with:", " #{install_command}") - end - missing_lines - when :setuid - [ - "Install a non-setuid Bubblewrap or put it earlier on `PATH`.", - ] - when :unavailable - [ - reason, - "", - "Homebrew's Linux sandbox requires rootless Bubblewrap and unprivileged", - "user namespaces. Run `sudo brew setup-sandbox` or check and update this system configuration:", - *::Sandbox.configuration_command_messages, - ] - else - [] - end - if state == :unavailable && inside_docker && GitHub::Actions.env_set? - fix_lines.push("", - "If this is a GitHub Actions container, add `options: --privileged` to the job's " \ - "`container` configuration.") + "Homebrew's Linux sandbox requires a kernel with Landlock enabled." end ::Homebrew::Diagnostic::Finding.new( - reason, - remediation: [ - *fix_lines, - "", - "As a final workaround, disable the Linux sandbox:", - " export HOMEBREW_NO_SANDBOX_LINUX=1", - ].join("\n").to_s, + ::Sandbox.failure_reason || "The Linux sandbox is not available.", + remediation: <<~EOS.chomp, + #{fix} + As a final workaround, disable the Linux sandbox: + export HOMEBREW_NO_SANDBOX_LINUX=1 + EOS ) end diff --git a/Library/Homebrew/extend/os/linux/formula.rb b/Library/Homebrew/extend/os/linux/formula.rb index 63452d2b42..6fb50dc865 100644 --- a/Library/Homebrew/extend/os/linux/formula.rb +++ b/Library/Homebrew/extend/os/linux/formula.rb @@ -37,7 +37,6 @@ module OS related_formula_names.merge(versioned_formulae_names) end [ - dependency_collector.bubblewrap_dep_if_needed(related_formula_names), dependency_collector.gcc_dep_if_needed(related_formula_names), dependency_collector.glibc_dep_if_needed(related_formula_names), ].compact.freeze diff --git a/Library/Homebrew/extend/os/linux/sandbox.rb b/Library/Homebrew/extend/os/linux/sandbox.rb index a72502550c..81bd0b1574 100644 --- a/Library/Homebrew/extend/os/linux/sandbox.rb +++ b/Library/Homebrew/extend/os/linux/sandbox.rb @@ -1,7 +1,6 @@ # typed: strict # frozen_string_literal: true -require "extend/os/linux/sandbox/bubblewrap" require "extend/os/linux/sandbox/landlock" module OS @@ -15,31 +14,6 @@ module OS TIOCSCTTY = 0x540E private_constant :TIOCSCTTY - sig { returns(::PATH) } - def self.bubblewrap_candidate_paths - ::Sandbox::Bubblewrap.executable_candidate_paths - end - - sig { returns(T.nilable(::Pathname)) } - def self.bubblewrap_executable - ::Sandbox::Bubblewrap.executable - end - - sig { returns(::Pathname) } - def self.bubblewrap_executable! - ::Sandbox::Bubblewrap.executable! - end - - sig { returns(T::Boolean) } - def self.landlock? - ENV.fetch("HOMEBREW_SANDBOX_LINUX_LANDLOCK", nil) == "1" - end - - sig { returns(T.any(T.class_of(::Sandbox::Bubblewrap), T.class_of(::Sandbox::Landlock))) } - def self.sandbox_implementation - landlock? ? ::Sandbox::Landlock : ::Sandbox::Bubblewrap - end - sig { void } def allow_write_temp_and_cache allow_write_path "/tmp" @@ -67,101 +41,31 @@ module OS requires_ancestor { T.class_of(::Sandbox) } - sig { returns(String) } - def executable_name - ::Sandbox::Bubblewrap.executable_name - end - - sig { params(candidate: ::Pathname).returns(T::Boolean) } - def executable_usable?(candidate) - ::Sandbox::Bubblewrap.executable_usable?(candidate) - end - - sig { returns(T::Array[String]) } - def system_bubblewrap_paths - ::Sandbox::Bubblewrap.system_paths - end - - sig { returns(::PATH) } - def executable_candidate_paths - ::Sandbox::Bubblewrap.executable_candidate_paths - end - - sig { returns(::PATH) } - def bubblewrap_candidate_paths - executable_candidate_paths - end - - sig { returns(T.nilable(::Pathname)) } - def bubblewrap_executable - ::Sandbox::Bubblewrap.executable - end - - sig { returns(::Pathname) } - def bubblewrap_executable! - ::Sandbox::Bubblewrap.executable! - end - - sig { params(install_from_tests: T::Boolean).void } - def ensure_sandbox_installed!(install_from_tests: false) - OS::Linux::Sandbox.sandbox_implementation.ensure_installed!(install_from_tests:) - end - sig { returns(T::Boolean) } def available? - OS::Linux::Sandbox.sandbox_implementation.available? + ::Sandbox::Landlock.available? end sig { returns(T::Boolean) } def full_write_isolation? - OS::Linux::Sandbox.sandbox_implementation.full_write_isolation? - end - - # Bubblewrap reports this specific namespace error when an outer - # Bubblewrap sandbox prevents Homebrew from creating another rootless - # sandbox. The shared `avoid_nested_sandboxing?` only calls this once the - # `$HOMEBREW_AVOID_NESTED_SANDBOXING` opt-in is set. - sig { returns(T::Boolean) } - def nested_sandbox? - OS::Linux::Sandbox.sandbox_implementation.nested_sandbox? + ::Sandbox::Landlock.full_write_isolation? end sig { returns(Symbol) } def state - OS::Linux::Sandbox.sandbox_implementation.state + ::Sandbox::Landlock.state end sig { void } def reset_state! - ::Sandbox::Bubblewrap.reset_state! ::Sandbox::Landlock.reset_state! end - sig { returns(T::Array[String]) } - def configuration_commands - OS::Linux::Sandbox.sandbox_implementation.configuration_commands - end - - sig { returns(T::Array[String]) } - def configuration_command_messages - OS::Linux::Sandbox.sandbox_implementation.configuration_command_messages - end - - sig { void } - def configure! - OS::Linux::Sandbox.sandbox_implementation.configure! - end - sig { returns(T.nilable(String)) } def failure_reason return super if self != ::Sandbox - OS::Linux::Sandbox.sandbox_implementation.failure_reason - end - - sig { returns(T.nilable(String)) } - def sandbox_install_command - OS::Linux::Sandbox.sandbox_implementation.install_command + ::Sandbox::Landlock.failure_reason end # `ioctl` request used to attach the sandboxed child to a controlling TTY. @@ -173,43 +77,24 @@ module OS sig { params(args: T.any(String, ::Pathname)).void } def run(*args) - implementation.run { super } - end - - sig { params(tmpdir: String).returns(T::Array[String]) } - def bubblewrap_args(tmpdir) - bubblewrap.arguments(tmpdir) - end - - sig { returns(T::Hash[String, Symbol]) } - def writable_paths - bubblewrap.writable_paths + landlock.run { super } end private sig { params(args: T::Array[T.any(String, ::Pathname)], tmpdir: String).returns(T::Array[T.any(String, ::Pathname)]) } def sandbox_command(args, tmpdir) - implementation.command(args, tmpdir) + landlock.command(args, tmpdir) end sig { void } def apply_sandbox - sandbox = implementation - sandbox.apply! if sandbox.is_a?(::Sandbox::Landlock) + landlock.apply! end - sig { returns(T.any(::Sandbox::Bubblewrap, ::Sandbox::Landlock)) } - def implementation - @implementation ||= T.let( - OS::Linux::Sandbox.sandbox_implementation.new(profile), - T.nilable(T.any(::Sandbox::Bubblewrap, ::Sandbox::Landlock)), - ) - end - - sig { returns(::Sandbox::Bubblewrap) } - def bubblewrap - @bubblewrap ||= T.let(::Sandbox::Bubblewrap.new(profile), T.nilable(::Sandbox::Bubblewrap)) + sig { returns(::Sandbox::Landlock) } + def landlock + @landlock ||= T.let(::Sandbox::Landlock.new(profile), T.nilable(::Sandbox::Landlock)) end end end diff --git a/Library/Homebrew/extend/os/linux/sandbox/bubblewrap.rb b/Library/Homebrew/extend/os/linux/sandbox/bubblewrap.rb deleted file mode 100644 index 0d919bcf97..0000000000 --- a/Library/Homebrew/extend/os/linux/sandbox/bubblewrap.rb +++ /dev/null @@ -1,372 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "fileutils" -require "env_config" -require "system_command" -require "utils/popen" -require "utils/github/actions" -require "extend/os/linux/sandbox/backend" - -class Sandbox - class Bubblewrap < LinuxBackend - extend SystemCommand::Mixin - extend Utils::Output::Mixin - - EXECUTABLE = "bwrap" - TEST_ARGS = [ - "--unshare-user", - "--unshare-ipc", - "--unshare-pid", - "--unshare-uts", - "--unshare-cgroup-try", - "--ro-bind", "/", "/", - "--proc", "/proc", - "--dev", "/dev", - "true" - ].freeze - SYSTEM_PATHS = %w[ - /usr/bin - /bin - ].freeze - HOMEBREW_PATHS = [ - "#{HOMEBREW_PREFIX}/bin", - ].freeze - NESTED_ERROR = "Creating new namespace failed: nesting depth or /proc/sys/user/max_*_namespaces exceeded" - class SysctlSetting < T::Struct - const :assignment, String - const :description, T::Array[String] - const :optional, T::Boolean, default: false - end - # These settings mirror the `sysctl` assignments in - # Library/Homebrew/cmd/setup-sandbox.sh; keep both in sync. - SYSCTL_SETTINGS = T.let([ - SysctlSetting.new( - assignment: "kernel.unprivileged_userns_clone=1", - description: [ - "Allows unprivileged processes to create user namespaces. Rootless", - "Bubblewrap needs this to isolate builds without elevated privileges.", - ], - ), - SysctlSetting.new( - assignment: "user.max_user_namespaces=28633", - description: [ - "Allows each user to allocate enough user namespaces. A zero or low", - "limit can prevent Bubblewrap from creating its sandbox.", - ], - ), - SysctlSetting.new( - assignment: "kernel.apparmor_restrict_unprivileged_userns=0", - description: [ - "Allows unprivileged user namespaces on AppArmor-enabled systems", - "that restrict them by default. Older kernels may not provide this", - "setting.", - ], - optional: true, - ), - ].freeze, T::Array[SysctlSetting]) - # Per-distro Bubblewrap install commands, detected by package manager and - # checked in priority order. Mirrors the build tools instructions in - # `Homebrew/install`'s `install.sh`. - INSTALL_COMMANDS = T.let({ - "apt-get" => "sudo apt-get install bubblewrap", - "dnf" => "sudo dnf install bubblewrap", - "yum" => "sudo yum install bubblewrap", - "pacman" => "sudo pacman -S bubblewrap", - "apk" => "sudo apk add bubblewrap", - }.freeze, T::Hash[String, String]) - private_constant :EXECUTABLE, :TEST_ARGS, :SYSTEM_PATHS, :HOMEBREW_PATHS, :NESTED_ERROR, :SysctlSetting, - :SYSCTL_SETTINGS, :INSTALL_COMMANDS - - class << self - sig { returns(String) } - def executable_name - EXECUTABLE - end - - sig { params(candidate: ::Pathname).returns(T::Boolean) } - def executable_usable?(candidate) - !File.stat(candidate).setuid? - end - - sig { returns(T::Array[String]) } - def system_paths - SYSTEM_PATHS - end - - sig { returns(::PATH) } - def executable_candidate_paths - PATH.new(HOMEBREW_PATHS, system_paths, ORIGINAL_PATHS, ENV.fetch("PATH"), HOMEBREW_ORIGINAL_BREW_FILE.dirname) - end - - sig { returns(T.nilable(::Pathname)) } - def executable - executable_candidate_paths.each do |path| - begin - candidate = ::Pathname.new(File.expand_path(executable_name, path)) - rescue ArgumentError - next - end - - next if !candidate.file? || !candidate.executable? - next unless executable_usable?(candidate) - - return candidate - end - - nil - end - - sig { returns(::Pathname) } - def executable! - executable || raise("Bubblewrap is required to use the Linux sandbox.") - end - - sig { params(install_from_tests: T::Boolean).void } - def ensure_installed!(install_from_tests: false) - return unless Homebrew::EnvConfig.sandbox_linux? - return if ENV["HOMEBREW_TESTS"] && !install_from_tests - return if ENV["HOMEBREW_INSTALLING_BUBBLEWRAP"] - return if executable - - begin - require "exceptions" - require "formula" - with_env(HOMEBREW_INSTALLING_BUBBLEWRAP: "1") do - ::Formula["bubblewrap"].ensure_installed!(reason: "Linux sandboxing") - end - reset_state! - return if executable - rescue ::FormulaUnavailableError - nil - end - - return unless GitHub::Actions.env_set? - return unless ENV.fetch("HOMEBREW_GITHUB_HOSTED_RUNNER", nil) - return unless which("apt-get") - - ohai "Installing Bubblewrap..." - command = ["apt-get", "install", "--yes", "bubblewrap"] - command.unshift("sudo") unless Process.euid.zero? - system(*command) - reset_state! - end - - sig { returns(T::Boolean) } - def available? - state == :available - end - - # Bubblewrap reports this specific namespace error when an outer - # Bubblewrap sandbox prevents Homebrew from creating another rootless - # sandbox. The shared `avoid_nested_sandboxing?` only calls this once the - # `$HOMEBREW_AVOID_NESTED_SANDBOXING` opt-in is set. - sig { returns(T::Boolean) } - def nested_sandbox? - return false unless Homebrew::EnvConfig.sandbox_linux? - - bubblewrap = executable - return false unless bubblewrap - - Utils.popen_read(bubblewrap.to_s, *TEST_ARGS, err: :out).include?(NESTED_ERROR) - end - - sig { returns(Symbol) } - def state - return :config_disabled unless Homebrew::EnvConfig.sandbox_linux? - - @state ||= T.let(compute_state, T.nilable(Symbol)) - end - - sig { void } - def reset_state! - @state = T.let(nil, T.nilable(Symbol)) - end - - sig { returns(T::Array[String]) } - def configuration_commands - SYSCTL_SETTINGS.map do |setting| - command = "sudo sysctl -w #{setting.assignment}" - command += " || true" if setting.optional - command - end - end - - sig { returns(T::Array[String]) } - def configuration_command_messages - commands = configuration_commands - SYSCTL_SETTINGS.each_with_index.flat_map do |setting, index| - [ - " #{commands.fetch(index)}", - *setting.description.map { |line| " #{line}" }, - ] - end - end - - sig { void } - def configure! - unless executable - ensure_installed!(install_from_tests: true) - unless executable - reset_state! - return - end - end - - ohai "Configuring Bubblewrap..." - command = [HOMEBREW_BREW_FILE.to_s, "setup-sandbox"] - command.unshift("sudo") unless Process.euid.zero? - raise ErrorDuringExecution.new(command, status: $CHILD_STATUS || 1) unless system(*command) - - reset_state! - end - - sig { returns(T.nilable(String)) } - def failure_reason - case state - when :config_disabled, :available - nil - when :missing - "Bubblewrap is required to use the Linux sandbox but was not found." - when :setuid - "A rootless Bubblewrap executable is required to use the Linux sandbox, " \ - "but all found `bwrap` executables are setuid." - when :unavailable - "Bubblewrap is installed but cannot create a rootless sandbox." - else - "The Linux sandbox is not available." - end - end - - sig { returns(T.nilable(String)) } - def install_command - INSTALL_COMMANDS.find { |package_manager, _| which(package_manager) }&.last - end - - private - - sig { returns(Symbol) } - def compute_state - bubblewraps = executables - return :missing if bubblewraps.empty? - - bubblewraps = bubblewraps.select { |candidate| executable_usable?(candidate) } - return :setuid if bubblewraps.empty? - - return :available if bubblewraps.any? { |candidate| sandbox_available?(candidate) } - - :unavailable - end - - sig { returns(T::Array[::Pathname]) } - def executables - executable_candidate_paths.filter_map do |path| - begin - candidate = ::Pathname.new(File.expand_path(executable_name, path)) - rescue ArgumentError - next - end - - candidate if candidate.file? && candidate.executable? - end - end - - sig { params(bubblewrap: ::Pathname).returns(T::Boolean) } - def sandbox_available?(bubblewrap) - result = system_command( - bubblewrap, - args: TEST_ARGS, - print_stderr: false, - ) - return true if result.success? - - opoo "bubblewrap test probe failed" - $stderr.print result.merged_output - false - end - end - - sig { params(profile: SandboxProfile).void } - def initialize(profile) - super - @masked_read_paths = T.let([], T::Array[::Pathname]) - end - - sig { params(block: T.proc.void).void } - def run(&block) - old_report_on_exception = T.let(Thread.report_on_exception, T.nilable(T::Boolean)) - Thread.report_on_exception = false - super - ensure - Thread.report_on_exception = old_report_on_exception unless old_report_on_exception.nil? - @masked_read_paths.reverse_each { |path| FileUtils.rm_rf(path) } - @masked_read_paths.clear - end - - sig { params(args: T::Array[T.any(String, ::Pathname)], tmpdir: String).returns(T::Array[T.any(String, ::Pathname)]) } - def command(args, tmpdir) - [self.class.executable!, *arguments(tmpdir), "--", *args] - end - - sig { params(tmpdir: String).returns(T::Array[String]) } - def arguments(tmpdir) - args = T.let([ - "--unshare-user", - "--unshare-ipc", - "--unshare-pid", - "--unshare-uts", - "--unshare-cgroup-try", - "--die-with-parent", - "--new-session", - "--ro-bind", "/", "/", - "--dev", "/dev", - "--proc", "/proc" - ], T::Array[String]) - args << "--unshare-net" if deny_all_network? - - writable_paths.each do |path, type| - prepare_writable_path(path, type) - args += ["--bind", path, path] - end - - denied_write_paths.each do |path| - next unless File.exist?(path) - - args += ["--ro-bind", path, path] - end - - denied_read_paths.each do |path| - next unless File.exist?(path) - - args += if File.directory?(path) - ["--bind", masked_read_path, path] - else - ["--ro-bind", File::NULL, path] - end - end - - args += ["--bind", tmpdir, tmpdir, "--chdir", tmpdir] - - args - end - - private - - sig { returns(T::Array[String]) } - def denied_write_paths - profile_paths(allow: false, operation: "file-write") - end - - sig { returns(T::Array[String]) } - def denied_read_paths - profile_paths(allow: false, operation: "file-read") - end - - sig { returns(String) } - def masked_read_path - path = ::Pathname.new(Dir.mktmpdir("homebrew-sandbox-deny-read", HOMEBREW_TEMP)) - @masked_read_paths << path - path.to_s - end - end -end diff --git a/Library/Homebrew/extend/os/linux/sandbox/landlock.rb b/Library/Homebrew/extend/os/linux/sandbox/landlock.rb index f6bf46a4f1..c52c8a37a4 100644 --- a/Library/Homebrew/extend/os/linux/sandbox/landlock.rb +++ b/Library/Homebrew/extend/os/linux/sandbox/landlock.rb @@ -94,8 +94,8 @@ class Sandbox class << self # Landlock cannot restrict chmod, chown, extended attributes or timestamp - # changes. Callers requiring Bubblewrap-equivalent write isolation must - # compensate for these limitations: + # changes. Callers requiring full write isolation must compensate for + # these limitations: # https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#filesystem-flags sig { returns(T::Boolean) } def full_write_isolation? = false @@ -159,26 +159,6 @@ class Sandbox @abi_version = T.let(nil, T.nilable(Integer)) end - sig { params(install_from_tests: T::Boolean).void } - def ensure_installed!(install_from_tests: false); end - - sig { void } - def configure! - ensure_available! - end - - sig { returns(T::Array[String]) } - def configuration_commands = [] - - sig { returns(T::Array[String]) } - def configuration_command_messages = [] - - sig { returns(T.nilable(String)) } - def install_command = nil - - sig { returns(T::Boolean) } - def nested_sandbox? = false - sig { params(attributes: T.nilable(String), size: Integer, flags: Integer).returns(Integer) } def landlock_create_ruleset(attributes, size, flags) @landlock_create_ruleset ||= T.let( @@ -264,13 +244,6 @@ class Sandbox private - sig { void } - def ensure_available! - return if available? - - raise failure_reason || "Landlock is not available." - end - sig { returns(Symbol) } def compute_state return :config_disabled unless Homebrew::EnvConfig.sandbox_linux? @@ -357,7 +330,7 @@ class Sandbox # https://github.com/torvalds/linux/blob/master/drivers/char/mem.c # POSIX shared memory and message queues use `/dev/shm` and # `/dev/mqueue`. These grants retain normal kernel permissions but do - # not provide Bubblewrap's private IPC namespace: + # not provide a private IPC namespace: # https://github.com/bminor/glibc/blob/master/sysdeps/posix/shm-directory.c # https://www.kernel.org/doc/html/latest/filesystems/mqueue.html device_path_rules = T.let({ diff --git a/Library/Homebrew/extend/os/linux/test_bot.rb b/Library/Homebrew/extend/os/linux/test_bot.rb index 51a42f0916..f1e24d7cf7 100644 --- a/Library/Homebrew/extend/os/linux/test_bot.rb +++ b/Library/Homebrew/extend/os/linux/test_bot.rb @@ -22,10 +22,7 @@ module OS sig { returns(T::Boolean) } def configure_sandbox! require "sandbox" - ::Sandbox.configure! ::Sandbox.available? - rescue ::ErrorDuringExecution - false end end diff --git a/Library/Homebrew/extend/os/mac/diagnostic.rb b/Library/Homebrew/extend/os/mac/diagnostic.rb index ecb8015fe4..828fc2eff0 100644 --- a/Library/Homebrew/extend/os/mac/diagnostic.rb +++ b/Library/Homebrew/extend/os/mac/diagnostic.rb @@ -64,7 +64,10 @@ module OS check_access_directories ] - # Developer tools are checked when building from source. + # We need the developer tools for `codesign` on Intel: + # https://github.com/Homebrew/brew/issues/23418 + checks << "check_for_installed_developer_tools" unless ::Hardware::CPU.arm? + checks.freeze end diff --git a/Library/Homebrew/extend/os/mac/keg.rb b/Library/Homebrew/extend/os/mac/keg.rb index 039d5961db..763d0437de 100644 --- a/Library/Homebrew/extend/os/mac/keg.rb +++ b/Library/Homebrew/extend/os/mac/keg.rb @@ -108,6 +108,43 @@ module OS def codesign_patched_binary(file) return if MacOS.version < :big_sur + unless ::Hardware::CPU.arm? + # Intel macOS rejects ruby-macho's ad-hoc signatures on larger + # binaries and does not require unsigned binaries to be signed, + # so use `codesign` to re-sign only the binaries whose existing + # signature our modifications have just broken: + # https://github.com/Homebrew/brew/issues/23418 + result = system_command("codesign", args: ["--verify", file], print_stderr: false) + return unless result.stderr.match?(/invalid signature/i) + + odebug "Codesigning #{file}" + return if quiet_system("codesign", "--sign", "-", "--force", + "--preserve-metadata=entitlements,requirements,flags,runtime", + file) + + # If the codesigning fails, it may be a bug in Apple's codesign utility. + # A known workaround is to copy the file to another inode, then move it back + # erasing the previous file. Then sign again. + Dir::Tmpname.create("workaround") do |tmppath| + FileUtils.cp file, tmppath + FileUtils.mv tmppath, file, force: true + end + + odebug "Codesigning (2nd try) #{file}" + result = system_command("codesign", args: [ + "--sign", "-", "--force", + "--preserve-metadata=entitlements,requirements,flags,runtime", + file + ], print_stderr: false) + return if result.success? + + onoe <<~EOS + Failed applying an ad-hoc signature to #{file}: + #{result.stderr} + EOS + return + end + require "macho" odebug "Codesigning #{file}" diff --git a/Library/Homebrew/extend/os/mac/readall.rb b/Library/Homebrew/extend/os/mac/readall.rb index 3c632f70f9..c28138396d 100644 --- a/Library/Homebrew/extend/os/mac/readall.rb +++ b/Library/Homebrew/extend/os/mac/readall.rb @@ -12,8 +12,15 @@ module OS requires_ancestor { Kernel } - sig { params(tap: ::Tap, os_name: T.nilable(Symbol), arch: T.nilable(Symbol)).returns(T::Boolean) } - def valid_casks?(tap, os_name: nil, arch: ::Hardware::CPU.type) + sig { + params( + tap: ::Tap, + os_name: T.nilable(Symbol), + arch: T.nilable(Symbol), + files: T.nilable(T::Array[::Pathname]), + ).returns(T::Boolean) + } + def valid_casks?(tap, os_name: nil, arch: ::Hardware::CPU.type, files: nil) return super if os_name == :linux current_macos_version = if os_name.is_a?(Symbol) @@ -23,7 +30,7 @@ module OS end success = T.let(true, T::Boolean) - tap.cask_files.each do |file| + (files || tap.cask_files).each do |file| cask = ::Cask::CaskLoader.load(file) # Fine to have missing URLs for unsupported macOS diff --git a/Library/Homebrew/extend/os/mac/system_config.rb b/Library/Homebrew/extend/os/mac/system_config.rb index 258e1ea208..b7c5bed29c 100644 --- a/Library/Homebrew/extend/os/mac/system_config.rb +++ b/Library/Homebrew/extend/os/mac/system_config.rb @@ -13,16 +13,17 @@ module OS sig { void } def initialize + super @xcode = T.let(nil, T.nilable(String)) @clt = T.let(nil, T.nilable(Version)) end sig { returns(String) } def describe_clang - return "N/A" if ::SystemConfig.clang.null? + return "N/A" if clang.null? - clang_build_info = ::SystemConfig.clang_build.null? ? "(parse error)" : ::SystemConfig.clang_build - "#{::SystemConfig.clang} build #{clang_build_info}" + clang_build_info = clang_build.null? ? "(parse error)" : clang_build + "#{clang} build #{clang_build_info}" end sig { returns(T.nilable(String)) } @@ -39,12 +40,6 @@ module OS @clt ||= MacOS::CLT.version if MacOS::CLT.installed? end - sig { params(out: T.any(File, StringIO, IO)).void } - def core_tap_config(out = $stdout) - dump_tap_config(CoreTap.instance, out) - dump_tap_config(CoreCaskTap.instance, out) - end - sig { returns(T.nilable(String)) } def metal_toolchain return unless ::Hardware::CPU.arm64? @@ -81,4 +76,5 @@ module OS end end end + SystemConfig.singleton_class.prepend(OS::Mac::SystemConfig::ClassMethods) diff --git a/Library/Homebrew/extend/os/mac/unpack_strategy/dmg.rb b/Library/Homebrew/extend/os/mac/unpack_strategy/dmg.rb new file mode 100644 index 0000000000..099f6724dc --- /dev/null +++ b/Library/Homebrew/extend/os/mac/unpack_strategy/dmg.rb @@ -0,0 +1,11 @@ +# typed: strict +# frozen_string_literal: true + +module UnpackStrategy + class Dmg + sig { returns(T::Boolean) } + def self.diskutil_image? + MacOS.version >= :sonoma + end + end +end diff --git a/Library/Homebrew/extend/os/unpack_strategy/dmg.rb b/Library/Homebrew/extend/os/unpack_strategy/dmg.rb new file mode 100644 index 0000000000..a866080a34 --- /dev/null +++ b/Library/Homebrew/extend/os/unpack_strategy/dmg.rb @@ -0,0 +1,4 @@ +# typed: strict +# frozen_string_literal: true + +require "extend/os/mac/unpack_strategy/dmg" if OS.mac? diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index fd1794172c..d1ab2054de 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -4083,6 +4083,8 @@ class Formula current_steps.concat( if block Homebrew::InstallSteps::DSL.build( + # TODO: Remove the undocumented `default_base: :var` compatibility default after official taps use + # explicit bases. default_base: :var, default_source_base: :prefix, default_target_base: :prefix, diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 09de5b61c7..22c1a5af36 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -55,6 +55,7 @@ class FormulaInstaller sig { params( formula: Formula, + download_queue: Homebrew::DownloadQueue, link_keg: T::Boolean, installed_on_request: T::Boolean, show_header: T::Boolean, @@ -83,6 +84,7 @@ class FormulaInstaller } def initialize( formula, + download_queue: Homebrew.default_download_queue, link_keg: false, installed_on_request: false, show_header: false, @@ -143,13 +145,15 @@ class FormulaInstaller @hold_locks = T.let(false, T::Boolean) @show_summary_heading = T.let(false, T::Boolean) @etc_var_preinstall = T.let([], T::Array[Pathname]) - @download_queue = T.let(Homebrew.default_download_queue, Homebrew::DownloadQueue) + @download_queue = download_queue @api_bottle = T.let(nil, T.nilable(Bottle)) @api_bottle_loaded = T.let(false, T::Boolean) + @enqueued_bottle_download = T.let(nil, T.nilable(Downloadable)) # Take the original formula instance, which might have been swapped from an API instance to a source instance @formula = T.let(T.must(previously_fetched_formula), Formula) if previously_fetched_formula + @ran_prelude_fetch_metadata = T.let(false, T::Boolean) @ran_prelude_fetch = T.let(false, T::Boolean) @ran_prelude = T.let(false, T::Boolean) end @@ -298,41 +302,47 @@ class FormulaInstaller ) || false end - sig { void } - def prelude_fetch - return if @ran_prelude_fetch + sig { params(metadata_only: T::Boolean).void } + def prelude_fetch(metadata_only: false) + unless @ran_prelude_fetch_metadata + deprecate_disable_type = DeprecateDisable.type(formula) + if deprecate_disable_type.present? + message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}" - deprecate_disable_type = DeprecateDisable.type(formula) - if deprecate_disable_type.present? - message = "#{formula.full_name} has been #{DeprecateDisable.message(formula)}" - - case deprecate_disable_type - when :deprecated - opoo message - when :disabled - if force? + case deprecate_disable_type + when :deprecated opoo message - else - GitHub::Actions.puts_annotation_if_env_set!(:error, message) - raise CannotInstallFormulaError, message + when :disabled + if force? + opoo message + else + GitHub::Actions.puts_annotation_if_env_set!(:error, message) + raise CannotInstallFormulaError, message + end end end + + # Run the formula-self forbidden checks before any source or bottle + # download is enqueued so a forbidden formula never triggers a fetch. + forbidden_tap_check(formula_only: true) + forbidden_formula_check(formula_only: true) + + # Needs to be done before expand_dependencies for compute_dependencies + fetch_bottle_tab(enqueue: true) if pour_bottle? + + fetch_fetch_deps unless ignore_deps? + + @ran_prelude_fetch_metadata = true end - # Run the formula-self forbidden checks before any source or bottle - # download is enqueued so a forbidden formula never triggers a fetch. - forbidden_tap_check(formula_only: true) - forbidden_formula_check(formula_only: true) + return if metadata_only || @ran_prelude_fetch if pour_bottle? - # Needs to be done before expand_dependencies for compute_dependencies - fetch_bottle_tab(enqueue: true) + @enqueued_bottle_download = enqueue_bottle_download(stage: true) elsif formula.loaded_from_api? Homebrew::API::Formula.source_download(formula, download_queue:, enqueue: true) end - fetch_fetch_deps unless ignore_deps? - @ran_prelude_fetch = true end @@ -438,20 +448,7 @@ class FormulaInstaller if Homebrew::EnvConfig.developer? # `recursive_dependencies` trims cyclic dependencies, so we do one level and take the recursive deps of that. # Mapping direct dependencies to deeper dependencies in a hash is also useful for the cyclic output below. - recursive_dep_map = formula.deps.to_h do |dep| - # We cheat a bit with bubblewrap. We eagerly add it to build dependencies on tier-one systems. - # But this cyclic dependency check is (intentionally) overly strict and forbids cyclic build dependencies, - # to help prevent cases that would break, for example, mass bottling. - recursive_deps = if dep.name == "bubblewrap" && dep.implicit? - [] - else - dep.to_formula.recursive_dependencies do |_dependent, recursive_dep| - Dependable::PRUNE if recursive_dep.name == "bubblewrap" && recursive_dep.implicit? - end - end - - [dep, recursive_deps] - end + recursive_dep_map = formula.deps.to_h { |dep| [dep, dep.to_formula.recursive_dependencies] } cyclic_dependencies = [] recursive_dep_map.each do |dep, recursive_deps| @@ -1056,8 +1053,6 @@ on_request: installed_on_request?, options:) # let's reset Utils::Git.available? if we just installed git Utils::Git.clear_available_cache if formula.name == "git" - Sandbox.reset_state! if formula.name == "bubblewrap" - # use installed ca-certificates when it's needed and available if formula.name == "ca-certificates" && !DevelopmentTools.ca_file_handles_most_https_certificates? @@ -1490,7 +1485,7 @@ on_request: installed_on_request?, options:) sig { void } def fetch enqueue_fetch - download_queue.fetch + download_queue.fetch(heading: "Fetching downloads for: #{Formatter.identifier(formula.full_name)}") end sig { void } @@ -1498,23 +1493,14 @@ on_request: installed_on_request?, options:) return if previously_fetched_formula downloadable_object = T.let(nil, T.nilable(Downloadable)) - bottle_download = T.let(nil, T.nilable(Downloadable)) check_attestation = T.let(false, T::Boolean) local_bottle_path = formula.local_bottle_path bottle_install = !only_deps? && local_bottle_path.nil? && pour_bottle?(output_warning: true) - # We skip `gh` to avoid a bootstrapping cycle, in the off-chance a user attempts - # to explicitly `brew install gh` without already having a version for bootstrapping. - # We also skip bottle installs from local bottle paths, as these are done in CI + # We skip bottle installs from local bottle paths, as these are done in CI # as part of the build lifecycle before attestations are produced. - verify_attestation = bottle_install && - Homebrew::EnvConfig.verify_attestations? && - (formula.tap&.core_tap? || false) && - formula.name != "gh" - if bottle_install && @ran_prelude - bottle_download = downloadable - check_attestation = verify_attestation && !bottle_download.cached_download.exist? - download_queue.enqueue(bottle_download, check_attestation:, stage: false) - end + verify_attestation = bottle_install && verify_bottle_attestation? + bottle_download = @enqueued_bottle_download + bottle_download = enqueue_bottle_download(stage: false) if bottle_download.nil? && bottle_install && @ran_prelude fetch_dependencies @@ -1535,8 +1521,10 @@ on_request: installed_on_request?, options:) downloadable_object = downloadable end - # Check attestation after download completes. - download_queue.enqueue(downloadable_object, check_attestation:) + # Check attestation after download completes. Skip downloads already + # enqueued (with staging) by `prelude_fetch` so a completed early fetch is + # not requeued and reported a second time. + download_queue.enqueue(downloadable_object, check_attestation:) if @enqueued_bottle_download.nil? self.class.fetched << formula rescue CannotInstallFormulaError @@ -1547,6 +1535,28 @@ on_request: installed_on_request?, options:) raise end + # Start the formula's own bottle download without waiting for its bottle + # manifest or dependency resolution; both call sites have already checked + # `pour_bottle?`. + sig { params(stage: T::Boolean).returns(T.nilable(Downloadable)) } + def enqueue_bottle_download(stage:) + return if only_deps? || formula.local_bottle_path + + bottle_download = downloadable + check_attestation = verify_bottle_attestation? && !bottle_download.cached_download.exist? + download_queue.enqueue(bottle_download, check_attestation:, stage:) + bottle_download + end + + sig { returns(T::Boolean) } + def verify_bottle_attestation? + # We skip `gh` to avoid a bootstrapping cycle, in the off-chance a user attempts + # to explicitly `brew install gh` without already having a version for bootstrapping. + Homebrew::EnvConfig.verify_attestations? && + (formula.tap&.core_tap? || false) && + formula.name != "gh" + end + sig { returns(Downloadable) } def downloadable if (bottle_path = formula.local_bottle_path) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 7c086685c3..7f92c0a2d4 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -126,7 +126,6 @@ module Formulary Homebrew::Trust.require_trusted_formula!(name, path) require "formula" - require "ignorable" require "stringio" # Capture stdout to prevent formulae from printing to stdout unexpectedly. @@ -144,16 +143,20 @@ module Formulary mod.const_set(:BUILD_FLAGS, flags) mod.module_eval(contents, path.to_s) rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersion::Error => e - if e.is_a?(Ignorable::ExceptionMixin) - e.ignore - else - remove_const(namespace) - raise FormulaUnreadableError.new(name, e) - end + remove_const(namespace) + raise FormulaUnreadableError.new(name, e) end ENV.clear_sensitive_environment_for_eval! do if ignore_errors - Ignorable.hook_raise(&eval_formula) + require "ignorable" + + on_ignorable = lambda do |e| + case e + when NameError, ArgumentError, MethodDeprecatedError, MacOSVersion::Error then :ignore + else :raise + end + end + Ignorable.hook_raise(on_ignorable:, &eval_formula) else eval_formula.call end @@ -942,7 +945,7 @@ module Formulary sig { overridable.params(flags: T::Array[String]).void } def load_from_api(flags:) formula_struct = Homebrew::API::Internal.formula_struct(name) - api_source = Homebrew::API::Internal.formula_hashes[name] + api_source = Homebrew::API::Internal.formula_hash(name) tap_git_head = Homebrew::API::Internal.formula_tap_git_head raise FormulaUnavailableError, name if api_source.nil? diff --git a/Library/Homebrew/github_runner_matrix.rb b/Library/Homebrew/github_runner_matrix.rb index c486c0a162..f0b5d8ea93 100644 --- a/Library/Homebrew/github_runner_matrix.rb +++ b/Library/Homebrew/github_runner_matrix.rb @@ -199,7 +199,7 @@ class GitHubRunnerMatrix unless self_hosted container = { image: "ghcr.io/homebrew/brew:main", - options: "--init --user linuxbrew --env HOMEBREW_SANDBOX_LINUX_LANDLOCK=1", + options: "--init --user linuxbrew", } workdir = "/github/home" end diff --git a/Library/Homebrew/ignorable.rb b/Library/Homebrew/ignorable.rb index 6cd7ba6fd5..6d3444c8b7 100644 --- a/Library/Homebrew/ignorable.rb +++ b/Library/Homebrew/ignorable.rb @@ -1,62 +1,63 @@ # typed: strict # frozen_string_literal: true -deprecated_warnings = Warning[:deprecated] -begin - Warning[:deprecated] = false - require "continuation" -ensure - Warning[:deprecated] = deprecated_warnings -end - # Provides the ability to optionally ignore errors raised and continue execution. module Ignorable - # Marks exceptions which can be ignored and provides - # the ability to jump back to where it was raised. - module ExceptionMixin - sig { returns(T.untyped) } - attr_accessor :continuation + # Marks exceptions which can be ignored and resumed from where they were raised. + module ExceptionMixin; end - sig { void } - def ignore - continuation.call - end - end + # Runs the block in a Fiber whose `raise` pauses at the raise site and passes + # the exception to `on_ignorable`. If it returns `:ignore`, execution resumes + # after the raise site, otherwise the exception is raised there as usual. + sig { + type_parameters(:U) + .params( + on_ignorable: T.proc.params(exception: Exception).returns(Symbol), + block: T.proc.returns(T.type_parameter(:U)), + ) + .returns(T.type_parameter(:U)) + } + def self.hook_raise(on_ignorable:, &block) + fiber = Fiber.new(&block) - sig { params(blk: T.nilable(T.proc.void)).void } - def self.hook_raise(&blk) Object.class_eval do - alias_method :original_raise, :raise - # `define_method` keeps Sorbet happy inside this `class_eval` block. - define_method(:raise) do |*args| - callcc do |continuation| - super(*args) - # Handle all possible exceptions. - rescue Exception => e # rubocop:disable Lint/RescueException - unless e.is_a?(ScriptError) - e.extend(ExceptionMixin) - T.cast(e, ExceptionMixin).continuation = continuation - end + define_method(:raise) do |*args, **kwargs| + super(*args, **kwargs) + # All possible exceptions must be pausable, not just `StandardError`. + rescue Exception => e # rubocop:disable Lint/RescueException + if e.is_a?(ScriptError) || Fiber.current != fiber super(e) + else + e.extend(ExceptionMixin) + super(e) if Fiber.yield(e) != :ignore end end alias_method :fail, :raise end - return unless block_given? + result = fiber.resume + while fiber.alive? + decision = begin + on_ignorable.call(result) + # Even `Interrupt` at the prompt must unwind the fiber, not abandon it. + rescue Exception => e # rubocop:disable Lint/RescueException + e + end - yield - unhook_raise - end - - sig { void } - def self.unhook_raise + result = case decision + when :ignore then fiber.resume(:ignore) + # Raise inside the fiber so its `ensure` blocks and rescues still run. + when Exception then fiber.raise(decision) + else fiber.resume(:raise) + end + end + result + ensure Object.class_eval do - alias_method :raise, :original_raise - alias_method :fail, :original_raise - undef_method :original_raise + remove_method(:raise) + remove_method(:fail) end end end diff --git a/Library/Homebrew/install.rb b/Library/Homebrew/install.rb index 97ec4b4902..c995f7697b 100644 --- a/Library/Homebrew/install.rb +++ b/Library/Homebrew/install.rb @@ -344,34 +344,30 @@ module Homebrew shutdown_download_queue: true, show_downloads_heading: true ) - formulae_names_to_install = formula_installers.map { |fi| fi.formula.name } - return formula_installers if formulae_names_to_install.empty? + return formula_installers if formula_installers.empty? download_queue = T.let(download_queue || Homebrew::DownloadQueue.new(pour: true), Homebrew::DownloadQueue) - if show_downloads_heading - formula_sentence = formulae_names_to_install.map { |name| Formatter.identifier(name) }.to_sentence - oh1 "Fetching downloads for: #{formula_sentence}", truncate: false - end - begin valid_formula_installers = prelude_fetch_formulae(formula_installers, download_queue:) - download_queue.fetch + # Wait on just the bottle manifests dependency resolution needs so + # in-flight bottles are only reported under the downloads heading. + download_queue.fetch(only: Resource::BottleManifest, heading: "Downloading bottle manifests", + allow_failures: true) [:prelude, :enqueue_fetch].each do |step| - valid_formula_installers.select! do |fi| - fi.public_send(step) - true - rescue CannotInstallFormulaError => e - ofail e.message - false - rescue UnsatisfiedRequirements, DownloadError, ChecksumMismatchError => e - ofail "#{fi.formula}: #{e}" - false - end + valid_formula_installers = select_formula_installers(valid_formula_installers, step:) next if step == :enqueue_fetch && !fetch_after_enqueue - download_queue.fetch + if step == :prelude + download_queue.fetch(only: Resource::BottleManifest, heading: "Downloading bottle manifests", + allow_failures: true) + else + heading = if show_downloads_heading + combined_fetch_downloads_heading(formula_names: valid_formula_installers.map { |fi| fi.formula.name }) + end + download_queue.fetch(heading:) + end end ensure download_queue.shutdown if shutdown_download_queue @@ -384,20 +380,39 @@ module Homebrew params( formula_installers: T::Array[FormulaInstaller], download_queue: Homebrew::DownloadQueue, + metadata_only: T::Boolean, ).returns(T::Array[FormulaInstaller]) } - def prelude_fetch_formulae(formula_installers, download_queue:) + def prelude_fetch_formulae(formula_installers, download_queue:, metadata_only: false) formula_installers.each do |fi| fi.download_queue = download_queue end + # Only pass the keyword when limiting the fetch so mocks and + # overrides expecting the historical no-argument call keep working. + action = ->(fi) { metadata_only ? fi.prelude_fetch(metadata_only: true) : fi.prelude_fetch } + select_formula_installers(formula_installers, action:) + end + + sig { + params( + formula_installers: T::Array[FormulaInstaller], + step: T.nilable(Symbol), + action: T.nilable(T.proc.params(formula_installer: FormulaInstaller).void), + ).returns(T::Array[FormulaInstaller]) + } + def select_formula_installers(formula_installers, step: nil, action: nil) formula_installers.select do |fi| - fi.prelude_fetch + if action + action.call(fi) + elsif step + fi.public_send(step) + end true rescue CannotInstallFormulaError => e ofail e.message false - rescue UnsatisfiedRequirements, DownloadError, ChecksumMismatchError => e + rescue => e ofail "#{fi.formula}: #{e}" false end @@ -414,27 +429,39 @@ module Homebrew ) end - sig { params(formula_names: T::Array[String], cask_names: T::Array[String]).void } - def show_combined_fetch_downloads_heading(formula_names: [], cask_names: []) + sig { params(formula_names: T::Array[String], cask_names: T::Array[String]).returns(T.nilable(String)) } + def combined_fetch_downloads_heading(formula_names: [], cask_names: []) combined_fetch_targets = formula_names.map { |name| Formatter.identifier(name) } + cask_names.map { |name| Formatter.identifier(name) } return if combined_fetch_targets.empty? - oh1 "Fetching downloads for: #{combined_fetch_targets.to_sentence}", truncate: false + "Fetching downloads for: #{combined_fetch_targets.to_sentence}" end sig { params(cask_installers: T::Array[T.untyped], download_queue: Homebrew::DownloadQueue).void } def enqueue_cask_installers(cask_installers, download_queue:) - if cask_installers.any?(&:source_download_requires_pre_fetch?) - source_downloads = cask_installers.filter_map(&:prelude_fetch_download) - if source_downloads.any? - oh1 "Downloading Cask files" - source_downloads.each { |source_download| download_queue.enqueue(source_download) } - download_queue.fetch + source_downloads = [] + valid_cask_installers = cask_installers.select do |cask_installer| + if cask_installer.source_download_requires_pre_fetch? && + (source_download = cask_installer.prelude_fetch_download) + source_downloads << source_download end + true + rescue => e + ofail "#{cask_installer.cask}: #{e}" + false end - cask_installers.each(&:enqueue_downloads) + if source_downloads.any? + source_downloads.each { |source_download| download_queue.enqueue(source_download) } + download_queue.fetch(only: Cask::Download, heading: "Downloading Cask files") + end + + valid_cask_installers.each do |cask_installer| + cask_installer.enqueue_downloads + rescue => e + ofail "#{cask_installer.cask}: #{e}" + end end sig { diff --git a/Library/Homebrew/install_steps.rb b/Library/Homebrew/install_steps.rb index 47e29dbc08..e6de9f8d5d 100644 --- a/Library/Homebrew/install_steps.rb +++ b/Library/Homebrew/install_steps.rb @@ -311,7 +311,7 @@ module Homebrew "source" => path_spec(source, base: source_base, default_base: @default_source_base), "target" => path_spec(target, base: target_base, default_base: @default_target_base), "recursive" => recursive, - "overwrite" => overwrite, + "overwrite" => (false unless overwrite), "source_glob" => source_glob) end @@ -382,8 +382,8 @@ module Homebrew default_base: @default_source_base), "target" => path_spec(target, base: target_base, formula: target_formula, default_base: @default_target_base), - "force" => force || overwrite, - "uninstall" => uninstall || remove_on_uninstall, + "force" => (true if force || overwrite), + "uninstall" => (true if uninstall || remove_on_uninstall), "source_glob" => source_glob, "sudo" => sudo.is_a?(::Symbol) ? sudo.to_s : sudo) end @@ -504,7 +504,7 @@ module Homebrew add_step("write", "path" => path_spec(path, base:, default_base: @default_base), "content" => content, - "overwrite" => overwrite) + "overwrite" => (true if overwrite)) end sig { @@ -641,7 +641,7 @@ module Homebrew add_step("set_ownership", "paths" => path_specs(paths, base:, default_base: @default_base), "user" => user, - "group" => group, + "group" => (group if group != "staff"), "non_recursive" => !recursive) end @@ -710,9 +710,9 @@ module Homebrew add_step("terminate_process", "name" => name, - "match" => match, + "match" => (match if match != "name"), "sudo" => sudo, - "attempts" => attempts, + "attempts" => (attempts if attempts != 1), "must_succeed" => must_succeed, "notices" => notices, "failure_message" => failure_message) @@ -1151,7 +1151,7 @@ module Homebrew ohai expand_template_tokens(notice) end name = expand_template_tokens(step_string(step, "name")) - if step_string(step, "match") == "full" + if step["match"] == "full" command = "/usr/bin/pkill" args = ["-f", name] else diff --git a/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md b/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md index 260931c66b..f04b58d56b 100644 --- a/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md +++ b/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md @@ -18,9 +18,9 @@ The canonical step methods follow existing Formula, Cask, `Pathname`, `FileUtils`, `SystemCommand` and utility naming where practical. Shared file operations use `mkdir_p`, `touch`, `move`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `symlink_tree`, `symlink_children` and `write_file`. -Formula steps default `mkdir_p` and `touch` paths to `var`, and source/target -paths to `prefix`. Cask steps default `base`, `source_base` and `target_base` -to `staged_path`. +Formula steps should specify `base: :var` for paths under `var`, while +source/target paths default to `prefix`. Cask steps default `base`, +`source_base` and `target_base` to `staged_path`. Formula `post_install_steps` may temporarily coexist with `post_install` so tap conversions can peel supported repeated statements out of larger hooks. Runtime @@ -427,8 +427,10 @@ is stripped during metadata serialisation. directory and `17` certificate/trust formulae could also move once their operations fit the supported step set. Runtime behaviour changes only for formulae that opt into `post_install_steps`. - Notes for implementation: default `mkdir_p`/`touch` to `var` and source/target - paths to `prefix`; expose the ordered array through `FormulaStruct`; make + Notes for implementation: formula definitions should specify `base: :var` + explicitly for `mkdir_p`/`touch` and other single-path steps, while + source/target paths default to `prefix`; expose the ordered array through + `FormulaStruct`; make `post_install_steps` run before any remaining `post_install`; document that the two forms may coexist only as an incremental conversion bridge. Keep the tap-wide autocorrect audit in a follow-up commit so the implementation can @@ -468,7 +470,7 @@ is stripped during metadata serialisation. - [x] PR 4.1, formula install-step sandboxing. Commit: `Sandbox formula install steps`. Scope: run structured formula steps inside the existing post-install child - process so macOS Seatbelt and Linux Bubblewrap apply the same filesystem and + process so macOS Seatbelt and Linux Landlock apply the same filesystem and network policy as legacy `post_install` hooks. This must land before any tap migrations use filesystem-mutating steps. - PR 5, default config and template writes (historical split workflow). @@ -687,3 +689,12 @@ is stripped during metadata serialisation. - [ ] PR 32, close the bridges and deprecate legacy hooks. Hard prerequisite: the merged `homebrew/core` head has no `post_install` methods and the merged `homebrew/cask` head has no legacy flight blocks. +- [ ] PR 33, remove the documented formula `var` default while retaining it + temporarily as a runtime compatibility fallback. +- [ ] PR 34, migrate every implicit `var` path in `homebrew/core` to an + explicit `base: :var`. `homebrew/cask` uses `staged_path` rather than `var` + as its install-step default and requires no matching migration. +- [ ] PR 35, audit and autocorrect implicit formula `var` paths so new + official-tap uses cannot be introduced. +- [ ] PR 36, remove the formula runtime compatibility fallback after the + official-tap migration and enforcement have landed. diff --git a/Library/Homebrew/on_system.rb b/Library/Homebrew/on_system.rb index 1f1771d065..daccf5af48 100644 --- a/Library/Homebrew/on_system.rb +++ b/Library/Homebrew/on_system.rb @@ -88,12 +88,15 @@ module OnSystem base.define_method(:"on_#{base_os}") do |&block| @on_system_blocks_exist = T.let(true, T.nilable(TrueClass)) @on_os_blocks_exist = T.let(true, T.nilable(TrueClass)) + @on_linux_blocks_exist = T.let(true, T.nilable(TrueClass)) if base_os == :linux return unless OnSystem.os_condition_met? OnSystem.condition_from_method_name(T.must(__method__)) @called_in_on_system_block = true + @called_in_on_os_block = T.let(true, T.nilable(T::Boolean)) result = block.call @called_in_on_system_block = false + @called_in_on_os_block = false result end @@ -102,6 +105,7 @@ module OnSystem base.define_method(:on_system) do |linux, macos:, &block| @on_system_blocks_exist = T.let(true, T.nilable(TrueClass)) @on_os_blocks_exist = T.let(true, T.nilable(TrueClass)) + @on_linux_blocks_exist = T.let(true, T.nilable(TrueClass)) raise ArgumentError, "The first argument to `on_system` must be `:linux`" if linux != :linux @@ -113,8 +117,10 @@ module OnSystem return if !OnSystem.os_condition_met?(os_version, or_condition) && !OnSystem.os_condition_met?(:linux) @called_in_on_system_block = true + @called_in_on_os_block = T.let(true, T.nilable(T::Boolean)) result = block.call @called_in_on_system_block = false + @called_in_on_os_block = false result end @@ -149,8 +155,10 @@ module OnSystem T.nilable(MacOSVersion), ) @called_in_on_system_block = T.let(true, T.nilable(T::Boolean)) + @called_in_on_os_block = T.let(true, T.nilable(T::Boolean)) result = block.call @called_in_on_system_block = false + @called_in_on_os_block = false result end diff --git a/Library/Homebrew/readall.rb b/Library/Homebrew/readall.rb index bbd3d06d54..22fe8914fe 100644 --- a/Library/Homebrew/readall.rb +++ b/Library/Homebrew/readall.rb @@ -3,34 +3,59 @@ require "formula" require "cask/cask_loader" -require "system_command" +require "tempfile" require "utils/output" # Helper module for validating syntax in taps. module Readall extend T::Generic extend Cachable - extend SystemCommand::Mixin extend Utils::Output::Mixin Cache = type_template { { fixed: T::Hash[Symbol, T.untyped] } } private_class_method :cache + MIN_FILES_PER_WORKER = 4 + private_constant :MIN_FILES_PER_WORKER + + # Buffers Ruby compile warnings from {.syntax_errors_or_warnings?} so they + # can be filtered before being printed to `$stderr`. + module WarningBuffer + sig { params(message: String, category: T.nilable(Symbol)).void } + def warn(message, category: nil) + buffer = Readall.warning_buffer + buffer ? buffer << message : super + end + end + private_constant :WarningBuffer + Warning.singleton_class.prepend(WarningBuffer) + + @warning_buffer = T.let(nil, T.nilable(T::Array[String])) + + class << self + sig { returns(T.nilable(T::Array[String])) } + attr_accessor :warning_buffer + end + sig { params(ruby_files: T::Array[Pathname]).returns(T::Boolean) } def self.valid_ruby_syntax?(ruby_files) - failed = T.let(false, T::Boolean) - ruby_files.each do |ruby_file| - # As a side effect, print syntax errors/warnings to `$stderr`. - failed = true if syntax_errors_or_warnings?(ruby_file) + parallel_slices_valid?(ruby_files) do |files| + failed = T.let(false, T::Boolean) + files.each do |ruby_file| + # As a side effect, print syntax errors/warnings to `$stderr`. + failed = true if syntax_errors_or_warnings?(ruby_file) + end + !failed end - !failed end sig { params(alias_dir: Pathname, formula_dir: Pathname).returns(T::Boolean) } def self.valid_aliases?(alias_dir, formula_dir) return true unless alias_dir.directory? + formula_basenames = Set.new(formula_dir.glob("**/*.rb").map { |formula_file| formula_file.basename.to_s }) + failed = T.let(false, T::Boolean) alias_dir.each_child do |f| if !f.symlink? @@ -41,7 +66,7 @@ module Readall failed = true end - if formula_dir.glob("**/#{f.basename}.rb").any?(&:exist?) + if formula_basenames.include?("#{f.basename}.rb") onoe "Formula duplicating alias: #{f}" failed = true end @@ -49,12 +74,16 @@ module Readall !failed end - sig { params(tap: Tap, bottle_tag: T.nilable(Utils::Bottles::Tag)).returns(T::Boolean) } - def self.valid_formulae?(tap, bottle_tag: nil) + sig { + params( + tap: Tap, bottle_tag: T.nilable(Utils::Bottles::Tag), files: T.nilable(T::Array[Pathname]), + ).returns(T::Boolean) + } + def self.valid_formulae?(tap, bottle_tag: nil, files: nil) cache[:valid_formulae] ||= {} success = T.let(true, T::Boolean) - tap.formula_files.each do |file| + (files || tap.formula_files).each do |file| valid = cache[:valid_formulae][file] next if valid == true || valid&.include?(bottle_tag) @@ -82,8 +111,12 @@ module Readall success end - sig { params(tap: Tap, os_name: T.nilable(Symbol), arch: T.nilable(Symbol)).returns(T::Boolean) } - def self.valid_casks?(tap, os_name: nil, arch: nil) + sig { + params( + tap: Tap, os_name: T.nilable(Symbol), arch: T.nilable(Symbol), files: T.nilable(T::Array[Pathname]), + ).returns(T::Boolean) + } + def self.valid_casks?(tap, os_name: nil, arch: nil, files: nil) validating_linux = if os_name.nil? Homebrew::SimulateSystem.current_os == :linux else @@ -95,9 +128,7 @@ module Readall os_and_arch += " on #{(arch == :intel) ? "Intel x86_64" : "ARM64"}" if arch success = T.let(true, T::Boolean) - tap.cask_files.each do |file| - next if file.read.match?(/^\s*depends_on(?:\s*\(\s*|\s+)(?::macos\b|macos:)/) - + (files || tap.cask_files).each do |file| cask = if arch Homebrew::SimulateSystem.with(os: :macos, arch:) do loaded_cask = Cask::CaskLoader.load(file) @@ -111,24 +142,28 @@ module Readall end next unless cask - linux_sha256 = if arch - Homebrew::SimulateSystem.with(os: :linux, arch:) do - cask.refresh - cask.sha256 - end + check_linux_sha256 = lambda do + cask.refresh + arch_types = cask.depends_on.arch&.map { |cask_arch| cask_arch[:type] } + # `depends_on arch:` excludes this architecture, so no Linux + # checksum is expected for it. + next true if arch_types&.exclude?(Homebrew::SimulateSystem.current_arch) + + !cask.sha256.nil? + end + linux_sha256_valid = if arch + Homebrew::SimulateSystem.with(os: :linux, arch:, &check_linux_sha256) else - Homebrew::SimulateSystem.with(os: :linux) do - cask.refresh - cask.sha256 - end + Homebrew::SimulateSystem.with(os: :linux, &check_linux_sha256) end # No `sha256` matched Linux, so the cask cannot be downloaded there # despite not being marked macOS-only. - next unless linux_sha256.nil? + next if linux_sha256_valid onoe "Invalid cask (#{os_and_arch}): #{file}" $stderr.puts "Missing Linux stanzas can leave Linux `sha256` as nil. " \ - "Add `depends_on :macos` if this cask is macOS-only." + "Add `depends_on :macos` if this cask is macOS-only or " \ + "`depends_on arch:` if it does not support this architecture." success = false rescue Interrupt raise @@ -155,40 +190,129 @@ module Readall success = false unless valid_aliases end - if no_simulate - success = false unless valid_formulae?(tap) - success = false unless valid_casks?(tap) - else - os_arch_combinations.each do |os, arch| - bottle_tag = Utils::Bottles::Tag.new(system: os, arch:) - next unless bottle_tag.valid_combination? + items = tap.formula_files.map { |file| [:formula, file] } + + tap.cask_files.map { |file| [:cask, file] } - Homebrew::SimulateSystem.with(os:, arch:) do - success = false unless valid_formulae?(tap, bottle_tag:) - success = false unless valid_casks?(tap, os_name: os, arch:) + all_files_valid = parallel_slices_valid?(items) do |slice| + formula_files = slice.filter_map { |type, file| file if type == :formula } + cask_files = slice.filter_map { |type, file| file if type == :cask } + + slice_success = T.let(true, T::Boolean) + if no_simulate + slice_success = false unless valid_formulae?(tap, files: formula_files) + slice_success = false unless valid_casks?(tap, files: cask_files) + else + os_arch_combinations.each do |os, arch| + bottle_tag = Utils::Bottles::Tag.new(system: os, arch:) + next unless bottle_tag.valid_combination? + + Homebrew::SimulateSystem.with(os:, arch:) do + slice_success = false unless valid_formulae?(tap, bottle_tag:, files: formula_files) + slice_success = false unless valid_casks?(tap, os_name: os, arch:, files: cask_files) + end end end + slice_success end + success = false unless all_files_valid success end sig { params(filename: Pathname).returns(T::Boolean) } private_class_method def self.syntax_errors_or_warnings?(filename) - # Retrieve messages about syntax errors/warnings printed to `$stderr`. - _, err, status = system_command(RUBY_PATH, args: ["-c", "-w", filename], print_stderr: false).to_a + # Compile in-process (much faster than spawning `ruby -c -w` per file), + # buffering compile warnings so they can be filtered. + error = T.let(nil, T.nilable(String)) + warnings = self.warning_buffer = [] + old_verbose = $VERBOSE + $VERBOSE = true + begin + RubyVM::InstructionSequence.compile_file(filename.to_s) + rescue ScriptError, ArgumentError => e + error = "#{e.message.chomp}\n" + ensure + $VERBOSE = old_verbose + self.warning_buffer = nil + end # Ignore unnecessary warning about named capture conflicts. # See https://bugs.ruby-lang.org/issues/12359. - messages = err.lines - .grep_v(/named capture conflicts a local variable/) - .join + messages = warnings.grep_v(/named capture conflicts a local variable/).join + messages += error if error $stderr.print messages - # Only syntax errors result in a non-zero status code. To detect syntax - # warnings we also need to inspect the output to `$stderr`. - !status.success? || !messages.chomp.empty? + # Both syntax errors and syntax warnings count as failures. + !messages.chomp.empty? + end + + sig { + type_parameters(:U).params( + items: T::Array[T.type_parameter(:U)], + _block: T.proc.params(arg0: T::Array[T.type_parameter(:U)]).returns(T::Boolean), + ).returns(T::Boolean) + } + private_class_method def self.parallel_slices_valid?(items, &_block) + require "hardware" + + worker_count = [Hardware::CPU.cores, items.length / MIN_FILES_PER_WORKER].min + return yield(items) if worker_count <= 1 + + workers = items.each_slice((items.length.to_f / worker_count).ceil).map do |slice| + reader, writer = IO.pipe + stdout_file = Tempfile.new("readall-stdout") + stderr_file = Tempfile.new("readall-stderr") + pid = Process.fork do + reader.close + success = begin + # Capture output so parallel workers cannot interleave lines. + $stdout = stdout_file.to_io + $stderr = stderr_file.to_io + yield(slice) + rescue Interrupt + false + # Report any worker exception as a validation failure. + rescue Exception => e # rubocop:disable Lint/RescueException + $stderr.puts e.full_message + false + ensure + $stdout.flush + $stderr.flush + end + writer.write(Marshal.dump(success)) + writer.close + exit!(true) + end + writer.close + [pid, reader, stdout_file, stderr_file] + end + + success = T.let(true, T::Boolean) + workers.each do |pid, reader, stdout_file, stderr_file| + worker_success = begin + # The data being loaded was written by our own forked child process. + Marshal.load(reader) # rubocop:disable Security/MarshalLoad + rescue EOFError + nil + end + reader.close + Process.wait(pid) + + [stdout_file, stderr_file].each(&:rewind) + $stdout.print stdout_file.read + $stderr.print stderr_file.read + [stdout_file, stderr_file].each(&:close!) + + case worker_success + when nil + onoe "readall worker exited unexpectedly!" + success = false + when false + success = false + end + end + success end end diff --git a/Library/Homebrew/rubocops/all.rb b/Library/Homebrew/rubocops/all.rb index 1293e278b3..23870ca732 100644 --- a/Library/Homebrew/rubocops/all.rb +++ b/Library/Homebrew/rubocops/all.rb @@ -13,6 +13,7 @@ require_relative "full_name_split" require_relative "io_read" require_relative "move_to_extend_os" require_relative "negate_include" +require_relative "no_base64" require_relative "no_fileutils_rmrf" require_relative "no_instance_variable_access_in_tests" require_relative "no_send_in_tests" diff --git a/Library/Homebrew/rubocops/no_base64.rb b/Library/Homebrew/rubocops/no_base64.rb new file mode 100644 index 0000000000..c733678d95 --- /dev/null +++ b/Library/Homebrew/rubocops/no_base64.rb @@ -0,0 +1,110 @@ +# typed: strict +# frozen_string_literal: true + +module RuboCop + module Cop + module Homebrew + # Enforces the use of `String#unpack1` and `Array#pack` over the + # `base64` gem, which Homebrew no longer includes. + # + # ### Example + # + # ```ruby + # # bad + # require "base64" + # Base64.decode64(encoded) + # Base64.strict_encode64(decoded) + # + # # good + # encoded.unpack1("m") + # [decoded].pack("m0") + # ``` + class NoBase64 < Base + include RangeHelp + extend AutoCorrector + + MSG = "Homebrew no longer includes the `base64` gem; " \ + "use `String#unpack1` or `Array#pack` instead." + + sig { params(node: RuboCop::AST::SendNode).void } + def on_send(node) + if require_base64?(node) + add_offense(node) do |corrector| + parent = node.parent + next if parent && !parent.begin_type? + + corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true)) + end + elsif top_level_const?(node.receiver, :Base64) + add_offense(node) do |corrector| + autocorrect_base64_call(corrector, node) + end + end + end + alias on_csend on_send + + sig { params(node: RuboCop::AST::ConstNode).void } + def on_const(node) + return unless top_level_const?(node, :Base64) + + parent = node.parent + return if parent.is_a?(RuboCop::AST::SendNode) && parent.receiver == node + # Formulae for base64 tools are legitimately named `Base64`. + return if parent.is_a?(RuboCop::AST::ClassNode) && parent.identifier == node + + add_offense(node) + end + + private + + sig { params(node: RuboCop::AST::SendNode).returns(T::Boolean) } + def require_base64?(node) + return false unless node.method?(:require) + + receiver = node.receiver + return false if receiver && !top_level_const?(receiver, :Kernel) + + arg = node.first_argument + node.arguments.one? && arg.is_a?(RuboCop::AST::StrNode) && arg.value == "base64" + end + + sig { params(node: T.nilable(RuboCop::AST::Node), name: Symbol).returns(T::Boolean) } + def top_level_const?(node, name) + return false unless node.is_a?(RuboCop::AST::ConstNode) + return false if node.short_name != name + + namespace = node.namespace + namespace.nil? || namespace.cbase_type? + end + + sig { params(corrector: RuboCop::Cop::Corrector, node: RuboCop::AST::SendNode).void } + def autocorrect_base64_call(corrector, node) + return unless node.arguments.one? + + arg = node.first_argument + replacement = case node.method_name + when :decode64, :strict_decode64 + directive = (node.method_name == :decode64) ? "m" : "m0" + "#{arg.source}.unpack1(\"#{directive}\")" if chainable?(arg) + when :encode64, :strict_encode64 + directive = (node.method_name == :encode64) ? "m" : "m0" + "[#{arg.source}].pack(\"#{directive}\")" if !arg.splat_type? && !arg.block_pass_type? + end + return if replacement.nil? + + corrector.replace(node, replacement) + end + + sig { params(node: RuboCop::AST::Node).returns(T::Boolean) } + def chainable?(node) + if node.is_a?(RuboCop::AST::SendNode) + !node.operator_method? && !node.assignment_method? + else + node.variable? || node.const_type? || node.begin_type? || + (node.literal? && !node.range_type?) + end + end + end + end + end +end diff --git a/Library/Homebrew/sandbox.rb b/Library/Homebrew/sandbox.rb index ada62f515d..a967a5025b 100644 --- a/Library/Homebrew/sandbox.rb +++ b/Library/Homebrew/sandbox.rb @@ -120,9 +120,6 @@ class Sandbox true end - sig { params(install_from_tests: T::Boolean).void } - def self.ensure_sandbox_installed!(install_from_tests: false); end - sig { void } def self.ensure_sandbox_available! return if available? @@ -145,24 +142,8 @@ class Sandbox sig { void } def self.reset_state!; end - sig { returns(T::Array[String]) } - def self.configuration_commands = [] - - sig { returns(T::Array[String]) } - def self.configuration_command_messages = [] - - sig { returns(T.nilable(String)) } - def self.sandbox_install_command = nil - - sig { void } - def self.configure! - ensure_sandbox_installed! - reset_state! - end - sig { params(command: T.any(String, Pathname), writable_path: T.any(String, Pathname), deny_network: T::Boolean).void } def self.run_command(*command, writable_path:, deny_network: false) - ensure_sandbox_installed! ensure_sandbox_available! writable_path = Pathname(writable_path).expand_path diff --git a/Library/Homebrew/services/formula_wrapper.rb b/Library/Homebrew/services/formula_wrapper.rb index 3c9c7a3cd5..e4f6b55725 100644 --- a/Library/Homebrew/services/formula_wrapper.rb +++ b/Library/Homebrew/services/formula_wrapper.rb @@ -263,13 +263,17 @@ module Homebrew end # Generate the service file content (plist or systemd unit), - # including any per-service user environment variable overrides. + # including any per-service user environment variable overrides, + # or read the package-provided service file if the formula's + # service block does not define a command. sig { returns(String) } def service_contents - if System.launchctl? - formula.service.to_plist + if !service? || !load_service.command? + service_file.read + elsif System.launchctl? + load_service.to_plist else - formula.service.to_systemd_unit + load_service.to_systemd_unit end end diff --git a/Library/Homebrew/simulate_system.rb b/Library/Homebrew/simulate_system.rb index 9fb8c67dbe..3517418d4e 100644 --- a/Library/Homebrew/simulate_system.rb +++ b/Library/Homebrew/simulate_system.rb @@ -76,6 +76,11 @@ module Homebrew @os = @arch = nil end + sig { returns(T::Boolean) } + def simulating? + os.present? || arch.present? + end + sig { returns(T::Boolean) } def simulating_or_running_on_macos? [:macos, *MacOSVersion::SYMBOLS.keys].include?(os) diff --git a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi index 38343e962d..05043d698e 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi @@ -159,6 +159,9 @@ class Cask::Cask sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def no_autobump_message(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) } + def on_linux_blocks_exist?(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T::Boolean) } def on_os_blocks_exist?(*args, &block); end diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/setup_sandbox.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/setup_sandbox.rbi deleted file mode 100644 index 5aba326c27..0000000000 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/setup_sandbox.rbi +++ /dev/null @@ -1,13 +0,0 @@ -# typed: true - -# DO NOT EDIT MANUALLY -# This is an autogenerated file for dynamic methods in `Homebrew::Cmd::SetupSandbox`. -# Please instead update this file by running `bin/tapioca dsl Homebrew::Cmd::SetupSandbox`. - - -class Homebrew::Cmd::SetupSandbox - sig { returns(Homebrew::Cmd::SetupSandbox::Args) } - def args; end -end - -class Homebrew::Cmd::SetupSandbox::Args < Homebrew::CLI::Args; end diff --git a/Library/Homebrew/sorbet/rbi/dsl/r_spec/matchers.rbi b/Library/Homebrew/sorbet/rbi/dsl/r_spec/matchers.rbi index 05360046e2..51bfecded6 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/r_spec/matchers.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/r_spec/matchers.rbi @@ -153,6 +153,9 @@ module RSpec::Matchers sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def be_installed_on_request(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def be_installed_on_request_present(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def be_latest_version_installed(*args, &block); end diff --git a/Library/Homebrew/sorbet/rbi/gems/csv@3.3.5.rbi b/Library/Homebrew/sorbet/rbi/gems/csv@3.3.6.rbi similarity index 85% rename from Library/Homebrew/sorbet/rbi/gems/csv@3.3.5.rbi rename to Library/Homebrew/sorbet/rbi/gems/csv@3.3.6.rbi index 963c003d2f..8a177c4d7a 100644 --- a/Library/Homebrew/sorbet/rbi/gems/csv@3.3.5.rbi +++ b/Library/Homebrew/sorbet/rbi/gems/csv@3.3.6.rbi @@ -19,12 +19,12 @@ class CSV def <<(row); end def add_row(row); end - def binmode(*args, **_arg1, &block); end + def binmode(*_arg0, **_arg1, &_arg2); end def binmode?; end - def close(*args, **_arg1, &block); end - def close_read(*args, **_arg1, &block); end - def close_write(*args, **_arg1, &block); end - def closed?(*args, **_arg1, &block); end + def close(*_arg0, **_arg1, &_arg2); end + def close_read(*_arg0, **_arg1, &_arg2); end + def close_write(*_arg0, **_arg1, &_arg2); end + def closed?(*_arg0, **_arg1, &_arg2); end def col_sep; end def convert(name = T.unsafe(nil), &converter); end def converters; end @@ -32,53 +32,53 @@ class CSV def encoding; end def eof; end def eof?; end - def external_encoding(*args, **_arg1, &block); end - def fcntl(*args, **_arg1, &block); end + def external_encoding(*_arg0, **_arg1, &_arg2); end + def fcntl(*_arg0, **_arg1, &_arg2); end def field_size_limit; end - def fileno(*args, **_arg1, &block); end + def fileno(*_arg0, **_arg1, &_arg2); end def flock(*args); end - def flush(*args, **_arg1, &block); end + def flush(*_arg0, **_arg1, &_arg2); end def force_quotes?; end - def fsync(*args, **_arg1, &block); end + def fsync(*_arg0, **_arg1, &_arg2); end def gets; end def header_convert(name = T.unsafe(nil), &converter); end def header_converters; end def header_row?; end def headers; end def inspect; end - def internal_encoding(*args, **_arg1, &block); end + def internal_encoding(*_arg0, **_arg1, &_arg2); end def ioctl(*args); end - def isatty(*args, **_arg1, &block); end + def isatty(*_arg0, **_arg1, &_arg2); end def liberal_parsing?; end def line; end def lineno; end def max_field_size; end def path; end - def pid(*args, **_arg1, &block); end - def pos(*args, **_arg1, &block); end - def pos=(*args, **_arg1, &block); end + def pid(*_arg0, **_arg1, &_arg2); end + def pos(*_arg0, **_arg1, &_arg2); end + def pos=(*_arg0, **_arg1, &_arg2); end def puts(row); end def quote_char; end def read; end def readline; end def readlines; end - def reopen(*args, **_arg1, &block); end + def reopen(*_arg0, **_arg1, &_arg2); end def return_headers?; end def rewind; end def row_sep; end - def seek(*args, **_arg1, &block); end + def seek(*_arg0, **_arg1, &_arg2); end def shift; end def skip_blanks?; end def skip_lines; end def stat(*args); end - def string(*args, **_arg1, &block); end - def sync(*args, **_arg1, &block); end - def sync=(*args, **_arg1, &block); end - def tell(*args, **_arg1, &block); end + def string(*_arg0, **_arg1, &_arg2); end + def sync(*_arg0, **_arg1, &_arg2); end + def sync=(*_arg0, **_arg1, &_arg2); end + def tell(*_arg0, **_arg1, &_arg2); end def to_i; end def to_io; end - def truncate(*args, **_arg1, &block); end - def tty?(*args, **_arg1, &block); end + def truncate(*_arg0, **_arg1, &_arg2); end + def tty?(*_arg0, **_arg1, &_arg2); end def unconverted_fields?; end def write_headers?; end @@ -93,7 +93,6 @@ class CSV def header_fields_converter; end def normalize_converters(converters); end def parser; end - def parser_enumerator; end def parser_fields_converter; end def parser_options; end def raw_encoding; end @@ -103,22 +102,22 @@ class CSV class << self def filter(input = T.unsafe(nil), output = T.unsafe(nil), **options); end - def foreach(path, mode = T.unsafe(nil), **options, &block); end + def foreach(path_or_io, mode = T.unsafe(nil), **options, &block); end def generate(str = T.unsafe(nil), **options); end def generate_line(row, **options); end def generate_lines(rows, **options); end def instance(data = T.unsafe(nil), **options); end - def open(filename_or_io, mode = T.unsafe(nil), **options); end + def open(path_or_io, mode = T.unsafe(nil), **options); end def parse(str, **options, &block); end def parse_line(line, **options); end - def read(path, **options); end - def readlines(path, **options); end - def table(path, **options); end + def read(path_or_io, **options); end + def readlines(path_or_io, **options); end + def table(path_or_io, **options); end private def create_stringio(str, mode, opts); end - def may_enable_bom_detection_automatically(filename_or_io, mode, options, file_opts); end + def may_enable_bom_detection_automatically(path_or_io, mode, options, file_opts); end end end @@ -187,6 +186,7 @@ class CSV::Parser def initialize(input, options); end def column_separator; end + def eof?; end def field_size_limit; end def header_row?; end def headers; end @@ -310,7 +310,7 @@ class CSV::Row def dig(index_or_header, *indexes); end def each(&block); end def each_pair(&block); end - def empty?(*args, **_arg1, &block); end + def empty?(*_arg0, **_arg1, &_arg2); end def fetch(header, *varargs); end def field(header_or_index, minimum_index = T.unsafe(nil)); end def field?(data); end @@ -324,10 +324,10 @@ class CSV::Row def index(header, minimum_index = T.unsafe(nil)); end def inspect; end def key?(header); end - def length(*args, **_arg1, &block); end + def length(*_arg0, **_arg1, &_arg2); end def member?(header); end def push(*args); end - def size(*args, **_arg1, &block); end + def size(*_arg0, **_arg1, &_arg2); end def to_ary(*_arg0); end def to_csv(**options); end def to_h; end @@ -368,13 +368,13 @@ class CSV::Table def delete_if(&block); end def dig(index_or_header, *index_or_headers); end def each(&block); end - def empty?(*args, **_arg1, &block); end + def empty?(*_arg0, **_arg1, &_arg2); end def headers; end def inspect; end - def length(*args, **_arg1, &block); end + def length(*_arg0, **_arg1, &_arg2); end def mode; end def push(*rows); end - def size(*args, **_arg1, &block); end + def size(*_arg0, **_arg1, &_arg2); end def to_a; end def to_csv(write_headers: T.unsafe(nil), limit: T.unsafe(nil), **options); end def to_s(write_headers: T.unsafe(nil), limit: T.unsafe(nil), **options); end diff --git a/Library/Homebrew/sorbet/rbi/gems/patchelf@1.5.2.rbi b/Library/Homebrew/sorbet/rbi/gems/patchelf@1.6.2.rbi similarity index 100% rename from Library/Homebrew/sorbet/rbi/gems/patchelf@1.5.2.rbi rename to Library/Homebrew/sorbet/rbi/gems/patchelf@1.6.2.rbi diff --git a/Library/Homebrew/sorbet/rbi/gems/rbs@4.0.3.rbi b/Library/Homebrew/sorbet/rbi/gems/rbs@4.1.2.rbi similarity index 97% rename from Library/Homebrew/sorbet/rbi/gems/rbs@4.0.3.rbi rename to Library/Homebrew/sorbet/rbi/gems/rbs@4.1.2.rbi index 15a49d8714..ff9086d0d4 100644 --- a/Library/Homebrew/sorbet/rbi/gems/rbs@4.0.3.rbi +++ b/Library/Homebrew/sorbet/rbi/gems/rbs@4.1.2.rbi @@ -494,6 +494,21 @@ class RBS::AST::Ruby::Annotations::ModuleAliasAnnotation < ::RBS::AST::Ruby::Ann def type_fingerprint; end end +class RBS::AST::Ruby::Annotations::ModuleSelfAnnotation < ::RBS::AST::Ruby::Annotations::Base + def initialize(location:, prefix_location:, keyword_location:, colon_location:, name:, args:, open_bracket_location:, close_bracket_location:, args_comma_locations:, comment_location:); end + + def args; end + def args_comma_locations; end + def close_bracket_location; end + def colon_location; end + def comment_location; end + def keyword_location; end + def map_type_name; end + def name; end + def open_bracket_location; end + def type_fingerprint; end +end + class RBS::AST::Ruby::Annotations::NodeTypeAssertion < ::RBS::AST::Ruby::Annotations::Base def initialize(location:, prefix_location:, type:); end @@ -723,9 +738,11 @@ class RBS::AST::Ruby::Members::Base end class RBS::AST::Ruby::Members::DefMember < ::RBS::AST::Ruby::Members::Base - def initialize(buffer, name, node, method_type, leading_comment); end + def initialize(buffer, name, node, method_type, leading_comment, kind: T.unsafe(nil)); end def annotations; end + def instance?; end + def kind; end def leading_comment; end def location; end def method_type; end @@ -734,6 +751,7 @@ class RBS::AST::Ruby::Members::DefMember < ::RBS::AST::Ruby::Members::Base def node; end def overloading?; end def overloads; end + def singleton?; end def type_fingerprint; end end @@ -804,6 +822,16 @@ class RBS::AST::Ruby::Members::MixinMember < ::RBS::AST::Ruby::Members::Base def type_fingerprint; end end +class RBS::AST::Ruby::Members::ModuleSelfMember < ::RBS::AST::Ruby::Members::Base + def initialize(buffer, annotation); end + + def annotation; end + def args; end + def location; end + def name; end + def type_fingerprint; end +end + class RBS::AST::Ruby::Members::PrependMember < ::RBS::AST::Ruby::Members::MixinMember; end class RBS::AST::TypeParam @@ -908,6 +936,7 @@ class RBS::Buffer def initialize(content:, name: T.unsafe(nil), parent: T.unsafe(nil)); end def absolute_position(position); end + def character_offset(byte_offset); end def content; end def detach; end def inspect; end @@ -922,7 +951,7 @@ class RBS::Buffer def pos_to_loc(pos); end def ranges; end def rbs_location(location, loc2 = T.unsafe(nil)); end - def sub_buffer(lines:); end + def sub_buffer(lines:, byte_lines_hint: T.unsafe(nil)); end def top_buffer; end end @@ -1069,6 +1098,7 @@ class RBS::Collection::Config::LockfileGenerator::GemfileLockMismatchError < ::S def message; end end +RBS::Collection::Config::LockfileGenerator::NONGEM_STDLIBS = T.let(T.unsafe(nil), Set) RBS::Collection::Config::PATH = T.let(T.unsafe(nil), Pathname) class RBS::Collection::Installer @@ -1863,6 +1893,7 @@ class RBS::InlineParser::Parser < ::Prism::Visitor def skip_node?(node); end def visit_call_node(node); end def visit_class_node(node); end + def visit_class_or_module_body(decl, node); end def visit_constant_path_write_node(node); end def visit_constant_write_node(node); end def visit_def_node(node); end @@ -2056,12 +2087,15 @@ class RBS::Namespace def to_type_name; end class << self + def [](path, absolute); end def empty; end def parse(string); end def root; end end end +module RBS::Namespace::INTERN_LEAF; end + class RBS::NoMixinFoundError < ::RBS::DefinitionError include ::RBS::DetailedMessageable @@ -2128,9 +2162,12 @@ class RBS::Parser def _parse_inline_leading_annotation(_arg0, _arg1, _arg2, _arg3); end def _parse_inline_trailing_annotation(_arg0, _arg1, _arg2, _arg3); end def _parse_method_type(_arg0, _arg1, _arg2, _arg3, _arg4); end + def _parse_method_type_to_bytes(_arg0, _arg1, _arg2, _arg3, _arg4); end def _parse_signature(_arg0, _arg1, _arg2); end + def _parse_signature_to_bytes(_arg0, _arg1, _arg2); end def _parse_type(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5, _arg6, _arg7); end def _parse_type_params(_arg0, _arg1, _arg2, _arg3); end + def _parse_type_to_bytes(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5, _arg6, _arg7); end def buffer(source); end def byte_range(char_range, content); end def lex(source); end @@ -2247,32 +2284,47 @@ class RBS::Prototype::RBI def initialize; end + def append_decl(decl); end + def attribute_type(kind, sigs); end def call_node?(node, name:, receiver: T.unsafe(nil), args: T.unsafe(nil)); end def const_to_name(node); end + def current_context; end + def current_context!; end def current_module; end def current_module!; end - def current_namespace; end def decls; end def each_arg(array, &block); end def each_child(node); end def join_comments(nodes, comments); end def last_sig; end + def member_visibility(context); end def method_type(args_node, type_node, variables:, overloads:); end def modules; end - def nested_name(name); end def node_to_hash(node); end def parse(string); end def parse_params(args_node, args, method_type, variables:, overloads:); end def pop_sig; end def proc_type?(type_node); end def process(node, comments:, outer: T.unsafe(nil)); end + def process_attribute(node, comments:); end + def process_visibility(node, outer:, comments:); end def push_class(name, super_class, comment:); end def push_module(name, comment:); end def push_sig(node); end + def sync_visibility(visibility); end def type_of(type_node, variables:); end def type_of0(type_node, variables:); end end +class RBS::Prototype::RBI::Context + def initialize(singleton:, visibility:); end + + def singleton; end + def singleton=(_arg0); end + def visibility; end + def visibility=(_arg0); end +end + class RBS::Prototype::Runtime include ::RBS::Prototype::Helpers include ::RBS::Prototype::Runtime::Helpers @@ -2509,13 +2561,28 @@ class RBS::Resolver::TypeNameResolver def resolve_namespace(type_name, context:); end def resolve_namespace0(type_name, context, visited); end def resolve_type_name(type_name, context); end - def try_cache(query); end + def try_cache(type_name, context); end class << self def build(env); end end end +class RBS::Rewriter + def initialize(buffer); end + + def add_comment(*locations, content:); end + def buffer; end + def delete_comment(comment); end + def replace_comment(comment, content:); end + def rewrite(location, string); end + def string; end + + private + + def format_comment(content, indent); end +end + module RBS::Source; end class RBS::Source::RBS @@ -2652,6 +2719,7 @@ class RBS::TypeName def with_prefix(namespace); end class << self + def [](namespace, name); end def parse(string); end end end diff --git a/Library/Homebrew/sorbet/rbi/gems/simplecov@1.0.2.rbi b/Library/Homebrew/sorbet/rbi/gems/simplecov@1.0.3.rbi similarity index 99% rename from Library/Homebrew/sorbet/rbi/gems/simplecov@1.0.2.rbi rename to Library/Homebrew/sorbet/rbi/gems/simplecov@1.0.3.rbi index a504482a33..7f950a30d1 100644 --- a/Library/Homebrew/sorbet/rbi/gems/simplecov@1.0.2.rbi +++ b/Library/Homebrew/sorbet/rbi/gems/simplecov@1.0.3.rbi @@ -578,7 +578,11 @@ class SimpleCov::Filter end end -module SimpleCov::Formatter; end +module SimpleCov::Formatter + class << self + def instance_for(formatter); end + end +end class SimpleCov::Formatter::Base def initialize(silent: T.unsafe(nil), output_dir: T.unsafe(nil)); end @@ -886,8 +890,10 @@ class SimpleCov::ResultAdapter def adapt_oneshot_lines_if_needed(file_name, cover_statistic); end def aggregate_duplicated_branches(cover_statistic); end def build_line_stub(file_name, oneshot_lines); end + def class_display_name(klass); end def normalize_method_key(key); end def normalize_method_keys(cover_statistic); end + def singleton_wrapper_name(klass); end class << self def call(*_arg0); end @@ -1119,6 +1125,7 @@ module SimpleCov::SourceFile::RubyDataParser def call(structure); end def parse_array_string(str); end + def parse_cache; end def parse_element(node); end def parse_integer_node(node); end def parse_symbol_node(node); end @@ -1129,6 +1136,7 @@ module SimpleCov::SourceFile::RubyDataParser class << self def call(structure); end def parse_array_string(str); end + def parse_cache; end def parse_element(node); end def parse_integer_node(node); end def parse_symbol_node(node); end diff --git a/Library/Homebrew/sorbet/rbi/shims/fiber.rbi b/Library/Homebrew/sorbet/rbi/shims/fiber.rbi new file mode 100644 index 0000000000..6dd9d548c7 --- /dev/null +++ b/Library/Homebrew/sorbet/rbi/shims/fiber.rbi @@ -0,0 +1,8 @@ +# typed: strict + +# Sorbet's core RBI for `Fiber` is missing `initialize` so does not know +# `Fiber.new` takes a block. +class Fiber + sig { params(blk: T.proc.returns(T.untyped)).void } + def initialize(&blk); end +end diff --git a/Library/Homebrew/style.rb b/Library/Homebrew/style.rb index b3fb4b68fa..f4d85e1620 100644 --- a/Library/Homebrew/style.rb +++ b/Library/Homebrew/style.rb @@ -3,6 +3,7 @@ require "shellwords" require "source_location" +require "stringio" require "system_command" require "tap" require "utils/output" @@ -88,9 +89,53 @@ module Homebrew end end - rubocop_result = if files.present? && ruby_files.empty? - (output_type == :json) ? [] : true - else + rubocop_needed = files.blank? || ruby_files.any? + shell_needed = files.blank? || shell_files.any? + + actionlint_files = github_workflow_files if files.blank? && actionlint_files.blank? + has_actionlint_workflow = actionlint_files.any? do |path| + path.to_s.end_with?("/.github/workflows/actionlint.yml") + end + odebug "actionlint workflow detected. Skipping actionlint checks." if has_actionlint_workflow + actionlint_needed = files.blank? || (!has_actionlint_workflow && actionlint_files.any?) + + # Resolve the linter executables (installing them if necessary) before + # spawning threads so those threads cannot race to install formulae. + shellcheck_path = (shellcheck if shell_needed || actionlint_needed) + shfmt_path = (shfmt_executable if shell_needed) + actionlint_path = (actionlint if actionlint_needed) + + shellcheck_out = StringIO.new + shellcheck_err = StringIO.new + shfmt_out = StringIO.new + shfmt_err = StringIO.new + actionlint_out = StringIO.new + actionlint_err = StringIO.new + + # Run the shell and GitHub Actions checks on background threads with + # buffered output while RuboCop runs on the main thread. + shell_thread = Thread.new do + shellcheck_result = if shell_needed + run_shellcheck(shell_files, output_type, fix:, shellcheck_path:, + out: shellcheck_out, err: shellcheck_err) + elsif output_type == :json + [] + else + true + end + # `shellcheck --fix` and `shfmt --write` may touch the same files so + # they must not run concurrently with each other. + shfmt_result = !shell_needed || run_shfmt!(shell_files, fix:, shfmt_path:, + out: shfmt_out, err: shfmt_err) + [shellcheck_result, shfmt_result] + end + actionlint_thread = Thread.new do + !actionlint_needed || + run_actionlint!(actionlint_files, actionlint_path:, shellcheck_path:, + out: actionlint_out, err: actionlint_err) + end + + rubocop_result = if rubocop_needed run_rubocop(ruby_files, output_type, fix:, todo:, @@ -98,24 +143,23 @@ module Homebrew display_cop_names:, reset_cache:, debug:, verbose:) - end - - shellcheck_result = if files.present? && shell_files.empty? - (output_type == :json) ? [] : true + elsif output_type == :json + [] else - run_shellcheck(shell_files, output_type, fix:) + true end - shfmt_result = files.present? && shell_files.empty? - shfmt_result ||= run_shfmt!(shell_files, fix:) + shellcheck_result, shfmt_result = shell_thread.value + actionlint_result = actionlint_thread.value - actionlint_files = github_workflow_files if files.blank? && actionlint_files.blank? - has_actionlint_workflow = actionlint_files.any? do |path| - path.to_s.end_with?("/.github/workflows/actionlint.yml") + [ + [shellcheck_out, shellcheck_err], + [shfmt_out, shfmt_err], + [actionlint_out, actionlint_err], + ].each do |out, err| + $stdout.print out.string + $stderr.print err.string end - odebug "actionlint workflow detected. Skipping actionlint checks." if has_actionlint_workflow - actionlint_result = files.present? && (has_actionlint_workflow || actionlint_files.empty?) - actionlint_result ||= run_actionlint!(actionlint_files) if output_type == :json Offenses.new( @@ -200,7 +244,7 @@ module Homebrew HOMEBREW_CACHE.mkpath cache_dir = HOMEBREW_CACHE.realpath/"style" cache_env = if (!cache_dir.exist? && cache_dir.parent.writable?) || cache_dir.writable? - args << "--parallel" unless fix + args << "--parallel" FileUtils.rm_rf cache_dir if reset_cache @@ -239,10 +283,17 @@ module Homebrew end sig { - params(files: T::Array[Pathname], output_type: Symbol, fix: T::Boolean) - .returns(T.nilable(T.any(T::Boolean, T::Array[T::Hash[String, T.untyped]]))) + params( + files: T::Array[Pathname], + output_type: Symbol, + fix: T::Boolean, + shellcheck_path: T.nilable(Pathname), + out: T.any(IO, StringIO), + err: T.any(IO, StringIO), + ).returns(T.nilable(T.any(T::Boolean, T::Array[T::Hash[String, T.untyped]]))) } - def self.run_shellcheck(files, output_type, fix: false) + def self.run_shellcheck(files, output_type, fix: false, shellcheck_path: nil, out: $stdout, err: $stderr) + shellcheck_path ||= shellcheck files = shell_scripts if files.blank? files = files.map(&:realpath) # use absolute file paths @@ -252,8 +303,6 @@ module Homebrew "--enable=all", "--external-sources", "--source-path=#{HOMEBREW_LIBRARY}", - "--", - *files, ] if fix @@ -264,17 +313,23 @@ module Homebrew # -p0 (--strip=0) : do not strip path prefixes, since we are at root directory # NOTE: We use short flags for compatibility. patch_command = %w[patch -g 0 -f -d / -p0] - patches = system_command(shellcheck, args: ["--format=diff", *args]).stdout + patches = shellcheck_chunks(shellcheck_path, files, ["--format=diff", *args]).map(&:stdout).join Utils.safe_popen_write(*patch_command) { |p| p.write(patches) } if patches.present? end case output_type when :print - system shellcheck, "--format=tty", *args - $CHILD_STATUS.success? + print_args = ["--format=tty", *args] + print_args << "--color=always" if Tty.color? + results = shellcheck_chunks(shellcheck_path, files, print_args) + results.each do |result| + out.print result.stdout + err.print result.stderr + end + results.all?(&:success?) when :json - result = system_command shellcheck, args: ["--format=json", *args] - json = json_result!(result) + results = shellcheck_chunks(shellcheck_path, files, ["--format=json", *args]) + json = results.flat_map { |result| json_result!(result) } # Convert to same format as RuboCop offenses. severity_hash = { "style" => "refactor", "info" => "convention" } @@ -312,8 +367,37 @@ module Homebrew end end - sig { params(files: T::Array[Pathname], fix: T::Boolean).returns(T::Boolean) } - def self.run_shfmt!(files, fix: false) + sig { + params( + shellcheck_path: Pathname, + files: T::Array[Pathname], + args: T::Array[String], + ).returns(T::Array[SystemCommand::Result]) + } + private_class_method def self.shellcheck_chunks(shellcheck_path, files, args) + require "hardware" + + chunk_count = [Hardware::CPU.cores, files.length].min + return [] if chunk_count.zero? + + files.each_slice((files.length.to_f / chunk_count).ceil).map do |chunk| + Thread.new do + system_command shellcheck_path, args: [*args, "--", *chunk], print_stderr: false + end + end.map(&:value) + end + + sig { + params( + files: T::Array[Pathname], + fix: T::Boolean, + shfmt_path: T.nilable(Pathname), + out: T.any(IO, StringIO), + err: T.any(IO, StringIO), + ).returns(T::Boolean) + } + def self.run_shfmt!(files, fix: false, shfmt_path: nil, out: $stdout, err: $stderr) + shfmt_path ||= shfmt_executable files = shell_scripts if files.blank? # Do not format completions and Dockerfile files.delete(HOMEBREW_REPOSITORY/"completions/bash/brew") @@ -322,23 +406,27 @@ module Homebrew args = ["--language-dialect", "bash", "--indent", "2", "--case-indent", "--", *files] args.unshift("--write") if fix # need to add before "--" - require "formula" - shfmt_executable = T.cast( - Formula["shfmt"].ensure_installed!(latest: true, - reason: "formatting shell scripts", - executable: "shfmt"), - Pathname, - ) - system( - { "HOMEBREW_SHFMT" => shfmt_executable.to_s }, - shfmt, - *args, - ) - $CHILD_STATUS.success? + result = system_command shfmt, + args:, + env: { "HOMEBREW_SHFMT" => shfmt_path.to_s }, + print_stderr: false + out.print result.stdout + err.print result.stderr + result.success? end - sig { params(files: T::Array[Pathname]).returns(T::Boolean) } - def self.run_actionlint!(files) + sig { + params( + files: T::Array[Pathname], + actionlint_path: T.nilable(Pathname), + shellcheck_path: T.nilable(Pathname), + out: T.any(IO, StringIO), + err: T.any(IO, StringIO), + ).returns(T::Boolean) + } + def self.run_actionlint!(files, actionlint_path: nil, shellcheck_path: nil, out: $stdout, err: $stderr) + actionlint_path ||= actionlint + shellcheck_path ||= shellcheck files = github_workflow_files if files.blank? tap_configs = files.filter_map do |f| @@ -350,18 +438,21 @@ module Homebrew end.uniq config_file = if tap_configs.one? - tap_configs.first + tap_configs.fetch(0) else HOMEBREW_REPOSITORY/".github/actionlint.yaml" end # the ignore is to avoid false positives in e.g. actions, homebrew-test-bot - system actionlint, "-shellcheck", shellcheck, - "-config-file", config_file, - "-ignore", "image: string; options: string", - "-ignore", "label .* is unknown", - *files - $CHILD_STATUS.success? + args = ["-shellcheck", shellcheck_path, + "-config-file", config_file, + "-ignore", "image: string; options: string", + "-ignore", "label .* is unknown"] + args << "-color" if Tty.color? + result = system_command actionlint_path, args: [*args, *files], print_stderr: false + out.print result.stdout + err.print result.stderr + result.success? end sig { params(result: SystemCommand::Result).returns(T.untyped) } @@ -412,6 +503,14 @@ module Homebrew HOMEBREW_LIBRARY/"Homebrew/utils/shfmt.sh" end + sig { returns(Pathname) } + private_class_method def self.shfmt_executable + require "formula" + T.cast(Formula["shfmt"].ensure_installed!(latest: true, + reason: "formatting shell scripts", + executable: "shfmt"), Pathname) + end + sig { returns(Pathname) } def self.actionlint require "formula" diff --git a/Library/Homebrew/system_config.rb b/Library/Homebrew/system_config.rb index 4e0e15e1d7..a7e8787438 100644 --- a/Library/Homebrew/system_config.rb +++ b/Library/Homebrew/system_config.rb @@ -153,6 +153,7 @@ module SystemConfig sig { params(out: T.any(File, StringIO, IO)).void } def core_tap_config(out = $stdout) dump_tap_config(CoreTap.instance, out) + dump_tap_config(CoreCaskTap.instance, out) end sig { params(out: T.any(File, StringIO, IO)).void } diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 2a5d232d60..8f047c0927 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -58,52 +58,53 @@ class AbstractTab sig { returns(RuntimeDependencies) } attr_accessor :runtime_dependencies - # TODO: Update attributes to only accept symbol keys (kwargs style). - sig { params(attributes: T.any(T::Hash[String, T.untyped], T::Hash[Symbol, T.untyped])).void } - def initialize(attributes = {}) - @installed_on_request = T.let(false, T::Boolean) - @installed_on_request_present = T.let(false, T::Boolean) - @homebrew_version = T.let(nil, T.nilable(String)) - @tabfile = T.let(nil, T.nilable(Pathname)) - @loaded_from_api = T.let(nil, T.nilable(T::Boolean)) - @loaded_from_internal_api = T.let(nil, T.nilable(T::Boolean)) - @time = T.let(nil, T.nilable(Integer)) - @arch = T.let(nil, T.nilable(T.any(String, Symbol))) - @source = T.let({}, T::Hash[String, T.untyped]) - @built_on = T.let(nil, T.nilable(T::Hash[String, T.untyped])) - @runtime_dependencies = T.let(nil, RuntimeDependencies) - - attributes.each do |key, value| - case key.to_sym - when :installed_on_request - @installed_on_request = value.nil? ? false : value - @installed_on_request_present = true - when :changed_files - @changed_files = T.let(value&.map { |f| Pathname(f) }, T.nilable(T::Array[Pathname])) - else - instance_variable_set(:"@#{key}", value) - end - end + # Unrecognised attributes are ignored so that receipts written by other + # Homebrew versions (e.g. the long-removed `installed_as_dependency`) still load. + sig { + params(homebrew_version: T.nilable(String), + tabfile: T.nilable(T.any(Pathname, String)), + loaded_from_api: T.nilable(T::Boolean), + loaded_from_internal_api: T.nilable(T::Boolean), + installed_on_request: T.nilable(T::Boolean), + time: T.nilable(Integer), + arch: T.nilable(T.any(String, Symbol)), + source: T.nilable(T::Hash[String, T.untyped]), + built_on: T.nilable(T::Hash[String, T.untyped]), + runtime_dependencies: RuntimeDependencies, + _unknown: T.anything).void + } + def initialize(homebrew_version: nil, tabfile: nil, loaded_from_api: nil, loaded_from_internal_api: nil, + installed_on_request: nil, time: nil, arch: nil, source: nil, built_on: nil, + runtime_dependencies: nil, **_unknown) + @installed_on_request = T.let(installed_on_request || false, T::Boolean) + @installed_on_request_present = T.let(!installed_on_request.nil?, T::Boolean) + @homebrew_version = homebrew_version + @tabfile = T.let(tabfile.nil? ? nil : Pathname(tabfile), T.nilable(Pathname)) + @loaded_from_api = loaded_from_api + @loaded_from_internal_api = loaded_from_internal_api + @time = time + @arch = arch + @source = T.let(source || {}, T::Hash[String, T.untyped]) + @built_on = built_on + @runtime_dependencies = runtime_dependencies end # Instantiates a {Tab} for a new installation of a formula or cask. sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(T.attached_class) } def self.create(formula_or_cask) - attributes = { - "homebrew_version" => HOMEBREW_VERSION, - "installed_on_request" => false, - "loaded_from_api" => formula_or_cask.loaded_from_api?, - "loaded_from_internal_api" => formula_or_cask.loaded_from_internal_api?, - "time" => Time.now.to_i, - "arch" => Hardware::CPU.arch, - "source" => { + new( + homebrew_version: HOMEBREW_VERSION, + installed_on_request: false, + loaded_from_api: formula_or_cask.loaded_from_api?, + loaded_from_internal_api: formula_or_cask.loaded_from_internal_api?, + time: Time.now.to_i, + arch: Hardware::CPU.arch, + source: { "tap" => formula_or_cask.tap&.name, "tap_git_head" => formula_or_cask.tap_git_head, }, - "built_on" => DevelopmentTools.build_system_info, - } - - new(attributes) + built_on: DevelopmentTools.build_system_info, + ) end # Returns the {Tab} for a formula or cask install receipt at `path`. @@ -129,28 +130,26 @@ class AbstractTab end attributes["tabfile"] = path - new(attributes) + new(**attributes.transform_keys(&:to_sym)) end sig { returns(T.attached_class) } def self.empty - attributes = { - "homebrew_version" => HOMEBREW_VERSION, - "installed_on_request" => false, - "loaded_from_api" => false, - "loaded_from_internal_api" => false, - "time" => nil, - "runtime_dependencies" => nil, - "arch" => nil, - "source" => { + new( + homebrew_version: HOMEBREW_VERSION, + installed_on_request: false, + loaded_from_api: false, + loaded_from_internal_api: false, + time: nil, + runtime_dependencies: nil, + arch: nil, + source: { "path" => nil, "tap" => nil, "tap_git_head" => nil, }, - "built_on" => DevelopmentTools.build_system_info, - } - - new(attributes) + built_on: DevelopmentTools.build_system_info, + ) end sig { params(formula: Formula, declared_deps: T::Array[String]).returns(T::Hash[String, T.untyped]) } diff --git a/Library/Homebrew/tab/tab.rb b/Library/Homebrew/tab/tab.rb index 7f10ce7575..fd8dde2292 100644 --- a/Library/Homebrew/tab/tab.rb +++ b/Library/Homebrew/tab/tab.rb @@ -37,20 +37,34 @@ class Tab < AbstractTab sig { returns(T.nilable(T::Array[Pathname])) } attr_accessor :changed_files - sig { params(attributes: T.any(T::Hash[String, T.untyped], T::Hash[Symbol, T.untyped])).void } - def initialize(attributes = {}) - @poured_from_bottle = T.let(nil, T.nilable(T::Boolean)) - @built_as_bottle = T.let(nil, T.nilable(T::Boolean)) - @changed_files = T.let(nil, T.nilable(T::Array[Pathname])) - @stdlib = T.let(nil, T.nilable(T.any(String, Symbol))) - @aliases = T.let(nil, T.nilable(T::Array[String])) - @used_options = T.let(nil, T.nilable(T::Array[String])) - @unused_options = T.let(nil, T.nilable(T::Array[String])) - @compiler = T.let(nil, T.nilable(T.any(String, Symbol))) - @source_modified_time = T.let(nil, T.nilable(Integer)) - @tapped_from = T.let(nil, T.nilable(String)) + sig { + params(poured_from_bottle: T.nilable(T::Boolean), + built_as_bottle: T.nilable(T::Boolean), + changed_files: T.nilable(T::Array[T.any(Pathname, String)]), + stdlib: T.nilable(T.any(String, Symbol)), + aliases: T.nilable(T::Array[String]), + used_options: T.nilable(T::Array[String]), + unused_options: T.nilable(T::Array[String]), + compiler: T.nilable(T.any(String, Symbol)), + source_modified_time: T.nilable(Integer), + tapped_from: T.nilable(String), + rest: T.untyped).void + } + def initialize(poured_from_bottle: nil, built_as_bottle: nil, changed_files: nil, stdlib: nil, aliases: nil, + used_options: nil, unused_options: nil, compiler: nil, source_modified_time: nil, + tapped_from: nil, **rest) + @poured_from_bottle = poured_from_bottle + @built_as_bottle = built_as_bottle + @changed_files = T.let(changed_files&.map { |f| Pathname(f) }, T.nilable(T::Array[Pathname])) + @stdlib = stdlib + @aliases = aliases + @used_options = used_options + @unused_options = unused_options + @compiler = compiler + @source_modified_time = source_modified_time + @tapped_from = tapped_from - super + super(**rest) end # Instantiates a {Tab} for a new installation of a formula. diff --git a/Library/Homebrew/test/ENV_spec.rb b/Library/Homebrew/test/ENV_spec.rb index 1af02bd51f..2834b9f53c 100644 --- a/Library/Homebrew/test/ENV_spec.rb +++ b/Library/Homebrew/test/ENV_spec.rb @@ -46,13 +46,14 @@ RSpec.describe "ENV" do end it "does not mutate the interface" do - expected = subject.methods + # Lazy-loaded gems may add methods to Hash without extending this object. + expected = subject.singleton_methods subject.with_build_environment do - expect(subject.methods).to eq(expected) + expect(subject.singleton_methods).to eq(expected) end - expect(subject.methods).to eq(expected) + expect(subject.singleton_methods).to eq(expected) end end diff --git a/Library/Homebrew/test/api/cask/cask_struct_generator_spec.rb b/Library/Homebrew/test/api/cask/cask_struct_generator_spec.rb index d11e2d9541..8a85b41a47 100644 --- a/Library/Homebrew/test/api/cask/cask_struct_generator_spec.rb +++ b/Library/Homebrew/test/api/cask/cask_struct_generator_spec.rb @@ -48,12 +48,27 @@ RSpec.describe Homebrew::API::Cask::CaskStructGenerator do { foo: ["arg1", "arg2"] }, { bar: ["arg1", "arg2", { kwarg1: "value1" }] }, { baz: [{ kwarg1: "value1" }] }, + { + target: "$HOMEBREW_PREFIX/share/zsh/site-functions/_foo", + zsh_completion: ["$APPDIR/Foo.app/Contents/Resources/completions/zsh/_foo"], + }, + { + zsh_completion: ["$APPDIR/Bar.app/Contents/Resources/completions/zsh/_bar"], + target: "$HOMEBREW_PREFIX/share/zsh/site-functions/_bar", + }, + { + target: "/Applications/Bar.app", + app: ["Foo.app", { target: "Bar.app" }], + }, ] expected_output = [ [:preflight, [], {}, Homebrew::API::CaskStruct::EMPTY_BLOCK], [:foo, ["arg1", "arg2"], {}, nil], [:bar, ["arg1", "arg2"], { kwarg1: "value1" }, nil], [:baz, [], { kwarg1: "value1" }, nil], + [:zsh_completion, ["$APPDIR/Foo.app/Contents/Resources/completions/zsh/_foo"], {}, nil], + [:zsh_completion, ["$APPDIR/Bar.app/Contents/Resources/completions/zsh/_bar"], {}, nil], + [:app, ["Foo.app"], { target: "Bar.app" }, nil], ] output = described_class.process_artifacts(input) expect(output).to eq expected_output diff --git a/Library/Homebrew/test/api/internal_spec.rb b/Library/Homebrew/test/api/internal_spec.rb index 8a251c76a1..3a95282a30 100644 --- a/Library/Homebrew/test/api/internal_spec.rb +++ b/Library/Homebrew/test/api/internal_spec.rb @@ -258,4 +258,48 @@ RSpec.describe Homebrew::API::Internal do cask_tap_migrations_output = described_class.cask_tap_migrations expect(cask_tap_migrations_output).to eq cask_tap_migrations end + + describe "with a cached payload sidecar" do + let(:compact_payload) { JSON.generate(JSON.parse(packages_json)) } + + before do + target = described_class.cached_packages_json_file_path + target.dirname.mkpath + target.write "envelope" + header = { + "protected" => "protected", + "signature" => "signature", + "source_size" => target.stat.size, + "source_mtime_ns" => (target.stat.mtime.to_r * 1_000_000_000).to_i, + } + Pathname("#{target}.payload").write("#{JSON.generate(header)}\n#{compact_payload}") + allow(Homebrew::API).to receive(:verify_jws_signature).and_return(nil) + allow(Utils::Curl).to receive(:curl_download).and_raise("sidecar-served loads must not download") + end + + it "builds an index on first load and serves formula structs from it afterwards" do + expect(described_class.formula_struct("foo")).to eq formula_structs.fetch("foo") + index_path = Homebrew::API::PackagesIndex.path_for(described_class.cached_packages_json_file_path) + expect(index_path).to exist + + described_class.clear_cache + loaded_index = T.let(nil, T.untyped) + expect(Homebrew::API::PackagesIndex).to receive(:load).and_wrap_original do |original, *args, **kwargs| + loaded_index = original.call(*args, **kwargs) + end + + expect(described_class.formula_struct("foo")).to eq formula_structs.fetch("foo") + expect(loaded_index).not_to be_nil + expect(described_class.formula_tap_git_head).to eq formula_tap_git_head + expect(described_class.formula_name?("bar")).to be true + expect(described_class.formula_name?("missing")).to be false + end + + it "materialises full hashes from an index-served payload" do + described_class.formula_struct("foo") + described_class.clear_cache + + expect(described_class.formula_hashes).to eq formula_hashes + end + end end diff --git a/Library/Homebrew/test/api/packages_index_spec.rb b/Library/Homebrew/test/api/packages_index_spec.rb new file mode 100644 index 0000000000..9a4ad34c35 --- /dev/null +++ b/Library/Homebrew/test/api/packages_index_spec.rb @@ -0,0 +1,91 @@ +# typed: true +# frozen_string_literal: true + +require "api" + +RSpec.describe Homebrew::API::PackagesIndex do + let(:cache_dir) { mktmpdir } + let(:target) { cache_dir/"packages.arm64_test.jws.json" } + let(:parsed) do + { + "formulae" => { + "foo" => { "desc" => "Foo formula", "stable_version" => "1.0.0" }, + "bar" => { "desc" => "Bar‑formula", "stable_version" => "0.4.0" }, + }, + "casks" => { + "foo" => { "desc" => "Foo cask", "version" => "2.0.0" }, + }, + "formula_aliases" => { "foo-alias" => "foo" }, + "formula_tap_git_head" => "b871900717ccbb3508ca93fa56e128940b9bd371", + } + end + let(:payload) { JSON.generate(parsed) } + + def write_index! + target.write("{}") + described_class.write!(target, payload:, parsed:, source_stat: target.stat) + end + + def load_index + described_class.load(target, payload:, source_stat: target.stat) + end + + it "serves entries and top-level values from a written index" do + write_index! + index = load_index + + expect(index).not_to be_nil + expect(index.formula_hash("foo")).to eq parsed.dig("formulae", "foo") + expect(index.formula_hash("bar")).to eq parsed.dig("formulae", "bar") + expect(index.cask_hash("foo")).to eq parsed.dig("casks", "foo") + expect(index.formula_hash("missing")).to be_nil + expect(index.formula_names).to eq %w[foo bar] + expect(index.cask_name?("foo")).to be true + expect(index.top_level_value("formula_aliases")).to eq parsed["formula_aliases"] + expect(index.top_level_value("formula_tap_git_head")).to eq parsed["formula_tap_git_head"] + expect(index.top_level_value("formulae")).to be_nil + end + + it "does not load an index whose source envelope changed" do + write_index! + FileUtils.touch target, mtime: target.stat.mtime + 1 + + expect(load_index).to be_nil + end + + it "does not load an index built for a different payload" do + write_index! + + expect(described_class.load(target, payload: "#{payload} ", source_stat: target.stat)).to be_nil + end + + it "raises on lookups whose recorded offsets do not match the payload" do + write_index! + index_path = described_class.path_for(target) + data = JSON.parse(index_path.read) + data["formulae"]["foo"] = data["formulae"]["bar"] + index_path.write(JSON.generate(data)) + + expect { load_index.formula_hash("foo") }.to raise_error(Homebrew::API::PackagesIndex::Invalid) + end + + it "raises on lookups remapped to a matching key in another section" do + write_index! + index_path = described_class.path_for(target) + data = JSON.parse(index_path.read) + data["formulae"]["foo"] = data["casks"]["foo"] + index_path.write(JSON.generate(data)) + + expect { load_index.formula_hash("foo") }.to raise_error(Homebrew::API::PackagesIndex::Invalid) + end + + it "does not load an index whose top-level spans do not tile the payload" do + write_index! + index_path = described_class.path_for(target) + data = JSON.parse(index_path.read) + data["top_level"]["formulae"][1] = data["payload_bytesize"] - data["top_level"]["formulae"][0] - 1 + index_path.write(JSON.generate(data)) + + expect(load_index).to be_nil + end +end diff --git a/Library/Homebrew/test/api_spec.rb b/Library/Homebrew/test/api_spec.rb index 2774d0a3dc..984d1dd121 100644 --- a/Library/Homebrew/test/api_spec.rb +++ b/Library/Homebrew/test/api_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Homebrew::API do describe "::formula_name?" do before do - allow(Homebrew::API::Internal).to receive(:formula_hashes).and_return({ "foo" => {} }) + allow(Homebrew::API::Internal).to receive(:formula_name?) { |name| name == "foo" } end it "returns true for a core formula name" do @@ -55,7 +55,7 @@ RSpec.describe Homebrew::API do describe "::cask_token?" do before do - allow(Homebrew::API::Internal).to receive(:cask_hashes).and_return({ "foo" => {} }) + allow(Homebrew::API::Internal).to receive(:cask_name?) { |token| token == "foo" } end it "returns true for a core cask token" do @@ -316,7 +316,7 @@ RSpec.describe Homebrew::API do let(:formulae) { { "foo" => { "executables" => ["foo-bin"] } } } def write_executables_file!(regenerate:) - described_class.write_executables_file!(formulae, regenerate:, source:) + described_class.write_executables_file!(regenerate:, source:) { formulae } end before do diff --git a/Library/Homebrew/test/bash_spec.rb b/Library/Homebrew/test/bash_spec.rb index 49e12ffebd..9bcc128eb7 100644 --- a/Library/Homebrew/test/bash_spec.rb +++ b/Library/Homebrew/test/bash_spec.rb @@ -22,21 +22,6 @@ RSpec.describe "Bash" do subject(:brew) { HOMEBREW_LIBRARY_PATH.parent.parent/"bin/brew" } it { is_expected.to have_valid_bash_syntax } - - it "selects Landlock on self-hosted Linux GitHub Actions runners", :needs_linux do - stdout, stderr, status = Open3.capture3( - { - "CI" => "1", - "GITHUB_ACTIONS" => "true", - "GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED" => "1", - "HOMEBREW_DEV_CMD_RUN" => "1", - "HOMEBREW_SANDBOX_LINUX_LANDLOCK" => nil, - }, - brew.to_s, "ruby", "--", "-e", "print OS::Linux::Sandbox.sandbox_implementation" - ) - - expect([stdout, stderr, status.success?]).to eq(["Sandbox::Landlock", "", true]) - end end describe "setup-locale" do diff --git a/Library/Homebrew/test/bundle/installer_spec.rb b/Library/Homebrew/test/bundle/installer_spec.rb index d3391d01ee..84a66c3ced 100644 --- a/Library/Homebrew/test/bundle/installer_spec.rb +++ b/Library/Homebrew/test/bundle/installer_spec.rb @@ -428,7 +428,7 @@ RSpec.describe Homebrew::Bundle::Installer do allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with("alpha").and_return(Set.new) allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with("beta").and_return(Set.new) allow(DependencyCollector).to receive(:new).and_return( - instance_double(DependencyCollector, implicit_dependency_names: Set["bubblewrap"]), + instance_double(DependencyCollector, implicit_dependency_names: Set["glibc"]), ) entries = [alpha_entry, beta_entry] @@ -447,7 +447,7 @@ RSpec.describe Homebrew::Bundle::Installer do allow(Homebrew::Bundle::Brew).to receive(:formulae_by_full_name).with(any_args).and_return({ dependencies: [] }) allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with(any_args).and_return(Set.new) allow(DependencyCollector).to receive(:new).and_return( - instance_double(DependencyCollector, implicit_dependency_names: Set["bubblewrap"]), + instance_double(DependencyCollector, implicit_dependency_names: Set["glibc"]), ) entries = [alpha_entry, beta_entry, gamma_entry] @@ -469,7 +469,7 @@ RSpec.describe Homebrew::Bundle::Installer do allow(Homebrew::Bundle::Brew).to receive(:recursive_dep_names).with("alpha").and_return(Set.new) allow(Homebrew::Bundle::Cask).to receive(:formula_dependencies).with(["google-chrome"]).and_return([]) allow(DependencyCollector).to receive(:new).and_return( - instance_double(DependencyCollector, implicit_dependency_names: Set["bubblewrap"]), + instance_double(DependencyCollector, implicit_dependency_names: Set["glibc"]), ) entries = [alpha_entry, installable_cask_entry] diff --git a/Library/Homebrew/test/cask/artifact/binary_spec.rb b/Library/Homebrew/test/cask/artifact/binary_spec.rb index c37ce088ac..4f3e82bbeb 100644 --- a/Library/Homebrew/test/cask/artifact/binary_spec.rb +++ b/Library/Homebrew/test/cask/artifact/binary_spec.rb @@ -86,6 +86,27 @@ RSpec.describe Cask::Artifact::Binary, :cask do expect(File.readlink(expected_path)).to eq("/tmp") end + it "skips linking when the target is already a symlink to the source" do + artifact = artifacts.first + expected_path.make_symlink(artifact.source) + + expect do + artifact.install_phase(command: NeverSudoSystemCommand, force: false) + end.to output(/is already linked/).to_stdout + + expect(expected_path.readlink).to eq(artifact.source) + end + + it "raises a clean error when the target symlink cannot be resolved" do + artifact = artifacts.first + expected_path.make_symlink(artifact.source) + allow(artifact.target).to receive(:realpath).and_raise(Errno::EACCES) + + expect do + artifact.install_phase(command: NeverSudoSystemCommand, force: false) + end.to raise_error(Cask::CaskError, /already a Binary/) + end + it "creates parent directory if it doesn't exist" do FileUtils.rmdir binarydir diff --git a/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb b/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb index 4106dc0c4a..a4464a6148 100644 --- a/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb +++ b/Library/Homebrew/test/cask/artifact/generated_completion_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do it "generates completion scripts for default shells" do artifact = cask.artifacts.grep(described_class).first - allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true) + allow(Sandbox).to receive(:available?).and_return(true) allow(Sandbox).to receive(:new) do instance_double(Sandbox).tap do |sandbox| allow(sandbox).to receive(:allow_read) @@ -61,7 +61,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do calls = [] homes = [] - allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true) + allow(Sandbox).to receive(:available?).and_return(true) allow(Sandbox).to receive(:new) do instance_double(Sandbox).tap do |sandbox| expect(sandbox).to receive(:allow_read).with(path: staged_path, type: :subpath) @@ -89,7 +89,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do it "warns and continues generating other shells" do artifact = cask.artifacts.grep(described_class).first - allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true) + allow(Sandbox).to receive(:available?).and_return(true) allow(Sandbox).to receive(:new) do instance_double(Sandbox).tap do |sandbox| allow(sandbox).to receive(:allow_read) @@ -146,7 +146,7 @@ RSpec.describe Cask::Artifact::GeneratedCompletion, :cask do artifact = cask.artifacts.grep(described_class).first captured_args = T.let([], T::Array[String]) - allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true) + allow(Sandbox).to receive(:available?).and_return(true) allow(Sandbox).to receive(:new) do instance_double(Sandbox).tap do |sandbox| allow(sandbox).to receive(:allow_read) diff --git a/Library/Homebrew/test/cask/audit_spec.rb b/Library/Homebrew/test/cask/audit_spec.rb index e3c1deead6..48be6e86af 100644 --- a/Library/Homebrew/test/cask/audit_spec.rb +++ b/Library/Homebrew/test/cask/audit_spec.rb @@ -1205,6 +1205,93 @@ RSpec.describe Cask::Audit, :cask do end end + describe "artifact case checks" do + let(:online) { true } + let(:only) { ["artifact_case"] } + let(:tmpdir) { mktmpdir } + let(:cask) do + Cask::Cask.new("artifact-case") do + version "1.0" + sha256 :no_check + url "https://brew.sh/artifact-case.zip" + name "Artifact Case" + homepage "https://brew.sh/" + + app "artifact case.app" + end + end + + before do + allow(audit).to receive(:extract_artifacts).and_yield(cask.artifacts, tmpdir) + end + + context "when the case matches" do + before { (tmpdir/"artifact case.app").mkpath } + + it { is_expected.to pass } + end + + context "when the case does not match" do + before { (tmpdir/"Artifact Case.app").mkpath } + + it { is_expected.to error_with(/does not match the case of the extracted/) } + end + + context "when both cases are present on disk" do + # Both spellings cannot be created on a case-insensitive filesystem. + before do + allow(tmpdir).to receive(:children) + .and_return([tmpdir/"Artifact Case.app", tmpdir/"artifact case.app"]) + end + + it { is_expected.to pass } + end + + context "when the artifact is missing" do + it { is_expected.to pass } + end + + context "when a binary in the appdir has the wrong case" do + let(:cask) do + Cask::Cask.new("artifact-case") do + version "1.0" + sha256 :no_check + url "https://brew.sh/artifact-case.zip" + name "Artifact Case" + homepage "https://brew.sh/" + + app "Artifact Case.app" + binary "#{appdir}/Artifact Case.app/Contents/MacOS/artifact" + end + end + + before do + (tmpdir/"Artifact Case.app/Contents/MacOS").mkpath + FileUtils.touch tmpdir/"Artifact Case.app/Contents/MacOS/Artifact" + end + + it { is_expected.to error_with(/does not match the case of the extracted/) } + end + + context "when a manual installer has the wrong case" do + let(:cask) do + Cask::Cask.new("artifact-case") do + version "1.0" + sha256 :no_check + url "https://brew.sh/artifact-case.zip" + name "Artifact Case" + homepage "https://brew.sh/" + + installer manual: "Artifact Case.app" + end + end + + before { (tmpdir/"Artifact Case.APP").mkpath } + + it { is_expected.to error_with(/does not match the case of the extracted/) } + end + end + describe "minimum OS checks" do let(:online) { true } let(:only) { ["min_os"] } diff --git a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb index a15e933fb8..46ff00612a 100644 --- a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb @@ -54,6 +54,8 @@ RSpec.describe Cask::CaskLoader::FromAPILoader, :cask do cask_renames: {}, cask_tap_migrations: {}, cask_tap_git_head: internal_tap_git_head) + allow(Homebrew::API::Internal).to receive(:cask_name?) { |token| casks_from_internal_api_hash.key?(token) } + allow(Homebrew::API::Internal).to receive(:cask_hash) { |token| casks_from_internal_api_hash[token] } # The call to `Cask::CaskLoader.load` above sets the Tap cache prematurely. Tap.clear_cache diff --git a/Library/Homebrew/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index d48ee98ffc..8b27af3d4f 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -701,9 +701,22 @@ RSpec.describe Cask::Cask, :cask do expect(cask.refresh_for_tag(tag) { cask.url.to_s }).to include("caffeine-intel-darwin") end - it "returns nil for a tag the cask does not support" do + it "yields for a Linux architecture whose checksum is missing" do tag = Utils::Bottles::Tag.new(system: :linux, arch: :arm) - expect(cask.refresh_for_tag(tag) { cask.url }).to be_nil + expect(cask.refresh_for_tag(tag) { cask.url.to_s }).to include("caffeine-arm-linux") + end + + it "returns nil for a tag the cask cannot be refreshed for" do + invalid_on_linux_cask = described_class.new("on-linux-invalid") do + on_macos do + version "1.2.3" + end + sha256 :no_check + url "https://brew.sh/foo-#{version.major_minor}.zip" + end + + tag = Utils::Bottles::Tag.new(system: :linux, arch: :arm) + expect(invalid_on_linux_cask.refresh_for_tag(tag) { invalid_on_linux_cask.url }).to be_nil end end @@ -900,12 +913,14 @@ RSpec.describe Cask::Cask, :cask do expect(JSON.pretty_generate(h["variations"])).to eq expected_sha256_variations_os.strip end - it "omits tags a cask intentionally doesn't define in on_system blocks" do + it "emits variations without checksums for Linux architectures a cask omits" do c = Cask::CaskLoader.load("on-linux-asymmetric") - h = c.to_hash_with_variations + h = JSON.parse(JSON.generate(c.to_hash_with_variations)) - expect(h["variations"]).to include(:x86_64_linux) - expect(h["variations"]).not_to include(:arm64_linux) + expect(h["variations"]["arm64_linux"]).to include( + "depends_on" => { "arch" => [{ "type" => "intel", "bits" => 64 }] }, + "sha256" => nil, + ) end it "emits Linux variations for a cask with Linux checksums but no `os` stanza" do @@ -925,6 +940,28 @@ RSpec.describe Cask::Cask, :cask do ) end + it "emits Linux variations for a cask with `on_linux` content but no `os` stanza" do + c = Cask::CaskLoader.load("on-linux-blocks") + h = JSON.parse(JSON.generate(c.to_hash_with_variations)) + + app_image_artifacts = [{ + "app_image" => ["Caffeine.AppImage"], + "target" => "#{TEST_TMPDIR}/cask-appimagedir/Caffeine.AppImage", + }] + expect(h["variations"].slice("x86_64_linux", "arm64_linux").transform_values do |v| + v.slice("sha256", "artifacts") + end).to eq( + "x86_64_linux" => { + "sha256" => "244d413861cecb3707cfbcc5c4346d5367daa827da5ea08fb3f3bc2b6276d239", + "artifacts" => app_image_artifacts, + }, + "arm64_linux" => { + "sha256" => "9a1c0967baa46828930ccbbc88668d1b0db07e6edf778800ed4da073c00054f8", + "artifacts" => app_image_artifacts, + }, + ) + end + # NOTE: The calls to `Cask.generating_hash!` and `Cask.generated_hash!` # are not idempotent so they can only be used in one test. it "returns the correct hash placeholders" do diff --git a/Library/Homebrew/test/cask/dsl_spec.rb b/Library/Homebrew/test/cask/dsl_spec.rb index ab4180e18f..156ba27995 100644 --- a/Library/Homebrew/test/cask/dsl_spec.rb +++ b/Library/Homebrew/test/cask/dsl_spec.rb @@ -185,17 +185,19 @@ RSpec.describe Cask::DSL, :cask, :no_api do end end - it "raises when the running-architecture macOS checksum is missing" do + it "has no checksum when simulating an architecture whose checksum is missing" do Homebrew::SimulateSystem.with(os: :macos, arch: :intel) do - expect do - Cask::Cask.new("checksum-cask") do - sha256 arm: "imasha2arm", arm64_linux: "imasha2armlinux" - end - end.to raise_error(Cask::CaskInvalidError, /invalid 'sha256' value/) + cask = Cask::Cask.new("checksum-cask") do + sha256 arm: "imasha2arm", arm64_linux: "imasha2armlinux" + end + + expect(cask.sha256).to be_nil end end - it "raises when the running-architecture Linux checksum is missing" do + it "raises on the real system when the running-architecture checksum is missing" do + allow(Homebrew::SimulateSystem).to receive(:simulating?).and_return(false) + Homebrew::SimulateSystem.with(os: :linux, arch: :intel) do expect do Cask::Cask.new("checksum-cask") do @@ -580,6 +582,23 @@ RSpec.describe Cask::DSL, :cask, :no_api do end end end + + context "when only an arch block declares the macOS version" do + it "requires macOS because arch blocks are evaluated on every OS" do + Homebrew::SimulateSystem.with(os: :linux, arch: :arm) do + cask = Cask::Cask.new("with-arch-scoped-macos-version") do + on_arm do + depends_on macos: :ventura + end + on_intel do + depends_on macos: :monterey + end + end + + expect(cask.depends_on.requires_macos?).to be true + end + end + end end describe "depends_on linux" do diff --git a/Library/Homebrew/test/cask/reinstall_spec.rb b/Library/Homebrew/test/cask/reinstall_spec.rb index 3edf0a1931..e1734a942f 100644 --- a/Library/Homebrew/test/cask/reinstall_spec.rb +++ b/Library/Homebrew/test/cask/reinstall_spec.rb @@ -11,7 +11,6 @@ RSpec.describe Cask::Reinstall, :cask do Cask::Installer.new(caffeine).install output = Regexp.new <<~EOS - ==> Fetching downloads for:.*caffeine ==> Uninstalling Cask local-caffeine ==> Backing up App 'Caffeine.app' to '.*Caffeine.app' ==> Removing App '.*Caffeine.app' @@ -23,7 +22,7 @@ RSpec.describe Cask::Reinstall, :cask do expect do described_class.reinstall_casks(Cask::CaskLoader.load("local-caffeine")) - end.to output(output).to_stdout + end.to output(output).to_stdout.and output(/==> Fetching downloads for:.*caffeine/).to_stderr end it "displays the reinstallation progress with zapping" do @@ -32,7 +31,6 @@ RSpec.describe Cask::Reinstall, :cask do Cask::Installer.new(caffeine).install output = Regexp.new <<~EOS - ==> Fetching downloads for:.*caffeine ==> Backing up App 'Caffeine.app' to '.*Caffeine.app' ==> Removing App '.*Caffeine.app' ==> Dispatching zap stanza @@ -46,7 +44,7 @@ RSpec.describe Cask::Reinstall, :cask do expect do described_class.reinstall_casks(Cask::CaskLoader.load("local-caffeine"), zap: true) - end.to output(output).to_stdout + end.to output(output).to_stdout.and output(/==> Fetching downloads for:.*caffeine/).to_stderr end it "allows reinstalling a Cask" do diff --git a/Library/Homebrew/test/cask/tab_spec.rb b/Library/Homebrew/test/cask/tab_spec.rb index 20fd1fef1d..3a96db4b39 100644 --- a/Library/Homebrew/test/cask/tab_spec.rb +++ b/Library/Homebrew/test/cask/tab_spec.rb @@ -6,24 +6,24 @@ require "cask" RSpec.describe Cask::Tab, :cask do subject(:tab) do described_class.new( - "homebrew_version" => HOMEBREW_VERSION, - "loaded_from_api" => false, - "loaded_from_internal_api" => false, - "uninstall_flight_blocks" => true, - "installed_on_request" => true, - "time" => time, - "runtime_dependencies" => { + homebrew_version: HOMEBREW_VERSION, + loaded_from_api: false, + loaded_from_internal_api: false, + uninstall_flight_blocks: true, + installed_on_request: true, + time:, + runtime_dependencies: { "cask" => [{ "full_name" => "bar", "version" => "2.0", "declared_directly" => false }], }, - "source" => { + source: { "path" => CoreCaskTap.instance.path.to_s, "tap" => CoreCaskTap.instance.to_s, "tap_git_head" => "8b79aa759500f0ffdf65a23e12950cbe3bf8fe17", "version" => "1.2.3", }, - "arch" => Hardware::CPU.arch, - "uninstall_artifacts" => [{ "app" => ["Foo.app"] }], - "built_on" => DevelopmentTools.build_system_info, + arch: Hardware::CPU.arch, + uninstall_artifacts: [{ "app" => ["Foo.app"] }], + built_on: DevelopmentTools.build_system_info, ) end @@ -301,7 +301,7 @@ RSpec.describe Cask::Tab, :cask do end specify "#to_json" do - json_tab = described_class.new(JSON.parse(tab.to_json)) + json_tab = described_class.new(**JSON.parse(tab.to_json).transform_keys(&:to_sym)) expect(json_tab.homebrew_version).to eq(tab.homebrew_version) expect(json_tab.loaded_from_api).to eq(tab.loaded_from_api) expect(json_tab.loaded_from_internal_api).to eq(tab.loaded_from_internal_api) diff --git a/Library/Homebrew/test/cleanup_spec.rb b/Library/Homebrew/test/cleanup_spec.rb index 94feb566a3..529a556996 100644 --- a/Library/Homebrew/test/cleanup_spec.rb +++ b/Library/Homebrew/test/cleanup_spec.rb @@ -572,9 +572,11 @@ RSpec.describe Homebrew::Cleanup do kept_files = [ api_internal/current_basename, api_internal/"#{current_basename}.payload", + api_internal/"#{current_basename}.payload.index", ] scrubbed_files = [ api_internal/"packages.stale.jws.json.payload", + api_internal/"packages.stale.jws.json.payload.index", api_internal/"#{current_basename}.payload.tmp", ] (kept_files + scrubbed_files).each do |file| @@ -584,7 +586,7 @@ RSpec.describe Homebrew::Cleanup do described_class.new(scrub: true, cache:).cleanup_cache - expect((kept_files + scrubbed_files).map(&:exist?)).to eq([true, true, false, false]) + expect((kept_files + scrubbed_files).map(&:exist?)).to eq([true, true, true, false, false, false]) end it "cleans up API source files and symlinks at any depth without cleaning directories" do diff --git a/Library/Homebrew/test/cmd/bundle_spec.rb b/Library/Homebrew/test/cmd/bundle_spec.rb index b0c837fb62..e82c8daa6a 100644 --- a/Library/Homebrew/test/cmd/bundle_spec.rb +++ b/Library/Homebrew/test/cmd/bundle_spec.rb @@ -100,6 +100,8 @@ RSpec.describe Homebrew::Cmd::Bundle do expect(subcommand_options.call("cleanup")["--no-mas"]) .to include("`cleanup` without Mac App Store dependencies.") expect(subcommand_options.call("cleanup")["--all"]).to eq("Clean up all supported dependencies.") + expect(subcommand_options.call("cleanup")["--force"]) + .to eq("Actually perform cleanup operations and reset Homebrew's global trust store to the `Brewfile` values.") expect(subcommand_options.call("dump")["--no-describe"]).to include("Description comments are the default") expect(subcommand_options.call("add")["--no-describe"]).to include("Description comments are the default") expect(subcommand_options.call("add")["--vscode"]) diff --git a/Library/Homebrew/test/cmd/fetch_spec.rb b/Library/Homebrew/test/cmd/fetch_spec.rb index 9c6fe6d878..532481638a 100644 --- a/Library/Homebrew/test/cmd/fetch_spec.rb +++ b/Library/Homebrew/test/cmd/fetch_spec.rb @@ -32,10 +32,10 @@ RSpec.describe Homebrew::Cmd::FetchCmd do allow(download_queue).to receive(:enqueue) { |download| enqueued_downloads << download } allow(Homebrew::API::Internal).to receive_messages( formula_aliases: {}, - formula_hashes: { "fast-fetch" => {} }, formula_renames: {}, formula_struct: formula_struct, ) + allow(Homebrew::API::Internal).to receive(:formula_name?) { |name| name == "fast-fetch" } expect(cmd.args.named).not_to receive(:to_formulae_and_casks) expect(Formulary).not_to receive(:factory) @@ -59,10 +59,10 @@ RSpec.describe Homebrew::Cmd::FetchCmd do allow(Homebrew::DownloadQueue).to receive(:new).and_return(download_queue) allow(download_queue).to receive(:enqueue) { |download| enqueued_downloads << download } allow(Homebrew::API::Internal).to receive_messages( - cask_hashes: { "fast-cask" => {} }, cask_renames: {}, cask_struct: cask_struct, ) + allow(Homebrew::API::Internal).to receive(:cask_name?) { |token| token == "fast-cask" } expect(cmd.args.named).not_to receive(:to_formulae_and_casks) expect(Cask::CaskLoader).not_to receive(:load) diff --git a/Library/Homebrew/test/cmd/install_spec.rb b/Library/Homebrew/test/cmd/install_spec.rb index c770d60309..f39994e88f 100644 --- a/Library/Homebrew/test/cmd/install_spec.rb +++ b/Library/Homebrew/test/cmd/install_spec.rb @@ -351,7 +351,7 @@ RSpec.describe Homebrew::Cmd::InstallCmd do allow(Homebrew.messages).to receive(:display_messages) expect(Homebrew::DownloadQueue).to receive(:new).ordered.and_return(download_queue) expect(formula_installer).to receive(:download_queue=).with(download_queue).ordered - expect(formula_installer).to receive(:prelude_fetch).ordered + expect(formula_installer).to receive(:prelude_fetch).with(no_args).ordered expect(Homebrew::Upgrade).to receive(:dependants).ordered.and_return(dependants) expect(Homebrew::Install).to receive(:enqueue_formulae) .with([formula_installer], download_queue:) @@ -363,6 +363,43 @@ RSpec.describe Homebrew::Cmd::InstallCmd do cmd.run end + it "drains metadata-only prelude fetches before the dry-run plan when asking" do + cmd = described_class.new(["testball"]) + download_queue = instance_double(Homebrew::DownloadQueue, shutdown: nil, + downloads: { instance_double(Downloadable) => nil }) + formula = formula("testball") do + T.bind(self, T.class_of(Formula)) + url "https://brew.sh/testball-0.1.tar.gz" + end + formula_installer = instance_double(FormulaInstaller, formula:) + dependants = Homebrew::Upgrade::Dependents.new(upgradeable: [], pinned: [], skipped: []) + + allow(Tap).to receive_messages(with_formula_name: nil, with_cask_token: nil) + allow(Homebrew::Trust).to receive(:trust_fully_qualified_items!) + allow(cmd.args.named).to receive(:to_formulae_and_casks).with(warn: false).and_return([formula]) + allow(Homebrew::Install).to receive(:perform_preinstall_checks_once) + allow(Homebrew::Install).to receive(:check_cc_argv) + allow(Homebrew::Install).to receive_messages(install_formula?: true, formula_installers: [formula_installer]) + allow(Homebrew::Install).to receive(:install_formulae) + allow(Homebrew::Upgrade).to receive(:upgrade_dependents) + allow(Homebrew::Cleanup).to receive(:periodic_clean!) + allow(Homebrew.messages).to receive(:display_messages) + expect(Homebrew::DownloadQueue).to receive(:new).ordered.and_return(download_queue) + expect(formula_installer).to receive(:download_queue=).with(download_queue).ordered + expect(formula_installer).to receive(:prelude_fetch).with(metadata_only: true).ordered + expect(Homebrew::Upgrade).to receive(:dependants).ordered.and_return(dependants) + expect(download_queue).to receive(:fetch).ordered + expect(Homebrew::Install).to receive(:ask_formulae).ordered + expect(Homebrew::Install).to receive(:enqueue_formulae) + .with([formula_installer], download_queue:) + .ordered + .and_return([formula_installer]) + expect(download_queue).to receive(:fetch).ordered + expect(download_queue).to receive(:shutdown).ordered + + cmd.run + end + it "does not install `homebrew/cask` when a cask remains unavailable" do cmd = described_class.new(["foo"]) cask_tap = CoreCaskTap.instance @@ -505,11 +542,12 @@ RSpec.describe Homebrew::Cmd::InstallCmd do true end + expect(download_queue).to receive(:fetch) + .with(heading: "Fetching downloads for: testball_bottle and codex") expect { cmd.run }.to output(<<~EOS).to_stdout ==> Upgrading 1 outdated package: codex 0.117.0 -> 0.118.0 - ==> Fetching downloads for: testball_bottle and codex EOS end end diff --git a/Library/Homebrew/test/cmd/readall_spec.rb b/Library/Homebrew/test/cmd/readall_spec.rb index 4dd796a6a1..f587114440 100644 --- a/Library/Homebrew/test/cmd/readall_spec.rb +++ b/Library/Homebrew/test/cmd/readall_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Homebrew::Cmd::ReadallCmd do name "Example" desc "macOS-only cask" homepage "https://example.invalid/" - depends_on macos: ">= :ventura" + depends_on macos: :ventura binary "x" end RUBY @@ -65,6 +65,83 @@ RSpec.describe Homebrew::Cmd::ReadallCmd do expect(success).to be false end + describe "Readall.valid_ruby_syntax?" do + it "returns true for valid Ruby files" do + file = mktmpdir/"valid.rb" + file.write "puts 1\n" + + success = T.let(false, T::Boolean) + expect { success = Readall.valid_ruby_syntax?([file]) }.not_to output.to_stderr + expect(success).to be true + end + + it "prints errors for files with invalid syntax" do + file = mktmpdir/"invalid.rb" + file.write "def foo(\n" + + success = T.let(true, T::Boolean) + expect { success = Readall.valid_ruby_syntax?([file]) }.to output(/syntax error/).to_stderr + expect(success).to be false + end + + it "prints warnings for files with questionable syntax" do + file = mktmpdir/"warning.rb" + file.write "def foo\n bar = 1\n nil\nend\n" + + success = T.let(true, T::Boolean) + expect { success = Readall.valid_ruby_syntax?([file]) }.to output(/unused variable/).to_stderr + expect(success).to be false + end + + it "aggregates failures across parallel worker processes" do + dir = mktmpdir + files = (1..9).map do |i| + file = dir/"valid#{i}.rb" + file.write "puts #{i}\n" + file + end + bad_file = dir/"invalid.rb" + bad_file.write "def foo(\n" + files << bad_file + + success = T.let(true, T::Boolean) + expect { success = Readall.valid_ruby_syntax?(files) }.to output(/syntax error/).to_stderr + expect(success).to be false + end + end + + it "validates tap files in parallel worker processes" do + tap_path = mktmpdir + cask_files = (1..8).map do |i| + file = tap_path/"Casks/linux-example#{i}.rb" + file.dirname.mkpath + file.write <<~RUBY + cask "linux-example#{i}" do + version "1.0" + sha256 arm: "0000000000000000000000000000000000000000000000000000000000000000" + url "https://example.invalid/x.tar.gz" + name "Example" + desc "Cask missing Linux stanzas" + homepage "https://example.invalid/" + binary "x" + end + RUBY + file + end + + success = T.let(true, T::Boolean) + expect do + success = Homebrew::SimulateSystem.with(os: :linux) do + Readall.valid_tap?( + instance_double(Tap, formula_files: [], cask_files:), + os_arch_combinations: [[:linux, :arm]], + ) + end + end.to output(a_string_matching(/(?=.*linux-example1\.rb)(?=.*linux-example8\.rb)/m)).to_stderr + + expect(success).to be false + end + it "explains nil sha256 values when loading tap casks on Linux" do tap_path = mktmpdir linux_cask_file = tap_path/"Casks/linux-example.rb" @@ -93,4 +170,76 @@ RSpec.describe Homebrew::Cmd::ReadallCmd do expect(success).to be false end + + it "reports Linux architectures missing a checksum despite an `on_macos` macOS dependency" do + tap_path = mktmpdir + cross_os_cask_file = tap_path/"Casks/cross-os-example.rb" + cross_os_cask_file.dirname.mkpath + cross_os_cask_file.write <<~RUBY + cask "cross-os-example" do + version "1.0" + sha256 arm: "0000000000000000000000000000000000000000000000000000000000000000", + intel: "1111111111111111111111111111111111111111111111111111111111111111", + x86_64_linux: "2222222222222222222222222222222222222222222222222222222222222222" + url "https://example.invalid/x.tar.gz" + name "Example" + desc "Cross-OS cask" + homepage "https://example.invalid/" + + on_macos do + depends_on macos: :ventura + end + + binary "x" + end + RUBY + + success = T.let(false, T::Boolean) + expect do + success = Homebrew::SimulateSystem.with(os: :linux) do + Readall.valid_tap?( + instance_double(Tap, formula_files: [], cask_files: [cross_os_cask_file]), + os_arch_combinations: [[:linux, :arm]], + ) + end + end.to output(/Missing Linux stanzas/).to_stderr + + expect(success).to be false + end + + it "allows Linux architectures excluded by `depends_on arch:`" do + tap_path = mktmpdir + linux_intel_cask_file = tap_path/"Casks/linux-intel-example.rb" + linux_intel_cask_file.dirname.mkpath + linux_intel_cask_file.write <<~RUBY + cask "linux-intel-example" do + version "1.0" + sha256 arm: "0000000000000000000000000000000000000000000000000000000000000000", + intel: "1111111111111111111111111111111111111111111111111111111111111111", + x86_64_linux: "2222222222222222222222222222222222222222222222222222222222222222" + url "https://example.invalid/x.tar.gz" + name "Example" + desc "Intel-only-on-Linux cask" + homepage "https://example.invalid/" + + on_linux do + depends_on arch: :x86_64 + end + + binary "x" + end + RUBY + + success = T.let(false, T::Boolean) + expect do + success = Homebrew::SimulateSystem.with(os: :linux) do + Readall.valid_tap?( + instance_double(Tap, formula_files: [], cask_files: [linux_intel_cask_file]), + os_arch_combinations: [[:linux, :arm], [:linux, :intel]], + ) + end + end.not_to output.to_stderr + + expect(success).to be true + end end diff --git a/Library/Homebrew/test/cmd/reinstall_spec.rb b/Library/Homebrew/test/cmd/reinstall_spec.rb index 82ad3c51c9..83b0b55b81 100644 --- a/Library/Homebrew/test/cmd/reinstall_spec.rb +++ b/Library/Homebrew/test/cmd/reinstall_spec.rb @@ -68,7 +68,6 @@ RSpec.describe Homebrew::Cmd::Reinstall do allow(Homebrew::Reinstall).to receive(:build_install_context).and_return(reinstall_context) allow(Homebrew::Upgrade).to receive(:dependants).and_return(dependants) allow(Homebrew::Install).to receive(:ask_formulae) - allow(Homebrew::Install).to receive(:show_combined_fetch_downloads_heading) allow(Homebrew::Install).to receive(:enqueue_formulae).and_return([formula_installer]) allow(Homebrew::Install).to receive(:enqueue_cask_installers) allow(Cask::Installer).to receive(:new).and_return(instance_double(Cask::Installer)) diff --git a/Library/Homebrew/test/cmd/setup-sandbox_spec.rb b/Library/Homebrew/test/cmd/setup-sandbox_spec.rb deleted file mode 100644 index 47bee45963..0000000000 --- a/Library/Homebrew/test/cmd/setup-sandbox_spec.rb +++ /dev/null @@ -1,186 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "fileutils" -require "open3" - -require "cmd/shared_examples/args_parse" -require "cmd/setup-sandbox" - -RSpec.describe Homebrew::Cmd::SetupSandbox do - let(:setup_sandbox_script) { HOMEBREW_LIBRARY_PATH/"cmd/setup-sandbox.sh" } - let(:proc_sys_root) { mktmpdir } - - it_behaves_like "parseable arguments" - - def run_setup_sandbox_shell(script, env = {}) - Bundler.with_unbundled_env do - Open3.capture3( - { "GITHUB_ACTIONS" => nil, "HOMEBREW_LINUX" => "1", "HOMEBREW_PROC_SYS" => proc_sys_root.to_s } - .merge(env), - "/bin/bash", "-c", script - ) - end - end - - def touch_proc_sys(path) - file = proc_sys_root/path - FileUtils.mkdir_p(file.dirname) - FileUtils.touch(file) - file - end - - it "does nothing on non-Linux systems" do - stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "HOMEBREW_LINUX" => nil) - source "#{setup_sandbox_script}" - sysctl() { printf 'sysctl %s\\n' "$*"; } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).to be_empty - end - - it "applies the sandbox sysctl settings when they are unset" do - touch_proc_sys "kernel/unprivileged_userns_clone" - touch_proc_sys "user/max_user_namespaces" - - stdout, _stderr, status = run_setup_sandbox_shell <<~SH - source "#{setup_sandbox_script}" - sysctl() { [[ "$1" == "-n" ]] && { echo 0; return; }; printf 'sysctl %s\\n' "$*"; } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).to eq(<<~EOS) - sysctl -w kernel.unprivileged_userns_clone=1 - sysctl -w user.max_user_namespaces=28633 - EOS - end - - it "leaves already-configured sysctls unchanged" do - touch_proc_sys "kernel/unprivileged_userns_clone" - touch_proc_sys "user/max_user_namespaces" - touch_proc_sys "kernel/apparmor_restrict_unprivileged_userns" - - stdout, _stderr, status = run_setup_sandbox_shell <<~SH - source "#{setup_sandbox_script}" - sysctl() { - if [[ "$1" == "-n" ]] - then - case "$2" in - kernel.unprivileged_userns_clone) echo 1;; - user.max_user_namespaces) echo 28633;; - kernel.apparmor_restrict_unprivileged_userns) echo 0;; - esac - return - fi - printf 'sysctl %s\\n' "$*" - } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).to be_empty - end - - it "skips missing sysctls and read-only sysctl writes" do - touch_proc_sys("user/max_user_namespaces").chmod(0444) - - stdout, stderr, status = run_setup_sandbox_shell <<~SH - source "#{setup_sandbox_script}" - sysctl_log="#{proc_sys_root}/sysctl.log" - sysctl() { - printf '%s\\n' "$*" >> "$sysctl_log" - if [[ "$1" == "-n" && "$2" == "user.max_user_namespaces" ]] - then - echo 1 - return - fi - printf 'unexpected sysctl %s\\n' "$*" >&2 - return 1 - } - homebrew-setup-sandbox - cat "$sysctl_log" - SH - - expect(status.success?).to be true - expect(stdout).to eq("-n user.max_user_namespaces\n") - expect(stderr).to be_empty - end - - it "does not hide sysctl write errors" do - touch_proc_sys "user/max_user_namespaces" - - stdout, stderr, status = run_setup_sandbox_shell <<~SH - source "#{setup_sandbox_script}" - sysctl() { - if [[ "$1" == "-n" ]] - then - echo 1 - return - fi - echo 'sysctl: setting key "user.max_user_namespaces", ignoring: Read-only file system' >&2 - return 1 - } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).to be_empty - expect(stderr).to eq("sysctl: setting key \"user.max_user_namespaces\", ignoring: Read-only file system\n") - end - - it "installs Bubblewrap on GitHub Actions when it is missing" do - stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "GITHUB_ACTIONS" => "true") - source "#{setup_sandbox_script}" - command() { case "$2" in bwrap) return 1;; apt-get) return 0;; *) return 1;; esac; } - apt-get() { printf 'apt-get %s\\n' "$*"; } - sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).to eq("apt-get install --yes bubblewrap\n") - end - - it "does not install Bubblewrap when it is already present" do - stdout, _stderr, status = run_setup_sandbox_shell(<<~SH, "GITHUB_ACTIONS" => "true") - source "#{setup_sandbox_script}" - command() { return 0; } - apt-get() { printf 'apt-get %s\\n' "$*"; } - sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).not_to include("apt-get") - end - - it "installs Bubblewrap when only the cgroup marks a GitHub Actions runner" do - stdout, _stderr, status = run_setup_sandbox_shell <<~SH - source "#{setup_sandbox_script}" - grep() { return 0; } - command() { case "$2" in bwrap) return 1;; apt-get) return 0;; *) return 1;; esac; } - apt-get() { printf 'apt-get %s\\n' "$*"; } - sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).to eq("apt-get install --yes bubblewrap\n") - end - - it "does not install Bubblewrap outside GitHub Actions" do - stdout, _stderr, status = run_setup_sandbox_shell <<~SH - source "#{setup_sandbox_script}" - grep() { return 1; } - apt-get() { printf 'apt-get %s\\n' "$*"; } - sysctl() { [[ "$1" == "-n" ]] && { echo 1; return; }; :; } - homebrew-setup-sandbox - SH - - expect(status.success?).to be true - expect(stdout).not_to include("apt-get") - end -end diff --git a/Library/Homebrew/test/cmd/update_spec.rb b/Library/Homebrew/test/cmd/update_spec.rb index 31f2cb8e93..c257c8a465 100644 --- a/Library/Homebrew/test/cmd/update_spec.rb +++ b/Library/Homebrew/test/cmd/update_spec.rb @@ -35,6 +35,45 @@ RSpec.describe Homebrew::Cmd::Update do end end + it "retries a failed conditional API download without the time condition" do + cache_path = test_root/"cache/api/formula.jws.json" + requests_file = test_root/"requests.txt" + update_failed_file = test_root/"update_failed.txt" + setup_update_utils + cache_path.dirname.mkpath + cache_path.write "cached" + + _stdout, stderr, status = run_update_shell( + <<~SH, + source "#{update_script}" + curl() { + if [[ "$*" == *"--time-cond"* ]] + then + echo conditional >> "#{requests_file}" + return 56 + fi + + echo unconditional >> "#{requests_file}" + printf fresh > "#{cache_path}" + } + fetch_api_file formula.jws.json "#{update_failed_file}" + SH + { + "HOMEBREW_API_DEFAULT_DOMAIN" => "https://formulae.example/api", + "HOMEBREW_API_DOMAIN" => nil, + "HOMEBREW_CACHE" => (test_root/"cache").to_s, + "HOMEBREW_CURL_SPEED_LIMIT" => "100", + "HOMEBREW_CURL_SPEED_TIME" => "5", + "HOMEBREW_LIBRARY" => (test_root/"Library").to_s, + "HOMEBREW_USER_AGENT_CURL" => "Homebrew/test", + }, + ) + + expect([status.success?, stderr, requests_file.read, cache_path.read, update_failed_file.exist?]).to eq( + [true, "", "conditional\nunconditional\n", "fresh", false], + ) + end + it "passes all arguments through to delegated upgrades" do args_file = test_root/"brew-args.txt" brew_wrapper = test_root/"brew-wrapper" diff --git a/Library/Homebrew/test/cmd/upgrade_spec.rb b/Library/Homebrew/test/cmd/upgrade_spec.rb index 24c3b84407..aa9535fdf6 100644 --- a/Library/Homebrew/test/cmd/upgrade_spec.rb +++ b/Library/Homebrew/test/cmd/upgrade_spec.rb @@ -427,12 +427,11 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do expect(cmd).to receive(:upgrade_outdated_formulae!) .with( [], - prefetch_only: true, + prefetch_only: true, download_queue:, - prefetch_names: [], - prefetch_upgrades: [], - show_upgrade_summary: false, - show_downloads_heading: false, + prefetch_names: [], + prefetch_upgrades: [], + show_upgrade_summary: false, ) .ordered .and_return(true) @@ -440,11 +439,10 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do .with( [], download_queue:, - prefetch_names: [], - prefetch_upgrades: [], - prefetch_casks: [], - prefetch_errors: [], - show_downloads_heading: false, + prefetch_names: [], + prefetch_upgrades: [], + prefetch_casks: [], + prefetch_errors: [], ) .ordered .and_return(true) @@ -758,12 +756,13 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do allow(Homebrew::Cleanup).to receive(:periodic_clean!) allow(Homebrew::Reinstall).to receive(:reinstall_pkgconf_if_needed!) allow(Homebrew.messages).to receive(:display_messages) + expect(download_queue).to receive(:fetch) + .with(heading: "Fetching downloads for: deno and codex") expect { cmd.run }.to output(<<~EOS).to_stdout ==> Upgrading 2 outdated packages: deno 2.7.10 -> 2.7.11 codex 0.117.0 -> 0.118.0 - ==> Fetching downloads for: deno and codex EOS end @@ -885,19 +884,21 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do allow(Cask::Installer).to receive(:new).and_return(installer) expect(installer).to receive(:prelude_fetch_download).and_return(source_download) expect(download_queue).to receive(:enqueue).with(source_download).ordered - expect(download_queue).to receive(:fetch).ordered - expect(download_queue).to receive(:fetch).ordered + expect(download_queue).to receive(:fetch) + .with(only: Cask::Download, heading: "Downloading Cask files") + .ordered + expect(download_queue).to receive(:fetch) + .with(heading: "Fetching downloads for: deno and codex") + .ordered allow(Cask::Upgrade).to receive_messages(outdated_casks: [cask], upgrade_casks!: true) allow(Homebrew::Cleanup).to receive(:periodic_clean!) allow(Homebrew::Reinstall).to receive(:reinstall_pkgconf_if_needed!) allow(Homebrew.messages).to receive(:display_messages) expect { cmd.run }.to output(<<~EOS).to_stdout - ==> Downloading Cask files ==> Upgrading 2 outdated packages: deno 2.7.10 -> 2.7.11 codex 0.117.0 -> 0.118.0 - ==> Fetching downloads for: deno and codex EOS end @@ -944,7 +945,6 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do prefetch_upgrades:, prefetch_casks:, prefetch_errors:, - show_downloads_heading: false, ), ).to be(true) expect(prefetch_names).to eq(["codex"]) @@ -986,7 +986,9 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do end allow(Cask::Installer).to receive(:new).and_return(installer) expect(installer).to receive(:prelude_fetch_download).and_return(nil) - expect(download_queue).to receive(:fetch).once + expect(download_queue).to receive(:fetch) + .with(heading: "Fetching downloads for: deno and codex") + .once allow(Cask::Upgrade).to receive_messages(outdated_casks: [cask], upgrade_casks!: true) allow(Homebrew::Cleanup).to receive(:periodic_clean!) allow(Homebrew::Reinstall).to receive(:reinstall_pkgconf_if_needed!) @@ -996,12 +998,10 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do ==> Upgrading 2 outdated packages: deno 2.7.10 -> 2.7.11 codex 0.117.0 -> 0.118.0 - ==> Fetching downloads for: deno and codex EOS end - it "prints a bottle manifest heading before formula prefetches" do - cmd = described_class.new([]) + it "passes a bottle manifest heading to the tab prefetch queue" do formula = formula("deno") do T.bind(self, T.class_of(Formula)) url "https://brew.sh/deno-2.7.11.tar.gz" @@ -1012,37 +1012,16 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97" end end + download_queue = instance_double(Homebrew::DownloadQueue, enqueue: nil, shutdown: nil) - allow(formula).to receive_messages(outdated?: true, latest_formula: formula, latest_version_installed?: false) - allow(Homebrew::Install).to receive(:perform_preinstall_checks_once) - allow(Homebrew::Upgrade).to receive(:formula_installers).and_return([]) + allow(formula).to receive(:latest_formula).and_return(formula) + allow(Migrator).to receive(:migrate_if_needed) + allow(Homebrew::DownloadQueue).to receive(:new).and_return(download_queue) + expect(Homebrew).not_to receive(:default_download_queue) + expect(download_queue).to receive(:fetch) + .with(only: Resource::BottleManifest, heading: "Downloading bottle manifests", allow_failures: true) - expect do - cmd.formulae_upgrade_context([formula], show_upgrade_summary: false) - end.to output("==> Downloading bottle manifests\n").to_stdout - end - - it "omits the bottle manifest heading for cached formula manifests" do - cmd = described_class.new([]) - formula = formula("deno") do - T.bind(self, T.class_of(Formula)) - url "https://brew.sh/deno-2.7.11.tar.gz" - - bottle do - root_url HOMEBREW_BOTTLE_DEFAULT_DOMAIN - sha256 cellar: :any_skip_relocation, - Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97" - end - end - - allow(formula).to receive_messages(outdated?: true, latest_formula: formula, latest_version_installed?: false) - allow(formula.bottle&.github_packages_manifest_resource).to receive(:downloaded_and_valid?).and_return(true) - allow(Homebrew::Install).to receive(:perform_preinstall_checks_once) - allow(Homebrew::Upgrade).to receive(:formula_installers).and_return([]) - - expect do - cmd.formulae_upgrade_context([formula], show_upgrade_summary: false) - end.not_to output(/Downloading bottle manifests/).to_stdout + Homebrew::Upgrade.formula_installers([formula], flags: []) end it "does not trust failed shared prefetches" do diff --git a/Library/Homebrew/test/dev-cmd/audit_spec.rb b/Library/Homebrew/test/dev-cmd/audit_spec.rb index 4c2965e361..f75a3a538b 100644 --- a/Library/Homebrew/test/dev-cmd/audit_spec.rb +++ b/Library/Homebrew/test/dev-cmd/audit_spec.rb @@ -13,7 +13,11 @@ RSpec.describe Homebrew::DevCmd::Audit do let(:tap_path) { mktmpdir } let(:macos_only_cask_file) { tap_path/"Casks/macos-only-example.rb" } let(:linux_cask_file) { tap_path/"Casks/linux-example.rb" } - let(:tap) { instance_double(Tap, formula_files: [], cask_files: [macos_only_cask_file, linux_cask_file]) } + let(:linux_only_cask_file) { tap_path/"Casks/linux-only-example.rb" } + let(:tap) do + instance_double(Tap, formula_files: [], cask_files: [macos_only_cask_file, linux_cask_file, + linux_only_cask_file]) + end before do macos_only_cask_file.dirname.mkpath @@ -22,11 +26,11 @@ RSpec.describe Homebrew::DevCmd::Audit do version "1.0" sha256 arm: "0000000000000000000000000000000000000000000000000000000000000000", intel: "1111111111111111111111111111111111111111111111111111111111111111" - url "https://example.invalid/x.pkg" + url "https://example.invalid/x-\#{version}.pkg" name "Example" desc "macOS-only cask" homepage "https://example.invalid/" - depends_on macos: ">= :ventura" + depends_on macos: :ventura binary "x" end RUBY @@ -35,13 +39,26 @@ RSpec.describe Homebrew::DevCmd::Audit do version "1.0" sha256 arm: "0000000000000000000000000000000000000000000000000000000000000000", intel: "1111111111111111111111111111111111111111111111111111111111111111" - url "https://example.invalid/x.tar.gz" + url "https://example.invalid/x-\#{version}.tar.gz" name "Example" desc "Linux-supported cask" homepage "https://example.invalid/" binary "x" end RUBY + linux_only_cask_file.write <<~RUBY + cask "linux-only-example" do + version "1.0" + sha256 arm64_linux: "0000000000000000000000000000000000000000000000000000000000000000", + x86_64_linux: "1111111111111111111111111111111111111111111111111111111111111111" + url "https://example.invalid/x-\#{version}.tar.gz" + name "Example" + desc "Linux-only cask" + homepage "https://example.invalid/" + depends_on :linux + binary "x" + end + RUBY allow(Homebrew).to receive(:install_bundler_gems!) ENV.activate_extensions! @@ -61,5 +78,11 @@ RSpec.describe Homebrew::DevCmd::Audit do .and output(/1 problem in 1 cask detected/).to_stderr end end + + it "audits Linux-only casks under Linux when running on macOS" do + Homebrew::SimulateSystem.with(os: :macos) do + expect { audit.run }.not_to output.to_stdout + end + end end end diff --git a/Library/Homebrew/test/dev-cmd/bump_spec.rb b/Library/Homebrew/test/dev-cmd/bump_spec.rb index 2c60ebf71a..421cf30935 100644 --- a/Library/Homebrew/test/dev-cmd/bump_spec.rb +++ b/Library/Homebrew/test/dev-cmd/bump_spec.rb @@ -222,6 +222,31 @@ RSpec.describe Homebrew::DevCmd::Bump do bump.retrieve_and_display_info_and_open_pr(c_basic, "basic-cask", [], ambiguous_cask: false) end + + it "notes when a newer upstream version was skipped due to release cooldown" do + version_info = Homebrew::DevCmd::Bump::VersionBumpInfo.new( + type: :formula, + deprecated: { general: false }, + multiple_versions: { current: false, new: false }, + version_name: "formula version:", + current_version: Homebrew::BumpVersionParser.new(general: Version.new("1.2.3")), + new_version: Homebrew::BumpVersionParser.new(general: Version.new("1.2.3")), + repology_latest: "not found", + newer_than_upstream: { general: false }, + cooldown_skipped_versions: { general: Version.new("1.2.4") }, + duplicate_pull_requests: nil, + maybe_duplicate_pull_requests: nil, + ) + allow(bump).to receive(:retrieve_versions_by_arch).and_return(version_info) + + expect { bump.retrieve_and_display_info_and_open_pr(f_basic, "basic_formula", [], ambiguous_cask: false) } + .to output(<<~EOS).to_stdout + ==> basic_formula has a new version in release cooldown + Current formula version: 1.2.3 + Latest livecheck version: 1.2.4 (released less than 1 day ago) + Bump-ready version: 1.2.3 + EOS + end end describe "::retrieve_versions_by_arch" do @@ -263,11 +288,26 @@ RSpec.describe Homebrew::DevCmd::Bump do end RUBY end + let(:c_multi_arch) do + Cask::CaskLoader.load(+<<-RUBY) + cask "multi_arch_cask" do + arch arm: "arm64", intel: "x64" + + version "1.2.3" + sha256 :no_check + + url "https://brew.sh/test-\#{arch}.dmg" + name "Multi Arch Cask" + desc "Multi arch cask" + homepage "https://brew.sh" + end + RUBY + end it "simulates only arm and consolidates to a general version when `depends_on arch:` restricts to arm-only" do allow(c_arm_only).to receive(:sourcefile_path).and_return(Pathname("arm_only_cask.rb")) allow(Cask::CaskLoader).to receive(:load).and_return(c_arm_only) - expect(bump).to receive(:livecheck_result).once.and_return(Version.new("1.2.4")) + expect(bump).to receive(:livecheck_result).once.and_return([Version.new("1.2.4"), nil]) version_info = bump.retrieve_versions_by_arch( formula_or_cask: c_arm_only, repositories: [], name: "arm-only-cask", @@ -278,13 +318,37 @@ RSpec.describe Homebrew::DevCmd::Bump do it "simulates only intel and consolidates to a general version when `depends_on arch:` restricts to intel-only" do allow(c_intel_only).to receive(:sourcefile_path).and_return(Pathname("intel_only_cask.rb")) allow(Cask::CaskLoader).to receive(:load).and_return(c_intel_only) - expect(bump).to receive(:livecheck_result).once.and_return(Version.new("1.2.4")) + expect(bump).to receive(:livecheck_result).once.and_return([Version.new("1.2.4"), nil]) version_info = bump.retrieve_versions_by_arch( formula_or_cask: c_intel_only, repositories: [], name: "intel-only-cask", ) expect(version_info.new_version).to eq(Homebrew::BumpVersionParser.new(general: Version.new("1.2.4"))) end + + it "records the upstream version skipped due to release cooldown" do + expect(bump).to receive(:livecheck_result).once.and_return([Version.new("1.2.3"), Version.new("1.2.4")]) + + version_info = bump.retrieve_versions_by_arch( + formula_or_cask: f_basic, repositories: [], name: "basic_formula", + ) + expect(version_info.cooldown_skipped_versions).to eq({ general: Version.new("1.2.4") }) + end + + it "records cooldown-skipped versions per architecture" do + allow(c_multi_arch).to receive(:sourcefile_path).and_return(Pathname("multi_arch_cask.rb")) + allow(Cask::CaskLoader).to receive(:load).and_return(c_multi_arch) + expect(bump).to receive(:livecheck_result).twice.and_return( + [Version.new("1.2.3"), Version.new("1.2.4")], + [Version.new("1.2.3"), Version.new("1.2.5")], + ) + + version_info = bump.retrieve_versions_by_arch( + formula_or_cask: c_multi_arch, repositories: [], name: "multi-arch-cask", + ) + expect(version_info.cooldown_skipped_versions).to eq({ arm: Version.new("1.2.5"), + intel: Version.new("1.2.4") }) + end end describe "::message?" do diff --git a/Library/Homebrew/test/dev-cmd/tap-new_spec.rb b/Library/Homebrew/test/dev-cmd/tap-new_spec.rb index 71b86f52c4..898651cc6f 100644 --- a/Library/Homebrew/test/dev-cmd/tap-new_spec.rb +++ b/Library/Homebrew/test/dev-cmd/tap-new_spec.rb @@ -21,6 +21,7 @@ RSpec.describe Homebrew::DevCmd::TapNew do dependabot_yml = (HOMEBREW_LIBRARY/"Taps/homebrew/homebrew-foo/.github/dependabot.yml").read tests_yml = (HOMEBREW_LIBRARY/"Taps/homebrew/homebrew-foo/.github/workflows/tests.yml").read publish_yml = (HOMEBREW_LIBRARY/"Taps/homebrew/homebrew-foo/.github/workflows/publish.yml").read + autobump_yml = (HOMEBREW_LIBRARY/"Taps/homebrew/homebrew-foo/.github/workflows/autobump.yml").read [dependabot_yml, tests_yml, publish_yml].each { YAML.parse(it) } expect(tests_yml).not_to include("HOMEBREW_DEVELOPER") expect(tests_yml).to include("options: --privileged") @@ -32,5 +33,13 @@ RSpec.describe Homebrew::DevCmd::TapNew do expect(publish_yml).not_to include("gh pr view") expect(publish_yml).to include('brew pr-pull --debug --tap="$GITHUB_REPOSITORY" --head-sha="$HEAD_SHA"') expect(publish_yml).to include('brew pr-pull --debug --tap="$GITHUB_REPOSITORY" "$PULL_REQUEST"') + expect(autobump_yml).not_to include("HOMEBREW_DEVELOPER") + expect(autobump_yml).not_to include("pull_request_target") + expect(autobump_yml).not_to include("workflow_run") + expect(autobump_yml).not_to include("TAP_NEW_") + expect(autobump_yml).not_to include("cron: \"1 1 1 1 1\"") + expect(autobump_yml).not_to include("# this will be changed later and randomised by brew tap-new") + expect(autobump_yml).to include("- main") + expect(autobump_yml).to include('brew bump --no-fork --open-pr --formulae --bump-synced --tap="$TAP_NAME"') end end diff --git a/Library/Homebrew/test/dev-cmd/tests_spec.rb b/Library/Homebrew/test/dev-cmd/tests_spec.rb index 3da3265b6e..6d106c84ec 100644 --- a/Library/Homebrew/test/dev-cmd/tests_spec.rb +++ b/Library/Homebrew/test/dev-cmd/tests_spec.rb @@ -15,61 +15,34 @@ RSpec.describe Homebrew::DevCmd::Tests do require "sandbox" allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true) - allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(false) allow(GitHub::Actions).to receive(:env_set?).and_return(false) end it "does not require the Linux sandbox when Linux sandboxing is disabled" do allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(false) allow(Sandbox).to receive_messages(available?: false, failure_reason: "sandbox unavailable") - expect(Sandbox).not_to receive(:ensure_sandbox_installed!) - expect(Sandbox).not_to receive(:configure!) + expect(Sandbox).not_to receive(:ensure_sandbox_available!) expect { tests.check_test_environment! }.not_to raise_error end - it "does not fail on GitHub Actions when requested Landlock is unavailable" do - allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(true) + it "does not fail on GitHub Actions when the Linux sandbox is unavailable" do allow(Sandbox).to receive(:available?).and_return(false) allow(GitHub::Actions).to receive(:env_set?).and_return(true) - expect(Sandbox).not_to receive(:ensure_sandbox_installed!) - expect(Sandbox).not_to receive(:configure!) + expect(Sandbox).not_to receive(:ensure_sandbox_available!) expect { tests.check_test_environment! }.not_to raise_error end - it "fails outside GitHub Actions when requested Landlock is unavailable" do - allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(true) + it "fails outside GitHub Actions when the Linux sandbox is unavailable" do allow(Sandbox).to receive_messages(available?: false, failure_reason: "Landlock is not available.") - expect(Sandbox).not_to receive(:ensure_sandbox_installed!) - expect(Sandbox).not_to receive(:configure!) expect { tests.check_test_environment! } .to raise_error(RuntimeError, "Landlock is not available.") end - it "configures requested Landlock when it is available" do - allow(OS::Linux::Sandbox).to receive(:landlock?).and_return(true) + it "passes when the Linux sandbox is available" do allow(Sandbox).to receive(:available?).and_return(true) - expect(Sandbox).not_to receive(:ensure_sandbox_installed!) - expect(Sandbox).to receive(:configure!) - - expect { tests.check_test_environment! }.not_to raise_error - end - - it "installs and checks Bubblewrap outside GitHub Actions" do - allow(Sandbox).to receive(:available?).and_return(true) - expect(Sandbox).to receive(:ensure_sandbox_installed!).with(install_from_tests: true) - expect(Sandbox).not_to receive(:configure!) - - expect { tests.check_test_environment! }.not_to raise_error - end - - it "configures and checks Bubblewrap on GitHub Actions" do - allow(GitHub::Actions).to receive(:env_set?).and_return(true) - allow(Sandbox).to receive(:available?).and_return(true) - expect(Sandbox).not_to receive(:ensure_sandbox_installed!) - expect(Sandbox).to receive(:configure!) expect { tests.check_test_environment! }.not_to raise_error end diff --git a/Library/Homebrew/test/download_queue_spec.rb b/Library/Homebrew/test/download_queue_spec.rb index 271066a71a..7550103dfd 100644 --- a/Library/Homebrew/test/download_queue_spec.rb +++ b/Library/Homebrew/test/download_queue_spec.rb @@ -71,6 +71,28 @@ RSpec.describe Homebrew::DownloadQueue do expect(failure_index).to be > show_cursor_index end + it "fetches only downloads of the given class and keeps others queued unreported" do + manifest = instance_double( + Resource::BottleManifest, + cached_download: HOMEBREW_CACHE/"downloads/testball_manifest.json", + checksum: nil, + downloaded_and_valid?: true, + downloader: nil, + download_queue_message: "Bottle Manifest testball", + download_queue_name: "testball", + download_queue_type: "Bottle Manifest", + ) + allow(manifest).to receive(:is_a?) { |klass| klass == Resource::BottleManifest } + + download_queue.enqueue(manifest) + download_queue.enqueue(downloadable) + + expect do + download_queue.fetch(only: Resource::BottleManifest) + end.to output(/Bottle Manifest testball/).to_stderr + expect(download_queue.downloads.keys).to eq [downloadable] + end + it "raises and clears queue state on a bottle manifest failure in parallel mode" do allow(retryable_download).to receive(:fetch).and_raise(Resource::BottleManifest::Error.new("manifest missing")) @@ -80,6 +102,53 @@ RSpec.describe Homebrew::DownloadQueue do expect(download_queue.downloads).to be_empty end + it "reports but tolerates failed downloads when failures are allowed" do + allow(retryable_download).to receive(:fetch).and_raise(Resource::BottleManifest::Error.new("manifest missing")) + + download_queue.enqueue(downloadable) + + expect { download_queue.fetch(allow_failures: true) }.to output(/✘/).to_stderr + expect(download_queue.fetch_failed).to be false + expect(Homebrew).not_to have_failed + end + + it "tolerates failed downloads in serial mode when failures are allowed" do + allow(Homebrew::EnvConfig).to receive(:download_concurrency).and_return(1) + allow(retryable_download).to receive(:fetch).and_raise(Resource::BottleManifest::Error.new("manifest missing")) + + download_queue.enqueue(downloadable) + + expect { download_queue.fetch(allow_failures: true) }.to output(/✘/).to_stderr + expect(Homebrew).not_to have_failed + end + + it "removes known-bad cached files for tolerated checksum mismatches" do + cached_download.dirname.mkpath + cached_download.write("corrupt") + allow(retryable_download).to receive(:fetch) + .and_raise(ChecksumMismatchError.new(cached_download, Checksum.new("aa" * 32), Checksum.new("bb" * 32))) + + download_queue.enqueue(downloadable) + + expect { download_queue.fetch(allow_failures: true) }.to output(/✘/).to_stderr + expect(cached_download).not_to exist + expect(Homebrew).not_to have_failed + end + + it "removes known-bad cached files for tolerated checksum mismatches in serial mode" do + allow(Homebrew::EnvConfig).to receive(:download_concurrency).and_return(1) + cached_download.dirname.mkpath + cached_download.write("corrupt") + allow(retryable_download).to receive(:fetch) + .and_raise(ChecksumMismatchError.new(cached_download, Checksum.new("aa" * 32), Checksum.new("bb" * 32))) + + download_queue.enqueue(downloadable) + + expect { download_queue.fetch(allow_failures: true) }.to output(/✘/).to_stderr + expect(cached_download).not_to exist + expect(Homebrew).not_to have_failed + end + it "cancels remaining downloads and raises on a bottle manifest failure in serial mode" do allow(Homebrew::EnvConfig).to receive(:download_concurrency).and_return(1) allow(retryable_download).to receive(:fetch).and_raise(Resource::BottleManifest::Error.new("manifest missing")) @@ -139,6 +208,38 @@ RSpec.describe Homebrew::DownloadQueue do expect { download_queue.fetch }.not_to output(/\e\[\?2026/).to_stdout end + it "prints the heading to stderr when stdout is not a TTY" do + allow($stdout).to receive(:tty?).and_return(false) + allow(retryable_download).to receive(:fetch).and_return(cached_download) + download_queue.enqueue(downloadable) + + expect { download_queue.fetch(heading: "Downloading Homebrew API data") } + .to output(/^==> Downloading Homebrew API data$/).to_stderr + end + + it "keeps the heading off stdout when stdout is not a TTY" do + allow($stdout).to receive(:tty?).and_return(false) + allow(retryable_download).to receive(:fetch).and_return(cached_download) + download_queue.enqueue(downloadable) + + expect { download_queue.fetch(heading: "Downloading Homebrew API data") } + .not_to output(/Downloading Homebrew API data/).to_stdout + end + + it "prints the heading to stdout on a TTY" do + allow($stdout).to receive(:tty?).and_return(true) + ENV["TERM"] = "xterm-256color" + allow(downloadable).to receive(:fetched_size).and_return(nil) + allow(retryable_download).to receive(:fetch).and_return(cached_download) + + # Build the queue while stdout is a TTY so it captures the TTY render path, + # before the output matcher swaps $stdout to capture the heading. + download_queue.enqueue(downloadable) + + expect { download_queue.fetch(heading: "Downloading Homebrew API data") } + .to output(/==>.*Downloading Homebrew API data/).to_stdout + end + it "wakes when downloads complete instead of polling with sleep" do allow($stdout).to receive(:tty?).and_return(false) allow(retryable_download).to receive(:fetch) do @@ -260,4 +361,24 @@ RSpec.describe Homebrew::DownloadQueue do download_queue.enqueue(bottle, check_attestation: true) download_queue.fetch end + + describe "Homebrew.default_download_queue", order: :defined do + it "memoizes the queue created on first use" do + queue = instance_double(described_class, shutdown: nil) + allow(described_class).to receive(:new).and_return(queue) + + expect(Homebrew.default_download_queue).to be(queue) + end + + it "does not leak a queue stubbed by an earlier example" do + expect(Homebrew.default_download_queue).to be_an_instance_of(described_class) + end + + it "shuts down a memoized real queue when reset" do + queue = Homebrew.default_download_queue + expect(queue).to receive(:shutdown) + + Homebrew.reset_default_download_queue + end + end end diff --git a/Library/Homebrew/test/formula_installer_spec.rb b/Library/Homebrew/test/formula_installer_spec.rb index 29e3c5ebb3..4cb761fbc1 100644 --- a/Library/Homebrew/test/formula_installer_spec.rb +++ b/Library/Homebrew/test/formula_installer_spec.rb @@ -380,6 +380,17 @@ RSpec.describe FormulaInstaller do installer.enqueue_fetch end + + it "does not requeue a bottle already enqueued by the prelude fetch" do + allow(installer).to receive(:pour_bottle?).and_return(true) + expect(download_queue).to receive(:enqueue) + .with(bottle, check_attestation: false, stage: true).once + installer.prelude_fetch + + expect(installer).to receive(:fetch_dependencies) + + installer.enqueue_fetch + end end describe "linking defaults" do @@ -889,78 +900,6 @@ RSpec.describe FormulaInstaller do end.to raise_error(CannotInstallFormulaError) end - it "does not raise on cyclic dependency through direct implicit Bubblewrap" do - ENV["HOMEBREW_DEVELOPER"] = "1" - - formula_name = "homebrew-test-formula" - f = formula formula_name do - T.bind(self, T.class_of(Formula)) - url "foo-1.0" - end - dep = Dependency.new("bubblewrap", [:implicit]) - - allow(f).to receive_messages(deps: [dep], recursive_dependencies: []) - - fi = described_class.new(f) - - expect do - fi.check_install_sanity - end.not_to raise_error - end - - it "does not raise on cyclic dependency through recursive implicit Bubblewrap" do - ENV["HOMEBREW_DEVELOPER"] = "1" - - formula_name = "homebrew-test-formula" - f = formula formula_name do - T.bind(self, T.class_of(Formula)) - url "foo-1.0" - end - dep = Dependency.new("cmake", [:build]) - implicit_bubblewrap = Dependency.new("bubblewrap", [:implicit]) - recursive_dep = Dependency.new(formula_name) - dep_formula = instance_double(Formula) - - allow(f).to receive_messages(deps: [dep], recursive_dependencies: []) - allow(dep).to receive(:to_formula).and_return(dep_formula) - allow(dep_formula).to receive(:recursive_dependencies) do |&block| - (block&.call(dep_formula, implicit_bubblewrap) == Dependable::PRUNE) ? [] : [recursive_dep] - end - - fi = described_class.new(f) - - expect do - fi.check_install_sanity - end.not_to raise_error - end - - it "raises on cyclic dependency through recursive explicit Bubblewrap" do - ENV["HOMEBREW_DEVELOPER"] = "1" - - formula_name = "homebrew-test-formula" - f = formula formula_name do - T.bind(self, T.class_of(Formula)) - url "foo-1.0" - end - dep = Dependency.new("cmake", [:build]) - explicit_bubblewrap = Dependency.new("bubblewrap") - recursive_dep = Dependency.new(formula_name) - dep_formula = instance_double(Formula) - - allow(f).to receive_messages(deps: [dep], recursive_dependencies: []) - allow(dep).to receive(:to_formula).and_return(dep_formula) - allow(dep_formula).to receive(:recursive_dependencies) do |&block| - block&.call(dep_formula, explicit_bubblewrap) - [recursive_dep] - end - - fi = described_class.new(f) - - expect do - fi.check_install_sanity - end.to raise_error(CannotInstallFormulaError) - end - it "raises on pinned dependency" do dep_name = "homebrew-test-dependency" dep_path = CoreTap.instance.new_formula_path(dep_name) @@ -1254,41 +1193,72 @@ RSpec.describe FormulaInstaller do end describe "#prelude_fetch" do - it "uses API bottle metadata for API-loaded formula manifests" do - formula = formula("deno") do - T.bind(self, T.class_of(Formula)) - url "https://brew.sh/deno-2.7.11.tar.gz" + context "with an API-loaded bottled formula" do + let(:deno_formula) do + formula("deno") do + T.bind(self, T.class_of(Formula)) + url "https://brew.sh/deno-2.7.11.tar.gz" + end + end + let(:formula_struct) do + Homebrew::API::FormulaStruct.new( + bottle_checksums: [ + { + cellar: :any_skip_relocation, + Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97", + }, + ], + bottle_present: true, + desc: "deno", + homepage: "https://brew.sh", + license: "MIT", + ruby_source_checksum: "abc123", + stable_present: true, + stable_version: "2.7.11", + ) + end + let(:installer) do + installer = described_class.new(deno_formula, ignore_deps: true) + installer.download_queue = instance_double(Homebrew::DownloadQueue) + installer end - formula_struct = Homebrew::API::FormulaStruct.new( - bottle_checksums: [ - { - cellar: :any_skip_relocation, - Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97", - }, - ], - bottle_present: true, - desc: "deno", - homepage: "https://brew.sh", - license: "MIT", - ruby_source_checksum: "abc123", - stable_present: true, - stable_version: "2.7.11", - ) - installer = described_class.new(formula, ignore_deps: true) - installer.download_queue = instance_double(Homebrew::DownloadQueue) - allow(formula).to receive_messages( - bottle_tag?: true, - core_formula?: true, - loaded_from_internal_api?: true, - pour_bottle?: true, - ) - allow(Homebrew::API::Internal).to receive(:formula_struct).with("deno").and_return(formula_struct) - expect(formula).not_to receive(:bottle_for_tag) - expect(formula).not_to receive(:bottle) - expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest)) + before do + allow(deno_formula).to receive_messages( + bottle_tag?: true, + core_formula?: true, + loaded_from_internal_api?: true, + pour_bottle?: true, + ) + allow(Homebrew::API::Internal).to receive(:formula_struct).with("deno").and_return(formula_struct) + end - installer.prelude_fetch + it "uses API bottle metadata to enqueue the manifest and bottle" do + expect(deno_formula).not_to receive(:bottle_for_tag) + expect(deno_formula).not_to receive(:bottle) + expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest)) + expect(installer.download_queue).to receive(:enqueue) + .with(an_instance_of(Bottle), check_attestation: false, stage: true) + + installer.prelude_fetch + end + + it "enqueues only the bottle manifest when fetching metadata" do + expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest)) + + installer.prelude_fetch(metadata_only: true) + end + + it "enqueues the bottle without repeating metadata work after a metadata-only run" do + expect(installer.download_queue).to receive(:enqueue).with(an_instance_of(Resource::BottleManifest)).once + + installer.prelude_fetch(metadata_only: true) + + expect(installer.download_queue).to receive(:enqueue) + .with(an_instance_of(Bottle), check_attestation: false, stage: true) + + installer.prelude_fetch + end end it "does not repeat source download prelude work" do @@ -1510,7 +1480,7 @@ RSpec.describe FormulaInstaller do sandbox = instance_double(Sandbox) allow(installer).to receive(:build_argv).and_return([]) - allow(Sandbox).to receive_messages(ensure_sandbox_installed!: nil, available?: true, new: sandbox) + allow(Sandbox).to receive_messages(available?: true, new: sandbox) allow(sandbox).to receive_messages(record_log: nil, allow_read_if_exists: nil, allow_write_temp_and_cache: nil, allow_write_log: nil, allow_cvs: nil, allow_fossil: nil, allow_write_xcode: nil, allow_write_cellar: nil, deny_read_home: nil, diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index be649b7b7c..8e70222ecc 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -56,6 +56,42 @@ RSpec.describe Formulary do end describe "::load_formula" do + it "continues evaluation after ignorable errors with ignore_errors" do + formula_class = described_class.load_formula( + "ignorable-error", + mktmpdir/"ignorable-error.rb", + <<~RUBY, + class IgnorableError < Formula + raise ArgumentError, "should be ignored" + url "https://brew.sh/ignorable-error-1.0.tar.gz" + end + RUBY + "IgnorableErrorNamespace", + flags: [], + ignore_errors: true, + ) + + expect(formula_class.stable.url).to eq("https://brew.sh/ignorable-error-1.0.tar.gz") + end + + it "raises FormulaUnreadableError for errors it cannot resume despite ignore_errors" do + expect do + described_class.load_formula( + "unreadable-error", + mktmpdir/"unreadable-error.rb", + <<~RUBY, + class UnreadableError < Formula + nonexistent_dsl_method "foo" + url "https://brew.sh/unreadable-error-1.0.tar.gz" + end + RUBY + "UnreadableErrorNamespace", + flags: [], + ignore_errors: true, + ) + end.to raise_error(FormulaUnreadableError) + end + it "masks sensitive environment variables while evaluating formulae" do with_env(HOMEBREW_SECRET_TOKEN: "password") do formula_class = described_class.load_formula( @@ -549,6 +585,10 @@ RSpec.describe Formulary do allow(Homebrew::API).to receive_messages(formula_names: [formula_name], formula_aliases: {}, formula_renames: {}) allow(Homebrew::API::Internal).to receive(:formula_hashes) { Homebrew::API::Formula.all_formulae } + allow(Homebrew::API::Internal).to receive(:formula_hash) { |name| Homebrew::API::Formula.all_formulae[name] } + allow(Homebrew::API::Internal).to receive(:formula_name?) do |name| + Homebrew::API::Formula.all_formulae.key?(name) + end allow(Homebrew::API::Internal).to receive(:formula_struct) do |name| Homebrew::API::Formula::FormulaStructGenerator.generate_formula_struct_hash( Homebrew::API::Formula.all_formulae.fetch(name), diff --git a/Library/Homebrew/test/github_runner_matrix_spec.rb b/Library/Homebrew/test/github_runner_matrix_spec.rb index 54efc6cbe3..fe8365e1e7 100644 --- a/Library/Homebrew/test/github_runner_matrix_spec.rb +++ b/Library/Homebrew/test/github_runner_matrix_spec.rb @@ -50,7 +50,7 @@ RSpec.describe GitHubRunnerMatrix, :no_api do ).to be(true) end - it "uses Landlock in unprivileged Linux containers" do + it "uses unprivileged Linux containers" do linux_containers = described_class.new([], ["deleted"], all_supported: false, dependent_matrix: false) .active_runner_specs_hash .filter_map { |runner| runner[:container] } @@ -58,7 +58,7 @@ RSpec.describe GitHubRunnerMatrix, :no_api do expect(linux_containers).to eq(Array.new(2) do { image: "ghcr.io/homebrew/brew:main", - options: "--init --user linuxbrew --env HOMEBREW_SANDBOX_LINUX_LANDLOCK=1", + options: "--init --user linuxbrew", } end) end diff --git a/Library/Homebrew/test/ignorable_spec.rb b/Library/Homebrew/test/ignorable_spec.rb new file mode 100644 index 0000000000..3ab10baa93 --- /dev/null +++ b/Library/Homebrew/test/ignorable_spec.rb @@ -0,0 +1,99 @@ +# typed: false +# frozen_string_literal: true + +require "ignorable" + +RSpec.describe Ignorable do + def raise_runtime_error + raise "raised in block" + end + + describe "::hook_raise" do + it "resumes execution after the raise site when the handler returns :ignore" do + steps = [] + result = described_class.hook_raise(on_ignorable: ->(_e) { :ignore }) do + steps << :before + raise_runtime_error + steps << :after + steps + end + expect(result).to eq([:before, :after]) + end + + it "extends exceptions passed to the handler with ExceptionMixin" do + exception = nil + described_class.hook_raise(on_ignorable: lambda { |e| + exception = e + :ignore + }) { raise_runtime_error } + expect(exception).to be_a(described_class::ExceptionMixin) + end + + it "raises at the raise site when the handler returns :raise" do + result = described_class.hook_raise(on_ignorable: ->(_e) { :raise }) do + raise_runtime_error + rescue RuntimeError + :rescued_in_block + end + expect(result).to eq(:rescued_in_block) + end + + it "propagates unrescued exceptions when the handler returns :raise" do + expect do + described_class.hook_raise(on_ignorable: ->(_e) { :raise }) { raise_runtime_error } + end.to raise_error(RuntimeError, "raised in block") + end + + it "preserves the exception's backtrace when the handler returns :raise" do + yielded_backtrace = nil + exception = nil + begin + described_class.hook_raise(on_ignorable: lambda { |e| + yielded_backtrace = e.backtrace.dup + :raise + }) { raise_runtime_error } + rescue RuntimeError => e + exception = e + end + expect(exception.backtrace).to eq(yielded_backtrace) + end + + it "runs the block's ensure blocks when the handler raises" do + ensured = false + expect do + described_class.hook_raise(on_ignorable: ->(e) { raise e }) do + raise_runtime_error + ensure + ensured = true + end + end.to raise_error(RuntimeError, "raised in block") + expect(ensured).to be(true) + end + + it "does not consult the handler for exceptions not raised from Ruby code" do + expect do + described_class.hook_raise(on_ignorable: ->(_e) { :ignore }) { Integer("nope") } + end.to raise_error(ArgumentError) + end + + it "does not consult the handler for ScriptError" do + expect do + described_class.hook_raise(on_ignorable: ->(_e) { :ignore }) { raise NotImplementedError } + end.to raise_error(NotImplementedError) + end + + it "restores the original raise afterwards" do + described_class.hook_raise(on_ignorable: ->(_e) { :raise }) { :noop } + expect(Object.instance_method(:raise).owner).to eq(Kernel) + end + + it "restores the original raise when an exception propagates" do + begin + described_class.hook_raise(on_ignorable: ->(_e) { :raise }) { raise_runtime_error } + rescue RuntimeError + nil + end + expect(Object.instance_method(:fail).owner).to eq(Kernel) + end + end +end diff --git a/Library/Homebrew/test/install_spec.rb b/Library/Homebrew/test/install_spec.rb index 4b090f2835..1d09f842f7 100644 --- a/Library/Homebrew/test/install_spec.rb +++ b/Library/Homebrew/test/install_spec.rb @@ -1,6 +1,7 @@ # typed: strict # frozen_string_literal: true +require "cask/installer" require "install" require "dependency" require "test/support/fixtures/testball" @@ -24,6 +25,49 @@ RSpec.describe Homebrew::Install do described_class.perform_preinstall_checks end + describe "::fetch_formulae" do + it "skips formulae whose fetch steps raise and continues with the rest" do + good_fi = FormulaInstaller.new(formula("good-bottle") do + T.bind(self, T.class_of(Formula)) + url "foo-1.0" + end) + bad_fi = FormulaInstaller.new(formula("bad-bottle") do + T.bind(self, T.class_of(Formula)) + url "foo-1.0" + end) + [good_fi, bad_fi].each do |fi| + allow(fi).to receive(:prelude_fetch) + allow(fi).to receive(:prelude) + end + allow(good_fi).to receive(:enqueue_fetch) + allow(bad_fi).to receive(:enqueue_fetch).and_raise("unexpected failure") + + expect do + expect(described_class.fetch_formulae([good_fi, bad_fi])).to eq([good_fi]) + end.to output(/Error: bad-bottle: unexpected failure/).to_stderr + end + end + + describe "::enqueue_cask_installers" do + it "skips casks whose enqueue raises and continues with the rest" do + bad_cask = instance_double(Cask::Cask, to_s: "bad-cask") + bad_installer = instance_double(Cask::Installer, cask: bad_cask, + source_download_requires_pre_fetch?: false) + allow(bad_installer).to receive(:enqueue_downloads) + .and_raise(URI::InvalidURIError, 'bad URI (is not URI?): "https://example.com/bad -cask.dmg"') + good_installer = instance_double(Cask::Installer, source_download_requires_pre_fetch?: false) + expect(good_installer).to receive(:enqueue_downloads) + + download_queue = Homebrew::DownloadQueue.new(pour: true) + begin + expect { described_class.enqueue_cask_installers([bad_installer, good_installer], download_queue:) } + .to output(/Error: bad-cask: bad URI/).to_stderr + ensure + download_queue.shutdown + end + end + end + describe "::print_dry_run_dependencies" do it "splits fresh installs and upgrades under separate headers" do fresh = formula("fresh-dep") do diff --git a/Library/Homebrew/test/install_steps_spec.rb b/Library/Homebrew/test/install_steps_spec.rb index 668e81fc11..6fe508ddbd 100644 --- a/Library/Homebrew/test/install_steps_spec.rb +++ b/Library/Homebrew/test/install_steps_spec.rb @@ -311,6 +311,22 @@ RSpec.describe Homebrew::InstallSteps do expect((root/"var/remove.txt").read).to be_empty end + specify "omits install step runtime defaults" do + steps = Homebrew::InstallSteps::DSL.build(default_base: :staged_path, + default_source_base: :staged_path, + default_target_base: :staged_path) do + copy "source", "target" + symlink "source", "target" + write "config", "content" + set_ownership "Example.app" + terminate_process "Example" + end + + expect(steps).to all(satisfy do |step| + (step.keys & %w[attempts force group match overwrite uninstall]).empty? + end) + end + specify "writes a default config file and preserves existing ones", :aggregate_failures do steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do write "config/new.conf", "fresh" diff --git a/Library/Homebrew/test/installed_dependents_spec.rb b/Library/Homebrew/test/installed_dependents_spec.rb index 8a8ebc62a4..d1c6881789 100644 --- a/Library/Homebrew/test/installed_dependents_spec.rb +++ b/Library/Homebrew/test/installed_dependents_spec.rb @@ -44,11 +44,11 @@ RSpec.describe InstalledDependents do def setup_test_keg(name, version, &block) keg = super tab = Tab.new( - "homebrew_version" => HOMEBREW_VERSION, - "installed_on_request" => false, - "loaded_from_api" => false, - "loaded_from_internal_api" => false, - "source" => { + homebrew_version: HOMEBREW_VERSION, + installed_on_request: false, + loaded_from_api: false, + loaded_from_internal_api: false, + source: { "path" => nil, "tap" => "homebrew/core", "tap_git_head" => nil, @@ -60,7 +60,7 @@ RSpec.describe InstalledDependents do "compatibility_version" => nil, }, }, - "built_on" => {}, + built_on: {}, ) tab.tabfile = keg/AbstractTab::FILENAME tab.stdlib = :libcxx diff --git a/Library/Homebrew/test/os/linux/dependency_collector_spec.rb b/Library/Homebrew/test/os/linux/dependency_collector_spec.rb index 15457af1f6..c29d9ee5aa 100644 --- a/Library/Homebrew/test/os/linux/dependency_collector_spec.rb +++ b/Library/Homebrew/test/os/linux/dependency_collector_spec.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "dependency_collector" -require "sandbox" RSpec.describe DependencyCollector do subject(:collector) { described_class.new } @@ -53,100 +52,13 @@ RSpec.describe DependencyCollector do end end - describe "#bubblewrap_dep_if_needed" do - let(:formulae) do - Hash.new { |hash, name| hash[name] = instance_double(Formula, deps: []) } - end - - around do |example| - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil, HOMEBREW_TESTS: nil) { example.run } - end - - before do - allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true) - allow(DevelopmentTools).to receive(:needs_build_formulae?).and_return(false) - allow(Sandbox).to receive(:executable) - allow(OS).to receive(:not_tier_one_configuration?).and_return(false) - allow(Formula).to receive(:[]) { |name| formulae[name] } - global_dep_tree.clear - end - - after do - global_dep_tree.clear - end - - def global_dep_tree - OS::Linux::DependencyCollector.module_eval { class_variable_get(:@@global_dep_tree) } - end - - it "returns a Bubblewrap implicit dependency when the Linux sandbox needs one" do - expect(collector.bubblewrap_dep_if_needed(Set.new)).to eq(Dependency.new("bubblewrap", [:implicit])) - end - - it "returns nil when Bubblewrap is already available" do - allow(Sandbox).to receive(:executable).and_return(Pathname("/usr/bin/bwrap")) - - expect(collector.bubblewrap_dep_if_needed(Set.new)).to be_nil - end - - it "returns nil when using Landlock" do - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do - expect(collector.bubblewrap_dep_if_needed(Set.new)).to be_nil - end - end - - it "returns nil for Bubblewrap and its dependencies" do - collector.global_dep_tree["bubblewrap"] = Set["libcap"] - - expect(collector.bubblewrap_dep_if_needed(Set["bubblewrap"])).to be_nil - expect(collector.bubblewrap_dep_if_needed(Set["libcap"])).to be_nil - end - - it "returns nil when Bubblewrap is already in the dependency tree" do - expect(collector.bubblewrap_dep_if_needed(Set["bubblewrap"])).to be_nil - end - - it "returns nil when a Bubblewrap runtime dependency is already in the dependency tree" do - formulae["bubblewrap"] = instance_double(Formula, deps: [Dependency.new("libcap")]) - - expect(collector.bubblewrap_dep_if_needed(Set["libcap"])).to be_nil - end - - it "ignores Bubblewrap build dependencies when build formulae are not needed" do - formulae["bubblewrap"] = instance_double(Formula, deps: [ - Dependency.new("libcap"), - Dependency.new("pkgconf", [:build]), - ]) - - expect(collector.bubblewrap_dep_if_needed(Set["pkgconf"])).to eq(Dependency.new("bubblewrap", [:implicit])) - end - - it "includes Bubblewrap build dependencies when build formulae are needed" do - allow(DevelopmentTools).to receive(:needs_build_formulae?).and_return(true) - formulae["bubblewrap"] = instance_double(Formula, deps: [ - Dependency.new("pkgconf", [:build]), - ]) - formulae["glibc"] - formulae[OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA] - - expect(collector.bubblewrap_dep_if_needed(Set["pkgconf"])).to be_nil - end - end - describe "#implicit_dependency_names" do let(:formulae) do Hash.new { |hash, name| hash[name] = instance_double(Formula, deps: []) } end - around do |example| - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil, HOMEBREW_TESTS: nil) { example.run } - end - before do - allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true) allow(DevelopmentTools).to receive_messages(needs_build_formulae?: false, needs_libc_formula?: false) - allow(Sandbox).to receive(:executable).and_return(nil) - allow(OS).to receive(:not_tier_one_configuration?).and_return(false) allow(Formula).to receive(:[]) { |name| formulae[name] } global_dep_tree.clear end @@ -159,13 +71,7 @@ RSpec.describe DependencyCollector do OS::Linux::DependencyCollector.module_eval { class_variable_get(:@@global_dep_tree) } end - it "includes bubblewrap when the sandbox needs it" do - expect(collector.implicit_dependency_names).to eq(Set["bubblewrap"]) - end - - it "is empty when bubblewrap is already available and build formulae aren't needed" do - allow(Sandbox).to receive(:executable).and_return(Pathname("/usr/bin/bwrap")) - + it "is empty when build formulae and a libc formula aren't needed" do expect(collector.implicit_dependency_names).to eq(Set.new) end diff --git a/Library/Homebrew/test/os/linux/diagnostic_spec.rb b/Library/Homebrew/test/os/linux/diagnostic_spec.rb index ce50003887..9f2b54440e 100644 --- a/Library/Homebrew/test/os/linux/diagnostic_spec.rb +++ b/Library/Homebrew/test/os/linux/diagnostic_spec.rb @@ -7,10 +7,6 @@ require "sandbox" RSpec.describe Homebrew::Diagnostic::Checks do subject(:checks) { described_class.new } - around do |example| - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil) { example.run } - end - before do allow(OS::Linux).to receive(:inside_docker?).and_return(false) end @@ -92,101 +88,53 @@ RSpec.describe Homebrew::Diagnostic::Checks do end end - specify "#check_linux_sandbox describes missing Bubblewrap" do - allow(Sandbox).to receive_messages( - state: :missing, - failure_reason: "Bubblewrap is required to use the Linux sandbox but was not found.", - sandbox_install_command: "sudo apt-get install bubblewrap", - ) - - with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox&.to_s - - expect(message) - .to include( - "Bubblewrap is required to use the Linux sandbox but was not found.", - "Install Bubblewrap and ensure a rootless `bwrap` executable is available on `PATH`.", - "On this system, install it with:", - " sudo apt-get install bubblewrap", - "export HOMEBREW_NO_SANDBOX_LINUX=1", - ) - expect(message).not_to include("sysctl") - expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1") - end - end - - specify "#check_linux_sandbox describes setuid Bubblewrap" do - allow(Sandbox).to receive_messages( - state: :setuid, - failure_reason: "All found `bwrap` executables are setuid.", - ) - - with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox&.to_s - - expect(message) - .to include( - "All found `bwrap` executables are setuid.", - "Homebrew's Linux sandbox requires a rootless `bwrap` executable.", - "Install a non-setuid Bubblewrap or put it earlier on `PATH`.", - "export HOMEBREW_NO_SANDBOX_LINUX=1", - ) - expect(message).not_to include("sysctl") - expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1") - end - end - - specify "#check_linux_sandbox describes Bubblewrap configuration" do - allow(Sandbox).to receive_messages( - state: :unavailable, - failure_reason: "Bubblewrap is installed but cannot create a rootless sandbox.", - ) - - with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox&.to_s - - expect(message) - .to include( - "Bubblewrap is installed but cannot create a rootless sandbox.", - "Homebrew's Linux sandbox requires rootless Bubblewrap and unprivileged", - "sudo sysctl -w kernel.unprivileged_userns_clone=1", - "Allows unprivileged processes to create user namespaces.", - "sudo sysctl -w user.max_user_namespaces=28633", - "Allows each user to allocate enough user namespaces.", - "sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true", - "Allows unprivileged user namespaces on AppArmor-enabled systems", - "export HOMEBREW_NO_SANDBOX_LINUX=1", - ) - expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1") - end - end - - specify "#check_linux_sandbox describes unavailable Landlock" do - allow(OS::Linux).to receive(:inside_docker?).and_return(true) + specify "#check_linux_sandbox describes unsupported Landlock" do allow(Sandbox).to receive_messages( state: :unsupported, failure_reason: "Landlock is not supported by this Linux kernel.", ) - with_env(GITHUB_ACTIONS: "true", HOMEBREW_NO_SANDBOX_LINUX: nil, HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do + with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do message = checks.check_linux_sandbox&.to_s - expect(message).to include("Landlock is not supported by this Linux kernel.") - expect(message).not_to include("Bubblewrap", "--privileged") + expect(message) + .to include( + "Landlock is not supported by this Linux kernel.", + "Homebrew's Linux sandbox requires a kernel with Landlock enabled.", + "export HOMEBREW_NO_SANDBOX_LINUX=1", + ) + expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1") end end - specify "#check_linux_sandbox suggests privileged GitHub Actions containers" do + specify "#check_linux_sandbox describes missing Fiddle" do + allow(Sandbox).to receive_messages( + state: :missing_fiddle, + failure_reason: "Landlock requires Ruby's bundled Fiddle library.", + ) + + with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do + message = checks.check_linux_sandbox&.to_s + + expect(message) + .to include( + "Landlock requires Ruby's bundled Fiddle library.", + "Run Homebrew with its vendored Ruby, which includes Fiddle.", + "export HOMEBREW_NO_SANDBOX_LINUX=1", + ) + expect(message).not_to include("kernel with Landlock") + end + end + + specify "#check_linux_sandbox describes unavailable Landlock inside Docker on GitHub Actions" do allow(OS::Linux).to receive(:inside_docker?).and_return(true) allow(Sandbox).to receive_messages( - state: :unavailable, - failure_reason: "Bubblewrap is installed but cannot create a rootless sandbox.", + state: :disabled, + failure_reason: "Landlock is disabled by this Linux kernel.", ) with_env(GITHUB_ACTIONS: "true", HOMEBREW_NO_SANDBOX_LINUX: nil) do - expect(checks.check_linux_sandbox&.to_s).to include( - "If this is a GitHub Actions container, add `options: --privileged` to the job's `container` configuration.", - ) + expect(checks.check_linux_sandbox&.to_s).to include("Landlock is disabled by this Linux kernel.") end end diff --git a/Library/Homebrew/test/os/mac/diagnostic_spec.rb b/Library/Homebrew/test/os/mac/diagnostic_spec.rb index fac1f43b55..ed5392010c 100644 --- a/Library/Homebrew/test/os/mac/diagnostic_spec.rb +++ b/Library/Homebrew/test/os/mac/diagnostic_spec.rb @@ -68,6 +68,12 @@ RSpec.describe Homebrew::Diagnostic::Checks do expect(checks.fatal_preinstall_checks).not_to include("check_for_installed_developer_tools") end + + it "requires developer tools on Intel" do + allow(Hardware::CPU).to receive(:arm?).and_return(false) + + expect(checks.fatal_preinstall_checks).to include("check_for_installed_developer_tools") + end end describe "#fatal_build_from_source_checks" do diff --git a/Library/Homebrew/test/os/mac/keg_spec.rb b/Library/Homebrew/test/os/mac/keg_spec.rb index eb31b00808..128b2e7b8e 100644 --- a/Library/Homebrew/test/os/mac/keg_spec.rb +++ b/Library/Homebrew/test/os/mac/keg_spec.rb @@ -43,12 +43,38 @@ RSpec.describe Keg do allow(MacOS).to receive(:version).and_return(MacOSVersion.new("11")) end - it "signs patched binaries using ruby-macho" do + it "signs patched binaries using ruby-macho on Apple Silicon" do + allow(Hardware::CPU).to receive(:arm?).and_return(true) expect(keg).not_to receive(:system_command).with("codesign", any_args) expect(keg).not_to receive(:quiet_system).with("codesign", any_args) expect(MachO).to receive(:codesign!).with(file) keg.codesign_patched_binary(file) end + + it "re-signs binaries whose signature has been broken using codesign on Intel" do + allow(Hardware::CPU).to receive(:arm?).and_return(false) + expect(MachO).not_to receive(:codesign!) + expect(keg).to receive(:system_command) + .with("codesign", args: ["--verify", file], print_stderr: false) + .and_return(instance_double(SystemCommand::Result, stderr: "#{file}: invalid signature")) + expect(keg).to receive(:quiet_system) + .with("codesign", "--sign", "-", "--force", + "--preserve-metadata=entitlements,requirements,flags,runtime", file) + .and_return(true) + + keg.codesign_patched_binary(file) + end + + it "does not sign unsigned binaries on Intel" do + allow(Hardware::CPU).to receive(:arm?).and_return(false) + expect(MachO).not_to receive(:codesign!) + expect(keg).to receive(:system_command) + .with("codesign", args: ["--verify", file], print_stderr: false) + .and_return(instance_double(SystemCommand::Result, stderr: "#{file}: code object is not signed at all")) + expect(keg).not_to receive(:quiet_system).with("codesign", any_args) + + keg.codesign_patched_binary(file) + end end end diff --git a/Library/Homebrew/test/os/mac/xcode_spec.rb b/Library/Homebrew/test/os/mac/xcode_spec.rb index bed0ce1ce7..a9b9122d27 100644 --- a/Library/Homebrew/test/os/mac/xcode_spec.rb +++ b/Library/Homebrew/test/os/mac/xcode_spec.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "os/mac/xcode" diff --git a/Library/Homebrew/test/rubocops/cask/stanza_grouping_spec.rb b/Library/Homebrew/test/rubocops/cask/stanza_grouping_spec.rb index fa0da9c33e..2cc0bdcaa0 100644 --- a/Library/Homebrew/test/rubocops/cask/stanza_grouping_spec.rb +++ b/Library/Homebrew/test/rubocops/cask/stanza_grouping_spec.rb @@ -21,6 +21,17 @@ RSpec.describe RuboCop::Cop::Cask::StanzaGrouping, :config do CASK end + it "groups completion generation with artifacts" do + expect_no_offenses <<~CASK + cask 'foo' do + binary 'foo' + generate_completions_from_executable 'foo', 'completions' + + zap trash: '~/.foo' + end + CASK + end + it "requires a group boundary after completion generation" do expect_offense <<~CASK cask 'foo' do diff --git a/Library/Homebrew/test/rubocops/cask/stanza_order_spec.rb b/Library/Homebrew/test/rubocops/cask/stanza_order_spec.rb index 43f86285b0..f4677125c8 100644 --- a/Library/Homebrew/test/rubocops/cask/stanza_order_spec.rb +++ b/Library/Homebrew/test/rubocops/cask/stanza_order_spec.rb @@ -11,6 +11,43 @@ RSpec.describe RuboCop::Cop::Cask::StanzaOrder, :config do ]) end + it "registers every new top-level cask DSL" do + expect(RuboCop::Cask::Constants::STANZA_ORDER).to include( + :on_macos, + :on_linux, + :on_system_conditional, + :appimage, + :generated_script, + :command_wrapper, + :generate_completions_from_executable, + :preflight_steps, + :postflight_steps, + :uninstall_preflight_steps, + :uninstall_postflight_steps, + ) + end + + it "orders system conditionals before version and URL stanzas" do + expect_offense <<~CASK + cask 'foo' do + version :latest + ^^^^^^^^^^^^^^^ `version` stanza out of order + url 'https://foo.brew.sh/foo.zip' + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `url` stanza out of order + artifact = on_system_conditional macos: 'foo.dmg', linux: 'foo.AppImage' + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `on_system_conditional` stanza out of order + end + CASK + + expect_correction <<~CASK + cask 'foo' do + artifact = on_system_conditional macos: 'foo.dmg', linux: 'foo.AppImage' + version :latest + url 'https://foo.brew.sh/foo.zip' + end + CASK + end + it "accepts a sole stanza" do expect_no_offenses <<~CASK cask 'foo' do diff --git a/Library/Homebrew/test/rubocops/helper_functions_spec.rb b/Library/Homebrew/test/rubocops/helper_functions_spec.rb index 1152b260d5..b8feae7982 100644 --- a/Library/Homebrew/test/rubocops/helper_functions_spec.rb +++ b/Library/Homebrew/test/rubocops/helper_functions_spec.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "rubocops/shared/helper_functions" diff --git a/Library/Homebrew/test/rubocops/no_base64_spec.rb b/Library/Homebrew/test/rubocops/no_base64_spec.rb new file mode 100644 index 0000000000..8229d4b56f --- /dev/null +++ b/Library/Homebrew/test/rubocops/no_base64_spec.rb @@ -0,0 +1,132 @@ +# typed: strict +# frozen_string_literal: true + +require "rubocops/no_base64" + +RSpec.describe RuboCop::Cop::Homebrew::NoBase64, :config do + it "registers an offense and removes `require \"base64\"`" do + expect_offense(<<~RUBY) + require "base64" + ^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + require "json" + RUBY + + expect_correction(<<~RUBY) + require "json" + RUBY + end + + it "registers an offense and removes `Kernel.require \"base64\"`" do + expect_offense(<<~RUBY) + Kernel.require "base64" + ^^^^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + require "json" + RUBY + + expect_correction(<<~RUBY) + require "json" + RUBY + end + + it "registers an offense and corrects `Base64.decode64`" do + expect_offense(<<~RUBY) + Base64.decode64(foo) + ^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_correction(<<~RUBY) + foo.unpack1("m") + RUBY + end + + it "registers an offense and corrects `Base64.strict_decode64`" do + expect_offense(<<~RUBY) + Base64.strict_decode64("aGVsbG8=") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_correction(<<~RUBY) + "aGVsbG8=".unpack1("m0") + RUBY + end + + it "registers an offense and corrects `Base64.encode64`" do + expect_offense(<<~RUBY) + Base64.encode64(file.read) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_correction(<<~RUBY) + [file.read].pack("m") + RUBY + end + + it "registers an offense and corrects `Base64.strict_encode64`" do + expect_offense(<<~RUBY) + Base64.strict_encode64("hello" * count) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_correction(<<~RUBY) + ["hello" * count].pack("m0") + RUBY + end + + it "registers an offense and corrects `::Base64` calls" do + expect_offense(<<~RUBY) + ::Base64.decode64(foo) + ^^^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_correction(<<~RUBY) + foo.unpack1("m") + RUBY + end + + it "registers an offense without correction for other `Base64` methods" do + expect_offense(<<~RUBY) + Base64.urlsafe_decode64(foo) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_no_corrections + end + + it "registers an offense without correction for a decode of a compound expression" do + expect_offense(<<~RUBY) + Base64.decode64(foo + bar) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_no_corrections + end + + it "registers an offense without correction for a bare `Base64` reference" do + expect_offense(<<~RUBY) + encoder = Base64 + ^^^^^^ Homebrew no longer includes the `base64` gem; use `String#unpack1` or `Array#pack` instead. + RUBY + + expect_no_corrections + end + + it "does not register an offense for a formula class named `Base64`" do + expect_no_offenses(<<~RUBY) + class Base64 < Formula + desc "Encode and decode base64 files" + end + RUBY + end + + it "does not register an offense for namespaced `Base64` constants" do + expect_no_offenses(<<~RUBY) + Foo::Base64.decode64(foo) + RUBY + end + + it "does not register an offense for other requires" do + expect_no_offenses(<<~RUBY) + require "json" + RUBY + end +end diff --git a/Library/Homebrew/test/sandbox_landlock_spec.rb b/Library/Homebrew/test/sandbox_landlock_spec.rb index 304b2952c3..c6d05ff219 100644 --- a/Library/Homebrew/test/sandbox_landlock_spec.rb +++ b/Library/Homebrew/test/sandbox_landlock_spec.rb @@ -77,13 +77,6 @@ RSpec.describe Sandbox::Landlock do expect(described_class.abi_version).to eq(1) expect(described_class.failure_reason).to eq("Landlock ABI 2 or later is required; found ABI 1.") end - - it "only raises when explicitly configuring unavailable Landlock" do - allow(described_class).to receive_messages(available?: false, failure_reason: "Landlock is not available.") - - expect { described_class.ensure_installed! }.not_to raise_error - expect { described_class.configure! }.to raise_error(RuntimeError, "Landlock is not available.") - end end describe "::kernel_abi_version" do diff --git a/Library/Homebrew/test/sandbox_linux_spec.rb b/Library/Homebrew/test/sandbox_linux_spec.rb index 91bba304df..4feaa9bd1b 100644 --- a/Library/Homebrew/test/sandbox_linux_spec.rb +++ b/Library/Homebrew/test/sandbox_linux_spec.rb @@ -7,455 +7,7 @@ require "extend/os/linux/sandbox" if OS.linux? RSpec.describe Sandbox, :needs_linux do subject(:sandbox) { described_class.new } - describe "::sandbox_implementation" do - it "uses Bubblewrap by default" do - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil) do - expect(OS::Linux::Sandbox.sandbox_implementation).to eq(Sandbox::Bubblewrap) - end - end - - it "uses Landlock when requested" do - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do - expect(OS::Linux::Sandbox.sandbox_implementation).to eq(Sandbox::Landlock) - end - end - end - - describe "::bubblewrap_executable" do - let(:sandbox_class) do - Class.new(Sandbox::Bubblewrap) do - class << self - attr_accessor :test_executable_candidate_paths - - def executable_candidate_paths = test_executable_candidate_paths - end - end - end - let(:setuid_dir) { mktmpdir } - let(:usable_dir) { mktmpdir } - let(:setuid_bubblewrap) { setuid_dir/"bwrap" } - let(:usable_bubblewrap) { usable_dir/"bwrap" } - - before do - FileUtils.touch setuid_bubblewrap - FileUtils.chmod "+x", setuid_bubblewrap - FileUtils.touch usable_bubblewrap - FileUtils.chmod "+x", usable_bubblewrap - sandbox_class.test_executable_candidate_paths = PATH.new(setuid_dir, usable_dir) - allow(File).to receive(:stat).and_call_original - allow(File).to receive(:stat).with(setuid_bubblewrap).and_return(instance_double(File::Stat, setuid?: true)) - end - - it "searches Homebrew Bubblewrap before system Bubblewrap and skips setuid candidates" do - expect(Sandbox::Bubblewrap.executable_candidate_paths.to_a).to start_with("#{HOMEBREW_PREFIX}/bin", "/usr/bin", - "/bin") - expect(sandbox_class.executable).to eq(usable_bubblewrap) - end - - it "raises when no suitable bubblewrap candidate exists" do - sandbox_class.test_executable_candidate_paths = PATH.new(mktmpdir) - - expect { sandbox_class.executable! } - .to raise_error(RuntimeError, "Bubblewrap is required to use the Linux sandbox.") - end - end - - describe "::available?" do - let(:sandbox_class) do - Class.new(Sandbox::Bubblewrap) do - class << self - attr_accessor :test_executable_candidate_paths - - def executable_candidate_paths = test_executable_candidate_paths - end - end - end - let(:bubblewrap_dir) { mktmpdir } - let(:bubblewrap) { bubblewrap_dir/"bwrap" } - let(:fallback_bubblewrap_dir) { mktmpdir } - let(:fallback_bubblewrap) { fallback_bubblewrap_dir/"bwrap" } - let(:successful_result) { instance_double(SystemCommand::Result, success?: true) } - let(:failed_result) { instance_double(SystemCommand::Result, success?: false, merged_output: "") } - let(:bubblewrap_probe_args) do - [ - "--unshare-user", - "--unshare-ipc", - "--unshare-pid", - "--unshare-uts", - "--unshare-cgroup-try", - "--ro-bind", "/", "/", - "--proc", "/proc", - "--dev", "/dev", - "true" - ] - end - let(:bubblewrap_test_args) do - [ - bubblewrap.to_s, - *bubblewrap_probe_args, - { err: :out }, - ] - end - - before do - allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true) - FileUtils.touch bubblewrap - FileUtils.chmod "+x", bubblewrap - sandbox_class.test_executable_candidate_paths = PATH.new(bubblewrap_dir) - end - - it "returns false when Linux sandboxing is disabled" do - allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(false) - - expect(sandbox_class.available?).to be(false) - expect(sandbox_class.state).to eq(:config_disabled) - end - - it "returns false when bubblewrap is unavailable" do - sandbox_class.test_executable_candidate_paths = PATH.new(mktmpdir) - - expect(sandbox_class.available?).to be(false) - expect(sandbox_class.state).to eq(:missing) - end - - it "probes unprivileged namespace support once" do - expect(sandbox_class).to receive(:system_command).once.with( - bubblewrap, - args: bubblewrap_probe_args, - print_stderr: false, - ).and_return(successful_result) - - expect(sandbox_class.available?).to be(true) - expect(sandbox_class.state).to eq(:available) - expect(sandbox_class.failure_reason).to be_nil - end - - it "probes later usable Bubblewrap candidates if earlier candidates fail" do - FileUtils.touch fallback_bubblewrap - FileUtils.chmod "+x", fallback_bubblewrap - sandbox_class.test_executable_candidate_paths = PATH.new(bubblewrap_dir, fallback_bubblewrap_dir) - - expect(sandbox_class).to receive(:system_command).with( - bubblewrap, - args: bubblewrap_probe_args, - print_stderr: false, - ).and_return(failed_result) - expect(sandbox_class).to receive(:system_command).with( - fallback_bubblewrap, - args: bubblewrap_probe_args, - print_stderr: false, - ).and_return(successful_result) - - expect(sandbox_class.available?).to be(true) - end - - it "reports setuid bubblewrap candidates" do - allow(File).to receive(:stat).and_call_original - allow(File).to receive(:stat).with(bubblewrap).and_return(instance_double(File::Stat, setuid?: true)) - - expect(sandbox_class.available?).to be(false) - expect(sandbox_class.state).to eq(:setuid) - expect(sandbox_class.failure_reason).to include("setuid") - end - - it "reports bubblewrap sandbox probe failures" do - allow(sandbox_class).to receive(:system_command).and_return(failed_result) - - expect(sandbox_class.available?).to be(false) - expect(sandbox_class.state).to eq(:unavailable) - expect(sandbox_class.failure_reason).to include("cannot create a rootless sandbox") - end - - it "prints bubblewrap sandbox probe failure output" do - expect(sandbox_class).to receive(:system_command) - .and_return(instance_double(SystemCommand::Result, success?: false, - merged_output: "bwrap stdout\nbwrap stderr\n")) - expect(sandbox_class).to receive(:opoo).with("bubblewrap test probe failed") - - expect { sandbox_class.available? } - .to output("bwrap stdout\nbwrap stderr\n").to_stderr - end - - it "does not treat generic bubblewrap sandbox probe failures as nested" do - FileUtils.touch fallback_bubblewrap - FileUtils.chmod "+x", fallback_bubblewrap - sandbox_class.test_executable_candidate_paths = PATH.new(bubblewrap_dir, fallback_bubblewrap_dir) - - expect(Utils).to receive(:popen_read) - .with(*bubblewrap_test_args) - .and_return("bwrap: No permissions to create a new namespace\n") - - expect(sandbox_class.nested_sandbox?).to be(false) - end - - it "treats a bubblewrap namespace nesting failure as nested" do - expect(Utils).to receive(:popen_read) - .with(*bubblewrap_test_args) - .and_return("bwrap: Creating new namespace failed: " \ - "nesting depth or /proc/sys/user/max_*_namespaces exceeded (ENOSPC)\n") - - expect(sandbox_class.nested_sandbox?).to be(true) - end - end - - describe "::configuration_commands" do - let(:sandbox_class) { Class.new(Sandbox::Bubblewrap) } - - around do |example| - with_env(GITHUB_ACTIONS: nil, HOMEBREW_GITHUB_HOSTED_RUNNER: nil) { example.run } - end - - it "lists Linux sandbox sysctl commands" do - expect(sandbox_class.configuration_commands).to eq([ - "sudo sysctl -w kernel.unprivileged_userns_clone=1", - "sudo sysctl -w user.max_user_namespaces=28633", - "sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true", - ]) - end - - it "uses system Bubblewrap when configuring Linux sandbox sysctls" do - allow(sandbox_class).to receive(:executable).and_return(Pathname("/usr/bin/bwrap")) - allow(Process).to receive(:euid).and_return(1000) - expect(sandbox_class).not_to receive(:ensure_installed!) - expect(sandbox_class).to receive(:ohai).with("Configuring Bubblewrap...").ordered - expect(sandbox_class).to receive(:system) - .with("sudo", HOMEBREW_BREW_FILE.to_s, "setup-sandbox").and_return(true).ordered - - sandbox_class.configure! - end - - it "does not configure Linux sandbox sysctls when Bubblewrap remains unavailable" do - expect(sandbox_class).to receive(:executable).twice.and_return(nil) - expect(sandbox_class).to receive(:ensure_installed!) - .with(install_from_tests: true) - expect(sandbox_class).not_to receive(:system) - - sandbox_class.configure! - end - - it "installs Bubblewrap and configures Linux sandbox sysctls as root" do - expect(sandbox_class).to receive(:executable) - .twice - .and_return(nil, Pathname(HOMEBREW_PREFIX/"bin/bwrap")) - allow(Process).to receive(:euid).and_return(0) - expect(sandbox_class).to receive(:ensure_installed!) - .with(install_from_tests: true) - expect(sandbox_class).to receive(:ohai).with("Configuring Bubblewrap...").ordered - expect(sandbox_class).to receive(:system) - .with(HOMEBREW_BREW_FILE.to_s, "setup-sandbox").and_return(true).ordered - - sandbox_class.configure! - end - - it "raises when configuring Linux sandbox sysctls fails" do - allow(sandbox_class).to receive(:executable).and_return(Pathname("/usr/bin/bwrap")) - allow(Process).to receive(:euid).and_return(0) - allow(sandbox_class).to receive(:ohai) - expect(sandbox_class).to receive(:system) - .with(HOMEBREW_BREW_FILE.to_s, "setup-sandbox").and_return(false) - - expect { sandbox_class.configure! }.to raise_error(ErrorDuringExecution) - end - end - - describe "::sandbox_install_command" do - let(:sandbox_class) { Class.new(Sandbox::Bubblewrap) } - - it "returns the distro-specific install command for the detected package manager" do - allow(sandbox_class).to receive(:which).with("apt-get").and_return(nil) - allow(sandbox_class).to receive(:which).with("dnf").and_return(Pathname("/usr/bin/dnf")) - expect(sandbox_class.install_command).to eq("sudo dnf install bubblewrap") - end - - it "returns nil when no known package manager is found" do - allow(sandbox_class).to receive(:which).and_return(nil) - expect(sandbox_class.install_command).to be_nil - end - end - - describe "::ensure_sandbox_installed!" do - let(:sandbox_class) { Class.new(Sandbox::Bubblewrap) } - - around do |example| - with_env(GITHUB_ACTIONS: nil, HOMEBREW_GITHUB_HOSTED_RUNNER: nil, - HOMEBREW_INSTALLING_BUBBLEWRAP: nil, HOMEBREW_TESTS: nil) { example.run } - end - - before do - allow(Homebrew::EnvConfig).to receive(:sandbox_linux?).and_return(true) - end - - it "does nothing when Homebrew Bubblewrap is already available" do - expect(sandbox_class).to receive(:executable) - .once - .and_return(Pathname(HOMEBREW_PREFIX/"bin/bwrap")) - expect(Formula).not_to receive(:[]) - expect(sandbox_class).not_to receive(:which) - expect(sandbox_class).not_to receive(:system) - - sandbox_class.ensure_installed! - end - - it "does nothing when system Bubblewrap is already available" do - expect(sandbox_class).to receive(:executable) - .once - .and_return(Pathname("/usr/bin/bwrap")) - expect(Formula).not_to receive(:[]) - expect(sandbox_class).not_to receive(:which) - expect(sandbox_class).not_to receive(:system) - - sandbox_class.ensure_installed! - end - - it "installs Bubblewrap with Homebrew before trying apt-get on GitHub Actions" do - expect(sandbox_class).to receive(:executable) - .twice - .and_return(nil, Pathname(HOMEBREW_PREFIX/"bin/bwrap")) - expect(Formula).to receive(:[]).with("bubblewrap") - .and_return(instance_double(Formula, ensure_installed!: nil)) - expect(sandbox_class).not_to receive(:which) - expect(sandbox_class).not_to receive(:system) - - with_env(GITHUB_ACTIONS: "true", HOMEBREW_GITHUB_HOSTED_RUNNER: "1") do - sandbox_class.ensure_installed! - end - end - - it "falls back to sudo apt-get on GitHub Actions Ubuntu when Homebrew Bubblewrap is unavailable" do - expect(sandbox_class).to receive(:executable) - .twice - .and_return(nil) - expect(Formula).to receive(:[]).with("bubblewrap") - .and_return(instance_double(Formula, ensure_installed!: nil)) - expect(sandbox_class).to receive(:which).with("apt-get").and_return(Pathname("/usr/bin/apt-get")) - expect(Process).to receive(:euid).and_return(1000) - expect(sandbox_class).to receive(:ohai).with("Installing Bubblewrap...") - expect(sandbox_class).to receive(:system) - .with("sudo", "apt-get", "install", "--yes", "bubblewrap") - .and_return(true) - - with_env(GITHUB_ACTIONS: "true", HOMEBREW_GITHUB_HOSTED_RUNNER: "1") do - sandbox_class.ensure_installed! - end - end - - it "falls back to apt-get as root on GitHub Actions Ubuntu when Homebrew Bubblewrap is unavailable" do - expect(sandbox_class).to receive(:executable) - .twice - .and_return(nil) - expect(Formula).to receive(:[]).with("bubblewrap") - .and_return(instance_double(Formula, ensure_installed!: nil)) - expect(sandbox_class).to receive(:which).with("apt-get").and_return(Pathname("/usr/bin/apt-get")) - expect(Process).to receive(:euid).and_return(0) - expect(sandbox_class).to receive(:ohai).with("Installing Bubblewrap...") - expect(sandbox_class).to receive(:system) - .with("apt-get", "install", "--yes", "bubblewrap") - .and_return(true) - - with_env(GITHUB_ACTIONS: "true", HOMEBREW_GITHUB_HOSTED_RUNNER: "1") do - sandbox_class.ensure_installed! - end - end - - it "does not fall back to apt-get outside GitHub Actions Ubuntu" do - expect(sandbox_class).to receive(:executable) - .twice - .and_return(nil, nil) - expect(Formula).to receive(:[]).with("bubblewrap") - .and_return(instance_double(Formula, ensure_installed!: nil)) - expect(sandbox_class).not_to receive(:which) - expect(sandbox_class).not_to receive(:system) - - with_env(GITHUB_ACTIONS: "true") do - sandbox_class.ensure_installed! - end - end - - it "does not fall back to apt-get outside GitHub Actions" do - expect(sandbox_class).to receive(:executable) - .twice - .and_return(nil, nil) - expect(Formula).to receive(:[]).with("bubblewrap") - .and_return(instance_double(Formula, ensure_installed!: nil)) - expect(sandbox_class).not_to receive(:which) - expect(sandbox_class).not_to receive(:system) - - sandbox_class.ensure_installed! - end - end - - describe "#bubblewrap_args" do - let(:dir) { mktmpdir } - let(:denied_dir) { mktmpdir } - let(:tmpdir) { mktmpdir } - let(:args) { sandbox.bubblewrap_args(tmpdir.to_s) } - - it "maps allowed and denied writes to bind mounts" do - sandbox.allow_write_path dir - sandbox.deny_write_path denied_dir - sandbox.deny_all_network - - expect(args).to include("--unshare-user", "--unshare-ipc", "--unshare-pid", "--unshare-net", "--new-session") - expect(args.each_cons(3)).to include(["--bind", dir.to_s, dir.to_s]) - expect(args.each_cons(3)).to include(["--ro-bind", denied_dir.to_s, denied_dir.to_s]) - end - - it "runs from the sandbox tmpdir" do - expect(args.each_cons(3)).to include(["--bind", tmpdir.to_s, tmpdir.to_s]) - expect(args.each_cons(2)).to include(["--chdir", tmpdir.to_s]) - end - - it "exposes the host filesystem read-only" do - expect(args.each_cons(3)).to include(["--ro-bind", "/", "/"]) - expect(args.index("--ro-bind")).to be < args.index("--dev") - end - - it "masks denied read directories" do - sandbox.deny_read_path dir - - bind = args.each_cons(3).find { |arg| arg.fetch(0) == "--bind" && arg.fetch(2) == dir.to_s } - expect(bind).not_to be_nil - expect(Pathname(bind.fetch(1)).children).to be_empty - end - - it "overlays Linux runtime filesystems" do - expect(args.each_cons(2)).to include(["--dev", "/dev"], ["--proc", "/proc"]) - end - - it "does not need explicit mounts for allowed reads" do - file = mktmpdir/"foo.rb" - FileUtils.touch file - sandbox.allow_read path: file - - expect(args.each_cons(3)).to include(["--ro-bind", "/", "/"]) - expect(args.each_cons(3)).not_to include(["--ro-bind", file.to_s, file.to_s]) - end - - it "uses Linux temp paths instead of macOS temp paths" do - sandbox.allow_write_temp_and_cache - - expect(args).to include("/tmp", "/var/tmp", HOMEBREW_TEMP.to_s, HOMEBREW_CACHE.to_s) - expect(args).not_to include("/private/tmp", "/private/var/tmp") - end - - it "does not add Xcode write paths" do - sandbox.allow_write_xcode - - expect(sandbox.writable_paths).to be_empty - end - - it "rejects regex path filters" do - sandbox.allow_write path: "^/tmp/homebrew-[^/]+$", type: :regex - - expect { args }.to raise_error(ArgumentError, /Linux sandbox does not support regex path filters/) - end - end - - describe "#run with Bubblewrap" do - around do |example| - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: nil) { example.run } - end - + describe "#run" do before do skip "Sandbox not available." unless described_class.available? end @@ -509,15 +61,10 @@ RSpec.describe Sandbox, :needs_linux do expect { sandbox.run "/bin/sh", "-c", 'exec "$1"', "brew-test", executable } .to raise_error(ErrorDuringExecution) end - end - describe "#run with Landlock" do it "allows standard devices and shared memory" do - skip "Landlock not available." unless Sandbox::Landlock.available? - - with_env(HOMEBREW_SANDBOX_LINUX_LANDLOCK: "1") do - landlock_sandbox = described_class.new - landlock_sandbox.run RUBY_PATH, "-rio/console", "-e", <<~'RUBY' + expect do + sandbox.run RUBY_PATH, "-rio/console", "-e", <<~'RUBY' begin File.open("/dev/tty", "r+") { |tty| tty.winsize } rescue Errno::ENXIO, Errno::ENOENT, Errno::EACCES, Errno::EPERM @@ -539,7 +86,7 @@ RSpec.describe Sandbox, :needs_linux do File.unlink(path) end RUBY - end + end.not_to raise_error end end end diff --git a/Library/Homebrew/test/sandbox_shared_spec.rb b/Library/Homebrew/test/sandbox_shared_spec.rb index 957b9775be..689d9d4189 100644 --- a/Library/Homebrew/test/sandbox_shared_spec.rb +++ b/Library/Homebrew/test/sandbox_shared_spec.rb @@ -12,9 +12,8 @@ RSpec.describe Sandbox do before do allow(described_class).to receive_messages( - ensure_sandbox_installed!: nil, - available?: true, - new: command_sandbox, + available?: true, + new: command_sandbox, ) allow(command_sandbox).to receive_messages( allow_write_temp_and_cache: nil, diff --git a/Library/Homebrew/test/search_spec.rb b/Library/Homebrew/test/search_spec.rb index 00d5a81316..06b8982409 100644 --- a/Library/Homebrew/test/search_spec.rb +++ b/Library/Homebrew/test/search_spec.rb @@ -182,6 +182,12 @@ RSpec.describe Homebrew::Search do before do allow(Homebrew::API::Internal).to receive_messages(formula_hashes: api_formulae, cask_hashes: api_casks) + allow(Homebrew::API::Internal).to receive(:formula_names) { api_formulae.keys } + allow(Homebrew::API::Internal).to receive(:formula_name?) { |name| api_formulae.key?(name) } + allow(Homebrew::API::Internal).to receive(:formula_hash) { |name| api_formulae[name] } + allow(Homebrew::API::Internal).to receive(:cask_names) { api_casks.keys } + allow(Homebrew::API::Internal).to receive(:cask_name?) { |token| api_casks.key?(token) } + allow(Homebrew::API::Internal).to receive(:cask_hash) { |token| api_casks[token] } end it "searches formula descriptions" do diff --git a/Library/Homebrew/test/services/formula_wrapper_spec.rb b/Library/Homebrew/test/services/formula_wrapper_spec.rb index 72b970138c..605ad35692 100644 --- a/Library/Homebrew/test/services/formula_wrapper_spec.rb +++ b/Library/Homebrew/test/services/formula_wrapper_spec.rb @@ -66,6 +66,41 @@ RSpec.describe Homebrew::Services::FormulaWrapper, :needs_daemon_manager do end end + describe "#service_contents" do + it "macOS - generates the plist from the formula service block" do + allow(Homebrew::Services::System).to receive(:launchctl?).and_return(true) + allow(service).to receive(:service?).and_return(true) + allow(service_object).to receive_messages(command?: true, to_plist: "plist contents") + + expect(service.service_contents).to eq("plist contents") + end + + it "systemD - generates the unit from the formula service block" do + allow(Homebrew::Services::System).to receive_messages(launchctl?: false, systemctl?: true) + allow(service).to receive(:service?).and_return(true) + allow(service_object).to receive_messages(command?: true, to_systemd_unit: "unit contents") + + expect(service.service_contents).to eq("unit contents") + end + + it "reads the package-provided service file when the service block has no command" do + service_file = mktmpdir/"custom.name.plist" + service_file.write("package plist") + allow(service).to receive_messages(service?: true, service_file:) + allow(service_object).to receive(:command?).and_return(false) + + expect(service.service_contents).to eq("package plist") + end + + it "reads the package-provided service file when the formula has no service block" do + service_file = mktmpdir/"custom.name.plist" + service_file.write("package plist") + allow(service).to receive(:service_file).and_return(service_file) + + expect(service.service_contents).to eq("package plist") + end + end + describe "#service_name" do it "macOS - outputs the service name" do allow(Homebrew::Services::System).to receive(:launchctl?).and_return(true) diff --git a/Library/Homebrew/test/simulate_system_spec.rb b/Library/Homebrew/test/simulate_system_spec.rb index 42ea512570..a53787f0a3 100644 --- a/Library/Homebrew/test/simulate_system_spec.rb +++ b/Library/Homebrew/test/simulate_system_spec.rb @@ -80,6 +80,25 @@ RSpec.describe Homebrew::SimulateSystem do end end + describe "::simulating?" do + it "returns false without any simulation" do + described_class.clear + expect(described_class.simulating?).to be false + end + + it "returns true when simulating an OS" do + described_class.clear + described_class.os = :linux + expect(described_class.simulating?).to be true + end + + it "returns true when simulating an architecture" do + described_class.clear + described_class.arch = :arm + expect(described_class.simulating?).to be true + end + end + describe "::current_arch" do it "returns the current architecture" do described_class.clear diff --git a/Library/Homebrew/test/spec_helper.rb b/Library/Homebrew/test/spec_helper.rb index 2811726f36..6372a3f6a6 100644 --- a/Library/Homebrew/test/spec_helper.rb +++ b/Library/Homebrew/test/spec_helper.rb @@ -364,6 +364,10 @@ RSpec.configure do |config| ENV.replace(@__env) Homebrew::SimulateSystem.clear Context.current = Context::ContextStruct.new + # Shut down and drop any memoized download queue so an example that + # stubbed `DownloadQueue.new` cannot leak a double into later examples + # or the `at_exit` shutdown hook. + Homebrew.reset_default_download_queue if Homebrew.respond_to?(:reset_default_download_queue) $stdout.reopen(@__stdout) $stderr.reopen(@__stderr) diff --git a/Library/Homebrew/test/style_spec.rb b/Library/Homebrew/test/style_spec.rb index 5c929c3f17..34de3183fc 100644 --- a/Library/Homebrew/test/style_spec.rb +++ b/Library/Homebrew/test/style_spec.rb @@ -52,11 +52,14 @@ RSpec.describe Homebrew::Style do end describe ".run_actionlint!" do + let(:actionlint_result) do + instance_double(SystemCommand::Result, success?: true, stdout: "", stderr: "") + end + before do allow(described_class).to receive_messages(actionlint: "actionlint", shellcheck: "shellcheck") - # Run a trivial command so $CHILD_STATUS is non-nil after the stubbed `system` call. - system("true") - allow(described_class).to receive(:system).and_return(true) + allow(Tty).to receive(:color?).and_return(false) + allow(described_class).to receive(:system_command).and_return(actionlint_result) end it "uses a tap's actionlint config when present" do @@ -69,13 +72,15 @@ RSpec.describe Homebrew::Style do tap_config = tap_path/".github/actionlint.yaml" tap_config.write "self-hosted-runner:\n labels: []\n" - expect(described_class).to receive(:system).with( - "actionlint", "-shellcheck", "shellcheck", - "-config-file", tap_config, - "-ignore", "image: string; options: string", - "-ignore", "label .* is unknown", - workflow - ) + expect(described_class).to receive(:system_command).with( + "actionlint", + args: ["-shellcheck", "shellcheck", + "-config-file", tap_config, + "-ignore", "image: string; options: string", + "-ignore", "label .* is unknown", + workflow], + print_stderr: false, + ).and_return(actionlint_result) described_class.run_actionlint!([workflow]) end @@ -87,13 +92,15 @@ RSpec.describe Homebrew::Style do workflow = workflows_dir/"ci.yml" workflow.write "name: CI" - expect(described_class).to receive(:system).with( - "actionlint", "-shellcheck", "shellcheck", - "-config-file", HOMEBREW_REPOSITORY/".github/actionlint.yaml", - "-ignore", "image: string; options: string", - "-ignore", "label .* is unknown", - workflow - ) + expect(described_class).to receive(:system_command).with( + "actionlint", + args: ["-shellcheck", "shellcheck", + "-config-file", HOMEBREW_REPOSITORY/".github/actionlint.yaml", + "-ignore", "image: string; options: string", + "-ignore", "label .* is unknown", + workflow], + print_stderr: false, + ).and_return(actionlint_result) described_class.run_actionlint!([workflow]) end @@ -111,13 +118,15 @@ RSpec.describe Homebrew::Style do workflow2 = tap2_path/".github/workflows/ci.yml" workflow2.write "name: CI" - expect(described_class).to receive(:system).with( - "actionlint", "-shellcheck", "shellcheck", - "-config-file", HOMEBREW_REPOSITORY/".github/actionlint.yaml", - "-ignore", "image: string; options: string", - "-ignore", "label .* is unknown", - workflow1, workflow2 - ) + expect(described_class).to receive(:system_command).with( + "actionlint", + args: ["-shellcheck", "shellcheck", + "-config-file", HOMEBREW_REPOSITORY/".github/actionlint.yaml", + "-ignore", "image: string; options: string", + "-ignore", "label .* is unknown", + workflow1, workflow2], + print_stderr: false, + ).and_return(actionlint_result) described_class.run_actionlint!([workflow1, workflow2]) end @@ -162,15 +171,47 @@ RSpec.describe Homebrew::Style do reason: "formatting shell scripts", executable: "shfmt") .and_return(Pathname.new("/usr/bin/shfmt")) - system("true") - expect(described_class).to receive(:system).with( - { "HOMEBREW_SHFMT" => "/usr/bin/shfmt" }, + shfmt_result = instance_double(SystemCommand::Result, success?: true, stdout: "", stderr: "") + expect(described_class).to receive(:system_command).with( HOMEBREW_LIBRARY/"Homebrew/utils/shfmt.sh", - "--language-dialect", "bash", "--indent", "2", "--case-indent", "--", shell_file - ).and_return(true) + args: ["--language-dialect", "bash", "--indent", "2", "--case-indent", "--", shell_file], + env: { "HOMEBREW_SHFMT" => "/usr/bin/shfmt" }, + print_stderr: false, + ).and_return(shfmt_result) - described_class.run_shfmt!([shell_file]) + expect(described_class.run_shfmt!([shell_file])).to be true + end + end + + describe ".run_shellcheck" do + it "runs shellcheck in parallel chunks and merges their JSON results" do + dir = mktmpdir + log = dir/"shellcheck-args.log" + fake_shellcheck = dir/"shellcheck" + fake_shellcheck.write <<~SCRIPT + #!/bin/bash + echo "$*" >> "#{log}" + echo "[]" + SCRIPT + fake_shellcheck.chmod 0755 + + files = (1..3).map do |i| + file = dir/"script#{i}.sh" + file.write "#!/bin/bash\n" + file + end + + allow(Hardware::CPU).to receive(:cores).and_return(2) + + offenses = described_class.run_shellcheck(files, :json, shellcheck_path: fake_shellcheck) + + expect(offenses).to eq [] + chunks = log.read.lines + expect(chunks.length).to eq 2 + first_chunk = chunks.find { |chunk| chunk.include?("script1.sh") } + expect(first_chunk).to include("script2.sh") + expect(first_chunk).not_to include("script3.sh") end end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/on-linux-blocks.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/on-linux-blocks.rb new file mode 100644 index 0000000000..34590cdba4 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/on-linux-blocks.rb @@ -0,0 +1,23 @@ +# typed: false + +cask "on-linux-blocks" do + version "1.2.3" + + on_macos do + sha256 "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94" + + url "file://#{TEST_FIXTURE_DIR}/cask/caffeine.zip" + + app "Caffeine.app" + end + on_linux do + sha256 x86_64_linux: "244d413861cecb3707cfbcc5c4346d5367daa827da5ea08fb3f3bc2b6276d239", + arm64_linux: "9a1c0967baa46828930ccbbc88668d1b0db07e6edf778800ed4da073c00054f8" + + url "file://#{TEST_FIXTURE_DIR}/cask/caffeine-linux.zip" + + app_image "Caffeine.AppImage" + end + + homepage "https://brew.sh/" +end diff --git a/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb b/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb index 5cfc6aa5f8..059df48fbe 100755 --- a/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb +++ b/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb @@ -66,7 +66,6 @@ UNDEFINED_CONSTANTS_AFTER_REQUIRE = T.let({ "downloadable" => %w[Concurrent], "extend/os/mac/extend/pathname" => %w[MachO], "formula_cellar_checks" => %w[Plist], - "ignorable" => %w[Warnings], "keg" => %w[MachO], "livecheck/livecheck" => %w[Addressable], "os/mac/xcode" => %w[Plist], diff --git a/Library/Homebrew/test/support/helper/formula.rb b/Library/Homebrew/test/support/helper/formula.rb index 02019bcaab..d2260b2587 100644 --- a/Library/Homebrew/test/support/helper/formula.rb +++ b/Library/Homebrew/test/support/helper/formula.rb @@ -13,7 +13,7 @@ module Test sig { params( name: String, path: T.nilable(Pathname), spec: Symbol, alias_path: T.nilable(Pathname), - tap: T.nilable(Tap), block: T.nilable(T.proc.bind(::Formula).void) + tap: T.nilable(Tap), block: T.nilable(T.proc.void) ).returns(::Formula) } def formula(name = "formula_name", path: nil, spec: :stable, alias_path: nil, tap: nil, &block) diff --git a/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb b/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb index c869c760f7..fa56302605 100644 --- a/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb +++ b/Library/Homebrew/test/support/helper/spec/shared_context/homebrew_cask.rb @@ -11,6 +11,7 @@ module Cask class Config DEFAULT_DIRS_PATHNAMES = { appdir: Pathname(TEST_TMPDIR)/"cask-appdir", + appimagedir: Pathname(TEST_TMPDIR)/"cask-appimagedir", keyboard_layoutdir: Pathname(TEST_TMPDIR)/"cask-keyboard-layoutdir", prefpanedir: Pathname(TEST_TMPDIR)/"cask-prefpanedir", qlplugindir: Pathname(TEST_TMPDIR)/"cask-qlplugindir", diff --git a/Library/Homebrew/test/tab_spec.rb b/Library/Homebrew/test/tab_spec.rb index 01f0fcc027..50d6ea8384 100644 --- a/Library/Homebrew/test/tab_spec.rb +++ b/Library/Homebrew/test/tab_spec.rb @@ -7,19 +7,19 @@ require "formula" RSpec.describe Tab do subject(:tab) do described_class.new( - "homebrew_version" => HOMEBREW_VERSION, - "used_options" => used_options.as_flags, - "unused_options" => unused_options.as_flags, - "built_as_bottle" => false, - "poured_from_bottle" => true, - "installed_on_request" => true, - "changed_files" => [], - "time" => time, - "source_modified_time" => 0, - "compiler" => "clang", - "stdlib" => "libcxx", - "runtime_dependencies" => [], - "source" => { + homebrew_version: HOMEBREW_VERSION, + used_options: used_options.as_flags, + unused_options: unused_options.as_flags, + built_as_bottle: false, + poured_from_bottle: true, + installed_on_request: true, + changed_files: [], + time:, + source_modified_time: 0, + compiler: "clang", + stdlib: "libcxx", + runtime_dependencies: [], + source: { "tap" => CoreTap.instance.to_s, "path" => CoreTap.instance.path.to_s, "spec" => "stable", @@ -29,8 +29,8 @@ RSpec.describe Tab do "head" => "HEAD-1111111", }, }, - "arch" => Hardware::CPU.arch, - "built_on" => DevelopmentTools.build_system_info, + arch: Hardware::CPU.arch, + built_on: DevelopmentTools.build_system_info, ) end @@ -143,6 +143,17 @@ RSpec.describe Tab do expect(tab).not_to be_loaded_from_internal_api end + specify "#initialize" do + # Receipts written by other Homebrew versions carry attributes we no longer know about. + tab = described_class.new(installed_as_dependency: true, homebrew_version: "1.2.3") + expect(tab.homebrew_version).to eq("1.2.3") + end + + specify "#installed_on_request_present?" do + expect(described_class.new).not_to be_installed_on_request_present + expect(described_class.new(installed_on_request: false)).to be_installed_on_request_present + end + specify "#parsed_homebrew_version" do tab = described_class.new expect(tab.parsed_homebrew_version).to be Version::NULL @@ -544,7 +555,7 @@ RSpec.describe Tab do end specify "#to_json" do - json_tab = described_class.new(JSON.parse(tab.to_json)) + json_tab = described_class.new(**JSON.parse(tab.to_json).transform_keys(&:to_sym)) expect(json_tab.homebrew_version).to eq(tab.homebrew_version) expect(json_tab.used_options.sort).to eq(tab.used_options.sort) expect(json_tab.unused_options.sort).to eq(tab.unused_options.sort) @@ -566,7 +577,7 @@ RSpec.describe Tab do end specify "#to_bottle_hash" do - json_tab = described_class.new(JSON.parse(tab.to_bottle_hash.to_json)) + json_tab = described_class.new(**JSON.parse(tab.to_bottle_hash.to_json).transform_keys(&:to_sym)) expect(json_tab.homebrew_version).to eq(tab.homebrew_version) expect(json_tab.changed_files).to eq(tab.changed_files) expect(json_tab.source_modified_time).to eq(tab.source_modified_time) diff --git a/Library/Homebrew/test/unpack_strategy/dmg_spec.rb b/Library/Homebrew/test/unpack_strategy/dmg_spec.rb index ab9b31f57f..76de45cab6 100644 --- a/Library/Homebrew/test/unpack_strategy/dmg_spec.rb +++ b/Library/Homebrew/test/unpack_strategy/dmg_spec.rb @@ -4,6 +4,75 @@ require_relative "shared_examples" RSpec.describe UnpackStrategy::Dmg, :needs_macos do + describe ".can_extract?" do + let(:path) { TEST_FIXTURE_DIR/"cask/container.dmg" } + let(:status) { instance_double(Process::Status, success?: true) } + let(:result) { instance_double(SystemCommand::Result, to_a: ["plist", "", status]) } + + before do + allow(described_class).to receive(:system_command).and_return(result) + end + + it "uses diskutil on the oldest supported macOS" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) + expect(described_class).to receive(:system_command) + .with("diskutil", args: ["image", "info", "--plist", "--", path], print_stderr: false) + .and_return(result) + + expect(described_class.can_extract?(path)).to be true + end + + it "uses hdiutil before macOS Sonoma" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:ventura)) + expect(described_class).to receive(:system_command) + .with("hdiutil", args: ["imageinfo", "-format", path], print_stderr: false) + .and_return(result) + + expect(described_class.can_extract?(path)).to be true + end + + it "detects a disk image when diskutil returns a license agreement" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) + failure_status = instance_double(Process::Status, success?: false) + diskutil_result = instance_double(SystemCommand::Result, to_a: ["license agreement", "", failure_status]) + + expect(described_class).to receive(:system_command) + .with("diskutil", args: ["image", "info", "--plist", "--", path], print_stderr: false) + .and_return(diskutil_result) + expect(described_class).not_to receive(:system_command).with("hdiutil", anything) + + expect(described_class.can_extract?(path)).to be true + end + + it "rejects unexpected stdout from a failed diskutil image info" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) + path = TEST_FIXTURE_DIR/"cask/container.xar" + failure_status = instance_double(Process::Status, success?: false) + diskutil_result = instance_double(SystemCommand::Result, to_a: ["unexpected output", "", failure_status]) + + expect(described_class).to receive(:system_command) + .with("diskutil", args: ["image", "info", "--plist", "--", path], print_stderr: false) + .and_return(diskutil_result) + expect(described_class).not_to receive(:system_command).with("hdiutil", anything) + + expect(described_class.can_extract?(path)).to be false + end + + it "rejects files too small to contain a UDIF trailer" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) + path = TEST_FIXTURE_DIR/"cask/container.tar.gz" + failure_status = instance_double(Process::Status, success?: false) + diskutil_result = instance_double(SystemCommand::Result, to_a: ["unexpected output", "", failure_status]) + + expect(described_class).to receive(:system_command) + .with("diskutil", args: ["image", "info", "--plist", "--", path], print_stderr: false) + .and_return(diskutil_result) + expect(described_class).not_to receive(:system_command).with("hdiutil", anything) + + expect(described_class.can_extract?(path)).to be false + end + end + describe "#mount" do let(:path) { TEST_FIXTURE_DIR/"cask/container.dmg" } @@ -29,9 +98,10 @@ RSpec.describe UnpackStrategy::Dmg, :needs_macos do end it "does not treat an unrelated attach failure as a license agreement" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) unpack_strategy = described_class.new(path) attach_result = instance_double(SystemCommand::Result, success?: false, stdout: "") - attach_error = ErrorDuringExecution.new(["hdiutil", "attach"], status: 1) + attach_error = ErrorDuringExecution.new(["diskutil", "image", "attach"], status: 1) allow(unpack_strategy).to receive(:system_command).and_return(attach_result) expect(attach_result).to receive(:assert_success!).and_raise(attach_error) @@ -39,5 +109,131 @@ RSpec.describe UnpackStrategy::Dmg, :needs_macos do expect { unpack_strategy.mount { nil } }.to raise_error(attach_error) end + + it "uses an isolated diskutil mount point" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) + attach_result = instance_double( + SystemCommand::Result, + success?: true, + plist: { "system-entities" => [] }, + ) + unpack_strategy = described_class.new(path) + + expect(unpack_strategy).to receive(:system_command).with( + "diskutil", + args: [ + "image", "attach", "--plist", "--readOnly", "--mountOptions", "nobrowse", + "--mountPoint", kind_of(Pathname), path + ], + input: "qn\n", + print_stderr: false, + verbose: false, + ).and_return(attach_result) + + unpack_strategy.mount { |mounts| expect(mounts).to be_empty } + end + + it "retries without a mount point for multi-volume disk images" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) + mount_point_result = instance_double(SystemCommand::Result, success?: false, stdout: "") + fallback_result = instance_double( + SystemCommand::Result, + success?: true, + plist: { "system-entities" => [] }, + ) + unpack_strategy = described_class.new(path) + + expect(unpack_strategy).to receive(:system_command).with( + "diskutil", + args: [ + "image", "attach", "--plist", "--readOnly", "--mountOptions", "nobrowse", + "--mountPoint", kind_of(Pathname), path + ], + input: "qn\n", + print_stderr: false, + verbose: false, + ).ordered.and_return(mount_point_result) + expect(unpack_strategy).to receive(:system_command).with( + "diskutil", + args: ["image", "attach", "--plist", "--readOnly", "--mountOptions", "nobrowse", path], + input: "qn\n", + print_stderr: false, + verbose: false, + ).ordered.and_return(fallback_result) + + unpack_strategy.mount { |mounts| expect(mounts).to be_empty } + end + + it "uses hdiutil before macOS Sonoma" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:ventura)) + attach_result = instance_double( + SystemCommand::Result, + success?: true, + plist: { "system-entities" => [] }, + ) + unpack_strategy = described_class.new(path) + + expect(unpack_strategy).to receive(:system_command).with( + "hdiutil", + args: [ + "attach", "-plist", "-nobrowse", "-readonly", + "-mountrandom", kind_of(Pathname), path + ], + input: "qn\n", + print_stderr: false, + verbose: false, + ).and_return(attach_result) + + unpack_strategy.mount { |mounts| expect(mounts).to be_empty } + end + + it "converts disk images with license agreements using diskutil" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:sonoma)) + eula_result = instance_double(SystemCommand::Result, success?: false, stdout: "license agreement") + converted_result = instance_double( + SystemCommand::Result, + success?: true, + assert_success!: nil, + plist: { "system-entities" => [] }, + ) + unpack_strategy = described_class.new(path) + + allow(unpack_strategy).to receive(:system_command).and_return(eula_result, converted_result) + expect(unpack_strategy).to receive(:system_command!).with( + "diskutil", + args: [ + "image", "create", "from", "--format", "RAW", path, + satisfy { |output| output.is_a?(Pathname) && output.extname == ".dmg" } + ], + print_stderr: false, + verbose: false, + ).and_return(instance_double(SystemCommand::Result)) + + unpack_strategy.mount { |mounts| expect(mounts).to be_empty } + end + + it "converts disk images with license agreements using hdiutil before macOS Sonoma" do + allow(MacOS).to receive(:version).and_return(MacOSVersion.from_symbol(:ventura)) + eula_result = instance_double(SystemCommand::Result, success?: false, stdout: "license agreement") + converted_result = instance_double( + SystemCommand::Result, + success?: true, + assert_success!: nil, + plist: { "system-entities" => [] }, + ) + unpack_strategy = described_class.new(path) + + allow(unpack_strategy).to receive(:system_command).and_return(eula_result, converted_result) + expect(unpack_strategy).to receive(:system_command!).with( + "hdiutil", + args: [ + "convert", "-quiet", "-format", "UDTO", "-o", + satisfy { |output| output.is_a?(Pathname) && output.extname == ".cdr" }, path + ], + verbose: false, + ).and_return(instance_double(SystemCommand::Result)) + + unpack_strategy.mount { |mounts| expect(mounts).to be_empty } + end end end diff --git a/Library/Homebrew/test/utils/github_spec.rb b/Library/Homebrew/test/utils/github_spec.rb index ff19c0beaf..4426c7b255 100644 --- a/Library/Homebrew/test/utils/github_spec.rb +++ b/Library/Homebrew/test/utils/github_spec.rb @@ -4,6 +4,24 @@ require "utils/github" RSpec.describe GitHub do + describe "::members_by_team" do + it "reports an inaccessible team without assuming the token scope is missing" do + allow(GitHub::API).to receive(:open_graphql).and_return({ + "organization" => { + "teams" => { "nodes" => [] }, + "team" => nil, + }, + }) + + expect { described_class.members_by_team("Homebrew", "maintainers") } + .to raise_error( + GitHub::API::Error, + "Could not access the team Homebrew/maintainers. Please check that your GitHub account has access to the " \ + "team and that your token has the required permissions.", + ) + end + end + describe "::API.commit" do it "fetches the main branch commit by default" do commit = { "sha" => "abc123" } diff --git a/Library/Homebrew/test/warnings_spec.rb b/Library/Homebrew/test/warnings_spec.rb index 702c682008..8428b0717b 100644 --- a/Library/Homebrew/test/warnings_spec.rb +++ b/Library/Homebrew/test/warnings_spec.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "warnings" diff --git a/Library/Homebrew/unpack_strategy/dmg.rb b/Library/Homebrew/unpack_strategy/dmg.rb index b2188041c9..e64ef9ad7d 100644 --- a/Library/Homebrew/unpack_strategy/dmg.rb +++ b/Library/Homebrew/unpack_strategy/dmg.rb @@ -172,14 +172,45 @@ module UnpackStrategy end private_constant :Mount + # UDIF disk images store the "koly" signature at the start of a 512-byte trailer. + UDIF_TRAILER_SIZE = 512 + private_constant :UDIF_TRAILER_SIZE + sig { override.returns(T::Array[String]) } def self.extensions [".dmg"] end + sig { returns(T::Boolean) } + def self.diskutil_image? = false + sig { override.params(path: Pathname).returns(T::Boolean) } def self.can_extract?(path) - stdout, _, status = system_command("hdiutil", args: ["imageinfo", "-format", path], print_stderr: false).to_a + if diskutil_image? + can_extract_with_diskutil?(path) + else + can_extract_with_hdiutil?(path) + end + end + + sig { params(path: Pathname).returns(T::Boolean) } + private_class_method def self.can_extract_with_diskutil?(path) + result = system_command("diskutil", args: ["image", "info", "--plist", "--", path], print_stderr: false) + + stdout, _, status = result.to_a + return !stdout.empty? if status.success? + return false if stdout.empty? + return false if path.size < UDIF_TRAILER_SIZE + + # `diskutil` exits unsuccessfully after printing an SLA. Confirm the image directly. + path.binread(4, path.size - UDIF_TRAILER_SIZE) == "koly" + end + + sig { params(path: Pathname).returns(T::Boolean) } + private_class_method def self.can_extract_with_hdiutil?(path) + stdout, _, status = system_command( + "hdiutil", args: ["imageinfo", "-format", path], print_stderr: false + ).to_a (status.success? && !stdout.empty?) || false end @@ -188,12 +219,9 @@ module UnpackStrategy Dir.mktmpdir("homebrew-dmg", HOMEBREW_TEMP) do |mount_dir| mount_dir = Pathname(mount_dir) - without_eula = system_command( - "hdiutil", - args: [ - "attach", "-plist", "-nobrowse", "-readonly", - "-mountrandom", mount_dir, path - ], + without_eula = attach_image( + path, + mount_dir:, input: "qn\n", print_stderr: false, verbose:, @@ -205,26 +233,15 @@ module UnpackStrategy else without_eula.assert_success! if without_eula.stdout.empty? - cdr_path = mount_dir/path.basename.sub_ext(".cdr") + converted_path = convert_image(path, mount_dir:, verbose:) - quiet_flag = "-quiet" unless verbose - - system_command!( - "hdiutil", - args: [ - "convert", *quiet_flag, "-format", "UDTO", "-o", cdr_path, path - ], - verbose:, - ) - - with_eula = system_command!( - "hdiutil", - args: [ - "attach", "-plist", "-nobrowse", "-readonly", - "-mountrandom", mount_dir, cdr_path - ], + with_eula = attach_image( + converted_path, + mount_dir:, + print_stderr: true, verbose:, ) + with_eula.assert_success! if verbose && !(eula_text = without_eula.stdout).empty? ohai "Software License Agreement for '#{path}':", eula_text @@ -253,6 +270,123 @@ module UnpackStrategy private + sig { + params( + image_path: Pathname, + mount_dir: Pathname, + input: T.any(String, T::Array[String]), + print_stderr: T::Boolean, + verbose: T::Boolean, + ).returns(SystemCommand::Result) + } + def attach_image(image_path, mount_dir:, input: [], print_stderr: true, verbose: false) + if self.class.diskutil_image? + attach_image_with_diskutil( + image_path, + mount_dir:, + input:, + print_stderr:, + verbose:, + ) + else + attach_image_with_hdiutil( + image_path, + mount_dir:, + input:, + print_stderr:, + verbose:, + ) + end + end + + sig { + params( + image_path: Pathname, + mount_dir: Pathname, + input: T.any(String, T::Array[String]), + print_stderr: T::Boolean, + verbose: T::Boolean, + ).returns(SystemCommand::Result) + } + def attach_image_with_diskutil(image_path, mount_dir:, input:, print_stderr:, verbose:) + args = ["image", "attach", "--plist", "--readOnly", "--mountOptions", "nobrowse"] + mount_point = mount_dir/"mount" + mount_point.mkpath + + result = system_command( + "diskutil", + args: [*args, "--mountPoint", mount_point, image_path], + input:, + print_stderr: false, + verbose:, + ) + return result if result.success? + return result unless result.stdout.empty? + + system_command( + "diskutil", + args: [*args, image_path], + input:, + print_stderr:, + verbose:, + ) + end + + sig { + params( + image_path: Pathname, + mount_dir: Pathname, + input: T.any(String, T::Array[String]), + print_stderr: T::Boolean, + verbose: T::Boolean, + ).returns(SystemCommand::Result) + } + def attach_image_with_hdiutil(image_path, mount_dir:, input:, print_stderr:, verbose:) + system_command( + "hdiutil", + args: [ + "attach", "-plist", "-nobrowse", "-readonly", + "-mountrandom", mount_dir, image_path + ], + input:, + print_stderr:, + verbose:, + ) + end + + sig { params(image_path: Pathname, mount_dir: Pathname, verbose: T::Boolean).returns(Pathname) } + def convert_image(image_path, mount_dir:, verbose:) + if self.class.diskutil_image? + convert_image_with_diskutil(image_path, mount_dir:, verbose:) + else + convert_image_with_hdiutil(image_path, mount_dir:, verbose:) + end + end + + sig { params(image_path: Pathname, mount_dir: Pathname, verbose: T::Boolean).returns(Pathname) } + def convert_image_with_diskutil(image_path, mount_dir:, verbose:) + converted_path = mount_dir/image_path.basename.sub_ext(".dmg") + system_command!( + "diskutil", + args: ["image", "create", "from", "--format", "RAW", image_path, converted_path], + print_stderr: verbose, + verbose:, + ) + converted_path + end + + sig { params(image_path: Pathname, mount_dir: Pathname, verbose: T::Boolean).returns(Pathname) } + def convert_image_with_hdiutil(image_path, mount_dir:, verbose:) + converted_path = mount_dir/image_path.basename.sub_ext(".cdr") + quiet_flag = "-quiet" unless verbose + system_command!( + "hdiutil", + args: ["convert", *quiet_flag, "-format", "UDTO", "-o", converted_path, image_path], + verbose:, + ) + converted_path + end + sig { override.params(unpack_dir: Pathname, basename: Pathname, verbose: T::Boolean).void } def extract_to_dir(unpack_dir, basename:, verbose:) mount(verbose:) do |mounts| @@ -265,3 +399,5 @@ module UnpackStrategy end end end + +require "extend/os/unpack_strategy/dmg" diff --git a/Library/Homebrew/upgrade.rb b/Library/Homebrew/upgrade.rb index c8e55922ba..a19a924803 100644 --- a/Library/Homebrew/upgrade.rb +++ b/Library/Homebrew/upgrade.rb @@ -111,6 +111,7 @@ module Homebrew fi = create_formula_installer( formula, flags:, + download_queue:, force_bottle:, build_from_source_formulae:, interactive:, @@ -122,7 +123,6 @@ module Homebrew quiet:, verbose:, ) - fi.download_queue = download_queue fi.fetch_bottle_tab(quiet: !debug, enqueue: true) fi rescue CannotInstallFormulaError => e @@ -134,7 +134,8 @@ module Homebrew end end - download_queue.fetch + download_queue.fetch(only: Resource::BottleManifest, heading: "Downloading bottle manifests", + allow_failures: true) ensure download_queue.shutdown end @@ -511,7 +512,8 @@ module Homebrew end sig { - params(formula: Formula, flags: T::Array[String], force_bottle: T::Boolean, + params(formula: Formula, flags: T::Array[String], download_queue: Homebrew::DownloadQueue, + force_bottle: T::Boolean, build_from_source_formulae: T::Array[String], interactive: T::Boolean, keep_tmp: T::Boolean, debug_symbols: T::Boolean, force: T::Boolean, overwrite: T::Boolean, debug: T::Boolean, quiet: T::Boolean, verbose: T::Boolean).returns(FormulaInstaller) @@ -519,6 +521,7 @@ module Homebrew def create_formula_installer( formula, flags:, + download_queue:, force_bottle: false, build_from_source_formulae: [], interactive: false, @@ -555,6 +558,7 @@ module Homebrew FormulaInstaller.new( formula, **{ + download_queue:, options:, link_keg:, installed_on_request:, diff --git a/Library/Homebrew/utils/gem_setup.rb b/Library/Homebrew/utils/gem_setup.rb index e6d697b396..e6c193cfd0 100644 --- a/Library/Homebrew/utils/gem_setup.rb +++ b/Library/Homebrew/utils/gem_setup.rb @@ -10,7 +10,7 @@ Homebrew::FastBootRequire.from_rubylibdir("English") module Homebrew # Bump this whenever a committed vendored gem is later added to or exclusion removed from gitignore. # This will trigger it to reinstall properly if `brew install-bundler-gems` needs it. - VENDOR_VERSION = 8 + VENDOR_VERSION = 9 private_constant :VENDOR_VERSION RUBY_BUNDLE_VENDOR_DIRECTORY = (HOMEBREW_LIBRARY_PATH/"vendor/bundle/ruby").freeze diff --git a/Library/Homebrew/utils/github.rb b/Library/Homebrew/utils/github.rb index 983833fe48..4cb2122c7c 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -411,11 +411,11 @@ module GitHub EOS result = API.open_graphql(query, scopes: ["read:org", "user"]) - if result["organization"]["teams"]["nodes"].blank? - raise API::Error, - "Your token needs the 'read:org' scope to access this API" + if result.dig("organization", "teams", "nodes").blank? || result.dig("organization", "team").blank? + raise API::Error, "Could not access the team #{org}/#{team}. " \ + "Please check that your GitHub account has access to the team and that your token has the " \ + "required permissions." end - raise API::Error, "The team #{org}/#{team} does not exist" if result["organization"]["team"].blank? result["organization"]["team"]["members"]["nodes"].to_h { |member| [member["login"], member["name"]] } end diff --git a/Library/Homebrew/utils/phase_timings.rb b/Library/Homebrew/utils/phase_timings.rb index f24a95556a..d9c79e06bc 100644 --- a/Library/Homebrew/utils/phase_timings.rb +++ b/Library/Homebrew/utils/phase_timings.rb @@ -54,7 +54,10 @@ module Homebrew if defined?(Homebrew::API::Internal) instrument(Homebrew::API::Internal.singleton_class, :formula_struct, "api_metadata_load") end - instrument(Homebrew::Install.singleton_class, :formula_installers, "planning") if defined?(Homebrew::Install) + if defined?(Homebrew::Install) + instrument(Homebrew::Install.singleton_class, :formula_installers, "planning") + instrument(Homebrew::Install.singleton_class, :perform_preinstall_checks_once, "preinstall_checks") + end if defined?(FormulaInstaller) instrument(FormulaInstaller, :prelude, "planning") instrument(FormulaInstaller, :compute_dependencies, "dependency_resolution") diff --git a/Library/Homebrew/utils/shared_audits.rb b/Library/Homebrew/utils/shared_audits.rb index 4d20461b19..9bb1b4dfaa 100644 --- a/Library/Homebrew/utils/shared_audits.rb +++ b/Library/Homebrew/utils/shared_audits.rb @@ -248,9 +248,10 @@ module SharedAudits "<#{notability_thresholds.fetch(:stars)} stars)" end - return if Date.parse(metadata["created_at"]) <= (Date.today - 30) + age_days = (Date.today - Date.parse(metadata["created_at"])).to_i + return if age_days >= 30 - "GitHub repository too new (<30 days old)" + "GitHub repository too new (#{age_days} days old, 30 days required)" end sig { params(user: String, repo: String, self_submission: T::Boolean).returns(T.nilable(String)) } @@ -273,9 +274,10 @@ module SharedAudits "<#{notability_thresholds.fetch(:stars)} stars)" end - return if Date.parse(metadata["created_at"]) <= (Date.today - 30) + age_days = (Date.today - Date.parse(metadata["created_at"])).to_i + return if age_days >= 30 - "GitLab repository too new (<30 days old)" + "GitLab repository too new (#{age_days} days old, 30 days required)" end sig { params(user: String, repo: String, self_submission: T::Boolean).returns(T.nilable(String)) } @@ -291,7 +293,8 @@ module SharedAudits return "Bitbucket fork (not canonical repository)" unless metadata["parent"].nil? - return "Bitbucket repository too new (<30 days old)" if Date.parse(metadata["created_on"]) >= (Date.today - 30) + age_days = (Date.today - Date.parse(metadata["created_on"])).to_i + return "Bitbucket repository too new (#{age_days} days old, 30 days required)" if age_days < 30 forks_result = Utils::Curl.curl_output("--request", "GET", "#{api_url}/forks") return unless forks_result.status.success? @@ -339,9 +342,10 @@ module SharedAudits "<#{notability_thresholds.fetch(:stars)} stars)" end - return if Date.parse(metadata["created_at"]) <= (Date.today - 30) + age_days = (Date.today - Date.parse(metadata["created_at"])).to_i + return if age_days >= 30 - "Forgejo repository too new (<30 days old)" + "Forgejo repository too new (#{age_days} days old, 30 days required)" end sig { params(url: String).returns(T.nilable(String)) } diff --git a/Library/Homebrew/vendor/bundle/bundler/setup.rb b/Library/Homebrew/vendor/bundle/bundler/setup.rb index 77a67c0a92..23165b1453 100644 --- a/Library/Homebrew/vendor/bundle/bundler/setup.rb +++ b/Library/Homebrew/vendor/bundle/bundler/setup.rb @@ -41,7 +41,7 @@ $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bigdecimal-4.1.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bindata-2.5.1/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/concurrent-ruby-1.3.8/lib/concurrent-ruby") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/csv-3.3.5/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/csv-3.3.6/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/diff-lcs-1.6.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/drb-2.2.3/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/elftools-1.3.1/lib") @@ -67,12 +67,12 @@ $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-20/#{Gem.extension_api_version}/racc-1.8.1") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/racc-1.8.1/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/parser-3.3.12.0/lib") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/patchelf-1.5.2/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/patchelf-1.6.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/plist-3.7.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rainbow-3.1.1/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/tsort-0.2.0/lib") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-20/#{Gem.extension_api_version}/rbs-4.0.3") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rbs-4.0.3/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-20/#{Gem.extension_api_version}/rbs-4.1.2") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rbs-4.1.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rbi-0.4.1/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/require-hooks-0.4.1/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rspec-support-3.13.7/lib") @@ -82,7 +82,7 @@ $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rspec-3.13.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rspec-github-3.0.0/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rspec-retry-0.6.2/lib") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-runtime-0.6.13359/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-runtime-0.6.13365/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rspec-sorbet-1.9.2/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rspec_junit_formatter-0.6.0/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubocop-ast-1.50.0/lib") @@ -99,11 +99,11 @@ $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-20/#{Gem.extension_api_version}/ruby-prof-2.0.5") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/ruby-prof-2.0.5/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/rubydex-0.3.0-arm64-darwin/lib") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/simplecov-1.0.2/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/simplecov-1.0.3/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/simplecov-cobertura-4.0.0/lib") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-static-0.6.13359-universal-darwin/lib") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-0.6.13359/lib") -$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-static-and-runtime-0.6.13359/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-static-0.6.13365-universal-darwin/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-0.6.13365/lib") +$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/sorbet-static-and-runtime-0.6.13365/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/thor-1.5.0/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/spoom-1.8.6/lib") $:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-20/#{Gem.extension_api_version}/stackprof-0.2.28") diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/COPYING b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/COPYING deleted file mode 100644 index 48e5a96de7..0000000000 --- a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/COPYING +++ /dev/null @@ -1,56 +0,0 @@ -Ruby is copyrighted free software by Yukihiro Matsumoto . -You can redistribute it and/or modify it under either the terms of the -2-clause BSDL (see the file BSDL), or the conditions below: - -1. You may make and give away verbatim copies of the source form of the - software without restriction, provided that you duplicate all of the - original copyright notices and associated disclaimers. - -2. You may modify your copy of the software in any way, provided that - you do at least ONE of the following: - - a. place your modifications in the Public Domain or otherwise - make them Freely Available, such as by posting said - modifications to Usenet or an equivalent medium, or by allowing - the author to include your modifications in the software. - - b. use the modified software only within your corporation or - organization. - - c. give non-standard binaries non-standard names, with - instructions on where to get the original software distribution. - - d. make other distribution arrangements with the author. - -3. You may distribute the software in object code or binary form, - provided that you do at least ONE of the following: - - a. distribute the binaries and library files of the software, - together with instructions (in the manual page or equivalent) - on where to get the original distribution. - - b. accompany the distribution with the machine-readable source of - the software. - - c. give non-standard binaries non-standard names, with - instructions on where to get the original software distribution. - - d. make other distribution arrangements with the author. - -4. You may modify and include the part of the software into any other - software (possibly commercial). But some files in the distribution - are not written by the author, so that they are not under these terms. - - For the list of those files and their copying conditions, see the - file LEGAL. - -5. The scripts and library files supplied as input to or produced as - output from the software do not automatically fall under the - copyright of the software, but belong to whomever generated them, - and may be sold commercially, and may be aggregated with this - software. - -6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR - IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - PURPOSE. diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/lib/base64.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/lib/base64.rb deleted file mode 100644 index 8c0145d258..0000000000 --- a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/base64-0.3.0/lib/base64.rb +++ /dev/null @@ -1,381 +0,0 @@ -# frozen_string_literal: true -# -# \Module \Base64 provides methods for: -# -# - \Encoding a binary string (containing non-ASCII characters) -# as a string of printable ASCII characters. -# - Decoding such an encoded string. -# -# \Base64 is commonly used in contexts where binary data -# is not allowed or supported: -# -# - Images in HTML or CSS files, or in URLs. -# - Email attachments. -# -# A \Base64-encoded string is about one-third larger that its source. -# See the {Wikipedia article}[https://en.wikipedia.org/wiki/Base64] -# for more information. -# -# This module provides three pairs of encode/decode methods. -# Your choices among these methods should depend on: -# -# - Which character set is to be used for encoding and decoding. -# - Whether "padding" is to be used. -# - Whether encoded strings are to contain newlines. -# -# Note: Examples on this page assume that the including program has executed: -# -# require 'base64' -# -# == \Encoding Character Sets -# -# A \Base64-encoded string consists only of characters from a 64-character set: -# -# - ('A'..'Z'). -# - ('a'..'z'). -# - ('0'..'9'). -# - =, the 'padding' character. -# - Either: -# - %w[+ /]: -# {RFC-2045-compliant}[https://datatracker.ietf.org/doc/html/rfc2045]; -# _not_ safe for URLs. -# - %w[- _]: -# {RFC-4648-compliant}[https://datatracker.ietf.org/doc/html/rfc4648]; -# safe for URLs. -# -# If you are working with \Base64-encoded strings that will come from -# or be put into URLs, you should choose this encoder-decoder pair -# of RFC-4648-compliant methods: -# -# - Base64.urlsafe_encode64 and Base64.urlsafe_decode64. -# -# Otherwise, you may choose any of the pairs in this module, -# including the pair above, or the RFC-2045-compliant pairs: -# -# - Base64.encode64 and Base64.decode64. -# - Base64.strict_encode64 and Base64.strict_decode64. -# -# == Padding -# -# \Base64-encoding changes a triplet of input bytes -# into a quartet of output characters. -# -# Padding in Encode Methods -# -# Padding -- extending an encoded string with zero, one, or two trailing -# = characters -- is performed by methods Base64.encode64, -# Base64.strict_encode64, and, by default, Base64.urlsafe_encode64: -# -# Base64.encode64('s') # => "cw==\n" -# Base64.strict_encode64('s') # => "cw==" -# Base64.urlsafe_encode64('s') # => "cw==" -# Base64.urlsafe_encode64('s', padding: false) # => "cw" -# -# When padding is performed, the encoded string is always of length 4n, -# where +n+ is a non-negative integer: -# -# - Input bytes of length 3n generate unpadded output characters -# of length 4n: -# -# # n = 1: 3 bytes => 4 characters. -# Base64.strict_encode64('123') # => "MDEy" -# # n = 2: 6 bytes => 8 characters. -# Base64.strict_encode64('123456') # => "MDEyMzQ1" -# -# - Input bytes of length 3n+1 generate padded output characters -# of length 4(n+1), with two padding characters at the end: -# -# # n = 1: 4 bytes => 8 characters. -# Base64.strict_encode64('1234') # => "MDEyMw==" -# # n = 2: 7 bytes => 12 characters. -# Base64.strict_encode64('1234567') # => "MDEyMzQ1Ng==" -# -# - Input bytes of length 3n+2 generate padded output characters -# of length 4(n+1), with one padding character at the end: -# -# # n = 1: 5 bytes => 8 characters. -# Base64.strict_encode64('12345') # => "MDEyMzQ=" -# # n = 2: 8 bytes => 12 characters. -# Base64.strict_encode64('12345678') # => "MDEyMzQ1Njc=" -# -# When padding is suppressed, for a positive integer n: -# -# - Input bytes of length 3n generate unpadded output characters -# of length 4n: -# -# # n = 1: 3 bytes => 4 characters. -# Base64.urlsafe_encode64('123', padding: false) # => "MDEy" -# # n = 2: 6 bytes => 8 characters. -# Base64.urlsafe_encode64('123456', padding: false) # => "MDEyMzQ1" -# -# - Input bytes of length 3n+1 generate unpadded output characters -# of length 4n+2, with two padding characters at the end: -# -# # n = 1: 4 bytes => 6 characters. -# Base64.urlsafe_encode64('1234', padding: false) # => "MDEyMw" -# # n = 2: 7 bytes => 10 characters. -# Base64.urlsafe_encode64('1234567', padding: false) # => "MDEyMzQ1Ng" -# -# - Input bytes of length 3n+2 generate unpadded output characters -# of length 4n+3, with one padding character at the end: -# -# # n = 1: 5 bytes => 7 characters. -# Base64.urlsafe_encode64('12345', padding: false) # => "MDEyMzQ" -# # m = 2: 8 bytes => 11 characters. -# Base64.urlsafe_encode64('12345678', padding: false) # => "MDEyMzQ1Njc" -# -# Padding in Decode Methods -# -# All of the \Base64 decode methods support (but do not require) padding. -# -# \Method Base64.decode64 does not check the size of the padding: -# -# Base64.decode64("MDEyMzQ1Njc") # => "01234567" -# Base64.decode64("MDEyMzQ1Njc=") # => "01234567" -# Base64.decode64("MDEyMzQ1Njc==") # => "01234567" -# -# \Method Base64.strict_decode64 strictly enforces padding size: -# -# Base64.strict_decode64("MDEyMzQ1Njc") # Raises ArgumentError -# Base64.strict_decode64("MDEyMzQ1Njc=") # => "01234567" -# Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError -# -# \Method Base64.urlsafe_decode64 allows padding in the encoded string, -# which if present, must be correct: -# see {Padding}[Base64.html#module-Base64-label-Padding], above: -# -# Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567" -# Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567" -# Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError. -# -# == Newlines -# -# An encoded string returned by Base64.encode64 or Base64.urlsafe_encode64 -# has an embedded newline character -# after each 60-character sequence, and, if non-empty, at the end: -# -# # No newline if empty. -# encoded = Base64.encode64("\x00" * 0) -# encoded.index("\n") # => nil -# -# # Newline at end of short output. -# encoded = Base64.encode64("\x00" * 1) -# encoded.size # => 4 -# encoded.index("\n") # => 4 -# -# # Newline at end of longer output. -# encoded = Base64.encode64("\x00" * 45) -# encoded.size # => 60 -# encoded.index("\n") # => 60 -# -# # Newlines embedded and at end of still longer output. -# encoded = Base64.encode64("\x00" * 46) -# encoded.size # => 65 -# encoded.rindex("\n") # => 65 -# encoded.split("\n").map {|s| s.size } # => [60, 4] -# -# The string to be encoded may itself contain newlines, -# which are encoded as \Base64: -# -# # Base64.encode64("\n\n\n") # => "CgoK\n" -# s = "This is line 1\nThis is line 2\n" -# Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n" -# -module Base64 - - VERSION = "0.3.0" - - module_function - - # :call-seq: - # Base64.encode64(string) -> encoded_string - # - # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+. - # - # Per RFC 2045, the returned string may contain the URL-unsafe characters - # + or /; - # see {Encoding Character Set}[Base64.html#module-Base64-label-Encoding+Character+Sets] above: - # - # Base64.encode64("\xFB\xEF\xBE") # => "++++\n" - # Base64.encode64("\xFF\xFF\xFF") # => "////\n" - # - # The returned string may include padding; - # see {Padding}[Base64.html#module-Base64-label-Padding] above. - # - # Base64.encode64('*') # => "Kg==\n" - # - # The returned string ends with a newline character, and if sufficiently long - # will have one or more embedded newline characters; - # see {Newlines}[Base64.html#module-Base64-label-Newlines] above: - # - # Base64.encode64('*') # => "Kg==\n" - # Base64.encode64('*' * 46) - # # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n" - # - # The string to be encoded may itself contain newlines, - # which will be encoded as ordinary \Base64: - # - # Base64.encode64("\n\n\n") # => "CgoK\n" - # s = "This is line 1\nThis is line 2\n" - # Base64.encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n" - # - def encode64(bin) - [bin].pack("m") - end - - # :call-seq: - # Base64.decode(encoded_string) -> decoded_string - # - # Returns a string containing the decoding of an RFC-2045-compliant - # \Base64-encoded string +encoded_string+: - # - # s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK\n" - # Base64.decode64(s) # => "This is line 1\nThis is line 2\n" - # - # Non-\Base64 characters in +encoded_string+ are ignored; - # see {Encoding Character Set}[Base64.html#module-Base64-label-Encoding+Character+Sets] above: - # these include newline characters and characters - and /: - # - # Base64.decode64("\x00\n-_") # => "" - # - # Padding in +encoded_string+ (even if incorrect) is ignored: - # - # Base64.decode64("MDEyMzQ1Njc") # => "01234567" - # Base64.decode64("MDEyMzQ1Njc=") # => "01234567" - # Base64.decode64("MDEyMzQ1Njc==") # => "01234567" - # - def decode64(str) - str.unpack1("m") - end - - # :call-seq: - # Base64.strict_encode64(string) -> encoded_string - # - # Returns a string containing the RFC-2045-compliant \Base64-encoding of +string+. - # - # Per RFC 2045, the returned string may contain the URL-unsafe characters - # + or /; - # see {Encoding Character Set}[Base64.html#module-Base64-label-Encoding+Character+Sets] above: - # - # Base64.strict_encode64("\xFB\xEF\xBE") # => "++++\n" - # Base64.strict_encode64("\xFF\xFF\xFF") # => "////\n" - # - # The returned string may include padding; - # see {Padding}[Base64.html#module-Base64-label-Padding] above. - # - # Base64.strict_encode64('*') # => "Kg==\n" - # - # The returned string will have no newline characters, regardless of its length; - # see {Newlines}[Base64.html#module-Base64-label-Newlines] above: - # - # Base64.strict_encode64('*') # => "Kg==" - # Base64.strict_encode64('*' * 46) - # # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg==" - # - # The string to be encoded may itself contain newlines, - # which will be encoded as ordinary \Base64: - # - # Base64.strict_encode64("\n\n\n") # => "CgoK" - # s = "This is line 1\nThis is line 2\n" - # Base64.strict_encode64(s) # => "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK" - # - def strict_encode64(bin) - [bin].pack("m0") - end - - # :call-seq: - # Base64.strict_decode64(encoded_string) -> decoded_string - # - # Returns a string containing the decoding of an RFC-2045-compliant - # \Base64-encoded string +encoded_string+: - # - # s = "VGhpcyBpcyBsaW5lIDEKVGhpcyBpcyBsaW5lIDIK" - # Base64.strict_decode64(s) # => "This is line 1\nThis is line 2\n" - # - # Non-\Base64 characters in +encoded_string+ are not allowed; - # see {Encoding Character Set}[Base64.html#module-Base64-label-Encoding+Character+Sets] above: - # these include newline characters and characters - and /: - # - # Base64.strict_decode64("\n") # Raises ArgumentError - # Base64.strict_decode64('-') # Raises ArgumentError - # Base64.strict_decode64('_') # Raises ArgumentError - # - # Padding in +encoded_string+, if present, must be correct: - # - # Base64.strict_decode64("MDEyMzQ1Njc") # Raises ArgumentError - # Base64.strict_decode64("MDEyMzQ1Njc=") # => "01234567" - # Base64.strict_decode64("MDEyMzQ1Njc==") # Raises ArgumentError - # - def strict_decode64(str) - str.unpack1("m0") - end - - # :call-seq: - # Base64.urlsafe_encode64(string) -> encoded_string - # - # Returns the RFC-4648-compliant \Base64-encoding of +string+. - # - # Per RFC 4648, the returned string will not contain the URL-unsafe characters - # + or /, - # but instead may contain the URL-safe characters - # - and _; - # see {Encoding Character Set}[Base64.html#module-Base64-label-Encoding+Character+Sets] above: - # - # Base64.urlsafe_encode64("\xFB\xEF\xBE") # => "----" - # Base64.urlsafe_encode64("\xFF\xFF\xFF") # => "____" - # - # By default, the returned string may have padding; - # see {Padding}[Base64.html#module-Base64-label-Padding], above: - # - # Base64.urlsafe_encode64('*') # => "Kg==" - # - # Optionally, you can suppress padding: - # - # Base64.urlsafe_encode64('*', padding: false) # => "Kg" - # - # The returned string will have no newline characters, regardless of its length; - # see {Newlines}[Base64.html#module-Base64-label-Newlines] above: - # - # Base64.urlsafe_encode64('*') # => "Kg==" - # Base64.urlsafe_encode64('*' * 46) - # # => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg==" - # - def urlsafe_encode64(bin, padding: true) - str = strict_encode64(bin) - str.chomp!("==") or str.chomp!("=") unless padding - str.tr!("+/", "-_") - str - end - - # :call-seq: - # Base64.urlsafe_decode64(encoded_string) -> decoded_string - # - # Returns the decoding of an RFC-4648-compliant \Base64-encoded string +encoded_string+: - # - # +encoded_string+ may not contain non-Base64 characters; - # see {Encoding Character Set}[Base64.html#module-Base64-label-Encoding+Character+Sets] above: - # - # Base64.urlsafe_decode64('+') # Raises ArgumentError. - # Base64.urlsafe_decode64('/') # Raises ArgumentError. - # Base64.urlsafe_decode64("\n") # Raises ArgumentError. - # - # Padding in +encoded_string+, if present, must be correct: - # see {Padding}[Base64.html#module-Base64-label-Padding], above: - # - # Base64.urlsafe_decode64("MDEyMzQ1Njc") # => "01234567" - # Base64.urlsafe_decode64("MDEyMzQ1Njc=") # => "01234567" - # Base64.urlsafe_decode64("MDEyMzQ1Njc==") # Raises ArgumentError. - # - def urlsafe_decode64(str) - # NOTE: RFC 4648 does say nothing about unpadded input, but says that - # "the excess pad characters MAY also be ignored", so it is inferred that - # unpadded input is also acceptable. - if !str.end_with?("=") && str.length % 4 != 0 - str = str.ljust((str.length + 3) & ~3, "=") - str.tr!("-_", "+/") - else - str = str.tr("-_", "+/") - end - strict_decode64(str) - end -end diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/LICENSE b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/LICENSE similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/LICENSE rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/LICENSE diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/alt_saver.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/alt_saver.rb similarity index 87% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/alt_saver.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/alt_saver.rb index aec713530c..d0c727c5cd 100644 --- a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/alt_saver.rb +++ b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/alt_saver.rb @@ -58,7 +58,7 @@ module PatchELF @out_file = out_file @set = set - f = File.open(in_file, 'rb') + f = File.open(in_file, 'rb') # rubocop:disable Style/FileOpen # the +@buffer+ and +@elf+ both could work on same +StringIO+ stream, # the updating of @buffer in place blocks us from looking up old values. # TODO: cache the values needed later, use same stream for +@buffer+ and +@elf+. @@ -130,14 +130,12 @@ module PatchELF find_section '.dynstr' end - # yields dynamic tag, and offset in buffer + # Yields dynamic tag, and its offset in @buffer. + # @yieldparam [ELFTools::Structs::ELF_Dyn] dyn + # @yieldparam [Integer] offset The offset of this dynamic tag within +@buffer+. def each_dynamic_tags - return unless block_given? - - sec = find_section '.dynamic' - return unless sec - - return if sec.header.sh_type == ELFTools::Constants::SHT_NOBITS + sec = find_section('.dynamic') + return if sec.nil? || sec.header.sh_type == ELFTools::Constants::SHT_NOBITS shdr = sec.header with_buf_at(shdr.sh_offset) do |buf| @@ -149,8 +147,8 @@ module PatchELF break if dyn.d_tag == ELFTools::Constants::DT_NULL yield dyn, buf_dyn_offset - # there's a possibility for caller to modify @buffer.pos, seek to avoid such issues - buf.seek buf_dyn_offset + dyn.num_bytes + # It's possible the caller may modify @buffer.pos, seek to ensure it points to the next tag. + buf.seek(buf_dyn_offset + dyn.num_bytes) end end end @@ -675,18 +673,13 @@ module PatchELF new_phdrs = [] while curr_off < end_off - size = 0 - sections_at_aligned_offset(curr_off) do |sec| - next if sec.type != ELFTools::Constants::SHT_NOTE + section = sections_at_aligned_offset(curr_off).find { |sec| sec.type == ELFTools::Constants::SHT_NOTE } + raise PatchError, 'cannot normalize PT_NOTE segment: non-contiguous SHT_NOTE sections' if section.nil? - size = sec.header.sh_size.to_i - curr_off = sec.header.sh_offset.to_i - break - end + size = section.header.sh_size.to_i + curr_off = section.header.sh_offset.to_i - raise PatchError, 'cannot normalize PT_NOTE segment: non-contiguous SHT_NOTE sections' if size.zero? - - if curr_off + size > end_off + if size.zero? || curr_off + size > end_off raise PatchError, 'cannot normalize PT_NOTE segment: partially mapped SHT_NOTE section' end @@ -710,6 +703,8 @@ module PatchELF end def sections_at_aligned_offset(offset) + return to_enum(__method__, offset) unless block_given? + @sections.each do |sec| shdr = sec.header @@ -743,7 +738,6 @@ module PatchELF phdr.p_vaddr -= shift if phdr.p_vaddr > shift end - # rubocop:disable Metrics/PerceivedComplexity def shift_segments(shift, start_offset) split_index = -1 split_shift = 0 @@ -752,9 +746,8 @@ module PatchELF phdr = seg.header p_start = phdr.p_offset - if p_start <= start_offset && p_start + phdr.p_filesz > start_offset && - phdr.p_type == ELFTools::Constants::PT_LOAD - raise PatchError, "split_index(#{split_index}) != -1" if split_index != -1 + if (p_start...(p_start + phdr.p_filesz)).cover?(start_offset) && phdr.p_type == ELFTools::Constants::PT_LOAD + raise PatchError, 'PT_LOAD segments overlapped, unable to shift segments' if split_index != -1 split_index = idx split_shift = start_offset - p_start @@ -775,11 +768,10 @@ module PatchELF end end - raise PatchError, "split_index(#{split_index}) == -1" if split_index == -1 + raise PatchError, "No PT_LOAD found covers offset 0x#{start_offset.to_s(16)}" if split_index == -1 [split_index, split_shift] end - # rubocop:enable Metrics/PerceivedComplexity def shift_file(extra_pages, start_offset, extra_bytes) raise PatchError, "start_offset(#{start_offset}) < ehdr.num_bytes" if start_offset < ehdr.num_bytes @@ -865,9 +857,9 @@ module PatchELF sec_name end - # given a +dyn.d_tag+, returns the section name it must be synced to. - # it may return nil, when given tag maps to no section, - # or when its okay to skip if section is not found. + # Given a +dyn.d_tag+, returns the section name it must be synced to. + # Returns +nil+ when given tag maps to no section, or when its okay to skip if section is not found. + # @return [String?] def dyn_tag_to_section_name(d_tag) case d_tag when ELFTools::Constants::DT_STRTAB, ELFTools::Constants::DT_STRSZ @@ -901,32 +893,37 @@ module PatchELF end end + # @return [ELFTools::Structs::ELF_Shdr?] + def dyn_tag_to_shdr(d_tag) + sec_name = dyn_tag_to_section_name(d_tag) + return if sec_name.nil? + + find_section(sec_name)&.header + end + # updates dyn tags by syncing it with @section values def sync_dyn_tags! + # Position of the fist dynamic tag. dyn_table_offset = nil each_dynamic_tags do |dyn, buf_off| dyn_table_offset ||= buf_off - sec_name = dyn_tag_to_section_name(dyn.d_tag) + if dyn.d_tag == ELFTools::Constants::DT_MIPS_RLD_MAP_REL + rld_map = find_section('.rld_map') + dyn.d_val = if rld_map + rld_map.header.sh_addr.to_i - (buf_off - dyn_table_offset) - + find_section('.dynamic').header.sh_addr.to_i + else + Logger.warn 'DT_MIPS_RLD_MAP_REL entry is present, but .rld_map section is not' + 0 + end + else + shdr = dyn_tag_to_shdr(dyn.d_tag) + next if shdr.nil? - unless sec_name - if dyn.d_tag == ELFTools::Constants::DT_MIPS_RLD_MAP_REL && ehdr.e_machine == ELFTools::Constants::EM_MIPS - rld_map = find_section('.rld_map') - dyn.d_val = if rld_map - rld_map.header.sh_addr.to_i - (buf_off - dyn_table_offset) - - find_section('.dynamic').header.sh_addr.to_i - else - Logger.warn 'DT_MIPS_RLD_MAP_REL entry is present, but .rld_map section is not' - 0 - end - end - - next + dyn.d_val = dyn.d_tag == ELFTools::Constants::DT_STRSZ ? shdr.sh_size.to_i : shdr.sh_addr.to_i end - shdr = find_section(sec_name).header - dyn.d_val = dyn.d_tag == ELFTools::Constants::DT_STRSZ ? shdr.sh_size.to_i : shdr.sh_addr.to_i - with_buf_at(buf_off) { |wbuf| dyn.write(wbuf) } end end @@ -945,6 +942,8 @@ module PatchELF nil end + # Some sections have their corresponding segment. + # Use this utility when a section is being patched so its segment should be updated with the same values. def sync_sec_to_seg(shdr, phdr) phdr.p_offset = shdr.sh_offset.to_i phdr.p_vaddr = phdr.p_paddr = shdr.sh_addr.to_i @@ -962,6 +961,8 @@ module PatchELF end # Returns a blank shdr if the section doesn't exist. + # + # @return [ELFTools::Structs::ELF_Shdr] def find_or_create_section_header(rsec_name) shdr = find_section(rsec_name)&.header shdr ||= ELFTools::Structs::ELF_Shdr.new(endian: endian, elf_class: elf_class) @@ -991,11 +992,54 @@ module PatchELF (s_start >= p_start && s_start < p_end) || (s_end > p_start && s_end <= p_end) end + # Used when patching sections. + # For sections with a correspondence segment, update the segment values. + # @return [void] + def section_sync_correspondence_segment(sec_name, shdr) + seg_type = { + '.interp' => ELFTools::Constants::PT_INTERP, + '.dynamic' => ELFTools::Constants::PT_DYNAMIC, + '.MIPS.abiflags' => ELFTools::Constants::PT_MIPS_ABIFLAGS, + '.note.gnu.property' => ELFTools::Constants::PT_GNU_PROPERTY + }[sec_name] + return if seg_type.nil? + + phdrs_by_type(seg_type) { |phdr| sync_sec_to_seg(shdr, phdr) } + end + + # Similar to +section_sync_correspondence_segment+ but dedicate for note sections as it is allowed to have multiple + # note segments. + # This function searches all note segments and only sync the one matched with the section values before patching. + # + # NOTE: This function is no-op if +shdr+ does not have section type +ELFTools::Constants::SHT_NOTE+. + # + # @param [Integer] orig_sh_offset The original section offset value. + # @param [Integer] orig_sh_size The original section size value. + # @param [ELFTools::Structs::ELF_Shdr] shdr The section header with values after patched. + # + # @return [void] + def sync_note_segment(orig_sh_offset, orig_sh_size, shdr) + return if shdr.sh_type != ELFTools::Constants::SHT_NOTE + + phdrs_by_type(ELFTools::Constants::PT_NOTE) do |phdr| + s_start = orig_sh_offset + s_end = s_start + orig_sh_size + p_start = phdr.p_offset + p_end = p_start + phdr.p_filesz + + # Skip if no overlap. + next unless section_bounds_within_segment?(s_start, s_end, p_start, p_end) + + # Only support exact matches. + raise PatchError, 'unsupported overlap of SHT_NOTE and PT_NOTE' unless [p_start, p_end] == [s_start, s_end] + + sync_sec_to_seg(shdr, phdr) + end + end + def write_replaced_sections(cur_off, start_addr, start_offset) overwrite_replaced_sections - noted_phdrs = Set.new - # the sort is necessary, the strategy in ruby and Cpp to iterate map/hash # is different, patchelf v0.10 iterates the replaced_sections sorted by # keys. @@ -1018,34 +1062,8 @@ module PatchELF shdr.sh_size = rsec_data.size write_section_alignment(shdr) - - seg_type = { - '.interp' => ELFTools::Constants::PT_INTERP, - '.dynamic' => ELFTools::Constants::PT_DYNAMIC, - '.MIPS.abiflags' => ELFTools::Constants::PT_MIPS_ABIFLAGS, - '.note.gnu.property' => ELFTools::Constants::PT_GNU_PROPERTY - }[rsec_name] - - phdrs_by_type(seg_type) { |phdr| sync_sec_to_seg(shdr, phdr) } - - if shdr.sh_type == ELFTools::Constants::SHT_NOTE - phdrs_by_type(ELFTools::Constants::PT_NOTE) do |phdr, idx| - next if noted_phdrs.include?(idx) - - s_start = orig_sh_offset - s_end = s_start + orig_sh_size - p_start = phdr.p_offset - p_end = p_start + phdr.p_filesz - - next unless section_bounds_within_segment?(s_start, s_end, p_start, p_end) - - raise PatchError, 'unsupported overlap of SHT_NOTE and PT_NOTE' if p_start != s_start || p_end != s_end - - sync_sec_to_seg(shdr, phdr) - - noted_phdrs << idx - end - end + section_sync_correspondence_segment(rsec_name, shdr) + sync_note_segment(orig_sh_offset, orig_sh_size, shdr) cur_off += Helper.alignup(rsec_data.size, @section_alignment) end diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/cli.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/cli.rb similarity index 99% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/cli.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/cli.rb index e6b866d7bd..e1d497356d 100644 --- a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/cli.rb +++ b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/cli.rb @@ -9,7 +9,7 @@ module PatchELF # For command line interface to parsing arguments. module CLI # Name of binary. - SCRIPT_NAME = 'patchelf.rb'.freeze + SCRIPT_NAME = 'patchelf.rb' # CLI usage string. USAGE = format('Usage: %s FILENAME [OUTPUT_FILE]', SCRIPT_NAME).freeze diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/exceptions.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/exceptions.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/exceptions.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/exceptions.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/helper.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/helper.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/helper.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/helper.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/logger.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/logger.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/logger.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/logger.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/mm.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/mm.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/mm.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/mm.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/patcher.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/patcher.rb similarity index 99% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/patcher.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/patcher.rb index 8a789f3db0..f1c6c42857 100644 --- a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/patcher.rb +++ b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/patcher.rb @@ -29,7 +29,7 @@ module PatchELF # :silent = ignore the errors def initialize(filename, on_error: :log, logging: true) @in_file = filename - f = File.open(filename) + f = File.open(filename) # rubocop:disable Style/FileOpen @elf = ELFTools::ELFFile.new(f) @set = {} @rpath_sym = :runpath diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/saver.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/saver.rb similarity index 99% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/saver.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/saver.rb index e43408bda5..d409c78e1f 100644 --- a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/saver.rb +++ b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/saver.rb @@ -32,7 +32,7 @@ module PatchELF @set = set # [{Integer => String}] @inline_patch = {} - f = File.open(in_file) + f = File.open(in_file) # rubocop:disable Style/FileOpen @elf = ELFTools::ELFFile.new(f) @mm = PatchELF::MM.new(@elf) @strtab_extend_requests = [] diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/version.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/version.rb similarity index 73% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/version.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/version.rb index dce0a5d1c8..476705a29e 100644 --- a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.5.2/lib/patchelf/version.rb +++ b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/patchelf-1.6.2/lib/patchelf/version.rb @@ -2,5 +2,5 @@ module PatchELF # Current gem version. - VERSION = '1.5.2'.freeze + VERSION = '1.6.2' end diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/sorbet-runtime.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/sorbet-runtime.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/sorbet-runtime.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/sorbet-runtime.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/_types.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/_types.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/_types.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/_types.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/abstract_utils.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/abstract_utils.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/abstract_utils.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/abstract_utils.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/boolean.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/boolean.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/boolean.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/boolean.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/compatibility_patches.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/compatibility_patches.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/compatibility_patches.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/compatibility_patches.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/configuration.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/configuration.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/configuration.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/configuration.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/def_mods.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/def_mods.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/def_mods.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/def_mods.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/enum.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/enum.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/enum.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/enum.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/generic.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/generic.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/generic.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/generic.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/helpers.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/helpers.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/helpers.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/helpers.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/data.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/data.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/data.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/data.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/declare.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/declare.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/declare.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/declare.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/hooks.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/hooks.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/hooks.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/hooks.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/validate.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/validate.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/abstract/validate.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/abstract/validate.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/caller_utils.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/caller_utils.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/caller_utils.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/caller_utils.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/casts.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/casts.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/casts.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/casts.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/class_utils.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/class_utils.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/class_utils.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/class_utils.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/decl_state.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/decl_state.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/decl_state.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/decl_state.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/final.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/final.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/final.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/final.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/_methods.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/_methods.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/_methods.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/_methods.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/call_validation.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/call_validation.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/call_validation.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/call_validation.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/call_validation_2_7.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/call_validation_2_7.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/call_validation_2_7.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/call_validation_2_7.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/decl_builder.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/decl_builder.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/decl_builder.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/decl_builder.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/modes.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/modes.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/modes.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/modes.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/signature.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/signature.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/signature.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/signature.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/signature_validation.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/signature_validation.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/methods/signature_validation.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/methods/signature_validation.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/mixins/mixins.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/mixins/mixins.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/mixins/mixins.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/mixins/mixins.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/retry.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/retry.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/retry.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/retry.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/runtime_levels.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/runtime_levels.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/runtime_levels.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/runtime_levels.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/sealed.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/sealed.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/sealed.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/sealed.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/not_typed.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/not_typed.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/not_typed.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/not_typed.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/simple_pair_union.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/simple_pair_union.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/simple_pair_union.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/simple_pair_union.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/string_holder.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/string_holder.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/string_holder.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/string_holder.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/type_alias.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/type_alias.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/type_alias.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/type_alias.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/void.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/void.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/private/types/void.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/private/types/void.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/_props.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/_props.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/_props.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/_props.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/constructor.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/constructor.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/constructor.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/constructor.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/custom_type.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/custom_type.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/custom_type.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/custom_type.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/decorator.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/decorator.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/decorator.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/decorator.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/errors.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/errors.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/errors.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/errors.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/generated_code_validation.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/generated_code_validation.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/generated_code_validation.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/generated_code_validation.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/has_lazily_specialized_methods.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/has_lazily_specialized_methods.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/has_lazily_specialized_methods.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/has_lazily_specialized_methods.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/optional.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/optional.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/optional.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/optional.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/plugin.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/plugin.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/plugin.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/plugin.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/pretty_printable.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/pretty_printable.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/pretty_printable.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/pretty_printable.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/apply_default.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/apply_default.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/apply_default.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/apply_default.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/deserializer_generator.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/deserializer_generator.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/deserializer_generator.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/deserializer_generator.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/parser.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/parser.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/parser.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/parser.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/serde_transform.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/serde_transform.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/serde_transform.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/serde_transform.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/serializer_generator.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/serializer_generator.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/serializer_generator.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/serializer_generator.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/setter_factory.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/setter_factory.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/private/setter_factory.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/private/setter_factory.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/serializable.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/serializable.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/serializable.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/serializable.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/type_validation.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/type_validation.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/type_validation.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/type_validation.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/utils.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/utils.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/utils.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/utils.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/weak_constructor.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/weak_constructor.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/props/weak_constructor.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/props/weak_constructor.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/sig.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/sig.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/sig.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/sig.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/struct.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/struct.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/struct.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/struct.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/syntax.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/syntax.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/syntax.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/syntax.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/anything.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/anything.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/anything.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/anything.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/attached_class.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/attached_class.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/attached_class.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/attached_class.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/base.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/base.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/base.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/base.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/class_of.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/class_of.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/class_of.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/class_of.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/enum.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/enum.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/enum.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/enum.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/fixed_array.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/fixed_array.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/fixed_array.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/fixed_array.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/fixed_hash.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/fixed_hash.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/fixed_hash.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/fixed_hash.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/intersection.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/intersection.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/intersection.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/intersection.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/noreturn.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/noreturn.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/noreturn.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/noreturn.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/proc.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/proc.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/proc.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/proc.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/self_type.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/self_type.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/self_type.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/self_type.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/simple.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/simple.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/simple.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/simple.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/t_enum.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/t_enum.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/t_enum.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/t_enum.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_member.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_member.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_member.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_member.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_parameter.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_parameter.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_parameter.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_parameter.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_template.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_template.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_template.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_template.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_variable.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_variable.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/type_variable.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/type_variable.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_array.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_array.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_array.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_array.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_class.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_class.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_class.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_class.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerable.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerable.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerable.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerable.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerator.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerator.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerator.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerator.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerator_chain.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerator_chain.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerator_chain.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerator_chain.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerator_lazy.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerator_lazy.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_enumerator_lazy.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_enumerator_lazy.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_hash.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_hash.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_hash.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_hash.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_module.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_module.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_module.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_module.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_range.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_range.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_range.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_range.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_set.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_set.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/typed_set.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/typed_set.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/union.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/union.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/union.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/union.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/untyped.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/untyped.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/types/untyped.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/types/untyped.rb diff --git a/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/utils.rb b/Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/utils.rb similarity index 100% rename from Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13359/lib/types/utils.rb rename to Library/Homebrew/vendor/bundle/ruby/4.0.0/gems/sorbet-runtime-0.6.13365/lib/types/utils.rb diff --git a/completions/bash/brew b/completions/bash/brew index 0bf149a62c..d2a324d357 100644 --- a/completions/bash/brew +++ b/completions/bash/brew @@ -2810,22 +2810,6 @@ _brew_setup_ruby() { __brew_complete_commands } -_brew_setup_sandbox() { - local cur="${COMP_WORDS[COMP_CWORD]}" - case "${cur}" in - -*) - __brewcomp " - --debug - --help - --quiet - --verbose - " - return - ;; - *) ;; - esac -} - _brew_sh() { local cur="${COMP_WORDS[COMP_CWORD]}" case "${cur}" in @@ -3842,7 +3826,6 @@ _brew() { search) _brew_search ;; services) _brew_services ;; setup-ruby) _brew_setup_ruby ;; - setup-sandbox) _brew_setup_sandbox ;; sh) _brew_sh ;; shellenv) _brew_shellenv ;; source) _brew_source ;; diff --git a/completions/fish/brew.fish b/completions/fish/brew.fish index deea33bfd4..6a4f1967ee 100644 --- a/completions/fish/brew.fish +++ b/completions/fish/brew.fish @@ -713,7 +713,7 @@ __fish_brew_complete_sub_arg 'bundle' 'cleanup' -l cask -d 'Clean up Homebrew ca __fish_brew_complete_sub_arg 'bundle' 'cleanup' -l debug -d 'Display any debugging information' __fish_brew_complete_sub_arg 'bundle' 'cleanup' -l file -d 'Read from or write to the `Brewfile` from this location. Use `--file=-` to pipe to stdin/stdout' __fish_brew_complete_sub_arg 'bundle' 'cleanup' -l flatpak -d 'Clean up Flatpak packages. Note: Linux only' -__fish_brew_complete_sub_arg 'bundle' 'cleanup' -l force -d 'Actually perform cleanup operations' +__fish_brew_complete_sub_arg 'bundle' 'cleanup' -l force -d 'Actually perform cleanup operations and reset Homebrew\'s global trust store to the `Brewfile` values' __fish_brew_complete_sub_arg 'bundle' 'cleanup' -l formula -d 'Clean up Homebrew formula dependencies' __fish_brew_complete_sub_arg 'bundle' 'cleanup' -l global -d 'Read from or write to the `Brewfile` from `$HOMEBREW_BUNDLE_FILE_GLOBAL` (if set), `${XDG_CONFIG_HOME}/homebrew/Brewfile` (if `$XDG_CONFIG_HOME` is set), `~/.homebrew/Brewfile` or `~/.Brewfile` otherwise' __fish_brew_complete_sub_arg 'bundle' 'cleanup' -l go -d 'Clean up Go packages' @@ -876,7 +876,7 @@ __fish_brew_complete_arg 'contributions' -l quiet -d 'Make some output more quie __fish_brew_complete_arg 'contributions' -l repositories -d 'Specify a comma-separated list of repositories to search. All repositories must be under the same user or organisation. Omitting this flag, or specifying `--repositories=primary`, searches only the main repositories: `Homebrew/brew`, `Homebrew/homebrew-core`, `Homebrew/homebrew-cask`' __fish_brew_complete_arg 'contributions' -l team -d 'Specify the team to populate users from. The first part of the team name will be used as the organisation' __fish_brew_complete_arg 'contributions' -l to -d 'Date (ISO 8601 format) to stop searching contributions' -__fish_brew_complete_arg 'contributions' -l user -d 'Specify a comma-separated list of GitHub usernames or email addresses to find contributions from. Omitting this flag searches Homebrew maintainers. With `--maintainer-report-csv`, only matching quarter-end Maintainers are included' +__fish_brew_complete_arg 'contributions' -l user -d 'Specify a comma-separated list of GitHub usernames or email addresses to find contributions from. Omitting this flag searches Homebrew maintainers and requires access to the `Homebrew/maintainers` team. With `--maintainer-report-csv`, only matching quarter-end Maintainers are included' __fish_brew_complete_arg 'contributions' -l verbose -d 'Make some output more verbose' @@ -1804,13 +1804,6 @@ __fish_brew_complete_arg 'setup-ruby' -l verbose -d 'Make some output more verbo __fish_brew_complete_arg 'setup-ruby' -a '(__fish_brew_suggest_commands)' -__fish_brew_complete_cmd 'setup-sandbox' 'Run any necessary commands to setup the Homebrew sandbox' -__fish_brew_complete_arg 'setup-sandbox' -l debug -d 'Display any debugging information' -__fish_brew_complete_arg 'setup-sandbox' -l help -d 'Show this message' -__fish_brew_complete_arg 'setup-sandbox' -l quiet -d 'Make some output more quiet' -__fish_brew_complete_arg 'setup-sandbox' -l verbose -d 'Make some output more verbose' - - __fish_brew_complete_cmd 'sh' 'Enter an interactive shell for Homebrew\'s build environment' __fish_brew_complete_arg 'sh' -l cmd -d 'Execute commands in a non-interactive shell' __fish_brew_complete_arg 'sh' -l debug -d 'Display any debugging information' diff --git a/completions/internal_commands_list.txt b/completions/internal_commands_list.txt index d2df9507f2..7a37ff8baf 100644 --- a/completions/internal_commands_list.txt +++ b/completions/internal_commands_list.txt @@ -76,7 +76,6 @@ sandbox-exec search services setup-ruby -setup-sandbox sh shellenv source diff --git a/completions/zsh/_brew b/completions/zsh/_brew index fd240f4ff8..481163c77b 100644 --- a/completions/zsh/_brew +++ b/completions/zsh/_brew @@ -234,7 +234,6 @@ __brew_internal_commands() { 'search:Perform a substring search of cask tokens and formula names for text' 'services:Manage background services with macOS'\'' `launchctl`(1) daemon manager or Linux'\''s `systemctl`(1) service manager' 'setup-ruby:Installs and configures Homebrew'\''s Ruby' - 'setup-sandbox:Run any necessary commands to setup the Homebrew sandbox' 'sh:Enter an interactive shell for Homebrew'\''s build environment' 'shellenv:Valid shells: bash|csh|fish|pwsh|sh|tcsh|zsh Print export statements' 'source:Open a formula'\''s source repository in a browser, or open Homebrew'\''s own repository if no argument is provided' @@ -926,7 +925,7 @@ cleanup) '--debug[Display any debugging information]' \ '(--global)--file[Read from or write to the `Brewfile` from this location. Use `--file=-` to pipe to stdin/stdout]' \ '(--no-flatpak --no-cleanup-flatpak --no-dump-flatpak)--flatpak[Clean up Flatpak packages. Note: Linux only]' \ - '--force[Actually perform cleanup operations]' \ + '--force[Actually perform cleanup operations and reset Homebrew'\''s global trust store to the `Brewfile` values]' \ '(--no-formula --no-formulae --no-brews --no-cleanup-brew --no-dump-brew)--formula[Clean up Homebrew formula dependencies]' \ '(--file)--global[Read from or write to the `Brewfile` from `$HOMEBREW_BUNDLE_FILE_GLOBAL` (if set), `${XDG_CONFIG_HOME}/homebrew/Brewfile` (if `$XDG_CONFIG_HOME` is set), `~/.homebrew/Brewfile` or `~/.Brewfile` otherwise]' \ '(--no-go --no-cleanup-go --no-dump-go)--go[Clean up Go packages]' \ @@ -1150,7 +1149,7 @@ _brew_contributions() { '(--organisation --maintainer-report-csv)--repositories[Specify a comma-separated list of repositories to search. All repositories must be under the same user or organisation. Omitting this flag, or specifying `--repositories=primary`, searches only the main repositories: `Homebrew/brew`, `Homebrew/homebrew-core`, `Homebrew/homebrew-cask`]' \ '(--organisation --user --maintainer-report-csv)--team[Specify the team to populate users from. The first part of the team name will be used as the organisation]' \ '(--maintainer-report-csv)--to[Date (ISO 8601 format) to stop searching contributions]' \ - '(--team)--user[Specify a comma-separated list of GitHub usernames or email addresses to find contributions from. Omitting this flag searches Homebrew maintainers. With `--maintainer-report-csv`, only matching quarter-end Maintainers are included]' \ + '(--team)--user[Specify a comma-separated list of GitHub usernames or email addresses to find contributions from. Omitting this flag searches Homebrew maintainers and requires access to the `Homebrew/maintainers` team. With `--maintainer-report-csv`, only matching quarter-end Maintainers are included]' \ '--verbose[Make some output more verbose]' } @@ -2325,15 +2324,6 @@ _brew_setup_ruby() { '*:command:__brew_commands' } -# brew setup-sandbox -_brew_setup_sandbox() { - _arguments \ - '--debug[Display any debugging information]' \ - '--help[Show this message]' \ - '--quiet[Make some output more quiet]' \ - '--verbose[Make some output more verbose]' -} - # brew sh _brew_sh() { _arguments \ diff --git a/docs/Brew-Bundle-and-Brewfile.md b/docs/Brew-Bundle-and-Brewfile.md index f87a6fd0b0..6c571083b5 100644 --- a/docs/Brew-Bundle-and-Brewfile.md +++ b/docs/Brew-Bundle-and-Brewfile.md @@ -152,6 +152,11 @@ Uninstalling gcc... (1,914 files, 459.8MB) Uninstalled 1 formula ``` +Cleanup also makes Homebrew's global trust store match the selected `Brewfile`. +It removes trust entries granted manually or by another `Brewfile` if they are +not declared in the selected file. A `Brewfile` with no trust declarations +removes every explicit trust entry. + ### `brew bundle list` If you want to get a list of all the formulae in your `Brewfile`, you can use: @@ -391,9 +396,12 @@ trusted `brew`, `cask` and whole-tap entries. It writes tap-level trust hashes for trusted formulae, casks and commands from a tap that are not otherwise present in the dumped `Brewfile`. -When `brew bundle cleanup --force` runs, it resets Homebrew's tap trust file to -the trust values declared by the `Brewfile` and removes trust entries that are -not declared there. +Whenever `brew bundle cleanup` performs cleanup, either because `--force` was +passed or the confirmation prompt was accepted, it resets Homebrew's global +trust store to the values declared by the selected `Brewfile`. This removes +trust granted manually or by another `Brewfile` when it is not declared in the +selected file. If the selected `Brewfile` has no trust declarations, every +explicit trust entry is removed. ## Versions diff --git a/docs/Cask-Cookbook.md b/docs/Cask-Cookbook.md index 456f61203f..71c521985d 100644 --- a/docs/Cask-Cookbook.md +++ b/docs/Cask-Cookbook.md @@ -49,7 +49,7 @@ Having a common order for stanzas makes casks easier to update and parse. Below version sha256 - on_ # blocks may be any supported macOS release (descending from oldest), `macos`, or `linux` + on_ # arm, intel, supported macOS releases (oldest first), macos, then linux language diff --git a/docs/Formula-Cookbook.md b/docs/Formula-Cookbook.md index c516a7c1a9..45785dac95 100644 --- a/docs/Formula-Cookbook.md +++ b/docs/Formula-Cookbook.md @@ -1075,8 +1075,8 @@ class Foo < Formula url "https://example.com/foo-1.0.tar.gz" post_install_steps do - mkdir_p "log/foo" - touch "foo/state" + mkdir_p "log/foo", base: :var + touch "foo/state", base: :var move "default.conf", "foo/default.conf" symlink "cert.pem", "foo/cert.pem", source_base: :relative end @@ -1091,35 +1091,35 @@ represented by structured steps. #### File preparation steps -`mkdir_p`, `touch`, `remove`, `inreplace`, `write_file`, `set_permissions` and guard paths default to paths relative to `var`. File steps with separate sources and targets default both to `prefix`. Use `base:`, `source_base:` or `target_base:` when a step needs another formula path such as `pkgetc`; use `source_base: :relative` for relative symlink sources. +Specify `base:` for paths such as `var`, `etc` or `pkgetc`. File steps with separate source and target paths default those paths to `prefix`; use `source_base:` or `target_base:` to select another formula path and `source_base: :relative` for relative symlink sources. -* `mkdir_p`: create a directory and any missing parents; example: `mkdir_p "log/foo"`. -* `touch`: create or update a file timestamp; example: `touch "foo/state"`. +* `mkdir_p`: create a directory and any missing parents; example: `mkdir_p "log/foo", base: :var`. +* `touch`: create or update a file timestamp; example: `touch "foo/state", base: :var`. * `move`: move one file or directory; example: `move "default.conf", "foo/default.conf"`. * `move_contents`: move the contents of one directory into another; example: `move_contents "defaults", "foo/defaults"`. * `copy`: copy a file or, with `recursive: true`, a directory; example: `copy "default.conf", "foo/default.conf"`. -* `remove`: remove one or more paths; example: `remove ["old.conf", "foo/*.bak"]`. Use `recursive: true` for directories. -* `inreplace`: replace a string or regular expression in a file; example: `inreplace "foo.conf", "@PREFIX@", "{{HOMEBREW_PREFIX}}"`. Its `audit_result:` and `global:` options match the formula `inreplace` helper. +* `remove`: remove one or more paths; example: `remove ["old.conf", "foo/*.bak"], base: :var`. Use `recursive: true` for directories. +* `inreplace`: replace a string or regular expression in a file; example: `inreplace "foo.conf", "@PREFIX@", "{{HOMEBREW_PREFIX}}", base: :etc`. Its `audit_result:` and `global:` options match the formula `inreplace` helper. * `symlink`: create a symlink; example: `symlink "cert.pem", "foo/cert.pem", source_base: :relative`. * `symlink_tree`: recursively symlink a directory's contents while preserving real target directories. * `symlink_children`: symlink each direct child of a directory, optionally adding a `prefix:` or `suffix:`. -* `set_permissions`: change existing path permissions; example: `set_permissions "foo", "0755"`. +* `set_permissions`: change existing path permissions; example: `set_permissions "foo", "0755", base: :var`. * `change_dylib_id`: change one Mach-O dynamic library ID; pass the complete source and new ID, use `resolve_source: true` for a source symlink and wrap the step in `on_macos`. `move` and `copy` replace an existing target by default, matching the corresponding file helpers; pass `overwrite: false` to reject replacement. `symlink` preserves an existing target by default, so pass `overwrite: true` when replacement is intentional. `move` and `copy` accept `source_glob: true` when the glob must resolve to exactly one source; `symlink` accepts one or more matching sources. Path collections used by `remove` and `set_permissions` expand globs automatically. `remove` ignores missing paths and may additionally be restricted with `symlink_target_contains:` or `content_contains:`. It accepts `sudo: true` or `sudo: :if_needed` for paths requiring elevated permissions. -Use `if_path_exists`, `unless_path_exists`, `on_macos` and `on_linux` blocks to guard one or more steps. A condition is evaluated once when its scope begins, so related steps make the same decision: +Use `if_path_exists`, `unless_path_exists`, `on_macos` and `on_linux` blocks to guard one or more steps. Specify the guard path's `base:` explicitly. A condition is evaluated once when its scope begins, so related steps make the same decision: ```ruby -unless_path_exists "foo/default.conf" do - mkdir_p "foo" +unless_path_exists "foo/default.conf", base: :var do + mkdir_p "foo", base: :var copy "default.conf", "foo/default.conf" end ``` #### Default config and template steps -`write_file` atomically writes its exact literal content, replacing an existing file. It defaults to the same base as the other file preparation steps; pass `base:` (such as `base: :etc`) to target another formula path. Use `unless_path_exists` when a default file should preserve user edits across upgrades: +`write_file` atomically writes its exact literal content, replacing an existing file. Specify its `base:`, such as `base: :etc`. Use `unless_path_exists` when a default file should preserve user edits across upgrades: ```ruby unless_path_exists "foo.conf", base: :etc do @@ -1171,18 +1171,18 @@ Use the named actions below for formula families that share post-install algorit `init_data_dir` creates a database service data directory and runs a supported bootstrap command unless the directory already contains the default marker -file. It defaults to paths relative to `var` and skips the bootstrap command in -Homebrew's GitHub Actions jobs. It does not change permissions or ownership. +file. Specify its `base:`, normally `base: :var`. It skips the bootstrap command +in Homebrew's GitHub Actions jobs and does not change permissions or ownership. * `init_data_dir` with `using: :postgresql`: initialise PostgreSQL with - `initdb`; example: `init_data_dir "postgresql@16", using: :postgresql`. + `initdb`; example: `init_data_dir "postgresql@16", using: :postgresql, base: :var`. PostgreSQL defaults to `locale: "en_US.UTF-8"` and can set another locale, - for example `init_data_dir "postgresql@12", using: :postgresql, locale: "C"`. + for example `init_data_dir "postgresql@12", using: :postgresql, locale: "C", base: :var`. * `init_data_dir` with `using: :mysql`: initialise MySQL with `mysqld --initialize-insecure`; example: - `init_data_dir "mysql", using: :mysql`. + `init_data_dir "mysql", using: :mysql, base: :var`. * `init_data_dir` with `using: :mariadb`: initialise MariaDB with - `mysql_install_db`; example: `init_data_dir "mysql", using: :mariadb`. + `mysql_install_db`; example: `init_data_dir "mysql", using: :mariadb, base: :var`. `symlink_tree` recursively links a source directory's contents into a target directory, preserving existing real directories and skipping `.DS_Store` files. diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index d0222310ae..a805f00137 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -5,7 +5,7 @@ GEM addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) afm (1.0.0) - async (2.44.0) + async (2.44.1) console (~> 1.29) fiber-annotation io-event (~> 1.11) @@ -20,7 +20,7 @@ GEM fiber-annotation fiber-local (~> 1.1) json - csv (3.3.5) + csv (3.3.6) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) @@ -51,7 +51,7 @@ GEM bigdecimal rake (~> 13.3) hashery (2.1.2) - html-proofer (5.2.1) + html-proofer (5.2.2) addressable (~> 2.3) async (~> 2.1) benchmark (~> 0.5) @@ -156,7 +156,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (4.0.3) + rbs (4.1.2) logger prism (>= 1.6.0) tsort @@ -179,7 +179,7 @@ GEM google-protobuf (~> 4.31) sass-embedded (1.102.0-x86_64-linux-gnu) google-protobuf (~> 4.31) - sorbet-runtime (0.6.13359) + sorbet-runtime (0.6.13403) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) tomlrb (2.0.4) @@ -195,7 +195,7 @@ GEM sorbet-runtime yard yell (2.2.2) - zeitwerk (2.8.2) + zeitwerk (2.8.3) PLATFORMS aarch64-linux-gnu @@ -223,7 +223,7 @@ CHECKSUMS Ascii85 (2.0.1) sha256=15cb5d941808543cbb9e7e6aea3c8ec3877f154c3461e8b3673e97f7ecedbe5a addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af afm (1.0.0) sha256=5bd4d6f6241e7014ef090985ec6f4c3e9745f6de0828ddd58bc1efdd138f4545 - async (2.44.0) sha256=888e0edda9643fac7bbdfd3073fee1713d62a586dbebaefbcc579e505c166a5a + async (2.44.1) sha256=079bce0d019fb27cc58ec110fd9b9af4991d97b801d1c6cc6def1d1954dcf9b2 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218 @@ -231,7 +231,7 @@ CHECKSUMS colorator (1.1.0) sha256=e2f85daf57af47d740db2a32191d1bdfb0f6503a0dfbc8327d0c9154d5ddfc38 concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 console (1.37.0) sha256=8093b286c2595a063849c098594fee5155ecc7967d36f3aa8cbe7569c8f3efd7 - csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 em-websocket (0.5.3) sha256=f56a92bde4e6cb879256d58ee31f124181f68f8887bd14d53d5d9a292758c6a8 erb (6.0.6) sha256=a9b24986700f5bf127c4f297c5403c3ca41b83b0a316c0cd09a096b56e644ae5 ethon (0.18.0) sha256=b598afc9f30448cb068b850714b7d6948e941476095d04f90a4ac65b8d6efcb2 @@ -249,7 +249,7 @@ CHECKSUMS google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd hashery (2.1.2) sha256=d239cc2310401903f6b79d458c2bbef5bf74c46f3f974ae9c1061fb74a404862 - html-proofer (5.2.1) sha256=fdd958a7cbf9c3255fb96fe7cfc4e611f64e2706e469488a3326309ad007d2fd + html-proofer (5.2.2) sha256=6e5e63c89ef9413246eeffaeba8dd03db62546992d7a2b85d166ac2a5903a241 http_parser.rb (0.8.1) sha256=9ae8df145b39aa5398b2f90090d651c67bd8e2ebfe4507c966579f641e11097a i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc @@ -290,7 +290,7 @@ CHECKSUMS rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e - rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14 + rbs (4.1.2) sha256=050eb1d8b508f1233bed929c0f2c7052302f7adf295230d9cb314e9024078f48 rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 @@ -301,7 +301,7 @@ CHECKSUMS sass-embedded (1.102.0-arm64-darwin) sha256=8e18b2dd94b232c2dd73984c579f77bbc1a066028f4675c8989ac34fa40d14dc sass-embedded (1.102.0-x86_64-darwin) sha256=3e78c6ac5be0ba4236d075bd044a49d0f515d54799f7d6f253c71d325ffedac0 sass-embedded (1.102.0-x86_64-linux-gnu) sha256=9815b7604123a9a44a65d44d5e58e7af1c7005174b745cda83bad4c9e3760ab3 - sorbet-runtime (0.6.13359) sha256=5edcd198c678d69f07ec2758b9d0f277bce2e322efd7d5bd902a34c1056a2e5a + sorbet-runtime (0.6.13403) sha256=8545fc4cd55fbf820f2000f6d13c0ea1c2e5f7b67369e76ebf39fea81fc490b3 terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 tomlrb (2.0.4) sha256=262f77947ac3ac9b3366a0a5940ecd238300c553e2e14f22009e2afcd2181b99 tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f @@ -312,7 +312,7 @@ CHECKSUMS yard (0.9.45) sha256=52e211493f7cb8a3ebf7e104a25a1e73937a3103092545d34cb88fafebb3dc51 yard-sorbet (0.9.0) sha256=03d1aa461b9e9c82b886919a13aa3e09fcf4d1852239d2967ed97e92723ffe21 yell (2.2.2) sha256=1d166f3cc3b6dc49a59778ea7156ed6d8de794c15106d48ffd6cbb061b9b26bc - zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 + zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5 RUBY VERSION ruby 4.0.6 diff --git a/docs/MCP-Server.md b/docs/MCP-Server.md index 64ba6e47cf..2f29e5c4d4 100644 --- a/docs/MCP-Server.md +++ b/docs/MCP-Server.md @@ -22,7 +22,7 @@ Press Ctrl-D or Ctrl-C to terminate it. ## Configuration -### Example configuration for [Cursor](https://www.cursor.com/) +### Example configuration for [Cursor](https://cursor.com/) ```json { diff --git a/docs/Manpage.md b/docs/Manpage.md index 166f5b02ba..039d6c444e 100644 --- a/docs/Manpage.md +++ b/docs/Manpage.md @@ -613,6 +613,9 @@ flags which will help with finding keg-only dependencies like `openssl`, This workflow is useful for maintainers or testers who regularly install lots of formulae. +When cleanup is performed, Homebrew's global trust store is reset to the trust +values declared by the `Brewfile`, removing trust entries not declared there. + Unless `--force` is passed, this prompts before removing anything and returns a 1 exit code if the prompt is declined or cannot be shown. @@ -622,7 +625,8 @@ Unless `--force` is passed, this prompts before removing anything and returns a `-f`, `--force` -: Actually perform cleanup operations. +: Actually perform cleanup operations and reset Homebrew's global trust store to + the `Brewfile` values. `--all` @@ -2112,11 +2116,6 @@ line; lines starting with `#` are comments. Changes take effect on the next Installs and configures Homebrew's Ruby. If `command` is passed, it will only run Bundler if necessary for that command. -### `setup-sandbox` - -Run any necessary commands to setup the Homebrew sandbox. Must be run with -`sudo`. Currently a no-op on non-Linux. - ### `shellenv` \[*`shell`* ...\] Valid shells: bash\|csh\|fish\|pwsh\|sh\|tcsh\|zsh @@ -3162,7 +3161,8 @@ Summarise contributions to Homebrew repositories. `--user` : Specify a comma-separated list of GitHub usernames or email addresses to find - contributions from. Omitting this flag searches Homebrew maintainers. With + contributions from. Omitting this flag searches Homebrew maintainers and + requires access to the `Homebrew/maintainers` team. With `--maintainer-report-csv`, only matching quarter-end Maintainers are included. `--repositories` diff --git a/docs/bin/jekyll b/docs/bin/jekyll index 7fd37449b4..a6f082e955 100755 --- a/docs/bin/jekyll +++ b/docs/bin/jekyll @@ -6,10 +6,8 @@ set -euo pipefail -if [[ "${HOMEBREW_INSIDE_BUNDLE:-}" != "1" ]]; then - export HOMEBREW_NO_AUTO_UPDATE=1 - eval "$(brew bundle --check --install env)" -fi +source "$(dirname "${BASH_SOURCE[0]}")/../script/bootstrap" +bundle_bootstrap export JEKYLL_ENV="${JEKYLL_ENV:-development}" diff --git a/docs/script/bootstrap b/docs/script/bootstrap new file mode 100755 index 0000000000..e1fede715d --- /dev/null +++ b/docs/script/bootstrap @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +bundle_bootstrap() { + if [[ "${HOMEBREW_INSIDE_BUNDLE:-}" != "1" ]]; then + export HOMEBREW_NO_AUTO_UPDATE=1 + eval "$(brew bundle --check --install env)" + fi + + # Share installed gems between worktrees belonging to the same repository. + local shared_bundle_path + shared_bundle_path="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")/vendor/bundle" + export BUNDLE_AUTO_INSTALL=1 + export BUNDLE_CLEAN=false + export BUNDLE_PATH="${shared_bundle_path}" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + set -euo pipefail + bundle_bootstrap + bundle check || bundle install +fi diff --git a/manpages/brew.1 b/manpages/brew.1 index e469626065..f8f54c9d87 100644 --- a/manpages/brew.1 +++ b/manpages/brew.1 @@ -395,13 +395,15 @@ Uninstall all dependencies not present in the \fBBrewfile\fP\&\. .P This workflow is useful for maintainers or testers who regularly install lots of formulae\. .P +When cleanup is performed, Homebrew\[u2019]s global trust store is reset to the trust values declared by the \fBBrewfile\fP, removing trust entries not declared there\. +.P Unless \fB\-\-force\fP is passed, this prompts before removing anything and returns a 1 exit code if the prompt is declined or cannot be shown\. .TP \fB\-\-install\fP Run \fBinstall\fP before cleaning up dependencies\. .TP \fB\-f\fP, \fB\-\-force\fP -Actually perform cleanup operations\. +Actually perform cleanup operations and reset Homebrew\[u2019]s global trust store to the \fBBrewfile\fP values\. .TP \fB\-\-all\fP Clean up all supported dependencies\. @@ -1344,8 +1346,6 @@ Output as JSON\. Remove all unused services\. .SS "\fBsetup\-ruby\fP \fR[\fIcommand\fP \.\.\.]" Installs and configures Homebrew\[u2019]s Ruby\. If \fBcommand\fP is passed, it will only run Bundler if necessary for that command\. -.SS "\fBsetup\-sandbox\fP" -Run any necessary commands to setup the Homebrew sandbox\. Must be run with \fBsudo\fP\&\. Currently a no\-op on non\-Linux\. .SS "\fBshellenv\fP \fR[\fIshell\fP \.\.\.]" Valid shells: bash|csh|fish|pwsh|sh|tcsh|zsh .P @@ -2004,7 +2004,7 @@ Treat all named arguments as casks\. Summarise contributions to Homebrew repositories\. .TP \fB\-\-user\fP -Specify a comma\-separated list of GitHub usernames or email addresses to find contributions from\. Omitting this flag searches Homebrew maintainers\. With \fB\-\-maintainer\-report\-csv\fP, only matching quarter\-end Maintainers are included\. +Specify a comma\-separated list of GitHub usernames or email addresses to find contributions from\. Omitting this flag searches Homebrew maintainers and requires access to the \fBHomebrew/maintainers\fP team\. With \fB\-\-maintainer\-report\-csv\fP, only matching quarter\-end Maintainers are included\. .TP \fB\-\-repositories\fP Specify a comma\-separated list of repositories to search\. All repositories must be under the same user or organisation\. Omitting this flag, or specifying \fB\-\-repositories=primary\fP, searches only the main repositories: \fBHomebrew/brew\fP, \fBHomebrew/homebrew\-core\fP, \fBHomebrew/homebrew\-cask\fP\&\.