aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/resource.rb
blob: 3dda50f8df72ba2a1b95f53d6219f03ad7361e71 (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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
require "download_strategy"
require "checksum"
require "version"

# Resource is the fundamental representation of an external resource. The
# primary formula download, along with other declared resources, are instances
# of this class.
class Resource
  include FileUtils

  attr_reader :mirrors, :specs, :using, :source_modified_time, :patches, :owner
  attr_writer :version
  attr_accessor :download_strategy, :checksum

  # Formula name must be set after the DSL, as we have no access to the
  # formula name before initialization of the formula
  attr_accessor :name

  class Download
    def initialize(resource)
      @resource = resource
    end

    def url
      @resource.url
    end

    def specs
      @resource.specs
    end

    def version
      @resource.version
    end

    def mirrors
      @resource.mirrors
    end
  end

  def initialize(name = nil, &block)
    @name = name
    @url = nil
    @version = nil
    @mirrors = []
    @specs = {}
    @checksum = nil
    @using = nil
    @patches = []
    instance_eval(&block) if block_given?
  end

  def owner=(owner)
    @owner = owner
    patches.each { |p| p.owner = owner }
  end

  def downloader
    download_strategy.new(download_name, Download.new(self))
  end

  # Removes /s from resource names; this allows go package names
  # to be used as resource names without confusing software that
  # interacts with download_name, e.g. github.com/foo/bar
  def escaped_name
    name.tr("/", "-")
  end

  def download_name
    name.nil? ? owner.name : "#{owner.name}--#{escaped_name}"
  end

  def cached_download
    downloader.cached_location
  end

  def clear_cache
    downloader.clear_cache
  end

  # Verifies download and unpacks it
  # The block may call `|resource,staging| staging.retain!` to retain the staging
  # directory. Subclasses that override stage should implement the tmp
  # dir using FileUtils.mktemp so that works with all subtypes.
  def stage(target = nil, &block)
    unless target || block
      raise ArgumentError, "target directory or block is required"
    end

    verify_download_integrity(fetch)
    prepare_patches
    unpack(target, &block)
  end

  def prepare_patches
    patches.grep(DATAPatch) { |p| p.path = owner.owner.path }

    patches.each do |patch|
      patch.verify_download_integrity(patch.fetch) if patch.external?
    end
  end

  def apply_patches
    return if patches.empty?
    ohai "Patching #{name}"
    patches.each(&:apply)
  end

  # If a target is given, unpack there; else unpack to a temp folder.
  # If block is given, yield to that block with |stage|, where stage
  # is a ResourceStagingContext.
  # A target or a block must be given, but not both.
  def unpack(target = nil)
    mktemp(download_name) do |staging|
      downloader.stage
      @source_modified_time = downloader.source_modified_time
      apply_patches
      if block_given?
        yield ResourceStageContext.new(self, staging)
      elsif target
        target = Pathname.new(target) unless target.is_a? Pathname
        target.install Pathname.pwd.children
      end
    end
  end

  Partial = Struct.new(:resource, :files)

  def files(*files)
    Partial.new(self, files)
  end

  def fetch
    HOMEBREW_CACHE.mkpath

    begin
      downloader.fetch
    rescue ErrorDuringExecution, CurlDownloadStrategyError => e
      raise DownloadError.new(self, e)
    end

    cached_download
  end

  def verify_download_integrity(fn)
    if fn.file?
      ohai "Verifying #{fn.basename} checksum" if ARGV.verbose?
      fn.verify_checksum(checksum)
    end
  rescue ChecksumMissingError
    opoo "Cannot verify integrity of #{fn.basename}"
    puts "A checksum was not provided for this resource"
    puts "For your reference the SHA256 is: #{fn.sha256}"
  end

  Checksum::TYPES.each do |type|
    define_method(type) { |val| @checksum = Checksum.new(type, val) }
  end

  def url(val = nil, specs = {})
    return @url if val.nil?
    @url = val
    @specs.merge!(specs)
    @using = @specs.delete(:using)
    @download_strategy = DownloadStrategyDetector.detect(url, using)
  end

  def version(val = nil)
    @version ||= begin
      version = detect_version(val)
      version.null? ? nil : version
    end
  end

  def mirror(val)
    mirrors << val
  end

  def patch(strip = :p1, src = nil, &block)
    p = Patch.create(strip, src, &block)
    patches << p
  end

  private

  def detect_version(val)
    return Version::NULL if val.nil? && url.nil?

    case val
    when nil     then Version.detect(url, specs)
    when String  then Version.create(val)
    when Version then val
    else
      raise TypeError, "version '#{val.inspect}' should be a string"
    end
  end

  class Go < Resource
    def stage(target)
      super(target/name)
    end
  end

  class PatchResource < Resource
    attr_reader :patch_files

    def initialize(&block)
      @patch_files = []
      super "patch", &block
    end

    def apply(*paths)
      paths.flatten!
      @patch_files.concat(paths)
      @patch_files.uniq!
    end
  end
end

# The context in which a Resource.stage() occurs. Supports access to both
# the Resource and associated Mktemp in a single block argument. The interface
# is back-compatible with Resource itself as used in that context.
class ResourceStageContext
  extend Forwardable

  # The Resource that is being staged
  attr_reader :resource
  # The Mktemp in which @resource is staged
  attr_reader :staging

  def_delegators :@resource, :version, :url, :mirrors, :specs, :using, :source_modified_time
  def_delegators :@staging, :retain!

  def initialize(resource, staging)
    @resource = resource
    @staging = staging
  end

  def to_s
    "<#{self.class}: resource=#{resource} staging=#{staging}>"
  end
end