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