#!/usr/bin/ruby require 'solv' require 'rubygems' require 'inifile' require 'tempfile' class Repo_generic def initialize(name, type, attribs = {}) @name = name @type = type @attribs = attribs.dup @incomplete = false end def enabled? return @attribs['enabled'].to_i != 0 end def autorefresh? return @attribs['autorefresh'].to_i != 0 end def id return @handle ? @handle.id : 0 end def calc_cookie_fp(f) chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256) chksum.add("1.1") chksum.add_fp(f) return chksum.raw end def calc_cookie_file(filename) chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256) chksum.add("1.1") chksum.add_stat(filename) return chksum.raw end def calc_cookie_ext(f, cookie) chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256) chksum.add("1.1") chksum.add(cookie) chksum.add_fstat(f.fileno) return chksum.raw() end def cachepath(ext = nil) path = @name.sub(/^\./, '_') path += ext ? "_#{ext}.solvx" : '.solv' return '/var/cache/solv/' + path.gsub(/\//, '_') end def load(pool) @handle = pool.add_repo(@name) @handle.appdata = self @handle.priority = 99 - @attribs['priority'].to_i if @attribs['priority'] dorefresh = autorefresh? if dorefresh begin s = File.stat(cachepath) dorefresh = false if s && (@attribs['metadata_expire'].to_i == -1 || Time.now - s.mtime < @attribs['metadata_expire'].to_i) rescue SystemCallError end end @cookie = nil @extcookie = nil if !dorefresh && usecachedrepo(nil) puts "repo: '#{@name}' cached" return true end return false end def load_ext(repodata) return false end def download(file, uncompress, chksum, markincomplete = false) url = @attribs['baseurl'] if !url puts "%{@name}: no baseurl" return nil end url = url.sub(/\/$/, '') + "/#{file}" f = Tempfile.new('rbsolv') f.unlink st = system('curl', '-f', '-s', '-L', '-o', "/dev/fd/" + f.fileno.to_s, '--', url) return nil if f.stat.size == 0 && (st || !chksum) if !st puts "#{file}: download error #{$? >> 8}" @incomplete = true if markincomplete return nil end if chksum fchksum = Solv::Chksum.new(chksum.type) fchksum.add_fd(f.fileno) if !fchksum == chksum puts "#{file}: checksum error" @incomplete = true if markincomplete return nil end end rf = nil if uncompress rf = Solv::xfopen_fd(file, f.fileno) else rf = Solv::xfopen_fd('', f.fileno) end f.close return rf end def usecachedrepo(ext, mark = false) cookie = ext ? @extcookie : @cookie begin repopath = cachepath(ext) f = File.new(repopath, "r") f.sysseek(-32, IO::SEEK_END) fcookie = f.sysread(32) return false if fcookie.length != 32 return false if cookie && fcookie != cookie if !ext && @type != 'system' f.sysseek(-32 * 2, IO::SEEK_END) fextcookie = f.sysread(32) return false if fextcookie.length != 32 end f.sysseek(0, IO::SEEK_SET) nf = Solv::xfopen_fd('', f.fileno) f.close flags = ext ? Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES : 0 flags |= Solv::Repo::REPO_LOCALPOOL if ext && ext != 'DL' if ! @handle.add_solv(nf, flags) nf.close return false end nf.close() @cookie = fcookie unless ext @extcookie = fextcookie if !ext && @type != 'system' now = Time.now begin File::utime(now, now, repopath) if mark rescue SystemCallError end return true rescue SystemCallError return false end return true end def writecachedrepo(ext, repodata = nil) return if @incomplete begin Dir::mkdir("/var/cache/solv", 0755) unless FileTest.directory?("/var/cache/solv") f = Tempfile.new('.newsolv-', '/var/cache/solv') f.chmod(0444) sf = Solv::xfopen_fd('', f.fileno) if !repodata @handle.write(sf) elsif ext repodata.write(sf) else @handle.write_first_repodata(sf) end sf.close f.sysseek(0, IO::SEEK_END) if @type != 'system' && !ext @extcookie = calc_cookie_ext(f, @cookie) unless @extcookie f.syswrite(@extcookie) end f.syswrite(ext ? @extcookie : @cookie) f.close if @handle.iscontiguous? sf = Solv::xfopen(f.path) if sf if !ext @handle.empty() abort("internal error, cannot reload solv file") unless @handle.add_solv(sf, repodata ? 0 : Solv::Repo::SOLV_ADD_NO_STUBS) else repodata.extend_to_repo() flags = Solv::Repo::REPO_EXTEND_SOLVABLES flags |= Solv::Repo::REPO_LOCALPOOL if ext != 'DL' repodata.add_solv(sf, flags) end sf.close end end File.rename(f.path, cachepath(ext)) f.unlink return true rescue SystemCallError return false end end def updateaddedprovides(addedprovides) return if @incomplete return unless @handle && !@handle.isempty? repodata = @handle.first_repodata() return unless repodata oldaddedprovides = repodata.lookup_idarray(Solv::SOLVID_META, Solv::REPOSITORY_ADDEDFILEPROVIDES) return if (oldaddedprovides | addedprovides) == oldaddedprovides for id in addedprovides repodata.add_idarray(Solv::SOLVID_META, Solv::REPOSITORY_ADDEDFILEPROVIDES, id) end repodata.internalize() writecachedrepo(nil, repodata) end def packagespath() return '' end @@langtags = { Solv::SOLVABLE_SUMMARY => Solv::REPOKEY_TYPE_STR, Solv::SOLVABLE_DESCRIPTION => Solv::REPOKEY_TYPE_STR, Solv::SOLVABLE_EULA => Solv::REPOKEY_TYPE_STR, Solv::SOLVABLE_MESSAGEINS => Solv::REPOKEY_TYPE_STR, Solv::SOLVABLE_MESSAGEDEL => Solv::REPOKEY_TYPE_STR, Solv::SOLVABLE_CATEGORY => Solv::REPOKEY_TYPE_ID, } def add_ext_keys(ext, repodata, h) if ext == 'DL' repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOSITORY_DELTAINFO) repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_FLEXARRAY) elsif ext == 'DU' repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_DISKUSAGE) repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRNUMNUMARRAY) elsif ext == 'FL' repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_FILELIST) repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRSTRARRAY) else @@langtags.sort.each do |langid, langtype| repodata.add_idarray(h, Solv::REPOSITORY_KEYS, @handle.pool.id2langid(langid, ext, true)) repodata.add_idarray(h, Solv::REPOSITORY_KEYS, langtype) end end end end class Repo_rpmmd < Repo_generic def find(what) di = @handle.Dataiterator_meta(Solv::REPOSITORY_REPOMD_TYPE, what, Solv::Dataiterator::SEARCH_STRING) di.prepend_keyname(Solv::REPOSITORY_REPOMD) for d in di dp = d.parentpos() filename = dp.lookup_str(Solv::REPOSITORY_REPOMD_LOCATION) next unless filename checksum = dp.lookup_checksum(Solv::REPOSITORY_REPOMD_CHECKSUM) if !checksum puts "no #{filename} checksum!" return nil, nil end return filename, checksum end return nil, nil end def load(pool) return true if super(pool) print "rpmmd repo '#{@name}: " f = download("repodata/repomd.xml", false, nil, nil) if !f puts "no repomd.xml file, skipped" @handle.free(true) @handle = nil return false end @cookie = calc_cookie_fp(f) if usecachedrepo(nil, true) puts "cached" f.close return true end @handle.add_repomdxml(f, 0) f.close puts "fetching" filename, filechksum = find('primary') if filename f = download(filename, true, filechksum, true) if f @handle.add_rpmmd(f, nil, 0) f.close end return false if @incomplete end filename, filechksum = find('updateinfo') if filename f = download(filename, true, filechksum, true) if f @handle.add_updateinfoxml(f, 0) f.close end end add_exts() writecachedrepo(nil) @handle.create_stubs() return true end def add_ext(repodata, what, ext) filename, filechksum = find(what) filename, filechksum = find('prestodelta') if !filename && what == 'deltainfo' return unless filename h = repodata.new_handle() repodata.set_poolstr(h, Solv::REPOSITORY_REPOMD_TYPE, what) repodata.set_str(h, Solv::REPOSITORY_REPOMD_LOCATION, filename) repodata.set_checksum(h, Solv::REPOSITORY_REPOMD_CHECKSUM, filechksum) add_ext_keys(ext, repodata, h) repodata.add_flexarray(Solv::SOLVID_META, Solv::REPOSITORY_EXTERNAL, h) end def add_exts repodata = @handle.add_repodata(0) repodata.extend_to_repo() add_ext(repodata, 'deltainfo', 'DL') add_ext(repodata, 'filelists', 'FL') repodata.internalize() end def load_ext(repodata) repomdtype = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_TYPE) if repomdtype == 'filelists' ext = 'FL' elsif repomdtype == 'deltainfo' ext = 'DL' else return false end print "[#{@name}:#{ext}: " STDOUT.flush if usecachedrepo(ext) puts "cached]\n" return true end puts "fetching]\n" filename = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_LOCATION) filechksum = repodata.lookup_checksum(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_CHECKSUM) f = download(filename, true, filechksum) return false unless f if ext == 'FL' @handle.add_rpmmd(f, 'FL', Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES|Solv::Repo::REPO_LOCALPOOL) elsif ext == 'DL' @handle.add_deltainfoxml(f, Solv::Repo::REPO_USE_LOADING) end f.close writecachedrepo(ext, repodata) return true end end class Repo_susetags < Repo_generic def find(what) di = @handle.Dataiterator_meta(Solv::SUSETAGS_FILE_NAME, what, Solv::Dataiterator::SEARCH_STRING) di.prepend_keyname(Solv::SUSETAGS_FILE) for d in di dp = d.parentpos() checksum = dp.lookup_checksum(Solv::SUSETAGS_FILE_CHECKSUM) return what, checksum end return nil, nil end def load(pool) return true if super(pool) print "susetags repo '#{@name}: " f = download("content", false, nil, nil) if !f puts "no content file, skipped" @handle.free(true) @handle = nil return false end @cookie = calc_cookie_fp(f) if usecachedrepo(nil, true) puts "cached" f.close return true end @handle.add_content(f, 0) f.close puts "fetching" defvendorid = @handle.meta.lookup_id(Solv::SUSETAGS_DEFAULTVENDOR) descrdir = @handle.meta.lookup_str(Solv::SUSETAGS_DESCRDIR) descrdir = "suse/setup/descr" unless descrdir (filename, filechksum) = find('packages.gz') (filename, filechksum) = find('packages') unless filename if filename f = download("#{descrdir}/#{filename}", true, filechksum, true) if f @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::SUSETAGS_RECORD_SHARES) f.close (filename, filechksum) = find('packages.en.gz') (filename, filechksum) = find('packages.en') unless filename if filename f = download("#{descrdir}/#{filename}", true, filechksum, true) if f @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::REPO_REUSE_REPODATA|Solv::Repo::REPO_EXTEND_SOLVABLES) f.close end end @handle.internalize() end end add_exts() writecachedrepo(nil) @handle.create_stubs() return true end def add_ext(repodata, what, ext) (filename, filechksum) = find(what) h = repodata.new_handle() repodata.set_str(h, Solv::SUSETAGS_FILE_NAME, filename) repodata.set_checksum(h, Solv::SUSETAGS_FILE_CHECKSUM, filechksum) add_ext_keys(ext, repodata, h) repodata.add_flexarray(Solv::SOLVID_META, Solv::REPOSITORY_EXTERNAL, h) end def add_exts repodata = @handle.add_repodata(0) di = @handle.Dataiterator_meta(Solv::SUSETAGS_FILE_NAME, nil, 0) di.prepend_keyname(Solv::SUSETAGS_FILE) for d in di filename = d.str next unless filename && filename =~ /^packages\.(..)(?:\..*)$/ next if $1 == 'en' || $1 == 'gz' add_ext(repodata, filename, $1) end repodata.internalize() end def load_ext(repodata) filename = repodata.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME) ext = filename[9,2] print "[#{@name}:#{ext}: " STDOUT.flush if usecachedrepo(ext) puts "cached]\n" return true end puts "fetching]\n" defvendorid = @handle.meta.lookup_id(Solv::SUSETAGS_DEFAULTVENDOR) descrdir = @handle.meta.lookup_str(Solv::SUSETAGS_DESCRDIR) descrdir = "suse/setup/descr" unless descrdir filechksum = repodata.lookup_checksum(Solv::SOLVID_META, Solv::SUSETAGS_FILE_CHECKSUM) f = download("#{descrdir}/#{filename}", true, filechksum) return false unless f flags = Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES flags |= Solv::Repo::REPO_LOCALPOOL if ext != 'DL' @handle.add_susetags(f, defvendorid, ext, flags) f.close writecachedrepo(ext, repodata) return true end def packagespath() datadir = @handle.meta.lookup_str(Solv::SUSETAGS_DATADIR) datadir = "suse" unless datadir return datadir + '/' end end class Repo_unknown < Repo_generic def load(pool) puts "unsupported repo '#{@name}: skipped" return false end end class Repo_system < Repo_generic def load(pool) @handle = pool.add_repo(@name) @handle.appdata = self pool.installed = @handle print "rpm database: " @cookie = calc_cookie_file("/var/lib/rpm/Packages") if usecachedrepo(nil) puts "cached" return true end puts "reading" if @handle.respond_to? :add_products @handle.add_products("/etc/products.d", Solv::Repo::REPO_NO_INTERNALIZE) end f = Solv::xfopen(cachepath()) @handle.add_rpmdb_reffp(f, Solv::Repo::REPO_REUSE_REPODATA) f.close if f writecachedrepo(nil) return true end end args = ARGV cmd = args.shift cmdabbrev = { 'ls' => 'list', 'in' => 'install', 'rm' => 'erase', 've' => 'verify', 'se' => 'search' } cmd = cmdabbrev[cmd] if cmdabbrev.has_key?(cmd) cmdactionmap = { 'install' => Solv::Job::SOLVER_INSTALL, 'erase' => Solv::Job::SOLVER_ERASE, 'up' => Solv::Job::SOLVER_UPDATE, 'dup' => Solv::Job::SOLVER_DISTUPGRADE, 'verify' => Solv::Job::SOLVER_VERIFY, 'list' => 0, 'info' => 0, } repos = [] reposdirs = [] if FileTest.directory?('/etc/zypp/repos.d') reposdirs = [ '/etc/zypp/repos.d' ] else reposdirs = [ '/etc/yum/repos.d' ] end for reposdir in reposdirs do next unless FileTest.directory?(reposdir) for reponame in Dir["#{reposdir}/*.repo"].sort do cfg = IniFile.load(reponame) cfg.each_section do |ali| repoattr = { 'alias' => ali, 'enabled' => 0, 'priority' => 99, 'autorefresh' => 1, 'type' => 'rpm-md', 'metadata_expire' => 900} repoattr.update(cfg[ali]) if repoattr['type'] == 'rpm-md' repo = Repo_rpmmd.new(ali, 'repomd', repoattr) elsif repoattr['type'] == 'yast2' repo = Repo_susetags.new(ali, 'susetags', repoattr) else repo = Repo_unknown.new(ali, 'unknown', repoattr) end repos.push(repo) end end end pool = Solv::Pool.new() pool.setarch() pool.set_loadcallback { |repodata| repo = repodata.repo.appdata repo ? repo.load_ext(repodata) : false } sysrepo = Repo_system.new('@System', 'system') sysrepo.load(pool) for repo in repos repo.load(pool) if repo.enabled? end if cmd == 'search' pool.createwhatprovides() sel = pool.Selection for di in pool.Dataiterator(Solv::SOLVABLE_NAME, args[0], Solv::Dataiterator::SEARCH_SUBSTRING | Solv::Dataiterator::SEARCH_NOCASE) sel.add_raw(Solv::Job::SOLVER_SOLVABLE, di.solvid) end for s in sel.solvables puts "- #{s.str} [#{s.repo.name}]: #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}" end exit end abort("unknown command '#{cmd}'\n") unless cmdactionmap.has_key?(cmd) addedprovides = pool.addfileprovides_queue() if !addedprovides.empty? sysrepo.updateaddedprovides(addedprovides) for repo in repos repo.updateaddedprovides(addedprovides) end end pool.createwhatprovides() jobs = [] for arg in args flags = Solv::Selection::SELECTION_NAME | Solv::Selection::SELECTION_PROVIDES | Solv::Selection::SELECTION_GLOB flags |= Solv::Selection::SELECTION_CANON | Solv::Selection::SELECTION_DOTARCH | Solv::Selection::SELECTION_REL if arg =~ /^\// flags |= Solv::Selection::SELECTION_FILELIST flags |= Solv::Selection::SELECTION_INSTALLED_ONLY if cmd == 'erase' end sel = pool.select(arg, flags) if sel.isempty? sel = pool.select(arg, flags | Solv::Selection::SELECTION_NOCASE) puts "[ignoring case for '#{arg}']" unless sel.isempty? end puts "[using file list match for '#{arg}']" if sel.flags & Solv::Selection::SELECTION_FILELIST != 0 puts "[using capability match for '#{arg}']" if sel.flags & Solv::Selection::SELECTION_PROVIDES != 0 jobs += sel.jobs(cmdactionmap[cmd]) end if jobs.empty? && (cmd == 'up' || cmd == 'dup' || cmd == 'verify') sel = pool.Selection_all() jobs += sel.jobs(cmdactionmap[cmd]) end abort("no package matched.") if jobs.empty? if cmd == 'list' || cmd == 'info' for job in jobs for s in job.solvables() if cmd == 'info' puts "Name: #{s.str}" puts "Repo: #{s.repo.name}" puts "Summary: #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}" str = s.lookup_str(Solv::SOLVABLE_URL) puts "Url: #{str}" if str str = s.lookup_str(Solv::SOLVABLE_LICENSE) puts "License: #{str}" if str puts "Description:\n#{s.lookup_str(Solv::SOLVABLE_DESCRIPTION)}" puts else puts " - #{s.str} [#{s.repo.name}]" puts " #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}" end end end exit end for job in jobs job.how ^= Solv::Job::SOLVER_UPDATE ^ Solv::Job::SOLVER_INSTALL if cmd == 'up' and job.isemptyupdate? end solver = pool.Solver solver.set_flag(Solv::Solver::SOLVER_FLAG_SPLITPROVIDES, 1) solver.set_flag(Solv::Solver::SOLVER_FLAG_ALLOW_UNINSTALL, 1) if cmd == 'erase' #pool.set_debuglevel(1) while true problems = solver.solve(jobs) break if problems.empty? for problem in problems puts "Problem #{problem.id}/#{problems.count}:" puts problem solutions = problem.solutions for solution in solutions puts " Solution #{solution.id}:" elements = solution.elements(true) for element in elements puts " - #{element.str}" end puts end sol = nil while true print "Please choose a solution: " STDOUT.flush sol = STDIN.gets.strip break if sol == 's' || sol == 'q' break if sol =~ /^\d+$/ && sol.to_i >= 1 && sol.to_i <= solutions.length end next if sol == 's' abort if sol == 'q' solution = solutions[sol.to_i - 1] for element in solution.elements newjob = element.Job() if element.type == Solv::Solver::SOLVER_SOLUTION_JOB jobs[element.jobidx] = newjob else jobs.push(newjob) if newjob && !jobs.include?(newjob) end end end end trans = solver.transaction solver = nil if trans.isempty? puts "Nothing to do." exit end puts "\nTransaction summary:\n" for cl in trans.classify(Solv::Transaction::SOLVER_TRANSACTION_SHOW_OBSOLETES | Solv::Transaction::SOLVER_TRANSACTION_OBSOLETE_IS_UPGRADE) if cl.type == Solv::Transaction::SOLVER_TRANSACTION_ERASE puts "#{cl.count} erased packages:" elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_INSTALL puts "#{cl.count} installed packages:" elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_REINSTALLED puts "#{cl.count} reinstalled packages:" elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED puts "#{cl.count} downgraded packages:" elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_CHANGED puts "#{cl.count} changed packages:" elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED puts "#{cl.count} upgraded packages:" elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_VENDORCHANGE puts "#{cl.count} vendor changes from '#{cl.fromstr}' to '#{cl.tostr}':" elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_ARCHCHANGE puts "#{cl.count} arch changes from '#{cl.fromstr}' to '#{cl.tostr}':" else next end for p in cl.solvables if cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED || cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED puts " - #{p.str} -> #{trans.othersolvable(p).str}" else puts " - #{p.str}" end end puts end puts "install size change: #{trans.calc_installsizechange()} K\n\n" while true print("OK to continue (y/n)? ") STDOUT.flush yn = STDIN.gets.strip break if yn == 'y' abort if yn == 'n' || yn == 'q' end newpkgs = trans.newsolvables() newpkgsfp = {} if !newpkgs.empty? downloadsize = 0 for p in newpkgs downloadsize += p.lookup_num(Solv::SOLVABLE_DOWNLOADSIZE) end puts "Downloading #{newpkgs.length} packages, #{downloadsize / 1024} K" for p in newpkgs repo = p.repo.appdata location, medianr = p.lookup_location() next unless location location = repo.packagespath + location chksum = p.lookup_checksum(Solv::SOLVABLE_CHECKSUM) f = repo.download(location, false, chksum) abort("\n#{@name}: #{location} not found in repository\n") unless f newpkgsfp[p.id] = f print "." STDOUT.flush() end puts end puts "Committing transaction:" puts trans.order() for p in trans.steps steptype = trans.steptype(p, Solv::Transaction::SOLVER_TRANSACTION_RPM_ONLY) if steptype == Solv::Transaction::SOLVER_TRANSACTION_ERASE puts "erase #{p.str}" next unless p.lookup_num(Solv::RPM_RPMDBID) evr = p.evr.sub(/^[0-9]+:/, '') system('rpm', '-e', '--nodeps', '--nodigest', '--nosignature', "#{p.name}-#{evr}.#{p.arch}") || abort("rpm failed: #{$? >> 8}") elsif (steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL || steptype == Solv::Transaction::SOLVER_TRANSACTION_MULTIINSTALL) puts "install #{p.str}" f = newpkgsfp.delete(p.id) next unless f mode = steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL ? '-U' : '-i' f.cloexec(0) system('rpm', mode, '--force', '--nodeps', '--nodigest', '--nosignature', "/dev/fd/#{f.fileno().to_s}") || abort("rpm failed: #{$? >> 8}") f.close end end