fix: NameError about unknown exception
[rbot] / setup.rb
1 #!/usr/bin/env ruby
2 #
3 # setup.rb
4 #
5 # Copyright (c) 2000-2005 Minero Aoki
6 #
7 # This program is free software.
8 # You can distribute/modify this program under the terms of
9 # the GNU LGPL, Lesser General Public License version 2.1.
10 #
11
12 unless Enumerable.method_defined?(:map)   # Ruby 1.4.6
13   module Enumerable
14     alias map collect
15   end
16 end
17
18 unless File.respond_to?(:read)   # Ruby 1.6
19   def File.read(fname)
20     open(fname) { |f| return f.read }
21   end
22 end
23
24 unless Errno.const_defined?(:ENOTEMPTY)   # Windows?
25   module Errno
26     class ENOTEMPTY
27       # We do not raise this exception, implementation is not needed.
28     end
29   end
30 end
31
32 def File.binread(fname)
33   open(fname, 'rb') { |f| return f.read }
34 end
35
36 # for corrupted Windows' stat(2)
37 def File.dir?(path)
38   File.directory?((path[-1,1] == '/') ? path : path + '/')
39 end
40
41
42 class ConfigTable
43
44   include Enumerable
45
46   def initialize(rbconfig)
47     @rbconfig = rbconfig
48     @items = []
49     @table = {}
50     # options
51     @install_prefix = nil
52     @config_opt = nil
53     @verbose = true
54     @no_harm = false
55   end
56
57   attr_accessor :install_prefix
58   attr_accessor :config_opt
59
60   attr_writer :verbose
61
62   def verbose?
63     @verbose
64   end
65
66   attr_writer :no_harm
67
68   def no_harm?
69     @no_harm
70   end
71
72   def [](key)
73     lookup(key).resolve(self)
74   end
75
76   def []=(key, val)
77     lookup(key).set val
78   end
79
80   def names
81     @items.map { |i| i.name }
82   end
83
84   def each(&block)
85     @items.each(&block)
86   end
87
88   def key?(name)
89     @table.key?(name)
90   end
91
92   def lookup(name)
93     @table[name] or setup_rb_error "no such config item: #{name}"
94   end
95
96   def add(item)
97     @items.push item
98     @table[item.name] = item
99   end
100
101   def remove(name)
102     item = lookup(name)
103     @items.delete_if { |i| i.name == name }
104     @table.delete_if { |name, i| i.name == name }
105     item
106   end
107
108   def load_script(path, inst = nil)
109     if File.file?(path)
110       MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
111     end
112   end
113
114   def savefile
115     '.config'
116   end
117
118   def load_savefile
119     begin
120       File.foreach(savefile()) do |line|
121         k, v = *line.split(/=/, 2)
122         self[k] = v.strip
123       end
124     rescue Errno::ENOENT
125       setup_rb_error $!.message + "\n#{File.basename($PROGRAM_NAME)} config first"
126     end
127   end
128
129   def save
130     @items.each { |i| i.value }
131     File.open(savefile(), 'w') { |f|
132       @items.each do |i|
133         f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
134       end
135     }
136   end
137
138   def load_standard_entries
139     standard_entries(@rbconfig).each do |ent|
140       add ent
141     end
142   end
143
144   def standard_entries(rbconfig)
145     c = rbconfig
146
147     rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
148
149     major = c['MAJOR'].to_i
150     minor = c['MINOR'].to_i
151     teeny = c['TEENY'].to_i
152     version = "#{major}.#{minor}"
153
154     # ruby ver. >= 1.4.4?
155     newpath_p = ((major >= 2) or
156                  ((major == 1) and
157                   ((minor >= 5) or
158                    ((minor == 4) and (teeny >= 4)))))
159
160     if c['rubylibdir']
161       # V > 1.6.3
162       libruby         = "#{c['prefix']}/lib/ruby"
163       librubyver      = c['rubylibdir']
164       librubyverarch  = c['archdir']
165       siteruby        = c['sitedir']
166       siterubyver     = c['sitelibdir']
167       siterubyverarch = c['sitearchdir']
168     elsif newpath_p
169       # 1.4.4 <= V <= 1.6.3
170       libruby         = "#{c['prefix']}/lib/ruby"
171       librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
172       librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
173       siteruby        = c['sitedir']
174       siterubyver     = "$siteruby/#{version}"
175       siterubyverarch = "$siterubyver/#{c['arch']}"
176     else
177       # V < 1.4.4
178       libruby         = "#{c['prefix']}/lib/ruby"
179       librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
180       librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
181       siteruby        = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
182       siterubyver     = siteruby
183       siterubyverarch = "$siterubyver/#{c['arch']}"
184     end
185     parameterize = lambda { |path|
186       path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
187     }
188
189     if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
190       makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
191     else
192       makeprog = 'make'
193     end
194
195     [
196       ExecItem.new('installdirs', 'std/site/home',
197                    'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
198           {|val, table|
199             case val
200             when 'std'
201               table['rbdir'] = '$librubyver'
202               table['sodir'] = '$librubyverarch'
203             when 'site'
204               table['rbdir'] = '$siterubyver'
205               table['sodir'] = '$siterubyverarch'
206             when 'home'
207               setup_rb_error '$HOME was not set' unless ENV['HOME']
208               table['prefix'] = ENV['HOME']
209               table['rbdir'] = '$libdir/ruby'
210               table['sodir'] = '$libdir/ruby'
211             end
212           },
213       PathItem.new('prefix', 'path', c['prefix'],
214                    'path prefix of target environment'),
215       PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
216                    'the directory for commands'),
217       PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
218                    'the directory for libraries'),
219       PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
220                    'the directory for shared data'),
221       PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
222                    'the directory for man pages'),
223       PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
224                    'the directory for system configuration files'),
225       PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
226                    'the directory for local state data'),
227       PathItem.new('libruby', 'path', libruby,
228                    'the directory for ruby libraries'),
229       PathItem.new('librubyver', 'path', librubyver,
230                    'the directory for standard ruby libraries'),
231       PathItem.new('librubyverarch', 'path', librubyverarch,
232                    'the directory for standard ruby extensions'),
233       PathItem.new('siteruby', 'path', siteruby,
234                    'the directory for version-independent aux ruby libraries'),
235       PathItem.new('siterubyver', 'path', siterubyver,
236                    'the directory for aux ruby libraries'),
237       PathItem.new('siterubyverarch', 'path', siterubyverarch,
238                    'the directory for aux ruby binaries'),
239       PathItem.new('rbdir', 'path', '$siterubyver',
240                    'the directory for ruby scripts'),
241       PathItem.new('sodir', 'path', '$siterubyverarch',
242                    'the directory for ruby extensions'),
243       PathItem.new('rubypath', 'path', rubypath,
244                    'the path to set to #! line'),
245       ProgramItem.new('rubyprog', 'name', rubypath,
246                       'the ruby program using for installation'),
247       ProgramItem.new('makeprog', 'name', makeprog,
248                       'the make program to compile ruby extensions'),
249       SelectItem.new('shebang', 'all/ruby/never', 'never',
250                      'shebang line (#!) editing mode'),
251       BoolItem.new('without-ext', 'yes/no', 'no',
252                    'does not compile/install ruby extensions')
253     ]
254   end
255   private :standard_entries
256
257   def load_multipackage_entries
258     multipackage_entries().each do |ent|
259       add ent
260     end
261   end
262
263   def multipackage_entries
264     [
265       PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
266                                'package names that you want to install'),
267       PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
268                                'package names that you do not want to install')
269     ]
270   end
271   private :multipackage_entries
272
273   ALIASES = {
274     'std-ruby'         => 'librubyver',
275     'stdruby'          => 'librubyver',
276     'rubylibdir'       => 'librubyver',
277     'archdir'          => 'librubyverarch',
278     'site-ruby-common' => 'siteruby',     # For backward compatibility
279     'site-ruby'        => 'siterubyver',  # For backward compatibility
280     'bin-dir'          => 'bindir',
281     'rb-dir'           => 'rbdir',
282     'so-dir'           => 'sodir',
283     'data-dir'         => 'datadir',
284     'ruby-path'        => 'rubypath',
285     'ruby-prog'        => 'rubyprog',
286     'ruby'             => 'rubyprog',
287     'make-prog'        => 'makeprog',
288     'make'             => 'makeprog'
289   }
290
291   def fixup
292     ALIASES.each do |ali, name|
293       @table[ali] = @table[name]
294     end
295     @items.freeze
296     @table.freeze
297     @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
298   end
299
300   def parse_opt(opt)
301     m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}"
302     m.to_a[1,2]
303   end
304
305   def dllext
306     @rbconfig['DLEXT']
307   end
308
309   def value_config?(name)
310     lookup(name).value?
311   end
312
313   class Item
314     def initialize(name, template, default, desc)
315       @name = name.freeze
316       @template = template
317       @value = default
318       @default = default
319       @description = desc
320     end
321
322     attr_reader :name
323     attr_reader :description
324
325     attr_accessor :default
326     alias help_default default
327
328     def help_opt
329       "--#{@name}=#{@template}"
330     end
331
332     def value?
333       true
334     end
335
336     def value
337       @value
338     end
339
340     def resolve(table)
341       @value.gsub(%r<\$([^/]+)>) { table[$1] }
342     end
343
344     def set(val)
345       @value = check(val)
346     end
347
348     private
349
350     def check(val)
351       setup_rb_error "config: --#{name} requires argument" unless val
352       val
353     end
354   end
355
356   class BoolItem < Item
357     def config_type
358       'bool'
359     end
360
361     def help_opt
362       "--#{@name}"
363     end
364
365     private
366
367     def check(val)
368       return 'yes' unless val
369       case val
370       when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
371       when /\An(o)?\z/i, /\Af(alse)\z/i  then 'no'
372       else
373         setup_rb_error "config: --#{@name} accepts only yes/no for argument"
374       end
375     end
376   end
377
378   class PathItem < Item
379     def config_type
380       'path'
381     end
382
383     private
384
385     def check(path)
386       setup_rb_error "config: --#{@name} requires argument" unless path
387       path[0,1] == '$' ? path : File.expand_path(path)
388     end
389   end
390
391   class ProgramItem < Item
392     def config_type
393       'program'
394     end
395   end
396
397   class SelectItem < Item
398     def initialize(name, selection, default, desc)
399       super
400       @ok = selection.split('/')
401     end
402
403     def config_type
404       'select'
405     end
406
407     private
408
409     def check(val)
410       unless @ok.include?(val.strip)
411         setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
412       end
413       val.strip
414     end
415   end
416
417   class ExecItem < Item
418     def initialize(name, selection, desc, &block)
419       super name, selection, nil, desc
420       @ok = selection.split('/')
421       @action = block
422     end
423
424     def config_type
425       'exec'
426     end
427
428     def value?
429       false
430     end
431
432     def resolve(table)
433       setup_rb_error "$#{name()} wrongly used as option value"
434     end
435
436     undef set
437
438     def evaluate(val, table)
439       v = val.strip.downcase
440       unless @ok.include?(v)
441         setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
442       end
443       @action.call v, table
444     end
445   end
446
447   class PackageSelectionItem < Item
448     def initialize(name, template, default, help_default, desc)
449       super name, template, default, desc
450       @help_default = help_default
451     end
452
453     attr_reader :help_default
454
455     def config_type
456       'package'
457     end
458
459     private
460
461     def check(val)
462       unless File.dir?("packages/#{val}")
463         setup_rb_error "config: no such package: #{val}"
464       end
465       val
466     end
467   end
468
469   class MetaConfigEnvironment
470     def initialize(config, installer)
471       @config = config
472       @installer = installer
473     end
474
475     def config_names
476       @config.names
477     end
478
479     def config?(name)
480       @config.key?(name)
481     end
482
483     def bool_config?(name)
484       @config.lookup(name).config_type == 'bool'
485     end
486
487     def path_config?(name)
488       @config.lookup(name).config_type == 'path'
489     end
490
491     def value_config?(name)
492       @config.lookup(name).config_type != 'exec'
493     end
494
495     def add_config(item)
496       @config.add item
497     end
498
499     def add_bool_config(name, default, desc)
500       @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
501     end
502
503     def add_path_config(name, default, desc)
504       @config.add PathItem.new(name, 'path', default, desc)
505     end
506
507     def set_config_default(name, default)
508       @config.lookup(name).default = default
509     end
510
511     def remove_config(name)
512       @config.remove(name)
513     end
514
515     # For only multipackage
516     def packages
517       raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
518       @installer.packages
519     end
520
521     # For only multipackage
522     def declare_packages(list)
523       raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
524       @installer.packages = list
525     end
526   end
527
528 end   # class ConfigTable
529
530
531 # This module requires: #verbose?, #no_harm?
532 module FileOperations
533
534   def mkdir_p(dirname, prefix = nil)
535     dirname = prefix + File.expand_path(dirname) if prefix
536     $stderr.puts "mkdir -p #{dirname}" if verbose?
537     return if no_harm?
538
539     # Does not check '/', it's too abnormal.
540     dirs = File.expand_path(dirname).split(%r<(?=/)>)
541     if /\A[a-z]:\z/i =~ dirs[0]
542       disk = dirs.shift
543       dirs[0] = disk + dirs[0]
544     end
545     dirs.each_index do |idx|
546       path = dirs[0..idx].join('')
547       Dir.mkdir path unless File.dir?(path)
548     end
549   end
550
551   def rm_f(path)
552     $stderr.puts "rm -f #{path}" if verbose?
553     return if no_harm?
554     force_remove_file path
555   end
556
557   def rm_rf(path)
558     $stderr.puts "rm -rf #{path}" if verbose?
559     return if no_harm?
560     remove_tree path
561   end
562
563   def remove_tree(path)
564     if File.symlink?(path)
565       remove_file path
566     elsif File.dir?(path)
567       remove_tree0 path
568     else
569       force_remove_file path
570     end
571   end
572
573   def remove_tree0(path)
574     Dir.foreach(path) do |ent|
575       next if ent == '.'
576       next if ent == '..'
577
578       entpath = "#{path}/#{ent}"
579       if File.symlink?(entpath)
580         remove_file entpath
581       elsif File.dir?(entpath)
582         remove_tree0 entpath
583       else
584         force_remove_file entpath
585       end
586     end
587     begin
588       Dir.rmdir path
589     rescue Errno::ENOTEMPTY
590       # directory may not be empty
591     end
592   end
593
594   def move_file(src, dest)
595     force_remove_file dest
596     begin
597       File.rename src, dest
598     rescue
599       File.open(dest, 'wb') { |f| f.write File.binread(src) }
600       File.chmod File.stat(src).mode, dest
601       File.unlink src
602     end
603   end
604
605   def force_remove_file(path)
606     begin
607       remove_file path
608     rescue
609     end
610   end
611
612   def remove_file(path)
613     File.chmod 0777, path
614     File.unlink path
615   end
616
617   def install(from, dest, mode, prefix = nil)
618     $stderr.puts "install #{from} #{dest}" if verbose?
619     return if no_harm?
620
621     realdest = prefix ? prefix + File.expand_path(dest) : dest
622     realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
623     str = File.binread(from)
624     if diff?(str, realdest)
625       verbose_off { rm_f realdest if File.exist?(realdest) }
626       File.open(realdest, 'wb') { |f| f.write str }
627       File.chmod mode, realdest
628
629       File.open("#{objdir_root()}/InstalledFiles", 'a') do |f|
630         if prefix
631           f.puts realdest.sub(prefix, '')
632         else
633           f.puts realdest
634         end
635       end
636     end
637   end
638
639   def diff?(new_content, path)
640     return true unless File.exist?(path)
641
642     new_content != File.binread(path)
643   end
644
645   def command(*args)
646     $stderr.puts args.join(' ') if verbose?
647     system(*args) or raise RuntimeError,
648         "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
649   end
650
651   def ruby(*args)
652     command config('rubyprog'), *args
653   end
654
655   def make(task = nil)
656     command(*[config('makeprog'), task].compact)
657   end
658
659   def extdir?(dir)
660     File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
661   end
662
663   def files_of(dir)
664     Dir.open(dir) { |d| return d.select { |ent| File.file?("#{dir}/#{ent}") } }
665   end
666
667   DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
668
669   def directories_of(dir)
670     Dir.open(dir) { |d| return d.select { |ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT }
671   end
672 end
673
674
675 # This module requires: #srcdir_root, #objdir_root, #relpath
676 module HookScriptAPI
677
678   def get_config(key)
679     @config[key]
680   end
681
682   alias config get_config
683
684   # obsolete: use metaconfig to change configuration
685   def set_config(key, val)
686     @config[key] = val
687   end
688
689   #
690   # srcdir/objdir (works only in the package directory)
691   #
692
693   def curr_srcdir
694     "#{srcdir_root()}/#{relpath()}"
695   end
696
697   def curr_objdir
698     "#{objdir_root()}/#{relpath()}"
699   end
700
701   def srcfile(path)
702     "#{curr_srcdir()}/#{path}"
703   end
704
705   def srcexist?(path)
706     File.exist?(srcfile(path))
707   end
708
709   def srcdirectory?(path)
710     File.dir?(srcfile(path))
711   end
712
713   def srcfile?(path)
714     File.file?(srcfile(path))
715   end
716
717   def srcentries(path = '.')
718     Dir.open("#{curr_srcdir()}/#{path}") { |d| return d.to_a - %w(. ..) }
719   end
720
721   def srcfiles(path = '.')
722     srcentries(path).select { |fname| File.file?(File.join(curr_srcdir(), path, fname)) }
723   end
724
725   def srcdirectories(path = '.')
726     srcentries(path).select { |fname| File.dir?(File.join(curr_srcdir(), path, fname)) }
727   end
728 end
729
730
731 class ToplevelInstaller
732
733   Version   = '3.4.1'
734   Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
735
736   TASKS = [
737     ['all',       'do config, setup, then install'],
738     ['config',    'saves your configurations'],
739     ['show',      'shows current configuration'],
740     ['setup',     'compiles ruby extensions and others'],
741     ['install',   'installs files'],
742     ['test',      'run all tests in test/'],
743     ['clean',     "does `make clean' for each extension"],
744     ['distclean', "does `make distclean' for each extension"]
745   ]
746
747   def ToplevelInstaller.invoke
748     config = ConfigTable.new(load_rbconfig())
749     config.load_standard_entries
750     config.load_multipackage_entries if multipackage?
751     config.fixup
752     klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
753     klass.new(File.dirname($PROGRAM_NAME), config).invoke
754   end
755
756   def ToplevelInstaller.multipackage?
757     File.dir?("#{File.dirname($PROGRAM_NAME)}/packages")
758   end
759
760   def ToplevelInstaller.load_rbconfig
761     if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
762       ARGV.delete(arg)
763       load File.expand_path(arg.split(/=/, 2)[1])
764       $LOADED_FEATURES.push 'rbconfig.rb'
765     else
766       require 'rbconfig'
767     end
768     ::Config::CONFIG
769   end
770
771   def initialize(ardir_root, config)
772     @ardir = File.expand_path(ardir_root)
773     @config = config
774     # cache
775     @valid_task_re = nil
776   end
777
778   def config(key)
779     @config[key]
780   end
781
782   def inspect
783     "#<#{self.class} #{__id__()}>"
784   end
785
786   def invoke
787     run_metaconfigs
788     case task = parsearg_global()
789     when nil, 'all'
790       parsearg_config
791       init_installers
792       exec_config
793       exec_setup
794       exec_install
795     else
796       case task
797       when 'config', 'test'
798         ;
799       when 'clean', 'distclean'
800         @config.load_savefile if File.exist?(@config.savefile)
801       else
802         @config.load_savefile
803       end
804       __send__ "parsearg_#{task}"
805       init_installers
806       __send__ "exec_#{task}"
807     end
808   end
809
810   def run_metaconfigs
811     @config.load_script "#{@ardir}/metaconfig"
812   end
813
814   def init_installers
815     @installer = Installer.new(@config, @ardir, File.expand_path('.'))
816   end
817
818   #
819   # Hook Script API bases
820   #
821
822   def srcdir_root
823     @ardir
824   end
825
826   def objdir_root
827     '.'
828   end
829
830   def relpath
831     '.'
832   end
833
834   #
835   # Option Parsing
836   #
837
838   def parsearg_global
839     while (arg = ARGV.shift)
840       case arg
841       when /\A\w+\z/
842         setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
843         return arg
844       when '-q', '--quiet'
845         @config.verbose = false
846       when '--verbose'
847         @config.verbose = true
848       when '--help'
849         print_usage $stdout
850         exit 0
851       when '--version'
852         puts "#{File.basename($PROGRAM_NAME)} version #{Version}"
853         exit 0
854       when '--copyright'
855         puts Copyright
856         exit 0
857       else
858         setup_rb_error "unknown global option '#{arg}'"
859       end
860     end
861     nil
862   end
863
864   def valid_task?(t)
865     valid_task_re() =~ t
866   end
867
868   def valid_task_re
869     @valid_task_re ||= /\A(?:#{TASKS.map { |task,desc| task }.join('|')})\z/
870   end
871
872   def parsearg_no_options
873     unless ARGV.empty?
874       task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1)
875       setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}"
876     end
877   end
878
879   alias parsearg_show       parsearg_no_options
880   alias parsearg_setup      parsearg_no_options
881   alias parsearg_test       parsearg_no_options
882   alias parsearg_clean      parsearg_no_options
883   alias parsearg_distclean  parsearg_no_options
884
885   def parsearg_config
886     evalopt = []
887     set = []
888     @config.config_opt = []
889     while (i = ARGV.shift)
890       if /\A--?\z/ =~ i
891         @config.config_opt = ARGV.dup
892         break
893       end
894       name, value = *@config.parse_opt(i)
895       if @config.value_config?(name)
896         @config[name] = value
897       else
898         evalopt.push [name, value]
899       end
900       set.push name
901     end
902     evalopt.each do |name, value|
903       @config.lookup(name).evaluate value, @config
904     end
905     # Check if configuration is valid
906     set.each do |n|
907       @config[n] if @config.value_config?(n)
908     end
909   end
910
911   def parsearg_install
912     @config.no_harm = false
913     @config.install_prefix = ''
914     while (a = ARGV.shift)
915       case a
916       when '--no-harm'
917         @config.no_harm = true
918       when /\A--prefix=/
919         path = a.split(/=/, 2)[1]
920         path = File.expand_path(path) unless path[0,1] == '/'
921         @config.install_prefix = path
922       else
923         setup_rb_error "install: unknown option #{a}"
924       end
925     end
926   end
927
928   def print_usage(out)
929     out.puts 'Typical Installation Procedure:'
930     out.puts "  $ ruby #{File.basename $PROGRAM_NAME} config"
931     out.puts "  $ ruby #{File.basename $PROGRAM_NAME} setup"
932     out.puts "  # ruby #{File.basename $PROGRAM_NAME} install (may require root privilege)"
933     out.puts
934     out.puts 'Detailed Usage:'
935     out.puts "  ruby #{File.basename $PROGRAM_NAME} <global option>"
936     out.puts "  ruby #{File.basename $PROGRAM_NAME} [<global options>] <task> [<task options>]"
937
938     fmt = "  %-24s %s\n"
939     out.puts
940     out.puts 'Global options:'
941     out.printf fmt, '-q,--quiet',     'suppress message outputs'
942     out.printf fmt, '   --verbose',   'output messages verbosely'
943     out.printf fmt, '   --help',      'print this message'
944     out.printf fmt, '   --version',   'print version and quit'
945     out.printf fmt, '   --copyright', 'print copyright and quit'
946     out.puts
947     out.puts 'Tasks:'
948     TASKS.each do |name, desc|
949       out.printf fmt, name, desc
950     end
951
952     fmt = "  %-24s %s [%s]\n"
953     out.puts
954     out.puts 'Options for CONFIG or ALL:'
955     @config.each do |item|
956       out.printf fmt, item.help_opt, item.description, item.help_default
957     end
958     out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
959     out.puts
960     out.puts 'Options for INSTALL:'
961     out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
962     out.printf fmt, '--prefix=path', 'install path prefix', ''
963     out.puts
964   end
965
966   #
967   # Task Handlers
968   #
969
970   def exec_config
971     @installer.exec_config
972     @config.save   # must be final
973   end
974
975   def exec_setup
976     @installer.exec_setup
977   end
978
979   def exec_install
980     @installer.exec_install
981   end
982
983   def exec_test
984     @installer.exec_test
985   end
986
987   def exec_show
988     @config.each do |i|
989       printf "%-20s %s\n", i.name, i.value if i.value?
990     end
991   end
992
993   def exec_clean
994     @installer.exec_clean
995   end
996
997   def exec_distclean
998     @installer.exec_distclean
999   end
1000 end   # class ToplevelInstaller
1001
1002
1003 class ToplevelInstallerMulti < ToplevelInstaller
1004
1005   include FileOperations
1006
1007   def initialize(ardir_root, config)
1008     super
1009     @packages = directories_of("#{@ardir}/packages")
1010     raise 'no package exists' if @packages.empty?
1011
1012     @root_installer = Installer.new(@config, @ardir, File.expand_path('.'))
1013   end
1014
1015   def run_metaconfigs
1016     @config.load_script "#{@ardir}/metaconfig", self
1017     @packages.each do |name|
1018       @config.load_script "#{@ardir}/packages/#{name}/metaconfig"
1019     end
1020   end
1021
1022   attr_reader :packages
1023
1024   def packages=(list)
1025     raise 'package list is empty' if list.empty?
1026
1027     list.each do |name|
1028       raise "directory packages/#{name} does not exist"\
1029               unless File.dir?("#{@ardir}/packages/#{name}")
1030     end
1031     @packages = list
1032   end
1033
1034   def init_installers
1035     @installers = {}
1036     @packages.each do |pack|
1037       @installers[pack] = Installer.new(@config,
1038                                         "#{@ardir}/packages/#{pack}",
1039                                         "packages/#{pack}")
1040     end
1041     with    = extract_selection(config('with'))
1042     without = extract_selection(config('without'))
1043     @selected = @installers.keys.select do |name|
1044       (with.empty? or with.include?(name)) and not without.include?(name)
1045     end
1046   end
1047
1048   def extract_selection(list)
1049     a = list.split(/,/)
1050     a.each do |name|
1051       setup_rb_error "no such package: #{name}" unless @installers.key?(name)
1052     end
1053     a
1054   end
1055
1056   def print_usage(f)
1057     super
1058     f.puts 'Inluded packages:'
1059     f.puts '  ' + @packages.sort.join(' ')
1060     f.puts
1061   end
1062
1063   #
1064   # Task Handlers
1065   #
1066
1067   def exec_config
1068     run_hook 'pre-config'
1069     each_selected_installers {|inst| inst.exec_config }
1070     run_hook 'post-config'
1071     @config.save   # must be final
1072   end
1073
1074   def exec_setup
1075     run_hook 'pre-setup'
1076     each_selected_installers {|inst| inst.exec_setup }
1077     run_hook 'post-setup'
1078   end
1079
1080   def exec_install
1081     run_hook 'pre-install'
1082     each_selected_installers {|inst| inst.exec_install }
1083     run_hook 'post-install'
1084   end
1085
1086   def exec_test
1087     run_hook 'pre-test'
1088     each_selected_installers {|inst| inst.exec_test }
1089     run_hook 'post-test'
1090   end
1091
1092   def exec_clean
1093     rm_f @config.savefile
1094     run_hook 'pre-clean'
1095     each_selected_installers {|inst| inst.exec_clean }
1096     run_hook 'post-clean'
1097   end
1098
1099   def exec_distclean
1100     rm_f @config.savefile
1101     run_hook 'pre-distclean'
1102     each_selected_installers {|inst| inst.exec_distclean }
1103     run_hook 'post-distclean'
1104   end
1105
1106   #
1107   # lib
1108   #
1109
1110   def each_selected_installers
1111     Dir.mkdir 'packages' unless File.dir?('packages')
1112     @selected.each do |pack|
1113       $stderr.puts "Processing the package `#{pack}' ..." if verbose?
1114       Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
1115       Dir.chdir "packages/#{pack}"
1116       yield @installers[pack]
1117       Dir.chdir '../..'
1118     end
1119   end
1120
1121   def run_hook(id)
1122     @root_installer.run_hook id
1123   end
1124
1125   # module FileOperations requires this
1126   def verbose?
1127     @config.verbose?
1128   end
1129
1130   # module FileOperations requires this
1131   def no_harm?
1132     @config.no_harm?
1133   end
1134 end   # class ToplevelInstallerMulti
1135
1136
1137 class Installer
1138
1139   FILETYPES = %w( bin lib ext data conf man )
1140
1141   include FileOperations
1142   include HookScriptAPI
1143
1144   def initialize(config, srcroot, objroot)
1145     @config = config
1146     @srcdir = File.expand_path(srcroot)
1147     @objdir = File.expand_path(objroot)
1148     @currdir = '.'
1149   end
1150
1151   def inspect
1152     "#<#{self.class} #{File.basename(@srcdir)}>"
1153   end
1154
1155   def noop(rel)
1156   end
1157
1158   #
1159   # Hook Script API base methods
1160   #
1161
1162   def srcdir_root
1163     @srcdir
1164   end
1165
1166   def objdir_root
1167     @objdir
1168   end
1169
1170   def relpath
1171     @currdir
1172   end
1173
1174   #
1175   # Config Access
1176   #
1177
1178   # module FileOperations requires this
1179   def verbose?
1180     @config.verbose?
1181   end
1182
1183   # module FileOperations requires this
1184   def no_harm?
1185     @config.no_harm?
1186   end
1187
1188   def verbose_off
1189     begin
1190       save, @config.verbose = @config.verbose?, false
1191       yield
1192     ensure
1193       @config.verbose = save
1194     end
1195   end
1196
1197   #
1198   # TASK config
1199   #
1200
1201   def exec_config
1202     exec_task_traverse 'config'
1203   end
1204
1205   alias config_dir_bin noop
1206   alias config_dir_lib noop
1207
1208   def config_dir_ext(rel)
1209     extconf if extdir?(curr_srcdir())
1210   end
1211
1212   alias config_dir_data noop
1213   alias config_dir_conf noop
1214   alias config_dir_man noop
1215
1216   def extconf
1217     ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt
1218   end
1219
1220   #
1221   # TASK setup
1222   #
1223
1224   def exec_setup
1225     exec_task_traverse 'setup'
1226   end
1227
1228   def setup_dir_bin(rel)
1229     files_of(curr_srcdir()).each do |fname|
1230       update_shebang_line "#{curr_srcdir()}/#{fname}"
1231     end
1232   end
1233
1234   alias setup_dir_lib noop
1235
1236   def setup_dir_ext(rel)
1237     make if extdir?(curr_srcdir())
1238   end
1239
1240   alias setup_dir_data noop
1241   alias setup_dir_conf noop
1242   alias setup_dir_man noop
1243
1244   def update_shebang_line(path)
1245     return if no_harm?
1246     return if config('shebang') == 'never'
1247
1248     old = Shebang.load(path)
1249     if old
1250       $stderr.puts "warning: #{path}: Shebang line includes too many args.  It is not portable and your program may not work." if old.args.size > 1
1251       new = new_shebang(old)
1252       return if new.to_s == old.to_s
1253     else
1254       return unless config('shebang') == 'all'
1255
1256       new = Shebang.new(config('rubypath'))
1257     end
1258     $stderr.puts "updating shebang: #{File.basename(path)}" if verbose?
1259     open_atomic_writer(path) do |output|
1260       File.open(path, 'rb') do |f|
1261         f.gets if old   # discard
1262         output.puts new.to_s
1263         output.print f.read
1264       end
1265     end
1266   end
1267
1268   def new_shebang(old)
1269     if /\Aruby/ =~ File.basename(old.cmd)
1270       Shebang.new(config('rubypath'), old.args)
1271     elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby'
1272       Shebang.new(config('rubypath'), old.args[1..-1])
1273     else
1274       return old unless config('shebang') == 'all'
1275
1276       Shebang.new(config('rubypath'))
1277     end
1278   end
1279
1280   def open_atomic_writer(path, &block)
1281     tmpfile = "#{File.basename(path)}.tmp"
1282     begin
1283       File.open(tmpfile, 'wb', &block)
1284       File.rename tmpfile, File.basename(path)
1285     ensure
1286       File.unlink tmpfile if File.exist?(tmpfile)
1287     end
1288   end
1289
1290   class Shebang
1291     def Shebang.load(path)
1292       line = nil
1293       File.open(path) { |f| line = f.gets }
1294       return nil unless /\A#!/ =~ line
1295
1296       parse(line)
1297     end
1298
1299     def Shebang.parse(line)
1300       cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ')
1301       new(cmd, args)
1302     end
1303
1304     def initialize(cmd, args = [])
1305       @cmd = cmd
1306       @args = args
1307     end
1308
1309     attr_reader :cmd
1310     attr_reader :args
1311
1312     def to_s
1313       "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}")
1314     end
1315   end
1316
1317   #
1318   # TASK install
1319   #
1320
1321   def exec_install
1322     rm_f 'InstalledFiles'
1323     exec_task_traverse 'install'
1324   end
1325
1326   def install_dir_bin(rel)
1327     install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755
1328   end
1329
1330   def install_dir_lib(rel)
1331     install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644
1332   end
1333
1334   def install_dir_ext(rel)
1335     return unless extdir?(curr_srcdir())
1336     install_files rubyextensions('.'),
1337                   "#{config('sodir')}/#{File.dirname(rel)}",
1338                   0555
1339   end
1340
1341   def install_dir_data(rel)
1342     install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644
1343   end
1344
1345   def install_dir_conf(rel)
1346     # FIXME: should not remove current config files
1347     # (rename previous file to .old/.org)
1348     install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644
1349   end
1350
1351   def install_dir_man(rel)
1352     install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644
1353   end
1354
1355   def install_files(list, dest, mode)
1356     mkdir_p dest, @config.install_prefix
1357     list.each do |fname|
1358       install fname, dest, mode, @config.install_prefix
1359     end
1360   end
1361
1362   def libfiles
1363     glob_reject(%w(*.y *.output), targetfiles())
1364   end
1365
1366   def rubyextensions(dir)
1367     ents = glob_select("*.#{@config.dllext}", targetfiles())
1368     if ents.empty?
1369       setup_rb_error "no ruby extension exists: 'ruby #{$PROGRAM_NAME} setup' first"
1370     end
1371     ents
1372   end
1373
1374   def targetfiles
1375     mapdir(existfiles() - hookfiles())
1376   end
1377
1378   def mapdir(ents)
1379     ents.map do |ent|
1380       if File.exist?(ent)
1381       then ent                         # objdir
1382       else "#{curr_srcdir()}/#{ent}"   # srcdir
1383       end
1384     end
1385   end
1386
1387   # picked up many entries from cvs-1.11.1/src/ignore.c
1388   JUNK_FILES = %w(
1389     core RCSLOG tags TAGS .make.state
1390     .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
1391     *~ *.old *.bak *.BAK *.orig *.rej _$* *$
1392
1393     *.org *.in .*
1394   )
1395
1396   def existfiles
1397     glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.')))
1398   end
1399
1400   def hookfiles
1401     %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map { |fmt|
1402       %w( config setup install clean ).map {|t| sprintf(fmt, t) }
1403     }.flatten
1404   end
1405
1406   def glob_select(pat, ents)
1407     re = globs2re([pat])
1408     ents.select { |ent| re =~ ent }
1409   end
1410
1411   def glob_reject(pats, ents)
1412     re = globs2re(pats)
1413     ents.reject { |ent| re =~ ent }
1414   end
1415
1416   GLOB2REGEX = {
1417     '.' => '\.',
1418     '$' => '\$',
1419     '#' => '\#',
1420     '*' => '.*'
1421   }
1422
1423   def globs2re(pats)
1424     /\A(?:#{
1425       pats.map { |pat| pat.gsub(/[\.\$\#\*]/) { |ch| GLOB2REGEX[ch] } }.join('|')
1426     })\z/
1427   end
1428
1429   #
1430   # TASK test
1431   #
1432
1433   TESTDIR = 'test'
1434
1435   def exec_test
1436     unless File.directory?('test')
1437       $stderr.puts 'no test in this package' if verbose?
1438       return
1439     end
1440     $stderr.puts 'Running tests...' if verbose?
1441     begin
1442       require 'test/unit'
1443     rescue LoadError
1444       setup_rb_error 'test/unit cannot loaded.  You need Ruby 1.8 or later to invoke this task.'
1445     end
1446     runner = Test::Unit::AutoRunner.new(true)
1447     runner.to_run << TESTDIR
1448     runner.run
1449   end
1450
1451   #
1452   # TASK clean
1453   #
1454
1455   def exec_clean
1456     exec_task_traverse 'clean'
1457     rm_f @config.savefile
1458     rm_f 'InstalledFiles'
1459   end
1460
1461   alias clean_dir_bin noop
1462   alias clean_dir_lib noop
1463   alias clean_dir_data noop
1464   alias clean_dir_conf noop
1465   alias clean_dir_man noop
1466
1467   def clean_dir_ext(rel)
1468     return unless extdir?(curr_srcdir())
1469     make 'clean' if File.file?('Makefile')
1470   end
1471
1472   #
1473   # TASK distclean
1474   #
1475
1476   def exec_distclean
1477     exec_task_traverse 'distclean'
1478     rm_f @config.savefile
1479     rm_f 'InstalledFiles'
1480   end
1481
1482   alias distclean_dir_bin noop
1483   alias distclean_dir_lib noop
1484
1485   def distclean_dir_ext(rel)
1486     return unless extdir?(curr_srcdir())
1487     make 'distclean' if File.file?('Makefile')
1488   end
1489
1490   alias distclean_dir_data noop
1491   alias distclean_dir_conf noop
1492   alias distclean_dir_man noop
1493
1494   #
1495   # Traversing
1496   #
1497
1498   def exec_task_traverse(task)
1499     run_hook "pre-#{task}"
1500     FILETYPES.each do |type|
1501       if type == 'ext' and config('without-ext') == 'yes'
1502         $stderr.puts 'skipping ext/* by user option' if verbose?
1503         next
1504       end
1505       traverse task, type, "#{task}_dir_#{type}"
1506     end
1507     run_hook "post-#{task}"
1508   end
1509
1510   def traverse(task, rel, mid)
1511     dive_into(rel) do
1512       run_hook "pre-#{task}"
1513       __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
1514       directories_of(curr_srcdir()).each do |d|
1515         traverse task, "#{rel}/#{d}", mid
1516       end
1517       run_hook "post-#{task}"
1518     end
1519   end
1520
1521   def dive_into(rel)
1522     return unless File.dir?("#{@srcdir}/#{rel}")
1523
1524     dir = File.basename(rel)
1525     Dir.mkdir dir unless File.dir?(dir)
1526     prevdir = Dir.pwd
1527     Dir.chdir dir
1528     $stderr.puts "---> #{rel}" if verbose?
1529     @currdir = rel
1530     yield
1531     Dir.chdir prevdir
1532     $stderr.puts "<--- #{rel}" if verbose?
1533     @currdir = File.dirname(rel)
1534   end
1535
1536   def run_hook(id)
1537     path = [ "#{curr_srcdir()}/#{id}",
1538              "#{curr_srcdir()}/#{id}.rb" ].detect { |cand| File.file?(cand) }
1539     return unless path
1540
1541     begin
1542       instance_eval File.read(path), path, 1
1543     rescue
1544       raise if $DEBUG
1545
1546       setup_rb_error "hook #{path} failed:\n" + $!.message
1547     end
1548   end
1549 end   # class Installer
1550
1551
1552 class SetupError < StandardError; end
1553
1554 def setup_rb_error(msg)
1555   raise SetupError, msg
1556 end
1557
1558 if $PROGRAM_NAME == __FILE__
1559   begin
1560     ToplevelInstaller.invoke
1561   rescue SetupError
1562     raise if $DEBUG
1563
1564     $stderr.puts $!.message
1565     $stderr.puts "Try 'ruby #{$PROGRAM_NAME} --help' for detailed usage."
1566     exit 1
1567   end
1568 end