require 'rbconfig'
require 'fileutils'

# some global variables: 
$LOCAL_LIBS = ""

# A string extension from old mkmf.rb
class String
  def quote
    /\s/ =~ self ? "\"#{self}\"" : self
  end

  # and a new function, that can come in useful in several places:
  def sanitize
    return self.tr("a-z./\055", "A-Z___")
  end
end

# An array extension from old mkmf.rb
class Array
  def quote
    map {|s| s.quote}
  end
end


=begin rdoc
:title: Mkmf2: a better replacement for mkmf
=end 

=begin rdoc

=Mkmf2: a drop-in replacement for mkmf

Mkmf2 aims at replacing the functionnality of the old mkmf, with a better and
cleaner code. It hopefully will keep all the previous mkmf files working,
while providing 
* better control over the Makefiles produced;
* installation to <tt>$HOME</tt>;
* uninstallation;
* more flexibility.

==Background

Mkmf2 was written to supplement a missing feature on the old +mkmf.rb+: it is
not possible to install include files along with the library, and not easy
to make several libraries, that link each other, and install them... So here
comes mkmf2.rb !


==Bugs

There are probably a huge number of bugs, or missing features, or
misleading features. In case you find one, please file a request
in the mkmf2 project on rubyforge, at the address
http://rubyforge.org/tracker/?group_id=1391

But before, please, check out the latest cvs version at
http://rubyforge.org/scm/?group_id=1391
and make sure the bug is still there...

==Note to anyone who wishes to contribute

Please, document any code you add, and keep the
changelog up to date...

==Copyright

This module is copyright 2006 by Vincent Fourmond. You can use and
redistribute it under the terms of the General Public License version 2.
As a special exception, you can distribute any verbatim copy along with
a program, provided that this program depends on mkmf2 to install.

=end

