aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cask/lib/hbc/utils.rb
blob: 5991275230f4d8487ed888702be29b91b5a2ddbe (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
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
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
module Hbc::Utils; end

require "yaml"
require "open3"
require "stringio"

require "hbc/utils/file"

UPDATE_CMD = "brew uninstall --force brew-cask; brew untap phinze/cask; brew untap caskroom/cask; brew update; brew cleanup; brew cask cleanup".freeze
ISSUES_URL = "https://github.com/caskroom/homebrew-cask#reporting-bugs".freeze

# monkeypatch Object - not a great idea
class Object
  def utf8_inspect
    return inspect unless defined?(Encoding)
    return map(&:utf8_inspect) if respond_to?(:map)
    inspect.force_encoding("UTF-8").sub(%r{\A"(.*)"\Z}, '\1')
  end
end

class Buffer < StringIO
  def initialize(tty = false)
    super()
    @tty = tty
  end

  def tty?
    @tty
  end
end

# global methods

def odebug(title, *sput)
  if Hbc.respond_to?(:debug) && Hbc.debug
    width = Tty.width * 4 - 6
    if $stdout.tty? && title.to_s.length > width
      title = title.to_s[0, width - 3] + "..."
    end
    puts "#{Tty.magenta}==>#{Tty.reset} #{Tty.white}#{title}#{Tty.reset}"
    puts sput unless sput.empty?
  end
end

module Hbc::Utils
  def self.which(cmd, path = ENV["PATH"])
    unless File.basename(cmd) == cmd.to_s
      # cmd contains a directory element
      cmd_pn = Pathname(cmd)
      return nil unless cmd_pn.absolute?
      return resolve_executable(cmd_pn)
    end
    path.split(File::PATH_SEPARATOR).each do |elt|
      fq_cmd = Pathname(elt).expand_path.join(cmd)
      resolved = resolve_executable fq_cmd
      return resolved if resolved
    end
    nil
  end

  def self.resolve_executable(cmd)
    cmd_pn = Pathname(cmd)
    return nil unless cmd_pn.exist?
    return nil unless cmd_pn.executable?
    begin
      cmd_pn = Pathname(cmd_pn.realpath)
    rescue RuntimeError
      return nil
    end
    return nil unless cmd_pn.file?
    cmd_pn
  end

  def self.gain_permissions_remove(path, command: Hbc::SystemCommand)
    if path.respond_to?(:rmtree) && path.exist?
      gain_permissions(path, ["-R"], command, &:rmtree)
    elsif File.symlink?(path)
      gain_permissions(path, ["-h"], command, &FileUtils.method(:rm_f))
    end
  end

  def self.gain_permissions(path, command_args, command)
    tried_permissions = false
    tried_ownership = false
    begin
      yield path
    rescue StandardError
      # in case of permissions problems
      unless tried_permissions
        # TODO: Better handling for the case where path is a symlink.
        #       The -h and -R flags cannot be combined, and behavior is
        #       dependent on whether the file argument has a trailing
        #       slash.  This should do the right thing, but is fragile.
        command.run("/usr/bin/chflags",
                    must_succeed: false,
                    args:         command_args + ["--", "000", path])
        command.run("/bin/chmod",
                    must_succeed: false,
                    args:         command_args + ["--", "u+rwx", path])
        command.run("/bin/chmod",
                    must_succeed: false,
                    args:         command_args + ["-N", path])
        tried_permissions = true
        retry # rmtree
      end
      unless tried_ownership
        # in case of ownership problems
        # TODO: Further examine files to see if ownership is the problem
        #       before using sudo+chown
        ohai "Using sudo to gain ownership of path '#{path}'"
        command.run("/usr/sbin/chown",
                    args: command_args + ["--", current_user, path],
                    sudo: true)
        tried_ownership = true
        # retry chflags/chmod after chown
        tried_permissions = false
        retry # rmtree
      end
    end
  end

  def self.current_user
    Etc.getpwuid(Process.euid).name
  end

  # paths that "look" descendant (textually) will still
  # return false unless both the given paths exist
  def self.file_is_descendant(file, dir)
    file = Pathname.new(file)
    dir  = Pathname.new(dir)
    return false unless file.exist? && dir.exist?
    unless dir.directory?
      onoe "Argument must be a directory: '#{dir}'"
      return false
    end
    unless file.absolute? && dir.absolute?
      onoe "Both arguments must be absolute: '#{file}', '#{dir}'"
      return false
    end
    while file.parent != file
      return true if File.identical?(file, dir)
      file = file.parent
    end
    false
  end

  def self.path_occupied?(path)
    File.exist?(path) || File.symlink?(path)
  end

  def self.error_message_with_suggestions
    <<-EOS.undent
      Most likely, this means you have an outdated version of Homebrew-Cask. Please run:

          #{Tty.green}#{UPDATE_CMD}

      #{Tty.reset}If this doesn’t fix the problem, please report this bug:

          #{Tty.em}#{ISSUES_URL}#{Tty.reset}

    EOS
  end

  def self.method_missing_message(method, token, section = nil)
    poo = []
    poo << "Unexpected method '#{method}' called"
    poo << "during #{section}" if section
    poo << "on Cask #{token}."

    opoo(poo.join(" ") + "\n" + error_message_with_suggestions)
  end

  def self.nowstamp_metadata_path(container_path)
    @timenow ||= Time.now.gmtime
    if container_path.respond_to?(:join)
      precision = 3
      timestamp = @timenow.strftime("%Y%m%d%H%M%S")
      fraction = format("%.#{precision}f", @timenow.to_f - @timenow.to_i)[1..-1]
      timestamp.concat(fraction)
      container_path.join(timestamp)
    end
  end

  def self.size_in_bytes(files)
    Array(files).reduce(0) { |a, e| a + (File.size?(e) || 0) }
  end

  def self.capture_stderr
    previous_stderr = $stderr
    $stderr = StringIO.new
    yield
    $stderr.string
  ensure
    $stderr = previous_stderr
  end
end