pysolv: do not import from solv
[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         try:
238             if not os.path.isdir("/var/cache/solv"):
239                 os.mkdir("/var/cache/solv", 0755)
240             (fd, tmpname) = tempfile.mkstemp(prefix='.newsolv-', dir='/var/cache/solv')
241             os.fchmod(fd, 0444)
242             f = os.fdopen(fd, 'w+')
243             if not info:
244                 self.handle.write(f)
245             elif ext:
246                 info.write(f)
247             else:       # rewrite_repos case
248                 self.handle.write_first_repodata(f)
249             if self.type != 'system' and not ext:
250                 if 'extcookie' not in self:
251                     self['extcookie'] = self.calc_cookie_ext(f, self['cookie'])
252                 f.write(self['extcookie'])
253             if not ext:
254                 f.write(self['cookie'])
255             else:
256                 f.write(self['extcookie'])
257             f.close()
258             if self.handle.iscontiguous():
259                 # switch to saved repo to activate paging and save memory
260                 nf = solv.xfopen(tmpname)
261                 if not ext:
262                     # main repo
263                     self.handle.empty()
264                     if not self.handle.add_solv(nf, solv.Repo.SOLV_ADD_NO_STUBS):
265                         sys.exit("internal error, cannot reload solv file")
266                 else:
267                     # extension repodata
268                     # need to extend to repo boundaries, as this is how
269                     # info.write() has written the data
270                     info.extend_to_repo()
271                     flags = solv.Repo.REPO_EXTEND_SOLVABLES
272                     if ext != 'DL':
273                         flags |= solv.Repo.REPO_LOCALPOOL
274                     info.add_solv(nf, flags)
275             os.rename(tmpname, self.cachepath(ext))
276         except IOError, e:
277             if tmpname:
278                 os.unlink(tmpname)
279                 
280     def updateaddedprovides(self, addedprovides):
281         if 'incomplete' in self:
282             return 
283         if not hasattr(self, 'handle'):
284             return 
285         if self.handle.isempty():
286             return
287         # make sure there's just one real repodata with extensions
288         repodata = self.handle.first_repodata()
289         if not repodata:
290             return
291         oldaddedprovides = repodata.lookup_idarray(solv.SOLVID_META, solv.REPOSITORY_ADDEDFILEPROVIDES)
292         if not set(addedprovides) <= set(oldaddedprovides):
293             for id in addedprovides:
294                 repodata.add_idarray(solv.SOLVID_META, solv.REPOSITORY_ADDEDFILEPROVIDES, id)
295             repodata.internalize()
296             self.writecachedrepo(None, repodata)
297
298     def packagespath(self):
299         return ''
300
301 class repo_repomd(repo_generic):
302     def load(self, pool):
303         if super(repo_repomd, self).load(pool):
304             return True
305         print "rpmmd repo '%s':" % self.name,
306         sys.stdout.flush()
307         f = self.download("repodata/repomd.xml", False, None, None)
308         if not f:
309             print "no repomd.xml file, skipped"
310             self.handle.free(True)
311             del self.handle
312             return False
313         self['cookie'] = self.calc_cookie_fp(f)
314         if self.usecachedrepo(None, True):
315             print "cached"
316             return True
317         self.handle.add_repomdxml(f, 0)
318         print "fetching"
319         (filename, filechksum) = self.find('primary')
320         if filename:
321             f = self.download(filename, True, filechksum, True)
322             if f:
323                 self.handle.add_rpmmd(f, None, 0)
324             if 'incomplete' in self:
325                 return False # hopeless, need good primary
326         (filename, filechksum) = self.find('updateinfo')
327         if filename:
328             f = self.download(filename, True, filechksum, True)
329             if f:
330                 self.handle.add_updateinfoxml(f, 0)
331         self.add_exts()
332         self.writecachedrepo(None)
333         # must be called after writing the repo
334         self.handle.create_stubs()
335         return True
336
337     def find(self, what):
338         di = self.handle.Dataiterator(solv.SOLVID_META, solv.REPOSITORY_REPOMD_TYPE, what, solv.Dataiterator.SEARCH_STRING)
339         di.prepend_keyname(solv.REPOSITORY_REPOMD)
340         for d in di:
341             dp = d.parentpos()
342             filename = dp.lookup_str(solv.REPOSITORY_REPOMD_LOCATION)
343             chksum = dp.lookup_checksum(solv.REPOSITORY_REPOMD_CHECKSUM)
344             if filename and not chksum:
345                 print "no %s file checksum!" % filename
346                 filename = None
347                 chksum = None
348             if filename:
349                 return (filename, chksum)
350         return (None, None)
351         
352     def add_ext(self, repodata, what, ext):
353         filename, chksum = self.find(what)
354         if not filename and what == 'deltainfo':
355             filename, chksum = self.find('prestodelta')
356         if not filename:
357             return
358         handle = repodata.new_handle()
359         repodata.set_poolstr(handle, solv.REPOSITORY_REPOMD_TYPE, what)
360         repodata.set_str(handle, solv.REPOSITORY_REPOMD_LOCATION, filename)
361         repodata.set_checksum(handle, solv.REPOSITORY_REPOMD_CHECKSUM, chksum)
362         if ext == 'DL':
363             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOSITORY_DELTAINFO)
364             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_FLEXARRAY)
365         elif ext == 'FL':
366             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_FILELIST)
367             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRSTRARRAY)
368         repodata.add_flexarray(solv.SOLVID_META, solv.REPOSITORY_EXTERNAL, handle)
369
370     def add_exts(self):
371         repodata = self.handle.add_repodata(0)
372         self.add_ext(repodata, 'deltainfo', 'DL')
373         self.add_ext(repodata, 'filelists', 'FL')
374         repodata.internalize()
375     
376     def load_ext(self, repodata):
377         repomdtype = repodata.lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_TYPE)
378         if repomdtype == 'filelists':
379             ext = 'FL'
380         elif repomdtype == 'deltainfo':
381             ext = 'DL'
382         else:
383             return False
384         sys.stdout.write("[%s:%s: " % (self.name, ext))
385         if self.usecachedrepo(ext):
386             sys.stdout.write("cached]\n")
387             sys.stdout.flush()
388             return True
389         sys.stdout.write("fetching]\n")
390         sys.stdout.flush()
391         filename = repodata.lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_LOCATION)
392         filechksum = repodata.lookup_checksum(solv.SOLVID_META, solv.REPOSITORY_REPOMD_CHECKSUM)
393         f = self.download(filename, True, filechksum)
394         if not f:
395             return False
396         if ext == 'FL':
397             self.handle.add_rpmmd(f, 'FL', solv.Repo.REPO_USE_LOADING|solv.Repo.REPO_EXTEND_SOLVABLES|solv.Repo.REPO_LOCALPOOL)
398         elif ext == 'DL':
399             self.handle.add_deltainfoxml(f, solv.Repo.REPO_USE_LOADING)
400         self.writecachedrepo(ext, repodata)
401         return True
402
403 class repo_susetags(repo_generic):
404     def load(self, pool):
405         if super(repo_susetags, self).load(pool):
406             return True
407         print "susetags repo '%s':" % self.name,
408         sys.stdout.flush()
409         f = self.download("content", False, None, None)
410         if not f:
411             print "no content file, skipped"
412             self.handle.free(True)
413             del self.handle
414             return False
415         self['cookie'] = self.calc_cookie_fp(f)
416         if self.usecachedrepo(None, True):
417             print "cached"
418             return True
419         self.handle.add_content(f, 0)
420         print "fetching"
421         defvendorid = self.handle.lookup_id(solv.SOLVID_META, solv.SUSETAGS_DEFAULTVENDOR)
422         descrdir = self.handle.lookup_str(solv.SOLVID_META, solv.SUSETAGS_DESCRDIR)
423         if not descrdir:
424             descrdir = "suse/setup/descr"
425         (filename, filechksum) = self.find('packages.gz')
426         if not filename:
427             (filename, filechksum) = self.find('packages')
428         if filename:
429             f = self.download(descrdir + '/' + filename, True, filechksum, True)
430             if f:
431                 self.handle.add_susetags(f, defvendorid, None, solv.Repo.REPO_NO_INTERNALIZE|solv.Repo.SUSETAGS_RECORD_SHARES)
432                 (filename, filechksum) = self.find('packages.en.gz')
433                 if not filename:
434                     (filename, filechksum) = self.find('packages.en')
435                 if filename:
436                     f = self.download(descrdir + '/' + filename, True, filechksum, True)
437                     if f:
438                         self.handle.add_susetags(f, defvendorid, None, solv.Repo.REPO_NO_INTERNALIZE|solv.Repo.REPO_REUSE_REPODATA|solv.Repo.REPO_EXTEND_SOLVABLES)
439                 self.handle.internalize()
440         self.add_exts()
441         self.writecachedrepo(None)
442         # must be called after writing the repo
443         self.handle.create_stubs()
444         return True
445
446     def find(self, what):
447         di = self.handle.Dataiterator(solv.SOLVID_META, solv.SUSETAGS_FILE_NAME, what, solv.Dataiterator.SEARCH_STRING)
448         di.prepend_keyname(solv.SUSETAGS_FILE)
449         for d in di:
450             dp = d.parentpos()
451             chksum = dp.lookup_checksum(solv.SUSETAGS_FILE_CHECKSUM)
452             return (what, chksum)
453         return (None, None)
454
455     def add_ext(self, repodata, what, ext):
456         (filename, chksum) = self.find(what)
457         if not filename:
458             return
459         handle = repodata.new_handle()
460         repodata.set_str(handle, solv.SUSETAGS_FILE_NAME, filename)
461         if chksum:
462             repodata.set_checksum(handle, solv.SUSETAGS_FILE_CHECKSUM, chksum)
463         if ext == 'DU':
464             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_DISKUSAGE)
465             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRNUMNUMARRAY)
466         elif ext == 'FL':
467             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_FILELIST)
468             repodata.add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRSTRARRAY)
469         else:
470             for langtag, langtagtype in [
471                 (solv.SOLVABLE_SUMMARY, solv.REPOKEY_TYPE_STR),
472                 (solv.SOLVABLE_DESCRIPTION, solv.REPOKEY_TYPE_STR),
473                 (solv.SOLVABLE_EULA, solv.REPOKEY_TYPE_STR),
474                 (solv.SOLVABLE_MESSAGEINS, solv.REPOKEY_TYPE_STR),
475                 (solv.SOLVABLE_MESSAGEDEL, solv.REPOKEY_TYPE_STR),
476                 (solv.SOLVABLE_CATEGORY, solv.REPOKEY_TYPE_ID)
477             ]:
478                 repodata.add_idarray(handle, solv.REPOSITORY_KEYS, self.handle.pool.id2langid(langtag, ext, 1))
479                 repodata.add_idarray(handle, solv.REPOSITORY_KEYS, langtagtype)
480         repodata.add_flexarray(solv.SOLVID_META, solv.REPOSITORY_EXTERNAL, handle)
481         
482     def add_exts(self):
483         repodata = self.handle.add_repodata(0)
484         di = self.handle.Dataiterator(solv.SOLVID_META, solv.SUSETAGS_FILE_NAME, None, 0)
485         di.prepend_keyname(solv.SUSETAGS_FILE)
486         for d in di:
487             filename = d.str()
488             if not filename:
489                 continue
490             if filename[0:9] != "packages.":
491                 continue
492             if len(filename) == 11 and filename != "packages.gz":
493                 ext = filename[9:11]
494             elif filename[11:12] == ".":
495                 ext = filename[9:11]
496             else:
497                 continue
498             if ext == "en":
499                 continue
500             self.add_ext(repodata, filename, ext)
501         repodata.internalize()
502
503     def load_ext(self, repodata):
504         filename = repodata.lookup_str(solv.SOLVID_META, solv.SUSETAGS_FILE_NAME)
505         ext = filename[9:11]
506         sys.stdout.write("[%s:%s: " % (self.name, ext))
507         if self.usecachedrepo(ext):
508             sys.stdout.write("cached]\n")
509             sys.stdout.flush()
510             return True
511         sys.stdout.write("fetching]\n")
512         sys.stdout.flush()
513         defvendorid = self.handle.lookup_id(solv.SOLVID_META, solv.SUSETAGS_DEFAULTVENDOR)
514         descrdir = self.handle.lookup_str(solv.SOLVID_META, solv.SUSETAGS_DESCRDIR)
515         if not descrdir:
516             descrdir = "suse/setup/descr"
517         filechksum = repodata.lookup_checksum(solv.SOLVID_META, solv.SUSETAGS_FILE_CHECKSUM)
518         f = self.download(descrdir + '/' + filename, True, filechksum)
519         if not f:
520             return False
521         flags = solv.Repo.REPO_USE_LOADING|solv.Repo.REPO_EXTEND_SOLVABLES
522         if ext != 'DL':
523             flags |= solv.Repo.REPO_LOCALPOOL
524         self.handle.add_susetags(f, defvendorid, ext, flags)
525         self.writecachedrepo(ext, repodata)
526         return True
527
528     def packagespath(self):
529         datadir = repo.handle.lookup_str(solv.SOLVID_META, solv.SUSETAGS_DATADIR)
530         if not datadir:
531             datadir = 'suse'
532         return datadir + '/'
533
534 class repo_unknown(repo_generic):
535     def load(self, pool):
536         print "unsupported repo '%s': skipped" % self.name
537         return False
538
539 class repo_system(repo_generic):
540     def load(self, pool):
541         self.handle = pool.add_repo(self.name)
542         self.handle.appdata = self
543         pool.installed = self.handle
544         print "rpm database:",
545         self['cookie'] = self.calc_cookie_file("/var/lib/rpm/Packages")
546         if self.usecachedrepo(None):
547             print "cached"
548             return True
549         print "reading"
550         if hasattr(self.handle.__class__, 'add_products'):
551             self.handle.add_products("/etc/products.d", solv.Repo.REPO_NO_INTERNALIZE)
552         f = solv.xfopen(self.cachepath())
553         self.handle.add_rpmdb_reffp(f, solv.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': solv.Job.SOLVER_INSTALL,
588   'erase':   solv.Job.SOLVER_ERASE,
589   'up':      solv.Job.SOLVER_UPDATE,
590   'dup':     solv.Job.SOLVER_DISTUPGRADE,
591   'verify':  solv.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(repo.handle.Selection(solv.Job.SOLVER_SETVENDOR))
648
649 if cmd == 'search':
650     pool.createwhatprovides()
651     sel = pool.Selection()
652     di = pool.Dataiterator(0, solv.SOLVABLE_NAME, args[0], solv.Dataiterator.SEARCH_SUBSTRING|solv.Dataiterator.SEARCH_NOCASE)
653     for d in di:
654         sel.add_raw(solv.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, solv.Repo.REPO_REUSE_REPODATA|solv.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(solv.Job.SOLVER_SOLVABLE, cmdlinerepo['packages'][arg]))
690     else:
691         flags = solv.Selection.SELECTION_NAME|solv.Selection.SELECTION_PROVIDES|solv.Selection.SELECTION_GLOB
692         flags |= solv.Selection.SELECTION_CANON|solv.Selection.SELECTION_DOTARCH|solv.Selection.SELECTION_REL
693         if len(arg) and arg[0] == '/':
694             flags |= solv.Selection.SELECTION_FILELIST
695             if cmd == 'erase':
696                 flags |= solv.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 | solv.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() & solv.Selection.SELECTION_FILELIST:
710             print "[using file list match for '%s']" % arg
711         if sel.flags() & solv.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_all()
717     if repofilter:
718        sel.filter(repofilter)
719     jobs += sel.jobs(cmdactionmap[cmd])
720
721 if not jobs:
722     print "no package matched."
723     sys.exit(1)
724
725 if cmd == 'list' or cmd == 'info':
726     for job in jobs:
727         for s in job.solvables():
728             if cmd == 'info':
729                 print "Name:        %s" % s
730                 print "Repo:        %s" % s.repo
731                 print "Summary:     %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
732                 str = s.lookup_str(solv.SOLVABLE_URL)
733                 if str:
734                     print "Url:         %s" % str
735                 str = s.lookup_str(solv.SOLVABLE_LICENSE)
736                 if str:
737                     print "License:     %s" % str
738                 print "Description:\n%s" % s.lookup_str(solv.SOLVABLE_DESCRIPTION)
739                 print
740             else:
741                 print "  - %s [%s]" % (s, s.repo)
742                 print "    %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
743     sys.exit(0)
744
745 # up magic: use install instead of update if no installed package matches
746 for job in jobs:
747     if cmd == 'up' and job.isemptyupdate():
748         job.how ^= solv.Job.SOLVER_UPDATE ^ solv.Job.SOLVER_INSTALL
749     if options.best:
750         job.how |= solv.Job.SOLVER_FORCEBEST
751     if options.clean:
752         job.how |= solv.Job.SOLVER_CLEANDEPS
753
754 #pool.set_debuglevel(2)
755 solver = pool.Solver()
756 solver.set_flag(solv.Solver.SOLVER_FLAG_SPLITPROVIDES, 1);
757 if cmd == 'erase':
758     solver.set_flag(solv.Solver.SOLVER_FLAG_ALLOW_UNINSTALL, 1);
759
760 while True:
761     problems = solver.solve(jobs)
762     if not problems:
763         break
764     for problem in problems:
765         print "Problem %d/%d:" % (problem.id, len(problems))
766         r = problem.findproblemrule()
767         ri = r.info()
768         print ri.problemstr()
769         solutions = problem.solutions()
770         for solution in solutions:
771             print "  Solution %d:" % solution.id
772             elements = solution.elements(True)
773             for element in elements:
774                 print "  - %s" % element.str()
775             print
776         sol = ''
777         while not (sol == 's' or sol == 'q' or (sol.isdigit() and int(sol) >= 1 and int(sol) <= len(solutions))):
778             sys.stdout.write("Please choose a solution: ")
779             sys.stdout.flush()
780             sol = sys.stdin.readline().strip()
781         if sol == 's':
782             continue        # skip problem
783         if sol == 'q':
784             sys.exit(1)
785         solution = solutions[int(sol) - 1]
786         for element in solution.elements():
787             newjob = element.Job()
788             if element.type == solv.Solver.SOLVER_SOLUTION_JOB:
789                 jobs[element.jobidx] = newjob
790             else:
791                 if newjob and newjob not in jobs:
792                     jobs.append(newjob)
793                     
794 # no problems, show transaction
795 trans = solver.transaction()
796 del solver
797 if trans.isempty():
798     print "Nothing to do."
799     sys.exit(0)
800 print
801 print "Transaction summary:"
802 print
803 for cl in trans.classify(solv.Transaction.SOLVER_TRANSACTION_SHOW_OBSOLETES | solv.Transaction.SOLVER_TRANSACTION_OBSOLETE_IS_UPGRADE):
804     if cl.type == solv.Transaction.SOLVER_TRANSACTION_ERASE:
805         print "%d erased packages:" % cl.count
806     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_INSTALL:
807         print "%d installed packages:" % cl.count
808     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_REINSTALLED:
809         print "%d reinstalled packages:" % cl.count
810     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED:
811         print "%d downgraded packages:" % cl.count
812     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_CHANGED:
813         print "%d changed packages:" % cl.count
814     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED:
815         print "%d upgraded packages:" % cl.count
816     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_VENDORCHANGE:
817         print "%d vendor changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
818     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_ARCHCHANGE:
819         print "%d arch changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
820     else:
821         continue
822     for p in cl.solvables():
823         if cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED:
824             op = trans.othersolvable(p)
825             print "  - %s -> %s" % (p, op)
826         else:
827             print "  - %s" % p
828     print
829 print "install size change: %d K" % trans.calc_installsizechange()
830 print
831
832 while True:
833     sys.stdout.write("OK to continue (y/n)? ")
834     sys.stdout.flush()
835     yn = sys.stdin.readline().strip()
836     if yn == 'y': break
837     if yn == 'n' or yn == 'q': sys.exit(1)
838 newpkgs = trans.newpackages()
839 newpkgsfp = {}
840 if newpkgs:
841     downloadsize = 0
842     for p in newpkgs:
843         downloadsize += p.lookup_num(solv.SOLVABLE_DOWNLOADSIZE)
844     print "Downloading %d packages, %d K" % (len(newpkgs), downloadsize)
845     for p in newpkgs:
846         repo = p.repo.appdata
847         location, medianr = p.lookup_location()
848         if not location:
849             continue
850         if repo.type == 'commandline':
851             f = solv.xfopen(location)
852             if not f:
853                 sys.exit("\n%s: %s not found" % location)
854             newpkgsfp[p.id] = f
855             continue
856         if not sysrepo.handle.isempty() and os.access('/usr/bin/applydeltarpm', os.X_OK):
857             pname = p.name
858             di = p.repo.Dataiterator(solv.SOLVID_META, solv.DELTA_PACKAGE_NAME, pname, solv.Dataiterator.SEARCH_STRING)
859             di.prepend_keyname(solv.REPOSITORY_DELTAINFO)
860             for d in di:
861                 dp = d.parentpos()
862                 if dp.lookup_id(solv.DELTA_PACKAGE_EVR) != p.evrid or dp.lookup_id(solv.DELTA_PACKAGE_ARCH) != p.archid:
863                     continue
864                 baseevrid = dp.lookup_id(solv.DELTA_BASE_EVR)
865                 candidate = None
866                 for installedp in pool.whatprovides(p.nameid):
867                     if installedp.isinstalled() and installedp.nameid == p.nameid and installedp.archid == p.archid and installedp.evrid == baseevrid:
868                         candidate = installedp
869                 if not candidate:
870                     continue
871                 seq = dp.lookup_deltaseq()
872                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, '-c', '-s', seq])
873                 if st:
874                     continue
875                 chksum = dp.lookup_checksum(solv.DELTA_CHECKSUM)
876                 if not chksum:
877                     continue
878                 dloc, dmedianr = dp.lookup_deltalocation()
879                 dloc = repo.packagespath() + dloc
880                 f = repo.download(dloc, False, chksum)
881                 if not f:
882                     continue
883                 nf = tempfile.TemporaryFile()
884                 nf = os.dup(nf.fileno())   # get rid of CLOEXEC
885                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, "/dev/fd/%d" % f.fileno(), "/dev/fd/%d" % nf])
886                 os.lseek(nf, 0, os.SEEK_SET)
887                 newpkgsfp[p.id] = solv.xfopen_fd("", nf)
888                 os.close(nf)
889                 break
890             if p.id in newpkgsfp:
891                 sys.stdout.write("d")
892                 sys.stdout.flush()
893                 continue
894                     
895         chksum = p.lookup_checksum(solv.SOLVABLE_CHECKSUM)
896         location = repo.packagespath() + location
897         f = repo.download(location, False, chksum)
898         if not f:
899             sys.exit("\n%s: %s not found in repository" % (repo.name, location))
900         newpkgsfp[p.id] = f
901         sys.stdout.write(".")
902         sys.stdout.flush()
903     print
904 print "Committing transaction:"
905 print
906 ts = rpm.TransactionSet('/')
907 ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
908 erasenamehelper = {}
909 for p in trans.steps():
910     type = trans.steptype(p, solv.Transaction.SOLVER_TRANSACTION_RPM_ONLY)
911     if type == solv.Transaction.SOLVER_TRANSACTION_ERASE:
912         rpmdbid = p.lookup_num(solv.RPM_RPMDBID)
913         erasenamehelper[p.name] = p
914         if not rpmdbid:
915             sys.exit("\ninternal error: installed package %s has no rpmdbid\n" % p)
916         ts.addErase(rpmdbid)
917     elif type == solv.Transaction.SOLVER_TRANSACTION_INSTALL:
918         f = newpkgsfp[p.id]
919         h = ts.hdrFromFdno(f.fileno())
920         os.lseek(f.fileno(), 0, os.SEEK_SET)
921         ts.addInstall(h, p, 'u')
922     elif type == solv.Transaction.SOLVER_TRANSACTION_MULTIINSTALL:
923         f = newpkgsfp[p.id]
924         h = ts.hdrFromFdno(f.fileno())
925         os.lseek(f.fileno(), 0, os.SEEK_SET)
926         ts.addInstall(h, p, 'i')
927 checkproblems = ts.check()
928 if checkproblems:
929     print checkproblems
930     sys.exit("Sorry.")
931 ts.order()
932 def runCallback(reason, amount, total, p, d):
933     if reason == rpm.RPMCALLBACK_INST_OPEN_FILE:
934         return newpkgsfp[p.id].fileno()
935     if reason == rpm.RPMCALLBACK_INST_START:
936         print "install", p
937     if reason == rpm.RPMCALLBACK_UNINST_START:
938         # argh, p is just the name of the package
939         if p in erasenamehelper:
940             p = erasenamehelper[p]
941             print "erase", p
942 runproblems = ts.run(runCallback, '')
943 if runproblems:
944     print runproblems
945     sys.exit(1)
946 sys.exit(0)
947
948 # vim: sw=4 et