1336 lines
37 KiB
Ruby
Raw Normal View History

# typed: true
# frozen_string_literal: true
require "commands"
require "completions"
require "extend/cachable"
require "description_cache_store"
require "settings"
# A {Tap} is used to extend the formulae provided by Homebrew core.
# Usually, it's synced with a remote Git repository. And it's likely
2018-09-04 09:58:58 +01:00
# a GitHub repository with the name of `user/homebrew-repo`. In such
# cases, `user/repo` will be used as the {#name} of this {Tap}, where
# {#user} represents the GitHub username and {#repo} represents the repository
# name without the leading `homebrew-`.
2015-06-10 15:39:47 +08:00
class Tap
extend Cachable
2019-04-19 21:46:20 +09:00
TAP_DIRECTORY = (HOMEBREW_LIBRARY/"Taps").freeze
2023-04-08 14:10:58 +02:00
HOMEBREW_TAP_CASK_RENAMES_FILE = "cask_renames.json"
2020-11-09 08:39:56 -05:00
HOMEBREW_TAP_FORMULA_RENAMES_FILE = "formula_renames.json"
HOMEBREW_TAP_MIGRATIONS_FILE = "tap_migrations.json"
HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR = "audit_exceptions"
HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR = "style_exceptions"
HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS = "pypi_formula_mappings.json"
2020-11-09 08:39:56 -05:00
TAP_MIGRATIONS_STALE_SECONDS = 86400 # 1 day
2020-11-09 08:39:56 -05:00
HOMEBREW_TAP_JSON_FILES = %W[
#{HOMEBREW_TAP_FORMULA_RENAMES_FILE}
2023-04-08 14:10:58 +02:00
#{HOMEBREW_TAP_CASK_RENAMES_FILE}
2020-11-09 08:39:56 -05:00
#{HOMEBREW_TAP_MIGRATIONS_FILE}
#{HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR}/*.json
#{HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR}/*.json
#{HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS}
2020-11-09 08:39:56 -05:00
].freeze
def self.fetch(*args)
case args.length
when 1
user, repo = args.first.split("/", 2)
when 2
2018-09-17 19:44:12 +02:00
user = args.first
repo = args.second
end
raise "Invalid tap name '#{args.join("/")}'" if [user, repo].any? { |part| part.nil? || part.include?("/") }
# We special case homebrew and linuxbrew so that users don't have to shift in a terminal.
user = user.capitalize if ["homebrew", "linuxbrew"].include? user
2019-02-25 22:56:29 +01:00
repo = repo.sub(HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX, "")
return CoreTap.instance if ["Homebrew", "Linuxbrew"].include?(user) && ["core", "homebrew"].include?(repo)
2023-07-13 19:28:39 +01:00
return CoreCaskTap.instance if user == "Homebrew" && repo == "cask"
cache_key = "#{user}/#{repo}".downcase
cache.fetch(cache_key) { |key| cache[key] = Tap.new(user, repo) }
end
def self.from_path(path)
match = File.expand_path(path).match(HOMEBREW_TAP_PATH_REGEX)
return if match.blank? || match[:user].blank? || match[:repo].blank?
2018-09-17 02:45:00 +02:00
2017-07-29 16:27:54 +02:00
fetch(match[:user], match[:repo])
end
# @private
sig { params(name: String).returns(T.nilable([T.attached_class, String])) }
def self.with_formula_name(name)
return unless (match = name.match(HOMEBREW_TAP_FORMULA_REGEX))
user = T.must(match[:user])
repo = T.must(match[:repo])
name = T.must(match[:name])
# Relative paths are not taps.
return if [user, repo].intersect?([".", ".."])
tap = fetch(user, repo)
[tap, name.downcase]
end
# @private
sig { params(token: String).returns(T.nilable([T.attached_class, String])) }
def self.with_cask_token(token)
return unless (match = token.match(HOMEBREW_TAP_CASK_REGEX))
user = T.must(match[:user])
repo = T.must(match[:repo])
token = T.must(match[:token])
# Relative paths are not taps.
return if [user, repo].intersect?([".", ".."])
tap = fetch(user, repo)
[tap, token.downcase]
end
2023-07-13 19:28:39 +01:00
sig { returns(CoreCaskTap) }
2018-06-09 10:13:28 +02:00
def self.default_cask_tap
odisabled "`Tap.default_cask_tap`", "`CoreCaskTap.instance`"
2023-07-13 19:28:39 +01:00
CoreCaskTap.instance
2018-06-09 10:13:28 +02:00
end
sig { params(force: T::Boolean).returns(T::Boolean) }
def self.install_default_cask_tap_if_necessary(force: false)
odisabled "`Tap.install_default_cask_tap_if_necessary`", "`CoreCaskTap.instance.ensure_installed!`"
2023-07-13 19:28:39 +01:00
false
end
2015-06-10 15:39:47 +08:00
extend Enumerable
2018-09-04 09:58:58 +01:00
# The user name of this {Tap}. Usually, it's the GitHub username of
# this {Tap}'s remote repository.
2015-06-10 15:39:47 +08:00
attr_reader :user
# The repository name of this {Tap} without the leading `homebrew-`.
2015-06-10 15:39:47 +08:00
attr_reader :repo
# The name of this {Tap}. It combines {#user} and {#repo} with a slash.
# {#name} is always in lowercase.
# e.g. `user/repo`
2015-06-10 15:39:47 +08:00
attr_reader :name
# The full name of this {Tap}, including the `homebrew-` prefix.
# It combines {#user} and 'homebrew-'-prefixed {#repo} with a slash.
# e.g. `user/homebrew-repo`
attr_reader :full_name
# The local path to this {Tap}.
# e.g. `/usr/local/Library/Taps/user/homebrew-repo`
2023-04-15 19:06:40 -07:00
sig { returns(Pathname) }
2015-06-10 15:39:47 +08:00
attr_reader :path
2023-04-15 19:06:40 -07:00
# The git repository of this {Tap}.
sig { returns(GitRepository) }
attr_reader :git_repo
2023-04-15 15:06:58 -07:00
# @private
def initialize(user, repo)
@user = user
2015-06-10 15:39:47 +08:00
@repo = repo
@name = "#{@user}/#{@repo}".downcase
@full_name = "#{@user}/homebrew-#{@repo}"
2023-04-15 19:06:40 -07:00
@path = TAP_DIRECTORY/@full_name.downcase
@git_repo = GitRepository.new(@path)
@alias_table = nil
@alias_reverse_table = nil
end
# Clear internal cache.
def clear_cache
@remote = nil
@repo_var_suffix = nil
@formula_dir = nil
2016-08-04 14:37:37 +04:00
@cask_dir = nil
2018-07-25 11:38:19 +02:00
@command_dir = nil
@formula_names = nil
@formula_files = nil
2024-02-23 15:03:33 +01:00
@formula_files_by_name = nil
2023-07-13 19:28:39 +01:00
@cask_files = nil
2024-02-23 15:03:05 +01:00
@cask_files_by_name = nil
@alias_dir = nil
@alias_files = nil
@aliases = nil
@alias_table = nil
@alias_reverse_table = nil
@command_files = nil
@formula_renames = nil
@tap_migrations = nil
@audit_exceptions = nil
@style_exceptions = nil
@pypi_formula_mappings = nil
@config = nil
2021-06-19 00:14:33 +01:00
@spell_checker = nil
remove_instance_variable(:@private) if instance_variable_defined?(:@private)
end
2023-07-13 19:28:39 +01:00
sig { void }
def ensure_installed!
return if installed?
install
end
# The remote path to this {Tap}.
# e.g. `https://github.com/user/homebrew-repo`
def remote
return default_remote unless installed?
2018-09-17 02:45:00 +02:00
2023-04-15 18:13:38 -07:00
@remote ||= git_repo.origin_url
2015-06-10 15:39:47 +08:00
end
# The remote repository name of this {Tap}.
# e.g. `user/homebrew-repo`
def remote_repo
return unless remote
@remote_repo ||= remote.delete_prefix("https://github.com/")
.delete_prefix("git@github.com:")
.delete_suffix(".git")
end
# The default remote path to this {Tap}.
2020-10-20 12:03:48 +02:00
sig { returns(String) }
def default_remote
"https://github.com/#{full_name}"
end
# @private
sig { returns(String) }
def repo_var_suffix
@repo_var_suffix ||= path.to_s
.delete_prefix(TAP_DIRECTORY.to_s)
.tr("^A-Za-z0-9", "_")
.upcase
2018-05-25 16:21:37 +02:00
end
# True if this {Tap} is a Git repository.
def git?
2023-04-15 19:06:40 -07:00
git_repo.git_repo?
end
# Git branch for this {Tap}.
2017-09-27 16:32:13 -04:00
def git_branch
raise TapUnavailableError, name unless installed?
2018-09-17 02:45:00 +02:00
2023-04-15 18:13:38 -07:00
git_repo.branch_name
2017-09-27 16:32:13 -04:00
end
# Git HEAD for this {Tap}.
def git_head
raise TapUnavailableError, name unless installed?
2018-09-17 02:45:00 +02:00
2023-04-15 18:13:38 -07:00
@git_head ||= git_repo.head_ref
end
# Time since last git commit for this {Tap}.
def git_last_commit
raise TapUnavailableError, name unless installed?
2018-09-17 02:45:00 +02:00
2023-04-15 18:13:38 -07:00
git_repo.last_committed
end
# The issues URL of this {Tap}.
# e.g. `https://github.com/user/homebrew-repo/issues`
2020-10-20 12:03:48 +02:00
sig { returns(T.nilable(String)) }
def issues_url
return if !official? && custom_remote?
2018-09-17 02:45:00 +02:00
"#{default_remote}/issues"
end
2015-06-10 15:39:47 +08:00
def to_s
name
end
# True if this {Tap} is an official Homebrew tap.
2015-06-10 15:39:47 +08:00
def official?
2015-12-06 20:57:28 +08:00
user == "Homebrew"
2015-06-10 15:39:47 +08:00
end
# True if the remote of this {Tap} is a private repository.
2015-06-10 15:39:47 +08:00
def private?
return @private if instance_variable_defined?(:@private)
2018-09-17 02:45:00 +02:00
@private = read_or_set_private_config
end
# {TapConfig} of this {Tap}.
def config
@config ||= begin
raise TapUnavailableError, name unless installed?
2018-09-17 02:45:00 +02:00
TapConfig.new(self)
end
2015-06-10 15:39:47 +08:00
end
# True if this {Tap} has been installed.
2015-06-10 15:39:47 +08:00
def installed?
2015-12-06 20:57:28 +08:00
path.directory?
2015-06-10 15:39:47 +08:00
end
# True if this {Tap} is not a full clone.
def shallow?
(path/".git/shallow").exist?
end
2015-12-07 14:12:57 +08:00
# @private
2020-10-20 12:03:48 +02:00
sig { returns(T::Boolean) }
def core_tap?
2015-12-07 14:12:57 +08:00
false
end
2023-07-13 19:28:39 +01:00
# @private
sig { returns(T::Boolean) }
def core_cask_tap?
false
end
# Install this {Tap}.
2015-11-07 16:25:34 +08:00
#
# @param clone_target [String] If passed, it will be used as the clone remote.
# @param force_auto_update [Boolean, nil] If present, whether to override the
# logic that skips non-GitHub repositories during auto-updates.
# @param quiet [Boolean] If set, suppress all output.
# @param custom_remote [Boolean] If set, change the tap's remote if already installed.
# @param verify [Boolean] If set, verify all the formula, casks and aliases in the tap are valid.
# @param force [Boolean] If set, force core and cask taps to install even under API mode.
def install(quiet: false, clone_target: nil, force_auto_update: nil,
custom_remote: false, verify: false, force: false)
require "descriptions"
2020-08-18 00:23:23 +01:00
require "readall"
2015-11-07 16:25:34 +08:00
if official? && DEPRECATED_OFFICIAL_TAPS.include?(repo)
odie "#{name} was deprecated. This tap is now empty and all its contents were either deleted or migrated."
2020-09-28 20:54:16 +02:00
elsif user == "caskroom" || name == "phinze/cask"
new_repo = (repo == "cask") ? "cask" : "cask-#{repo}"
odie "#{name} was moved. Tap homebrew/#{new_repo} instead."
end
raise TapNoCustomRemoteError, name if custom_remote && clone_target.nil?
requested_remote = clone_target || default_remote
if installed? && !custom_remote
raise TapRemoteMismatchError.new(name, @remote, requested_remote) if clone_target && requested_remote != remote
raise TapAlreadyTappedError, name if force_auto_update.nil? && !shallow?
end
2016-02-25 21:09:50 +08:00
2015-11-07 16:25:34 +08:00
# ensure git is installed
2020-08-23 06:32:26 +02:00
Utils::Git.ensure_installed!
if installed?
if requested_remote != remote # we are sure that clone_target is not nil and custom_remote is true here
fix_remote_configuration(requested_remote: requested_remote, quiet: quiet)
end
unless force_auto_update.nil?
if force_auto_update
config["forceautoupdate"] = force_auto_update
elsif config["forceautoupdate"] == "true"
config.delete("forceautoupdate")
end
2021-05-06 09:56:30 -04:00
return
end
2021-05-06 09:56:30 -04:00
$stderr.ohai "Unshallowing #{name}" if shallow? && !quiet
args = %w[fetch]
# Git throws an error when attempting to unshallow a full clone
args << "--unshallow" if shallow?
args << "-q" if quiet
path.cd { safe_system "git", *args }
return
2023-07-13 19:28:39 +01:00
elsif (core_tap? || core_cask_tap?) && !Homebrew::EnvConfig.no_install_from_api? && !force
odie "Tapping #{name} is no longer typically necessary.\n" \
"Add #{Formatter.option("--force")} if you are sure you need it done."
end
clear_cache
2020-09-17 04:18:13 +05:30
$stderr.ohai "Tapping #{name}" unless quiet
2016-06-20 13:03:27 +01:00
args = %W[clone #{requested_remote} #{path}]
# Override possible user configs like:
# git config --global clone.defaultRemoteName notorigin
args << "--origin=origin"
2016-02-25 21:09:50 +08:00
args << "-q" if quiet
2015-11-07 16:25:34 +08:00
2021-05-14 10:47:52 -04:00
# Override user-set default template
args << "--template="
# prevent fsmonitor from watching this repo
args << "--config" << "core.fsmonitor=false"
2021-05-14 10:47:52 -04:00
2015-11-07 16:25:34 +08:00
begin
safe_system "git", *args
if verify && !Homebrew::EnvConfig.developer? && !Readall.valid_tap?(self, aliases: true)
2020-09-01 14:05:52 +01:00
raise "Cannot tap #{name}: invalid syntax in tap!"
end
2018-09-02 23:30:07 +02:00
rescue Interrupt, RuntimeError
2015-11-07 16:25:34 +08:00
ignore_interrupts do
# wait for git to possibly cleanup the top directory when interrupt happens.
sleep 0.1
2023-04-15 18:13:38 -07:00
FileUtils.rm_rf path
2015-12-06 20:57:28 +08:00
path.parent.rmdir_if_possible
2015-11-07 16:25:34 +08:00
end
raise
end
config["forceautoupdate"] = force_auto_update unless force_auto_update.nil?
Commands.rebuild_commands_completion_list
link_completions_and_manpages
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
2020-09-17 04:18:13 +05:30
$stderr.puts "Tapped#{formatted_contents} (#{path.abv})." unless quiet
CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.update_from_formula_names!(formula_names)
end
CacheStoreDatabase.use(:cask_descriptions) do |db|
CaskDescriptionCacheStore.new(db)
.update_from_cask_tokens!(cask_tokens)
end
2015-11-07 16:25:34 +08:00
2021-01-19 17:55:03 -05:00
if official?
2021-01-20 12:02:24 -05:00
untapped = self.class.untapped_official_taps
2021-01-19 17:55:03 -05:00
untapped -= [name]
if untapped.empty?
Homebrew::Settings.delete :untapped
else
Homebrew::Settings.write :untapped, untapped.join(";")
end
end
return if clone_target
2016-09-23 22:02:23 +02:00
return unless private?
return if quiet
2018-09-17 02:45:00 +02:00
path.cd do
return if Utils.popen_read("git", "config", "--get", "credential.helper").present?
end
2020-09-17 04:18:13 +05:30
$stderr.puts <<~EOS
2016-09-23 22:02:23 +02:00
It looks like you tapped a private repository. To avoid entering your
credentials each time you update, you can use git HTTP credential
caching or issue the following command:
cd #{path}
git remote set-url origin git@github.com:#{full_name}.git
2016-09-23 22:02:23 +02:00
EOS
2015-11-07 16:25:34 +08:00
end
def link_completions_and_manpages
command = "brew tap --repair"
Utils::Link.link_manpages(path, command)
Homebrew::Completions.show_completions_message_if_needed
if official? || Homebrew::Completions.link_completions?
Utils::Link.link_completions(path, command)
else
Utils::Link.unlink_completions(path)
end
end
def fix_remote_configuration(requested_remote: nil, quiet: false)
if requested_remote.present?
path.cd do
safe_system "git", "remote", "set-url", "origin", requested_remote
safe_system "git", "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"
end
$stderr.ohai "#{name}: changed remote from #{remote} to #{requested_remote}" unless quiet
end
return unless remote
2023-04-15 19:35:00 -07:00
current_upstream_head = T.must(git_repo.origin_branch_name)
2023-04-15 18:13:38 -07:00
return if requested_remote.blank? && git_repo.origin_has_branch?(current_upstream_head)
args = %w[fetch]
args << "--quiet" if quiet
args << "origin"
args << "+refs/heads/*:refs/remotes/origin/*"
safe_system "git", "-C", path, *args
2023-04-15 18:13:38 -07:00
git_repo.set_head_origin_auto
2023-04-15 19:35:00 -07:00
new_upstream_head = T.must(git_repo.origin_branch_name)
return if new_upstream_head == current_upstream_head
safe_system "git", "-C", path, "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"
2023-04-15 18:13:38 -07:00
git_repo.rename_branch old: current_upstream_head, new: new_upstream_head
git_repo.set_upstream_branch local: new_upstream_head, origin: new_upstream_head
2021-10-12 20:38:09 +08:00
return if quiet
$stderr.ohai "#{name}: changed default branch name from #{current_upstream_head} to #{new_upstream_head}!"
end
# Uninstall this {Tap}.
def uninstall(manual: false)
require "descriptions"
2015-11-07 16:25:34 +08:00
raise TapUnavailableError, name unless installed?
2020-09-17 04:18:13 +05:30
$stderr.puts "Untapping #{name}..."
2018-06-19 17:59:25 +02:00
abv = path.abv
formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
2018-06-19 17:59:25 +02:00
CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.delete_from_formula_names!(formula_names)
end
CacheStoreDatabase.use(:cask_descriptions) do |db|
CaskDescriptionCacheStore.new(db)
.delete_from_cask_tokens!(cask_tokens)
end
Utils::Link.unlink_manpages(path)
Utils::Link.unlink_completions(path)
2015-12-06 20:57:28 +08:00
path.rmtree
path.parent.rmdir_if_possible
2020-09-17 04:18:13 +05:30
$stderr.puts "Untapped#{formatted_contents} (#{abv})."
Commands.rebuild_commands_completion_list
clear_cache
2021-01-19 17:55:03 -05:00
return if !manual || !official?
2021-01-19 17:55:03 -05:00
2021-01-20 12:02:24 -05:00
untapped = self.class.untapped_official_taps
2021-01-19 17:55:03 -05:00
return if untapped.include? name
untapped << name
Homebrew::Settings.write :untapped, untapped.join(";")
2015-11-07 16:25:34 +08:00
end
# True if the {#remote} of {Tap} is customized.
2015-06-10 15:39:47 +08:00
def custom_remote?
return true unless remote
2018-09-17 02:45:00 +02:00
remote.casecmp(default_remote).nonzero?
2015-06-10 15:39:47 +08:00
end
# Path to the directory of all {Formula} files for this {Tap}.
sig { returns(Pathname) }
2015-12-06 20:59:31 +08:00
def formula_dir
# Official formulae taps always use this directory, saves time to hardcode.
@formula_dir ||= if official?
path/"Formula"
else
potential_formula_dirs.find(&:directory?) || (path/"Formula")
end
end
sig { returns(T::Array[Pathname]) }
def potential_formula_dirs
2023-04-15 18:13:38 -07:00
@potential_formula_dirs ||= [path/"Formula", path/"HomebrewFormula", path].freeze
2015-12-06 20:59:31 +08:00
end
sig { params(name: String).returns(Pathname) }
def new_formula_path(name)
formula_dir/"#{name.downcase}.rb"
end
# Path to the directory of all {Cask} files for this {Tap}.
sig { returns(Pathname) }
2016-08-04 14:37:37 +04:00
def cask_dir
@cask_dir ||= path/"Casks"
2016-08-04 14:37:37 +04:00
end
sig { params(token: String).returns(Pathname) }
def new_cask_path(token)
cask_dir/"#{token.downcase}.rb"
end
sig { params(token: String).returns(String) }
def relative_cask_path(token)
new_cask_path(token).to_s
.delete_prefix("#{path}/")
end
2018-06-20 20:35:24 +02:00
def contents
contents = []
if (command_count = command_files.count).positive?
contents << Utils.pluralize("command", command_count, include_count: true)
2018-06-20 20:35:24 +02:00
end
if (cask_count = cask_files.count).positive?
contents << Utils.pluralize("cask", cask_count, include_count: true)
2018-06-20 20:35:24 +02:00
end
if (formula_count = formula_files.count).positive?
contents << Utils.pluralize("formula", formula_count, plural: "e", include_count: true)
2018-06-20 20:35:24 +02:00
end
contents
end
# An array of all {Formula} files of this {Tap}.
sig { returns(T::Array[Pathname]) }
2015-06-10 15:39:47 +08:00
def formula_files
@formula_files ||= if formula_dir.directory?
if formula_dir == path
# We only want the top level here so we don't treat commands & casks as formulae.
# Sharding is only supported in Formula/ and HomebrewFormula/.
formula_dir.children
else
formula_dir.find
end.select(&method(:formula_file?))
else
[]
end
end
2024-02-23 15:03:33 +01:00
# A mapping of {Formula} names to {Formula} file paths.
#
# @private
sig { returns(T::Hash[String, Pathname]) }
def formula_files_by_name
2024-02-23 15:03:33 +01:00
@formula_files_by_name ||= formula_files.each_with_object({}) do |file, hash|
# If there's more than one file with the same basename: use the longer one to prioritise more specific results.
basename = file.basename(".rb").to_s
existing_file = hash[basename]
hash[basename] = file if existing_file.nil? || existing_file.to_s.length < file.to_s.length
end
end
# An array of all {Cask} files of this {Tap}.
sig { returns(T::Array[Pathname]) }
def cask_files
@cask_files ||= if cask_dir.directory?
cask_dir.find.select(&method(:ruby_file?))
else
[]
end
2015-06-10 15:39:47 +08:00
end
2024-02-23 15:03:05 +01:00
# A mapping of {Cask} tokens to {Cask} file paths.
#
2023-07-13 19:28:39 +01:00
# @private
sig { returns(T::Hash[String, Pathname]) }
def cask_files_by_name
2024-02-23 15:03:05 +01:00
@cask_files_by_name ||= cask_files.each_with_object({}) do |file, hash|
# If there's more than one file with the same basename: use the longer one to prioritise more specific results.
basename = file.basename(".rb").to_s
existing_file = hash[basename]
hash[basename] = file if existing_file.nil? || existing_file.to_s.length < file.to_s.length
end
end
# returns true if the file has a Ruby extension
# @private
sig { params(file: Pathname).returns(T::Boolean) }
def ruby_file?(file)
file.extname == ".rb"
end
2023-04-06 04:59:55 +08:00
# returns true if given path would present a {Formula} file in this {Tap}.
# accepts both absolute path and relative path (relative to this {Tap}'s path)
# @private
sig { params(file: T.any(String, Pathname)).returns(T::Boolean) }
def formula_file?(file)
file = Pathname.new(file) unless file.is_a? Pathname
2023-04-15 18:13:38 -07:00
file = file.expand_path(path)
return false unless ruby_file?(file)
2023-04-06 05:03:56 +08:00
return false if cask_file?(file)
file.to_s.start_with?("#{formula_dir}/")
end
2023-04-06 04:59:55 +08:00
# returns true if given path would present a {Cask} file in this {Tap}.
2016-08-04 14:37:37 +04:00
# accepts both absolute path and relative path (relative to this {Tap}'s path)
# @private
sig { params(file: T.any(String, Pathname)).returns(T::Boolean) }
2016-08-04 14:37:37 +04:00
def cask_file?(file)
file = Pathname.new(file) unless file.is_a? Pathname
2023-04-15 18:13:38 -07:00
file = file.expand_path(path)
return false unless ruby_file?(file)
file.to_s.start_with?("#{cask_dir}/")
2016-08-04 14:37:37 +04:00
end
# An array of all {Formula} names of this {Tap}.
sig { returns(T::Array[String]) }
2015-06-10 15:39:47 +08:00
def formula_names
@formula_names ||= formula_files.map(&method(:formula_file_to_name))
end
# A hash of all {Formula} name prefixes to versioned {Formula} in this {Tap}.
# @private
sig { returns(T::Hash[String, T::Array[String]]) }
def prefix_to_versioned_formulae_names
@prefix_to_versioned_formulae_names ||= formula_names
.select { |name| name.include?("@") }
.group_by { |name| name.gsub(/(@[\d.]+)?$/, "") }
.transform_values(&:sort)
.freeze
end
# An array of all {Cask} tokens of this {Tap}.
sig { returns(T::Array[String]) }
def cask_tokens
@cask_tokens ||= cask_files.map(&method(:formula_file_to_name))
2015-06-10 15:39:47 +08:00
end
2015-12-06 21:15:43 +08:00
# path to the directory of all alias files for this {Tap}.
# @private
sig { returns(Pathname) }
2015-12-06 21:15:43 +08:00
def alias_dir
@alias_dir ||= path/"Aliases"
2015-12-06 21:15:43 +08:00
end
2015-09-12 18:21:22 +08:00
# an array of all alias files of this {Tap}.
# @private
sig { returns(T::Array[Pathname]) }
2015-09-12 18:21:22 +08:00
def alias_files
2015-12-06 21:15:43 +08:00
@alias_files ||= Pathname.glob("#{alias_dir}/*").select(&:file?)
2015-09-12 18:21:22 +08:00
end
# an array of all aliases of this {Tap}.
# @private
sig { returns(T::Array[String]) }
2015-09-12 18:21:22 +08:00
def aliases
@aliases ||= alias_table.keys
2015-09-12 18:21:22 +08:00
end
# a table mapping alias to formula name
# @private
sig { returns(T::Hash[String, String]) }
def alias_table
return @alias_table if @alias_table
2018-09-17 02:45:00 +02:00
@alias_table = {}
alias_files.each do |alias_file|
@alias_table[alias_file_to_name(alias_file)] = formula_file_to_name(alias_file.resolved_path)
end
@alias_table
end
# a table mapping formula name to aliases
# @private
def alias_reverse_table
return @alias_reverse_table if @alias_reverse_table
2018-09-17 02:45:00 +02:00
@alias_reverse_table = {}
alias_table.each do |alias_name, formula_name|
@alias_reverse_table[formula_name] ||= []
@alias_reverse_table[formula_name] << alias_name
end
@alias_reverse_table
end
sig { returns(Pathname) }
2018-07-25 11:38:19 +02:00
def command_dir
@command_dir ||= path/"cmd"
end
# An array of all commands files of this {Tap}.
sig { returns(T::Array[Pathname]) }
2015-06-10 15:39:47 +08:00
def command_files
2018-07-25 11:38:19 +02:00
@command_files ||= if command_dir.directory?
Commands.find_commands(command_dir)
2018-07-25 11:38:19 +02:00
else
[]
end
2015-06-10 15:39:47 +08:00
end
sig { returns(Hash) }
2015-06-10 15:39:47 +08:00
def to_hash
hash = {
2018-11-02 17:18:07 +00:00
"name" => name,
"user" => user,
"repo" => repo,
"path" => path.to_s,
"installed" => installed?,
"official" => official?,
2015-06-10 15:39:47 +08:00
"formula_names" => formula_names,
"cask_tokens" => cask_tokens,
2015-06-10 15:39:47 +08:00
}
if installed?
hash["formula_files"] = formula_files.map(&:to_s)
hash["cask_files"] = cask_files.map(&:to_s)
hash["command_files"] = command_files.map(&:to_s)
hash["remote"] = remote
hash["custom_remote"] = custom_remote?
hash["private"] = private?
end
hash
2015-06-10 15:39:47 +08:00
end
2023-04-08 14:10:58 +02:00
# Hash with tap cask renames.
sig { returns(T::Hash[String, String]) }
def cask_renames
2023-07-13 19:28:39 +01:00
@cask_renames ||= if (rename_file = path/HOMEBREW_TAP_CASK_RENAMES_FILE).file?
2023-04-08 14:10:58 +02:00
JSON.parse(rename_file.read)
else
{}
end
end
2024-02-13 06:03:10 +01:00
# Hash with tap formula old names. Reverse of {#formula_renames}.
sig { returns(T::Hash[String, T::Array[String]]) }
def reverse_cask_renames
@reverse_cask_renames ||= cask_renames.each_with_object({}) do |(old_name, new_name), hash|
hash[new_name] ||= []
hash[new_name] << old_name
end
end
# Hash with tap formula renames.
2023-04-08 14:10:58 +02:00
sig { returns(T::Hash[String, String]) }
def formula_renames
2020-11-09 08:39:56 -05:00
@formula_renames ||= if (rename_file = path/HOMEBREW_TAP_FORMULA_RENAMES_FILE).file?
JSON.parse(rename_file.read)
else
{}
end
end
# Hash with tap formula old names. Reverse of {#formula_renames}.
sig { returns(T::Hash[String, T::Array[String]]) }
2024-02-13 06:03:10 +01:00
def formula_reverse_renames
@formula_reverse_renames ||= formula_renames.each_with_object({}) do |(old_name, new_name), hash|
hash[new_name] ||= []
hash[new_name] << old_name
end
end
2024-02-13 06:03:10 +01:00
sig { returns(T::Hash[String, T::Array[String]]) }
def self.reverse_tap_migrations_renames
Tap.each_with_object({}) do |tap, hash|
tap.tap_migrations.each do |old_name, new_name|
new_tap_user, new_tap_repo, new_name = new_name.split("/", 3)
next unless new_name
new_tap = Tap.fetch(new_tap_user, new_tap_repo)
hash["#{new_tap}/#{new_name}"] ||= []
hash["#{new_tap}/#{new_name}"] << old_name
end
end
end
# Hash with tap migrations.
2024-02-13 06:03:10 +01:00
sig { returns(T::Hash[String, String]) }
def tap_migrations
2023-07-13 19:28:39 +01:00
@tap_migrations ||= if (migration_file = path/HOMEBREW_TAP_MIGRATIONS_FILE).file?
JSON.parse(migration_file.read)
else
{}
end
end
# Hash with audit exceptions
sig { returns(Hash) }
def audit_exceptions
@audit_exceptions = read_formula_list_directory "#{HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR}/*"
end
# Hash with style exceptions
sig { returns(Hash) }
def style_exceptions
@style_exceptions = read_formula_list_directory "#{HOMEBREW_TAP_STYLE_EXCEPTIONS_DIR}/*"
end
# Hash with pypi formula mappings
def pypi_formula_mappings
@pypi_formula_mappings = read_formula_list path/HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS
end
# @private
sig { returns(T::Boolean) }
def should_report_analytics?
2023-07-13 19:28:39 +01:00
installed? && !private?
end
sig { params(other: T.nilable(T.any(String, Tap))).returns(T::Boolean) }
2015-12-06 22:21:27 +08:00
def ==(other)
other = Tap.fetch(other) if other.is_a?(String)
self.class == other.class && name == other.name
2015-12-06 22:21:27 +08:00
end
def self.each(&block)
2020-11-16 22:18:56 +01:00
return to_enum unless block
2024-02-20 15:10:26 +01:00
installed_taps = if TAP_DIRECTORY.directory?
TAP_DIRECTORY.subdirs
.flat_map(&:subdirs)
.map(&method(:from_path))
else
[]
2015-06-10 15:39:47 +08:00
end
2024-02-20 15:10:26 +01:00
available_taps = if Homebrew::EnvConfig.no_install_from_api?
installed_taps
else
default_taps = T.let([CoreTap.instance], T::Array[Tap])
default_taps << CoreCaskTap.instance if OS.mac? # rubocop:disable Homebrew/MoveToExtendOS
installed_taps + default_taps
end.sort_by(&:name).uniq
available_taps.each(&block)
2015-06-10 15:39:47 +08:00
end
# An array of all installed {Tap} names.
sig { returns(T::Array[String]) }
2015-06-10 15:39:47 +08:00
def self.names
# odeprecated "`#{self}.names`"
2017-10-14 06:13:40 +01:00
map(&:name).sort
2015-06-10 15:39:47 +08:00
end
# An array of all tap cmd directory {Pathname}s.
2020-10-20 12:03:48 +02:00
sig { returns(T::Array[Pathname]) }
def self.cmd_directories
Pathname.glob TAP_DIRECTORY/"*/*/cmd"
end
2021-01-20 12:02:24 -05:00
# An array of official taps that have been manually untapped
sig { returns(T::Array[String]) }
def self.untapped_official_taps
Homebrew::Settings.read(:untapped)&.split(";") || []
end
# @private
sig { params(file: Pathname).returns(String) }
def formula_file_to_name(file)
"#{name}/#{file.basename(".rb")}"
end
# @private
sig { params(file: Pathname).returns(String) }
def alias_file_to_name(file)
"#{name}/#{file.basename}"
end
2021-10-04 21:45:20 -04:00
def audit_exception(list, formula_or_cask, value = nil)
return false if audit_exceptions.blank?
return false unless audit_exceptions.key? list
list = audit_exceptions[list]
case list
when Array
list.include? formula_or_cask
when Hash
return false unless list.include? formula_or_cask
return list[formula_or_cask] if value.blank?
list[formula_or_cask] == value
end
end
private
def read_or_set_private_config
case config["private"]
when "true" then true
when "false" then false
else
config["private"] = begin
if custom_remote?
true
else
GitHub.private_repo?(full_name)
end
2021-02-15 21:48:21 +05:30
rescue GitHub::API::HTTPNotFoundError
true
2021-02-15 21:48:21 +05:30
rescue GitHub::API::Error
false
end
end
end
sig { params(file: Pathname).returns(T.any(T::Array[String], Hash)) }
def read_formula_list(file)
JSON.parse file.read
rescue JSON::ParserError
opoo "#{file} contains invalid JSON"
{}
rescue Errno::ENOENT
{}
end
sig { params(directory: String).returns(Hash) }
def read_formula_list_directory(directory)
list = {}
Pathname.glob(path/directory).each do |exception_file|
list_name = exception_file.basename.to_s.chomp(".json").to_sym
list_contents = read_formula_list exception_file
next if list_contents.blank?
list[list_name] = list_contents
end
list
end
2015-06-10 15:39:47 +08:00
end
2023-07-13 19:28:39 +01:00
class AbstractCoreTap < Tap
extend T::Helpers
2023-07-13 19:28:39 +01:00
abstract!
sig { returns(T.attached_class) }
def self.instance
2023-07-13 19:28:39 +01:00
@instance ||= T.unsafe(self).new
end
2023-07-13 19:28:39 +01:00
sig { override.void }
def ensure_installed!
return unless Homebrew::EnvConfig.no_install_from_api?
return if Homebrew::EnvConfig.automatically_set_no_install_from_api?
2018-09-17 02:45:00 +02:00
2023-07-13 19:28:39 +01:00
super
end
sig { void }
def self.ensure_installed!
# odeprecated "`#{self}.ensure_installed!`", "`#{self}.instance.ensure_installed!`"
2023-07-13 19:28:39 +01:00
instance.ensure_installed!
end
# @private
sig { override.returns(T::Boolean) }
def should_report_analytics?
return super if Homebrew::EnvConfig.no_install_from_api?
true
end
end
# A specialized {Tap} class for the core formulae.
class CoreTap < AbstractCoreTap
# @private
sig { void }
def initialize
super "Homebrew", "core"
end
sig { override.void }
def ensure_installed!
2022-11-09 14:45:43 +00:00
return if ENV["HOMEBREW_TESTS"]
2023-07-13 19:28:39 +01:00
super
end
sig { returns(T.nilable(String)) }
def remote
return super if Homebrew::EnvConfig.no_install_from_api?
Homebrew::EnvConfig.core_git_remote
end
# CoreTap never allows shallow clones (on request from GitHub).
def install(quiet: false, clone_target: nil, force_auto_update: nil,
custom_remote: false, verify: false, force: false)
remote = Homebrew::EnvConfig.core_git_remote # set by HOMEBREW_CORE_GIT_REMOTE
requested_remote = clone_target || remote
2023-02-10 23:15:40 -05:00
# The remote will changed again on `brew update` since remotes for homebrew/core are mismatched
raise TapCoreRemoteMismatchError.new(name, remote, requested_remote) if requested_remote != remote
2020-09-17 04:18:13 +05:30
if remote != default_remote
2023-02-10 23:15:40 -05:00
$stderr.puts "HOMEBREW_CORE_GIT_REMOTE set: using #{remote} as the Homebrew/homebrew-core Git remote."
2020-09-17 04:18:13 +05:30
end
super(quiet: quiet, clone_target: remote, force_auto_update: force_auto_update,
custom_remote: custom_remote, force: force)
end
# @private
2021-06-12 18:02:23 -04:00
sig { params(manual: T::Boolean).void }
def uninstall(manual: false)
raise "Tap#uninstall is not available for CoreTap" if Homebrew::EnvConfig.no_install_from_api?
2021-06-12 18:02:23 -04:00
super
end
# @private
2020-10-20 12:03:48 +02:00
sig { returns(T::Boolean) }
def core_tap?
true
end
# @private
sig { returns(T::Boolean) }
def linuxbrew_core?
remote_repo.to_s.end_with?("/linuxbrew-core") || remote_repo == "Linuxbrew/homebrew-core"
end
# @private
sig { returns(Pathname) }
def formula_dir
@formula_dir ||= begin
ensure_installed!
super
end
end
sig { params(name: String).returns(Pathname) }
def new_formula_path(name)
formula_subdir = if name.start_with?("lib")
"lib"
else
name[0].to_s
end
return super unless (formula_dir/formula_subdir).directory?
formula_dir/formula_subdir/"#{name.downcase}.rb"
end
# @private
sig { returns(Pathname) }
def alias_dir
@alias_dir ||= begin
ensure_installed!
super
end
end
# @private
2023-04-08 14:10:58 +02:00
sig { returns(T::Hash[String, String]) }
def formula_renames
2023-04-27 04:09:41 +01:00
@formula_renames ||= if Homebrew::EnvConfig.no_install_from_api?
ensure_installed!
super
2023-04-27 04:09:41 +01:00
else
Homebrew::API::Formula.all_renames
end
end
# @private
sig { returns(Hash) }
def tap_migrations
@tap_migrations ||= if Homebrew::EnvConfig.no_install_from_api?
ensure_installed!
super
elsif Homebrew::API.internal_json_v3?
Homebrew::API::Formula.tap_migrations
else
migrations, = Homebrew::API.fetch_json_api_file "formula_tap_migrations.jws.json",
stale_seconds: TAP_MIGRATIONS_STALE_SECONDS
migrations
end
end
# @private
sig { returns(Hash) }
def audit_exceptions
@audit_exceptions ||= begin
ensure_installed!
super
end
end
# @private
sig { returns(Hash) }
def style_exceptions
@style_exceptions ||= begin
ensure_installed!
super
end
end
# @private
sig { returns(Hash) }
def pypi_formula_mappings
@pypi_formula_mappings ||= begin
ensure_installed!
super
end
end
# @private
sig { params(file: Pathname).returns(String) }
def formula_file_to_name(file)
file.basename(".rb").to_s
end
# @private
sig { params(file: Pathname).returns(String) }
def alias_file_to_name(file)
file.basename.to_s
end
# @private
sig { returns(T::Hash[String, String]) }
def alias_table
@alias_table ||= if Homebrew::EnvConfig.no_install_from_api?
super
else
Homebrew::API::Formula.all_aliases
end
end
# @private
sig { returns(T::Array[Pathname]) }
def formula_files
return super if Homebrew::EnvConfig.no_install_from_api?
formula_files_by_name.values
end
# @private
sig { returns(T::Array[String]) }
def formula_names
return super if Homebrew::EnvConfig.no_install_from_api?
Homebrew::API::Formula.all_formulae.keys
end
# @private
sig { returns(T::Hash[String, Pathname]) }
def formula_files_by_name
return super if Homebrew::EnvConfig.no_install_from_api?
tap_path = path.to_s
2023-07-13 19:28:39 +01:00
Homebrew::API::Formula.all_formulae.each_with_object({}) do |item, hash|
name, formula_hash = item
# If there's more than one item with the same path: use the longer one to prioritise more specific results.
existing_path = hash[name]
# Pathname equivalent is slow in a tight loop
new_path = File.join(tap_path, formula_hash.fetch("ruby_source_path"))
hash[name] = Pathname(new_path) if existing_path.nil? || existing_path.to_s.length < new_path.length
end
end
sig { returns(T::Hash[String, T.untyped]) }
def to_api_hash
formulae_api_hash = formula_names.to_h do |name|
formula = Formulary.factory(name)
formula_hash = formula.to_hash_with_variations(hash_method: :to_api_hash)
[name, formula_hash]
end
{
"tap_git_head" => git_head,
"aliases" => alias_table,
"renames" => formula_renames,
"tap_migrations" => tap_migrations,
"formulae" => formulae_api_hash,
}
end
2023-07-13 19:28:39 +01:00
end
2023-07-13 19:28:39 +01:00
# A specialized {Tap} class for homebrew-cask.
class CoreCaskTap < AbstractCoreTap
# @private
sig { void }
def initialize
super "Homebrew", "cask"
end
# @private
2023-07-13 19:28:39 +01:00
sig { override.returns(T::Boolean) }
def core_cask_tap?
true
end
sig { params(token: String).returns(Pathname) }
def new_cask_path(token)
cask_subdir = token[0].to_s
cask_dir/cask_subdir/"#{token.downcase}.rb"
end
2023-07-13 19:28:39 +01:00
sig { override.returns(T::Array[Pathname]) }
def cask_files
return super if Homebrew::EnvConfig.no_install_from_api?
2023-07-13 19:28:39 +01:00
cask_files_by_name.values
2023-07-13 19:28:39 +01:00
end
sig { override.returns(T::Array[String]) }
def cask_tokens
return super if Homebrew::EnvConfig.no_install_from_api?
Homebrew::API::Cask.all_casks.keys
end
# @private
sig { override.returns(T::Hash[String, Pathname]) }
def cask_files_by_name
return super if Homebrew::EnvConfig.no_install_from_api?
Homebrew::API::Cask.all_casks.each_with_object({}) do |item, hash|
name, cask_hash = item
# If there's more than one item with the same path: use the longer one to prioritise more specific results.
existing_path = hash[name]
new_path = path/cask_hash["ruby_source_path"]
hash[name] = new_path if existing_path.nil? || existing_path.to_s.length < new_path.to_s.length
2023-07-13 19:28:39 +01:00
end
end
sig { override.returns(T::Hash[String, String]) }
def cask_renames
@cask_renames ||= if Homebrew::EnvConfig.no_install_from_api?
super
else
Homebrew::API::Cask.all_renames
end
end
sig { override.returns(Hash) }
def tap_migrations
@tap_migrations ||= if Homebrew::EnvConfig.no_install_from_api?
super
else
migrations, = Homebrew::API.fetch_json_api_file "cask_tap_migrations.jws.json",
stale_seconds: TAP_MIGRATIONS_STALE_SECONDS
migrations
end
end
end
# Permanent configuration per {Tap} using `git-config(1)`.
class TapConfig
attr_reader :tap
sig { params(tap: Tap).void }
def initialize(tap)
@tap = tap
end
def [](key)
return unless tap.git?
2020-08-23 06:32:26 +02:00
return unless Utils::Git.available?
Homebrew::Settings.read key, repo: tap.path
end
def []=(key, value)
return unless tap.git?
2020-08-23 06:32:26 +02:00
return unless Utils::Git.available?
Homebrew::Settings.write key, value.to_s, repo: tap.path
end
def delete(key)
return unless tap.git?
return unless Utils::Git.available?
Homebrew::Settings.delete key, repo: tap.path
end
end
require "extend/os/tap"