#!/usr/bin/python -t # -*- mode: Python; indent-tabs-mode: nil; -*- # # 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 import errno import fcntl, fnmatch import os, sys import re import shutil import string import tempfile import time import rpmUtils, rpmUtils.miscutils, rpmUtils.transaction import Utils, MultiLib, Comps, WhatsNew, BlackList import RepoBuild, RepoPrune, RepoView try: from plague.LockFile import LockFile, LockFileLocked except: from LockFile import LockFile, LockFileLocked from BuildSys import LocalPlague from BuildReport import * # Lockfile name supported by plague for mutual exclusion during # access to needsign repository (i.e. 'stagesdir'). REPO_LOCKFILE_NAME = '.repo-update.lock' class opts: cont_mode = False force = False ts = rpmUtils.transaction.initReadOnlyTransaction() # Further globals: cfg # ==================================================================== def getexcludearch(pkgpath,srpmspath): """Returns list of excluded archs for a binary rpm. Needs access to the repository root directory where it examines the source rpm.""" global ts hdr = rpmUtils.miscutils.hdrFromPackage(ts,pkgpath) source_rpm = hdr['sourcerpm'] srpmloc = os.path.join( srpmspath, source_rpm) hdr = rpmUtils.miscutils.hdrFromPackage(ts,srpmloc) return hdr['excludearch'] def sign_pkgs(filelist,cmd=None): """gpg sign all the rpms""" numfiles = len(filelist) if numfiles < 1: print "No packages to sign" return 0 while numfiles > 0: if numfiles > 256: files = filelist[:256] del filelist[:256] else: files = filelist filelist = [] foo = string.join(files) if not cmd: cmd = 'rpm --define "_gpg_name %s" --resign' % cfg.signkeyname result = os.system( 'echo %s | xargs %s' % (foo,cmd) ) if result != 0: return result numfiles = len(filelist) return 0 class PushWarning(Exception): pass class PushFatal(Exception): pass def push(br,dist,destroot,buildreport): """push the files found within a single package build-results directory""" rollback = [] try: push_with_rollback(rollback,br,dist,destroot,buildreport) except: print 'Rollback:', rollback for f in rollback: try: os.remove(f) except: pass raise def push_with_rollback(rollback,br,dist,destroot,buildreport): print ' ', br # go through each package and move it to the right arch location # if it is a src.rpm package, move it into the SRPMS location # if it is a noarch package, copy2 it to all arch locations # if it is a debuginfo package, move it into the 'debug' dir for that arch if len(br.filedict['srpm']) != 1: br.MarkPushed() bi = BuildInfo(br.__str__(),'INVALID build results, not published!',False,True) buildreport.Add(bi) raise PushWarning, 'WARNING: %d source rpms in %s' % (len(br.filedict['srpm']),br.GetHome()) srpmspath = Utils.srpm_repodir(cfg,dist,destroot) package = br.filedict['srpm'][0] (n,a,e,v,r) = Utils.naevr(package) summary = Utils.get_pkg_header(package)['summary'].rstrip() pkg_fn = os.path.basename(package) destloc = os.path.join(srpmspath, pkg_fn) if not os.path.exists(destloc): rollback.append(destloc) Utils.install_move(package,destloc) if WhatsNew.queryname(dist,n): buildreportinfo = BuildInfo('%s-%s-%s' % (n,v,r)) else: buildreportinfo = BuildInfo('%s-%s-%s' % (n,v,r), summary, True) else: # src.rpm published before, exclude entire build job br.MarkPushed() bi = BuildInfo(br.__str__(),'INVALID rebuild, not published!',False,True) buildreport.Add(bi) raise PushWarning, 'WARNING: %s published before' % pkg_fn if len(br.filedict['rpm'])==0: # no builds (2008-04-16 with amarok-1.4.9.1-1.el5) br.MarkPushed() bi = BuildInfo(br.__str__(),'INVALID build results, not published!',False,True) buildreport.Add(bi) raise PushWarning, 'WARNING: no built rpms in %s' % br.GetHome() for package in br.filedict['rpm'] + br.filedict['debuginfo']: pkg_fn = os.path.basename(package) (n,a,e,v,r) = Utils.naevr(package) if a == 'noarch': excludearch = getexcludearch(package,srpmspath) linkloc = None for basearch in cfg.archdict[dist]: if basearch in excludearch: print 'EXCLUDEARCH: Not releasing %s for %s.' % (pkg_fn,basearch) continue # with next basearch destloc = os.path.join( Utils.rpm_repodir(cfg,dist,basearch,destroot), pkg_fn ) rollback.append(destloc) if linkloc: Utils.install_link_or_copy(linkloc,destloc) else: Utils.install_copy(package,destloc) linkloc = destloc continue # with next package # rpmUtils.arch.getBaseArch(a) elif a in ['i386', 'i486', 'i586', 'i686', 'athlon']: basearch = 'i386' elif a in ['x86_64', 'ia32e', 'amd64']: basearch = 'x86_64' elif a in ['ppc', 'ppc32']: basearch = 'ppc' elif a in ['ppc64']: basearch = 'ppc64' else: print 'Unknown arch %s' % a continue # with next package if package in br.filedict['debuginfo']: destloc = os.path.join( Utils.debug_repodir(cfg,dist,basearch,destroot), pkg_fn ) else: destloc = os.path.join( Utils.rpm_repodir(cfg,dist,basearch,destroot), pkg_fn ) rollback.append(destloc) Utils.install_move(package,destloc) # Mark successfully signed packages as PUSHED. br.MarkPushed() del rollback[:] buildreport.Add(buildreportinfo) def copy_sign_move(dist,whitelist=['.*']): """Copies, signs and moves packages for the given dist. Returns (errcode, changedrepo) where errcode is an error code and changedrepo is either the dist id of the target repo that was changed or None.""" changedrepo = None srcdist = dist # the plague-results dist number # When we're in updates-testing override mode, adjust the "srcdist" # parameter to point to the single plague-results directory, and # reject pushing to non-testing repositories. if hasattr(cfg.opts,'pushtotesting') and cfg.opts.pushtotesting: if dist.startswith('testing/'): srcdist = dist.replace('testing/','') elif dist == 'development': pass else: print "Operating in push-to-testing mode, but dist '%s' doesn't start with 'testing/', skipping copy+sign+move." % dist return 0, changedrepo distdir = '%s-%s-%s' % (cfg.distro, srcdist, cfg.project) needsignroot = os.path.join(cfg.stagesdir, distdir) destroot = Utils.get_reporoot(cfg,dist) print '='*70 print 'Target repository:', destroot print '='*70 Utils.make_std_repodirs(cfg,dist) repolockname = os.path.join(needsignroot,REPO_LOCKFILE_NAME) if not os.path.exists(os.path.dirname(repolockname)): os.makedirs(os.path.dirname(repolockname)) locktype = 'flock' if hasattr(cfg.opts,'locktype'): locktype = cfg.opts.locktype repolock = LockFile(name=repolockname,blocking=True,type=locktype) try: print 'Waiting for repository lock on %s:' % needsignroot, sys.stdout.flush() repolock.lock() print 'OK' except IOError, (err, strerr): print "\nERROR: lockfile %s failure: %s (error %d)" % (repolockname, strerr, err) sys.exit(err) bs = LocalPlague(needsignroot, cfg.createrepo) bs.PruneBuildResults() results = bs.GetBuildResults() results = BlackList.get_filtered_build_results(cfg,srcdist,results) results = apply_whitelist(results,whitelist) print 'Package directories found: %d' % len(results) if len(results) == 0: return 0, changedrepo print 'Copying build results to temporary working directory:' signtmpdir = tempfile.mkdtemp('','.push',cfg.treedir) if signtmpdir == cfg.treedir: # paranoid, should never happen sys.exit(errno.EPERM) try: for br in results: print ' ', br newhome = os.path.join(signtmpdir,br.__str__()) shutil.copytree(br.GetHome(),newhome) br.SetHome(newhome) except: # everything is fatal print 'ERROR: Creating temporary working copy failed.' shutil.rmtree(signtmpdir) raise # Now we have a temporary copy of everything from the needsign repository # we want. repolock.unlock() try: WhatsNew.load(cfg.rundir) print "Signing Packages:" signcmd = None # use the old hardcoded default if hasattr(cfg,'rpmsigncmds'): if dist in cfg.rpmsigncmds.keys(): signcmd = cfg.rpmsigncmds[dist] elif 'DEFAULT' in cfg.rpmsigncmds.keys(): signcmd = cfg.rpmsigncmds['DEFAULT'] while (True): rv = sign_pkgs( Utils.find_files(signtmpdir,'*.rpm'), cmd=signcmd ) if not rv: break while (True): print 'Retry? (y/n)', a = raw_input().lower() if a=='n': raise PushFatal if a=='y': break print "Copying packages into repositories:" for br in results: name = br.GetName() clogdiff = WhatsNew.getclogdiff(dist,name,br.GetSourcePkg(),ts) newclog = WhatsNew.readclog(br.GetSourcePkg(),ts) if hasattr(cfg,'diversion_dict') and name in cfg.diversion_dict: # install this elsewhere? buildreport = NoBuildReportManager() targetroot = os.path.join(cfg.diversion_dict[name],dist) if os.path.exists(targetroot): shutil.rmtree(targetroot) Utils.make_std_repodirs(cfg,dist,targetroot) else: buildreport = BuildReportManager(cfg.rundir,dist) targetroot = destroot changedrepo = dist try: push(br,dist,targetroot,buildreport) # TODO: report these via mail (or so) except PushWarning, e: print e except rpmUtils.RpmUtilsError, e: print e WhatsNew.putname(dist,name) WhatsNew.putclog(dist,name,newclog) WhatsNew.putclogdiff(dist,br.__str__(),clogdiff) WhatsNew.save(cfg.rundir) except PushFatal: shutil.rmtree(signtmpdir) sys.exit(1) except: shutil.rmtree(signtmpdir) raise print('Removing tmp tree: %s' % signtmpdir) shutil.rmtree(signtmpdir) return 0, changedrepo def apply_whitelist(results, whitelist): newresults = [] for br in results: for r in whitelist: if re.compile('^'+r+'$').search(br.GetName()): newresults.append(br) break return newresults # ==================================================================== import datetime import smtplib from email.MIMEText import MIMEText import email.Charset def email_list(distlist): """email mailing list with the new package listing""" body = '' for dist in cfg.alldists: # we do this for sorting the dists if not dist in distlist: continue brm = BuildReportManager(cfg.rundir,dist) count = brm.GetCount() if not count: continue body += "\n"+'='*76+"\nPackages built and released for %s %s: %s\n\n" % (cfg.project_hr, dist, count) body += brm.GetReport() body += '\n\n' for dist in cfg.alldists: # we do this for sorting the dists if not dist in distlist: continue brm = BuildReportManager(cfg.rundir,dist) count = brm.GetCount() if not count: continue body += "\n"+'='*76+"\nChanges in %s %s: \n\n" % (cfg.project_hr, dist) try: WhatsNew.load(cfg.rundir) body += '\n'+WhatsNew.getallclogdiffs(dist) except: pass if not len(body): print 'Empty build report. Nothing sent.' return body += cfg.mail_footer email.Charset.add_charset('utf-8',email.Charset.SHORTEST,None,None) msg = MIMEText(body,'plain','utf-8') subject = '%s Package Build Report %s' % (cfg.project_hr,datetime.date.today()) msg['Subject'] = subject msg['From'] = cfg.mail_from msg['To'] = cfg.mail_to s = smtplib.SMTP() if cfg.smtp_server: s.connect(cfg.smtp_server) else: s.connect() s.sendmail(cfg.mail_from, [cfg.mail_to], msg.as_string()) s.close() # Build report has been mailed, we can delete the run-files. for dist in distlist: b = BuildReportManager(cfg.rundir,dist) b.Clear() # ==================================================================== def usage(): print 'Usage: %s [release]...\n' % os.path.basename(sys.argv[0]) sys.exit(errno.EINVAL) if __name__ == '__main__': if len(sys.argv) < 3: usage() cfg = Utils.load_config_module(sys.argv[1]) os.umask(cfg.signersumask) Utils.signer_gid_check(cfg.signersgid) if cfg.opts.signkeycheck: Utils.sign_key_check_all(cfg) if '-c' in sys.argv[2:]: sys.argv.remove('-c') opts.cont_mode = True # don't push new packages if '-f' in sys.argv[2:]: sys.argv.remove('-f') opts.force = True if '-s' in sys.argv[2:]: sys.argv.remove('-s') cfg.post_cmds = [] cfg.opts.repoprune = False cfg.opts.repoview = False diststopush = [] for d in sys.argv[2:]: if d == 'all': diststopush = cfg.alldists continue if d not in cfg.alldists: print "ERROR: No Distribution named '%s' found" % d sys.exit(errno.EINVAL) if d not in diststopush: diststopush.append(d) if not len(diststopush): usage() if not os.path.exists(cfg.rundir): os.makedirs(cfg.rundir) lockfile = os.path.join(cfg.rundir,'pushscript.lock') lock = LockFile(lockfile) try: lock.lock() except LockFileLocked: print 'Script locked via %s\nIt seems to be in use already.' % lockfile sys.exit(errno.EPERM) if hasattr(cfg,'comps_up_pass1') and cfg.comps_up_pass1: print 'Processing comps.xml updates (Ctrl+C to skip)' sys.stdout.flush() Comps.main(cfg) changed = [] # dists with changes in them mustfinish = [] # dists with pending build report totalchanges = 0 for dist in diststopush: if opts.cont_mode: # only complete previously pushed pkgs whitelist = [] # none else: whitelist = ['.*'] # all (regexp) if dist in cfg.frozendists: whitelist = [] # Check for external changes to repository. if Utils.is_any_repo_changed(cfg,dist) and not dist in changed: print "Change detected:", dist changed.append(dist) # Release build jobs from needsign queue. result, changedrepo = copy_sign_move(dist,whitelist) if result: sys.exit(result) if changedrepo and changedrepo not in changed: changed.append(changedrepo) brm = BuildReportManager(cfg.rundir,dist) buildjobs = brm.GetCount() totalchanges += buildjobs if buildjobs: mustfinish.append(dist) print 'Total number of pushed build jobs to report: %d' % totalchanges # Option -f re-runs repobuild/repoview for all dists to push, # even if no new packages have been pushed for a dist. if opts.force: changed = diststopush for dist in changed: if cfg.opts.repoprune: RepoPrune.main(cfg,dist) RepoBuild.main(cfg,dist) MultiLib.main(cfg,changed) for dist in changed: if cfg.opts.repoview: RepoView.main(cfg,dist) for dist in diststopush: if not RepoBuild.SignRepoMD(cfg,dist): print 'ERROR: Could not GPG sign repomd.xml files!' lock.unlock() sys.exit(1) if changed or mustfinish: Utils.run_and_check( getattr(cfg,'sync_cmd','') ) if cfg.opts.mail: email_list(diststopush) for d in diststopush: WhatsNew.clearclogdiffs(cfg.rundir,d) for cmd in cfg.post_cmds: Utils.run_and_check(cmd) lock.unlock() sys.exit(0)