module Mkmf2

  include Config

  # The CVS tag used for the release.
  CVS_TAG = '$Name:  $'

  # The module version; it is computed from CVS_TAG
  VERSION = CVS_TAG.match(/\D+(.*?)\s*\$?$/)[1].tr('_-','..')

  # The entities: a list of Mkmf2::MfEntity representing what
  # we are currently building.
  @@entities = []

  # User-defined rules:
  @@user_rules = []


  # we first start the module with a whole bunch of small subclasses
  # that will come in really useful later.

  # This class provides a crude functionnality for defining a rule
  # in a Makefile.
  class MfRule
    attr_accessor :name, :rule, :dependencies

    # converts this rule to a string that can be readily printed to
    # the Makefile
    def to_s
      return "#{@name}: #{@dependencies.join ' '}" + 
        if @rule.empty? # we don't need a second line
          "\n" 
        else
          "\n\t#{@rule.join "\n\t"}\n" 
        end 
    end
    
    # +name+:: the name of the rule.
    # +rule+::
    #   a string or an array of lines; strings are split if necessary
    # +deps+:: a string or an array of dependencies
    def initialize(name, rule = nil, deps = nil)
      @name = name 

      if rule.is_a? Array
        @rule = rule
      elsif rule.nil?
        @rule = []
      else
        @rule = rule.split /\s*\n\t?\s*/
      end

      if deps.is_a? Array
        @dependencies = deps
      elsif deps.nil?
        @dependencies = []
      else
        @dependencies = deps.split /\s+/
      end
    end
    
  end 

  # A small class to include comments into the Makefile, just to help anyone
  # who has to debug something. And it annoys me not to be able to clearly
  # read the output of the test.rb script !
  class MfComment < MfRule
    
    # The actual text of the comment.
    attr_accessor :text

    # +str+ is the text meant to be displayed in the makefile. If it is nil
    # the rule just ends up being a blank line, to allow for easy separation
    # of the rules.
    def initialize(str = nil)
      @text = str
    end

    def to_s
      if @text.nil?
        return "\n"
      else
        lines = @text.to_s.split '\n'
        lines.unshift ""
        return "#{lines.join "\n# "}\n" 
      end
    end
    
  end
    


  # An MfEntity object reprensents something that is part of the package.
  # In this way, it needs to provide:
  # 
  # * a way to *install* itself;
  # * a way to *uninstall* itself;
  # * a way to *clean* the source directory;
  # * and, possibly, a way to *build* itself.
  #
  # This class should be subclassed for objects that need building.
  # It should be enough though for many simple objects.
  #
  # The module will hold a list of all the MfEntities, and use them in turn
  # to create the Makefile. 
  class MfEntity

    # +install_files+ is a hash that associates files in the building
    # tree to files where the installation is done. To be more precise
    # the install path for the files is expressed relative to
    # +install_path+
    attr_accessor :install_files

    # This name has to be registered by Mkmf2.register_name. Its lowercase
    # version will be used to name the targets (install_#{name}), etc.), and
    # it's uppercase version can be used as a prefix to name Makefile
    # variables, that we can for instance depend on.
    attr_accessor :name

    # It represents the type of the entity. It should be a valid entry
    # for Mkmf2.install_path.
    attr_accessor :kind

    # It is used to ask Mkmf2.install_files for the right rule for
    # installing files (they can be data, scripts, programs, and so on...)
    attr_accessor :install_rule

    # The +name+ parameter is turned into a unique name and ends up as
    # an identifier for the stuffs related to this element in the Makefile.
    # The +install+ parameter directly becomes install_files.
    # The +kind+ parameter describes the kind of thing we're going
    # to install. See Mkmf2.install_path.

    def initialize(name, install, kind = 'lib',
                   install_rule = 'install_data')

      # we append kind to avoid overlaps in case of
      # a malchosen name (even if this is guaranteed by register_name),
      # it will look better.
      @name = Mkmf2::register_name(kind + "_" + name)
      @install_files = install.dup
      @kind = kind
      @install_rule = install_rule
    end

    # Returns the install path for the current object. The implementation
    # should call Mkmf2.install_path with an appropriate argument.
    def install_path
      return Mkmf2::install_path(@kind)
    end

    
    # Returns an array containing
    # * a string as the first element which should be appendended
    #   as a dependency for the install target
    # * a set of MfRules, describing what need to be done
    #   for that peculiar install.

    def install_rules
      rules = ["install_#{@name.downcase}"]

      rules.push MfComment.new("installation rules for #{name}")

      rules.push MfRule.new("install_#{@name.downcase}",
                            Mkmf2.install_files_rules(@install_rule,
                                                      install_path,
                                                      @install_files),
                            ["$(#{@name.upcase}_SOURCE_FILES)",
                             "$(#{@name.upcase}_INSTALL_DIRS)"] 
                            )
    end

    # Rules for uninstalling the entity.
    # See install_rules for the return values.
    def uninstall_rules
      rules = ["uninstall_#{@name.downcase}"]

      rules.push MfComment.new("uninstallation rules for #{name}")
      files = [] # the rules to remove the installed files
      dirs = [] # the directories created during install
      @install_files.each_value do |v|
        dest = File.join(install_path,v)
        files.push Mkmf2.rule('remove',dest)
        dirs << File.dirname(dest)
      end
      dirs.uniq!
      for dir in dirs
        files << Mkmf2.rule('remove_path',dir)
      end
      rules.push MfRule.new("uninstall_#{@name.downcase}",
                            files)
    end

    # Rules for cleaning the source directory. See #install_rules for
    # an explanation about return values. This implementation does
    # nothing, as there is nothing to be built.
    def clean_rules
      return []
    end
      
    # Rules for building the entity. See #install_rules for
    # an explanation about return values.
    def build_rules
      return []
    end
    
    # Returns all the variables that should be added to the Makefile.
    # It should return a simple hash containing the values.
    def variables
      # we need to define this variable:
      ret = {}
      install = []
      source = []
      dirs = []
      @install_files.each do |k,f|
        file = File.join(install_path,f)
        install << file
        source << k
        dirs << File.dirname(file) + "/"
      end
      dirs.uniq! 
      ret["#{@name.upcase}_INSTALL_FILES"] = 
        Mkmf2.pretty_print_list(install,"#{@name.upcase}_INSTALL_FILES=")
      ret["#{@name.upcase}_INSTALL_DIRS"] = 
        Mkmf2.pretty_print_list(dirs,"#{@name.upcase}_INSTALL_DIRS=")
      ret["#{@name.upcase}_SOURCE_FILES"] = 
        Mkmf2.pretty_print_list(source,"#{@name.upcase}_SOURCE_FILES=")
      return ret
    end
  end

  # The class to create libraries that need to be built (that is basically
  # why I wrote the whole stuff !).
  class MfBinaryLibEntity < MfEntity

    # The source files of the library.
    attr_accessor :sources

    # The object files for the library
    attr_accessor :objects

    # The name of the file produced in the source directory.
    attr_accessor :target_file

    # The libraries on which the linking does depend
    attr_accessor :lib_depends

    # Supplementary command-line arguments for the linking step.
    # It will be used for instance to store library info.
    attr_accessor :suppl_args

    # +name+::  of the binary library produce, for instance "Biniou/Bidule"
    # +sources+:: sources of the files, defaults to all the potential
    #             +C+/+C\+\++ in the current directory.
    #             The sources are further expanded with Dir.glob.
    #             If you give then name of a directory, it will be
    #             expanded 
    # +target_file+:: how the files built in the current directory
    #                 will be called. Defaults to the same file name
    #                 as +name+.
    # +lib_depends+:: an array of objects, which designate ruby binary
    #                 libraries that this library does depend on;

    def initialize(name, sources = nil, target_file = nil,
                   lib_depends = nil)
      if sources.nil?
        sources = ["."]
      end

      @sources = []
      add_sources(sources)


      if target_file.nil?
        @target_file = File.basename(name) + ".#{CONFIG["DLEXT"]}"
      else
        @target_file = target_file
      end

      install_hash = { @target_file => name + ".#{CONFIG["DLEXT"]}" }
      # Then, we call the function of the parent
      super(name, install_hash,'bin_lib','install_bin')
      @suppl_args = ""
    end

    # Adds source files to the library. Can come in useful inside a block
    def add_sources(*sources)
      # we flatten the array, just to make sure everything is top-level
      sources.flatten!
      
      # We expand directory into the files in them
      # (no subdirs)
      sources.collect! do |f|
        if File.directory?(f)
          temp = []
          for ext in Mkmf2.source_files_extensions
            temp << File.join(f,"*.#{ext}")
          end
          temp
        else
          f
        end
      end

      # Then, we expand the files:
      for f in sources
        files = Dir.glob(f)
        if files.empty?
          @sources << f
        else
          @sources += files
        end
      end

      # and we update the list of current object files
      @objects = @sources.collect do |f|
        f.gsub(/\.[^.]+$/, ".#{CONFIG["OBJEXT"]}")
      end
      
    end
    # 
    def build_rules
      return [#"build_#{@name.downcase}",
              @target_file,
              # better to write this way... more clear on the
              # zsh command-line completion ;-) !
              MfRule.new(@target_file,
                         Mkmf2.rule('build_library', 
                                    "$(#{name.upcase}_OBJS)", 
                                    @suppl_args),
                         "$(#{name.upcase}_OBJS)")
             ]
    end

    # This removes the object files for the target...
    def clean_rules
      rules = ["clean_#{@name.downcase}"]

      rules.push MfComment.new("cleaning rules for #{name}")
      files = [] # the rules to clean the files
      @objects.each do |v|
        files.push Mkmf2.rule('remove',v)
      end

      files.push Mkmf2.rule('remove', @target_file)
      rules.push MfRule.new("clean_#{@name.downcase}",
                            files)
      return rules
    end

    def variables
      vars = super
      # We add the object list variable
      vars["#{name.upcase}_OBJS"] = 
        Mkmf2.pretty_print_list(@objects,"#{name.upcase}_OBJS=")
      return vars
    end
    
  end

  # Returns a list of the potential source files extensions.
  def Mkmf2.source_files_extensions
    return Mkmf2.cpp_files_extensions + 
      Mkmf2.c_files_extensions
  end 

  # Return the extensions for C++ files.
  def Mkmf2.cpp_files_extensions
    return %w{cpp cc cxx}
  end 

  # Return extensions for C files.
  def Mkmf2.c_files_extensions
    return %w{c}
  end 


  # This function declares a shared binary library that will be built
  # and installed using the appropriate functions.
  #
  # +libname+ is what is usually passed as an argument to the
  # +create_makefile+ function of the old 
  #
  # +sources+ is the source files. They are processed using Dir.glob, so
  # they can contain wildcards.
  #
  # This function contends itselfs to *declare* the library, it does not
  # *build* it, as this will be done by +make+.

  def declare_binary_library(libname, *sources, &b)
    add_entity(MfBinaryLibEntity.new(libname, sources), &b)
  end
  
  def add_entity(entity)
    yield entity if block_given?
    @@entities << entity
    return entity
  end

  # This function declares a Ruby library, namely files that want
  # to be installed directly in the rubylibdir (or equivalent, depending
  # on the user settings).
  # +target_dir+:: the target directory, relative to the installation
  #                directory;
  # +files+:: the files to install. Defaults to "lib/**.rb"
  #
  # For instance,
  #  declare_library("Biniou", "toto.rb")
  # will install the file "Biniou/toto.rb" in the common library
  # directory. 
              
  def declare_library(target_dir,*files, &b)
    if files.empty?
      files << "lib/**/*.rb" 
    end
    declare_file_set(target_dir, files, 'lib', false, &b)
  end

  # Declares a set of files that should be executed directly be the user.
  def declare_exec(*files, &b)
    # No need for a target directory.
    declare_file_set("", files, 'exec', true, 'install_script',&b)
  end

  # Declares a documentation that doesn't need to be built. For the arguments,
  # see #declare_library.
  def declare_doc(target_dir, *files,&b)
    if files.empty?
      files << "doc/**/*"
    end
    declare_file_set(target_dir, files, 'doc', false,&b)
  end

  # Declares a set of include files to be installed. Defaults to
  # all the files in the include subdirectory.
  def declare_includes(target_dir,*files,&b)
    if files.empty?
      files << "include/**/*.h" 
    end
    declare_file_set(target_dir, files, 'include', true,&b)
  end
    
    

  # The main place for declaring files to be installed.
  # +target_dir+:: the target directory relative to the install path
  #                if it is nil or empty, then just install in the
  #                base directory.
  # +files+:: the source files
  # +kind+:: the kind of target (see again and again install_path)
  # +basename+:: wether or not to strip the leading directories ?
  # +skip+:: a regular expression (or whatever object that has a ===
  #          method that matches strings) saying wich files should
  #          be excluded. Defaults to /~$/.
  # _base_dir_:: a source base directory to be stripped from the target
  #              name

  def declare_file_set(target_dir, files, kind = 'lib', 
                       basename = true, 
                       rule = 'install_data',
                       skip = /~$/, delete = /((^.*\/)?lib\/)?/,
                       base_dir = false, &b)
    source_files = []
    for glob in files
      # If glob looks like a glob, we use Dir.glob, else we add it
      # without modification
      if glob =~ /\*|\[|\?/
        source_files += Dir[glob]
      else
        source_files << glob
      end
    end
    install_hash = {}
    for f in source_files
      next if skip === f
      next if File.directory? f
      filename = if basename 
                   File.basename(f)
                 else
                   f.dup
                 end
      filename.gsub!(delete, '')
      
      if target_dir.nil? or target_dir.empty?
        install_hash[f] = filename
      else
        install_hash[f] = File.join(target_dir, filename) 
      end
    end
    add_entity(MfEntity.new(target_dir, install_hash, 
                            kind, rule), &b)
  end
  
  @@model = "local"

  # Sets the model for directory installation. There are for now 
  # three values:
  # local:: for a standard installation (like the default in mkmf.rb)
  # dist:: to use in a packageing system
  # home:: to install to a home directory (basically, prefix= <tt>$HOME</tt>)
  #
  # Please do not modify @@model directly.

  def Mkmf2.set_model(model = "local")
    @@model = model
  end

  # Sets up a whole bunch of variables necessary for installation, depending
  # on the current value of the @@model parameter. This is the place where we
  # setup the variables used by the installation process, namely:
  # +RUBYLIB_INSTALL_DIR+:: the directory where the text libraries
  #                         are to be installed;
  # +RUBYARCHLIB_INSTALL_DIR+:: the directory where architecture
  #                             dependent libraries are installed;
  # +INCLUDE_INSTALL_DIR+:: the directory where include files should go
  #                         if necessary;
  # And for now, that is all.
  def setup_model
    case @@model
    when "home"
      # I made a mistake for the home scheme.
      # we ensure the prefix is set to $HOME 
      MAKEFILE_CONFIG["RUBYLIB_INSTALL_DIR"] = 
        "$(DESTDIR)$(HOME)/lib/ruby"
      # For the HOME scheme, binaries and .rb files go
      # to the same directory.
      MAKEFILE_CONFIG["RUBYARCHLIB_INSTALL_DIR"] = 
        "$(DESTDIR)$(HOME)/lib/ruby"
      MAKEFILE_CONFIG["INCLUDE_INSTALL_DIR"] = 
        "$(DESTDIR)$(HOME)/include"
      MAKEFILE_CONFIG["EXEC_INSTALL_DIR"] = 
        "$(DESTDIR)$(HOME)/bin"
        
    when "dist"
      # shares some code with the previous item, don't forget to
      # update *BOTH* !
      MAKEFILE_CONFIG["RUBYLIB_INSTALL_DIR"] = 
        "$(DESTDIR)$(rubylibdir)"
      MAKEFILE_CONFIG["RUBYARCHLIB_INSTALL_DIR"] = 
        "$(DESTDIR)$(archdir)"
      MAKEFILE_CONFIG["INCLUDE_INSTALL_DIR"] = 
        "$(DESTDIR)$(includedir)"
      MAKEFILE_CONFIG["EXEC_INSTALL_DIR"] = 
        File.join("$(DESTDIR)$(prefix)", "bin")

    when "local"
      MAKEFILE_CONFIG["RUBYLIB_INSTALL_DIR"] = 
        "$(sitelibdir)"
      MAKEFILE_CONFIG["RUBYARCHLIB_INSTALL_DIR"] = 
        "$(sitearchdir)"

      # To make the base directory for installation, I propose
      # to strip all the directories containing ruby from the
      # $(sitedir) directory and strip one more directory.

      basedir = MAKEFILE_CONFIG["sitedir"]
      while File.basename(basedir) =~ /ruby/
        basedir = File.dirname(basedir)
      end
      # Strip one more:
      basedir = File.dirname(basedir)
      MAKEFILE_CONFIG["basedir"] = basedir
      MAKEFILE_CONFIG["INCLUDE_INSTALL_DIR"] = 
        "$(basedir)/include"
      MAKEFILE_CONFIG["EXEC_INSTALL_DIR"] = 
        "$(basedir)/bin"
    end

  end


  # Returns the installation path for a given thing (+what+). This function
  # basically returns a simple MAKEFILE_CONFIG variable, which has otherwise
  # been setup by setup_model. What is available for now:
  # +lib+:: where to install .rb library
  # +bin_lib+:: where to install binary libs
  # +include+:: include files

  def Mkmf2.install_path(what)
    # we should definitely append a $(DESTDIR) to every single
    # directory we return, since it will make debugging
    
    case what
    when 'lib'
      return Mkmf2.config_var('RUBYLIB_INSTALL_DIR')
    when 'exec'
      return Mkmf2.config_var('EXEC_INSTALL_DIR')
    when 'bin_lib'
      return Mkmf2.config_var('RUBYARCHLIB_INSTALL_DIR')
    when 'include'
      return Mkmf2.config_var('INCLUDE_INSTALL_DIR')
    else
      raise "Unkown installation directory specification: #{what}"
    end 
  end

  @@registered_names = {} # a hash containing already registered names
  # to avoid namespace clobbering.
  
  # Register a new name, making sure the name returned is unique.
  def Mkmf2.register_name(name)
    # first, transform all that is not letter into underscore
    name.gsub!(/[^a-zA-Z]/,'_')
    # pretty-print a bit
    name.gsub!(/^_|_$/,'')
    name.gsub!(/_+/, '_')
    name.downcase!
    # then, increment the number
    if @@registered_names[name]
      i = 1
      while @@registered_names["#{name}_#{i}"]
        i += 1
      end
      name = "#{name}_#{i}"
    end
    @@registered_names[name] = true
    return name
  end
  

  # Stores which configuration variables are used
  @@config_variables_used = []
  # Returns the given config key, that will be used to write a rule
  # in the Makefile. For better output, the function does store a list of
  # all the config variables which are in use, and simply outputs 
  # +$(VARIABLE)+. 
  def Mkmf2.config_var(str)
    @@config_variables_used << str
    return "$(#{str})"
  end

  # This hash says which of the "CONFIG" variables we should make available
  # as a global variable -- and use it back to output the variables. If non
  # nil, the second part says which name it should have as global variable.
  MKMF_GLOBAL_VARIABLES =   {
    "CFLAGS" => nil, 
    "CXXFLAGS" => nil, 
    "LDFLAGS" => nil, 
    "DLDFLAGS" => nil,
    "CPPFLAGS" => nil,
    "LIBS" => nil,
    "LIBRUBYARG" => nil,
    "LOCAL_LIBS" => nil,
    "libs" => nil,
  }
                           

  # Takes the config variables, turn them into global variables.
  def config_to_global
    for var,name in MKMF_GLOBAL_VARIABLES 
      name = var if name.nil?
      value = MAKEFILE_CONFIG[var].to_s # make sure its a string
      # in case it doesn't exist there...
      # Nice trick to get around quoting...
      block = eval "proc {|x| $#{name} = x}"
      block.call(value)
    end
    $LIBRUBYARG = "" # seems the default in mkmf.rb
  end

  # Takes the global variables taken from the config, and put them back into
  # the config hash.
  def global_to_config
    for var,name in MKMF_GLOBAL_VARIABLES 
      name = var if name.nil?
      value = eval "$#{name}"
      MAKEFILE_CONFIG[var] = value
    end
  end



  # A constant to get a configuration variable
  MAKE_VARIABLE = /\$\((\w+)\)/

  # List config variables referenced to by the string _str_. If
  # hash is specified, only config variables that are keys of this hash
  # will be listed.
  def subvars(str, hash = nil)
    vars = []
    str.gsub(MAKE_VARIABLE) { |k|
      # we add the key to the list only if it exists, else the
      # environment variable gets overridden by what is written
      # in the Makefile (for $HOME, for instance)
      vars << $1 if ( hash.nil? || hash.key?($1) )
    }
    return vars
  end

  # A way to deal with compound make variables (that is,
  # make variables that get more than just themselves on the output)

  COMPOUND_MAKE_VARIABLES = {
    "CFLAGS" => "$(CFLAGS) $(ARCH_FLAG)", 
    "DLDFLAGS" => "$(DLDFLAGS) $(ARCH_FLAG)",
    "LIBS" => "$(LIBRUBYARG) $(libs) $(LIBS)",
  }


  # Returns the contents of a "compound" variable.
  def compound_var_expand(var, value = nil, hash = MAKEFILE_CONFIG)
    if value.nil?
      value = MAKEFILE_CONFIG[var]
    end
    COMPOUND_MAKE_VARIABLES[var].gsub("$(#{var})",value)
  end


  # Returns a string containing all the configuration variables. We
  # use the MAKEFILE_CONFIG for more flexibility in the Makefile: the
  # variables can then be redefined on the make command-line.
  def output_config_variables
    str = ""
    keys = @@config_variables_used.uniq
    global_to_config
    new_keys = []
    begin 
      # we merge the new keys with the old
      keys += new_keys
      keys.uniq!
      new_keys = []
      keys.each do |k|
        if MAKEFILE_CONFIG.key? k
          new_keys += subvars(MAKEFILE_CONFIG[k], MAKEFILE_CONFIG)
        end
        if COMPOUND_MAKE_VARIABLES.key? k
          new_keys += subvars(COMPOUND_MAKE_VARIABLES[k], MAKEFILE_CONFIG)
        end
      end
    end until (keys + new_keys).uniq.length == keys.length
    
    for var in keys.uniq.sort
      # We output the variable only if it is not empty: makes
      # it a lot easier to modify them from outside...
      if COMPOUND_MAKE_VARIABLES.key?(var)
        str += "#{var}=#{compound_var_expand(var)}\n"
      elsif MAKEFILE_CONFIG[var] =~ /\S/
        str += "#{var}=#{MAKEFILE_CONFIG[var]}\n"
      end
    end
    return str
  end
    

  # This hash contains replacements for basic filesystem functions
  # based on ruby and FileUtils. They are used only if the corresponding
  # element is missing in CONFIG. They also contains some other default
  # values for possibly missing CONFIG keys.
  CONFIG_DEFAULTS = {
    # first, a short for the rest...
    "RB_FILE_UTILS" => "$(RUBY_INSTALL_NAME) -r fileutils", 
    "INSTALL_SCRIPT" => 
    "$(RB_FILE_UTILS) -e 'FileUtils.install(ARGV[0],ARGV[1],:mode => 0755)'",
    "INSTALL_DATA" => 
    "$(RB_FILE_UTILS) -e 'FileUtils.install(ARGV[0],ARGV[1],:mode => 0644)'",
    "INSTALL_PROGRAM" =>  "$(INSTALL_SCRIPT) ",
    "MAKEDIRS" => 
    "$(RB_FILE_UTILS) -e 'FileUtils.makedirs(ARGV)'",
    "RM" => 
    "$(RB_FILE_UTILS) -e 'FileUtils.rm(ARGV)'",
    "RM_PATH" => # a trick to remove a path
    "$(RB_FILE_UTILS) -e 'd = ARGV[0]; begin ;while FileUtils.rmdir d, :verbose=>true; d = File.dirname(d);end; rescue ; end;'",
    "DEFINES" => "",
    "LIBARG" => "-l%s",
    "LIBS_SUP" => "", 
  } 
  
  # This functions checks for missing features in the CONFIG hash and 
  # supplements them using the FILE_UTILS_COMMAND hash if necessary.
  def check_missing_features
    CONFIG.delete("INSTALL_SCRIPT")   # Not consistent, it's better to use
    # the Fileutils stuff
    for key, val in CONFIG_DEFAULTS 
      if CONFIG.key?(key) and CONFIG[key] =~ /\w/
        # everything is fine
      else
        # we supplement the feature.
        MAKEFILE_CONFIG[key] = val
      end
    end
  end

  # Takes a list of CONFIG variables and returns them joined by spaces. 
  def Mkmf2.config_join(*vars)
    return vars.collect { |v|
      Mkmf2.config_var(v)
    }.join(' ')
  end
    

  # Returns the way to represent the dependencies of a rule in the
  # Makefile.
  def Mkmf2.mf_deps
    return " $(@D)"
  end

  # Returns the way to represent the current target in the Makefile.
  def Mkmf2.mf_target
    return " $@"
  end

  # A small helper function to write quickly rules, based on a configuration
  # item.
  def Mkmf2.mf_rule(cfg,*args)
    if args.empty?
      return Mkmf2.config_var(cfg) + 
        Mkmf2.mf_deps + Mkmf2.mf_target
    else
      return Mkmf2.config_var(cfg) + ' ' + args.join(' ')
    end
  end

  # This function returns the common build rules for C and C++ files.
  def Mkmf2.common_build_rules
    rules = []
    for ext in Mkmf2.c_files_extensions
      rules << MfRule.new(".#{ext}.#{CONFIG["OBJEXT"]}",
                          Mkmf2.config_join("CC",
                                            "CFLAGS",
                                            "CPPFLAGS",
                                            "INCLUDEDIRS",
                                            "DEFINES") +
                          " -c $< #{CONFIG["OUTFLAG"]} $@" 
                          ) 
    end 

    # The same, but with C++:
    for ext in Mkmf2.cpp_files_extensions
      rules << MfRule.new(".#{ext}.#{CONFIG["OBJEXT"]}",
                          Mkmf2.config_join("CXX",
                                            "CXXFLAGS",
                                            "CPPFLAGS",
                                            "INCLUDEDIRS",
                                            "DEFINES") +
                          " -c $< #{CONFIG["OUTFLAG"]} $@" 
                          ) 
    end 
    # A simple rule for making directories:
    rules << MfRule.new("%/",
                        Mkmf2.config_join("MAKEDIRS") + " $@")
    return rules
  end 

  # Now, the infrastructure for dealing with include and library
  # directories:
  if CONFIG.key?('rubyhdrdir')
    @@include_path = [
                      Mkmf2.config_var("rubyhdrdir")+"/ruby",
                      Mkmf2.config_var("rubyhdrdir"),
                      Mkmf2.config_var("rubyhdrdir") + "/" + 
                      Mkmf2.config_var("arch")
                     ] 
  else
    @@include_path = [ Mkmf2.config_var("rubylibdir") ]
  end

  @@include_path += [
                    Mkmf2.config_var("archdir"),
                    '.',
                    File.join('.','include')
                   ]

  
  # Adds one or more +paths+ to the current include path.
  def add_include_path(*paths)
    @@include_path += paths.flatten
  end

  def output_include_path
    return @@include_path.collect {|v|
      "-I#{v}"
    }.join(' ')
  end

  @@library_path = []

  # Adds one or more +paths+ to the current library path.
  def add_library_path(*paths)
    @@library_path += paths.flatten
  end

  def output_library_path
    return @@library_path.collect {|v|
      "-L#{v}"
    }.join(' ')
  end

  # I know this is bad design, but that's the best I think of for now.
  # This function better be called before using MAKEFILE_CONFIG directly.
  def update_makefile_config
    global_to_config
    setup_paths_variables
  end

  # Sets up the various variables pertaining to include
  # and library paths.
  def setup_paths_variables
    MAKEFILE_CONFIG["INCLUDEDIRS"] = output_include_path
    MAKEFILE_CONFIG["LIBDIRS"] = output_library_path
  end

  # A recommandation for line size in the Makefile
  MAKEFILE_LINE_SIZE = 40
  
  # Returns a string where all the elements are joined together. The lines
  # will break if they exceed +line_size+, but the elements themselves
  # will not be broken. They will be indented
  def Mkmf2.pretty_print_list(list, indent = 0,
                              line_size = MAKEFILE_LINE_SIZE)
    if indent.is_a? String
      indent = indent.length
    end
    lines = []
    for elem in list
      if lines.last.nil? || 
          (lines.last + elem).length > line_size
        lines << elem.dup # necessary to force ruby to create
      else
        lines.last.concat(' ' + elem)
      end
    end
    return lines.join("\\\n#{' ' * indent}")
  end

  
  # Returns the string corresponding to a rule, given by the string +rule+,
  # which can take the following values:
  # +install_data+::   returns the rule for installing a data file to a
  #                    given place
  # +install_script+:: the rule for installing code at a given place
  # +remove+::         to remove files
  # +directory+:: to create a directory
  # +build_library+:: to build a library; in that case, the first argument
  #                   is the name of the target, and the ones after
  #                   the objects.
  #
  # the optional arguments are the arguments to the rule, if they exist. Else,
  # they default to $(@D) $@ (or something like that).
  
  def Mkmf2.rule(rule, *args)
    case rule
    when 'install_data'
      return Mkmf2.mf_rule("INSTALL_DATA",args)
    when 'install_script'
      return Mkmf2.mf_rule("INSTALL_SCRIPT",args)
    when 'install_bin'
      return Mkmf2.mf_rule("INSTALL_PROGRAM",args)
    when 'remove'
      return "-" + Mkmf2.mf_rule("RM",args)
    when 'remove_path'
      return "-" + Mkmf2.mf_rule("RM_PATH", args)
    when 'directory' # to create a directory
      return Mkmf2.mf_rule("MAKEDIRS", args)
    when 'build_library' 
      return Mkmf2.mf_rule("LDSHARED",
                           Mkmf2.config_join("DLDFLAGS",
                                             "LIBDIRS"),
                           CONFIG["OUTFLAG"],
                           "$@", # the target,
                           args, # and the source
                           # The libraries should better come in the end,
                           # since for instance the standard linux ld
                           # does only link with the symbols that have been
                           # reported missing in the previous files.
                           Mkmf2.config_join("LIBRUBYARG_SHARED", 
                                             # we shouldn't forget this one !!
                                             "LOCAL_LIBS",
                                             "LIBS",
                                             # and then, the global libraries
                                             # that have been added using
                                             # have_library
                                             "LIBS_SUP")
                           )
    end
  end
  
  @@directories = {}
  # Register a directory that we might need to create. Make sure that
  # the rules don't appear twice.
  def Mkmf2.register_dir(dir)
    if ! @@directories.key?(dir)
      @@directories[dir] = true
    end
  end

  # Return the rules to create the necessary directories
  def directory_rules
    rules = []
    for dir in @@directories.keys 
      rules << MfRule.new(dir, Mkmf2.rule('directory',dir))
    end
    return rules
  end


  # A small helper function to extract the install directory name
  # for one file. +file+ is the file, +install_dir+ where we want to
  # install it. This should take care of messy dots.
  def Mkmf2.dest_dir(file, install_dir)
      dir = File::dirname(file)
      if dir == "."
        return install_dir
      else
        return File.join(install_dir,dir)
      end
  end

  # A helper function for MfEntity instances that will have to install
  # files into directories.
  # +rule+:: the rule to use, see Mkmf2::rule
  # +install_path+:: the installation path
  # +files+:: a hash containing original -> destination pairs.

  def Mkmf2.install_files(rule, install_path,files)
    rules = []
    files.each do |k,v|
      destdir = Mkmf2.dest_dir(v, install_path)
      Mkmf2.register_dir(destdir)
      rules.push(MfRule.new(File.join(install_path,v),
                            Mkmf2::rule(rule, k,
                                        File.join(install_path,v)),
                            [k,destdir])) 
      # depends on both the file
      # that will be installed and the directory where to install it.
    end

    return rules
  end

  # The companion of install_files, returning a list of strings
  # rather than MfRules. Bascially behaves the same way. It is probably
  # a better thing to use this function now.
  def Mkmf2.install_files_rules(rule, install_path,files)
    rules = []
    files.each do |k,v|
      dir = File::dirname(v)
      Mkmf2.register_dir(Mkmf2.dest_dir(v,install_path))
      rules.push Mkmf2::rule(rule, k, File.join(install_path,v))
      
      # depends on both the file
      # that will be installed and the directory where to install it.
    end

    return rules
  end

  # Small helper function for write_makefile. +target+ is
  # a hash containing a "deps" array and a "rules" array
  # which are created in case of need.
  def unwrap_rules(target, source)
    # we first make sure that the keys exist, even if we don't
    # have anything to append to them.
    if ! target.has_key? "deps"
      target["deps"] = []
    end
    if ! target.has_key? "rules"
      target["rules"] = []
    end

    return if source.empty?
    target["deps"] << source.shift
    target["rules"] += source
  end

  # Writes the Makefile using @@entities -- and others...
  def write_makefile(file_name = "Makefile")
    puts "Writing #{file_name}"
    
    install = {}
    uninstall = {}
    build = {}
    clean = {}
    vars = []



    for entity in @@entities
      t = entity.install_rules
      unwrap_rules(install,t)

      t = entity.uninstall_rules
      unwrap_rules(uninstall,t)
      
      t = entity.build_rules
      unwrap_rules(build,t)

      t = entity.clean_rules
      unwrap_rules(clean,t)

      vars << entity.variables
    end

    # Common rules: 
    common = Mkmf2.common_build_rules()
    
    # Setup the path variables:
    setup_paths_variables
    dir_rules = directory_rules()

    # OK, now, everything is prepared, we just need to create the
    # makefile and output everything into it...

    f = open(file_name, File::WRONLY|File::CREAT|File::TRUNC)


    # First, the variables in use:
    f.puts "# Configurations variables, from rbconfig.rb"
    f.puts output_config_variables

    # build has to be the first target so that simply
    # invoking make does the building, but not the installing.
    # now, the main rules
    f.puts "\n\n# main rules"
    # we force the dependence on build for install so that
    # ruby library files don't get installed before the c code is
    # compiled...
    # build is output first so that invoking make without
    # arguments builds.
    f.print MfRule.new("build", nil, build["deps"]).to_s
    f.print MfRule.new("install", nil, ["build"] + install["deps"]).to_s
    f.print MfRule.new("uninstall", nil, uninstall["deps"]).to_s
    f.print MfRule.new("clean", 
                       [Mkmf2.rule('remove',"**/*~")], # remove archive files
                       # by default in the clean target.
                       clean["deps"]).to_s

    # Add a distclean rule, to make debuild happy.
    f.print <<"EOR"
distclean: clean 
\t@-$(RM) Makefile extconf.h conftest.* mkmf2.log
\t@-$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES) 
EOR

    # Phony targets:
    f.puts ".PHONY: build install uninstall"

    f.puts "\n\n# Common rules:"
    common.each {|v|
      f.print v.to_s
    }

    
    # We write the variables:
    f.puts "\n\n# entities-dependent variables"
    for v in vars 
      v.each do |k,v|
        f.print "#{k}=#{v}\n"
      end
    end

    f.puts "\n\n# Rules to make directories"

    # directory rules
    for rule in dir_rules
      f.print rule.to_s
    end

    f.puts "\n\n# entities-dependent rules"
    # We write the rules
    for rule in (install['rules'] + 
                 uninstall['rules'] +
                 build['rules'] + clean['rules'])
      f.print rule.to_s
    end

    if @@user_rules.length > 0
      f.puts "\n\n# User-defined rules"
      for rule in @@user_rules
        f.puts rule.to_s
      end
    end


    f.close # not necessary, but good practice ;-) ?? 

  end

  # Adds a custom rule to the Makefile. If only the first argument
  # is specified, it is written as is to the Makefile. If at least the
  # second or the third is non-nil, a MfRule is created with the arguments
  # and written to the Makefile.
  def custom_rule(str, rule = nil, deps = nil)
    if rule || deps 
      @@user_rules  << MfRule.new(str, rule, deps)
    else
      @@user_rules << str
    end
  end

  # *The* compatibility function with the mkmf.rb !
  def create_makefile(target)
    declare_library(File.dirname(target))
    declare_binary_library(target, "**/*.c")

    write_makefile
  end

  # A function to ease the task of producing several libraries from the
  # same source tree. It sets up a library context for one directory,
  # adding it to the include path, and taking care of the files in:
  # +lib/+:: the ruby library files;
  # +include/+:: the include files
  #
  # +dir+ is the directory in the source, +target_dir+ the base directory
  # for target files. It is pretty rudimentary, but should do the job
  # for many cases (and especially Tioga ;-) !). +name+ is the name
  # of the binary library. If nil, doesn't try to build a binary library.
  # +include_dir+ is the target name for includes. If +nil+, doesn't
  # try to install them.
  #
  # You can use either the return value or a block to change somehow the
  # behavior of the different objects produced (like adding a binary library
  # for instance).
  
  def setup_dir(dir, target_dir, bin_name = nil,
                include_dir = nil)
    add_include_path(dir, File.join(dir, 'include'))

    lib = declare_library(target_dir, "#{dir}/lib/**/*.rb")
    binlib = declare_binary_library(bin_name, 
                                    "#{dir}/**/*.c") if ! bin_name.nil?
    include = declare_includes(include_dir, 
                     "#{dir}/include/**/*.h") if ! include_dir.nil?
    yield lib, binlib, include if block_given?
    [lib, binlib, include]
  end

  # the module variable that will hold the command-line arguments:
  # even with a similar name, the contents of this variable will
  # be quite different from the old $configure_args
  @@configure_args = {}

  # Parses command-line arguments, wiping the ones that we understood.
  def parse_cmdline
    $*.delete_if do |arg|
      if arg =~ /^--(.*)/ # does look like a command-line argument...
        case $1
        when "local" # installation to sitedir
          Mkmf2.set_model("local")
          true
        when "dist"
          Mkmf2.set_model("dist")
          true
        when "home"
          Mkmf2.set_model("home")
          true
        when /^--with(.*)/ # this is my understanding of the
          # with-config stuff. It is a complete rewrite, I don't like
          # much the old code
          rhs = $1 # right-hand side
          case rhs
          when /-([\w-]+)=(.*)/
            @@configure_args[$1] = $2
          when /out-([\w-]+)$/
            @@configure_args[$1] = false
          when /-([\w-]+)$/
            @@configure_args[$1] = true
          else
            raise "Argument #{arg} not understood"
          end
        else
          false
        end
      else
        false
      end
    end
  end

  # This function is a try at reproducing the functionnality of the
  # old mkmf.rb's dir_config. It is not a copy, and might not work
  # well in all situations. Basically, if --with-target-dir has been
  # specified, use dir/include dir/lib. If --with-target-(include|lib)
  # have been specified, use them !
  def dir_config(target)
    ldir = nil
    idir = nil
    if @@configure_args.key?("#{target}-lib") 
      ldir = @@configure_args["#{target}-lib"]
    elsif @@configure_args.key?("#{target}-dir") 
      ldir = File::join(@@configure_args["#{target}-dir"], "lib")
    end
    if ldir
      add_library_path(ldir.split(File::PATH_SEPARATOR))
    end

    if @@configure_args.key?("#{target}-include") 
      idir = @@configure_args["#{target}-include"]
    elsif @@configure_args.key?("#{target}-dir") 
      idir = File::join(@@configure_args["#{target}-dir"], "include")
    end
    if idir
      add_include_path(idir.split(File::PATH_SEPARATOR))
    end
    return [idir, ldir]
  end

  ###########################################################################
  # This module is copied verbatim from the old mkmf.rb code. It comes dead #
  # useful for logging things. I hope it will not cause licence conflicts.  #
  ###########################################################################

  module Logging
    @log = nil
    @logfile = 'mkmf2.log'
    @orgerr = $stderr.dup
    @orgout = $stdout.dup
    @postpone = 0
    
    def self::open
      @log ||= File::open(@logfile, 'w')
      @log.sync = true
      $stderr.reopen(@log)
    $stdout.reopen(@log)
      yield
    ensure
      $stderr.reopen(@orgerr)
      $stdout.reopen(@orgout)
    end
    
    def self::message(*s)
      @log ||= File::open(@logfile, 'w')
      @log.sync = true
      @log.printf(*s)
    end
    
    def self::logfile file
      @logfile = file
      if @log and not @log.closed?
        @log.flush
        @log.close
        @log = nil
      end
    end
    
    def self::postpone
      tmplog = "mkmftmp#{@postpone += 1}.log"
      open do
        log, *save = @log, @logfile, @orgout, @orgerr
        @log, @logfile, @orgout, @orgerr = nil, tmplog, log, log
        begin
          log.print(open {yield})
          @log.close
          File::open(tmplog) {|t| FileUtils.copy_stream(t, log)}
        ensure
          @log, @logfile, @orgout, @orgerr = log, *save
          @postpone -= 1
          rm_f tmplog
        end
      end
    end
  end

  # Also from old mkmf.rb
  CONFTEST_C = "conftest.c"

  def xsystem(command)
    Logging::open do
      puts command.quote
      system(command)
    end
  end
  
  # Also from old mkmf.rb
  def log_src(src)
    Logging::message <<"EOM", src
