mirror of
https://github.com/Homebrew/brew.git
synced 2026-10-28 09:34:42 +08:00
Merge branch 'main' into sbom-purl-registry
This commit is contained in:
32 files changed
+909
-108
No files matched your search
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user