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