diff --git a/Library/Homebrew/.rubocop.yml b/Library/Homebrew/.rubocop.yml index 9ef9b0adc0..4448ac866a 100644 --- a/Library/Homebrew/.rubocop.yml +++ b/Library/Homebrew/.rubocop.yml @@ -74,6 +74,7 @@ Style/Documentation: - resource.rb - startup/config.rb - utils/inreplace.rb + - utils/output.rb - utils/shebang.rb - utils/string_inreplace_extension.rb - version.rb diff --git a/Library/Homebrew/abstract_command.rb b/Library/Homebrew/abstract_command.rb index 03b493c834..35cc322782 100644 --- a/Library/Homebrew/abstract_command.rb +++ b/Library/Homebrew/abstract_command.rb @@ -3,6 +3,7 @@ require "cli/parser" require "shell_command" +require "utils/output" module Homebrew # Subclass this to implement a `brew` command. This is preferred to declaring a named function in the `Homebrew` @@ -19,6 +20,7 @@ module Homebrew # @api public class AbstractCommand extend T::Helpers + include Utils::Output::Mixin abstract! diff --git a/Library/Homebrew/aliases/alias.rb b/Library/Homebrew/aliases/alias.rb index 143c760784..5d0b934657 100644 --- a/Library/Homebrew/aliases/alias.rb +++ b/Library/Homebrew/aliases/alias.rb @@ -2,10 +2,13 @@ # frozen_string_literal: true require "fileutils" +require "utils/output" module Homebrew module Aliases class Alias + include ::Utils::Output::Mixin + sig { returns(String) } attr_accessor :name diff --git a/Library/Homebrew/aliases/aliases.rb b/Library/Homebrew/aliases/aliases.rb index a684816442..b8aa064a4b 100644 --- a/Library/Homebrew/aliases/aliases.rb +++ b/Library/Homebrew/aliases/aliases.rb @@ -2,9 +2,12 @@ # frozen_string_literal: true require "aliases/alias" +require "utils/output" module Homebrew module Aliases + extend Utils::Output::Mixin + RESERVED = T.let(( Commands.internal_commands + Commands.internal_developer_commands + diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index 6f2043728a..4cb5adb48e 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -6,10 +6,13 @@ require "api/cask" require "api/formula" require "api/internal" require "base64" +require "utils/output" module Homebrew # Helper functions for using Homebrew's formulae.brew.sh API. module API + extend Utils::Output::Mixin + extend Cachable HOMEBREW_CACHE_API = T.let((HOMEBREW_CACHE/"api").freeze, Pathname) diff --git a/Library/Homebrew/attestation.rb b/Library/Homebrew/attestation.rb index 0461bdd4f6..87a4d57ba3 100644 --- a/Library/Homebrew/attestation.rb +++ b/Library/Homebrew/attestation.rb @@ -7,10 +7,12 @@ require "utils/popen" require "utils/github/api" require "exceptions" require "system_command" +require "utils/output" module Homebrew module Attestation extend SystemCommand::Mixin + extend Utils::Output::Mixin # @api private HOMEBREW_CORE_REPO = "Homebrew/homebrew-core" diff --git a/Library/Homebrew/brew.rb b/Library/Homebrew/brew.rb index f38ef7ec7e..3aec973b6b 100644 --- a/Library/Homebrew/brew.rb +++ b/Library/Homebrew/brew.rb @@ -16,6 +16,7 @@ end std_trap = trap("INT") { exit! 130 } # no backtrace thanks require_relative "global" +require "utils/output" begin trap("INT", std_trap) # restore default CTRL-C handler @@ -115,14 +116,14 @@ begin converted_cmd = cmd.downcase.tr("-", "_") case_error = "undefined method `#{converted_cmd}' for module Homebrew" private_method_error = "private method `#{converted_cmd}' called for module Homebrew" - odie "Unknown command: brew #{cmd}" if [case_error, private_method_error].include?(e.message) + Utils::Output.odie "Unknown command: brew #{cmd}" if [case_error, private_method_error].include?(e.message) raise end end elsif (path = Commands.external_ruby_cmd_path(cmd)) Homebrew.running_command = cmd - require?(path) + Homebrew.require?(path) exit Homebrew.failed? ? 1 : 0 elsif Commands.external_cmd_path(cmd) %w[CACHE LIBRARY_PATH].each do |env| @@ -139,14 +140,16 @@ begin possible_tap.installed? || (blocked_tap = Tap.untapped_official_taps.include?(possible_tap.name)) if blocked_tap - onoe <<~EOS + Utils::Output.onoe <<~EOS `brew #{cmd}` is unavailable because #{possible_tap.name} was manually untapped. Run `brew tap #{possible_tap.name}` to reenable `brew #{cmd}`. EOS end # Check for cask explicitly because it's very common in old guides - odie "`brew cask` is no longer a `brew` command. Use `brew --cask` instead." if cmd == "cask" - odie "Unknown command: brew #{cmd}" + if cmd == "cask" + Utils::Output.odie "`brew cask` is no longer a `brew` command. Use `brew --cask` instead." + end + Utils::Output.odie "Unknown command: brew #{cmd}" end # Unset HOMEBREW_HELP to avoid confusing the tap @@ -171,7 +174,7 @@ rescue UsageError => e require "help" Homebrew::Help.help cmd, remaining_args: args&.remaining || [], usage_error: e.message rescue SystemExit => e - onoe "Kernel.exit" if args&.debug? && !e.success? + Utils::Output.onoe "Kernel.exit" if args&.debug? && !e.success? if args&.debug? || ARGV.include?("--debug") require "utils/backtrace" $stderr.puts Utils::Backtrace.clean(e) @@ -209,7 +212,7 @@ rescue BuildError => e rescue RuntimeError, SystemCallError => e raise if e.message.empty? - onoe e + Utils::Output.onoe e if args&.debug? || ARGV.include?("--debug") require "utils/backtrace" $stderr.puts Utils::Backtrace.clean(e) @@ -218,7 +221,7 @@ rescue RuntimeError, SystemCallError => e exit 1 # Catch any other types of exceptions. rescue Exception => e # rubocop:disable Lint/RescueException - onoe e + Utils::Output.onoe e method_deprecated_error = e.is_a?(MethodDeprecatedError) require "utils/backtrace" diff --git a/Library/Homebrew/build.rb b/Library/Homebrew/build.rb index 0cfdd8d5e6..69ba43e9de 100644 --- a/Library/Homebrew/build.rb +++ b/Library/Homebrew/build.rb @@ -16,9 +16,12 @@ require "fcntl" require "utils/socket" require "cmd/install" require "json/add/exception" +require "utils/output" # A formula build. class Build + include Utils::Output::Mixin + attr_reader :formula, :deps, :reqs, :args def initialize(formula, options, args:) diff --git a/Library/Homebrew/bundle/commands/exec.rb b/Library/Homebrew/bundle/commands/exec.rb index 27d89cb193..df418787b8 100644 --- a/Library/Homebrew/bundle/commands/exec.rb +++ b/Library/Homebrew/bundle/commands/exec.rb @@ -6,11 +6,14 @@ require "exceptions" require "extend/ENV" require "utils" require "PATH" +require "utils/output" module Homebrew module Bundle module Commands module Exec + extend Utils::Output::Mixin + PATH_LIKE_ENV_REGEX = /.+#{File::PATH_SEPARATOR}/ sig { diff --git a/Library/Homebrew/bundle/formula_dumper.rb b/Library/Homebrew/bundle/formula_dumper.rb index b39514137f..635ab09507 100644 --- a/Library/Homebrew/bundle/formula_dumper.rb +++ b/Library/Homebrew/bundle/formula_dumper.rb @@ -3,11 +3,14 @@ require "json" require "tsort" +require "utils/output" module Homebrew module Bundle # TODO: refactor into multiple modules module FormulaDumper + extend Utils::Output::Mixin + def self.reset! require "bundle/brew_services" diff --git a/Library/Homebrew/bundle/remover.rb b/Library/Homebrew/bundle/remover.rb index c99e329fd3..91eb11ee77 100644 --- a/Library/Homebrew/bundle/remover.rb +++ b/Library/Homebrew/bundle/remover.rb @@ -1,9 +1,13 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "utils/output" + module Homebrew module Bundle module Remover + extend ::Utils::Output::Mixin + def self.remove(*args, type:, global:, file:) require "bundle/brewfile" require "bundle/dumper" diff --git a/Library/Homebrew/bundle/whalebrew_dumper.rb b/Library/Homebrew/bundle/whalebrew_dumper.rb index 0fe8f99af6..12ce6b99a4 100644 --- a/Library/Homebrew/bundle/whalebrew_dumper.rb +++ b/Library/Homebrew/bundle/whalebrew_dumper.rb @@ -1,9 +1,13 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Homebrew module Bundle module WhalebrewDumper + extend Utils::Output::Mixin + sig { void } def self.reset! @images = T.let(nil, T.nilable(T::Array[String])) diff --git a/Library/Homebrew/bundle/whalebrew_installer.rb b/Library/Homebrew/bundle/whalebrew_installer.rb index f8a6221463..21bb0d2adf 100644 --- a/Library/Homebrew/bundle/whalebrew_installer.rb +++ b/Library/Homebrew/bundle/whalebrew_installer.rb @@ -1,9 +1,13 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "utils/output" + module Homebrew module Bundle module WhalebrewInstaller + extend Utils::Output::Mixin + def self.reset! @installed_images = nil end diff --git a/Library/Homebrew/cask/artifact/abstract_artifact.rb b/Library/Homebrew/cask/artifact/abstract_artifact.rb index d7373510f9..eb833f904f 100644 --- a/Library/Homebrew/cask/artifact/abstract_artifact.rb +++ b/Library/Homebrew/cask/artifact/abstract_artifact.rb @@ -2,16 +2,19 @@ # frozen_string_literal: true require "extend/object/deep_dup" +require "utils/output" module Cask module Artifact # Abstract superclass for all artifacts. class AbstractArtifact extend T::Helpers + extend ::Utils::Output::Mixin abstract! include Comparable + include ::Utils::Output::Mixin def self.english_name @english_name ||= T.must(name).sub(/^.*:/, "").gsub(/(.)([A-Z])/, '\1 \2') diff --git a/Library/Homebrew/cask/audit.rb b/Library/Homebrew/cask/audit.rb index 7e3e3a4455..eee0e3d9e4 100644 --- a/Library/Homebrew/cask/audit.rb +++ b/Library/Homebrew/cask/audit.rb @@ -14,12 +14,14 @@ require "formula_name_cask_token_auditor" require "utils/curl" require "utils/git" require "utils/shared_audits" +require "utils/output" module Cask # Audit a cask for various problems. class Audit include SystemCommand::Mixin include ::Utils::Curl + include ::Utils::Output::Mixin Error = T.type_alias do { diff --git a/Library/Homebrew/cask/auditor.rb b/Library/Homebrew/cask/auditor.rb index 32c0e61aaa..f2c373faea 100644 --- a/Library/Homebrew/cask/auditor.rb +++ b/Library/Homebrew/cask/auditor.rb @@ -2,10 +2,13 @@ # frozen_string_literal: true require "cask/audit" +require "utils/output" module Cask # Helper class for auditing all available languages of a cask. class Auditor + include ::Utils::Output::Mixin + # TODO: use argument forwarding (...) when Sorbet supports it in strict mode sig { params( diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index ac81399ff7..33e34fe56d 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -8,6 +8,7 @@ require "cask/dsl" require "cask/metadata" require "cask/tab" require "utils/bottles" +require "utils/output" require "api_hashable" module Cask @@ -15,6 +16,7 @@ module Cask class Cask extend Forwardable extend APIHashable + extend ::Utils::Output::Mixin include Metadata # The token of this {Cask}. diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 53e0a09b2a..b0ca3d55a8 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -5,6 +5,7 @@ require "cask/cache" require "cask/cask" require "uri" require "utils/curl" +require "utils/output" require "extend/hash/keys" require "api" @@ -12,12 +13,14 @@ module Cask # Loads a cask from various sources. module CaskLoader extend Context + extend ::Utils::Output::Mixin ALLOWED_URL_SCHEMES = %w[file].freeze private_constant :ALLOWED_URL_SCHEMES module ILoader extend T::Helpers + include ::Utils::Output::Mixin interface! diff --git a/Library/Homebrew/cask/caskroom.rb b/Library/Homebrew/cask/caskroom.rb index eb3c6566ca..1b83d33feb 100644 --- a/Library/Homebrew/cask/caskroom.rb +++ b/Library/Homebrew/cask/caskroom.rb @@ -2,12 +2,15 @@ # frozen_string_literal: true require "utils/user" +require "utils/output" module Cask # Helper functions for interacting with the `Caskroom` directory. # # @api internal module Caskroom + extend ::Utils::Output::Mixin + sig { returns(Pathname) } def self.path @path ||= T.let(HOMEBREW_PREFIX/"Caskroom", T.nilable(Pathname)) diff --git a/Library/Homebrew/cask/dsl.rb b/Library/Homebrew/cask/dsl.rb index d0df974906..04174e9809 100644 --- a/Library/Homebrew/cask/dsl.rb +++ b/Library/Homebrew/cask/dsl.rb @@ -5,6 +5,7 @@ require "autobump_constants" require "locale" require "lazy_object" require "livecheck" +require "utils/output" require "cask/artifact" require "cask/artifact_set" @@ -32,6 +33,8 @@ require "on_system" module Cask # Class representing the domain-specific language used for casks. class DSL + include ::Utils::Output::Mixin + ORDINARY_ARTIFACT_CLASSES = [ Artifact::Installer, Artifact::App, diff --git a/Library/Homebrew/cask/dsl/conflicts_with.rb b/Library/Homebrew/cask/dsl/conflicts_with.rb index fc38c89f6f..0e5aec8342 100644 --- a/Library/Homebrew/cask/dsl/conflicts_with.rb +++ b/Library/Homebrew/cask/dsl/conflicts_with.rb @@ -3,6 +3,7 @@ require "delegate" require "extend/hash/keys" +require "utils/output" module Cask class DSL @@ -23,7 +24,7 @@ module Cask options.assert_valid_keys(*VALID_KEYS, *ODEPRECATED_KEYS) options.keys.intersection(ODEPRECATED_KEYS).each do |key| - Kernel.odeprecated "conflicts_with #{key}:" + ::Utils::Output.odeprecated "conflicts_with #{key}:" end conflicts = options.transform_values { |v| Set.new(Kernel.Array(v)) } diff --git a/Library/Homebrew/cask/info.rb b/Library/Homebrew/cask/info.rb index 174665fb08..bbc9856a7a 100644 --- a/Library/Homebrew/cask/info.rb +++ b/Library/Homebrew/cask/info.rb @@ -3,9 +3,12 @@ require "json" require "cmd/info" +require "utils/output" module Cask class Info + extend ::Utils::Output::Mixin + sig { params(cask: Cask).returns(String) } def self.get_info(cask) require "cask/installer" diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index 90893a9f3f..134e03de29 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -5,6 +5,7 @@ require "formula_installer" require "unpack_strategy" require "utils/topological_hash" require "utils/analytics" +require "utils/output" require "cask/config" require "cask/download" @@ -15,6 +16,9 @@ require "cask/tab" module Cask # Installer for a {Cask}. class Installer + extend ::Utils::Output::Mixin + include ::Utils::Output::Mixin + sig { params( cask: ::Cask::Cask, command: T::Class[SystemCommand], force: T::Boolean, adopt: T::Boolean, diff --git a/Library/Homebrew/cask/list.rb b/Library/Homebrew/cask/list.rb index 87aa1460f1..60c17213f4 100644 --- a/Library/Homebrew/cask/list.rb +++ b/Library/Homebrew/cask/list.rb @@ -2,9 +2,12 @@ # frozen_string_literal: true require "cask/artifact/relocated" +require "utils/output" module Cask class List + extend ::Utils::Output::Mixin + sig { params(casks: Cask, one: T::Boolean, full_name: T::Boolean, versions: T::Boolean).void } def self.list_casks(*casks, one: false, full_name: false, versions: false) output = if casks.any? diff --git a/Library/Homebrew/cask/metadata.rb b/Library/Homebrew/cask/metadata.rb index 39ab2d8c5b..bc7e48e96a 100644 --- a/Library/Homebrew/cask/metadata.rb +++ b/Library/Homebrew/cask/metadata.rb @@ -1,10 +1,13 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "utils/output" + module Cask # Helper module for reading and writing cask metadata. module Metadata extend T::Helpers + include ::Utils::Output::Mixin METADATA_SUBDIR = ".metadata" TIMESTAMP_FORMAT = "%Y%m%d%H%M%S.%L" diff --git a/Library/Homebrew/cask/migrator.rb b/Library/Homebrew/cask/migrator.rb index 1b96488695..14b6aef949 100644 --- a/Library/Homebrew/cask/migrator.rb +++ b/Library/Homebrew/cask/migrator.rb @@ -3,9 +3,12 @@ require "cask/cask_loader" require "utils/inreplace" +require "utils/output" module Cask class Migrator + include ::Utils::Output::Mixin + sig { returns(Cask) } attr_reader :old_cask, :new_cask diff --git a/Library/Homebrew/cask/pkg.rb b/Library/Homebrew/cask/pkg.rb index 39f13c54f7..cf726b6479 100644 --- a/Library/Homebrew/cask/pkg.rb +++ b/Library/Homebrew/cask/pkg.rb @@ -2,10 +2,13 @@ # frozen_string_literal: true require "cask/macos" +require "utils/output" module Cask # Helper class for uninstalling `.pkg` installers. class Pkg + include ::Utils::Output::Mixin + sig { params(regexp: String, command: T.class_of(SystemCommand)).returns(T::Array[Pkg]) } def self.all_matching(regexp, command) command.run("/usr/sbin/pkgutil", args: ["--pkgs=#{regexp}"]).stdout.split("\n").map do |package_id| diff --git a/Library/Homebrew/cask/quarantine.rb b/Library/Homebrew/cask/quarantine.rb index 79da5cc280..68b0e80d22 100644 --- a/Library/Homebrew/cask/quarantine.rb +++ b/Library/Homebrew/cask/quarantine.rb @@ -4,11 +4,13 @@ require "development_tools" require "cask/exceptions" require "system_command" +require "utils/output" module Cask # Helper module for quarantining files. module Quarantine extend SystemCommand::Mixin + extend ::Utils::Output::Mixin QUARANTINE_ATTRIBUTE = "com.apple.quarantine" diff --git a/Library/Homebrew/cask/reinstall.rb b/Library/Homebrew/cask/reinstall.rb index 970a5daf6b..94c5635606 100644 --- a/Library/Homebrew/cask/reinstall.rb +++ b/Library/Homebrew/cask/reinstall.rb @@ -1,8 +1,12 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Cask class Reinstall + extend ::Utils::Output::Mixin + sig { params( casks: ::Cask::Cask, verbose: T::Boolean, force: T::Boolean, skip_cask_deps: T::Boolean, binaries: T::Boolean, diff --git a/Library/Homebrew/cask/staged.rb b/Library/Homebrew/cask/staged.rb index 315e37a3ed..a12a7e4d69 100644 --- a/Library/Homebrew/cask/staged.rb +++ b/Library/Homebrew/cask/staged.rb @@ -2,10 +2,12 @@ # frozen_string_literal: true require "utils/user" +require "utils/output" module Cask # Helper functions for staged casks. module Staged + include ::Utils::Output::Mixin extend T::Helpers requires_ancestor { ::Cask::DSL::Base } diff --git a/Library/Homebrew/cask/uninstall.rb b/Library/Homebrew/cask/uninstall.rb index 38f2587e13..92ba0d960f 100644 --- a/Library/Homebrew/cask/uninstall.rb +++ b/Library/Homebrew/cask/uninstall.rb @@ -1,8 +1,12 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Cask class Uninstall + extend ::Utils::Output::Mixin + sig { params(casks: ::Cask::Cask, binaries: T::Boolean, force: T::Boolean, verbose: T::Boolean).void } def self.uninstall_casks(*casks, binaries: false, force: false, verbose: false) require "cask/installer" diff --git a/Library/Homebrew/cask/upgrade.rb b/Library/Homebrew/cask/upgrade.rb index 7b4d1fbeab..0b78670c54 100644 --- a/Library/Homebrew/cask/upgrade.rb +++ b/Library/Homebrew/cask/upgrade.rb @@ -3,9 +3,12 @@ require "env_config" require "cask/config" +require "utils/output" module Cask class Upgrade + extend ::Utils::Output::Mixin + sig { params( casks: Cask, diff --git a/Library/Homebrew/cask/utils.rb b/Library/Homebrew/cask/utils.rb index 976ec7f22f..1545abbc4a 100644 --- a/Library/Homebrew/cask/utils.rb +++ b/Library/Homebrew/cask/utils.rb @@ -3,10 +3,13 @@ require "utils/user" require "open3" +require "utils/output" module Cask # Helper functions for various cask operations. module Utils + extend ::Utils::Output::Mixin + BUG_REPORTS_URL = "https://github.com/Homebrew/homebrew-cask#reporting-bugs" def self.gain_permissions_mkpath(path, command: SystemCommand) diff --git a/Library/Homebrew/cleaner.rb b/Library/Homebrew/cleaner.rb index 625c152c4c..4ae3970362 100644 --- a/Library/Homebrew/cleaner.rb +++ b/Library/Homebrew/cleaner.rb @@ -1,6 +1,8 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + # Cleans a newly installed keg. # By default: # @@ -13,6 +15,7 @@ # * removes unresolved symlinks class Cleaner include Context + include Utils::Output::Mixin # Create a cleaner for the given formula. sig { params(formula: Formula).void } diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb index 37ed04e13a..c48f2320fe 100644 --- a/Library/Homebrew/cleanup.rb +++ b/Library/Homebrew/cleanup.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "utils/bottles" +require "utils/output" require "formula" require "cask/cask_loader" @@ -9,6 +10,9 @@ require "cask/cask_loader" module Homebrew # Helper class for cleaning up the Homebrew cache. class Cleanup + extend Utils::Output::Mixin + include Utils::Output::Mixin + CLEANUP_DEFAULT_DAYS = Homebrew::EnvConfig.cleanup_periodic_full_days.to_i.freeze GH_ACTIONS_ARTIFACT_CLEANUP_DAYS = 3 private_constant :CLEANUP_DEFAULT_DAYS, :GH_ACTIONS_ARTIFACT_CLEANUP_DAYS diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 4500547035..58153426e6 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -2,11 +2,13 @@ # frozen_string_literal: true require "cli/args" +require "utils/output" module Homebrew module CLI # Helper class for loading formulae/casks from named arguments. class NamedArgs < Array + include Utils::Output::Mixin extend T::Generic Elem = type_member(:out) { { fixed: String } } diff --git a/Library/Homebrew/cli/parser.rb b/Library/Homebrew/cli/parser.rb index 7ffe3e0c72..6e70f6aad6 100644 --- a/Library/Homebrew/cli/parser.rb +++ b/Library/Homebrew/cli/parser.rb @@ -10,10 +10,13 @@ require "commands" require "optparse" require "utils/tty" require "utils/formatter" +require "utils/output" module Homebrew module CLI class Parser + include Utils::Output::Mixin + ArgType = T.type_alias { T.any(NilClass, Symbol, T::Array[String], T::Array[Symbol]) } HIDDEN_DESC_PLACEHOLDER = "@@HIDDEN@@" SYMBOL_TO_USAGE_MAPPING = T.let({ @@ -40,7 +43,7 @@ module Homebrew cmd_name = cmd_args_method_name.to_s.delete_suffix("_args").tr("_", "-") begin - if require?(cmd_path) + if Homebrew.require?(cmd_path) cmd = Homebrew::AbstractCommand.command(cmd_name) if cmd cmd.parser diff --git a/Library/Homebrew/cmd/update-report.rb b/Library/Homebrew/cmd/update-report.rb index 0736e4cc41..60b563933e 100644 --- a/Library/Homebrew/cmd/update-report.rb +++ b/Library/Homebrew/cmd/update-report.rb @@ -425,6 +425,8 @@ end require "extend/os/cmd/update-report" class Reporter + include Utils::Output::Mixin + class ReporterRevisionUnsetError < RuntimeError sig { params(var_name: String).void } def initialize(var_name) @@ -782,6 +784,8 @@ class Reporter end class ReporterHub + include Utils::Output::Mixin + sig { returns(T::Array[Reporter]) } attr_reader :reporters diff --git a/Library/Homebrew/commands.rb b/Library/Homebrew/commands.rb index bff04823ef..7f948ab23b 100644 --- a/Library/Homebrew/commands.rb +++ b/Library/Homebrew/commands.rb @@ -1,6 +1,8 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "utils" + # Helper functions for commands. module Commands HOMEBREW_CMD_PATH = (HOMEBREW_LIBRARY_PATH/"cmd").freeze @@ -34,11 +36,11 @@ module Commands DESCRIPTION_SPLITTING_PATTERN = /\.(?>\s|$)/ def self.valid_internal_cmd?(cmd) - require?(HOMEBREW_CMD_PATH/cmd) + Homebrew.require?(HOMEBREW_CMD_PATH/cmd) end def self.valid_internal_dev_cmd?(cmd) - require?(HOMEBREW_DEV_CMD_PATH/cmd) + Homebrew.require?(HOMEBREW_DEV_CMD_PATH/cmd) end def self.valid_ruby_cmd?(cmd) @@ -76,7 +78,7 @@ module Commands # Ruby commands which can be `require`d without being run. def self.external_ruby_v2_cmd_path(cmd) path = which("#{cmd}.rb", tap_cmd_directories) - path if require?(path) + path if Homebrew.require?(path) end # Ruby commands which are run by being `require`d. diff --git a/Library/Homebrew/completions.rb b/Library/Homebrew/completions.rb index ac90caf1e7..f0ec29e4b3 100644 --- a/Library/Homebrew/completions.rb +++ b/Library/Homebrew/completions.rb @@ -8,6 +8,8 @@ require "erb" module Homebrew # Helper functions for generating shell completions. module Completions + extend Utils::Output::Mixin + Variables = Struct.new( :aliases, :builtin_command_descriptions, diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index 05deedeabc..9f9ebdb1b5 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -8,6 +8,7 @@ require "formulary" require "version" require "development_tools" require "utils/shell" +require "utils/output" require "system_config" require "cask/caskroom" require "cask/quarantine" @@ -16,6 +17,8 @@ require "system_command" module Homebrew # Module containing diagnostic checks. module Diagnostic + extend Utils::Output::Mixin + sig { params(formulae: T::Array[Formula], hide: T::Array[String], _block: T.nilable( T.proc.params(formula_name: String, missing_dependencies: T::Array[Formula]).void, diff --git a/Library/Homebrew/download_queue.rb b/Library/Homebrew/download_queue.rb index 5044bca446..e3cb255f52 100644 --- a/Library/Homebrew/download_queue.rb +++ b/Library/Homebrew/download_queue.rb @@ -6,9 +6,12 @@ require "concurrent/promises" require "concurrent/executors" require "retryable_download" require "resource" +require "utils/output" module Homebrew class DownloadQueue + include Utils::Output::Mixin + sig { params(retries: Integer, force: T::Boolean, pour: T::Boolean).void } def initialize(retries: 1, force: false, pour: false) @concurrency = T.let(EnvConfig.download_concurrency, Integer) diff --git a/Library/Homebrew/download_strategy.rb b/Library/Homebrew/download_strategy.rb index dcb4b7834a..f0965698c8 100644 --- a/Library/Homebrew/download_strategy.rb +++ b/Library/Homebrew/download_strategy.rb @@ -7,6 +7,7 @@ require "unpack_strategy" require "lazy_object" require "lock_file" require "system_command" +require "utils/output" # Need to define this before requiring Mechanize to avoid: # uninitialized constant Mechanize @@ -27,6 +28,7 @@ class AbstractDownloadStrategy include FileUtils include Context include SystemCommand::Mixin + include Utils::Output::Mixin abstract! diff --git a/Library/Homebrew/downloadable.rb b/Library/Homebrew/downloadable.rb index bb5c241d77..dfb21475b9 100644 --- a/Library/Homebrew/downloadable.rb +++ b/Library/Homebrew/downloadable.rb @@ -4,9 +4,11 @@ require "url" require "checksum" require "download_strategy" +require "utils/output" module Downloadable include Context + include Utils::Output::Mixin extend T::Helpers abstract! diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb index 2e56473472..328bd734f8 100644 --- a/Library/Homebrew/env_config.rb +++ b/Library/Homebrew/env_config.rb @@ -1,11 +1,16 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Homebrew # Helper module for querying Homebrew-specific environment variables. # # @api internal module EnvConfig + include Utils::Output::Mixin + extend Utils::Output::Mixin + module_function ENVS = T.let({ diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index 969e941508..394f92ff68 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -1,6 +1,8 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "utils/output" + # Raised when a command is used wrong. # # @api internal @@ -463,6 +465,8 @@ end # Raised when an error occurs during a formula build. class BuildError < RuntimeError + include Utils::Output::Mixin + attr_reader :cmd, :args, :env attr_accessor :formula, :options diff --git a/Library/Homebrew/extend/ENV/super.rb b/Library/Homebrew/extend/ENV/super.rb index bf083f3165..03fe313b47 100644 --- a/Library/Homebrew/extend/ENV/super.rb +++ b/Library/Homebrew/extend/ENV/super.rb @@ -3,6 +3,7 @@ require "extend/ENV/shared" require "development_tools" +require "utils/output" # ### Why `superenv`? # @@ -16,6 +17,7 @@ require "development_tools" # 8. Build-system agnostic configuration of the toolchain module Superenv include SharedEnvExtension + include Utils::Output::Mixin attr_accessor :keg_only_deps, :deps, :run_time_deps diff --git a/Library/Homebrew/extend/kernel.rb b/Library/Homebrew/extend/kernel.rb index 5e3824e051..848cca8161 100644 --- a/Library/Homebrew/extend/kernel.rb +++ b/Library/Homebrew/extend/kernel.rb @@ -1,9 +1,7 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true -# Contains shorthand Homebrew utility methods like `ohai`, `opoo`, `odisabled`. -# TODO: move these out of `Kernel` into `Homebrew::GlobalMethods` and add -# necessary Sorbet and global Kernel inclusions. +require "utils/output" module Kernel sig { params(env: T.nilable(String)).returns(T::Boolean) } @@ -14,279 +12,6 @@ module Kernel end private :superenv? - sig { params(path: T.nilable(T.any(String, Pathname))).returns(T::Boolean) } - def require?(path) - return false if path.nil? - - if defined?(Warnings) - # Work around require warning when done repeatedly: - # https://bugs.ruby-lang.org/issues/21091 - Warnings.ignore(/already initialized constant/, /previous definition of/) do - require path.to_s - end - else - require path.to_s - end - true - rescue LoadError - false - end - - sig { params(title: String).returns(String) } - def ohai_title(title) - verbose = if respond_to?(:verbose?) - T.unsafe(self).verbose? - else - Context.current.verbose? - end - - title = Tty.truncate(title.to_s) if $stdout.tty? && !verbose - Formatter.headline(title, color: :blue) - end - - sig { params(title: T.any(String, Exception), sput: T.anything).void } - def ohai(title, *sput) - puts ohai_title(title.to_s) - puts sput - end - - sig { params(title: T.any(String, Exception), sput: T.anything, always_display: T::Boolean).void } - def odebug(title, *sput, always_display: false) - debug = if respond_to?(:debug) - T.unsafe(self).debug? - else - Context.current.debug? - end - - return if !debug && !always_display - - $stderr.puts Formatter.headline(title.to_s, color: :magenta) - $stderr.puts sput unless sput.empty? - end - - sig { params(title: String, truncate: T.any(Symbol, T::Boolean)).returns(String) } - def oh1_title(title, truncate: :auto) - verbose = if respond_to?(:verbose?) - T.unsafe(self).verbose? - else - Context.current.verbose? - end - - title = Tty.truncate(title.to_s) if $stdout.tty? && !verbose && truncate == :auto - Formatter.headline(title, color: :green) - end - - sig { params(title: String, truncate: T.any(Symbol, T::Boolean)).void } - def oh1(title, truncate: :auto) - puts oh1_title(title, truncate:) - end - - # Print a warning message. - # - # @api public - sig { params(message: T.any(String, Exception)).void } - def opoo(message) - require "utils/github/actions" - return if GitHub::Actions.puts_annotation_if_env_set!(:warning, message.to_s) - - require "utils/formatter" - - Tty.with($stderr) do |stderr| - stderr.puts Formatter.warning(message, label: "Warning") - end - end - - # Print a warning message only if not running in GitHub Actions. - # - # @api public - sig { params(message: T.any(String, Exception)).void } - def opoo_outside_github_actions(message) - require "utils/github/actions" - return if GitHub::Actions.env_set? - - opoo(message) - end - - # Print an error message. - # - # @api public - sig { params(message: T.any(String, Exception)).void } - def onoe(message) - require "utils/github/actions" - return if GitHub::Actions.puts_annotation_if_env_set!(:error, message.to_s) - - require "utils/formatter" - - Tty.with($stderr) do |stderr| - stderr.puts Formatter.error(message, label: "Error") - end - end - - # Print an error message and fail at the end of the program. - # - # @api public - sig { params(error: T.any(String, Exception)).void } - def ofail(error) - onoe error - Homebrew.failed = true - end - - # Print an error message and fail immediately. - # - # @api public - sig { params(error: T.any(String, Exception)).returns(T.noreturn) } - def odie(error) - onoe error - exit 1 - end - - # Output a deprecation warning/error message. - sig { - params(method: String, replacement: T.nilable(T.any(String, Symbol)), disable: T::Boolean, - disable_on: T.nilable(Time), disable_for_developers: T::Boolean, caller: T::Array[String]).void - } - def odeprecated(method, replacement = nil, - disable: false, - disable_on: nil, - disable_for_developers: true, - caller: send(:caller)) - replacement_message = if replacement - "Use #{replacement} instead." - else - "There is no replacement." - end - - unless disable_on.nil? - if disable_on > Time.now - will_be_disabled_message = " and will be disabled on #{disable_on.strftime("%Y-%m-%d")}" - else - disable = true - end - end - - verb = if disable - "disabled" - else - "deprecated#{will_be_disabled_message}" - end - - # Try to show the most relevant location in message, i.e. (if applicable): - # - Location in a formula. - # - Location of caller of deprecated method (if all else fails). - backtrace = caller - - # Don't throw deprecations at all for cached, .brew or .metadata files. - return if backtrace.any? do |line| - next true if line.include?(HOMEBREW_CACHE.to_s) - next true if line.include?("/.brew/") - next true if line.include?("/.metadata/") - - next false unless line.match?(HOMEBREW_TAP_PATH_REGEX) - - path = Pathname(line.split(":", 2).first) - next false unless path.file? - next false unless path.readable? - - formula_contents = path.read - formula_contents.include?(" deprecate! ") || formula_contents.include?(" disable! ") - end - - tap_message = T.let(nil, T.nilable(String)) - - backtrace.each do |line| - next unless (match = line.match(HOMEBREW_TAP_PATH_REGEX)) - - require "tap" - - tap = Tap.fetch(match[:user], match[:repository]) - tap_message = "\nPlease report this issue to the #{tap.full_name} tap" - tap_message += " (not Homebrew/* repositories)" unless tap.official? - tap_message += ", or even better, submit a PR to fix it" if replacement - tap_message << ":\n #{line.sub(/^(.*:\d+):.*$/, '\1')}\n\n" - break - end - file, line, = backtrace.first.split(":") - line = line.to_i if line.present? - - message = "Calling #{method} is #{verb}! #{replacement_message}" - message << tap_message if tap_message - message.freeze - - disable = true if disable_for_developers && Homebrew::EnvConfig.developer? - if disable || Homebrew.raise_deprecation_exceptions? - require "utils/github/actions" - GitHub::Actions.puts_annotation_if_env_set!(:error, message, file:, line:) - exception = MethodDeprecatedError.new(message) - exception.set_backtrace(backtrace) - raise exception - elsif !Homebrew.auditing? - opoo message - end - end - - sig { - params(method: String, replacement: T.nilable(T.any(String, Symbol)), - disable_on: T.nilable(Time), disable_for_developers: T::Boolean, caller: T::Array[String]).void - } - def odisabled(method, replacement = nil, - disable_on: nil, - disable_for_developers: true, - caller: send(:caller)) - # This odeprecated should stick around indefinitely. - odeprecated(method, replacement, disable: true, disable_on:, disable_for_developers:, caller:) - end - - sig { params(string: String).returns(String) } - def pretty_installed(string) - if !$stdout.tty? - string - elsif Homebrew::EnvConfig.no_emoji? - Formatter.success("#{Tty.bold}#{string} (installed)#{Tty.reset}") - else - "#{Tty.bold}#{string} #{Formatter.success("✔")}#{Tty.reset}" - end - end - - sig { params(string: String).returns(String) } - def pretty_outdated(string) - if !$stdout.tty? - string - elsif Homebrew::EnvConfig.no_emoji? - Formatter.error("#{Tty.bold}#{string} (outdated)#{Tty.reset}") - else - "#{Tty.bold}#{string} #{Formatter.warning("⚠")}#{Tty.reset}" - end - end - - sig { params(string: String).returns(String) } - def pretty_uninstalled(string) - if !$stdout.tty? - string - elsif Homebrew::EnvConfig.no_emoji? - Formatter.error("#{Tty.bold}#{string} (uninstalled)#{Tty.reset}") - else - "#{Tty.bold}#{string} #{Formatter.error("✘")}#{Tty.reset}" - end - end - - sig { params(seconds: T.nilable(T.any(Integer, Float))).returns(String) } - def pretty_duration(seconds) - seconds = seconds.to_i - res = +"" - - if seconds > 59 - minutes = seconds / 60 - seconds %= 60 - res = +Utils.pluralize("minute", minutes, include_count: true) - return res.freeze if seconds.zero? - - res << " " - end - - res << Utils.pluralize("second", seconds, include_count: true) - res.freeze - end - sig { params(formula: T.nilable(Formula)).void } def interactive_shell(formula = nil) unless formula.nil? @@ -368,7 +93,7 @@ module Kernel editor ||= "vim" unless silent - opoo <<~EOS + Utils::Output.opoo <<~EOS Using #{editor} because no editor was set in the environment. This may change in the future, so we recommend setting `$EDITOR` or `$HOMEBREW_EDITOR` to your preferred text editor. diff --git a/Library/Homebrew/extend/os/linux/install.rb b/Library/Homebrew/extend/os/linux/install.rb index 2420531048..cbfa62ece1 100644 --- a/Library/Homebrew/extend/os/linux/install.rb +++ b/Library/Homebrew/extend/os/linux/install.rb @@ -1,6 +1,8 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module OS module Linux module Install @@ -108,7 +110,7 @@ module OS ::Kernel.system HOMEBREW_PREFIX/"opt/glibc/sbin/ldconfig" end else - ::Kernel.odie "#{HOMEBREW_PREFIX}/lib does not exist!" unless (HOMEBREW_PREFIX/"lib").readable? + Utils::Output.odie "#{HOMEBREW_PREFIX}/lib does not exist!" unless (HOMEBREW_PREFIX/"lib").readable? end GCC_RUNTIME_LIBS.each do |library| diff --git a/Library/Homebrew/extend/os/mac/readall.rb b/Library/Homebrew/extend/os/mac/readall.rb index 6d926a87f3..9fef3d1f54 100644 --- a/Library/Homebrew/extend/os/mac/readall.rb +++ b/Library/Homebrew/extend/os/mac/readall.rb @@ -1,11 +1,14 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module OS module Mac module Readall module ClassMethods extend T::Helpers + include ::Utils::Output::Mixin requires_ancestor { Kernel } diff --git a/Library/Homebrew/extend/pathname.rb b/Library/Homebrew/extend/pathname.rb index e957431625..40b6356d7f 100644 --- a/Library/Homebrew/extend/pathname.rb +++ b/Library/Homebrew/extend/pathname.rb @@ -4,12 +4,14 @@ require "system_command" require "extend/pathname/disk_usage_extension" require "extend/pathname/observer_pathname_extension" +require "utils/output" # Homebrew extends Ruby's `Pathname` to make our code more readable. # @see https://ruby-doc.org/stdlib-2.6.3/libdoc/pathname/rdoc/Pathname.html Ruby's Pathname API class Pathname include SystemCommand::Mixin include DiskUsageExtension + include Utils::Output::Mixin # Moves a file from the original location to the {Pathname}'s. # diff --git a/Library/Homebrew/extend/time.rb b/Library/Homebrew/extend/time.rb index 55302feacc..cdd9bfcbb9 100644 --- a/Library/Homebrew/extend/time.rb +++ b/Library/Homebrew/extend/time.rb @@ -2,8 +2,11 @@ # frozen_string_literal: true require "time" +require "utils/output" class Time + include Utils::Output::Mixin + # Backwards compatibility for formulae that used this ActiveSupport extension sig { returns(String) } def rfc3339 diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index d7b150fc46..5c2a9a5b26 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -41,6 +41,7 @@ require "utils/spdx" require "on_system" require "api" require "api_hashable" +require "utils/output" # A formula provides instructions and metadata for Homebrew to install a piece # of software. Every Homebrew formula is a {Formula}. @@ -75,6 +76,7 @@ class Formula include FileUtils include Utils::Shebang include Utils::Shell + include Utils::Output::Mixin include Context include OnSystem::MacOSAndLinux include Homebrew::Livecheck::Constants @@ -82,6 +84,7 @@ class Formula extend Cachable extend APIHashable extend T::Helpers + extend Utils::Output::Mixin abstract! diff --git a/Library/Homebrew/formula_assertions.rb b/Library/Homebrew/formula_assertions.rb index 12913f36ef..9d132dbe75 100644 --- a/Library/Homebrew/formula_assertions.rb +++ b/Library/Homebrew/formula_assertions.rb @@ -1,10 +1,13 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Homebrew # Helper functions available in formula `test` blocks. module Assertions include Context + include ::Utils::Output::Mixin extend T::Helpers requires_ancestor { Kernel } diff --git a/Library/Homebrew/formula_auditor.rb b/Library/Homebrew/formula_auditor.rb index cec1bfbbb5..2017a4e34f 100644 --- a/Library/Homebrew/formula_auditor.rb +++ b/Library/Homebrew/formula_auditor.rb @@ -6,12 +6,14 @@ require "formula_versions" require "formula_name_cask_token_auditor" require "resource_auditor" require "utils/shared_audits" +require "utils/output" module Homebrew # Auditor for checking common violations in {Formula}e. class FormulaAuditor include FormulaCellarChecks include Utils::Curl + include Utils::Output::Mixin attr_reader :formula, :text, :problems, :new_formula_problems diff --git a/Library/Homebrew/formula_creator.rb b/Library/Homebrew/formula_creator.rb index 4c10a631e8..06ac6335bf 100644 --- a/Library/Homebrew/formula_creator.rb +++ b/Library/Homebrew/formula_creator.rb @@ -4,10 +4,13 @@ require "digest" require "erb" require "utils/github" +require "utils/output" module Homebrew # Class for generating a formula from a template. class FormulaCreator + include Utils::Output::Mixin + sig { returns(String) } attr_accessor :name diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 6bf6be477f..257e2f2ce1 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -24,10 +24,12 @@ require "service" require "attestation" require "sbom" require "utils/fork" +require "utils/output" # Installer for a formula. class FormulaInstaller include FormulaCellarChecks + include Utils::Output::Mixin ETC_VAR_DIRS = T.let([HOMEBREW_PREFIX/"etc", HOMEBREW_PREFIX/"var"].freeze, T::Array[Pathname]) diff --git a/Library/Homebrew/formula_versions.rb b/Library/Homebrew/formula_versions.rb index eb687c68b4..867fbcfb49 100644 --- a/Library/Homebrew/formula_versions.rb +++ b/Library/Homebrew/formula_versions.rb @@ -2,12 +2,14 @@ # frozen_string_literal: true require "formula" +require "utils/output" # Helper class for traversing a formula's previous versions. # # @api internal class FormulaVersions include Context + include Utils::Output::Mixin IGNORED_EXCEPTIONS = [ ArgumentError, NameError, SyntaxError, TypeError, diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index d57a8606ca..cd2a0ac7d5 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -6,6 +6,7 @@ require "cachable" require "tab" require "utils" require "utils/bottles" +require "utils/output" require "service" require "utils/curl" require "deprecate_disable" @@ -18,6 +19,8 @@ require "tap" module Formulary extend Context extend Cachable + extend Utils::Output::Mixin + include Utils::Output::Mixin ALLOWED_URL_SCHEMES = %w[file].freeze private_constant :ALLOWED_URL_SCHEMES @@ -559,6 +562,7 @@ module Formulary # Subclasses implement loaders for particular sources of formulae. class FormulaLoader include Context + include Utils::Output::Mixin # The formula's name. sig { returns(String) } @@ -609,6 +613,8 @@ module Formulary # Loads a formula from a bottle. class FromBottleLoader < FormulaLoader + include Utils::Output::Mixin + sig { params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean) .returns(T.nilable(T.attached_class)) diff --git a/Library/Homebrew/formulary.rbi b/Library/Homebrew/formulary.rbi deleted file mode 100644 index 5c149eeb10..0000000000 --- a/Library/Homebrew/formulary.rbi +++ /dev/null @@ -1,5 +0,0 @@ -# typed: strict - -module Formulary - include Kernel -end diff --git a/Library/Homebrew/github_packages.rb b/Library/Homebrew/github_packages.rb index 8e9c37f5ca..77b5b9ccbe 100644 --- a/Library/Homebrew/github_packages.rb +++ b/Library/Homebrew/github_packages.rb @@ -3,6 +3,7 @@ require "utils/curl" require "utils/gzip" +require "utils/output" require "json" require "zlib" require "extend/hash/keys" @@ -12,6 +13,7 @@ require "system_command" class GitHubPackages include Context include SystemCommand::Mixin + include Utils::Output::Mixin URL_DOMAIN = "ghcr.io" URL_PREFIX = "https://#{URL_DOMAIN}/v2/".freeze diff --git a/Library/Homebrew/github_releases.rb b/Library/Homebrew/github_releases.rb index e11f5e61c5..9f3435fc45 100644 --- a/Library/Homebrew/github_releases.rb +++ b/Library/Homebrew/github_releases.rb @@ -2,11 +2,13 @@ # frozen_string_literal: true require "utils/github" +require "utils/output" require "json" # GitHub Releases client. class GitHubReleases include Context + include Utils::Output::Mixin URL_REGEX = %r{https://github\.com/([\w-]+)/([\w-]+)?/releases/download/(.+)} diff --git a/Library/Homebrew/help.rb b/Library/Homebrew/help.rb index ef6d975f50..95bef13b42 100644 --- a/Library/Homebrew/help.rb +++ b/Library/Homebrew/help.rb @@ -3,10 +3,13 @@ require "cli/parser" require "commands" +require "utils/output" module Homebrew # Helper module for printing help output. module Help + extend Utils::Output::Mixin + sig { params( cmd: T.nilable(String), diff --git a/Library/Homebrew/install.rb b/Library/Homebrew/install.rb index eafed0ffcc..4c46a38075 100644 --- a/Library/Homebrew/install.rb +++ b/Library/Homebrew/install.rb @@ -7,10 +7,13 @@ require "hardware" require "development_tools" require "upgrade" require "download_queue" +require "utils/output" module Homebrew # Helper module for performing (pre-)install checks. module Install + extend Utils::Output::Mixin + class << self sig { params(all_fatal: T::Boolean).void } def perform_preinstall_checks_once(all_fatal: false) diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 34bdcaf389..0e909bf74d 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -5,10 +5,12 @@ require "keg_relocate" require "language/python" require "lock_file" require "cachable" +require "utils/output" # Installation prefix of a formula. class Keg extend Cachable + include Utils::Output::Mixin # Error for when a keg is already linked. class AlreadyLinkedError < RuntimeError diff --git a/Library/Homebrew/language/node.rb b/Library/Homebrew/language/node.rb index cfa093e4db..6852022155 100644 --- a/Library/Homebrew/language/node.rb +++ b/Library/Homebrew/language/node.rb @@ -1,11 +1,15 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Language # Helper functions for Node formulae. # # @api public module Node + extend ::Utils::Output::Mixin + sig { returns(String) } def self.npm_cache_config "cache=#{HOMEBREW_CACHE}/npm_cache" diff --git a/Library/Homebrew/linkage_checker.rb b/Library/Homebrew/linkage_checker.rb index 0db9ad05db..5f8e584db3 100644 --- a/Library/Homebrew/linkage_checker.rb +++ b/Library/Homebrew/linkage_checker.rb @@ -5,9 +5,12 @@ require "keg" require "formula" require "linkage_cache_store" require "fiddle" +require "utils/output" # Check for broken/missing linkage in a formula's keg. class LinkageChecker + include Utils::Output::Mixin + attr_reader :undeclared_deps, :keg, :formula, :store def initialize(keg, formula = nil, cache_db:, rebuild_cache: false) diff --git a/Library/Homebrew/livecheck/livecheck.rb b/Library/Homebrew/livecheck/livecheck.rb index 12f2b2b426..be5c29ae77 100644 --- a/Library/Homebrew/livecheck/livecheck.rb +++ b/Library/Homebrew/livecheck/livecheck.rb @@ -7,12 +7,15 @@ require "livecheck/livecheck_version" require "livecheck/skip_conditions" require "livecheck/strategy" require "addressable" +require "utils/output" module Homebrew # The {Livecheck} module consists of methods used by the `brew livecheck` # command. These methods print the requested livecheck information # for formulae. module Livecheck + extend Utils::Output::Mixin + NO_CURRENT_VERSION_MSG = "Unable to identify current version" NO_VERSIONS_MSG = "Unable to get versions" diff --git a/Library/Homebrew/lock_file.rb b/Library/Homebrew/lock_file.rb index a49a6bb0a4..ed47b01f87 100644 --- a/Library/Homebrew/lock_file.rb +++ b/Library/Homebrew/lock_file.rb @@ -2,9 +2,12 @@ # frozen_string_literal: true require "fcntl" +require "utils/output" # A lock file to prevent multiple Homebrew processes from modifying the same path. class LockFile + include Utils::Output::Mixin + class OpenFileChangedOnDisk < RuntimeError; end private_constant :OpenFileChangedOnDisk diff --git a/Library/Homebrew/messages.rb b/Library/Homebrew/messages.rb index 3ed0a2831a..49b2ebd5c3 100644 --- a/Library/Homebrew/messages.rb +++ b/Library/Homebrew/messages.rb @@ -1,9 +1,13 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + # A {Messages} object collects messages that may need to be displayed together # at the end of a multi-step `brew` command run. class Messages + include ::Utils::Output::Mixin + sig { returns(T::Array[{ package: String, caveats: T.any(String, Caveats) }]) } attr_reader :caveats diff --git a/Library/Homebrew/migrator.rb b/Library/Homebrew/migrator.rb index de6fc0732c..02610a2475 100644 --- a/Library/Homebrew/migrator.rb +++ b/Library/Homebrew/migrator.rb @@ -4,10 +4,13 @@ require "lock_file" require "keg" require "tab" +require "utils/output" # Helper class for migrating a formula from an old to a new name. class Migrator + extend Utils::Output::Mixin include Context + include Utils::Output::Mixin # Error for when a migration is necessary. class MigrationNeededError < RuntimeError diff --git a/Library/Homebrew/missing_formula.rb b/Library/Homebrew/missing_formula.rb index 43e82914e3..05000bd5a6 100644 --- a/Library/Homebrew/missing_formula.rb +++ b/Library/Homebrew/missing_formula.rb @@ -2,10 +2,13 @@ # frozen_string_literal: true require "formulary" +require "utils/output" module Homebrew # Helper module for checking if there is a reason a formula is missing. module MissingFormula + extend Utils::Output::Mixin + class << self sig { params(name: String, silent: T::Boolean, show_info: T::Boolean).returns(T.nilable(String)) } def reason(name, silent: false, show_info: false) diff --git a/Library/Homebrew/mktemp.rb b/Library/Homebrew/mktemp.rb index 989d904d20..407e16b88e 100644 --- a/Library/Homebrew/mktemp.rb +++ b/Library/Homebrew/mktemp.rb @@ -1,10 +1,14 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + # Performs {Formula#mktemp}'s functionality and tracks the results. # Each instance is only intended to be used once. # Can also be used to create a temporary directory with the brew instance's group. class Mktemp + include Utils::Output::Mixin + # Path to the tmpdir used in this run sig { returns(T.nilable(Pathname)) } attr_reader :tmpdir diff --git a/Library/Homebrew/patch.rb b/Library/Homebrew/patch.rb index 27638207b0..8fef04b3d9 100644 --- a/Library/Homebrew/patch.rb +++ b/Library/Homebrew/patch.rb @@ -3,6 +3,7 @@ require "resource" require "erb" +require "utils/output" # Helper module for creating patches. module Patch @@ -96,6 +97,8 @@ end # A file containing a patch. class ExternalPatch + include Utils::Output::Mixin + extend Forwardable attr_reader :resource, :strip diff --git a/Library/Homebrew/readall.rb b/Library/Homebrew/readall.rb index 36ae737fcf..38fe38102b 100644 --- a/Library/Homebrew/readall.rb +++ b/Library/Homebrew/readall.rb @@ -4,11 +4,13 @@ require "formula" require "cask/cask_loader" require "system_command" +require "utils/output" # Helper module for validating syntax in taps. module Readall extend Cachable extend SystemCommand::Mixin + extend Utils::Output::Mixin # TODO: remove this once the `MacOS` module is undefined on Linux MACOS_MODULE_REGEX = /\b(MacOS|OS::Mac)(\.|::)\b/ diff --git a/Library/Homebrew/reinstall.rb b/Library/Homebrew/reinstall.rb index cfb9a3db78..571719e70e 100644 --- a/Library/Homebrew/reinstall.rb +++ b/Library/Homebrew/reinstall.rb @@ -3,6 +3,7 @@ require "development_tools" require "messages" +require "utils/output" # Needed to handle circular require dependency. # rubocop:disable Lint/EmptyClass @@ -11,6 +12,8 @@ class FormulaInstaller; end module Homebrew module Reinstall + extend Utils::Output::Mixin + class InstallationContext < T::Struct const :formula_installer, ::FormulaInstaller const :keg, T.nilable(Keg) diff --git a/Library/Homebrew/requirement.rb b/Library/Homebrew/requirement.rb index ac38b7418a..0aec74dff7 100644 --- a/Library/Homebrew/requirement.rb +++ b/Library/Homebrew/requirement.rb @@ -5,12 +5,14 @@ require "dependable" require "dependency" require "dependencies" require "build_environment" +require "utils/output" # A base class for non-formula requirements needed by formulae. # A fatal requirement is one that will fail the build if it is not present. # By default, requirements are non-fatal. class Requirement include Dependable + include Utils::Output::Mixin extend Cachable extend T::Helpers diff --git a/Library/Homebrew/resource.rb b/Library/Homebrew/resource.rb index a6a4c0e9fd..85de6b51d8 100644 --- a/Library/Homebrew/resource.rb +++ b/Library/Homebrew/resource.rb @@ -5,6 +5,7 @@ require "downloadable" require "mktemp" require "livecheck" require "on_system" +require "utils/output" # Resource is the fundamental representation of an external resource. The # primary formula download, along with other declared resources, are instances @@ -13,6 +14,7 @@ class Resource include Downloadable include FileUtils include OnSystem::MacOSAndLinux + include Utils::Output::Mixin attr_reader :source_modified_time, :patches, :owner attr_writer :checksum diff --git a/Library/Homebrew/retryable_download.rb b/Library/Homebrew/retryable_download.rb index 06203579f0..0428e1d104 100644 --- a/Library/Homebrew/retryable_download.rb +++ b/Library/Homebrew/retryable_download.rb @@ -3,10 +3,12 @@ require "bottle" require "api/json_download" +require "utils/output" module Homebrew class RetryableDownload include Downloadable + include Utils::Output::Mixin sig { override.returns(T.any(NilClass, String, URL)) } def url = downloadable.url diff --git a/Library/Homebrew/sandbox.rb b/Library/Homebrew/sandbox.rb index 53f915295d..375588421f 100644 --- a/Library/Homebrew/sandbox.rb +++ b/Library/Homebrew/sandbox.rb @@ -6,9 +6,12 @@ require "io/console" require "pty" require "tempfile" require "utils/fork" +require "utils/output" # Helper class for running a sub-process inside of a sandboxed environment. class Sandbox + include Utils::Output::Mixin + SANDBOX_EXEC = "/usr/bin/sandbox-exec" # This is defined in the macOS SDK but Ruby unfortunately does not expose it. diff --git a/Library/Homebrew/sbom.rb b/Library/Homebrew/sbom.rb index 4df1d7e815..b0b463a004 100644 --- a/Library/Homebrew/sbom.rb +++ b/Library/Homebrew/sbom.rb @@ -6,9 +6,12 @@ require "json" require "development_tools" require "cachable" require "utils/curl" +require "utils/output" # Rather than calling `new` directly, use one of the class methods like {SBOM.create}. class SBOM + include Utils::Output::Mixin + FILENAME = "sbom.spdx.json" SCHEMA_FILE = (HOMEBREW_LIBRARY_PATH/"data/schemas/sbom.json").freeze @@ -87,7 +90,7 @@ class SBOM sig { params(bottling: T::Boolean).returns(T::Array[T::Hash[String, T.untyped]]) } def schema_validation_errors(bottling: false) - unless require? "json_schemer" + unless Homebrew.require? "json_schemer" error_message = "Need json_schemer to validate SBOM, run `brew install-bundler-gems --add-groups=bottle`!" odie error_message if ENV["HOMEBREW_ENFORCE_SBOM"] return [] diff --git a/Library/Homebrew/search.rb b/Library/Homebrew/search.rb index 343c11eef3..362079307b 100644 --- a/Library/Homebrew/search.rb +++ b/Library/Homebrew/search.rb @@ -2,10 +2,13 @@ # frozen_string_literal: true require "description_cache_store" +require "utils/output" module Homebrew # Helper module for searching formulae or casks. module Search + extend Utils::Output::Mixin + def self.query_regexp(query) if (m = query.match(%r{^/(.*)/$})) Regexp.new(m[1]) diff --git a/Library/Homebrew/services/cli.rb b/Library/Homebrew/services/cli.rb index e3c7a7aae0..ab94a84382 100644 --- a/Library/Homebrew/services/cli.rb +++ b/Library/Homebrew/services/cli.rb @@ -3,11 +3,13 @@ require "services/formula_wrapper" require "fileutils" +require "utils/output" module Homebrew module Services module Cli extend FileUtils + extend Utils::Output::Mixin sig { returns(T.nilable(String)) } def self.sudo_service_user diff --git a/Library/Homebrew/services/commands/list.rb b/Library/Homebrew/services/commands/list.rb index b1ddc80b7d..fa5c0cf93d 100644 --- a/Library/Homebrew/services/commands/list.rb +++ b/Library/Homebrew/services/commands/list.rb @@ -3,11 +3,14 @@ require "services/cli" require "services/formulae" +require "utils/output" module Homebrew module Services module Commands module List + extend Utils::Output::Mixin + TRIGGERS = [nil, "list", "ls"].freeze sig { params(json: T::Boolean).void } diff --git a/Library/Homebrew/services/formula_wrapper.rb b/Library/Homebrew/services/formula_wrapper.rb index c8539c52da..9db4d4917d 100644 --- a/Library/Homebrew/services/formula_wrapper.rb +++ b/Library/Homebrew/services/formula_wrapper.rb @@ -1,11 +1,15 @@ # typed: true # rubocop:todo Sorbet/StrictSigil # frozen_string_literal: true +require "utils/output" + # Wrapper for a formula to handle service-related stuff like parsing and # generating the service/plist files. module Homebrew module Services class FormulaWrapper + include Utils::Output::Mixin + # Access the `Formula` instance. sig { returns(Formula) } attr_reader :formula diff --git a/Library/Homebrew/services/system.rb b/Library/Homebrew/services/system.rb index ef8eda32d0..d4595682d9 100644 --- a/Library/Homebrew/services/system.rb +++ b/Library/Homebrew/services/system.rb @@ -2,10 +2,13 @@ # frozen_string_literal: true require_relative "system/systemctl" +require "utils/output" module Homebrew module Services module System + extend Utils::Output::Mixin + LAUNCHCTL_DOMAIN_ACTION_NOT_SUPPORTED = T.let(125, Integer) # Path to launchctl binary. diff --git a/Library/Homebrew/style.rb b/Library/Homebrew/style.rb index 80600a494c..2a55175806 100644 --- a/Library/Homebrew/style.rb +++ b/Library/Homebrew/style.rb @@ -4,10 +4,12 @@ require "shellwords" require "source_location" require "system_command" +require "utils/output" module Homebrew # Helper module for running RuboCop. module Style + extend Utils::Output::Mixin extend SystemCommand::Mixin # Checks style for a list of files, printing simple RuboCop output. diff --git a/Library/Homebrew/system_command.rb b/Library/Homebrew/system_command.rb index d8016a024a..6d7d221ec0 100644 --- a/Library/Homebrew/system_command.rb +++ b/Library/Homebrew/system_command.rb @@ -8,6 +8,7 @@ require "uri" require "context" require "readline_nonblock" require "utils/timer" +require "utils/output" # Class for running sub-processes and capturing their output and exit status. # @@ -380,6 +381,7 @@ class SystemCommand # Result containing the output and exit status of a finished sub-process. class Result include Context + include Utils::Output::Mixin attr_accessor :command, :status, :exit_status diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 76f40d5bbc..5d2fcfbf44 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -4,6 +4,7 @@ require "api" require "commands" require "settings" +require "utils/output" # A {Tap} is used to encapsulate Homebrew formulae, casks and custom commands. # Usually, it's synced with a remote Git repository. And it's likely @@ -13,6 +14,8 @@ require "settings" # repository name without the leading `homebrew-`. class Tap extend Cachable + extend Utils::Output::Mixin + include Utils::Output::Mixin HOMEBREW_TAP_CASK_RENAMES_FILE = "cask_renames.json" private_constant :HOMEBREW_TAP_CASK_RENAMES_FILE diff --git a/Library/Homebrew/test/bundle/dsl_spec.rb b/Library/Homebrew/test/bundle/dsl_spec.rb index abdd1e729a..cf0a1ba2f2 100644 --- a/Library/Homebrew/test/bundle/dsl_spec.rb +++ b/Library/Homebrew/test/bundle/dsl_spec.rb @@ -103,7 +103,6 @@ RSpec.describe Homebrew::Bundle::Dsl do end it ".sanitize_cask_name" do - allow_any_instance_of(Object).to receive(:opoo) expect(described_class.send(:sanitize_cask_name, "homebrew/cask-versions/adoptopenjdk8")).to eql("adoptopenjdk8") expect(described_class.send(:sanitize_cask_name, "adoptopenjdk8")).to eql("adoptopenjdk8") end diff --git a/Library/Homebrew/test/cask/dsl_spec.rb b/Library/Homebrew/test/cask/dsl_spec.rb index df761eefd0..c53c7419e8 100644 --- a/Library/Homebrew/test/cask/dsl_spec.rb +++ b/Library/Homebrew/test/cask/dsl_spec.rb @@ -22,9 +22,7 @@ RSpec.describe Cask::DSL, :cask, :no_api do it "prints an error that it has encountered an unexpected method" do expected = Regexp.compile(<<~EOS.lines.map(&:chomp).join) (?m) - Error: - .* - Unexpected method 'future_feature' called on Cask unexpected-method-cask\\. + Error: Unexpected method 'future_feature' called on Cask unexpected-method-cask\\. .* https://github.com/Homebrew/homebrew-cask#reporting-bugs EOS diff --git a/Library/Homebrew/test/extend/kernel_spec.rb b/Library/Homebrew/test/extend/kernel_spec.rb index 7d595d3f8a..f71b341cdc 100644 --- a/Library/Homebrew/test/extend/kernel_spec.rb +++ b/Library/Homebrew/test/extend/kernel_spec.rb @@ -3,92 +3,6 @@ RSpec.describe Kernel do let(:dir) { mktmpdir } - def esc(code) - /(\e\[\d+m)*\e\[#{code}m/ - end - - describe "#ofail" do - it "sets Homebrew.failed to true" do - expect do - ofail "foo" - end.to output("Error: foo\n").to_stderr - - expect(Homebrew).to have_failed - end - end - - describe "#odie" do - it "exits with 1" do - expect do - odie "foo" - end.to output("Error: foo\n").to_stderr.and raise_error SystemExit - end - end - - describe "#pretty_installed" do - subject(:pretty_installed_output) { pretty_installed("foo") } - - context "when $stdout is a TTY" do - before { allow($stdout).to receive(:tty?).and_return(true) } - - context "with HOMEBREW_NO_EMOJI unset" do - it "returns a string with a colored checkmark" do - expect(pretty_installed_output) - .to match(/#{esc 1}foo #{esc 32}✔#{esc 0}/) - end - end - - context "with HOMEBREW_NO_EMOJI set" do - before { ENV["HOMEBREW_NO_EMOJI"] = "1" } - - it "returns a string with colored info" do - expect(pretty_installed_output) - .to match(/#{esc 1}foo \(installed\)#{esc 0}/) - end - end - end - - context "when $stdout is not a TTY" do - before { allow($stdout).to receive(:tty?).and_return(false) } - - it "returns plain text" do - expect(pretty_installed_output).to eq("foo") - end - end - end - - describe "#pretty_uninstalled" do - subject(:pretty_uninstalled_output) { pretty_uninstalled("foo") } - - context "when $stdout is a TTY" do - before { allow($stdout).to receive(:tty?).and_return(true) } - - context "with HOMEBREW_NO_EMOJI unset" do - it "returns a string with a colored checkmark" do - expect(pretty_uninstalled_output) - .to match(/#{esc 1}foo #{esc 31}✘#{esc 0}/) - end - end - - context "with HOMEBREW_NO_EMOJI set" do - before { ENV["HOMEBREW_NO_EMOJI"] = "1" } - - it "returns a string with colored info" do - expect(pretty_uninstalled_output) - .to match(/#{esc 1}foo \(uninstalled\)#{esc 0}/) - end - end - end - - context "when $stdout is not a TTY" do - before { allow($stdout).to receive(:tty?).and_return(false) } - - it "returns plain text" do - expect(pretty_uninstalled_output).to eq("foo") - end - end - end - describe "#interactive_shell" do let(:shell) { dir/"myshell" } @@ -155,16 +69,6 @@ RSpec.describe Kernel do expect(which_editor).to eq("vemate -w") end - describe "#pretty_duration" do - it "converts seconds to a human-readable string" do - expect(pretty_duration(1)).to eq("1 second") - expect(pretty_duration(2.5)).to eq("2 seconds") - expect(pretty_duration(42)).to eq("42 seconds") - expect(pretty_duration(240)).to eq("4 minutes") - expect(pretty_duration(252.45)).to eq("4 minutes 12 seconds") - end - end - specify "#disk_usage_readable" do expect(disk_usage_readable(1)).to eq("1B") expect(disk_usage_readable(1000)).to eq("1000B") @@ -198,22 +102,6 @@ RSpec.describe Kernel do expect(s).to eq(("x" * (n - glue.length)) + glue) end - describe "#odeprecated" do - it "raises a MethodDeprecatedError when `disable` is true" do - ENV.delete("HOMEBREW_DEVELOPER") - expect do - odeprecated( - "method", "replacement", - caller: ["#{HOMEBREW_LIBRARY}/Taps/playbrew/homebrew-play/"], - disable: true - ) - end.to raise_error( - MethodDeprecatedError, - %r{method.*replacement.*playbrew/homebrew-play.*/Taps/playbrew/homebrew-play/}m, - ) - end - end - describe "#with_env" do it "sets environment variables within the block" do expect(ENV.fetch("PATH")).not_to eq("/bin") diff --git a/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb b/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb index 0596fd1f8a..cc7b6ba90a 100755 --- a/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb +++ b/Library/Homebrew/test/support/helper/cmd/brew-verify-undefined.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "cli/parser" +require "utils/output" UNDEFINED_CONSTANTS = %w[ AbstractDownloadStrategy @@ -74,7 +75,7 @@ parser.parse UNDEFINED_CONSTANTS.each do |constant_name| Object.const_get(constant_name) - ofail "#{constant_name} should not be defined at startup" + Utils::Output.ofail "#{constant_name} should not be defined at startup" rescue NameError # We expect this to error as it should not be defined. end diff --git a/Library/Homebrew/test/tap_spec.rb b/Library/Homebrew/test/tap_spec.rb index 6f60403281..0852d3ecd6 100644 --- a/Library/Homebrew/test/tap_spec.rb +++ b/Library/Homebrew/test/tap_spec.rb @@ -22,6 +22,9 @@ RSpec.describe Tap do path.mkpath (path/"audit_exceptions").mkpath (path/"style_exceptions").mkpath + + # requiring utils/output in tap.rb should be enough but it's not for no apparent reason. + $stderr.extend(Utils::Output::Mixin) end def setup_tap_files diff --git a/Library/Homebrew/test/utils/output_spec.rb b/Library/Homebrew/test/utils/output_spec.rb new file mode 100644 index 0000000000..0b3d21e599 --- /dev/null +++ b/Library/Homebrew/test/utils/output_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require "utils/output" + +RSpec.describe Utils::Output do + def esc(code) + /(\e\[\d+m)*\e\[#{code}m/ + end + + describe "#pretty_installed" do + subject(:pretty_installed_output) { described_class.pretty_installed("foo") } + + context "when $stdout is a TTY" do + before { allow($stdout).to receive(:tty?).and_return(true) } + + context "with HOMEBREW_NO_EMOJI unset" do + it "returns a string with a colored checkmark" do + expect(pretty_installed_output) + .to match(/#{esc 1}foo #{esc 32}✔#{esc 0}/) + end + end + + context "with HOMEBREW_NO_EMOJI set" do + before { ENV["HOMEBREW_NO_EMOJI"] = "1" } + + it "returns a string with colored info" do + expect(pretty_installed_output) + .to match(/#{esc 1}foo \(installed\)#{esc 0}/) + end + end + end + + context "when $stdout is not a TTY" do + before { allow($stdout).to receive(:tty?).and_return(false) } + + it "returns plain text" do + expect(pretty_installed_output).to eq("foo") + end + end + end + + describe "#pretty_uninstalled" do + subject(:pretty_uninstalled_output) { described_class.pretty_uninstalled("foo") } + + context "when $stdout is a TTY" do + before { allow($stdout).to receive(:tty?).and_return(true) } + + context "with HOMEBREW_NO_EMOJI unset" do + it "returns a string with a colored checkmark" do + expect(pretty_uninstalled_output) + .to match(/#{esc 1}foo #{esc 31}✘#{esc 0}/) + end + end + + context "with HOMEBREW_NO_EMOJI set" do + before { ENV["HOMEBREW_NO_EMOJI"] = "1" } + + it "returns a string with colored info" do + expect(pretty_uninstalled_output) + .to match(/#{esc 1}foo \(uninstalled\)#{esc 0}/) + end + end + end + + context "when $stdout is not a TTY" do + before { allow($stdout).to receive(:tty?).and_return(false) } + + it "returns plain text" do + expect(pretty_uninstalled_output).to eq("foo") + end + end + end + + describe "#pretty_duration" do + it "converts seconds to a human-readable string" do + expect(described_class.pretty_duration(1)).to eq("1 second") + expect(described_class.pretty_duration(2.5)).to eq("2 seconds") + expect(described_class.pretty_duration(42)).to eq("42 seconds") + expect(described_class.pretty_duration(240)).to eq("4 minutes") + expect(described_class.pretty_duration(252.45)).to eq("4 minutes 12 seconds") + end + end + + describe "#ofail" do + it "sets Homebrew.failed to true" do + expect do + described_class.ofail "foo" + end.to output("Error: foo\n").to_stderr + + expect(Homebrew).to have_failed + end + end + + describe "#odie" do + it "exits with 1" do + expect do + described_class.odie "foo" + end.to output("Error: foo\n").to_stderr.and raise_error SystemExit + end + end + + describe "#odeprecated" do + it "raises a MethodDeprecatedError when `disable` is true" do + ENV.delete("HOMEBREW_DEVELOPER") + expect do + described_class.odeprecated( + "method", "replacement", + caller: ["#{HOMEBREW_LIBRARY}/Taps/playbrew/homebrew-play/"], + disable: true + ) + end.to raise_error( + MethodDeprecatedError, + %r{method.*replacement.*playbrew/homebrew-play.*/Taps/playbrew/homebrew-play/}m, + ) + end + end +end diff --git a/Library/Homebrew/uninstall.rb b/Library/Homebrew/uninstall.rb index 6d88d2fc3b..62c421cd7f 100644 --- a/Library/Homebrew/uninstall.rb +++ b/Library/Homebrew/uninstall.rb @@ -2,10 +2,13 @@ # frozen_string_literal: true require "installed_dependents" +require "utils/output" module Homebrew # Helper module for uninstalling kegs. module Uninstall + extend ::Utils::Output::Mixin + def self.uninstall_kegs(kegs_by_rack, casks: [], force: false, ignore_dependencies: false, named_args: []) handle_unsatisfied_dependents(kegs_by_rack, casks:, @@ -122,6 +125,8 @@ module Homebrew end class DependentsMessage + include ::Utils::Output::Mixin + attr_reader :reqs, :deps, :named_args def initialize(requireds, dependents, named_args: []) diff --git a/Library/Homebrew/unpack_strategy.rb b/Library/Homebrew/unpack_strategy.rb index 641439634f..6b1fbe945d 100644 --- a/Library/Homebrew/unpack_strategy.rb +++ b/Library/Homebrew/unpack_strategy.rb @@ -3,11 +3,14 @@ require "mktemp" require "system_command" +require "utils/output" # Module containing all available strategies for unpacking archives. module UnpackStrategy extend T::Helpers + extend Utils::Output::Mixin include SystemCommand::Mixin + include Utils::Output::Mixin abstract! diff --git a/Library/Homebrew/unpack_strategy/dmg.rb b/Library/Homebrew/unpack_strategy/dmg.rb index b12a5ab2c9..ca8ca097ed 100644 --- a/Library/Homebrew/unpack_strategy/dmg.rb +++ b/Library/Homebrew/unpack_strategy/dmg.rb @@ -3,6 +3,7 @@ require "tempfile" require "system_command" +require "utils/output" module UnpackStrategy # Strategy for unpacking disk images. @@ -12,6 +13,7 @@ module UnpackStrategy # Helper module for listing the contents of a volume mounted from a disk image. module Bom + extend Utils::Output::Mixin extend SystemCommand::Mixin DMG_METADATA = T.let(Set.new([ diff --git a/Library/Homebrew/unversioned_cask_checker.rb b/Library/Homebrew/unversioned_cask_checker.rb index 195ecdd184..0594236fe0 100644 --- a/Library/Homebrew/unversioned_cask_checker.rb +++ b/Library/Homebrew/unversioned_cask_checker.rb @@ -5,12 +5,14 @@ require "bundle_version" require "cask/cask" require "cask/installer" require "system_command" +require "utils/output" module Homebrew # Check unversioned casks for updates by extracting their # contents and guessing the version from contained files. class UnversionedCaskChecker include SystemCommand::Mixin + include Utils::Output::Mixin sig { returns(Cask::Cask) } attr_reader :cask diff --git a/Library/Homebrew/upgrade.rb b/Library/Homebrew/upgrade.rb index 5ec9e528d5..c3ce323abc 100644 --- a/Library/Homebrew/upgrade.rb +++ b/Library/Homebrew/upgrade.rb @@ -7,10 +7,13 @@ require "development_tools" require "messages" require "cleanup" require "utils/topological_hash" +require "utils/output" module Homebrew # Helper functions for upgrading formulae. module Upgrade + extend Utils::Output::Mixin + class Dependents < T::Struct const :upgradeable, T::Array[Formula] const :pinned, T::Array[Formula] diff --git a/Library/Homebrew/utils.rb b/Library/Homebrew/utils.rb index 57059f3c2a..4ced10e762 100644 --- a/Library/Homebrew/utils.rb +++ b/Library/Homebrew/utils.rb @@ -6,6 +6,24 @@ require "context" module Homebrew extend Context + sig { params(path: T.nilable(T.any(String, Pathname))).returns(T::Boolean) } + def self.require?(path) + return false if path.nil? + + if defined?(Warnings) + # Work around require warning when done repeatedly: + # https://bugs.ruby-lang.org/issues/21091 + Warnings.ignore(/already initialized constant/, /previous definition of/) do + require path.to_s + end + else + require path.to_s + end + true + rescue LoadError + false + end + # Need to keep this naming as-is for backwards compatibility. # rubocop:disable Naming/PredicateMethod def self._system(cmd, *args, **options) diff --git a/Library/Homebrew/utils/analytics.rb b/Library/Homebrew/utils/analytics.rb index a22d3f8bae..5af6765cf8 100644 --- a/Library/Homebrew/utils/analytics.rb +++ b/Library/Homebrew/utils/analytics.rb @@ -5,6 +5,7 @@ require "context" require "erb" require "settings" require "cachable" +require "utils/output" module Utils # Helper module for fetching and reporting analytics data. @@ -14,6 +15,7 @@ module Utils INFLUX_HOST = "https://eu-central-1-1.aws.cloud2.influxdata.com" INFLUX_ORG = "d81a3e6d582d485f" + extend Utils::Output::Mixin extend Cachable class << self diff --git a/Library/Homebrew/utils/backtrace.rb b/Library/Homebrew/utils/backtrace.rb index 376e5fc5c9..e1c2f2b510 100644 --- a/Library/Homebrew/utils/backtrace.rb +++ b/Library/Homebrew/utils/backtrace.rb @@ -1,8 +1,12 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Utils module Backtrace + extend Utils::Output::Mixin + @print_backtrace_message = T.let(false, T::Boolean) # Cleans `sorbet-runtime` gem paths from the backtrace unless... diff --git a/Library/Homebrew/utils/cpan.rb b/Library/Homebrew/utils/cpan.rb index bd44987eb2..aefdbd89f1 100644 --- a/Library/Homebrew/utils/cpan.rb +++ b/Library/Homebrew/utils/cpan.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "utils/inreplace" +require "utils/output" # Helper functions for updating CPAN resources. module CPAN @@ -9,6 +10,8 @@ module CPAN CPAN_ARCHIVE_REGEX = /^(.+)-([0-9.v]+)\.(?:tar\.gz|tgz)$/ private_constant :METACPAN_URL_PREFIX, :CPAN_ARCHIVE_REGEX + extend Utils::Output::Mixin + # Represents a Perl package from an existing resource. class Package sig { params(resource_name: String, resource_url: String).void } diff --git a/Library/Homebrew/utils/gems.rb b/Library/Homebrew/utils/gems.rb index 30c0a806cf..9aa138740a 100644 --- a/Library/Homebrew/utils/gems.rb +++ b/Library/Homebrew/utils/gems.rb @@ -58,7 +58,7 @@ module Homebrew def self.ohai_if_defined(message) if defined?(ohai) - $stderr.ohai message + ohai message else $stderr.puts "==> #{message}" end @@ -66,7 +66,7 @@ module Homebrew def self.opoo_if_defined(message) if defined?(opoo) - $stderr.opoo message + opoo message else $stderr.puts "Warning: #{message}" end diff --git a/Library/Homebrew/env_config.rbi b/Library/Homebrew/utils/gems.rbi similarity index 60% rename from Library/Homebrew/env_config.rbi rename to Library/Homebrew/utils/gems.rbi index d6e994727e..849498daa9 100644 --- a/Library/Homebrew/env_config.rbi +++ b/Library/Homebrew/utils/gems.rbi @@ -2,7 +2,5 @@ # frozen_string_literal: true module Homebrew - module EnvConfig - include Kernel - end + extend Utils::Output::Mixin end diff --git a/Library/Homebrew/utils/github.rb b/Library/Homebrew/utils/github.rb index 430d1cd14e..9779723924 100644 --- a/Library/Homebrew/utils/github.rb +++ b/Library/Homebrew/utils/github.rb @@ -4,6 +4,7 @@ require "uri" require "utils/github/actions" require "utils/github/api" +require "utils/output" require "system_command" @@ -13,6 +14,8 @@ require "system_command" module GitHub extend SystemCommand::Mixin + extend Utils::Output::Mixin + def self.check_runs(repo: nil, commit: nil, pull_request: nil) if pull_request repo = pull_request.fetch("base").fetch("repo").fetch("full_name") diff --git a/Library/Homebrew/utils/github/api.rb b/Library/Homebrew/utils/github/api.rb index 048183b7cd..53d4733bba 100644 --- a/Library/Homebrew/utils/github/api.rb +++ b/Library/Homebrew/utils/github/api.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "system_command" +require "utils/output" module GitHub sig { params(scopes: T::Array[String]).returns(String) } @@ -39,9 +40,12 @@ module GitHub # @api internal module API extend SystemCommand::Mixin + extend Utils::Output::Mixin # Generic API error. class Error < RuntimeError + include Utils::Output::Mixin + sig { returns(T.nilable(String)) } attr_reader :github_message diff --git a/Library/Homebrew/utils/gzip.rb b/Library/Homebrew/utils/gzip.rb index d4e927309d..051892204b 100644 --- a/Library/Homebrew/utils/gzip.rb +++ b/Library/Homebrew/utils/gzip.rb @@ -1,9 +1,13 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Utils # Helper functions for creating gzip files. module Gzip + extend ::Utils::Output::Mixin + # Apple's gzip also uses zlib so use the same buffer size here. # https://github.com/apple-oss-distributions/file_cmds/blob/file_cmds-400/gzip/gzip.c#L147 GZIP_BUFFER_SIZE = T.let(64 * 1024, Integer) diff --git a/Library/Homebrew/utils/link.rb b/Library/Homebrew/utils/link.rb index 01a5211b3e..7911a65c65 100644 --- a/Library/Homebrew/utils/link.rb +++ b/Library/Homebrew/utils/link.rb @@ -1,9 +1,13 @@ # typed: strict # frozen_string_literal: true +require "utils/output" + module Utils # Helper functions for creating symlinks. module Link + extend Utils::Output::Mixin + sig { params(src_dir: Pathname, dst_dir: Pathname, command: String, link_dir: T::Boolean).void } def self.link_src_dst_dirs(src_dir, dst_dir, command, link_dir: false) return unless src_dir.exist? diff --git a/Library/Homebrew/utils/output.rb b/Library/Homebrew/utils/output.rb new file mode 100644 index 0000000000..cfc9a2c6d9 --- /dev/null +++ b/Library/Homebrew/utils/output.rb @@ -0,0 +1,271 @@ +# typed: strict +# frozen_string_literal: true + +module Utils + module Output + module Mixin + extend T::Helpers + + requires_ancestor { Kernel } + + sig { params(title: String).returns(String) } + def ohai_title(title) + verbose = if respond_to?(:verbose?) + T.unsafe(self).verbose? + else + Context.current.verbose? + end + + title = Tty.truncate(title.to_s) if $stdout.tty? && !verbose + Formatter.headline(title, color: :blue) + end + + sig { params(title: T.any(String, Exception), sput: T.anything).void } + def ohai(title, *sput) + puts ohai_title(title.to_s) + puts sput + end + + sig { params(title: T.any(String, Exception), sput: T.anything, always_display: T::Boolean).void } + def odebug(title, *sput, always_display: false) + debug = if respond_to?(:debug) + T.unsafe(self).debug? + else + Context.current.debug? + end + + return if !debug && !always_display + + $stderr.puts Formatter.headline(title.to_s, color: :magenta) + $stderr.puts sput unless sput.empty? + end + + sig { params(title: String, truncate: T.any(Symbol, T::Boolean)).returns(String) } + def oh1_title(title, truncate: :auto) + verbose = if respond_to?(:verbose?) + T.unsafe(self).verbose? + else + Context.current.verbose? + end + + title = Tty.truncate(title.to_s) if $stdout.tty? && !verbose && truncate == :auto + Formatter.headline(title, color: :green) + end + + sig { params(title: String, truncate: T.any(Symbol, T::Boolean)).void } + def oh1(title, truncate: :auto) + puts oh1_title(title, truncate:) + end + + # Print a warning message. + # + # @api public + sig { params(message: T.any(String, Exception)).void } + def opoo(message) + require "utils/github/actions" + return if GitHub::Actions.puts_annotation_if_env_set!(:warning, message.to_s) + + require "utils/formatter" + + Tty.with($stderr) do |stderr| + stderr.puts Formatter.warning(message, label: "Warning") + end + end + + # Print a warning message only if not running in GitHub Actions. + # + # @api public + sig { params(message: T.any(String, Exception)).void } + def opoo_outside_github_actions(message) + require "utils/github/actions" + return if GitHub::Actions.env_set? + + opoo(message) + end + + # Print an error message. + # + # @api public + sig { params(message: T.any(String, Exception)).void } + def onoe(message) + require "utils/github/actions" + return if GitHub::Actions.puts_annotation_if_env_set!(:error, message.to_s) + + require "utils/formatter" + + Tty.with($stderr) do |stderr| + stderr.puts Formatter.error(message, label: "Error") + end + end + + # Print an error message and fail at the end of the program. + # + # @api public + sig { params(error: T.any(String, Exception)).void } + def ofail(error) + onoe error + Homebrew.failed = true + end + + # Print an error message and fail immediately. + # + # @api public + sig { params(error: T.any(String, Exception)).returns(T.noreturn) } + def odie(error) + onoe error + exit 1 + end + + # Output a deprecation warning/error message. + sig { + params(method: String, replacement: T.nilable(T.any(String, Symbol)), disable: T::Boolean, + disable_on: T.nilable(Time), disable_for_developers: T::Boolean, caller: T::Array[String]).void + } + def odeprecated(method, replacement = nil, + disable: false, + disable_on: nil, + disable_for_developers: true, + caller: send(:caller)) + replacement_message = if replacement + "Use #{replacement} instead." + else + "There is no replacement." + end + + unless disable_on.nil? + if disable_on > Time.now + will_be_disabled_message = " and will be disabled on #{disable_on.strftime("%Y-%m-%d")}" + else + disable = true + end + end + + verb = if disable + "disabled" + else + "deprecated#{will_be_disabled_message}" + end + + # Try to show the most relevant location in message, i.e. (if applicable): + # - Location in a formula. + # - Location of caller of deprecated method (if all else fails). + backtrace = caller + + # Don't throw deprecations at all for cached, .brew or .metadata files. + return if backtrace.any? do |line| + next true if line.include?(HOMEBREW_CACHE.to_s) + next true if line.include?("/.brew/") + next true if line.include?("/.metadata/") + + next false unless line.match?(HOMEBREW_TAP_PATH_REGEX) + + path = Pathname(line.split(":", 2).first) + next false unless path.file? + next false unless path.readable? + + formula_contents = path.read + formula_contents.include?(" deprecate! ") || formula_contents.include?(" disable! ") + end + + tap_message = T.let(nil, T.nilable(String)) + + backtrace.each do |line| + next unless (match = line.match(HOMEBREW_TAP_PATH_REGEX)) + + require "tap" + + tap = Tap.fetch(match[:user], match[:repository]) + tap_message = "\nPlease report this issue to the #{tap.full_name} tap" + tap_message += " (not Homebrew/* repositories)" unless tap.official? + tap_message += ", or even better, submit a PR to fix it" if replacement + tap_message << ":\n #{line.sub(/^(.*:\d+):.*$/, '\1')}\n\n" + break + end + file, line, = backtrace.first.split(":") + line = line.to_i if line.present? + + message = "Calling #{method} is #{verb}! #{replacement_message}" + message << tap_message if tap_message + message.freeze + + disable = true if disable_for_developers && Homebrew::EnvConfig.developer? + if disable || Homebrew.raise_deprecation_exceptions? + require "utils/github/actions" + GitHub::Actions.puts_annotation_if_env_set!(:error, message, file:, line:) + exception = MethodDeprecatedError.new(message) + exception.set_backtrace(backtrace) + raise exception + elsif !Homebrew.auditing? + opoo message + end + end + + sig { + params(method: String, replacement: T.nilable(T.any(String, Symbol)), + disable_on: T.nilable(Time), disable_for_developers: T::Boolean, caller: T::Array[String]).void + } + def odisabled(method, replacement = nil, + disable_on: nil, + disable_for_developers: true, + caller: send(:caller)) + # This odeprecated should stick around indefinitely. + odeprecated(method, replacement, disable: true, disable_on:, disable_for_developers:, caller:) + end + + sig { params(string: String).returns(String) } + def pretty_installed(string) + if !$stdout.tty? + string + elsif Homebrew::EnvConfig.no_emoji? + Formatter.success("#{Tty.bold}#{string} (installed)#{Tty.reset}") + else + "#{Tty.bold}#{string} #{Formatter.success("✔")}#{Tty.reset}" + end + end + + sig { params(string: String).returns(String) } + def pretty_outdated(string) + if !$stdout.tty? + string + elsif Homebrew::EnvConfig.no_emoji? + Formatter.error("#{Tty.bold}#{string} (outdated)#{Tty.reset}") + else + "#{Tty.bold}#{string} #{Formatter.warning("⚠")}#{Tty.reset}" + end + end + + sig { params(string: String).returns(String) } + def pretty_uninstalled(string) + if !$stdout.tty? + string + elsif Homebrew::EnvConfig.no_emoji? + Formatter.error("#{Tty.bold}#{string} (uninstalled)#{Tty.reset}") + else + "#{Tty.bold}#{string} #{Formatter.error("✘")}#{Tty.reset}" + end + end + + sig { params(seconds: T.nilable(T.any(Integer, Float))).returns(String) } + def pretty_duration(seconds) + seconds = seconds.to_i + res = +"" + + if seconds > 59 + minutes = seconds / 60 + seconds %= 60 + res = +Utils.pluralize("minute", minutes, include_count: true) + return res.freeze if seconds.zero? + + res << " " + end + + res << Utils.pluralize("second", seconds, include_count: true) + res.freeze + end + end + + extend Mixin + $stdout.extend Mixin + $stderr.extend Mixin + end +end diff --git a/Library/Homebrew/utils/pypi.rb b/Library/Homebrew/utils/pypi.rb index d1548868de..ace69cab10 100644 --- a/Library/Homebrew/utils/pypi.rb +++ b/Library/Homebrew/utils/pypi.rb @@ -2,9 +2,12 @@ # frozen_string_literal: true require "utils/inreplace" +require "utils/output" # Helper functions for updating PyPI resources. module PyPI + extend Utils::Output::Mixin + PYTHONHOSTED_URL_PREFIX = "https://files.pythonhosted.org/packages/" private_constant :PYTHONHOSTED_URL_PREFIX @@ -12,6 +15,8 @@ module PyPI # This package can be a PyPI package (either by name/version or PyPI distribution URL), # or it can be a non-PyPI URL. class Package + include Utils::Output::Mixin + sig { params(package_string: String, is_url: T::Boolean, python_name: String).void } def initialize(package_string, is_url: false, python_name: "python") @pypi_info = T.let(nil, T.nilable(T::Array[String])) diff --git a/Library/Homebrew/utils/repology.rb b/Library/Homebrew/utils/repology.rb index a008be62a3..ae96d5255c 100644 --- a/Library/Homebrew/utils/repology.rb +++ b/Library/Homebrew/utils/repology.rb @@ -2,9 +2,12 @@ # frozen_string_literal: true require "utils/curl" +require "utils/output" # Repology API client. module Repology + extend Utils::Output::Mixin + HOMEBREW_CORE = "homebrew" HOMEBREW_CASK = "homebrew_casks" MAX_PAGINATION = 15 diff --git a/Library/Homebrew/utils/string_inreplace_extension.rb b/Library/Homebrew/utils/string_inreplace_extension.rb index 71e9b5f331..520d0c4dde 100644 --- a/Library/Homebrew/utils/string_inreplace_extension.rb +++ b/Library/Homebrew/utils/string_inreplace_extension.rb @@ -1,8 +1,12 @@ # typed: strong # frozen_string_literal: true +require "utils/output" + # Used by the {Utils::Inreplace.inreplace} function. class StringInreplaceExtension + include Utils::Output::Mixin + sig { returns(T::Array[String]) } attr_accessor :errors diff --git a/Library/Homebrew/utils/svn.rb b/Library/Homebrew/utils/svn.rb index 049e95e798..2ea4f93d52 100644 --- a/Library/Homebrew/utils/svn.rb +++ b/Library/Homebrew/utils/svn.rb @@ -2,12 +2,14 @@ # frozen_string_literal: true require "system_command" +require "utils/output" module Utils # Helper functions for querying SVN information. module Svn class << self include SystemCommand::Mixin + include Utils::Output::Mixin sig { returns(T::Boolean) } def available? diff --git a/Library/Homebrew/utils/tar.rb b/Library/Homebrew/utils/tar.rb index 8614ee0ec3..6878e702c5 100644 --- a/Library/Homebrew/utils/tar.rb +++ b/Library/Homebrew/utils/tar.rb @@ -2,12 +2,14 @@ # frozen_string_literal: true require "system_command" +require "utils/output" module Utils # Helper functions for interacting with tar files. module Tar class << self include SystemCommand::Mixin + include Utils::Output::Mixin TAR_FILE_EXTENSIONS = %w[.tar .tb2 .tbz .tbz2 .tgz .tlz .txz .tZ].freeze