checked program was:
/* begin */
%s/* end */

EOM
  end
  
  # Also from old mkmf.rb
  def create_tmpsrc(src)
    src = yield(src) if block_given?
    src = src.sub(/[^\n]\z/, "\\&\n")
    open(CONFTEST_C, "wb") do |cfile|
      cfile.print src
    end
    src
  end
  
  # Also from old mkmf.rb
  def try_do(src, command, &b)
    src = create_tmpsrc(src, &b)
    xsystem(command)
  ensure
    log_src(src)
  end

  # Also from old mkmf.rb
  def checking_for(m, fmt = nil)
    f = caller[0][/in `(.*)'$/, 1] and f << ": " #` for vim
    m = "checking for #{m}... "
    print m
    a = r = nil
    Logging::postpone do
      r = yield
      a = (fmt ? fmt % r : r ? "yes" : "no") << "\n"
      "#{f}#{m}-------------------- #{a}\n"
    end
    puts a
    $stdout.flush
    Logging::message "--------------------\n\n"
    r
  end

  # also taken straight from mkmf.rb
  def rm_f(*files)
    FileUtils.rm_f(Dir[files.join("\0")])
  end

  # A small wrapper around Config::expand which diminishes the size of the code
  # and makes sure the MAKEFILE_CONFIG hash is updated.
  def expand_vars(str)
    update_makefile_config
    string = Config::expand(str,MAKEFILE_CONFIG)
    # then, we need to turn all the remaining $(THING) into $THING, so that
    # the shell doesn't spawn subshells ? 
    return string.gsub(/\$\((\w+)\)/) { "$#$1" }
  end

  # Add defines to the build
  def add_define(d)
        MAKEFILE_CONFIG["DEFINES"] += 
          " -D_#{d}"
  end

  # This is a compatibility function with the previous mkmf.rb. It does check
  # for the presence of a header in the current include directories. If found,
  # it returns true and sets the define HAVE_...
  def have_header(header, &b)
    checking_for header do
      if try_do("#include <#{header}>",
                expand_vars("$(CPP) $(INCLUDEDIRS) " \
                            "$(CPPFLAGS) $(CFLAGS) $(DEFINES) "\
                            "#{CONFTEST_C} $(CPPOUTFILE)"),&b)
        add_define("HAVE_#{header.sanitize}")
        true
      else
        false
      end
    end
  ensure
    rm_f("conftest*")
  end

  # The same as have_header, but fails if the header is not found...
  def require_header(header, message = nil ,&b)
    if ! have_header(header,&b)
      if message
        puts message
      end
      raise "Header #{header} not found, stopping\n"
    end
  end

  def mkmf2_init
    check_missing_features
    parse_cmdline
    setup_model
    config_to_global
  end

  def headers(header)
    headers = ""
    if header
      if header.is_a?(Array) 
        header.each {|h|
          headers += "#include <#{h.to_s}>\n"
        }
      else
        headers += "#include <#{header.to_s}>\n"
      end
    end
    return headers
  end

  # Tries to link the given code with the extra flags given
  def try_link(code, extras = "")
    return try_do(code,
                expand_vars("$(CC) $(OUTFLAG)conftest $(INCFLAGS) " +
                            "#{CONFTEST_C} " + 
                            " -I$(hdrdir) $(CPPFLAGS) $(CFLAGS) $(src)" + 
                            " $(LIBPATH) $(LDFLAGS) $(ARCH_FLAG) " +
                            " $(LOCAL_LIBS) $(LIBS) $(LIBS_SUP) #{extras}"
                            )
                )
  ensure
    rm_f("conftest*")
  end

  def try_func(func, extra, h)
    headers = headers(h)

    try_link(<<"SRC", extra) or try_link(<<"SRC", extra)
#{headers}
/*top*/
int main() { return 0; }
int t() { #{func}(); return 0; }
SRC
#{headers}
/*top*/
int main() { return 0; }
int t() { void ((*volatile p)()); p = (void ((*)()))#{func}; return 0; }
SRC
end


  # Compatibility function from mkmf.rb. Checks if the compiler
  # can find the given function in the given library. If the function
  # is not given, we look for main but it's definitely not a good idea.
  # +header+ is a header that can be included to get the prototype for
  # this function. It can possibly be an array, in which case it is
  # interpreted as a list of headers that should be included.

  def have_library(lib, func = nil, header=nil, &b)
    if func.nil?
      func = "main"
    end
    libarg = "#{MAKEFILE_CONFIG["LIBARG"]%lib}"
    checking_for "#{func}() in #{libarg}" do
      if try_func(func, libarg, header)
        add_define("HAVE_#{lib.sanitize}")
        MAKEFILE_CONFIG["LIBS_SUP"] += " #{libarg}" 
        true
      else
        false
      end
    end
  end
  
  # Returns true if a function could be found
  def have_func(func, header = nil)
    checking_for "#{func}() " do
      if try_func(func, "", header)
        add_define("HAVE_#{func.sanitize}")
        true
      else
        false
      end
    end
  end
end

include Mkmf2
mkmf2_init
