Selection: rename addsimple method to add_raw in the bindings
[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(None, 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")
572 (options, args) = parser.parse_args()
573 if not args:
574     parser.print_help(sys.stderr)
575     sys.exit(1)
576
577 cmd = args[0]
578 args = args[1:]
579 if cmd == 'li':
580     cmd = 'list'
581 if cmd == 'in':
582     cmd = 'install'
583 if cmd == 'rm':
584     cmd = 'erase'
585 if cmd == 've':
586     cmd = 'verify'
587 if cmd == 'se':
588     cmd = 'search'
589
590
591 # read all repo configs
592 repos = []
593 reposdirs = []
594 if os.path.isdir("/etc/zypp/repos.d"):
595   reposdirs = [ "/etc/zypp/repos.d" ]
596 else:
597   reposdirs = [ "/etc/yum/repos.d" ]
598
599 for reposdir in reposdirs:
600     if not os.path.isdir(reposdir):
601         continue
602     for reponame in sorted(glob.glob('%s/*.repo' % reposdir)):
603         cfg = INIConfig(open(reponame))
604         for alias in cfg:
605             repoattr = {'enabled': 0, 'priority': 99, 'autorefresh': 1, 'type': 'rpm-md', 'metadata_expire': 900}
606             for k in cfg[alias]:
607                 repoattr[k] = cfg[alias][k]
608             if 'mirrorlist' in repoattr and 'metalink' not in repoattr:
609                 if repoattr['mirrorlist'].find('/metalink'):
610                     repoattr['metalink'] = repoattr['mirrorlist']
611                     del repoattr['mirrorlist']
612             if repoattr['type'] == 'rpm-md':
613                 repo = repo_repomd(alias, 'repomd', repoattr)
614             elif repoattr['type'] == 'yast2':
615                 repo = repo_susetags(alias, 'susetags', repoattr)
616             else:
617                 repo = repo_unknown(alias, 'unknown', repoattr)
618             repos.append(repo)
619
620 pool = solv.Pool()
621 pool.setarch()
622 pool.set_loadcallback(load_stub)
623
624 # now load all enabled repos into the pool
625 sysrepo = repo_system('@System', 'system')
626 sysrepo.load(pool)
627 for repo in repos:
628     if int(repo['enabled']):
629         repo.load(pool)
630     
631 repolimiter = None
632 if options.repos:
633     for reponame in options.repos:
634         mrepos = [ repo for repo in repos if repo.name == reponame ]
635         if not mrepos:
636             print "no repository matches '%s'" % reponame
637             sys.exit(1)
638         repo = mrepos[0]
639         if hasattr(repo, 'handle'):
640             if not repolimiter:
641                 repolimiter = pool.Selection()
642             repolimiter.add_raw(Job.SOLVER_SOLVABLE_REPO|Job.SOLVER_SETREPO|Job.SOLVER_SETVENDOR, repo.handle.id)
643
644 if cmd == 'search':
645     matches = {}
646     di = pool.Dataiterator(0, solv.SOLVABLE_NAME, args[0], Dataiterator.SEARCH_SUBSTRING|Dataiterator.SEARCH_NOCASE)
647     for d in di:
648         matches[d.solvid] = True
649     for solvid in sorted(matches.keys()):
650         print " - %s [%s]: %s" % (pool.solvid2str(solvid), pool.solvables[solvid].repo.name, pool.lookup_str(solvid, solv.SOLVABLE_SUMMARY))
651     sys.exit(0)
652
653 cmdlinerepo = None
654 if cmd == 'list' or cmd == 'info' or cmd == 'install':
655     for arg in args:
656         if arg.endswith(".rpm") and os.access(arg, os.R_OK):
657             if not cmdlinerepo:
658                 cmdlinerepo = repo_cmdline('@commandline', 'cmdline')
659                 cmdlinerepo.load(pool)
660                 cmdlinerepo['packages'] = {}
661             cmdlinerepo['packages'][arg] = cmdlinerepo.handle.add_rpm(arg, Repo.REPO_REUSE_REPODATA|Repo.REPO_NO_INTERNALIZE)
662     if cmdlinerepo:
663         cmdlinerepo.handle.internalize()
664
665 addedprovides = pool.addfileprovides_queue()
666 if addedprovides:
667     sysrepo.updateaddedprovides(addedprovides)
668     for repo in repos:
669         repo.updateaddedprovides(addedprovides)
670
671 pool.createwhatprovides()
672
673 # convert arguments into jobs
674 jobs = []
675 for arg in args:
676     if cmdlinerepo and arg in cmdlinerepo['packages']:
677         jobs.append(pool.Job(Job.SOLVER_SOLVABLE, cmdlinerepo['packages'][arg]))
678     else:
679         flags = Selection.SELECTION_NAME|Selection.SELECTION_PROVIDES|Selection.SELECTION_GLOB
680         if len(arg) and arg[0] == '/':
681             flags |= Selection.SELECTION_FILELIST
682             if cmd == 'erase':
683                 flags |= Selection.SELECTION_INSTALLED_ONLY
684         sel = pool.select(arg, flags)
685         if repolimiter:
686            sel.limit(repolimiter)
687         if sel.isempty():
688             sel = pool.select(arg, flags | Selection.SELECTION_NOCASE)
689             if repolimiter:
690                sel.limit(repolimiter)
691             if not sel.isempty():
692                 print "[ignoring case for '%s']" % arg
693         if sel.isempty():
694             print "nothing matches '%s'" % arg
695             sys.exit(1)
696         if sel.flags() & Selection.SELECTION_FILELIST:
697             print "[using file list match for '%s']" % arg
698         if sel.flags() & Selection.SELECTION_PROVIDES:
699             print "[using capability match for '%s']" % arg
700         jobs += sel.jobs(0)
701
702 if not jobs and (cmd == 'up' or cmd == 'dup' or cmd == 'verify' or repolimiter):
703     sel = pool.Selection()
704     sel.add_raw(Job.SOLVER_SOLVABLE_ALL, 0)
705     if repolimiter:
706        sel.limit(repolimiter)
707     jobs += sel.jobs(0)
708
709 if cmd == 'list' or cmd == 'info':
710     if not jobs:
711         print "no package matched."
712         sys.exit(1)
713     for job in jobs:
714         for s in job.solvables():
715             if cmd == 'info':
716                 print "Name:        %s" % s
717                 print "Repo:        %s" % s.repo
718                 print "Summary:     %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
719                 str = s.lookup_str(solv.SOLVABLE_URL)
720                 if str:
721                     print "Url:         %s" % str
722                 str = s.lookup_str(solv.SOLVABLE_LICENSE)
723                 if str:
724                     print "License:     %s" % str
725                 print "Description:\n%s" % s.lookup_str(solv.SOLVABLE_DESCRIPTION)
726                 print
727             else:
728                 print "  - %s [%s]" % (s, s.repo)
729                 print "    %s" % s.lookup_str(solv.SOLVABLE_SUMMARY)
730     sys.exit(0)
731
732 if cmd == 'install' or cmd == 'erase' or cmd == 'up' or cmd == 'dup' or cmd == 'verify':
733     if not jobs:
734         print "no package matched."
735         sys.exit(1)
736     for job in jobs:
737         if cmd == 'up':
738             job.how |= Job.SOLVER_UPDATE
739             # up magic: use install instead of update if no installed package matches
740             if job.isemptyupdate():
741                 job.how ^= Job.SOLVER_UPDATE ^ Job.SOLVER_INSTALL
742         elif cmd == 'install':
743             job.how |= Job.SOLVER_INSTALL
744         elif cmd == 'erase':
745             job.how |= Job.SOLVER_ERASE
746         elif cmd == 'dup':
747             job.how |= Job.SOLVER_DISTUPGRADE
748         elif cmd == 'verify':
749             job.how |= Job.SOLVER_VERIFY
750
751     #pool.set_debuglevel(2)
752     solver = None
753     while True:
754         solver = pool.Solver()
755         solver.set_flag(Solver.SOLVER_FLAG_SPLITPROVIDES, 1);
756         if cmd == 'erase':
757             solver.set_flag(Solver.SOLVER_FLAG_ALLOW_UNINSTALL, 1);
758         problems = solver.solve(jobs)
759         if not problems:
760             break
761         for problem in problems:
762             print "Problem %d:" % problem.id
763             r = problem.findproblemrule()
764             ri = r.info()
765             print ri.problemstr()
766             solutions = problem.solutions()
767             for solution in solutions:
768                 print "  Solution %d:" % solution.id
769                 elements = solution.elements(True)
770                 for element in elements:
771                     print "  - %s" % element.str()
772                 print
773             sol = ''
774             while not (sol == 's' or sol == 'q' or (sol.isdigit() and int(sol) >= 1 and int(sol) <= len(solutions))):
775                 sys.stdout.write("Please choose a solution: ")
776                 sys.stdout.flush()
777                 sol = sys.stdin.readline().strip()
778             if sol == 's':
779                 continue        # skip problem
780             if sol == 'q':
781                 sys.exit(1)
782             solution = solutions[int(sol) - 1]
783             for element in solution.elements():
784                 newjob = element.Job()
785                 if element.type == Solver.SOLVER_SOLUTION_JOB:
786                     jobs[element.jobidx] = newjob
787                 else:
788                     if newjob and newjob not in jobs:
789                         jobs.append(newjob)
790                         
791     # no problems, show transaction
792     trans = solver.transaction()
793     del solver
794     if trans.isempty():
795         print "Nothing to do."
796         sys.exit(0)
797     print
798     print "Transaction summary:"
799     print
800     for cl in trans.classify():
801         if cl.type == Transaction.SOLVER_TRANSACTION_ERASE:
802             print "%d erased packages:" % cl.count
803         elif cl.type == Transaction.SOLVER_TRANSACTION_INSTALL:
804             print "%d installed packages:" % cl.count
805         elif cl.type == Transaction.SOLVER_TRANSACTION_REINSTALLED:
806             print "%d reinstalled packages:" % cl.count
807         elif cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
808             print "%d downgraded packages:" % cl.count
809         elif cl.type == Transaction.SOLVER_TRANSACTION_CHANGED:
810             print "%d changed packages:" % cl.count
811         elif cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED:
812             print "%d upgraded packages:" % cl.count
813         elif cl.type == Transaction.SOLVER_TRANSACTION_VENDORCHANGE:
814             print "%d vendor changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
815         elif cl.type == Transaction.SOLVER_TRANSACTION_ARCHCHANGE:
816             print "%d arch changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
817         else:
818             continue
819         for p in cl.solvables():
820             if cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
821                 op = trans.othersolvable(p)
822                 print "  - %s -> %s" % (p, op)
823             else:
824                 print "  - %s" % p
825         print
826     print "install size change: %d K" % trans.calc_installsizechange()
827     print
828     
829     while True:
830         sys.stdout.write("OK to continue (y/n)? ")
831         sys.stdout.flush()
832         yn = sys.stdin.readline().strip()
833         if yn == 'y': break
834         if yn == 'n': sys.exit(1)
835     newpkgs = trans.newpackages()
836     newpkgsfp = {}
837     if newpkgs:
838         downloadsize = 0
839         for p in newpkgs:
840             downloadsize += p.lookup_num(solv.SOLVABLE_DOWNLOADSIZE)
841         print "Downloading %d packages, %d K" % (len(newpkgs), downloadsize)
842         for p in newpkgs:
843             repo = p.repo.appdata
844             location, medianr = p.lookup_location()
845             if not location:
846                 continue
847             if repo.type == 'commandline':
848                 f = solv.xfopen(location)
849                 if not f:
850                     sys.exit("\n%s: %s not found" % location)
851                 newpkgsfp[p.id] = f
852                 continue
853             if not sysrepo.handle.isempty() and os.access('/usr/bin/applydeltarpm', os.X_OK):
854                 pname = p.name
855                 di = p.repo.Dataiterator(solv.SOLVID_META, solv.DELTA_PACKAGE_NAME, pname, Dataiterator.SEARCH_STRING)
856                 di.prepend_keyname(solv.REPOSITORY_DELTAINFO)
857                 for d in di:
858                     dp = d.parentpos()
859                     if dp.lookup_id(solv.DELTA_PACKAGE_EVR) != p.evrid or dp.lookup_id(solv.DELTA_PACKAGE_ARCH) != p.archid:
860                         continue
861                     baseevrid = dp.lookup_id(solv.DELTA_BASE_EVR)
862                     candidate = None
863                     for installedp in pool.whatprovides(p.nameid):
864                         if installedp.isinstalled() and installedp.nameid == p.nameid and installedp.archid == p.archid and installedp.evrid == baseevrid:
865                             candidate = installedp
866                     if not candidate:
867                         continue
868                     seq = dp.lookup_deltaseq()
869                     st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, '-c', '-s', seq])
870                     if st:
871                         continue
872                     chksum = dp.lookup_checksum(solv.DELTA_CHECKSUM)
873                     if not chksum:
874                         continue
875                     dloc, dmedianr = dp.lookup_deltalocation()
876                     dloc = repo.packagespath() + dloc
877                     f = repo.download(dloc, False, chksum)
878                     if not f:
879                         continue
880                     nf = tempfile.TemporaryFile()
881                     nf = os.dup(nf.fileno())   # get rid of CLOEXEC
882                     st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, "/dev/fd/%d" % f.fileno(), "/dev/fd/%d" % nf])
883                     os.lseek(nf, 0, os.SEEK_SET)
884                     newpkgsfp[p.id] = solv.xfopen_fd("", nf)
885                     os.close(nf)
886                     break
887                 if p.id in newpkgsfp:
888                     sys.stdout.write("d")
889                     sys.stdout.flush()
890                     continue
891                         
892             chksum = p.lookup_checksum(solv.SOLVABLE_CHECKSUM)
893             location = repo.packagespath() + location
894             f = repo.download(location, False, chksum)
895             if not f:
896                 sys.exit("\n%s: %s not found in repository" % (repo.name, location))
897             newpkgsfp[p.id] = f
898             sys.stdout.write(".")
899             sys.stdout.flush()
900         print
901     print "Committing transaction:"
902     print
903     ts = rpm.TransactionSet('/')
904     ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
905     erasenamehelper = {}
906     for p in trans.steps():
907         type = trans.steptype(p, Transaction.SOLVER_TRANSACTION_RPM_ONLY)
908         if type == Transaction.SOLVER_TRANSACTION_ERASE:
909             rpmdbid = p.lookup_num(solv.RPM_RPMDBID)
910             erasenamehelper[p.name] = p
911             if not rpmdbid:
912                 sys.exit("\ninternal error: installed package %s has no rpmdbid\n" % p)
913             ts.addErase(rpmdbid)
914         elif type == Transaction.SOLVER_TRANSACTION_INSTALL:
915             f = newpkgsfp[p.id]
916             h = ts.hdrFromFdno(solv.xfileno(f))
917             os.lseek(solv.xfileno(f), 0, os.SEEK_SET)
918             ts.addInstall(h, p, 'u')
919         elif type == Transaction.SOLVER_TRANSACTION_MULTIINSTALL:
920             f = newpkgsfp[p.id]
921             h = ts.hdrFromFdno(solv.xfileno(f))
922             os.lseek(solv.xfileno(f), 0, os.SEEK_SET)
923             ts.addInstall(h, p, 'i')
924     checkproblems = ts.check()
925     if checkproblems:
926         print checkproblems
927         sys.exit("Sorry.")
928     ts.order()
929     def runCallback(reason, amount, total, p, d):
930         if reason == rpm.RPMCALLBACK_INST_OPEN_FILE:
931             return solv.xfileno(newpkgsfp[p.id])
932         if reason == rpm.RPMCALLBACK_INST_START:
933             print "install", p
934         if reason == rpm.RPMCALLBACK_UNINST_START:
935             # argh, p is just the name of the package
936             if p in erasenamehelper:
937                 p = erasenamehelper[p]
938             print "erase", p
939     runproblems = ts.run(runCallback, '')
940     if runproblems:
941         print runproblems
942         sys.exit(1)
943     sys.exit(0)
944
945 print "unknown command", cmd
946 sys.exit(1)
947
948 # vim: sw=4 et