rbsolv: implement repo download and load callback
[platform/upstream/libsolv.git] / examples / rbsolv
1 #!/usr/bin/ruby
2
3 #  bool: method?
4 #  inplace mod: method!
5 #  set method: method=
6
7 # map  => collect
8 # grep => find_all
9
10 require 'solv'
11 require 'rubygems'
12 require 'inifile'
13 require 'tempfile'
14
15 class Repo_generic
16   def initialize(name, type, attribs = {})
17     @name = name
18     @type = type
19     @attribs = attribs.dup
20     @incomplete = false
21   end
22
23   def enabled?
24     return @attribs['enabled'].to_i != 0
25   end
26
27   def autorefresh?
28     return @attribs['autorefresh'].to_i != 0
29   end
30
31   def calc_cookie_fp(f)
32     chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
33     chksum.add_fp(f)
34     return chksum.raw
35   end
36
37   def calc_cookie_file(filename)
38     chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
39     chksum.add("1.1")
40     chksum.add_stat(filename)
41     return chksum.raw
42   end
43
44   def cachepath(ext = nil)
45     path = @name.sub(/^\./, '_')
46     path += ext ? "_#{ext}.solvx" : '.solv'
47     return '/var/cache/solv/' + path.gsub(/\//, '_')
48   end
49
50   def load(pool)
51     @handle = pool.add_repo(@name)
52     @handle.appdata = self
53     @handle.priority = 99 - @attribs['priority'].to_i if @attribs['priority']
54     dorefresh = autorefresh?
55     if dorefresh
56       begin
57         s = File.stat(cachepath)
58         dorefresh = false if s && Time.now - s.mtime < @attribs['metadata_expire'].to_i
59       rescue SystemCallError
60       end
61     end
62     @cookie = nil
63     if !dorefresh && usecachedrepo(nil)
64       puts "repo: '#{@name}' cached"
65       return true
66     end
67     return load_if_changed()
68   end
69
70   def load_ext(repodata)
71     return false
72   end
73
74   def load_if_changed
75     return false
76   end
77
78   def download(file, uncompress, chksum, markincomplete = false)
79     url = @attribs['baseurl']
80     if !url
81       puts "%{@name}: no baseurl"
82       return nil
83     end
84     url = url.sub(/\/$/, '') + "/#{file}"
85     f =  Tempfile.new('rbsolv')
86     f.unlink
87     st = system('curl', '-f', '-s', '-L', '-o', "/dev/fd/" + f.fileno.to_s, '--', url)
88     return nil if f.stat.size == 0 && (st || !chksum)
89     if !st
90         puts "#{file}: download error #{$? >> 8}"
91         @incomplete = true if markincomplete
92         return nil
93     end
94     if chksum
95       fchksum = Solv::Chksum.new(chksum.type)
96       fchksum.add_fd(f.fileno)
97       if !fchksum.matches(chksum)
98         puts "#{file}: checksum error"
99         @incomplete = true if markincomplete
100         return nil
101       end
102     end
103     if uncompress
104       return Solv::xfopen_dup(file, f.fileno)
105     else
106       return Solv::xfopen_dup('', f.fileno)
107     end
108   end
109
110   def usecachedrepo(ext, mark = false)
111     cookie = ext ? @extcookie : @cookie
112     begin
113       repopath = cachepath(ext)
114       f = File.new(repopath, "r")
115       f.sysseek(-32, IO::SEEK_END)
116       fcookie = f.sysread(32)
117       return false if fcookie.length != 32
118       return false if cookie && fcookie != cookie
119       if !ext && @type != 'system'
120         f.sysseek(-32 * 2, IO::SEEK_END)
121         fextcookie = f.sysread(32)
122         return false if fextcookie.length != 32
123       end
124       f.sysseek(0, IO::SEEK_SET)
125       f = Solv::xfopen_dup('', f.fileno)
126       flags = ext ? Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES : 0
127       flags |= Solv::Repo::REPO_LOCALPOOL if ext && ext != 'DL'
128       if ! @handle.add_solv(f, flags)
129         Solv::xfclose(f)
130         return false
131       end
132       Solv::xfclose(f)
133       @cookie = fcookie unless ext
134       @extcookie = fextcookie if !ext && @type != 'system'
135       now = Time.now
136       begin
137         File::utime(now, now, repopath) if mark
138       rescue SystemCallError
139       end
140       return true
141     rescue SystemCallError
142       return false
143     end
144     return true
145   end
146
147   def genextcookie(f)
148     chksum = Solv::Chksum.new(Solv::REPOKEY_TYPE_SHA256)
149     chksum.add(@cookie)
150     if f
151       s = f.stat()
152       chksum.add(s.dev.to_s);
153       chksum.add(s.ino.to_s);
154       chksum.add(s.size.to_s);
155       chksum.add(s.mtime.to_s);
156     end
157     @extcookie = chksum.raw()
158     @extcookie[0] = 1 if @extcookie[0] == 0
159   end
160
161   def writecachedrepo(ext, info = nil)
162     begin
163       Dir::mkdir("/var/cache/solv", 0755) unless FileTest.directory?("/var/cache/solv")
164       f =  Tempfile.new('.newsolv-', '/var/cache/solv')
165       f.chmod(0444)
166       sf = Solv::xfopen_dup('', f.fileno)
167       if !info
168         @handle.write(sf)
169       elsif ext
170         info.write(sf)
171       else
172         @handle.write_first_repodata(sf)
173       end
174       Solv::xfclose(sf)
175       f.sysseek(0, IO::SEEK_END)
176       if @type != 'system' && !ext
177         genextcookie(f) unless @extcookie
178         f.syswrite(@extcookie)
179       end
180       f.syswrite(ext ? @extcookie : @cookie)
181       f.close(false)
182       File.rename(f.path, cachepath(ext))
183       f.unlink
184       return true
185     rescue SystemCallError
186       return false
187     end
188   end
189
190 end
191
192 class Repo_rpmmd < Repo_generic
193
194   def find(what)
195     di = @handle.Dataiterator(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_TYPE, what, Solv::Dataiterator::SEARCH_STRING)
196     di.prepend_keyname(Solv::REPOSITORY_REPOMD)
197     for d in di
198       d.setpos_parent()
199       filename = d.pool.lookup_str(Solv::SOLVID_POS, Solv::REPOSITORY_REPOMD_LOCATION)
200       next unless filename
201       checksum = d.pool.lookup_checksum(Solv::SOLVID_POS, Solv::REPOSITORY_REPOMD_CHECKSUM)
202       if !checksum
203         puts "no #{filename} checksum!"
204         return nil, nil
205       end
206       return filename, checksum
207     end
208     return nil, nil
209   end
210
211   def load_if_changed
212     print "rpmmd repo '#{@name}: "
213     f = download("repodata/repomd.xml", false, nil, nil)
214     if !f
215       puts "no repomd.xml file, skipped"
216       @handle.free(true)
217       @handle = nil
218       return false
219     end
220     @cookie = calc_cookie_fp(f)
221     if usecachedrepo(nil, true)
222       puts "cached"
223       Solv.xfclose(f)
224       return true
225     end
226     @handle.add_repomdxml(f, 0)
227     Solv::xfclose(f)
228     puts "fetching"
229     filename, filechksum = find('primary')
230     if filename
231       f = download(filename, true, filechksum, true)
232       if f
233         @handle.add_rpmmd(f, nil, 0)
234         Solv::xfclose(f)
235       end
236       return false if @incomplete
237     end
238     filename, filechksum = find('updateinfo')
239     if filename
240       f = download(filename, true, filechksum, true)
241       if f
242         @handle.add_updateinfoxml(f, 0)
243         Solv::xfclose(f)
244       end
245     end
246     add_exts()
247     writecachedrepo(nil) unless @incomplete
248     @handle.create_stubs()
249     return true
250   end
251
252   def add_ext(repodata, what, ext)
253     filename, filechksum = find(what)
254     filename, filechksum = find('prestodelta') if !filename && what == 'deltainfo'
255     return unless filename
256     h = repodata.new_handle()
257     repodata.set_poolstr(h, Solv::REPOSITORY_REPOMD_TYPE, what)
258     repodata.set_str(h, Solv::REPOSITORY_REPOMD_LOCATION, filename)
259     repodata.set_checksum(h, Solv::REPOSITORY_REPOMD_CHECKSUM, filechksum)
260     if ext == 'DL'
261       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOSITORY_DELTAINFO)
262       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_FLEXARRAY)
263     elsif ext == 'FL'
264       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::SOLVABLE_FILELIST)
265       repodata.add_idarray(h, Solv::REPOSITORY_KEYS, Solv::REPOKEY_TYPE_DIRSTRARRAY)
266     end
267     repodata.add_flexarray(Solv::SOLVID_META, Solv::REPOSITORY_EXTERNAL, h)
268   end
269
270   def add_exts
271     repodata = @handle.add_repodata(0)
272     add_ext(repodata, 'deltainfo', 'DL')
273     add_ext(repodata, 'filelists', 'FL')
274     repodata.internalize()
275   end
276
277   def load_ext(repodata)
278     repomdtype = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_TYPE)
279     if repomdtype == 'filelists'
280       ext = 'FL'
281     elsif repomdtype == 'deltainfo'
282       ext = 'DL'
283     else
284       return false
285     end
286     print "[#{@name}:#{ext}: "
287     STDOUT.flush
288     if usecachedrepo(ext)
289       puts "cached]\n"
290       return true
291     end
292     puts "fetching]\n"
293     filename = repodata.lookup_str(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_LOCATION)
294     filechksum = repodata.lookup_checksum(Solv::SOLVID_META, Solv::REPOSITORY_REPOMD_CHECKSUM)
295     f = download(filename, true, filechksum)
296     return false unless f
297     if ext == 'FL'
298       @handle.add_rpmmd(f, 'FL', Solv::Repo::REPO_USE_LOADING|Solv::Repo::REPO_EXTEND_SOLVABLES)
299     elsif ext == 'DL'
300       @handle.add_deltainfoxml(f, Solv::Repo::REPO_USE_LOADING)
301     end
302     Solv::xfclose(f)
303     writecachedrepo(ext, repodata)
304     return true
305   end
306
307 end
308
309 class Repo_susetags < Repo_generic
310
311   def find(what)
312     di = @handle.Dataiterator(Solv::SOLVID_META, Solv::SUSETAGS_FILE_NAME, what, Solv::Dataiterator::SEARCH_STRING)
313     di.prepend_keyname(Solv::SUSETAGS_FILE)
314     for d in di
315       d.setpos_parent()
316       checksum = d.pool.lookup_checksum(Solv::SOLVID_POS, Solv::SUSETAGS_FILE_CHECKSUM)
317       return what, checksum
318     end
319     return nil, nil
320   end
321
322   def load_if_changed
323     print "susetags repo '#{@name}: "
324     f = download("content", false, nil, nil)
325     if !f
326       puts "no content file, skipped"
327       @handle.free(true)
328       @handle = nil
329       return false
330     end
331     @cookie = calc_cookie_fp(f)
332     if usecachedrepo(nil, true)
333       puts "cached"
334       Solv.xfclose(f)
335       return true
336     end
337     @handle.add_content(f, 0)
338     Solv::xfclose(f)
339     puts "fetching"
340     defvendorid = @handle.lookup_id(Solv::SOLVID_META, Solv::SUSETAGS_DEFAULTVENDOR)
341     descrdir = @handle.lookup_str(Solv::SOLVID_META, Solv::SUSETAGS_DESCRDIR)
342     descrdir = "suse/setup/descr" unless descrdir
343     (filename, filechksum) = find('packages.gz')
344     (filename, filechksum) = find('packages') unless filename
345     if filename
346       f = download("#{descrdir}/#{filename}", true, filechksum, true)
347       if f
348         @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::SUSETAGS_RECORD_SHARES)
349         Solv::xfclose(f)
350         (filename, filechksum) = find('packages.en.gz')
351         (filename, filechksum) = find('packages.en') unless filename
352         if filename
353           f = download("#{descrdir}/#{filename}", true, filechksum, true)
354           if f
355             @handle.add_susetags(f, defvendorid, nil, Solv::Repo::REPO_NO_INTERNALIZE|Solv::Repo::REPO_REUSE_REPODATA|Solv::Repo::REPO_EXTEND_SOLVABLES)
356             Solv::xfclose(f)
357           end
358         end
359         @handle.internalize()
360       end
361     end
362     add_exts()
363     writecachedrepo(nil) unless @incomplete
364     @handle.create_stubs()
365     return true
366   end
367
368   def add_exts
369     repodata = @handle.add_repodata(0)
370     repodata.internalize()
371   end
372
373 end
374
375 class Repo_unknown < Repo_generic
376   def load(pool)
377     puts "unsupported repo '#{@name}: skipped"
378     return false
379   end
380 end
381
382 class Repo_system < Repo_generic
383   def load(pool)
384     @handle = pool.add_repo(@name)
385     @handle.appdata = self
386     pool.installed = @handle
387     print "rpm database: "
388     @cookie = calc_cookie_file("/var/lib/rpm/Packages")
389     if usecachedrepo(nil)
390       puts "cached"
391       return true
392     end
393     puts "reading"
394     @handle.add_products("/etc/products.d", Solv::Repo::REPO_NO_INTERNALIZE)
395     @handle.add_rpmdb(nil, Solv::Repo::REPO_REUSE_REPODATA)
396     writecachedrepo(nil)
397     return true
398   end
399 end
400
401
402
403 def depglob(pool, name, globname, globdep)
404   id = pool.str2id(name, false)
405   if id != 0
406     match = false
407     providers = pool.providers(id)
408     if globname && providers.find {|s| s.nameid == id }
409       return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_NAME, id) ]
410     end
411     if !providers.empty?
412       puts "[using capability match for '#{name}']" if globname && globdep
413       return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_PROVIDES, id) ]
414     end
415   end
416   return [] unless name =~ /[\[*?]/;
417   if globname
418     idmatches = {}
419     for d in pool.Dataiterator(0, Solv::SOLVABLE_NAME, name, Solv::Dataiterator::SEARCH_GLOB)
420       s = d.solvable
421       idmatches[s.nameid] = 1 if s.installable?
422     end
423     if !idmatches.empty?
424       return idmatches.keys.sort.collect { |id| pool.Job(Solv::Job::SOLVER_SOLVABLE_NAME, id) }
425     end
426   end
427   if globdep
428     idmatches = pool.matchprovidingids(name, Solv::Dataiterator::SEARCH_GLOB);
429     if !idmatches.empty?
430       puts "[using capability match for '#{name}']"
431       return idmatches.sort.collect { |id| pool.Job(Solv::Job::SOLVER_SOLVABLE_PROVIDES, id) }
432     end
433   end
434   return []
435 end
436
437 def mkjobs_filelist(pool, cmd, arg)
438   type = Solv::Dataiterator::SEARCH_STRING
439   type = Solv::Dataiterator::SEARCH_GLOB if arg =~ /[\[*?]/
440   if cmd == 'erase'
441     di = pool.installed.Dataiterator(0, Solv::SOLVABLE_FILELIST, arg, type | Solv::Dataiterator::SEARCH_FILES|Solv::Dataiterator::SEARCH_COMPLETE_FILELIST)
442   else
443     di = pool.Dataiterator(0, Solv::SOLVABLE_FILELIST, arg, type | Solv::Dataiterator::SEARCH_FILES|Solv::Dataiterator::SEARCH_COMPLETE_FILELIST)
444   end
445   matches = []
446   for d in di
447     s = d.solvable
448     next unless s && s.installable?
449     matches.push(s.id)
450     di.skip_solvable()
451   end
452   return [] if matches.empty?
453   puts "[using file list match for '#{arg}'"
454   if matches.length > 1
455     return [ pool.Job(Solv::Job::SOLVER_SOLVABLE_ONE_OF, pool.towhatprovides(matches)) ]
456   else
457     return [ pool.Job(Solv::Job::SOLVER_SOLVABLE | Solv::Job::SOLVER_NOAUTOSET, matches[0]) ]
458   end
459 end
460
461 def mkjobs(pool, cmd, arg)
462   if arg =~ /^\//
463     jobs = mkjobs_filelist(pool, cmd, arg)
464     return jobs unless jobs.empty?
465   end
466   return depglob(pool, arg, true, true)
467 end
468
469 args = ARGV
470 cmd = args.shift
471 cmd = 'list' if cmd == 'li'
472 cmd = 'install' if cmd == 'in'
473 cmd = 'erase' if cmd == 'rm'
474 cmd = 'verify' if cmd == 've'
475 cmd = 'search' if cmd == 'se'
476
477 repos = []
478 for reposdir in [ '/etc/zypp/repos.d' ] do
479   next unless FileTest.directory?(reposdir)
480   for reponame in Dir["#{reposdir}/*.repo"].sort do
481     cfg = IniFile.new(reponame)
482     cfg.each_section do |ali|
483       repoattr = { 'alias' => ali, 'enabled' => 0, 'priority' => 99, 'autorefresh' => 1, 'type' => 'rpm-md', 'metadata_expire' => 900}
484       repoattr.update(cfg[ali])
485       if repoattr['type'] == 'rpm-md'
486         repo = Repo_rpmmd.new(ali, 'repomd', repoattr)
487       elsif repoattr['type'] == 'yast2'
488         repo = Repo_susetags.new(ali, 'susetags', repoattr)
489       else
490         repo = Repo_unknown.new(ali, 'unknown', repoattr)
491       end
492       repos.push(repo)
493     end
494   end
495 end
496
497 pool = Solv::Pool.new()
498 # require 'sys/uname' ; sysarch = Sys::Uname.machine
499 sysarch = `uname -p`.strip
500 pool.setarch(sysarch)
501
502 pool.set_loadcallback { |repodata|
503   repo = repodata.repo.appdata
504   repo ? repo.load_ext(repodata) : false
505 }
506
507 sysrepo = Repo_system.new('@System', 'system')
508 sysrepo.load(pool)
509 for repo in repos
510   repo.load(pool) if repo.enabled?
511 end
512
513 if cmd == 'search'
514   matches = {}
515   for di in pool.Dataiterator(0, Solv::SOLVABLE_NAME, args[0], Solv::Dataiterator::SEARCH_SUBSTRING | Solv::Dataiterator::SEARCH_NOCASE)
516     matches[di.solvid] = true
517   end
518   for solvid in matches.keys.sort
519     s = pool.solvables[solvid]
520     puts "- #{s.str} [#{s.repo.name}]"
521   end
522   exit
523 end
524
525 pool.addfileprovides
526 pool.createwhatprovides
527
528 jobs = []
529 for arg in args
530   njobs = mkjobs(pool, cmd, ARGV[0])
531   abort("nothing matches '#{arg}'") if njobs.empty?
532   jobs += njobs
533 end
534
535 for job in jobs
536   job.how |= Solv::Job::SOLVER_ERASE
537 end
538
539 solver = pool.Solver
540 problems = solver.solve(jobs)
541 for problem in problems
542   puts "Problem #{problem.id}:"
543   puts problem.findproblemrule.info.problemstr
544   solutions = problem.solutions
545   for solution in solutions
546     puts "  Solution #{solution.id}:"
547     elements = solution.elements
548     for element in elements
549       puts "  - type #{element.type}"
550     end
551   end
552 end