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