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