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