Initial commit. master
authorTilman Sauerbeck <tilman@code-monkey.de>
Mon, 30 Apr 2007 10:46:37 +0000 (12:46 +0200)
committerTilman Sauerbeck <tilman@code-monkey.de>
Mon, 30 Apr 2007 10:46:37 +0000 (12:46 +0200)
.gitignore [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
README [new file with mode: 0644]
Rakefile [new file with mode: 0644]
ext/ext.c [new file with mode: 0644]
lib/discid.rb [new file with mode: 0644]
rake/configuretask.rb [new file with mode: 0644]
rake/extensiontask.rb [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..36f73a9
--- /dev/null
@@ -0,0 +1,6 @@
+ChangeLog
+doc
+pkg
+*.so
+*.eet
+.configure_state.yaml
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..5864b3d
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Tilman Sauerbeck (tilman at code-monkey de)
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..45cdf09
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,20 @@
+Copyright (c) 2007 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.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..447cfad
--- /dev/null
+++ b/README
@@ -0,0 +1,43 @@
+= ruby-discid
+
+ruby-discid is a set of Ruby bindings to MusicBrainz' libdiscid.
+
+ruby-discid is maintained by:
+
+:include: AUTHORS
+
+== License
+
+ruby-discid is available under an MIT-style license.
+
+:include: COPYING
+
+== Download
+
+The latest version of ruby-discid can be found at
+http://code-monkey.de/pages/ruby-discid
+
+Online documentation is available at
+http://docs.code-monkey.de/ruby-discid
+
+== Dependencies
+
+ruby-discid depends on Rake[http://rake.rubyforge.org] 0.5.0
+or greater and libdiscid[http://musicbrainz.org/doc/libdiscid].
+
+== Installation
+
+Run "rake install" to install ruby-discid.
+
+== Usage
+
+Have a look at this example:
+
+  require "discid"
+
+  discid = DiscID::DiscID.read("/dev/hdc")
+
+  puts discid.id
+  puts discid.submission_url
+
+That's basically all you need.
diff --git a/Rakefile b/Rakefile
new file mode 100644 (file)
index 0000000..e3a90d0
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,109 @@
+require "rake/clean"
+require "rake/testtask"
+require "spec/rake/spectask"
+require "rake/rdoctask"
+require "rake/packagetask"
+require "rake/contrib/compositepublisher"
+require "rake/contrib/sshpublisher"
+
+require "rake/configuretask"
+require "rake/extensiontask"
+
+PKG_NAME = "ruby-discid"
+PKG_VERSION = File.read("lib/discid.rb").
+              match(/^\s*VERSION = \"(.*)\"$/).captures.first
+PKG_FILES = FileList[
+       "AUTHORS", "COPYING", "README", "Rakefile",
+       "rake/configuretask.rb", "rake/extensiontask.rb",
+       "ext/ext.c", "lib/discid.rb"
+]
+
+ext_objs = [:ext]
+
+task :default => [:ext]
+task :extension => [:install, :clobber]
+
+config = Rake::ConfigureTask.new do |t|
+       t.tests << Rake::ConfigureTask::
+                  PkgConfigTest.new("libdiscid", :is_critical => true)
+end
+
+task :ext => [:pre_ext]
+
+ext = Rake::ExtensionTask.new :ext => ext_objs do |t|
+       t.dir = "ext"
+       t.lib_name = "#{t.dir}/discid_ext.so"
+end
+
+task :pre_ext => [:configure] do
+       ext.link_libs << config.libdiscid.libs
+
+       cflags = [
+               ext.env[:cflags],
+               config.libdiscid.cflags
+       ]
+
+       ext.env.update(:cflags => cflags)
+end
+
+task :install => [:ext] do |t|
+       destdir = ENV["DESTDIR"] || ""
+
+       tmp = ENV["RUBYARCHDIR"] || Config::CONFIG["sitearchdir"]
+       ddir = File.join(destdir, tmp)
+
+       unless File.expand_path(ext.lib_name) ==
+              File.join(ddir, File.basename(ext.lib_name))
+               FileUtils::Verbose.mkdir_p(ddir) unless File.directory?(ddir)
+               FileUtils::Verbose.install(ext.lib_name, ddir, :mode => 0755)
+       end
+
+       tmp = ENV["RUBYLIBDIR"] || Config::CONFIG["sitelibdir"]
+       ddir = File.join(destdir, tmp)
+
+       PKG_FILES.each do |file|
+               next unless file.pathmap("%1d") == "lib"
+
+               dest_file = file.pathmap("%{^lib,#{ddir}}p")
+
+               next if File.expand_path(file) == dest_file
+
+               FileUtils::Verbose.mkdir_p(File.dirname(dest_file))
+               FileUtils::Verbose.install(file, dest_file, :mode => 0644)
+       end
+end
+
+task :test => [:ext]
+
+test = Rake::TestTask.new do |t|
+       t.libs = ["lib", "ext"]
+       t.warning = true
+end
+
+rdoc = Rake::RDocTask.new do |t|
+       t.rdoc_dir = "doc"
+       t.title = PKG_NAME
+       t.options = ["--line-numbers", "--inline-source", "--main", "README"]
+       t.rdoc_files = FileList[
+               "README", "COPYING", "AUTHORS",
+               "ext/ext.c", "lib/discid.rb"
+       ]
+end
+
+Rake::PackageTask.new(PKG_NAME, PKG_VERSION) do |t|
+       t.need_tar_gz = true
+       t.package_files = PKG_FILES
+end
+
+task :publish => [:rdoc, :package] do
+       p = Rake::CompositePublisher.new
+       p.add(Rake::SshFreshDirPublisher.new("code-monkey.de",
+                                            "public_docs/" +
+                                            PKG_NAME, "doc"))
+
+       p.add(Rake::SshFilePublisher.new("code-monkey.de",
+                                        ".", "pkg",
+                                        "#{PKG_NAME}-#{PKG_VERSION}.tar.gz"))
+       p.upload
+end
+
diff --git a/ext/ext.c b/ext/ext.c
new file mode 100644 (file)
index 0000000..9e538a0
--- /dev/null
+++ b/ext/ext.c
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2007 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.
+ */
+
+#include <ruby.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <assert.h>
+
+#include <discid/discid.h>
+
+static VALUE eReadError;
+
+typedef struct {
+       DiscId *real;
+} RbDiscID;
+
+static void
+c_free (RbDiscID *o)
+{
+       discid_free (o->real);
+
+       ruby_xfree (o);
+}
+
+static VALUE
+c_alloc (VALUE klass)
+{
+       RbDiscID *o;
+
+       return Data_Make_Struct (klass, RbDiscID, NULL, c_free, o);
+}
+
+/*
+ * call-seq:
+ *  DiscID.default_device -> string
+ *
+ * Returns the default device that's used in DiscID::DiscID.read if
+ * the device isn't specified explicitly.
+ */
+static VALUE
+m_default_device (VALUE self)
+{
+       return rb_str_new2 (discid_get_default_device ());
+}
+
+/*
+ * call-seq:
+ *  DiscID::DiscID.read([device]) -> object
+ *
+ * Read the disc id from the medium in the given device.
+ * If the argument is ommitted, DiscID.default_device is used instead.
+ */
+static VALUE
+c_read (int argc, VALUE *argv, VALUE klass)
+{
+       VALUE self, device;
+       RbDiscID *o;
+       char *cdev = NULL;
+       int s;
+
+       rb_scan_args (argc, argv, "01", &device);
+
+       if (!NIL_P (device))
+               cdev = StringValuePtr (device);
+
+       self = rb_class_new_instance (0, NULL, klass);
+
+       Data_Get_Struct (self, RbDiscID, o);
+
+       s = discid_read (o->real, cdev);
+       if (!s)
+               rb_raise (eReadError, discid_get_error_msg (o->real));
+
+       return self;
+}
+
+static VALUE
+c_init (VALUE self)
+{
+       RbDiscID *o;
+
+       Data_Get_Struct (self, RbDiscID, o);
+
+       o->real = discid_new ();
+
+       return self;
+}
+
+/*
+ * call-seq:
+ *  discid.id -> string
+ *
+ * Returns the MusicBrainz disc id.
+ */
+static VALUE
+c_id (VALUE self)
+{
+       RbDiscID *o;
+       char *s;
+
+       Data_Get_Struct (self, RbDiscID, o);
+
+       s = discid_get_id (o->real);
+
+       return rb_str_new2 (s);
+}
+
+/*
+ * call-seq:
+ *  discid.submission_url -> string
+ *
+ * Returns the submission url for the disc id.
+ */
+static VALUE
+c_submission_url (VALUE self)
+{
+       RbDiscID *o;
+       char *s;
+
+       Data_Get_Struct (self, RbDiscID, o);
+
+       s = discid_get_submission_url (o->real);
+
+       return rb_str_new2 (s);
+}
+
+/*
+ * call-seq:
+ *  discid.freedb_id -> string
+ *
+ * Returns the FreeDB disc id.
+ */
+static VALUE
+c_freedb_id (VALUE self)
+{
+       RbDiscID *o;
+       char *s;
+
+       Data_Get_Struct (self, RbDiscID, o);
+
+       s = discid_get_freedb_id (o->real);
+
+       return rb_str_new2 (s);
+}
+
+void
+Init_discid_ext (void)
+{
+       VALUE m, c;
+       VALUE eDiscIDError;
+
+       m = rb_define_module ("DiscID");
+       c = rb_define_class_under (m, "DiscID", rb_cObject);
+
+       rb_define_alloc_func (c, c_alloc);
+
+       rb_define_module_function (m, "default_device", m_default_device, 0);
+       rb_define_singleton_method (c, "read", c_read, -1);
+       rb_define_method (c, "initialize", c_init, 0);
+       rb_define_method (c, "id", c_id, 0);
+       rb_define_method (c, "submission_url", c_submission_url, 0);
+       rb_define_method (c, "freedb_id", c_freedb_id, 0);
+
+       eDiscIDError = rb_define_class_under (m, "DiscIDError", rb_eStandardError);
+       eReadError = rb_define_class_under (m, "ReadError", eDiscIDError);
+}
diff --git a/lib/discid.rb b/lib/discid.rb
new file mode 100644 (file)
index 0000000..292e3d4
--- /dev/null
@@ -0,0 +1,27 @@
+#--
+# Copyright (c) 2007 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 "discid_ext"
+
+module DiscID
+       VERSION = "0.1.0"
+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..baff54b
--- /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},
+                '-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.
+      @@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