After banging my head against mkmf for a few days, I decided it was time to
get Rake to build my extensions.  I think I've gotten things working well
enough[0] to allow it to escape.

Attached are two tasks -- ExtensionTask for linking together Ruby extension
libraries, and SWIGExtensionTask for generating libraries from SWIG Interface
files -- plus rules for compiling C and C++ files.  It uses rbconfig to
determine the appropriate compiler/linker options.

First and foremost, I welcome feedback on my efforts.  I'm not sure if it's
completely Rakish (nor, for that matter, completely Rubyish) so wanted input
from any gurus.  Assuming it's at least satisfactory, I'd like to get these
into the Rake contribs directory (so I don't have to include it in my project).

Share and enjoy ...

[0] Using it as build system for RDBXML (http://rdbxml.rubyforge.org)

-- Steve
require 'rake'
require 'rake/clean'
require 'rake/tasklib'

module Rake

  # Create a build task that will generate a Ruby extension (e.g. .so) from one 
or more
  # C (.c) or C++ (.cc, .cpp, .cxx) files, and is intended as a replcaement for 
mkmf.
  # It determines platform-specific settings (e.g. file extensions, compiler 
flags, etc.)
  # from rbconfig (note: examples assume *nix file extensions).
  #
  # *Note*: Strings vs Symbols
  # In places where filenames are expected (i.e. lib_name and objs), +String+s 
are used
  # as verbatim filenames, while, +Symbol+s have the platform-dependant 
extension
  # appended (e.g. '.so' for libraries and '.o' for objects).
  #
  # Example:
  #   desc "build sample extension"
  #   # build sample.so (from foo.{c,cc,cxx,cpp}, through foo.o)
  #   Rake::ExtensionTask.new :sample => :foo do |t|
  #     # all extension files under this directory
  #     t.dir = 'ext'
  #     # don't include, but rebuild library if it changes
  #     t.deps << 'config.h'
  #     # link libraries (libbar.so)
  #     t.link_libs << 'bar'
  #   end
  #
  # Author::    Steve Sloan (mailto:[EMAIL PROTECTED])
  # Copyright:: Copyright (c) 2006 Steve Sloan
  # License::   GPL

  class ExtensionTask < Rake::TaskLib
    # The name of the extension
    attr_accessor :name

    # The filename of the extension library file (e.g. 'extension.so')
    attr_accessor :lib_name

    # Object files to build and link into the extension.
    attr_accessor :objs

    # Depency files that aren't linked into the library, but cause it to be
    # rebuilt when they change.
    attr_accessor :deps

    # The directory where the extension files (source, output, and
    # intermediate) are stored.
    attr_accessor :dir

    # Environment configuration -- i.e. CONFIG from rbconfig, with a few other
    # settings, and converted to lowercase-symbols.
    attr_accessor :env

    # Additional link libraries
    attr_accessor :link_libs


    # List of paths to object files to build
    def output_objs
      @objs.collect do |o|
        f = (Symbol === o) ? "#{o}.#{env[:objext]}" : o
        File.join( dir, f )
      end
    end

    # Path to the output library file
    def output_lib
      File.join( dir, lib_name )
    end

    # Same arguments as Rake::define_task
    def initialize( args, &blk )
      @env = @@DefaultEnv.dup
      @name, @objs = resolve_args(args)
      set_defaults
      yield self  if block_given?
      define_tasks
    end

    # Generate default values.  This is called from initialize _before_ the
    # yield block.
    #
    # Defaults:
    # - lib_name: name.so
    # - objs: name.o
    # - dir: .
    def set_defaults
      @lib_name = (Symbol === name) ? "#{name}.#{env[:dlext]}" : name
      @objs = [name.to_sym]
      @dir = '.'
      @deps, @link_libs = [], []
    end

    # Defines the library task.
    def define_tasks
      task name => (deps.collect { |d| File.join( dir, d ) } << output_lib)  do 
end

      file output_lib => output_objs  do |t|
        sh_cmd :ldshared, {'-L' => :libdirs}, '-o', t.name, t.prerequisites,
                          {'-l' => link_libs}, :libs, :dlibs
      end

      CLEAN.include output_objs
      CLOBBER.include output_lib
      define_rules
    end

    # Defines C and C++ source-to-object rules, using the source extensions 
from env.
    def define_rules
      for ext in env[:c_exts]
        Rake::Task.create_rule '.'+env[:objext] => '.'+ext do |r|
          sh_cmd :cc, :cflags, :cppflags, {'-D' => :defines}, {'-I' => 
:includedirs}, {'-I' => :topdir},
                '-c', '-o', r.name, r.sources
        end
      end

      for ext in env[:cpp_exts]
        Rake::Task.create_rule '.'+env[:objext] => '.'+ext do |r|
          sh_cmd :cxx, :cxxflags, :cppflags, {'-D' => :defines}, {'-I' => 
:includedirs}, {'-I' => :topdir},
                '-o', r.name, '-c', r.sources
        end
      end
    end

    class << self
      # The default environment for all extensions.
      def env
        @@DefaultEnv
      end

      @@DefaultEnv = {
        :cxx => ENV['CXX'] || 'c++',
        :cxxflags => ENV['CXXFLAGS'] || '',

        :c_exts => ['c'],
        :cpp_exts => ['cc', 'cxx', 'cpp'],
        :swig => 'swig',
        :swig_flags => ['-ruby', '-c++'],
        :swig_includedirs => ['.'],

        :includedirs => [], #['/usr/local/include'],
        :libdirs => [], #['/usr/local/lib'],
      }
      Config::CONFIG.each { |k, v| @@DefaultEnv[k.downcase.to_sym] = v }
    end

  protected

    # Convenience function for cnstructing command lines for build tools.
    def optify( *opts )
      return optify(*opts.first)  if opts.size == 1 and opts.first.kind_of? 
Array
      opts.collect do |opt|
        case opt
          when String then  opt
          when Symbol then  optify env[opt]
          when Hash
            opt.collect do |k, v|
              v = env[v]  if v.kind_of? Symbol
              if v.kind_of? Array
                optify v.collect { |w| k.to_s + w.to_s }
              elsif v
                k.to_s + v.to_s
              end
            end
          else
            opt.to_s
        end
      end.join(' ')
    end

    def sh_cmd( cmd, *opts )
      sh optify( cmd, *opts )
    end

    # For some reason, Rake::TaskManager.resolve_args can't be found, so snarf 
it.
    def resolve_args(args)
      case args
      when Hash
        fail "Too Many Task Names: #{args.keys.join(' ')}" if args.size > 1
        fail "No Task Name Given" if args.size < 1
        task_name = args.keys[0]
        deps = args[task_name]
        deps = [deps] if (String===deps) || (Regexp===deps) || (Proc===deps)
      else
        task_name = args
        deps = []
      end
      [task_name, deps]
    end

  end

end
require 'rake'
require 'rake/extensiontask'

module Rake

  # Create a build task that will generate a Ruby wrapper extension from
  # a SWIG interface definition.  Requires SWIG[http://www.swig.org] version 
1.3.x.
  #
  # See ExtensionTask for more information.
  #
  # Example (from RDBXML):
  #   # dbxml.i -> dbxml_wrap.cc -> dbxml_wrap.o -> dbxml.so
  #   Rake::SWIGExtensionTask.new :dbxml do |t|
  #     # keep it all under ext/
  #     t.dir = 'ext'
  #     # dbxml.i includes dbxml_ruby.i -- rebuild if it changes
  #     t.deps << 'dbxml_ruby.i'
  #     # link in dbxml libraries
  #     t.link_libs += ['db', 'db_cxx', 'dbxml', 'xquery', 'xerces-c', 'pathan']
  #   end
  #
  # Author::    Steve Sloan (mailto:[EMAIL PROTECTED])
  # Copyright:: Copyright (c) 2006 Steve Sloan
  # License::   GPL
  class SWIGExtensionTask < ExtensionTask
    # Defaults:
    # - lib_name: name.so
    # - objs: name_wrap.o
    # - deps: name.i
    # - dir: .
    def set_defaults
      super
      @lib_name = (Symbol === name) ? "#{name}.#{env[:dlext]}" : name
      @objs = ["#{name}_wrap".to_sym]
      @deps = ["#{name}.i"]
    end

    # Add rule for generating C++ wrapper code (_wrap.cc) from SWIG interface 
definition (.i).
    def define_rules
      verify_swig_version
      Rake::Task.create_rule( '_wrap.cc' => [proc {|t| t.gsub /_wrap\.cc$/, 
'.i' }] )  do |r|
        sh_cmd :swig, :swig_flags, {'-I' => :swig_includedirs}, {'-I' => 
:includedirs},
                '-o', r.name, r.sources
      end
      super
    end

  protected

    # Raise an exception unless we have SWIG version 1.3 or later.
    def verify_swig_version
      @@swig_version ||= IO.popen "#{env[:swig]} -version 2>&1" do |swig|
        banner = swig.readlines.reject { |l| l.strip.empty? }
        banner[0].match(/SWIG Version ([^ ]+)/i)[1]
      end
      unless @@swig_version >= '1.3'
        raise "Need SWIG version 1.3 or later (have #{@@swig_version[0]})"
      end
    end
  end

end
_______________________________________________
Rake-devel mailing list
[email protected]
http://rubyforge.org/mailman/listinfo/rake-devel

Reply via email to