1#
2# setup.rb
3#
4# Copyright (c) 2000-2005 Minero Aoki
5#
6# This program is free software.
7# You can distribute/modify this program under the terms of
8# the GNU LGPL, Lesser General Public License version 2.1.
9#
10
11unless Enumerable.method_defined?(:map)   # Ruby 1.4.6
12  module Enumerable
13    alias map collect
14  end
15end
16
17unless File.respond_to?(:read)   # Ruby 1.6
18  def File.read(fname)
19    open(fname) {|f|
20      return f.read
21    }
22  end
23end
24
25unless 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
31end
32
33def File.binread(fname)
34  open(fname, 'rb') {|f|
35    return f.read
36  }
37end
38
39# for corrupted Windows' stat(2)
40def File.dir?(path)
41  File.directory?((path[-1,1] == '/') ? path : path + '/')
42end
43
44
45class ConfigTable
46
47  include Enumerable
48
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
59
60  attr_accessor :install_prefix
61  attr_accessor :config_opt
62
63  attr_writer :verbose
64
65  def verbose?
66    @verbose
67  end
68
69  attr_writer :no_harm
70
71  def no_harm?
72    @no_harm
73  end
74
75  def [](key)
76    lookup(key).resolve(self)
77  end
78
79  def []=(key, val)
80    lookup(key).set val
81  end
82
83  def names
84    @items.map {|i| i.name }
85  end
86
87  def each(&block)
88    @items.each(&block)
89  end
90
91  def key?(name)
92    @table.key?(name)
93  end
94
95  def lookup(name)
96    @table[name] or setup_rb_error "no such config item: #{name}"
97  end
98
99  def add(item)
100    @items.push item
101    @table[item.name] = item
102  end
103
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
110
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
116
117  def savefile
118    '.config'
119  end
120
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
131
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
140
141  def load_standard_entries
142    standard_entries(@rbconfig).each do |ent|
143      add ent
144    end
145  end
146
147  def standard_entries(rbconfig)
148    c = rbconfig
149
150    rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
151
152    major = c['MAJOR'].to_i
153    minor = c['MINOR'].to_i
154    teeny = c['TEENY'].to_i
155    version = "#{major}.#{minor}"
156
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)))))
162
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    }
191
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
197
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
259
260  def load_multipackage_entries
261    multipackage_entries().each do |ent|
262      add ent
263    end
264  end
265
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
275
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  }
294
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
303
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
308
309  def dllext
310    @rbconfig['DLEXT']
311  end
312
313  def value_config?(name)
314    lookup(name).value?
315  end
316
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
325
326    attr_reader :name
327    attr_reader :description
328
329    attr_accessor :default
330    alias help_default default
331
332    def help_opt
333      "--#{@name}=#{@template}"
334    end
335
336    def value?
337      true
338    end
339
340    def value
341      @value
342    end
343
344    def resolve(table)
345      @value.gsub(%r<\$([^/]+)>) { table[$1] }
346    end
347
348    def set(val)
349      @value = check(val)
350    end
351
352    private
353
354    def check(val)
355      setup_rb_error "config: --#{name} requires argument" unless val
356      val
357    end
358  end
359
360  class BoolItem < Item
361    def config_type
362      'bool'
363    end
364
365    def help_opt
366      "--#{@name}"
367    end
368
369    private
370
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
381
382  class PathItem < Item
383    def config_type
384      'path'
385    end
386
387    private
388
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
394
395  class ProgramItem < Item
396    def config_type
397      'program'
398    end
399  end
400
401  class SelectItem < Item
402    def initialize(name, selection, default, desc)
403      super
404      @ok = selection.split('/')
405    end
406
407    def config_type
408      'select'
409    end
410
411    private
412
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
420
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
427
428    def config_type
429      'exec'
430    end
431
432    def value?
433      false
434    end
435
436    def resolve(table)
437      setup_rb_error "$#{name()} wrongly used as option value"
438    end
439
440    undef set
441
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
450
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
456
457    attr_reader :help_default
458
459    def config_type
460      'package'
461    end
462
463    private
464
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
472
473  class MetaConfigEnvironment
474    def initialize(config, installer)
475      @config = config
476      @installer = installer
477    end
478
479    def config_names
480      @config.names
481    end
482
483    def config?(name)
484      @config.key?(name)
485    end
486
487    def bool_config?(name)
488      @config.lookup(name).config_type == 'bool'
489    end
490
491    def path_config?(name)
492      @config.lookup(name).config_type == 'path'
493    end
494
495    def value_config?(name)
496      @config.lookup(name).config_type != 'exec'
497    end
498
499    def add_config(item)
500      @config.add item
501    end
502
503    def add_bool_config(name, default, desc)
504      @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
505    end
506
507    def add_path_config(name, default, desc)
508      @config.add PathItem.new(name, 'path', default, desc)
509    end
510
511    def set_config_default(name, default)
512      @config.lookup(name).default = default
513    end
514
515    def remove_config(name)
516      @config.remove(name)
517    end
518
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
524
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
531
532end   # class ConfigTable
533
534
535# This module requires: #verbose?, #no_harm?
536module FileOperations
537
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?
542
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
554
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
560
561  def rm_rf(path)
562    $stderr.puts "rm -rf #{path}" if verbose?
563    return if no_harm?
564    remove_tree path
565  end
566
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
576
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
596
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
609
610  def force_remove_file(path)
611    begin
612      remove_file path
613    rescue
614    end
615  end
616
617  def remove_file(path)
618    File.chmod 0777, path
619    File.unlink path
620  end
621
622  def install(from, dest, mode, prefix = nil)
623    $stderr.puts "install #{from} #{dest}" if verbose?
624    return if no_harm?
625
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
637
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
647
648  def diff?(new_content, path)
649    return true unless File.exist?(path)
650    new_content != File.binread(path)
651  end
652
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
658
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
666
667  def extdir?(dir)
668    File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
669  end
670
671  def files_of(dir)
672    Dir.open(dir) {|d|
673      return d.select {|ent| File.file?("#{dir}/#{ent}") }
674    }
675  end
676
677  DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
678
679  def directories_of(dir)
680    Dir.open(dir) {|d|
681      return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
682    }
683  end
684
685end
686
687
688# This module requires: #srcdir_root, #objdir_root, #relpath
689module HookScriptAPI
690
691  def get_config(key)
692    @config[key]
693  end
694
695  alias config get_config
696
697  # obsolete: use metaconfig to change configuration
698  def set_config(key, val)
699    @config[key] = val
700  end
701
702  #
703  # srcdir/objdir (works only in the package directory)
704  #
705
706  def curr_srcdir
707    "#{srcdir_root()}/#{relpath()}"
708  end
709
710  def curr_objdir
711    "#{objdir_root()}/#{relpath()}"
712  end
713
714  def srcfile(path)
715    "#{curr_srcdir()}/#{path}"
716  end
717
718  def srcexist?(path)
719    File.exist?(srcfile(path))
720  end
721
722  def srcdirectory?(path)
723    File.dir?(srcfile(path))
724  end
725  
726  def srcfile?(path)
727    File.file?(srcfile(path))
728  end
729
730  def srcentries(path = '.')
731    Dir.open("#{curr_srcdir()}/#{path}") {|d|
732      return d.to_a - %w(. ..)
733    }
734  end
735
736  def srcfiles(path = '.')
737    srcentries(path).select {|fname|
738      File.file?(File.join(curr_srcdir(), path, fname))
739    }
740  end
741
742  def srcdirectories(path = '.')
743    srcentries(path).select {|fname|
744      File.dir?(File.join(curr_srcdir(), path, fname))
745    }
746  end
747
748end
749
750
751class ToplevelInstaller
752
753  Version   = '3.4.1'
754  Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
755
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  ]
766
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
775
776  def ToplevelInstaller.multipackage?
777    File.dir?(File.dirname($0) + '/packages')
778  end
779
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
790
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
797
798  def config(key)
799    @config[key]
800  end
801
802  def inspect
803    "#<#{self.class} #{__id__()}>"
804  end
805
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
833
834  def init_installers
835    @installer = Installer.new(@config, @ardir, File.expand_path('.'))
836  end
837
838  #
839  # Hook Script API bases
840  #
841
842  def srcdir_root
843    @ardir
844  end
845
846  def objdir_root
847    '.'
848  end
849
850  def relpath
851    '.'
852  end
853
854  #
855  # Option Parsing
856  #
857
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
883
884  def valid_task?(t)
885    valid_task_re() =~ t
886  end
887
888  def valid_task_re
889    @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/
890  end
891
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
898
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
904
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
930
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
947
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>]"
957
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
971
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
985
986  #
987  # Task Handlers
988  #
989
990  def exec_config
991    @installer.exec_config
992    @config.save   # must be final
993  end
994
995  def exec_setup
996    @installer.exec_setup
997  end
998
999  def exec_install
1000    @installer.exec_install
1001  end
1002
1003  def exec_test
1004    @installer.exec_test
1005  end
1006
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
1012
1013  def exec_clean
1014    @installer.exec_clean
1015  end
1016
1017  def exec_distclean
1018    @installer.exec_distclean
1019  end
1020
1021end   # class ToplevelInstaller
1022
1023
1024class ToplevelInstallerMulti < ToplevelInstaller
1025
1026  include FileOperations
1027
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
1034
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
1041
1042  attr_reader :packages
1043
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
1052
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
1067
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
1075
1076  def print_usage(f)
1077    super
1078    f.puts 'Inluded packages:'
1079    f.puts '  ' + @packages.sort.join(' ')
1080    f.puts
1081  end
1082
1083  #
1084  # Task Handlers
1085  #
1086
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
1093
1094  def exec_setup
1095    run_hook 'pre-setup'
1096    each_selected_installers {|inst| inst.exec_setup }
1097    run_hook 'post-setup'
1098  end
1099
1100  def exec_install
1101    run_hook 'pre-install'
1102    each_selected_installers {|inst| inst.exec_install }
1103    run_hook 'post-install'
1104  end
1105
1106  def exec_test
1107    run_hook 'pre-test'
1108    each_selected_installers {|inst| inst.exec_test }
1109    run_hook 'post-test'
1110  end
1111
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
1118
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
1125
1126  #
1127  # lib
1128  #
1129
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
1140
1141  def run_hook(id)
1142    @root_installer.run_hook id
1143  end
1144
1145  # module FileOperations requires this
1146  def verbose?
1147    @config.verbose?
1148  end
1149
1150  # module FileOperations requires this
1151  def no_harm?
1152    @config.no_harm?
1153  end
1154
1155end   # class ToplevelInstallerMulti
1156
1157
1158class Installer
1159
1160  FILETYPES = %w( bin lib ext data conf man )
1161
1162  include FileOperations
1163  include HookScriptAPI
1164
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
1171
1172  def inspect
1173    "#<#{self.class} #{File.basename(@srcdir)}>"
1174  end
1175
1176  def noop(rel)
1177  end
1178
1179  #
1180  # Hook Script API base methods
1181  #
1182
1183  def srcdir_root
1184    @srcdir
1185  end
1186
1187  def objdir_root
1188    @objdir
1189  end
1190
1191  def relpath
1192    @currdir
1193  end
1194
1195  #
1196  # Config Access
1197  #
1198
1199  # module FileOperations requires this
1200  def verbose?
1201    @config.verbose?
1202  end
1203
1204  # module FileOperations requires this
1205  def no_harm?
1206    @config.no_harm?
1207  end
1208
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
1217
1218  #
1219  # TASK config
1220  #
1221
1222  def exec_config
1223    exec_task_traverse 'config'
1224  end
1225
1226  alias config_dir_bin noop
1227  alias config_dir_lib noop
1228
1229  def config_dir_ext(rel)
1230    extconf if extdir?(curr_srcdir())
1231  end
1232
1233  alias config_dir_data noop
1234  alias config_dir_conf noop
1235  alias config_dir_man noop
1236
1237  def extconf
1238    ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt
1239  end
1240
1241  #
1242  # TASK setup
1243  #
1244
1245  def exec_setup
1246    exec_task_traverse 'setup'
1247  end
1248
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
1254
1255  alias setup_dir_lib noop
1256
1257  def setup_dir_ext(rel)
1258    make if extdir?(curr_srcdir())
1259  end
1260
1261  alias setup_dir_data noop
1262  alias setup_dir_conf noop
1263  alias setup_dir_man noop
1264
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
1286
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
1297
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
1307
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
1317
1318    def Shebang.parse(line)
1319      cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ')
1320      new(cmd, args)
1321    end
1322
1323    def initialize(cmd, args = [])
1324      @cmd = cmd
1325      @args = args
1326    end
1327
1328    attr_reader :cmd
1329    attr_reader :args
1330
1331    def to_s
1332      "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}")
1333    end
1334  end
1335
1336  #
1337  # TASK install
1338  #
1339
1340  def exec_install
1341    rm_f 'InstalledFiles'
1342    exec_task_traverse 'install'
1343  end
1344
1345  def install_dir_bin(rel)
1346    install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755
1347  end
1348
1349  def install_dir_lib(rel)
1350    install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644
1351  end
1352
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
1359
1360  def install_dir_data(rel)
1361    install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644
1362  end
1363
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
1369
1370  def install_dir_man(rel)
1371    install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644
1372  end
1373
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
1380
1381  def libfiles
1382    glob_reject(%w(*.y *.output), targetfiles())
1383  end
1384
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
1392
1393  def targetfiles
1394    mapdir(existfiles() - hookfiles())
1395  end
1396
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
1405
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 _$* *$
1411
1412    *.org *.in .*
1413  )
1414
1415  def existfiles
1416    glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.')))
1417  end
1418
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
1424
1425  def glob_select(pat, ents)
1426    re = globs2re([pat])
1427    ents.select {|ent| re =~ ent }
1428  end
1429
1430  def glob_reject(pats, ents)
1431    re = globs2re(pats)
1432    ents.reject {|ent| re =~ ent }
1433  end
1434
1435  GLOB2REGEX = {
1436    '.' => '\.',
1437    '$' => '\$',
1438    '#' => '\#',
1439    '*' => '.*'
1440  }
1441
1442  def globs2re(pats)
1443    /\A(?:#{
1444      pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|')
1445    })\z/
1446  end
1447
1448  #
1449  # TASK test
1450  #
1451
1452  TESTDIR = 'test'
1453
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
1469
1470  #
1471  # TASK clean
1472  #
1473
1474  def exec_clean
1475    exec_task_traverse 'clean'
1476    rm_f @config.savefile
1477    rm_f 'InstalledFiles'
1478  end
1479
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
1485
1486  def clean_dir_ext(rel)
1487    return unless extdir?(curr_srcdir())
1488    make 'clean' if File.file?('Makefile')
1489  end
1490
1491  #
1492  # TASK distclean
1493  #
1494
1495  def exec_distclean
1496    exec_task_traverse 'distclean'
1497    rm_f @config.savefile
1498    rm_f 'InstalledFiles'
1499  end
1500
1501  alias distclean_dir_bin noop
1502  alias distclean_dir_lib noop
1503
1504  def distclean_dir_ext(rel)
1505    return unless extdir?(curr_srcdir())
1506    make 'distclean' if File.file?('Makefile')
1507  end
1508
1509  alias distclean_dir_data noop
1510  alias distclean_dir_conf noop
1511  alias distclean_dir_man noop
1512
1513  #
1514  # Traversing
1515  #
1516
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
1528
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
1539
1540  def dive_into(rel)
1541    return unless File.dir?("#{@srcdir}/#{rel}")
1542
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
1554
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
1566
1567end   # class Installer
1568
1569
1570class SetupError < StandardError; end
1571
1572def setup_rb_error(msg)
1573  raise SetupError, msg
1574end
1575
1576if $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
1585end
1586