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