Class: Cask::Artifact::AppImage Private

Inherits:
Moved show all
Defined in:
cask/artifact/appimage.rb

Overview

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

Artifact corresponding to the app_image stanza.

Constant Summary

Constants inherited from AbstractArtifact

Cask::Artifact::AbstractArtifact::DirectivesType

Instance Attribute Summary

Attributes inherited from AbstractArtifact

#cask

Instance Method Summary collapse

Methods inherited from Moved

#adopted?, #backup_copy_args, english_description, #summarize_installed

Methods included from OS::Linux::Cask::Artifact::Moved

#backup_copy_args

Methods included from OS::Mac::Cask::Artifact::Moved

#backup_copy_args, #undeletable?

Methods inherited from Relocated

#add_altname_metadata, from_args, #initialize, #source, #summarize, #target, #to_a

Methods included from OS::Linux::Cask::Artifact::Relocated

#add_altname_metadata

Methods inherited from AbstractArtifact

#cask_sandbox, #config, dirmethod, dsl_key, english_article, english_name, #initialize, read_script_arguments, #requires_sudo?, #run_cask_sandbox, #sort_order, #staged_path_join_executable, #summarize, #to_args

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_removed, #pretty_uninstalled, #pretty_unmarked, #pretty_upgradable, #pretty_warning

Constructor Details

This class inherits a constructor from Cask::Artifact::Relocated

Instance Method Details

#install_phase(adopt: false, auto_updates: false, force: false, verbose: false, predecessor: nil, successor: nil, reinstall: false, command: SystemCommand) ⇒ 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:

  • adopt (Boolean) (defaults to: false)
  • auto_updates (Boolean, nil) (defaults to: false)
  • force (Boolean) (defaults to: false)
  • verbose (Boolean) (defaults to: false)
  • predecessor (Cask, nil) (defaults to: nil)
  • successor (Cask, nil) (defaults to: nil)
  • reinstall (Boolean) (defaults to: false)
  • command (T.class_of(SystemCommand)) (defaults to: SystemCommand)


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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
# File 'cask/artifact/appimage.rb', line 28

def install_phase(adopt: false, auto_updates: false, force: false, verbose: false, predecessor: nil,
                  successor: nil, reinstall: false, command: SystemCommand)
  # Some apps self-integrate their entries, which can be prevented by setting this var.
  # Ref: https://github.com/AppImage/AppImageSpec/blob/master/draft.md#desktop-integration
  environment = { "DESKTOPINTEGRATION" => "1" }

  # Read the desktop entries from the staged AppImage
  # so a conflicting entry fails the install before anything is moved.
  make_executable(source, command:) if source.file?
  desktop_entries = Dir.mktmpdir("appimage-", HOMEBREW_TEMP) do |tmpdir|
    extract_root = Pathname(tmpdir)/"squashfs-root"
    extract_from_appimage("*.desktop", extract_root:, command:)
      .each_with_object({}) do |embedded_desktop_file, entries|
      # Filter out large non-text binaries named `*.desktop` (e.g. cask opencode-desktop)
      next if !embedded_desktop_file.file? || embedded_desktop_file.size > DESKTOP_ENTRY_MAX_BYTES

      desktop_file = DesktopFileParser.new(embedded_desktop_file.read)
      unless desktop_file.desktop_entry?
        odebug "Skipping '#{embedded_desktop_file.basename}' as it is not a desktop entry"
        next
      end

      begin
        desktop_file.replace_program(target.to_s, environment:)
      rescue ArgumentError
        odebug "Skipping '#{embedded_desktop_file.basename}' due to malformed `Exec`"
        next
      end

      # The AppImage spec puts the icon `Icon` names at the root of the image.
      # A path would point outside it, usually to the developer's machine.
      icon_name = desktop_file.get("Icon").to_s
      if icon_name.present? && icon_name.exclude?("/")
        embedded_icon = extract_from_appimage("#{icon_name}.*", extract_root:, command:).find do |path|
          path.file? && DESKTOP_ENTRY_ICON_EXTENSIONS.include?(path.extname)
        end
        icon_extension = embedded_icon&.extname
      end
      # Fall back to the spec's `.DirIcon`, the only icon in e.g. cask vscodium,
      # typed by its contents as it has no extension.
      embedded_icon ||= extract_from_appimage(".DirIcon", extract_root:, command:).find(&:file?)
      icon_extension ||= case embedded_icon&.binread(DESKTOP_ENTRY_ICON_HEADER_BYTES)
      when /\A\x89PNG/n then ".png"
      when /<svg/ then ".svg"
      when %r{\A/\* XPM \*/} then ".xpm"
      end
      if embedded_icon && icon_extension
        # Name it after the entry, as `Icon` may be a path.
        icon = cask.staged_path/"#{embedded_desktop_file.basename(".desktop")}#{icon_extension}"
        FileUtils.cp embedded_icon, icon
        desktop_file.update("Icon") { icon.to_s }
      end

      entries[embedded_desktop_file.basename.to_s] = desktop_file
    end
  rescue SystemCallError => e
    odebug "Failed to extract the desktop entries from #{source}: #{e}"
    {}
  end

  desktop_entry_dir = Pathname(ENV.fetch("HOMEBREW_XDG_DATA_HOME", "~/.local/share")).expand_path/"applications"
  desktop_entries.each_key do |desktop_entry_name|
    desktop_entry = desktop_entry_dir/desktop_entry_name
    next if !desktop_entry.exist? && !desktop_entry.symlink?

    message = "It seems there is already a desktop entry at '#{desktop_entry}'"
    # An upgrade rewrites the entry whatever it contains.
    raise CaskError, "#{message}. Use `--force` to overwrite." if !force && !adopt && !predecessor

    opoo "#{message}; overwriting."
  end

  super

  make_executable(target, command:)

  desktop_entries.each do |desktop_entry_name, desktop_file|
    desktop_entry_dir.mkpath
    desktop_entry = desktop_entry_dir/desktop_entry_name
    ohai "Creating desktop entry '#{desktop_entry}'"
    if desktop_entry.exist? || desktop_entry.symlink?
      # Take over the entry from any other cask that links to it,
      # so uninstalling that cask doesn't remove this one's entry.
      Caskroom.path.glob("*/*/*.desktop").each do |desktop_entry_link|
        desktop_entry_link.unlink if desktop_entry_link.symlink? && desktop_entry_link.readlink == desktop_entry
      end
      desktop_entry.unlink
    end
    desktop_entry.write(desktop_file.to_s)
    FileUtils.ln_sf desktop_entry, cask.staged_path/desktop_entry_name
  end

  # Update mimetype cache if possible
  return if desktop_entries.values.none? { |desktop_file| desktop_file.get("MimeType") }

  update_mime_info_cache(desktop_entry_dir, command:)
