| #!/usr/bin/env python | |
| # Copyright 2009 The Go Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style | |
| # license that can be found in the LICENSE file. | |
| # This is a utility script for implementing a Go build slave. | |
| import binascii | |
| import httplib | |
| import os | |
| import struct | |
| import subprocess | |
| import sys | |
| import time | |
| buildhost = '' | |
| buildport = -1 | |
| buildkey = '' | |
| def main(args): | |
| global buildport, buildhost, buildkey | |
| if len(args) < 2: | |
| return usage(args[0]) | |
| if 'BUILDHOST' not in os.environ: | |
| print >>sys.stderr, "Please set $BUILDHOST" | |
| return | |
| buildhost = os.environ['BUILDHOST'] | |
| if 'BUILDPORT' not in os.environ: | |
| buildport = 80 | |
| else: | |
| buildport = int(os.environ['BUILDPORT']) | |
| try: | |
| buildkey = file('%s/.gobuildkey-%s' % (os.environ['HOME'], os.environ['BUILDER']), 'r').read().strip() | |
| except IOError: | |
| try: | |
| buildkey = file('%s/.gobuildkey' % os.environ['HOME'], 'r').read().strip() | |
| except IOError: | |
| print >>sys.stderr, "Need key in ~/.gobuildkey-%s or ~/.gobuildkey" % os.environ['BUILDER'] | |
| return | |
| if args[1] == 'init': | |
| return doInit(args) | |
| elif args[1] == 'hwget': | |
| return doHWGet(args) | |
| elif args[1] == 'hwset': | |
| return doHWSet(args) | |
| elif args[1] == 'next': | |
| return doNext(args) | |
| elif args[1] == 'record': | |
| return doRecord(args) | |
| elif args[1] == 'benchmarks': | |
| return doBenchmarks(args) | |
| else: | |
| return usage(args[0]) | |
| def usage(name): | |
| sys.stderr.write('''Usage: %s <command> | |
| Commands: | |
| init <rev>: init the build bot with the given commit as the first in history | |
| hwget <builder>: get the most recent revision built by the given builder | |
| hwset <builder> <rev>: get the most recent revision built by the given builder | |
| next <builder>: get the next revision number to by built by the given builder | |
| record <builder> <rev> <ok|log file>: record a build result | |
| benchmarks <builder> <rev> <log file>: record benchmark numbers | |
| ''' % name) | |
| return 1 | |
| def doInit(args): | |
| if len(args) != 3: | |
| return usage(args[0]) | |
| c = getCommit(args[2]) | |
| if c is None: | |
| fatal('Cannot get commit %s' % args[2]) | |
| return command('init', {'node': c.node, 'date': c.date, 'user': c.user, 'desc': c.desc}) | |
| def doHWGet(args, retries = 0): | |
| if len(args) != 3: | |
| return usage(args[0]) | |
| conn = httplib.HTTPConnection(buildhost, buildport, True) | |
| conn.request('GET', '/hw-get?builder=%s' % args[2]); | |
| reply = conn.getresponse() | |
| if reply.status == 200: | |
| print reply.read() | |
| elif reply.status == 500 and retries < 3: | |
| time.sleep(3) | |
| return doHWGet(args, retries = retries + 1) | |
| else: | |
| raise Failed('get-hw returned %d' % reply.status) | |
| return 0 | |
| def doHWSet(args): | |
| if len(args) != 4: | |
| return usage(args[0]) | |
| c = getCommit(args[3]) | |
| if c is None: | |
| fatal('Cannot get commit %s' % args[3]) | |
| return command('hw-set', {'builder': args[2], 'hw': c.node}) | |
| def doNext(args): | |
| if len(args) != 3: | |
| return usage(args[0]) | |
| conn = httplib.HTTPConnection(buildhost, buildport, True) | |
| conn.request('GET', '/hw-get?builder=%s' % args[2]); | |
| reply = conn.getresponse() | |
| if reply.status == 200: | |
| rev = reply.read() | |
| else: | |
| raise Failed('get-hw returned %d' % reply.status) | |
| c = getCommit(rev) | |
| next = getCommit(str(c.num + 1)) | |
| if next is not None and next.parent == c.node: | |
| print c.num + 1 | |
| else: | |
| print "<none>" | |
| return 0 | |
| def doRecord(args): | |
| if len(args) != 5: | |
| return usage(args[0]) | |
| builder = args[2] | |
| rev = args[3] | |
| c = getCommit(rev) | |
| if c is None: | |
| print >>sys.stderr, "Bad revision:", rev | |
| return 1 | |
| logfile = args[4] | |
| log = '' | |
| if logfile != 'ok': | |
| log = file(logfile, 'r').read() | |
| return command('build', {'node': c.node, 'parent': c.parent, 'date': c.date, 'user': c.user, 'desc': c.desc, 'log': log, 'builder': builder}) | |
| def doBenchmarks(args): | |
| if len(args) != 5: | |
| return usage(args[0]) | |
| builder = args[2] | |
| rev = args[3] | |
| c = getCommit(rev) | |
| if c is None: | |
| print >>sys.stderr, "Bad revision:", rev | |
| return 1 | |
| benchmarks = {} | |
| for line in file(args[4], 'r').readlines(): | |
| if 'Benchmark' in line and 'ns/op' in line: | |
| parts = line.split() | |
| if parts[3] == 'ns/op': | |
| benchmarks[parts[0]] = (parts[1], parts[2]) | |
| e = [] | |
| for (name, (a, b)) in benchmarks.items(): | |
| e.append(struct.pack('>H', len(name))) | |
| e.append(name) | |
| e.append(struct.pack('>H', len(a))) | |
| e.append(a) | |
| e.append(struct.pack('>H', len(b))) | |
| e.append(b) | |
| return command('benchmarks', {'node': c.node, 'builder': builder, 'benchmarkdata': binascii.b2a_base64(''.join(e))}) | |
| def encodeMultipartFormdata(fields, files): | |
| """fields is a sequence of (name, value) elements for regular form fields. | |
| files is a sequence of (name, filename, value) elements for data to be uploaded as files""" | |
| BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' | |
| CRLF = '\r\n' | |
| L = [] | |
| for (key, value) in fields.items(): | |
| L.append('--' + BOUNDARY) | |
| L.append('Content-Disposition: form-data; name="%s"' % key) | |
| L.append('') | |
| L.append(value) | |
| for (key, filename, value) in files: | |
| L.append('--' + BOUNDARY) | |
| L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) | |
| L.append('Content-Type: %s' % get_content_type(filename)) | |
| L.append('') | |
| L.append(value) | |
| L.append('--' + BOUNDARY + '--') | |
| L.append('') | |
| body = CRLF.join(L) | |
| content_type = 'multipart/form-data; boundary=%s' % BOUNDARY | |
| return content_type, body | |
| def unescapeXML(s): | |
| return s.replace('<', '<').replace('>', '>').replace('&', '&') | |
| class Commit: | |
| pass | |
| def getCommit(rev): | |
| output, stderr = subprocess.Popen(['hg', 'log', '-r', rev, '-l', '1', '--template', '{rev}>{node|escape}>{author|escape}>{date}>{desc}'], stdout = subprocess.PIPE, stderr = subprocess.PIPE, close_fds = True).communicate() | |
| if len(stderr) > 0: | |
| return None | |
| [n, node, user, date, desc] = output.split('>', 4) | |
| c = Commit() | |
| c.num = int(n) | |
| c.node = unescapeXML(node) | |
| c.user = unescapeXML(user) | |
| c.date = unescapeXML(date) | |
| c.desc = desc | |
| c.parent = '' | |
| if c.num > 0: | |
| output, _ = subprocess.Popen(['hg', 'log', '-r', str(c.num - 1), '-l', '1', '--template', '{node}'], stdout = subprocess.PIPE, close_fds = True).communicate() | |
| c.parent = output | |
| return c | |
| class Failed(Exception): | |
| def __init__(self, msg): | |
| self.msg = msg | |
| def __str__(self): | |
| return self.msg | |
| def command(cmd, args, retries = 0): | |
| args['key'] = buildkey | |
| contentType, body = encodeMultipartFormdata(args, []) | |
| print body | |
| conn = httplib.HTTPConnection(buildhost, buildport, True) | |
| conn.request('POST', '/' + cmd, body, {'Content-Type': contentType}) | |
| reply = conn.getresponse() | |
| if reply.status != 200: | |
| print "Command failed. Output:" | |
| print reply.read() | |
| if reply.status == 500 and retries < 3: | |
| print "Was a 500. Waiting two seconds and trying again." | |
| time.sleep(2) | |
| return command(cmd, args, retries = retries + 1) | |
| if reply.status != 200: | |
| raise Failed('Command "%s" returned %d' % (cmd, reply.status)) | |
| if __name__ == '__main__': | |
| sys.exit(main(sys.argv)) |