#!/usr/bin/env python # # Script to check for new packages in the incoming directory and process # them. # # Checks for .dsc files, verifies all files they reference and complete # and that their size/md5sum matches. If so they are moved to the build # directory and a build is spawned for them. # # Care is taken to ensure only one package is built at a time. # # This script must be run as the sbuild user # # This script is licensed under the GPLv2 # # Author: Matt Brown # Version: $Id: process_packages 10 2006-06-20 00:56:09Z mglb1 $ import os, os.path, sys import syslog import re import smtplib import shutil INCOMING_DIR = "/home/sbuild/incoming" BUILD_DIR = "/home/sbuild/build" REPO_BASE_DIR = "/home/httpd/packages.crc.net.nz/repository" DISTS = [ "crcnet-bpc", "crcnet", "woody" ] ############################################################################### # FUNCTIONS ############################################################################## def dscerror(maintainer, basedir, dscfile, error): """Generates an error about a DSC file""" syslog.syslog(syslog.LOG_NOTICE, "%s. Processing of %s aborted!" % \ (error, dscfile)) # Get maintainer email from string p = maintainer.split("<") if len(p) > 1: p2 = p[1].split(">") email = p2[0] else: email = "" msg = """From: CRCnet Build Daemon To: %s Subject: Invalid DSC File: %s Hi, This is a notification from the CRCnet Build Daemon to tell you that you have placed an invalid .dsc file in the incoming directory. Invalid File: %s Error: %s This dsc file will not be processed and has been disabled. Please try your upload again. If you have further problems please contact Matt Brown. """ % (email, dscfile, dscfile, error) server = smtplib.SMTP('localhost') server.sendmail("sbuild@localhost", ["root@localhost",email], msg) server.quit() # Move the file os.rename("%s/%s" % (basedir, dscfile), \ "%s/%s.invalid" % (basedir, dscfile)) def package_added(changedby, package, changes, dist): """Generates an install notice for a maintainer""" # Get maintainer email from string p = changedby.split("<") if len(p) > 1: p2 = p[1].split(">") email = p2[0] else: email = "" msg = """From: CRCnet Archive Daemon To: %s Subject: %s ACCEPTED into %s testing archive %s """ % (email, package, dist, changes) server = smtplib.SMTP('localhost') server.sendmail("sbuild@localhost", ["root@localhost",email], msg) server.quit() def process_dsc(basedir, filename): """Processes a dsc file""" dscfile = open("%s/%s" % (basedir, filename), "r") lines = dscfile.readlines() mode=1 files = [] maintainer = "" for line in lines: if line.startswith("Maintainer:"): maintainer = line if line.startswith("Files:"): mode=2 continue if mode !=2: continue # Check for blank line - end of files if line.strip() == "": break # Check for the existance of a file parts = line.strip().split(" ") fname = "%s/%s" % (basedir, parts[2]) if not os.path.exists(fname): dscerror(maintainer, basedir, filename, \ "Missing file '%s'" % parts[2]) return -1 # Check md5sum md5sum = getmd5(fname) if md5sum != parts[0]: dscerror(maintainer, basedir, filename, \ "Invalid checksum on '%s'" % parts[2]) return -1 # OK files.append(parts[2]) # All files are OK syslog.syslog(syslog.LOG_NOTICE, "%s accepted for processing" % \ filename) move2builddir(basedir, filename, files) init_build(filename, dist, files) return 0 def getmd5(filename): fp = os.popen("/usr/bin/md5sum %s" % filename, "r") sum = fp.readline() fp.close() parts = sum.split(" ") return parts[0] def move2builddir(basedir, dscfile, files): shutil.copy("%s/%s" % (basedir, dscfile), \ "%s/%s" % (BUILD_DIR, dscfile)) os.unlink("%s/%s" % (basedir, dscfile)) for file in files: shutil.copy("%s/%s" % (basedir, file), \ "%s/%s" % (BUILD_DIR, file)) os.unlink("%s/%s" % (basedir, file)) def init_build(dscfile, dist, files): res = os.fork() if res==0: # Child os.chdir(BUILD_DIR) os.execlp("sbuild", "sbuild", "-A", "-s", "--include-source-orig", \ "-p", "always", "-d", dist, dscfile) else: # Parent fp = open("%s/.buildlock-%s" % (BUILD_DIR, dist), "w") fp.write("%s\n" % dscfile) fp.write("%d\n" % res) fp.close() syslog.syslog(syslog.LOG_NOTICE, "Build of %s spawned. " \ "Pid %d. Distribution %s." % (dscfile, res, dist)) # Wait for the build to finish (pid, rv) = os.waitpid(res, 0) if rv != 0: syslog.syslog(syslog.LOG_NOTICE, "Build of %s appears to " \ " have failed. Cleaning up." % dscfile) for file in files: os.unlink("%s/%s" % (BUILD_DIR, file)) else: syslog.syslog(syslog.LOG_NOTICE, "Build of %s completed " \ "successfully." % dscfile) add_package(dscfile, dist) # Remove the lock os.unlink("%s/.buildlock-%s" % (BUILD_DIR, dist)) def add_package(dscfile, dist): """Inserts a package that has successfully built into the repository""" # Calculate what the changes filename should be changes = "%s_i386.changes" % dscfile[:-4] if not os.path.exists("%s/%s" % (BUILD_DIR, changes)): syslog.syslog(syslog.LOG_ERR, "Cannot find changes file (%s) " \ "for %s!" % (changes, dscfile)) return # Call reprepro to insert the package into the repository fp = os.popen("reprepro -b %s include %s-testing %s/%s 2>&1" % \ (REPO_BASE_DIR, dist, BUILD_DIR, changes)) lines = fp.readlines() rv = fp.close() if rv != None: syslog.syslog(syslog.LOG_ERR, "Failed to add %s!" % dscfile) syslog.syslog(syslog.LOG_DEBUG, "".join(lines)) return # Added OK, parse changes to get list of files to delete and person # to email changesfile = open("%s/%s" % (BUILD_DIR, changes), "r") lines = changesfile.readlines() changesfile.close() mode=1 changedby = "" for line in lines: if line.startswith("Changed-By:"): changedby = line if line.startswith("Files:"): mode=2 continue if mode !=2: continue # Check for blank line - end of files if line.strip() == "": break # Check for the existance of a file parts = line.strip().split(" ") fname = "%s/%s" % (BUILD_DIR, parts[4]) os.unlink(fname) os.unlink("%s/%s" % (BUILD_DIR, changes)) # Send email parts = dscfile.split("_") package_added(changedby, parts[0], "".join(lines), dist) def is_build_running(dist): """Checks if the specified distribution is busy building or not""" lockfile = "%s/.buildlock-%s" % (BUILD_DIR, dist) # If a build is currently running do nothing if os.path.exists(lockfile): # Check if the build is still in progress fp = open(lockfile, "r") lines = fp.readlines() if len(lines) != 2: syslog.syslog(syslog.LOG_NOTICE, "Invalid lockfile - %s! " \ "Removing" % dist) os.unlink(lockfile) return 0 alive=0 try: os.kill(int(lines[1]), 0) alive=1 except: pass if alive==1: syslog.syslog(syslog.LOG_NOTICE, "Build of %s (%s) still in " \ "progress. Aborting run" % (lines[0], dist)) return -1 else: syslog.syslog(syslog.LOG_NOTICE, "Build of %s (%s) appears to " \ "have completed. Removing lockfile." % (lines[0], dist)) os.unlink(lockfile) return 0 def process_dist(dist): incoming_dir = "%s/%s" % (INCOMING_DIR, dist) if is_build_running(dist) < 0: return #syslog.syslog(syslog.LOG_NOTICE, "Scanning %s" % incoming_dir) dscpat = re.compile(".*\\.dsc$") # Process all files in the incoming directory files = os.listdir(incoming_dir) c=0 for file in files: # Skip files not matching the .dsc pattern m = dscpat.match(file) if m == None: continue # Found a dsc file, process it c+=1 if process_dsc(incoming_dir, file) >= 0: break #syslog.syslog(syslog.LOG_NOTICE, "Run completed for %s. %d dsc files " \ # "processed." % (dist, c)) ############################################################################### # MAINLINE ############################################################################### # Setup logging syslog.openlog("process-packages", 0, syslog.LOG_DAEMON) # Process each distribution in turn for dist in DISTS: process_dist(dist) # vim: ts=4 sts=4 sw=4 et