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
11
unless Enumerable.method_defined?(:map)   # Ruby 1.4.6
12
  module Enumerable
13
    alias map collect
14
  end
15
end
16
17
unless File.respond_to?(:read)   # Ruby 1.6
18
  def File.read(fname)
19
    open(fname) {|f|
20
      return f.read
21
    }
22
  end
23
end
24
25
unless Errno.const_defined?(:ENOTEMPTY)   # Windows?
26
  module Errno
27
    class ENOTEMPTY
28
      # We do not raise this exception, implementation is not needed.
29
    end
30
  end
31
end
32
33
def File.binread(fname)
34
  open(fname, 'rb') {|f|
35
    return f.read
36
  }
37
end
38
39
# for corrupted Windows' stat(2)
40
def File.dir?(path)
41
  File.directory?((path[-1,1] == '/') ? path : path + '/')
42
end
43
44
45
class 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
532
end   # class ConfigTable
533
534
535
# This module requires: #verbose?, #no_harm?
536
module 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
685
end
686
687
688
# This module requires: #srcdir_root, #objdir_root, #relpath
689
module 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
748
end
749
750
751
class 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
1021
end   # class ToplevelInstaller
1022
1023
1024
class 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
1155
end   # class ToplevelInstallerMulti
1156
1157
1158
class 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
1567
end   # class Installer
1568
1569
1570
class SetupError < StandardError; end
1571
1572
def setup_rb_error(msg)
1573
  raise SetupError, msg
1574
end
1575
1576
if $0 == __FILE__
1577
  begin
1578
    ToplevelInstaller.invoke
1579
  rescue SetupError
1580
    raise if $DEBUG
1581
    $stderr.puts $!.message
1582
    $stderr.puts "Try 'ruby #{$0} --help' for detailed usage."
1583
    exit 1
1584
  end
1585
end