0.9.5b2
[booh] / setup.rb
1 #
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 = ::RbConfig::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     'bin-dir'          => 'bindir',
359     'rb-dir'           => 'rbdir',
360     'so-dir'           => 'sodir',
361     'data-dir'         => 'datadir',
362     'ruby-path'        => 'rubypath',
363     'ruby-prog'        => 'rubyprog',
364     'ruby'             => 'rubyprog',
365     'make-prog'        => 'makeprog',
366     'make'             => 'makeprog'
367   }
368 end
369 multipackage_conf = [
370   PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
371                            'package names that you want to install'),
372   PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
373                            'package names that you do not want to install')
374 ]
375 if multipackage_install?
376   ConfigTable = ConfigTable_class.new(common_conf + multipackage_conf)
377 else
378   ConfigTable = ConfigTable_class.new(common_conf)
379 end
380
381
382 module MetaConfigAPI
383
384   def eval_file_ifexist(fname)
385     instance_eval File.read(fname), fname, 1 if File.file?(fname)
386   end
387
388   def config_names
389     ConfigTable.map {|i| i.name }
390   end
391
392   def config?(name)
393     ConfigTable.key?(name)
394   end
395
396   def bool_config?(name)
397     ConfigTable.lookup(name).config_type == 'bool'
398   end
399
400   def path_config?(name)
401     ConfigTable.lookup(name).config_type == 'path'
402   end
403
404   def value_config?(name)
405     case ConfigTable.lookup(name).config_type
406     when 'bool', 'path'
407       true
408     else
409       false
410     end
411   end
412
413   def add_config(item)
414     ConfigTable.add item
415   end
416
417   def add_bool_config(name, default, desc)
418     ConfigTable.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
419   end
420
421   def add_path_config(name, default, desc)
422     ConfigTable.add PathItem.new(name, 'path', default, desc)
423   end
424
425   def set_config_default(name, default)
426     ConfigTable.lookup(name).default = default
427   end
428
429   def remove_config(name)
430     ConfigTable.remove(name)
431   end
432
433 end
434
435
436 #
437 # File Operations
438 #
439
440 module FileOperations
441
442   def mkdir_p(dirname, prefix = nil)
443     dirname = prefix + File.expand_path(dirname) if prefix
444     $stderr.puts "mkdir -p #{dirname}" if verbose?
445     return if no_harm?
446
447     # does not check '/'... it's too abnormal case
448     dirs = File.expand_path(dirname).split(%r<(?=/)>)
449     if /\A[a-z]:\z/i =~ dirs[0]
450       disk = dirs.shift
451       dirs[0] = disk + dirs[0]
452     end
453     dirs.each_index do |idx|
454       path = dirs[0..idx].join('')
455       Dir.mkdir path unless File.dir?(path)
456     end
457   end
458
459   def rm_f(fname)
460     $stderr.puts "rm -f #{fname}" if verbose?
461     return if no_harm?
462
463     if File.exist?(fname) or File.symlink?(fname)
464       File.chmod 0777, fname
465       File.unlink fname
466     end
467   end
468
469   def rm_rf(dn)
470     $stderr.puts "rm -rf #{dn}" if verbose?
471     return if no_harm?
472
473     Dir.chdir dn
474     Dir.foreach('.') do |fn|
475       next if fn == '.'
476       next if fn == '..'
477       if File.dir?(fn)
478         verbose_off {
479           rm_rf fn
480         }
481       else
482         verbose_off {
483           rm_f fn
484         }
485       end
486     end
487     Dir.chdir '..'
488     Dir.rmdir dn
489   end
490
491   def move_file(src, dest)
492     File.unlink dest if File.exist?(dest)
493     begin
494       File.rename src, dest
495     rescue
496       File.open(dest, 'wb') {|f| f.write File.binread(src) }
497       File.chmod File.stat(src).mode, dest
498       File.unlink src
499     end
500   end
501
502   def install(from, dest, mode, prefix = nil)
503     $stderr.puts "install #{from} #{dest}" if verbose?
504     return if no_harm?
505
506     realdest = prefix ? prefix + File.expand_path(dest) : dest
507     realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
508     str = File.binread(from)
509     if diff?(str, realdest)
510       verbose_off {
511         rm_f realdest if File.exist?(realdest)
512       }
513       File.open(realdest, 'wb') {|f|
514         f.write str
515       }
516       File.chmod mode, realdest
517
518       File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
519         if prefix
520           f.puts realdest.sub(prefix, '')
521         else
522           f.puts realdest
523         end
524       }
525     end
526   end
527
528   def diff?(new_content, path)
529     return true unless File.exist?(path)
530     new_content != File.binread(path)
531   end
532
533   def command(str)
534     $stderr.puts str if verbose?
535     system str or raise RuntimeError, "'system #{str}' failed"
536   end
537
538   def ruby(str)
539     command config('rubyprog') + ' ' + str
540   end
541   
542   def make(task = '')
543     command config('makeprog') + ' ' + task
544   end
545
546   def extdir?(dir)
547     File.exist?(dir + '/MANIFEST')
548   end
549
550   def all_files_in(dirname)
551     Dir.open(dirname) {|d|
552       return d.select {|ent| File.file?("#{dirname}/#{ent}") }
553     }
554   end
555
556   REJECT_DIRS = %w(
557     CVS SCCS RCS CVS.adm .svn
558   )
559
560   def all_dirs_in(dirname)
561     Dir.open(dirname) {|d|
562       return d.select {|n| File.dir?("#{dirname}/#{n}") } - %w(. ..) - REJECT_DIRS
563     }
564   end
565
566 end
567
568
569 #
570 # Main Installer
571 #
572
573 module HookUtils
574
575   def run_hook(name)
576     try_run_hook "#{curr_srcdir()}/#{name}" or
577     try_run_hook "#{curr_srcdir()}/#{name}.rb"
578   end
579
580   def try_run_hook(fname)
581     return false unless File.file?(fname)
582     begin
583       instance_eval File.read(fname), fname, 1
584     rescue
585       setup_rb_error "hook #{fname} failed:\n" + $!.message
586     end
587     true
588   end
589
590 end
591
592
593 module HookScriptAPI
594
595   def get_config(key)
596     @config[key]
597   end
598
599   alias config get_config
600
601   def set_config(key, val)
602     @config[key] = val
603   end
604
605   #
606   # srcdir/objdir (works only in the package directory)
607   #
608
609   #abstract srcdir_root
610   #abstract objdir_root
611   #abstract relpath
612
613   def curr_srcdir
614     "#{srcdir_root()}/#{relpath()}"
615   end
616
617   def curr_objdir
618     "#{objdir_root()}/#{relpath()}"
619   end
620
621   def srcfile(path)
622     "#{curr_srcdir()}/#{path}"
623   end
624
625   def srcexist?(path)
626     File.exist?(srcfile(path))
627   end
628
629   def srcdirectory?(path)
630     File.dir?(srcfile(path))
631   end
632   
633   def srcfile?(path)
634     File.file? srcfile(path)
635   end
636
637   def srcentries(path = '.')
638     Dir.open("#{curr_srcdir()}/#{path}") {|d|
639       return d.to_a - %w(. ..)
640     }
641   end
642
643   def srcfiles(path = '.')
644     srcentries(path).select {|fname|
645       File.file?(File.join(curr_srcdir(), path, fname))
646     }
647   end
648
649   def srcdirectories(path = '.')
650     srcentries(path).select {|fname|
651       File.dir?(File.join(curr_srcdir(), path, fname))
652     }
653   end
654
655 end
656
657
658 class ToplevelInstaller
659
660   Version   = '3.3.1'
661   Copyright = 'Copyright (c) 2000-2004 Minero Aoki'
662
663   TASKS = [
664     [ 'all',      'do config, setup, then install' ],
665     [ 'config',   'saves your configurations' ],
666     [ 'show',     'shows current configuration' ],
667     [ 'setup',    'compiles ruby extentions and others' ],
668     [ 'install',  'installs files' ],
669     [ 'clean',    "does `make clean' for each extention" ],
670     [ 'distclean',"does `make distclean' for each extention" ]
671   ]
672
673   def ToplevelInstaller.invoke
674     instance().invoke
675   end
676
677   @singleton = nil
678
679   def ToplevelInstaller.instance
680     @singleton ||= new(File.dirname($0))
681     @singleton
682   end
683
684   include MetaConfigAPI
685
686   def initialize(ardir_root)
687     @config = nil
688     @options = { 'verbose' => true }
689     @ardir = File.expand_path(ardir_root)
690   end
691
692   def inspect
693     "#<#{self.class} #{__id__()}>"
694   end
695
696   def invoke
697     run_metaconfigs
698     case task = parsearg_global()
699     when nil, 'all'
700       @config = load_config('config')
701       parsearg_config
702       init_installers
703       exec_config
704       exec_setup
705       exec_install
706     else
707       @config = load_config(task)
708       __send__ "parsearg_#{task}"
709       init_installers
710       __send__ "exec_#{task}"
711     end
712   end
713   
714   def run_metaconfigs
715     eval_file_ifexist "#{@ardir}/metaconfig"
716   end
717
718   def load_config(task)
719     case task
720     when 'config'
721       ConfigTable.new
722     when 'clean', 'distclean'
723       if File.exist?(ConfigTable.savefile)
724       then ConfigTable.load
725       else ConfigTable.new
726       end
727     else
728       ConfigTable.load
729     end
730   end
731
732   def init_installers
733     @installer = Installer.new(@config, @options, @ardir, File.expand_path('.'))
734   end
735
736   #
737   # Hook Script API bases
738   #
739
740   def srcdir_root
741     @ardir
742   end
743
744   def objdir_root
745     '.'
746   end
747
748   def relpath
749     '.'
750   end
751
752   #
753   # Option Parsing
754   #
755
756   def parsearg_global
757     valid_task = /\A(?:#{TASKS.map {|task,desc| task }.join '|'})\z/
758
759     while arg = ARGV.shift
760       case arg
761       when /\A\w+\z/
762         setup_rb_error "invalid task: #{arg}" unless valid_task =~ arg
763         return arg
764
765       when '-q', '--quiet'
766         @options['verbose'] = false
767
768       when       '--verbose'
769         @options['verbose'] = true
770
771       when '-h', '--help'
772         print_usage $stdout
773         exit 0
774
775       when '-v', '--version'
776         puts "#{File.basename($0)} version #{Version}"
777         exit 0
778       
779       when '--copyright'
780         puts Copyright
781         exit 0
782
783       else
784         setup_rb_error "unknown global option '#{arg}'"
785       end
786     end
787
788     nil
789   end
790
791
792   def parsearg_no_options
793     unless ARGV.empty?
794       setup_rb_error "#{task}:  unknown options: #{ARGV.join ' '}"
795     end
796   end
797
798   alias parsearg_show       parsearg_no_options
799   alias parsearg_setup      parsearg_no_options
800   alias parsearg_clean      parsearg_no_options
801   alias parsearg_distclean  parsearg_no_options
802
803   def parsearg_config
804     re = /\A--(#{ConfigTable.map {|i| i.name }.join('|')})(?:=(.*))?\z/
805     @options['config-opt'] = []
806
807     while i = ARGV.shift
808       if /\A--?\z/ =~ i
809         @options['config-opt'] = ARGV.dup
810         break
811       end
812       m = re.match(i)  or setup_rb_error "config: unknown option #{i}"
813       name, value = *m.to_a[1,2]
814       @config[name] = value
815     end
816   end
817
818   def parsearg_install
819     @options['no-harm'] = false
820     @options['install-prefix'] = ''
821     while a = ARGV.shift
822       case a
823       when /\A--no-harm\z/
824         @options['no-harm'] = true
825       when /\A--prefix=(.*)\z/
826         path = $1
827         path = File.expand_path(path) unless path[0,1] == '/'
828         @options['install-prefix'] = path
829       else
830         setup_rb_error "install: unknown option #{a}"
831       end
832     end
833   end
834
835   def print_usage(out)
836     out.puts 'Typical Installation Procedure:'
837     out.puts "  $ ruby #{File.basename $0} config"
838     out.puts "  $ ruby #{File.basename $0} setup"
839     out.puts "  # ruby #{File.basename $0} install (may require root privilege)"
840     out.puts
841     out.puts 'Detailed Usage:'
842     out.puts "  ruby #{File.basename $0} <global option>"
843     out.puts "  ruby #{File.basename $0} [<global options>] <task> [<task options>]"
844
845     fmt = "  %-24s %s\n"
846     out.puts
847     out.puts 'Global options:'
848     out.printf fmt, '-q,--quiet',   'suppress message outputs'
849     out.printf fmt, '   --verbose', 'output messages verbosely'
850     out.printf fmt, '-h,--help',    'print this message'
851     out.printf fmt, '-v,--version', 'print version and quit'
852     out.printf fmt, '   --copyright',  'print copyright and quit'
853     out.puts
854     out.puts 'Tasks:'
855     TASKS.each do |name, desc|
856       out.printf fmt, name, desc
857     end
858
859     fmt = "  %-24s %s [%s]\n"
860     out.puts
861     out.puts 'Options for CONFIG or ALL:'
862     ConfigTable.each do |item|
863       out.printf fmt, item.help_opt, item.description, item.help_default
864     end
865     out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
866     out.puts
867     out.puts 'Options for INSTALL:'
868     out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
869     out.printf fmt, '--prefix=path',  'install path prefix', '$prefix'
870     out.puts
871   end
872
873   #
874   # Task Handlers
875   #
876
877   def exec_config
878     @installer.exec_config
879     @config.save   # must be final
880   end
881
882   def exec_setup
883     @installer.exec_setup
884   end
885
886   def exec_install
887     @installer.exec_install
888   end
889
890   def exec_show
891     ConfigTable.each do |i|
892       printf "%-20s %s\n", i.name, i.value
893     end
894   end
895
896   def exec_clean
897     @installer.exec_clean
898   end
899
900   def exec_distclean
901     @installer.exec_distclean
902   end
903
904 end
905
906
907 class ToplevelInstallerMulti < ToplevelInstaller
908
909   include HookUtils
910   include HookScriptAPI
911   include FileOperations
912
913   def initialize(ardir)
914     super
915     @packages = all_dirs_in("#{@ardir}/packages")
916     raise 'no package exists' if @packages.empty?
917   end
918
919   def run_metaconfigs
920     eval_file_ifexist "#{@ardir}/metaconfig"
921     @packages.each do |name|
922       eval_file_ifexist "#{@ardir}/packages/#{name}/metaconfig"
923     end
924   end
925
926   def init_installers
927     @installers = {}
928     @packages.each do |pack|
929       @installers[pack] = Installer.new(@config, @options,
930                                        "#{@ardir}/packages/#{pack}",
931                                        "packages/#{pack}")
932     end
933
934     with    = extract_selection(config('with'))
935     without = extract_selection(config('without'))
936     @selected = @installers.keys.select {|name|
937                   (with.empty? or with.include?(name)) \
938                       and not without.include?(name)
939                 }
940   end
941
942   def extract_selection(list)
943     a = list.split(/,/)
944     a.each do |name|
945       setup_rb_error "no such package: #{name}"  unless @installers.key?(name)
946     end
947     a
948   end
949
950   def print_usage(f)
951     super
952     f.puts 'Inluded packages:'
953     f.puts '  ' + @packages.sort.join(' ')
954     f.puts
955   end
956
957   #
958   # multi-package metaconfig API
959   #
960
961   attr_reader :packages
962
963   def declare_packages(list)
964     raise 'package list is empty' if list.empty?
965     list.each do |name|
966       raise "directory packages/#{name} does not exist"\
967               unless File.dir?("#{@ardir}/packages/#{name}")
968     end
969     @packages = list
970   end
971
972   #
973   # Task Handlers
974   #
975
976   def exec_config
977     run_hook 'pre-config'
978     each_selected_installers {|inst| inst.exec_config }
979     run_hook 'post-config'
980     @config.save   # must be final
981   end
982
983   def exec_setup
984     run_hook 'pre-setup'
985     each_selected_installers {|inst| inst.exec_setup }
986     run_hook 'post-setup'
987   end
988
989   def exec_install
990     run_hook 'pre-install'
991     each_selected_installers {|inst| inst.exec_install }
992     run_hook 'post-install'
993   end
994
995   def exec_clean
996     rm_f ConfigTable.savefile
997     run_hook 'pre-clean'
998     each_selected_installers {|inst| inst.exec_clean }
999     run_hook 'post-clean'
1000   end
1001
1002   def exec_distclean
1003     rm_f ConfigTable.savefile
1004     run_hook 'pre-distclean'
1005     each_selected_installers {|inst| inst.exec_distclean }
1006     run_hook 'post-distclean'
1007   end
1008
1009   #
1010   # lib
1011   #
1012
1013   def each_selected_installers
1014     Dir.mkdir 'packages' unless File.dir?('packages')
1015     @selected.each do |pack|
1016       $stderr.puts "Processing the package `#{pack}' ..." if @options['verbose']
1017       Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}")
1018       Dir.chdir "packages/#{pack}"
1019       yield @installers[pack]
1020       Dir.chdir '../..'
1021     end
1022   end
1023
1024   def verbose?
1025     @options['verbose']
1026   end
1027
1028   def no_harm?
1029     @options['no-harm']
1030   end
1031
1032 end
1033
1034
1035 class Installer
1036
1037   FILETYPES = %w( bin lib ext data )
1038
1039   include HookScriptAPI
1040   include HookUtils
1041   include FileOperations
1042
1043   def initialize(config, opt, srcroot, objroot)
1044     @config = config
1045     @options = opt
1046     @srcdir = File.expand_path(srcroot)
1047     @objdir = File.expand_path(objroot)
1048     @currdir = '.'
1049   end
1050
1051   def inspect
1052     "#<#{self.class} #{File.basename(@srcdir)}>"
1053   end
1054
1055   #
1056   # Hook Script API base methods
1057   #
1058
1059   def srcdir_root
1060     @srcdir
1061   end
1062
1063   def objdir_root
1064     @objdir
1065   end
1066
1067   def relpath
1068     @currdir
1069   end
1070
1071   #
1072   # configs/options
1073   #
1074
1075   def no_harm?
1076     @options['no-harm']
1077   end
1078
1079   def verbose?
1080     @options['verbose']
1081   end
1082
1083   def verbose_off
1084     begin
1085       save, @options['verbose'] = @options['verbose'], false
1086       yield
1087     ensure
1088       @options['verbose'] = save
1089     end
1090   end
1091
1092   #
1093   # TASK config
1094   #
1095
1096   def exec_config
1097     exec_task_traverse 'config'
1098   end
1099
1100   def config_dir_bin(rel)
1101   end
1102
1103   def config_dir_lib(rel)
1104   end
1105
1106   def config_dir_ext(rel)
1107     extconf if extdir?(curr_srcdir())
1108   end
1109
1110   def extconf
1111     opt = @options['config-opt'].join(' ')
1112     command "#{config('rubyprog')} #{curr_srcdir()}/extconf.rb #{opt}"
1113   end
1114
1115   def config_dir_data(rel)
1116   end
1117
1118   #
1119   # TASK setup
1120   #
1121
1122   def exec_setup
1123     exec_task_traverse 'setup'
1124   end
1125
1126   def setup_dir_bin(rel)
1127     all_files_in(curr_srcdir()).each do |fname|
1128       adjust_shebang "#{curr_srcdir()}/#{fname}"
1129     end
1130   end
1131
1132   def adjust_shebang(path)
1133     return if no_harm?
1134     tmpfile = File.basename(path) + '.tmp'
1135     begin
1136       File.open(path, 'rb') {|r|
1137         first = r.gets
1138         return unless File.basename(config('rubypath')) == 'ruby'
1139         return unless File.basename(first.sub(/\A\#!/, '').split[0]) == 'ruby'
1140         $stderr.puts "adjusting shebang: #{File.basename(path)}" if verbose?
1141         File.open(tmpfile, 'wb') {|w|
1142           w.print first.sub(/\A\#!\s*\S+/, '#! ' + config('rubypath'))
1143           w.write r.read
1144         }
1145         move_file tmpfile, File.basename(path)
1146       }
1147     ensure
1148       File.unlink tmpfile if File.exist?(tmpfile)
1149     end
1150   end
1151
1152   def setup_dir_lib(rel)
1153   end
1154
1155   def setup_dir_ext(rel)
1156     make if extdir?(curr_srcdir())
1157   end
1158
1159   def setup_dir_data(rel)
1160   end
1161
1162   #
1163   # TASK install
1164   #
1165
1166   def exec_install
1167     rm_f 'InstalledFiles'
1168     exec_task_traverse 'install'
1169   end
1170
1171   def install_dir_bin(rel)
1172     install_files collect_filenames_auto(), "#{config('bindir')}/#{rel}", 0755
1173   end
1174
1175   def install_dir_lib(rel)
1176     install_files ruby_scripts(), "#{config('rbdir')}/#{rel}", 0644
1177   end
1178
1179   def install_dir_ext(rel)
1180     return unless extdir?(curr_srcdir())
1181     install_files ruby_extentions('.'),
1182                   "#{config('sodir')}/#{File.dirname(rel)}",
1183                   0555
1184   end
1185
1186   def install_dir_data(rel)
1187     install_files collect_filenames_auto(), "#{config('datadir')}/#{rel}", 0644
1188   end
1189
1190   def install_files(list, dest, mode)
1191     mkdir_p dest, @options['install-prefix']
1192     list.each do |fname|
1193       install fname, dest, mode, @options['install-prefix']
1194     end
1195   end
1196
1197   def ruby_scripts
1198     collect_filenames_auto().select {|n| /\.rb\z/ =~ n }
1199   end
1200   
1201   # picked up many entries from cvs-1.11.1/src/ignore.c
1202   reject_patterns = %w( 
1203     core RCSLOG tags TAGS .make.state
1204     .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
1205     *~ *.old *.bak *.BAK *.orig *.rej _$* *$
1206
1207     *.org *.in .*
1208   )
1209   mapping = {
1210     '.' => '\.',
1211     '$' => '\$',
1212     '#' => '\#',
1213     '*' => '.*'
1214   }
1215   REJECT_PATTERNS = Regexp.new('\A(?:' +
1216                                reject_patterns.map {|pat|
1217                                  pat.gsub(/[\.\$\#\*]/) {|ch| mapping[ch] }
1218                                }.join('|') +
1219                                ')\z')
1220
1221   def collect_filenames_auto
1222     mapdir((existfiles() - hookfiles()).reject {|fname|
1223              REJECT_PATTERNS =~ fname
1224            })
1225   end
1226
1227   def existfiles
1228     all_files_in(curr_srcdir()) | all_files_in('.')
1229   end
1230
1231   def hookfiles
1232     %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt|
1233       %w( config setup install clean ).map {|t| sprintf(fmt, t) }
1234     }.flatten
1235   end
1236
1237   def mapdir(filelist)
1238     filelist.map {|fname|
1239       if File.exist?(fname)   # objdir
1240         fname
1241       else                    # srcdir
1242         File.join(curr_srcdir(), fname)
1243       end
1244     }
1245   end
1246
1247   def ruby_extentions(dir)
1248     Dir.open(dir) {|d|
1249       ents = d.select {|fname| /\.#{::RbConfig::CONFIG['DLEXT']}\z/ =~ fname }
1250       if ents.empty?
1251         setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first"
1252       end
1253       return ents
1254     }
1255   end
1256
1257   #
1258   # TASK clean
1259   #
1260
1261   def exec_clean
1262     exec_task_traverse 'clean'
1263     rm_f ConfigTable.savefile
1264     rm_f 'InstalledFiles'
1265   end
1266
1267   def clean_dir_bin(rel)
1268   end
1269
1270   def clean_dir_lib(rel)
1271   end
1272
1273   def clean_dir_ext(rel)
1274     return unless extdir?(curr_srcdir())
1275     make 'clean' if File.file?('Makefile')
1276   end
1277
1278   def clean_dir_data(rel)
1279   end
1280
1281   #
1282   # TASK distclean
1283   #
1284
1285   def exec_distclean
1286     exec_task_traverse 'distclean'
1287     rm_f ConfigTable.savefile
1288     rm_f 'InstalledFiles'
1289   end
1290
1291   def distclean_dir_bin(rel)
1292   end
1293
1294   def distclean_dir_lib(rel)
1295   end
1296
1297   def distclean_dir_ext(rel)
1298     return unless extdir?(curr_srcdir())
1299     make 'distclean' if File.file?('Makefile')
1300   end
1301
1302   #
1303   # lib
1304   #
1305
1306   def exec_task_traverse(task)
1307     run_hook "pre-#{task}"
1308     FILETYPES.each do |type|
1309       if config('without-ext') == 'yes' and type == 'ext'
1310         $stderr.puts 'skipping ext/* by user option' if verbose?
1311         next
1312       end
1313       traverse task, type, "#{task}_dir_#{type}"
1314     end
1315     run_hook "post-#{task}"
1316   end
1317
1318   def traverse(task, rel, mid)
1319     dive_into(rel) {
1320       run_hook "pre-#{task}"
1321       __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '')
1322       all_dirs_in(curr_srcdir()).each do |d|
1323         traverse task, "#{rel}/#{d}", mid
1324       end
1325       run_hook "post-#{task}"
1326     }
1327   end
1328
1329   def dive_into(rel)
1330     return unless File.dir?("#{@srcdir}/#{rel}")
1331
1332     dir = File.basename(rel)
1333     Dir.mkdir dir unless File.dir?(dir)
1334     prevdir = Dir.pwd
1335     Dir.chdir dir
1336     $stderr.puts '---> ' + rel if verbose?
1337     @currdir = rel
1338     yield
1339     Dir.chdir prevdir
1340     $stderr.puts '<--- ' + rel if verbose?
1341     @currdir = File.dirname(rel)
1342   end
1343
1344 end
1345
1346
1347 if $0 == __FILE__
1348   begin
1349     if multipackage_install?
1350       ToplevelInstallerMulti.invoke
1351     else
1352       ToplevelInstaller.invoke
1353     end
1354   rescue SetupError
1355     raise if $DEBUG
1356     $stderr.puts $!.message
1357     $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
1358     exit 1
1359   end
1360 end