aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cask/lib/hbc/pkg.rb
blob: 39252b48a5d778f2ab22bdddbba31c232cfe2147 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
125
126
127
128
module Hbc
  class Pkg
    def self.all_matching(regexp, command)
      command.run("/usr/sbin/pkgutil", args: ["--pkgs=#{regexp}"]).stdout.split("\n").map do |package_id|
        new(package_id.chomp, command)
      end
    end

    attr_reader :package_id

    def initialize(package_id, command = SystemCommand)
      @package_id = package_id
      @command = command
    end

    def uninstall
      odebug "Deleting pkg files"
      pkgutil_bom_files.each_slice(500) do |file_slice|
        @command.run("/bin/rm", args: file_slice.unshift("-f", "--"), sudo: true)
      end
      odebug "Deleting pkg symlinks and special files"
      pkgutil_bom_specials.each_slice(500) do |file_slice|
        @command.run("/bin/rm", args: file_slice.unshift("-f", "--"), sudo: true)
      end
      odebug "Deleting pkg directories"
      _deepest_path_first(pkgutil_bom_dirs).each do |dir|
        next unless dir.exist? && !MacOS.undeletable?(dir)
        _with_full_permissions(dir) do
          _delete_broken_file_dir(dir) && next
          _clean_broken_symlinks(dir)
          _clean_ds_store(dir)
          _rmdir(dir)
        end
      end
      forget
    end

    def forget
      odebug "Unregistering pkg receipt (aka forgetting)"
      @command.run!("/usr/sbin/pkgutil", args: ["--forget", package_id], sudo: true)
    end

    def pkgutil_bom(*type)
      @command.run!("/usr/sbin/pkgutil", args: [*type, "--files", package_id].compact)
              .stdout
              .split("\n")
              .map { |path| root.join(path) }
    end

    def pkgutil_bom_files
      @pkgutil_bom_files ||= pkgutil_bom("--only-files")
    end

    def pkgutil_bom_dirs
      @pkgutil_bom_dirs ||= pkgutil_bom("--only-dirs")
    end

    def pkgutil_bom_all
      @pkgutil_bom_all ||= pkgutil_bom
    end

    def pkgutil_bom_specials
      pkgutil_bom_all - pkgutil_bom_files - pkgutil_bom_dirs
    end

    def root
      @root ||= Pathname(info.fetch("volume")).join(info.fetch("install-location"))
    end

    def info
      @command.run!("/usr/sbin/pkgutil", args: ["--pkg-info-plist", package_id])
              .plist
    end

    def _rmdir(path)
      return unless path.children.empty?
      if path.symlink?
        @command.run!("/bin/rm", args: ["-f", "--", path], sudo: true)
      else
        @command.run!("/bin/rmdir", args: ["--", path], sudo: true)
      end
    end

    def _with_full_permissions(path)
      original_mode = (path.stat.mode % 01000).to_s(8)
      # TODO: similarly read and restore macOS flags (cf man chflags)
      @command.run!("/bin/chmod", args: ["--", "777", path], sudo: true)
      yield
    ensure
      if path.exist? # block may have removed dir
        @command.run!("/bin/chmod", args: ["--", original_mode, path], sudo: true)
      end
    end

    def _deepest_path_first(paths)
      paths.sort do |path_a, path_b|
        path_b.to_s.split("/").count <=> path_a.to_s.split("/").count
      end
    end

    # Some pkgs incorrectly report files (generally nibs)
    # as directories; we remove these as files instead.
    def _delete_broken_file_dir(path)
      return unless path.file? && !path.symlink?
      @command.run!("/bin/rm", args: ["-f", "--", path], sudo: true)
    end

    # Some pkgs leave broken symlinks hanging around; we clean them out before
    # attempting to rmdir to prevent extra cruft from lying around after
    # uninstall
    def _clean_broken_symlinks(dir)
      dir.children.each do |child|
        if _broken_symlink?(child)
          @command.run!("/bin/rm", args: ["--", child], sudo: true)
        end
      end
    end

    def _clean_ds_store(dir)
      ds_store = dir.join(".DS_Store")
      @command.run!("/bin/rm", args: ["--", ds_store], sudo: true) if ds_store.exist?
    end

    def _broken_symlink?(path)
      path.symlink? && !path.exist?
    end
  end
end