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