beautify examples a bit
[platform/upstream/libsolv.git] / examples / pysolv
1 #!/usr/bin/python
2
3 #
4 # Copyright (c) 2011, Novell Inc.
5 #
6 # This program is licensed under the BSD license, read LICENSE.BSD
7 # for further information
8 #
9
10 # pysolv a little software installer demoing the sat solver library/bindings
11
12 # things it does:
13 # - understands globs for package names / dependencies
14 # - understands .arch suffix
15 # - repository data caching
16 # - on demand loading of secondary repository data
17 # - checksum verification
18 # - deltarpm support
19 # - installation of commandline packages
20 #
21 # things not yet ported:
22 # - gpg verification
23 # - file conflicts
24 # - fastestmirror implementation
25 #
26 # things available in the library but missing from pysolv:
27 # - vendor policy loading
28 # - soft locks file handling
29 # - multi version handling
30
31 import sys
32 import os
33 import glob
34 import solv
35 import re
36 import tempfile
37 import time
38 import subprocess
39 import rpm
40 from stat import *
41 from solv import Pool, Repo, Dataiterator, Job, Solver, Transaction, Selection
42 from iniparse import INIConfig
43 from optparse import OptionParser
44
45 #import gc
46 #gc.set_debug(gc.DEBUG_LEAK)
47
48 class repo_generic(dict):
49     def __init__(self, name, type, attribs = {}):
50         for k in attribs:
51             self[k] = attribs[k]
52         self.name = name
53         self.type = type
54
55     def calc_cookie_file(self, filename):
56         chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256)
57         chksum.add("1.1")
58         chksum.add_stat(filename)
59         return chksum.raw()
60
61     def calc_cookie_fp(self, fp):
62         chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256)
63         chksum.add("1.1");
64         chksum.add_fp(fp)
65         return chksum.raw()
66
67     def calc_cookie_ext(self, f, cookie):
68         chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256)
69         chksum.add("1.1");
70         chksum.add(cookie)
71         chksum.add_fstat(f.fileno())
72         extcookie = chksum.raw()
73         # compatibility to c code
74         if ord(extcookie[0]) == 0:
75             extcookie[0] = chr(1)
76         return extcookie
77
78     def cachepath(self, ext = None):
79         path = re.sub(r'^\.', '_', self.name)
80         if ext:
81             path += "_" + ext + ".solvx"
82         else:
83             path += ".solv"
84         return "/var/cache/solv/" + re.sub(r'[/]', '_', path)
85         
86     def load(self, pool):
87         self.handle = pool.add_repo(self.name)
88         self.handle.appdata = self
89         self.handle.priority = 99 - self['priority']
90         dorefresh = bool(int(self['autorefresh']))
91         if dorefresh:
92             try:
93                 st = os.stat(self.cachepath())
94                 if self['metadata_expire'] == -1 or time.time() - st[ST_MTIME] < self['metadata_expire']:
95                     dorefresh = False
96             except OSError, e:
97                 pass
98         self['cookie'] = ''
99         if not dorefresh and self.usecachedrepo(None):
100             print "repo: '%s': cached" % self.name
101             return True
102         return self.load_if_changed()
103
104     def load_if_changed(self):
105         return False
106
107     def load_ext(self, repodata):
108         return False
109
110     def setfromurls(self, urls):
111         if not urls:
112             return
113         url = urls[0]
114         print "[using mirror %s]" % re.sub(r'^(.*?/...*?)/.*$', r'\1', url)
115         self['baseurl'] = url
116
117     def setfrommetalink(self, metalink):
118         nf = self.download(metalink, False, None)
119         if not nf:
120             return None
121         f = os.fdopen(os.dup(solv.xfileno(nf)), 'r')
122         solv.xfclose(nf)
123         urls = []
124         chksum = None
125         for l in f.readlines():
126             l = l.strip()
127             m = re.match(r'^<hash type="sha256">([0-9a-fA-F]{64})</hash>', l)
128             if m:
129                 chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256, m.group(1))
130             m = re.match(r'^<url.*>(https?://.+)repodata/repomd.xml</url>', l)
131             if m:
132                 urls.append(m.group(1))
133         if not urls:
134             chksum = None       # in case the metalink is about a different file
135         f.close()
136         self.setfromurls(urls)
137         return chksum
138         
139     def setfrommirrorlist(self, mirrorlist):
140         nf = self.download(mirrorlist, False, None)
141         if not nf:
142             return
143         f = os.fdopen(os.dup(solv.xfileno(nf)), 'r')
144         solv.xfclose(nf)
145         urls = []
146         for l in f.readline():
147             l = l.strip()
148             if l[0:6] == 'http://' or l[0:7] == 'https://':
149                 urls.append(l)
150         self.setfromurls(urls)
151         f.close()
152         
153     def download(self, file, uncompress, chksum, markincomplete=False):
154         url = None
155         if 'baseurl' not in self:
156             if 'metalink' in self:
157                 if file != self['metalink']:
158                     metalinkchksum = self.setfrommetalink(self['metalink'])
159                     if file == 'repodata/repomd.xml' and metalinkchksum and not chksum:
160                         chksum = metalinkchksum
161                 else:
162                     url = file
163             elif 'mirrorlist' in self:
164                 if file != self['mirrorlist']:
165                     self.setfrommirrorlist(self['mirrorlist'])
166                 else:
167                     url = file
168         if not url:
169             if 'baseurl' not in self:
170                 print "%s: no baseurl" % self.name
171                 return None
172             url = re.sub(r'/$', '', self['baseurl']) + '/' + file
173         f = tempfile.TemporaryFile()
174         st = subprocess.call(['curl', '-f', '-s', '-L', url], stdout=f.fileno())
175         if os.lseek(f.fileno(), 0, os.SEEK_CUR) == 0 and (st == 0 or not chksum):
176             return None
177         os.lseek(f.fileno(), 0, os.SEEK_SET)
178         if st:
179             print "%s: download error %d" % (file, st)
180             if markincomplete:
181                 self['incomplete'] = True
182             return None
183         if chksum:
184             fchksum = solv.Chksum(chksum.type)
185             if not fchksum:
186                 print "%s: unknown checksum type" % file
187                 if markincomplete:
188                     self['incomplete'] = True
189                 return None
190             fchksum.add_fd(f.fileno())
191             if fchksum != chksum:
192                 print "%s: checksum mismatch" % file
193                 if markincomplete:
194                     self['incomplete'] = True
195                 return None
196         if uncompress:
197             return solv.xfopen_fd(file, os.dup(f.fileno()))
198         return solv.xfopen_fd(None, os.dup(f.fileno()))
199
200     def usecachedrepo(self, ext, mark=False):
201         if not ext:
202             cookie = self['cookie']
203         else:
204             cookie = self['extcookie']
205         try: 
206             repopath = self.cachepath(ext)
207             f = open(repopath, 'r')
208             f.seek(-32, os.SEEK_END)
209             fcookie = f.read(32)
210             if len(fcookie) != 32:
211                 return False
212             if cookie and fcookie != cookie:
213                 return False
214             if self.type != 'system' and not ext:
215                 f.seek(-32 * 2, os.SEEK_END)
216                 fextcookie = f.read(32)
217                 if len(fextcookie) != 32:
218                     return False
219             f.seek(0)
220             flags = 0
221             if ext:
222                 flags = Repo.REPO_USE_LOADING|Repo.REPO_EXTEND_SOLVABLES
223                 if ext != 'DL':
224                     flags |= Repo.REPO_LOCALPOOL
225             if not self.handle.add_solv(f, flags):
226                 return False
227             if self.type != 'system' and not ext:
228                 self['cookie'] = fcookie
229                 self['extcookie'] = fextcookie
230             if mark:
231                 # no futimes in python?
232                 try:
233                     os.utime(repopath, None)
234                 except Exception, e:
235                     pass
236         except IOError, e:
237             return False
238         return True
239
240     def writecachedrepo(self, ext, info=None):
241         try:
242             if not os.path.isdir("/var/cache/solv"):
243                 os.mkdir("/var/cache/solv", 0755)
244             (fd, tmpname) = tempfile.mkstemp(prefix='.newsolv-', dir='/var/cache/solv')
245             os.fchmod(fd, 0444)
246             f = os.fdopen(fd, 'w+')
247             if not info:
248                 self.handle.write(f)
249             elif ext:
250                 info.write(f)
251             else:       # rewrite_repos case
252                 self.handle.write_first_repodata(f)
253             if self.type != 'system' and not ext:
254                 if 'extcookie' not in self:
255                     self['extcookie'] = self.calc_cookie_ext(f, self['cookie'])
256                 f.write(self['extcookie'])
257             if not ext:
258                 f.write(self['cookie'])
259             else:
260                 f.write(self['extcookie'])
261             f.close()
262             if self.handle.iscontiguous():
263                 # switch to saved repo to activate paging and save memory
264                 nf = solv.xfopen(tmpname)
265                 if not ext:
266                     # main repo
267                     self.handle.empty()
268                     if not self.handle.add_solv(nf, Repo.SOLV_ADD_NO_STUBS):
269                         sys.exit("internal error, cannot reload solv file")
270                 else:
271                     # extension repodata
272                     # need to extend to repo boundaries, as this is how
273                     # info.write() has written the data
274                     info.extend_to_repo()
275                     # LOCALPOOL does not help as pool already contains all ids
276                     info.add_solv(nf, Repo.REPO_EXTEND_SOLVABLES)
277                 solv.xfclose(nf)
278             os.rename(tmpname, self.cachepath(ext))
279         except IOError, e:
280             if tmpname:
281                 os.unlink(tmpname)
282                 
283     def updateaddedprovides(self, addedprovides):
284         if 'incomplete' in self:
285             return 
286         if not hasattr(self, 'handle'):
287             return 
288         if self.handle.isempty():
289             return
290         # make sure there's just one real repodata with extensions
291         repodata = self.handle.first_repodata()
292         if not repodata:
293             return
294         oldaddedprovides = repodata.lookup_idarray(solv.SOLVID_META, solv.REPOSITORY_ADDEDFILEPROVIDES)
295         if not set(addedprovides) <= set(oldaddedprovides):
296             for id in addedprovides:
297                 repodata.add_idarray(solv.SOLVID_META, solv.REPOSITORY_ADDEDFILEPROVIDES, id)
298             repodata.internalize()
299             self.writecachedrepo(None, repodata)
300
301     def packagespath(self):
302         return ''
303
304 class repo_repomd(repo_generic):
305     def load_if_changed(self):
306         print "rpmmd repo '%s':" % self.name,
307         sys.stdout.flush()
308         f = self.download("repodata/repomd.xml", False, None, None)
309         if not f:
310             print "no repomd.xml file, skipped"
311             self.handle.free(True)
312             del self.handle
313             return False
314         self['cookie'] = self.calc_cookie_fp(f)
315         if self.usecachedrepo(None, True):
316             print "cached"
317             solv.xfclose(f)
318             return True
319         self.handle.add_repomdxml(f, 0)
320         solv.xfclose(f)
321         print "fetching"
322         (filename, filechksum) = self.find('primary')
323         if filename:
324             f = self.download(filename, True, filechksum, True)
325             if f:
326                 self.handle.add_rpmmd(f, None, 0)
327                 solv.xfclose(f)
328             if 'incomplete' in self:
329                 return False # hopeless, need good primary
330         (filename, filechksum) = self.find('updateinfo')
331         if filename:
332             f = self.download(filename, True, filechksum, True)
333             if f:
334                 self.handle.add_updateinfoxml(f, 0)
335                 solv.xfclose(f)
336         self.add_exts()
337         if 'incomplete' not in self:
338             self.writecachedrepo(None)
339         # must be called after writing the repo
340         self.handle.create_stubs()
341         return True
342
343     def find(self, what):
344         di = self.handle.Dataiterator(solv.SOLVID_META, solv.REPOSITORY_REPOMD_TYPE, what, Dataiterator.SEARCH_STRING)
345         di.prepend_keyname(solv.REPOSITORY_REPOMD)
346         for d in di:
347             dp = d.parentpos()
348             filename = dp.lookup_str(solv.REPOSITORY_REPOMD_LOCATION)
349             chksum = dp.lookup_checksum(solv.REPOSITORY_REPOMD_CHECKSUM)
350             if filename and not chksum:
351                 print "no %s file checksum!" % filename
352                 filename = None
353                 chksum = None
354             if filename:
355                 return (filename, chksum)
356         return (None, None)
357         
358     def add_ext(self, repodata, what, ext):
359         filename, chksum = self.find(what)
360         if not filename and what == 'deltainfo':
361             filename, chksum = self.find('prestodelta')
362         if not filename:
363             return
364         handle = repodata.new_handle()
365         repodata.set_poolstr(handle, solv.REPOSITORY_REPOMD_TYPE, what)
366         repodata.set_str(handle, solv.REPOSITORY_REPOMD_LOCATION, filename)
367         repodata.set_checksum(handle, solv.REPOSITORY_REPOMD_CHECKSUM, chksum)
368         if ext == 'DL':
369             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOSITORY_DELTAINFO)
370             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_FLEXARRAY)
371         elif ext == 'FL':
372             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_FILELIST)
373             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRSTRARRAY)
374         repodata.add_flexarray(solv.SOLVID_META, solv.REPOSITORY_EXTERNAL, handle)
375
376     def add_exts(self):
377         repodata = self.handle.add_repodata(0)
378         self.add_ext(repodata, 'deltainfo', 'DL')
379         self.add_ext(repodata, 'filelists', 'FL')
380         repodata.internalize()
381     
382     def load_ext(self, repodata):
383         repomdtype = repodata.lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_TYPE)
384         if repomdtype == 'filelists':
385             ext = 'FL'
386         elif repomdtype == 'deltainfo':
387             ext = 'DL'
388         else:
389             return False
390         sys.stdout.write("[%s:%s: " % (self.name, ext))
391         if self.usecachedrepo(ext):
392             sys.stdout.write("cached]\n")
393             sys.stdout.flush()
394             return True
395         sys.stdout.write("fetching]\n")
396         sys.stdout.flush()
397         filename = repodata.lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_LOCATION)
398         filechksum = repodata.lookup_checksum(solv.SOLVID_META, solv.REPOSITORY_REPOMD_CHECKSUM)
399         f = self.download(filename, True, filechksum)
400         if not f:
401             return False
402         if ext == 'FL':
403             self.handle.add_rpmmd(f, 'FL', Repo.REPO_USE_LOADING|Repo.REPO_EXTEND_SOLVABLES)
404         elif ext == 'DL':
405             self.handle.add_deltainfoxml(f, Repo.REPO_USE_LOADING)
406         solv.xfclose(f)
407         self.writecachedrepo(ext, repodata)
408         return True
409
410 class repo_susetags(repo_generic):
411     def load_if_changed(self):
412         print "susetags repo '%s':" % self.name,
413         sys.stdout.flush()
414         f = self.download("content", False, None, None)
415         if not f:
416             print "no content file, skipped"
417             self.handle.free(True)
418             del self.handle
419             return False
420         self['cookie'] = self.calc_cookie_fp(f)
421         if self.usecachedrepo(None, True):
422             print "cached"
423             solv.xfclose(f)
424             return True
425         self.handle.add_content(f, 0)
426         solv.xfclose(f)
427         print "fetching"
428         defvendorid = self.handle.lookup_id(solv.SOLVID_META, solv.SUSETAGS_DEFAULTVENDOR)
429         descrdir = self.handle.lookup_str(solv.SOLVID_META, solv.SUSETAGS_DESCRDIR)
430         if not descrdir:
431             descrdir = "suse/setup/descr"
432         (filename, filechksum) = self.find('packages.gz')
433         if not filename:
434             (filename, filechksum) = self.find('packages')
435         if filename:
436             f = self.download(descrdir + '/' + filename, True, filechksum, True)
437             if f:
438                 self.handle.add_susetags(f, defvendorid, None, Repo.REPO_NO_INTERNALIZE|Repo.SUSETAGS_RECORD_SHARES)
439                 solv.xfclose(f)
440                 (filename, filechksum) = self.find('packages.en.gz')
441                 if not filename:
442                     (filename, filechksum) = self.find('packages.en')
443                 if filename:
444                     f = self.download(descrdir + '/' + filename, True, filechksum, True)
445                     if f:
446                         self.handle.add_susetags(f, defvendorid, None, Repo.REPO_NO_INTERNALIZE|Repo.REPO_REUSE_REPODATA|Repo.REPO_EXTEND_SOLVABLES)
447                         solv.xfclose(f)
448                 self.handle.internalize()
449         self.add_exts()
450         if 'incomplete' not in self:
451             self.writecachedrepo(None)
452         # must be called after writing the repo
453         self.handle.create_stubs()
454         return True
455
456     def find(self, what):
457         di = self.handle.Dataiterator(solv.SOLVID_META, solv.SUSETAGS_FILE_NAME, what, Dataiterator.SEARCH_STRING)
458         di.prepend_keyname(solv.SUSETAGS_FILE)
459         for d in di:
460             dp = d.parentpos()
461             chksum = dp.lookup_checksum(solv.SUSETAGS_FILE_CHECKSUM)
462             return (what, chksum)
463         return (None, None)
464
465     def add_ext(self, repodata, what, ext):
466         (filename, chksum) = self.find(what)
467         if not filename:
468             return
469         handle = repodata.new_handle()
470         repodata.set_str(handle, solv.SUSETAGS_FILE_NAME, filename)
471         if chksum:
472             repodata.set_checksum(handle, solv.SUSETAGS_FILE_CHECKSUM, chksum)
473         if ext == 'DU':
474             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_DISKUSAGE)
475             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRNUMNUMARRAY)
476         elif ext == 'FL':
477             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_FILELIST)
478             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRSTRARRAY)
479         else:
480             for langtag, langtagtype in [
481                 (solv.SOLVABLE_SUMMARY, solv.REPOKEY_TYPE_STR),
482                 (solv.SOLVABLE_DESCRIPTION, solv.REPOKEY_TYPE_STR),
483                 (solv.SOLVABLE_EULA, solv.REPOKEY_TYPE_STR),
484                 (solv.SOLVABLE_MESSAGEINS, solv.REPOKEY_TYPE_STR),
485                 (solv.SOLVABLE_MESSAGEDEL, solv.REPOKEY_TYPE_STR),
486                 (solv.SOLVABLE_CATEGORY, solv.REPOKEY_TYPE_ID)
487             ]:
488                 repodata.add_idarray(handle, solv.REPOSITORY_KEYS, self.handle.pool.id2langid(langtag, ext, 1))
489                 repodata.add_idarray(handle, solv.REPOSITORY_KEYS, langtagtype)
490         repodata.add_flexarray(solv.SOLVID_META, solv.REPOSITORY_EXTERNAL, handle)
491         
492     def add_exts(self):
493         repodata = self.handle.add_repodata(0)
494         di = self.handle.Dataiterator(solv.SOLVID_META, solv.SUSETAGS_FILE_NAME, None, 0)
495         di.prepend_keyname(solv.SUSETAGS_FILE)
496         for d in di:
497             filename = d.str()
498             if not filename:
499                 continue
500             if filename[0:9] != "packages.":
501                 continue
502             if len(filename) == 11 and filename != "packages.gz":
503                 ext = filename[9:11]
504             elif filename[11:12] == ".":
505                 ext = filename[9:11]
506             else:
507                 continue
508             if ext == "en":
509                 continue
510             self.add_ext(repodata, filename, ext)
511         repodata.internalize()
512
513     def load_ext(self, repodata):
514         filename = repodata.lookup_str(solv.SOLVID_META, solv.SUSETAGS_FILE_NAME)
515         ext = filename[9:11]
516         sys.stdout.write("[%s:%s: " % (self.name, ext))
517         if self.usecachedrepo(ext):
518             sys.stdout.write("cached]\n")
519             sys.stdout.flush()
520             return True
521         sys.stdout.write("fetching]\n")
522         sys.stdout.flush()
523         defvendorid = self.handle.lookup_id(solv.SOLVID_META, solv.SUSETAGS_DEFAULTVENDOR)
524         descrdir = self.handle.lookup_str(solv.SOLVID_META, solv.SUSETAGS_DESCRDIR)
525         if not descrdir:
526             descrdir = "suse/setup/descr"
527         filechksum = repodata.lookup_checksum(solv.SOLVID_META, solv.SUSETAGS_FILE_CHECKSUM)
528         f = self.download(descrdir + '/' + filename, True, filechksum)
529         if not f:
530             return False
531         self.handle.add_susetags(f, defvendorid, ext, Repo.REPO_USE_LOADING|Repo.REPO_EXTEND_SOLVABLES)
532         solv.xfclose(f)
533         self.writecachedrepo(ext, repodata)
534         return True
535
536     def packagespath(self):
537         datadir = repo.handle.lookup_str(solv.SOLVID_META, solv.SUSETAGS_DATADIR)
538         if not datadir:
539             datadir = 'suse'
540         return datadir + '/'
541
542 class repo_unknown(repo_generic):
543     def load(self, pool):
544         print "unsupported repo '%s': skipped" % self.name
545         return False
546
547 class repo_system(repo_generic):
548     def load(self, pool):
549         self.handle = pool.add_repo(self.name)
550         self.handle.appdata = self
551         pool.installed = self.handle
552         print "rpm database:",
553         self['cookie'] = self.calc_cookie_file("/var/lib/rpm/Packages")
554         if self.usecachedrepo(None):
555             print "cached"
556             return True
557         print "reading"
558         if hasattr(self.handle.__class__, 'add_products'):
559             self.handle.add_products("/etc/products.d", Repo.REPO_NO_INTERNALIZE)
560         self.handle.add_rpmdb(None, Repo.REPO_REUSE_REPODATA)
561         self.writecachedrepo(None)
562         return True
563
564 class repo_cmdline(repo_generic):
565     def load(self, pool):
566         self.handle = pool.add_repo(self.name)
567         self.handle.appdata = self 
568         return True
569
570 def load_stub(repodata):
571     repo = repodata.repo.appdata
572     if repo:
573         return repo.load_ext(repodata)
574     return False
575
576
577 parser = OptionParser(usage="usage: solv.py [options] COMMAND")
578 parser.add_option('-r', '--repo', action="append", type="string", dest="repos")
579 (options, args) = parser.parse_args()
580 if not args:
581     parser.print_help(sys.stderr)
582     sys.exit(1)
583
584 cmd = args[0]
585 args = args[1:]
586 if cmd == 'li':
587     cmd = 'list'
588 if cmd == 'in':
589     cmd = 'install'
590 if cmd == 'rm':
591     cmd = 'erase'
592 if cmd == 've':
593     cmd = 'verify'
594 if cmd == 'se':
595     cmd = 'search'
596
597
598 # read all repo configs
599 repos = []
600 reposdirs = []
601 if os.path.isdir("/etc/zypp/repos.d"):
602   reposdirs = [ "/etc/zypp/repos.d" ]
603 else:
604   reposdirs = [ "/etc/yum/repos.d" ]
605
606 for reposdir in ["/etc/zypp/repos.d"]:
607     if not os.path.isdir(reposdir):
608         continue
609     for reponame in sorted(glob.glob('%s/*.repo' % reposdir)):
610         cfg = INIConfig(open(reponame))
611         for alias in cfg:
612             repoattr = {'enabled': 0, 'priority': 99, 'autorefresh': 1, 'type': 'rpm-md', 'metadata_expire': 900}
613             for k in cfg[alias]:
614                 repoattr[k] = cfg[alias][k]
615             if 'mirrorlist' in repoattr and 'metalink' not in repoattr:
616                 if repoattr['mirrorlist'].find('/metalink'):
617                     repoattr['metalink'] = repoattr['mirrorlist']
618                     del repoattr['mirrorlist']
619             if repoattr['type'] == 'rpm-md':
620                 repo = repo_repomd(alias, 'repomd', repoattr)
621             elif repoattr['type'] == 'yast2':
622                 repo = repo_susetags(alias, 'susetags', repoattr)
623             else:
624                 repo = repo_unknown(alias, 'unknown', repoattr)
625             repos.append(repo)
626
627 pool = solv.Pool()
628 pool.setarch(os.uname()[4])
629 pool.set_loadcallback(load_stub)
630
631 # now load all enabled repos into the pool
632 sysrepo = repo_system('@System', 'system')
633 sysrepo.load(pool)
634 for repo in repos:
635     if int(repo['enabled']):
636         repo.load(pool)
637     
638 repolimiter = None
639 if options.repos:
640     for reponame in options.repos:
641         mrepos = [ repo for repo in repos if repo.name == reponame ]
642         if not mrepos:
643             print "no repository matches '%s'" % reponame
644             sys.exit(1)
645         repo = mrepos[0]
646         if hasattr(repo, 'handle'):
647             if not repolimiter:
648                 repolimiter = pool.Selection()
649             repolimiter.addsimple(Job.SOLVER_SOLVABLE_REPO|Job.SOLVER_SETREPO|Job.SOLVER_SETVENDOR, repo.handle.id)
650
651 if cmd == 'search':
652     matches = {}
653     di = pool.Dataiterator(0, solv.SOLVABLE_NAME, args[0], Dataiterator.SEARCH_SUBSTRING|Dataiterator.SEARCH_NOCASE)
654     for d in di:
655         matches[d.solvid] = True
656     for solvid in sorted(matches.keys()):
657         print " - %s [%s]: %s" % (pool.solvid2str(solvid), pool.solvables[solvid].repo.name, pool.lookup_str(solvid, solv.SOLVABLE_SUMMARY))
658     sys.exit(0)
659
660 cmdlinerepo = None
661 if cmd == 'list' or cmd == 'info' or cmd == 'install':
662     for arg in args:
663         if arg.endswith(".rpm") and os.access(arg, os.R_OK):
664             if not cmdlinerepo:
665                 cmdlinerepo = repo_cmdline('@commandline', 'cmdline')
666                 cmdlinerepo.load(pool)
667                 cmdlinerepo['packages'] = {}
668             cmdlinerepo['packages'][arg] = cmdlinerepo.handle.add_rpm(arg, Repo.REPO_REUSE_REPODATA|Repo.REPO_NO_INTERNALIZE)
669     if cmdlinerepo:
670         cmdlinerepo.handle.internalize()
671
672 addedprovides = pool.addfileprovides_queue()
673 if addedprovides:
674     sysrepo.updateaddedprovides(addedprovides)
675     for repo in repos:
676         repo.updateaddedprovides(addedprovides)
677
678 pool.createwhatprovides()
679
680 # convert arguments into jobs
681 jobs = []
682 for arg in args:
683     if cmdlinerepo and arg in cmdlinerepo['packages']:
684         jobs.append(pool.Job(Job.SOLVER_SOLVABLE, cmdlinerepo['packages'][arg]))
685     else:
686         flags = Selection.SELECTION_NAME|Selection.SELECTION_PROVIDES|Selection.SELECTION_GLOB
687         if len(arg) and arg[0] == '/':
688             flags |= Selection.SELECTION_FILELIST
689             if cmd == 'erase':
690                 flags |= Selection.SELECTION_INSTALLED_ONLY
691         sel = pool.select(arg, flags)
692         if repolimiter:
693            sel.limit(repolimiter)
694         if sel.isempty():
695             sel = pool.select(arg, flags | Selection.SELECTION_NOCASE)
696             if repolimiter:
697                sel.limit(repolimiter)
698             if not sel.isempty():
699                 print "[ignoring case for '%s']" % arg
700         if sel.isempty():
701             print "nothing matches '%s'" % arg
702             sys.exit(1)
703         if sel.flags() & Selection.SELECTION_FILELIST:
704             print "[using file list match for '%s']" % arg
705         if sel.flags() & Selection.SELECTION_PROVIDES:
706             print "[using capability match for '%s']" % arg
707         jobs += sel.jobs(0)
708
709 if not jobs and (cmd == 'up' or cmd == 'dup' or cmd == 'verify' or repolimiter):
710     sel = pool.Selection()
711     sel.addsimple(Job.SOLVER_SOLVABLE_ALL, 0)
712     if repolimiter:
713        sel.limit(repolimiter)
714     jobs += sel.jobs(0)
715
716 if cmd == 'list' or cmd == 'info':
717     if not jobs:
718         print "no package matched."
719         sys.exit(1)
720     for job in jobs:
721         for s in job.solvables():
722             if cmd == 'info':
723                 print "Name:        %s" % s
724                 print "Repo:        %s" % s.repo
725                 print "Summary:     %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
726                 str = s.lookup_str(solv.SOLVABLE_URL)
727                 if str:
728                     print "Url:         %s" % str
729                 str = s.lookup_str(solv.SOLVABLE_LICENSE)
730                 if str:
731                     print "License:     %s" % str
732                 print "Description:\n%s" % s.lookup_str(solv.SOLVABLE_DESCRIPTION)
733                 print
734             else:
735                 print "  - %s [%s]" % (s, s.repo)
736                 print "    %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
737     sys.exit(0)
738
739 if cmd == 'install' or cmd == 'erase' or cmd == 'up' or cmd == 'dup' or cmd == 'verify':
740     if not jobs:
741         print "no package matched."
742         sys.exit(1)
743     for job in jobs:
744         if cmd == 'up':
745             # up magic: use install instead of update if no installed package matches
746             if job.how == Job.SOLVER_SOLVABLE_ALL or filter(lambda s: s.isinstalled(), job.solvables()):
747                 job.how |= Job.SOLVER_UPDATE
748             else:
749                 job.how |= Job.SOLVER_INSTALL
750         elif cmd == 'install':
751             job.how |= Job.SOLVER_INSTALL
752         elif cmd == 'erase':
753             job.how |= Job.SOLVER_ERASE
754         elif cmd == 'dup':
755             job.how |= Job.SOLVER_DISTUPGRADE
756         elif cmd == 'verify':
757             job.how |= Job.SOLVER_VERIFY
758
759     #pool.set_debuglevel(2)
760     solver = None
761     while True:
762         solver = pool.Solver()
763         solver.set_flag(Solver.SOLVER_FLAG_SPLITPROVIDES, 1);
764         if cmd == 'erase':
765             solver.set_flag(Solver.SOLVER_FLAG_ALLOW_UNINSTALL, 1);
766         problems = solver.solve(jobs)
767         if not problems:
768             break
769         for problem in problems:
770             print "Problem %d:" % problem.id
771             r = problem.findproblemrule()
772             ri = r.info()
773             print ri.problemstr()
774             solutions = problem.solutions()
775             for solution in solutions:
776                 print "  Solution %d:" % solution.id
777                 elements = solution.elements(True)
778                 for element in elements:
779                     print "  - %s" % element.str()
780                 print
781             sol = ''
782             while not (sol == 's' or sol == 'q' or (sol.isdigit() and int(sol) >= 1 and int(sol) <= len(solutions))):
783                 sys.stdout.write("Please choose a solution: ")
784                 sys.stdout.flush()
785                 sol = sys.stdin.readline().strip()
786             if sol == 's':
787                 continue        # skip problem
788             if sol == 'q':
789                 sys.exit(1)
790             solution = solutions[int(sol) - 1]
791             for element in solution.elements():
792                 newjob = element.Job()
793                 if element.type == Solver.SOLVER_SOLUTION_JOB:
794                     jobs[element.jobidx] = newjob
795                 else:
796                     if newjob and newjob not in jobs:
797                         jobs.append(newjob)
798                         
799     # no problems, show transaction
800     trans = solver.transaction()
801     del solver
802     if trans.isempty():
803         print "Nothing to do."
804         sys.exit(0)
805     print
806     print "Transaction summary:"
807     print
808     for cl in trans.classify():
809         if cl.type == Transaction.SOLVER_TRANSACTION_ERASE:
810             print "%d erased packages:" % cl.count
811         elif cl.type == Transaction.SOLVER_TRANSACTION_INSTALL:
812             print "%d installed packages:" % cl.count
813         elif cl.type == Transaction.SOLVER_TRANSACTION_REINSTALLED:
814             print "%d reinstalled packages:" % cl.count
815         elif cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
816             print "%d downgraded packages:" % cl.count
817         elif cl.type == Transaction.SOLVER_TRANSACTION_CHANGED:
818             print "%d changed packages:" % cl.count
819         elif cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED:
820             print "%d upgraded packages:" % cl.count
821         elif cl.type == Transaction.SOLVER_TRANSACTION_VENDORCHANGE:
822             print "%d vendor changes from '%s' to '%s':" % (cl.count, pool.id2str(cl.fromid), pool.id2str(cl.toid))
823         elif cl.type == Transaction.SOLVER_TRANSACTION_ARCHCHANGE:
824             print "%d arch changes from '%s' to '%s':" % (cl.count, pool.id2str(cl.fromid), pool.id2str(cl.toid))
825         else:
826             continue
827         for p in cl.solvables():
828             if cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
829                 op = trans.othersolvable(p)
830                 print "  - %s -> %s" % (p, op)
831             else:
832                 print "  - %s" % p
833         print
834     print "install size change: %d K" % trans.calc_installsizechange()
835     print
836     
837     while True:
838         sys.stdout.write("OK to continue (y/n)? ")
839         sys.stdout.flush()
840         yn = sys.stdin.readline().strip()
841         if yn == 'y': break
842         if yn == 'n': sys.exit(1)
843     newpkgs = trans.newpackages()
844     newpkgsfp = {}
845     if newpkgs:
846         downloadsize = 0
847         for p in newpkgs:
848             downloadsize += p.lookup_num(solv.SOLVABLE_DOWNLOADSIZE)
849         print "Downloading %d packages, %d K" % (len(newpkgs), downloadsize)
850         for p in newpkgs:
851             repo = p.repo.appdata
852             location, medianr = p.lookup_location()
853             if not location:
854                 continue
855             if repo.type == 'commandline':
856                 f = solv.xfopen(location)
857                 if not f:
858                     sys.exit("\n%s: %s not found" % location)
859                 newpkgsfp[p.id] = f
860                 continue
861             if not sysrepo.handle.isempty() and os.access('/usr/bin/applydeltarpm', os.X_OK):
862                 pname = p.name
863                 di = p.repo.Dataiterator(solv.SOLVID_META, solv.DELTA_PACKAGE_NAME, pname, Dataiterator.SEARCH_STRING)
864                 di.prepend_keyname(solv.REPOSITORY_DELTAINFO)
865                 for d in di:
866                     dp = d.parentpos()
867                     if dp.lookup_id(solv.DELTA_PACKAGE_EVR) != p.evrid or dp.lookup_id(solv.DELTA_PACKAGE_ARCH) != p.archid:
868                         continue
869                     baseevrid = dp.lookup_id(solv.DELTA_BASE_EVR)
870                     candidate = None
871                     for installedp in pool.whatprovides(p.nameid):
872                         if installedp.isinstalled() and installedp.nameid == p.nameid and installedp.archid == p.archid and installedp.evrid == baseevrid:
873                             candidate = installedp
874                     if not candidate:
875                         continue
876                     seq = dp.lookup_str(solv.DELTA_SEQ_NAME) + '-' + dp.lookup_str(solv.DELTA_SEQ_EVR) + '-' + dp.lookup_str(solv.DELTA_SEQ_NUM)
877                     st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, '-c', '-s', seq])
878                     if st:
879                         continue
880                     chksum = dp.lookup_checksum(solv.DELTA_CHECKSUM)
881                     if not chksum:
882                         continue
883                     dloc = dp.lookup_str(solv.DELTA_LOCATION_DIR) + '/' + dp.lookup_str(solv.DELTA_LOCATION_NAME) + '-' + dp.lookup_str(solv.DELTA_LOCATION_EVR) + '.' + dp.lookup_str(solv.DELTA_LOCATION_SUFFIX)
884                     f = repo.download(dloc, False, chksum)
885                     if not f:
886                         continue
887                     nf = tempfile.TemporaryFile()
888                     nf = os.dup(nf.fileno())
889                     st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, "/dev/fd/%d" % solv.xfileno(f), "/dev/fd/%d" % nf])
890                     solv.xfclose(f)
891                     os.lseek(nf, 0, os.SEEK_SET)
892                     newpkgsfp[p.id] = solv.xfopen_fd("", nf)
893                     break
894                 if p.id in newpkgsfp:
895                     sys.stdout.write("d")
896                     sys.stdout.flush()
897                     continue
898                         
899             chksum = p.lookup_checksum(solv.SOLVABLE_CHECKSUM)
900             location = repo.packagespath() + location
901             f = repo.download(location, False, chksum)
902             if not f:
903                 sys.exit("\n%s: %s not found in repository" % (repo.name, location))
904             newpkgsfp[p.id] = f
905             sys.stdout.write(".")
906             sys.stdout.flush()
907         print
908     print "Committing transaction:"
909     print
910     ts = rpm.TransactionSet('/')
911     ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
912     erasenamehelper = {}
913     for p in trans.steps():
914         type = trans.steptype(p, Transaction.SOLVER_TRANSACTION_RPM_ONLY)
915         if type == Transaction.SOLVER_TRANSACTION_ERASE:
916             rpmdbid = p.lookup_num(solv.RPM_RPMDBID)
917             erasenamehelper[p.name] = p
918             if not rpmdbid:
919                 sys.exit("\ninternal error: installed package %s has no rpmdbid\n" % p)
920             ts.addErase(rpmdbid)
921         elif type == Transaction.SOLVER_TRANSACTION_INSTALL:
922             f = newpkgsfp[p.id]
923             h = ts.hdrFromFdno(solv.xfileno(f))
924             os.lseek(solv.xfileno(f), 0, os.SEEK_SET)
925             ts.addInstall(h, p, 'u')
926         elif type == Transaction.SOLVER_TRANSACTION_MULTIINSTALL:
927             f = newpkgsfp[p.id]
928             h = ts.hdrFromFdno(solv.xfileno(f))
929             os.lseek(solv.xfileno(f), 0, os.SEEK_SET)
930             ts.addInstall(h, p, 'i')
931     checkproblems = ts.check()
932     if checkproblems:
933         print checkproblems
934         sys.exit("Sorry.")
935     ts.order()
936     def runCallback(reason, amount, total, p, d):
937         if reason == rpm.RPMCALLBACK_INST_OPEN_FILE:
938             return solv.xfileno(newpkgsfp[p.id])
939         if reason == rpm.RPMCALLBACK_INST_START:
940             print "install", p
941         if reason == rpm.RPMCALLBACK_UNINST_START:
942             # argh, p is just the name of the package
943             if p in erasenamehelper:
944                 p = erasenamehelper[p]
945                 print "erase", p
946     runproblems = ts.run(runCallback, '')
947     if runproblems:
948         print runproblems
949         sys.exit(1)
950     sys.exit(0)
951
952 print "unknown command", cmd
953 sys.exit(1)
954
955 # vim: sw=4 et