Skip to content
Snippets Groups Projects
install.rb 35.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • # Copyright (c) 2000-2005 Minero Aoki
    
    # This program is free software.
    # You can distribute/modify this program under the terms of
    # the GNU LGPL, Lesser General Public License version 2.1.
    
    unless Enumerable.method_defined?(:map)   # Ruby 1.4.6
      module Enumerable
    
        alias map collect
      end
    
    unless File.respond_to?(:read)   # Ruby 1.6
      def File.read(fname)
        open(fname) {|f|
          return f.read
        }
    
    unless Errno.const_defined?(:ENOTEMPTY)   # Windows?
      module Errno
        class ENOTEMPTY
          # We do not raise this exception, implementation is not needed.
    
    def File.binread(fname)
      open(fname, 'rb') {|f|
        return f.read
      }
    
    # for corrupted Windows' stat(2)
    def File.dir?(path)
      File.directory?((path[-1,1] == '/') ? path : path + '/')
    
      def initialize(rbconfig)
        @rbconfig = rbconfig
        @items = []
        @table = {}
        # options
        @install_prefix = nil
        @config_opt = nil
        @verbose = true
        @no_harm = false
      end
    
      attr_accessor :install_prefix
      attr_accessor :config_opt
    
      def [](key)
        lookup(key).resolve(self)
    
      def []=(key, val)
        lookup(key).set val
    
      def names
        @items.map {|i| i.name }
    
      def each(&block)
        @items.each(&block)
    
      def key?(name)
        @table.key?(name)
    
      def lookup(name)
        @table[name] or setup_rb_error "no such config item: #{name}"
    
      def add(item)
        @items.push item
        @table[item.name] = item
    
      def remove(name)
        item = lookup(name)
        @items.delete_if {|i| i.name == name }
        @table.delete_if {|name, i| i.name == name }
        item
    
      def load_script(path, inst = nil)
        if File.file?(path)
          MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path
        end
    
      def load_savefile
        begin
          File.foreach(savefile()) do |line|
            k, v = *line.split(/=/, 2)
            self[k] = v.strip
    
        rescue Errno::ENOENT
          setup_rb_error $!.message + "\n#{File.basename($0)} config first"
    
      def save
        @items.each {|i| i.value }
        File.open(savefile(), 'w') {|f|
          @items.each do |i|
            f.printf "%s=%s\n", i.name, i.value if i.value? and i.value
          end
        }
    
      def load_standard_entries
        standard_entries(@rbconfig).each do |ent|
          add ent
    
      def standard_entries(rbconfig)
        c = rbconfig
    
        rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT'])
    
        major = c['MAJOR'].to_i
        minor = c['MINOR'].to_i
        teeny = c['TEENY'].to_i
        version = "#{major}.#{minor}"
    
        # ruby ver. >= 1.4.4?
        newpath_p = ((major >= 2) or
                     ((major == 1) and
                      ((minor >= 5) or
                       ((minor == 4) and (teeny >= 4)))))
    
        if c['rubylibdir']
          # V > 1.6.3
          libruby         = "#{c['prefix']}/lib/ruby"
          librubyver      = c['rubylibdir']
          librubyverarch  = c['archdir']
          siteruby        = c['sitedir']
          siterubyver     = c['sitelibdir']
          siterubyverarch = c['sitearchdir']
        elsif newpath_p
          # 1.4.4 <= V <= 1.6.3
          libruby         = "#{c['prefix']}/lib/ruby"
          librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
          librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
          siteruby        = c['sitedir']
          siterubyver     = "$siteruby/#{version}"
          siterubyverarch = "$siterubyver/#{c['arch']}"
        else
          # V < 1.4.4
          libruby         = "#{c['prefix']}/lib/ruby"
          librubyver      = "#{c['prefix']}/lib/ruby/#{version}"
          librubyverarch  = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}"
          siteruby        = "#{c['prefix']}/lib/ruby/#{version}/site_ruby"
          siterubyver     = siteruby
          siterubyverarch = "$siterubyver/#{c['arch']}"
        end
        parameterize = lambda {|path|
          path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix')
    
        if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg }
          makeprog = arg.sub(/'/, '').split(/=/, 2)[1]
    
          makeprog = 'make'
        end
    
        [
          ExecItem.new('installdirs', 'std/site/home',
                       'std: install under libruby; site: install under site_ruby; home: install under $HOME')\
              {|val, table|
                case val
                when 'std'
                  table['rbdir'] = '$librubyver'
                  table['sodir'] = '$librubyverarch'
                when 'site'
                  table['rbdir'] = '$siterubyver'
                  table['sodir'] = '$siterubyverarch'
                when 'home'
                  setup_rb_error '$HOME was not set' unless ENV['HOME']
                  table['prefix'] = ENV['HOME']
                  table['rbdir'] = '$libdir/ruby'
                  table['sodir'] = '$libdir/ruby'
                end
              },
          PathItem.new('prefix', 'path', c['prefix'],
                       'path prefix of target environment'),
          PathItem.new('bindir', 'path', parameterize.call(c['bindir']),
                       'the directory for commands'),
          PathItem.new('libdir', 'path', parameterize.call(c['libdir']),
                       'the directory for libraries'),
          PathItem.new('datadir', 'path', parameterize.call(c['datadir']),
                       'the directory for shared data'),
          PathItem.new('mandir', 'path', parameterize.call(c['mandir']),
                       'the directory for man pages'),
          PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']),
                       'the directory for system configuration files'),
          PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']),
                       'the directory for local state data'),
          PathItem.new('libruby', 'path', libruby,
                       'the directory for ruby libraries'),
          PathItem.new('librubyver', 'path', librubyver,
                       'the directory for standard ruby libraries'),
          PathItem.new('librubyverarch', 'path', librubyverarch,
                       'the directory for standard ruby extensions'),
          PathItem.new('siteruby', 'path', siteruby,
              'the directory for version-independent aux ruby libraries'),
          PathItem.new('siterubyver', 'path', siterubyver,
                       'the directory for aux ruby libraries'),
          PathItem.new('siterubyverarch', 'path', siterubyverarch,
                       'the directory for aux ruby binaries'),
          PathItem.new('rbdir', 'path', '$siterubyver',
                       'the directory for ruby scripts'),
          PathItem.new('sodir', 'path', '$siterubyverarch',
                       'the directory for ruby extentions'),
          PathItem.new('rubypath', 'path', rubypath,
                       'the path to set to #! line'),
          ProgramItem.new('rubyprog', 'name', rubypath,
                          'the ruby program using for installation'),
          ProgramItem.new('makeprog', 'name', makeprog,
                          'the make program to compile ruby extentions'),
          SelectItem.new('shebang', 'all/ruby/never', 'ruby',
                         'shebang line (#!) editing mode'),
          BoolItem.new('without-ext', 'yes/no', 'no',
                       'does not compile/install ruby extentions')
        ]
      end
      private :standard_entries
    
      def load_multipackage_entries
        multipackage_entries().each do |ent|
          add ent
        end
      end
    
      def multipackage_entries
        [
          PackageSelectionItem.new('with', 'name,name...', '', 'ALL',
                                   'package names that you want to install'),
          PackageSelectionItem.new('without', 'name,name...', '', 'NONE',
                                   'package names that you do not want to install')
        ]
      end
      private :multipackage_entries
    
      ALIASES = {
        'std-ruby'         => 'librubyver',
        'stdruby'          => 'librubyver',
        'rubylibdir'       => 'librubyver',
        'archdir'          => 'librubyverarch',
        'site-ruby-common' => 'siteruby',     # For backward compatibility
        'site-ruby'        => 'siterubyver',  # For backward compatibility
        'bin-dir'          => 'bindir',
        'bin-dir'          => 'bindir',
        'rb-dir'           => 'rbdir',
        'so-dir'           => 'sodir',
        'data-dir'         => 'datadir',
        'ruby-path'        => 'rubypath',
        'ruby-prog'        => 'rubyprog',
        'ruby'             => 'rubyprog',
        'make-prog'        => 'makeprog',
        'make'             => 'makeprog'
      }
    
      def fixup
        ALIASES.each do |ali, name|
          @table[ali] = @table[name]
    
        @items.freeze
        @table.freeze
        @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/
    
    
      def parse_opt(opt)
        m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}"
        m.to_a[1,2]
    
      def dllext
        @rbconfig['DLEXT']
    
      def value_config?(name)
        lookup(name).value?
    
      class Item
        def initialize(name, template, default, desc)
          @name = name.freeze
          @template = template
          @value = default
          @default = default
          @description = desc
        end
    
        attr_reader :name
        attr_reader :description
    
        attr_accessor :default
        alias help_default default
    
        def help_opt
          "--#{@name}=#{@template}"
        end
    
        def value
          @value
        end
    
        def resolve(table)
          @value.gsub(%r<\$([^/]+)>) { table[$1] }
        end
    
        def set(val)
          @value = check(val)
        end
    
        private
    
        def check(val)
          setup_rb_error "config: --#{name} requires argument" unless val
          val
        end
    
      class BoolItem < Item
        def config_type
          'bool'
        end
    
        def help_opt
          "--#{@name}"
        end
    
        private
    
        def check(val)
          return 'yes' unless val
          case val
          when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes'
          when /\An(o)?\z/i, /\Af(alse)\z/i  then 'no'
          else
            setup_rb_error "config: --#{@name} accepts only yes/no for argument"
          end
        end
    
      class PathItem < Item
        def config_type
          'path'
        end
    
        private
    
        def check(path)
          setup_rb_error "config: --#{@name} requires argument"  unless path
          path[0,1] == '$' ? path : File.expand_path(path)
        end
    
      class ProgramItem < Item
        def config_type
          'program'
        end
    
      class SelectItem < Item
        def initialize(name, selection, default, desc)
          super
          @ok = selection.split('/')
        end
    
        def config_type
          'select'
        end
    
        private
    
        def check(val)
          unless @ok.include?(val.strip)
            setup_rb_error "config: use --#{@name}=#{@template} (#{val})"
          end
          val.strip
        end
    
      class ExecItem < Item
        def initialize(name, selection, desc, &block)
          super name, selection, nil, desc
          @ok = selection.split('/')
          @action = block
        end
    
        def config_type
          'exec'
        end
    
        def value?
          false
        end
    
        def resolve(table)
          setup_rb_error "$#{name()} wrongly used as option value"
        end
    
        undef set
    
        def evaluate(val, table)
          v = val.strip.downcase
          unless @ok.include?(v)
            setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})"
          end
          @action.call v, table
        end
    
      class PackageSelectionItem < Item
        def initialize(name, template, default, help_default, desc)
          super name, template, default, desc
          @help_default = help_default
        end
    
        attr_reader :help_default
    
        def config_type
          'package'
        end
    
        private
    
        def check(val)
          unless File.dir?("packages/#{val}")
            setup_rb_error "config: no such package: #{val}"
          end
          val
    
      class MetaConfigEnvironment
        def initialize(config, installer)
          @config = config
          @installer = installer
        end
    
        def config_names
          @config.names
        end
    
        def config?(name)
          @config.key?(name)
        end
    
        def bool_config?(name)
          @config.lookup(name).config_type == 'bool'
        end
    
        def path_config?(name)
          @config.lookup(name).config_type == 'path'
        end
    
        def value_config?(name)
          @config.lookup(name).config_type != 'exec'
        end
    
        def add_config(item)
          @config.add item
        end
    
        def add_bool_config(name, default, desc)
          @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc)
        end
    
        def add_path_config(name, default, desc)
          @config.add PathItem.new(name, 'path', default, desc)
        end
    
        def set_config_default(name, default)
          @config.lookup(name).default = default
        end
    
        def remove_config(name)
          @config.remove(name)
        end
    
        # For only multipackage
        def packages
          raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer
          @installer.packages
        end
    
        # For only multipackage
        def declare_packages(list)
          raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer
          @installer.packages = list
        end
    
    # This module requires: #verbose?, #no_harm?
    
    module FileOperations
    
    
      def mkdir_p(dirname, prefix = nil)
        dirname = prefix + File.expand_path(dirname) if prefix
        $stderr.puts "mkdir -p #{dirname}" if verbose?
    
        return if no_harm?
    
    
        # Does not check '/', it's too abnormal.
        dirs = File.expand_path(dirname).split(%r<(?=/)>)
        if /\A[a-z]:\z/i =~ dirs[0]
    
          disk = dirs.shift
          dirs[0] = disk + dirs[0]
        end
        dirs.each_index do |idx|
          path = dirs[0..idx].join('')
    
          Dir.mkdir path unless File.dir?(path)
    
      def rm_f(path)
        $stderr.puts "rm -f #{path}" if verbose?
    
        return if no_harm?
    
      def rm_rf(path)
        $stderr.puts "rm -rf #{path}" if verbose?
    
        return if no_harm?
    
        remove_tree path
      end
    
      def remove_tree(path)
        if File.symlink?(path)
          remove_file path
        elsif File.dir?(path)
          remove_tree0 path
        else
          force_remove_file path
        end
      end
    
      def remove_tree0(path)
        Dir.foreach(path) do |ent|
          next if ent == '.'
          next if ent == '..'
          entpath = "#{path}/#{ent}"
          if File.symlink?(entpath)
            remove_file entpath
          elsif File.dir?(entpath)
            remove_tree0 entpath
    
            force_remove_file entpath
    
        begin
          Dir.rmdir path
        rescue Errno::ENOTEMPTY
          # directory may not be empty
        end
    
      def move_file(src, dest)
        force_remove_file dest
    
          File.open(dest, 'wb') {|f|
            f.write File.binread(src)
          }
    
          File.chmod File.stat(src).mode, dest
    
          File.unlink src
        end
      end
    
      def force_remove_file(path)
        begin
          remove_file path
        rescue
    
      def remove_file(path)
        File.chmod 0777, path
        File.unlink path
      end
    
      def install(from, dest, mode, prefix = nil)
    
        $stderr.puts "install #{from} #{dest}" if verbose?
        return if no_harm?
    
    
        realdest = prefix ? prefix + File.expand_path(dest) : dest
        realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest)
        str = File.binread(from)
        if diff?(str, realdest)
    
          verbose_off {
    
            rm_f realdest if File.exist?(realdest)
          }
          File.open(realdest, 'wb') {|f|
            f.write str
    
          }
          File.chmod mode, realdest
    
    
          File.open("#{objdir_root()}/InstalledFiles", 'a') {|f|
            if prefix
              f.puts realdest.sub(prefix, '')
            else
              f.puts realdest
            end
          }
    
      def diff?(new_content, path)
        return true unless File.exist?(path)
        new_content != File.binread(path)
    
      def command(*args)
        $stderr.puts args.join(' ') if verbose?
        system(*args) or raise RuntimeError,
            "system(#{args.map{|a| a.inspect }.join(' ')}) failed"
    
      def ruby(*args)
        command config('rubyprog'), *args
      end
      
      def make(task = nil)
        command(*[config('makeprog'), task].compact)
    
      def extdir?(dir)
        File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb")
    
      def files_of(dir)
        Dir.open(dir) {|d|
          return d.select {|ent| File.file?("#{dir}/#{ent}") }
    
      DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn )
    
      def directories_of(dir)
        Dir.open(dir) {|d|
          return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT
    
    # This module requires: #srcdir_root, #objdir_root, #relpath
    module HookScriptAPI
    
        @config[key]
      end
    
      alias config get_config
    
    
      # obsolete: use metaconfig to change configuration
      def set_config(key, val)
    
        @config[key] = val
      end
    
      #
    
      # srcdir/objdir (works only in the package directory)
    
        "#{srcdir_root()}/#{relpath()}"
    
      end
    
      def curr_objdir
    
        "#{objdir_root()}/#{relpath()}"
    
      def srcfile(path)
        "#{curr_srcdir()}/#{path}"
    
      def srcexist?(path)
        File.exist?(srcfile(path))
    
      def srcdirectory?(path)
        File.dir?(srcfile(path))
    
      def srcfile?(path)
        File.file?(srcfile(path))
    
      def srcentries(path = '.')
        Dir.open("#{curr_srcdir()}/#{path}") {|d|
          return d.to_a - %w(. ..)
    
      def srcfiles(path = '.')
    
        srcentries(path).select {|fname|
    
          File.file?(File.join(curr_srcdir(), path, fname))
    
      def srcdirectories(path = '.')
    
        srcentries(path).select {|fname|
    
          File.dir?(File.join(curr_srcdir(), path, fname))
    
      Version   = '3.4.1'
      Copyright = 'Copyright (c) 2000-2005 Minero Aoki'
    
      TASKS = [
        [ 'all',      'do config, setup, then install' ],
        [ 'config',   'saves your configurations' ],
        [ 'show',     'shows current configuration' ],
        [ 'setup',    'compiles ruby extentions and others' ],
        [ 'install',  'installs files' ],
        [ 'test',     'run all tests in test/' ],
        [ 'clean',    "does `make clean' for each extention" ],
        [ 'distclean',"does `make distclean' for each extention" ]
      ]
    
      def ToplevelInstaller.invoke
        config = ConfigTable.new(load_rbconfig())
        config.load_standard_entries
        config.load_multipackage_entries if multipackage?
        config.fixup
        klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller)
        klass.new(File.dirname($0), config).invoke
    
      def ToplevelInstaller.multipackage?
        File.dir?(File.dirname($0) + '/packages')
    
      def ToplevelInstaller.load_rbconfig
        if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg }
          ARGV.delete(arg)
          load File.expand_path(arg.split(/=/, 2)[1])
          $".push 'rbconfig.rb'
        else
          require 'rbconfig'
        end
        ::Config::CONFIG
    
      def initialize(ardir_root, config)
        @ardir = File.expand_path(ardir_root)
        @config = config
        # cache
        @valid_task_re = nil
    
      def config(key)
        @config[key]
    
      def inspect
        "#<#{self.class} #{__id__()}>"
    
      def invoke
        run_metaconfigs
        case task = parsearg_global()
        when nil, 'all'
          parsearg_config
          init_installers
          exec_config
          exec_setup
          exec_install
        else
          case task
          when 'config', 'test'
            ;
          when 'clean', 'distclean'
            @config.load_savefile if File.exist?(@config.savefile)
          else
            @config.load_savefile
          end
          __send__ "parsearg_#{task}"
          init_installers
          __send__ "exec_#{task}"
    
      
      def run_metaconfigs
        @config.load_script "#{@ardir}/metaconfig"
    
      def init_installers
        @installer = Installer.new(@config, @ardir, File.expand_path('.'))
    
      #
      # Option Parsing
      #
    
      def parsearg_global
        while arg = ARGV.shift
          case arg
          when /\A\w+\z/
            setup_rb_error "invalid task: #{arg}" unless valid_task?(arg)
            return arg
          when '-q', '--quiet'
            @config.verbose = false
          when '--verbose'
            @config.verbose = true
          when '--help'
            print_usage $stdout
            exit 0
          when '--version'
            puts "#{File.basename($0)} version #{Version}"
            exit 0
          when '--copyright'
            puts Copyright
            exit 0
          else
            setup_rb_error "unknown global option '#{arg}'"
          end
        end
        nil
    
      def valid_task?(t)
        valid_task_re() =~ t
    
      def valid_task_re
        @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/
      end
    
      def parsearg_no_options
        unless ARGV.empty?
          task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1)
          setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}"
    
      alias parsearg_show       parsearg_no_options
      alias parsearg_setup      parsearg_no_options
      alias parsearg_test       parsearg_no_options
      alias parsearg_clean      parsearg_no_options
      alias parsearg_distclean  parsearg_no_options
    
      def parsearg_config
        evalopt = []
        set = []
        @config.config_opt = []
        while i = ARGV.shift
          if /\A--?\z/ =~ i
            @config.config_opt = ARGV.dup
            break
          end
          name, value = *@config.parse_opt(i)
          if @config.value_config?(name)
            @config[name] = value
          else
            evalopt.push [name, value]
          end
          set.push name
        end
        evalopt.each do |name, value|
          @config.lookup(name).evaluate value, @config
        end
        # Check if configuration is valid
        set.each do |n|
          @config[n] if @config.value_config?(n)
    
      def parsearg_install
        @config.no_harm = false
        @config.install_prefix = ''
        while a = ARGV.shift
          case a
          when '--no-harm'
            @config.no_harm = true
          when /\A--prefix=/
            path = a.split(/=/, 2)[1]
            path = File.expand_path(path) unless path[0,1] == '/'
            @config.install_prefix = path
          else
            setup_rb_error "install: unknown option #{a}"
          end
        end
    
      def print_usage(out)
        out.puts 'Typical Installation Procedure:'
        out.puts "  $ ruby #{File.basename $0} config"
        out.puts "  $ ruby #{File.basename $0} setup"
        out.puts "  # ruby #{File.basename $0} install (may require root privilege)"
        out.puts
        out.puts 'Detailed Usage:'
        out.puts "  ruby #{File.basename $0} <global option>"
        out.puts "  ruby #{File.basename $0} [<global options>] <task> [<task options>]"
    
        fmt = "  %-24s %s\n"
        out.puts
        out.puts 'Global options:'
        out.printf fmt, '-q,--quiet',   'suppress message outputs'
        out.printf fmt, '   --verbose', 'output messages verbosely'
        out.printf fmt, '   --help',    'print this message'
        out.printf fmt, '   --version', 'print version and quit'
        out.printf fmt, '   --copyright',  'print copyright and quit'
        out.puts
        out.puts 'Tasks:'
        TASKS.each do |name, desc|
          out.printf fmt, name, desc
        end
    
        fmt = "  %-24s %s [%s]\n"
        out.puts
        out.puts 'Options for CONFIG or ALL:'
        @config.each do |item|
          out.printf fmt, item.help_opt, item.description, item.help_default
        end
        out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's"
        out.puts
        out.puts 'Options for INSTALL:'
        out.printf fmt, '--no-harm', 'only display what to do if given', 'off'
        out.printf fmt, '--prefix=path',  'install path prefix', ''
        out.puts
    
      #
      # Task Handlers
      #
    
      def exec_config
        @installer.exec_config
        @config.save   # must be final
    
      def exec_setup
        @installer.exec_setup
    
      def exec_install
        @installer.exec_install