From accf071397f858cc48f1452d184535dd698a783b Mon Sep 17 00:00:00 2001 From: Mo Morsi Date: Mon, 5 May 2014 14:39:24 -0400 Subject: [PATCH] Relax polisher deps, gracefully fail if missing (rev 4) Now polisher gem contains minimal runtime deps w/ mechanism to catch runtime load errors. Components missing reqs will not be accessible / calling methods on them will indicate to install optional dependencies Rebased against latest master --- bin/git_gem_updater.rb | 3 - lib/polisher/component.rb | 30 ++ lib/polisher/errata.rb | 112 ++--- lib/polisher/fedora.rb | 66 +-- lib/polisher/gem.rb | 567 +++++++++++----------- lib/polisher/gemfile.rb | 175 +++---- lib/polisher/git/pkg.rb | 353 +++++++------- lib/polisher/git/repo.rb | 124 ++--- lib/polisher/koji.rb | 173 +++---- lib/polisher/rpm/requirement.rb | 350 +++++++------- lib/polisher/rpm/spec.rb | 833 ++++++++++++++++---------------- lib/polisher/yum.rb | 40 +- polisher.gemspec | 19 +- 13 files changed, 1435 insertions(+), 1410 deletions(-) create mode 100644 lib/polisher/component.rb diff --git a/bin/git_gem_updater.rb b/bin/git_gem_updater.rb index c2d2c03..7880e3f 100755 --- a/bin/git_gem_updater.rb +++ b/bin/git_gem_updater.rb @@ -11,10 +11,7 @@ # Copyright (C) 2013-2014 Red Hat, Inc. require 'colored' -require 'curb' -require 'json' require 'optparse' -require 'nokogiri' require 'polisher/git' require 'polisher/gem' diff --git a/lib/polisher/component.rb b/lib/polisher/component.rb new file mode 100644 index 0000000..e5bada6 --- /dev/null +++ b/lib/polisher/component.rb @@ -0,0 +1,30 @@ +# Polisher Components & Component Helpers +# +# Licensed under the MIT license +# Copyright (C) 2014 Red Hat, Inc. + +require 'active_support/core_ext' + +module Polisher + module Component + class Missing + def initialize(*args) + raise "polisher is missing a dependency - cannot instantiate" + end + + def method_missing(method_id, *args, &bl) + raise "polisher is missing a dependency - cannot invoke #{method_id} on #{self}" + end + end # class MissingComponent + + def self.verify(polisher_klass, *dependencies) + dependencies.each { |dep| require dep } + rescue LoadError + klass = polisher_klass.demodulize + polisher_module = "Polisher::#{polisher_klass.deconstantize}" + polisher_module.constantize.const_set(klass, Missing) + else + yield + end + end # module Component +end # module Polisher diff --git a/lib/polisher/errata.rb b/lib/polisher/errata.rb index 2d9e798..1078485 100644 --- a/lib/polisher/errata.rb +++ b/lib/polisher/errata.rb @@ -3,73 +3,73 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'json' -require 'curb' - require 'polisher/core' +require 'polisher/component' module Polisher - class Errata - extend ConfHelpers + Component.verify("Errata", 'json', 'curb') do + class Errata + extend ConfHelpers + + conf_attr :advisory_url, '' + conf_attr :package_prefix, 'rubygem-' - conf_attr :advisory_url, '' - conf_attr :package_prefix, 'rubygem-' + # Initialize/return singleton curl handle to query errata + def self.client + @curl ||= begin + curl = Curl::Easy.new + curl.ssl_verify_peer = false + curl.ssl_verify_host = false + curl.http_auth_types = :negotiate + curl.userpwd = ':' + curl + end + end - # Initialize/return singleton curl handle to query errata - def self.client - @curl ||= begin - curl = Curl::Easy.new - curl.ssl_verify_peer = false - curl.ssl_verify_host = false - curl.http_auth_types = :negotiate - curl.userpwd = ':' - curl + def self.clear! + @cached_url = nil + @cached_builds = nil + self end - end - def self.clear! - @cached_url = nil - @cached_builds = nil - self - end + def self.builds + @cached_url ||= advisory_url + @cached_builds ||= nil - def self.builds - @cached_url ||= advisory_url - @cached_builds ||= nil + if @cached_url != advisory_url || @cached_builds.nil? + client.url = "#{advisory_url}/builds" + @cached_builds = client.get + @cached_builds = JSON.parse(client.body_str) + end - if @cached_url != advisory_url || @cached_builds.nil? - client.url = "#{advisory_url}/builds" - @cached_builds = client.get - @cached_builds = JSON.parse(client.body_str) + @cached_builds end - @cached_builds - end - - def self.versions_for(name, &bl) - versions = builds.collect do |tag, builds| - ErrataBuild.builds_matching(builds, name) - end.flatten - bl.call(:errata, name, versions) unless(bl.nil?) - versions - end - end + def self.versions_for(name, &bl) + versions = builds.collect do |tag, builds| + ErrataBuild.builds_matching(builds, name) + end.flatten + bl.call(:errata, name, versions) unless(bl.nil?) + versions + end + end # class Errata - class ErrataBuild - def self.builds_matching(builds, name) - builds.collect { |build| - self.build_matches?(build, name) ? self.build_version(build, name) : nil - }.compact - end + class ErrataBuild + def self.builds_matching(builds, name) + builds.collect { |build| + self.build_matches?(build, name) ? self.build_version(build, name) : nil + }.compact + end - def self.build_matches?(build, name) - pkg,meta = *build.flatten - pkg =~ /^#{Errata.package_prefix}#{name}-([^-]*)-.*$/ - end + def self.build_matches?(build, name) + pkg,meta = *build.flatten + pkg =~ /^#{Errata.package_prefix}#{name}-([^-]*)-.*$/ + end - def self.build_version(build, name) - pkg,meta = *build.flatten - pkg.gsub(Errata.package_prefix, '').split('-')[1] - end - end -end + def self.build_version(build, name) + pkg,meta = *build.flatten + pkg.gsub(Errata.package_prefix, '').split('-')[1] + end + end # class ErrataBuild + end # Component.verify(Errata) +end # module Polisher diff --git a/lib/polisher/fedora.rb b/lib/polisher/fedora.rb index 2ce607a..0a0f3cb 100644 --- a/lib/polisher/fedora.rb +++ b/lib/polisher/fedora.rb @@ -3,44 +3,44 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'curb' -require 'pkgwat' - require 'polisher/bodhi' +require 'polisher/component' module Polisher - class Fedora - PACKAGE_LIST = 'https://admin.fedoraproject.org/pkgdb/users/packages/' + Component.verify("Fedora", "curb", "pkgwat", "nokogiri") do + class Fedora + PACKAGE_LIST = 'https://admin.fedoraproject.org/pkgdb/users/packages/' - def self.client - @client ||= Curl::Easy.new - end + def self.client + @client ||= Curl::Easy.new + end - # Retrieve list of gems owned by the specified user - # - # @param [String] user Fedora username to lookup - # @return [Array] list of gems which the user owns/has access to - def self.gems_owned_by(user) - client.url = "#{PACKAGE_LIST}#{user}" - client.http_get - packages = client.body_str - # TODO instantiate Polisher::Gem instances & return - Nokogiri::HTML(packages).xpath("//a[@class='PackageName']"). - select { |i| i.text =~ /rubygem-.*/ }. - collect { |i| i.text.gsub(/rubygem-/, '') } - end + # Retrieve list of gems owned by the specified user + # + # @param [String] user Fedora username to lookup + # @return [Array] list of gems which the user owns/has access to + def self.gems_owned_by(user) + client.url = "#{PACKAGE_LIST}#{user}" + client.http_get + packages = client.body_str + # TODO instantiate Polisher::Gem instances & return + Nokogiri::HTML(packages).xpath("//a[@class='PackageName']"). + select { |i| i.text =~ /rubygem-.*/ }. + collect { |i| i.text.gsub(/rubygem-/, '') } + end - # Retrieve list of the versions of the specified package in the various - # Fedora releases. - # - # @param [String] name name of the package to lookup - # @param [Callable] bl optional callback to invoke with versions retrieved - # @return [Array] list of versions in Fedora - def self.versions_for(name, &bl) - # simply dispatch to bodhi to get latest updates - Polisher::Bodhi.versions_for name do |target,name,versions| - bl.call(:fedora, name, versions) unless(bl.nil?) + # Retrieve list of the versions of the specified package in the various + # Fedora releases. + # + # @param [String] name name of the package to lookup + # @param [Callable] bl optional callback to invoke with versions retrieved + # @return [Array] list of versions in Fedora + def self.versions_for(name, &bl) + # simply dispatch to bodhi to get latest updates + Polisher::Bodhi.versions_for name do |target,name,versions| + bl.call(:fedora, name, versions) unless(bl.nil?) + end end - end - end # class Fedora + end # class Fedora + end # Component.verify("Fedora") end # module Polisher diff --git a/lib/polisher/gem.rb b/lib/polisher/gem.rb index cc1d4bb..2d47377 100644 --- a/lib/polisher/gem.rb +++ b/lib/polisher/gem.rb @@ -3,319 +3,312 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'curb' -require 'json' -require 'yaml' -require 'tempfile' -require 'pathname' -require 'fileutils' -require 'awesome_spawn' -require 'rubygems/installer' -require 'active_support' -require 'active_support/core_ext' - -require 'polisher/version_checker' -require 'polisher/gem_cache' require 'polisher/vendor' +require 'polisher/component' +require 'polisher/gem_cache' +require 'polisher/version_checker' module Polisher - class Gem - include HasVendoredDeps - - GEM_CMD = '/usr/bin/gem' - DIFF_CMD = '/usr/bin/diff' - - # Common files shipped in gems that we should ignore - IGNORE_FILES = ['.gemtest', '.gitignore', '.travis.yml', - /.*.gemspec/, /Gemfile.*/, 'Rakefile', - /rspec.*/, '.yardopts', '.rvmrc'] - - # Common files shipped in gems that we should mark as doc - DOC_FILES = [/CHANGELOG.*/, /CONTRIBUTING.*/, /README.*/] - - attr_accessor :spec - attr_accessor :name - attr_accessor :version - attr_accessor :deps - attr_accessor :dev_deps - - attr_accessor :path - - def initialize(args={}) - @spec = args[:spec] - @name = args[:name] - @version = args[:version] - @path = args[:path] - @deps = args[:deps] || [] - @dev_deps = args[:dev_deps] || [] - end - - # Return bool indicating if the specified file is on the IGNORE_FILES list - def self.ignorable_file?(file) - IGNORE_FILES.any? do |ignore| - ignore.is_a?(Regexp) ? ignore.match(file) : ignore == file + deps = ['curb', 'json', 'yaml', 'tempfile', 'pathname', 'fileutils', + 'awesome_spawn', 'rubygems/installer', 'active_support', 'active_support/core_ext'] + Component.verify("Gem", *deps) do + class Gem + include HasVendoredDeps + + GEM_CMD = '/usr/bin/gem' + DIFF_CMD = '/usr/bin/diff' + + # Common files shipped in gems that we should ignore + IGNORE_FILES = ['.gemtest', '.gitignore', '.travis.yml', + /.*.gemspec/, /Gemfile.*/, 'Rakefile', + /rspec.*/, '.yardopts', '.rvmrc'] + + # Common files shipped in gems that we should mark as doc + DOC_FILES = [/CHANGELOG.*/, /CONTRIBUTING.*/, /README.*/] + + attr_accessor :spec + attr_accessor :name + attr_accessor :version + attr_accessor :deps + attr_accessor :dev_deps + + attr_accessor :path + + def initialize(args={}) + @spec = args[:spec] + @name = args[:name] + @version = args[:version] + @path = args[:path] + @deps = args[:deps] || [] + @dev_deps = args[:dev_deps] || [] end - end - # Return bool indicating if the specified file is on the DOC_FILES list - def self.doc_file?(file) - DOC_FILES.any? do |doc| - doc.is_a?(Regexp) ? doc.match(file) : doc == file + # Return bool indicating if the specified file is on the IGNORE_FILES list + def self.ignorable_file?(file) + IGNORE_FILES.any? do |ignore| + ignore.is_a?(Regexp) ? ignore.match(file) : ignore == file + end end - end - - # Retrieve list of the versions of the specified gem installed locally - # - # @param [String] name name of the gem to lookup - # @param [Callable] bl optional block to invoke with versions retrieved - # @return [Array] list of versions of gem installed locally - def self.local_versions_for(name, &bl) - silence_warnings do - @local_db ||= ::Gem::Specification.all + + # Return bool indicating if the specified file is on the DOC_FILES list + def self.doc_file?(file) + DOC_FILES.any? do |doc| + doc.is_a?(Regexp) ? doc.match(file) : doc == file + end end - versions = @local_db.select { |s| s.name == name }.collect { |s| s.version } - bl.call(:local_gem, name, versions) unless(bl.nil?) - versions - end - - # Return new instance of Gem from JSON Specification - def self.from_json(json) - specj = JSON.parse(json) - metadata = {} - metadata[:spec] = specj - metadata[:name] = specj['name'] - metadata[:version] = specj['version'] - - metadata[:deps] = - specj['dependencies']['runtime'].collect { |d| - ::Gem::Dependency.new d['name'], *d['requirements'].split(',') - } - - metadata[:dev_deps] = - specj['dependencies']['development'].collect { |d| - ::Gem::Dependency.new d['name'], d['requirements'].split(',') - } - - self.new metadata - end - - # Return new instance of Gem from Gemspec - def self.from_gemspec(gemspec) - gemspec = - ::Gem::Specification.load(gemspec) if !gemspec.is_a?(::Gem::Specification) && - File.exists?(gemspec) - - metadata = {} - metadata[:spec] = gemspec - metadata[:name] = gemspec.name - metadata[:version] = gemspec.version.to_s - - metadata[:deps] = - gemspec.dependencies.select { |dep| - dep.type == :runtime - }.collect { |dep| dep } - - metadata[:dev_deps] = - gemspec.dependencies.select { |dep| - dep.type == :development - }.collect { |dep| dep } - - self.new metadata - end - - # Return new instance of Gem from rubygem - def self.from_gem(gem_path) - gem = self.parse :gemspec => ::Gem::Package.new(gem_path).spec - gem.path = gem_path - gem - end - - # Parse the specified gemspec & return new Gem instance from metadata - # - # @param [String,Hash] args contents of actual gemspec of option hash - # specifying location of gemspec to parse - # @option args [String] :gemspec path to gemspec to load / parse - # @return [Polisher::Gem] gem instantiated from gemspec metadata - def self.parse(args={}) - if args.is_a?(String) - return self.from_json args - - elsif args.has_key?(:gemspec) - return self.from_gemspec args[:gemspec] - - elsif args.has_key?(:gem) - return self.from_gem args[:gem] + # Retrieve list of the versions of the specified gem installed locally + # + # @param [String] name name of the gem to lookup + # @param [Callable] bl optional block to invoke with versions retrieved + # @return [Array] list of versions of gem installed locally + def self.local_versions_for(name, &bl) + silence_warnings do + @local_db ||= ::Gem::Specification.all + end + versions = @local_db.select { |s| s.name == name }.collect { |s| s.version } + bl.call(:local_gem, name, versions) unless(bl.nil?) + versions end - self.new - end - - # Return handler to internal curl helper - def self.client - @client ||= Curl::Easy.new - end - - # Download the specified gem and return the binary file contents as a string - # - # @return [String] binary gem contents - def self.download_gem(name, version) - cached = GemCache.get(name, version) - return cached unless cached.nil? - - client.url = "https://rubygems.org/gems/#{name}-#{version}.gem" - client.follow_location = true - client.http_get - gemf = client.body_str - - GemCache.set(name, version, gemf) - gemf - end - - # Download the local gem and return it as a string - def download_gem - self.class.download_gem @name, @version - end - - # Download the specified gem / version from rubygems and - # return instance of Polisher::Gem class corresponding to it - def self.from_rubygems(name, version) - download_gem name, version - self.from_gem downloaded_gem_path(name, version) - end - - # Returns path to downloaded gem - # - # @return [String] path to downloaded gem - def self.downloaded_gem_path(name, version) - # ensure gem is downloaded - self.download_gem(name, version) - GemCache.path_for(name, version) - end - - # Return path to downloaded gem - def downloaded_gem_path - self.class.downloaded_gem_path @name, @version - end - - # Returns path to gem, either specified one of downloaded one - def gem_path - @path || downloaded_gem_path - end - - # Unpack files & return unpacked directory - # - # If block is specified, it will be invoked - # with directory after which directory will be removed - def unpack(&bl) - dir = nil - pkg = ::Gem::Installer.new gem_path, :unpack => true - - if bl - Dir.mktmpdir { |dir| - pkg.unpack dir - bl.call dir - } - else - dir = Dir.mktmpdir - pkg.unpack dir + # Return new instance of Gem from JSON Specification + def self.from_json(json) + specj = JSON.parse(json) + metadata = {} + metadata[:spec] = specj + metadata[:name] = specj['name'] + metadata[:version] = specj['version'] + + metadata[:deps] = + specj['dependencies']['runtime'].collect { |d| + ::Gem::Dependency.new d['name'], *d['requirements'].split(',') + } + + metadata[:dev_deps] = + specj['dependencies']['development'].collect { |d| + ::Gem::Dependency.new d['name'], d['requirements'].split(',') + } + + self.new metadata + end + + # Return new instance of Gem from Gemspec + def self.from_gemspec(gemspec) + gemspec = + ::Gem::Specification.load(gemspec) if !gemspec.is_a?(::Gem::Specification) && + File.exists?(gemspec) + + metadata = {} + metadata[:spec] = gemspec + metadata[:name] = gemspec.name + metadata[:version] = gemspec.version.to_s + + metadata[:deps] = + gemspec.dependencies.select { |dep| + dep.type == :runtime + }.collect { |dep| dep } + + metadata[:dev_deps] = + gemspec.dependencies.select { |dep| + dep.type == :development + }.collect { |dep| dep } + + self.new metadata + end + + # Return new instance of Gem from rubygem + def self.from_gem(gem_path) + gem = self.parse :gemspec => ::Gem::Package.new(gem_path).spec + gem.path = gem_path + gem end - dir - end + # Parse the specified gemspec & return new Gem instance from metadata + # + # @param [String,Hash] args contents of actual gemspec of option hash + # specifying location of gemspec to parse + # @option args [String] :gemspec path to gemspec to load / parse + # @return [Polisher::Gem] gem instantiated from gemspec metadata + def self.parse(args={}) + if args.is_a?(String) + return self.from_json args + + elsif args.has_key?(:gemspec) + return self.from_gemspec args[:gemspec] + + elsif args.has_key?(:gem) + return self.from_gem args[:gem] - # Iterate over each file in gem invoking block with path - def each_file(&bl) - self.unpack do |dir| - Pathname(dir).find do |path| - next if path.to_s == dir.to_s - pathstr = path.to_s.gsub("#{dir}/", '') - bl.call pathstr unless pathstr.blank? end + + self.new + end + + # Return handler to internal curl helper + def self.client + @client ||= Curl::Easy.new end - end - - # Retrieve the list of paths to files in the gem - # - # @return [Array] list of files in the gem - def file_paths - @file_paths ||= begin - files = [] - self.each_file do |path| - files << path + + # Download the specified gem and return the binary file contents as a string + # + # @return [String] binary gem contents + def self.download_gem(name, version) + cached = GemCache.get(name, version) + return cached unless cached.nil? + + client.url = "https://rubygems.org/gems/#{name}-#{version}.gem" + client.follow_location = true + client.http_get + gemf = client.body_str + + GemCache.set(name, version, gemf) + gemf + end + + # Download the local gem and return it as a string + def download_gem + self.class.download_gem @name, @version + end + + # Download the specified gem / version from rubygems and + # return instance of Polisher::Gem class corresponding to it + def self.from_rubygems(name, version) + download_gem name, version + self.from_gem downloaded_gem_path(name, version) + end + + # Returns path to downloaded gem + # + # @return [String] path to downloaded gem + def self.downloaded_gem_path(name, version) + # ensure gem is downloaded + self.download_gem(name, version) + GemCache.path_for(name, version) + end + + # Return path to downloaded gem + def downloaded_gem_path + self.class.downloaded_gem_path @name, @version + end + + # Returns path to gem, either specified one of downloaded one + def gem_path + @path || downloaded_gem_path + end + + # Unpack files & return unpacked directory + # + # If block is specified, it will be invoked + # with directory after which directory will be removed + def unpack(&bl) + dir = nil + pkg = ::Gem::Installer.new gem_path, :unpack => true + + if bl + Dir.mktmpdir { |dir| + pkg.unpack dir + bl.call dir + } + else + dir = Dir.mktmpdir + pkg.unpack dir end - files + + dir end - end - - # Retrieve gem metadata and contents from rubygems.org - # - # @param [String] name string name of gem to retrieve - # @return [Polisher::Gem] representation of gem - def self.retrieve(name) - gem_json_path = "https://rubygems.org/api/v1/gems/#{name}.json" - spec = Curl::Easy.http_get(gem_json_path).body_str - gem = self.parse spec - gem - end - - # Retreive versions of gem available in all configured targets (optionally recursively) - # - # @param [Hash] args hash of options to configure retrieval - # @option args [Boolean] :recursive indicates if versions of dependencies - # should also be retrieved - # @option args [Boolean] :dev_deps indicates if versions of development - # dependencies should also be retrieved - # @return [Hash] hash of name to list of versions for gem - # (and dependencies if specified) - def versions(args={}, &bl) - recursive = args[:recursive] - dev_deps = args[:dev_deps] - versions = args[:versions] || {} - - gem_versions = Polisher::VersionChecker.versions_for(self.name, &bl) - versions.merge!({ self.name => gem_versions }) - args[:versions] = versions - - if recursive - self.deps.each { |dep| - unless versions.has_key?(dep.name) - gem = Polisher::Gem.retrieve(dep.name) - versions.merge! gem.versions(args, &bl) + + # Iterate over each file in gem invoking block with path + def each_file(&bl) + self.unpack do |dir| + Pathname(dir).find do |path| + next if path.to_s == dir.to_s + pathstr = path.to_s.gsub("#{dir}/", '') + bl.call pathstr unless pathstr.blank? end - } + end + end + + # Retrieve the list of paths to files in the gem + # + # @return [Array] list of files in the gem + def file_paths + @file_paths ||= begin + files = [] + self.each_file do |path| + files << path + end + files + end + end - if dev_deps - self.dev_deps.each { |dep| + # Retrieve gem metadata and contents from rubygems.org + # + # @param [String] name string name of gem to retrieve + # @return [Polisher::Gem] representation of gem + def self.retrieve(name) + gem_json_path = "https://rubygems.org/api/v1/gems/#{name}.json" + spec = Curl::Easy.http_get(gem_json_path).body_str + gem = self.parse spec + gem + end + + # Retreive versions of gem available in all configured targets (optionally recursively) + # + # @param [Hash] args hash of options to configure retrieval + # @option args [Boolean] :recursive indicates if versions of dependencies + # should also be retrieved + # @option args [Boolean] :dev_deps indicates if versions of development + # dependencies should also be retrieved + # @return [Hash] hash of name to list of versions for gem + # (and dependencies if specified) + def versions(args={}, &bl) + recursive = args[:recursive] + dev_deps = args[:dev_deps] + versions = args[:versions] || {} + + gem_versions = Polisher::VersionChecker.versions_for(self.name, &bl) + versions.merge!({ self.name => gem_versions }) + args[:versions] = versions + + if recursive + self.deps.each { |dep| unless versions.has_key?(dep.name) gem = Polisher::Gem.retrieve(dep.name) versions.merge! gem.versions(args, &bl) end } + + if dev_deps + self.dev_deps.each { |dep| + unless versions.has_key?(dep.name) + gem = Polisher::Gem.retrieve(dep.name) + versions.merge! gem.versions(args, &bl) + end + } + end end - end - versions - end - - # Return diff of content in this gem against other - def diff(other) - out = nil - - begin - this_dir = self.unpack - other_dir = other.is_a?(Polisher::Gem) ? other.unpack : - (other.is_a?(Polisher::Git::Repo) ? other.path : other) - result = AwesomeSpawn.run("#{DIFF_CMD} -r #{this_dir} #{other_dir}") - out = result.output.gsub("#{this_dir}", 'a').gsub("#{other_dir}", 'b') - rescue - ensure - FileUtils.rm_rf this_dir unless this_dir.nil? - FileUtils.rm_rf other_dir unless other_dir.nil? || - !other.is_a?(Polisher::Gem) + versions end - out - end + # Return diff of content in this gem against other + def diff(other) + out = nil + + begin + this_dir = self.unpack + other_dir = other.is_a?(Polisher::Gem) ? other.unpack : + (other.is_a?(Polisher::Git::Repo) ? other.path : other) + result = AwesomeSpawn.run("#{DIFF_CMD} -r #{this_dir} #{other_dir}") + out = result.output.gsub("#{this_dir}", 'a').gsub("#{other_dir}", 'b') + rescue + ensure + FileUtils.rm_rf this_dir unless this_dir.nil? + FileUtils.rm_rf other_dir unless other_dir.nil? || + !other.is_a?(Polisher::Gem) + end - end # class Gem + out + end + end # class Gem + end # Component.verify("Gem") end # module Polisher diff --git a/lib/polisher/gemfile.rb b/lib/polisher/gemfile.rb index 2530687..aac62c5 100644 --- a/lib/polisher/gemfile.rb +++ b/lib/polisher/gemfile.rb @@ -3,96 +3,97 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'bundler' - require 'polisher/git' require 'polisher/gem' +require 'polisher/component' require 'polisher/version_checker' module Polisher - class Gemfile - include VersionedDependencies - - # always nil, for interface compatability - attr_accessor :version - - attr_accessor :deps - attr_accessor :dev_deps - - # always empty array, for interface compatability - attr_accessor :file_paths - - attr_accessor :definition - - def initialize(args={}) - @version = nil - @deps = args[:deps] - @dev_deps = args[:dev_deps] - @definition = args[:definition] - @file_paths = [] - end - - # Parse the specified gemfile & return new Gemfile instance from metadata - # - # @param [String] path to gemfile to parse - # @return [Polisher::Gemfile] gemfile instantiated from parsed metadata - def self.parse(path, args={}) - groups = args[:groups] - - definition = nil - path,g = File.split(path) - Dir.chdir(path){ - begin - definition = Bundler::Definition.build(g, nil, false) - rescue Bundler::GemfileNotFound - raise ArgumentError, "invalid gemfile: #{path}" - end - } - - metadata = {} - metadata[:deps] = - definition.dependencies.select { |d| - groups.nil? || groups.empty? || # groups not specified - groups.any? { |g| d.groups.include?(g.intern) } # dep in any group - }.collect { |d| d.name } - - metadata[:dev_deps] = [] # TODO - metadata[:definition] = definition - - self.new metadata - end - - # Simply alias for all dependencies in Gemfile - def vendored - deps + dev_deps - end - - # Retrieve gems which differ from - # rubygems.org/other upstream sources - def patched - vendored.collect do |dep| - bundler_dep = @definition ? - @definition.dependencies.find { |d| d.name == dep } : nil - - # TODO right now just handling git based alternate sources, - # should be able to handle other types bundler supports - # (path and alternate rubygems src) - next unless bundler_dep && bundler_dep.source.is_a?(Bundler::Source::Git) - src = bundler_dep.source - - # retrieve gem - gem = src.version ? - Polisher::Gem.new(:name => dep, :version => src.version) : - Polisher::Gem.retrieve(dep) - - # retrieve dep - git = Polisher::Git::Repo.new :url => src.uri - git.clone unless git.cloned? - git.checkout src.ref if src.ref - - # diff gem against git - gem.diff(git.path) - end.compact! - end - end # class Gemfile + Component.verify("Gemfile", 'bundler') do + class Gemfile + include VersionedDependencies + + # always nil, for interface compatability + attr_accessor :version + + attr_accessor :deps + attr_accessor :dev_deps + + # always empty array, for interface compatability + attr_accessor :file_paths + + attr_accessor :definition + + def initialize(args={}) + @version = nil + @deps = args[:deps] + @dev_deps = args[:dev_deps] + @definition = args[:definition] + @file_paths = [] + end + + # Parse the specified gemfile & return new Gemfile instance from metadata + # + # @param [String] path to gemfile to parse + # @return [Polisher::Gemfile] gemfile instantiated from parsed metadata + def self.parse(path, args={}) + groups = args[:groups] + + definition = nil + path,g = File.split(path) + Dir.chdir(path){ + begin + definition = Bundler::Definition.build(g, nil, false) + rescue Bundler::GemfileNotFound + raise ArgumentError, "invalid gemfile: #{path}" + end + } + + metadata = {} + metadata[:deps] = + definition.dependencies.select { |d| + groups.nil? || groups.empty? || # groups not specified + groups.any? { |g| d.groups.include?(g.intern) } # dep in any group + }.collect { |d| d.name } + + metadata[:dev_deps] = [] # TODO + metadata[:definition] = definition + + self.new metadata + end + + # Simply alias for all dependencies in Gemfile + def vendored + deps + dev_deps + end + + # Retrieve gems which differ from + # rubygems.org/other upstream sources + def patched + vendored.collect do |dep| + bundler_dep = @definition ? + @definition.dependencies.find { |d| d.name == dep } : nil + + # TODO right now just handling git based alternate sources, + # should be able to handle other types bundler supports + # (path and alternate rubygems src) + next unless bundler_dep && bundler_dep.source.is_a?(Bundler::Source::Git) + src = bundler_dep.source + + # retrieve gem + gem = src.version ? + Polisher::Gem.new(:name => dep, :version => src.version) : + Polisher::Gem.retrieve(dep) + + # retrieve dep + git = Polisher::Git::Repo.new :url => src.uri + git.clone unless git.cloned? + git.checkout src.ref if src.ref + + # diff gem against git + gem.diff(git.path) + end.compact! + end + end # class Gemfile + end # Component.verify("Gemfile") end # module Polisher diff --git a/lib/polisher/git/pkg.rb b/lib/polisher/git/pkg.rb index 8a1afba..07bee0c 100644 --- a/lib/polisher/git/pkg.rb +++ b/lib/polisher/git/pkg.rb @@ -3,189 +3,190 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'awesome_spawn' - require 'polisher/error' require 'polisher/git/repo' require 'polisher/rpm/spec' +require 'polisher/component' module Polisher module Git - # Git Repository - class Pkg < Repo - attr_accessor :name - attr_accessor :version - - conf_attr :rpm_prefix, 'rubygem-' - conf_attr :pkg_cmd, '/usr/bin/fedpkg' - conf_attr :build_cmd, '/usr/bin/koji' - conf_attr :build_tgt, 'rawhide' - conf_attr :md5sum_cmd, '/usr/bin/md5sum' - conf_attr :dist_git_url, 'git://pkgs.fedoraproject.org/' - conf_attr :fetch_tgt, 'master' - - def initialize(args={}) - @name = args[:name] - @version = args[:version] - super(args) - end - - # Return full rpm name of package containing optional prefix - def rpm_name - @rpm_name ||= "#{rpm_prefix}#{self.name}" - end - - # Return full srpm file name of package - def srpm - @srpm ||= "#{rpm_name}-#{self.version}-1.*.src.rpm" - end - - # Return full spec file name - def spec_file - @spec_path ||= "#{rpm_name}.spec" - end - - # Return handle to instance of Polisher::RPM::Spec corresponding to spec - def spec - @spec ||= in_repo { Polisher::RPM::Spec.parse File.read(spec_file) } - end - - # Files representing pkg tracked by git - def pkg_files - @pkg_files ||= [spec_file, 'sources', '.gitignore'] - end - - # Override path to reference pkg name - # @override - def path - GitCache.path_for(rpm_name) - end - - # Alias orig clone method to git_clone - alias :git_clone :clone - - # Override clone to use PKG_PCMD - # @override - def clone - self.clobber! - Dir.mkdir path unless File.directory? path - in_repo do - result = AwesomeSpawn.run "#{pkg_cmd} clone #{rpm_name}" - raise PolisherError, - "could not clone #{rpm_name}" unless result.exit_status == 0 - - # pkg_cmd will clone into the rpm_name subdir, - # move everything up a dir - Dir.foreach("#{rpm_name}/") do |f| - orig = "#{rpm_name}/#{f}" - skip = ['.', '..'].include?(f) - FileUtils.move orig, '.' unless skip + Component.verify("Git::Pkg", 'awesome_spawn') do + # Git Based Package + class Pkg < Repo + attr_accessor :name + attr_accessor :version + + conf_attr :rpm_prefix, 'rubygem-' + conf_attr :pkg_cmd, '/usr/bin/fedpkg' + conf_attr :build_cmd, '/usr/bin/koji' + conf_attr :build_tgt, 'rawhide' + conf_attr :md5sum_cmd, '/usr/bin/md5sum' + conf_attr :dist_git_url, 'git://pkgs.fedoraproject.org/' + conf_attr :fetch_tgt, 'master' + + def initialize(args={}) + @name = args[:name] + @version = args[:version] + super(args) + end + + # Return full rpm name of package containing optional prefix + def rpm_name + @rpm_name ||= "#{rpm_prefix}#{self.name}" + end + + # Return full srpm file name of package + def srpm + @srpm ||= "#{rpm_name}-#{self.version}-1.*.src.rpm" + end + + # Return full spec file name + def spec_file + @spec_path ||= "#{rpm_name}.spec" + end + + # Return handle to instance of Polisher::RPM::Spec corresponding to spec + def spec + @spec ||= in_repo { Polisher::RPM::Spec.parse File.read(spec_file) } + end + + # Files representing pkg tracked by git + def pkg_files + @pkg_files ||= [spec_file, 'sources', '.gitignore'] + end + + # Override path to reference pkg name + # @override + def path + GitCache.path_for(rpm_name) + end + + # Alias orig clone method to git_clone + alias :git_clone :clone + + # Override clone to use PKG_PCMD + # @override + def clone + self.clobber! + Dir.mkdir path unless File.directory? path + in_repo do + result = AwesomeSpawn.run "#{pkg_cmd} clone #{rpm_name}" + raise PolisherError, + "could not clone #{rpm_name}" unless result.exit_status == 0 + + # pkg_cmd will clone into the rpm_name subdir, + # move everything up a dir + Dir.foreach("#{rpm_name}/") do |f| + orig = "#{rpm_name}/#{f}" + skip = ['.', '..'].include?(f) + FileUtils.move orig, '.' unless skip + end + + FileUtils.rm_rf rpm_name + end + + self + end + + # Return boolean indicating if package is marked as dead (retired/obsolete/etc) + def dead? + in_repo { File.exists?('dead.package') } + end + + # Clone / init GitPkg + def fetch + clone unless cloned? + raise Exception, "Dead package detected" if dead? + checkout fetch_tgt + reset! + pull + + self + end + + # Update the local spec to the specified gem version + def update_spec_to(gem) + in_repo do + spec.update_to(gem) + File.write(spec_file, spec.to_string) + end + end + + # Generate new sources file + def gen_sources_for(gem) + in_repo do + AwesomeSpawn.run "#{md5sum_cmd} #{gem.gem_path} > sources" + File.write('sources', File.read('sources').gsub("#{GemCache::DIR}/", '')) + end + end + + # Update git ignore to ignore gem + def ignore(gem) + in_repo do + nl = File.exists?('.gitignore') ? "\n" : '' + content = "#{nl}#{gem.name}-#{gem.version}.gem" + File.open(".gitignore", 'a') { |f| f.write content } + end + end + + # Update the local pkg to specified gem + # + # @param [Polisher::Gem] gem instance of gem containing metadata to update to + def update_to(gem) + update_spec_to gem + gen_sources_for gem + ignore gem + self + end + + # Override commit, generate a default msg, always add pkg files + # @override + def commit(msg=nil) + in_repo { AwesomeSpawn.run "#{git_cmd} add #{pkg_files.join(' ')}" } + super(msg.nil? ? "updated to #{version}" : msg) + self + end + + # Build the srpm + def build_srpm + in_repo { AwesomeSpawn.run "#{pkg_cmd} srpm" } + self + end + + # Run a scratch build + def scratch_build + # TODO if build fails, raise error, else return output + cmd = "#{build_cmd} build --scratch #{build_tgt} #{srpm}" + in_repo { AwesomeSpawn.run(cmd) } + self + end + + # Build the pkg + def build + build_srpm + scratch_build + self + end + + # Retrieve list of the version of the specified package in git + # + # @param [String] name name of package to lookup + # @param [Callable] bl optional block to invoke with version retrieved + # @return [String] version retrieved, or nil if none found + def self.version_for(name, &bl) + version = nil + gitpkg = self.new :name => name + gitpkg.url = "#{dist_git_url}#{gitpkg.rpm_name}.git" + gitpkg.git_clone + begin + version = gitpkg.spec.version + rescue => e end - FileUtils.rm_rf rpm_name - end - - self - end - - # Return boolean indicating if package is marked as dead (retired/obsolete/etc) - def dead? - in_repo { File.exists?('dead.package') } - end - - # Clone / init GitPkg - def fetch - clone unless cloned? - raise Exception, "Dead package detected" if dead? - checkout fetch_tgt - reset! - pull - - self - end - - # Update the local spec to the specified gem version - def update_spec_to(gem) - in_repo do - spec.update_to(gem) - File.write(spec_file, spec.to_string) - end - end - - # Generate new sources file - def gen_sources_for(gem) - in_repo do - AwesomeSpawn.run "#{md5sum_cmd} #{gem.gem_path} > sources" - File.write('sources', File.read('sources').gsub("#{GemCache::DIR}/", '')) - end - end - - # Update git ignore to ignore gem - def ignore(gem) - in_repo do - nl = File.exists?('.gitignore') ? "\n" : '' - content = "#{nl}#{gem.name}-#{gem.version}.gem" - File.open(".gitignore", 'a') { |f| f.write content } - end - end - - # Update the local pkg to specified gem - # - # @param [Polisher::Gem] gem instance of gem containing metadata to update to - def update_to(gem) - update_spec_to gem - gen_sources_for gem - ignore gem - self - end - - # Override commit, generate a default msg, always add pkg files - # @override - def commit(msg=nil) - in_repo { AwesomeSpawn.run "#{git_cmd} add #{pkg_files.join(' ')}" } - super(msg.nil? ? "updated to #{version}" : msg) - self - end - - # Build the srpm - def build_srpm - in_repo { AwesomeSpawn.run "#{pkg_cmd} srpm" } - self - end - - # Run a scratch build - def scratch_build - # TODO if build fails, raise error, else return output - cmd = "#{build_cmd} build --scratch #{build_tgt} #{srpm}" - in_repo { AwesomeSpawn.run(cmd) } - self - end - - # Build the pkg - def build - build_srpm - scratch_build - self - end - - # Retrieve list of the version of the specified package in git - # - # @param [String] name name of package to lookup - # @param [Callable] bl optional block to invoke with version retrieved - # @return [String] version retrieved, or nil if none found - def self.version_for(name, &bl) - version = nil - gitpkg = self.new :name => name - gitpkg.url = "#{dist_git_url}#{gitpkg.rpm_name}.git" - gitpkg.git_clone - begin - version = gitpkg.spec.version - rescue => e - end - - bl.call(:git, name, [version]) unless(bl.nil?) - version - end - end # module Pkg + bl.call(:git, name, [version]) unless(bl.nil?) + version + end + end # module Pkg + end # Component.verify("Git::Pkgrrata") end # module Git end # module Polisher diff --git a/lib/polisher/git/repo.rb b/lib/polisher/git/repo.rb index 3d395a4..79d05a2 100644 --- a/lib/polisher/git/repo.rb +++ b/lib/polisher/git/repo.rb @@ -3,72 +3,74 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'awesome_spawn' require 'polisher/core' require 'polisher/git_cache' +require 'polisher/component' module Polisher module Git - # Git Repository - class Repo - extend ConfHelpers - - # TODO use ruby git api - conf_attr :git_cmd, '/usr/bin/git' - - attr_accessor :url - - def initialize(args={}) - @url = args[:url] - end - - def path - GitCache.path_for(@url) - end - - # Clobber the git repo - def clobber! - FileUtils.rm_rf path - end - - def clone - AwesomeSpawn.run! "#{git_cmd} clone #{url} #{path}" - end - - def cloned? - File.directory?(path) - end - - def in_repo - Dir.chdir path do - yield + Component.verify("Git::Repo", 'awesome_spawn') do + # Git Repository + class Repo + extend ConfHelpers + + # TODO use ruby git api + conf_attr :git_cmd, '/usr/bin/git' + + attr_accessor :url + + def initialize(args={}) + @url = args[:url] + end + + def path + GitCache.path_for(@url) + end + + # Clobber the git repo + def clobber! + FileUtils.rm_rf path + end + + def clone + AwesomeSpawn.run! "#{git_cmd} clone #{url} #{path}" + end + + def cloned? + File.directory?(path) + end + + def in_repo + Dir.chdir path do + yield + end + end + + def file_paths + in_repo { Dir['**/*'] } + end + + # Note be careful when invoking: + def reset! + in_repo { AwesomeSpawn.run! "#{git_cmd} reset HEAD~ --hard" } + self + end + + def pull + in_repo { AwesomeSpawn.run! "#{git_cmd} pull" } + self + end + + def checkout(tgt) + in_repo { AwesomeSpawn.run! "#{git_cmd} checkout #{tgt}" } + self + end + + def commit(msg) + in_repo { AwesomeSpawn.run! "#{git_cmd} commit -m '#{msg}'" } + self end - end - - def file_paths - in_repo { Dir['**/*'] } - end - - # Note be careful when invoking: - def reset! - in_repo { AwesomeSpawn.run! "#{git_cmd} reset HEAD~ --hard" } - self - end - - def pull - in_repo { AwesomeSpawn.run! "#{git_cmd} pull" } - self - end - - def checkout(tgt) - in_repo { AwesomeSpawn.run! "#{git_cmd} checkout #{tgt}" } - self - end - - def commit(msg) - in_repo { AwesomeSpawn.run! "#{git_cmd} commit -m '#{msg}'" } - self - end - end # class Repo + end # class Repo + end # Component.verify("Git::Repo") end # module Git end # module Polisher diff --git a/lib/polisher/koji.rb b/lib/polisher/koji.rb index 7173a8f..e48dfa3 100644 --- a/lib/polisher/koji.rb +++ b/lib/polisher/koji.rb @@ -3,106 +3,107 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'xmlrpc/client' -require 'active_support' -require 'active_support/core_ext/kernel/reporting' -silence_warnings do - XMLRPC::Config::ENABLE_NIL_PARSER = true - XMLRPC::Config::ENABLE_NIL_CREATE = true -end - require 'polisher/core' +require 'polisher/component' module Polisher - class Koji - extend ConfHelpers + deps = ['xmlrpc/client', 'active_support', 'active_support/core_ext/kernel/reporting'] + Component.verify("Koji", *deps) do + silence_warnings do + XMLRPC::Config::ENABLE_NIL_PARSER = true + XMLRPC::Config::ENABLE_NIL_CREATE = true + end - # TODO Koji#build (on class or instance?) + class Koji + extend ConfHelpers - conf_attr :koji_url, 'koji.fedoraproject.org/kojihub' - conf_attr :koji_tag, 'f21' - conf_attr :package_prefix, 'rubygem-' + # TODO Koji#build (on class or instance?) - def self.koji_tags - [koji_tag].flatten - end + conf_attr :koji_url, 'koji.fedoraproject.org/kojihub' + conf_attr :koji_tag, 'f21' + conf_attr :package_prefix, 'rubygem-' - # Retrieve shared instance of xmlrpc client to use - def self.client - @client ||= begin - url = koji_url.split('/') - XMLRPC::Client.new(url[0..-2].join('/'), - "/#{url.last}") + def self.koji_tags + [koji_tag].flatten end - end - # Return bool indiciating if koji has a build exactly - # matching the specified version - def self.has_build?(name, version) - versions = self.versions_for name - versions.include?(version) - end + # Retrieve shared instance of xmlrpc client to use + def self.client + @client ||= begin + url = koji_url.split('/') + XMLRPC::Client.new(url[0..-2].join('/'), + "/#{url.last}") + end + end - # Return bool indicating if koji has a build which - # satisfies the specified ruby dependency - def self.has_build_satisfying?(name, version) - dep = ::Gem::Dependency.new name, version - self.versions_for(name).any? { |v| - dep.match?(name, v) - } - end + # Return bool indiciating if koji has a build exactly + # matching the specified version + def self.has_build?(name, version) + versions = self.versions_for name + versions.include?(version) + end - # Return list of tags for which a package exists - # - # @param [String] name of package to lookup - # @return [Hash] hash of tag names to package versions for tags - # which package was found in - def self.tagged_in(name) - # tagid userid pkgid prefix inherit with_dups - pkgs = client.call('listPackages', nil, nil, "rubygem-#{name}", nil, false, true) - pkgs.collect { |pkg| pkg['tag_name'] } - end + # Return bool indicating if koji has a build which + # satisfies the specified ruby dependency + def self.has_build_satisfying?(name, version) + dep = ::Gem::Dependency.new name, version + self.versions_for(name).any? { |v| + dep.match?(name, v) + } + end - # Retrieve list of the versions of the specified package in koji - # - # @param [String] name name of package to lookup - # @param [Callable] bl optional block to invoke with versions retrieved - # @return [Array] versions retrieved, empty array if none found - def self.versions_for(name, &bl) - # koji xmlrpc call - builds = - koji_tags.collect do |tag| - # tag event inherit prefix latest - client.call('listTagged', tag, nil, true, nil, false, - "#{package_prefix}#{name}") - end.flatten - versions = builds.collect { |b| b['version'] } - bl.call(:koji, name, versions) unless(bl.nil?) - versions - end + # Return list of tags for which a package exists + # + # @param [String] name of package to lookup + # @return [Hash] hash of tag names to package versions for tags + # which package was found in + def self.tagged_in(name) + # tagid userid pkgid prefix inherit with_dups + pkgs = client.call('listPackages', nil, nil, "rubygem-#{name}", nil, false, true) + pkgs.collect { |pkg| pkg['tag_name'] } + end - # Return diff between list of packages in two tags in koji - def self.diff(tag1, tag2) - builds1 = client.call('listTagged', tag1, nil, false, nil, false) - builds2 = client.call('listTagged', tag2, nil, false, nil, false) - builds = {} - builds1.each do |build| - name = build['package_name'] - build2 = builds2.find { |b| b['name'] == name } - version1 = build['version'] - version2 = build2 && build2['version'] - builds[name] = {tag1 => version1, tag2 => version2} + # Retrieve list of the versions of the specified package in koji + # + # @param [String] name name of package to lookup + # @param [Callable] bl optional block to invoke with versions retrieved + # @return [Array] versions retrieved, empty array if none found + def self.versions_for(name, &bl) + # koji xmlrpc call + builds = + koji_tags.collect do |tag| + # tag event inherit prefix latest + client.call('listTagged', tag, nil, true, nil, false, + "#{package_prefix}#{name}") + end.flatten + versions = builds.collect { |b| b['version'] } + bl.call(:koji, name, versions) unless(bl.nil?) + versions end - builds2.each do |build| - name = build['package_name'] - next if builds.key?(name) + # Return diff between list of packages in two tags in koji + def self.diff(tag1, tag2) + builds1 = client.call('listTagged', tag1, nil, false, nil, false) + builds2 = client.call('listTagged', tag2, nil, false, nil, false) + builds = {} + builds1.each do |build| + name = build['package_name'] + build2 = builds2.find { |b| b['name'] == name } + version1 = build['version'] + version2 = build2 && build2['version'] + builds[name] = {tag1 => version1, tag2 => version2} + end - version = build['version'] - builds[name] = {tag1 => nil, tag2 => version} - end + builds2.each do |build| + name = build['package_name'] + next if builds.key?(name) - builds - end - end -end + version = build['version'] + builds[name] = {tag1 => nil, tag2 => version} + end + + builds + end + end # class Koji + end # Component.verify("Koji") +end # module Polisher diff --git a/lib/polisher/rpm/requirement.rb b/lib/polisher/rpm/requirement.rb index cb03472..e42a41d 100644 --- a/lib/polisher/rpm/requirement.rb +++ b/lib/polisher/rpm/requirement.rb @@ -3,195 +3,195 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'gem2rpm' -require 'versionomy' -require 'active_support/core_ext' - -require 'polisher/core' require 'polisher/gem' +require 'polisher/core' +require 'polisher/component' module Polisher - module RPM - class Requirement - extend ConfHelpers + deps = ['gem2rpm', 'versionomy', 'active_support/core_ext'] + Component.verify("RPM::Requirement", *deps) do + module RPM + class Requirement + extend ConfHelpers - conf_attr :rubygem_prefix, 'rubygem' - conf_attr :scl_prefix, '' # set to %{?scl_prefix} to enable scl's + conf_attr :rubygem_prefix, 'rubygem' + conf_attr :scl_prefix, '' # set to %{?scl_prefix} to enable scl's - def self.prefix - "#{scl_prefix}#{rubygem_prefix}" - end + def self.prefix + "#{scl_prefix}#{rubygem_prefix}" + end - # Bool indiciating if req is a BR - attr_accessor :br + # Bool indiciating if req is a BR + attr_accessor :br - # Name of requirement - attr_accessor :name + # Name of requirement + attr_accessor :name - # Condition, eg >=, =, etc - attr_accessor :condition + # Condition, eg >=, =, etc + attr_accessor :condition - # Version number - attr_accessor :version + # Version number + attr_accessor :version - # Requirement string - def str - sp = self.specifier - sp.nil? ? "#{@name}" : "#{@name} #{sp}" - end + # Requirement string + def str + sp = self.specifier + sp.nil? ? "#{@name}" : "#{@name} #{sp}" + end - # Specified string - def specifier - @version.nil? ? nil : "#{@condition} #{@version}" - end + # Specified string + def specifier + @version.nil? ? nil : "#{@condition} #{@version}" + end - # Instantiate / return new rpm spec requirements from string - def self.parse(str, opts={}) - stra = str.split - br = str.include?('BuildRequires') - name = condition = version = nil + # Instantiate / return new rpm spec requirements from string + def self.parse(str, opts={}) + stra = str.split + br = str.include?('BuildRequires') + name = condition = version = nil + + if str.include?('Requires') + name = stra[1] + condition = stra[2] + version = stra[3] + + else + name = stra[0] + condition = stra[1] + version = stra[2] + + end + + req = self.new({:name => name, + :condition => condition, + :version => version, + :br => br}.merge(opts)) + req + end - if str.include?('Requires') - name = stra[1] - condition = stra[2] - version = stra[3] + # Instantiate / return new rpm spec requirements from gem dependency. + # + # Because a gem dependency may result in multiple spec requirements + # this will always return an array of Requirement instances + def self.from_gem_dep(gem_dep, br=false) + gem_dep.requirement.to_s.split(',').collect { |req| + expanded = Gem2Rpm::Helpers.expand_requirement [req.split] + expanded.collect { |e| + new :name => "#{prefix}(#{gem_dep.name})", + :condition => e.first.to_s, + :version => e.last.to_s, + :br => br + } + }.flatten + end + + def initialize(args={}) + @br = args[:br] || false + @name = args[:name] + @condition = args[:condition] + @version = args[:version] + + @name.strip! unless @name.nil? + @condition.strip! unless @condition.nil? + @version.strip! unless @version.nil? + end - else - name = stra[0] - condition = stra[1] - version = stra[2] + def ==(other) + @br == other.br && + @name == other.name && + @condition == other.condition && + @version == other.version + end + # Greatest Common Denominator, + # Max version in list that is less than the local version + def gcd(versions) + lversion = Versionomy.parse(self.version) + versions.collect { |v| Versionomy.parse(v) }. + sort { |a,b| a <=> b }.reverse. + find { |v| v < lversion }.to_s end - req = self.new({:name => name, - :condition => condition, - :version => version, - :br => br}.merge(opts)) - req - end - - # Instantiate / return new rpm spec requirements from gem dependency. - # - # Because a gem dependency may result in multiple spec requirements - # this will always return an array of Requirement instances - def self.from_gem_dep(gem_dep, br=false) - gem_dep.requirement.to_s.split(',').collect { |req| - expanded = Gem2Rpm::Helpers.expand_requirement [req.split] - expanded.collect { |e| - new :name => "#{prefix}(#{gem_dep.name})", - :condition => e.first.to_s, - :version => e.last.to_s, - :br => br - } - }.flatten - end - - def initialize(args={}) - @br = args[:br] || false - @name = args[:name] - @condition = args[:condition] - @version = args[:version] - - @name.strip! unless @name.nil? - @condition.strip! unless @condition.nil? - @version.strip! unless @version.nil? - end - - def ==(other) - @br == other.br && - @name == other.name && - @condition == other.condition && - @version == other.version - end - - # Greatest Common Denominator, - # Max version in list that is less than the local version - def gcd(versions) - lversion = Versionomy.parse(self.version) - versions.collect { |v| Versionomy.parse(v) }. - sort { |a,b| a <=> b }.reverse. - find { |v| v < lversion }.to_s - end - - # Minimum gem version which satisfies this dependency - def min_satisfying_version - return "0.0" if self.version.nil? || - self.condition == '<' || - self.condition == '<=' - return self.version if self.condition == '=' || - self.condition == '>=' - Versionomy.parse(self.version).bump(:tiny).to_s # self.condition == '>' - end - - # Max gem version which satisfies this dependency - # - # Can't automatically deduce in '<' case, so if that is the conditional - # we require a version list, and will return the gcd from it - def max_satisfying_version(versions=nil) - return Float::INFINITY if self.version.nil? || - self.condition == '>' || - self.condition == '>=' - return self.version if self.condition == '=' || - self.condition == '<=' - - raise ArgumentError if versions.nil? - self.gcd(versions) - end - - # Minimum gem version for which this dependency fails - def min_failing_version - raise ArgumentError if self.version.nil? - return "0.0" if self.condition == '>' || - self.condition == '>=' - return self.version if self.condition == '<' - Versionomy.parse(self.version).bump(:tiny).to_s # self.condition == '<=' and '=' - end - - # Max gem version for which this dependency fails - # - # Can't automatically deduce in '>=', and '=' cases, so if that is the - # conditional we require a version list, and will return the gcd from it - def max_failing_version(versions=nil) - raise ArgumentError if self.version.nil? || - self.condition == '<=' || - self.condition == '<' - return self.version if self.condition == '>' - - raise ArgumentError if versions.nil? - self.gcd(versions) - end - - # Return bool indicating if requirement matches specified - # depedency. - # - # Comparison mechanism will depend on type of class - # passed to this. Valid types include - # - Polisher::RPM::Requirements - # - ::Gem::Dependency - def matches?(dep) - return self == dep if dep.is_a?(self.class) - raise ArgumentError unless dep.is_a?(::Gem::Dependency) - - return false if !self.gem? || self.gem_name != dep.name - return true if self.version.nil? - - Gem2Rpm::Helpers.expand_requirement([dep.requirement.to_s.split]). - any?{ |req| - req.first == self.condition && req.last.to_s == self.version - } - end - - # Whether or not this requirement specified a ruby gem dependency - def gem? - !!(self.str =~ RPM::Spec::SPEC_GEM_REQ_MATCHER) - end - - # Return the name of the gem which this requirement is for. - # Returns nil if this is not a gem requirement - def gem_name - # XXX need to explicitly run regex here to get $1 - !!(self.str =~ RPM::Spec::SPEC_GEM_REQ_MATCHER) ? $1 : nil - end - end # class Requirement - end # module RPM -end + # Minimum gem version which satisfies this dependency + def min_satisfying_version + return "0.0" if self.version.nil? || + self.condition == '<' || + self.condition == '<=' + return self.version if self.condition == '=' || + self.condition == '>=' + Versionomy.parse(self.version).bump(:tiny).to_s # self.condition == '>' + end + + # Max gem version which satisfies this dependency + # + # Can't automatically deduce in '<' case, so if that is the conditional + # we require a version list, and will return the gcd from it + def max_satisfying_version(versions=nil) + return Float::INFINITY if self.version.nil? || + self.condition == '>' || + self.condition == '>=' + return self.version if self.condition == '=' || + self.condition == '<=' + + raise ArgumentError if versions.nil? + self.gcd(versions) + end + + # Minimum gem version for which this dependency fails + def min_failing_version + raise ArgumentError if self.version.nil? + return "0.0" if self.condition == '>' || + self.condition == '>=' + return self.version if self.condition == '<' + Versionomy.parse(self.version).bump(:tiny).to_s # self.condition == '<=' and '=' + end + + # Max gem version for which this dependency fails + # + # Can't automatically deduce in '>=', and '=' cases, so if that is the + # conditional we require a version list, and will return the gcd from it + def max_failing_version(versions=nil) + raise ArgumentError if self.version.nil? || + self.condition == '<=' || + self.condition == '<' + return self.version if self.condition == '>' + + raise ArgumentError if versions.nil? + self.gcd(versions) + end + + # Return bool indicating if requirement matches specified + # depedency. + # + # Comparison mechanism will depend on type of class + # passed to this. Valid types include + # - Polisher::RPM::Requirements + # - ::Gem::Dependency + def matches?(dep) + return self == dep if dep.is_a?(self.class) + raise ArgumentError unless dep.is_a?(::Gem::Dependency) + + return false if !self.gem? || self.gem_name != dep.name + return true if self.version.nil? + + Gem2Rpm::Helpers.expand_requirement([dep.requirement.to_s.split]). + any?{ |req| + req.first == self.condition && req.last.to_s == self.version + } + end + + # Whether or not this requirement specified a ruby gem dependency + def gem? + !!(self.str =~ RPM::Spec::SPEC_GEM_REQ_MATCHER) + end + + # Return the name of the gem which this requirement is for. + # Returns nil if this is not a gem requirement + def gem_name + # XXX need to explicitly run regex here to get $1 + !!(self.str =~ RPM::Spec::SPEC_GEM_REQ_MATCHER) ? $1 : nil + end + end # class Requirement + end # module RPM + end # Component.verify("RPM::Requirement") +end # module Polisher diff --git a/lib/polisher/rpm/spec.rb b/lib/polisher/rpm/spec.rb index 1762dbb..b522f88 100644 --- a/lib/polisher/rpm/spec.rb +++ b/lib/polisher/rpm/spec.rb @@ -3,444 +3,443 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'gem2rpm' -require 'versionomy' -require 'active_support' -require 'active_support/core_ext' - require 'polisher/core' require 'polisher/gem' require 'polisher/rpm/requirement' +require 'polisher/component' module Polisher - module RPM - class Spec - # RPM Spec Requirement Prefix - def self.requirement_prefix - Requirement.prefix - end - - def requirement_prefix - self.class.requirement_prefix - end - - def self.package_prefix - requirement_prefix - end - - AUTHOR = "#{ENV['USER']} <#{ENV['USER']}@localhost.localdomain>" - - COMMENT_MATCHER = /^\s*#.*/ - GEM_NAME_MATCHER = /^%global\s*gem_name\s(.*)$/ - SPEC_NAME_MATCHER = /^Name:\s*#{package_prefix}-(.*)$/ - SPEC_VERSION_MATCHER = /^Version:\s*(.*)$/ - SPEC_RELEASE_MATCHER = /^Release:\s*(.*)$/ - SPEC_REQUIRES_MATCHER = /^Requires:\s*(.*)$/ - SPEC_BUILD_REQUIRES_MATCHER = /^BuildRequires:\s*(.*)$/ - SPEC_GEM_REQ_MATCHER = /^.*\s*#{requirement_prefix}\((.*)\)(\s*(.*))?$/ - SPEC_SUBPACKAGE_MATCHER = /^%package\s(.*)$/ - SPEC_CHANGELOG_MATCHER = /^%changelog$/ - SPEC_FILES_MATCHER = /^%files$/ - SPEC_SUBPKG_FILES_MATCHER = /^%files\s*(.*)$/ - SPEC_DOC_FILES_MATCHER = /^%files doc$/ - SPEC_CHECK_MATCHER = /^%check$/ - - FILE_MACRO_MATCHERS = - [/^%doc\s/, /^%config\s/, /^%attr\s/, - /^%verify\s/, /^%docdir.*/, /^%dir\s/, - /^%defattr.*/, /^%exclude\s/, /^%{gem_instdir}\/+/] - - FILE_MACRO_REPLACEMENTS = - {"%{_bindir}" => 'bin', - "%{gem_libdir}" => 'lib'} - - attr_accessor :metadata - - # Return the currently configured author - def self.current_author - ENV['POLISHER_AUTHOR'] || AUTHOR - end - - def initialize(metadata={}) - @metadata = metadata - end - - # Dispatch all missing methods to lookup calls in rpm spec metadata - def method_missing(method, *args, &block) - # proxy to metadata - if @metadata.has_key?(method) - @metadata[method] - - else - super(method, *args, &block) + deps = ['gem2rpm', 'versionomy', 'active_support', 'active_support/core_ext'] + Component.verify("RPM::Spec", *deps) do + module RPM + class Spec + # RPM Spec Requirement Prefix + def self.requirement_prefix + Requirement.prefix end - end - # Return gem corresponding to spec name/version - def upstream_gem - @gem ||= Polisher::Gem.from_rubygems gem_name, version - end - - # Return boolean indicating if spec has a %check section - def has_check? - @metadata.has_key?(:has_check) && @metadata[:has_check] - end - - # Return all the Requires for the specified gem - def requirements_for_gem(gem_name) - @metadata[:requires].nil? ? [] : - @metadata[:requires].select { |r| r.gem_name == gem_name } - end - - # Return all the BuildRequires for the specified gem - def build_requirements_for_gem(gem_name) - @metadata[:build_requires].nil? ? [] : - @metadata[:build_requires].select { |r| r.gem_name == gem_name } - end - - # Return bool indicating if this spec specifies all the - # requirements in the specified gem dependency - # - # @param [Gem::Dependency] gem_dep dependency which to retreive / compare - # requirements - def has_all_requirements_for?(gem_dep) - reqs = self.requirements_for_gem gem_dep.name - # create a spec requirement dependency for each expanded subrequirement, - # verify we can find a match for that - gem_dep.requirement.to_s.split(',').all? { |greq| - Gem2Rpm::Helpers.expand_requirement([greq.split]).all? { |ereq| - tereq = Requirement.new :name => "#{requirement_prefix}(#{gem_dep.name})", - :condition => ereq.first, - :version => ereq.last.to_s - reqs.any? { |req| req.matches?(tereq)} - } - } - end - - # Return list of gem dependencies for which we have no - # corresponding requirements - def missing_deps_for(gem) - # Comparison by name here assuming if it is in existing spec, - # spec author will have ensured versions are correct for their purposes - gem.deps.select { |dep| requirements_for_gem(dep.name).empty? } - end - - # Return list of gem dev dependencies for which we have - # no corresponding requirements - def missing_dev_deps_for(gem) - # Same note as in #missing_deps_for above - gem.dev_deps.select { |dep| build_requirements_for_gem(dep.name).empty? } - end - - # Return list of dependencies of upstream gem which - # have not been included - def excluded_deps - missing_deps_for(upstream_gem) - end - - # Return boolean indicating if the specified gem is on excluded list - def excludes_dep?(gem_name) - excluded_deps.any? { |d| d.name == gem_name } - end - - # Return list of dev dependencies of upstream gem which - # have not been included - def excluded_dev_deps - missing_dev_deps_for(upstream_gem) - end - - # Return boolean indicating if the specified gem is on - # excluded dev dep list - def excludes_dev_dep?(gem_name) - excluded_dev_deps.any? { |d| d.name == gem_name } - end - - # Return all gem Requires - def gem_requirements - @metadata[:requires].nil? ? [] : - @metadata[:requires].select { |r| r.gem? } - end - - # Return all gem BuildRequires - def gem_build_requirements - @metadata[:build_requires].nil? ? [] : - @metadata[:build_requires].select { |r| r.gem? } - end - - # Return all non gem Requires - def non_gem_requirements - @metadata[:requires].nil? ? [] : - @metadata[:requires].select { |r| !r.gem? } - end - - # Return all non gem BuildRequires - def non_gem_build_requirements - @metadata[:build_requires].nil? ? [] : - @metadata[:build_requires].select { |r| !r.gem? } - end - - # Return all gem requirements _not_ in the specified gem - def extra_gem_requirements(gem) - gem_reqs = gem.deps.collect { |d| requirements_for_gem(d.name) }.flatten - gem_requirements - gem_reqs - end - - # Return all gem build requirements _not_ in the specified gem - def extra_gem_build_requirements(gem) - gem_reqs = gem.deps.collect { |d| requirements_for_gem(d.name) }.flatten - gem_build_requirements - gem_reqs - end - - # Parse the specified rpm spec and return new RPM::Spec instance from metadata - # - # @param [String] string contents of spec to parse - # @return [Polisher::RPM::Spec] spec instantiated from rpmspec metadata - def self.parse(spec) - in_subpackage = false - in_changelog = false - in_files = false - subpkg_name = nil - meta = {:contents => spec} - spec.each_line { |l| - if l =~ COMMENT_MATCHER - ; - - # TODO support optional gem prefix - elsif l =~ GEM_NAME_MATCHER - meta[:gem_name] = $1.strip - meta[:gem_name] = $1.strip - - elsif l =~ SPEC_NAME_MATCHER && - $1.strip != "%{gem_name}" - meta[:gem_name] = $1.strip - - elsif l =~ SPEC_VERSION_MATCHER - meta[:version] = $1.strip - - elsif l =~ SPEC_RELEASE_MATCHER - meta[:release] = $1.strip - - elsif l =~ SPEC_SUBPACKAGE_MATCHER - subpkg_name = $1.strip - in_subpackage = true - - elsif l =~ SPEC_REQUIRES_MATCHER && - !in_subpackage - meta[:requires] ||= [] - meta[:requires] << RPM::Requirement.parse($1.strip) - - elsif l =~ SPEC_BUILD_REQUIRES_MATCHER && - !in_subpackage - meta[:build_requires] ||= [] - meta[:build_requires] << RPM::Requirement.parse($1.strip) - - elsif l =~ SPEC_CHANGELOG_MATCHER - in_changelog = true - - elsif l =~ SPEC_FILES_MATCHER - subpkg_name = nil - in_files = true - - elsif l =~ SPEC_SUBPKG_FILES_MATCHER - subpkg_name = $1.strip - in_files = true - - elsif l =~ SPEC_CHECK_MATCHER - meta[:has_check] = true - - elsif in_changelog - meta[:changelog] ||= "" - meta[:changelog] << l - - elsif in_files - tgt = subpkg_name.nil? ? meta[:gem_name] : subpkg_name - meta[:files] ||= {} - meta[:files][tgt] ||= [] - - sl = l.strip.unrpmize - meta[:files][tgt] << sl unless sl.blank? + def requirement_prefix + self.class.requirement_prefix + end + + def self.package_prefix + requirement_prefix + end + + AUTHOR = "#{ENV['USER']} <#{ENV['USER']}@localhost.localdomain>" + + COMMENT_MATCHER = /^\s*#.*/ + GEM_NAME_MATCHER = /^%global\s*gem_name\s(.*)$/ + SPEC_NAME_MATCHER = /^Name:\s*#{package_prefix}-(.*)$/ + SPEC_VERSION_MATCHER = /^Version:\s*(.*)$/ + SPEC_RELEASE_MATCHER = /^Release:\s*(.*)$/ + SPEC_REQUIRES_MATCHER = /^Requires:\s*(.*)$/ + SPEC_BUILD_REQUIRES_MATCHER = /^BuildRequires:\s*(.*)$/ + SPEC_GEM_REQ_MATCHER = /^.*\s*#{requirement_prefix}\((.*)\)(\s*(.*))?$/ + SPEC_SUBPACKAGE_MATCHER = /^%package\s(.*)$/ + SPEC_CHANGELOG_MATCHER = /^%changelog$/ + SPEC_FILES_MATCHER = /^%files$/ + SPEC_SUBPKG_FILES_MATCHER = /^%files\s*(.*)$/ + SPEC_DOC_FILES_MATCHER = /^%files doc$/ + SPEC_CHECK_MATCHER = /^%check$/ + + FILE_MACRO_MATCHERS = + [/^%doc\s/, /^%config\s/, /^%attr\s/, + /^%verify\s/, /^%docdir.*/, /^%dir\s/, + /^%defattr.*/, /^%exclude\s/, /^%{gem_instdir}\/+/] + + FILE_MACRO_REPLACEMENTS = + {"%{_bindir}" => 'bin', + "%{gem_libdir}" => 'lib'} + + attr_accessor :metadata + + # Return the currently configured author + def self.current_author + ENV['POLISHER_AUTHOR'] || AUTHOR + end + + def initialize(metadata={}) + @metadata = metadata + end + + # Dispatch all missing methods to lookup calls in rpm spec metadata + def method_missing(method, *args, &block) + # proxy to metadata + if @metadata.has_key?(method) + @metadata[method] + + else + super(method, *args, &block) end - } - - meta[:changelog_entries] = meta[:changelog] ? - meta[:changelog].split("\n\n") : [] - meta[:changelog_entries].collect! { |c| c.strip }.compact! + end + + # Return gem corresponding to spec name/version + def upstream_gem + @gem ||= Polisher::Gem.from_rubygems gem_name, version + end + + # Return boolean indicating if spec has a %check section + def has_check? + @metadata.has_key?(:has_check) && @metadata[:has_check] + end + + # Return all the Requires for the specified gem + def requirements_for_gem(gem_name) + @metadata[:requires].nil? ? [] : + @metadata[:requires].select { |r| r.gem_name == gem_name } + end + + # Return all the BuildRequires for the specified gem + def build_requirements_for_gem(gem_name) + @metadata[:build_requires].nil? ? [] : + @metadata[:build_requires].select { |r| r.gem_name == gem_name } + end + + # Return bool indicating if this spec specifies all the + # requirements in the specified gem dependency + # + # @param [Gem::Dependency] gem_dep dependency which to retreive / compare + # requirements + def has_all_requirements_for?(gem_dep) + reqs = self.requirements_for_gem gem_dep.name + # create a spec requirement dependency for each expanded subrequirement, + # verify we can find a match for that + gem_dep.requirement.to_s.split(',').all? { |greq| + Gem2Rpm::Helpers.expand_requirement([greq.split]).all? { |ereq| + tereq = Requirement.new :name => "#{requirement_prefix}(#{gem_dep.name})", + :condition => ereq.first, + :version => ereq.last.to_s + reqs.any? { |req| req.matches?(tereq)} + } + } + end + + # Return list of gem dependencies for which we have no + # corresponding requirements + def missing_deps_for(gem) + # Comparison by name here assuming if it is in existing spec, + # spec author will have ensured versions are correct for their purposes + gem.deps.select { |dep| requirements_for_gem(dep.name).empty? } + end - self.new meta - end + # Return list of gem dev dependencies for which we have + # no corresponding requirements + def missing_dev_deps_for(gem) + # Same note as in #missing_deps_for above + gem.dev_deps.select { |dep| build_requirements_for_gem(dep.name).empty? } + end - # Update RPM::Spec metadata to new gem - # - # @param [Polisher::Gem] new_source new gem to update rpmspec to - def update_to(new_source) - update_deps_from(new_source) - update_files_from(new_source) - update_metadata_from(new_source) - end + # Return list of dependencies of upstream gem which + # have not been included + def excluded_deps + missing_deps_for(upstream_gem) + end - private - - # Update spec dependencies from new source - def update_deps_from(new_source) - @metadata[:requires] = - non_gem_requirements + - extra_gem_requirements(new_source) + - new_source.deps.select { |r| !excludes_dep?(r.name) } - .collect { |r| RPM::Requirement.from_gem_dep(r) }.flatten - - @metadata[:build_requires] = - non_gem_build_requirements + - extra_gem_build_requirements(new_source) + - new_source.dev_deps.select { |r| !excludes_dev_dep?(r.name) } - .collect { |r| RPM::Requirement.from_gem_dep(r, true) }.flatten - end + # Return boolean indicating if the specified gem is on excluded list + def excludes_dep?(gem_name) + excluded_deps.any? { |d| d.name == gem_name } + end - # Internal helper to update spec files from new source - def update_files_from(new_source) - to_add = new_source.file_paths - @metadata[:files] ||= {} - @metadata[:files].each { |pkg,spec_files| - (new_source.file_paths & to_add).each { |gem_file| - # skip files already included in spec or in dir in spec - has_file = spec_files.any? { |sf| - gem_file.gsub(sf,'') != gem_file - } + # Return list of dev dependencies of upstream gem which + # have not been included + def excluded_dev_deps + missing_dev_deps_for(upstream_gem) + end - to_add.delete(gem_file) - to_add << gem_file.rpmize if !has_file && - !Gem.ignorable_file?(gem_file) + # Return boolean indicating if the specified gem is on + # excluded dev dep list + def excludes_dev_dep?(gem_name) + excluded_dev_deps.any? { |d| d.name == gem_name } + end + + # Return all gem Requires + def gem_requirements + @metadata[:requires].nil? ? [] : + @metadata[:requires].select { |r| r.gem? } + end + + # Return all gem BuildRequires + def gem_build_requirements + @metadata[:build_requires].nil? ? [] : + @metadata[:build_requires].select { |r| r.gem? } + end + + # Return all non gem Requires + def non_gem_requirements + @metadata[:requires].nil? ? [] : + @metadata[:requires].select { |r| !r.gem? } + end + + # Return all non gem BuildRequires + def non_gem_build_requirements + @metadata[:build_requires].nil? ? [] : + @metadata[:build_requires].select { |r| !r.gem? } + end + + # Return all gem requirements _not_ in the specified gem + def extra_gem_requirements(gem) + gem_reqs = gem.deps.collect { |d| requirements_for_gem(d.name) }.flatten + gem_requirements - gem_reqs + end + + # Return all gem build requirements _not_ in the specified gem + def extra_gem_build_requirements(gem) + gem_reqs = gem.deps.collect { |d| requirements_for_gem(d.name) }.flatten + gem_build_requirements - gem_reqs + end + + # Parse the specified rpm spec and return new RPM::Spec instance from metadata + # + # @param [String] string contents of spec to parse + # @return [Polisher::RPM::Spec] spec instantiated from rpmspec metadata + def self.parse(spec) + in_subpackage = false + in_changelog = false + in_files = false + subpkg_name = nil + meta = {:contents => spec} + spec.each_line { |l| + if l =~ COMMENT_MATCHER + ; + + # TODO support optional gem prefix + elsif l =~ GEM_NAME_MATCHER + meta[:gem_name] = $1.strip + meta[:gem_name] = $1.strip + + elsif l =~ SPEC_NAME_MATCHER && + $1.strip != "%{gem_name}" + meta[:gem_name] = $1.strip + + elsif l =~ SPEC_VERSION_MATCHER + meta[:version] = $1.strip + + elsif l =~ SPEC_RELEASE_MATCHER + meta[:release] = $1.strip + + elsif l =~ SPEC_SUBPACKAGE_MATCHER + subpkg_name = $1.strip + in_subpackage = true + + elsif l =~ SPEC_REQUIRES_MATCHER && + !in_subpackage + meta[:requires] ||= [] + meta[:requires] << RPM::Requirement.parse($1.strip) + + elsif l =~ SPEC_BUILD_REQUIRES_MATCHER && + !in_subpackage + meta[:build_requires] ||= [] + meta[:build_requires] << RPM::Requirement.parse($1.strip) + + elsif l =~ SPEC_CHANGELOG_MATCHER + in_changelog = true + + elsif l =~ SPEC_FILES_MATCHER + subpkg_name = nil + in_files = true + + elsif l =~ SPEC_SUBPKG_FILES_MATCHER + subpkg_name = $1.strip + in_files = true + + elsif l =~ SPEC_CHECK_MATCHER + meta[:has_check] = true + + elsif in_changelog + meta[:changelog] ||= "" + meta[:changelog] << l + + elsif in_files + tgt = subpkg_name.nil? ? meta[:gem_name] : subpkg_name + meta[:files] ||= {} + meta[:files][tgt] ||= [] + + sl = l.strip.unrpmize + meta[:files][tgt] << sl unless sl.blank? + end } - } - - @metadata[:new_files] = to_add.select { |f| !Gem.doc_file?(f) } - @metadata[:new_docs] = to_add - @metadata[:new_files] - end - - # Internal helper to update spec metadata from new source - def update_metadata_from(new_source) - # update to new version - @metadata[:version] = new_source.version - @metadata[:release] = "1%{?dist}" - - # add changelog entry - changelog_entry = < lbrp ? lrp : lbrp - - ltpn = contents.index "\n", ltp - - contents.slice!(tp...ltpn) - contents.insert tp, - (@metadata[:requires].collect { |r| "Requires: #{r.str}" } + - @metadata[:build_requires].collect { |r| "BuildRequires: #{r.str}" }).join("\n") - - # add new files - fp = contents.index SPEC_FILES_MATCHER - lfp = contents.index SPEC_SUBPKG_FILES_MATCHER, fp + 1 - lfp = contents.index SPEC_CHANGELOG_MATCHER if lfp.nil? - - contents.insert lfp - 1, @metadata[:new_files].join("\n") + "\n" + @metadata[:changelog_entries] ||= [] + @metadata[:changelog_entries].unshift changelog_entry.rstrip + end - # add new doc files - fp = contents.index SPEC_DOC_FILES_MATCHER - fp = contents.index SPEC_FILES_MATCHER if fp.nil? - lfp = contents.index SPEC_SUBPKG_FILES_MATCHER, fp + 1 - lfp = contents.index SPEC_CHANGELOG_MATCHER if lfp.nil? + public - contents.insert lfp - 1, @metadata[:new_docs].join("\n") + "\n" + # Return properly formatted rpmspec as string + # + # @return [String] string representation of rpm spec + def to_string + contents = @metadata[:contents] - # return new contents - contents - end - - # Compare this spec to a sepecified upstream gem source - # and return result. - # - # upstream_source should be an instance of Polisher::Gem, - # Polisher::Gemfile, or other class defining a 'deps' - # accessor that returns an array of Gem::Requirement dependencies - # - # Result will be a hash containing the shared dependencies as - # well as those that differ and their respective differences - def compare(upstream_source) - same = {} - diff = {} - upstream_source.deps.each do |d| - spec_reqs = self.requirements_for_gem(d.name) - spec_reqs_specifier = spec_reqs.empty? ? nil : - spec_reqs.collect { |req| req.specifier } - - if spec_reqs.nil? - diff[d.name] = {:spec => nil, - :upstream => d.requirement.to_s} - - elsif !spec_reqs.any? { |req| req.matches?(d) } || - !self.has_all_requirements_for?(d) - diff[d.name] = {:spec => spec_reqs_specifier, - :upstream => d.requirement.to_s} + # replace version / release + contents.gsub!(SPEC_VERSION_MATCHER, "Version: #{@metadata[:version]}") + contents.gsub!(SPEC_RELEASE_MATCHER, "Release: #{@metadata[:release]}") + + # add changelog entry + cp = contents.index SPEC_CHANGELOG_MATCHER + cpn = contents.index "\n", cp + contents = contents[0...cpn+1] + + @metadata[:changelog_entries].join("\n\n") + + # update requires/build requires + rp = contents.index SPEC_REQUIRES_MATCHER + brp = contents.index SPEC_BUILD_REQUIRES_MATCHER + tp = rp < brp ? rp : brp + + pp = contents.index SPEC_SUBPACKAGE_MATCHER + pp = -1 if pp.nil? + + lrp = contents.rindex SPEC_REQUIRES_MATCHER, pp + lbrp = contents.rindex SPEC_BUILD_REQUIRES_MATCHER, pp + ltp = lrp > lbrp ? lrp : lbrp + + ltpn = contents.index "\n", ltp + + contents.slice!(tp...ltpn) + contents.insert tp, + (@metadata[:requires].collect { |r| "Requires: #{r.str}" } + + @metadata[:build_requires].collect { |r| "BuildRequires: #{r.str}" }).join("\n") + + # add new files + fp = contents.index SPEC_FILES_MATCHER + lfp = contents.index SPEC_SUBPKG_FILES_MATCHER, fp + 1 + lfp = contents.index SPEC_CHANGELOG_MATCHER if lfp.nil? + + contents.insert lfp - 1, @metadata[:new_files].join("\n") + "\n" + + # add new doc files + fp = contents.index SPEC_DOC_FILES_MATCHER + fp = contents.index SPEC_FILES_MATCHER if fp.nil? + lfp = contents.index SPEC_SUBPKG_FILES_MATCHER, fp + 1 + lfp = contents.index SPEC_CHANGELOG_MATCHER if lfp.nil? - elsif !diff.has_key?(d.name) - same[d.name] = {:spec => spec_reqs_specifier, - :upstream => d.requirement.to_s } - end + contents.insert lfp - 1, @metadata[:new_docs].join("\n") + "\n" + + # return new contents + contents end - - @metadata[:requires].each do |req| - next unless req.gem? - - upstream_dep = upstream_source.deps.find { |d| d.name == req.gem_name } - - if upstream_dep.nil? - diff[req.gem_name] = {:spec => req.specifier, - :upstream => nil} - - elsif !req.matches?(upstream_dep) - diff[req.gem_name] = {:spec => req.specifier, - :upstream => upstream_dep.requirement.to_s } - - elsif !diff.has_key?(req.gem_name) - same[req.gem_name] = {:spec => req.specifier, - :upstream => upstream_dep.requirement.to_s } + + # Compare this spec to a sepecified upstream gem source + # and return result. + # + # upstream_source should be an instance of Polisher::Gem, + # Polisher::Gemfile, or other class defining a 'deps' + # accessor that returns an array of Gem::Requirement dependencies + # + # Result will be a hash containing the shared dependencies as + # well as those that differ and their respective differences + def compare(upstream_source) + same = {} + diff = {} + upstream_source.deps.each do |d| + spec_reqs = self.requirements_for_gem(d.name) + spec_reqs_specifier = spec_reqs.empty? ? nil : + spec_reqs.collect { |req| req.specifier } + + if spec_reqs.nil? + diff[d.name] = {:spec => nil, + :upstream => d.requirement.to_s} + + elsif !spec_reqs.any? { |req| req.matches?(d) } || + !self.has_all_requirements_for?(d) + diff[d.name] = {:spec => spec_reqs_specifier, + :upstream => d.requirement.to_s} + + elsif !diff.has_key?(d.name) + same[d.name] = {:spec => spec_reqs_specifier, + :upstream => d.requirement.to_s } + end end - end unless @metadata[:requires].nil? - - {:same => same, :diff => diff} - end - - end # class Spec - end # module RPM + + @metadata[:requires].each do |req| + next unless req.gem? + + upstream_dep = upstream_source.deps.find { |d| d.name == req.gem_name } + + if upstream_dep.nil? + diff[req.gem_name] = {:spec => req.specifier, + :upstream => nil} + + elsif !req.matches?(upstream_dep) + diff[req.gem_name] = {:spec => req.specifier, + :upstream => upstream_dep.requirement.to_s } + + elsif !diff.has_key?(req.gem_name) + same[req.gem_name] = {:spec => req.specifier, + :upstream => upstream_dep.requirement.to_s } + end + end unless @metadata[:requires].nil? + + {:same => same, :diff => diff} + end + + end # class Spec + end # module RPM + end # Component.verify("RPM::Spec") end # module Polisher diff --git a/lib/polisher/yum.rb b/lib/polisher/yum.rb index 359f9c1..69d0159 100644 --- a/lib/polisher/yum.rb +++ b/lib/polisher/yum.rb @@ -3,28 +3,30 @@ # Licensed under the MIT license # Copyright (C) 2013-2014 Red Hat, Inc. -require 'awesome_spawn' +require 'polisher/component' module Polisher - class Yum - YUM_CMD = '/usr/bin/yum' + Component.verify("Yum", 'awesome_spawn') do + class Yum + YUM_CMD = '/usr/bin/yum' - # Retrieve version of gem available in yum - # - # @param [String] name name of gem to loopup - # @param [Callable] bl optional callback to invoke with version retrieved - # @returns [String] version of gem in yum or nil if not found - def self.version_for(name, &bl) - version = nil - result = AwesomeSpawn.run "#{YUM_CMD} info rubygem-#{name}" - out = result.output + # Retrieve version of gem available in yum + # + # @param [String] name name of gem to loopup + # @param [Callable] bl optional callback to invoke with version retrieved + # @returns [String] version of gem in yum or nil if not found + def self.version_for(name, &bl) + version = nil + result = AwesomeSpawn.run "#{YUM_CMD} info rubygem-#{name}" + out = result.output - if out.include?("Version") - version = out.lines.to_a.find { |l| l =~ /^Version.*/ } - version = version.split(':').last.strip + if out.include?("Version") + version = out.lines.to_a.find { |l| l =~ /^Version.*/ } + version = version.split(':').last.strip + end + bl.call(:yum, name, [version]) unless(bl.nil?) + version end - bl.call(:yum, name, [version]) unless(bl.nil?) - version end - end -end + end # Component.verify("Yum") +end # module Polisher diff --git a/polisher.gemspec b/polisher.gemspec index 9f47da0..a03af16 100644 --- a/polisher.gemspec +++ b/polisher.gemspec @@ -28,17 +28,16 @@ Gem::Specification.new do |s| 'git_gem_updater.rb', 'ruby_rpm_spec_updater.rb'] s.require_paths = ['lib'] - # TODO tighten up deps, some may be optional - s.add_dependency('json') - s.add_dependency('curb') - s.add_dependency('activesupport') - s.add_dependency('i18n') - s.add_dependency('bundler') - s.add_dependency('pkgwat') s.add_dependency('colored') - s.add_dependency('awesome_spawn') - s.add_dependency('gem2rpm') - s.add_dependency('versionomy') + s.add_dependency('activesupport') s.add_development_dependency('rspec', '>= 2.0.0') s.add_development_dependency('coveralls') + s.add_development_dependency('json') + s.add_development_dependency('curb') + s.add_development_dependency('i18n') + s.add_development_dependency('bundler') + s.add_development_dependency('pkgwat') + s.add_development_dependency('awesome_spawn') + s.add_development_dependency('gem2rpm') + s.add_development_dependency('versionomy') end