remove trailing whitespace
[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         print problem
772         solutions = problem.solutions()
773         for solution in solutions:
774             print "  Solution %d:" % solution.id
775             elements = solution.elements(True)
776             for element in elements:
777                 print "  - %s" % element.str()
778             print
779         sol = ''
780         while not (sol == 's' or sol == 'q' or (sol.isdigit() and int(sol) >= 1 and int(sol) <= len(solutions))):
781             sys.stdout.write("Please choose a solution: ")
782             sys.stdout.flush()
783             sol = sys.stdin.readline().strip()
784         if sol == 's':
785             continue        # skip problem
786         if sol == 'q':
787             sys.exit(1)
788         solution = solutions[int(sol) - 1]
789         for element in solution.elements():
790             newjob = element.Job()
791             if element.type == solv.Solver.SOLVER_SOLUTION_JOB:
792                 jobs[element.jobidx] = newjob
793             else:
794                 if newjob and newjob not in jobs:
795                     jobs.append(newjob)
796                     
797 # no problems, show transaction
798 trans = solver.transaction()
799 del solver
800 if trans.isempty():
801     print "Nothing to do."
802     sys.exit(0)
803 print
804 print "Transaction summary:"
805 print
806 for cl in trans.classify(solv.Transaction.SOLVER_TRANSACTION_SHOW_OBSOLETES | solv.Transaction.SOLVER_TRANSACTION_OBSOLETE_IS_UPGRADE):
807     if cl.type == solv.Transaction.SOLVER_TRANSACTION_ERASE:
808         print "%d erased packages:" % cl.count
809     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_INSTALL:
810         print "%d installed packages:" % cl.count
811     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_REINSTALLED:
812         print "%d reinstalled packages:" % cl.count
813     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED:
814         print "%d downgraded packages:" % cl.count
815     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_CHANGED:
816         print "%d changed packages:" % cl.count
817     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED:
818         print "%d upgraded packages:" % cl.count
819     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_VENDORCHANGE:
820         print "%d vendor changes from '%s' to '%s':" % (cl.count, cl.fromstr, cl.tostr)
821     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_ARCHCHANGE:
822         print "%d arch changes from '%s' to '%s':" % (cl.count, cl.fromstr, cl.tostr)
823     else:
824         continue
825     for p in cl.solvables():
826         if cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED:
827             op = trans.othersolvable(p)
828             print "  - %s -> %s" % (p, op)
829         else:
830             print "  - %s" % p
831     print
832 print "install size change: %d K" % trans.calc_installsizechange()
833 print
834
835 while True:
836     sys.stdout.write("OK to continue (y/n)? ")
837     sys.stdout.flush()
838     yn = sys.stdin.readline().strip()
839     if yn == 'y': break
840     if yn == 'n' or yn == 'q': sys.exit(1)
841 newpkgs = trans.newsolvables()
842 newpkgsfp = {}
843 if newpkgs:
844     downloadsize = 0
845     for p in newpkgs:
846         downloadsize += p.lookup_num(solv.SOLVABLE_DOWNLOADSIZE)
847     print "Downloading %d packages, %d K" % (len(newpkgs), downloadsize)
848     for p in newpkgs:
849         repo = p.repo.appdata
850         location, medianr = p.lookup_location()
851         if not location:
852             continue
853         if repo.type == 'commandline':
854             f = solv.xfopen(location)
855             if not f:
856                 sys.exit("\n%s: %s not found" % location)
857             newpkgsfp[p.id] = f
858             continue
859         if not sysrepo.handle.isempty() and os.access('/usr/bin/applydeltarpm', os.X_OK):
860             pname = p.name
861             di = p.repo.Dataiterator(solv.SOLVID_META, solv.DELTA_PACKAGE_NAME, pname, solv.Dataiterator.SEARCH_STRING)
862             di.prepend_keyname(solv.REPOSITORY_DELTAINFO)
863             for d in di:
864                 dp = d.parentpos()
865                 if dp.lookup_id(solv.DELTA_PACKAGE_EVR) != p.evrid or dp.lookup_id(solv.DELTA_PACKAGE_ARCH) != p.archid:
866                     continue
867                 baseevrid = dp.lookup_id(solv.DELTA_BASE_EVR)
868                 candidate = None
869                 for installedp in pool.whatprovides(p.nameid):
870                     if installedp.isinstalled() and installedp.nameid == p.nameid and installedp.archid == p.archid and installedp.evrid == baseevrid:
871                         candidate = installedp
872                 if not candidate:
873                     continue
874                 seq = dp.lookup_deltaseq()
875                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, '-c', '-s', seq])
876                 if st:
877                     continue
878                 chksum = dp.lookup_checksum(solv.DELTA_CHECKSUM)
879                 if not chksum:
880                     continue
881                 dloc, dmedianr = dp.lookup_deltalocation()
882                 dloc = repo.packagespath() + dloc
883                 f = repo.download(dloc, False, chksum)
884                 if not f:
885                     continue
886                 nf = tempfile.TemporaryFile()
887                 nf = os.dup(nf.fileno())   # get rid of CLOEXEC
888                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, "/dev/fd/%d" % f.fileno(), "/dev/fd/%d" % nf])
889                 if st:
890                     os.close(nf)
891                     continue
892                 os.lseek(nf, 0, os.SEEK_SET)
893                 newpkgsfp[p.id] = solv.xfopen_fd("", nf)
894                 os.close(nf)
895                 break
896             if p.id in newpkgsfp:
897                 sys.stdout.write("d")
898                 sys.stdout.flush()
899                 continue
900                     
901         chksum = p.lookup_checksum(solv.SOLVABLE_CHECKSUM)
902         location = repo.packagespath() + location
903         f = repo.download(location, False, chksum)
904         if not f:
905             sys.exit("\n%s: %s not found in repository" % (repo.name, location))
906         newpkgsfp[p.id] = f
907         sys.stdout.write(".")
908         sys.stdout.flush()
909     print
910 print "Committing transaction:"
911 print
912 ts = rpm.TransactionSet('/')
913 ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
914 erasenamehelper = {}
915 for p in trans.steps():
916     type = trans.steptype(p, solv.Transaction.SOLVER_TRANSACTION_RPM_ONLY)
917     if type == solv.Transaction.SOLVER_TRANSACTION_ERASE:
918         rpmdbid = p.lookup_num(solv.RPM_RPMDBID)
919         erasenamehelper[p.name] = p
920         if not rpmdbid:
921             sys.exit("\ninternal error: installed package %s has no rpmdbid\n" % p)
922         ts.addErase(rpmdbid)
923     elif type == solv.Transaction.SOLVER_TRANSACTION_INSTALL:
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, 'u')
928     elif type == solv.Transaction.SOLVER_TRANSACTION_MULTIINSTALL:
929         f = newpkgsfp[p.id]
930         h = ts.hdrFromFdno(f.fileno())
931         os.lseek(f.fileno(), 0, os.SEEK_SET)
932         ts.addInstall(h, p, 'i')
933 checkproblems = ts.check()
934 if checkproblems:
935     print checkproblems
936     sys.exit("Sorry.")
937 ts.order()
938 def runCallback(reason, amount, total, p, d):
939     if reason == rpm.RPMCALLBACK_INST_OPEN_FILE:
940         return newpkgsfp[p.id].fileno()
941     if reason == rpm.RPMCALLBACK_INST_START:
942         print "install", p
943     if reason == rpm.RPMCALLBACK_UNINST_START:
944         # argh, p is just the name of the package
945         if p in erasenamehelper:
946             p = erasenamehelper[p]
947             print "erase", p
948 runproblems = ts.run(runCallback, '')
949 if runproblems:
950     print runproblems
951     sys.exit(1)
952 sys.exit(0)
953
954 # vim: sw=4 et