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
9 changes: 5 additions & 4 deletions Library/Homebrew/cask/caskroom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,16 @@ def self.expected_caskroom_group
Process.egid.to_s
end

# Get all installed casks.
# Get installed casks, optionally limited to specific Caskroom tokens.
#
# A Caskroom directory for a cask that has been renamed but not yet migrated loads
# as the cask it was renamed to, so deduplicate to avoid listing it twice.
#
# @api internal
sig { params(config: T.nilable(Config)).returns(T::Array[Cask]) }
def self.casks(config: nil)
return [] unless any_casks_installed?
sig { params(config: T.nilable(Config), tokens: T.nilable(T::Array[String])).returns(T::Array[Cask]) }
def self.casks(config: nil, tokens: nil)
tokens ||= self.tokens
return [] if tokens.empty?

require "cask/cask_loader"

Expand Down
19 changes: 13 additions & 6 deletions Library/Homebrew/cask/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

require "json"
require "cmd/info"
require "cask/cask_loader"
require "utils/output"
require "utils/info_display"

Expand Down Expand Up @@ -74,7 +75,16 @@ def self.installation_info(cask, installed:)
sig { params(cask: Cask).returns(T::Array[::Utils::InfoDisplay::InstalledVersion]) }
def self.installed_section_rows(cask)
base_token = cask.token.split("@", 2).fetch(0)
related_casks = [*Caskroom.casks, cask].uniq(&:full_name).select do |other|
matching_token = ->(token) { token == base_token || token.start_with?("#{base_token}@") }
tokens = Caskroom.tokens
renamed_tokens = (tokens.empty? ? [] : Tap.to_a).flat_map do |tap|
(tap.cask_renames.keys | tap.tap_migrations.keys).intersection(tokens).select do |token|
resolved = CaskLoader.tap_cask_token_type("#{tap}/#{token}", warn: false)
resolved && matching_token.call(resolved.first)
end
end
related_tokens = tokens.select { |token| matching_token.call(token) || renamed_tokens.include?(token) }
related_casks = [*Caskroom.casks(tokens: related_tokens), cask].uniq(&:token).select do |other|
other.token == base_token || other.token.start_with?("#{base_token}@")
end
related_casks.sort_by(&:full_name).filter_map do |installed_cask|
Expand All @@ -89,11 +99,7 @@ def self.installed_section_rows(cask)
else
""
end
details = if path_exists
["Caskroom: #{versioned_staged_path}"]
else
["#{versioned_staged_path} (#{Formatter.error("does not exist")})"]
end
details = path_exists ? ["Caskroom: #{versioned_staged_path}"] : []
if (tab = Tab.for_cask(installed_cask)).tabfile&.exist?
details << tab.to_s
end
Expand All @@ -109,6 +115,7 @@ def self.installed_section_rows(cask)
size:,
linked: false,
details:,
warning: ("#{versioned_staged_path} (#{Formatter.error("does not exist")})" unless path_exists),
)
end
end
Expand Down
2 changes: 0 additions & 2 deletions Library/Homebrew/cmd/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -827,5 +827,3 @@ def print_sizes(formulae: [], casks: [])
end
end
end

require "extend/os/cmd/info"
5 changes: 0 additions & 5 deletions Library/Homebrew/extend/os/cmd/info.rb

This file was deleted.

19 changes: 0 additions & 19 deletions Library/Homebrew/extend/os/linux/cmd/info.rb

This file was deleted.

19 changes: 0 additions & 19 deletions Library/Homebrew/extend/os/mac/cmd/info.rb

This file was deleted.

43 changes: 42 additions & 1 deletion Library/Homebrew/test/cask/caskroom_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# frozen_string_literal: true

require "cask/caskroom"
require "cask/info"

RSpec.describe Cask::Caskroom do
before { described_class.expected_caskroom_group = nil }
Expand Down Expand Up @@ -218,6 +219,18 @@ def setup_cask_metadata(dir, token, tap: nil, version: "1.0")
end
end

it "loads only the requested installed tokens" do
Dir.mktmpdir do |dir|
allow(described_class).to receive(:path).and_return(Pathname(dir))
setup_cask_metadata(Pathname(dir), "selected-cask")
setup_cask_metadata(Pathname(dir), "unrelated-cask")
expect(Cask::CaskLoader).to receive(:load_prefer_installed).with("selected-cask", config: nil, warn: false)
.and_call_original

expect(described_class.casks(tokens: ["selected-cask"]).map(&:token)).to eq(["selected-cask"])
end
end

