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