Merge branch 'main' into vulns-fix-available-flag

This commit is contained in:
Andrew Nesbitt
2026-08-05 08:33:53 +01:00
committed by GitHub
288 changed files with 4241 additions and 3323 deletions
+2
View File
@@ -65,6 +65,8 @@ updates:
- dependency-type: all
cooldown:
default-days: 7
exclude:
- Homebrew/actions/*
- package-ecosystem: uv
directories:
- "/Library/Homebrew/formula-analytics/"
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -9
View File
@@ -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 <<COMMENT
<!-- incomplete-pr-template -->
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
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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 }}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+5 -5
View File
@@ -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: >
+5 -5
View File
@@ -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: >
+5 -5
View File
@@ -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: >
@@ -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: >
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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]
+42
View File
@@ -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"
+13 -3
View File
@@ -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
+9 -2
View File
@@ -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 }}
+15 -17
View File
@@ -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
+3 -3
View File
@@ -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 }}
+1 -1
View File
@@ -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
+1
View File
@@ -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-*/
-1
View File
@@ -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 \
+8
View File
@@ -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"
-1
View File
@@ -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"
+23 -24
View File
@@ -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
+74 -17
View File
@@ -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]) }
+1 -1
View File
@@ -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
@@ -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.
+3 -3
View File
@@ -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
+177 -67
View File
@@ -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
+299
View File
@@ -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 `"<name>":` 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 `"<key>":<value>` 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
+1 -1
View File
@@ -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
-7
View File
@@ -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
#####
@@ -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",
@@ -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|
@@ -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)) }
+49
View File
@@ -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?
+9 -6
View File
@@ -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? &&
+3 -1
View File
@@ -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)
+15 -3
View File
@@ -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
+16 -11
View File
@@ -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?
+3 -3
View File
@@ -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
+9 -5
View File
@@ -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.
+5 -5
View File
@@ -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
+8 -3
View File
@@ -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
+5 -5
View File
@@ -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])
+35 -31
View File
@@ -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
+1 -1
View File
@@ -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
+4 -6
View File
@@ -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
-20
View File
@@ -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
-67
View File
@@ -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
}
+30 -11
View File
@@ -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
+23 -43
View File
@@ -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
+6 -16
View File
@@ -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) }
-4
View File
@@ -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)
+4
View File
@@ -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}"
+37 -11
View File
@@ -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 {
+2 -1
View File
@@ -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. " \
+14 -3
View File
@@ -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}" : "" }
-1
View File
@@ -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
+92 -13
View File
@@ -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
+1 -1
View File
@@ -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,
@@ -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
[]
@@ -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
+10 -50
View File
@@ -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
@@ -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
+10 -125
View File
@@ -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
@@ -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
@@ -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({
@@ -22,10 +22,7 @@ module OS
sig { returns(T::Boolean) }
def configure_sandbox!
require "sandbox"
::Sandbox.configure!
::Sandbox.available?
rescue ::ErrorDuringExecution
false
end
end
+4 -1
View File
@@ -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
+37
View File
@@ -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}"
+10 -3
View File
@@ -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
@@ -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)
@@ -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
@@ -0,0 +1,4 @@
# typed: strict
# frozen_string_literal: true
require "extend/os/mac/unpack_strategy/dmg" if OS.mac?
+2
View File
@@ -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,
+66 -56
View File
@@ -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)
+12 -9
View File
@@ -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?
+1 -1
View File
@@ -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
+42 -41
View File
@@ -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
+59 -32
View File
@@ -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 {
+8 -8
View File
@@ -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
@@ -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.
+8
View File
@@ -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
+169 -45
View File
@@ -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
+1
View File
@@ -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"
+110
View File
@@ -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
-19
View File
@@ -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
+8 -4
View File
@@ -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
+5
View File
@@ -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)
+3
View File
@@ -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
@@ -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
+3
View File
@@ -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
@@ -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
@@ -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

Some files were not shown because too many files have changed in this diff Show More