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