module RPM CallbackData = Struct.new(:type, :key, :package, :amount, :total) do def to_s "#{type} #{key} #{package} #{amount} #{total}" end end class Transaction def self.release(ptr) RPM::C.rpmtsFree(ptr) end def initialize(opts = {}) # http://markmail.org/message/ypsiqxop442p7rzz # The key pointer needs to stay valid during commit # so we keep a reference to them mapping from # object_id to ruby object. @keys = {} opts[:root] ||= '/' @ptr = ::FFI::AutoPointer.new(RPM::C.rpmtsCreate, Transaction.method(:release)) RPM::C.rpmtsSetRootDir(@ptr, opts[:root]) end # @return [RPM::MatchIterator] Creates an iterator for +tag+ and +val+ def init_iterator(tag, val) raise TypeError if val && !val.is_a?(String) it_ptr = RPM::C.rpmtsInitIterator(@ptr, tag.nil? ? 0 : tag, val, 0) raise "Can't init iterator for [#{tag}] -> '#{val}'" if it_ptr.null? MatchIterator.from_ptr(it_ptr) end # @visibility private attr_reader :ptr # # @yield [Package] Called for each match # @param [Number] key RPM tag key # @param [String] val Value to match # @example # RPM.transaction do |t| # t.each_match(RPM::TAG_ARCH, "x86_64") do |pkg| # puts pkg.name # end # end # def each_match(key, val, &block) it = init_iterator(key, val) return it unless block_given? it.each(&block) end # # @yield [Package] Called for each package in the database # @example # db.each do |pkg| # puts pkg.name # end # def each(&block) each_match(0, nil, &block) end # Add a install operation to the transaction # @param [Package] pkg Package to install # @param [String] key e.g. filename where to install from def install(pkg, key) install_element(pkg, key, upgrade: false) end # Add an upgrade operation to the transaction # @param [Package] pkg Package to upgrade # @param [String] key e.g. filename where to install from def upgrade(pkg, key) install_element(pkg, key, upgrade: true) end # Add a delete operation to the transaction # @param [String, Package, Dependency] pkg Package to delete def delete(pkg) iterator = case pkg when Package pkg[:sigmd5] ? each_match(:sigmd5, pkg[:sigmd5]) : each_match(:label, pkg[:label]) when String each_match(:label, pkg) when Dependency each_match(:label, pkg.name).set_iterator_version(pkg.version) else raise TypeError, 'illegal argument type' end iterator.each do |header| ret = RPM::C.rpmtsAddEraseElement(@ptr, header.ptr, iterator.offset) raise "Error while adding erase/#{pkg} to transaction" if ret != 0 end end # Sets the root directory for this transaction # @param [String] root directory def root_dir=(dir) rc = RPM::C.rpmtsSetRootDir(@ptr, dir) raise "Can't set #{dir} as root directory" if rc < 0 end # @return [String ] the root directory for this transaction def root_dir RPM::C.rpmtsRootDir(@ptr) end def flags=(fl) RPM::C.rpmtsSetFlags(@ptr, fl) end def flags RPM::C.rpmtsFlags(@ptr) end # Determine package order in the transaction according to dependencies # # The final order ends up as installed packages followed by removed # packages, with packages removed for upgrades immediately following # the new package to be installed. # # @returns [Fixnum] no. of (added) packages that could not be ordered def order RPM::C.rpmtsOrder(@ptr) end # Free memory needed only for dependency checks and ordering. def clean RPM::C.rpmtsClean(@ptr) end def check rc = RPM::C.rpmtsCheck(@ptr) probs = RPM::C.rpmtsProblems(@ptr) return if rc < 0 begin psi = RPM::C.rpmpsInitIterator(probs) while RPM::C.rpmpsNextIterator(psi) >= 0 problem = Problem.from_ptr(RPM::C.rpmpsGetProblem(psi)) yield problem end ensure RPM::C.rpmpsFree(probs) end end # Performs the transaction. # @param [Number] flag Transaction flags, default +RPM::TRANS_FLAG_NONE+ # @param [Number] filter Transaction filter, default +RPM::PROB_FILTER_NONE+ # @example # transaction.commit # You can supply your own callback # @example # transaction.commit do |data| # end # end # @yield [CallbackData] sig Transaction progress def commit flags = RPM::C::TransFlags[:none] callback = proc do |hdr, type, amount, total, key_ptr, data_ignored| key_id = key_ptr.address key = @keys.include?(key_id) ? @keys[key_id] : nil if block_given? package = hdr.null? ? nil : Package.new(hdr) data = CallbackData.new(type, key, package, amount, total) yield(data) else RPM::C.rpmShowProgress(hdr, type, amount, total, key, data_ignored) end end # We create a callback to pass to the C method and we # call the user supplied callback from there # # The C callback expects you to return a file handle, # We expect from the user to get a File, which we # then convert to a file handle to return. callback = proc do |hdr, type, amount, total, key_ptr, data_ignored| key_id = key_ptr.address key = @keys.include?(key_id) ? @keys[key_id] : nil if block_given? package = hdr.null? ? nil : Package.new(hdr) data = CallbackData.new(type, key, package, amount, total) ret = yield(data) # For OPEN_FILE we need to do some type conversion # for certain callback types we need to do some case type when :inst_open_file # For :inst_open_file the user callback has to # return the open file unless ret.is_a?(::File) raise TypeError, "illegal return value type #{ret.class}. Expected File." end fdt = RPM::C.fdDup(ret.to_i) if fdt.null? || RPM::C.Ferror(fdt) != 0 raise "Can't use opened file #{data.key}: #{RPM::C.Fstrerror(fdt)}" RPM::C.Fclose(fdt) unless fdt.nil? else fdt = RPM::C.fdLink(fdt) @fdt = fdt end # return the (RPM type) file handle fdt when :inst_close_file fdt = @fdt RPM::C.Fclose(fdt) @fdt = nil else ret end else # No custom callback given, use the default to show progress RPM::C.rpmShowProgress(hdr, type, amount, total, key, data_ignored) end end rc = RPM::C.rpmtsSetNotifyCallback(@ptr, callback, nil) raise "Can't set commit callback" if rc != 0 rc = RPM::C.rpmtsRun(@ptr, nil, :none) raise "#{self}: #{RPM::C.rpmlogMessage}" if rc < 0 if rc > 0 ps = RPM::C.rpmtsProblems(@ptr) psi = RPM::C.rpmpsInitIterator(ps) while RPM::C.rpmpsNextIterator(psi) >= 0 problem = Problem.from_ptr(RPM::C.rpmpsGetProblem(psi)) STDERR.puts problem end RPM::C.rpmpsFree(ps) end end # @return [DB] the database associated with this transaction def db RPM::DB.new(self) end private # @param [Package] pkg package to install # @param [String] key e.g. filename where to install from # @param opts options # @option :upgrade Upgrade packages if true def install_element(pkg, key, opts = {}) raise TypeError, 'illegal argument type' unless pkg.is_a?(RPM::Package) raise ArgumentError, "#{self}: key '#{key}' must be unique" if @keys.include?(key.object_id) # keep a reference to the key as rpmtsAddInstallElement will keep a copy # of the passed pointer (we pass the object_id) @keys[key.object_id] = key ret = RPM::C.rpmtsAddInstallElement(@ptr, pkg.ptr, FFI::Pointer.new(key.object_id), opts[:upgrade] ? 1 : 0, nil) raise RuntimeError if ret != 0 nil end end end