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