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