end

#resolve_target(target, base_dir: nil) ⇒ 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:



12
13
14
# File 'cask/artifact/appimage.rb', line 12

def resolve_target(target, base_dir: nil)
  Pathname.new("#{config.appimagedir}/#{target}")
end

#uninstall_phase(skip: false, force: false, adopt: false, verbose: false, successor: nil, upgrade: false, reinstall: false, command: SystemCommand) ⇒ 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:

  • skip (Boolean) (defaults to: false)
  • force (Boolean) (defaults to: false)
  • adopt (Boolean) (defaults to: false)
  • verbose (Boolean) (defaults to: false)
  • successor (Cask, nil) (defaults to: nil)
  • upgrade (Boolean) (defaults to: false)
  • reinstall (Boolean) (defaults to: false)
  • command (T.class_of(SystemCommand)) (defaults to: SystemCommand)


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'cask/artifact/appimage.rb', line 138

def uninstall_phase(skip: false, force: false, adopt: false, verbose: false, successor: nil, upgrade: false,
                    reinstall: false, command: SystemCommand)
  desktop_entry_dirs_with_mime_types = cask.staged_path.glob("*.desktop").select(&:symlink?)
                                           .filter_map do |desktop_entry_link|
    desktop_entry = desktop_entry_link.readlink
    desktop_entry_link.unlink
    next if !desktop_entry.exist? && !desktop_entry.symlink?

    mime_types = DesktopFileParser.new(desktop_entry.read).get("MimeType") if desktop_entry.file?
    ohai "Removing desktop entry '#{desktop_entry}'"
    desktop_entry.unlink
    desktop_entry.dirname if mime_types.present?
  end
  # Stop the mimetype cache from offering the removed entries
  desktop_entry_dirs_with_mime_types.uniq.each do |desktop_entry_dir|
    update_mime_info_cache(desktop_entry_dir, command:)
  end

  # Migration shim for the `Symlinked` -> `Moved` transition.
  # Old installs have the real AppImage in the versioned Caskroom `source`
  # and a symlink at `target` pointing back to it,
  # which `Moved#move_back` cannot reverse
  # (a non-forced uninstall errors; a skipping one leaves the target behind).
  # The old layout is identified by `target` being that back-symlink
  # (in the new layout, `target` is the real file),
  # regardless of whether `source` still exists:
  # the very breakage this stanza fixes
  # is a self-updater having already deleted the versioned `source`,
  # leaving `target` dangling. Reverse the old layout directly instead.
  # This can be removed once pre-transition installs have aged out.
  if target.symlink? && target.dirname.join(target.readlink) == source
    Utils.gain_permissions_remove(target, command:)
    # Preserve `source` during an upgrade
    # so that it is captured in the staged-directory backup
    # that `revert_upgrade` restores (and reinstalls from)
    # if the new install fails;
    # a successful upgrade purges that backup afterwards.
    Utils.gain_permissions_remove(source, command:) unless upgrade
    return
  end

  super
end