mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
Merge branch 'main' into vulns-fix-available-flag
This commit is contained in:
@@ -65,6 +65,8 @@ updates:
|
||||
- dependency-type: all
|
||||
cooldown:
|
||||
default-days: 7
|
||||
exclude:
|
||||
- Homebrew/actions/*
|
||||
- package-ecosystem: uv
|
||||
directories:
|
||||
- "/Library/Homebrew/formula-analytics/"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: >
|
||||
|
||||
@@ -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: >
|
||||
|
||||
@@ -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: >
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-*/
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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]) }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)) }
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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? &&
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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. " \
|
||||
|
||||
@@ -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}" : "" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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?
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
Generated
+36
-36
@@ -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
|
||||
Generated
+73
-5
@@ -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
Reference in New Issue
Block a user