test_each(%w[rb json].product([true, false])) do |(extension, tap_source_exists)|
it "lists untrusted installed #{extension} casks with tap source present: #{tap_source_exists}" do
ENV["HOMEBREW_REQUIRE_TAP_TRUST"] = "1"
Expand All @@ -236,7 +249,7 @@ def setup_cask_metadata(dir, token, tap: nil, version: "1.0")
(casks_dir/"#{token}.rb").write 'raise "untrusted installed cask evaluated"'
(casks_dir/"#{token}.json").write("{}") if extension == "json"

expect(described_class.casks)
expect(described_class.casks(tokens: [token]))
.to contain_exactly(have_attributes(token:, installed_version: "1.0", tap:))
end
ensure
Expand Down Expand Up @@ -269,6 +282,34 @@ def setup_cask_metadata(dir, token, tap: nil, version: "1.0")
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
end

it "finds a renamed installed sibling without loading unrelated casks" do
tap = Tap.fetch("thirdparty", "foo")
cask_path = tap.cask_dir/"example@dev.rb"
cask_path.dirname.mkpath
cask_path.write <<~RUBY
cask "example@dev" do
version "2.0"
end
RUBY
(tap.path/"cask_renames.json").write JSON.generate("old-cask" => "example@dev")
tap.clear_cache
Homebrew::Trust.trust!(:tap, tap.name)
allow(Tap).to receive(:to_a).and_return([tap])

Dir.mktmpdir do |dir|
allow(described_class).to receive(:path).and_return(Pathname(dir))
setup_cask_metadata(Pathname(dir), "old-cask", tap:, version: "1.0")
setup_cask_metadata(Pathname(dir), "unrelated-cask", tap:, version: "1.0")
allow(Cask::CaskLoader).to receive(:load_prefer_installed).and_call_original
expect(Cask::CaskLoader).not_to receive(:load_prefer_installed).with("unrelated-cask", anything)

cask = Cask::Cask.new("example") { version "1.0" }
expect(Cask::Info.installed_section_rows(cask).map(&:name)).to eq(["thirdparty/foo/example@dev"])
end
ensure
FileUtils.rm_rf HOMEBREW_TAP_DIRECTORY/"thirdparty"
end

it "does not error for ambiguous installed casks when an ambiguous tap is untrusted" do
token = "ambiguous-untrusted-cask"
taps = [Tap.fetch("thirdparty", "foo"), Tap.fetch("thirdparty", "bar")]
Expand Down
141 changes: 133 additions & 8 deletions Library/Homebrew/test/cask/info_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,20 @@ def mock_cask_installed(cask_name)
cask = Cask::CaskLoader.load("local-transmission")
allow(DeprecateDisable).to receive(:message).with(cask) { +"deprecated!\nReplacement:\n example\n" }

