Merge branch 'main' into sbom-purl-registry

This commit is contained in:
Andrew Nesbitt authored and GitHub committed 2026-08-12 15:54:24 +01:00
commit 04cfe2a760
32 files changed
+909 -108

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
ARG HOMEBREW_BREW_IMAGE=ghcr.io/homebrew/brew:main
FROM ghcr.io/actions/actions-runner:latest AS runner-source
FROM ${HOMEBREW_BREW_IMAGE}
COPY --from=runner-source --chown=linuxbrew:linuxbrew /home/runner/ /home/linuxbrew/actions-runner/
COPY --chown=linuxbrew:linuxbrew .github/scripts/startup_long_runner.sh /home/linuxbrew/startup_long_runner.sh
RUN chmod +x /home/linuxbrew/startup_long_runner.sh
# Must be set when running the container (e.g., via `docker run -e RUNNER_JITCONFIG=...`)
# This caries the GitHub configuration the runner needs to start, so we can't set it to any meaningful default.
# This line ensures that it is set.
ENV RUNNER_JITCONFIG=
# Do not let .NET load icu to avoid polluting the image
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENTRYPOINT ["/home/linuxbrew/startup_long_runner.sh"]
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
# Validates runtime context, configures the GitHub Actions long runner from RUNNER_JITCONFIG, then starts it.
set -euo pipefail
RUNNER_DIRECTORY="${HOME}/actions-runner"
if [[ "$(id -un)" != "linuxbrew" ]]
then
echo "Startup script run as incorrect user." >&2
exit 1
fi
if [[ -f "${RUNNER_DIRECTORY}/.runner" ]]
then
echo "Runner already configured, starting..."
exec "${RUNNER_DIRECTORY}/run.sh"
fi
if [[ -z "${RUNNER_JITCONFIG:-}" ]]
then
echo "Not configuring runner: missing RUNNER_JITCONFIG." >&2
exit 1
fi
echo "Configuring long runner..."
cd "${RUNNER_DIRECTORY}"
jitconfig_json=$(printf '%s' "${RUNNER_JITCONFIG}" | base64 -d) || {
echo "Failed to decode RUNNER_JITCONFIG: invalid base64 data." >&2
exit 1
}
if ! jq -e 'type == "object"' <<<"${jitconfig_json}" >/dev/null
then
echo "Failed to parse RUNNER_JITCONFIG: expected a JSON object." >&2
exit 1
fi
config_files=$(jq -er 'keys[]' <<<"${jitconfig_json}") || {
echo "Failed to parse RUNNER_JITCONFIG: could not list config keys." >&2
exit 1
}
while IFS= read -r config_file
do
if [[ "${config_file}" != "$(basename "${config_file}")" || "${config_file:0:1}" != "." ]]
then
echo "Invalid file name in JIT config: ${config_file}" >&2
continue
fi
jq -er --arg key "${config_file}" '.[$key]' <<<"${jitconfig_json}" | base64 -d | install -m 600 /dev/stdin "${config_file}"
done <<<"${config_files}"
echo "GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED=1" >.env
echo "Runner configured."
exec ./run.sh
+152 -2
View File
@@ -227,7 +227,6 @@ jobs:
outputs: type=image,name=ghcr.io/homebrew/ubuntu${{ matrix.version }},name-canonical=true,push=true,push-by-digest=true
- name: Export the Docker image digest
if: fromJSON(steps.attributes.outputs.push)
run: |
mkdir -p "${RUNNER_TEMP}"/digests
echo "${DIGEST#sha256:}" >"${RUNNER_TEMP}/digests/${VERSION}-${ARCH}"
@@ -237,12 +236,30 @@ jobs:
ARCH: ${{ matrix.arch }}
- name: Upload the Docker image digest
if: fromJSON(steps.attributes.outputs.push)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digest-${{ matrix.version }}-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
- name: Export base image for long runner
if: matrix.version == '24.04'
run: |
mkdir -p "${RUNNER_TEMP}/long-runner-base"
image_name="long-runner-base:${VERSION}-${ARCH}"
docker tag brew "${image_name}"
docker save "${image_name}" | gzip >"${RUNNER_TEMP}/long-runner-base/image-${VERSION}-${ARCH}.tar.gz"
env:
VERSION: ${{ matrix.version }}
ARCH: ${{ matrix.arch }}
DIGEST: ${{ steps.digest.outputs.digest }}
- name: Upload base image artifact for long runner
if: matrix.version == '24.04'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: long-runner-base-24.04-${{ matrix.arch }}
path: ${{ runner.temp }}/long-runner-base/*
merge:
needs: [generate-tags, build]
if: github.repository_owner == 'Homebrew' && fromJSON(needs.generate-tags.outputs.merge)
@@ -298,3 +315,136 @@ jobs:
echo "Push failed (attempt $attempts). Retrying in ${delay} seconds..."
sleep ${delay}
done
build-and-publish-long-runner:
name: Build runner Docker image (${{ matrix.platform }})
needs: [generate-tags, build]
if: github.repository_owner == 'Homebrew'
runs-on: ${{ matrix.runner }}
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
include:
- platform: linux/amd64
platform_tag: linux-amd64
arch: x86_64
runner: ubuntu-24.04
- platform: linux/arm64
platform_tag: linux-arm64
arch: arm64
runner: ubuntu-24.04-arm
steps:
- env:
IMAGE_VALUE: ghcr.io/${{ github.repository_owner }}/runner
run: echo "IMAGE=${IMAGE_VALUE,,}" >> "$GITHUB_ENV"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Retrieve build attributes
id: attributes
env:
VERSION: 24.04
PUSH: ${{ needs.generate-tags.outputs.push }}
run: |
filter="$(printf '.["%s"]' "${VERSION}")"
echo "push=$(jq --raw-output "${filter}" <<<"${PUSH}")" >>"${GITHUB_OUTPUT}"
- name: Download long runner base image artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: long-runner-base-24.04-${{ matrix.arch }}
path: ${{ runner.temp }}/long-runner-base
- name: Load long runner base image
id: base_image
run: |
artifact_dir="${{ runner.temp }}/long-runner-base"
image_archive="${artifact_dir}/image-24.04-${ARCH}.tar.gz"
[[ -f "${image_archive}" ]] || {
echo "Missing brew image archive: ${image_archive}" >&2
exit 1
}
base_image="long-runner-base:24.04-${ARCH}"
gunzip --stdout "${image_archive}" | docker load
docker image inspect "${base_image}" >/dev/null
echo "image=${base_image}" >> "$GITHUB_OUTPUT"
env:
ARCH: ${{ matrix.arch }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
with:
# Use the local Docker daemon so Buildx can resolve
# the base image loaded from the artifact tarball.
driver: docker
- name: Build ${{ matrix.platform }} runner Docker image
id: runner_build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: .github/scripts/Dockerfile.long_runner
platforms: ${{ matrix.platform }}
build-args: HOMEBREW_BREW_IMAGE=${{ steps.base_image.outputs.image }}
tags: runner:build-${{ matrix.platform_tag }}
- name: Verify AWS deployment secrets
if: fromJSON(steps.attributes.outputs.push)
shell: bash
env:
AWS_PROJECT_ID: ${{ secrets.AWS_PROJECT_ID }}
run: |
missing=()
[ -z "$AWS_PROJECT_ID" ] && missing+=("AWS_PROJECT_ID")
if [ "${#missing[@]}" -ne 0 ]; then
printf 'Missing required deployment secrets: %s\n' "${missing[*]}" >&2
exit 1
fi
- name: Configure AWS Credentials
if: fromJSON(steps.attributes.outputs.push)
id: aws_registry_credentials
uses: aws-actions/configure-aws-credentials@517a711dbcd0e402f90c77e7e2f81e849156e31d # v6.2.2
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_PROJECT_ID }}:role/GithubActionsRoleECRPushBrew
aws-region: us-east-1
- name: AWS Elastic Container Registry Login
if: fromJSON(steps.attributes.outputs.push)
id: aws_registry_login
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2.1.6
- name: Generate AWS image name
id: aws_image
env:
AWS_REGISTRY: ${{ steps.aws_registry_login.outputs.registry }}
LONG_RUNNER_IMAGE_DIGEST: ${{ steps.runner_build.outputs.digest }}
run: |
digest_without_prefix="${LONG_RUNNER_IMAGE_DIGEST#sha256:}"
[[ -n "${digest_without_prefix}" ]] || {
echo "Missing long runner image digest from build output" >&2
exit 1
}
echo "image=${AWS_REGISTRY}/runner-ecr:${digest_without_prefix}-${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
echo "image_latest=${AWS_REGISTRY}/runner-ecr:latest" >> "$GITHUB_OUTPUT"
- name: Push image to AWS Elastic Container Registry
if: fromJSON(steps.attributes.outputs.push)
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: .github/scripts/Dockerfile.long_runner
platforms: ${{ matrix.platform }}
build-args: HOMEBREW_BREW_IMAGE=${{ steps.base_image.outputs.image }}
push: true
tags: |
${{ steps.aws_image.outputs.image }}
${{ steps.aws_image.outputs.image_latest }}
@@ -201,13 +201,13 @@ module Cask
cask.config
end
sig { returns(T.nilable(Sandbox)) }
def cask_sandbox
sig { params(network_access_allowed: T::Boolean).returns(T.nilable(Sandbox)) }
def cask_sandbox(network_access_allowed: false)
return unless Sandbox.use_for?("running cask artifact operations")
Sandbox.new.tap do |sandbox|
sandbox.allow_read(path: cask.staged_path, type: :subpath)
sandbox.add_install_hook_rules(network_access_allowed: false)
sandbox.add_install_hook_rules(network_access_allowed:)
end
end
@@ -33,7 +33,9 @@ module Cask
sig { params(command: T.class_of(SystemCommand), phase: Symbol).void }
def run_steps(command, phase: :install)
runner = Homebrew::InstallSteps::Runner.new(context: cask, command:)
sandbox = cask_sandbox
sandbox = cask_sandbox(network_access_allowed: steps.any? do |step|
step["type"] == "run" && step["network_access"] == true
end)
unless sandbox
runner.run(steps, phase:)
return
+13 -4
View File
@@ -344,10 +344,19 @@ module Cask
add_error "a #{sym} stanza is required" unless cask.public_send(sym)
end
add_error "at least one name stanza is required" if cask.name.empty?
# TODO: specific DSL knowledge should not be spread around in various files like this
rejected_artifacts = [:uninstall, :zap]
installable_artifacts = cask.artifacts.reject { |k| rejected_artifacts.include?(k) }
add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty?
installable_artifact = if cask.on_system_blocks_exist?
begin
OnSystem::VALID_OS_ARCH_TAGS.any? do |tag|
cask.refresh_for_tag(tag) { cask.installable_artifact? }
end
ensure
cask.refresh
end
else
cask.installable_artifact?
end
add_error "at least one installable artifact stanza is required" unless installable_artifact
end
sig { void }
+61 -2
View File
@@ -256,6 +256,31 @@ module Cask
artifacts.all?(Artifact::Font)
end
sig { returns(T::Boolean) }
def installable_artifact?
artifacts.any? do |artifact|
artifact.respond_to?(:install_phase) || artifact.is_a?(Artifact::StageOnly)
end
end
sig { params(os: Symbol).returns(T::Boolean) }
def artifacts_supported_on_os?(os)
case os
when :linux
artifacts.all? do |artifact|
if artifact.is_a?(Artifact::Installer)
!artifact.manual_install
else
Artifact::MACOS_ONLY_ARTIFACTS.exclude?(artifact.class)
end
end
when :macos
artifacts.none? { |artifact| Artifact::LINUX_ONLY_ARTIFACTS.include?(artifact.class) }
else
raise ArgumentError, "Unsupported operating system: #{os.inspect}"
end
end
sig { returns(T::Boolean) }
def supports_linux?
return true if depends_on.requires_linux?
@@ -612,17 +637,24 @@ module Cask
hash = to_h_with_language_variations
variations = {}
supported_platforms = []
on_system_blocks_exist = dsl!.on_system_blocks_exist?
if dsl!.on_system_blocks_exist?
if on_system_blocks_exist
begin
OnSystem::VALID_OS_ARCH_TAGS.each do |bottle_tag|
macos_requirements = [depends_on.macos, depends_on.maximum_macos].compact
next if bottle_tag.macos? &&
macos_requirements.present? &&
!dsl!.depends_on_set_in_block? &&
macos_requirements.any? { |requirement| !requirement.allows?(bottle_tag.to_macos_version) }
macos_requirements.any? do |requirement|
# Avoid recursive equality between cached version-comparison keys across casks.
!requirement.allows?(MacOSVersion.from_symbol(bottle_tag.system))
end
refresh_for_tag(bottle_tag) do
supported_platforms << bottle_tag.to_sym if platform_supported?(bottle_tag)
to_h_with_language_variations.each do |key, value|
next if HASH_KEYS_TO_SKIP.include? key
next if value.to_s == hash[key].to_s
@@ -635,9 +667,14 @@ module Cask
ensure
refresh
end
else
supported_platforms = OnSystem::VALID_OS_ARCH_TAGS.filter_map do |bottle_tag|
bottle_tag.to_sym if platform_supported?(bottle_tag)
end
end
hash["variations"] = variations
hash["supported_platforms"] = supported_platforms
hash
end
@@ -720,6 +757,28 @@ module Cask
private
sig { params(bottle_tag: ::Utils::Bottles::Tag).returns(T::Boolean) }
def platform_supported?(bottle_tag)
return false if bottle_tag.linux? && !supports_linux?
return false if bottle_tag.macos? && !supports_macos?
return false if version.blank? || sha256.blank? || url.blank?
return false unless installable_artifact?
return false if bottle_tag.linux? && !artifacts_supported_on_os?(:linux)
return false if bottle_tag.macos? && !artifacts_supported_on_os?(:macos)
arch_supported = depends_on.arch&.any? do |arch|
required_arch = ::Utils::Bottles::Tag.new(system: bottle_tag.system, arch: arch[:type]).standardized_arch
required_arch == bottle_tag.standardized_arch
end
return false if arch_supported == false
return true unless bottle_tag.macos?
[depends_on.macos, depends_on.maximum_macos].compact.all? do |requirement|
requirement.allows?(bottle_tag.to_macos_version)
end
end
# Returns caveats text for API serialization, excluding conditional
# built-in caveats that depend on the current machine's state.
# These are stored as separate boolean fields (e.g. caveats_rosetta)
+10 -2
View File
@@ -59,7 +59,8 @@ module Cask
.returns(Pathname)
}
def fetch(quiet: nil, verify_download_integrity: true, timeout: nil)
verify_has_sha if @require_sha
verify_has_sha if @require_sha ||
(@cask.sha256.nil? && (@cask.on_system_blocks_exist? || @cask.loaded_from_api?))
downloader.quiet! if quiet
begin
@@ -233,7 +234,14 @@ module Cask
sig { void }
def verify_has_sha
return if @cask.sha256 != :no_check
return if @cask.sha256.is_a?(Checksum)
unless @require_sha
raise CaskError, <<~EOS
Cask '#{@cask}' does not have a sha256 checksum defined for this platform.
Add an appropriate `depends_on` stanza if the cask does not support this platform.
EOS
end
raise CaskError, <<~EOS
Cask '#{@cask}' does not have a sha256 checksum defined.
+10 -18
View File
@@ -201,7 +201,6 @@ module Cask
@cask = cask
@caveats = T.let(DSL::Caveats.new(cask), DSL::Caveats)
@conflicts_with = T.let(nil, T.nilable(DSL::ConflictsWith))
@conflicts_with_set_in_block = T.let(false, T::Boolean)
@container = T.let(nil, T.nilable(DSL::Container))
@container_set_in_block = T.let(false, T::Boolean)
@depends_on = T.let(DSL::DependsOn.new, DSL::DependsOn)
@@ -562,21 +561,6 @@ module Cask
when String
Checksum.new(val)
when nil
# Checksums declared for only the other OS mean no checksum for the
# 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 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
if running_os_checksums.any?(&:present?) && !Homebrew::SimulateSystem.simulating?
raise CaskInvalidError.new(cask, "invalid 'sha256' value: nil")
end
nil
else
raise CaskInvalidError.new(cask, "invalid 'sha256' value: #{val.inspect}")
@@ -664,11 +648,19 @@ module Cask
# Declare conflicts that keep a cask from installing or working correctly.
#
# NOTE: Multiple `conflicts_with` stanzas can be specified; they are merged.
#
# @api public
sig { params(kwargs: T.anything).returns(T.nilable(DSL::ConflictsWith)) }
def conflicts_with(**kwargs)
# TODO: Remove this constraint and instead merge multiple `conflicts_with` stanzas
set_unique_stanza(:conflicts_with, kwargs.empty?) { DSL::ConflictsWith.new(**kwargs) }
return @conflicts_with if kwargs.empty?
new_conflicts = DSL::ConflictsWith.new(**kwargs)
@conflicts_with = @conflicts_with&.merge!(new_conflicts) || new_conflicts
rescue CaskInvalidError
raise
rescue => e
raise CaskInvalidError.new(cask, "'conflicts_with' stanza failed with: #{e}")
end
sig { returns(Pathname) }
@@ -21,6 +21,12 @@ module Cask
super(conflicts)
end
sig { params(other: ConflictsWith).returns(T.self_type) }
def merge!(other)
other.to_h.each { |key, values| __getobj__[key] |= Set.new(values) }
self
end
sig { returns(T::Hash[Symbol, T::Array[String]]) }
def to_h
__getobj__.transform_values(&:to_a)
+2 -5
View File
@@ -372,13 +372,10 @@ on_request: true)
sig { void }
def check_supported_system
# Audited casks always have an activatable artifact for the systems they
# support, so API data without one means this system is unsupported.
# API data without an installable artifact means this system is unsupported.
# Source loads keep working for unaudited casks, e.g. naked containers.
return unless @cask.loaded_from_api?
return if @cask.artifacts.any? do |artifact|
artifact.respond_to?(:install_phase) || artifact.is_a?(Artifact::StageOnly)
end
return if @cask.installable_artifact?
os_name = Homebrew::SimulateSystem.simulating_or_running_on_macos? ? "macOS" : "Linux"
raise CaskError, "#{@cask}: This cask is not available on #{os_name}."
+6 -2
View File
@@ -62,9 +62,13 @@ module Cask
next false
end
if cask.outdated?(greedy: true)
version = cask.version
if version.nil?
opoo "Not upgrading #{cask.token}, no version is available for the current platform" unless quiet
false
elsif cask.outdated?(greedy: true)
true
elsif cask.version.latest?
elsif version.latest?
opoo "Not upgrading #{cask.token}, the downloaded artifact has not changed" unless quiet
false
else
+11 -9
View File
@@ -636,7 +636,8 @@ module Homebrew
formula_version_changes = formula_upgrade_descriptions(context.formulae_installer.map(&:formula),
include_sizes: dry_run)
dependent_version_changes = formula_upgrade_descriptions(context.dependants.upgradeable,
dependent_formulae = context.dependants.upgradeable.dup
dependent_version_changes = formula_upgrade_descriptions(dependent_formulae,
include_sizes: dry_run)
if dry_run
record_formula_upgrade_summary(context,
@@ -663,7 +664,7 @@ module Homebrew
skip_formula_names:,
)
Upgrade.upgrade_dependents(
upgraded_dependent_formulae = Upgrade.upgrade_dependents(
context.dependants, context.formulae_to_install,
flags: args.flags_only,
dry_run:,
@@ -680,17 +681,18 @@ module Homebrew
)
unless dry_run
upgraded_formula_installers_by_identity = T.let({}.compare_by_identity,
T::Hash[FormulaInstaller, T::Boolean])
upgraded_formula_installers.each do |formula_installer|
upgraded_formula_installers_by_identity[formula_installer] = true
upgraded_formulae_by_identity = T.let({}.compare_by_identity, T::Hash[Formula, T::Boolean])
(upgraded_formula_installers.map(&:formula) + upgraded_dependent_formulae).each do |formula|
upgraded_formulae_by_identity[formula] = true
end
planned_formulae = context.formulae_installer.map(&:formula) + dependent_formulae
version_changes = formula_version_changes + dependent_version_changes
record_formula_upgrade_summary(
context,
formulae_installer: upgraded_formula_installers,
version_changes: context.formulae_installer.each_with_index.filter_map do |formula_installer, index|
formula_version_changes.fetch(index) if upgraded_formula_installers_by_identity.key?(formula_installer)
end + dependent_version_changes,
version_changes: planned_formulae.each_with_index.filter_map do |formula, index|
version_changes.fetch(index) if upgraded_formulae_by_identity.key?(formula)
end,
)
end
+8
View File
@@ -293,6 +293,9 @@ module Homebrew
generate_system_options(cask, new_version).each do |os, arch|
tag = Utils::Bottles::Tag.new(system: os, arch:)
old_cask.refresh_for_tag(tag) do
next if tag.macos? && !old_cask.supports_macos?
next if tag.linux? && !old_cask.supports_linux?
# Skip archs excluded by the cask's `depends_on arch:`.
reloaded_archs = old_cask.depends_on.arch&.filter_map { |a| a[:type] }&.uniq
next if reloaded_archs.present? && reloaded_archs.exclude?(arch)
@@ -317,6 +320,11 @@ module Homebrew
tmp_cask = Cask::CaskLoader::FromContentLoader.new(contents)
.load(config: nil)
old_hash = tmp_cask.sha256
if old_hash.nil?
raise Cask::CaskError, "#{cask}: No checksum is defined for #{tag.to_sym.inspect}. " \
"Add `depends_on arch:` or an operating system `depends_on` to " \
"declare unsupported platforms."
end
next if new_hash.is_a?(String) && old_hash.to_s == new_hash
checksum_scope = cask_stanza_scope(contents, :sha256, arch)
@@ -11,19 +11,10 @@ module OS
sig { void }
def check_stanza_os_requirements
return if !cask.depends_on.requires_macos? && artifacts.all? { |artifact| supported_artifact?(artifact) }
return if !cask.depends_on.requires_macos? && cask.artifacts_supported_on_os?(:linux)
raise ::Cask::CaskError, "#{cask}: This cask requires macOS."
end
private
sig { params(artifact: ::Cask::Artifact::AbstractArtifact).returns(T::Boolean) }
def supported_artifact?(artifact)
return !artifact.manual_install if artifact.is_a?(::Cask::Artifact::Installer)
::Cask::Artifact::MACOS_ONLY_ARTIFACTS.exclude?(artifact.class)
end
end
end
end
@@ -11,17 +11,10 @@ module OS
sig { void }
def check_stanza_os_requirements
return if !cask.depends_on.requires_linux? && artifacts.all? { |artifact| supported_artifact?(artifact) }
return if !cask.depends_on.requires_linux? && cask.artifacts_supported_on_os?(:macos)
raise ::Cask::CaskError, "#{cask}: This cask requires Linux."
end
private
sig { params(artifact: ::Cask::Artifact::AbstractArtifact).returns(T::Boolean) }
def supported_artifact?(artifact)
::Cask::Artifact::LINUX_ONLY_ARTIFACTS.exclude?(artifact.class)
end
end
end
end
+4 -2
View File
@@ -683,11 +683,12 @@ module Homebrew
chdir: ::T.nilable(::T.any(::String, ::Pathname)),
writable_paths: Paths,
writable_base: ::T.nilable(::T.any(::String, ::Symbol)),
network_access: ::T::Boolean,
).void
}
def run(command, args: [], base: nil, env: {}, sudo: false, must_succeed: true, print_stdout: false,
print_stderr: true, stdin_path: nil, stdout_path: nil, chdir: nil, writable_paths: [],
writable_base: nil)
writable_base: nil, network_access: false)
add_step("run",
"command" => path_spec(command, base:, default_base: nil),
"args" => args.map(&:to_s),
@@ -703,7 +704,8 @@ module Homebrew
writable_paths,
base: writable_base,
default_base: @default_base,
))
),
"network_access" => network_access)
end
sig {
@@ -8,9 +8,9 @@ module RuboCop
include CaskHelp
# These stanzas can be overridden by `on_*` blocks, so take them into account.
# TODO: Update this list if new stanzas are added to `Cask::DSL` that call `set_unique_stanza`.
# TODO: Update this list when stanzas in `Cask::DSL` start or stop calling `set_unique_stanza`.
OVERRIDABLE_METHODS = [
:appcast, :arch, :auto_updates, :conflicts_with, :container,
:appcast, :arch, :auto_updates, :container,
:desc, :homepage, :os, :sha256, :url, :version
].freeze
+1
View File
@@ -472,6 +472,7 @@ module Homebrew
HOMEBREW_REPOSITORY/"completions/bash/brew",
HOMEBREW_REPOSITORY/"Dockerfile",
*HOMEBREW_REPOSITORY.glob(".devcontainer/**/*.sh"),
*HOMEBREW_REPOSITORY.glob(".github/scripts/*.sh"),
*HOMEBREW_REPOSITORY.glob("package/scripts/*"),
*HOMEBREW_LIBRARY.glob("Homebrew/**/*.sh").reject { |path| path.to_s.include?("/vendor/") },
*HOMEBREW_LIBRARY.glob("Homebrew/shims/**/*").map(&:realpath).uniq
@@ -64,7 +64,7 @@ RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do
artifact = cask.artifacts.find { |candidate| candidate.is_a?(Cask::Artifact::PostflightSteps) }
run_step = artifact.steps.find { |step| step["type"] == "run" }
expect(run_step).not_to include("print_stdout", "suppress_stderr", "writable_paths")
expect(run_step).not_to include("print_stdout", "suppress_stderr", "writable_paths", "network_access")
end
it "sandboxes complete step blocks, including system commands" do
@@ -119,6 +119,28 @@ RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do
expect(original_home/"Library/Application Support/cask-home-state").to exist
end
it "allows network access for runs that request it" do
cask = Cask::Cask.new("with-networked-install-step") do
version "1.2.3"
sha256 :no_check
url "file://#{TEST_FIXTURE_DIR}/cask/container.zip"
postflight_steps do
run "/usr/bin/true", network_access: true
end
end
sandbox = instance_double(Sandbox).as_null_object
cask.staged_path.mkpath
cask.config_path.dirname.mkpath
allow(Sandbox).to receive_messages(available?: true, new: sandbox)
allow(Sandbox).to receive(:with_preserved_brew_file).and_yield
expect(sandbox).to receive(:add_install_hook_rules).with(network_access_allowed: true)
expect(sandbox).to receive(:run)
Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts
end
context "when install steps may require sudo" do
{
"an explicitly privileged command" => proc { run "/usr/bin/true", sudo: true },
+36
View File
@@ -170,6 +170,42 @@ RSpec.describe Cask::Audit, :cask do
it { is_expected.to error_with(/#{stanza} stanza is required/) }
end
end
context "when a cask has only uninstall artifacts" do
let(:cask) do
Cask::Cask.new("uninstall-only") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.pkg"
name "Uninstall Only"
homepage "https://brew.sh/"
uninstall pkgutil: "org.example.foo"
zap trash: "~/Library/Caches/org.example.foo"
end
end
it { is_expected.to error_with("at least one installable artifact stanza is required") }
end
context "when a cask has an installable artifact only on Linux" do
let(:cask) do
Homebrew::SimulateSystem.with(os: :sequoia, arch: :arm) do
Cask::Cask.new("linux-only-artifact") do
version :latest
sha256 :no_check
url "https://brew.sh/foo"
name "Linux Only Artifact"
homepage "https://brew.sh/"
on_linux do
binary "foo"
end
end
end
end
it { is_expected.not_to error_with("at least one installable artifact stanza is required") }
end
end
describe "checking homepage availability" do
+197
View File
@@ -646,6 +646,13 @@ RSpec.describe Cask::Cask, :cask do
end
end
describe "#artifacts_supported_on_os?" do
it "rejects unknown operating systems" do
expect { cask.artifacts_supported_on_os?(:windows) }
.to raise_error(ArgumentError, "Unsupported operating system: :windows")
end
end
describe "#outdated_info" do
it "includes pinned cask details" do
cask = Cask::CaskLoader.load("local-caffeine")
@@ -979,6 +986,196 @@ RSpec.describe Cask::Cask, :cask do
)
end
context "when recording supported platforms" do
let(:platform_tags) do
[
Utils::Bottles::Tag.new(system: :sonoma, arch: :intel),
Utils::Bottles::Tag.new(system: :sonoma, arch: :arm),
Utils::Bottles::Tag.new(system: :monterey, arch: :intel),
Utils::Bottles::Tag.new(system: :monterey, arch: :arm),
Utils::Bottles::Tag.new(system: :catalina, arch: :intel),
Utils::Bottles::Tag.new(system: :linux, arch: :intel),
Utils::Bottles::Tag.new(system: :linux, arch: :arm),
]
end
let(:macos_platforms) { [:sonoma, :arm64_sonoma, :monterey, :arm64_monterey, :catalina] }
before do
stub_const("OnSystem::VALID_OS_ARCH_TAGS", platform_tags)
end
it "records platforms allowed by scoped macOS requirements" do
c = Cask::CaskLoader.load("with-depends-on-macos-in-on-macos")
expect(c.to_hash_with_variations["supported_platforms"]).to eq(
[:sonoma, :arm64_sonoma, :monterey, :arm64_monterey, :x86_64_linux, :arm64_linux],
)
end
it "excludes platforms without complete download data" do
c = Cask::CaskLoader.load("multiple-versions")
expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
end
it "excludes platforms rejected by architecture requirements" do
c = described_class.new("architecture-restricted") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
on_linux do
depends_on arch: :x86_64
end
binary "foo"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(
[*macos_platforms, :x86_64_linux],
)
end
it "excludes Linux for macOS-only artifacts" do
c = described_class.new("macos-artifact") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
app "Foo.app"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
end
it "excludes Linux for manual installers" do
c = described_class.new("manual-installer") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
installer manual: "Foo.app"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(macos_platforms)
end
it "excludes macOS for Linux-only artifacts" do
c = described_class.new("linux-artifact") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
app_image "Foo.AppImage"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq([:x86_64_linux, :arm64_linux])
end
it "includes stage-only casks" do
c = described_class.new("stage-only") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
stage_only true
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(platform_tags.map(&:to_sym))
end
it "records no supported platforms for a cask without an installable artifact" do
c = described_class.new("zap-only") do
version :latest
arch arm: "arm64", intel: "x86_64"
sha256 :no_check
url "https://brew.sh/#{arch}.zip"
zap trash: "~/Library/Caches/brew-test"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq([])
end
it "records every platform when a cask has no platform variations" do
c = described_class.new("no-platform-variations") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
binary "foo"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq(platform_tags.map(&:to_sym))
end
it "records top-level platform requirements without variations" do
c = described_class.new("top-level-platform-requirements") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
depends_on macos: :monterey
depends_on arch: :x86_64
binary "foo"
end
expect(c.to_hash_with_variations["supported_platforms"]).to eq([:sonoma, :monterey])
end
it "serializes architecture-varying and universal casks with the same macOS requirement" do
tags = OnSystem::ALL_OS_ARCH_COMBINATIONS.filter_map do |os, arch|
tag = Utils::Bottles::Tag.new(system: os, arch:)
tag if tag.valid_combination?
end
stub_const("OnSystem::VALID_OS_ARCH_TAGS", tags)
architecture_varying = described_class.new("architecture-varying") do
arch arm: "ARM64", intel: "64"
version "1.2.3"
sha256 arm: "a" * 64,
intel: "b" * 64
url "https://brew.sh/#{arch}.zip"
depends_on macos: :big_sur
app "Foo.app"
end
universal = described_class.new("universal") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
depends_on macos: :big_sur
app "Foo.app"
end
architecture_varying.to_hash_with_variations
supported_platforms = Timeout.timeout(5) do
universal.to_hash_with_variations["supported_platforms"]
end
expected_platforms = tags.filter_map do |tag|
tag.to_sym if tag.macos? && tag.system != :catalina
end
expect(supported_platforms).to eq(expected_platforms)
end
it "isolates macOS requirement comparisons between casks" do
supported_platforms = [:monterey, :sonoma].map do |minimum_macos|
c = described_class.new("requires-#{minimum_macos}") do
version :latest
sha256 :no_check
url "https://brew.sh/foo.zip"
depends_on macos: minimum_macos
binary "foo"
end
c.to_hash_with_variations["supported_platforms"]
end
expect(supported_platforms).to eq(
[
[:sonoma, :arm64_sonoma, :monterey, :arm64_monterey],
[:sonoma, :arm64_sonoma],
],
)
end
end
# NOTE: The calls to `Cask.generating_hash!` and `Cask.generated_hash!`
# are not idempotent so they can only be used in one test.
it "returns the correct hash placeholders" do
@@ -55,6 +55,28 @@ RSpec.describe Cask::Download, :cask do
expect(download).not_to receive(:downloader)
expect { download.fetch }.to raise_error(/--require-sha/)
end
it "fails before downloading if sha256 is nil with --require-sha" do
missing_checksum = Cask::CaskLoader.load(cask_path("missing-checksum"))
download = described_class.new(missing_checksum, require_sha: true)
allow(download).to receive(:downloader) { raise "download attempted" }
expect { download.fetch }.to raise_error(Cask::CaskError, /--require-sha/)
end
it "fails before downloading if a platform checksum is missing" do
Homebrew::SimulateSystem.with(os: :macos, arch: :intel) do
cask = Cask::Cask.new("missing-platform-checksum") do
version "1.2.3"
sha256 arm: "0000000000000000000000000000000000000000000000000000000000000000"
url "https://brew.sh/example.zip"
end
download = described_class.new(cask)
allow(download).to receive(:downloader) { raise "download attempted" }
expect { download.fetch }.to raise_error(Cask::CaskError, /`depends_on`/)
end
end
end
describe "#stage_from_download_queue?" do
+17 -6
View File
@@ -195,15 +195,16 @@ RSpec.describe Cask::DSL, :cask, :no_api do
end
end
it "raises on the real system when the running-architecture checksum is missing" do
it "loads the architecture requirement when the running-architecture checksum is missing" do
allow(Homebrew::SimulateSystem).to receive(:simulating?).and_return(false)
Homebrew::SimulateSystem.with(os: :linux, arch: :intel) do
expect do
Cask::Cask.new("checksum-cask") do
sha256 arm64_linux: "imasha2armlinux", intel: "imasha2intel"
end
end.to raise_error(Cask::CaskInvalidError, /invalid 'sha256' value/)
cask = Cask::Cask.new("checksum-cask") do
sha256 arm64_linux: "imasha2armlinux", intel: "imasha2intel"
depends_on arch: :arm64
end
expect([cask.sha256, cask.depends_on.arch]).to eq([nil, [{ type: :arm, bits: 64 }]])
end
end
end
@@ -737,6 +738,16 @@ RSpec.describe Cask::DSL, :cask, :no_api do
end
end
context "when specified multiple times" do
let(:token) { "with-conflicts-with-multiple" }
it "merges and deduplicates all conflicts_with stanzas" do
os_conflict = OS.mac? ? "macos-caffeine" : "linux-caffeine"
expect(cask.conflicts_with[:cask])
.to eq(Set.new(["local-caffeine", "with-caffeine", os_conflict]))
end
end
context "with invalid conflicts_with key" do
let(:token) { "invalid-conflicts-with-key" }
@@ -47,6 +47,20 @@ RSpec.describe Cask::Upgrade, :cask do
allow(Homebrew::EnvConfig).to receive(:upgrade_auto_updates_casks?).and_return(true)
end
it "warns and excludes casks with no version for the current platform" do
cask = Homebrew::SimulateSystem.with(os: :linux) do
Cask::Cask.new("macos-only") do
on_macos do
version "1.2.3"
end
end
end
expect do
expect(described_class.outdated_casks([cask], args:, force: true, quiet: false)).to be_empty
end.to output(/Not upgrading macos-only, no version is available for the current platform/).to_stderr
end
context "when the upgrade is a dry run" do
# Use stub installation for dry-run tests since they mock upgrade_cask
# and only need to verify installation state, not perform real upgrades.
+67 -3
View File
@@ -687,6 +687,25 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
end
end
it "does not claim to upgrade dependents whose runtime dependencies are satisfied" do
formula = formula("sqlite") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/sqlite-3.53.2.tar.gz"
end
dependent = formula("python@3.14") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/python@3.14-3.14.5.tar.gz"
end
dependants = Homebrew::Upgrade::Dependents.new(upgradeable: [dependent], pinned: [], skipped: [])
allow(Homebrew::Upgrade).to receive(:formula_installers).and_return([])
allow(FormulaInstaller).to receive(:installed).and_return([])
expect do
Homebrew::Upgrade.upgrade_dependents(dependants, [formula], flags: [])
end.not_to output(/Upgrading.*python@3\.14/m).to_stdout
end
it "does not print aggregate package sizes" do
cmd = described_class.new(["--dry-run"])
summary = Homebrew::Cmd::UpgradeCmd::FinalUpgradeSummary.new(
@@ -1200,7 +1219,7 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
allow(formula).to receive(:opt_prefix).and_return(new_keg)
[formula_installer]
end
allow(Homebrew::Upgrade).to receive(:upgrade_dependents)
allow(Homebrew::Upgrade).to receive(:upgrade_dependents).and_return([])
cmd.upgrade_outdated_formulae!([])
@@ -1234,8 +1253,8 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
dependants: Homebrew::Upgrade::Dependents.new(upgradeable: [], pinned: [], skipped: []),
),
)
allow(Homebrew::Upgrade).to receive(:upgrade_formulae).and_return([successful_formula_installer])
allow(Homebrew::Upgrade).to receive(:upgrade_dependents)
allow(Homebrew::Upgrade).to receive_messages(upgrade_formulae: [successful_formula_installer],
upgrade_dependents: [])
cmd.upgrade_outdated_formulae!([])
@@ -1245,5 +1264,50 @@ RSpec.describe Homebrew::Cmd::UpgradeCmd do
)
end
it "reports only successful dependent version changes in the final summary" do
formula = formula("testball") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/testball-0.2"
end
upgraded_dependent = formula("upgraded-dependent") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/upgraded-dependent-0.2"
end
skipped_dependent = formula("skipped-dependent") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/skipped-dependent-0.2"
end
formula_installer = FormulaInstaller.new(formula)
old_formula_keg = HOMEBREW_CELLAR/"testball/0.1"
old_upgraded_dependent_keg = HOMEBREW_CELLAR/"upgraded-dependent/0.1"
old_skipped_dependent_keg = HOMEBREW_CELLAR/"skipped-dependent/0.1"
old_formula_keg.mkpath
old_upgraded_dependent_keg.mkpath
old_skipped_dependent_keg.mkpath
allow(formula).to receive_messages(optlinked?: true, opt_prefix: old_formula_keg)
allow(upgraded_dependent).to receive_messages(optlinked?: true, opt_prefix: old_upgraded_dependent_keg)
allow(skipped_dependent).to receive_messages(optlinked?: true, opt_prefix: old_skipped_dependent_keg)
cmd = described_class.new([])
allow(cmd).to receive(:formulae_upgrade_context).and_return(
Homebrew::Cmd::UpgradeCmd::FormulaeUpgradeContext.new(
formulae_to_install: [formula],
formulae_installer: [formula_installer],
dependants: Homebrew::Upgrade::Dependents.new(
upgradeable: [upgraded_dependent, skipped_dependent], pinned: [], skipped: [],
),
),
)
allow(Homebrew::Upgrade).to receive_messages(
upgrade_formulae: [formula_installer],
upgrade_dependents: [upgraded_dependent],
)
cmd.upgrade_outdated_formulae!([])
expect(cmd.final_upgrade_summary.version_changes)
.to contain_exactly("testball 0.1 -> 0.2", "upgraded-dependent 0.1 -> 0.2")
end
it_behaves_like "reinstall_pkgconf_if_needed"
end
@@ -350,6 +350,7 @@ RSpec.describe Homebrew::DevCmd::BumpCaskPr do
url "https://brew.sh/foo-\#{arch}-\#{version}.dmg"
name "Foo"
depends_on :macos
end
RUBY
cask = Homebrew::SimulateSystem.with(os: newest_macos, arch: :arm) { cask_from_contents(contents) }
@@ -371,6 +372,7 @@ RSpec.describe Homebrew::DevCmd::BumpCaskPr do
url "https://brew.sh/foo-\#{arch}-\#{version}.dmg"
name "Foo"
depends_on :macos
end
RUBY
end
@@ -521,6 +523,37 @@ RSpec.describe Homebrew::DevCmd::BumpCaskPr do
RUBY
end
it "requires depends_on arch when a checksum is missing" do
contents = <<~RUBY
cask "foo" do
arch arm: "arm64", intel: "x64"
os macos: "macos", linux: "linux"
version "1.0"
sha256 arm: "#{old_hash}",
arm64_linux: "#{new_hash}",
x86_64_linux: "#{intel_hash}"
url "https://brew.sh/foo-\#{os}-\#{arch}-\#{version}.zip"
name "Foo"
on_macos do
app "Foo.app"
end
on_linux do
binary "foo"
end
end
RUBY
cask = cask_from_contents(contents)
new_version = Homebrew::BumpVersionParser.new(general: "2.0")
allow(Cask::Download).to receive(:new) { raise "download attempted" }
expect do
bump_cask_pr.replace_version_and_checksum(cask, nil, new_version, contents)
end.to raise_error(Cask::CaskError, /No checksum.*`depends_on arch:`/)
end
it "leaves nested architecture stanzas unchanged when matching values could be replaced globally" do
contents = <<~RUBY
cask "foo" do
@@ -153,6 +153,19 @@ RSpec.describe RuboCop::Cop::Cask::NoOverrides, :config do
CASK
end
it "accepts `conflicts_with` in both top-level and `on_*` blocks" do
expect_no_offenses <<~CASK
cask "foo" do
version "1.2.3"
conflicts_with cask: "foo-beta"
on_sequoia :or_older do
conflicts_with cask: "foo-legacy"
end
end
CASK
end
it "reports an offense when `on_*` blocks override a single upper-level stanza" do
expect_offense <<~CASK
cask 'foo' do
@@ -0,0 +1,20 @@
# typed: false
cask "with-conflicts-with-multiple" do
version "1.2.3"
sha256 "8dd95f83a6cbf67dd73f003c476ea38a5aab367e3af8f56d485c0ff9017b6ee5"
on_macos do
conflicts_with cask: "macos-caffeine"
end
on_linux do
conflicts_with cask: "linux-caffeine"
end
url "https://brew.sh/ConflictsWith-1.2.3.dmg"
name "ConflictsWith"
homepage "https://brew.sh/"
conflicts_with cask: "local-caffeine"
conflicts_with cask: ["local-caffeine", "with-caffeine"]
end
+52
View File
@@ -70,4 +70,56 @@ RSpec.describe Homebrew::Upgrade do
.to output(/Would upgrade.*python@3.14 3.7.1 -> 3.14.6/m).to_stdout
end
end
describe "::formula_installers" do
it "explains when installed dependencies satisfy the bottle metadata" do
dependent = formula("dependent") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/dependent-2.0"
end
formula_installer = instance_double(
FormulaInstaller,
bottle_tab_runtime_dependencies: { "dependency" => { "version" => "2.0", "revision" => "0" } },
determine_bottle_tab_attributes: nil,
fetch_bottle_tab: nil,
formula: dependent,
)
dependency = instance_double(Dependency)
download_queue = instance_double(Homebrew::DownloadQueue, fetch: nil, shutdown: nil)
allow(Migrator).to receive(:migrate_if_needed)
allow(described_class).to receive(:create_formula_installer).and_return(formula_installer)
allow(Homebrew::DownloadQueue).to receive(:new).and_return(download_queue)
allow(Dependency).to receive(:new).with("dependency").and_return(dependency)
allow(dependency).to receive(:installed?)
.with(minimum_version: Version.new("2.0"), minimum_revision: 0)
.and_return(true)
expect do
described_class.formula_installers([dependent], flags: [], dependents: true)
end.to output(
"==> Not upgrading dependent: installed runtime dependencies satisfy bottle metadata\n",
).to_stdout
end
end
describe "::upgrade_dependents" do
it "returns installed dependents unless they are primary formulae" do
installed_dependent = formula("installed-dependent") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/installed-dependent-2.0"
end
primary_formula = formula("primary") do
T.bind(self, T.class_of(Formula))
url "https://brew.sh/primary-2.0"
end
FormulaInstaller.installed.merge([installed_dependent, primary_formula])
dependants = Homebrew::Upgrade::Dependents.new(
upgradeable: [installed_dependent, primary_formula], pinned: [], skipped: [],
)
expect(described_class.upgrade_dependents(dependants, [primary_formula], flags: []))
.to contain_exactly(installed_dependent)
end
end
end
+40 -27
View File
@@ -152,8 +152,8 @@ module Homebrew
end
if all_runtime_deps_installed
# Don't need to install this bottle if all of the runtime
# dependencies have the same or newer version already installed.
ohai "Not upgrading #{fi.formula.full_specified_name}: " \
"installed runtime dependencies satisfy bottle metadata"
next
end
end
@@ -278,7 +278,7 @@ module Homebrew
dry_run: T::Boolean, installed_on_request: T::Boolean, force_bottle: T::Boolean,
build_from_source_formulae: T::Array[String], interactive: T::Boolean, keep_tmp: T::Boolean,
debug_symbols: T::Boolean, force: T::Boolean, debug: T::Boolean, quiet: T::Boolean,
verbose: T::Boolean, skip_formula_names: T::Array[String]).void
verbose: T::Boolean, skip_formula_names: T::Array[String]).returns(T::Array[Formula])
}
def upgrade_dependents(deps, formulae,
flags:,
@@ -294,7 +294,7 @@ module Homebrew
quiet: false,
verbose: false,
skip_formula_names: [])
return if deps.blank?
return [] if deps.blank?
upgradeable = deps.upgradeable
pinned = deps.pinned
@@ -314,8 +314,38 @@ module Homebrew
EOS
end
installed_formulae = FormulaInstaller.installed
upgraded_formulae = T.let([], T::Array[Formula])
unless dry_run
primary_formula_names = formulae.map(&:full_name)
upgraded_formulae.concat(upgradeable.select do |f|
installed_formulae.include?(f) && primary_formula_names.exclude?(f.full_name)
end)
end
upgradeable.reject! do |f|
FormulaInstaller.installed.include?(f) || (dry_run && skip_formula_names.include?(f.full_name))
installed_formulae.include?(f) || (dry_run && skip_formula_names.include?(f.full_name))
end
return upgraded_formulae if upgradeable.blank?
dependent_installers = T.let([], T::Array[FormulaInstaller])
unless dry_run
dependent_installers = formula_installers(
upgradeable.dup,
flags:,
force_bottle:,
build_from_source_formulae:,
dependents: true,
interactive:,
keep_tmp:,
debug_symbols:,
force:,
debug:,
quiet:,
verbose:,
)
upgradeable = dependent_installers.map(&:formula)
end
# Print the upgradable dependents.
@@ -337,30 +367,12 @@ module Homebrew
puts format_upgrade_summary(formulae_upgrades).join("\n")
end
return if upgradeable.blank?
unless dry_run
dependent_installers = formula_installers(
upgradeable,
flags:,
force_bottle:,
build_from_source_formulae:,
dependents: true,
interactive:,
keep_tmp:,
debug_symbols:,
force:,
debug:,
quiet:,
verbose:,
)
upgrade_formulae(dependent_installers, dry_run:, verbose:)
end
upgraded_formulae.concat(upgrade_formulae(dependent_installers, verbose:).map(&:formula)) unless dry_run
# Update non-core installed formulae for linkage checks after upgrading
# Don't need to check core formulae because we do so at CI time.
installed_non_core_formulae = FormulaInstaller.installed.to_a.reject(&:core_formula?)
return if installed_non_core_formulae.blank?
return upgraded_formulae if installed_non_core_formulae.blank?
# Assess the dependents tree again now we've upgraded.
unless dry_run
@@ -376,7 +388,7 @@ module Homebrew
else
ohai "No broken dependents found!"
end
return
return upgraded_formulae
end
reinstallable_broken_dependents =
@@ -409,7 +421,7 @@ module Homebrew
.join(", ")
end
return if dry_run
return upgraded_formulae if dry_run
reinstall_contexts = reinstallable_broken_dependents.map do |formula|
Reinstall.build_install_context(
@@ -444,6 +456,7 @@ module Homebrew
puts
Homebrew.failed = true
end
upgraded_formulae
end
sig {
+1 -1
View File
@@ -659,7 +659,7 @@ Relative paths default to `staged_path` for `base:`, `source_base:` and `target_
Use `if_path_exists`, `unless_path_exists`, `on_macos` and `on_linux` blocks to guard one or more steps. A condition is evaluated once when its scope begins, so related steps make the same decision. Use `unless_path_exists` around `write_file` when an existing file must be preserved.
`run` does not evaluate a shell command string. It supports a literal `env:`, `stdin_path:`, `stdout_path:`, `chdir:` and `sudo:`. Standard output is hidden by default and standard error is printed; use `print_stdout: true` or `print_stderr: false` to change that behaviour. Failure aborts the installation or uninstallation; pass `must_succeed: false` when a non-zero exit status is expected and should be ignored, such as a cleanup command that cannot run when its dependency is absent. Nothing is written to `stdout_path:` when an ignored command fails. The complete steps block runs in an isolated Ruby subprocess without network or general home-directory access where Homebrew has an available sandbox. The sandbox permits writes to declared step destinations, the caskroom, app directory, temporary and cache directories and Homebrew's link directories. Ruby file operations and system or cask-provided commands therefore share the same restrictions. Use `writable_paths:` with directory roots, and `writable_base:` for relative roots, when an opaque command needs another declared write location.
`run` does not evaluate a shell command string. It supports a literal `env:`, `stdin_path:`, `stdout_path:`, `chdir:` and `sudo:`. Standard output is hidden by default and standard error is printed; use `print_stdout: true` or `print_stderr: false` to change that behaviour. Failure aborts the installation or uninstallation; pass `must_succeed: false` when a non-zero exit status is expected and should be ignored, such as a cleanup command that cannot run when its dependency is absent. Nothing is written to `stdout_path:` when an ignored command fails. The complete steps block runs in an isolated Ruby subprocess without network or general home-directory access where Homebrew has an available sandbox. Pass `network_access: true` when a `run` command must access the network. The sandbox permits writes to declared step destinations, the caskroom, app directory, temporary and cache directories and Homebrew's link directories. Ruby file operations and system or cask-provided commands therefore share the same restrictions. Use `writable_paths:` with directory roots, and `writable_base:` for relative roots, when an opaque command needs another declared write location.
#### Interpolation in steps blocks