#!/usr/bin/python # # ketchup 0.9.8 # http://selenic.com/ketchup/wiki # # 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 # # You can override some variables by creating a ~/.ketchuprc file. # The ~/.ketchuprc is just a Python script, eg. it might look like this: # # kernel_url = 'http://kernel.localdomain/pub/linux/kernel' # archive = os.environ["HOME"] + '/tmp/ketchup-archive' # gpg = '/weird/path/to/gpg' # import re, sys, urllib, os, getopt, glob, shutil def error(*args): sys.stderr.write("ketchup: ") for a in args: sys.stderr.write(str(a)) sys.stderr.write("\n") def qprint(*args): if not options["quiet"]: sys.stdout.write(" ".join(map(str, args))) sys.stdout.write("\n") def lprint(*args): sys.stdout.write(" ".join(map(str, args))) sys.stdout.write("\n") def fancyopts(args, options, state, syntax=''): long = [] short = '' map = {} dt = {} def help(state, opt, arg, options = options, syntax = syntax): lprint("Usage: ", syntax) for s, l, d, c in options: opt = ' ' if s: opt = opt + '-' + s + ' ' if l: opt = opt + '--' + l + ' ' if d: opt = opt + '(' + str(d) + ')' lprint(opt) if c: lprint(' %s' % c) sys.exit(0) options = [('h', 'help', help, 'Show usage info')] + options for s, l, d, c in options: map['-'+s] = map['--'+l]=l state[l] = d dt[l] = type(d) if not d is None and not type(d) is type(help): s, l = s + ':', l + '=' if s: short = short + s if l: long.append(l) if os.environ.has_key("KETCHUP_OPTS"): args = os.environ["KETCHUP_OPTS"].split() + args try: opts, args = getopt.getopt(args, short, long) except getopt.GetoptError: help(state, None, args) sys.exit(-1) for opt, arg in opts: if dt[map[opt]] is type(help): state[map[opt]](state,map[opt],arg) elif dt[map[opt]] is type(1): state[map[opt]] = int(arg) elif dt[map[opt]] is type(''): state[map[opt]] = arg elif dt[map[opt]] is type([]): state[map[opt]].append(arg) elif dt[map[opt]] is type(None): state[map[opt]] = 1 return args # Default values kernel_url = 'http://www.kernel.org/pub/linux/kernel' archive = os.environ["HOME"] + "/.ketchup" rename_prefix = 'linux-' rename_with_localversion = False wget = "/usr/bin/wget" gpg = "/usr/bin/gpg" precommand = postcommand = None default_tree = None local_trees = {} # 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+(\.\d+)?-((rc|pre)\d+)', ver).group(2) except: return None def post(ver): try: return re.match(r'\d+\.\d+\.\d+\.(\d+)', ver).group(1) except: return None def pretype(ver): try: return re.match(r'\d+\.\d+\.\d+(\.\d+)?-((rc|pre)\d+)', ver).group(3) 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 revbase(ver): return "%s.%s" % (tree(ver), rev(ver)) def base(ver): v = revbase(ver) if post(ver): v += "." + post(ver) return v def forkname(ver): try: return re.match(r'\d+.\d+.\d+(\.\d+)?(-(rc|pre)\d+)?(-(\w+?)\d+)?', ver).group(5) except: return None def forknum(ver): try: return int( re.match(r'\d+.\d+.\d+(\.\d+)?(-(rc|pre)\d+)?(-(\w+?)(\d+))?', ver).group(6)) except: return None def fork(ver): try: return re.match(r'\d+.\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 get_localversion(): v = '' for name in glob.glob('localversion*'): try: v += open(name).readline().strip() except: pass try: c = open('.config').read() v += re.search(r'^CONFIG_LOCALVERSION="(.+)"', c, re.M).group(1) except: pass return v 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, -bk, -git, and -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(int(post(a) or 0), int(post(b) or 0)) 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, pat="(.*/)"): for l in urllib.urlopen(url).readlines(): m = re.search('(?i)' % pat, 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_ck(url, pat): url = "http://ck.kolivas.org/patches/2.6/pre-releases/" url += last(url) part = last(url) pre = part[:-1] url = "http://ck.kolivas.org/patches/2.6/" url += last(url,"(2.6.*/)") part = last(url) rel = part[:-1] l = [pre, rel] l.sort(compare_ver) return l[-1] 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] # mbligh is lazy and has a bunch of empty directories def latest_mjb(url, pat): url = kernel_url + '/people/mbligh/' # find the last Linus release and search backwards l = [find_ver('2.6'), find_ver("2.6-pre")] l.sort(compare_ver) linus = l[-1] p = [] for l in urllib.urlopen(url).readlines(): m = re.search('"(2\.6\..*/)"', l) if m: v = m.group(1) if compare_ver(v, linus) <= 0: p.append(v) p.sort(compare_ver) p.reverse() for ver in p: mjb = latest_dir(url + ver, pat) if mjb: return mjb return None def latest_26_tip(url, pat): l = [find_ver('2.6'), find_ver('2.6-git'), find_ver('2.6-pre')] l.sort(compare_ver) return l[-1] def find_info(ver): 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 return version_info[s] def version_urls(ver): """ Return the URL for the patch associated with the specified version """ i = find_info(ver)[1] if type(i) != type([]): i = [i] v = { 'full': ver, 'tree': tree(ver), 'base': base(ver), 'prebase': prebase(ver) } l = [] for e in i: l.append(e % v) return l def patch_path(ver): return os.path.join(archive, os.path.basename(version_urls(ver)[0])) def download(url, f): qprint("Downloading %s" % os.path.basename(url)) if options["dry-run"]: return 1 if not options["wget"]: p = urllib.urlopen(url).read() if p.find("404") != -1: return None open(f, 'w').write(p) else: e = os.system("%s -c -O %s %s" % (options["wget"], f + ".partial", url)) if e: return None os.rename(f + ".partial", f) return 1 def verify(url, f, sign): if options["no-gpg"] or options["dry-run"] or not options["gpg-path"]: return 1 sf = f + sign if not download(url + sign, sf): error("signature download failed") error("removing files...") os.unlink(f) return 0 qprint("Verifying signature...") r = os.system("%s --verify %s %s" % (options["gpg-path"], sf, f)) if r: error("gpg returned %d" % r) error("removing files...") os.unlink(f) os.unlink(sf) return 0 return 1 def trydownload(urls, f, sign): for url in urls: if download(url, f): if not sign or verify(url, f, sign): return f if url[-4:] == ".bz2": f2 = f[:-4] + ".gz" url2 = url[:-4] + ".gz" if download(url2, f2): if not sign or verify(url2, f2, sign): return f2 return None 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 if f[-4:] == ".bz2": f2 = f[:-4] + ".gz" if os.path.exists(f2): return f2 urls = version_urls(ver) sign = find_info(ver)[3] if sign == 1: sign = ".sign" f = trydownload(urls, f, sign) if not f: error("patch download failed") 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" qprint("Applying %s%s" % (os.path.basename(p), r)) if options["dry-run"]: return ver def cmd(patch, reverse, dry): base = "patch -l -p1%s" % reverse if dry: base += " --dry-run" if p[-4:] == ".bz2": pipe = "bzcat %s | %s" % (patch, base) elif p[-3:] == ".gz": pipe = "zcat %s | %s" % (patch, base) else: pipe = "%s < %s" % (base, patch) err = os.system(pipe + " > .patchdiag") if err: sys.stderr.write(open(".patchdiag").read()) os.unlink(".patchdiag") return err err = cmd(p, r, 1) if err: error("patch %s failed: %d" % (p, err)) sys.exit(-1) err = cmd(p, r, 0) if err: error("patch %s failed while it was supposed to apply: %d" % (p, err)) sys.exit(-1) def untar(tarfile): old = os.getcwd() os.mkdir("ketchup-tmp") os.chdir("ketchup-tmp") err = os.system("bzcat %s | tar -xf -" % tarfile) if err: error("Unpacking failed: ", err) sys.exit(-1) err = os.system("mv linux*/* linux*/.[^.]* ..; rmdir linux*") if err: error("Unpacking failed: ", err) sys.exit(-1) os.chdir(old) shutil.rmtree("ketchup-tmp") def install_nearest(ver): t = tree(ver) tarballs = glob.glob(archive + "/linux-%s.*.tar.bz2" % t) list = [] for f in tarballs: m = re.match(r'.*/linux-(.*).tar.bz2$', f) v = m.group(1) d = abs(rev(v) - rev(ver)) list.append((d, f, v)) list.sort() if not list or (options["full-tarball"] and list[0][0]): f = "linux-%s.tar.bz2" % ver url = "%s/v%s/%s" % (kernel_url, t, f) f = archive + "/" + f sign = find_info(ver)[3] if sign == 1: sign = ".sign" f = trydownload([url], f, sign) if not f: error("Tarball download failed") sys.exit(-1) else: f = list[0][1] ver = list[0][2] qprint("Unpacking %s" % os.path.basename(f)) if options["dry-run"]: return ver untar(f) return ver def find_ver(ver): if ver in version_info.keys(): v = version_info[ver] d = v[1] if type(d) is type([]): d = d[0] for n in range(5): return v[0](os.path.dirname(d), v[2]) error('retrying version lookup for %s' % ver) else: return ver def transform(a, b): if a == b: qprint("Nothing to do!") return if not a: a = install_nearest(base(b)) t = tree(a) if t != tree(b): error("Can't patch %s to %s" % (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) if post(a) and post(a) != post(b): apply_patch(prebase(a), 1) 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 = revbase(b) if post(b) and post(a) != post(b): apply_patch(prebase(b), 0) a = base(b) if pre(b): apply_patch(prebase(b)) a = prebase(b) if fork(b): a = apply_patch(b) def rename_dir(v): """Rename the current directory to linux-v, where v is the function arg""" if rename_with_localversion: v += get_localversion() cwd = os.getcwd() basedir = os.path.dirname(cwd) newdir = os.path.join(basedir, rename_prefix + v) if newdir == cwd: return if os.access(newdir, os.F_OK): error("Cannot rename directory, destination exists: %s", newdir); return os.rename(cwd, newdir) qprint('Current directory renamed to %s' % newdir) # latest lookup function, canonical urls, 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_dir, kernel_url + "/v2.6" + "/patch-%(prebase)s.bz2", r'patch-(.*?).bz2', 1, "current stable kernel series"), '2.6-rc': (latest_dir, kernel_url + "/v2.6" + "/testing/patch-%(prebase)s.bz2", r'patch-(.*?).bz2', 1, "current stable kernel series prereleases"), '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-git': (latest_dir, [kernel_url + "/v2.6" + "/snapshots/patch-%(full)s.bz2", kernel_url + "/v2.6" + "/snapshots/old/patch-%(full)s.bz2"], r'patch-(.*?).bz2', 1, "current stable kernel series snapshots"), '2.6-bk': (latest_dir, [kernel_url + "/v2.6" + "/snapshots/patch-%(full)s.bz2", kernel_url + "/v2.6" + "/snapshots/old/patch-%(full)s.bz2"], r'patch-(.*?).bz2', 1, "old 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"), '2.6-mjb': (latest_mjb, kernel_url + "/people/mbligh/%(prebase)s/patch-%(full)s.bz2", r'patch-(2.6.*?).bz2', 1, "Martin Bligh's random collection 'o crap"), '2.6-rt': (latest_dir, ["http://people.redhat.com/mingo/" + "realtime-preempt/patch-%(full)s", "http://people.redhat.com/mingo/" + "realtime-preempt/older/patch-%(full)s"], r'patch-(2.6.*?)', 0, "Ingo Molnar's realtime-preempt kernel"), '2.6-ck': (latest_ck, ["http://ck.kolivas.org/patches/2.6/" + "%(prebase)s/%(full)s/patch-%(full)s.bz2", "http://ck.kolivas.org/patches/2.6/pre-releases/" + "%(prebase)s/%(full)s/patch-%(full)s.bz2"], "", ".sig", "Con Kolivas' patches for system responsiveness (desktop)"), '2.6-cks': (latest_dir, "http://ck.kolivas.org/patches/cks/patch-%(full)s.bz2", r'patch-(2.6.*?).bz2', ".sig", "Con Kolivas' patches for system responsiveness (server)") } # Override defaults with ~/.ketchuprc which is just a Python script rcpath = os.path.expanduser('~/.ketchuprc') if os.path.isfile(rcpath): try: execfile(rcpath) except Exception, e: sys.exit('Failed parsing %s\nError was: %s' % (rcpath, e)) # Add local trees for k,v in local_trees.items(): version_info[k] = v # Environment variables override defaults and ketchuprc kernel_url = os.environ.get("KETCHUP_URL", kernel_url) archive = os.environ.get("KETCHUP_ARCH", archive) # And finally command line overrides everything if not os.path.exists(wget): wget = "" if not os.path.exists(gpg): gpg = "" options = {} opts = [ ('a', 'archive', archive, 'cache directory'), ('d', 'directory', '.', 'directory to update'), ('f', 'full-tarball', None, 'if unpacking a tarball, download the latest'), ('g', 'gpg-path', gpg, 'path for GnuPG'), ('G', 'no-gpg', None, 'disable GPG signature verification'), ('k', 'kernel-url', kernel_url, 'base url for kernel.org mirror'), ('l', 'list-trees', None, 'list supported trees'), ('m', 'show-makefile', None, 'output version in makefile <arg>'), ('n', 'dry-run', None, 'don\'t download or apply patches'), ('p', 'show-previous', None, 'output version previous to <arg>'), ('q', 'quiet', None, 'reduce output'), ('r', 'rename-directory', None, 'rename updated directory to %s<v>' % rename_prefix), ('s', 'show-latest', None, 'output the latest version of <arg>'), ('u', 'show-url', None, 'output URL for <arg>'), ('w', 'wget', wget, 'command to use for wget'), ] args = fancyopts(sys.argv[1:], opts, options, 'ketchup [options] [ver]') archive = options["archive"] kernel_url = options["kernel-url"] if options["no-gpg"]: options["gpg-path"] = '' # Process args if not os.path.exists(options["directory"]): qprint("Creating target directory", options["directory"]) os.mkdir(options["directory"]) os.chdir(options["directory"]) if os.path.isfile(".ketchuprc"): try: execfile(".ketchuprc") except Exception, e: sys.exit('Failed parsing .ketchuprc\nError was: %s' % (e)) if options["list-trees"]: l = version_info.keys() l.sort() for tree in l: if version_info[tree][3] == 0: lprint(tree, "(unsigned)") else: lprint(tree, "(signed)") lprint(" " + version_info[tree][4]) sys.exit(0) if options["show-makefile"] and len(args) < 2: if not args: lprint(get_ver("Makefile")) else: lprint(get_ver(args[0])) sys.exit(0) if len(args) == 0 and default_tree: qprint("Using default tree \"%s\"" % (default_tree)) args.append(default_tree) if len(args) != 1: error("No version given on command line and no default in configuration") sys.exit(-1) if options["show-latest"]: lprint(find_ver(args[0])) sys.exit(0) if options["show-url"]: lprint(version_urls(find_ver(args[0]))[0]) sys.exit(0) if options["show-previous"]: 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" lprint(p) sys.exit(0) if not os.path.exists(options["archive"]): qprint("Creating cache directory", options["archive"]) os.mkdir(options["archive"]) if precommand and os.system(precommand): sys.exit('Precommand "%s" failed!' % precommand) try: a = get_ver('Makefile') except: a = None if not a and os.listdir("."): error("Can't find kernel version for non-empty directory") sys.exit(-1) b = find_ver(args[0]) qprint("%s -> %s" % (a, b)) transform(a, b) if options["rename-directory"] and not options["dry-run"]: rename_dir(b) if postcommand and os.system(postcommand): sys.exit('Postcommand "%s" failed!' % postcommand)