mini_portile 0.6.2 → 0.7.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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