expect { described_class.info(cask, args:) }
.to output(%r{https://transmissionbt.com/\nDeprecated!\nReplacement:\n example\nNot installed}).to_stdout
expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
==> local-transmission (Transmission): 2.61
BitTorrent client
https://transmissionbt.com/
Deprecated!
Replacement:
example
Not installed
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/l/local-transmission.rb
==> Requirements
Required: macOS >= 11
==> Apps
Transmission.app
EOS
end

it "prints inline summary information for casks" do
Expand All @@ -134,9 +146,22 @@ def mock_cask_installed(cask_name)
outdated?: true,
version: Cask::DSL::Version.new("2.61"),
supports_linux?: false)
(Cask::Caskroom.path/"local-transmission/2.60").mkpath
allow(Cask::Caskroom).to receive(:casks).and_return([])
allow(Cask::Tab).to receive(:for_cask).with(cask).and_return(Cask::Tab.empty)

expect { described_class.info(cask, args:) }
.to output(/==> .*↑.*: 2\.60 → 2\.61/).to_stdout
expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
==> local-transmission #{Formatter.success("↑")} (Transmission): 2.60 → 2.61
BitTorrent client
https://transmissionbt.com/
Installed (as dependency)
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/l/local-transmission.rb
==> Installed Versions
local-transmission #{Formatter.success("↑")} 2.60 → 2.61 (0B)
#{requirements_section(installed("macOS >= 11"))}
==> Apps
Transmission.app
EOS
end

describe "installed sibling cask versions" do
Expand Down Expand Up @@ -191,9 +216,104 @@ def mock_cask_installed(cask_name)
it "shows the available version for an outdated sibling" do
allow(installed_cask).to receive_messages(outdated?: true, version: Cask::DSL::Version.new("4.0"))

expect { described_class.info(cask, args:) }
.to output(/example@dev.*3\.2\.0-RC3 → 4\.0/).to_stdout
.and not_to_output.to_stderr
expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
#{oh1_title uninstalled("example")}: 3.0
Not installed
==> Installed Versions
example@dev #{Formatter.success("↑")} 3.2.0-RC3 → 4.0 (844MB)
EOS
end

it "shows a missing sibling directory without --verbose" do
missing_path = Cask::Caskroom.path/"missing-example/3.2.0-RC3"
allow(installed_cask).to receive(:caskroom_path).and_return(missing_path.parent)

expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
#{oh1_title uninstalled("example")}: 3.0
Not installed
==> Installed Versions
#{installed("example@dev")} 3.2.0-RC3
#{missing_path} (#{Formatter.error("does not exist")})
EOS
end
end

it "loads only matching installed casks, including renamed siblings" do
cask = Cask::Cask.new("example") { version "3.0" }
tap = Tap.fetch("homebrew/cask")
allow(Cask::Caskroom).to receive(:tokens).and_return(%w[example@dev old-example unrelated])
allow(Tap).to receive(:to_a).and_return([tap])
allow(tap).to receive_messages(cask_renames: { "old-example" => "example@beta" }, tap_migrations: {})
expect(Cask::Caskroom).to receive(:casks).with(tokens: %w[example@dev old-example]).and_return([])

expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
==> example: 3.0
Not installed
EOS
end

it "does not inspect tap renames when no casks are installed" do
cask = Cask::Cask.new("example") { version "3.0" }
allow(Cask::Caskroom).to receive(:tokens).and_return([])
expect(Tap).not_to receive(:to_a)

expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
==> example: 3.0
Not installed
EOS
end

it "lists an installed cask only once when another tap has the same token" do
cask = Cask::Cask.new("example", tap: Tap.fetch("other/tools")) { version "2.0" }
installed_cask = Cask::Cask.new("example", tap: Tap.fetch("original/tools")) { version "1.0" }
(Cask::Caskroom.path/"example/1.0").mkpath
allow(cask).to receive_messages(installed?: true, installed_version: "1.0", outdated?: false)
allow(installed_cask).to receive_messages(installed?: true, installed_version: "1.0", outdated?: false)
allow(Cask::Caskroom).to receive(:casks).and_return([installed_cask])
allow(Cask::Tab).to receive(:for_cask).and_return(Cask::Tab.empty)
allow(described_class).to receive(:repo_url).with(cask).and_return(nil)

expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
==> example: 2.0
Installed (as dependency)
==> Installed Versions
original/tools/example 1.0 (0B)
EOS
end

describe "a missing installed cask directory" do
let(:cask) { Cask::Cask.new("example") { version "1.0" } }
let(:installed_path) { Cask::Caskroom.path/"example/1.0" }
let(:time) { 1_720_189_863 }

before do
allow(cask).to receive_messages(installed?: true, installed_version: "1.0", outdated?: false)
allow(Cask::Caskroom).to receive(:casks).and_return([])
allow(Cask::Tab).to receive(:for_cask).with(cask).and_return(
Cask::Tab.new(loaded_from_api: true, tabfile: TEST_FIXTURE_DIR/"cask_receipt.json", time:),
)
end

it "shows the missing path without --verbose" do
expect { described_class.info(cask, args:) }.to output(<<~EOS).to_stdout
==> example: 1.0
Installed (as dependency)
==> Installed Versions
example 1.0
#{installed_path} (does not exist)
EOS
end

it "shows the missing path once and the receipt with --verbose" do
verbose_args = Homebrew::Cmd::Info.new(["--verbose"]).args
expect { described_class.info(cask, args: verbose_args) }.to output(<<~EOS).to_stdout
==> example: 1.0
Installed (as dependency)
==> Installed Versions
example 1.0
#{installed_path} (does not exist)
Installed using the formulae.brew.sh API on #{Time.at(time).strftime("%Y-%m-%d at %H:%M:%S")}
EOS
end
end

Expand Down Expand Up @@ -273,6 +393,8 @@ def mock_cask_installed(cask_name)
allow(Cask::Caskroom).to receive(:casks).and_return([])

expected_output = <<~EOS
==> grouped-artifacts: 1.0
Not installed
==> Apps
Example.app -> Renamed.app
==> Fonts
Expand All @@ -289,8 +411,11 @@ def mock_cask_installed(cask_name)
Example.qlgenerator (Quick Look Plugin)
==> Artifacts
Example.pkg (Pkg)
==> Binaries
example
EOS
expect(described_class.artifact_info(cask)).to eq(expected_output.chomp)
expect { described_class.info(cask, args: Homebrew::Cmd::Info.new(["--verbose"]).args) }
.to output(expected_output).to_stdout
end

it "lists command wrappers under Binaries only with --verbose" do
Expand Down
Loading
Loading