# 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].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)), ).void } def write_file(path, content, base: nil) add_step("write", "path" => path_spec(path, base:, default_base: @default_base), "content" => content, "overwrite" => true) 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 # 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, 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)), ).void } def run(command, args: [], base: nil, env: {}, sudo: false, print_stdout: false, print_stderr: true, stdin_path: nil, stdout_path: nil, chdir: nil) add_step("run", "command" => path_spec(command, base:, default_base: nil), "args" => args.map(&:to_s), "env" => env, "sudo" => sudo, "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)) 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 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 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:, print_stdout: step["print_stdout"] == true, print_stderr: step["suppress_stderr"] != true, reset_uid: true, chdir: working_directory) return unless step.key?("stdout_path") 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) # Load the formula so missing helper formulae fail before running a guessed path. # rubocop:disable Homebrew/FormulaPathMethods run_command Formula[formula].opt_bin/executable, *args # rubocop:enable Homebrew/FormulaPathMethods end sig { params(base: String, formula: T.nilable(String)).returns(Pathname) } def root_path(base, formula) case base when "home" 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 ::Formula[formula].pkgetc 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"