Use a configure task and an extension task to build ruby-edje.
authorTilman Sauerbeck <tilman@code-monkey.de>
Thu, 5 Apr 2007 19:59:23 +0000 (21:59 +0200)
committerTilman Sauerbeck <tilman@code-monkey.de>
Thu, 5 Apr 2007 19:59:23 +0000 (21:59 +0200)
Note that I had to edit extensiontask.rb to include sitearchdir
so that the ruby-evas headers would be picked up.

README
Rakefile
library.rb [deleted file]
rake/configuretask.rb [new file with mode: 0644]
rake/extensiontask.rb [new file with mode: 0644]

diff --git a/README b/README
index 010b1ac..5678654 100644 (file)
--- a/README
+++ b/README
@@ -14,10 +14,6 @@ ruby-edje is available under the GNU LGPL.
 == Installation
 
 Run "rake install" to install ruby-edje.
-If you didn't install Edje to /usr/local, execute the following line
-instead:
-
-EDJE_PREFIX=/prefix/edje/is/installed/in rake install
 
 == Documentation
 
index 9048ee3..a8313fb 100644 (file)
--- a/Rakefile
+++ b/Rakefile
@@ -1,4 +1,3 @@
-require "library"
 require "rake/clean"
 require "rake/testtask"
 require "rake/rdoctask"
@@ -6,34 +5,45 @@ require "rake/packagetask"
 require "rake/contrib/compositepublisher"
 require "rake/contrib/sshpublisher"
 
+require "rake/configuretask"
+require "rake/extensiontask"
+
 PKG_NAME = "ruby-edje"
 
-CLOBBER.include("src/*.{s,}o")
+ext_objs = [:rb_edje_main, :rb_edje, :rb_part]
+
+task :default => [:ext]
 
-ext_obj = Dir["src/*.c"].map { |f| f.sub(/\.[^.]+$/, ".o") }
-ext_lib = Library.new("edje", ENV["EDJE_PREFIX"] || "/usr/local", "src")
+config = Rake::ConfigureTask.new do |t|
+       t.tests << Rake::ConfigureTask::
+                  PkgConfigTest.new("edje", :is_critical => true)
+end
 
-task :default => [ext_lib.so]
+task :ext => [:pre_ext]
 
-file ext_lib.so => ext_lib.objects do |t|
-       sh "cc #{ext_lib.ldflags} -shared -Wl " +
-           " #{t.prerequisites.join(" ")} -o #{t.name}"
+ext = Rake::ExtensionTask.new :ext => ext_objs do |t|
+       t.dir = "src"
+       t.lib_name = "#{t.dir}/edje.so"
 end
 
-ext_lib.objects.each do |object|
-       file object => object.sub(/\.[^.]+$/, ".c") do |t|
-               sh "cc #{ext_lib.cflags} #{t.prerequisites.first} " +
-                  "-c -o #{t.name}"
-       end
+task :pre_ext => [:configure] do
+       ext.link_libs << config.edje.libs
+
+       cflags = [
+               ext.env[:cflags],
+               config.edje.cflags
+       ]
+
+       ext.env.update(:cflags => cflags)
 end
 
-task :install => [ext_lib.so] do |t|
+task :install => [:ext] do |t|
        destdir = "#{ENV["DESTDIR"]}"
        sitearchdir = ENV["RUBYARCHDIR"] || Config::CONFIG['sitearchdir']
 
        ddir = destdir + sitearchdir
        FileUtils::Verbose.mkdir_p(ddir) unless File.directory?(ddir)
-       FileUtils::Verbose.install(ext_lib.so, ddir, :mode => 0755)
+       FileUtils::Verbose.install(ext.lib_name, ddir, :mode => 0755)
 end
 
 Rake::RDocTask.new do |t|
