#!/usr/bin/python # # GKB - GNU Kernel Builder # Copyright (C) 2003-2004 Tobias McNulty, Mark Guertin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Ported from PHP version Copyright (c) Tobias McNulty 2000-2003 import sys, os, ConfigParser, popen2 import xml.parsers.expat import md5, string from ftplib import FTP from commands import getoutput from shutil import copy, move from string import split from urllib2 import urlopen from ClientForm import ParseResponse debug=0 clean=1 dont_build=0 verbose=1 # parse host-specific configuration information from gkb.cfg config = ConfigParser.ConfigParser() config.readfp(open('gkb-host.cfg')) buildroot=config.get("options","buildroot") host=config.get("options","host") passwd=config.get("options","passwd") # this one is passed verbatim before the make command. might be useful for alternate toolchains, etc # example: premake="HOST=powerpc-unknown-linux-gnu" premake=config.get("options","premake") # make options, passed verbatim after make command makeopts=config.get("options","makeopts") # parse manager-related configuration from gkb-manager.cfg config.readfp(open('gkb-manager.cfg')) msite=config.get('options','msite') # these values will likely stay fairly static across different builds patchdir="%s/patches" % buildroot # full path to patch file dir configdir="%s/configs" % buildroot # path to where configs are to be stored logdir="%s/logs" % buildroot # the logs will go here bindir="%s/bin" % buildroot # the directory that binaries built will be sent for pkging workdir="%s/work" % buildroot # work directory, where builds take place # setup the global dicts to hold build data, imported from xml mastertrees={} builds={} class Error(Exception): """Base class for exceptions in this module.""" pass class BuildError(Error): """Exception raised for build-level errors. Attributes: message -- explanation of the error """ def __init__(self, message): self.message = message class FatalError(Error): """Exception for fatal errors that prevent program continuation. Attributes: message -- explanation of why the specific transition is not allowed """ def __init__(self, message): self.message = message def printverbose(myoutput): """Supporting method to output info to stdout if verbosity level is set""" if verbose==1: print myoutput def clean(text): """clean a string of passwords, etc. to make it suitable for logging""" result = string.replace(text,passwd,"XXXXXXXX") return result def linkpipes(input, output): """link two pipes together, writing all the data from input to output""" data = input.read(2048) while data: output.write(data) data = input.read(2048) def runcmd(cmd, kinfo=None, outfile=None, append=False, infile=None): """run the specified command, with the optional input and output documents""" log("running " + clean(cmd), kinfo) p4obj = popen2.Popen4(cmd) pin = p4obj.tochild pouterr = p4obj.fromchild if infile != None: inobj = open(infile, "r") linkpipes(inobj, pin) inobj.close() if outfile != None: if append: mode = "a" else: mode = "w" outobj = open(outfile, mode) linkpipes(pouterr,outobj) outobj.close() err = p4obj.wait() if err == None: err = 0 log("result code: %i" % err, kinfo) return err def log(text,kinfo=0): if kinfo: printverbose(kinfo["name"] + " : " + text) else: printverbose("gkb : " + text) def verifydir(mydir,kinfo=0): """Supporting method to verify a given exists, if not it will create it""" log("verifying directory " + mydir,kinfo) if not os.path.isdir(mydir): log("%s doesn't exist, creating" % mydir,kinfo) os.mkdir(mydir) return mydir def chdir(mydir,kinfo): verifydir(mydir,kinfo) log("entering "+mydir,kinfo) os.chdir(mydir) def verifyfile(myfile,kinfo): """Supporting method to verify file exists, if not it will exit with error""" if not os.path.isfile(myfile): raise BuildError("cannot find file expected at %s, exiting" % myfile) return myfile def md5sum(fileobj): """calculates the md5sum for fileobj and returns it in hexadecimal""" md = md5.new() data = fileobj.read(8192) while (data): md.update(data) data = fileobj.read(8192) digest = md.hexdigest() return digest def krn_querymgr(command,kinfo): """queries the build host manager with a variety of commands, such as checkout, checkin, etc.""" version=krn_localversion(kinfo) log("querying distribution site manager with command '%s' (kernel version=%s)" % (command,version),kinfo) result=getoutput("wget --quiet --output-document=- \"%s/manager.php?cmd=%s&host=%s&pass=%s&build=%s&version=%s\"" % (msite,command,host,passwd,kinfo["name"],version)) log("result: '"+result+"'",kinfo) return result=="1" def krn_localversion(kinfo): """Checks the version of the local source tree specified in kinfo""" version=getoutput("""awk -F '=' '/^VERSION/{v=$2} /^PATCHLEVEL/{p=$2} /^SUBLEVEL/{s=$2} /^EXTRAVERSION/{e=$2} END { printf("%s.%s.%s%s\\n", v, p, s, e) }' """ + kinfo["workdir"] + """/Makefile | sed "s/ //g" """) return version def gkb_build(root,kinfo): """performs actual kernel builds given root, kinfo (tuple containing relevant data) , premake and makeopts""" # make sure needed directoriess (for this build) exist, if not create them verifydir("%s/%s" % (logdir, kinfo["name"]),kinfo) verifydir("%s/%s" % (patchdir, kinfo["name"]),kinfo) # sync the source to make sure we are up to date ... log("calling gkb_getsource",kinfo) gkb_getsource(kinfo) # go into the work directory chdir(workdir, kinfo) # archive the clean source for later uploading log("archiving source to " + kinfo["mastertree"] + ".tar.bz2", kinfo) runcmd("tar cjf " + kinfo["mastertree"] + ".tar.bz2 " + kinfo["mastertree"], kinfo) # now we'll go into the tree's work dir chdir(kinfo["workdir"],kinfo) # check for patches and apply if necessary gkb_patch(kinfo) if kinfo["type"] == "kernel24": krn_build24(kinfo) elif kinfo["type"] == "kernel26": krn_build24(kinfo) #run kernel24 for now, change later # Gerk comment: # Do we need to change this with 24/26? we probably don't ... I'd rather see us define the list of make targets # i.e. "dep","clean","vmlinux","modules","modules_install" or "clean","all","modules_install" for 2.6 # also of note, make all is not what we will always want with 2.6 kernels ... i.e. zImage.prep, zImage.chrp, etc # also for backward compat they will continue to support calling things individual .. the makefile just does # this for us with 'all' def krn_build24(kinfo): """build a kernel, version 2.4.x""" if krn_querymgr("checkout",kinfo): try: myversion=krn_localversion(kinfo) # fetch and cp the config file to work/.config krn_config(kinfo) # **Note** : we set the preprocessing command to premake inline instead # of globally as it is only needed in this target if dont_build==0: gkb_runmake("oldconfig",kinfo,premake+" /bin/cat %s/newlines | " % buildroot,makeopts) # give option to only repackage for testing purposes, comment out clean=1 at top of this file to use this feature if (clean==1 and dont_build==0): gkb_runmake("clean", kinfo, premake, makeopts) if dont_build==0: gkb_runmake("dep", kinfo, premake, makeopts) gkb_runmake(kinfo["binname"], kinfo, premake, makeopts) # We should check to see if binary built ok, if not bail out mybindir=bindir+"/linux-"+kinfo["name"]+"-"+myversion verifydir(mybindir,kinfo) verifydir(mybindir+"/boot",kinfo) kbinloc=kinfo["workdir"]+"/"+kinfo["binpath"]+"/"+kinfo["binname"] if verifyfile(kbinloc,kinfo): # the binary exists, so let's cp it to bin... copy(kbinloc,mybindir+"/boot/"+kinfo["binname"]+"-"+myversion) else: # the binary is not there, inform user and bail out with error raise BuildError("%s is not present, assuming build failure and exiting. See log for details." % kbinloc) # now that we know he binary built, let's continue if dont_build==0: gkb_runmake("modules",kinfo, premake, makeopts) # **Note** : we prepend the INSTALL_MOD_PATH to the makeopts inline instead # of globally as it is only needed in this target gkb_runmake("modules_install", kinfo, premake, "INSTALL_MOD_PATH=%s %s" % (mybindir, makeopts)) # compress and upload source archive chdir(kinfo["workdir"]+"/..",kinfo) archive_name = "src-%s.tar.bz2" % kinfo["mastertree"], # compress and upload kernel binary chdir(bindir,kinfo) archive_name = "linux-%s-%s.tar.bz2" % (kinfo["name"], myversion) log("compressing binary archive "+archive_name,kinfo) if runcmd("tar cjf "+archive_name+" "+os.path.basename(mybindir), kinfo): raise BuildError("failed to `tar cjf %s`" % archive_name) runcmd("rm -rf %s" % mybindir, kinfo) krn_upload(archive_name,"kernel",myversion,kinfo) krn_querymgr("checkin",kinfo) if (os.fork() == 0): #in child krn_upload(workdir + "/" + kinfo["mastertree"] + ".tar.bz2", "source", myversion, kinfo) except BuildError, e: log(e.message, kinfo) krn_querymgr("checkin",kinfo) except: krn_querymgr("checkin",kinfo) raise def krn_build26(kinfo): """build a kernel, version 2.6""" # source get routines def gkb_getsource(kinfo): """method to perform source sync on demand, currenty only supports rsync, but others can be added""" method=mastertrees[kinfo["mastertree"]]["method"] if method == "rsync": get_rsync(kinfo) elif method == "wget": get_wget(kinfo) elif method == "vanilla": get_vanilla(kinfo) def get_rsync(kinfo): """sync the source using rsync""" args=mastertrees[kinfo["mastertree"]]["args"] if clean==1: syncoptions="rsync -azv --delete" else: syncoptions="rsync -azv" syncline=args+" "+kinfo["workdir"] log("running rsync : %s %s" % (syncoptions, syncline),kinfo) if runcmd("%s %s" % (syncoptions, syncline), kinfo, "%s/%s/rsync.log" % (logdir, kinfo["name"])): raise BuildError("sync failed, tried %s. See log for details." % syncline) def get_wget(kinfo): #needs work """sync the source using wget and a tar.bz2""" args=mastertrees[kinfo["mastertree"]]["args"] log("fetching source archive " % args,kinfo) mysourcefile="%s/%s.tar.bz2" % (kinfo["workdir"],kinfo["name"]) if runcmd("wget --quiet --output-document=%s %s/configs/%s" % (myconfigfile,msite,kinfo["name"]), kinfo): raise BuildError("unable to download configfile, aborting.") log("decompressing source file",kinfo) if runcmd("tar xjf " % (myconfigfile,msite,kinfo["name"]), kinfo): raise BuildError("unable to decompress source file, aborting.") def get_vanilla(kinfo): args=mastertrees[kinfo["mastertree"]]["args"] log("fetching source archive %s" % args) mysourcefile="%s/%s.tar.bz2" % (workdir,kinfo["name"]) if runcmd("wget -c --output-document=%s %s" % (mysourcefile,args), kinfo): raise BuildError("unable to download source file %s, aborting." % args) log("decompressing source file %s" % mysourcefile,kinfo) #save the current working ectory cwd=getoutput("pwd") chdir(workdir,kinfo) pfd=os.popen("tar vxjf %s" % mysourcefile,"r") trash=data=pfd.read(255) while trash: trash=pfd.read(4096) if pfd.close(): raise BuildError("unable to decompress source file, aborting.") fnames=split(data) name=fnames[0] runcmd("rm -rf %s" % kinfo["workdir"], kinfo) move(work + "/" + dirname, kinfo["workdir"]) #restore the previous working ectory chdir(cwd,kinfo) def gkb_patch(kinfo): """method that applies patch found in kinfo config files for running kernel build""" if kinfo["patches"]: patches=split(kinfo["patches"],";") for patch in patches: log("downloading patch %s" % patch,kinfo) #set a name for this patch file mypatchfile="%s/%s/%s.patch" % (patchdir,kinfo["name"],patch) # download pathfile or bail if runcmd("wget --quiet --output-document=%s %s/patches/%s/%s" % (mypatchfile,msite,kinfo["name"],patch), kinfo): raise BuildError("unable to download patchfile %s, aborting." % patch) # we have a patch file, apply it or bail log("perfoming patch with %s" % patch,kinfo) # former patchcommand was: #patchcommand="patch -p1 < %s > %s/%s/patch-%s.log 2>&1" % (mypatchfile,logdir,kinfo["name"],patch) #log("using %s from %s" % (patchcommand,kinfo["workdir"]),kinfo) chdir(kinfo["workdir"],kinfo) if runcmd("patch -p1", kinfo, "%s/%s/patch-%s.log" % (logdir,kinfo["name"],patch), False, mypatchfile): raise BuildError("patchfile %s failed, aborting. See patch log for details." % patch) else: log("no patchfiles, continuing",kinfo) def krn_config(kinfo): """method to fetch and place config file for running kernel build""" if kinfo["config"]==1: log("fetching config file",kinfo) myconfigfile="%s/%s.config" % (configdir,kinfo["name"]) if runcmd("wget --quiet --output-document=%s %s/configs/%s" % (myconfigfile,msite,kinfo["name"]), kinfo): raise BuildError("unable to download configfile, aborting.") log("copying config file to %s/.config" % kinfo["workdir"],kinfo) copy(verifyfile(myconfigfile,kinfo),"%s/.config" % kinfo["workdir"]) # Methods to support build() def gkb_runmake(command, kinfo, premake, makeopts): """Supporting method for build() ... a stub to run a make target and auto log it, given make target (command) and name (kernel name)""" log("running make %s" % command,kinfo) if runcmd("%s make %s %s" % (premake, makeopts, command), kinfo, "%s/%s/make-%s.log" % (logdir, kinfo["name"], command)): raise BuildError("unable to run make %s, aborting." % command) def gkb_parsexml(name): """parse the xml build file into mastertrees and builds""" def start_element(name, attrs): #print "In start_element: %s %s" % (name, attrs) if name=="mastertree": mastertrees[attrs["name"]]=attrs elif name=="build": builds[attrs["name"]]=attrs p = xml.parsers.expat.ParserCreate() p.StartElementHandler = start_element p.ParseFile(open(name)) def krn_upload(file, type, version, kinfo): """upload the indicated file to the distribution site (kernel archives)""" if type=="kernel": log("uploading kernel version "+version+" to "+msite,kinfo) elif type=="source": log("uploading source "+kinfo["mastertree"]+" to "+msite,kinfo) else: raise BuildException, "invalid file upload type: " + type forms = ParseResponse(urlopen(msite+"/upload.html")) form = forms[0] form["host"] = host form["pass"] = passwd form["type"] = type form["tree"] = kinfo["mastertree"] form["build"] = kinfo["name"] form["version"] = version form.add_file(open(file), "application/x-bzip2", os.path.basename(file)) # form.click() returns a urllib2.Request object # (see HTMLForm.click.__doc__ if you don't have urllib2) response = urlopen(form.click("cmd")) if debug: print response.geturl() print response.info() # headers print response.read() # body log("response: " + response.read(),kinfo) response.close() def main(): """ main program gets executed here """ # get our working ectory root=getoutput("pwd") today=getoutput("date +%D") buildtime=getoutput("date +'%R:%S %Z'") print "GKB started %s %s" % (today,buildtime) # verify the existence important directories, and create if necessary verifydir(logdir) verifydir(configdir) verifydir(patchdir) verifydir(bindir) verifydir("%s/work" % buildroot) # build dir, make sure it exists # download the build jobs from the master site if runcmd("wget --quiet --output-document=gkb.xml \"%s/manager.php?cmd=getjobs&host=%s&pass=%s\"" % (msite,host,passwd)): raise FatalError, "Unable to download build config from master site %s, aborting." % msite # sets up 'mastertrees' and 'builds' dicts gkb_parsexml('gkb.xml') for bdict in builds.values(): myworkdir="%s/%s" % (workdir,bdict["mastertree"]) bdict["workdir"]=myworkdir try: # for now just call the build gkb_build(root, bdict) except BuildError, e: log(e.message, bdict) except KeyboardInterrupt, e: log("Caught keyboard interrupt, exiting...") break endtime=getoutput("date +'%R:%S %Z'") print "GKB finished %s %s" % (today,endtime) # and finally, call the mainloop to execute main()