- rename SOLVER_FLAG_IGNORE_ALREADY_RECOMMENDED to SOLVER_FLAG_ADD_ALREADY_RECOMMENDE...
[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     @handle.add_products("/etc/products.d", Solv::Repo::REPO_NO_INTERNALIZE)
488     @handle.add_rpmdb(nil, Solv::Repo::REPO_REUSE_REPODATA)
489     writecachedrepo(nil)
490     return true
491   end
492 end
493
494
495 def validarch?(pool, arch)
496   return false unless arch && arch != ''
497   id = pool.str2id(arch, false)
498   return id != 0 && pool.isknownarch?(id)
499 end
500
501 def depglob(pool, name, globname, globdep)
502   id = pool.str2id(name, false)
503   if id != 0
504     match = false
505     providers = pool.whatprovides(id)
506     if globname && providers.find {|s| s.nameid == id }
507       return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_NAME, id) ]
508     end
509     if !providers.empty?
510       puts "[using capability match for '#{name}']" if globname && globdep
511       return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_PROVIDES, id) ]
512     end
513   end
514   return [] unless name =~ /[\[*?]/
515   if globname
516     idmatches = {}
517     for d in pool.Dataiterator(0, Solv::SOLVABLE_NAME, name, Solv::Dataiterator::SEARCH_GLOB)
518       s = d.solvable
519       idmatches[s.nameid] = 1 if s.installable?
520     end
521     if !idmatches.empty?
522       return idmatches.keys.sort.collect { |id| pool.Job(Solv::Job::SOLVER_SOLVABLE_NAME, id) }
523     end
524   end
525   if globdep
526     idmatches = pool.matchprovidingids(name, Solv::Dataiterator::SEARCH_GLOB)
527     if !idmatches.empty?
528       puts "[using capability match for '#{name}']"
529       return idmatches.sort.collect { |id| pool.Job(Solv::Job::SOLVER_SOLVABLE_PROVIDES, id) }
530     end
531   end
532   return []
533 end
534
535 def limitjobs(pool, jobs, flags, evrstr)
536   njobs = []
537   evr = pool.str2id(evrstr)
538   for j in jobs
539     how = j.how
540     sel = how & Solv::Job::SOLVER_SELECTMASK
541     what = pool.rel2id(j.what, evr, flags)
542     if flags == Solv::REL_ARCH
543       how |= Solv::Job::SOLVER_SETARCH
544     elsif flags == Solv::REL_EQ && sel == Solv::Job::SOLVER_SOLVABLE_NAME
545       how |= evrstr.include?(?-) ? Solv::Job::SOLVER_SETEVR : Solv::Job::SOLVER_SETEV
546     end
547     njobs << pool.Job(how, what)
548   end
549   return njobs
550 end
551
552 def limitjobs_evrarch(pool, jobs, flags, evrstr)
553   if evrstr =~ /^(.+)\.(.+?)$/ && validarch?(pool, $2)
554     evrstr = $1
555     jobs = limitjobs(pool, jobs, Solv::REL_ARCH, $2)
556   end
557   return limitjobs(pool, jobs, flags, evrstr)
558 end
559
560 def mkjobs_rel(pool, cmd, name, rel, evr)
561   flags = 0
562   flags |= Solv::REL_LT if rel.include?(?<)
563   flags |= Solv::REL_EQ if rel.include?(?=)
564   flags |= Solv::REL_GT if rel.include?(?>)
565   jobs = depglob(pool, name, true, true)
566   return limitjobs(pool, jobs, flags, evr) unless jobs.empty?
567   if (name =~ /^(.+)\.(.+?)$/) && validarch?(pool, $2)
568     arch = $2
569     jobs = depglob(pool, name, true, true)
570     return [] if jobs.empty?
571     jobs = limitjobs(pool, jobs, Solv::REL_ARCH, arch)
572     return limitjobs(pool, jobs, flags, evr)
573   end
574   return []
575 end
576
577 def mkjobs_nevra(pool, cmd, arg)
578   jobs = depglob(pool, arg, true, true)
579   return jobs unless jobs.empty?
580   if ((arg =~ /^(.+)\.(.+?)$/) && validarch?(pool, $2))
581     arch = $2
582     jobs = depglob(pool, $1, true, true)
583     return limitjobs(pool, jobs, Solv::REL_ARCH, arch) unless jobs.empty?
584   end
585   if (arg =~ /^(.+)-(.+?)$/)
586     evr = $2
587     jobs = depglob(pool, $1, true, false)
588     return limitjobs_evrarch(pool, jobs, Solv::REL_EQ, evr) unless jobs.empty?
589   end
590   if (arg =~ /^(.+)-(.+?-.+?)$/)
591     evr = $2
592     jobs = depglob(pool, $1, true, false)
593     return limitjobs_evrarch(pool, jobs, Solv::REL_EQ, evr) unless jobs.empty?
594   end
595   return []
596 end
597
598 def mkjobs_filelist(pool, cmd, arg)
599   type = Solv::Dataiterator::SEARCH_STRING
600   type = Solv::Dataiterator::SEARCH_GLOB if arg =~ /[\[*?]/
601   if cmd == 'erase'
602     di = pool.installed.Dataiterator(0, Solv::SOLVABLE_FILELIST, arg, type | Solv::Dataiterator::SEARCH_FILES|Solv::Dataiterator::SEARCH_COMPLETE_FILELIST)
603   else
604     di = pool.Dataiterator(0, Solv::SOLVABLE_FILELIST, arg, type | Solv::Dataiterator::SEARCH_FILES|Solv::Dataiterator::SEARCH_COMPLETE_FILELIST)
605   end
606   matches = []
607   for d in di
608     s = d.solvable
609     next unless s && s.installable?
610     matches.push(s.id)
611     di.skip_solvable()
612   end
613   return [] if matches.empty?
614   puts "[using file list match for '#{arg}']"
615   if matches.length > 1
616     return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_ONE_OF, pool.towhatprovides(matches)) ]
617   else
618     return [ pool.Job(Solv::Job::SOLVER_SOLVABLE | Solv::Job::SOLVER_NOAUTOSET, matches[0]) ]
619   end
620 end
621
622 def mkjobs(pool, cmd, arg)
623   if arg =~ /^\//
624     jobs = mkjobs_filelist(pool, cmd, arg)
625     return jobs unless jobs.empty?
626   end
627   if (arg =~ /^(.+?)\s*([<=>]+)\s*(.+?)$/)
628     return mkjobs_rel(pool, cmd, $1, $2, $3)
629   else
630     return mkjobs_nevra(pool, cmd, arg)
631   end
632 end
633
634 args = ARGV
635 cmd = args.shift
636 cmd = 'list' if cmd == 'li'
637 cmd = 'install' if cmd == 'in'
638 cmd = 'erase' if cmd == 'rm'
639 cmd = 'verify' if cmd == 've'
640 cmd = 'search' if cmd == 'se'
641
642 repos = []
643 for reposdir in [ '/etc/zypp/repos.d' ] do
644   next unless FileTest.directory?(reposdir)
645   for reponame in Dir["#{reposdir}/*.repo"].sort do
646     cfg = IniFile.new(reponame)
647     cfg.each_section do |ali|
648       repoattr = { 'alias' => ali, 'enabled' => 0, 'priority' => 99, 'autorefresh' => 1, 'type' => 'rpm-md', 'metadata_expire' => 900}
649       repoattr.update(cfg[ali])
650       if repoattr['type'] == 'rpm-md'
651         repo = Repo_rpmmd.new(ali, 'repomd', repoattr)
652       elsif repoattr['type'] == 'yast2'
653         repo = Repo_susetags.new(ali, 'susetags', repoattr)
654       else
655         repo = Repo_unknown.new(ali, 'unknown', repoattr)
656       end
657       repos.push(repo)
658     end
659   end
660 end
661
662 pool = Solv::Pool.new()
663 # require 'sys/uname' ; sysarch = Sys::Uname.machine
664 sysarch = `uname -p`.strip
665 pool.setarch(sysarch)
666
667 pool.set_loadcallback { |repodata|
668   repo = repodata.repo.appdata
669   repo ? repo.load_ext(repodata) : false
670 }
671
672 sysrepo = Repo_system.new('@System', 'system')
673 sysrepo.load(pool)
674 for repo in repos
675   repo.load(pool) if repo.enabled?
676 end
677
678 if cmd == 'search'
679   matches = {}
680   for di in pool.Dataiterator(0, Solv::SOLVABLE_NAME, args[0], Solv::Dataiterator::SEARCH_SUBSTRING | Solv::Dataiterator::SEARCH_NOCASE)
681     matches[di.solvid] = true
682   end
683   for solvid in matches.keys.sort
684     s = pool.solvables[solvid]
685     puts "- #{s.str} [#{s.repo.name}]"
686   end
687   exit
688 end
689
690 addedprovides = pool.addfileprovides_queue()
691 if !addedprovides.empty?
692   sysrepo.updateaddedprovides(addedprovides)
693   for repo in repos
694     repo.updateaddedprovides(addedprovides)
695   end
696 end
697 pool.createwhatprovides()
698
699 jobs = []
700 for arg in args
701   njobs = mkjobs(pool, cmd, ARGV[0])
702   abort("nothing matches '#{arg}'") if njobs.empty?
703   jobs += njobs
704 end
705
706 if cmd == 'list' || cmd == 'info'
707   abort("no package matched.") if jobs.empty?
708     for job in jobs
709       for s in job.solvables()
710         if cmd == 'info'
711           puts "Name:        #{s.str}"
712           puts "Repo:        #{s.repo.name}"
713           puts "Summary:     #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}"
714           str = s.lookup_str(Solv::SOLVABLE_URL)
715           puts "Url:         #{str}" if str
716           str = s.lookup_str(Solv::SOLVABLE_LICENSE)
717           puts "License:     #{str}" if str
718           puts "Description:\n#{s.lookup_str(Solv::SOLVABLE_DESCRIPTION)}"
719           puts
720         else
721           puts "  - #{s.str} [#{s.repo.name}]"
722           puts "    #{s.lookup_str(Solv::SOLVABLE_SUMMARY)}"
723         end
724       end
725     end
726   exit
727 end
728
729 if cmd == 'install' || cmd == 'erase' || cmd == 'up' || cmd == 'dup' || cmd == 'verify'
730   if jobs.empty?
731     if cmd == 'up' || cmd == 'verify' || cmd == 'dup'
732       jobs = [ pool.Job(Solv::Job::SOLVER_SOLVABLE_ALL, 0) ]
733     else
734       abort("no package matched.")
735     end
736   end
737   for job in jobs
738     if cmd == 'up'
739       if job.how == Solv::Job::SOLVER_SOLVABLE_ALL || job.solvables.any? {|s| s.isinstalled?}
740         job.how |= Solv::Job::SOLVER_UPDATE
741       else
742         job.how |= Solv::Job::SOLVER_INSTALL
743       end
744     elsif cmd == 'install'
745       job.how |= Solv::Job::SOLVER_INSTALL
746     elsif cmd == 'erase'
747       job.how |= Solv::Job::SOLVER_ERASE
748     elsif cmd == 'dup'
749       job.how |= Solv::Job::SOLVER_DISTUPGRADE
750     elsif cmd == 'verify'
751       job.how |= Solv::Job::SOLVER_VERIFY
752     end
753   end
754
755   solver = nil
756   #pool.set_debuglevel(1)
757   while true
758     solver = pool.Solver
759     solver.set_flag(Solv::Solver::SOLVER_FLAG_SPLITPROVIDES, 1)
760     solver.set_flag(Solv::Solver::SOLVER_FLAG_ALLOW_UNINSTALL, 1) if cmd == 'erase'
761     problems = solver.solve(jobs)
762     break if problems.empty?
763     for problem in problems
764       puts "Problem #{problem.id}:"
765       puts problem.findproblemrule.info.problemstr
766       solutions = problem.solutions
767       for solution in solutions
768         puts "  Solution #{solution.id}:"
769         elements = solution.elements(true)
770         for element in elements
771           puts "  - #{element.str}"
772         end
773         puts
774       end
775       sol = nil
776       while true
777         print "Please choose a solution: "
778         STDOUT.flush
779         sol = STDIN.gets.strip
780         break if sol == 's' || sol == 'q'
781         break if sol =~ /^\d+$/ && sol.to_i >= 1 && sol.to_i <= solutions.length
782       end
783       next if sol == 's'
784       abort if sol == 'q'
785       solution = solutions[sol.to_i - 1]
786       for element in solution.elements
787         newjob = element.Job()
788         if element.type == Solv::Solver::SOLVER_SOLUTION_JOB
789           jobs[element.jobidx] = newjob
790         else
791           jobs.push(newjob) if newjob && !jobs.include?(newjob)
792         end
793       end
794     end
795   end
796   trans = solver.transaction
797   solver = nil
798   if trans.isempty?
799     puts "Nothing to do."
800     exit
801   end
802   puts "\nTransaction summary:\n"
803   for cl in trans.classify()
804     if cl.type == Solv::Transaction::SOLVER_TRANSACTION_ERASE
805       puts "#{cl.count} erased packages:"
806     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_INSTALL
807       puts "#{cl.count} installed packages:"
808     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_REINSTALLED
809       puts "#{cl.count} reinstalled packages:"
810     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED
811       puts "#{cl.count} downgraded packages:"
812     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_CHANGED
813       puts "#{cl.count} changed packages:"
814     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED
815       puts "#{cl.count} upgraded packages:"
816     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_VENDORCHANGE
817       puts "#{cl.count} vendor changes from '#{pool.id2str(cl.fromid)}' to '#{pool.id2str(cl.toid)}':"
818     elsif cl.type == Solv::Transaction::SOLVER_TRANSACTION_ARCHCHANGE
819       puts "#{cl.count} arch changes from '#{pool.id2str(cl.fromid)}' to '#{pool.id2str(cl.toid)}':"
820     else
821       next
822     end
823     for p in cl.solvables
824       if cl.type == Solv::Transaction::SOLVER_TRANSACTION_UPGRADED || cl.type == Solv::Transaction::SOLVER_TRANSACTION_DOWNGRADED
825         puts "  - #{p.str} -> #{trans.othersolvable(p).str}"
826       else
827         puts "  - #{p.str}"
828       end
829     end
830     puts
831   end
832   puts "install size change: #{trans.calc_installsizechange()} K\n\n"
833   while true:
834     print("OK to continue (y/n)? ")
835     STDOUT.flush
836     yn = STDIN.gets.strip
837     break if yn == 'y'
838     abort if yn == 'n'
839   end
840   newpkgs = trans.newpackages()
841   newpkgsfp = {}
842   if !newpkgs.empty?
843     downloadsize = 0
844     for p in newpkgs
845       downloadsize += p.lookup_num(Solv::SOLVABLE_DOWNLOADSIZE)
846     end
847     puts "Downloading #{newpkgs.length} packages, #{downloadsize} K"
848     for p in newpkgs
849       repo = p.repo.appdata
850       location, medianr = p.lookup_location()
851       next unless location
852       chksum = p.lookup_checksum(Solv::SOLVABLE_CHECKSUM)
853       f = repo.download_location(location, chksum)
854       newpkgsfp[p.id] = f
855       print "."
856       STDOUT.flush()
857     end
858     puts
859   end
860   puts "Committing transaction:"
861   puts
862   trans.order(0)
863   for p in trans.steps
864     steptype = trans.steptype(p, Solv::Transaction::SOLVER_TRANSACTION_RPM_ONLY)
865     if steptype == Solv::Transaction::SOLVER_TRANSACTION_ERASE
866       puts "erase #{p.str}"
867       next unless p.lookup_num(Solv::RPM_RPMDBID)
868       evr = p.evr.sub(/^[0-9]+:/, '')
869       system('rpm', '-e', '--nodeps', '--nodigest', '--nosignature', "#{p.name}-#{evr}.#{p.arch}") || abort("rpm failed: #{$? >> 8}") 
870     elsif (steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL || steptype == Solv::Transaction::SOLVER_TRANSACTION_MULTIINSTALL)
871       puts "install #{p.str}"
872       f = newpkgsfp.delete(p.id)
873       next unless f
874       mode = steptype == Solv::Transaction::SOLVER_TRANSACTION_INSTALL ? '-U' : '-i'
875       system('rpm', mode, '--force', '--nodeps', '--nodigest', '--nosignature', "/dev/fd/#{Solv::xfileno(f).to_s}") || abort("rpm failed: #{$? >> 8}")
876       solv::xfclose(f)
877     end
878   end
879 end