use "isemptyupdate" method to check for updates with no matching installed package
[platform/upstream/libsolv.git] / examples / rbsolv
1 #!/usr/bin/ruby
2
3 require 'solv'
4 require 'rubygems'
5 require 'inifile'
6 require 'tempfile'
7
8 class Repo_generic
9   def initialize(name, type, attribs = {})
10     @name = name
11     @type = type
12     @attribs = attribs.dup
13     @incomplete = false
14   end
15
16   def enabled?
17     return @attribs['enabled'].to_i != 0
18   end
19
20   def autorefresh?
21     return @attribs['autorefresh'].to_i != 0
22   end
23
24   def id
25     return @handle ? @handle.id : 0
26   end
27
28   def calc_cookie_fp(f)
29     chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
30     chksum.add("1.1")
31     chksum.add_fp(f)
32     return chksum.raw
33   end
34
35   def calc_cookie_file(filename)
36     chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
37     chksum.add("1.1")
38     chksum.add_stat(filename)
39     return chksum.raw
40   end
41
42   def calc_cookie_ext(f, cookie)
43     chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
44     chksum.add("1.1")
45     chksum.add(cookie)
46     chksum.add_fstat(f.fileno)
47     extcookie = chksum.raw()
48     extcookie[0] = 1 if extcookie[0] == 0
49     return extcookie
50   end
51
52   def cachepath(ext = nil)
53     path = @name.sub(/^\./, '_')
54     path += ext ? "_#{ext}.solvx" : '.solv'
55     return '/var/cache/solv/' + path.gsub(/\//, '_')
56   end
57
58   def load(pool)
59     @handle = pool.add_repo(@name)
60     @handle.appdata = self
61     @handle.priority = 99 - @attribs['priority'].to_i if @attribs['priority']
62     dorefresh = autorefresh?
63     if dorefresh
64       begin
65         s = File.stat(cachepath)
66         dorefresh = false if s && (@attribs['metadata_expire'].to_i == -1 || Time.now - s.mtime < @attribs['metadata_expire'].to_i)
67       rescue SystemCallError
68       end
69     end
70     @cookie = nil
71     if !dorefresh && usecachedrepo(nil)
72       puts "repo: '#{@name}' cached"
73       return true
74     end
75     return false
76   end
77
78   def load_ext(repodata)
79     return false
80   end
81
82   def download(file, uncompress, chksum, markincomplete = false)
83     url = @attribs['baseurl']
84     if !url
85       puts "%{@name}: no baseurl"
86       return nil
87     end
88     url = url.sub(/\/$/, '') + "/#{file}"
89     f =  Tempfile.new('rbsolv')
90     f.unlink
91     st = system('curl', '-f', '-s', '-L', '-o', "/dev/fd/" + f.fileno.to_s, '--', url)
92     return nil if f.stat.size == 0 && (st || !chksum)
93     if !st
94         puts "#{file}: download error #{$? >> 8}"
95         @incomplete = true if markincomplete
96         return nil
97     end
98     if chksum
99       fchksum = Solv::Chksum.new(chksum.type)
100       fchksum.add_fd(f.fileno)
101       if !fchksum == chksum
102         puts "#{file}: checksum error"
103         @incomplete = true if markincomplete
104         return nil
105       end
106     end
107     rf = nil
108     if uncompress
109       rf = Solv::xfopen_fd(file, f.fileno)
110     else
111       rf = Solv::xfopen_fd('', f.fileno)
112     end
113     f.close
114     return rf
115   end
116
117   def usecachedrepo(ext, mark = false)
118     cookie = ext ? @extcookie : @cookie
119     begin
120       repopath = cachepath(ext)
121       f = File.new(repopath, "r")
122       f.sysseek(-32, IO::SEEK_END)
123       fcookie = f.sysread(32)
124       return false if fcookie.length != 32
125       return false if cookie && fcookie != cookie
126       if !ext && @type != 'system'
127         f.sysseek(-32 * 2, IO::SEEK_END)
128         fextcookie = f.sysread(32)
129         return false if fextcookie.length != 32
130       end
131       f.sysseek(0, IO::SEEK_SET)
132       nf = Solv::xfopen_fd('', f.fileno)
133       f.close
134       flags = ext ? Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES : 0
135       flags |= Solv::Repo::REPO_LOCALPOOL if ext && ext != 'DL'
136       if ! @handle.add_solv(nf, flags)
137         nf.close
138         return false
139       end
140       nf.close()
141       @cookie = fcookie unless ext
142       @extcookie = fextcookie if !ext && @type != 'system'
143       now = Time.now
144       begin
145         File::utime(now, now, repopath) if mark
146       rescue SystemCallError
147       end
148       return true
149     rescue SystemCallError
150       return false
151     end
152     return true
153   end
154
155   def writecachedrepo(ext, info = nil)
156     return if @incomplete
157     begin
158       Dir::mkdir("/var/cache/solv", 0755) unless FileTest.directory?("/var/cache/solv")
159       f =  Tempfile.new('.newsolv-', '/var/cache/solv')
160       f.chmod(0444)
161       sf = Solv::xfopen_fd('', f.fileno)
162       if !info
163         @handle.write(sf)
164       elsif ext
165         info.write(sf)
166       else
167         @handle.write_first_repodata(sf)
168       end
169       sf.close
170       f.sysseek(0, IO::SEEK_END)
171       if @type != 'system' && !ext
172         @extcookie = calc_cookie_ext(f, @cookie) unless @extcookie
173         f.syswrite(@extcookie)
174       end
175       f.syswrite(ext ? @extcookie : @cookie)
176       f.close
177       if @handle.iscontiguous?
178         sf = Solv::xfopen(f.path)
179         if sf
180           if !ext
181             @handle.empty()
182             abort("internal error, cannot reload solv file") unless @handle.add_solv(sf, Solv::Repo::SOLV_ADD_NO_STUBS)
183           else
184             info.extend_to_repo()
185             info.add_solv(sf, Solv::Repo::REPO_EXTEND_SOLVABLES)
186           end
187           sf.close
188         end
189       end
190       File.rename(f.path, cachepath(ext))
191       f.unlink
192       return true
193     rescue SystemCallError
194       return false
195     end
196   end
197
198   def updateaddedprovides(addedprovides)
199     return if @incomplete
200     return unless @handle && !@handle.isempty?
201     repodata = @handle.first_repodata()
202     return unless repodata
203     oldaddedprovides = repodata.lookup_idarray(Solv::SOLVID_META, Solv::REPOSITORY_ADDEDFILEPROVIDES)
204     return if (oldaddedprovides | addedprovides) == oldaddedprovides
205     for id in addedprovides
206       repodata.add_idarray(Solv::SOLVID_META, Solv::REPOSITORY_ADDEDFILEPROVIDES, id)
207     end
208     repodata.internalize()
209     writecachedrepo(nil, repodata)
210   end
211
212   def packagespath()
213     return ''
214   end
215 end
216
217 class Repo_rpmmd < Repo_generic
218
219   def find(what)
220     di = @handle.Dataiterator(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_TYPE, what, Solv::Dataiterator::SEARCH_STRING)
221     di.prepend_keyname(Solv::REPOSITORY_REPOMD)
222     for d in di
223       dp = d.parentpos()
224       filename = dp.lookup_str(Solv::REPOSITORY_REPOMD_LOCATION)
225       next unless filename
226       checksum = dp.lookup_checksum(Solv::REPOSITORY_REPOMD_CHECKSUM)
227       if !checksum
228         puts "no #{filename} checksum!"
229         return nil, nil
230       end
231       return filename, checksum
232     end
233     return nil, nil
234   end
235
236   def load(pool)
237     return true if super(pool)
238     print "rpmmd repo '#{@name}: "
239     f = download("repodata/repomd.xml", false, nil, nil)
240     if !f
241       puts "no repomd.xml file, skipped"
242       @handle.free(true)
243       @handle = nil
244       return false
245     end
246     @cookie = calc_cookie_fp(f)
247     if usecachedrepo(nil, true)
248       puts "cached"
249       f.close
250       return true
251     end
252     @handle.add_repomdxml(f, 0)
253     f.close
254     puts "fetching"
255     filename, filechksum = find('primary')
256     if filename
257       f = download(filename, true, filechksum, true)
258       if f
259         @handle.add_rpmmd(f, nil, 0)
260         f.close
261       end
262       return false if @incomplete
263     end
264     filename, filechksum = find('updateinfo')
265     if filename
266       f = download(filename, true, filechksum, true)
267       if f
268         @handle.add_updateinfoxml(f, 0)
269         f.close
270       end
271     end
272     add_exts()
273     writecachedrepo(nil)
274     @handle.create_stubs()
275     return true
276   end
277
278   def add_ext(repodata, what, ext)
279     filename, filechksum = find(what)
280     filename, filechksum = find('prestodelta') if !filename && what == 'deltainfo'
281     return unless filename
282     h = repodata.new_handle()
283     repodata.set_poolstr(h, Solv::REPOSITORY_REPOMD_TYPE, what)
284     repodata.set_str(h, Solv::REPOSITORY_REPOMD_LOCATION, filename)
285     repodata.set_checksum(h, Solv::REPOSITORY_REPOMD_CHECKSUM, filechksum)
286     if ext == 'DL'
287       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOSITORY_DELTAINFO)
288       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_FLEXARRAY)
289     elsif ext == 'FL'
290       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_FILELIST)
291       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRSTRARRAY)
292     end
293     repodata.add_flexarray(Solv::SOLVID_META, Solv::REPOSITORY_EXTERNAL, h)
294   end
295
296   def add_exts
297     repodata = @handle.add_repodata(0)
298     add_ext(repodata, 'deltainfo', 'DL')
299     add_ext(repodata, 'filelists', 'FL')
300     repodata.internalize()
301   end
302
303   def load_ext(repodata)
304     repomdtype = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_TYPE)
305     if repomdtype == 'filelists'
306       ext = 'FL'
307     elsif repomdtype == 'deltainfo'
308       ext = 'DL'
309     else
310       return false
311     end
312     print "[#{@name}:#{ext}: "
313     STDOUT.flush
314     if usecachedrepo(ext)
315       puts "cached]\n"
316       return true
317     end
318     puts "fetching]\n"
319     filename = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_LOCATION)
320     filechksum = repodata.lookup_checksum(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_CHECKSUM)
321     f = download(filename, true, filechksum)
322     return false unless f
323     if ext == 'FL'
324       @handle.add_rpmmd(f, 'FL', Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES)
325     elsif ext == 'DL'
326       @handle.add_deltainfoxml(f, Solv::Repo::REPO_USE_LOADING)
327     end
328     f.close
329     writecachedrepo(ext, repodata)
330     return true
331   end
332
333 end
334
335 class Repo_susetags < Repo_generic
336
337   def find(what)
338     di = @handle.Dataiterator(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME, what, Solv::Dataiterator::SEARCH_STRING)
339     di.prepend_keyname(Solv::SUSETAGS_FILE)
340     for d in di
341       dp = d.parentpos()
342       checksum = dp.lookup_checksum(Solv::SUSETAGS_FILE_CHECKSUM)
343       return what, checksum
344     end
345     return nil, nil
346   end
347
348   def load(pool)
349     return true if super(pool)
350     print "susetags repo '#{@name}: "
351     f = download("content", false, nil, nil)
352     if !f
353       puts "no content file, skipped"
354       @handle.free(true)
355       @handle = nil
356       return false
357     end
358     @cookie = calc_cookie_fp(f)
359     if usecachedrepo(nil, true)
360       puts "cached"
361       f.close
362       return true
363     end
364     @handle.add_content(f, 0)
365     f.close
366     puts "fetching"
367     defvendorid = @handle.lookup_id(Solv::SOLVID_META, Solv::SUSETAGS_DEFAULTVENDOR)
368     descrdir = @handle.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_DESCRDIR)
369     descrdir = "suse/setup/descr" unless descrdir
370     (filename, filechksum) = find('packages.gz')
371     (filename, filechksum) = find('packages') unless filename
372     if filename
373       f = download("#{descrdir}/#{filename}", true, filechksum, true)
374       if f
375         @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::SUSETAGS_RECORD_SHARES)
376         f.close
377         (filename, filechksum) = find('packages.en.gz')
378         (filename, filechksum) = find('packages.en') unless filename
379         if filename
380           f = download("#{descrdir}/#{filename}", true, filechksum, true)
381           if f
382             @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::REPO_REUSE_REPODATA|Solv::Repo::REPO_EXTEND_SOLVABLES)
383             f.close
384           end
385         end
386         @handle.internalize()
387       end
388     end
389     add_exts()
390     writecachedrepo(nil)
391     @handle.create_stubs()
392     return true
393   end
394
395   @@langtags = {
396     Solv::SOLVABLE_SUMMARY     => Solv::REPOKEY_TYPE_STR,
397     Solv::SOLVABLE_DESCRIPTION => Solv::REPOKEY_TYPE_STR,
398     Solv::SOLVABLE_EULA        => Solv::REPOKEY_TYPE_STR,
399     Solv::SOLVABLE_MESSAGEINS  => Solv::REPOKEY_TYPE_STR,
400     Solv::SOLVABLE_MESSAGEDEL  => Solv::REPOKEY_TYPE_STR,
401     Solv::SOLVABLE_CATEGORY    => Solv::REPOKEY_TYPE_ID,
402   }
403
404   def add_ext(repodata, what, ext)
405     (filename, filechksum) = find(what)
406     h = repodata.new_handle()
407     repodata.set_str(h, Solv::SUSETAGS_FILE_NAME, filename)
408     repodata.set_checksum(h, Solv::SUSETAGS_FILE_CHECKSUM, filechksum)
409     if ext == 'DL'
410       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOSITORY_DELTAINFO)
411       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_FLEXARRAY)
412     elsif ext == 'DU'
413       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_DISKUSAGE)
414       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRNUMNUMARRAY)
415     elsif ext == 'FL'
416       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_FILELIST)
417       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRSTRARRAY)
418     else
419       @@langtags.sort.each do |langid, langtype|
420         repodata.add_idarray(h, Solv::REPOSITORY_KEYS, @handle.pool.id2langid(langid, ext, true))
421         repodata.add_idarray(h, Solv::REPOSITORY_KEYS, langtype)
422       end
423     end
424     repodata.add_flexarray(Solv::SOLVID_META, Solv::REPOSITORY_EXTERNAL, h)
425   end
426
427   def add_exts
428     repodata = @handle.add_repodata(0)
429     di = @handle.Dataiterator(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME, nil, 0)
430     di.prepend_keyname(Solv::SUSETAGS_FILE)
431     for d in di
432       filename = d.str
433       next unless filename && filename =~ /^packages\.(..)(?:\..*)$/
434       next if $1 == 'en' || $1 == 'gz'
435       add_ext(repodata, filename, $1)
436     end
437     repodata.internalize()
438   end
439
440   def load_ext(repodata)
441     filename = repodata.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME)
442     ext = filename[9,2]
443     print "[#{@name}:#{ext}: "
444     STDOUT.flush
445     if usecachedrepo(ext)
446       puts "cached]\n"
447       return true
448     end
449     puts "fetching]\n"
450     defvendorid = @handle.lookup_id(Solv::SOLVID_META, Solv::SUSETAGS_DEFAULTVENDOR)
451     descrdir = @handle.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_DESCRDIR)
452     descrdir = "suse/setup/descr" unless descrdir
453     filechksum = repodata.lookup_checksum(Solv::SOLVID_META, Solv::SUSETAGS_FILE_CHECKSUM)
454     f = download("#{descrdir}/#{filename}", true, filechksum)
455     return false unless f
456     @handle.add_susetags(f, defvendorid, ext, Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES)
457     f.close
458     writecachedrepo(ext, repodata)
459     return true
460   end
461
462   def packagespath()
463     datadir = @handle.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_DATADIR)
464     datadir = "suse" unless datadir
465     return datadir + '/'
466   end
467 end
468
469 class Repo_unknown < Repo_generic
470   def load(pool)
471     puts "unsupported repo '#{@name}: skipped"
472     return false
473   end
474 end
475
476 class Repo_system < Repo_generic
477   def load(pool)
478     @handle = pool.add_repo(@name)
479     @handle.appdata = self
480     pool.installed = @handle
481     print "rpm database: "
482     @cookie = calc_cookie_file("/var/lib/rpm/Packages")
483     if usecachedrepo(nil)
484       puts "cached"
485       return true
486     end
487     puts "reading"
488     if @handle.respond_to? :add_products
489       @handle.add_products("/etc/products.d", Solv::Repo::REPO_NO_INTERNALIZE)
490     end
491     @handle.add_rpmdb(nil, Solv::Repo::REPO_REUSE_REPODATA)
492     writecachedrepo(nil)
493     return true
494   end
495 end
496
497 args = ARGV
498 cmd = args.shift
499 cmd = 'list' if cmd == 'li'
500 cmd = 'install' if cmd == 'in'
501 cmd = 'erase' if cmd == 'rm'
502 cmd = 'verify' if cmd == 've'
503 cmd = 'search' if cmd == 'se'
504
505 repos = []
506 reposdirs = []
507 if FileTest.directory?('/etc/zypp/repos.d')
508   reposdirs = [ '/etc/zypp/repos.d' ]
509 else
510   reposdirs = [ '/etc/yum/repos.d' ]
511 end
512 for reposdir in reposdirs do
513   next unless FileTest.directory?(reposdir)
514   for reponame in Dir["#{reposdir}/*.repo"].sort do
515     cfg = IniFile.new(reponame)
516     cfg.each_section do |ali|
517       repoattr = { 'alias' => ali, 'enabled' => 0, 'priority' => 99, 'autorefresh' => 1, 'type' => 'rpm-md', 'metadata_expire' => 900}
518       repoattr.update(cfg[ali])
519       if repoattr['type'] == 'rpm-md'
520         repo = Repo_rpmmd.new(ali, 'repomd', repoattr)
521       elsif repoattr['type'] == 'yast2'
522         repo = Repo_susetags.new(ali, 'susetags', repoattr)
523       else
524         repo = Repo_unknown.new(ali, 'unknown', repoattr)
525       end
526       repos.push(repo)
527     end
528   end
529 end
530
531 pool = Solv::Pool.new()
532 pool.setarch()
533
534 pool.set_loadcallback { |repodata|
535   repo = repodata.repo.appdata
536   repo ? repo.load_ext(repodata) : false
537 }
538
539 sysrepo = Repo_system.new('@System', 'system')
540 sysrepo.load(pool)
541 for repo in repos
542   repo.load(pool) if repo.enabled?
543 end
544
545 if cmd == 'search'
546   matches = {}
547   for di in pool.Dataiterator(0, Solv::SOLVABLE_NAME, args[0], Solv::Dataiterator::SEARCH_SUBSTRING | Solv::Dataiterator::SEARCH_NOCASE)
548     matches[di.solvid] = true
549   end
550   for solvid in matches.keys.sort
551     s = pool.solvables[solvid]
552     puts "- #{s.str} [#{s.repo.name}]"
553   end
554   exit
555 end
556
557 addedprovides = pool.addfileprovides_queue()
558 if !addedprovides.empty?
559   sysrepo.updateaddedprovides(addedprovides)
560   for repo in repos
561     repo.updateaddedprovides(addedprovides)
562   end
563 end
564 pool.createwhatprovides()
565
566 jobs = []
567 for arg in args
568   flags = Solv::Selection::SELECTION_NAME | Solv::Selection::SELECTION_PROVIDES|Solv::Selection::SELECTION_GLOB
569   if arg =~ /^\//
570     flags |= Solv::Selection::SELECTION_FILELIST
571     flags |= Solv::Selection::SELECTION_INSTALLED_ONLY if cmd == 'erase'
572   end
573   sel = pool.select(arg, flags)
574   if sel.isempty?
575     sel = pool.select(arg, flags |  Solv::Selection::SELECTION_NOCASE)
576     puts "[ignoring case for '#{arg}']" unless sel.isempty?
577   end
578   puts "[using file list match for '#{arg}']" if sel.flags & Solv::Selection::SELECTION_FILELIST != 0
579   puts "[using capability match for '#{arg}']" if sel.flags & Solv::Selection::SELECTION_PROVIDES != 0
580   jobs += sel.jobs(0)
581 end
582
583 if jobs.empty? && (cmd == 'up' || cmd == 'dup' || cmd == 'verify')
584   sel = pool.Selection()
585   sel.addsimple(Solv::Job::SOLVER_SOLVABLE_ALL, 0)
586   jobs += sel.jobs(0)
587 end
588
589 if cmd == 'list' || cmd == 'info'
590   abort("no package matched.") if jobs.empty?
591     for job in jobs
592       for s in job.solvables()
593         if cmd == 'info'
594           puts "Name:        #{s.str}"
595           puts "Repo:        #{s.repo.name}"
596           puts "Summary:     #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}"
597           str = s.lookup_str(Solv::SOLVABLE_URL)
598           puts "Url:         #{str}" if str
599           str = s.lookup_str(Solv::SOLVABLE_LICENSE)
600           puts "License:     #{str}" if str
601           puts "Description:\n#{s.lookup_str(Solv::SOLVABLE_DESCRIPTION)}"
602           puts
603         else
604           puts "  - #{s.str} [#{s.repo.name}]"
605           puts "    #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}"
606         end
607       end
608     end
609   exit
610 end
611
612 if cmd == 'install' || cmd == 'erase' || cmd == 'up' || cmd == 'dup' || cmd == 'verify'
613   abort("no package matched.") if jobs.empty?
614   for job in jobs
615     if cmd == 'up'
616       job.how |= Solv::Job::SOLVER_UPDATE
617       job.how ^= Solv::Job::SOLVER_UPDATE ^ Solv::Job::SOLVER_INSTALL if job.isemptyupdate?
618     elsif cmd == 'install'
619       job.how |= Solv::Job::SOLVER_INSTALL
620     elsif cmd == 'erase'
621       job.how |= Solv::Job::SOLVER_ERASE
622     elsif cmd == 'dup'
623       job.how |= Solv::Job::SOLVER_DISTUPGRADE
624     elsif cmd == 'verify'
625       job.how |= Solv::Job::SOLVER_VERIFY
626     end
627   end
628
629   solver = nil
630   #pool.set_debuglevel(1)
631   while true
632     solver = pool.Solver
633     solver.set_flag(Solv::Solver::SOLVER_FLAG_SPLITPROVIDES, 1)
634     solver.set_flag(Solv::Solver::SOLVER_FLAG_ALLOW_UNINSTALL, 1) if cmd == 'erase'
635     problems = solver.solve(jobs)
636     break if problems.empty?
637     for problem in problems
638       puts "Problem #{problem.id}:"
639       puts problem.findproblemrule.info.problemstr
640       solutions = problem.solutions
641       for solution in solutions
642         puts "  Solution #{solution.id}:"
643         elements = solution.elements(true)
644         for element in elements
645           puts "  - #{element.str}"
646         end
647         puts
648       end
649       sol = nil
650       while true
651         print "Please choose a solution: "
652         STDOUT.flush
653         sol = STDIN.gets.strip
654         break if sol == 's' || sol == 'q'
655         break if sol =~ /^\d+$/ && sol.to_i >= 1 && sol.to_i <= solutions.length
656       end
657       next if sol == 's'
658       abort if sol == 'q'
659       solution = solutions[sol.to_i - 1]
660       for element in solution.elements
661         newjob = element.Job()
662         if element.type == Solv::Solver::SOLVER_SOLUTION_JOB
663           jobs[element.jobidx] = newjob
664         else
665           jobs.push(newjob) if newjob && !jobs.include?(newjob)
666         end
667       end
668     end
669   end
670   trans = solver.transaction
671   solver = nil
672   if trans.isempty?
673     puts "Nothing to do."
674     exit
675   end
676   puts "\nTransaction summary:\n"
677   for cl in trans.classify()
678     if cl.type == Solv::Transaction::SOLVER_TRANSACTION_ERASE
679       puts "#{cl.count} erased packages:"
680     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_INSTALL
681       puts "#{cl.count} installed packages:"
682     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_REINSTALLED
683       puts "#{cl.count} reinstalled packages:"
684     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED
685       puts "#{cl.count} downgraded packages:"
686     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_CHANGED
687       puts "#{cl.count} changed packages:"
688     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED
689       puts "#{cl.count} upgraded packages:"
690     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_VENDORCHANGE
691       puts "#{cl.count} vendor changes from '#{cl.fromdep}' to '#{cl.todep}':"
692     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_ARCHCHANGE
693       puts "#{cl.count} arch changes from '#{cl.fromdep}' to '#{cl.todep}':"
694     else
695       next
696     end
697     for p in cl.solvables
698       if cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED || cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED
699         puts "  - #{p.str} -> #{trans.othersolvable(p).str}"
700       else
701         puts "  - #{p.str}"
702       end
703     end
704     puts
705   end
706   puts "install size change: #{trans.calc_installsizechange()} K\n\n"
707   while true:
708     print("OK to continue (y/n)? ")
709     STDOUT.flush
710     yn = STDIN.gets.strip
711     break if yn == 'y'
712     abort if yn == 'n'
713   end
714   newpkgs = trans.newpackages()
715   newpkgsfp = {}
716   if !newpkgs.empty?
717     downloadsize = 0
718     for p in newpkgs
719       downloadsize += p.lookup_num(Solv::SOLVABLE_DOWNLOADSIZE)
720     end
721     puts "Downloading #{newpkgs.length} packages, #{downloadsize} K"
722     for p in newpkgs
723       repo = p.repo.appdata
724       location, medianr = p.lookup_location()
725       next unless location
726       location = repo.packagespath + location
727       chksum = p.lookup_checksum(Solv::SOLVABLE_CHECKSUM)
728       f = repo.download(location, false, chksum)
729       abort("\n#{@name}: #{location} not found in repository\n") unless f
730       newpkgsfp[p.id] = f
731       print "."
732       STDOUT.flush()
733     end
734     puts
735   end
736   puts "Committing transaction:"
737   puts
738   trans.order(0)
739   for p in trans.steps
740     steptype = trans.steptype(p, Solv::Transaction::SOLVER_TRANSACTION_RPM_ONLY)
741     if steptype == Solv::Transaction::SOLVER_TRANSACTION_ERASE
742       puts "erase #{p.str}"
743       next unless p.lookup_num(Solv::RPM_RPMDBID)
744       evr = p.evr.sub(/^[0-9]+:/, '')
745       system('rpm', '-e', '--nodeps', '--nodigest', '--nosignature', "#{p.name}-#{evr}.#{p.arch}") || abort("rpm failed: #{$? >> 8}") 
746     elsif (steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL || steptype == Solv::Transaction::SOLVER_TRANSACTION_MULTIINSTALL)
747       puts "install #{p.str}"
748       f = newpkgsfp.delete(p.id)
749       next unless f
750       mode = steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL ? '-U' : '-i'
751       system('rpm', mode, '--force', '--nodeps', '--nodigest', '--nosignature', "/dev/fd/#{f.fileno().to_s}") || abort("rpm failed: #{$? >> 8}")
752       f.close
753     end
754   end
755 end