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