Imported Upstream version 0.6.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
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         self.add_ext(repodata, 'deltainfo', 'DL')
395         self.add_ext(repodata, 'filelists', 'FL')
396         repodata.internalize()
397     
398     def load_ext(self, repodata):
399         repomdtype = repodata.lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_TYPE)
400         if repomdtype == 'filelists':
401             ext = 'FL'
402         elif repomdtype == 'deltainfo':
403             ext = 'DL'
404         else:
405             return False
406         sys.stdout.write("[%s:%s: " % (self.name, ext))
407         if self.usecachedrepo(ext):
408             sys.stdout.write("cached]\n")
409             sys.stdout.flush()
410             return True
411         sys.stdout.write("fetching]\n")
412         sys.stdout.flush()
413         filename = repodata.lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_LOCATION)
414         filechksum = repodata.lookup_checksum(solv.SOLVID_META, solv.REPOSITORY_REPOMD_CHECKSUM)
415         f = self.download(filename, True, filechksum)
416         if not f:
417             return False
418         if ext == 'FL':
419             self.handle.add_rpmmd(f, 'FL', solv.Repo.REPO_USE_LOADING|solv.Repo.REPO_EXTEND_SOLVABLES|solv.Repo.REPO_LOCALPOOL)
420         elif ext == 'DL':
421             self.handle.add_deltainfoxml(f, solv.Repo.REPO_USE_LOADING)
422         self.writecachedrepo(ext, repodata)
423         return True
424
425 class repo_susetags(repo_generic):
426     def load(self, pool):
427         if super(repo_susetags, self).load(pool):
428             return True
429         sys.stdout.write("susetags repo '%s': " % self.name)
430         sys.stdout.flush()
431         f = self.download("content", False, None, None)
432         if not f:
433             print("no content file, skipped")
434             self.handle.free(True)
435             del self.handle
436             return False
437         self['cookie'] = self.calc_cookie_fp(f)
438         if self.usecachedrepo(None, True):
439             print("cached")
440             return True
441         self.handle.add_content(f, 0)
442         print("fetching")
443         defvendorid = self.handle.meta.lookup_id(solv.SUSETAGS_DEFAULTVENDOR)
444         descrdir = self.handle.meta.lookup_str(solv.SUSETAGS_DESCRDIR)
445         if not descrdir:
446             descrdir = "suse/setup/descr"
447         (filename, filechksum) = self.find('packages.gz')
448         if not filename:
449             (filename, filechksum) = self.find('packages')
450         if filename:
451             f = self.download(descrdir + '/' + filename, True, filechksum, True)
452             if f:
453                 self.handle.add_susetags(f, defvendorid, None, solv.Repo.REPO_NO_INTERNALIZE|solv.Repo.SUSETAGS_RECORD_SHARES)
454                 (filename, filechksum) = self.find('packages.en.gz')
455                 if not filename:
456                     (filename, filechksum) = self.find('packages.en')
457                 if filename:
458                     f = self.download(descrdir + '/' + filename, True, filechksum, True)
459                     if f:
460                         self.handle.add_susetags(f, defvendorid, None, solv.Repo.REPO_NO_INTERNALIZE|solv.Repo.REPO_REUSE_REPODATA|solv.Repo.REPO_EXTEND_SOLVABLES)
461                 self.handle.internalize()
462         self.add_exts()
463         self.writecachedrepo(None)
464         # must be called after writing the repo
465         self.handle.create_stubs()
466         return True
467
468     def find(self, what):
469         di = self.handle.Dataiterator_meta(solv.SUSETAGS_FILE_NAME, what, solv.Dataiterator.SEARCH_STRING)
470         di.prepend_keyname(solv.SUSETAGS_FILE)
471         for d in di:
472             dp = d.parentpos()
473             chksum = dp.lookup_checksum(solv.SUSETAGS_FILE_CHECKSUM)
474             return (what, chksum)
475         return (None, None)
476
477     def add_ext(self, repodata, what, ext):
478         (filename, chksum) = self.find(what)
479         if not filename:
480             return
481         handle = repodata.new_handle()
482         repodata.set_str(handle, solv.SUSETAGS_FILE_NAME, filename)
483         if chksum:
484             repodata.set_checksum(handle, solv.SUSETAGS_FILE_CHECKSUM, chksum)
485         self.add_ext_keys(ext, repodata, handle)
486         repodata.add_flexarray(solv.SOLVID_META, solv.REPOSITORY_EXTERNAL, handle)
487         
488     def add_exts(self):
489         repodata = self.handle.add_repodata(0)
490         di = self.handle.Dataiterator_meta(solv.SUSETAGS_FILE_NAME, None, 0)
491         di.prepend_keyname(solv.SUSETAGS_FILE)
492         for d in di:
493             filename = d.str
494             if not filename:
495                 continue
496             if filename[0:9] != "packages.":
497                 continue
498             if len(filename) == 11 and filename != "packages.gz":
499                 ext = filename[9:11]
500             elif filename[11:12] == ".":
501                 ext = filename[9:11]
502             else:
503                 continue
504             if ext == "en":
505                 continue
506             self.add_ext(repodata, filename, ext)
507         repodata.internalize()
508
509     def load_ext(self, repodata):
510         filename = repodata.lookup_str(solv.SOLVID_META, solv.SUSETAGS_FILE_NAME)
511         ext = filename[9:11]
512         sys.stdout.write("[%s:%s: " % (self.name, ext))
513         if self.usecachedrepo(ext):
514             sys.stdout.write("cached]\n")
515             sys.stdout.flush()
516             return True
517         sys.stdout.write("fetching]\n")
518         sys.stdout.flush()
519         defvendorid = self.handle.meta.lookup_id(solv.SUSETAGS_DEFAULTVENDOR)
520         descrdir = self.handle.meta.lookup_str(solv.SUSETAGS_DESCRDIR)
521         if not descrdir:
522             descrdir = "suse/setup/descr"
523         filechksum = repodata.lookup_checksum(solv.SOLVID_META, solv.SUSETAGS_FILE_CHECKSUM)
524         f = self.download(descrdir + '/' + filename, True, filechksum)
525         if not f:
526             return False
527         flags = solv.Repo.REPO_USE_LOADING|solv.Repo.REPO_EXTEND_SOLVABLES
528         if ext != 'DL':
529             flags |= solv.Repo.REPO_LOCALPOOL
530         self.handle.add_susetags(f, defvendorid, ext, flags)
531         self.writecachedrepo(ext, repodata)
532         return True
533
534     def packagespath(self):
535         datadir = repo.handle.meta.lookup_str(solv.SUSETAGS_DATADIR)
536         if not datadir:
537             datadir = 'suse'
538         return datadir + '/'
539
540 class repo_unknown(repo_generic):
541     def load(self, pool):
542         print("unsupported repo '%s': skipped" % self.name)
543         return False
544
545 class repo_system(repo_generic):
546     def load(self, pool):
547         self.handle = pool.add_repo(self.name)
548         self.handle.appdata = self
549         pool.installed = self.handle
550         sys.stdout.write("rpm database: ")
551         self['cookie'] = self.calc_cookie_file("/var/lib/rpm/Packages")
552         if self.usecachedrepo(None):
553             print("cached")
554             return True
555         print("reading")
556         if hasattr(self.handle.__class__, 'add_products'):
557             self.handle.add_products("/etc/products.d", solv.Repo.REPO_NO_INTERNALIZE)
558         f = solv.xfopen(self.cachepath())
559         self.handle.add_rpmdb_reffp(f, solv.Repo.REPO_REUSE_REPODATA)
560         self.writecachedrepo(None)
561         return True
562
563 class repo_cmdline(repo_generic):
564     def load(self, pool):
565         self.handle = pool.add_repo(self.name)
566         self.handle.appdata = self 
567         return True
568
569 def load_stub(repodata):
570     repo = repodata.repo.appdata
571     if repo:
572         return repo.load_ext(repodata)
573     return False
574
575
576 parser = OptionParser(usage="usage: solv.py [options] COMMAND")
577 parser.add_option('-r', '--repo', action="append", type="string", dest="repos", help="limit to specified repositories")
578 parser.add_option('--best', action="store_true", dest="best", help="force installation/update to best packages")
579 parser.add_option('--clean', action="store_true", dest="clean", help="delete no longer needed packages")
580 (options, args) = parser.parse_args()
581 if not args:
582     parser.print_help(sys.stderr)
583     sys.exit(1)
584
585 cmd = args[0]
586 args = args[1:]
587
588 cmdabbrev = {'ls': 'list', 'in': 'install', 'rm': 'erase', 've': 'verify', 'se': 'search'}
589 if cmd in cmdabbrev:
590     cmd = cmdabbrev[cmd]
591
592 cmdactionmap = {
593   'install': solv.Job.SOLVER_INSTALL,
594   'erase':   solv.Job.SOLVER_ERASE,
595   'up':      solv.Job.SOLVER_UPDATE,
596   'dup':     solv.Job.SOLVER_DISTUPGRADE,
597   'verify':  solv.Job.SOLVER_VERIFY,
598   'list':    0,
599   'info':    0
600 }
601
602 # read all repo configs
603 repos = []
604 reposdirs = []
605 if os.path.isdir("/etc/zypp/repos.d"):
606   reposdirs = [ "/etc/zypp/repos.d" ]
607 else:
608   reposdirs = [ "/etc/yum/repos.d" ]
609
610 for reposdir in reposdirs:
611     if not os.path.isdir(reposdir):
612         continue
613     for reponame in sorted(glob.glob('%s/*.repo' % reposdir)):
614         cfg = INIConfig(open(reponame))
615         for alias in cfg:
616             repoattr = {'enabled': 0, 'priority': 99, 'autorefresh': 1, 'type': 'rpm-md', 'metadata_expire': 900}
617             for k in cfg[alias]:
618                 repoattr[k] = cfg[alias][k]
619             if 'mirrorlist' in repoattr and 'metalink' not in repoattr:
620                 if repoattr['mirrorlist'].find('/metalink'):
621                     repoattr['metalink'] = repoattr['mirrorlist']
622                     del repoattr['mirrorlist']
623             if repoattr['type'] == 'rpm-md':
624                 repo = repo_repomd(alias, 'repomd', repoattr)
625             elif repoattr['type'] == 'yast2':
626                 repo = repo_susetags(alias, 'susetags', repoattr)
627             else:
628                 repo = repo_unknown(alias, 'unknown', repoattr)
629             repos.append(repo)
630
631 pool = solv.Pool()
632 pool.setarch()
633 pool.set_loadcallback(load_stub)
634
635 # now load all enabled repos into the pool
636 sysrepo = repo_system('@System', 'system')
637 sysrepo.load(pool)
638 for repo in repos:
639     if int(repo['enabled']):
640         repo.load(pool)
641     
642 repofilter = None
643 if options.repos:
644     for reponame in options.repos:
645         mrepos = [ repo for repo in repos if repo.name == reponame ]
646         if not mrepos:
647             print("no repository matches '%s'" % reponame)
648             sys.exit(1)
649         repo = mrepos[0]
650         if hasattr(repo, 'handle'):
651             if not repofilter:
652                 repofilter = pool.Selection()
653             repofilter.add(repo.handle.Selection(solv.Job.SOLVER_SETVENDOR))
654
655 if cmd == 'search':
656     pool.createwhatprovides()
657     sel = pool.Selection()
658     di = pool.Dataiterator(solv.SOLVABLE_NAME, args[0], solv.Dataiterator.SEARCH_SUBSTRING|solv.Dataiterator.SEARCH_NOCASE)
659     for d in di:
660         sel.add_raw(solv.Job.SOLVER_SOLVABLE, d.solvid)
661     if repofilter:
662        sel.filter(repofilter)
663     for s in sel.solvables():
664         print(" - %s [%s]: %s" % (s, s.repo.name, s.lookup_str(solv.SOLVABLE_SUMMARY)))
665     sys.exit(0)
666
667 if cmd not in cmdactionmap:
668     print("unknown command %s" % cmd)
669     sys.exit(1)
670
671 cmdlinerepo = None
672 if cmd == 'list' or cmd == 'info' or cmd == 'install':
673     for arg in args:
674         if arg.endswith(".rpm") and os.access(arg, os.R_OK):
675             if not cmdlinerepo:
676                 cmdlinerepo = repo_cmdline('@commandline', 'cmdline')
677                 cmdlinerepo.load(pool)
678                 cmdlinerepo['packages'] = {}
679             s = cmdlinerepo.handle.add_rpm(arg, solv.Repo.REPO_REUSE_REPODATA|solv.Repo.REPO_NO_INTERNALIZE)
680             if not s:
681                 print(pool.errstr)
682                 sys.exit(1)
683             cmdlinerepo['packages'][arg] = s
684     if cmdlinerepo:
685         cmdlinerepo.handle.internalize()
686
687 addedprovides = pool.addfileprovides_queue()
688 if addedprovides:
689     sysrepo.updateaddedprovides(addedprovides)
690     for repo in repos:
691         repo.updateaddedprovides(addedprovides)
692
693 pool.createwhatprovides()
694
695 # convert arguments into jobs
696 jobs = []
697 for arg in args:
698     if cmdlinerepo and arg in cmdlinerepo['packages']:
699         jobs.append(pool.Job(solv.Job.SOLVER_SOLVABLE, cmdlinerepo['packages'][arg].id))
700     else:
701         flags = solv.Selection.SELECTION_NAME|solv.Selection.SELECTION_PROVIDES|solv.Selection.SELECTION_GLOB
702         flags |= solv.Selection.SELECTION_CANON|solv.Selection.SELECTION_DOTARCH|solv.Selection.SELECTION_REL
703         if len(arg) and arg[0] == '/':
704             flags |= solv.Selection.SELECTION_FILELIST
705             if cmd == 'erase':
706                 flags |= solv.Selection.SELECTION_INSTALLED_ONLY
707         sel = pool.select(arg, flags)
708         if repofilter:
709            sel.filter(repofilter)
710         if sel.isempty():
711             sel = pool.select(arg, flags | solv.Selection.SELECTION_NOCASE)
712             if repofilter:
713                sel.filter(repofilter)
714             if not sel.isempty():
715                 print("[ignoring case for '%s']" % arg)
716         if sel.isempty():
717             print("nothing matches '%s'" % arg)
718             sys.exit(1)
719         if sel.flags() & solv.Selection.SELECTION_FILELIST:
720             print("[using file list match for '%s']" % arg)
721         if sel.flags() & solv.Selection.SELECTION_PROVIDES:
722             print("[using capability match for '%s']" % arg)
723         jobs += sel.jobs(cmdactionmap[cmd])
724
725 if not jobs and (cmd == 'up' or cmd == 'dup' or cmd == 'verify' or repofilter):
726     sel = pool.Selection_all()
727     if repofilter:
728        sel.filter(repofilter)
729     jobs += sel.jobs(cmdactionmap[cmd])
730
731 if not jobs:
732     print("no package matched.")
733     sys.exit(1)
734
735 if cmd == 'list' or cmd == 'info':
736     for job in jobs:
737         for s in job.solvables():
738             if cmd == 'info':
739                 print("Name:        %s" % s)
740                 print("Repo:        %s" % s.repo)
741                 print("Summary:     %s" % s.lookup_str(solv.SOLVABLE_SUMMARY))
742                 str = s.lookup_str(solv.SOLVABLE_URL)
743                 if str:
744                     print("Url:         %s" % str)
745                 str = s.lookup_str(solv.SOLVABLE_LICENSE)
746                 if str:
747                     print("License:     %s" % str)
748                 print("Description:\n%s" % s.lookup_str(solv.SOLVABLE_DESCRIPTION))
749                 print('')
750             else:
751                 print("  - %s [%s]" % (s, s.repo))
752                 print("    %s" % s.lookup_str(solv.SOLVABLE_SUMMARY))
753     sys.exit(0)
754
755 # up magic: use install instead of update if no installed package matches
756 for job in jobs:
757     if cmd == 'up' and job.isemptyupdate():
758         job.how ^= solv.Job.SOLVER_UPDATE ^ solv.Job.SOLVER_INSTALL
759     if options.best:
760         job.how |= solv.Job.SOLVER_FORCEBEST
761     if options.clean:
762         job.how |= solv.Job.SOLVER_CLEANDEPS
763
764 #pool.set_debuglevel(2)
765 solver = pool.Solver()
766 solver.set_flag(solv.Solver.SOLVER_FLAG_SPLITPROVIDES, 1);
767 if cmd == 'erase':
768     solver.set_flag(solv.Solver.SOLVER_FLAG_ALLOW_UNINSTALL, 1);
769
770 while True:
771     problems = solver.solve(jobs)
772     if not problems:
773         break
774     for problem in problems:
775         print("Problem %d/%d:" % (problem.id, len(problems)))
776         print(problem)
777         solutions = problem.solutions()
778         for solution in solutions:
779             print("  Solution %d:" % solution.id)
780             elements = solution.elements(True)
781             for element in elements:
782                 print("  - %s" % element.str())
783             print('')
784         sol = ''
785         while not (sol == 's' or sol == 'q' or (sol.isdigit() and int(sol) >= 1 and int(sol) <= len(solutions))):
786             sys.stdout.write("Please choose a solution: ")
787             sys.stdout.flush()
788             sol = sys.stdin.readline().strip()
789         if sol == 's':
790             continue        # skip problem
791         if sol == 'q':
792             sys.exit(1)
793         solution = solutions[int(sol) - 1]
794         for element in solution.elements():
795             newjob = element.Job()
796             if element.type == solv.Solver.SOLVER_SOLUTION_JOB:
797                 jobs[element.jobidx] = newjob
798             else:
799                 if newjob and newjob not in jobs:
800                     jobs.append(newjob)
801                     
802 # no problems, show transaction
803 trans = solver.transaction()
804 del solver
805 if trans.isempty():
806     print("Nothing to do.")
807     sys.exit(0)
808 print('')
809 print("Transaction summary:")
810 print('')
811 for cl in trans.classify(solv.Transaction.SOLVER_TRANSACTION_SHOW_OBSOLETES | solv.Transaction.SOLVER_TRANSACTION_OBSOLETE_IS_UPGRADE):
812     if cl.type == solv.Transaction.SOLVER_TRANSACTION_ERASE:
813         print("%d erased packages:" % cl.count)
814     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_INSTALL:
815         print("%d installed packages:" % cl.count)
816     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_REINSTALLED:
817         print("%d reinstalled packages:" % cl.count)
818     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED:
819         print("%d downgraded packages:" % cl.count)
820     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_CHANGED:
821         print("%d changed packages:" % cl.count)
822     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED:
823         print("%d upgraded packages:" % cl.count)
824     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_VENDORCHANGE:
825         print("%d vendor changes from '%s' to '%s':" % (cl.count, cl.fromstr, cl.tostr))
826     elif cl.type == solv.Transaction.SOLVER_TRANSACTION_ARCHCHANGE:
827         print("%d arch changes from '%s' to '%s':" % (cl.count, cl.fromstr, cl.tostr))
828     else:
829         continue
830     for p in cl.solvables():
831         if cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED:
832             op = trans.othersolvable(p)
833             print("  - %s -> %s" % (p, op))
834         else:
835             print("  - %s" % p)
836     print('')
837 print("install size change: %d K" % trans.calc_installsizechange())
838 print('')
839
840 while True:
841     sys.stdout.write("OK to continue (y/n)? ")
842     sys.stdout.flush()
843     yn = sys.stdin.readline().strip()
844     if yn == 'y': break
845     if yn == 'n' or yn == 'q': sys.exit(1)
846 newpkgs = trans.newsolvables()
847 newpkgsfp = {}
848 if newpkgs:
849     downloadsize = 0
850     for p in newpkgs:
851         downloadsize += p.lookup_num(solv.SOLVABLE_DOWNLOADSIZE)
852     print("Downloading %d packages, %d K" % (len(newpkgs), downloadsize / 1024))
853     for p in newpkgs:
854         repo = p.repo.appdata
855         location, medianr = p.lookup_location()
856         if not location:
857             continue
858         if repo.type == 'commandline':
859             f = solv.xfopen(location)
860             if not f:
861                 sys.exit("\n%s: %s not found" % location)
862             newpkgsfp[p.id] = f
863             continue
864         if not sysrepo.handle.isempty() and os.access('/usr/bin/applydeltarpm', os.X_OK):
865             pname = p.name
866             di = p.repo.Dataiterator_meta(solv.DELTA_PACKAGE_NAME, pname, solv.Dataiterator.SEARCH_STRING)
867             di.prepend_keyname(solv.REPOSITORY_DELTAINFO)
868             for d in di:
869                 dp = d.parentpos()
870                 if dp.lookup_id(solv.DELTA_PACKAGE_EVR) != p.evrid or dp.lookup_id(solv.DELTA_PACKAGE_ARCH) != p.archid:
871                     continue
872                 baseevrid = dp.lookup_id(solv.DELTA_BASE_EVR)
873                 candidate = None
874                 for installedp in pool.whatprovides(p.nameid):
875                     if installedp.isinstalled() and installedp.nameid == p.nameid and installedp.archid == p.archid and installedp.evrid == baseevrid:
876                         candidate = installedp
877                 if not candidate:
878                     continue
879                 seq = dp.lookup_deltaseq()
880                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, '-c', '-s', seq])
881                 if st:
882                     continue
883                 chksum = dp.lookup_checksum(solv.DELTA_CHECKSUM)
884                 if not chksum:
885                     continue
886                 dloc, dmedianr = dp.lookup_deltalocation()
887                 dloc = repo.packagespath() + dloc
888                 f = repo.download(dloc, False, chksum)
889                 if not f:
890                     continue
891                 nf = tempfile.TemporaryFile()
892                 nf = os.dup(nf.fileno())   # get rid of CLOEXEC
893                 f.cloexec(0)
894                 st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, "/dev/fd/%d" % f.fileno(), "/dev/fd/%d" % nf])
895                 if st:
896                     os.close(nf)
897                     continue
898                 os.lseek(nf, 0, os.SEEK_SET)
899                 newpkgsfp[p.id] = solv.xfopen_fd("", nf)
900                 os.close(nf)
901                 break
902             if p.id in newpkgsfp:
903                 sys.stdout.write("d")
904                 sys.stdout.flush()
905                 continue
906                     
907         chksum = p.lookup_checksum(solv.SOLVABLE_CHECKSUM)
908         location = repo.packagespath() + location
909         f = repo.download(location, False, chksum)
910         if not f:
911             sys.exit("\n%s: %s not found in repository" % (repo.name, location))
912         newpkgsfp[p.id] = f
913         sys.stdout.write(".")
914         sys.stdout.flush()
915     print('')
916 print("Committing transaction:")
917 print('')
918 ts = rpm.TransactionSet('/')
919 ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
920 erasenamehelper = {}
921 for p in trans.steps():
922     type = trans.steptype(p, solv.Transaction.SOLVER_TRANSACTION_RPM_ONLY)
923     if type == solv.Transaction.SOLVER_TRANSACTION_ERASE:
924         rpmdbid = p.lookup_num(solv.RPM_RPMDBID)
925         erasenamehelper[p.name] = p
926         if not rpmdbid:
927             sys.exit("\ninternal error: installed package %s has no rpmdbid\n" % p)
928         ts.addErase(rpmdbid)
929     elif type == solv.Transaction.SOLVER_TRANSACTION_INSTALL:
930         f = newpkgsfp[p.id]
931         h = ts.hdrFromFdno(f.fileno())
932         os.lseek(f.fileno(), 0, os.SEEK_SET)
933         ts.addInstall(h, p, 'u')
934     elif type == solv.Transaction.SOLVER_TRANSACTION_MULTIINSTALL:
935         f = newpkgsfp[p.id]
936         h = ts.hdrFromFdno(f.fileno())
937         os.lseek(f.fileno(), 0, os.SEEK_SET)
938         ts.addInstall(h, p, 'i')
939 checkproblems = ts.check()
940 if checkproblems:
941     print(checkproblems)
942     sys.exit("Sorry.")
943 ts.order()
944 def runCallback(reason, amount, total, p, d):
945     if reason == rpm.RPMCALLBACK_INST_OPEN_FILE:
946         return newpkgsfp[p.id].fileno()
947     if reason == rpm.RPMCALLBACK_INST_START:
948         print("install %s" % p)
949     if reason == rpm.RPMCALLBACK_UNINST_START:
950         # argh, p is just the name of the package
951         if p in erasenamehelper:
952             p = erasenamehelper[p]
953             print("erase %s" % p)
954 runproblems = ts.run(runCallback, '')
955 if runproblems:
956     print(runproblems)
957     sys.exit(1)
958 sys.exit(0)
959
960 # vim: sw=4 et