From 6c5226c06abbd23838d0fcea14374b1cace684df Mon Sep 17 00:00:00 2001 From: Tilman Sauerbeck Date: Mon, 30 Apr 2007 12:46:37 +0200 Subject: [PATCH 1/1] Initial commit. --- .gitignore | 6 + AUTHORS | 1 + COPYING | 20 +++ README | 43 ++++++ Rakefile | 109 ++++++++++++++++ ext/ext.c | 187 ++++++++++++++++++++++++++ lib/discid.rb | 27 ++++ rake/configuretask.rb | 296 ++++++++++++++++++++++++++++++++++++++++++ rake/extensiontask.rb | 189 +++++++++++++++++++++++++++ 9 files changed, 878 insertions(+) create mode 100644 .gitignore create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 README create mode 100644 Rakefile create mode 100644 ext/ext.c create mode 100644 lib/discid.rb create mode 100644 rake/configuretask.rb create mode 100644 rake/extensiontask.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36f73a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +ChangeLog +doc +pkg +*.so +*.eet +.configure_state.yaml diff --git a/AUTHORS b/AUTHORS new file mode 100644 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 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 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 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 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 +#include +#include +#include +#include + +#include + +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 index 0000000..292e3d4 --- /dev/null +++ b/lib/discid.rb @@ -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 index 0000000..b673c84 --- /dev/null +++ b/rake/configuretask.rb @@ -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 =< :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: + 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 -- 2.30.2