mini_portile 0.6.2 → 0.7.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,494 @@
1
+ require 'rbconfig'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'net/ftp'
5
+ require 'fileutils'
6
+ require 'tempfile'
7
+ require 'digest/md5'
8
+ require 'open-uri'
9
+ require 'cgi'
10
+ require 'rbconfig'
11
+
12
+ # Monkey patch for Net::HTTP by ruby open-uri fix:
13
+ # https://github.com/ruby/ruby/commit/58835a9
14
+ class Net::HTTP
15
+ private
16
+ remove_method(:edit_path)
17
+ def edit_path(path)
18
+ if proxy?
19
+ if path.start_with?("ftp://") || use_ssl?
20
+ path
21
+ else
22
+ "http://#{addr_port}#{path}"
23
+ end
24
+ else
25
+ path
26
+ end
27
+ end
28
+ end
29
+
30
+ class MiniPortile
31
+ attr_reader :name, :version, :original_host
32
+ attr_writer :configure_options
33
+ attr_accessor :host, :files, :patch_files, :target, :logger
34
+
35
+ def initialize(name, version)
36
+ @name = name
37
+ @version = version
38
+ @target = 'ports'
39
+ @files = []
40
+ @patch_files = []
41
+ @log_files = {}
42
+ @logger = STDOUT
43
+
44
+ @original_host = @host = detect_host
45
+ end
46
+
47
+ def download
48
+ files_hashs.each do |file|
49
+ download_file(file[:url], file[:local_path])
50
+ verify_file(file)
51
+ end
52
+ end
53
+
54
+ def extract
55
+ files_hashs.each do |file|
56
+ extract_file(file[:local_path], tmp_path)
57
+ end
58
+ end
59
+
60
+ def apply_patch(patch_file)
61
+ (
62
+ # Not a class variable because closures will capture self.
63
+ @apply_patch ||=
64
+ case
65
+ when which('git')
66
+ lambda { |file|
67
+ message "Running git apply with #{file}... "
68
+ # By --work-tree=. git-apply uses the current directory as
69
+ # the project root and will not search upwards for .git.
70
+ execute('patch', ["git", "--work-tree=.", "apply", file], :initial_message => false)
71
+ }
72
+ when which('patch')
73
+ lambda { |file|
74
+ message "Running patch with #{file}... "
75
+ execute('patch', ["patch", "-p1", "-i", file], :initial_message => false)
76
+ }
77
+ else
78
+ raise "Failed to complete patch task; patch(1) or git(1) is required."
79
+ end
80
+ ).call(patch_file)
81
+ end
82
+
83
+ def patch
84
+ @patch_files.each do |full_path|
85
+ next unless File.exist?(full_path)
86
+ apply_patch(full_path)
87
+ end
88
+ end
89
+
90
+ def configure_options
91
+ @configure_options ||= configure_defaults
92
+ end
93
+
94
+ def configure
95
+ return if configured?
96
+
97
+ md5_file = File.join(tmp_path, 'configure.md5')
98
+ digest = Digest::MD5.hexdigest(computed_options.to_s)
99
+ File.open(md5_file, "w") { |f| f.write digest }
100
+
101
+ execute('configure', %w(sh configure) + computed_options)
102
+ end
103
+
104
+ def compile
105
+ execute('compile', make_cmd)
106
+ end
107
+
108
+ def install
109
+ return if installed?
110
+ execute('install', %Q(#{make_cmd} install))
111
+ end
112
+
113
+ def downloaded?
114
+ missing = files_hashs.detect do |file|
115
+ !File.exist?(file[:local_path])
116
+ end
117
+
118
+ missing ? false : true
119
+ end
120
+
121
+ def configured?
122
+ configure = File.join(work_path, 'configure')
123
+ makefile = File.join(work_path, 'Makefile')
124
+ md5_file = File.join(tmp_path, 'configure.md5')
125
+
126
+ stored_md5 = File.exist?(md5_file) ? File.read(md5_file) : ""
127
+ current_md5 = Digest::MD5.hexdigest(computed_options.to_s)
128
+
129
+ (current_md5 == stored_md5) && newer?(makefile, configure)
130
+ end
131
+
132
+ def installed?
133
+ makefile = File.join(work_path, 'Makefile')
134
+ target_dir = Dir.glob("#{port_path}/*").find { |d| File.directory?(d) }
135
+
136
+ newer?(target_dir, makefile)
137
+ end
138
+
139
+ def cook
140
+ download unless downloaded?
141
+ extract
142
+ patch
143
+ configure unless configured?
144
+ compile
145
+ install unless installed?
146
+
147
+ return true
148
+ end
149
+
150
+ def activate
151
+ lib_path = File.join(port_path, "lib")
152
+ vars = {
153
+ 'PATH' => File.join(port_path, 'bin'),
154
+ 'CPATH' => File.join(port_path, 'include'),
155
+ 'LIBRARY_PATH' => lib_path
156
+ }.reject { |env, path| !File.directory?(path) }
157
+
158
+ output "Activating #{@name} #{@version} (from #{port_path})..."
159
+ vars.each do |var, path|
160
+ full_path = File.expand_path(path)
161
+
162
+ # turn into a valid Windows path (if required)
163
+ full_path.gsub!(File::SEPARATOR, File::ALT_SEPARATOR) if File::ALT_SEPARATOR
164
+
165
+ # save current variable value
166
+ old_value = ENV[var] || ''
167
+
168
+ unless old_value.include?(full_path)
169
+ ENV[var] = "#{full_path}#{File::PATH_SEPARATOR}#{old_value}"
170
+ end
171
+ end
172
+
173
+ # rely on LDFLAGS when cross-compiling
174
+ if File.exist?(lib_path) && (@host != @original_host)
175
+ full_path = File.expand_path(lib_path)
176
+
177
+ old_value = ENV.fetch("LDFLAGS", "")
178
+
179
+ unless old_value.include?(full_path)
180
+ ENV["LDFLAGS"] = "-L#{full_path} #{old_value}".strip
181
+ end
182
+ end
183
+ end
184
+
185
+ def path
186
+ File.expand_path(port_path)
187
+ end
188
+
189
+ private
190
+
191
+ def tmp_path
192
+ "tmp/#{@host}/ports/#{@name}/#{@version}"
193
+ end
194
+
195
+ def port_path
196
+ "#{@target}/#{@host}/#{@name}/#{@version}"
197
+ end
198
+
199
+ def archives_path
200
+ "#{@target}/archives"
201
+ end
202
+
203
+ def work_path
204
+ Dir.glob("#{tmp_path}/*").find { |d| File.directory?(d) }
205
+ end
206
+
207
+ def configure_defaults
208
+ [
209
+ "--host=#{@host}", # build for specific target (host)
210
+ "--enable-static", # build static library
211
+ "--disable-shared" # disable generation of shared object
212
+ ]
213
+ end
214
+
215
+ def configure_prefix
216
+ "--prefix=#{File.expand_path(port_path)}"
217
+ end
218
+
219
+ def computed_options
220
+ [
221
+ configure_options, # customized or default options
222
+ configure_prefix, # installation target
223
+ ].flatten
224
+ end
225
+
226
+ def files_hashs
227
+ @files.map do |file|
228
+ hash = case file
229
+ when String
230
+ { :url => file }
231
+ when Hash
232
+ file.dup
233
+ else
234
+ raise ArgumentError, "files must be an Array of Stings or Hashs"
235
+ end
236
+
237
+ url = hash.fetch(:url){ raise ArgumentError, "no url given" }
238
+ filename = File.basename(url)
239
+ hash[:local_path] = File.join(archives_path, filename)
240
+ hash
241
+ end
242
+ end
243
+
244
+ def verify_file(file)
245
+ digest = case
246
+ when exp=file[:sha256] then Digest::SHA256
247
+ when exp=file[:sha1] then Digest::SHA1
248
+ when exp=file[:md5] then Digest::MD5
249
+ end
250
+ if digest
251
+ is = digest.file(file[:local_path]).hexdigest
252
+ unless is == exp.downcase
253
+ raise "Downloaded file '#{file[:local_path]}' has wrong hash: expected: #{exp} is: #{is}"
254
+ end
255
+ end
256
+ end
257
+
258
+ def log_file(action)
259
+ @log_files[action] ||=
260
+ File.expand_path("#{action}.log", tmp_path).tap { |file|
261
+ File.unlink(file) if File.exist?(file)
262
+ }
263
+ end
264
+
265
+ def tar_exe
266
+ @@tar_exe ||= begin
267
+ %w[gtar bsdtar tar basic-bsdtar].find { |c|
268
+ which(c)
269
+ }
270
+ end
271
+ end
272
+
273
+ def tar_compression_switch(filename)
274
+ case File.extname(filename)
275
+ when '.gz', '.tgz'
276
+ 'z'
277
+ when '.bz2', '.tbz2'
278
+ 'j'
279
+ when '.Z'
280
+ 'Z'
281
+ else
282
+ ''
283
+ end
284
+ end
285
+
286
+ # From: http://stackoverflow.com/a/5471032/7672
287
+ # Thanks, Mislav!
288
+ #
289
+ # Cross-platform way of finding an executable in the $PATH.
290
+ #
291
+ # which('ruby') #=> /usr/bin/ruby
292
+ def which(cmd)
293
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
294
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
295
+ exts.each { |ext|
296
+ exe = File.join(path, "#{cmd}#{ext}")
297
+ return exe if File.executable? exe
298
+ }
299
+ end
300
+ return nil
301
+ end
302
+
303
+ def detect_host
304
+ return @detect_host if defined?(@detect_host)
305
+
306
+ begin
307
+ ENV["LC_ALL"], old_lc_all = "C", ENV["LC_ALL"]
308
+
309
+ output = `#{gcc_cmd} -v 2>&1`
310
+ if m = output.match(/^Target\: (.*)$/)
311
+ @detect_host = m[1]
312
+ end
313
+
314
+ @detect_host
315
+ ensure
316
+ ENV["LC_ALL"] = old_lc_all
317
+ end
318
+ end
319
+
320
+ def extract_file(file, target)
321
+ filename = File.basename(file)
322
+ FileUtils.mkdir_p target
323
+
324
+ message "Extracting #{filename} into #{target}... "
325
+ execute('extract', [tar_exe, "#{tar_compression_switch(filename)}xf", file, "-C", target], {:cd => Dir.pwd})
326
+ end
327
+
328
+ def execute(action, command, options={})
329
+ log_out = log_file(action)
330
+
331
+ Dir.chdir (options.fetch(:cd){ work_path }) do
332
+ if options.fetch(:initial_message){ true }
333
+ message "Running '#{action}' for #{@name} #{@version}... "
334
+ end
335
+
336
+ if Process.respond_to?(:spawn) && ! RbConfig.respond_to?(:java)
337
+ args = [command].flatten + [{[:out, :err]=>[log_out, "a"]}]
338
+ pid = spawn(*args)
339
+ Process.wait(pid)
340
+ else
341
+ if command.kind_of?(Array)
342
+ system(*command)
343
+ else
344
+ redirected = %Q{#{command} > "#{log_out}" 2>&1}
345
+ system(redirected)
346
+ end
347
+ end
348
+
349
+ if $?.success?
350
+ output "OK"
351
+ return true
352
+ else
353
+ if File.exist? log_out
354
+ output "ERROR, review '#{log_out}' to see what happened. Last lines are:"
355
+ output("=" * 72)
356
+ log_lines = File.readlines(log_out)
357
+ output(log_lines[-[log_lines.length, 20].min .. -1])
358
+ output("=" * 72)
359
+ end
360
+ raise "Failed to complete #{action} task"
361
+ end
362
+ end
363
+ end
364
+
365
+ def newer?(target, checkpoint)
366
+ if (target && File.exist?(target)) && (checkpoint && File.exist?(checkpoint))
367
+ File.mtime(target) > File.mtime(checkpoint)
368
+ else
369
+ false
370
+ end
371
+ end
372
+
373
+ # print out a message with the logger
374
+ def message(text)
375
+ @logger.print text
376
+ @logger.flush
377
+ end
378
+
379
+ # print out a message using the logger but return to a new line
380
+ def output(text = "")
381
+ @logger.puts text
382
+ @logger.flush
383
+ end
384
+
385
+ # Slighly modified from RubyInstaller uri_ext, Rubinius configure
386
+ # and adaptations of Wayne's RailsInstaller
387
+ def download_file(url, full_path, count = 3)
388
+ return if File.exist?(full_path)
389
+ uri = URI.parse(url)
390
+ begin
391
+ case uri.scheme.downcase
392
+ when /ftp/
393
+ download_file_ftp(uri, full_path)
394
+ when /http|https/
395
+ download_file_http(url, full_path, count)
396
+ end
397
+ rescue Exception => e
398
+ File.unlink full_path if File.exist?(full_path)
399
+ output "ERROR: #{e.message}"
400
+ raise "Failed to complete download task"
401
+ end
402
+ end
403
+
404
+ def download_file_http(url, full_path, count = 3)
405
+ filename = File.basename(full_path)
406
+ with_tempfile(filename, full_path) do |temp_file|
407
+ progress = 0
408
+ total = 0
409
+ params = {
410
+ "Accept-Encoding" => 'identity',
411
+ :content_length_proc => lambda{|length| total = length },
412
+ :progress_proc => lambda{|bytes|
413
+ new_progress = (bytes * 100) / total
414
+ message "\rDownloading %s (%3d%%) " % [filename, new_progress]
415
+ progress = new_progress
416
+ }
417
+ }
418
+ proxy_uri = URI.parse(url).scheme.downcase == 'https' ?
419
+ ENV["https_proxy"] :
420
+ ENV["http_proxy"]
421
+ if proxy_uri
422
+ _, userinfo, _p_host, _p_port = URI.split(proxy_uri)
423
+ if userinfo
424
+ proxy_user, proxy_pass = userinfo.split(/:/).map{|s| CGI.unescape(s) }
425
+ params[:proxy_http_basic_authentication] =
426
+ [proxy_uri, proxy_user, proxy_pass]
427
+ end
428
+ end
429
+ begin
430
+ OpenURI.open_uri(url, 'rb', params) do |io|
431
+ temp_file << io.read
432
+ end
433
+ output
434
+ rescue OpenURI::HTTPRedirect => redirect
435
+ raise "Too many redirections for the original URL, halting." if count <= 0
436
+ count = count - 1
437
+ return download_file(redirect.url, full_path, count - 1)
438
+ rescue => e
439
+ output e.message
440
+ return false
441
+ end
442
+ end
443
+ end
444
+
445
+ def download_file_ftp(uri, full_path)
446
+ filename = File.basename(uri.path)
447
+ with_tempfile(filename, full_path) do |temp_file|
448
+ progress = 0
449
+ total = 0
450
+ params = {
451
+ :content_length_proc => lambda{|length| total = length },
452
+ :progress_proc => lambda{|bytes|
453
+ new_progress = (bytes * 100) / total
454
+ message "\rDownloading %s (%3d%%) " % [filename, new_progress]
455
+ progress = new_progress
456
+ }
457
+ }
458
+ if ENV["ftp_proxy"]
459
+ _, userinfo, _p_host, _p_port = URI.split(ENV['ftp_proxy'])
460
+ if userinfo
461
+ proxy_user, proxy_pass = userinfo.split(/:/).map{|s| CGI.unescape(s) }
462
+ params[:proxy_http_basic_authentication] =
463
+ [ENV['ftp_proxy'], proxy_user, proxy_pass]
464
+ end
465
+ end
466
+ OpenURI.open_uri(uri, 'rb', params) do |io|
467
+ temp_file << io.read
468
+ end
469
+ output
470
+ end
471
+ rescue Net::FTPError
472
+ return false
473
+ end
474
+
475
+ def with_tempfile(filename, full_path)
476
+ temp_file = Tempfile.new("download-#{filename}")
477
+ temp_file.binmode
478
+ yield temp_file
479
+ temp_file.close
480
+ File.unlink full_path if File.exist?(full_path)
481
+ FileUtils.mkdir_p File.dirname(full_path)
482
+ FileUtils.mv temp_file.path, full_path, :force => true
483
+ end
484
+
485
+ def gcc_cmd
486
+ cc = ENV["CC"] || RbConfig::CONFIG["CC"] || "gcc"
487
+ return cc.dup
488
+ end
489
+
490
+ def make_cmd
491
+ m = ENV['MAKE'] || ENV['make'] || 'make'
492
+ return m.dup
493
+ end
494
+ end