e898c095cc9f113694d751b5c88b2f610904204a
[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 False
103
104     def load_ext(self, repodata):
105         return False
106
107     def setfromurls(self, urls):
108         if not urls:
109             return
110         url = urls[0]
111         print "[using mirror %s]" % re.sub(r'^(.*?/...*?)/.*$', r'\1', url)
112         self['baseurl'] = url
113
114     def setfrommetalink(self, metalink):
115         f = self.download(metalink, False, None)
116         if not f:
117             return None
118         f = os.fdopen(f.dup(), 'r')
119         urls = []
120         chksum = None
121         for l in f.readlines():
122             l = l.strip()
123             m = re.match(r'^<hash type="sha256">([0-9a-fA-F]{64})</hash>', l)
124             if m:
125                 chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256, m.group(1))
126             m = re.match(r'^<url.*>(https?://.+)repodata/repomd.xml</url>', l)
127             if m:
128                 urls.append(m.group(1))
129         if not urls:
130             chksum = None       # in case the metalink is about a different file
131         f.close()
132         self.setfromurls(urls)
133         return chksum
134         
135     def setfrommirrorlist(self, mirrorlist):
136         f = self.download(mirrorlist, False, None)
137         if not f:
138             return
139         f = os.fdopen(f.dup(), 'r')
140         urls = []
141         for l in f.readline():
142             l = l.strip()
143             if l[0:6] == 'http://' or l[0:7] == 'https://':
144                 urls.append(l)
145         self.setfromurls(urls)
146         f.close()
147         
148     def download(self, file, uncompress, chksum, markincomplete=False):
149         url = None
150         if 'baseurl' not in self:
151             if 'metalink' in self:
152                 if file != self['metalink']:
153                     metalinkchksum = self.setfrommetalink(self['metalink'])
154                     if file == 'repodata/repomd.xml' and metalinkchksum and not chksum:
155                         chksum = metalinkchksum
156                 else:
157                     url = file
158             elif 'mirrorlist' in self:
159                 if file != self['mirrorlist']:
160                     self.setfrommirrorlist(self['mirrorlist'])
161                 else:
162                     url = file
163         if not url:
164             if 'baseurl' not in self:
165                 print "%s: no baseurl" % self.name
166                 return None
167             url = re.sub(r'/$', '', self['baseurl']) + '/' + file
168         f = tempfile.TemporaryFile()
169         st = subprocess.call(['curl', '-f', '-s', '-L', url], stdout=f.fileno())
170         if os.lseek(f.fileno(), 0, os.SEEK_CUR) == 0 and (st == 0 or not chksum):
171             return None
172         os.lseek(f.fileno(), 0, os.SEEK_SET)
173         if st:
174             print "%s: download error %d" % (file, st)
175             if markincomplete:
176                 self['incomplete'] = True
177             return None
178         if chksum:
179             fchksum = solv.Chksum(chksum.type)
180             if not fchksum:
181                 print "%s: unknown checksum type" % file
182                 if markincomplete:
183                     self['incomplete'] = True
184                 return None
185             fchksum.add_fd(f.fileno())
186             if fchksum != chksum:
187                 print "%s: checksum mismatch" % file
188                 if markincomplete:
189                     self['incomplete'] = True
190                 return None
191         if uncompress:
192             return solv.xfopen_fd(file, f.fileno())
193         return solv.xfopen_fd(None, f.fileno())
194
195     def usecachedrepo(self, ext, mark=False):
196         try: 
197             repopath = self.cachepath(ext)
198             f = open(repopath, 'r')
199             f.seek(-32, os.SEEK_END)
200             fcookie = f.read(32)
201             if len(fcookie) != 32:
202                 return False
203             if not ext:
204                 cookie = self['cookie']
205             else:
206                 cookie = self['extcookie']
207             if cookie and fcookie != cookie:
208                 return False
209             if self.type != 'system' and not ext:
210                 f.seek(-32 * 2, os.SEEK_END)
211                 fextcookie = f.read(32)
212                 if len(fextcookie) != 32:
213                     return False
214             f.seek(0)
215             flags = 0
216             if ext:
217                 flags = Repo.REPO_USE_LOADING|Repo.REPO_EXTEND_SOLVABLES
218                 if ext != 'DL':
219                     flags |= Repo.REPO_LOCALPOOL
220             if not self.handle.add_solv(f, flags):
221                 return False
222             if self.type != 'system' and not ext:
223                 self['cookie'] = fcookie
224                 self['extcookie'] = fextcookie
225             if mark:
226                 # no futimes in python?
227                 try:
228                     os.utime(repopath, None)
229                 except Exception, e:
230                     pass
231         except IOError, e:
232             return False
233         return True
234
235     def writecachedrepo(self, ext, info=None):
236         if 'incomplete' in self:
237             return
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, 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 = Repo.REPO_EXTEND_SOLVABLES
273                     if ext != 'DL':
274                         flags |= Repo.REPO_LOCALPOOL
275                     info.add_solv(nf, flags)
276             os.rename(tmpname, self.cachepath(ext))
277         except 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, 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', Repo.REPO_USE_LOADING|Repo.REPO_EXTEND_SOLVABLES|Repo.REPO_LOCALPOOL)
399         elif ext == 'DL':
400             self.handle.add_deltainfoxml(f, 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.lookup_id(solv.SOLVID_META, solv.SUSETAGS_DEFAULTVENDOR)
423         descrdir = self.handle.lookup_str(solv.SOLVID_META, 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, Repo.REPO_NO_INTERNALIZE|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, Repo.REPO_NO_INTERNALIZE|Repo.REPO_REUSE_REPODATA|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, 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.lookup_id(solv.SOLVID_META, solv.SUSETAGS_DEFAULTVENDOR)
515         descrdir = self.handle.lookup_str(solv.SOLVID_META, 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 = Repo.REPO_USE_LOADING|Repo.REPO_EXTEND_SOLVABLES
523         if ext != 'DL':
524             flags |= 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.lookup_str(solv.SOLVID_META, 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", Repo.REPO_NO_INTERNALIZE)
553         self.handle.add_rpmdb(None, Repo.REPO_REUSE_REPODATA)
554         self.writecachedrepo(None)
555         return True
556
557 class repo_cmdline(repo_generic):
558     def load(self, pool):
559         self.handle = pool.add_repo(self.name)
560         self.handle.appdata = self 
561         return True
562
563 def load_stub(repodata):
564     repo = repodata.repo.appdata
565     if repo:
566         return repo.load_ext(repodata)
567     return False
568
569
570 parser = OptionParser(usage="usage: solv.py [options] COMMAND")
571 parser.add_option('-r', '--repo', action="append", type="string", dest="repos", help="limit to specified repositories")
572 parser.add_option('--best', action="store_true", dest="best", help="force installation/update to best packages")
573 parser.add_option('--clean', action="store_true", dest="clean", help="delete no longer needed packages")
574 (options, args) = parser.parse_args()
575 if not args:
576     parser.print_help(sys.stderr)
577     sys.exit(1)
578
579 cmd = args[0]
580 args = args[1:]
581
582 cmdabbrev = {'li': 'list', 'in': 'install', 'rm': 'erase', 've': 'verify', 'se': 'search'}
583 if cmd in cmdabbrev:
584     cmd = cmdabbrev[cmd]
585
586 cmdactionmap = {
587   'install': Job.SOLVER_INSTALL,
588   'erase':   Job.SOLVER_ERASE,
589   'up':      Job.SOLVER_UPDATE,
590   'dup':     Job.SOLVER_DISTUPGRADE,
591   'verify':  Job.SOLVER_VERIFY,
592   'list':    0,
593   'info':    0
594 }
595
596 # read all repo configs
597 repos = []
598 reposdirs = []
599 if os.path.isdir("/etc/zypp/repos.d"):
600   reposdirs = [ "/etc/zypp/repos.d" ]
601 else:
602   reposdirs = [ "/etc/yum/repos.d" ]
603
604 for reposdir in reposdirs:
605     if not os.path.isdir(reposdir):
606         continue
607     for reponame in sorted(glob.glob('%s/*.repo' % reposdir)):
608         cfg = INIConfig(open(reponame))
609         for alias in cfg:
610             repoattr = {'enabled': 0, 'priority': 99, 'autorefresh': 1, 'type': 'rpm-md', 'metadata_expire': 900}
611             for k in cfg[alias]:
612                 repoattr[k] = cfg[alias][k]
613             if 'mirrorlist' in repoattr and 'metalink' not in repoattr:
614                 if repoattr['mirrorlist'].find('/metalink'):
615                     repoattr['metalink'] = repoattr['mirrorlist']
616                     del repoattr['mirrorlist']
617             if repoattr['type'] == 'rpm-md':
618                 repo = repo_repomd(alias, 'repomd', repoattr)
619             elif repoattr['type'] == 'yast2':
620                 repo = repo_susetags(alias, 'susetags', repoattr)
621             else:
622                 repo = repo_unknown(alias, 'unknown', repoattr)
623             repos.append(repo)
624
625 pool = solv.Pool()
626 pool.setarch()
627 pool.set_loadcallback(load_stub)
628
629 # now load all enabled repos into the pool
630 sysrepo = repo_system('@System', 'system')
631 sysrepo.load(pool)
632 for repo in repos:
633     if int(repo['enabled']):
634         repo.load(pool)
635     
636 repofilter = None
637 if options.repos:
638     for reponame in options.repos:
639         mrepos = [ repo for repo in repos if repo.name == reponame ]
640         if not mrepos:
641             print "no repository matches '%s'" % reponame
642             sys.exit(1)
643         repo = mrepos[0]
644         if hasattr(repo, 'handle'):
645             if not repofilter:
646                 repofilter = pool.Selection()
647             repofilter.add_raw(Job.SOLVER_SOLVABLE_REPO|Job.SOLVER_SETREPO|Job.SOLVER_SETVENDOR, repo.handle.id)
648
649 if cmd == 'search':
650     pool.createwhatprovides()
651     sel = pool.Selection()
652     di = pool.Dataiterator(0, solv.SOLVABLE_NAME, args[0], Dataiterator.SEARCH_SUBSTRING|Dataiterator.SEARCH_NOCASE)
653     for d in di:
654         sel.add_raw(Job.SOLVER_SOLVABLE, d.solvid)
655     if repofilter:
656        sel.filter(repofilter)
657     for s in sel.solvables():
658         print " - %s [%s]: %s" % (s, s.repo.name, s.lookup_str(solv.SOLVABLE_SUMMARY))
659     sys.exit(0)
660
661 if cmd not in cmdactionmap:
662     print "unknown command", cmd
663     sys.exit(1)
664
665 cmdlinerepo = None
666 if cmd == 'list' or cmd == 'info' or cmd == 'install':
667     for arg in args:
668         if arg.endswith(".rpm") and os.access(arg, os.R_OK):
669             if not cmdlinerepo:
670                 cmdlinerepo = repo_cmdline('@commandline', 'cmdline')
671                 cmdlinerepo.load(pool)
672                 cmdlinerepo['packages'] = {}
673             cmdlinerepo['packages'][arg] = cmdlinerepo.handle.add_rpm(arg, Repo.REPO_REUSE_REPODATA|Repo.REPO_NO_INTERNALIZE)
674     if cmdlinerepo:
675         cmdlinerepo.handle.internalize()
676
677 addedprovides = pool.addfileprovides_queue()
678 if addedprovides:
679     sysrepo.updateaddedprovides(addedprovides)
680     for repo in repos:
681         repo.updateaddedprovides(addedprovides)
682
683 pool.createwhatprovides()
684
685 # convert arguments into jobs
686 jobs = []
687 for arg in args:
688     if cmdlinerepo and arg in cmdlinerepo['packages']:
689         jobs.append(pool.Job(Job.SOLVER_SOLVABLE, cmdlinerepo['packages'][arg]))
690     else:
691         flags = Selection.SELECTION_NAME|Selection.SELECTION_PROVIDES|Selection.SELECTION_GLOB
692         flags |= Selection.SELECTION_CANON|Selection.SELECTION_DOTARCH|Selection.SELECTION_REL
693         if len(arg) and arg[0] == '/':
694             flags |= Selection.SELECTION_FILELIST
695             if cmd == 'erase':
696                 flags |= Selection.SELECTION_INSTALLED_ONLY
697         sel = pool.select(arg, flags)
698         if repofilter:
699            sel.filter(repofilter)
700         if sel.isempty():
701             sel = pool.select(arg, flags | Selection.SELECTION_NOCASE)
702             if repofilter:
703                sel.filter(repofilter)
704             if not sel.isempty():
705                 print "[ignoring case for '%s']" % arg
706         if sel.isempty():
707             print "nothing matches '%s'" % arg
708             sys.exit(1)
709         if sel.flags() & Selection.SELECTION_FILELIST:
710             print "[using file list match for '%s']" % arg
711         if sel.flags() & Selection.SELECTION_PROVIDES:
712             print "[using capability match for '%s']" % arg
713         jobs += sel.jobs(cmdactionmap[cmd])
714
715 if not jobs and (cmd == 'up' or cmd == 'dup' or cmd == 'verify' or repofilter):
716     sel = pool.Selection()
717     sel.add_raw(Job.SOLVER_SOLVABLE_ALL, 0)
718     if repofilter:
719        sel.filter(repofilter)
720     jobs += sel.jobs(cmdactionmap[cmd])
721
722 if not jobs:
723     print "no package matched."
724     sys.exit(1)
725
726 if cmd == 'list' or cmd == 'info':
727     for job in jobs:
728         for s in job.solvables():
729             if cmd == 'info':
730                 print "Name:        %s" % s
731                 print "Repo:        %s" % s.repo
732                 print "Summary:     %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
733                 str = s.lookup_str(solv.SOLVABLE_URL)
734                 if str:
735                     print "Url:         %s" % str
736                 str = s.lookup_str(solv.SOLVABLE_LICENSE)
737                 if str:
738                     print "License:     %s" % str
739                 print "Description:\n%s" % s.lookup_str(solv.SOLVABLE_DESCRIPTION)
740                 print
741             else:
742                 print "  - %s [%s]" % (s, s.repo)
743                 print "    %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
744     sys.exit(0)
745
746 # up magic: use install instead of update if no installed package matches
747 for job in jobs:
748     if cmd == 'up' and job.isemptyupdate():
749         job.how ^= Job.SOLVER_UPDATE ^ Job.SOLVER_INSTALL
750     if options.best:
751         job.how |= Job.SOLVER_FORCEBEST
752     if options.clean:
753         job.how |= Job.SOLVER_CLEANDEPS
754
755 #pool.set_debuglevel(2)
756 solver = None
757 while True:
758     solver = pool.Solver()
759     solver.set_flag(Solver.SOLVER_FLAG_SPLITPROVIDES, 1);
760     if cmd == 'erase':
761         solver.set_flag(Solver.SOLVER_FLAG_ALLOW_UNINSTALL, 1);
762     problems = solver.solve(jobs)
763     if not problems:
764         break
765     for problem in problems:
766         print "Problem %d:" % problem.id
767         r = problem.findproblemrule()
768         ri = r.info()
769         print ri.problemstr()
770         solutions = problem.solutions()
771         for solution in solutions:
772             print "  Solution %d:" % solution.id
773             elements = solution.elements(True)
774             for element in elements:
775                 print "  - %s" % element.str()
776             print
777         sol = ''
778         while not (sol == 's' or sol == 'q' or (sol.isdigit() and int(sol) >= 1 and int(sol) <= len(solutions))):
779             sys.stdout.write("Please choose a solution: ")
780             sys.stdout.flush()
781             sol = sys.stdin.readline().strip()
782         if sol == 's':
783             continue        # skip problem
784         if sol == 'q':
785             sys.exit(1)
786         solution = solutions[int(sol) - 1]
787         for element in solution.elements():
788             newjob = element.Job()
789             if element.type == Solver.SOLVER_SOLUTION_JOB:
790                 jobs[element.jobidx] = newjob
791             else:
792                 if newjob and newjob not in jobs:
793                     jobs.append(newjob)
794                     
795 # no problems, show transaction
796 trans = solver.transaction()
797 del solver
798 if trans.isempty():
799     print "Nothing to do."
800     sys.exit(0)
801 print
802 print "Transaction summary:"
803 print
804 for cl in trans.classify():
805     if cl.type == Transaction.SOLVER_TRANSACTION_ERASE:
806         print "%d erased packages:" % cl.count
807     elif cl.type == Transaction.SOLVER_TRANSACTION_INSTALL:
808         print "%d installed packages:" % cl.count
809     elif cl.type == Transaction.SOLVER_TRANSACTION_REINSTALLED:
810         print "%d reinstalled packages:" % cl.count
811     elif cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
812         print "%d downgraded packages:" % cl.count
813     elif cl.type == Transaction.SOLVER_TRANSACTION_CHANGED:
814         print "%d changed packages:" % cl.count
815     elif cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED:
816         print "%d upgraded packages:" % cl.count
817     elif cl.type == Transaction.SOLVER_TRANSACTION_VENDORCHANGE:
818         print "%d vendor changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
819     elif cl.type == Transaction.SOLVER_TRANSACTION_ARCHCHANGE:
820         print "%d arch changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
821     else:
822         continue
823     for p in cl.solvables():
824         if cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
825             op = trans.othersolvable(p)
826             print "  - %s -> %s" % (p, op)
827         else:
828             print "  - %s" % p
829     print
830 print "install size change: %d K" % trans.calc_installsizechange()
831 print
832
833 while True:
834     sys.stdout.write("OK to continue (y/n)? ")
835     sys.stdout.flush()
836     yn = sys.stdin.readline().strip()
837     if yn == 'y': break
838     if yn == 'n': sys.exit(1)
839 newpkgs = trans.newpackages()
840 newpkgsfp = {}
841 if newpkgs:
842     downloadsize = 0
843     for p in newpkgs:
844         downloadsize += p.lookup_num(solv.SOLVABLE_DOWNLOADSIZE)
845     print "Downloading %d packages, %d K" % (len(newpkgs), downloadsize)
846     for p in newpkgs:
847         repo = p.repo.appdata
848         location, medianr = p.lookup_location()
849         if not location:
850             continue
851         if repo.type == 'commandline':
852             f = solv.xfopen(location)
853             if not f:
854                 sys.exit("\n%s: %s not found" % location)
855             newpkgsfp[p.id] = f
856             continue
857         if not sysrepo.handle.isempty() and os.access('/usr/bin/applydeltarpm', os.X_OK):
858             pname = p.name
859             di = p.repo.Dataiterator(solv.SOLVID_META, solv.DELTA_PACKAGE_NAME, pname, Dataiterator.SEARCH_STRING)
860             di.prepend_keyname(solv.REPOSITORY_DELTAINFO)
861             for d in di:
862                 dp = d.parentpos()
863                 if dp.lookup_id(solv.DELTA_PACKAGE_EVR) != p.evrid or dp.lookup_id(solv.DELTA_PACKAGE_ARCH) != p.archid:
864                     continue
865                 baseevrid = dp.lookup_id(solv.DELTA_BASE_EVR)
866                 candidate = None
867                 for installedp in pool.whatprovides(p.nameid):
868                     if installedp.isinstalled() and installedp.nameid == p.nameid and installedp.archid == p.archid and installedp.evrid == baseevrid:
869                         candidate = installedp
870                 if not candidate:
871                     continue
872                 seq = dp.lookup_deltaseq()
873                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, '-c', '-s', seq])
874                 if st:
875                     continue
876                 chksum = dp.lookup_checksum(solv.DELTA_CHECKSUM)
877                 if not chksum:
878                     continue
879                 dloc, dmedianr = dp.lookup_deltalocation()
880                 dloc = repo.packagespath() + dloc
881                 f = repo.download(dloc, False, chksum)
882                 if not f:
883                     continue
884                 nf = tempfile.TemporaryFile()
885                 nf = os.dup(nf.fileno())   # get rid of CLOEXEC
886                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, "/dev/fd/%d" % f.fileno(), "/dev/fd/%d" % nf])
887                 os.lseek(nf, 0, os.SEEK_SET)
888                 newpkgsfp[p.id] = solv.xfopen_fd("", nf)
889                 os.close(nf)
890                 break
891             if p.id in newpkgsfp:
892                 sys.stdout.write("d")
893                 sys.stdout.flush()
894                 continue
895                     
896         chksum = p.lookup_checksum(solv.SOLVABLE_CHECKSUM)
897         location = repo.packagespath() + location
898         f = repo.download(location, False, chksum)
899         if not f:
900             sys.exit("\n%s: %s not found in repository" % (repo.name, location))
901         newpkgsfp[p.id] = f
902         sys.stdout.write(".")
903         sys.stdout.flush()
904     print
905 print "Committing transaction:"
906 print
907 ts = rpm.TransactionSet('/')
908 ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
909 erasenamehelper = {}
910 for p in trans.steps():
911     type = trans.steptype(p, Transaction.SOLVER_TRANSACTION_RPM_ONLY)
912     if type == Transaction.SOLVER_TRANSACTION_ERASE:
913         rpmdbid = p.lookup_num(solv.RPM_RPMDBID)
914         erasenamehelper[p.name] = p
915         if not rpmdbid:
916             sys.exit("\ninternal error: installed package %s has no rpmdbid\n" % p)
917         ts.addErase(rpmdbid)
918     elif type == Transaction.SOLVER_TRANSACTION_INSTALL:
919         f = newpkgsfp[p.id]
920         h = ts.hdrFromFdno(f.fileno())
921         os.lseek(f.fileno(), 0, os.SEEK_SET)
922         ts.addInstall(h, p, 'u')
923     elif type == Transaction.SOLVER_TRANSACTION_MULTIINSTALL:
924         f = newpkgsfp[p.id]
925         h = ts.hdrFromFdno(f.fileno())
926         os.lseek(f.fileno(), 0, os.SEEK_SET)
927         ts.addInstall(h, p, 'i')
928 checkproblems = ts.check()
929 if checkproblems:
930     print checkproblems
931     sys.exit("Sorry.")
932 ts.order()
933 def runCallback(reason, amount, total, p, d):
934     if reason == rpm.RPMCALLBACK_INST_OPEN_FILE:
935         return newpkgsfp[p.id].fileno()
936     if reason == rpm.RPMCALLBACK_INST_START:
937         print "install", p
938     if reason == rpm.RPMCALLBACK_UNINST_START:
939         # argh, p is just the name of the package
940         if p in erasenamehelper:
941             p = erasenamehelper[p]
942             print "erase", p
943 runproblems = ts.run(runCallback, '')
944 if runproblems:
945     print runproblems
946     sys.exit(1)
947 sys.exit(0)
948
949 # vim: sw=4 et