#!/usr/bin/python # # ketchup v0.5 "mystery girl" # # Copyright 2004 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. # # Usage: # # in an existing kernel directory, run: # # ketchup # # where version is a complete kernel version, or a branch name to grab # the latest version # # Note: you must set KETCHUP_ARCH to a directory to cache patches in import re, sys, urllib, os, getopt try: opts, args = getopt.getopt(sys.argv[1:], "qnsupmg", ["quiet", "dry-run", "show-latest", "no-gpg" "show-url", "show-previous", "show-makefile"]) except: print "usage: ketchup [-qnsupm] " options = lambda x: x options.quiet = options.dryrun = options.show = 0 options.showurl = options.prev = options.makefile = 0 options.nogpg = 0 for o,a in opts: if o in ("-q", "--quiet"): options.quiet = 1 if o in ("-n", "--dry-run"): options.dryrun = 1 if o in ("-s", "--show-latest"): options.show = 1 if o in ("-u", "--show-url"): options.showurl = 1 if o in ("-p", "--show-previous"): options.prev = 1 if o in ("-m", "--show-makefile"): options.makefile = 1 if o in ("-g", "--no-gpg"): options.nogpg = 1 archive = "" try: kernel_url = os.environ["KETCHUP_URL"] except: kernel_url = 'http://www.kernel.org/pub/linux/kernel' # Functions to parse version strings def tree(ver): return float(re.match(r'(\d+\.\d+)', ver).group(1)) def rev(ver): p = pre(ver) r = int(re.match(r'\d+\.\d+\.(\d+)', ver).group(1)) if p: r = r - 1 return r def pre(ver): try: return re.match(r'\d+\.\d+\.\d+-((rc|pre)\d+)', ver).group(1) except: return None def pretype(ver): try: return re.match(r'\d+\.\d+\.\d+-((rc|pre)\d+)', ver).group(2) except: return None def prenum(ver): try: return int(re.match(r'\d+\.\d+\.\d+-((rc|pre)(\d+))', ver).group(3)) except: return None def prebase(ver): return re.match(r'(\d+\.\d+\.\d+(-(rc|pre)\d+)?)', ver).group(1) def base(ver): return "%s.%s" % (tree(ver), rev(ver)) def forkname(ver): try: return re.match(r'\d+.\d+.\d+(-(rc|pre)\d+)?(-(\w+?)\d+)?', ver).group(4) except: return None def forknum(ver): try: return int(re.match(r'\d+.\d+.\d+(-(rc|pre)\d+)?(-(\w+?)(\d+))?', ver).group(5)) except: return None def fork(ver): try: return re.match(r'\d+.\d+.\d+(-(rc|pre)\d+)?(-(\w+))?', ver).group(4) except: return None def get_ver(makefile): """ Read the version information from the specified makefile """ part = {} parts = "VERSION PATCHLEVEL SUBLEVEL EXTRAVERSION".split(' ') m = open(makefile) for l in m.readlines(): for p in parts: try: part[p] = re.match(r'%s\s*=\s*(\S+)' % p, l).group(1) except: pass version = "%s.%s.%s" % tuple([part[p] for p in parts[:3]]) version += part.get("EXTRAVERSION","") return version def compare_ver(a, b): """ Compare kernel versions a and b Note that -pre and -rc versions sort before the version they modify, -pre sorts before -rc, and -bk, -mm, etc. sort alphabetically. """ if a == b: return 0 c = cmp(float(tree(a)), float(tree(b))) if c: return c c = cmp(rev(a), rev(b)) if c: return c c = cmp(pretype(a), pretype(b)) # pre sorts before rc if c: return c c = cmp(prenum(a), prenum(b)) if c: return c c = cmp(forkname(a), forkname(b)) if c: return c return cmp(forknum(a), forknum(b)) def last(url): for l in urllib.urlopen(url).readlines(): m=re.search('(?i)', l) if m: n = m.group(1) return n def latest_mm(url, pat): url = kernel_url + '/people/akpm/patches/2.6/' url += last(url) part = last(url) return part[:-1] def latest_26(url, pat): for l in urllib.urlopen(url).readlines(): m = re.search('"LATEST-IS-(.*)"', l) if m: p = m.group(1) return p def latest_dir(url, pat): """Find the latest link matching pat at url after sorting""" p = [] for l in urllib.urlopen(url).readlines(): m = re.search('"%s"' % pat, l) if m: p.append(m.group(1)) if not p: return None p.sort(compare_ver) return p[-1] def latest_26_tip(url, pat): l = [ find_ver('2.6'), find_ver('2.6-bk'), find_ver('2.6-pre') ] l.sort(compare_ver) return l[-1] # latest lookup function, canonical url, pattern for lookup function, # signature flag, description version_info = { '2.4': (latest_dir, kernel_url + "/v2.4" + "/patch-%(base)s.bz2", r'patch-(.*?).bz2', 1, "old stable kernel series"), '2.4-pre': (latest_dir, kernel_url + "/v2.4" + "/testing/patch-%(prebase)s.bz2", r'patch-(.*?).bz2', 1, "old stable kernel series prereleases"), '2.6': (latest_26, kernel_url + "/v2.6" + "/patch-%(base)s.bz2", "", 1, "current stable kernel series"), '2.6-pre': (latest_dir, kernel_url + "/v2.6" + "/testing/patch-%(prebase)s.bz2", r'patch-(.*?).bz2', 1, "current stable kernel series prereleases"), '2.6-bk': (latest_dir, kernel_url + "/v2.6" + "/snapshots/patch-%(full)s.bz2", r'patch-(.*?).bz2', 1, "current stable kernel series snapshots"), '2.6-tip': (latest_26_tip, "", "", 1, "current stable kernel series tip"), '2.6-mm': (latest_mm, kernel_url + "/people/akpm/patches/" + "%(tree)s/%(prebase)s/%(full)s/%(full)s.bz2", "", 1, "Andrew Morton's -mm development tree"), '2.6-tiny': (latest_dir, "http://www.selenic.com/tiny/%(full)s.patch.bz2", r'(2.6.*?).patch.bz2', 1, "Matt Mackall's -tiny tree for small systems") } def version_url(ver, sign = 0): """ Return the URL for the patch associated with the specified version """ b = "%.1f" % tree(ver) f = forkname(ver) p = pre(ver) s = b if f: s = "%s-%s" % (b, f) elif p: s = "%s-pre" % b if sign and options.nogpg: return None if sign and not version_info[s][3]: return None v = { 'full': ver, 'tree': tree(ver), 'base': base(ver), 'prebase': prebase(ver) } u = version_info[s][1] % v if sign: u += ".sign" return u def patch_path(ver): return os.path.join(archive, os.path.basename(version_url(ver))) def get_patch(ver): """Return the path to patch for given ver, downloading if necessary""" f = patch_path(ver) if os.path.exists(f): return f url = version_url(ver) if not options.quiet: print "Downloading %s" % os.path.basename(url) if options.dryrun: return f p = urllib.urlopen(url).read() if "404" in p: print "patch not found" sys.exit(-1) open(f, 'w').write(p) sign = version_url(ver, 1) if sign: if not options.quiet: print "Downloading %s" % os.path.basename(sign) sig = urllib.urlopen(sign).read() sf = f + ".sign" if "<title>404" in sig: print "signature not found" print "removing files..." os.unlink(f) sys.exit(-1) open(sf, 'w').write(sig) print "Verifying signature..." r = os.system("gpg --verify %s %s" % (sf, f)) if r: print "error: gpg returned %d" % r print "removing files..." os.unlink(f) os.unlink(sf) sys.exit(-1) return f def apply_patch(ver, reverse = 0): """Find the patch to upgrade from the predecessor of ver to ver and apply or reverse it.""" p = get_patch(ver) r = "" if reverse: r = "-R" if not options.quiet: print "Applying %s %s" % (os.path.basename(p), r) if options.dryrun: return ver if p[-4:] == ".bz2": err = os.system("bzcat %s | patch -l -p1 %s > .patchdiag" % (p, r)) elif p[-3:] == ".gz": err = os.system("zcat %s | patch -l -p1 %s > .patchdiag" % (p, r)) else: os.system("patch -l -p1 %s < %s > .patchdiag" % (r, p)) if err: sys.stderr.write(open(".patchdiag").read()) sys.stderr.write("patch %s failed: %d\n" % (p, err)) sys.exit(-1) def install(ver): print "Wanted to install %s", ver return ver def find_ver(ver): if ver in version_info: v = version_info[ver] for n in range(5): return v[0](os.path.dirname(v[1]), v[2]) sys.stderr.write('retrying version lookup for %s\n' % ver) else: return ver def transform(a, b): if not a: install(base(b)) a = base(b) if a == b: if not options.quiet: print "Nothing to do!" sys.exit(0) t = tree(a) if t != tree(b): sys.stderr.write("Can't patch %s to %s\n" % (tree(a), tree(b))) sys.exit(-1) if fork(a): apply_patch(a, 1) a = prebase(a) if prebase(a) != prebase(b): if pre(a): apply_patch(a, 1) a = base(a) ra, rb = rev(a), rev(b) if ra > rb: for r in range(ra, rb, -1): apply_patch("%s.%s" % (t, r), -1) if ra < rb: for r in range(ra + 1, rb + 1): apply_patch("%s.%s" % (t, r)) a = base(b) if pre(b): a = apply_patch(prebase(b)) a = prebase(b) if fork(b): a = apply_patch(b) if len(args) != 1: print "incorrect number of arguments" if options.makefile: print get_ver(args[0]) elif options.show: print find_ver(args[0]) elif options.showurl: print version_url(find_ver(args[0])) elif options.prev: v = find_ver(args[0]) p = prebase(v) if p == v: p = base(v) if p == v: if rev(v) > 0: p = "%.1f.%s" % (tree(v), rev(v) -1) else: p = "unknown" print p else: try: archive = os.environ["KETCHUP_ARCH"] except: sys.stderr.write("Must set KETCHUP_ARCH to patch archive directory\n") sys.exit(0) a = get_ver('Makefile') b = find_ver(args[0]) if not options.quiet: print "%s -> %s" % (a, b) if options.dryrun and not options.quiet: print "(simulated)" transform(a, b)