* 2022-01-18 [ci skip]
[ruby-80x24.org.git] / tool / extlibs.rb
blobcd8e5239b33b7848bda2727d9e2623bb2b768722
1 #!/usr/bin/ruby
3 # Used to download, extract and patch extension libraries (extlibs)
4 # for Ruby. See common.mk for Ruby's usage.
6 require 'digest'
7 require_relative 'downloader'
8 require_relative 'lib/colorize'
10 class Vars < Hash
11   def pattern
12     /\$\((#{Regexp.union(keys)})\)/
13   end
15   def expand(str)
16     if empty?
17       str
18     else
19       str.gsub(pattern) {self[$1]}
20     end
21   end
22 end
24 class ExtLibs
25   def initialize
26     @colorize = Colorize.new
27   end
29   def cache_file(url, cache_dir)
30     Downloader.cache_file(url, nil, cache_dir).to_path
31   end
33   def do_download(url, cache_dir)
34     Downloader.download(url, nil, nil, nil, :cache_dir => cache_dir)
35   end
37   def do_checksum(cache, chksums)
38     chksums.each do |sum|
39       name, sum = sum.split(/:/)
40       if $VERBOSE
41         $stdout.print "checking #{name} of #{cache} ..."
42         $stdout.flush
43       end
44       hd = Digest(name.upcase).file(cache).hexdigest
45       if $VERBOSE
46         $stdout.print " "
47         $stdout.puts hd == sum ? @colorize.pass("OK") : @colorize.fail("NG")
48         $stdout.flush
49       end
50       unless hd == sum
51         raise "checksum mismatch: #{cache}, #{name}:#{hd}, expected #{sum}"
52       end
53     end
54   end
56   def do_extract(cache, dir)
57     if $VERBOSE
58       $stdout.puts "extracting #{cache} into #{dir}"
59       $stdout.flush
60     end
61     ext = File.extname(cache)
62     case ext
63     when '.gz', '.tgz'
64       f = IO.popen(["gzip", "-dc", cache])
65       cache = cache.chomp('.gz')
66     when '.bz2', '.tbz'
67       f = IO.popen(["bzip2", "-dc", cache])
68       cache = cache.chomp('.bz2')
69     when '.xz', '.txz'
70       f = IO.popen(["xz", "-dc", cache])
71       cache = cache.chomp('.xz')
72     else
73       inp = cache
74     end
75     inp ||= f.binmode
76     ext = File.extname(cache)
77     case ext
78     when '.tar', /\A\.t[gbx]z\z/
79       pid = Process.spawn("tar", "xpf", "-", in: inp, chdir: dir)
80     when '.zip'
81       pid = Process.spawn("unzip", inp, "-d", dir)
82     end
83     f.close if f
84     Process.wait(pid)
85     $?.success? or raise "failed to extract #{cache}"
86   end
88   def do_patch(dest, patch, args)
89     if $VERBOSE
90       $stdout.puts "applying #{patch} under #{dest}"
91       $stdout.flush
92     end
93     Process.wait(Process.spawn(ENV.fetch("PATCH", "patch"), "-d", dest, "-i", patch, *args))
94     $?.success? or raise "failed to patch #{patch}"
95   end
97   def do_link(file, src, dest)
98     file = File.join(dest, file)
99     if (target = src).start_with?("/")
100       target = File.join([".."] * file.count("/"), src)
101     end
102     return unless File.exist?(File.expand_path(target, File.dirname(file)))
103     File.unlink(file) rescue nil
104     begin
105       File.symlink(target, file)
106     rescue
107     else
108       if $VERBOSE
109         $stdout.puts "linked #{target} to #{file}"
110         $stdout.flush
111       end
112       return
113     end
114     begin
115       src = src.sub(/\A\//, '')
116       File.copy_stream(src, file)
117     rescue
118       if $VERBOSE
119         $stdout.puts "failed to link #{src} to #{file}: #{$!.message}"
120       end
121     else
122       if $VERBOSE
123         $stdout.puts "copied #{src} to #{file}"
124       end
125     end
126   end
128   def do_exec(command, dir, dest)
129     dir = dir ? File.join(dest, dir) : dest
130     if $VERBOSE
131       $stdout.puts "running #{command.dump} under #{dir}"
132       $stdout.flush
133     end
134     system(command, chdir: dir) or raise "failed #{command.dump}"
135   end
137   def do_command(mode, dest, url, cache_dir, chksums)
138     extracted = false
139     base = /.*(?=\.tar(?:\.\w+)?\z)/
141     case mode
142     when :download
143       cache = do_download(url, cache_dir)
144       do_checksum(cache, chksums)
145     when :extract
146       cache = cache_file(url, cache_dir)
147       target = File.join(dest, File.basename(cache)[base])
148       unless File.directory?(target)
149         do_checksum(cache, chksums)
150         extracted = do_extract(cache, dest)
151       end
152     when :all
153       cache = do_download(url, cache_dir)
154       target = File.join(dest, File.basename(cache)[base])
155       unless File.directory?(target)
156         do_checksum(cache, chksums)
157         extracted = do_extract(cache, dest)
158       end
159     end
160     extracted
161   end
163   def run(argv)
164     cache_dir = nil
165     mode = :all
166     until argv.empty?
167       case argv[0]
168       when '--download'
169         mode = :download
170       when '--extract'
171         mode = :extract
172       when '--patch'
173         mode = :patch
174       when '--all'
175         mode = :all
176       when '--cache'
177         argv.shift
178         cache_dir = argv[0]
179       when /\A--cache=/
180         cache_dir = $'
181       when '--'
182         argv.shift
183         break
184       when /\A-/
185         warn "unknown option: #{argv[0]}"
186         return false
187       else
188         break
189       end
190       argv.shift
191     end
193     success = true
194     argv.each do |dir|
195       Dir.glob("#{dir}/**/extlibs") do |list|
196         if $VERBOSE
197           $stdout.puts "downloading for #{list}"
198           $stdout.flush
199         end
200         vars = Vars.new
201         extracted = false
202         dest = File.dirname(list)
203         url = chksums = nil
204         IO.foreach(list) do |line|
205           line.sub!(/\s*#.*/, '')
206           if /^(\w+)\s*=\s*(.*)/ =~ line
207             vars[$1] = vars.expand($2)
208             next
209           end
210           if chksums
211             chksums.concat(line.split)
212           elsif /^\t/ =~ line
213             if extracted and (mode == :all or mode == :patch)
214               patch, *args = line.split.map {|s| vars.expand(s)}
215               do_patch(dest, patch, args)
216             end
217             next
218           elsif /^!\s*(?:chdir:\s*([^|\s]+)\|\s*)?(.*)/ =~ line
219             if extracted and (mode == :all or mode == :patch)
220               command = vars.expand($2.strip)
221               chdir = $1 and chdir = vars.expand(chdir)
222               do_exec(command, chdir, dest)
223             end
224             next
225           elsif /->/ =~ line
226             if extracted and (mode == :all or mode == :patch)
227               link, file = $`.strip, $'.strip
228               do_link(vars.expand(link), vars.expand(file), dest)
229             end
230             next
231           else
232             url, *chksums = line.split(' ')
233           end
234           if chksums.last == '\\'
235             chksums.pop
236             next
237           end
238           unless url
239             chksums = nil
240             next
241           end
242           url = vars.expand(url)
243           begin
244             extracted = do_command(mode, dest, url, cache_dir, chksums)
245           rescue => e
246             warn e.full_message
247             success = false
248           end
249           url = chksums = nil
250         end
251       end
252     end
253     success
254   end
256   def self.run(argv)
257     self.new.run(argv)
258   end
261 if $0 == __FILE__
262   exit ExtLibs.run(ARGV)