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_outside_github_actions, opoo_without_github_actions_annotation, pretty_deprecated, pretty_disabled, pretty_duration, pretty_install_status, pretty_installed, pretty_uninstalled, pretty_unmarked, pretty_upgradable, pretty_warning

Class Attribute Details

.expected_caskroom_groupString

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:



216
217
218
# File 'cask/caskroom.rb', line 216

def self.expected_caskroom_group
  "admin"
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)


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

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:



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'cask/caskroom.rb', line 227

def self.casks(config: nil)
  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:



196
197
198
# File 'cask/caskroom.rb', line 196

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

.corrupt_cask_dirsArray<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:



161
162
163
# File 'cask/caskroom.rb', line 161

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

.ensure_caskroom_existsvoid

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.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'cask/caskroom.rb', line 178

def self.ensure_caskroom_exists
  return if path.exist?

  sudo = !path.parent.writable?

  if sudo && !ENV.key?("SUDO_ASKPASS") && $stdout.tty?
    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:)
  SystemCommand.run("chmod", args: ["g+rwx", path], sudo:)
  SystemCommand.run("chown", args: [User.current.to_s, path], sudo:)

  chgrp_path(path, sudo) 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
# File 'cask/caskroom.rb', line 72

def self.migrate_caskfile_to_json(caskfile)
  # 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

.pathPathname

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

.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:



172
173
174
175
# File 'cask/caskroom.rb', line 172

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

.tokensArray<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