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