blob: 0e35cbe413e28cabf2c5845964be26367d8fd46f [file] [log] [blame]
# LLVM buildbot needs to watch multiple projects within a single repository.
# Based on the buildbot.changes.svnpoller.SVNPoller source code.
from twisted.python import log
from twisted.internet import defer, utils
from buildbot import util
from buildbot.changes import base
import xml.dom.minidom
import os, urllib, collections
import re
class LLVMPoller(base.PollingChangeSource, util.ComparableMixin):
"""
Poll LLVM repository for changes and submit them to the change master.
Following Multiple Projects.
"""
_svnurl="http://llvm.org/svn/llvm-project"
_revlinktmpl="http://llvm.org/viewvc/llvm-project/?view=rev&revision=%s"
compare_attrs = ["svnurl", "split_svn_path",
"svnuser", "svnpasswd",
"pollInterval", "histmax",
"svnbin", "category", "cachepath",
"projects"]
parent = None # filled in when we're added
last_change = None
loop = None
projects = None # Projects and branches to watch.
def __init__(self, svnurl=_svnurl, svnuser=None, svnpasswd=None,
pollInterval=2*60, histmax=10,
svnbin='svn', revlinktmpl=_revlinktmpl, category=None,
projects=None, cachepath=None):
self.cleanRe = re.compile(r"Require(?:s?)\s*.*\s*clean build", re.IGNORECASE + re.MULTILINE)
self.cleanCfg = re.compile(r"(CMakeLists\.txt$|\.cmake$|\.cmake\.in$)")
# projects is a list of projects to watch or None to watch all.
if projects:
if isinstance(projects, str) or isinstance(projects, tuple):
projects = [projects]
assert isinstance(projects, list)
assert len(projects) > 0
# Each project to watch is a string (project name) or a tuple
# (project name, branch) like ('llvm', 'branches/release_30').
# But we want it always to be a tuple, so we convert a project
# name string to a tuple (project, 'trunk').
self.projects = set()
for project in projects:
if isinstance(project, str):
project = (project, 'trunk')
assert isinstance(project, tuple)
self.projects.add(project)
if svnurl.endswith("/"):
svnurl = svnurl[:-1] # strip the trailing slash.
self.svnurl = svnurl
self._prefix = svnurl # svnurl is the LLVM repository root.
self.svnuser = svnuser
self.svnpasswd = svnpasswd
self.revlinktmpl = revlinktmpl
self.environ = os.environ.copy() # include environment variables
# required for ssh-agent auth.
self.svnbin = svnbin
self.pollInterval = pollInterval
self.histmax = histmax
self.category = category
self.cachepath = cachepath
if self.cachepath and os.path.exists(self.cachepath):
try:
f = open(self.cachepath, "r")
self.last_change = int(f.read().strip())
log.msg("LLVMPoller(%s): Setting last_change to %s" % (self.svnurl, self.last_change))
f.close()
# try writing it, too
f = open(self.cachepath, "w")
f.write(str(self.last_change))
f.close()
except:
self.cachepath = None
log.msg(("LLVMPoller(%s): Cache file corrupt or unwriteable; " +
"skipping and not using") % self.svnurl)
log.err()
def describe(self):
return "LLVMPoller: watching %s" % self.svnurl
def poll(self):
# Return value is only used for unit testing.
if self.projects:
log.msg("LLVMPoller(%s): Polling %s projects" % (self.svnurl, self.projects))
else:
log.msg("LLVMPoller(%s): Polling all projects" % self.svnurl)
d = defer.succeed(None)
d.addCallback(self.get_logs)
d.addCallback(self.parse_logs)
d.addCallback(self.get_new_logentries)
d.addCallback(self.create_changes)
d.addCallback(self.submit_changes)
d.addCallback(self.finished_ok)
d.addErrback(log.err, 'LLVMPoller: Error in while polling') # eat errors
return d
def getProcessOutput(self, args):
# This exists so we can override it during the unit tests.
d = utils.getProcessOutput(self.svnbin, args, self.environ)
return d
def get_logs(self, _):
args = []
args.extend(["log", "--xml", "--verbose", "--non-interactive"])
if self.svnuser:
args.extend(["--username=%s" % self.svnuser])
if self.svnpasswd:
args.extend(["--password=%s" % self.svnpasswd])
args.extend(["--limit=%d" % (self.histmax), self.svnurl])
d = self.getProcessOutput(args)
return d
def parse_logs(self, output):
# Parse the XML output, return a list of <logentry> nodes.
try:
doc = xml.dom.minidom.parseString(output)
except xml.parsers.expat.ExpatError:
log.msg("LLVMPoller(%s): LLVMPoller.parse_logs: ExpatError in '%s'" % (self.svnurl, output))
raise
logentries = doc.getElementsByTagName("logentry")
return logentries
def get_new_logentries(self, logentries):
last_change = old_last_change = self.last_change
# Given a list of logentries, calculate new_last_change, and
# new_logentries, where new_logentries contains only the ones after
# last_change.
new_last_change = None
new_logentries = []
if logentries:
new_last_change = int(logentries[0].getAttribute("revision"))
if last_change is None:
# If this is the first time we've been run, ignore any changes
# that occurred before now. This prevents a build at every
# startup.
log.msg('LLVMPoller(%s): Starting at change %s' % (self.svnurl, new_last_change))
elif last_change == new_last_change:
# An unmodified repository will hit this case.
log.msg('LLVMPoller(%s): No changes' % self.svnurl)
assert len(new_logentries) == 0
return [] # No new logentries.
else:
for el in logentries:
if last_change == int(el.getAttribute("revision")):
break
new_logentries.append(el)
new_logentries.reverse() # Return the oldest first.
self.last_change = new_last_change
log.msg('LLVMPoller(%s): Last change set from %s to %s' %
(self.svnurl, old_last_change, new_last_change))
return new_logentries
def _get_text(self, element, tag_name):
try:
child_nodes = element.getElementsByTagName(tag_name)[0].childNodes
text = "".join([t.data for t in child_nodes])
except:
text = "<unknown>"
return text
def _transform_path(self, path):
"""
Parses the given path, and returns a three-entry tuple
(PROJECT, BRANCH, FILEPATH) if PROJECT is watched one,
or None otherwise.
"""
relative_path = path
if relative_path.startswith(self._prefix):
relative_path = path[len(self._prefix):]
if relative_path.startswith("/"):
relative_path = relative_path[1:]
# turn llvm/trunk/lib/CodeGen/Analysis.cpp into
# ("llvm", "trunk", "lib/CodeGen/Analysis.cpp")
# llvm/branches/release_30/lib/CodeGen/Analysis.cpp into
# ("llvm", "branches/release_30", "lib/CodeGen/Analysis.cpp")
# and llvm/tags/RELEASE_30/rc1/lib/CodeGen/Analysis.cpp into
# ("llvm", "tags/RELEASE_30/rc1", "lib/CodeGen/Analysis.cpp")
# and filter projects/branches we are not watching.
pieces = relative_path.split('/')
project = pieces.pop(0)
branch = None
file_path = None
if pieces[0] == "trunk":
branch = pieces[0]
file_path = '/'.join(pieces[1:])
elif pieces[0] == "branches":
branch = '/'.join(pieces[0:2])
file_path = '/'.join(pieces[2:])
elif pieces[0] == "tags":
branch = '/'.join(pieces[0:3])
file_path = '/'.join(pieces[3:])
else:
# Something we do not expect.
log.msg("LLVMPoller(%s) cannot parse the path (%s). Ignored." % (self.svnurl, path))
return None
if self.projects:
if (project, branch) not in self.projects:
return None
return (project, branch, file_path)
def create_changes(self, new_logentries):
changes = []
categories = {
# Project: Category:
'llvm' : 'llvm',
'cfe' : 'clang',
'polly' : 'polly',
'compiler-rt' : 'compiler-rt',
'libcxx' : 'libcxx',
'libcxxabi' : 'libcxxabi',
'lld' : 'lld',
'lldb' : 'lldb',
'llgo' : 'llgo',
'openmp' : 'openmp',
}
for el in new_logentries:
revision = str(el.getAttribute("revision"))
revlink = ''
if self.revlinktmpl:
if revision:
revlink = self.revlinktmpl % urllib.quote_plus(revision)
log.msg("LLVMPoller(%s): Adding change revision %s" % (self.svnurl, revision))
author = self._get_text(el, "author")
comments = self._get_text(el, "msg")
# there is a "date" field, but it provides localtime in the
# repository's timezone, whereas we care about buildmaster's
# localtime (since this will get used to position the boxes on
# the Waterfall display, etc). So ignore the date field, and
# addChange will fill in with the current time
branches = {}
try:
pathlist = el.getElementsByTagName("paths")[0]
except IndexError: # weird, we got an empty revision
log.msg("LLVMPoller(%s): Ignoring commit with no paths." % self.svnurl)
continue
for p in pathlist.getElementsByTagName("path"):
action = p.getAttribute("action")
path = "".join([t.data for t in p.childNodes])
# the rest of buildbot is certaily not yet ready to handle
# unicode filenames, because they get put in RemoteCommands
# which get sent via PB to the buildslave, and PB doesn't
# handle unicode.
path = path.encode("ascii")
if path.startswith("/"):
path = path[1:]
where = self._transform_path(path)
# if 'where' is None, the file was outside any project that
# we care about and we should ignore it.
if where:
assert len(where) == 3
project, branch, filename = where
if not branch in branches:
branches[branch] = {'files': []}
branches[branch]['files'].append(filename)
if not branches[branch].has_key('action'):
branches[branch]['action'] = action
for branch in branches.keys():
action = branches[branch]['action']
files = branches[branch]['files']
number_of_files_changed = len(files)
if action == u'D' and number_of_files_changed == 1 and files[0] == '':
log.msg("LLVMPoller(%s): Ignoring deletion of branch '%s'" % (self.svnurl, branch))
else:
properties = dict()
if self.cleanRe.search(comments) or \
any([m for f in files for m in [self.cleanCfg.search(f)] if m]):
log.msg("Creating a change with the 'clean' property for r%s" % revision)
properties['clean_obj'] = (True, "change")
chdict = dict(author=author,
files=files,
comments=comments,
revision=revision,
branch=branch,
revlink=revlink,
category=categories.get(project, None),
repository=self.svnurl,
project=project,
properties=properties)
changes.append(chdict)
return changes
@defer.deferredGenerator
def submit_changes(self, changes):
for chdict in changes:
wfd = defer.waitForDeferred(self.master.addChange(**chdict))
yield wfd
wfd.getResult()
def finished_ok(self, res):
if self.cachepath:
f = open(self.cachepath, "w")
f.write(str(self.last_change))
f.close()
log.msg("LLVMPoller: Finished polling with res %s" % res)
return res