Files
brew/Library/Homebrew/install_steps.rb
T
Mike McQuaid 8b3a119e78 Allow cask runs to access the network
Structured cask install steps run in a sandbox that denies network
access. This breaks casks whose upstream setup commands must retrieve
installation-time dependencies, including `gcloud-cli` virtualenv
creation.

- Add a serialised `network_access:` option to `run` steps.
- Omit the option from API data when it retains the default denial.
- Enable network access only when an explicit run requests it.
- Preserve network denial for existing steps and generated completions.
- Document the opt-in and cover both sandbox policies in tests.

This avoids disabling network restrictions for all cask operations while
giving affected casks an explicit, auditable escape hatch.

See #23495.
2026-08-11 12:02:16 +01:00

1582 lines
60 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "simulate_system"
require "system_command"
require "utils/output"
module Homebrew
# Declarative install steps that can be serialised through the JSON APIs.
module InstallSteps
PathSpec = T.type_alias { T::Hash[String, String] }
PathSpecs = T.type_alias { T::Array[PathSpec] }
StepValue = T.type_alias { T.any(String, Integer, T::Boolean, T::Array[String], PathSpec, PathSpecs) }
Step = T.type_alias { T::Hash[String, StepValue] }
Steps = T.type_alias { T::Array[Step] }
Paths = T.type_alias { T.any(String, Pathname, T::Array[T.any(String, Pathname)]) }
RawPathSpec = T.type_alias { T::Hash[T.any(String, Symbol), T.nilable(T.any(String, Symbol, Pathname))] }
RawPathSpecs = T.type_alias { T::Array[T.any(String, Symbol, Pathname, RawPathSpec)] }
RawStepValue = T.type_alias do
T.nilable(T.any(String, Symbol, Integer, T::Boolean, Pathname, RawPathSpec, RawPathSpecs))
end
RawStep = T.type_alias { T::Hash[T.any(String, Symbol), RawStepValue] }
SystemCommandArg = T.type_alias { T.any(String, Pathname) }
TemplateTokenValue = T.type_alias { T.any(String, Pathname) }
sig { params(file: Pathname, id: T.any(String, Pathname), resolve_source: T::Boolean).void }
def self.change_dylib_id(file, id, resolve_source: false)
file = file.realpath if resolve_source
require "macho"
file.ensure_writable do
MachO::Tools.change_dylib_id file, id.to_s
MachO.codesign! file if Hardware::CPU.arm?
end
end
class DSL
((instance_methods + private_instance_methods) -
(BasicObject.instance_methods + BasicObject.private_instance_methods) -
[:__callee__, :__method__, :class, :object_id]).each { |method| undef_method method }
class TemplateVersion
sig { returns(String) }
def to_s
"{{version}}"
end
sig { returns(String) }
def major
"{{version.major}}"
end
sig { returns(String) }
def major_minor
"{{version.major_minor}}"
end
end
private_constant :TemplateVersion
TEMPLATE_VERSION = TemplateVersion.new.freeze
private_constant :TEMPLATE_VERSION
ABSOLUTE_TEMPLATE_TOKENS = %w[
HOMEBREW_PREFIX HOMEBREW_CELLAR prefix opt_prefix bin sbin lib libexec share pkgshare var etc pkgetc
staged_path appdir caskroom_path temp rack bash_completion zsh_completion fish_completion pwsh_completion
].freeze
private_constant :ABSOLUTE_TEMPLATE_TOKENS
PRESERVED_STEP_VALUE_KEYS = %w[after args before content env overwrite].freeze
private_constant :PRESERVED_STEP_VALUE_KEYS
sig {
params(
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
default_source_base: ::T.nilable(::T.any(::String, ::Symbol)),
default_target_base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def initialize(default_base: nil, default_source_base: nil, default_target_base: nil)
@default_base = default_base
@default_source_base = default_source_base
@default_target_base = default_target_base
@steps = ::T.let([], Steps)
@guards = ::T.let([], PathSpecs)
@next_guard_id = ::T.let(0, ::Integer)
end
sig { returns(Steps) }
attr_reader :steps
# odeprecated
sig { returns(String) }
def name
"{{name}}"
end
sig { returns(String) }
def formula_name
"{{formula_name}}"
end
sig { returns(String) }
def token
"{{token}}"
end
sig { returns(TemplateVersion) }
def version
TEMPLATE_VERSION
end
sig {
params(
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
default_source_base: ::T.nilable(::T.any(::String, ::Symbol)),
default_target_base: ::T.nilable(::T.any(::String, ::Symbol)),
block: ::T.nilable(::T.proc.bind(DSL).void),
).returns(Steps)
}
def self.build(default_base: nil, default_source_base: nil, default_target_base: nil, &block)
dsl = new(default_base:, default_source_base:, default_target_base:)
dsl.instance_eval(&block) if block
dsl.steps
end
sig {
params(
path: ::T.any(::String, ::Pathname),
base: ::T.nilable(::T.any(::String, ::Symbol)),
block: ::T.proc.void,
).void
}
def if_path_exists(path, base: nil, &block)
with_guard(path_spec(path, base:, default_base: @default_base).merge("condition" => "if_exists"), &block)
end
sig {
params(
path: ::T.any(::String, ::Pathname),
base: ::T.nilable(::T.any(::String, ::Symbol)),
block: ::T.proc.void,
).void
}
def unless_path_exists(path, base: nil, &block)
with_guard(path_spec(path, base:, default_base: @default_base).merge("condition" => "unless_exists"), &block)
end
sig { params(block: ::T.proc.void).void }
def on_macos(&block)
with_guard({ "condition" => "on", "value" => "macos" }, &block)
end
sig { params(block: ::T.proc.void).void }
def on_linux(&block)
with_guard({ "condition" => "on", "value" => "linux" }, &block)
end
sig { params(steps: ::T::Array[RawStep]).returns(Steps) }
def self.normalise_steps(steps)
steps.map do |step|
step = step.to_h do |key, value|
key = key.to_s
[key, normalise_step_value(key, value)]
end
compact_step(step)
end
end
sig { params(step: ::T::Hash[String, ::T.nilable(StepValue)]).returns(Step) }
def self.compact_step(step)
compacted_step = ::T.cast(
::Utils.deep_compact_blank(step.except(*PRESERVED_STEP_VALUE_KEYS)) || {},
Step,
)
PRESERVED_STEP_VALUE_KEYS.each do |key|
value = step[key]
next if value.nil?
next if %w[args env].include?(key) && [[], {}].include?(value)
compacted_step[key] = value
end
compacted_step
end
private_class_method :compact_step
sig { params(key: String, obj: RawStepValue).returns(::T.nilable(StepValue)) }
def self.normalise_step_value(key, obj)
case obj
when Symbol
obj.to_s
when Array
if %w[guards paths writable_paths].include?(key)
obj.map { |value| normalise_path_value(value) }
else
obj.map(&:to_s)
end
when Hash
if key == "env"
::T.cast(obj.to_h { |env_key, value| [env_key.to_s, value&.to_s] }.compact, PathSpec)
else
normalise_path_value(obj)
end
when String, Pathname
if %w[
path source target command matching_certificate stdin_path stdout_path chdir
].include?(key)
normalise_path_value(obj)
else
obj.to_s
end
else
obj
end
end
private_class_method :normalise_step_value
sig { params(obj: T.any(String, Symbol, Pathname, RawPathSpec)).returns(PathSpec) }
def self.normalise_path_value(obj)
case obj
when Hash
::T.cast(obj.to_h { |key, value| [key.to_s, value&.to_s] }.compact_blank, PathSpec)
else
{ "path" => obj.to_s }
end
end
private_class_method :normalise_path_value
# odeprecated
sig { params(path: ::T.any(::String, ::Pathname), base: ::T.nilable(::T.any(::String, ::Symbol))).void }
def mkdir(path, base: nil)
add_step("mkdir", "path" => path_spec(path, base:, default_base: @default_base))
end
sig { params(path: ::T.any(::String, ::Pathname), base: ::T.nilable(::T.any(::String, ::Symbol))).void }
def mkdir_p(path, base: nil)
add_step("mkdir_p", "path" => path_spec(path, base:, default_base: @default_base))
end
sig { params(path: ::T.any(::String, ::Pathname), base: ::T.nilable(::T.any(::String, ::Symbol))).void }
def touch(path, base: nil)
add_step("touch", "path" => path_spec(path, base:, default_base: @default_base))
end
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
# odeprecated
force: ::T::Boolean,
overwrite: ::T::Boolean,
source_glob: ::T::Boolean,
).void
}
def move(source, target, source_base: nil, target_base: nil, force: false, overwrite: true, source_glob: false)
add_step("move",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target, base: target_base, default_base: @default_target_base),
"force" => force,
"overwrite" => overwrite,
"source_glob" => source_glob)
end
# odeprecated
alias mv move
# odeprecated
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def move_children(source, target, source_base: nil, target_base: nil)
add_step("move_children",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target, base: target_base, default_base: @default_target_base))
end
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def move_contents(source, target, source_base: nil, target_base: nil)
add_step("move_contents",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target, base: target_base, default_base: @default_target_base))
end
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
recursive: ::T::Boolean,
overwrite: ::T::Boolean,
source_glob: ::T::Boolean,
).void
}
def copy(source, target, source_base: nil, target_base: nil, recursive: false, overwrite: true,
source_glob: false)
add_step("copy",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target, base: target_base, default_base: @default_target_base),
"recursive" => recursive,
"overwrite" => (false unless overwrite),
"source_glob" => source_glob)
end
sig {
params(
paths: Paths,
base: ::T.nilable(::T.any(::String, ::Symbol)),
recursive: ::T::Boolean,
sudo: ::T.any(::T::Boolean, ::Symbol),
symlink_target_contains: ::T.nilable(::String),
content_contains: ::T.nilable(::String),
).void
}
def remove(paths, base: nil, recursive: false, sudo: false, symlink_target_contains: nil,
content_contains: nil)
add_step("remove",
"paths" => path_specs(paths, base:, default_base: @default_base),
"recursive" => recursive,
"sudo" => sudo.is_a?(::Symbol) ? sudo.to_s : sudo,
"symlink_target_contains" => symlink_target_contains,
"content_contains" => content_contains)
end
sig {
params(
path: ::T.any(::String, ::Pathname),
before: ::T.any(::String, ::Regexp),
after: ::String,
base: ::T.nilable(::T.any(::String, ::Symbol)),
audit_result: ::T::Boolean,
global: ::T::Boolean,
).void
}
def inreplace(path, before, after, base: nil, audit_result: true, global: true)
add_step("inreplace",
"path" => path_spec(path, base:, default_base: @default_base),
"before" => before.is_a?(::Regexp) ? before.source : before,
"after" => after,
"regexp" => before.is_a?(::Regexp),
"regexp_options" => (before.options if before.is_a?(::Regexp)),
"skip_audit" => !audit_result,
"first_only" => !global)
end
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
source_formula: ::T.nilable(::String),
target_formula: ::T.nilable(::String),
# odeprecated
force: ::T::Boolean,
# odeprecated
uninstall: ::T::Boolean,
overwrite: ::T::Boolean,
remove_on_uninstall: ::T::Boolean,
source_glob: ::T::Boolean,
sudo: ::T.any(::T::Boolean, ::Symbol),
).void
}
def symlink(source, target, source_base: nil, target_base: nil, source_formula: nil, target_formula: nil,
force: false, uninstall: false, overwrite: false, remove_on_uninstall: false,
source_glob: false, sudo: false)
add_step("symlink",
"source" => path_spec(source, base: source_base, formula: source_formula,
default_base: @default_source_base),
"target" => path_spec(target, base: target_base, formula: target_formula,
default_base: @default_target_base),
"force" => (true if force || overwrite),
"uninstall" => (true if uninstall || remove_on_uninstall),
"source_glob" => source_glob,
"sudo" => sudo.is_a?(::Symbol) ? sudo.to_s : sudo)
end
# odeprecated
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
source_formula: ::T.nilable(::String),
target_formula: ::T.nilable(::String),
force: ::T::Boolean,
uninstall: ::T::Boolean,
).void
}
def ln_s(source, target, source_base: nil, target_base: nil, source_formula: nil, target_formula: nil,
force: false, uninstall: false)
symlink(source, target, source_base:, target_base:, source_formula:, target_formula:, force:, uninstall:)
end
# odeprecated
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
source_formula: ::T.nilable(::String),
target_formula: ::T.nilable(::String),
uninstall: ::T::Boolean,
).void
}
def ln_sf(source, target, source_base: nil, target_base: nil, source_formula: nil, target_formula: nil,
uninstall: false)
symlink(source, target, source_base:, target_base:, source_formula:, target_formula:, force: true, uninstall:)
end
# odeprecated
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def link_dir(source, target, source_base: nil, target_base: :homebrew_prefix)
add_step("link_dir",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target, base: target_base, default_base: @default_target_base))
end
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def symlink_tree(source, target, source_base: nil, target_base: :homebrew_prefix)
add_step("link_dir",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target, base: target_base, default_base: @default_target_base))
end
# odeprecated
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.nilable(::T.any(::String, ::Pathname)),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
prefix: ::String,
suffix: ::String,
).void
}
def link_children(source, target = nil, source_base: nil, target_base: :homebrew_prefix, prefix: "", suffix: "")
add_step("link_children",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target || source, base: target_base, default_base: @default_target_base),
"prefix" => prefix,
"suffix" => suffix)
end
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.nilable(::T.any(::String, ::Pathname)),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
prefix: ::String,
suffix: ::String,
).void
}
def symlink_children(source, target = nil, source_base: nil, target_base: :homebrew_prefix, prefix: "",
suffix: "")
add_step("link_children",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target || source, base: target_base, default_base: @default_target_base),
"prefix" => prefix,
"suffix" => suffix)
end
# odeprecated
sig {
params(
path: ::T.any(::String, ::Pathname),
content: ::String,
base: ::T.nilable(::T.any(::String, ::Symbol)),
overwrite: ::T::Boolean,
).void
}
def write(path, content, base: nil, overwrite: false)
content = "#{content}\n" unless content.end_with?("\n")
add_step("write",
"path" => path_spec(path, base:, default_base: @default_base),
"content" => content,
"overwrite" => (true if overwrite))
end
sig {
params(
path: ::T.any(::String, ::Pathname),
content: ::String,
base: ::T.nilable(::T.any(::String, ::Symbol)),
overwrite: ::T::Boolean,
append_newline: ::T::Boolean,
).void
}
def write_file(path, content, base: nil, overwrite: true, append_newline: false)
content = "#{content}\n" if append_newline && !content.end_with?("\n")
add_step("write",
"path" => path_spec(path, base:, default_base: @default_base),
"content" => content,
"overwrite" => (true if overwrite))
end
sig {
params(
path: ::T.any(::String, ::Pathname),
using: ::T.any(::String, ::Symbol),
base: ::T.nilable(::T.any(::String, ::Symbol)),
locale: ::T.nilable(::String),
).void
}
def init_data_dir(path, using:, base: nil, locale: nil)
using = case using.to_s
when "postgresql" then "postgresql_initdb"
when "mysql" then "mysql_initialize"
when "mariadb" then "mariadb_install_db"
else using.to_s
end
add_step("init_data_dir",
"path" => path_spec(path, base:, default_base: @default_base),
"using" => using,
"locale" => locale)
end
sig { void }
def compile_gsettings_schemas
add_rebuild_action("compile_gsettings_schemas", "share/glib-2.0/schemas")
end
# odeprecated
sig { void }
def gio_querymodules
add_rebuild_action("gio_querymodules", "lib/gio/modules")
end
sig { void }
def update_gio_modules_cache
add_rebuild_action("gio_querymodules", "lib/gio/modules")
end
# odeprecated
sig { void }
def gdk_pixbuf_query_loaders
add_step("gdk_pixbuf_query_loaders")
end
sig { void }
def update_gdk_pixbuf_loaders_cache
add_step("gdk_pixbuf_query_loaders")
end
# odeprecated
sig { void }
def gtk_update_icon_cache
add_rebuild_action("gtk_update_icon_cache", "share/icons/hicolor")
end
sig { void }
def update_gtk_icon_cache
add_rebuild_action("gtk_update_icon_cache", "share/icons/hicolor")
end
sig { void }
def update_mime_database
add_rebuild_action("update_mime_database", "share/mime")
end
sig { void }
def update_desktop_database
add_rebuild_action("update_desktop_database", "share/applications")
end
# odeprecated
sig {
params(
name: ::String,
matching_certificate: ::T.nilable(::T.any(::String, ::Pathname)),
base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def delete_keychain_certificate(name, matching_certificate: nil, base: nil)
add_step("delete_keychain_certificate",
"name" => name,
"matching_certificate" => (path_spec(matching_certificate, base:, default_base: nil) if
matching_certificate))
end
sig {
params(
name: ::String,
fingerprint_of: ::T.nilable(::T.any(::String, ::Pathname)),
base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def delete_keychain_certificates(name, fingerprint_of: nil, base: nil)
add_step("delete_keychain_certificate",
"name" => name,
"matching_certificate" => (path_spec(fingerprint_of, base:, default_base: nil) if fingerprint_of))
end
sig {
params(
paths: Paths,
permissions: ::String,
base: ::T.nilable(::T.any(::String, ::Symbol)),
recursive: ::T::Boolean,
).void
}
def set_permissions(paths, permissions, base: nil, recursive: true)
add_step("set_permissions",
"paths" => path_specs(paths, base:, default_base: @default_base),
"permissions" => permissions,
"non_recursive" => !recursive)
end
sig {
params(
paths: Paths,
user: ::T.nilable(::String),
group: ::String,
base: ::T.nilable(::T.any(::String, ::Symbol)),
recursive: ::T::Boolean,
).void
}
def set_ownership(paths, user: nil, group: "staff", base: nil, recursive: true)
add_step("set_ownership",
"paths" => path_specs(paths, base:, default_base: @default_base),
"user" => user,
"group" => (group if group != "staff"),
"non_recursive" => !recursive)
end
sig {
params(
source: ::T.any(::String, ::Pathname),
id: ::T.any(::String, ::Pathname),
base: ::T.nilable(::T.any(::String, ::Symbol)),
resolve_source: ::T::Boolean,
).void
}
def change_dylib_id(source, id, base: nil, resolve_source: false)
add_step("change_dylib_id",
"source" => path_spec(source, base:, default_base: @default_source_base),
"id" => id.to_s,
"resolve_source" => resolve_source)
end
sig {
params(
command: ::T.any(::String, ::Pathname),
args: ::T::Array[::T.any(::String, ::Pathname)],
base: ::T.nilable(::T.any(::String, ::Symbol)),
env: ::T::Hash[::String, ::String],
sudo: ::T::Boolean,
must_succeed: ::T::Boolean,
print_stdout: ::T::Boolean,
print_stderr: ::T::Boolean,
stdin_path: ::T.nilable(::T.any(::String, ::Pathname)),
stdout_path: ::T.nilable(::T.any(::String, ::Pathname)),
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, network_access: false)
add_step("run",
"command" => path_spec(command, base:, default_base: nil),
"args" => args.map(&:to_s),
"env" => env,
"sudo" => sudo,
"allow_failure" => !must_succeed,
"print_stdout" => print_stdout,
"suppress_stderr" => !print_stderr,
"stdin_path" => optional_path_spec(stdin_path, default_base: @default_base),
"stdout_path" => optional_path_spec(stdout_path, default_base: @default_base),
"chdir" => optional_path_spec(chdir, default_base: @default_base),
"writable_paths" => path_specs(
writable_paths,
base: writable_base,
default_base: @default_base,
),
"network_access" => network_access)
end
sig {
params(
name: ::String,
match: ::T.any(::String, ::Symbol),
sudo: ::T::Boolean,
attempts: ::Integer,
must_succeed: ::T::Boolean,
notices: ::T::Array[::String],
failure_message: ::T.nilable(::String),
).void
}
def terminate_process(name, match: :name, sudo: false, attempts: 1, must_succeed: false,
notices: [], failure_message: nil)
::Kernel.raise ::ArgumentError, "terminate_process attempts must be positive" if attempts < 1
match = match.to_s
unless %w[name full].include?(match)
::Kernel.raise ::ArgumentError, "terminate_process match must be :name or :full"
end
add_step("terminate_process",
"name" => name,
"match" => (match if match != "name"),
"sudo" => sudo,
"attempts" => (attempts if attempts != 1),
"must_succeed" => must_succeed,
"notices" => notices,
"failure_message" => failure_message)
end
sig { params(message: ::String).void }
def warn(message)
add_step("warn", "message" => message)
end
sig { void }
def configure_gcc_runtime
add_step("configure_gcc_runtime")
end
sig {
params(
source: ::T.any(::String, ::Pathname),
target: ::T.any(::String, ::Pathname),
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
).void
}
def install_gzipped_executable(source, target, source_base: nil, target_base: nil)
add_step("install_gzipped_executable",
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
"target" => path_spec(target, base: target_base, default_base: @default_target_base))
end
sig { void }
def configure_glibc_runtime
add_step("configure_glibc_runtime")
end
sig { void }
def configure_clang_system
add_step("configure_clang_system")
end
sig { void }
def configure_php
add_step("configure_php")
end
sig { void }
def bootstrap_cpython
add_step("bootstrap_cpython")
end
sig { params(abi_version: ::String).void }
def bootstrap_pypy(abi_version:)
add_step("bootstrap_pypy", "abi_version" => abi_version)
end
private
sig { params(guard: PathSpec, block: ::T.proc.bind(DSL).void).void }
def with_guard(guard, &block)
previous_guards = ::T.let(nil, ::T.nilable(PathSpecs))
previous_guards = @guards
@next_guard_id += 1
@guards = [*@guards, guard.merge("id" => @next_guard_id.to_s)]
instance_eval(&block)
ensure
@guards = previous_guards if previous_guards
end
sig { params(type: ::String, fields: ::T.nilable(StepValue)).void }
def add_step(type, **fields)
step = fields.transform_keys(&:to_s)
step["guards"] = @guards unless @guards.empty?
step["type"] = type
@steps.concat(::Homebrew::InstallSteps::DSL.normalise_steps([step]))
end
sig { params(type: ::String, path: ::String).void }
def add_rebuild_action(type, path)
add_step(type, "path" => path_spec(path, base: :homebrew_prefix))
end
sig {
params(
path: ::T.any(::String, ::Pathname),
base: ::T.nilable(::T.any(::String, ::Symbol)),
formula: ::T.nilable(::String),
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
).returns(PathSpec)
}
def path_spec(path, base:, formula: nil, default_base: nil)
{
"base" => (base || default_base_for(path, default_base))&.to_s,
"formula" => formula,
"path" => path.to_s,
}.compact_blank
end
sig {
params(
paths: Paths,
base: ::T.nilable(::T.any(::String, ::Symbol)),
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
).returns(PathSpecs)
}
def path_specs(paths, base:, default_base:)
paths = [paths] unless paths.is_a?(Array)
paths.map { |path| path_spec(path, base:, default_base:) }
end
sig {
params(
path: ::T.nilable(::T.any(::String, ::Pathname)),
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
).returns(::T.nilable(PathSpec))
}
def optional_path_spec(path, default_base:)
path_spec(path, base: nil, default_base:) if path
end
sig {
params(
path: ::T.any(::String, ::Pathname),
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
).returns(::T.nilable(::T.any(::String, ::Symbol)))
}
def default_base_for(path, default_base)
path = path.to_s
return if path.start_with?("/", "~")
return if ABSOLUTE_TEMPLATE_TOKENS.any? { |token| path.start_with?("{{#{token}}}") }
default_base
end
end
class Runner
include SystemCommand::Mixin
include ::Utils::Output::Mixin
# Path tokens reuse the step base resolution; formula metadata tokens are
# resolved separately. Anything else is left verbatim so literal braces in
# templates are never rewritten.
CONTENT_PATH_TOKENS = %w[
prefix opt_prefix bin sbin lib libexec share pkgshare var etc pkgetc staged_path appdir caskroom_path
temp rack
bash_completion zsh_completion fish_completion pwsh_completion
].freeze
IMPLICIT_SUDO_STEP_TYPES = %w[delete_keychain_certificate set_ownership].freeze
sig { params(context: Object, command: T.class_of(SystemCommand)).void }
def initialize(context:, command: SystemCommand)
@context = context
@command = command
@guard_results = T.let({}, T::Hash[PathSpec, T::Boolean])
end
sig { params(steps: Steps, phase: Symbol).void }
def run(steps, phase: :install)
@guard_results.clear
DSL.normalise_steps(steps).each do |step|
if phase == :uninstall
run_uninstall_step(step)
else
run_install_step(step)
end
end
end
sig { params(steps: Steps, phase: Symbol).returns(T::Array[Pathname]) }
def sandbox_write_paths(steps, phase: :install)
DSL.normalise_steps(steps).flat_map do |step|
if phase == :uninstall
next [] if step["type"] != "symlink" || step["uninstall"] != true
next [resolve_path(step_path(step, "target")).parent]
end
case step.fetch("type")
when "mkdir", "mkdir_p", "touch", "write"
[resolve_path(step_path(step, "path")).parent]
when "move"
[resolve_path(step_path(step, "source")).parent, resolve_path(step_path(step, "target")).parent]
when "move_children", "move_contents"
[resolve_path(step_path(step, "source")), resolve_path(step_path(step, "target"))]
when "copy", "symlink"
[resolve_path(step_path(step, "target")).parent]
when "remove"
step_paths(step, "paths").flat_map { |path| expand_path_glob(path) }.map(&:parent)
when "inreplace", "change_dylib_id"
key = (step["type"] == "inreplace") ? "path" : "source"
[resolve_path(step_path(step, key))]
when "link_dir", "link_children"
[resolve_path(step_path(step, "target"))]
when "run"
paths = step.key?("stdout_path") ? [resolve_path(step_path(step, "stdout_path")).parent] : []
if step.key?("writable_paths")
paths.concat(step_paths(step, "writable_paths").map do |path|
resolve_path(path)
end)
end
paths
when "set_permissions", "set_ownership"
existing_step_paths(step)
else
[]
end
end.uniq
end
sig { params(steps: Steps).returns(T::Boolean) }
def sudo_required?(steps)
DSL.normalise_steps(steps).any? do |step|
step["sudo"] == true || step["sudo"] == "if_needed" ||
IMPLICIT_SUDO_STEP_TYPES.include?(step["type"])
end
end
private
sig { params(step: Step).void }
def run_install_step(step)
return unless step_guards_match?(step)
case step.fetch("type")
when "mkdir"
resolve_path(step_path(step, "path")).mkdir
when "mkdir_p"
resolve_path(step_path(step, "path")).mkpath
when "init_data_dir"
run_init_data_dir(step)
when "touch"
path = resolve_path(step_path(step, "path"))
path.dirname.mkpath
FileUtils.touch path
when "move"
source = resolve_step_source(step)
target = resolve_path(step_path(step, "target"))
target.dirname.mkpath
destination = step_destination(source, target)
if step.key?("overwrite")
overwrite = step["overwrite"] == true || step["force"] == true
raise Errno::EEXIST, destination.to_s if destination.exist? && !overwrite
FileUtils.rm_rf destination if overwrite && destination != source && (source.exist? || source.symlink?)
FileUtils.mv source, target
else
FileUtils.mv source, target, force: step["force"] == true
end
when "move_children", "move_contents"
source = resolve_path(step_path(step, "source"))
target = resolve_path(step_path(step, "target"))
target.mkpath
children = source.children.reject { |child| child == target }
return if children.empty?
FileUtils.mv children, target
when "copy"
source = resolve_step_source(step)
target = resolve_path(step_path(step, "target"))
target.dirname.mkpath
destination = step_destination(source, target)
overwrite = step["overwrite"] != false
raise Errno::EEXIST, destination.to_s if destination.exist? && !overwrite
if step["recursive"] == true
FileUtils.cp_r source, target, remove_destination: overwrite
else
FileUtils.rm_f destination if overwrite && destination.symlink?
FileUtils.cp source, target
end
when "remove"
paths = step_paths(step, "paths").flat_map { |path| expand_path_glob(path) }
if step.key?("symlink_target_contains")
paths.select! do |path|
path.symlink? && path.readlink.to_s.include?(step_string(step, "symlink_target_contains"))
end
end
if step.key?("content_contains")
paths.select! do |path|
path.file? && path.readable? && path.read.include?(step_string(step, "content_contains"))
end
end
paths.each do |path|
if step["sudo"] == true || (step["sudo"] == "if_needed" && !path.dirname.writable?)
require "cask/utils"
::Cask::Utils.gain_permissions_remove(path, command: @command)
elsif step["recursive"] == true
FileUtils.rm_rf path
else
FileUtils.rm_f path
end
end
when "inreplace"
require "utils/inreplace"
path = resolve_path(step_path(step, "path"))
before = expand_template_tokens(step_string(step, "before"))
after = expand_template_tokens(step_string(step, "after"))
regexp_options = T.cast(step["regexp_options"], T.nilable(Integer))
before = Regexp.new(before, regexp_options || 0) if step["regexp"] == true
Utils::Inreplace.inreplace(path, before, after,
audit_result: step["skip_audit"] != true,
global: step["first_only"] != true)
when "link_dir"
source_dir = resolve_path(step_path(step, "source"))
target_dir = resolve_path(step_path(step, "target"))
source_dir.find do |source|
link_target = target_dir/source.relative_path_from(source_dir)
next if source.basename.to_s == ".DS_Store"
next if link_target.directory? && !link_target.symlink?
FileUtils.rm_f(link_target) if link_target.exist? || link_target.symlink?
if source.symlink? || source.file?
link_target.parent.install_symlink source
elsif source.directory?
link_target.mkpath
end
end
when "link_children"
target_dir = resolve_path(step_path(step, "target"))
target_dir.mkpath
link_prefix = expand_template_tokens(step["prefix"].to_s)
link_suffix = expand_template_tokens(step["suffix"].to_s)
resolve_path(step_path(step, "source")).each_child do |source|
target_dir.install_symlink source => "#{link_prefix}#{source.basename}#{link_suffix}"
end
when "symlink"
target = resolve_path(step_path(step, "target"))
if step["source_glob"] == true
sources = expand_path_glob(step_path(step, "source"))
return if sources.empty?
if sources.length > 1 || target.directory?
target.mkpath
sources.each { |source| create_symlink(source, target/source.basename, step) }
else
source = sources.first
create_symlink(source, target, step) if source
end
else
create_symlink(link_source(step_path(step, "source")), target, step)
end
when "write"
content = T.cast(step["content"], T.nilable(String))
raise ArgumentError, "install step write requires content" if content.nil?
path = resolve_path(step_path(step, "path"))
if step["overwrite"] == true || !path.exist?
path.dirname.mkpath
path.atomic_write(expand_template_tokens(content))
end
when "run"
run_serialised_command(step)
when "terminate_process"
run_terminate_process(step)
when "change_dylib_id"
Homebrew::InstallSteps.change_dylib_id(
resolve_path(step_path(step, "source")),
expand_template_tokens(step_string(step, "id")),
resolve_source: step["resolve_source"] == true,
)
when "warn"
opoo expand_template_tokens(step_string(step, "message"))
when "configure_gcc_runtime"
run_configure_gcc_runtime
when "install_gzipped_executable"
run_install_gzipped_executable(step)
when "configure_glibc_runtime"
run_configure_glibc_runtime
when "configure_clang_system"
run_configure_clang_system
when "configure_php"
run_configure_php
when "bootstrap_cpython"
run_bootstrap_cpython
when "bootstrap_pypy"
run_bootstrap_pypy(step_string(step, "abi_version"))
when "set_permissions"
run_set_permissions(step)
when "set_ownership"
run_set_ownership(step)
when "compile_gsettings_schemas"
run_formula_tool("glib", "glib-compile-schemas", resolve_path(step_path(step, "path")))
when "gio_querymodules"
run_formula_tool("glib", "gio-querymodules", resolve_path(step_path(step, "path")))
when "gdk_pixbuf_query_loaders"
run_formula_tool("gdk-pixbuf", "gdk-pixbuf-query-loaders", "--update-cache")
when "gtk_update_icon_cache"
require "utils/path"
if Utils::Path.formula_any_version_installed?("gtk4")
run_formula_tool("gtk4", "gtk4-update-icon-cache", "-q", "-t", "-f",
resolve_path(step_path(step, "path")))
else
run_formula_tool("gtk+3", "gtk3-update-icon-cache", "-q", "-t", "-f",
resolve_path(step_path(step, "path")))
end
when "update_mime_database"
run_formula_tool("shared-mime-info", "update-mime-database", resolve_path(step_path(step, "path")))
when "update_desktop_database"
run_formula_tool("desktop-file-utils", "update-desktop-database", resolve_path(step_path(step, "path")))
when "delete_keychain_certificate"
certificate_hash = nil
if step.key?("matching_certificate")
certificate = resolve_path(step_path(step, "matching_certificate"))
return unless certificate.exist?
certificate_hash = run_command_output("/usr/bin/openssl", "x509", "-fingerprint", "-sha256", "-noout",
"-in", certificate)
.lines
.first
.to_s
.split("=", 2)[1]
.to_s
.delete(":")
.strip
.upcase
return if certificate_hash.blank?
end
certificate_hashes = run_command_output(
"/usr/bin/security", "find-certificate", "-a", "-c", step_string(step, "name"), "-Z",
sudo: true
).lines.filter_map { |line| line[/\ASHA-256 hash:\s*(\S+)/, 1]&.upcase }
if certificate_hash
run_command "/usr/bin/security", "delete-certificate", "-Z", certificate_hash, sudo: true if
certificate_hashes.include?(certificate_hash)
else
certificate_hashes.each do |matching_certificate_hash|
run_command "/usr/bin/security", "delete-certificate", "-Z", matching_certificate_hash, sudo: true
end
end
else
raise ArgumentError, "unknown install step: #{step.fetch("type")}"
end
end
sig { params(step: Step).returns(T::Boolean) }
def step_guards_match?(step)
guards = T.cast(step["guards"], T.nilable(PathSpecs))
guards.nil? || guards.all? { |guard| guard_matches?(guard) }
end
sig { params(guard: PathSpec).returns(T::Boolean) }
def guard_matches?(guard)
return @guard_results.fetch(guard) if @guard_results.key?(guard)
matches = case guard.fetch("condition")
when "if_exists"
path_spec_exists?(guard)
when "unless_exists"
!path_spec_exists?(guard)
when "on"
case guard.fetch("value")
when "macos" then Homebrew::SimulateSystem.simulating_or_running_on_macos?
when "linux" then Homebrew::SimulateSystem.simulating_or_running_on_linux?
else false
end
else
false
end
@guard_results[guard] = matches
end
sig { params(step: Step).void }
def run_serialised_command(step)
command = resolve_command(step_path(step, "command"))
args = T.cast(step["args"], T.nilable(T::Array[String]))&.map { |arg| expand_template_tokens(arg) } || []
environment = T.cast(step["env"] || {}, PathSpec)
.transform_values { |value| expand_template_tokens(value.to_s) }
input = step.key?("stdin_path") ? resolve_path(step_path(step, "stdin_path")).read : []
working_directory = resolve_path(step_path(step, "chdir")) if step.key?("chdir")
result = @command.run(command, args:, sudo: step["sudo"] == true, env: environment, input:,
must_succeed: step["allow_failure"] != true,
print_stdout: step["print_stdout"] == true,
print_stderr: step["suppress_stderr"] != true, reset_uid: true,
chdir: working_directory)
return unless step.key?("stdout_path")
return unless result.success?
output_path = resolve_path(step_path(step, "stdout_path"))
output_path.dirname.mkpath
output_path.write(result.stdout)
end
sig { params(step: Step).void }
def run_terminate_process(step)
T.cast(step["notices"], T.nilable(T::Array[String]))&.each do |notice|
ohai expand_template_tokens(notice)
end
name = expand_template_tokens(step_string(step, "name"))
if step["match"] == "full"
command = "/usr/bin/pkill"
args = ["-f", name]
else
command = "/usr/bin/killall"
args = [name]
end
attempts = T.cast(step["attempts"] || 1, Integer)
begin
run_command command, *args, sudo: step["sudo"] == true
rescue ErrorDuringExecution
attempts -= 1
if attempts <= 0
failure_message = T.cast(step["failure_message"], T.nilable(String))
opoo expand_template_tokens(failure_message) if failure_message
raise if step["must_succeed"] == true
return
end
sleep 1
retry
end
end
sig { params(source: SystemCommandArg, target: Pathname, step: Step).void }
def create_symlink(source, target, step)
target.dirname.mkpath
if step["sudo"] == true || (step["sudo"] == "if_needed" && !target.dirname.writable?)
args = ["-s"]
args << "-f" if step["force"] == true
@command.run!("/bin/ln", args: [*args, source, target], sudo: true)
else
FileUtils.rm_f target if step["force"] == true
File.symlink source, target
end
end
sig { params(step: Step).void }
def run_set_permissions(step)
paths = existing_step_paths(step)
return if paths.empty?
args = []
args << "-R" if step["non_recursive"] != true
@command.run!("chmod", args: [*args, "--", step_string(step, "permissions"), *paths], sudo: false)
end
sig { params(step: Step).void }
def run_set_ownership(step)
require "cask/quarantine"
require "utils/user"
paths = existing_step_paths(step)
return if paths.empty?
paths.each do |path|
next if ::Cask::Quarantine.app_management_permissions_granted?(app: path, command: @command)
raise ::Cask::CaskError, <<~EOS
Cannot change the ownership of '#{path}' because your terminal does not have App Management permissions.
macOS prevents modifying apps without these permissions, even when using `sudo`.
To fix this, approve the permissions prompt (if one was just shown) or go to
System Settings Privacy & Security App Management and add or enable your terminal.
Then run this command again.
EOS
end
ohai "Changing ownership of paths required by #{@context} with `sudo` (which may request your password)..."
args = []
args << "-R" if step["non_recursive"] != true
@command.run!("chown", args: [*args, "--", "#{step["user"] || ::User.current}:#{step["group"] || "staff"}",
*paths],
sudo: true)
end
sig { params(step: Step).void }
def run_uninstall_step(step)
return if step.fetch("type") != "symlink"
return if step["uninstall"] != true
target = resolve_path(step_path(step, "target"))
return unless target.symlink?
return if target.readlink != Pathname(link_source(step_path(step, "source")))
if step["sudo"] == true || (step["sudo"] == "if_needed" && !target.dirname.writable?)
require "cask/utils"
::Cask::Utils.gain_permissions_remove(target, command: @command)
else
FileUtils.rm_f target
end
end
sig { params(step: Step).void }
def run_init_data_dir(step)
using = step_string(step, "using")
marker = case using
when "postgresql_initdb"
"PG_VERSION"
when "mysql_initialize"
"mysql/general_log.CSM"
when "mariadb_install_db"
"mysql/user.frm"
else
raise ArgumentError, "unknown data directory initialiser: #{using}"
end
path = resolve_path(step_path(step, "path"))
path.mkpath
return if ENV["HOMEBREW_GITHUB_ACTIONS"].present?
return if (path/marker).exist?
bin = context_path("bin")
prefix = context_path("prefix")
case using
when "postgresql_initdb"
run_command bin/"initdb", "--locale=#{step["locale"] || "en_US.UTF-8"}", "-E", "UTF-8", path
when "mysql_initialize"
with_env(TMPDIR: nil) do
run_command bin/"mysqld", "--initialize-insecure", "--user=#{ENV.fetch("USER")}",
"--basedir=#{prefix}", "--datadir=#{path}", "--tmpdir=/tmp"
end
when "mariadb_install_db"
with_env(TMPDIR: nil) do
run_command bin/"mysql_install_db", "--verbose", "--user=#{ENV.fetch("USER")}",
"--basedir=#{prefix}", "--datadir=#{path}", "--tmpdir=/tmp"
end
end
end
sig { params(content: String).returns(String) }
def expand_template_tokens(content)
content.gsub(/\{\{([A-Za-z_][\w.]*)\}\}/) do |match|
value = template_token_value(T.must(Regexp.last_match(1)))
value.nil? ? match : value.to_s
end
end
sig { params(token: String).returns(T.nilable(TemplateTokenValue)) }
def template_token_value(token)
case token
when "HOMEBREW_BREW_FILE"
HOMEBREW_BREW_FILE
when "HOMEBREW_CELLAR"
HOMEBREW_CELLAR
when "HOMEBREW_PREFIX"
HOMEBREW_PREFIX
when "formula_name"
context_value(:name)&.to_s
when "name"
context_name
when "token"
context_value(:token)&.to_s
when "user"
ENV.fetch("USER")
when "version"
context_version
when "version.major"
context_version_major
when "version.major_minor"
context_version_major_minor
else
root_path(token, nil) if CONTENT_PATH_TOKENS.include?(token)
end
end
sig { params(step: Step, key: String).returns(PathSpec) }
def step_path(step, key)
T.cast(step.fetch(key), PathSpec)
end
sig { params(step: Step, key: String).returns(PathSpecs) }
def step_paths(step, key)
T.cast(step.fetch(key), PathSpecs)
end
sig { params(step: Step).returns(T::Array[Pathname]) }
def existing_step_paths(step)
step_paths(step, "paths").flat_map { |spec| expand_path_glob(spec) }.select(&:exist?)
end
sig { params(step: Step).returns(Pathname) }
def resolve_step_source(step)
source_spec = step_path(step, "source")
source = resolve_path(source_spec)
return source if step["source_glob"] != true
sources = expand_path_glob(source_spec).select { |path| path.exist? || path.symlink? }.uniq
raise ArgumentError, "install step source glob must match exactly one path: #{source}" if sources.length != 1
sources.fetch(0)
end
sig { params(source: Pathname, target: Pathname).returns(Pathname) }
def step_destination(source, target)
target.directory? ? target/source.basename : target
end
sig { params(spec: PathSpec).returns(T::Array[Pathname]) }
def expand_path_glob(spec)
base = spec["base"]
# odeprecated
base = "search_path" if base == "path"
if base == "search_path"
path = expand_template_tokens(spec.fetch("path"))
return ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).flat_map do |directory|
candidate = Pathname(directory)/path
candidate.to_s.match?(/[?*\[{]/) ? Pathname.glob(candidate.to_s) : [candidate]
end
end
path = resolve_path(spec).expand_path
return [path] unless path.to_s.match?(/[?*\[{]/)
Pathname.glob(path.to_s)
end
sig { params(spec: PathSpec).returns(T::Boolean) }
def path_spec_exists?(spec)
expand_path_glob(spec).any?(&:exist?)
end
sig { params(step: Step, key: String).returns(String) }
def step_string(step, key)
T.cast(step.fetch(key), String)
end
sig { returns(T.nilable(String)) }
def context_name
value = context_value(:name) || context_value(:token)
value&.to_s
end
sig { returns(T.nilable(String)) }
def context_version
context_value(:version)&.to_s
end
sig { returns(T.nilable(String)) }
def context_version_major
context_version_value = context_version
return if context_version_value.blank?
Version.new(context_version_value).major&.to_s
end
sig { returns(T.nilable(String)) }
def context_version_major_minor
context_version_value = context_version
return if context_version_value.blank?
Version.new(context_version_value).major_minor.to_s
end
sig { params(spec: PathSpec).returns(Pathname) }
def resolve_path(spec)
path = Pathname(expand_template_tokens(spec.fetch("path")))
base = spec["base"]
return path.expand_path if base.blank? || base == "absolute"
return path if base == "relative"
root_path(base, spec["formula"])/path
end
sig { params(spec: PathSpec).returns(SystemCommandArg) }
def resolve_command(spec)
return expand_template_tokens(spec.fetch("path")) if spec["base"].blank? || spec["base"] == "relative"
resolve_path(spec)
end
sig { params(spec: PathSpec).returns(String) }
def link_source(spec)
return expand_template_tokens(spec.fetch("path")) if spec["base"] == "relative"
resolve_path(spec).to_s
end
sig { params(formula: String, executable: String, args: SystemCommandArg).void }
def run_formula_tool(formula, executable, *args)
require "utils/path"
tool = Utils::Path.formula_opt_bin(formula)/executable
raise ArgumentError, "#{formula} is missing required executable: #{tool}" unless tool.executable?
run_command tool, *args
end
sig { params(base: String, formula: T.nilable(String)).returns(Pathname) }
def root_path(base, formula)
case base
when "home"
context_value(:home) ? context_path(base) : Pathname(Dir.home)
when "temp"
HOMEBREW_TEMP
when "homebrew_prefix"
HOMEBREW_PREFIX
when "formula_pkgetc"
formula_base(formula, :pkgetc)
when "formula_opt_prefix"
formula_base(formula, :opt_prefix)
else
context_path(base)
end
end
sig { params(base: String).returns(Pathname) }
def context_path(base)
method = base.to_sym
value = context_value(method) || context_config_value(method)
raise ArgumentError, "unknown install step base: #{base}" if value.nil?
Pathname(value.to_s)
end
sig { params(formula: T.nilable(String), method: Symbol).returns(Pathname) }
def formula_base(formula, method)
raise ArgumentError, "missing formula for install step base" if formula.blank?
case method
when :pkgetc
HOMEBREW_PREFIX/"etc"/Utils.name_from_full_name(formula)
when :opt_prefix
Utils::Path.formula_opt_prefix(formula)
else
raise ArgumentError, "unknown formula install step base: #{method}"
end
end
sig { params(method: Symbol).returns(T.nilable(Object)) }
def context_value(method)
@context.public_send(method) if @context.respond_to?(method)
end
sig { params(method: Symbol).returns(T.nilable(Object)) }
def context_config_value(method)
config = context_value(:config)
config.public_send(method) if config.respond_to?(method)
end
sig { params(command: SystemCommandArg, args: SystemCommandArg, sudo: T::Boolean).void }
def run_command(command, *args, sudo: false)
@command.run!(command, args: args, sudo:, print_stdout: true, print_stderr: true, reset_uid: true)
end
sig { params(command: SystemCommandArg, args: SystemCommandArg, sudo: T::Boolean).returns(String) }
def run_command_output(command, *args, sudo: false)
@command.run!(command, args: args, sudo:, print_stderr: true, reset_uid: true).stdout
end
end
end
end
require "install_steps/formula_actions"