Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 32 additions & 34 deletions Library/Homebrew/bundle/cask.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,6 @@ def reset!
@outdated_casks = T.let(nil, T.nilable(T::Array[String]))
end

private

sig { params(no_upgrade: T::Boolean, name: String, options: Homebrew::Bundle::EntryOptions).returns(T::Boolean) }
def upgrading?(no_upgrade, name, options)
return false if no_upgrade
return true if cask_upgradable?(name)
return false unless options[:greedy]

cask_is_outdated_using_greedy?(name)
end

sig { params(name: String, options: Homebrew::Bundle::EntryOptions, verbose: T::Boolean).returns(T::Boolean) }
def postinstall_change_state!(name:, options:, verbose:)
postinstall = T.cast(options.fetch(:postinstall, nil), T.nilable(String))
return true if postinstall.blank?

puts "Running postinstall for #{name}: #{postinstall}" if verbose
Kernel.system(postinstall) || false
end

sig { returns(T::Array[::Cask::Cask]) }
def casks
return [] unless Bundle.cask_installed?
Expand All @@ -55,20 +35,6 @@ def casks
@casks ||= T.let(::Cask::Caskroom.casks, T.nilable(T::Array[::Cask::Cask]))
end

sig { params(cask_config: ::Cask::Config).returns(String) }
def explicit_s(cask_config)
cask_config.explicit.map do |key, value|
# inverse of #env - converts :languages config key back to --language flag
if key == :languages
key = "language"
value = Array(cask_config.explicit.fetch(:languages, [])).join(",")
end
"#{key}: \"#{value.to_s.sub(/^#{Dir.home}/, "~")}\""
end.join(", ")
end

public

