From addc4fc500e382e5003a78b1040b905f09c2a739 Mon Sep 17 00:00:00 2001 From: Duncan Mac-Vicar P Date: Fri, 4 Jan 2008 22:59:37 +0100 Subject: [PATCH] Initial commit --- LICENSE | 1 + README | 17 +++ Rakefile | 35 ++++++ bin/rpmer.rb | 77 +++++++++++++ lib/rpmer.rb | 3 + lib/rpmer/cmds/import.rb | 159 ++++++++++++++++++++++++++ lib/rpmer/rpmbuild.rb | 103 +++++++++++++++++ lib/rpmer/rpmspec.rb | 197 +++++++++++++++++++++++++++++++++ lib/rpmer/rubygemtranslator.rb | 164 +++++++++++++++++++++++++++ lib/rpmer/util.rb | 9 ++ spec/rpmer/rpmspec_spec.rb | 14 +++ 11 files changed, 779 insertions(+) create mode 100644 LICENSE create mode 100644 README create mode 100644 Rakefile create mode 100644 bin/rpmer.rb create mode 100644 lib/rpmer.rb create mode 100644 lib/rpmer/cmds/import.rb create mode 100644 lib/rpmer/rpmbuild.rb create mode 100644 lib/rpmer/rpmspec.rb create mode 100644 lib/rpmer/rubygemtranslator.rb create mode 100644 lib/rpmer/util.rb create mode 100644 spec/rpmer/rpmspec_spec.rb diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f08f4ca --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +http://www.ruby-lang.org/en/LICENSE.txt \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..9e51a30 --- /dev/null +++ b/README @@ -0,0 +1,17 @@ + +Tool to generate spec files for the openSUSE distro. + +Q: only openSUSE? +A: for now I use those rpm conventions. + +Q: Why? +A: Because I don't like to type the same thing 1000 times? + +Q: Is it good to generate .spec files automatically? +A: Probably not. You can always review them. No excuse to avoid generating + the repetitive parts + +Q: It generates... from? +A: Right now gem files, based on gem2rpm code from Marek Gilbert + In the future: cpan, kde tarballs, java jars, etc. + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d40f6a6 --- /dev/null +++ b/Rakefile @@ -0,0 +1,35 @@ +require 'rubygems' +require 'rake/gempackagetask' + +if `ruby -Ilib ./bin/rpmer --version` =~ /\S+$/ + CURRENT_VERSION = $& +else + CURRENT_VERSION = "0.0.0" +end + +PKG_FILES = FileList[ + 'bin/**/*', + 'lib/**/*', + 'LICENSE', + 'README' + #SPEC_FILE +] + +spec = Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.summary = "rpm packaging tools" + s.name = 'rpmer' + s.version = '0.1' + s.requirements << 'cmdparse' + s.require_path = 'lib' + s.autorequire = 'rake' + s.files = PKG_FILES.to_a + s.description = < se +# log.info se.message +# rescue RuntimeError => e +# log.error e.message +# rescue Exception => e +# log.error e.message + end + +end + +if __FILE__ == $0 + main(ARGV) +end + + + diff --git a/lib/rpmer.rb b/lib/rpmer.rb new file mode 100644 index 0000000..bd5b0a9 --- /dev/null +++ b/lib/rpmer.rb @@ -0,0 +1,3 @@ +require 'rpmer/rpmbuild' +require 'rpmer/rpmspec' +require 'rpmer/rubygemtranslator' \ No newline at end of file diff --git a/lib/rpmer/cmds/import.rb b/lib/rpmer/cmds/import.rb new file mode 100644 index 0000000..bd585d4 --- /dev/null +++ b/lib/rpmer/cmds/import.rb @@ -0,0 +1,159 @@ + +require 'fileutils' +require 'optparse' +require 'ostruct' +require 'rpmer/rpmbuild' +require 'rpmer/rpmspec' +require 'rpmer/rubygemtranslator' + +# gem2rpm supports bootstrapping rubygems itself into an rpm. When doing +# this, Gem classes won't be available. +$gems_present = true +begin + require 'rubygems' + require 'rubygems/package' + + # XXX Brutal hack that's required to interrogate requirements in a + # gem. This allows us to get the requirements in their raw form, rather + # than re-parsing their textual form. + class Gem::Version::Requirement + attr_reader :requirements + end + +rescue LoadError + $gems_present = false +end + +#Development/Languages/Ruby + +# generate from a gem +class ImportGemCmd < CmdParse::Command + def initialize( logger ) + @log = logger + super('gem', false) + #puts options + + gemopts = OpenStruct.new() + gemopts.arch = nil + gemopts.group = "Development/Languages/Ruby" + gemopts.license = "REVIEW" + gemopts.output_dir = nil + gemopts.release = '1' + gemopts.rpm_build_opts = RpmBuildOption::BINARIES + gemopts.show_gem_spec = false + gemopts.show_rpm_spec = false + gemopts.verbose = false + gemopts.gems_prefix = 'rubygem' + + self.short_desc = "generate a spec file from ruby gems" + self.options = CmdParse::OptionParserWrapper.new do |op| + op.on('-G', '--show-gem', "show the gem spec file on stdout") do + gemopts.show_gem_spec = true + end + + op.on('-R', '--show-rpm', "show the RPM spec file on stdout") do + gemopts.show_rpm_spec = true + end + + @output_dir = Dir.pwd + op.on('-O', '--output-dir [DIR]', "output directory") do |d| + @output_dir = d + end + + @output_dir = Dir.pwd + op.on('-p', '--gems-prefix value', "rpm gems prefix (default: rubygem-foo.rpm") do |d| + gemopts.gems_prefix = d + end + end + + @gemopts = gemopts + end + + def execute (args) + #@log.info "create repo rpm-md #{args} #{super_command.input}" + + if args.empty? + @log.error "no input gem specified." and exit + end + + @input = args[0] + + if @output_dir.nil? + @log.error "Please specify a output directory" and exit + end + + if not ( File.exists?(@output_dir) and File.directory?(@output_dir) ) + @log.error "specified output directory #{@output_dir} is not valid" + exit + end + + src_file = @input + rpm = nil + build = RpmBuild.new() + case File.basename(src_file) + when /\.gem$/ + if !$gems_present + @log.error("Cannot process #{src_file} until rubygems is itself installed") + exit(1) + end + + trans = RubyGemTranslator.from_gem(src_file) + trans.translate_gem(@gemopts) + trans.add_external_requirements(@gemopts) + rpm = trans.rpm + + if @gemopts.show_gem_spec + @log.info("##### gem spec contents: #####") + $stderr.puts trans.gem.to_ruby + @log.info("##### end gem spec contents #####") + end + + when /^rubygems-[0-9\.]*\.tgz$/ + rpm = build_rubygems_spec(build, src_file, $options) + + else + @log.error("Don't know how to make an rpm from #{src_file}") + exit(1) + end + + # Create the spec file + @log.info("# creating #{rpm.spec_filename}") + File.open(rpm.spec_filename, 'w') do |fd| + rpm_out = RpmSpecWriter.new(fd) + rpm.write(rpm_out) + end + + if @show_rpm_spec + @log.info("##### rpm spec contents: #####") + File.open(rpm.spec_filename, 'r') do |fd| + fd.each_line do |line| + $stderr.puts(line) + end + end + ptrace("##### end rpm spec contents #####") + end + @log.info "done. wrote #{rpm.spec_filename}" + + end +end + +class ImportCmd < CmdParse::Command + attr_accessor :input + def initialize(logger) + @log = logger + super('import', true) + @input = Array.new + self.short_desc = "Generates .spec files" + self.options = CmdParse::OptionParserWrapper.new do |opt| + #opt.on( :REQUIRED, '-i', '--input x,y,z', Array, 'input object' ) do | inputopt | + # @input = inputopt + #end + opt + end + self.add_command(ImportGemCmd.new(@log)) + end + + def execute(args) + #@log.info "create #{args}" + end +end \ No newline at end of file diff --git a/lib/rpmer/rpmbuild.rb b/lib/rpmer/rpmbuild.rb new file mode 100644 index 0000000..0d3b2fd --- /dev/null +++ b/lib/rpmer/rpmbuild.rb @@ -0,0 +1,103 @@ +# This file can be distributed under the same terms as ruby itself. +# Author: Marek Gilbert + +# A simple wrapper around rpmbuild's --showrc command that allows rpm to +# tell gem2rpm where to install things. +class RpmRc + def initialize() + IO.popen('rpmbuild --showrc', 'r') do |fd| + read_rc(fd) + end + end + + # Read the rc file and define all the macros in it. + def read_rc(fd) + @macros = {} + fd.each_line() do |line| + if line =~ /^-14:\s+(\w+)\s+(\S+)/ + @macros[$1] = $2 + end + end + end + + # Lookup a macro by name. Use only the name part and leave out the + # leading '%{' and trailing '}'. If any value exists for the macro, it is + # evaluated such that the result will be macro-free. + def lookup(name) + val = @macros[name] + return evaluate(val) + end + + # Recursively examine the string and substitute any macros with their + # values. + def evaluate(value) + return nil if value.nil? +# value = value.gsub(/%\(([^\)]+)\)/) do |x| +# puts $1 +# `#{$1}` +# end + return value.gsub(/%\{([^}]+)\}/) { |x| lookup($1) } + end +end + +class RpmBuildOption + attr_reader :name, :bits, :flags + + def initialize(bits, name, flags) + @bits = bits + @name = name + @flags = flags + end + + def includes(other) + 0 != @bits & other.bits + end + + BINARIES = RpmBuildOption.new(1, 'binary', '-bb') + SOURCES = RpmBuildOption.new(2, 'sources', '-bs') + ALL = RpmBuildOption.new(1 + 2, 'all', '-ba') +end + +# Wrapper around the rpmbuild command. +class RpmBuild + attr_reader :rc + + def initialize() + @rc = RpmRc.new() + end + + def build_binary(rpm) + SysUtils.run("rpmbuild #{$options.rpm_build_opts.flags} " + + "#{rpm.spec_filename}") + end + + def build_dir + @rc.lookup('_builddir') + end + + def rpm_dir + @rc.lookup('_rpmdir') + end + + def src_rpm_dir + @rc.lookup('_srcrpmdir') + end + + def sources_dir + @rc.lookup('_sourcedir') + end + + def spec_dir + @rc.lookup('_specdir') + end + + def rpm_filename(rpm) + return File.join(rpm_dir, rpm.build_arch, + "#{rpm.name}-#{rpm.version}-#{rpm.release}.#{rpm.build_arch}.rpm") + end + + def srpm_filename(rpm) + return File.join(src_rpm_dir, + "#{rpm.name}-#{rpm.version}-#{rpm.release}.src.rpm") + end +end diff --git a/lib/rpmer/rpmspec.rb b/lib/rpmer/rpmspec.rb new file mode 100644 index 0000000..b73b55f --- /dev/null +++ b/lib/rpmer/rpmspec.rb @@ -0,0 +1,197 @@ +# This file can be distributed under the same terms as ruby itself. +# Author: Marek Gilbert + +# RpmSpecWriter has formatting primitives that simplify generating RPM spec +# files. +class RpmSpecWriter + def initialize(fd) + @fd = fd + end + + # Write out the contents of +line+ and a newline, just like + # +Kernel::puts+. + def puts(line = '') + @fd.puts(line) + end + + # Write out an RPM header. If the header is a multi-valued header then + # the value is joined with ", ". If the value is nil, nothing is written. + def header(name, value) + return if value.nil? + if value.kind_of?(Array) + return if value.length == 0 + + value = value.join(', ') + end + puts("#{name}: #{value}") + end + + # Declare a section and yield. + def section(name, rest = nil) + out = '%' + name + if !rest.nil? + out += ' ' + rest + end + + puts() + puts(out) + yield + end +end + +# A simple abstraction for a %name section in an rpm file. Every section +# has a %-sign, a name, possibly some text following the %name (e.g. +# %files -f %{name}.files), and a block of lines that follow. +class RpmSection + # The name of the section + attr_accessor :name + + # The rest of the line following the section declaration, if any. + attr_accessor :rest + + # The block of lines that follow the section declaration after a newline. + attr_accessor :text + + def initialize(name, rest = nil) + @name = name + @rest = rest + @text = '' + end + + # Add a line or lines to +text+. + def <<(line) + @text << "\n" if @text.length > 0 + @text << line + end +end + +# A generic interface to an RPM specification, consisiting of headers, +# sections, and a little bit of glue to connect to RpmSpecWriter. +class RpmSpec + @@headers = [] + @@sections = [] + + # RPM header names aren't suitable method names because Ruby wants at + # least the initial character of the method to be lower case. Generate + # good ruby names from the RPM names by converting from CamelCase to + # camel_case. + def self.sym_name(name) + case name + when 'URL' + return name.downcase + + else + sym = name.dup + sym.sub!(/^[A-Z]/) { |x| x.downcase } + sym.gsub!(/[A-Z]/) { |x| '_' + x.downcase } + return sym + end + end + + + # Declares an RPM header. +default_value+ can be lambda in which case a + # new value is taken from the result of calling the lambda with no + # arguments. + def self.rpm_header(name, default_value = nil) + get = sym_name(name) + set = get + '=' + + @@headers.push([name, get.intern, set.intern, default_value]) + attr_accessor(get.intern) + end + + # Declares an RPM header whose value consists of a list of things. + def self.rpm_list_header(name) + default_value = lambda do + Array.new() + end + rpm_header(name, default_value) + end + + # Declares an RPM section. + def self.rpm_section(name) + get = sym_name(name) + set = get + '=' + @@sections.push([name, get.intern, set.intern]) + attr_accessor(get.intern) + end + + attr_accessor :spec_filename + + rpm_header('Name') + rpm_header('Version') + rpm_header('Release') + rpm_header('Summary') + rpm_header('License') + rpm_header('Group') + rpm_header('URL') + + #rpm_header('AutoReqProv') + rpm_list_header('Conflicts') + rpm_list_header('Provides') + rpm_list_header('Requires') + rpm_list_header('Obsoletes') + + rpm_list_header('Prereq') + + rpm_header('BuildArch') + rpm_header('BuildRoot') + rpm_list_header('BuildConflicts') + rpm_list_header('BuildRequires') + + # The +sources+ array generates the Source0, Source1, etc headers. + attr_reader :sources + + # The +patches+ array generates the Patch0, Patch1, etc headers. + attr_reader :patches + + rpm_section('description') + rpm_section('prep') + rpm_section('build') + rpm_section('install') + rpm_section('clean') + rpm_section('files') + rpm_section('changelog') + + # Create a new RpmSpec that is intended to be written to the file + # at +filename+. + def initialize(filename) + @spec_filename = filename + + @@headers.each do |name, get, set, default_value| + if default_value.kind_of?(Proc) + default_value = default_value.call() + end + self.send(set, default_value) + end + + @sources = [] + @patches = [] + + @@sections.each do |name, get, set| + self.send(set, RpmSection.new(name)) + end + end + + # Write the RpmSpec out to the +io+ which is already open for write. + def write(io) + @@headers.each do |name, get, set, | + io.header(name, self.send(get)) + end + + @sources.each_with_index do |src, i| + io.header('Source' + i.to_s, src) + end + + @patches.each_with_index do |src, i| + io.header('Source' + i.to_s, src) + end + + @@sections.each do |name, get, set| + sec = self.send(get) + io.section(sec.name, sec.rest) do + io.puts(sec.text) + end + end + end +end \ No newline at end of file diff --git a/lib/rpmer/rubygemtranslator.rb b/lib/rpmer/rubygemtranslator.rb new file mode 100644 index 0000000..b8d2b2c --- /dev/null +++ b/lib/rpmer/rubygemtranslator.rb @@ -0,0 +1,164 @@ +# This file can be distributed under the same terms as ruby itself. +# Author: Marek Gilbert + +require 'rpmer/util' + +# Does the actual work of mapping GEM metadata to RPM metadata and +# generating RPM spec contents that will result in a successful build of the +# RPM. +class RubyGemTranslator + attr_accessor :gem + attr_accessor :rpm + + def initialize(gem_spec) + @gem = gem_spec + #@rpm = RpmSpec.new(File.join(build.spec_dir, gem.name + '.spec')) + @rpm = RpmSpec.new(gem.name + '.spec') + end + + # Factory method that creates an RpmTranslator from a +build+ and a GEM + # filename. + def self.from_gem(filename) + gem_spec = nil + File.open(filename, 'r') do |fd| + Gem::Package::open_from_io(fd) do |g| + gem_spec = g.metadata + gem_spec.loaded_from = filename + end + end + rt = RubyGemTranslator.new(gem_spec) + return rt + end + + def add_external_requirements(options) + # rubygems itself doesn't require rubygems, but everything else + # does. + @rpm.requires << "rubygems" + + # most packages require ri and rdoc to properly install + @rpm.build_requires << 'ruby' + + # FIXME add hook for multiple distros + #@rpm.build_requires << 'rubygems' + @rpm.build_requires << 'rubygems_with_buildroot_patch' + + if @gem.has_rdoc? + @rpm.build_requires << 'ruby-rdoc' # or maybe /usr/bin/rdoc? + @rpm.build_requires << 'ruby-ri' # or maybe /usr/bin/ri? + end + end + + def add_requirement(name, op, ver) + case op + when '~>' + @rpm.requires << "#{name} >= #{ver}" + @rpm.requires << "#{name} < #{ver.bump}" + else + @rpm.requires << "#{name} #{op} #{ver}" + end + end + + def add_build_requirement(name, op, ver) + case op + when '~>' + @rpm.build_requires << "#{name} >= #{ver}" + @rpm.build_requires << "#{name} < #{ver.bump}" + else + @rpm.build_requires << "#{name} #{op} #{ver}" + end + end + + # Translate the gem to an RPM using the given options. + def translate_gem(options) + name_ver = @gem.name + '-' + @gem.version.to_s + gem_basename = File.basename(@gem.loaded_from) + + @rpm.sources << gem_basename + + @rpm.name = "rubygem-#{@gem.name}" + @rpm.version = @gem.version.to_s + @rpm.release = options.release + @rpm.summary = @gem.summary + @rpm.license = options.license + @rpm.group = options.group + @rpm.url = @gem.homepage + + if !options.arch.nil? + @rpm.build_arch = options.arch + else + case @gem.platform + when Gem::Platform::RUBY + @rpm.build_arch = 'noarch' + + when /(i.86)-linux/ + @rpm.build_arch = 'i386' + + else + raise "failed to convert unknown gem platform '#{@gem.platform}' to equivalent rpm BuildArch." + end + end + + @rpm.build_root = '%{_tmppath}/%{name}-%{version}-%{release}-root' + + # Populate dependencies. Note that GEM can express multiple + # requirements on a given package and its syntax for that is + # incompatible with RPM. Iterate over each version requirement + # so as to build up the list correctly. + @gem.dependencies.each do |dep| + dep.version_requirements.requirements.each do |x| + if options.gems_prefix.empty? + add_requirement( dep.name, *x) + add_build_requirement( dep.name, *x) + else + add_requirement( "rubygem-#{dep.name}", *x) + add_build_requirement( "rubygem-#{dep.name}", *x) + end + + end + end + + @rpm.description << wrap_text(@gem.description, 76) + + @rpm.prep << < - #{gem.version}-#{options.release} +- #{gem.name} release #{gem.version}. +EOF + end +end + diff --git a/lib/rpmer/util.rb b/lib/rpmer/util.rb new file mode 100644 index 0000000..3b13f58 --- /dev/null +++ b/lib/rpmer/util.rb @@ -0,0 +1,9 @@ +# wrap_text takes +text+ and wraps it such that no line exeeds +col+ +# characters. +def wrap_text(text, col = 80) + if text + text.gsub(/(.{1,#{col}})(\s+|$)\n?/, "\\1\n") + else + '' + end +end \ No newline at end of file diff --git a/spec/rpmer/rpmspec_spec.rb b/spec/rpmer/rpmspec_spec.rb new file mode 100644 index 0000000..c32481e --- /dev/null +++ b/spec/rpmer/rpmspec_spec.rb @@ -0,0 +1,14 @@ +$:.push(File.expand_path(File.dirname(__FILE__) + "../../lib")) +require 'rpmer' + +describe RPMSpec do + before(:each) do + @bowling = RPMSPec.new + end + + + it "should score 0 for gutter game" do + 20.times { @bowling.hit(0) } + @bowling.score.should == 0 + end +end -- 2.39.2