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