# Override makes `name` a required argument unlike the parent's default-argument signature.
# rubocop:disable Sorbet/AllowIncompatibleOverride
sig {
Expand Down Expand Up @@ -270,6 +236,38 @@ def formula_dependencies(cask_list)
cask.depends_on[:formula]
end.compact
end

private

sig { params(no_upgrade: T::Boolean, name: String, options: Homebrew::Bundle::EntryOptions).returns(T::Boolean) }
def upgrading?(no_upgrade, name, options)
return false if no_upgrade
return true if cask_upgradable?(name)
return false unless options[:greedy]

cask_is_outdated_using_greedy?(name)
end

sig { params(name: String, options: Homebrew::Bundle::EntryOptions, verbose: T::Boolean).returns(T::Boolean) }
def postinstall_change_state!(name:, options:, verbose:)
postinstall = T.cast(options.fetch(:postinstall, nil), T.nilable(String))
return true if postinstall.blank?

puts "Running postinstall for #{name}: #{postinstall}" if verbose
Kernel.system(postinstall) || false
end

sig { params(cask_config: ::Cask::Config).returns(String) }
def explicit_s(cask_config)
cask_config.explicit.map do |key, value|
# inverse of #env - converts :languages config key back to --language flag
if key == :languages
key = "language"
value = Array(cask_config.explicit.fetch(:languages, [])).join(",")
end
"#{key}: \"#{value.to_s.sub(/^#{Dir.home}/, "~")}\""
end.join(", ")
end
end

sig { override.params(cask: Object, no_upgrade: T::Boolean).returns(T::Boolean) }
Expand Down
16 changes: 15 additions & 1 deletion Library/Homebrew/bundle/dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,25 @@ def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, extens
selected_package_types[:tap] = taps
selected_package_types[:brew] = formulae
selected_package_types[:cask] = casks
dumped_formulae = if formulae
Homebrew::Bundle::Brew.formulae.filter_map { |f| f[:full_name] if f[:installed_on_request?] }
else
[]
end
dumped_casks = if casks
Homebrew::Bundle::Cask.casks.map(&:full_name)
else
[]
end
content = []
Homebrew::Bundle.dump_package_types.select(&:dump_supported?).each do |package_type|
next unless selected_package_types.fetch(package_type.type, false)

content << package_type.dump_output(describe:, no_restart:)
content << if package_type == Homebrew::Bundle::Tap
Homebrew::Bundle::Tap.dump(dumped_formulae:, dumped_casks:)
else
package_type.dump_output(describe:, no_restart:)
end
end
"#{content.reject(&:empty?).join("\n")}\n"
end
Expand Down
50 changes: 14 additions & 36 deletions Library/Homebrew/bundle/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require "bundle/dsl"
require "bundle/package_types"
require "bundle/skipper"
require "bundle/trust"
require "trust"
require "utils/output"

module Homebrew
Expand Down Expand Up @@ -53,7 +55,9 @@ def self.install!(entries, global: false, file: nil, no_lock: false, no_upgrade:
success = 0
failure = 0

installable_entries = entries.filter_map do |entry|
installable_entries = T.let([], T::Array[InstallableEntry])
installable_brewfile_entries = T.let([], T::Array[Dsl::Entry])
entries.each do |entry|
next if Homebrew::Bundle::Skipper.skip? entry

name = entry.name
Expand All @@ -62,10 +66,17 @@ def self.install!(entries, global: false, file: nil, no_lock: false, no_upgrade:
cls = Homebrew::Bundle.installable(type)
next if cls.nil? || !cls.install_supported?

InstallableEntry.new(name:, options:, verb: cls.install_verb(name, options), cls:)
installable_brewfile_entries << entry
installable_entries << InstallableEntry.new(name:, options:, verb: cls.install_verb(name, options), cls:)
end

apply_trust!(installable_entries)
# Apply `trusted: true` Brewfile options before anything fetches or
# loads the entries: the fetch phase and upgrade checks load formulae
# and casks, which triggers the tap trust check before the per-entry
# install step could grant trust.
Homebrew::Bundle::Trust.entries(installable_brewfile_entries).each do |type, name|
Homebrew::Trust.trust!(type, name)
end

if (fetchable_names = fetchable_formulae_and_casks(installable_entries, no_upgrade:).presence)
fetchable_names_joined = fetchable_names.join(", ")
Expand Down Expand Up @@ -111,39 +122,6 @@ def self.install!(entries, global: false, file: nil, no_lock: false, no_upgrade:
true
end

# Apply `trusted: true` Brewfile options before anything fetches or
# loads the entries: the fetch phase and upgrade checks load formulae
# and casks, which triggers the tap trust check before the per-entry
# install step could grant trust.
sig { params(entries: T::Array[InstallableEntry]).void }
def self.apply_trust!(entries)
entries.each do |entry|
next unless entry.options[:trusted]

require "trust"

if entry.cls == Tap
clone_target = entry.options[:clone_target].presence
reference = if clone_target
require "tap"
::Tap.remote_to_reference(clone_target.to_s) || clone_target.to_s
else
entry.name
end
Homebrew::Trust.trust!(:tap, reference)
elsif ::Utils.full_name?(entry.full_name)
# Only fully-qualified names map to a tap, so unqualified names
# cannot be meaningfully trusted.
if entry.cls == Brew
Homebrew::Trust.trust!(:formula, entry.full_name)
elsif entry.cls == Cask
Homebrew::Trust.trust!(:cask, entry.full_name)
end
end
end
end
private_class_method :apply_trust!

sig {
params(
entries: T::Array[InstallableEntry],
Expand Down
11 changes: 8 additions & 3 deletions Library/Homebrew/bundle/subcommand/cleanup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
require "utils"
require "bundle/dsl"
require "bundle/extensions"
require "bundle/trust"
require "trust"
require "ask"
module Homebrew
module Cmd
Expand Down Expand Up @@ -145,6 +147,11 @@ def self.cleanup(global: false, file: nil, force: false, zap: false, dsl: nil,
[extension, extension.cleanup_items(@dsl.entries)]
end
if force
dsl = @dsl
raise ArgumentError, "dsl is unset!" unless dsl

Homebrew::Trust.replace!(Homebrew::Bundle::Trust.entries(dsl.entries))

if casks.any?
args = if zap
["--zap"]
Expand All @@ -156,11 +163,9 @@ def self.cleanup(global: false, file: nil, force: false, zap: false, dsl: nil,
end

if formulae.any?
raise ArgumentError, "dsl is unset!" unless @dsl

# Mark Brewfile formulae as installed_on_request to prevent autoremove
# from removing them when their dependents are uninstalled
Homebrew::Bundle.mark_as_installed_on_request!(@dsl.entries)
Homebrew::Bundle.mark_as_installed_on_request!(dsl.entries)

Kernel.system HOMEBREW_BREW_FILE, "uninstall", "--formula", "--force", *formulae
puts "Uninstalled #{formulae.size} formula#{"e" if formulae.size != 1}"
Expand Down
36 changes: 33 additions & 3 deletions Library/Homebrew/bundle/tap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ def install_verb(_name = "", _options = {})
"Tapping"
end

sig { override.returns(String) }
def dump
sig { override.params(dumped_formulae: T::Array[String], dumped_casks: T::Array[String]).returns(String) }
def dump(dumped_formulae: [], dumped_casks: [])
taps.map do |tap|
remote = if (tap_remote = tap.remote) && tap_remote != tap.default_remote
if (api_token = ENV.fetch("HOMEBREW_GITHUB_API_TOKEN", false).presence)
Expand All @@ -97,7 +97,37 @@ def dump
", \"#{tap_remote}\""
end
tapline = "tap \"#{tap.name}\"#{remote}"
tapline += ", trusted: true" if Homebrew::Trust.explicitly_trusted_tap?(tap)
trusted = if Homebrew::Trust.explicitly_trusted_tap?(tap)
true
else
tap_trust = T.let({}, T::Hash[Symbol, T::Array[String]])
{
formula: [:formulae, dumped_formulae],
cask: [:casks, dumped_casks],
command: [:commands, []],
}.each do |type, values|
key, dumped_items = values
trusted_items = Homebrew::Trust.trusted_entries(type).filter_map do |entry|
reference, _, item = entry.rpartition("/")
next if reference.blank? || item.blank?
next if reference != tap.name && !tap.matches_reference?(reference)
next if dumped_items.include?("#{tap.name}/#{item}")

item
end.sort.uniq
tap_trust[key] = trusted_items if trusted_items.present?
end
tap_trust.presence
end

if trusted == true
tapline += ", trusted: true"
elsif trusted.present?
trusted_options = trusted.map do |key, values|
"#{key}: [#{values.map(&:inspect).join(", ")}]"
end.join(", ")
tapline += ", trusted: { #{trusted_options} }"
end
tapline
end.sort.uniq.join("\n")
end
Expand Down
70 changes: 70 additions & 0 deletions Library/Homebrew/bundle/trust.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# typed: strict
# frozen_string_literal: true

require "bundle/dsl"
require "utils"

module Homebrew
module Bundle
# Converts Brewfile `trusted` options into trust-store entries.
module Trust
TRUSTED_ITEM_KEYS = T.let({
formula: [:formula, :formulae],
cask: [:cask, :casks],
command: [:command, :commands],
}.freeze, T::Hash[Symbol, T::Array[Symbol]])
private_constant :TRUSTED_ITEM_KEYS

sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).returns(T::Array[[Symbol, String]]) }
def self.entries(entries)
entries.flat_map do |entry|
trusted = entry.options[:trusted]
full_name = T.cast(entry.options.fetch(:full_name, entry.name), String)

entry_type = entry.type

case entry_type
when :tap
next [] if trusted.blank?

clone_target = entry.options[:clone_target].presence
tap_reference = if clone_target
require "tap"
::Tap.remote_to_reference(clone_target.to_s) || clone_target.to_s
else
entry.name
end
next [[:tap, tap_reference]] if trusted == true
next [] unless trusted.is_a?(Hash)

unsupported_keys = trusted.keys - TRUSTED_ITEM_KEYS.values.flatten
raise UsageError, "Unsupported trusted keys: #{unsupported_keys.join(", ")}" if unsupported_keys.present?

TRUSTED_ITEM_KEYS.flat_map do |type, keys|
keys.flat_map do |key|
Array(trusted[key]).filter_map do |item|
item_name = case item
when String, Symbol, Integer
Utils.name_from_full_name(item.to_s)
end
next if item_name.blank?

[type, "#{tap_reference}/#{item_name}"]
end
end
end
when :brew, :cask
# Only fully-qualified names map to a tap, so unqualified names
# cannot be meaningfully trusted.
next [] if trusted != true || !Utils.full_name?(full_name)

type = (entry_type == :brew) ? :formula : :cask
[[type, full_name]]
else
[]
end
end.uniq
end
end
end
end
37 changes: 37 additions & 0 deletions Library/Homebrew/test/bundle/dumper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,43 @@
)).to eql("cask \"google-chrome\"\ncask \"java\"\ncask \"homebrew/cask-versions/iterm2-beta\"\n")
end

it "dumps tap trust entries not represented by dumped formulae" do
tap = instance_double(Tap, name: "thirdparty/tap", custom_remote?: false, remote: nil)
allow(tap).to receive(:matches_reference?) { |reference| reference == "thirdparty/tap" }
allow(Tap).to receive(:select).and_return([tap])
allow(Homebrew::Bundle::Brew).to receive(:formulae).and_return([
{
args: [],
full_name: "thirdparty/tap/requested",
installed_on_request?: true,
link?: nil,
},
{
args: [],
full_name: "thirdparty/tap/dependency",
installed_on_request?: false,
link?: nil,
},
])
allow(Homebrew::Bundle::Brew::Services).to receive(:started?).and_return(false)
allow(Homebrew::Trust).to receive(:trusted_entries).with(:tap).and_return([])
allow(Homebrew::Trust).to receive(:trusted_entries).with(:formula)
.and_return(%w[
thirdparty/tap/dependency
thirdparty/tap/requested
])
allow(Homebrew::Trust).to receive(:trusted_entries).with(:cask).and_return([])
allow(Homebrew::Trust).to receive(:trusted_entries).with(:command).and_return([])

expect(dumper.build_brewfile(
describe: false, no_restart: false, formulae: true, taps: true, casks: false,
extension_types: {}
)).to eql(<<~BREWFILE)
tap "thirdparty/tap", trusted: { formulae: ["dependency"] }
brew "thirdparty/tap/requested", trusted: true
BREWFILE
end

it "determines the brewfile correctly" do
expect(dumper.brewfile_path).to eql(Pathname.new(Dir.pwd).join("Brewfile"))
end
Expand Down
Loading
Loading