# -*- python -*- # ex: set syntax=python: import json import os def _path(name): return os.path.join(os.path.dirname(__file__), name) # This is a buildmaster config file. It must be installed as # 'master.cfg' in your buildmaster's base directory. # This is the dictionary that the buildmaster pays attention to. We also use # a shorter alias to save typing. c = BuildmasterConfig = {} # Master variable to toggle config between testing on your personal system # (where /opt/local shouldn't be messed with) and the production server (where # it has to be) production = False privkey = '' if os.path.exists(_path('config.json')): with open(_path('config.json')) as f: configdata = json.load(f) production = configdata['production'] privkey = configdata['privkey'] # platforms we are building for if production: # this list should get longer in future build_platforms = ["snowleopard-x86_64", "lion-x86_64", "mtln-x86_64"] else: build_platforms = ["snowleopard-x86_64"] # Allow spaces and tabs in property values import re c['validation'] = { 'property_value' : re.compile(r'^[ \t\w\.\-\/\~:]*$') } ####### BUILDSLAVES # The 'slaves' list defines the set of recognized buildslaves. Each element is # a BuildSlave object, specifying a unique slave name and password. The same # slave name and password must be configured on the slave. from buildbot.buildslave import BuildSlave c['slaves'] = [] slavedata = {} if production: with open(_path('slaves.json')) as f: slavedata = json.load(f) for slave, pwd in slavedata.items(): c['slaves'].append(BuildSlave(slave, pwd)) else: c['slaves'] = [BuildSlave("snowleopard-x86_64", "pass")] # 'slavePortnum' defines the TCP port to listen on for connections from slaves. # This must match the value configured into the buildslaves (with their # --master option) c['slavePortnum'] = 17000 ####### CHANGESOURCES # the 'change_source' setting tells the buildmaster how it should find out # about source code changes. # poller is used for local testing but PBChangeSource (which relies on # notifications from a post-commit script) should be used in production if production: from buildbot.changes.pb import PBChangeSource sourcedata = [] with open(_path('source.json')) as f: sourcedata = json.load(f) c['change_source'] = PBChangeSource(user=sourcedata[0], passwd=sourcedata[1], port=sourcedata[2]) else: from buildbot.changes.svnpoller import SVNPoller c['change_source'] = [SVNPoller( 'https://svn.macports.org/repository/macports/trunk', svnbin='/opt/local/bin/svn', pollinterval=300), ] ####### SCHEDULERS def change_has_ports(change): for f in change.files: if "_resources" in f: continue # should actually skip changes to files/ only, but only if we know the # last build of the port succeeded if "dports" in f and ("Portfile" in f or "files" in f): return True return False def change_has_base(change): for f in change.files: if f.startswith('base'): return True return False from buildbot.changes.filter import ChangeFilter portsfilter = ChangeFilter(filter_fn=change_has_ports) basefilter = ChangeFilter(filter_fn=change_has_base) base_buildernames = ["buildbase-"+plat for plat in build_platforms] ports_buildernames = ["buildports-"+plat for plat in build_platforms if 'linux' not in plat] from buildbot.schedulers.basic import SingleBranchScheduler from buildbot.schedulers.forcesched import ForceScheduler c['schedulers'] = [SingleBranchScheduler( name="base", treeStableTimer=None, change_filter = basefilter, builderNames=base_buildernames), SingleBranchScheduler( name="ports", treeStableTimer=None, change_filter = portsfilter, builderNames=ports_buildernames), ForceScheduler( name="force", builderNames=ports_buildernames+base_buildernames) ] ####### BUILDERS #c['mergeRequests'] = True # The 'builders' list defines the Builders, which tell Buildbot how to perform a build: # what steps, and which slaves can execute them. Note that any particular build will # only take place on one slave. from buildbot.process.factory import BuildFactory, GNUAutoconf from buildbot.process.properties import WithProperties from buildbot.steps.source import SVN from buildbot.steps.shell import ShellCommand, Compile, Configure base_factory = BuildFactory() base_factory.addStep(SVN(baseURL='https://svn.macports.org/repository/macports/%%BRANCH%%/base', mode="copy", defaultBranch="trunk")) maybe_sudo=""" if [[ `id -u` -eq 0 ]]; then [[ -n "$SUDO_USER" ]] || SUDO_USER=buildbot CMD_PREFIX="sudo -u $SUDO_USER" INSTALL_USER=$SUDO_USER else INSTALL_USER=$USER fi """ base_factory.addStep(Configure(command=WithProperties(maybe_sudo+""" $CMD_PREFIX env PATH=/usr/bin:/bin:/usr/sbin:/sbin ./configure --enable-readline \ --prefix=%(workdir)s/opt/local \ --with-applications-dir=%(workdir)s/opt/local/Applications \ --with-install-user=$INSTALL_USER --with-install-group=`id -gn $INSTALL_USER` """))) base_factory.addStep(Compile(command=maybe_sudo+""" $CMD_PREFIX make -j`sysctl -n hw.activecpu` """)) base_factory.addStep(ShellCommand(command=maybe_sudo+""" $CMD_PREFIX make install """, name="install", description="install")) base_factory.addStep(ShellCommand(command=maybe_sudo+""" $CMD_PREFIX make test """, name="test", description="test")) base_factory.addStep(ShellCommand(command=WithProperties("make distclean; rm -rf %(workdir)s/opt/local"), name="clean", description="clean")) # custom class to make the file list available on the slave... class ShellCommandWithPortList(ShellCommand): name = 'write portlist file' description = 'write portlist file' def setBuild(self, build): ShellCommand.setBuild(self, build) # support forced build properties try: portlist = self.getProperty('portlist').strip() except: portlist = '' portset = set() # paths should be dports/category/portdir(/...) for f in self.build.allFiles(): comps = f.split('/') if len(comps) >= 3 and comps[0] == 'dports' and comps[1] != '_resources': portset.add(comps[2]) portlist += ' ' + ' '.join(portset) self.setProperty('portlist', portlist) # can't run with PREFIX/SRC_PREFIX inside the workdir in production, # because archives must be built with prefix=/opt/local if production: prefix='/opt/local' src_prefix='/opt/mports' dlhost='packages@packages.macports.org' dlpath='/var/www/html/packages' else: prefix='%(workdir)s/opt/local' src_prefix='%(workdir)s/opt/mports' dlpath='./deployed_archives' dlhost='' ulpath='archive_staging' ulpath_unique=ulpath+'-%(buildername)s' ports_factory = BuildFactory() # get MPAB itself; we'll do the checkout of base and dports via MPAB's script ports_factory.addStep(SVN(svnurl='https://svn.macports.org/repository/macports/contrib/mpab', mode="update")) ports_factory.addStep(ShellCommand(command=["./mpsync.sh"], name="sync", description="sync", env={'PREFIX': WithProperties(prefix), 'SRC_PREFIX': WithProperties(src_prefix), 'BASE_UPDATE': 'selfupdate'})) ports_factory.addStep(ShellCommandWithPortList(command=WithProperties('rm -f portlist; for portname in %(portlist)s; do for subport in `./subports.tcl $portname`; do echo $subport >> portlist; done; done'))) # run MPAB on the port list ports_factory.addStep(Compile(command=["./mpab", "buildports", "portlist"], env={'PREFIX': WithProperties(prefix), 'SRC_PREFIX': WithProperties(src_prefix)}, logfiles={"progress": "progress.log"}, haltOnFailure=False)) ports_factory.addStep(ShellCommand(command=["./gather_archives.sh"], name="gather archives", description="gather distributable archives", env={'PREFIX': WithProperties(prefix), 'ULPATH': ulpath})) # upload archives from build slave to master from buildbot.steps.transfer import DirectoryUpload ports_factory.addStep(DirectoryUpload(slavesrc=ulpath, masterdest=WithProperties(ulpath_unique))) # sign generated binaries and sync to download server (if distributable) from buildbot.steps.master import MasterShellCommand ports_factory.addStep(MasterShellCommand(command=["./deploy_archives.sh", WithProperties(ulpath_unique)], name="deploy archives", description="deploy archives", env={'PRIVKEY': privkey, 'DLHOST': dlhost, 'DLPATH': dlpath})) # make a logfile summarising the success/failure status for each port ports_factory.addStep(ShellCommand(command=["./do_status.sh"], name="status", description="status", env={'PREFIX': WithProperties(prefix) }, logfiles={"portstatus": "portstatus.log"})) # TODO: do we want to upload the individual logs so maintainers can review them? ports_factory.addStep(ShellCommand(command="./cleanup_old.sh", name="cleanup", description="cleanup", env={'PREFIX': WithProperties(prefix), 'ULPATH': ulpath})) from buildbot.config import BuilderConfig portsslaves = {} baseslaves = {} if production: slavenames = slavedata.keys() for plat in build_platforms: baseslaves[plat] = filter(lambda x: x.endswith(plat+"-base"), slavenames) portsslaves[plat] = filter(lambda x: x.endswith(plat+"-ports"), slavenames) else: slavenames = ["snowleopard-x86_64"] portsslaves = {build_platforms[0]:slavenames[0]} baseslaves = {build_platforms[0]:slavenames[0]} c['builders']=[] for plat in build_platforms: c['builders']+= [BuilderConfig(name="buildbase-"+plat, slavenames=baseslaves[plat], factory=base_factory)] if 'linux' not in plat: c['builders']+= [BuilderConfig(name="buildports-"+plat, slavenames=portsslaves[plat], factory=ports_factory)] ####### STATUS TARGETS # 'status' is a list of Status Targets. The results of each build will be # pushed to these targets. buildbot/status/*.py has a variety to choose from, # including web pages, email senders, and IRC bots. c['status'] = [] from buildbot.status import html from buildbot.status.web import auth, authz if production: from buildbot.status.web.auth import HTPasswdAuth htauth = HTPasswdAuth('htpasswd') authz_cfg=authz.Authz( auth=htauth, gracefulShutdown = 'auth', forceBuild = 'auth', forceAllBuilds = 'auth', pingBuilder = 'auth', stopBuild = 'auth', stopAllBuilds = 'auth', cancelPendingBuild = 'auth', ) else: authz_cfg=authz.Authz( gracefulShutdown = True, forceBuild = True, forceAllBuilds = True, pingBuilder = True, stopBuild = True, stopAllBuilds = True, cancelPendingBuild = True ) if production: # send mail about base failures to users on the blamelist from buildbot.status.mail import MailNotifier mn = MailNotifier(fromaddr="noreply@macports.org", lookup="", relayhost="localhost", builders=base_buildernames, mode="problem") c['status'].append(mn) import subprocess from twisted.internet import defer # notifier that sends mail to last committers and maintainers of failed ports class PortsMailNotifier(MailNotifier): # would make more sense to override getInterestedUsers() in BuildStatus, # but it seems almost impossible to tell a builder to use a different # class for status in its Build objects def useLookup(self, build): failedPorts = set() interestedUsers = set() statusStep = [x for x in build.getSteps() if x.getName() == "status"][0] statusLog = [x for x in statusStep.getLogs() if x.getName() == "portstatus"][0] for line in statusLog.getText().splitlines(): halves = line.split() if halves[0] == "[FAIL]": failedPorts.add(halves[1]) fakeAddresses = {'nomaintainer', 'nomaintainer@macports.org', 'openmaintainer', 'openmaintainer@macports.org'} for p in failedPorts: output = subprocess.Popen(['/opt/local/bin/port', 'info', '--index', '--maintainers', '--line', p], stdout=subprocess.PIPE).communicate()[0].strip() for m in output.split(','): if m not in fakeAddresses: interestedUsers.add(m) ss = build.getSourceStamp() if ss: for c in ss.changes: interesting = False for f in c.files: comps = f.split('/') if len(comps) >= 3 and comps[2] in failedPorts and comps[0] == 'dports' and comps[1] != '_resources': interesting = True break if interesting: interestedUsers.add(c.who) dl = [] for u in interestedUsers: d = defer.maybeDeferred(self.lookup.getAddress, u) dl.append(d) return defer.gatherResults(dl) mn = PortsMailNotifier(fromaddr="noreply@macports.org", lookup="", relayhost="localhost", builders=ports_buildernames, mode="failing") c['status'].append(mn) ####### PROJECT IDENTITY # the 'title' string will appear at the top of this buildbot # installation's html.WebStatus home page (linked to the # 'titleURL') and is embedded in the title of the waterfall HTML page. c['title'] = "MacPorts" c['titleURL'] = "http://www.macports.org/" if production: c['buildbotURL'] = "http://build.macports.org/" c['status'].append(html.WebStatus(http_port=8710, authz=authz_cfg)) else: c['buildbotURL'] = "http://localhost:8010/" c['status'].append(html.WebStatus(http_port=8010, authz=authz_cfg)) ####### DB URL # This specifies what database buildbot uses to store change and scheduler # state. You can leave this at its default for all but the largest # installations. c['db_url'] = "sqlite:///state.sqlite" ######## Cache settings c['buildHorizon'] = 10000 c['logHorizon'] = 5000 c['eventHorizon'] = 2000 c['buildCacheSize'] = 600