Module: Cask::Caskroom

Extended by:
Utils::Output::Mixin
Defined in:
cask/caskroom.rb

Overview

This module is part of an internal API. This module may only be used internally in repositories owned by Homebrew, except in casks or formulae. Third parties should avoid using this module if possible, as it may be removed or changed without warning.

Helper functions for interacting with the Caskroom directory.

Constant Summary collapse

CASKFILE_EXTENSIONS =

This constant is part of an internal API. This constant may only be used internally in repositories owned by Homebrew, except in casks or formulae. Third parties should avoid using this constant if possible, as it may be removed or changed without warning.

%w[json internal.json rb].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Methods included from Utils::Output::Mixin

issue_reporting_message, odebug, odeprecated, odie, odisabled, ofail, oh1, oh1_title, ohai, ohai_title, onoe, opoo, opoo_once, opoo_outside_github_actions, opoo_without_github_actions_annotation, pretty_cannot_install, pretty_deprecated, pretty_disabled, pretty_duration, pretty_install_status, pretty_installed, pretty_uninstalled, pretty_unmarked, pretty_upgradable, pretty_warning

Class Attribute Details

.expected_caskroom_group ⇒ String

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



226
227
228
229
230
231
232
# File 'cask/caskroom.rb', line 226

def self.expected_caskroom_group
  if !Homebrew::EnvConfig.no_sudo? && (group = Etc.getgrnam("admin")) && Process.groups.include?(group.gid)
    return "admin"
  end

  Etc.getgrgid(Process.egid)&.name || "staff"
end

Class Method Details

.any_casks_installed? ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:

  • (Boolean)


37
38
39
# File 'cask/caskroom.rb', line 37

def self.any_casks_installed?
  paths.any?
end

.cask_installed?(token) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:

  • (Boolean)


42
43
44
# File 'cask/caskroom.rb', line 42

def self.cask_installed?(token)
  !cask_installed_version(token).nil?
end

.cask_installed_caskfile(token, old_tokens: []) ⇒ Pathname?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'cask/caskroom.rb', line 47

def self.cask_installed_caskfile(token, old_tokens: [])
  # Check if the cask is installed with an old name.
  [token, *old_tokens].map { |cask_token| token_from_full_token(cask_token) }.uniq.each do |cask_token|
    caskroom_path = path/cask_token
    next if !caskroom_path.directory? || caskroom_path.symlink?

    timestamped_path = Pathname.glob((caskroom_path/".metadata/*/*").to_s).max_by { |p| p.basename.to_s }
    next unless timestamped_path

    caskfile = CASKFILE_EXTENSIONS.map { |ext| timestamped_path/"Casks/#{cask_token}.#{ext}" }
                                  .find(&:exist?)
    return caskfile if caskfile
  end

  nil
end

.cask_installed_version(token, old_tokens: []) ⇒ String?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



65
66
67
68
69
# File 'cask/caskroom.rb', line 65

def self.cask_installed_version(token, old_tokens: [])
  return unless (caskfile = cask_installed_caskfile(token, old_tokens:))

  caskfile.dirname.dirname.dirname.basename.to_s
end

.caskroom_group_correct?(path) ⇒ Boolean

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:

  • (Boolean)


211
212
213
214
215
216
# File 'cask/caskroom.rb', line 211

def self.caskroom_group_correct?(path)
  group = Etc.getgrnam(expected_caskroom_group)
  return false if group.nil?

  path.stat.gid == group.gid
end

.casks(config: nil) ⇒ Array<Cask>

This method is part of an internal API. This method may only be used internally in repositories owned by Homebrew, except in casks or formulae. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Get all installed casks.

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.

Parameters:

  • config (Config, nil) (defaults to: nil)

Returns:



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'cask/caskroom.rb', line 241

def self.casks(config: nil)
  return [] unless any_casks_installed?

  require "cask/cask_loader"

  tokens.sort.filter_map do |token|
    # This is nested so that the rescue can catch errors from both branches
    begin
      CaskLoader.load_prefer_installed(token, config:, warn: false)
    rescue TapCaskAmbiguityError => e
      e.loaders.fetch(0).load(config:)
    end
  rescue Homebrew::UntrustedTapError
    # If the tap is untrusted the only place we can load the cask from is the installed cask file, if it exists.
    begin
      CaskLoader::FromInstalledPathLoader.try_new(token, warn: false)&.load(config:)
    rescue
      nil
    end
  rescue
    # Don't blow up because of a single unavailable cask.
    nil
  end.select(&:installed?).uniq(&:full_name)
end

.chgrp_path(path, sudo) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



206
207
208
# File 'cask/caskroom.rb', line 206

def self.chgrp_path(path, sudo)
  SystemCommand.run("chgrp", args: [expected_caskroom_group, path], sudo:)
end

.corrupt_cask_dirs ⇒ Array<String>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Return tokens for Caskroom directories missing expected installed metadata.

Returns:



163
164
165
# File 'cask/caskroom.rb', line 163

def self.corrupt_cask_dirs
  paths.filter_map { |p| p.basename.to_s unless cask_with_metadata?(p) }
end

.ensure_caskroom_exists ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'cask/caskroom.rb', line 180

def self.ensure_caskroom_exists
  return if path.exist?

  if !path.parent.writable? && !ENV.key?("SUDO_ASKPASS") && $stdout.tty? && SystemCommand.sudo_available?
    ohai "Creating Caskroom directory: #{path}",
         "We'll set permissions properly so we won't need sudo in the future."
  end

  SystemCommand.run("mkdir", args: ["-p", path], sudo: nil)
  mode = "g+rwx"
  if expected_caskroom_group == shared_caskroom_group
    admin_group = Etc.getgrnam("admin")
    mode = "go-w" if !admin_group || Process.groups.exclude?(admin_group.gid)
  end
  SystemCommand.run("chmod", args: [mode, path], sudo: nil)
  SystemCommand.run("chown", args: [User.current.to_s, path], sudo: nil)

  chgrp_path(path, nil) unless caskroom_group_correct?(path)
