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