support medianr in lookup_deltalocation, change lookup_location to return medianr...
[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                     # 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             job.how |= Job.SOLVER_UPDATE
734             # up magic: use install instead of update if no installed package matches
735             if job.isemptyupdate():
736                 job.how ^= Job.SOLVER_UPDATE ^ Job.SOLVER_INSTALL
737         elif cmd == 'install':
738             job.how |= Job.SOLVER_INSTALL
739         elif cmd == 'erase':
740             job.how |= Job.SOLVER_ERASE
741         elif cmd == 'dup':
742             job.how |= Job.SOLVER_DISTUPGRADE
743         elif cmd == 'verify':
744             job.how |= Job.SOLVER_VERIFY
745
746     #pool.set_debuglevel(2)
747     solver = None
748     while True:
749         solver = pool.Solver()
750         solver.set_flag(Solver.SOLVER_FLAG_SPLITPROVIDES, 1);
751         if cmd == 'erase':
752             solver.set_flag(Solver.SOLVER_FLAG_ALLOW_UNINSTALL, 1);
753         problems = solver.solve(jobs)
754         if not problems:
755             break
756         for problem in problems:
757             print "Problem %d:" % problem.id
758             r = problem.findproblemrule()
759             ri = r.info()
760             print ri.problemstr()
761             solutions = problem.solutions()
762             for solution in solutions:
763                 print "  Solution %d:" % solution.id
764                 elements = solution.elements(True)
765                 for element in elements:
766                     print "  - %s" % element.str()
767                 print
768             sol = ''
769             while not (sol == 's' or sol == 'q' or (sol.isdigit() and int(sol) >= 1 and int(sol) <= len(solutions))):
770                 sys.stdout.write("Please choose a solution: ")
771                 sys.stdout.flush()
772                 sol = sys.stdin.readline().strip()
773             if sol == 's':
774                 continue        # skip problem
775             if sol == 'q':
776                 sys.exit(1)
777             solution = solutions[int(sol) - 1]
778             for element in solution.elements():
779                 newjob = element.Job()
780                 if element.type == Solver.SOLVER_SOLUTION_JOB:
781                     jobs[element.jobidx] = newjob
782                 else:
783                     if newjob and newjob not in jobs:
784                         jobs.append(newjob)
785                         
786     # no problems, show transaction
787     trans = solver.transaction()
788     del solver
789     if trans.isempty():
790         print "Nothing to do."
791         sys.exit(0)
792     print
793     print "Transaction summary:"
794     print
795     for cl in trans.classify():
796         if cl.type == Transaction.SOLVER_TRANSACTION_ERASE:
797             print "%d erased packages:" % cl.count
798         elif cl.type == Transaction.SOLVER_TRANSACTION_INSTALL:
799             print "%d installed packages:" % cl.count
800         elif cl.type == Transaction.SOLVER_TRANSACTION_REINSTALLED:
801             print "%d reinstalled packages:" % cl.count
802         elif cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
803             print "%d downgraded packages:" % cl.count
804         elif cl.type == Transaction.SOLVER_TRANSACTION_CHANGED:
805             print "%d changed packages:" % cl.count
806         elif cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED:
807             print "%d upgraded packages:" % cl.count
808         elif cl.type == Transaction.SOLVER_TRANSACTION_VENDORCHANGE:
809             print "%d vendor changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
810         elif cl.type == Transaction.SOLVER_TRANSACTION_ARCHCHANGE:
811             print "%d arch changes from '%s' to '%s':" % (cl.count, cl.fromdep(), cl.todep())
812         else:
813             continue
814         for p in cl.solvables():
815             if cl.type == Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == Transaction.SOLVER_TRANSACTION_DOWNGRADED:
816                 op = trans.othersolvable(p)
817                 print "  - %s -> %s" % (p, op)
818             else:
819                 print "  - %s" % p
820         print
821     print "install size change: %d K" % trans.calc_installsizechange()
822     print
823     
824     while True:
825         sys.stdout.write("OK to continue (y/n)? ")
826         sys.stdout.flush()
827         yn = sys.stdin.readline().strip()
828         if yn == 'y': break
829         if yn == 'n': sys.exit(1)
830     newpkgs = trans.newpackages()
831     newpkgsfp = {}
832     if newpkgs:
833         downloadsize = 0
834         for p in newpkgs:
835             downloadsize += p.lookup_num(solv.SOLVABLE_DOWNLOADSIZE)
836         print "Downloading %d packages, %d K" % (len(newpkgs), downloadsize)
837         for p in newpkgs:
838             repo = p.repo.appdata
839             location, medianr = p.lookup_location()
840             if not location:
841                 continue
842             if repo.type == 'commandline':
843                 f = solv.xfopen(location)
844                 if not f:
845                     sys.exit("\n%s: %s not found" % location)
846                 newpkgsfp[p.id] = f
847                 continue
848             if not sysrepo.handle.isempty() and os.access('/usr/bin/applydeltarpm', os.X_OK):
849                 pname = p.name
850                 di = p.repo.Dataiterator(solv.SOLVID_META, solv.DELTA_PACKAGE_NAME, pname, Dataiterator.SEARCH_STRING)
851                 di.prepend_keyname(solv.REPOSITORY_DELTAINFO)
852                 for d in di:
853                     dp = d.parentpos()
854                     if dp.lookup_id(solv.DELTA_PACKAGE_EVR) != p.evrid or dp.lookup_id(solv.DELTA_PACKAGE_ARCH) != p.archid:
855                         continue
856                     baseevrid = dp.lookup_id(solv.DELTA_BASE_EVR)
857                     candidate = None
858                     for installedp in pool.whatprovides(p.nameid):
859                         if installedp.isinstalled() and installedp.nameid == p.nameid and installedp.archid == p.archid and installedp.evrid == baseevrid:
860                             candidate = installedp
861                     if not candidate:
862                         continue
863                     seq = dp.lookup_deltaseq()
864                     st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, '-c', '-s', seq])
865                     if st:
866                         continue
867                     chksum = dp.lookup_checksum(solv.DELTA_CHECKSUM)
868                     if not chksum:
869                         continue
870                     dloc, dmedianr = dp.lookup_deltalocation()
871                     dloc = repo.packagespath() + dloc
872                     f = repo.download(dloc, False, chksum)
873                     if not f:
874                         continue
875                     nf = tempfile.TemporaryFile()
876                     nf = os.dup(nf.fileno())   # get rid of CLOEXEC
877                     st = subprocess.call(['/usr/bin/applydeltarpm', '-a', p.arch, "/dev/fd/%d" % f.fileno(), "/dev/fd/%d" % nf])
878                     os.lseek(nf, 0, os.SEEK_SET)
879                     newpkgsfp[p.id] = solv.xfopen_fd("", nf)
880                     os.close(nf)
881                     break
882                 if p.id in newpkgsfp:
883                     sys.stdout.write("d")
884                     sys.stdout.flush()
885                     continue
886                         
887             chksum = p.lookup_checksum(solv.SOLVABLE_CHECKSUM)
888             location = repo.packagespath() + location
889             f = repo.download(location, False, chksum)
890             if not f:
891                 sys.exit("\n%s: %s not found in repository" % (repo.name, location))
892             newpkgsfp[p.id] = f
893             sys.stdout.write(".")
894             sys.stdout.flush()
895         print
896     print "Committing transaction:"
897     print
898     ts = rpm.TransactionSet('/')
899     ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
900     erasenamehelper = {}
901     for p in trans.steps():
902         type = trans.steptype(p, Transaction.SOLVER_TRANSACTION_RPM_ONLY)
903         if type == Transaction.SOLVER_TRANSACTION_ERASE:
904             rpmdbid = p.lookup_num(solv.RPM_RPMDBID)
905             erasenamehelper[p.name] = p
906             if not rpmdbid:
907                 sys.exit("\ninternal error: installed package %s has no rpmdbid\n" % p)
908             ts.addErase(rpmdbid)
909         elif type == Transaction.SOLVER_TRANSACTION_INSTALL:
910             f = newpkgsfp[p.id]
911             h = ts.hdrFromFdno(solv.xfileno(f))
912             os.lseek(solv.xfileno(f), 0, os.SEEK_SET)
913             ts.addInstall(h, p, 'u')
914         elif type == Transaction.SOLVER_TRANSACTION_MULTIINSTALL:
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, 'i')
919     checkproblems = ts.check()
920     if checkproblems:
921         print checkproblems
922         sys.exit("Sorry.")
923     ts.order()
924     def runCallback(reason, amount, total, p, d):
925         if reason == rpm.RPMCALLBACK_INST_OPEN_FILE:
926             return solv.xfileno(newpkgsfp[p.id])
927         if reason == rpm.RPMCALLBACK_INST_START:
928             print "install", p
929         if reason == rpm.RPMCALLBACK_UNINST_START:
930             # argh, p is just the name of the package
931             if p in erasenamehelper:
932                 p = erasenamehelper[p]
933             print "erase", p
934     runproblems = ts.run(runCallback, '')
935     if runproblems:
936         print runproblems
937         sys.exit(1)
938     sys.exit(0)
939
940 print "unknown command", cmd
941 sys.exit(1)
942
943 # vim: sw=4 et