end

.migrate_caskfile_to_json(caskfile) ⇒ void

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

This method returns an undefined value.

Parameters:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'cask/caskroom.rb', line 72

def self.migrate_caskfile_to_json(caskfile)
  require "cask/cask_loader"

  # Parse regular installed JSON so current files can be skipped and useful URL data can survive repairs.
  token = CaskLoader.token_from_path(caskfile)
  installed_json_caskfile = CaskLoader.installed_json_caskfile?(caskfile)
  source_json = CaskLoader.load_installed_json(caskfile)

  source_artifacts = nil
  source_url_specs = nil
  current_json = false
  if source_json
    raw_source_artifacts = source_json["artifacts"]
    raw_source_version = source_json["version"]
    raw_source_url_specs = source_json["url_specs"]
    source_artifacts = raw_source_artifacts if raw_source_artifacts.is_a?(Array)
    source_url_specs = raw_source_url_specs if raw_source_url_specs.is_a?(Hash)

    # Installed JSON only supplements metadata available from the path or receipt: artifacts and version preserve
    # otherwise-lost installed values, while url_specs preserves an artifact's staged source path.
    current_json = (source_json.keys - %w[artifacts url_specs version]).empty? &&
                   (raw_source_artifacts.nil? || !source_artifacts.nil?) &&
                   (raw_source_version.nil? || raw_source_version.is_a?(String)) &&
                   (raw_source_url_specs.nil? || !source_url_specs.nil?)
  end

  # Recover missing receipt and legacy caskfile data before deciding what must be stored in the JSON.
  tab = CaskLoader.load_installed_tab(token)

  cask = begin
    if installed_json_caskfile
      CaskLoader.load_from_installed_caskfile(caskfile)
    else
      CaskLoader.load(caskfile, warn: false)
    end
  rescue CaskInvalidError, CaskUnavailableError, MethodDeprecatedError, JSON::ParserError, NoMethodError,
         TypeError
    nil
  end
  return if current_json && cask && (!source_artifacts.nil? || tab.uninstall_artifacts.present?)
  return if cask&.uninstall_flight_blocks? || tab.uninstall_flight_blocks

  cask ||= CaskLoader.recover_from_installed_caskfile(caskfile, tab:)
  return unless cask

  # Preserve the original version and artifacts whenever the receipt cannot reproduce them.
  version = cask.version.to_s
  json_uninstall_artifacts = JSON.parse(JSON.generate(cask.artifacts_list(uninstall_only: true)))
  # Keep missing artifacts distinguishable from an intentional empty artifact list.
  return if source_artifacts.nil? && tab.uninstall_artifacts.blank? && json_uninstall_artifacts.empty?

  installed_json = cask.to_installed_json_hash
  installed_json["url_specs"] ||= source_url_specs if source_url_specs
  receipt_artifacts = tab.uninstall_artifacts.presence
  if receipt_artifacts.nil? || !artifacts_equivalent?(receipt_artifacts, json_uninstall_artifacts)
    installed_json["artifacts"] = json_uninstall_artifacts
  end
  installed_json["version"] = version if caskfile.dirname.dirname.dirname.basename.to_s != version

  # Replace the old metadata only after the new JSON reloads with the selected version and artifacts.
  json_caskfile = caskfile.dirname/"#{token}.json"
  original_contents = caskfile.read if caskfile == json_caskfile
  json_caskfile.atomic_write(JSON.pretty_generate(installed_json))
  begin
    # Only durable on-disk data may satisfy this check: the API fallback would mask a
    # migrated caskfile that lost its artifacts for as long as the API definition matches.
    migrated_cask = CaskLoader.load_from_installed_caskfile(json_caskfile, api_fallback: false)
    migrated_artifacts = JSON.parse(JSON.generate(migrated_cask.artifacts_list(uninstall_only: true)))
    if migrated_cask.version.to_s != version ||
       !artifacts_equivalent?(migrated_artifacts, json_uninstall_artifacts)
      raise "migrated Cask metadata differs from the original after preserving version and artifacts"
    end
  rescue
    if original_contents
      json_caskfile.atomic_write(original_contents)
    elsif json_caskfile.exist?
      json_caskfile.unlink
    end
    raise
  end
  caskfile.unlink if caskfile != json_caskfile
end

.path ⇒ Pathname

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



17
18
19
# File 'cask/caskroom.rb', line 17

def self.path
  @path ||= T.let(HOMEBREW_PREFIX/"Caskroom", T.nilable(Pathname))
end

.shared_caskroom_group ⇒ String?

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Returns:



201
202
203
# File 'cask/caskroom.rb', line 201

def self.shared_caskroom_group
  "staff"
end

.token_from_full_token(token) ⇒ String

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Parameters:

Returns:



174
175
176
177
# File 'cask/caskroom.rb', line 174

def self.token_from_full_token(token)
  _, _, cask_token = token.split("/", 3)
  cask_token || token
end

.tokens ⇒ Array<String>

This method is part of a private API. This method may only be used in the Homebrew/brew repository. Third parties should avoid using this method if possible, as it may be removed or changed without warning.

Return all tokens for installed casks.

Returns:



32
33
34
# File 'cask/caskroom.rb', line 32

def self.tokens
  paths.map { |path| path.basename.to_s }
end