diff --git a/Library/Homebrew/abstract_command.rb b/Library/Homebrew/abstract_command.rb index 95b086f022..9927c0fa87 100644 --- a/Library/Homebrew/abstract_command.rb +++ b/Library/Homebrew/abstract_command.rb @@ -1,12 +1,15 @@ # typed: strong # frozen_string_literal: true +require "cli/parser" + module Homebrew # Subclass this to implement a `brew` command. This is preferred to declaring a named function in the `Homebrew` # module, because: # - Each Command lives in an isolated namespace. # - Each Command implements a defined interface. # - `args` is available as an ivar, and thus does not need to be passed as an argument to helper methods. + # - Subclasses no longer need to reference `CLI::Parser` or parse args explicitly. # # To subclass, implement a `run` method and provide a `cmd_args` block to document the command and its allowed args. # To generate method signatures for command args, run `brew typecheck --update`. diff --git a/Library/Homebrew/build.rb b/Library/Homebrew/build.rb index 663b2f04b5..9795c2e5af 100644 --- a/Library/Homebrew/build.rb +++ b/Library/Homebrew/build.rb @@ -217,7 +217,7 @@ class Build end begin - args = Homebrew.install_args.parse + args = Homebrew::Cmd::InstallCmd.new.args Context.current = args.context error_pipe = UNIXSocket.open(ENV.fetch("HOMEBREW_ERROR_PIPE"), &:recv_io) diff --git a/Library/Homebrew/cmd/analytics.rb b/Library/Homebrew/cmd/analytics.rb index f5ac8ef8a0..6531b823c8 100644 --- a/Library/Homebrew/cmd/analytics.rb +++ b/Library/Homebrew/cmd/analytics.rb @@ -1,51 +1,48 @@ # typed: strict # frozen_string_literal: true -require "cli/parser" +require "abstract_command" module Homebrew - module_function + module Cmd + class Analytics < AbstractCommand + cmd_args do + description <<~EOS + Control Homebrew's anonymous aggregate user behaviour analytics. + Read more at . - sig { returns(CLI::Parser) } - def analytics_args - Homebrew::CLI::Parser.new do - description <<~EOS - Control Homebrew's anonymous aggregate user behaviour analytics. - Read more at . + `brew analytics` [`state`]: + Display the current state of Homebrew's analytics. - `brew analytics` [`state`]: - Display the current state of Homebrew's analytics. + `brew analytics` (`on`|`off`): + Turn Homebrew's analytics on or off respectively. + EOS - `brew analytics` (`on`|`off`): - Turn Homebrew's analytics on or off respectively. - EOS - - named_args %w[state on off regenerate-uuid], max: 1 - end - end - - sig { void } - def analytics - args = analytics_args.parse - - case args.named.first - when nil, "state" - if Utils::Analytics.disabled? - puts "InfluxDB analytics are disabled." - else - puts "InfluxDB analytics are enabled." + named_args %w[state on off regenerate-uuid], max: 1 + end + + sig { override.void } + def run + case args.named.first + when nil, "state" + if Utils::Analytics.disabled? + puts "InfluxDB analytics are disabled." + else + puts "InfluxDB analytics are enabled." + end + puts "Google Analytics were destroyed." + when "on" + Utils::Analytics.enable! + when "off" + Utils::Analytics.disable! + when "regenerate-uuid" + Utils::Analytics.delete_uuid! + opoo "Homebrew no longer uses an analytics UUID so this has been deleted!" + puts "brew analytics regenerate-uuid is no longer necessary." + else + raise UsageError, "unknown subcommand: #{args.named.first}" + end end - puts "Google Analytics were destroyed." - when "on" - Utils::Analytics.enable! - when "off" - Utils::Analytics.disable! - when "regenerate-uuid" - Utils::Analytics.delete_uuid! - opoo "Homebrew no longer uses an analytics UUID so this has been deleted!" - puts "brew analytics regenerate-uuid is no longer necessary." - else - raise UsageError, "unknown subcommand: #{args.named.first}" end end end diff --git a/Library/Homebrew/cmd/autoremove.rb b/Library/Homebrew/cmd/autoremove.rb index 6f2f724bc1..45f22bde1f 100644 --- a/Library/Homebrew/cmd/autoremove.rb +++ b/Library/Homebrew/cmd/autoremove.rb @@ -1,27 +1,26 @@ # typed: strict # frozen_string_literal: true +require "abstract_command" require "cleanup" -require "cli/parser" module Homebrew - sig { returns(CLI::Parser) } - def self.autoremove_args - Homebrew::CLI::Parser.new do - description <<~EOS - Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed. - EOS - switch "-n", "--dry-run", - description: "List what would be uninstalled, but do not actually uninstall anything." + module Cmd + class Autoremove < AbstractCommand + cmd_args do + description <<~EOS + Uninstall formulae that were only installed as a dependency of another formula and are now no longer needed. + EOS + switch "-n", "--dry-run", + description: "List what would be uninstalled, but do not actually uninstall anything." - named_args :none + named_args :none + end + + sig { override.void } + def run + Cleanup.autoremove(dry_run: args.dry_run?) + end end end - - sig { void } - def self.autoremove - args = autoremove_args.parse - - Cleanup.autoremove(dry_run: args.dry_run?) - end end diff --git a/Library/Homebrew/cmd/cleanup.rb b/Library/Homebrew/cmd/cleanup.rb index 470e08c7e3..6960f61725 100644 --- a/Library/Homebrew/cmd/cleanup.rb +++ b/Library/Homebrew/cmd/cleanup.rb @@ -1,75 +1,72 @@ # typed: strict # frozen_string_literal: true +require "abstract_command" require "cleanup" -require "cli/parser" module Homebrew - module_function + module Cmd + class CleanupCmd < AbstractCommand + cmd_args do + days = Homebrew::EnvConfig::ENVS[:HOMEBREW_CLEANUP_MAX_AGE_DAYS][:default] + description <<~EOS + Remove stale lock files and outdated downloads for all formulae and casks, + and remove old versions of installed formulae. If arguments are specified, + only do this for the given formulae and casks. Removes all downloads more than + #{days} days old. This can be adjusted with `HOMEBREW_CLEANUP_MAX_AGE_DAYS`. + EOS + flag "--prune=", + description: "Remove all cache files older than specified . " \ + "If you want to remove everything, use `--prune=all`." + switch "-n", "--dry-run", + description: "Show what would be removed, but do not actually remove anything." + switch "-s", + description: "Scrub the cache, including downloads for even the latest versions. " \ + "Note that downloads for any installed formulae or casks will still not be deleted. " \ + "If you want to delete those too: `rm -rf \"$(brew --cache)\"`" + switch "--prune-prefix", + description: "Only prune the symlinks and directories from the prefix and remove no other files." - sig { returns(CLI::Parser) } - def cleanup_args - Homebrew::CLI::Parser.new do - days = Homebrew::EnvConfig::ENVS[:HOMEBREW_CLEANUP_MAX_AGE_DAYS][:default] - description <<~EOS - Remove stale lock files and outdated downloads for all formulae and casks, - and remove old versions of installed formulae. If arguments are specified, - only do this for the given formulae and casks. Removes all downloads more than - #{days} days old. This can be adjusted with `HOMEBREW_CLEANUP_MAX_AGE_DAYS`. - EOS - flag "--prune=", - description: "Remove all cache files older than specified . " \ - "If you want to remove everything, use `--prune=all`." - switch "-n", "--dry-run", - description: "Show what would be removed, but do not actually remove anything." - switch "-s", - description: "Scrub the cache, including downloads for even the latest versions. " \ - "Note that downloads for any installed formulae or casks will still not be deleted. " \ - "If you want to delete those too: `rm -rf \"$(brew --cache)\"`" - switch "--prune-prefix", - description: "Only prune the symlinks and directories from the prefix and remove no other files." + named_args [:formula, :cask] + end - named_args [:formula, :cask] - end - end + sig { override.void } + def run + days = args.prune.presence&.then do |prune| + case prune + when /\A\d+\Z/ + prune.to_i + when "all" + 0 + else + raise UsageError, "`--prune` expects an integer or `all`." + end + end - sig { void } - def cleanup - args = cleanup_args.parse + cleanup = Cleanup.new(*args.named, dry_run: args.dry_run?, scrub: args.s?, days:) + if args.prune_prefix? + cleanup.prune_prefix_symlinks_and_directories + return + end - days = args.prune.presence&.then do |prune| - case prune - when /\A\d+\Z/ - prune.to_i - when "all" - 0 - else - raise UsageError, "`--prune` expects an integer or `all`." + cleanup.clean!(quiet: args.quiet?, periodic: false) + + unless cleanup.disk_cleanup_size.zero? + disk_space = disk_usage_readable(cleanup.disk_cleanup_size) + if args.dry_run? + ohai "This operation would free approximately #{disk_space} of disk space." + else + ohai "This operation has freed approximately #{disk_space} of disk space." + end + end + + return if cleanup.unremovable_kegs.empty? + + ofail <<~EOS + Could not cleanup old kegs! Fix your permissions on: + #{cleanup.unremovable_kegs.join "\n "} + EOS end end - - cleanup = Cleanup.new(*args.named, dry_run: args.dry_run?, scrub: args.s?, days:) - if args.prune_prefix? - cleanup.prune_prefix_symlinks_and_directories - return - end - - cleanup.clean!(quiet: args.quiet?, periodic: false) - - unless cleanup.disk_cleanup_size.zero? - disk_space = disk_usage_readable(cleanup.disk_cleanup_size) - if args.dry_run? - ohai "This operation would free approximately #{disk_space} of disk space." - else - ohai "This operation has freed approximately #{disk_space} of disk space." - end - end - - return if cleanup.unremovable_kegs.empty? - - ofail <<~EOS - Could not cleanup old kegs! Fix your permissions on: - #{cleanup.unremovable_kegs.join "\n "} - EOS end end diff --git a/Library/Homebrew/cmd/commands.rb b/Library/Homebrew/cmd/commands.rb index f28546ade6..6ec4cf889e 100644 --- a/Library/Homebrew/cmd/commands.rb +++ b/Library/Homebrew/cmd/commands.rb @@ -1,49 +1,46 @@ # typed: strict # frozen_string_literal: true -require "cli/parser" +require "abstract_command" module Homebrew - module_function + module Cmd + class CommandsCmd < AbstractCommand + cmd_args do + description <<~EOS + Show lists of built-in and external commands. + EOS + switch "-q", "--quiet", + description: "List only the names of commands without category headers." + switch "--include-aliases", + depends_on: "--quiet", + description: "Include aliases of internal commands." - sig { returns(CLI::Parser) } - def commands_args - Homebrew::CLI::Parser.new do - description <<~EOS - Show lists of built-in and external commands. - EOS - switch "-q", "--quiet", - description: "List only the names of commands without category headers." - switch "--include-aliases", - depends_on: "--quiet", - description: "Include aliases of internal commands." + named_args :none + end - named_args :none - end - end + sig { override.void } + def run + if args.quiet? + puts Formatter.columns(Commands.commands(aliases: args.include_aliases?)) + return + end - sig { void } - def commands - args = commands_args.parse + prepend_separator = T.let(false, T::Boolean) - if args.quiet? - puts Formatter.columns(Commands.commands(aliases: args.include_aliases?)) - return - end + { + "Built-in commands" => Commands.internal_commands, + "Built-in developer commands" => Commands.internal_developer_commands, + "External commands" => Commands.external_commands, + }.each do |title, commands| + next if commands.blank? - prepend_separator = T.let(false, T::Boolean) + puts if prepend_separator + ohai title, Formatter.columns(commands) - { - "Built-in commands" => Commands.internal_commands, - "Built-in developer commands" => Commands.internal_developer_commands, - "External commands" => Commands.external_commands, - }.each do |title, commands| - next if commands.blank? - - puts if prepend_separator - ohai title, Formatter.columns(commands) - - prepend_separator ||= true + prepend_separator ||= true + end + end end end end diff --git a/Library/Homebrew/cmd/completions.rb b/Library/Homebrew/cmd/completions.rb index ac7a8e2a5f..2fd15596ce 100644 --- a/Library/Homebrew/cmd/completions.rb +++ b/Library/Homebrew/cmd/completions.rb @@ -1,49 +1,46 @@ # typed: strict # frozen_string_literal: true -require "cli/parser" +require "abstract_command" require "completions" module Homebrew - module_function + module Cmd + class CompletionsCmd < AbstractCommand + cmd_args do + description <<~EOS + Control whether Homebrew automatically links external tap shell completion files. + Read more at . - sig { returns(CLI::Parser) } - def completions_args - Homebrew::CLI::Parser.new do - description <<~EOS - Control whether Homebrew automatically links external tap shell completion files. - Read more at . + `brew completions` [`state`]: + Display the current state of Homebrew's completions. - `brew completions` [`state`]: - Display the current state of Homebrew's completions. + `brew completions` (`link`|`unlink`): + Link or unlink Homebrew's completions. + EOS - `brew completions` (`link`|`unlink`): - Link or unlink Homebrew's completions. - EOS - - named_args %w[state link unlink], max: 1 - end - end - - sig { void } - def completions - args = completions_args.parse - - case args.named.first - when nil, "state" - if Completions.link_completions? - puts "Completions are linked." - else - puts "Completions are not linked." + named_args %w[state link unlink], max: 1 + end + + sig { override.void } + def run + case args.named.first + when nil, "state" + if Completions.link_completions? + puts "Completions are linked." + else + puts "Completions are not linked." + end + when "link" + Completions.link! + puts "Completions are now linked." + when "unlink" + Completions.unlink! + puts "Completions are no longer linked." + else + raise UsageError, "unknown subcommand: #{args.named.first}" + end end - when "link" - Completions.link! - puts "Completions are now linked." - when "unlink" - Completions.unlink! - puts "Completions are no longer linked." - else - raise UsageError, "unknown subcommand: #{args.named.first}" end end end diff --git a/Library/Homebrew/cmd/config.rb b/Library/Homebrew/cmd/config.rb index d75bc5ce54..515139b7ee 100644 --- a/Library/Homebrew/cmd/config.rb +++ b/Library/Homebrew/cmd/config.rb @@ -1,28 +1,25 @@ # typed: strict # frozen_string_literal: true +require "abstract_command" require "system_config" -require "cli/parser" module Homebrew - module_function + module Cmd + class Config < AbstractCommand + cmd_args do + description <<~EOS + Show Homebrew and system configuration info useful for debugging. If you file + a bug report, you will be required to provide this information. + EOS - sig { returns(CLI::Parser) } - def config_args - Homebrew::CLI::Parser.new do - description <<~EOS - Show Homebrew and system configuration info useful for debugging. If you file - a bug report, you will be required to provide this information. - EOS + named_args :none + end - named_args :none + sig { override.void } + def run + SystemConfig.dump_verbose_config + end end end - - sig { void } - def config - config_args.parse - - SystemConfig.dump_verbose_config - end end diff --git a/Library/Homebrew/cmd/deps.rb b/Library/Homebrew/cmd/deps.rb index 2a664ebaa0..fee15efb43 100644 --- a/Library/Homebrew/cmd/deps.rb +++ b/Library/Homebrew/cmd/deps.rb @@ -1,340 +1,339 @@ # typed: true # frozen_string_literal: true +require "abstract_command" require "formula" -require "cli/parser" require "cask/caskroom" require "dependencies_helpers" module Homebrew - extend DependenciesHelpers + module Cmd + class Deps < AbstractCommand + include DependenciesHelpers + cmd_args do + description <<~EOS + Show dependencies for . When given multiple formula arguments, + show the intersection of dependencies for each formula. By default, `deps` + shows all required and recommended dependencies. - sig { returns(CLI::Parser) } - def self.deps_args - Homebrew::CLI::Parser.new do - description <<~EOS - Show dependencies for . When given multiple formula arguments, - show the intersection of dependencies for each formula. By default, `deps` - shows all required and recommended dependencies. + If any version of each formula argument is installed and no other options + are passed, this command displays their actual runtime dependencies (similar + to `brew linkage`), which may differ from the current versions' stated + dependencies if the installed versions are outdated. - If any version of each formula argument is installed and no other options - are passed, this command displays their actual runtime dependencies (similar - to `brew linkage`), which may differ from the current versions' stated - dependencies if the installed versions are outdated. + *Note:* `--missing` and `--skip-recommended` have precedence over `--include-*`. + EOS + switch "-n", "--topological", + description: "Sort dependencies in topological order." + switch "-1", "--direct", "--declared", "--1", + description: "Show only the direct dependencies declared in the formula." + switch "--union", + description: "Show the union of dependencies for multiple , instead of the intersection." + switch "--full-name", + description: "List dependencies by their full name." + switch "--include-build", + description: "Include `:build` dependencies for ." + switch "--include-optional", + description: "Include `:optional` dependencies for ." + switch "--include-test", + description: "Include `:test` dependencies for (non-recursive)." + switch "--skip-recommended", + description: "Skip `:recommended` dependencies for ." + switch "--include-requirements", + description: "Include requirements in addition to dependencies for ." + switch "--tree", + description: "Show dependencies as a tree. When given multiple formula arguments, " \ + "show individual trees for each formula." + switch "--graph", + description: "Show dependencies as a directed graph." + switch "--dot", + depends_on: "--graph", + description: "Show text-based graph description in DOT format." + switch "--annotate", + description: "Mark any build, test, implicit, optional, or recommended dependencies as " \ + "such in the output." + switch "--installed", + description: "List dependencies for formulae that are currently installed. If is " \ + "specified, list only its dependencies that are currently installed." + switch "--missing", + description: "Show only missing dependencies." + switch "--eval-all", + description: "Evaluate all available formulae and casks, whether installed or not, to list " \ + "their dependencies." + switch "--for-each", + description: "Switch into the mode used by the `--eval-all` option, but only list dependencies " \ + "for each provided , one formula per line. This is used for " \ + "debugging the `--installed`/`--eval-all` display mode." + switch "--HEAD", + description: "Show dependencies for HEAD version instead of stable version." + switch "--formula", "--formulae", + description: "Treat all named arguments as formulae." + switch "--cask", "--casks", + description: "Treat all named arguments as casks." - *Note:* `--missing` and `--skip-recommended` have precedence over `--include-*`. - EOS - switch "-n", "--topological", - description: "Sort dependencies in topological order." - switch "-1", "--direct", "--declared", "--1", - description: "Show only the direct dependencies declared in the formula." - switch "--union", - description: "Show the union of dependencies for multiple , instead of the intersection." - switch "--full-name", - description: "List dependencies by their full name." - switch "--include-build", - description: "Include `:build` dependencies for ." - switch "--include-optional", - description: "Include `:optional` dependencies for ." - switch "--include-test", - description: "Include `:test` dependencies for (non-recursive)." - switch "--skip-recommended", - description: "Skip `:recommended` dependencies for ." - switch "--include-requirements", - description: "Include requirements in addition to dependencies for ." - switch "--tree", - description: "Show dependencies as a tree. When given multiple formula arguments, " \ - "show individual trees for each formula." - switch "--graph", - description: "Show dependencies as a directed graph." - switch "--dot", - depends_on: "--graph", - description: "Show text-based graph description in DOT format." - switch "--annotate", - description: "Mark any build, test, implicit, optional, or recommended dependencies as " \ - "such in the output." - switch "--installed", - description: "List dependencies for formulae that are currently installed. If is " \ - "specified, list only its dependencies that are currently installed." - switch "--missing", - description: "Show only missing dependencies." - switch "--eval-all", - description: "Evaluate all available formulae and casks, whether installed or not, to list " \ - "their dependencies." - switch "--for-each", - description: "Switch into the mode used by the `--eval-all` option, but only list dependencies " \ - "for each provided , one formula per line. This is used for " \ - "debugging the `--installed`/`--eval-all` display mode." - switch "--HEAD", - description: "Show dependencies for HEAD version instead of stable version." - switch "--formula", "--formulae", - description: "Treat all named arguments as formulae." - switch "--cask", "--casks", - description: "Treat all named arguments as casks." + conflicts "--tree", "--graph" + conflicts "--installed", "--missing" + conflicts "--installed", "--eval-all" + conflicts "--formula", "--cask" + formula_options - conflicts "--tree", "--graph" - conflicts "--installed", "--missing" - conflicts "--installed", "--eval-all" - conflicts "--formula", "--cask" - formula_options + named_args [:formula, :cask] + end - named_args [:formula, :cask] - end - end + sig { override.void } + def run + all = args.eval_all? - def self.deps - args = deps_args.parse + Formulary.enable_factory_cache! - all = args.eval_all? + recursive = !args.direct? + installed = args.installed? || dependents(args.named.to_formulae_and_casks).all?(&:any_version_installed?) - Formulary.enable_factory_cache! + @use_runtime_dependencies = installed && recursive && + !args.tree? && + !args.graph? && + !args.HEAD? && + !args.include_build? && + !args.include_test? && + !args.include_optional? && + !args.skip_recommended? && + !args.missing? - recursive = !args.direct? - installed = args.installed? || dependents(args.named.to_formulae_and_casks).all?(&:any_version_installed?) + if args.tree? || args.graph? + dependents = if args.named.present? + sorted_dependents(args.named.to_formulae_and_casks) + elsif args.installed? + case args.only_formula_or_cask + when :formula + sorted_dependents(Formula.installed) + when :cask + sorted_dependents(Cask::Caskroom.casks) + else + sorted_dependents(Formula.installed + Cask::Caskroom.casks) + end + else + raise FormulaUnspecifiedError + end - @use_runtime_dependencies = installed && recursive && - !args.tree? && - !args.graph? && - !args.HEAD? && - !args.include_build? && - !args.include_test? && - !args.include_optional? && - !args.skip_recommended? && - !args.missing? + if args.graph? + dot_code = dot_code(dependents, recursive:) + if args.dot? + puts dot_code + else + exec_browser "https://dreampuf.github.io/GraphvizOnline/##{ERB::Util.url_encode(dot_code)}" + end + return + end - if args.tree? || args.graph? - dependents = if args.named.present? - sorted_dependents(args.named.to_formulae_and_casks) - elsif args.installed? - case args.only_formula_or_cask - when :formula - sorted_dependents(Formula.installed) - when :cask - sorted_dependents(Cask::Caskroom.casks) + puts_deps_tree(dependents, recursive:) + return + elsif all + puts_deps(sorted_dependents( + Formula.all(eval_all: args.eval_all?) + Cask::Cask.all(eval_all: args.eval_all?), + ), recursive:) + return + elsif !args.no_named? && args.for_each? + puts_deps(sorted_dependents(args.named.to_formulae_and_casks), recursive:) + return + end + + if args.no_named? + raise FormulaUnspecifiedError unless args.installed? + + sorted_dependents_formulae_and_casks = case args.only_formula_or_cask + when :formula + sorted_dependents(Formula.installed) + when :cask + sorted_dependents(Cask::Caskroom.casks) + else + sorted_dependents(Formula.installed + Cask::Caskroom.casks) + end + puts_deps(sorted_dependents_formulae_and_casks, recursive:) + return + end + + dependents = dependents(args.named.to_formulae_and_casks) + check_head_spec(dependents) if args.HEAD? + + all_deps = deps_for_dependents(dependents, recursive:, &(args.union? ? :| : :&)) + condense_requirements(all_deps) + all_deps.map! { |d| dep_display_name(d) } + all_deps.uniq! + all_deps.sort! unless args.topological? + puts all_deps + end + + private + + def sorted_dependents(formulae_or_casks) + dependents(formulae_or_casks).sort_by(&:name) + end + + def condense_requirements(deps) + deps.select! { |dep| dep.is_a?(Dependency) } unless args.include_requirements? + deps.select! { |dep| dep.is_a?(Requirement) || dep.installed? } if args.installed? + end + + def dep_display_name(dep) + str = if dep.is_a? Requirement + if args.include_requirements? + ":#{dep.display_s}" + else + # This shouldn't happen, but we'll put something here to help debugging + "::#{dep.name}" + end + elsif args.full_name? + dep.to_formula.full_name else - sorted_dependents(Formula.installed + Cask::Caskroom.casks) + dep.name end - else - raise FormulaUnspecifiedError + + if args.annotate? + str = "#{str} " if args.tree? + str = "#{str} [build]" if dep.build? + str = "#{str} [test]" if dep.test? + str = "#{str} [optional]" if dep.optional? + str = "#{str} [recommended]" if dep.recommended? + str = "#{str} [implicit]" if dep.implicit? + end + + str end - if args.graph? - dot_code = dot_code(dependents, recursive:, args:) - if args.dot? - puts dot_code + def deps_for_dependent(dependency, recursive: false) + includes, ignores = args_includes_ignores(args) + + deps = dependency.runtime_dependencies if @use_runtime_dependencies + + if recursive + deps ||= recursive_includes(Dependency, dependency, includes, ignores) + reqs = recursive_includes(Requirement, dependency, includes, ignores) else - exec_browser "https://dreampuf.github.io/GraphvizOnline/##{ERB::Util.url_encode(dot_code)}" + deps ||= select_includes(dependency.deps, ignores, includes) + reqs = select_includes(dependency.requirements, ignores, includes) end - return + + deps + reqs.to_a end - puts_deps_tree(dependents, recursive:, args:) - return - elsif all - puts_deps(sorted_dependents( - Formula.all(eval_all: args.eval_all?) + Cask::Cask.all(eval_all: args.eval_all?), - ), recursive:, args:) - return - elsif !args.no_named? && args.for_each? - puts_deps(sorted_dependents(args.named.to_formulae_and_casks), recursive:, args:) - return - end - - if args.no_named? - raise FormulaUnspecifiedError unless args.installed? - - sorted_dependents_formulae_and_casks = case args.only_formula_or_cask - when :formula - sorted_dependents(Formula.installed) - when :cask - sorted_dependents(Cask::Caskroom.casks) - else - sorted_dependents(Formula.installed + Cask::Caskroom.casks) + def deps_for_dependents(dependents, recursive: false, &block) + dependents.map { |d| deps_for_dependent(d, recursive:) }.reduce(&block) end - puts_deps(sorted_dependents_formulae_and_casks, recursive:, args:) - return - end - dependents = dependents(args.named.to_formulae_and_casks) - check_head_spec(dependents) if args.HEAD? - - all_deps = deps_for_dependents(dependents, recursive:, args:, &(args.union? ? :| : :&)) - condense_requirements(all_deps, args:) - all_deps.map! { |d| dep_display_name(d, args:) } - all_deps.uniq! - all_deps.sort! unless args.topological? - puts all_deps - end - - def self.sorted_dependents(formulae_or_casks) - dependents(formulae_or_casks).sort_by(&:name) - end - - def self.condense_requirements(deps, args:) - deps.select! { |dep| dep.is_a?(Dependency) } unless args.include_requirements? - deps.select! { |dep| dep.is_a?(Requirement) || dep.installed? } if args.installed? - end - - def self.dep_display_name(dep, args:) - str = if dep.is_a? Requirement - if args.include_requirements? - ":#{dep.display_s}" - else - # This shouldn't happen, but we'll put something here to help debugging - "::#{dep.name}" + def check_head_spec(dependents) + headless = dependents.select { |d| d.is_a?(Formula) && d.active_spec_sym != :head } + .to_sentence two_words_connector: " or ", last_word_connector: " or " + opoo "No head spec for #{headless}, using stable spec instead" unless headless.empty? end - elsif args.full_name? - dep.to_formula.full_name - else - dep.name - end - if args.annotate? - str = "#{str} " if args.tree? - str = "#{str} [build]" if dep.build? - str = "#{str} [test]" if dep.test? - str = "#{str} [optional]" if dep.optional? - str = "#{str} [recommended]" if dep.recommended? - str = "#{str} [implicit]" if dep.implicit? - end - - str - end - - def self.deps_for_dependent(dependency, args:, recursive: false) - includes, ignores = args_includes_ignores(args) - - deps = dependency.runtime_dependencies if @use_runtime_dependencies - - if recursive - deps ||= recursive_includes(Dependency, dependency, includes, ignores) - reqs = recursive_includes(Requirement, dependency, includes, ignores) - else - deps ||= select_includes(dependency.deps, ignores, includes) - reqs = select_includes(dependency.requirements, ignores, includes) - end - - deps + reqs.to_a - end - - def self.deps_for_dependents(dependents, args:, recursive: false, &block) - dependents.map { |d| deps_for_dependent(d, recursive:, args:) }.reduce(&block) - end - - def self.check_head_spec(dependents) - headless = dependents.select { |d| d.is_a?(Formula) && d.active_spec_sym != :head } - .to_sentence two_words_connector: " or ", last_word_connector: " or " - opoo "No head spec for #{headless}, using stable spec instead" unless headless.empty? - end - - def self.puts_deps(dependents, args:, recursive: false) - check_head_spec(dependents) if args.HEAD? - dependents.each do |dependent| - deps = deps_for_dependent(dependent, recursive:, args:) - condense_requirements(deps, args:) - deps.sort_by!(&:name) - deps.map! { |d| dep_display_name(d, args:) } - puts "#{dependent.full_name}: #{deps.join(" ")}" - end - end - - def self.dot_code(dependents, recursive:, args:) - dep_graph = {} - dependents.each do |d| - graph_deps(d, dep_graph:, recursive:, args:) - end - - dot_code = dep_graph.map do |d, deps| - deps.map do |dep| - attributes = [] - attributes << "style = dotted" if dep.build? - attributes << "arrowhead = empty" if dep.test? - if dep.optional? - attributes << "color = red" - elsif dep.recommended? - attributes << "color = green" + def puts_deps(dependents, recursive: false) + check_head_spec(dependents) if args.HEAD? + dependents.each do |dependent| + deps = deps_for_dependent(dependent, recursive:) + condense_requirements(deps) + deps.sort_by!(&:name) + deps.map! { |d| dep_display_name(d) } + puts "#{dependent.full_name}: #{deps.join(" ")}" end - comment = " # #{dep.tags.map(&:inspect).join(", ")}" if dep.tags.any? - " \"#{d.name}\" -> \"#{dep}\"#{" [#{attributes.join(", ")}]" if attributes.any?}#{comment}" end - end.flatten.join("\n") - "digraph {\n#{dot_code}\n}" - end - def self.graph_deps(formula, dep_graph:, recursive:, args:) - return if dep_graph.key?(formula) + def dot_code(dependents, recursive:) + dep_graph = {} + dependents.each do |d| + graph_deps(d, dep_graph:, recursive:) + end - dependables = dependables(formula, args:) - dep_graph[formula] = dependables - return unless recursive + dot_code = dep_graph.map do |d, deps| + deps.map do |dep| + attributes = [] + attributes << "style = dotted" if dep.build? + attributes << "arrowhead = empty" if dep.test? + if dep.optional? + attributes << "color = red" + elsif dep.recommended? + attributes << "color = green" + end + comment = " # #{dep.tags.map(&:inspect).join(", ")}" if dep.tags.any? + " \"#{d.name}\" -> \"#{dep}\"#{" [#{attributes.join(", ")}]" if attributes.any?}#{comment}" + end + end.flatten.join("\n") + "digraph {\n#{dot_code}\n}" + end - dependables.each do |dep| - next unless dep.is_a? Dependency + def graph_deps(formula, dep_graph:, recursive:) + return if dep_graph.key?(formula) - graph_deps(Formulary.factory(dep.name), - dep_graph:, - recursive: true, - args:) + dependables = dependables(formula) + dep_graph[formula] = dependables + return unless recursive + + dependables.each do |dep| + next unless dep.is_a? Dependency + + graph_deps(Formulary.factory(dep.name), + dep_graph:, + recursive: true) + end + end + + def puts_deps_tree(dependents, recursive: false) + check_head_spec(dependents) if args.HEAD? + dependents.each do |d| + puts d.full_name + recursive_deps_tree(d, dep_stack: [], prefix: "", recursive:) + puts + end + end + + def dependables(formula) + includes, ignores = args_includes_ignores(args) + deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps + deps = select_includes(deps, ignores, includes) + reqs = select_includes(formula.requirements, ignores, includes) if args.include_requirements? + reqs ||= [] + reqs + deps + end + + def recursive_deps_tree(formula, dep_stack:, prefix:, recursive:) + dependables = dependables(formula) + max = dependables.length - 1 + dep_stack.push formula.name + dependables.each_with_index do |dep, i| + tree_lines = if i == max + "└──" + else + "├──" + end + + display_s = "#{tree_lines} #{dep_display_name(dep)}" + + # Detect circular dependencies and consider them a failure if present. + is_circular = dep_stack.include?(dep.name) + if is_circular + display_s = "#{display_s} (CIRCULAR DEPENDENCY)" + Homebrew.failed = true + end + + puts "#{prefix}#{display_s}" + + next if !recursive || is_circular + + prefix_addition = if i == max + " " + else + "│ " + end + + next unless dep.is_a? Dependency + + recursive_deps_tree(Formulary.factory(dep.name), + dep_stack:, + prefix: prefix + prefix_addition, + recursive: true) + end + + dep_stack.pop + end end end - - def self.puts_deps_tree(dependents, args:, recursive: false) - check_head_spec(dependents) if args.HEAD? - dependents.each do |d| - puts d.full_name - recursive_deps_tree(d, dep_stack: [], prefix: "", recursive:, args:) - puts - end - end - - def self.dependables(formula, args:) - includes, ignores = args_includes_ignores(args) - deps = @use_runtime_dependencies ? formula.runtime_dependencies : formula.deps - deps = select_includes(deps, ignores, includes) - reqs = select_includes(formula.requirements, ignores, includes) if args.include_requirements? - reqs ||= [] - reqs + deps - end - - def self.recursive_deps_tree(formula, dep_stack:, prefix:, recursive:, args:) - dependables = dependables(formula, args:) - max = dependables.length - 1 - dep_stack.push formula.name - dependables.each_with_index do |dep, i| - tree_lines = if i == max - "└──" - else - "├──" - end - - display_s = "#{tree_lines} #{dep_display_name(dep, args:)}" - - # Detect circular dependencies and consider them a failure if present. - is_circular = dep_stack.include?(dep.name) - if is_circular - display_s = "#{display_s} (CIRCULAR DEPENDENCY)" - Homebrew.failed = true - end - - puts "#{prefix}#{display_s}" - - next if !recursive || is_circular - - prefix_addition = if i == max - " " - else - "│ " - end - - next unless dep.is_a? Dependency - - recursive_deps_tree(Formulary.factory(dep.name), - dep_stack:, - prefix: prefix + prefix_addition, - recursive: true, - args:) - end - - dep_stack.pop - end end diff --git a/Library/Homebrew/cmd/desc.rb b/Library/Homebrew/cmd/desc.rb index aa43dc61df..7e963e21cb 100644 --- a/Library/Homebrew/cmd/desc.rb +++ b/Library/Homebrew/cmd/desc.rb @@ -1,74 +1,75 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "abstract_command" require "descriptions" require "search" require "description_cache_store" -require "cli/parser" module Homebrew - module_function + module Cmd + class Desc < AbstractCommand + cmd_args do + description <<~EOS + Display 's name and one-line description. + The cache is created on the first search, making that search slower than subsequent ones. + EOS + switch "-s", "--search", + description: "Search both names and descriptions for . If is flanked by " \ + "slashes, it is interpreted as a regular expression." + switch "-n", "--name", + description: "Search just names for . If is flanked by slashes, it is " \ + "interpreted as a regular expression." + switch "-d", "--description", + description: "Search just descriptions for . If is flanked by slashes, " \ + "it is interpreted as a regular expression." + switch "--eval-all", + description: "Evaluate all available formulae and casks, whether installed or not, to search their " \ + "descriptions. Implied if `HOMEBREW_EVAL_ALL` is set." + switch "--formula", "--formulae", + description: "Treat all named arguments as formulae." + switch "--cask", "--casks", + description: "Treat all named arguments as casks." - sig { returns(CLI::Parser) } - def desc_args - Homebrew::CLI::Parser.new do - description <<~EOS - Display 's name and one-line description. - The cache is created on the first search, making that search slower than subsequent ones. - EOS - switch "-s", "--search", - description: "Search both names and descriptions for . If is flanked by " \ - "slashes, it is interpreted as a regular expression." - switch "-n", "--name", - description: "Search just names for . If is flanked by slashes, it is " \ - "interpreted as a regular expression." - switch "-d", "--description", - description: "Search just descriptions for . If is flanked by slashes, " \ - "it is interpreted as a regular expression." - switch "--eval-all", - description: "Evaluate all available formulae and casks, whether installed or not, to search their " \ - "descriptions. Implied if `HOMEBREW_EVAL_ALL` is set." - switch "--formula", "--formulae", - description: "Treat all named arguments as formulae." - switch "--cask", "--casks", - description: "Treat all named arguments as casks." + conflicts "--search", "--name", "--description" - conflicts "--search", "--name", "--description" + named_args [:formula, :cask, :text_or_regex], min: 1 + end - named_args [:formula, :cask, :text_or_regex], min: 1 - end - end + sig { override.void } + def run + if !args.eval_all? && !Homebrew::EnvConfig.eval_all? + raise UsageError, "`brew desc` needs `--eval-all` passed or `HOMEBREW_EVAL_ALL` set!" + end - def desc - args = desc_args.parse + search_type = if args.search? + :either + elsif args.name? + :name + elsif args.description? + :desc + end - if !args.eval_all? && !Homebrew::EnvConfig.eval_all? - raise UsageError, "`brew desc` needs `--eval-all` passed or `HOMEBREW_EVAL_ALL` set!" - end - - search_type = if args.search? - :either - elsif args.name? - :name - elsif args.description? - :desc - end - - if search_type.blank? - desc = {} - args.named.to_formulae_and_casks.each do |formula_or_cask| - if formula_or_cask.is_a? Formula - desc[formula_or_cask.full_name] = formula_or_cask.desc + if search_type.blank? + desc = {} + args.named.to_formulae_and_casks.each do |formula_or_cask| + case formula_or_cask + when Formula + desc[formula_or_cask.full_name] = formula_or_cask.desc + when Cask::Cask + description = formula_or_cask.desc.presence || Formatter.warning("[no description]") + desc[formula_or_cask.full_name] = "(#{formula_or_cask.name.join(", ")}) #{description}" + else + raise TypeError, "Unsupported formula_or_cask type: #{formula_or_cask.class}" + end + end + Descriptions.new(desc).print else - description = formula_or_cask.desc.presence || Formatter.warning("[no description]") - desc[formula_or_cask.full_name] = "(#{formula_or_cask.name.join(", ")}) #{description}" + query = args.named.join(" ") + string_or_regex = Search.query_regexp(query) + Search.search_descriptions(string_or_regex, args, search_type:) end end - Descriptions.new(desc).print - else - query = args.named.join(" ") - string_or_regex = Search.query_regexp(query) - Search.search_descriptions(string_or_regex, args, search_type:) end end end diff --git a/Library/Homebrew/cmd/developer.rb b/Library/Homebrew/cmd/developer.rb index 9f3ebc30ea..f3f43c41e6 100644 --- a/Library/Homebrew/cmd/developer.rb +++ b/Library/Homebrew/cmd/developer.rb @@ -1,56 +1,55 @@ -# typed: true +# typed: strict # frozen_string_literal: true -require "cli/parser" +require "abstract_command" module Homebrew - module_function + module Cmd + class Developer < AbstractCommand + cmd_args do + description <<~EOS + Control Homebrew's developer mode. When developer mode is enabled, + `brew update` will update Homebrew to the latest commit on the `master` + branch instead of the latest stable version along with some other behaviour changes. - sig { returns(CLI::Parser) } - def developer_args - Homebrew::CLI::Parser.new do - description <<~EOS - Control Homebrew's developer mode. When developer mode is enabled, - `brew update` will update Homebrew to the latest commit on the `master` - branch instead of the latest stable version along with some other behaviour changes. + `brew developer` [`state`]: + Display the current state of Homebrew's developer mode. - `brew developer` [`state`]: - Display the current state of Homebrew's developer mode. + `brew developer` (`on`|`off`): + Turn Homebrew's developer mode on or off respectively. + EOS - `brew developer` (`on`|`off`): - Turn Homebrew's developer mode on or off respectively. - EOS - - named_args %w[state on off], max: 1 - end - end - - def developer - args = developer_args.parse - - env_vars = [] - env_vars << "HOMEBREW_DEVELOPER" if Homebrew::EnvConfig.developer? - env_vars << "HOMEBREW_UPDATE_TO_TAG" if Homebrew::EnvConfig.update_to_tag? - env_vars.map! do |var| - "#{Tty.bold}#{var}#{Tty.reset}" - end - - case args.named.first - when nil, "state" - if env_vars.any? - puts "Developer mode is enabled because #{env_vars.to_sentence} #{(env_vars.count == 1) ? "is" : "are"} set." - elsif Homebrew::Settings.read("devcmdrun") == "true" - puts "Developer mode is enabled." - else - puts "Developer mode is disabled." + named_args %w[state on off], max: 1 + end + + sig { override.void } + def run + env_vars = [] + env_vars << "HOMEBREW_DEVELOPER" if Homebrew::EnvConfig.developer? + env_vars << "HOMEBREW_UPDATE_TO_TAG" if Homebrew::EnvConfig.update_to_tag? + env_vars.map! do |var| + "#{Tty.bold}#{var}#{Tty.reset}" + end + + case args.named.first + when nil, "state" + if env_vars.any? + verb = (env_vars.count == 1) ? "is" : "are" + puts "Developer mode is enabled because #{env_vars.to_sentence} #{verb} set." + elsif Homebrew::Settings.read("devcmdrun") == "true" + puts "Developer mode is enabled." + else + puts "Developer mode is disabled." + end + when "on" + Homebrew::Settings.write "devcmdrun", true + when "off" + Homebrew::Settings.delete "devcmdrun" + puts "To fully disable developer mode, you must unset #{env_vars.to_sentence}." if env_vars.any? + else + raise UsageError, "unknown subcommand: #{args.named.first}" + end end - when "on" - Homebrew::Settings.write "devcmdrun", true - when "off" - Homebrew::Settings.delete "devcmdrun" - puts "To fully disable developer mode, you must unset #{env_vars.to_sentence}." if env_vars.any? - else - raise UsageError, "unknown subcommand: #{args.named.first}" end end end diff --git a/Library/Homebrew/cmd/docs.rb b/Library/Homebrew/cmd/docs.rb index 2ac9ef42af..9d5cedac92 100644 --- a/Library/Homebrew/cmd/docs.rb +++ b/Library/Homebrew/cmd/docs.rb @@ -1,22 +1,21 @@ # typed: strict # frozen_string_literal: true -require "cli/parser" +require "abstract_command" module Homebrew - module_function + module Cmd + class Docs < AbstractCommand + cmd_args do + description <<~EOS + Open Homebrew's online documentation at <#{HOMEBREW_DOCS_WWW}> in a browser. + EOS + end - sig { returns(CLI::Parser) } - def docs_args - Homebrew::CLI::Parser.new do - description <<~EOS - Open Homebrew's online documentation at <#{HOMEBREW_DOCS_WWW}> in a browser. - EOS + sig { override.void } + def run + exec_browser HOMEBREW_DOCS_WWW + end end end - - sig { void } - def docs - exec_browser HOMEBREW_DOCS_WWW - end end diff --git a/Library/Homebrew/cmd/doctor.rb b/Library/Homebrew/cmd/doctor.rb index d3677fc624..a273921d8e 100644 --- a/Library/Homebrew/cmd/doctor.rb +++ b/Library/Homebrew/cmd/doctor.rb @@ -1,80 +1,80 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "abstract_command" require "diagnostic" -require "cli/parser" require "cask/caskroom" module Homebrew - sig { returns(CLI::Parser) } - def self.doctor_args - Homebrew::CLI::Parser.new do - description <<~EOS - Check your system for potential problems. Will exit with a non-zero status - if any potential problems are found. + module Cmd + class Doctor < AbstractCommand + cmd_args do + description <<~EOS + Check your system for potential problems. Will exit with a non-zero status + if any potential problems are found. - Please note that these warnings are just used to help the Homebrew maintainers - with debugging if you file an issue. If everything you use Homebrew for - is working fine: please don't worry or file an issue; just ignore this. - EOS - switch "--list-checks", - description: "List all audit methods, which can be run individually " \ - "if provided as arguments." - switch "-D", "--audit-debug", - description: "Enable debugging and profiling of audit methods." - - named_args :diagnostic_check - end - end - - def self.doctor - args = doctor_args.parse - - inject_dump_stats!(Diagnostic::Checks, /^check_*/) if args.audit_debug? - - checks = Diagnostic::Checks.new(verbose: args.verbose?) - - if args.list_checks? - puts checks.all - return - end - - if args.no_named? - slow_checks = %w[ - check_for_broken_symlinks - check_missing_deps - ] - methods = (checks.all - slow_checks) + slow_checks - methods -= checks.cask_checks unless Cask::Caskroom.any_casks_installed? - else - methods = args.named - end - - first_warning = T.let(true, T::Boolean) - methods.each do |method| - $stderr.puts Formatter.headline("Checking #{method}", color: :magenta) if args.debug? - unless checks.respond_to?(method) - ofail "No check available by the name: #{method}" - next - end - - out = checks.send(method) - next if out.blank? - - if first_warning - $stderr.puts <<~EOS - #{Tty.bold}Please note that these warnings are just used to help the Homebrew maintainers - with debugging if you file an issue. If everything you use Homebrew for is - working fine: please don't worry or file an issue; just ignore this. Thanks!#{Tty.reset} + Please note that these warnings are just used to help the Homebrew maintainers + with debugging if you file an issue. If everything you use Homebrew for + is working fine: please don't worry or file an issue; just ignore this. EOS + switch "--list-checks", + description: "List all audit methods, which can be run individually " \ + "if provided as arguments." + switch "-D", "--audit-debug", + description: "Enable debugging and profiling of audit methods." + + named_args :diagnostic_check end - $stderr.puts - opoo out - Homebrew.failed = true - first_warning = false - end + sig { override.void } + def run + Homebrew.inject_dump_stats!(Diagnostic::Checks, /^check_*/) if args.audit_debug? - puts "Your system is ready to brew." if !Homebrew.failed? && !args.quiet? + checks = Diagnostic::Checks.new(verbose: args.verbose?) + + if args.list_checks? + puts checks.all + return + end + + if args.no_named? + slow_checks = %w[ + check_for_broken_symlinks + check_missing_deps + ] + methods = (checks.all - slow_checks) + slow_checks + methods -= checks.cask_checks unless Cask::Caskroom.any_casks_installed? + else + methods = args.named + end + + first_warning = T.let(true, T::Boolean) + methods.each do |method| + $stderr.puts Formatter.headline("Checking #{method}", color: :magenta) if args.debug? + unless checks.respond_to?(method) + ofail "No check available by the name: #{method}" + next + end + + out = checks.send(method) + next if out.blank? + + if first_warning + $stderr.puts <<~EOS + #{Tty.bold}Please note that these warnings are just used to help the Homebrew maintainers + with debugging if you file an issue. If everything you use Homebrew for is + working fine: please don't worry or file an issue; just ignore this. Thanks!#{Tty.reset} + EOS + end + + $stderr.puts + opoo out + Homebrew.failed = true + first_warning = false + end + + puts "Your system is ready to brew." if !Homebrew.failed? && !args.quiet? + end + end end end diff --git a/Library/Homebrew/cmd/fetch.rb b/Library/Homebrew/cmd/fetch.rb index 028cc3bc49..168c671d4d 100644 --- a/Library/Homebrew/cmd/fetch.rb +++ b/Library/Homebrew/cmd/fetch.rb @@ -1,256 +1,257 @@ # typed: true # frozen_string_literal: true +require "abstract_command" require "formula" require "fetch" -require "cli/parser" require "cask/download" module Homebrew - extend Fetch + module Cmd + class FetchCmd < AbstractCommand + include Fetch + FETCH_MAX_TRIES = 5 - FETCH_MAX_TRIES = 5 + cmd_args do + description <<~EOS + Download a bottle (if available) or source packages for e + and binaries for s. For files, also print SHA-256 checksums. + EOS + flag "--os=", + description: "Download for the given operating system. " \ + "(Pass `all` to download for all operating systems.)" + flag "--arch=", + description: "Download for the given CPU architecture. " \ + "(Pass `all` to download for all architectures.)" + flag "--bottle-tag=", + description: "Download a bottle for given tag." + switch "--HEAD", + description: "Fetch HEAD version instead of stable version." + switch "-f", "--force", + description: "Remove a previously cached version and re-fetch." + switch "-v", "--verbose", + description: "Do a verbose VCS checkout, if the URL represents a VCS. This is useful for " \ + "seeing if an existing VCS cache has been updated." + switch "--retry", + description: "Retry if downloading fails or re-download if the checksum of a previously cached " \ + "version no longer matches. Tries at most #{FETCH_MAX_TRIES} times with " \ + "exponential backoff." + switch "--deps", + description: "Also download dependencies for any listed ." + switch "-s", "--build-from-source", + description: "Download source packages rather than a bottle." + switch "--build-bottle", + description: "Download source packages (for eventual bottling) rather than a bottle." + switch "--force-bottle", + description: "Download a bottle if it exists for the current or newest version of macOS, " \ + "even if it would not be used during installation." + switch "--[no-]quarantine", + description: "Disable/enable quarantining of downloads (default: enabled).", + env: :cask_opts_quarantine + switch "--formula", "--formulae", + description: "Treat all named arguments as formulae." + switch "--cask", "--casks", + description: "Treat all named arguments as casks." - sig { returns(CLI::Parser) } - def self.fetch_args - Homebrew::CLI::Parser.new do - description <<~EOS - Download a bottle (if available) or source packages for e - and binaries for s. For files, also print SHA-256 checksums. - EOS - flag "--os=", - description: "Download for the given operating system. " \ - "(Pass `all` to download for all operating systems.)" - flag "--arch=", - description: "Download for the given CPU architecture. " \ - "(Pass `all` to download for all architectures.)" - flag "--bottle-tag=", - description: "Download a bottle for given tag." - switch "--HEAD", - description: "Fetch HEAD version instead of stable version." - switch "-f", "--force", - description: "Remove a previously cached version and re-fetch." - switch "-v", "--verbose", - description: "Do a verbose VCS checkout, if the URL represents a VCS. This is useful for " \ - "seeing if an existing VCS cache has been updated." - switch "--retry", - description: "Retry if downloading fails or re-download if the checksum of a previously cached " \ - "version no longer matches. Tries at most #{FETCH_MAX_TRIES} times with " \ - "exponential backoff." - switch "--deps", - description: "Also download dependencies for any listed ." - switch "-s", "--build-from-source", - description: "Download source packages rather than a bottle." - switch "--build-bottle", - description: "Download source packages (for eventual bottling) rather than a bottle." - switch "--force-bottle", - description: "Download a bottle if it exists for the current or newest version of macOS, " \ - "even if it would not be used during installation." - switch "--[no-]quarantine", - description: "Disable/enable quarantining of downloads (default: enabled).", - env: :cask_opts_quarantine - switch "--formula", "--formulae", - description: "Treat all named arguments as formulae." - switch "--cask", "--casks", - description: "Treat all named arguments as casks." + conflicts "--build-from-source", "--build-bottle", "--force-bottle", "--bottle-tag" + conflicts "--cask", "--HEAD" + conflicts "--cask", "--deps" + conflicts "--cask", "-s" + conflicts "--cask", "--build-bottle" + conflicts "--cask", "--force-bottle" + conflicts "--cask", "--bottle-tag" + conflicts "--formula", "--cask" + conflicts "--os", "--bottle-tag" + conflicts "--arch", "--bottle-tag" - conflicts "--build-from-source", "--build-bottle", "--force-bottle", "--bottle-tag" - conflicts "--cask", "--HEAD" - conflicts "--cask", "--deps" - conflicts "--cask", "-s" - conflicts "--cask", "--build-bottle" - conflicts "--cask", "--force-bottle" - conflicts "--cask", "--bottle-tag" - conflicts "--formula", "--cask" - conflicts "--os", "--bottle-tag" - conflicts "--arch", "--bottle-tag" - - named_args [:formula, :cask], min: 1 - end - end - - def self.fetch - args = fetch_args.parse - - Formulary.enable_factory_cache! - - bucket = if args.deps? - args.named.to_formulae_and_casks.flat_map do |formula_or_cask| - case formula_or_cask - when Formula - formula = formula_or_cask - [formula, *formula.recursive_dependencies.map(&:to_formula)] - else - formula_or_cask - end + named_args [:formula, :cask], min: 1 end - else - args.named.to_formulae_and_casks - end.uniq - os_arch_combinations = args.os_arch_combinations + sig { override.void } + def run + Formulary.enable_factory_cache! - puts "Fetching: #{bucket * ", "}" if bucket.size > 1 - bucket.each do |formula_or_cask| - case formula_or_cask - when Formula - formula = T.cast(formula_or_cask, Formula) - ref = formula.loaded_from_api? ? formula.full_name : formula.path + bucket = if args.deps? + args.named.to_formulae_and_casks.flat_map do |formula_or_cask| + case formula_or_cask + when Formula + formula = formula_or_cask + [formula, *formula.recursive_dependencies.map(&:to_formula)] + else + formula_or_cask + end + end + else + args.named.to_formulae_and_casks + end.uniq - os_arch_combinations.each do |os, arch| - SimulateSystem.with(os:, arch:) do - formula = Formulary.factory(ref, args.HEAD? ? :head : :stable) + os_arch_combinations = args.os_arch_combinations - formula.print_tap_action verb: "Fetching" + puts "Fetching: #{bucket * ", "}" if bucket.size > 1 + bucket.each do |formula_or_cask| + case formula_or_cask + when Formula + formula = T.cast(formula_or_cask, Formula) + ref = formula.loaded_from_api? ? formula.full_name : formula.path - fetched_bottle = false - if fetch_bottle?( - formula, - force_bottle: args.force_bottle?, - bottle_tag: args.bottle_tag&.to_sym, - build_from_source_formulae: args.build_from_source_formulae, - os: args.os&.to_sym, - arch: args.arch&.to_sym, - ) - begin - formula.clear_cache if args.force? + os_arch_combinations.each do |os, arch| + SimulateSystem.with(os:, arch:) do + formula = Formulary.factory(ref, args.HEAD? ? :head : :stable) - bottle_tag = if (bottle_tag = args.bottle_tag&.to_sym) - Utils::Bottles::Tag.from_symbol(bottle_tag) - else - Utils::Bottles::Tag.new(system: os, arch:) + formula.print_tap_action verb: "Fetching" + + fetched_bottle = false + if fetch_bottle?( + formula, + force_bottle: args.force_bottle?, + bottle_tag: args.bottle_tag&.to_sym, + build_from_source_formulae: args.build_from_source_formulae, + os: args.os&.to_sym, + arch: args.arch&.to_sym, + ) + begin + formula.clear_cache if args.force? + + bottle_tag = if (bottle_tag = args.bottle_tag&.to_sym) + Utils::Bottles::Tag.from_symbol(bottle_tag) + else + Utils::Bottles::Tag.new(system: os, arch:) + end + + bottle = formula.bottle_for_tag(bottle_tag) + + if bottle.nil? + opoo "Bottle for tag #{bottle_tag.to_sym.inspect} is unavailable." + next + end + + begin + bottle.fetch_tab + rescue DownloadError + retry if retry_fetch?(bottle) + raise + end + fetch_formula(bottle) + rescue Interrupt + raise + rescue => e + raise if Homebrew::EnvConfig.developer? + + fetched_bottle = false + onoe e.message + opoo "Bottle fetch failed, fetching the source instead." + else + fetched_bottle = true + end end - bottle = formula.bottle_for_tag(bottle_tag) + next if fetched_bottle - if bottle.nil? - opoo "Bottle for tag #{bottle_tag.to_sym.inspect} is unavailable." + fetch_formula(formula) + + formula.resources.each do |r| + fetch_resource(r) + r.patches.each { |p| fetch_patch(p) if p.external? } + end + + formula.patchlist.each { |p| fetch_patch(p) if p.external? } + end + end + else + cask = formula_or_cask + ref = cask.loaded_from_api? ? cask.full_name : cask.sourcefile_path + + os_arch_combinations.each do |os, arch| + next if os == :linux + + SimulateSystem.with(os:, arch:) do + cask = Cask::CaskLoader.load(ref) + + if cask.url.nil? || cask.sha256.nil? + opoo "Cask #{cask} is not supported on os #{os} and arch #{arch}" next end - begin - bottle.fetch_tab - rescue DownloadError - retry if retry_fetch?(bottle, args:) - raise - end - fetch_formula(bottle, args:) - rescue Interrupt - raise - rescue => e - raise if Homebrew::EnvConfig.developer? + quarantine = args.quarantine? + quarantine = true if quarantine.nil? - fetched_bottle = false - onoe e.message - opoo "Bottle fetch failed, fetching the source instead." - else - fetched_bottle = true + download = Cask::Download.new(cask, quarantine:) + fetch_cask(download) end end - - next if fetched_bottle - - fetch_formula(formula, args:) - - formula.resources.each do |r| - fetch_resource(r, args:) - r.patches.each { |p| fetch_patch(p, args:) if p.external? } - end - - formula.patchlist.each { |p| fetch_patch(p, args:) if p.external? } - end - end - else - cask = formula_or_cask - ref = cask.loaded_from_api? ? cask.full_name : cask.sourcefile_path - - os_arch_combinations.each do |os, arch| - next if os == :linux - - SimulateSystem.with(os:, arch:) do - cask = Cask::CaskLoader.load(ref) - - if cask.url.nil? || cask.sha256.nil? - opoo "Cask #{cask} is not supported on os #{os} and arch #{arch}" - next - end - - quarantine = args.quarantine? - quarantine = true if quarantine.nil? - - download = Cask::Download.new(cask, quarantine:) - fetch_cask(download, args:) end end end + + private + + def fetch_resource(resource) + puts "Resource: #{resource.name}" + fetch_fetchable resource + rescue ChecksumMismatchError => e + retry if retry_fetch?(resource) + opoo "Resource #{resource.name} reports different sha256: #{e.expected}" + end + + def fetch_formula(formula) + fetch_fetchable(formula) + rescue ChecksumMismatchError => e + retry if retry_fetch?(formula) + opoo "Formula reports different sha256: #{e.expected}" + end + + def fetch_cask(cask_download) + fetch_fetchable(cask_download) + rescue ChecksumMismatchError => e + retry if retry_fetch?(cask_download) + opoo "Cask reports different sha256: #{e.expected}" + end + + def fetch_patch(patch) + fetch_fetchable(patch) + rescue ChecksumMismatchError => e + opoo "Patch reports different sha256: #{e.expected}" + Homebrew.failed = true + end + + def retry_fetch?(formula) + @fetch_tries ||= Hash.new { |h, k| h[k] = 1 } + if args.retry? && (@fetch_tries[formula] < FETCH_MAX_TRIES) + wait = 2 ** @fetch_tries[formula] + remaining = FETCH_MAX_TRIES - @fetch_tries[formula] + what = Utils.pluralize("tr", remaining, plural: "ies", singular: "y") + + ohai "Retrying download in #{wait}s... (#{remaining} #{what} left)" + sleep wait + + formula.clear_cache + @fetch_tries[formula] += 1 + true + else + Homebrew.failed = true + false + end + end + + def fetch_fetchable(formula) + formula.clear_cache if args.force? + + already_fetched = formula.cached_download.exist? + + begin + download = formula.fetch(verify_download_integrity: false) + rescue DownloadError + retry if retry_fetch?(formula) + raise + end + + return unless download.file? + + puts "Downloaded to: #{download}" unless already_fetched + puts "SHA256: #{download.sha256}" + + formula.verify_download_integrity(download) + end end end - - def self.fetch_resource(resource, args:) - puts "Resource: #{resource.name}" - fetch_fetchable resource, args: - rescue ChecksumMismatchError => e - retry if retry_fetch?(resource, args:) - opoo "Resource #{resource.name} reports different sha256: #{e.expected}" - end - - def self.fetch_formula(formula, args:) - fetch_fetchable(formula, args:) - rescue ChecksumMismatchError => e - retry if retry_fetch?(formula, args:) - opoo "Formula reports different sha256: #{e.expected}" - end - - def self.fetch_cask(cask_download, args:) - fetch_fetchable(cask_download, args:) - rescue ChecksumMismatchError => e - retry if retry_fetch?(cask_download, args:) - opoo "Cask reports different sha256: #{e.expected}" - end - - def self.fetch_patch(patch, args:) - fetch_fetchable(patch, args:) - rescue ChecksumMismatchError => e - opoo "Patch reports different sha256: #{e.expected}" - Homebrew.failed = true - end - - def self.retry_fetch?(formula, args:) - @fetch_tries ||= Hash.new { |h, k| h[k] = 1 } - if args.retry? && (@fetch_tries[formula] < FETCH_MAX_TRIES) - wait = 2 ** @fetch_tries[formula] - remaining = FETCH_MAX_TRIES - @fetch_tries[formula] - what = Utils.pluralize("tr", remaining, plural: "ies", singular: "y") - - ohai "Retrying download in #{wait}s... (#{remaining} #{what} left)" - sleep wait - - formula.clear_cache - @fetch_tries[formula] += 1 - true - else - Homebrew.failed = true - false - end - end - - def self.fetch_fetchable(formula, args:) - formula.clear_cache if args.force? - - already_fetched = formula.cached_download.exist? - - begin - download = formula.fetch(verify_download_integrity: false) - rescue DownloadError - retry if retry_fetch?(formula, args:) - raise - end - - return unless download.file? - - puts "Downloaded to: #{download}" unless already_fetched - puts "SHA256: #{download.sha256}" - - formula.verify_download_integrity(download) - end end diff --git a/Library/Homebrew/cmd/gist-logs.rb b/Library/Homebrew/cmd/gist-logs.rb index f18e99d67b..8ab830d8ca 100644 --- a/Library/Homebrew/cmd/gist-logs.rb +++ b/Library/Homebrew/cmd/gist-logs.rb @@ -1,130 +1,132 @@ # typed: true # frozen_string_literal: true +require "abstract_command" require "formula" require "install" require "system_config" require "stringio" require "socket" -require "cli/parser" module Homebrew - extend Install + module Cmd + class GistLogs < AbstractCommand + include Install + cmd_args do + description <<~EOS + Upload logs for a failed build of to a new Gist. Presents an + error message if no logs are found. + EOS + switch "--with-hostname", + description: "Include the hostname in the Gist." + switch "-n", "--new-issue", + description: "Automatically create a new issue in the appropriate GitHub repository " \ + "after creating the Gist." + switch "-p", "--private", + description: "The Gist will be marked private and will not appear in listings but will " \ + "be accessible with its link." - module_function + named_args :formula, number: 1 + end - sig { returns(CLI::Parser) } - def gist_logs_args - Homebrew::CLI::Parser.new do - description <<~EOS - Upload logs for a failed build of to a new Gist. Presents an - error message if no logs are found. - EOS - switch "--with-hostname", - description: "Include the hostname in the Gist." - switch "-n", "--new-issue", - description: "Automatically create a new issue in the appropriate GitHub repository " \ - "after creating the Gist." - switch "-p", "--private", - description: "The Gist will be marked private and will not appear in listings but will " \ - "be accessible with its link." + sig { override.void } + def run + Install.perform_preinstall_checks(all_fatal: true) + Install.perform_build_from_source_checks(all_fatal: true) + gistify_logs(args.named.to_resolved_formulae.first) + end - named_args :formula, number: 1 - end - end + private - def gistify_logs(formula, args:) - files = load_logs(formula.logs) - build_time = formula.logs.ctime - timestamp = build_time.strftime("%Y-%m-%d_%H-%M-%S") + def gistify_logs(formula) + files = load_logs(formula.logs) + build_time = formula.logs.ctime + timestamp = build_time.strftime("%Y-%m-%d_%H-%M-%S") - s = StringIO.new - SystemConfig.dump_verbose_config s - # Dummy summary file, asciibetically first, to control display title of gist - files["# #{formula.name} - #{timestamp}.txt"] = { - content: brief_build_info(formula, with_hostname: args.with_hostname?), - } - files["00.config.out"] = { content: s.string } - files["00.doctor.out"] = { content: Utils.popen_read("#{HOMEBREW_PREFIX}/bin/brew", "doctor", err: :out) } - unless formula.core_formula? - tap = <<~EOS - Formula: #{formula.name} - Tap: #{formula.tap} - Path: #{formula.path} - EOS - files["00.tap.out"] = { content: tap } - end - - odie "`brew gist-logs` requires HOMEBREW_GITHUB_API_TOKEN to be set!" if GitHub::API.credentials_type == :none - - # Description formatted to work well as page title when viewing gist - descr = if formula.core_formula? - "#{formula.name} on #{OS_VERSION} - Homebrew build logs" - else - "#{formula.name} (#{formula.full_name}) on #{OS_VERSION} - Homebrew build logs" - end - - begin - url = GitHub.create_gist(files, descr, private: args.private?) - rescue GitHub::API::HTTPNotFoundError - odie <<~EOS - Your GitHub API token likely doesn't have the `gist` scope. - #{GitHub.pat_blurb(GitHub::CREATE_GIST_SCOPES)} - EOS - end - - url = GitHub.create_issue(formula.tap, "#{formula.name} failed to build on #{OS_VERSION}", url) if args.new_issue? - - puts url if url - end - - def brief_build_info(formula, with_hostname:) - build_time_string = formula.logs.ctime.strftime("%Y-%m-%d %H:%M:%S") - string = +<<~EOS - Homebrew build logs for #{formula.full_name} on #{OS_VERSION} - EOS - if with_hostname - hostname = Socket.gethostname - string << "Host: #{hostname}\n" - end - string << "Build date: #{build_time_string}\n" - string.freeze - end - - # Causes some terminals to display secure password entry indicators. - def noecho_gets - system "stty", "-echo" - result = $stdin.gets - system "stty", "echo" - puts - result - end - - def load_logs(dir, basedir = dir) - logs = {} - if dir.exist? - dir.children.sort.each do |file| - if file.directory? - logs.merge! load_logs(file, basedir) - else - contents = file.size? ? file.read : "empty log" - # small enough to avoid GitHub "unicorn" page-load-timeout errors - max_file_size = 1_000_000 - contents = truncate_text_to_approximate_size(contents, max_file_size, front_weight: 0.2) - logs[file.relative_path_from(basedir).to_s.tr("/", ":")] = { content: contents } + s = StringIO.new + SystemConfig.dump_verbose_config s + # Dummy summary file, asciibetically first, to control display title of gist + files["# #{formula.name} - #{timestamp}.txt"] = { + content: brief_build_info(formula, with_hostname: args.with_hostname?), + } + files["00.config.out"] = { content: s.string } + files["00.doctor.out"] = { content: Utils.popen_read("#{HOMEBREW_PREFIX}/bin/brew", "doctor", err: :out) } + unless formula.core_formula? + tap = <<~EOS + Formula: #{formula.name} + Tap: #{formula.tap} + Path: #{formula.path} + EOS + files["00.tap.out"] = { content: tap } end + + odie "`brew gist-logs` requires HOMEBREW_GITHUB_API_TOKEN to be set!" if GitHub::API.credentials_type == :none + + # Description formatted to work well as page title when viewing gist + descr = if formula.core_formula? + "#{formula.name} on #{OS_VERSION} - Homebrew build logs" + else + "#{formula.name} (#{formula.full_name}) on #{OS_VERSION} - Homebrew build logs" + end + + begin + url = GitHub.create_gist(files, descr, private: args.private?) + rescue GitHub::API::HTTPNotFoundError + odie <<~EOS + Your GitHub API token likely doesn't have the `gist` scope. + #{GitHub.pat_blurb(GitHub::CREATE_GIST_SCOPES)} + EOS + end + + if args.new_issue? + url = GitHub.create_issue(formula.tap, "#{formula.name} failed to build on #{OS_VERSION}", + url) + end + + puts url if url + end + + def brief_build_info(formula, with_hostname:) + build_time_string = formula.logs.ctime.strftime("%Y-%m-%d %H:%M:%S") + string = +<<~EOS + Homebrew build logs for #{formula.full_name} on #{OS_VERSION} + EOS + if with_hostname + hostname = Socket.gethostname + string << "Host: #{hostname}\n" + end + string << "Build date: #{build_time_string}\n" + string.freeze + end + + # Causes some terminals to display secure password entry indicators. + def noecho_gets + system "stty", "-echo" + result = $stdin.gets + system "stty", "echo" + puts + result + end + + def load_logs(dir, basedir = dir) + logs = {} + if dir.exist? + dir.children.sort.each do |file| + if file.directory? + logs.merge! load_logs(file, basedir) + else + contents = file.size? ? file.read : "empty log" + # small enough to avoid GitHub "unicorn" page-load-timeout errors + max_file_size = 1_000_000 + contents = truncate_text_to_approximate_size(contents, max_file_size, front_weight: 0.2) + logs[file.relative_path_from(basedir).to_s.tr("/", ":")] = { content: contents } + end + end + end + odie "No logs." if logs.empty? + + logs end end - odie "No logs." if logs.empty? - - logs - end - - def gist_logs - args = gist_logs_args.parse - - Install.perform_preinstall_checks(all_fatal: true) - Install.perform_build_from_source_checks(all_fatal: true) - gistify_logs(args.named.to_resolved_formulae.first, args:) end end diff --git a/Library/Homebrew/cmd/help.rb b/Library/Homebrew/cmd/help.rb index 2d286ad465..66d1ec8dac 100644 --- a/Library/Homebrew/cmd/help.rb +++ b/Library/Homebrew/cmd/help.rb @@ -1,11 +1,16 @@ -# typed: strict +# typed: strong # frozen_string_literal: true +require "abstract_command" require "help" module Homebrew - sig { returns(T.noreturn) } - def help - Help.help + module Cmd + class HelpCmd < AbstractCommand + sig { override.void } + def run + Help.help + end + end end end diff --git a/Library/Homebrew/cmd/home.rb b/Library/Homebrew/cmd/home.rb index 8fe5bcd9a6..b10ccb406a 100644 --- a/Library/Homebrew/cmd/home.rb +++ b/Library/Homebrew/cmd/home.rb @@ -1,54 +1,53 @@ # typed: true # frozen_string_literal: true -require "cli/parser" +require "abstract_command" require "formula" module Homebrew - module_function + module Cmd + class Home < AbstractCommand + cmd_args do + description <<~EOS + Open a or 's homepage in a browser, or open + Homebrew's own homepage if no argument is provided. + EOS + switch "--formula", "--formulae", + description: "Treat all named arguments as formulae." + switch "--cask", "--casks", + description: "Treat all named arguments as casks." - sig { returns(CLI::Parser) } - def home_args - Homebrew::CLI::Parser.new do - description <<~EOS - Open a or 's homepage in a browser, or open - Homebrew's own homepage if no argument is provided. - EOS - switch "--formula", "--formulae", - description: "Treat all named arguments as formulae." - switch "--cask", "--casks", - description: "Treat all named arguments as casks." + conflicts "--formula", "--cask" - conflicts "--formula", "--cask" + named_args [:formula, :cask] + end - named_args [:formula, :cask] - end - end + sig { override.void } + def run + if args.no_named? + exec_browser HOMEBREW_WWW + return + end - sig { void } - def home - args = home_args.parse + # to_formulae_and_casks is typed to possibly return Kegs (but won't without explicitly asking) + formulae_or_casks = T.cast(args.named.to_formulae_and_casks, T::Array[T.any(Formula, Cask::Cask)]) + homepages = formulae_or_casks.map do |formula_or_cask| + puts "Opening homepage for #{name_of(formula_or_cask)}" + formula_or_cask.homepage + end - if args.no_named? - exec_browser HOMEBREW_WWW - return - end + exec_browser(*T.unsafe(homepages)) + end - # to_formulae_and_casks is typed to possibly return Kegs (but won't without explicitly asking) - formulae_or_casks = T.cast(args.named.to_formulae_and_casks, T::Array[T.any(Formula, Cask::Cask)]) - homepages = formulae_or_casks.map do |formula_or_cask| - puts "Opening homepage for #{name_of(formula_or_cask)}" - formula_or_cask.homepage - end + private - exec_browser(*T.unsafe(homepages)) - end - - def name_of(formula_or_cask) - if formula_or_cask.is_a? Formula - "Formula #{formula_or_cask.name}" - else - "Cask #{formula_or_cask.token}" + def name_of(formula_or_cask) + if formula_or_cask.is_a? Formula + "Formula #{formula_or_cask.name}" + else + "Cask #{formula_or_cask.token}" + end + end end end end diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb index 0685a959e3..c39b0eecbc 100644 --- a/Library/Homebrew/cmd/info.rb +++ b/Library/Homebrew/cmd/info.rb @@ -1,9 +1,9 @@ # typed: true # frozen_string_literal: true +require "abstract_command" require "missing_formula" require "caveats" -require "cli/parser" require "options" require "formula" require "keg" @@ -14,364 +14,363 @@ require "deprecate_disable" require "api" module Homebrew - module_function + module Cmd + class Info < AbstractCommand + VALID_DAYS = %w[30 90 365].freeze + VALID_FORMULA_CATEGORIES = %w[install install-on-request build-error].freeze + VALID_CATEGORIES = (VALID_FORMULA_CATEGORIES + %w[cask-install os-version]).freeze - VALID_DAYS = %w[30 90 365].freeze - VALID_FORMULA_CATEGORIES = %w[install install-on-request build-error].freeze - VALID_CATEGORIES = (VALID_FORMULA_CATEGORIES + %w[cask-install os-version]).freeze + cmd_args do + description <<~EOS + Display brief statistics for your Homebrew installation. + If a or is provided, show summary of information about it. + EOS + switch "--analytics", + description: "List global Homebrew analytics data or, if specified, installation and " \ + "build error data for (provided neither `HOMEBREW_NO_ANALYTICS` " \ + "nor `HOMEBREW_NO_GITHUB_API` are set)." + flag "--days=", + depends_on: "--analytics", + description: "How many days of analytics data to retrieve. " \ + "The value for must be `30`, `90` or `365`. The default is `30`." + flag "--category=", + depends_on: "--analytics", + description: "Which type of analytics data to retrieve. " \ + "The value for must be `install`, `install-on-request` or `build-error`; " \ + "`cask-install` or `os-version` may be specified if is not. " \ + "The default is `install`." + switch "--github-packages-downloads", + description: "Scrape GitHub Packages download counts from HTML for a core formula.", + hidden: true + switch "--github", + description: "Open the GitHub source page for and in a browser. " \ + "To view the history locally: `brew log -p` or " + flag "--json", + description: "Print a JSON representation. Currently the default value for is `v1` for " \ + ". For and use `v2`. See the docs for examples of using the " \ + "JSON output: " + switch "--installed", + depends_on: "--json", + description: "Print JSON of formulae that are currently installed." + switch "--eval-all", + depends_on: "--json", + description: "Evaluate all available formulae and casks, whether installed or not, to print their " \ + "JSON. Implied if `HOMEBREW_EVAL_ALL` is set." + switch "--variations", + depends_on: "--json", + description: "Include the variations hash in each formula's JSON output." + switch "-v", "--verbose", + description: "Show more verbose analytics data for ." + switch "--formula", "--formulae", + description: "Treat all named arguments as formulae." + switch "--cask", "--casks", + description: "Treat all named arguments as casks." - sig { returns(CLI::Parser) } - def info_args - Homebrew::CLI::Parser.new do - description <<~EOS - Display brief statistics for your Homebrew installation. - If a or is provided, show summary of information about it. - EOS - switch "--analytics", - description: "List global Homebrew analytics data or, if specified, installation and " \ - "build error data for (provided neither `HOMEBREW_NO_ANALYTICS` " \ - "nor `HOMEBREW_NO_GITHUB_API` are set)." - flag "--days=", - depends_on: "--analytics", - description: "How many days of analytics data to retrieve. " \ - "The value for must be `30`, `90` or `365`. The default is `30`." - flag "--category=", - depends_on: "--analytics", - description: "Which type of analytics data to retrieve. " \ - "The value for must be `install`, `install-on-request` or `build-error`; " \ - "`cask-install` or `os-version` may be specified if is not. " \ - "The default is `install`." - switch "--github-packages-downloads", - description: "Scrape GitHub Packages download counts from HTML for a core formula.", - hidden: true - switch "--github", - description: "Open the GitHub source page for and in a browser. " \ - "To view the history locally: `brew log -p` or " - flag "--json", - description: "Print a JSON representation. Currently the default value for is `v1` for " \ - ". For and use `v2`. See the docs for examples of using the " \ - "JSON output: " - switch "--installed", - depends_on: "--json", - description: "Print JSON of formulae that are currently installed." - switch "--eval-all", - depends_on: "--json", - description: "Evaluate all available formulae and casks, whether installed or not, to print their " \ - "JSON. Implied if `HOMEBREW_EVAL_ALL` is set." - switch "--variations", - depends_on: "--json", - description: "Include the variations hash in each formula's JSON output." - switch "-v", "--verbose", - description: "Show more verbose analytics data for ." - switch "--formula", "--formulae", - description: "Treat all named arguments as formulae." - switch "--cask", "--casks", - description: "Treat all named arguments as casks." + conflicts "--installed", "--eval-all" + conflicts "--installed", "--all" + conflicts "--formula", "--cask" - conflicts "--installed", "--eval-all" - conflicts "--installed", "--all" - conflicts "--formula", "--cask" - - named_args [:formula, :cask] - end - end - - sig { void } - def info - args = info_args.parse - - if args.analytics? - if args.days.present? && VALID_DAYS.exclude?(args.days) - raise UsageError, "`--days` must be one of #{VALID_DAYS.join(", ")}." + named_args [:formula, :cask] end - if args.category.present? - if args.named.present? && VALID_FORMULA_CATEGORIES.exclude?(args.category) - raise UsageError, - "`--category` must be one of #{VALID_FORMULA_CATEGORIES.join(", ")} when querying formulae." - end + sig { override.void } + def run + if args.analytics? + if args.days.present? && VALID_DAYS.exclude?(args.days) + raise UsageError, "`--days` must be one of #{VALID_DAYS.join(", ")}." + end - unless VALID_CATEGORIES.include?(args.category) - raise UsageError, "`--category` must be one of #{VALID_CATEGORIES.join(", ")}." + if args.category.present? + if args.named.present? && VALID_FORMULA_CATEGORIES.exclude?(args.category) + raise UsageError, + "`--category` must be one of #{VALID_FORMULA_CATEGORIES.join(", ")} when querying formulae." + end + + unless VALID_CATEGORIES.include?(args.category) + raise UsageError, "`--category` must be one of #{VALID_CATEGORIES.join(", ")}." + end + end + + print_analytics + elsif args.json + all = args.eval_all? + + print_json(all) + elsif args.github? + raise FormulaOrCaskUnspecifiedError if args.no_named? + + exec_browser(*args.named.to_formulae_and_casks.map { |f| github_info(f) }) + elsif args.no_named? + print_statistics + else + print_info end end - print_analytics(args:) - elsif args.json - all = args.eval_all? - - print_json(all, args:) - elsif args.github? - raise FormulaOrCaskUnspecifiedError if args.no_named? - - exec_browser(*args.named.to_formulae_and_casks.map { |f| github_info(f) }) - elsif args.no_named? - print_statistics - else - print_info(args:) - end - end - - sig { void } - def print_statistics - return unless HOMEBREW_CELLAR.exist? - - count = Formula.racks.length - puts "#{Utils.pluralize("keg", count, include_count: true)}, #{HOMEBREW_CELLAR.dup.abv}" - end - - sig { params(args: CLI::Args).void } - def print_analytics(args:) - if args.no_named? - Utils::Analytics.output(args:) - return - end - - args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i| - puts unless i.zero? - - case obj - when Formula - Utils::Analytics.formula_output(obj, args:) - when Cask::Cask - Utils::Analytics.cask_output(obj, args:) - when FormulaOrCaskUnavailableError - Utils::Analytics.output(filter: obj.name, args:) - else - raise - end - end - end - - sig { params(args: CLI::Args).void } - def print_info(args:) - args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i| - puts unless i.zero? - - case obj - when Formula - info_formula(obj, args:) - when Cask::Cask - info_cask(obj, args:) - when FormulaUnreadableError, FormulaClassUnavailableError, - TapFormulaUnreadableError, TapFormulaClassUnavailableError, - Cask::CaskUnreadableError - # We found the formula/cask, but failed to read it - $stderr.puts obj.backtrace if Homebrew::EnvConfig.developer? - ofail obj.message - when FormulaOrCaskUnavailableError - # The formula/cask could not be found - ofail obj.message - # No formula with this name, try a missing formula lookup - if (reason = MissingFormula.reason(obj.name, show_info: true)) - $stderr.puts reason + def github_remote_path(remote, path) + if remote =~ %r{^(?:https?://|git(?:@|://))github\.com[:/](.+)/(.+?)(?:\.git)?$} + "https://github.com/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}/blob/HEAD/#{path}" + else + "#{remote}/#{path}" end - else - raise - end - end - end - - def json_version(version) - version_hash = { - true => :default, - "v1" => :v1, - "v2" => :v2, - } - - raise UsageError, "invalid JSON version: #{version}" unless version_hash.include?(version) - - version_hash[version] - end - - sig { params(all: T::Boolean, args: T.untyped).void } - def print_json(all, args:) - raise FormulaOrCaskUnspecifiedError if !(all || args.installed?) && args.no_named? - - json = case json_version(args.json) - when :v1, :default - raise UsageError, "Cannot specify `--cask` when using `--json=v1`!" if args.cask? - - formulae = if all - Formula.all(eval_all: args.eval_all?).sort - elsif args.installed? - Formula.installed.sort - else - args.named.to_formulae end - if args.variations? - formulae.map(&:to_hash_with_variations) - else - formulae.map(&:to_hash) + private + + sig { void } + def print_statistics + return unless HOMEBREW_CELLAR.exist? + + count = Formula.racks.length + puts "#{Utils.pluralize("keg", count, include_count: true)}, #{HOMEBREW_CELLAR.dup.abv}" end - when :v2 - formulae, casks = if all - [ - Formula.all(eval_all: args.eval_all?).sort, - Cask::Cask.all(eval_all: args.eval_all?).sort_by(&:full_name), + + sig { void } + def print_analytics + if args.no_named? + Utils::Analytics.output(args:) + return + end + + args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i| + puts unless i.zero? + + case obj + when Formula + Utils::Analytics.formula_output(obj, args:) + when Cask::Cask + Utils::Analytics.cask_output(obj, args:) + when FormulaOrCaskUnavailableError + Utils::Analytics.output(filter: obj.name, args:) + else + raise + end + end + end + + sig { void } + def print_info + args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i| + puts unless i.zero? + + case obj + when Formula + info_formula(obj) + when Cask::Cask + info_cask(obj) + when FormulaUnreadableError, FormulaClassUnavailableError, + TapFormulaUnreadableError, TapFormulaClassUnavailableError, + Cask::CaskUnreadableError + # We found the formula/cask, but failed to read it + $stderr.puts obj.backtrace if Homebrew::EnvConfig.developer? + ofail obj.message + when FormulaOrCaskUnavailableError + # The formula/cask could not be found + ofail obj.message + # No formula with this name, try a missing formula lookup + if (reason = MissingFormula.reason(obj.name, show_info: true)) + $stderr.puts reason + end + else + raise + end + end + end + + def json_version(version) + version_hash = { + true => :default, + "v1" => :v1, + "v2" => :v2, + } + + raise UsageError, "invalid JSON version: #{version}" unless version_hash.include?(version) + + version_hash[version] + end + + sig { params(all: T::Boolean).void } + def print_json(all) + raise FormulaOrCaskUnspecifiedError if !(all || args.installed?) && args.no_named? + + json = case json_version(args.json) + when :v1, :default + raise UsageError, "Cannot specify `--cask` when using `--json=v1`!" if args.cask? + + formulae = if all + Formula.all(eval_all: args.eval_all?).sort + elsif args.installed? + Formula.installed.sort + else + args.named.to_formulae + end + + if args.variations? + formulae.map(&:to_hash_with_variations) + else + formulae.map(&:to_hash) + end + when :v2 + formulae, casks = if all + [ + Formula.all(eval_all: args.eval_all?).sort, + Cask::Cask.all(eval_all: args.eval_all?).sort_by(&:full_name), + ] + elsif args.installed? + [Formula.installed.sort, Cask::Caskroom.casks.sort_by(&:full_name)] + else + args.named.to_formulae_to_casks + end + + if args.variations? + { + "formulae" => formulae.map(&:to_hash_with_variations), + "casks" => casks.map(&:to_hash_with_variations), + } + else + { + "formulae" => formulae.map(&:to_hash), + "casks" => casks.map(&:to_h), + } + end + else + raise + end + + puts JSON.pretty_generate(json) + end + + def github_info(formula_or_cask) + return formula_or_cask.path if formula_or_cask.tap.blank? || formula_or_cask.tap.remote.blank? + + path = case formula_or_cask + when Formula + formula = formula_or_cask + formula.path.relative_path_from(T.must(formula.tap).path) + when Cask::Cask + cask = formula_or_cask + if cask.sourcefile_path.blank? + return "#{cask.tap.default_remote}/blob/HEAD/#{cask.tap.relative_cask_path(cask.token)}" + end + + cask.sourcefile_path.relative_path_from(cask.tap.path) + end + + github_remote_path(formula_or_cask.tap.remote, path) + end + + def info_formula(formula) + specs = [] + + if (stable = formula.stable) + string = "stable #{stable.version}" + string += " (bottled)" if stable.bottled? && formula.pour_bottle? + specs << string + end + + specs << "HEAD" if formula.head + + attrs = [] + attrs << "pinned at #{formula.pinned_version}" if formula.pinned? + attrs << "keg-only" if formula.keg_only? + + puts "#{oh1_title(formula.full_name)}: #{specs * ", "}#{" [#{attrs * ", "}]" unless attrs.empty?}" + puts formula.desc if formula.desc + puts Formatter.url(formula.homepage) if formula.homepage + + deprecate_disable_info_string = DeprecateDisable.message(formula) + puts deprecate_disable_info_string.capitalize if deprecate_disable_info_string.present? + + conflicts = formula.conflicts.map do |conflict| + reason = " (because #{conflict.reason})" if conflict.reason + "#{conflict.name}#{reason}" + end.sort! + unless conflicts.empty? + puts <<~EOS + Conflicts with: + #{conflicts.join("\n ")} + EOS + end + + kegs = formula.installed_kegs + heads, versioned = kegs.partition { |k| k.version.head? } + kegs = [ + *heads.sort_by { |k| -Tab.for_keg(k).time.to_i }, + *Keg.sort(versioned), ] - elsif args.installed? - [Formula.installed.sort, Cask::Caskroom.casks.sort_by(&:full_name)] - else - args.named.to_formulae_to_casks + if kegs.empty? + puts "Not installed" + else + kegs.each do |keg| + puts "#{keg} (#{keg.abv})#{" *" if keg.linked?}" + tab = Tab.for_keg(keg).to_s + puts " #{tab}" unless tab.empty? + end + end + + puts "From: #{Formatter.url(github_info(formula))}" + + puts "License: #{SPDX.license_expression_to_string formula.license}" if formula.license.present? + + unless formula.deps.empty? + ohai "Dependencies" + %w[build required recommended optional].map do |type| + deps = formula.deps.send(type).uniq + puts "#{type.capitalize}: #{decorate_dependencies deps}" unless deps.empty? + end + end + + unless formula.requirements.to_a.empty? + ohai "Requirements" + %w[build required recommended optional].map do |type| + reqs = formula.requirements.select(&:"#{type}?") + next if reqs.to_a.empty? + + puts "#{type.capitalize}: #{decorate_requirements(reqs)}" + end + end + + if !formula.options.empty? || formula.head + ohai "Options" + Options.dump_for_formula formula + end + + caveats = Caveats.new(formula) + ohai "Caveats", caveats.to_s unless caveats.empty? + + Utils::Analytics.formula_output(formula, args:) end - if args.variations? - { - "formulae" => formulae.map(&:to_hash_with_variations), - "casks" => casks.map(&:to_hash_with_variations), - } - else - { - "formulae" => formulae.map(&:to_hash), - "casks" => casks.map(&:to_h), - } - end - else - raise - end - - puts JSON.pretty_generate(json) - end - - def github_remote_path(remote, path) - if remote =~ %r{^(?:https?://|git(?:@|://))github\.com[:/](.+)/(.+?)(?:\.git)?$} - "https://github.com/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}/blob/HEAD/#{path}" - else - "#{remote}/#{path}" - end - end - - def github_info(formula_or_cask) - return formula_or_cask.path if formula_or_cask.tap.blank? || formula_or_cask.tap.remote.blank? - - path = case formula_or_cask - when Formula - formula = formula_or_cask - formula.path.relative_path_from(T.must(formula.tap).path) - when Cask::Cask - cask = formula_or_cask - if cask.sourcefile_path.blank? - return "#{cask.tap.default_remote}/blob/HEAD/#{cask.tap.relative_cask_path(cask.token)}" + def decorate_dependencies(dependencies) + deps_status = dependencies.map do |dep| + if dep.satisfied?([]) + pretty_installed(dep_display_s(dep)) + else + pretty_uninstalled(dep_display_s(dep)) + end + end + deps_status.join(", ") end - cask.sourcefile_path.relative_path_from(cask.tap.path) - end + def decorate_requirements(requirements) + req_status = requirements.map do |req| + req_s = req.display_s + req.satisfied? ? pretty_installed(req_s) : pretty_uninstalled(req_s) + end + req_status.join(", ") + end - github_remote_path(formula_or_cask.tap.remote, path) - end + def dep_display_s(dep) + return dep.name if dep.option_tags.empty? - def info_formula(formula, args:) - specs = [] + "#{dep.name} #{dep.option_tags.map { |o| "--#{o}" }.join(" ")}" + end - if (stable = formula.stable) - string = "stable #{stable.version}" - string += " (bottled)" if stable.bottled? && formula.pour_bottle? - specs << string - end + def info_cask(cask) + require "cask/info" - specs << "HEAD" if formula.head - - attrs = [] - attrs << "pinned at #{formula.pinned_version}" if formula.pinned? - attrs << "keg-only" if formula.keg_only? - - puts "#{oh1_title(formula.full_name)}: #{specs * ", "}#{" [#{attrs * ", "}]" unless attrs.empty?}" - puts formula.desc if formula.desc - puts Formatter.url(formula.homepage) if formula.homepage - - deprecate_disable_info_string = DeprecateDisable.message(formula) - puts deprecate_disable_info_string.capitalize if deprecate_disable_info_string.present? - - conflicts = formula.conflicts.map do |conflict| - reason = " (because #{conflict.reason})" if conflict.reason - "#{conflict.name}#{reason}" - end.sort! - unless conflicts.empty? - puts <<~EOS - Conflicts with: - #{conflicts.join("\n ")} - EOS - end - - kegs = formula.installed_kegs - heads, versioned = kegs.partition { |k| k.version.head? } - kegs = [ - *heads.sort_by { |k| -Tab.for_keg(k).time.to_i }, - *Keg.sort(versioned), - ] - if kegs.empty? - puts "Not installed" - else - kegs.each do |keg| - puts "#{keg} (#{keg.abv})#{" *" if keg.linked?}" - tab = Tab.for_keg(keg).to_s - puts " #{tab}" unless tab.empty? + Cask::Info.info(cask) end end - - puts "From: #{Formatter.url(github_info(formula))}" - - puts "License: #{SPDX.license_expression_to_string formula.license}" if formula.license.present? - - unless formula.deps.empty? - ohai "Dependencies" - %w[build required recommended optional].map do |type| - deps = formula.deps.send(type).uniq - puts "#{type.capitalize}: #{decorate_dependencies deps}" unless deps.empty? - end - end - - unless formula.requirements.to_a.empty? - ohai "Requirements" - %w[build required recommended optional].map do |type| - reqs = formula.requirements.select(&:"#{type}?") - next if reqs.to_a.empty? - - puts "#{type.capitalize}: #{decorate_requirements(reqs)}" - end - end - - if !formula.options.empty? || formula.head - ohai "Options" - Options.dump_for_formula formula - end - - caveats = Caveats.new(formula) - ohai "Caveats", caveats.to_s unless caveats.empty? - - Utils::Analytics.formula_output(formula, args:) - end - - def decorate_dependencies(dependencies) - deps_status = dependencies.map do |dep| - if dep.satisfied?([]) - pretty_installed(dep_display_s(dep)) - else - pretty_uninstalled(dep_display_s(dep)) - end - end - deps_status.join(", ") - end - - def decorate_requirements(requirements) - req_status = requirements.map do |req| - req_s = req.display_s - req.satisfied? ? pretty_installed(req_s) : pretty_uninstalled(req_s) - end - req_status.join(", ") - end - - def dep_display_s(dep) - return dep.name if dep.option_tags.empty? - - "#{dep.name} #{dep.option_tags.map { |o| "--#{o}" }.join(" ")}" - end - - def info_cask(cask, args:) - require "cask/info" - - Cask::Info.info(cask) end end diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index 921bcd9c51..13b65b40c0 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -1,6 +1,7 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "abstract_command" require "cask/config" require "cask/installer" require "cask_dependent" @@ -9,400 +10,402 @@ require "formula_installer" require "development_tools" require "install" require "cleanup" -require "cli/parser" require "upgrade" module Homebrew - sig { returns(CLI::Parser) } - def self.install_args - Homebrew::CLI::Parser.new do - description <<~EOS - Install a or . Additional options specific to a may be - appended to the command. + module Cmd + class InstallCmd < AbstractCommand + cmd_args do + description <<~EOS + Install a or . Additional options specific to a may be + appended to the command. - Unless `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK` is set, `brew upgrade` or `brew reinstall` will be run for - outdated dependents and dependents with broken linkage, respectively. + Unless `HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK` is set, `brew upgrade` or `brew reinstall` will be run for + outdated dependents and dependents with broken linkage, respectively. - Unless `HOMEBREW_NO_INSTALL_CLEANUP` is set, `brew cleanup` will then be run for - the installed formulae or, every 30 days, for all formulae. + Unless `HOMEBREW_NO_INSTALL_CLEANUP` is set, `brew cleanup` will then be run for + the installed formulae or, every 30 days, for all formulae. - Unless `HOMEBREW_NO_INSTALL_UPGRADE` is set, `brew install` will upgrade if it - is already installed but outdated. - EOS - switch "-d", "--debug", - description: "If brewing fails, open an interactive debugging session with access to IRB " \ - "or a shell inside the temporary build directory." - switch "-f", "--force", - description: "Install formulae without checking for previously installed keg-only or " \ - "non-migrated versions. When installing casks, overwrite existing files " \ - "(binaries and symlinks are excluded, unless originally from the same cask)." - switch "-v", "--verbose", - description: "Print the verification and post-install steps." - switch "-n", "--dry-run", - description: "Show what would be installed, but do not actually install anything." - [ - [:switch, "--formula", "--formulae", { - description: "Treat all named arguments as formulae.", - }], - [:flag, "--env=", { - description: "Disabled other than for internal Homebrew use.", - hidden: true, - }], - [:switch, "--ignore-dependencies", { - description: "An unsupported Homebrew development option to skip installing any dependencies of any " \ - "kind. If the dependencies are not already present, the formula will have issues. If you're " \ - "not developing Homebrew, consider adjusting your PATH rather than using this option.", - }], - [:switch, "--only-dependencies", { - description: "Install the dependencies with specified options but do not install the " \ - "formula itself.", - }], - [:flag, "--cc=", { - description: "Attempt to compile using the specified , which should be the name of the " \ - "compiler's executable, e.g. `gcc-7` for GCC 7. In order to use LLVM's clang, specify " \ - "`llvm_clang`. To use the Apple-provided clang, specify `clang`. This option will only " \ - "accept compilers that are provided by Homebrew or bundled with macOS. Please do not " \ - "file issues if you encounter errors while using this option.", - }], - [:switch, "-s", "--build-from-source", { - description: "Compile from source even if a bottle is provided. " \ - "Dependencies will still be installed from bottles if they are available.", - }], - [:switch, "--force-bottle", { - description: "Install from a bottle if it exists for the current or newest version of " \ - "macOS, even if it would not normally be used for installation.", - }], - [:switch, "--include-test", { - description: "Install testing dependencies required to run `brew test` .", - }], - [:switch, "--HEAD", { - description: "If defines it, install the HEAD version, aka. main, trunk, unstable, master.", - }], - [:switch, "--fetch-HEAD", { - description: "Fetch the upstream repository to detect if the HEAD installation of the " \ - "formula is outdated. Otherwise, the repository's HEAD will only be checked for " \ - "updates when a new stable or development version has been released.", - }], - [:switch, "--keep-tmp", { - description: "Retain the temporary files created during installation.", - }], - [:switch, "--debug-symbols", { - depends_on: "--build-from-source", - description: "Generate debug symbols on build. Source will be retained in a cache directory.", - }], - [:switch, "--build-bottle", { - description: "Prepare the formula for eventual bottling during installation, skipping any " \ - "post-install steps.", - }], - [:switch, "--skip-post-install", { - description: "Install but skip any post-install steps.", - }], - [:flag, "--bottle-arch=", { - depends_on: "--build-bottle", - description: "Optimise bottles for the specified architecture rather than the oldest " \ - "architecture supported by the version of macOS the bottles are built on.", - }], - [:switch, "--display-times", { - env: :display_install_times, - description: "Print install times for each package at the end of the run.", - }], - [:switch, "-i", "--interactive", { - description: "Download and patch , then open a shell. This allows the user to " \ - "run `./configure --help` and otherwise determine how to turn the software " \ - "package into a Homebrew package.", - }], - [:switch, "-g", "--git", { - description: "Create a Git repository, useful for creating patches to the software.", - }], - [:switch, "--overwrite", { - description: "Delete files that already exist in the prefix while linking.", - }], - ].each do |args| - options = args.pop - send(*args, **options) - conflicts "--cask", args.last - end - formula_options - [ - [:switch, "--cask", "--casks", { description: "Treat all named arguments as casks." }], - [:switch, "--[no-]binaries", { - description: "Disable/enable linking of helper executables (default: enabled).", - env: :cask_opts_binaries, - }], - [:switch, "--require-sha", { - description: "Require all casks to have a checksum.", - env: :cask_opts_require_sha, - }], - [:switch, "--[no-]quarantine", { - description: "Disable/enable quarantining of downloads (default: enabled).", - env: :cask_opts_quarantine, - }], - [:switch, "--adopt", { - description: "Adopt existing artifacts in the destination that are identical to those being installed. " \ - "Cannot be combined with `--force`.", - }], - [:switch, "--skip-cask-deps", { - description: "Skip installing cask dependencies.", - }], - [:switch, "--zap", { - description: "For use with `brew reinstall --cask`. Remove all files associated with a cask. " \ - "*May remove files which are shared between applications.*", - }], - ].each do |args| - options = args.pop - send(*args, **options) - conflicts "--formula", args.last - end - cask_options - - conflicts "--ignore-dependencies", "--only-dependencies" - conflicts "--build-from-source", "--build-bottle", "--force-bottle" - conflicts "--adopt", "--force" - - named_args [:formula, :cask], min: 1 - end - end - - def self.install - args = install_args.parse - - if args.env.present? - # Can't use `replacement: false` because `install_args` are used by - # `build.rb`. Instead, `hide_from_man_page` and don't do anything with - # this argument here. - # This odisabled should stick around indefinitely. - odisabled "brew install --env", "`env :std` in specific formula files" - end - - args.named.each do |name| - if (tap_with_name = Tap.with_formula_name(name)) - tap, = tap_with_name - elsif (tap_with_token = Tap.with_cask_token(name)) - tap, = tap_with_token - end - - tap&.ensure_installed! - end - - if args.ignore_dependencies? - opoo <<~EOS - #{Tty.bold}`--ignore-dependencies` is an unsupported Homebrew developer option!#{Tty.reset} - Adjust your PATH to put any preferred versions of applications earlier in the - PATH rather than using this unsupported option! - - EOS - end - - begin - formulae, casks = args.named.to_formulae_and_casks(warn: false) - .partition { |formula_or_cask| formula_or_cask.is_a?(Formula) } - rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError - cask_tap = CoreCaskTap.instance - if !cask_tap.installed? && (args.cask? || Tap.untapped_official_taps.exclude?(cask_tap.name)) - cask_tap.ensure_installed! - retry if cask_tap.installed? - end - - raise - end - - if casks.any? - - if args.dry_run? - if (casks_to_install = casks.reject(&:installed?).presence) - ohai "Would install #{::Utils.pluralize("cask", casks_to_install.count, include_count: true)}:" - puts casks_to_install.map(&:full_name).join(" ") + Unless `HOMEBREW_NO_INSTALL_UPGRADE` is set, `brew install` will upgrade if it + is already installed but outdated. + EOS + switch "-d", "--debug", + description: "If brewing fails, open an interactive debugging session with access to IRB " \ + "or a shell inside the temporary build directory." + switch "-f", "--force", + description: "Install formulae without checking for previously installed keg-only or " \ + "non-migrated versions. When installing casks, overwrite existing files " \ + "(binaries and symlinks are excluded, unless originally from the same cask)." + switch "-v", "--verbose", + description: "Print the verification and post-install steps." + switch "-n", "--dry-run", + description: "Show what would be installed, but do not actually install anything." + [ + [:switch, "--formula", "--formulae", { + description: "Treat all named arguments as formulae.", + }], + [:flag, "--env=", { + description: "Disabled other than for internal Homebrew use.", + hidden: true, + }], + [:switch, "--ignore-dependencies", { + description: "An unsupported Homebrew development option to skip installing any dependencies of any " \ + "kind. If the dependencies are not already present, the formula will have issues. If " \ + "you're not developing Homebrew, consider adjusting your PATH rather than using this " \ + "option.", + }], + [:switch, "--only-dependencies", { + description: "Install the dependencies with specified options but do not install the " \ + "formula itself.", + }], + [:flag, "--cc=", { + description: "Attempt to compile using the specified , which should be the name of the " \ + "compiler's executable, e.g. `gcc-7` for GCC 7. In order to use LLVM's clang, specify " \ + "`llvm_clang`. To use the Apple-provided clang, specify `clang`. This option will only " \ + "accept compilers that are provided by Homebrew or bundled with macOS. Please do not " \ + "file issues if you encounter errors while using this option.", + }], + [:switch, "-s", "--build-from-source", { + description: "Compile from source even if a bottle is provided. " \ + "Dependencies will still be installed from bottles if they are available.", + }], + [:switch, "--force-bottle", { + description: "Install from a bottle if it exists for the current or newest version of " \ + "macOS, even if it would not normally be used for installation.", + }], + [:switch, "--include-test", { + description: "Install testing dependencies required to run `brew test` .", + }], + [:switch, "--HEAD", { + description: "If defines it, install the HEAD version, aka. main, trunk, unstable, master.", + }], + [:switch, "--fetch-HEAD", { + description: "Fetch the upstream repository to detect if the HEAD installation of the " \ + "formula is outdated. Otherwise, the repository's HEAD will only be checked for " \ + "updates when a new stable or development version has been released.", + }], + [:switch, "--keep-tmp", { + description: "Retain the temporary files created during installation.", + }], + [:switch, "--debug-symbols", { + depends_on: "--build-from-source", + description: "Generate debug symbols on build. Source will be retained in a cache directory.", + }], + [:switch, "--build-bottle", { + description: "Prepare the formula for eventual bottling during installation, skipping any " \ + "post-install steps.", + }], + [:switch, "--skip-post-install", { + description: "Install but skip any post-install steps.", + }], + [:flag, "--bottle-arch=", { + depends_on: "--build-bottle", + description: "Optimise bottles for the specified architecture rather than the oldest " \ + "architecture supported by the version of macOS the bottles are built on.", + }], + [:switch, "--display-times", { + env: :display_install_times, + description: "Print install times for each package at the end of the run.", + }], + [:switch, "-i", "--interactive", { + description: "Download and patch , then open a shell. This allows the user to " \ + "run `./configure --help` and otherwise determine how to turn the software " \ + "package into a Homebrew package.", + }], + [:switch, "-g", "--git", { + description: "Create a Git repository, useful for creating patches to the software.", + }], + [:switch, "--overwrite", { + description: "Delete files that already exist in the prefix while linking.", + }], + ].each do |args| + options = args.pop + send(*args, **options) + conflicts "--cask", args.last end - casks.each do |cask| - dep_names = CaskDependent.new(cask) - .runtime_dependencies - .reject(&:installed?) - .map(&:to_formula) - .map(&:name) - next if dep_names.blank? + formula_options + [ + [:switch, "--cask", "--casks", { description: "Treat all named arguments as casks." }], + [:switch, "--[no-]binaries", { + description: "Disable/enable linking of helper executables (default: enabled).", + env: :cask_opts_binaries, + }], + [:switch, "--require-sha", { + description: "Require all casks to have a checksum.", + env: :cask_opts_require_sha, + }], + [:switch, "--[no-]quarantine", { + description: "Disable/enable quarantining of downloads (default: enabled).", + env: :cask_opts_quarantine, + }], + [:switch, "--adopt", { + description: "Adopt existing artifacts in the destination that are identical to those being installed. " \ + "Cannot be combined with `--force`.", + }], + [:switch, "--skip-cask-deps", { + description: "Skip installing cask dependencies.", + }], + [:switch, "--zap", { + description: "For use with `brew reinstall --cask`. Remove all files associated with a cask. " \ + "*May remove files which are shared between applications.*", + }], + ].each do |args| + options = args.pop + send(*args, **options) + conflicts "--formula", args.last + end + cask_options - ohai "Would install #{::Utils.pluralize("dependenc", dep_names.count, plural: "ies", singular: "y", + conflicts "--ignore-dependencies", "--only-dependencies" + conflicts "--build-from-source", "--build-bottle", "--force-bottle" + conflicts "--adopt", "--force" + + named_args [:formula, :cask], min: 1 + end + + sig { override.void } + def run + if args.env.present? + # Can't use `replacement: false` because `install_args` are used by + # `build.rb`. Instead, `hide_from_man_page` and don't do anything with + # this argument here. + # This odisabled should stick around indefinitely. + odisabled "brew install --env", "`env :std` in specific formula files" + end + + args.named.each do |name| + if (tap_with_name = Tap.with_formula_name(name)) + tap, = tap_with_name + elsif (tap_with_token = Tap.with_cask_token(name)) + tap, = tap_with_token + end + + tap&.ensure_installed! + end + + if args.ignore_dependencies? + opoo <<~EOS + #{Tty.bold}`--ignore-dependencies` is an unsupported Homebrew developer option!#{Tty.reset} + Adjust your PATH to put any preferred versions of applications earlier in the + PATH rather than using this unsupported option! + + EOS + end + + begin + formulae, casks = T.cast( + args.named.to_formulae_and_casks(warn: false).partition { _1.is_a?(Formula) }, + [T::Array[Formula], T::Array[Cask::Cask]], + ) + rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError + cask_tap = CoreCaskTap.instance + if !cask_tap.installed? && (args.cask? || Tap.untapped_official_taps.exclude?(cask_tap.name)) + cask_tap.ensure_installed! + retry if cask_tap.installed? + end + + raise + end + + if casks.any? + + if args.dry_run? + if (casks_to_install = casks.reject(&:installed?).presence) + ohai "Would install #{::Utils.pluralize("cask", casks_to_install.count, include_count: true)}:" + puts casks_to_install.map(&:full_name).join(" ") + end + casks.each do |cask| + dep_names = CaskDependent.new(cask) + .runtime_dependencies + .reject(&:installed?) + .map(&:to_formula) + .map(&:name) + next if dep_names.blank? + + ohai "Would install #{::Utils.pluralize("dependenc", dep_names.count, plural: "ies", singular: "y", include_count: true)} for #{cask.full_name}:" - puts dep_names.join(" ") + puts dep_names.join(" ") + end + return + end + + require "cask/installer" + + installed_casks, new_casks = casks.partition(&:installed?) + + new_casks.each do |cask| + Cask::Installer.new( + cask, + binaries: args.binaries?, + verbose: args.verbose?, + force: args.force?, + adopt: args.adopt?, + require_sha: args.require_sha?, + skip_cask_deps: args.skip_cask_deps?, + quarantine: args.quarantine?, + quiet: args.quiet?, + ).install + end + + if !Homebrew::EnvConfig.no_install_upgrade? && installed_casks.any? + require "cask/upgrade" + + Cask::Upgrade.upgrade_casks( + *installed_casks, + force: args.force?, + dry_run: args.dry_run?, + binaries: args.binaries?, + quarantine: args.quarantine?, + require_sha: args.require_sha?, + skip_cask_deps: args.skip_cask_deps?, + verbose: args.verbose?, + args:, + ) + end end - return - end - require "cask/installer" + # if the user's flags will prevent bottle only-installations when no + # developer tools are available, we need to stop them early on + build_flags = [] + unless DevelopmentTools.installed? + build_flags << "--HEAD" if args.HEAD? + build_flags << "--build-bottle" if args.build_bottle? + build_flags << "--build-from-source" if args.build_from_source? - installed_casks, new_casks = casks.partition(&:installed?) + raise BuildFlagsError.new(build_flags, bottled: formulae.all?(&:bottled?)) if build_flags.present? + end - new_casks.each do |cask| - Cask::Installer.new( - cask, - binaries: args.binaries?, - verbose: args.verbose?, - force: args.force?, - adopt: args.adopt?, - require_sha: args.require_sha?, - skip_cask_deps: args.skip_cask_deps?, - quarantine: args.quarantine?, - quiet: args.quiet?, - ).install - end + if build_flags.present? && !Homebrew::EnvConfig.developer? + opoo "building from source is not supported!" + puts "You're on your own. Failures are expected so don't create any issues, please!" + end - if !Homebrew::EnvConfig.no_install_upgrade? && installed_casks.any? - require "cask/upgrade" + installed_formulae = formulae.select do |f| + Install.install_formula?( + f, + head: args.HEAD?, + fetch_head: args.fetch_HEAD?, + only_dependencies: args.only_dependencies?, + force: args.force?, + quiet: args.quiet?, + ) + end - Cask::Upgrade.upgrade_casks( - *installed_casks, - force: args.force?, - dry_run: args.dry_run?, - binaries: args.binaries?, - quarantine: args.quarantine?, - require_sha: args.require_sha?, - skip_cask_deps: args.skip_cask_deps?, - verbose: args.verbose?, - args:, + return if installed_formulae.empty? + + Install.perform_preinstall_checks(cc: args.cc) + + Install.install_formulae( + installed_formulae, + build_bottle: args.build_bottle?, + force_bottle: args.force_bottle?, + bottle_arch: args.bottle_arch, + ignore_deps: args.ignore_dependencies?, + only_deps: args.only_dependencies?, + include_test_formulae: args.include_test_formulae, + build_from_source_formulae: args.build_from_source_formulae, + cc: args.cc, + git: args.git?, + interactive: args.interactive?, + keep_tmp: args.keep_tmp?, + debug_symbols: args.debug_symbols?, + force: args.force?, + overwrite: args.overwrite?, + debug: args.debug?, + quiet: args.quiet?, + verbose: args.verbose?, + dry_run: args.dry_run?, + skip_post_install: args.skip_post_install?, ) + + Upgrade.check_installed_dependents( + installed_formulae, + flags: args.flags_only, + installed_on_request: args.named.present?, + force_bottle: args.force_bottle?, + build_from_source_formulae: args.build_from_source_formulae, + interactive: args.interactive?, + keep_tmp: args.keep_tmp?, + debug_symbols: args.debug_symbols?, + force: args.force?, + debug: args.debug?, + quiet: args.quiet?, + verbose: args.verbose?, + dry_run: args.dry_run?, + ) + + Cleanup.periodic_clean!(dry_run: args.dry_run?) + + Homebrew.messages.display_messages(display_times: args.display_times?) + rescue FormulaUnreadableError, FormulaClassUnavailableError, + TapFormulaUnreadableError, TapFormulaClassUnavailableError => e + # Need to rescue before `FormulaUnavailableError` (superclass of this) + # is handled, as searching for a formula doesn't make sense here (the + # formula was found, but there's a problem with its implementation). + $stderr.puts Utils::Backtrace.clean(e) if Homebrew::EnvConfig.developer? + ofail e.message + rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e + Homebrew.failed = true + + # formula name or cask token + name = case e + when FormulaOrCaskUnavailableError then e.name + when Cask::CaskUnavailableError then e.token + else T.absurd(e) + end + + if name == "updog" + ofail "What's updog?" + return + end + + opoo e + + reason = MissingFormula.reason(name, silent: true) + if !args.cask? && reason + $stderr.puts reason + return + end + + # We don't seem to get good search results when the tap is specified + # so we might as well return early. + return if name.include?("/") + + require "search" + + package_types = [] + package_types << "formulae" unless args.cask? + package_types << "casks" unless args.formula? + + ohai "Searching for similarly named #{package_types.join(" and ")}..." + + # Don't treat formula/cask name as a regex + string_or_regex = name + all_formulae, all_casks = Search.search_names(string_or_regex, args) + + if all_formulae.any? + ohai "Formulae", Formatter.columns(all_formulae) + first_formula = all_formulae.first.to_s + puts <<~EOS + + To install #{first_formula}, run: + brew install #{first_formula} + EOS + end + puts if all_formulae.any? && all_casks.any? + if all_casks.any? + ohai "Casks", Formatter.columns(all_casks) + first_cask = all_casks.first.to_s + puts <<~EOS + + To install #{first_cask}, run: + brew install --cask #{first_cask} + EOS + end + return if all_formulae.any? || all_casks.any? + + odie "No #{package_types.join(" or ")} found for #{name}." end end - - # if the user's flags will prevent bottle only-installations when no - # developer tools are available, we need to stop them early on - build_flags = [] - unless DevelopmentTools.installed? - build_flags << "--HEAD" if args.HEAD? - build_flags << "--build-bottle" if args.build_bottle? - build_flags << "--build-from-source" if args.build_from_source? - - raise BuildFlagsError.new(build_flags, bottled: formulae.all?(&:bottled?)) if build_flags.present? - end - - if build_flags.present? && !Homebrew::EnvConfig.developer? - opoo "building from source is not supported!" - puts "You're on your own. Failures are expected so don't create any issues, please!" - end - - installed_formulae = formulae.select do |f| - Install.install_formula?( - f, - head: args.HEAD?, - fetch_head: args.fetch_HEAD?, - only_dependencies: args.only_dependencies?, - force: args.force?, - quiet: args.quiet?, - ) - end - - return if installed_formulae.empty? - - Install.perform_preinstall_checks(cc: args.cc) - - Install.install_formulae( - installed_formulae, - build_bottle: args.build_bottle?, - force_bottle: args.force_bottle?, - bottle_arch: args.bottle_arch, - ignore_deps: args.ignore_dependencies?, - only_deps: args.only_dependencies?, - include_test_formulae: args.include_test_formulae, - build_from_source_formulae: args.build_from_source_formulae, - cc: args.cc, - git: args.git?, - interactive: args.interactive?, - keep_tmp: args.keep_tmp?, - debug_symbols: args.debug_symbols?, - force: args.force?, - overwrite: args.overwrite?, - debug: args.debug?, - quiet: args.quiet?, - verbose: args.verbose?, - dry_run: args.dry_run?, - skip_post_install: args.skip_post_install?, - ) - - Upgrade.check_installed_dependents( - installed_formulae, - flags: args.flags_only, - installed_on_request: args.named.present?, - force_bottle: args.force_bottle?, - build_from_source_formulae: args.build_from_source_formulae, - interactive: args.interactive?, - keep_tmp: args.keep_tmp?, - debug_symbols: args.debug_symbols?, - force: args.force?, - debug: args.debug?, - quiet: args.quiet?, - verbose: args.verbose?, - dry_run: args.dry_run?, - ) - - Cleanup.periodic_clean!(dry_run: args.dry_run?) - - Homebrew.messages.display_messages(display_times: args.display_times?) - rescue FormulaUnreadableError, FormulaClassUnavailableError, - TapFormulaUnreadableError, TapFormulaClassUnavailableError => e - # Need to rescue before `FormulaUnavailableError` (superclass of this) - # is handled, as searching for a formula doesn't make sense here (the - # formula was found, but there's a problem with its implementation). - $stderr.puts Utils::Backtrace.clean(e) if Homebrew::EnvConfig.developer? - ofail e.message - rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e - Homebrew.failed = true - - # formula name or cask token - name = case e - when FormulaOrCaskUnavailableError then e.name - when Cask::CaskUnavailableError then e.token - else T.absurd(e) - end - - if name == "updog" - ofail "What's updog?" - return - end - - opoo e - - reason = MissingFormula.reason(name, silent: true) - if !args.cask? && reason - $stderr.puts reason - return - end - - # We don't seem to get good search results when the tap is specified - # so we might as well return early. - return if name.include?("/") - - require "search" - - package_types = [] - package_types << "formulae" unless args.cask? - package_types << "casks" unless args.formula? - - ohai "Searching for similarly named #{package_types.join(" and ")}..." - - # Don't treat formula/cask name as a regex - string_or_regex = name - all_formulae, all_casks = Search.search_names(string_or_regex, args) - - if all_formulae.any? - ohai "Formulae", Formatter.columns(all_formulae) - first_formula = all_formulae.first.to_s - puts <<~EOS - - To install #{first_formula}, run: - brew install #{first_formula} - EOS - end - puts if all_formulae.any? && all_casks.any? - if all_casks.any? - ohai "Casks", Formatter.columns(all_casks) - first_cask = all_casks.first.to_s - puts <<~EOS - - To install #{first_cask}, run: - brew install --cask #{first_cask} - EOS - end - return if all_formulae.any? || all_casks.any? - - odie "No #{package_types.join(" or ")} found for #{name}." end end diff --git a/Library/Homebrew/cmd/leaves.rb b/Library/Homebrew/cmd/leaves.rb index e6fe1abd58..d679fbee08 100644 --- a/Library/Homebrew/cmd/leaves.rb +++ b/Library/Homebrew/cmd/leaves.rb @@ -1,51 +1,51 @@ # typed: true # frozen_string_literal: true +require "abstract_command" require "formula" require "cask_dependent" -require "cli/parser" module Homebrew - module_function + module Cmd + class Leaves < AbstractCommand + cmd_args do + description <<~EOS + List installed formulae that are not dependencies of another installed formula or cask. + EOS + switch "-r", "--installed-on-request", + description: "Only list leaves that were manually installed." + switch "-p", "--installed-as-dependency", + description: "Only list leaves that were installed as dependencies." - sig { returns(CLI::Parser) } - def leaves_args - Homebrew::CLI::Parser.new do - description <<~EOS - List installed formulae that are not dependencies of another installed formula or cask. - EOS - switch "-r", "--installed-on-request", - description: "Only list leaves that were manually installed." - switch "-p", "--installed-as-dependency", - description: "Only list leaves that were installed as dependencies." + conflicts "--installed-on-request", "--installed-as-dependency" - conflicts "--installed-on-request", "--installed-as-dependency" + named_args :none + end - named_args :none + sig { override.void } + def run + leaves_list = Formula.installed - Formula.installed.flat_map(&:runtime_formula_dependencies) + casks_runtime_dependencies = Cask::Caskroom.casks.flat_map do |cask| + CaskDependent.new(cask).runtime_dependencies.map(&:to_formula) + end + leaves_list -= casks_runtime_dependencies + leaves_list.select!(&method(:installed_on_request?)) if args.installed_on_request? + leaves_list.select!(&method(:installed_as_dependency?)) if args.installed_as_dependency? + + leaves_list.map(&:full_name) + .sort + .each(&method(:puts)) + end + + private + + def installed_on_request?(formula) + Tab.for_keg(formula.any_installed_keg).installed_on_request + end + + def installed_as_dependency?(formula) + Tab.for_keg(formula.any_installed_keg).installed_as_dependency + end end end - - def installed_on_request?(formula) - Tab.for_keg(formula.any_installed_keg).installed_on_request - end - - def installed_as_dependency?(formula) - Tab.for_keg(formula.any_installed_keg).installed_as_dependency - end - - def leaves - args = leaves_args.parse - - leaves_list = Formula.installed - Formula.installed.flat_map(&:runtime_formula_dependencies) - casks_runtime_dependencies = Cask::Caskroom.casks.flat_map do |cask| - CaskDependent.new(cask).runtime_dependencies.map(&:to_formula) - end - leaves_list -= casks_runtime_dependencies - leaves_list.select!(&method(:installed_on_request?)) if args.installed_on_request? - leaves_list.select!(&method(:installed_as_dependency?)) if args.installed_as_dependency? - - leaves_list.map(&:full_name) - .sort - .each(&method(:puts)) - end end diff --git a/Library/Homebrew/cmd/link.rb b/Library/Homebrew/cmd/link.rb index 8e63c1a0ba..23440f1c35 100644 --- a/Library/Homebrew/cmd/link.rb +++ b/Library/Homebrew/cmd/link.rb @@ -1,135 +1,136 @@ # typed: true # frozen_string_literal: true +require "abstract_command" require "caveats" -require "cli/parser" require "unlink" module Homebrew - module_function - - sig { returns(CLI::Parser) } - def link_args - Homebrew::CLI::Parser.new do - description <<~EOS - Symlink all of 's installed files into Homebrew's prefix. - This is done automatically when you install formulae but can be useful - for manual installations. - EOS - switch "--overwrite", - description: "Delete files that already exist in the prefix while linking." - switch "-n", "--dry-run", - description: "List files which would be linked or deleted by " \ - "`brew link --overwrite` without actually linking or deleting any files." - switch "-f", "--force", - description: "Allow keg-only formulae to be linked." - switch "--HEAD", - description: "Link the HEAD version of the formula if it is installed." - - named_args :installed_formula, min: 1 - end - end - - def link - args = link_args.parse - - options = { - overwrite: args.overwrite?, - dry_run: args.dry_run?, - verbose: args.verbose?, - } - - kegs = if args.HEAD? - args.named.to_kegs.group_by(&:name).filter_map do |name, resolved_kegs| - head_keg = resolved_kegs.find { |keg| keg.version.head? } - next head_keg if head_keg.present? - - opoo <<~EOS - No HEAD keg installed for #{name} - To install, run: - brew install --HEAD #{name} + module Cmd + class Link < AbstractCommand + cmd_args do + description <<~EOS + Symlink all of 's installed files into Homebrew's prefix. + This is done automatically when you install formulae but can be useful + for manual installations. EOS - end - else - args.named.to_latest_kegs - end + switch "--overwrite", + description: "Delete files that already exist in the prefix while linking." + switch "-n", "--dry-run", + description: "List files which would be linked or deleted by " \ + "`brew link --overwrite` without actually linking or deleting any files." + switch "-f", "--force", + description: "Allow keg-only formulae to be linked." + switch "--HEAD", + description: "Link the HEAD version of the formula if it is installed." - kegs.freeze.each do |keg| - keg_only = Formulary.keg_only?(keg.rack) - - if keg.linked? - opoo "Already linked: #{keg}" - name_and_flag = "#{"--HEAD " if args.HEAD?}#{"--force " if keg_only}#{keg.name}" - puts <<~EOS - To relink, run: - brew unlink #{keg.name} && brew link #{name_and_flag} - EOS - next + named_args :installed_formula, min: 1 end - if args.dry_run? - if args.overwrite? - puts "Would remove:" + sig { override.void } + def run + options = { + overwrite: args.overwrite?, + dry_run: args.dry_run?, + verbose: args.verbose?, + } + + kegs = if args.HEAD? + args.named.to_kegs.group_by(&:name).filter_map do |name, resolved_kegs| + head_keg = resolved_kegs.find { |keg| keg.version.head? } + next head_keg if head_keg.present? + + opoo <<~EOS + No HEAD keg installed for #{name} + To install, run: + brew install --HEAD #{name} + EOS + end else - puts "Would link:" - end - keg.link(**options) - puts_keg_only_path_message(keg) if keg_only - next - end - - formula = begin - keg.to_formula - rescue FormulaUnavailableError - # Not all kegs may belong to formulae - nil - end - - if keg_only - if HOMEBREW_PREFIX.to_s == HOMEBREW_DEFAULT_PREFIX && formula.present? && formula.keg_only_reason.by_macos? - caveats = Caveats.new(formula) - opoo <<~EOS - Refusing to link macOS provided/shadowed software: #{keg.name} - #{caveats.keg_only_text(skip_reason: true).strip} - EOS - next + args.named.to_latest_kegs end - if !args.force? && (formula.blank? || !formula.keg_only_reason.versioned_formula?) - opoo "#{keg.name} is keg-only and must be linked with `--force`." - puts_keg_only_path_message(keg) - next + kegs.freeze.each do |keg| + keg_only = Formulary.keg_only?(keg.rack) + + if keg.linked? + opoo "Already linked: #{keg}" + name_and_flag = "#{"--HEAD " if args.HEAD?}#{"--force " if keg_only}#{keg.name}" + puts <<~EOS + To relink, run: + brew unlink #{keg.name} && brew link #{name_and_flag} + EOS + next + end + + if args.dry_run? + if args.overwrite? + puts "Would remove:" + else + puts "Would link:" + end + keg.link(**options) + puts_keg_only_path_message(keg) if keg_only + next + end + + formula = begin + keg.to_formula + rescue FormulaUnavailableError + # Not all kegs may belong to formulae + nil + end + + if keg_only + if HOMEBREW_PREFIX.to_s == HOMEBREW_DEFAULT_PREFIX && formula.present? && + formula.keg_only_reason.by_macos? + caveats = Caveats.new(formula) + opoo <<~EOS + Refusing to link macOS provided/shadowed software: #{keg.name} + #{caveats.keg_only_text(skip_reason: true).strip} + EOS + next + end + + if !args.force? && (formula.blank? || !formula.keg_only_reason.versioned_formula?) + opoo "#{keg.name} is keg-only and must be linked with `--force`." + puts_keg_only_path_message(keg) + next + end + end + + Unlink.unlink_versioned_formulae(formula, verbose: args.verbose?) if formula + + keg.lock do + print "Linking #{keg}... " + puts if args.verbose? + + begin + n = keg.link(**options) + rescue Keg::LinkError + puts + raise + else + puts "#{n} symlinks created." + end + + puts_keg_only_path_message(keg) if keg_only && !Homebrew::EnvConfig.developer? + end end end - Unlink.unlink_versioned_formulae(formula, verbose: args.verbose?) if formula + private - keg.lock do - print "Linking #{keg}... " - puts if args.verbose? + def puts_keg_only_path_message(keg) + bin = keg/"bin" + sbin = keg/"sbin" + return if !bin.directory? && !sbin.directory? - begin - n = keg.link(**options) - rescue Keg::LinkError - puts - raise - else - puts "#{n} symlinks created." - end - - puts_keg_only_path_message(keg) if keg_only && !Homebrew::EnvConfig.developer? + opt = HOMEBREW_PREFIX/"opt/#{keg.name}" + puts "\nIf you need to have this software first in your PATH instead consider running:" + puts " #{Utils::Shell.prepend_path_in_profile(opt/"bin")}" if bin.directory? + puts " #{Utils::Shell.prepend_path_in_profile(opt/"sbin")}" if sbin.directory? end end end - - def puts_keg_only_path_message(keg) - bin = keg/"bin" - sbin = keg/"sbin" - return if !bin.directory? && !sbin.directory? - - opt = HOMEBREW_PREFIX/"opt/#{keg.name}" - puts "\nIf you need to have this software first in your PATH instead consider running:" - puts " #{Utils::Shell.prepend_path_in_profile(opt/"bin")}" if bin.directory? - puts " #{Utils::Shell.prepend_path_in_profile(opt/"sbin")}" if sbin.directory? - end end diff --git a/Library/Homebrew/commands.rb b/Library/Homebrew/commands.rb index d44418a0ce..2afbd84c6a 100644 --- a/Library/Homebrew/commands.rb +++ b/Library/Homebrew/commands.rb @@ -177,7 +177,10 @@ module Commands external_commands_file.atomic_write("#{external_commands.sort.join("\n")}\n") end + sig { params(command: String).returns(T.nilable(T::Array[[String, String]])) } def self.command_options(command) + return if command == "help" + path = self.path(command) return if path.blank? @@ -207,7 +210,7 @@ module Commands if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(path)) if short - cmd_parser.description.split(DESCRIPTION_SPLITTING_PATTERN).first + cmd_parser.description&.split(DESCRIPTION_SPLITTING_PATTERN)&.first else cmd_parser.description end diff --git a/Library/Homebrew/dev-cmd/audit.rb b/Library/Homebrew/dev-cmd/audit.rb index 6e29e4fa37..a7bbe9597e 100644 --- a/Library/Homebrew/dev-cmd/audit.rb +++ b/Library/Homebrew/dev-cmd/audit.rb @@ -15,7 +15,6 @@ require "style" require "date" require "missing_formula" require "digest" -require "cli/parser" require "json" require "formula_auditor" require "tap_auditor" diff --git a/Library/Homebrew/dev-cmd/bottle.rb b/Library/Homebrew/dev-cmd/bottle.rb index 14462a0d2c..1d748ec507 100644 --- a/Library/Homebrew/dev-cmd/bottle.rb +++ b/Library/Homebrew/dev-cmd/bottle.rb @@ -8,7 +8,6 @@ require "utils/bottles" require "tab" require "keg" require "formula_versions" -require "cli/parser" require "utils/inreplace" require "erb" require "utils/gzip" @@ -108,6 +107,110 @@ module Homebrew end end + def generate_sha256_line(tag, digest, cellar, tag_column, digest_column) + line = "sha256 " + tag_column += line.length + digest_column += line.length + if cellar.is_a?(Symbol) + line += "cellar: :#{cellar}," + elsif cellar_parameter_needed?(cellar) + line += %Q(cellar: "#{cellar}",) + end + line += " " * (tag_column - line.length) + line += "#{tag}:" + line += " " * (digest_column - line.length) + %Q(#{line}"#{digest}") + end + + def bottle_output(bottle, root_url_using) + cellars = bottle.checksums.filter_map do |checksum| + cellar = checksum["cellar"] + next unless cellar_parameter_needed? cellar + + case cellar + when String + %Q("#{cellar}") + when Symbol + ":#{cellar}" + end + end + tag_column = cellars.empty? ? 0 : "cellar: #{cellars.max_by(&:length)}, ".length + + tags = bottle.checksums.map { |checksum| checksum["tag"] } + # Start where the tag ends, add the max length of the tag, add two for the `: ` + digest_column = tag_column + tags.max_by(&:length).length + 2 + + sha256_lines = bottle.checksums.map do |checksum| + generate_sha256_line(checksum["tag"], checksum["digest"], checksum["cellar"], tag_column, digest_column) + end + erb_binding = bottle.instance_eval { binding } + erb_binding.local_variable_set(:sha256_lines, sha256_lines) + erb_binding.local_variable_set(:root_url_using, root_url_using) + erb = ERB.new BOTTLE_ERB + erb.result(erb_binding).gsub(/^\s*$\n/, "") + end + + def parse_json_files(filenames) + filenames.map do |filename| + JSON.parse(File.read(filename)) + end + end + + def merge_json_files(json_files) + json_files.reduce({}) do |hash, json_file| + json_file.each_value do |json_hash| + json_bottle = json_hash["bottle"] + cellar = json_bottle.delete("cellar") + json_bottle["tags"].each_value do |json_platform| + json_platform["cellar"] ||= cellar + end + end + hash.deep_merge(json_file) + end + end + + def merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash) + mismatches = [] + checksums = [] + + new_values = { + root_url: new_bottle_hash["root_url"], + rebuild: new_bottle_hash["rebuild"], + } + + skip_keys = [:sha256, :cellar] + old_keys.each do |key| + next if skip_keys.include?(key) + + old_value = old_bottle_spec.send(key).to_s + new_value = new_values[key].to_s + + next if old_value.present? && new_value == old_value + + mismatches << "#{key}: old: #{old_value.inspect}, new: #{new_value.inspect}" + end + + return [mismatches, checksums] if old_keys.exclude? :sha256 + + old_bottle_spec.collector.each_tag do |tag| + old_tag_spec = old_bottle_spec.collector.specification_for(tag) + old_hexdigest = old_tag_spec.checksum.hexdigest + old_cellar = old_tag_spec.cellar + new_value = new_bottle_hash.dig("tags", tag.to_s) + if new_value.present? && new_value["sha256"] != old_hexdigest + mismatches << "sha256 #{tag}: old: #{old_hexdigest.inspect}, new: #{new_value["sha256"].inspect}" + elsif new_value.present? && new_value["cellar"] != old_cellar.to_s + mismatches << "cellar #{tag}: old: #{old_cellar.to_s.inspect}, new: #{new_value["cellar"].inspect}" + else + checksums << { cellar: old_cellar, tag.to_sym => old_hexdigest } + end + end + + [mismatches, checksums] + end + + private + def keg_contain?(string, keg, ignores, formula_and_runtime_deps_names = nil) @put_string_exists_header, @put_filenames = nil @@ -186,49 +289,6 @@ module Homebrew cellar.present? && default_cellars.exclude?(cellar) end - def generate_sha256_line(tag, digest, cellar, tag_column, digest_column) - line = "sha256 " - tag_column += line.length - digest_column += line.length - if cellar.is_a?(Symbol) - line += "cellar: :#{cellar}," - elsif cellar_parameter_needed?(cellar) - line += %Q(cellar: "#{cellar}",) - end - line += " " * (tag_column - line.length) - line += "#{tag}:" - line += " " * (digest_column - line.length) - %Q(#{line}"#{digest}") - end - - def bottle_output(bottle, root_url_using) - cellars = bottle.checksums.filter_map do |checksum| - cellar = checksum["cellar"] - next unless cellar_parameter_needed? cellar - - case cellar - when String - %Q("#{cellar}") - when Symbol - ":#{cellar}" - end - end - tag_column = cellars.empty? ? 0 : "cellar: #{cellars.max_by(&:length)}, ".length - - tags = bottle.checksums.map { |checksum| checksum["tag"] } - # Start where the tag ends, add the max length of the tag, add two for the `: ` - digest_column = tag_column + tags.max_by(&:length).length + 2 - - sha256_lines = bottle.checksums.map do |checksum| - generate_sha256_line(checksum["tag"], checksum["digest"], checksum["cellar"], tag_column, digest_column) - end - erb_binding = bottle.instance_eval { binding } - erb_binding.local_variable_set(:sha256_lines, sha256_lines) - erb_binding.local_variable_set(:root_url_using, root_url_using) - erb = ERB.new BOTTLE_ERB - erb.result(erb_binding).gsub(/^\s*$\n/, "") - end - def sudo_purge return unless ENV["HOMEBREW_BOTTLE_SUDO_PURGE"] @@ -601,25 +661,6 @@ module Homebrew json_path.write(JSON.pretty_generate(json)) end - def parse_json_files(filenames) - filenames.map do |filename| - JSON.parse(File.read(filename)) - end - end - - def merge_json_files(json_files) - json_files.reduce({}) do |hash, json_file| - json_file.each_value do |json_hash| - json_bottle = json_hash["bottle"] - cellar = json_bottle.delete("cellar") - json_bottle["tags"].each_value do |json_platform| - json_platform["cellar"] ||= cellar - end - end - hash.deep_merge(json_file) - end - end - def merge bottles_hash = merge_json_files(parse_json_files(args.named)) @@ -774,46 +815,6 @@ module Homebrew end end - def merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash) - mismatches = [] - checksums = [] - - new_values = { - root_url: new_bottle_hash["root_url"], - rebuild: new_bottle_hash["rebuild"], - } - - skip_keys = [:sha256, :cellar] - old_keys.each do |key| - next if skip_keys.include?(key) - - old_value = old_bottle_spec.send(key).to_s - new_value = new_values[key].to_s - - next if old_value.present? && new_value == old_value - - mismatches << "#{key}: old: #{old_value.inspect}, new: #{new_value.inspect}" - end - - return [mismatches, checksums] if old_keys.exclude? :sha256 - - old_bottle_spec.collector.each_tag do |tag| - old_tag_spec = old_bottle_spec.collector.specification_for(tag) - old_hexdigest = old_tag_spec.checksum.hexdigest - old_cellar = old_tag_spec.cellar - new_value = new_bottle_hash.dig("tags", tag.to_s) - if new_value.present? && new_value["sha256"] != old_hexdigest - mismatches << "sha256 #{tag}: old: #{old_hexdigest.inspect}, new: #{new_value["sha256"].inspect}" - elsif new_value.present? && new_value["cellar"] != old_cellar.to_s - mismatches << "cellar #{tag}: old: #{old_cellar.to_s.inspect}, new: #{new_value["cellar"].inspect}" - else - checksums << { cellar: old_cellar, tag.to_sym => old_hexdigest } - end - end - - [mismatches, checksums] - end - def old_checksums(formula, formula_ast, bottle_hash) bottle_node = formula_ast.bottle_block return if bottle_node.nil? diff --git a/Library/Homebrew/dev-cmd/bump-cask-pr.rb b/Library/Homebrew/dev-cmd/bump-cask-pr.rb index 4f520d67c1..a4552cc767 100644 --- a/Library/Homebrew/dev-cmd/bump-cask-pr.rb +++ b/Library/Homebrew/dev-cmd/bump-cask-pr.rb @@ -5,7 +5,6 @@ require "abstract_command" require "bump_version_parser" require "cask" require "cask/download" -require "cli/parser" require "utils/tar" module Homebrew diff --git a/Library/Homebrew/dev-cmd/bump-formula-pr.rb b/Library/Homebrew/dev-cmd/bump-formula-pr.rb index 2c972aab94..fdbaf0bd4e 100644 --- a/Library/Homebrew/dev-cmd/bump-formula-pr.rb +++ b/Library/Homebrew/dev-cmd/bump-formula-pr.rb @@ -4,7 +4,6 @@ require "abstract_command" require "fileutils" require "formula" -require "cli/parser" require "utils/pypi" require "utils/tar" diff --git a/Library/Homebrew/dev-cmd/bump-revision.rb b/Library/Homebrew/dev-cmd/bump-revision.rb index 2bbf5b085f..4f83e95d86 100644 --- a/Library/Homebrew/dev-cmd/bump-revision.rb +++ b/Library/Homebrew/dev-cmd/bump-revision.rb @@ -3,7 +3,6 @@ require "abstract_command" require "formula" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb b/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb index 47d064ed64..9014f5af52 100644 --- a/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb +++ b/Library/Homebrew/dev-cmd/bump-unversioned-casks.rb @@ -5,7 +5,6 @@ require "timeout" require "cask/download" require "cask/installer" require "cask/cask_loader" -require "cli/parser" require "system_command" require "tap" require "unversioned_cask_checker" diff --git a/Library/Homebrew/dev-cmd/bump.rb b/Library/Homebrew/dev-cmd/bump.rb index 3de0bd860a..d28eeb62ba 100644 --- a/Library/Homebrew/dev-cmd/bump.rb +++ b/Library/Homebrew/dev-cmd/bump.rb @@ -3,7 +3,6 @@ require "abstract_command" require "bump_version_parser" -require "cli/parser" require "livecheck/livecheck" module Homebrew diff --git a/Library/Homebrew/dev-cmd/cat.rb b/Library/Homebrew/dev-cmd/cat.rb index 8b7bbed67a..a5e8180ca6 100644 --- a/Library/Homebrew/dev-cmd/cat.rb +++ b/Library/Homebrew/dev-cmd/cat.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "fileutils" module Homebrew diff --git a/Library/Homebrew/dev-cmd/command.rb b/Library/Homebrew/dev-cmd/command.rb index 54784a48f0..595e00e4b8 100644 --- a/Library/Homebrew/dev-cmd/command.rb +++ b/Library/Homebrew/dev-cmd/command.rb @@ -3,7 +3,6 @@ require "abstract_command" require "commands" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/contributions.rb b/Library/Homebrew/dev-cmd/contributions.rb index 1e0359a24e..efb00fcd34 100644 --- a/Library/Homebrew/dev-cmd/contributions.rb +++ b/Library/Homebrew/dev-cmd/contributions.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "csv" module Homebrew diff --git a/Library/Homebrew/dev-cmd/create.rb b/Library/Homebrew/dev-cmd/create.rb index 75b8a79ac1..089f65a9c5 100644 --- a/Library/Homebrew/dev-cmd/create.rb +++ b/Library/Homebrew/dev-cmd/create.rb @@ -4,7 +4,6 @@ require "formula" require "formula_creator" require "missing_formula" -require "cli/parser" require "utils/pypi" require "cask/cask_loader" diff --git a/Library/Homebrew/dev-cmd/determine-test-runners.rb b/Library/Homebrew/dev-cmd/determine-test-runners.rb index 72a63260a0..cba08ded8e 100644 --- a/Library/Homebrew/dev-cmd/determine-test-runners.rb +++ b/Library/Homebrew/dev-cmd/determine-test-runners.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "test_runner_formula" require "github_runner_matrix" diff --git a/Library/Homebrew/dev-cmd/dispatch-build-bottle.rb b/Library/Homebrew/dev-cmd/dispatch-build-bottle.rb index 29fd01138b..6ec3f3ecc3 100644 --- a/Library/Homebrew/dev-cmd/dispatch-build-bottle.rb +++ b/Library/Homebrew/dev-cmd/dispatch-build-bottle.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/github" module Homebrew diff --git a/Library/Homebrew/dev-cmd/edit.rb b/Library/Homebrew/dev-cmd/edit.rb index 1892d67502..cbea9d7320 100644 --- a/Library/Homebrew/dev-cmd/edit.rb +++ b/Library/Homebrew/dev-cmd/edit.rb @@ -3,7 +3,6 @@ require "abstract_command" require "formula" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/extract.rb b/Library/Homebrew/dev-cmd/extract.rb index f77505a323..3735541c8a 100644 --- a/Library/Homebrew/dev-cmd/extract.rb +++ b/Library/Homebrew/dev-cmd/extract.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/git" require "formulary" require "software_spec" diff --git a/Library/Homebrew/dev-cmd/formula.rb b/Library/Homebrew/dev-cmd/formula.rb index e0189b4b81..47b0e9783e 100644 --- a/Library/Homebrew/dev-cmd/formula.rb +++ b/Library/Homebrew/dev-cmd/formula.rb @@ -3,7 +3,6 @@ require "abstract_command" require "formula" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/generate-cask-api.rb b/Library/Homebrew/dev-cmd/generate-cask-api.rb index 584f46a6a7..9985ac727b 100644 --- a/Library/Homebrew/dev-cmd/generate-cask-api.rb +++ b/Library/Homebrew/dev-cmd/generate-cask-api.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "cask/cask" require "fileutils" require "formula" diff --git a/Library/Homebrew/dev-cmd/generate-formula-api.rb b/Library/Homebrew/dev-cmd/generate-formula-api.rb index 0036d25bec..5a06a0e0ed 100644 --- a/Library/Homebrew/dev-cmd/generate-formula-api.rb +++ b/Library/Homebrew/dev-cmd/generate-formula-api.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "fileutils" require "formula" diff --git a/Library/Homebrew/dev-cmd/install-bundler-gems.rb b/Library/Homebrew/dev-cmd/install-bundler-gems.rb index 6ac110609d..a4e5956bce 100644 --- a/Library/Homebrew/dev-cmd/install-bundler-gems.rb +++ b/Library/Homebrew/dev-cmd/install-bundler-gems.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/irb.rb b/Library/Homebrew/dev-cmd/irb.rb index 9da9a012fa..18fcc2e010 100644 --- a/Library/Homebrew/dev-cmd/irb.rb +++ b/Library/Homebrew/dev-cmd/irb.rb @@ -4,7 +4,6 @@ require "abstract_command" require "formulary" require "cask/cask_loader" -require "cli/parser" class String def f(*args) diff --git a/Library/Homebrew/dev-cmd/linkage.rb b/Library/Homebrew/dev-cmd/linkage.rb index 233b028ab8..23462cc98a 100644 --- a/Library/Homebrew/dev-cmd/linkage.rb +++ b/Library/Homebrew/dev-cmd/linkage.rb @@ -4,7 +4,6 @@ require "abstract_command" require "cache_store" require "linkage_checker" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/livecheck.rb b/Library/Homebrew/dev-cmd/livecheck.rb index c02deece81..6cad963d55 100644 --- a/Library/Homebrew/dev-cmd/livecheck.rb +++ b/Library/Homebrew/dev-cmd/livecheck.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "formula" require "livecheck/livecheck" require "livecheck/strategy" diff --git a/Library/Homebrew/dev-cmd/pr-automerge.rb b/Library/Homebrew/dev-cmd/pr-automerge.rb index 6995de1332..2cfb1b989b 100644 --- a/Library/Homebrew/dev-cmd/pr-automerge.rb +++ b/Library/Homebrew/dev-cmd/pr-automerge.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/github" module Homebrew diff --git a/Library/Homebrew/dev-cmd/pr-publish.rb b/Library/Homebrew/dev-cmd/pr-publish.rb index a6b83183c4..00ba5ca338 100644 --- a/Library/Homebrew/dev-cmd/pr-publish.rb +++ b/Library/Homebrew/dev-cmd/pr-publish.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/github" module Homebrew diff --git a/Library/Homebrew/dev-cmd/pr-pull.rb b/Library/Homebrew/dev-cmd/pr-pull.rb index d6b714819f..da8444543d 100644 --- a/Library/Homebrew/dev-cmd/pr-pull.rb +++ b/Library/Homebrew/dev-cmd/pr-pull.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "fileutils" require "utils/github" require "utils/github/artifacts" diff --git a/Library/Homebrew/dev-cmd/pr-upload.rb b/Library/Homebrew/dev-cmd/pr-upload.rb index 540580aae8..7711a141a7 100644 --- a/Library/Homebrew/dev-cmd/pr-upload.rb +++ b/Library/Homebrew/dev-cmd/pr-upload.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "formula" require "github_packages" require "github_releases" diff --git a/Library/Homebrew/dev-cmd/prof.rb b/Library/Homebrew/dev-cmd/prof.rb index 1242b3eace..c0389d60ef 100644 --- a/Library/Homebrew/dev-cmd/prof.rb +++ b/Library/Homebrew/dev-cmd/prof.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/release.rb b/Library/Homebrew/dev-cmd/release.rb index 75fbab3511..badf7ca582 100644 --- a/Library/Homebrew/dev-cmd/release.rb +++ b/Library/Homebrew/dev-cmd/release.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/ruby.rb b/Library/Homebrew/dev-cmd/ruby.rb index db071c75a6..db9c9be93c 100644 --- a/Library/Homebrew/dev-cmd/ruby.rb +++ b/Library/Homebrew/dev-cmd/ruby.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/sh.rb b/Library/Homebrew/dev-cmd/sh.rb index 6b295dfd93..e4c35c85c6 100644 --- a/Library/Homebrew/dev-cmd/sh.rb +++ b/Library/Homebrew/dev-cmd/sh.rb @@ -4,7 +4,6 @@ require "abstract_command" require "extend/ENV" require "formula" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/style.rb b/Library/Homebrew/dev-cmd/style.rb index 58de7af85b..3182430c86 100644 --- a/Library/Homebrew/dev-cmd/style.rb +++ b/Library/Homebrew/dev-cmd/style.rb @@ -5,7 +5,6 @@ require "abstract_command" require "json" require "open3" require "style" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/tap-new.rb b/Library/Homebrew/dev-cmd/tap-new.rb index 1c36553ec0..bb8099a07b 100644 --- a/Library/Homebrew/dev-cmd/tap-new.rb +++ b/Library/Homebrew/dev-cmd/tap-new.rb @@ -4,7 +4,6 @@ require "abstract_command" require "fileutils" require "tap" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/test.rb b/Library/Homebrew/dev-cmd/test.rb index b7f2b8dc2f..de847d0ae0 100644 --- a/Library/Homebrew/dev-cmd/test.rb +++ b/Library/Homebrew/dev-cmd/test.rb @@ -5,7 +5,6 @@ require "abstract_command" require "extend/ENV" require "sandbox" require "timeout" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/tests.rb b/Library/Homebrew/dev-cmd/tests.rb index fb948c1013..16c30ef90f 100644 --- a/Library/Homebrew/dev-cmd/tests.rb +++ b/Library/Homebrew/dev-cmd/tests.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "fileutils" require "system_command" diff --git a/Library/Homebrew/dev-cmd/typecheck.rb b/Library/Homebrew/dev-cmd/typecheck.rb index 2d1c19ffeb..40f8b052ef 100644 --- a/Library/Homebrew/dev-cmd/typecheck.rb +++ b/Library/Homebrew/dev-cmd/typecheck.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "fileutils" module Homebrew diff --git a/Library/Homebrew/dev-cmd/unbottled.rb b/Library/Homebrew/dev-cmd/unbottled.rb index df18a3a21f..359724dfbb 100644 --- a/Library/Homebrew/dev-cmd/unbottled.rb +++ b/Library/Homebrew/dev-cmd/unbottled.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "formula" require "api" require "os/mac/xcode" diff --git a/Library/Homebrew/dev-cmd/unpack.rb b/Library/Homebrew/dev-cmd/unpack.rb index dcf50f2899..8ecc1c1f2e 100644 --- a/Library/Homebrew/dev-cmd/unpack.rb +++ b/Library/Homebrew/dev-cmd/unpack.rb @@ -5,7 +5,6 @@ require "abstract_command" require "fileutils" require "stringio" require "formula" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/dev-cmd/update-license-data.rb b/Library/Homebrew/dev-cmd/update-license-data.rb index c68fe365e7..5d2ed16ac7 100644 --- a/Library/Homebrew/dev-cmd/update-license-data.rb +++ b/Library/Homebrew/dev-cmd/update-license-data.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/spdx" require "system_command" diff --git a/Library/Homebrew/dev-cmd/update-maintainers.rb b/Library/Homebrew/dev-cmd/update-maintainers.rb index 1bb8734f55..e21aef44e2 100644 --- a/Library/Homebrew/dev-cmd/update-maintainers.rb +++ b/Library/Homebrew/dev-cmd/update-maintainers.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/github" require "manpages" require "system_command" diff --git a/Library/Homebrew/dev-cmd/update-python-resources.rb b/Library/Homebrew/dev-cmd/update-python-resources.rb index 50fc287cbe..865103698f 100644 --- a/Library/Homebrew/dev-cmd/update-python-resources.rb +++ b/Library/Homebrew/dev-cmd/update-python-resources.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/pypi" module Homebrew diff --git a/Library/Homebrew/dev-cmd/update-sponsors.rb b/Library/Homebrew/dev-cmd/update-sponsors.rb index 8d213a503f..248a290413 100644 --- a/Library/Homebrew/dev-cmd/update-sponsors.rb +++ b/Library/Homebrew/dev-cmd/update-sponsors.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "utils/github" require "system_command" diff --git a/Library/Homebrew/dev-cmd/update-test.rb b/Library/Homebrew/dev-cmd/update-test.rb index 0c2c42a131..c2b637f686 100644 --- a/Library/Homebrew/dev-cmd/update-test.rb +++ b/Library/Homebrew/dev-cmd/update-test.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" require "fileutils" module Homebrew diff --git a/Library/Homebrew/dev-cmd/vendor-gems.rb b/Library/Homebrew/dev-cmd/vendor-gems.rb index 063fb9242e..792a218be1 100644 --- a/Library/Homebrew/dev-cmd/vendor-gems.rb +++ b/Library/Homebrew/dev-cmd/vendor-gems.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "abstract_command" -require "cli/parser" module Homebrew module DevCmd diff --git a/Library/Homebrew/test/cmd/analytics_spec.rb b/Library/Homebrew/test/cmd/analytics_spec.rb index c0cc6c75a2..bd354451e9 100644 --- a/Library/Homebrew/test/cmd/analytics_spec.rb +++ b/Library/Homebrew/test/cmd/analytics_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/analytics" require "cmd/shared_examples/args_parse" -RSpec.describe "brew analytics" do +RSpec.describe Homebrew::Cmd::Analytics do it_behaves_like "parseable arguments" it "when HOMEBREW_NO_ANALYTICS is unset is disabled after running `brew analytics off`", :integration_test do diff --git a/Library/Homebrew/test/cmd/autoremove_spec.rb b/Library/Homebrew/test/cmd/autoremove_spec.rb index 86aea1e75c..db0a80e621 100644 --- a/Library/Homebrew/test/cmd/autoremove_spec.rb +++ b/Library/Homebrew/test/cmd/autoremove_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/autoremove" require "cmd/shared_examples/args_parse" -RSpec.describe "brew autoremove" do +RSpec.describe Homebrew::Cmd::Autoremove do it_behaves_like "parseable arguments" describe "integration test" do diff --git a/Library/Homebrew/test/cmd/cleanup_spec.rb b/Library/Homebrew/test/cmd/cleanup_spec.rb index 170668f417..098dc356d7 100644 --- a/Library/Homebrew/test/cmd/cleanup_spec.rb +++ b/Library/Homebrew/test/cmd/cleanup_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/cleanup" require "cmd/shared_examples/args_parse" -RSpec.describe "brew cleanup" do +RSpec.describe Homebrew::Cmd::CleanupCmd do before do FileUtils.mkdir_p HOMEBREW_LIBRARY/"Homebrew/vendor/" FileUtils.touch HOMEBREW_LIBRARY/"Homebrew/vendor/portable-ruby-version" diff --git a/Library/Homebrew/test/cmd/commands_spec.rb b/Library/Homebrew/test/cmd/commands_spec.rb index b49068378d..9f7de67f11 100644 --- a/Library/Homebrew/test/cmd/commands_spec.rb +++ b/Library/Homebrew/test/cmd/commands_spec.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true require "cmd/commands" -require "fileutils" - require "cmd/shared_examples/args_parse" -RSpec.describe "brew commands" do +RSpec.describe Homebrew::Cmd::CommandsCmd do it_behaves_like "parseable arguments" it "prints a list of all available commands", :integration_test do diff --git a/Library/Homebrew/test/cmd/completions_spec.rb b/Library/Homebrew/test/cmd/completions_spec.rb index ab0bd90a63..6cc7cc2496 100644 --- a/Library/Homebrew/test/cmd/completions_spec.rb +++ b/Library/Homebrew/test/cmd/completions_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/completions" require "cmd/shared_examples/args_parse" -RSpec.describe "brew completions" do +RSpec.describe Homebrew::Cmd::CompletionsCmd do it_behaves_like "parseable arguments" it "runs the status subcommand correctly", :integration_test do diff --git a/Library/Homebrew/test/cmd/config_spec.rb b/Library/Homebrew/test/cmd/config_spec.rb index d5621f4541..9abdc63405 100644 --- a/Library/Homebrew/test/cmd/config_spec.rb +++ b/Library/Homebrew/test/cmd/config_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/config" require "cmd/shared_examples/args_parse" -RSpec.describe "brew config" do +RSpec.describe Homebrew::Cmd::Config do it_behaves_like "parseable arguments" it "prints information about the current Homebrew configuration", :integration_test do diff --git a/Library/Homebrew/test/cmd/deps_spec.rb b/Library/Homebrew/test/cmd/deps_spec.rb index aa6277196b..7171ba0303 100644 --- a/Library/Homebrew/test/cmd/deps_spec.rb +++ b/Library/Homebrew/test/cmd/deps_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/deps" require "cmd/shared_examples/args_parse" -RSpec.describe "brew deps" do +RSpec.describe Homebrew::Cmd::Deps do it_behaves_like "parseable arguments" it "outputs all of a Formula's dependencies and their dependencies on separate lines", :integration_test do diff --git a/Library/Homebrew/test/cmd/desc_spec.rb b/Library/Homebrew/test/cmd/desc_spec.rb index 28872ed0c9..0bd9a87840 100644 --- a/Library/Homebrew/test/cmd/desc_spec.rb +++ b/Library/Homebrew/test/cmd/desc_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/desc" require "cmd/shared_examples/args_parse" -RSpec.describe "brew desc" do +RSpec.describe Homebrew::Cmd::Desc do it_behaves_like "parseable arguments" it "shows a given Formula's description", :integration_test do diff --git a/Library/Homebrew/test/cmd/developer_spec.rb b/Library/Homebrew/test/cmd/developer_spec.rb index 1b6bc17622..fcaefacdf8 100644 --- a/Library/Homebrew/test/cmd/developer_spec.rb +++ b/Library/Homebrew/test/cmd/developer_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +require "cmd/developer" require "cmd/shared_examples/args_parse" -RSpec.describe "brew developer" do +RSpec.describe Homebrew::Cmd::Developer do it_behaves_like "parseable arguments" end diff --git a/Library/Homebrew/test/cmd/docs_spec.rb b/Library/Homebrew/test/cmd/docs_spec.rb index 5379c21717..6262e54cf4 100644 --- a/Library/Homebrew/test/cmd/docs_spec.rb +++ b/Library/Homebrew/test/cmd/docs_spec.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -RSpec.describe "brew docs" do +require "cmd/docs" +require "cmd/shared_examples/args_parse" + +RSpec.describe Homebrew::Cmd::Docs do + it_behaves_like "parseable arguments" + it "opens the docs page", :integration_test do expect { brew "docs", "HOMEBREW_BROWSER" => "echo" } .to output("https://docs.brew.sh\n").to_stdout diff --git a/Library/Homebrew/test/cmd/doctor_spec.rb b/Library/Homebrew/test/cmd/doctor_spec.rb index c82dfb3458..ca20e4a470 100644 --- a/Library/Homebrew/test/cmd/doctor_spec.rb +++ b/Library/Homebrew/test/cmd/doctor_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/doctor" require "cmd/shared_examples/args_parse" -RSpec.describe "brew doctor" do +RSpec.describe Homebrew::Cmd::Doctor do it_behaves_like "parseable arguments" specify "check_integration_test", :integration_test do diff --git a/Library/Homebrew/test/cmd/fetch_spec.rb b/Library/Homebrew/test/cmd/fetch_spec.rb index 96c07722d9..7b0cc3de7a 100644 --- a/Library/Homebrew/test/cmd/fetch_spec.rb +++ b/Library/Homebrew/test/cmd/fetch_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/fetch" require "cmd/shared_examples/args_parse" -RSpec.describe "brew fetch" do +RSpec.describe Homebrew::Cmd::FetchCmd do it_behaves_like "parseable arguments" it "downloads the Formula's URL", :integration_test do diff --git a/Library/Homebrew/test/cmd/gist-logs_spec.rb b/Library/Homebrew/test/cmd/gist-logs_spec.rb index 25bb9ff393..98a272251b 100644 --- a/Library/Homebrew/test/cmd/gist-logs_spec.rb +++ b/Library/Homebrew/test/cmd/gist-logs_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +require "cmd/gist-logs" require "cmd/shared_examples/args_parse" -RSpec.describe "brew gist-logs" do +RSpec.describe Homebrew::Cmd::GistLogs do it_behaves_like "parseable arguments" end diff --git a/Library/Homebrew/test/cmd/help_spec.rb b/Library/Homebrew/test/cmd/help_spec.rb index ff82501f5a..1301b5909d 100644 --- a/Library/Homebrew/test/cmd/help_spec.rb +++ b/Library/Homebrew/test/cmd/help_spec.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -RSpec.describe "brew", :integration_test do +require "cmd/help" +require "cmd/shared_examples/args_parse" + +RSpec.describe Homebrew::Cmd::HelpCmd, :integration_test do + it_behaves_like "parseable arguments" + describe "help" do it "prints help for a documented Ruby command" do expect { brew "help", "cat" } diff --git a/Library/Homebrew/test/cmd/home_spec.rb b/Library/Homebrew/test/cmd/home_spec.rb index 65886736bd..5dfe5dde99 100644 --- a/Library/Homebrew/test/cmd/home_spec.rb +++ b/Library/Homebrew/test/cmd/home_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/home" require "cmd/shared_examples/args_parse" -RSpec.describe "brew home" do +RSpec.describe Homebrew::Cmd::Home do let(:testballhome_homepage) do Formula["testballhome"].homepage end diff --git a/Library/Homebrew/test/cmd/info_spec.rb b/Library/Homebrew/test/cmd/info_spec.rb index de636a6e4e..84adab7e9f 100644 --- a/Library/Homebrew/test/cmd/info_spec.rb +++ b/Library/Homebrew/test/cmd/info_spec.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true require "cmd/info" - require "cmd/shared_examples/args_parse" -RSpec.describe "brew info" do +RSpec.describe Homebrew::Cmd::Info do it_behaves_like "parseable arguments" it "prints as json with the --json=v1 flag", :integration_test do @@ -25,23 +24,21 @@ RSpec.describe "brew info" do .and be_a_success end - describe Homebrew do - describe "::github_remote_path" do - let(:remote) { "https://github.com/Homebrew/homebrew-core" } + describe "::github_remote_path" do + let(:remote) { "https://github.com/Homebrew/homebrew-core" } - specify "returns correct URLs" do - expect(described_class.github_remote_path(remote, "Formula/git.rb")) - .to eq("https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/git.rb") + specify "returns correct URLs" do + expect(described_class.new([]).github_remote_path(remote, "Formula/git.rb")) + .to eq("https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/git.rb") - expect(described_class.github_remote_path("#{remote}.git", "Formula/git.rb")) - .to eq("https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/git.rb") + expect(described_class.new([]).github_remote_path("#{remote}.git", "Formula/git.rb")) + .to eq("https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/git.rb") - expect(described_class.github_remote_path("git@github.com:user/repo", "foo.rb")) - .to eq("https://github.com/user/repo/blob/HEAD/foo.rb") + expect(described_class.new([]).github_remote_path("git@github.com:user/repo", "foo.rb")) + .to eq("https://github.com/user/repo/blob/HEAD/foo.rb") - expect(described_class.github_remote_path("https://mywebsite.com", "foo/bar.rb")) - .to eq("https://mywebsite.com/foo/bar.rb") - end + expect(described_class.new([]).github_remote_path("https://mywebsite.com", "foo/bar.rb")) + .to eq("https://mywebsite.com/foo/bar.rb") end end end diff --git a/Library/Homebrew/test/cmd/install_spec.rb b/Library/Homebrew/test/cmd/install_spec.rb index c78c737f50..4772947bf0 100644 --- a/Library/Homebrew/test/cmd/install_spec.rb +++ b/Library/Homebrew/test/cmd/install_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/install" require "cmd/shared_examples/args_parse" -RSpec.describe "brew install" do +RSpec.describe Homebrew::Cmd::InstallCmd do it_behaves_like "parseable arguments" it "installs formulae", :integration_test do diff --git a/Library/Homebrew/test/cmd/leaves_spec.rb b/Library/Homebrew/test/cmd/leaves_spec.rb index 2b6fc51bee..9e00a21ca1 100644 --- a/Library/Homebrew/test/cmd/leaves_spec.rb +++ b/Library/Homebrew/test/cmd/leaves_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/leaves" require "cmd/shared_examples/args_parse" -RSpec.describe "brew leaves" do +RSpec.describe Homebrew::Cmd::Leaves do it_behaves_like "parseable arguments" context "when there are no installed Formulae", :integration_test do diff --git a/Library/Homebrew/test/cmd/link_spec.rb b/Library/Homebrew/test/cmd/link_spec.rb index ea0f99411c..0c4039364e 100644 --- a/Library/Homebrew/test/cmd/link_spec.rb +++ b/Library/Homebrew/test/cmd/link_spec.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true +require "cmd/link" require "cmd/shared_examples/args_parse" -RSpec.describe "brew link" do +RSpec.describe Homebrew::Cmd::Link do it_behaves_like "parseable arguments" it "links a given Formula", :integration_test do diff --git a/Library/Homebrew/test/formula_installer_bottle_spec.rb b/Library/Homebrew/test/formula_installer_bottle_spec.rb index 17ca53d48a..e2197a7a43 100644 --- a/Library/Homebrew/test/formula_installer_bottle_spec.rb +++ b/Library/Homebrew/test/formula_installer_bottle_spec.rb @@ -70,7 +70,7 @@ RSpec.describe FormulaInstaller do # rubocop:disable RSpec/NoExpectationExample specify "basic bottle install" do allow(DevelopmentTools).to receive(:installed?).and_return(false) - Homebrew.install_args.parse(["testball_bottle"]) + Homebrew::Cmd::InstallCmd.new(["testball_bottle"]) temporarily_install_bottle(TestballBottle.new) do |f| test_basic_formula_setup(f) end @@ -79,7 +79,7 @@ RSpec.describe FormulaInstaller do specify "basic bottle install with cellar information on sha256 line" do allow(DevelopmentTools).to receive(:installed?).and_return(false) - Homebrew.install_args.parse(["testball_bottle_cellar"]) + Homebrew::Cmd::InstallCmd.new(["testball_bottle_cellar"]) temporarily_install_bottle(TestballBottleCellar.new) do |f| test_basic_formula_setup(f)