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