diff --git a/library.rb b/library.rb
deleted file mode 100644 (file)
index 21edae1..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-require "rbconfig"
-
-class Library
-       def initialize(name, prefix, dir, lib = name)
-               @name = name
-               @prefix = prefix
-               @dir = dir
-               @lib = lib
-       end
-
-       def ldflags
-               s = "#{ENV["LDFLAGS"]} -L #{@prefix}/lib -L " +
-                   Config::CONFIG["libdir"] + " " +
-                   Config::CONFIG["LIBRUBYARG_SHARED"] +
-                       " -shared -Wl"
-               s << " -l#{@lib}" unless @lib.nil?
-               s
-       end
-
-       def cflags
-               "#{ENV["CFLAGS"]} -I #{@prefix}/include " +
-               "-I #{Config::CONFIG['archdir']} " +
-               "-I #{Config::CONFIG['sitearchdir']} -fPIC"
-       end
-
-       def so
-               "#{@dir}/#{@name}.so"
-       end
-
-       def objects
-               Dir["#{@dir}/*.c"].map { |f| f.sub(/\.[^.]+$/, ".o") }
-       end
-end
diff --git a/rake/configuretask.rb b/rake/configuretask.rb
new file mode 100644 (file)
index 0000000..b673c84
--- /dev/null
@@ -0,0 +1,296 @@
+#
+# Copyright (c) 2005, 2006 Tilman Sauerbeck (tilman at code-monkey de)
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require "rake/tasklib"
+require "rake/clean"
+require "yaml"
+require "fileutils"
+
+module Rake
+       class ConfigureTask < TaskLib
+               CACHE_FILE = ".configure_state.yaml"
+
+               attr_reader :tests
+
+               def initialize # :yield: self
+                       @tests = TestList.new(load_tests || [])
+
+                       yield self if block_given?
+
+                       define
+               end
+
+               # returns the test with the specified name
+               def [](name)
+                       @tests.find { |t| t.name == name }
+               end
+
+               def method_missing(m)
+                       self[m.to_s]
+               end
+
+               private
+               def load_tests
+                       r = YAML.load(File.read(CACHE_FILE)) rescue nil
+
+                       r.is_a?(TestList) ? r : nil
+               end
+
+               def define
+                       desc "Remove configure results"
+                       task :clobber_configure do
+                               FileUtils::Verbose.rm_f(CACHE_FILE)
+                       end
+
+                       task :clobber => :clobber_configure
+
+                       desc "Configure this package"
+                       task :configure => [CACHE_FILE]
+
+                       file CACHE_FILE do
+                               @tests.each do |t|
+                                       t.on_checking.reverse_each { |b| b.call }
+
+                                       if t.invoke
+                                               t.on_success.reverse_each { |b| b.call }
+                                       else
+                                               t.on_failure.reverse_each { |b| b.call }
+                                       end
+                               end
+
+                               # store the test results in CACHE_FILE
+                               File.open(CACHE_FILE, "w") { |f| YAML.dump(@tests, f) }
+                       end
+               end
+
+               class TestList < Array
+                       def initialize(stored_tests)
+                               @stored_tests = stored_tests
+                       end
+
+                       def <<(arg)
+                               assign_result(arg)
+                               super
+                       end
+
+                       def push(*args)
+                               args.each { |a| assign_result(a) }
+                               super
+                       end
+
+                       def unshift(arg)
+                               assign_result(arg)
+                               super
+                       end
+
+                       private
+                       def assign_result(test)
+                               st = @stored_tests.find { |st| st.name == test.name }
+                               test.result = st.result unless st.nil?
+                       end
+               end
+
+               class Test
+                       attr_reader :name, :on_checking, :on_success, :on_failure
+                       attr_accessor :result
+
+                       def initialize(name, opts = {}) # :yield: self
+                               @name = name
+                               @opts = opts
+
+                               @result = nil
+                               @on_checking = []
+                               @on_success = []
+                               @on_failure = []
+
+                               if opts[:is_critical]
+                                       @on_failure << lambda { raise }
+                               end
+
+                               yield self if block_given?
+                       end
+
+                       def to_yaml_properties
+                               ["@name", "@result"]
+                       end
+
+                       def invoke
+                       end
+
+                       protected
+                       def can_exec_binary?(bin)
+                               fork do
+                                       STDOUT.reopen("/dev/null")
+                                       STDERR.reopen("/dev/null")
+
+                                       begin
+                                               exec(bin)
+                                       rescue SystemCallError
+                                               exit 255
+                                       end
+                               end
+
+                               Process.wait
+
+                               $?.exitstatus != 255
+                       end
+               end
+
+               class FooConfigTest < Test
+                       def initialize(name, opts = {})
+                               super
+
+                               @result = {}
+                               @command = "#{name}-config"
+
+                               @on_checking << lambda do
+                                       print "checking for #{name}... "
+                                       STDOUT.flush
+                               end
+
+                               @on_success << lambda { puts "yes (#{version})" }
+                               @on_failure << lambda { puts "no" }
+                       end
+
+                       def method_missing(m)
+                               @result[m]
+                       end
+
+                       def invoke
+                               return false unless can_exec_command?
+
+                               begin
+                                       [:version, :cflags, :libs].each do |f|
+                                               @result[f] = lookup_flags(f)
+                                       end
+                               rescue Exception
+                                       @result.clear
+                               end
+
+                               !@result.empty?
+                       end
+
+                       protected
+                       def lookup_flags(f)
+                               tmp = `#{@command} --#{f}`.strip
+
+                               raise unless $?.exitstatus.zero?
+                               tmp
+                       end
+
+                       private
+                       def can_exec_command?
+                               can_exec_binary?(@command)
+                       end
+               end
+
+               class PkgConfigTest < FooConfigTest
+                       def initialize(name, opts = {})
+                               super
+
+                               @command = "pkg-config"
+                       end
+
+                       protected
+                       def lookup_flags(f)
+                               f = :modversion if f == :version
+
+                               tmp = `#{@command} --silence-errors --#{f} #{@name}`.
+                                     strip.tr("\n", "/")
+
+                               raise unless $?.exitstatus.zero?
+                               tmp
+                       end
+               end
+
+               class CompileTest < Test
+                       TMP_FILE = ".compile_test"
+
+                       def CompileTest.cflags
+                               @@cflags
+                       end
+
+                       def CompileTest.cflags=(f)
+                               @@cflags = f
+                       end
+
+                       def initialize(name, code, opts = {})
+                               super(name, opts)
+
+                               @code = code
+                       end
+
+                       def invoke
+                               @result = false
+
+                               cc = ENV["CC"] || "cc"
+                               flags = (ENV["CFLAGS"] || "").dup
+                               flags << " -I" + Config::CONFIG["archdir"]
+
+                               unless @opts[:try_link]
+                                       flags << " -c"
+                               end
+
+                               File.open(TMP_FILE + ".c", "w") do |f|
+                                       f << @code << "\n"
+                               end
+
+                               `#{cc} #{flags} #{TMP_FILE}.c -o #{TMP_FILE}.o > /dev/null 2>&1`
+                               @result = $?.exitstatus.zero?
+                       ensure
+                               FileUtils.rm_f("#{TMP_FILE}.c")
+                               FileUtils.rm_f("#{TMP_FILE}.o")
+                       end
+               end
+
+               class HaveFuncTest < CompileTest
+                       def initialize(name, includes = [], opts = {})
+                               super(name, assemble_code(name, includes), opts)
+
+                               @on_checking << lambda do
+                                       print "checking for #{name}... "
+                                       STDOUT.flush
+                               end
+
+                               @on_success << lambda { puts "yes" }
+                               @on_failure << lambda { puts "no" }
+                       end
+
+                       private
+                       def assemble_code(func, includes)
+                               header = includes.inject("") do |a, h|
+                                       a << "#include <#{h}>\n"
+                               end
+
+                               body =<<EOF
+int main () {
+       void *foo = (void *) #{func};
+       foo = (void *) 0;
+       return 0;
+}
+EOF
+
+                               header + body
+                       end
+               end
+       end
+end
diff --git a/rake/extensiontask.rb b/rake/extensiontask.rb
new file mode 100644 (file)
index 0000000..ae3901c
--- /dev/null
@@ -0,0 +1,189 @@
+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 (e.g. lib_name and objs), Strings are used
+  # as verbatim filenames, while, Symbols have the platform-dependant extension
+  # appended (e.g. '.so' for libraries and '.o' for objects).  Also, only Symbols
+  # have #dir prepended to them.
+  #
+  # 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'
+  #     # link libraries (libbar.so)
+  #     t.link_libs << 'bar'
+  #   end
+  #
+  # Author::    Steve Sloan (mailto:steve@finagle.org)
+  # 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
+
+    # 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
+
+    # 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 (<- name.{c,cxx,cpp,cc})
+    # - dir: .
+    # - link_libs: <none>
+    def set_defaults
+      @lib_name ||= name.to_sym
+      @objs ||= [name.to_sym]
+      @dir ||= '.'
+      @link_libs ||= []
+    end
+
+    # Defines the library task.
+    def define_tasks
+      output_objs = @objs.collect { |obj| filepath obj, :objext }
+      output_lib = filepath lib_name, :dlext
+
+      task name => output_lib
+
+      file output_lib => output_objs do |t|
+        sh_cmd :ldshared, :dldflags, :ldflags,
+               {'-L' => :libdirs}, '-o', output_lib,
+               output_objs.join(' '),
+               link_libs.join(' '),
+               :libs, :dldlibs, :librubyarg_shared
+      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}, {'-I' => :sitearchdir},
+                '-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}, {'-I' => :sitearchdir},
+                '-o', r.name, '-c', r.sources
+        end
+      end
+    end
+
+    class << self
+      # The default environment for all extensions.
+      @@DefaultEnv = {}
+      def env
+        @@DefaultEnv
+      end
+      def env=(e)
+        @@DefaultEnv = e
+      end
+
+      Config::CONFIG.merge(ENV).each { |k, v| @@DefaultEnv[k.downcase.to_sym] = v }
+      @@DefaultEnv = {
+        :cxx => 'c++',
+        :cxxflags => '',
+        :c_exts => ['c'],
+        :cpp_exts => ['cc', 'cxx', 'cpp'],
+        :includedirs => [],
+        :libdirs => [],
+      }.update(@@DefaultEnv)
+    end
+
+  protected
+
+    # Handles convenience filenames:
+    # * f (String) => f
+    # * f (Symbol) => dir/f.ext
+    def filepath( f, ext )
+      ext = env[ext]  if Symbol === ext
+      Symbol === f ? File.join( dir, "#{f}.#{ext}" ) : f
+    end
+
+    # 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(' ').squeeze(' ')
+    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