Index: third_party/buildbot_7_12/buildbot/slave/commands.py |
diff --git a/third_party/buildbot_7_12/buildbot/slave/commands.py b/third_party/buildbot_7_12/buildbot/slave/commands.py |
deleted file mode 100755 |
index d203139f2af1bb50319eb3bd45687e57066090a3..0000000000000000000000000000000000000000 |
--- a/third_party/buildbot_7_12/buildbot/slave/commands.py |
+++ /dev/null |
@@ -1,3117 +0,0 @@ |
-# -*- test-case-name: buildbot.test.test_slavecommand -*- |
- |
-import os, sys, re, signal, shutil, types, time, tarfile, tempfile |
-from stat import ST_CTIME, ST_MTIME, ST_SIZE |
-from xml.dom.minidom import parseString |
- |
-from zope.interface import implements |
-from twisted.internet.protocol import ProcessProtocol |
-from twisted.internet import reactor, defer, task |
-from twisted.python import log, failure, runtime |
-from twisted.python.procutils import which |
- |
-from buildbot.slave.interfaces import ISlaveCommand |
-from buildbot.slave.registry import registerSlaveCommand |
-from buildbot.util import to_text, remove_userpassword |
- |
-# this used to be a CVS $-style "Revision" auto-updated keyword, but since I |
-# moved to Darcs as the primary repository, this is updated manually each |
-# time this file is changed. The last cvs_ver that was here was 1.51 . |
-command_version = "2.9" |
- |
-# version history: |
-# >=1.17: commands are interruptable |
-# >=1.28: Arch understands 'revision', added Bazaar |
-# >=1.33: Source classes understand 'retry' |
-# >=1.39: Source classes correctly handle changes in branch (except Git) |
-# Darcs accepts 'revision' (now all do but Git) (well, and P4Sync) |
-# Arch/Baz should accept 'build-config' |
-# >=1.51: (release 0.7.3) |
-# >= 2.1: SlaveShellCommand now accepts 'initial_stdin', 'keep_stdin_open', |
-# and 'logfiles'. It now sends 'log' messages in addition to |
-# stdout/stdin/header/rc. It acquired writeStdin/closeStdin methods, |
-# but these are not remotely callable yet. |
-# (not externally visible: ShellCommandPP has writeStdin/closeStdin. |
-# ShellCommand accepts new arguments (logfiles=, initialStdin=, |
-# keepStdinOpen=) and no longer accepts stdin=) |
-# (release 0.7.4) |
-# >= 2.2: added monotone, uploadFile, and downloadFile (release 0.7.5) |
-# >= 2.3: added bzr (release 0.7.6) |
-# >= 2.4: Git understands 'revision' and branches |
-# >= 2.5: workaround added for remote 'hg clone --rev REV' when hg<0.9.2 |
-# >= 2.6: added uploadDirectory |
-# >= 2.7: added usePTY option to SlaveShellCommand |
-# >= 2.8: added username and password args to SVN class |
- |
-class CommandInterrupted(Exception): |
- pass |
-class TimeoutError(Exception): |
- pass |
- |
-class Obfuscated: |
- """An obfuscated string in a command""" |
- def __init__(self, real, fake): |
- self.real = real |
- self.fake = fake |
- |
- def __str__(self): |
- return self.fake |
- |
- def __repr__(self): |
- return `self.fake` |
- |
- def get_real(command): |
- rv = command |
- if type(command) == types.ListType: |
- rv = [] |
- for elt in command: |
- if isinstance(elt, Obfuscated): |
- rv.append(elt.real) |
- else: |
- rv.append(to_text(elt)) |
- return rv |
- get_real = staticmethod(get_real) |
- |
- def get_fake(command): |
- rv = command |
- if type(command) == types.ListType: |
- rv = [] |
- for elt in command: |
- if isinstance(elt, Obfuscated): |
- rv.append(elt.fake) |
- else: |
- rv.append(to_text(elt)) |
- return rv |
- get_fake = staticmethod(get_fake) |
- |
-class AbandonChain(Exception): |
- """A series of chained steps can raise this exception to indicate that |
- one of the intermediate ShellCommands has failed, such that there is no |
- point in running the remainder. 'rc' should be the non-zero exit code of |
- the failing ShellCommand.""" |
- |
- def __repr__(self): |
- return "<AbandonChain rc=%s>" % self.args[0] |
- |
-def getCommand(name): |
- possibles = which(name) |
- if not possibles: |
- raise RuntimeError("Couldn't find executable for '%s'" % name) |
- return possibles[0] |
- |
-def rmdirRecursive(dir): |
- """This is a replacement for shutil.rmtree that works better under |
- windows. Thanks to Bear at the OSAF for the code.""" |
- if not os.path.exists(dir): |
- return |
- |
- if os.path.islink(dir): |
- os.remove(dir) |
- return |
- |
- # Verify the directory is read/write/execute for the current user |
- os.chmod(dir, 0700) |
- |
- for name in os.listdir(dir): |
- full_name = os.path.join(dir, name) |
- # on Windows, if we don't have write permission we can't remove |
- # the file/directory either, so turn that on |
- if os.name == 'nt': |
- if not os.access(full_name, os.W_OK): |
- # I think this is now redundant, but I don't have an NT |
- # machine to test on, so I'm going to leave it in place |
- # -warner |
- os.chmod(full_name, 0600) |
- |
- if os.path.isdir(full_name): |
- rmdirRecursive(full_name) |
- else: |
- if os.path.isfile(full_name): |
- os.chmod(full_name, 0700) |
- os.remove(full_name) |
- os.rmdir(dir) |
- |
-class ShellCommandPP(ProcessProtocol): |
- debug = False |
- |
- def __init__(self, command): |
- self.command = command |
- self.pending_stdin = "" |
- self.stdin_finished = False |
- |
- def writeStdin(self, data): |
- assert not self.stdin_finished |
- if self.connected: |
- self.transport.write(data) |
- else: |
- self.pending_stdin += data |
- |
- def closeStdin(self): |
- if self.connected: |
- if self.debug: log.msg(" closing stdin") |
- self.transport.closeStdin() |
- self.stdin_finished = True |
- |
- def connectionMade(self): |
- if self.debug: |
- log.msg("ShellCommandPP.connectionMade") |
- if not self.command.process: |
- if self.debug: |
- log.msg(" assigning self.command.process: %s" % |
- (self.transport,)) |
- self.command.process = self.transport |
- |
- # TODO: maybe we shouldn't close stdin when using a PTY. I can't test |
- # this yet, recent debian glibc has a bug which causes thread-using |
- # test cases to SIGHUP trial, and the workaround is to either run |
- # the whole test with /bin/sh -c " ".join(argv) (way gross) or to |
- # not use a PTY. Once the bug is fixed, I'll be able to test what |
- # happens when you close stdin on a pty. My concern is that it will |
- # SIGHUP the child (since we are, in a sense, hanging up on them). |
- # But it may well be that keeping stdout open prevents the SIGHUP |
- # from being sent. |
- #if not self.command.usePTY: |
- |
- if self.pending_stdin: |
- if self.debug: log.msg(" writing to stdin") |
- self.transport.write(self.pending_stdin) |
- if self.stdin_finished: |
- if self.debug: log.msg(" closing stdin") |
- self.transport.closeStdin() |
- |
- def outReceived(self, data): |
- if self.debug: |
- log.msg("ShellCommandPP.outReceived") |
- self.command.addStdout(data) |
- |
- def errReceived(self, data): |
- if self.debug: |
- log.msg("ShellCommandPP.errReceived") |
- self.command.addStderr(data) |
- |
- def processEnded(self, status_object): |
- if self.debug: |
- log.msg("ShellCommandPP.processEnded", status_object) |
- # status_object is a Failure wrapped around an |
- # error.ProcessTerminated or and error.ProcessDone. |
- # requires twisted >= 1.0.4 to overcome a bug in process.py |
- sig = status_object.value.signal |
- rc = status_object.value.exitCode |
- self.command.finished(sig, rc) |
- |
-class LogFileWatcher: |
- POLL_INTERVAL = 2 |
- |
- def __init__(self, command, name, logfile, follow=False): |
- self.command = command |
- self.name = name |
- self.logfile = logfile |
- |
- log.msg("LogFileWatcher created to watch %s" % logfile) |
- # we are created before the ShellCommand starts. If the logfile we're |
- # supposed to be watching already exists, record its size and |
- # ctime/mtime so we can tell when it starts to change. |
- self.old_logfile_stats = self.statFile() |
- self.started = False |
- |
- # follow the file, only sending back lines |
- # added since we started watching |
- self.follow = follow |
- |
- # every 2 seconds we check on the file again |
- self.poller = task.LoopingCall(self.poll) |
- |
- def start(self): |
- self.poller.start(self.POLL_INTERVAL).addErrback(self._cleanupPoll) |
- |
- def _cleanupPoll(self, err): |
- log.err(err, msg="Polling error") |
- self.poller = None |
- |
- def stop(self): |
- self.poll() |
- if self.poller is not None: |
- self.poller.stop() |
- if self.started: |
- self.f.close() |
- |
- def statFile(self): |
- if os.path.exists(self.logfile): |
- s = os.stat(self.logfile) |
- return (s[ST_CTIME], s[ST_MTIME], s[ST_SIZE]) |
- return None |
- |
- def poll(self): |
- if not self.started: |
- s = self.statFile() |
- if s == self.old_logfile_stats: |
- return # not started yet |
- if not s: |
- # the file was there, but now it's deleted. Forget about the |
- # initial state, clearly the process has deleted the logfile |
- # in preparation for creating a new one. |
- self.old_logfile_stats = None |
- return # no file to work with |
- self.f = open(self.logfile, "rb") |
- # if we only want new lines, seek to |
- # where we stat'd so we only find new |
- # lines |
- if self.follow: |
- self.f.seek(s[2], 0) |
- self.started = True |
- self.f.seek(self.f.tell(), 0) |
- while True: |
- data = self.f.read(10000) |
- if not data: |
- return |
- self.command.addLogfile(self.name, data) |
- |
- |
-class ShellCommand: |
- # This is a helper class, used by SlaveCommands to run programs in a |
- # child shell. |
- |
- notreally = False |
- BACKUP_TIMEOUT = 5 |
- KILL = "KILL" |
- CHUNK_LIMIT = 128*1024 |
- |
- # For sending elapsed time: |
- startTime = None |
- elapsedTime = None |
- # I wish we had easy access to CLOCK_MONOTONIC in Python: |
- # http://www.opengroup.org/onlinepubs/000095399/functions/clock_getres.html |
- # Then changes to the system clock during a run wouldn't effect the "elapsed |
- # time" results. |
- |
- def __init__(self, builder, command, |
- workdir, environ=None, |
- sendStdout=True, sendStderr=True, sendRC=True, |
- timeout=None, maxTime=None, initialStdin=None, |
- keepStdinOpen=False, keepStdout=False, keepStderr=False, |
- logEnviron=True, logfiles={}, usePTY="slave-config"): |
- """ |
- |
- @param keepStdout: if True, we keep a copy of all the stdout text |
- that we've seen. This copy is available in |
- self.stdout, which can be read after the command |
- has finished. |
- @param keepStderr: same, for stderr |
- |
- @param usePTY: "slave-config" -> use the SlaveBuilder's usePTY; |
- otherwise, true to use a PTY, false to not use a PTY. |
- """ |
- |
- self.builder = builder |
- self.command = Obfuscated.get_real(command) |
- self.fake_command = Obfuscated.get_fake(command) |
- self.sendStdout = sendStdout |
- self.sendStderr = sendStderr |
- self.sendRC = sendRC |
- self.logfiles = logfiles |
- self.workdir = workdir |
- if not os.path.exists(workdir): |
- os.makedirs(workdir) |
- self.environ = os.environ.copy() |
- if environ: |
- if environ.has_key('PYTHONPATH'): |
- ppath = environ['PYTHONPATH'] |
- # Need to do os.pathsep translation. We could either do that |
- # by replacing all incoming ':'s with os.pathsep, or by |
- # accepting lists. I like lists better. |
- if not isinstance(ppath, str): |
- # If it's not a string, treat it as a sequence to be |
- # turned in to a string. |
- ppath = os.pathsep.join(ppath) |
- |
- if self.environ.has_key('PYTHONPATH'): |
- # special case, prepend the builder's items to the |
- # existing ones. This will break if you send over empty |
- # strings, so don't do that. |
- ppath = ppath + os.pathsep + self.environ['PYTHONPATH'] |
- |
- environ['PYTHONPATH'] = ppath |
- |
- self.environ.update(environ) |
- self.initialStdin = initialStdin |
- self.keepStdinOpen = keepStdinOpen |
- self.logEnviron = logEnviron |
- self.timeout = timeout |
- self.timer = None |
- self.maxTime = maxTime |
- self.maxTimer = None |
- self.keepStdout = keepStdout |
- self.keepStderr = keepStderr |
- |
- |
- if usePTY == "slave-config": |
- self.usePTY = self.builder.usePTY |
- else: |
- self.usePTY = usePTY |
- |
- # usePTY=True is a convenience for cleaning up all children and |
- # grandchildren of a hung command. Fall back to usePTY=False on systems |
- # and in situations where ptys cause problems. PTYs are posix-only, |
- # and for .closeStdin to matter, we must use a pipe, not a PTY |
- if runtime.platformType != "posix" or initialStdin is not None: |
- if self.usePTY and usePTY != "slave-config": |
- self.sendStatus({'header': "WARNING: disabling usePTY for this command"}) |
- self.usePTY = False |
- |
- self.logFileWatchers = [] |
- for name,filevalue in self.logfiles.items(): |
- filename = filevalue |
- follow = False |
- |
- # check for a dictionary of options |
- # filename is required, others are optional |
- if type(filevalue) == dict: |
- filename = filevalue['filename'] |
- follow = filevalue.get('follow', False) |
- |
- w = LogFileWatcher(self, name, |
- os.path.join(self.workdir, filename), |
- follow=follow) |
- self.logFileWatchers.append(w) |
- |
- def __repr__(self): |
- return "<slavecommand.ShellCommand '%s'>" % self.fake_command |
- |
- def sendStatus(self, status): |
- self.builder.sendUpdate(status) |
- |
- def start(self): |
- # return a Deferred which fires (with the exit code) when the command |
- # completes |
- if self.keepStdout: |
- self.stdout = "" |
- if self.keepStderr: |
- self.stderr = "" |
- self.deferred = defer.Deferred() |
- try: |
- self._startCommand() |
- except: |
- log.msg("error in ShellCommand._startCommand") |
- log.err() |
- # pretend it was a shell error |
- self.deferred.errback(AbandonChain(-1)) |
- return self.deferred |
- |
- def _startCommand(self): |
- # ensure workdir exists. Use os.path.normpath because this can be |
- # called with trailing '..' components, which can cause os.makedirs |
- # to fail. |
- workdir = os.path.normpath(self.workdir) |
- if not os.path.isdir(workdir): |
- os.makedirs(workdir) |
- log.msg("ShellCommand._startCommand") |
- if self.notreally: |
- self.sendStatus({'header': "command '%s' in dir %s" % \ |
- (self.fake_command, self.workdir)}) |
- self.sendStatus({'header': "(not really)\n"}) |
- self.finished(None, 0) |
- return |
- |
- self.pp = ShellCommandPP(self) |
- |
- if type(self.command) in types.StringTypes: |
- if runtime.platformType == 'win32': |
- argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args |
- if '/c' not in argv: argv += ['/c'] |
- argv += [self.command] |
- else: |
- # for posix, use /bin/sh. for other non-posix, well, doesn't |
- # hurt to try |
- argv = ['/bin/sh', '-c', self.command] |
- display = self.fake_command |
- else: |
- if runtime.platformType == 'win32' and not self.command[0].lower().endswith(".exe"): |
- argv = os.environ['COMSPEC'].split() # allow %COMSPEC% to have args |
- if '/c' not in argv: argv += ['/c'] |
- argv += list(self.command) |
- else: |
- argv = self.command |
- display = " ".join(self.fake_command) |
- |
- # $PWD usually indicates the current directory; spawnProcess may not |
- # update this value, though, so we set it explicitly here. This causes |
- # weird problems (bug #456) on msys, though.. |
- if not self.environ.get('MACHTYPE', None) == 'i686-pc-msys': |
- self.environ['PWD'] = os.path.abspath(self.workdir) |
- |
- # self.stdin is handled in ShellCommandPP.connectionMade |
- |
- # first header line is the command in plain text, argv joined with |
- # spaces. You should be able to cut-and-paste this into a shell to |
- # obtain the same results. If there are spaces in the arguments, too |
- # bad. |
- log.msg(" " + display) |
- self.sendStatus({'header': display+"\n"}) |
- |
- # then comes the secondary information |
- msg = " in dir %s" % (self.workdir,) |
- if self.timeout: |
- msg += " (timeout %d secs)" % (self.timeout,) |
- log.msg(" " + msg) |
- self.sendStatus({'header': msg+"\n"}) |
- |
- msg = " watching logfiles %s" % (self.logfiles,) |
- log.msg(" " + msg) |
- self.sendStatus({'header': msg+"\n"}) |
- |
- # then the obfuscated command array for resolving unambiguity |
- msg = " argv: %s" % (self.fake_command,) |
- log.msg(" " + msg) |
- self.sendStatus({'header': msg+"\n"}) |
- |
- # then the environment, since it sometimes causes problems |
- if self.logEnviron: |
- msg = " environment:\n" |
- env_names = self.environ.keys() |
- env_names.sort() |
- for name in env_names: |
- msg += " %s=%s\n" % (name, self.environ[name]) |
- log.msg(" environment: %s" % (self.environ,)) |
- self.sendStatus({'header': msg}) |
- |
- if self.initialStdin: |
- msg = " writing %d bytes to stdin" % len(self.initialStdin) |
- log.msg(" " + msg) |
- self.sendStatus({'header': msg+"\n"}) |
- |
- if self.keepStdinOpen: |
- msg = " leaving stdin open" |
- else: |
- msg = " closing stdin" |
- log.msg(" " + msg) |
- self.sendStatus({'header': msg+"\n"}) |
- |
- msg = " using PTY: %s" % bool(self.usePTY) |
- log.msg(" " + msg) |
- self.sendStatus({'header': msg+"\n"}) |
- |
- # this will be buffered until connectionMade is called |
- if self.initialStdin: |
- self.pp.writeStdin(self.initialStdin) |
- if not self.keepStdinOpen: |
- self.pp.closeStdin() |
- |
- # win32eventreactor's spawnProcess (under twisted <= 2.0.1) returns |
- # None, as opposed to all the posixbase-derived reactors (which |
- # return the new Process object). This is a nuisance. We can make up |
- # for it by having the ProcessProtocol give us their .transport |
- # attribute after they get one. I'd prefer to get it from |
- # spawnProcess because I'm concerned about returning from this method |
- # without having a valid self.process to work with. (if kill() were |
- # called right after we return, but somehow before connectionMade |
- # were called, then kill() would blow up). |
- self.process = None |
- self.startTime = time.time() |
- |
- p = reactor.spawnProcess(self.pp, argv[0], argv, |
- self.environ, |
- self.workdir, |
- usePTY=self.usePTY) |
- # connectionMade might have been called during spawnProcess |
- if not self.process: |
- self.process = p |
- |
- # connectionMade also closes stdin as long as we're not using a PTY. |
- # This is intended to kill off inappropriately interactive commands |
- # better than the (long) hung-command timeout. ProcessPTY should be |
- # enhanced to allow the same childFDs argument that Process takes, |
- # which would let us connect stdin to /dev/null . |
- |
- if self.timeout: |
- self.timer = reactor.callLater(self.timeout, self.doTimeout) |
- |
- if self.maxTime: |
- self.maxTimer = reactor.callLater(self.maxTime, self.doMaxTimeout) |
- |
- for w in self.logFileWatchers: |
- w.start() |
- |
- |
- def _chunkForSend(self, data): |
- # limit the chunks that we send over PB to 128k, since it has a |
- # hardwired string-size limit of 640k. |
- LIMIT = self.CHUNK_LIMIT |
- for i in range(0, len(data), LIMIT): |
- yield data[i:i+LIMIT] |
- |
- def addStdout(self, data): |
- if self.sendStdout: |
- for chunk in self._chunkForSend(data): |
- self.sendStatus({'stdout': chunk}) |
- if self.keepStdout: |
- self.stdout += data |
- if self.timer: |
- self.timer.reset(self.timeout) |
- |
- def addStderr(self, data): |
- if self.sendStderr: |
- for chunk in self._chunkForSend(data): |
- self.sendStatus({'stderr': chunk}) |
- if self.keepStderr: |
- self.stderr += data |
- if self.timer: |
- self.timer.reset(self.timeout) |
- |
- def addLogfile(self, name, data): |
- for chunk in self._chunkForSend(data): |
- self.sendStatus({'log': (name, chunk)}) |
- if self.timer: |
- self.timer.reset(self.timeout) |
- |
- def finished(self, sig, rc): |
- self.elapsedTime = time.time() - self.startTime |
- log.msg("command finished with signal %s, exit code %s, elapsedTime: %0.6f" % (sig,rc,self.elapsedTime)) |
- for w in self.logFileWatchers: |
- # this will send the final updates |
- w.stop() |
- if sig is not None: |
- rc = -1 |
- if self.sendRC: |
- if sig is not None: |
- self.sendStatus( |
- {'header': "process killed by signal %d\n" % sig}) |
- self.sendStatus({'rc': rc}) |
- self.sendStatus({'header': "elapsedTime=%0.6f\n" % self.elapsedTime}) |
- if self.timer: |
- self.timer.cancel() |
- self.timer = None |
- if self.maxTimer: |
- self.maxTimer.cancel() |
- self.maxTimer = None |
- d = self.deferred |
- self.deferred = None |
- if d: |
- d.callback(rc) |
- else: |
- log.msg("Hey, command %s finished twice" % self) |
- |
- def failed(self, why): |
- log.msg("ShellCommand.failed: command failed: %s" % (why,)) |
- if self.timer: |
- self.timer.cancel() |
- self.timer = None |
- if self.maxTimer: |
- self.maxTimer.cancel() |
- self.maxTimer = None |
- d = self.deferred |
- self.deferred = None |
- if d: |
- d.errback(why) |
- else: |
- log.msg("Hey, command %s finished twice" % self) |
- |
- def doTimeout(self): |
- self.timer = None |
- msg = "command timed out: %d seconds without output" % self.timeout |
- self.kill(msg) |
- |
- def doMaxTimeout(self): |
- self.maxTimer = None |
- msg = "command timed out: %d seconds elapsed" % self.maxTime |
- self.kill(msg) |
- |
- def kill(self, msg): |
- # This may be called by the timeout, or when the user has decided to |
- # abort this build. |
- if self.timer: |
- self.timer.cancel() |
- self.timer = None |
- if self.maxTimer: |
- self.maxTimer.cancel() |
- self.maxTimer = None |
- if hasattr(self.process, "pid") and self.process.pid is not None: |
- msg += ", killing pid %s" % self.process.pid |
- log.msg(msg) |
- self.sendStatus({'header': "\n" + msg + "\n"}) |
- |
- hit = 0 |
- if runtime.platformType == "posix": |
- try: |
- # really want to kill off all child processes too. Process |
- # Groups are ideal for this, but that requires |
- # spawnProcess(usePTY=1). Try both ways in case process was |
- # not started that way. |
- |
- # the test suite sets self.KILL=None to tell us we should |
- # only pretend to kill the child. This lets us test the |
- # backup timer. |
- |
- sig = None |
- if self.KILL is not None: |
- sig = getattr(signal, "SIG"+ self.KILL, None) |
- |
- if self.KILL == None: |
- log.msg("self.KILL==None, only pretending to kill child") |
- elif sig is None: |
- log.msg("signal module is missing SIG%s" % self.KILL) |
- elif not hasattr(os, "kill"): |
- log.msg("os module is missing the 'kill' function") |
- elif not hasattr(self.process, "pid") or self.process.pid is None: |
- log.msg("self.process has no pid") |
- else: |
- log.msg("trying os.kill(-pid, %d)" % (sig,)) |
- # TODO: maybe use os.killpg instead of a negative pid? |
- os.kill(-self.process.pid, sig) |
- log.msg(" signal %s sent successfully" % sig) |
- hit = 1 |
- except OSError: |
- # probably no-such-process, maybe because there is no process |
- # group |
- pass |
- if not hit: |
- try: |
- if self.KILL is None: |
- log.msg("self.KILL==None, only pretending to kill child") |
- else: |
- log.msg("trying process.signalProcess('KILL')") |
- self.process.signalProcess(self.KILL) |
- log.msg(" signal %s sent successfully" % (self.KILL,)) |
- hit = 1 |
- except OSError: |
- # could be no-such-process, because they finished very recently |
- pass |
- if not hit: |
- log.msg("signalProcess/os.kill failed both times") |
- |
- if runtime.platformType == "posix": |
- # we only do this under posix because the win32eventreactor |
- # blocks here until the process has terminated, while closing |
- # stderr. This is weird. |
- self.pp.transport.loseConnection() |
- |
- # finished ought to be called momentarily. Just in case it doesn't, |
- # set a timer which will abandon the command. |
- self.timer = reactor.callLater(self.BACKUP_TIMEOUT, |
- self.doBackupTimeout) |
- |
- def doBackupTimeout(self): |
- log.msg("we tried to kill the process, and it wouldn't die.." |
- " finish anyway") |
- self.timer = None |
- self.sendStatus({'header': "SIGKILL failed to kill process\n"}) |
- if self.sendRC: |
- self.sendStatus({'header': "using fake rc=-1\n"}) |
- self.sendStatus({'rc': -1}) |
- self.failed(TimeoutError("SIGKILL failed to kill process")) |
- |
- |
- def writeStdin(self, data): |
- self.pp.writeStdin(data) |
- |
- def closeStdin(self): |
- self.pp.closeStdin() |
- |
- |
-class Command: |
- implements(ISlaveCommand) |
- |
- """This class defines one command that can be invoked by the build master. |
- The command is executed on the slave side, and always sends back a |
- completion message when it finishes. It may also send intermediate status |
- as it runs (by calling builder.sendStatus). Some commands can be |
- interrupted (either by the build master or a local timeout), in which |
- case the step is expected to complete normally with a status message that |
- indicates an error occurred. |
- |
- These commands are used by BuildSteps on the master side. Each kind of |
- BuildStep uses a single Command. The slave must implement all the |
- Commands required by the set of BuildSteps used for any given build: |
- this is checked at startup time. |
- |
- All Commands are constructed with the same signature: |
- c = CommandClass(builder, args) |
- where 'builder' is the parent SlaveBuilder object, and 'args' is a |
- dict that is interpreted per-command. |
- |
- The setup(args) method is available for setup, and is run from __init__. |
- |
- The Command is started with start(). This method must be implemented in a |
- subclass, and it should return a Deferred. When your step is done, you |
- should fire the Deferred (the results are not used). If the command is |
- interrupted, it should fire the Deferred anyway. |
- |
- While the command runs. it may send status messages back to the |
- buildmaster by calling self.sendStatus(statusdict). The statusdict is |
- interpreted by the master-side BuildStep however it likes. |
- |
- A separate completion message is sent when the deferred fires, which |
- indicates that the Command has finished, but does not carry any status |
- data. If the Command needs to return an exit code of some sort, that |
- should be sent as a regular status message before the deferred is fired . |
- Once builder.commandComplete has been run, no more status messages may be |
- sent. |
- |
- If interrupt() is called, the Command should attempt to shut down as |
- quickly as possible. Child processes should be killed, new ones should |
- not be started. The Command should send some kind of error status update, |
- then complete as usual by firing the Deferred. |
- |
- .interrupted should be set by interrupt(), and can be tested to avoid |
- sending multiple error status messages. |
- |
- If .running is False, the bot is shutting down (or has otherwise lost the |
- connection to the master), and should not send any status messages. This |
- is checked in Command.sendStatus . |
- |
- """ |
- |
- # builder methods: |
- # sendStatus(dict) (zero or more) |
- # commandComplete() or commandInterrupted() (one, at end) |
- |
- debug = False |
- interrupted = False |
- running = False # set by Builder, cleared on shutdown or when the |
- # Deferred fires |
- |
- def __init__(self, builder, stepId, args): |
- self.builder = builder |
- self.stepId = stepId # just for logging |
- self.args = args |
- self.setup(args) |
- |
- def setup(self, args): |
- """Override this in a subclass to extract items from the args dict.""" |
- pass |
- |
- def doStart(self): |
- self.running = True |
- d = defer.maybeDeferred(self.start) |
- d.addBoth(self.commandComplete) |
- return d |
- |
- def start(self): |
- """Start the command. This method should return a Deferred that will |
- fire when the command has completed. The Deferred's argument will be |
- ignored. |
- |
- This method should be overridden by subclasses.""" |
- raise NotImplementedError, "You must implement this in a subclass" |
- |
- def sendStatus(self, status): |
- """Send a status update to the master.""" |
- if self.debug: |
- log.msg("sendStatus", status) |
- if not self.running: |
- log.msg("would sendStatus but not .running") |
- return |
- self.builder.sendUpdate(status) |
- |
- def doInterrupt(self): |
- self.running = False |
- self.interrupt() |
- |
- def interrupt(self): |
- """Override this in a subclass to allow commands to be interrupted. |
- May be called multiple times, test and set self.interrupted=True if |
- this matters.""" |
- pass |
- |
- def commandComplete(self, res): |
- self.running = False |
- return res |
- |
- # utility methods, mostly used by SlaveShellCommand and the like |
- |
- def _abandonOnFailure(self, rc): |
- if type(rc) is not int: |
- log.msg("weird, _abandonOnFailure was given rc=%s (%s)" % \ |
- (rc, type(rc))) |
- assert isinstance(rc, int) |
- if rc != 0: |
- raise AbandonChain(rc) |
- return rc |
- |
- def _sendRC(self, res): |
- self.sendStatus({'rc': 0}) |
- |
- def _checkAbandoned(self, why): |
- log.msg("_checkAbandoned", why) |
- why.trap(AbandonChain) |
- log.msg(" abandoning chain", why.value) |
- self.sendStatus({'rc': why.value.args[0]}) |
- return None |
- |
- |
- |
-class SlaveFileUploadCommand(Command): |
- """ |
- Upload a file from slave to build master |
- Arguments: |
- |
- - ['workdir']: base directory to use |
- - ['slavesrc']: name of the slave-side file to read from |
- - ['writer']: RemoteReference to a transfer._FileWriter object |
- - ['maxsize']: max size (in bytes) of file to write |
- - ['blocksize']: max size for each data block |
- """ |
- debug = False |
- |
- def setup(self, args): |
- self.workdir = args['workdir'] |
- self.filename = args['slavesrc'] |
- self.writer = args['writer'] |
- self.remaining = args['maxsize'] |
- self.blocksize = args['blocksize'] |
- self.stderr = None |
- self.rc = 0 |
- |
- def start(self): |
- if self.debug: |
- log.msg('SlaveFileUploadCommand started') |
- |
- # Open file |
- self.path = os.path.join(self.builder.basedir, |
- self.workdir, |
- os.path.expanduser(self.filename)) |
- try: |
- self.fp = open(self.path, 'rb') |
- if self.debug: |
- log.msg('Opened %r for upload' % self.path) |
- except: |
- # TODO: this needs cleanup |
- self.fp = None |
- self.stderr = 'Cannot open file %r for upload' % self.path |
- self.rc = 1 |
- if self.debug: |
- log.msg('Cannot open file %r for upload' % self.path) |
- |
- self.sendStatus({'header': "sending %s" % self.path}) |
- |
- d = defer.Deferred() |
- reactor.callLater(0, self._loop, d) |
- def _close(res): |
- # close the file, but pass through any errors from _loop |
- d1 = self.writer.callRemote("close") |
- d1.addErrback(log.err) |
- d1.addCallback(lambda ignored: res) |
- return d1 |
- d.addBoth(_close) |
- d.addBoth(self.finished) |
- return d |
- |
- def _loop(self, fire_when_done): |
- d = defer.maybeDeferred(self._writeBlock) |
- def _done(finished): |
- if finished: |
- fire_when_done.callback(None) |
- else: |
- self._loop(fire_when_done) |
- def _err(why): |
- fire_when_done.errback(why) |
- d.addCallbacks(_done, _err) |
- return None |
- |
- def _writeBlock(self): |
- """Write a block of data to the remote writer""" |
- |
- if self.interrupted or self.fp is None: |
- if self.debug: |
- log.msg('SlaveFileUploadCommand._writeBlock(): end') |
- return True |
- |
- length = self.blocksize |
- if self.remaining is not None and length > self.remaining: |
- length = self.remaining |
- |
- if length <= 0: |
- if self.stderr is None: |
- self.stderr = 'Maximum filesize reached, truncating file %r' \ |
- % self.path |
- self.rc = 1 |
- data = '' |
- else: |
- data = self.fp.read(length) |
- |
- if self.debug: |
- log.msg('SlaveFileUploadCommand._writeBlock(): '+ |
- 'allowed=%d readlen=%d' % (length, len(data))) |
- if len(data) == 0: |
- log.msg("EOF: callRemote(close)") |
- return True |
- |
- if self.remaining is not None: |
- self.remaining = self.remaining - len(data) |
- assert self.remaining >= 0 |
- d = self.writer.callRemote('write', data) |
- d.addCallback(lambda res: False) |
- return d |
- |
- def interrupt(self): |
- if self.debug: |
- log.msg('interrupted') |
- if self.interrupted: |
- return |
- if self.stderr is None: |
- self.stderr = 'Upload of %r interrupted' % self.path |
- self.rc = 1 |
- self.interrupted = True |
- # the next _writeBlock call will notice the .interrupted flag |
- |
- def finished(self, res): |
- if self.debug: |
- log.msg('finished: stderr=%r, rc=%r' % (self.stderr, self.rc)) |
- if self.stderr is None: |
- self.sendStatus({'rc': self.rc}) |
- else: |
- self.sendStatus({'stderr': self.stderr, 'rc': self.rc}) |
- return res |
- |
-registerSlaveCommand("uploadFile", SlaveFileUploadCommand, command_version) |
- |
- |
-class SlaveDirectoryUploadCommand(SlaveFileUploadCommand): |
- """ |
- Upload a directory from slave to build master |
- Arguments: |
- |
- - ['workdir']: base directory to use |
- - ['slavesrc']: name of the slave-side directory to read from |
- - ['writer']: RemoteReference to a transfer._DirectoryWriter object |
- - ['maxsize']: max size (in bytes) of file to write |
- - ['blocksize']: max size for each data block |
- - ['compress']: one of [None, 'bz2', 'gz'] |
- """ |
- debug = True |
- |
- def setup(self, args): |
- self.workdir = args['workdir'] |
- self.dirname = args['slavesrc'] |
- self.writer = args['writer'] |
- self.remaining = args['maxsize'] |
- self.blocksize = args['blocksize'] |
- self.compress = args['compress'] |
- self.stderr = None |
- self.rc = 0 |
- |
- def start(self): |
- if self.debug: |
- log.msg('SlaveDirectoryUploadCommand started') |
- |
- self.path = os.path.join(self.builder.basedir, |
- self.workdir, |
- os.path.expanduser(self.dirname)) |
- if self.debug: |
- log.msg("path: %r" % self.path) |
- |
- # Create temporary archive |
- fd, self.tarname = tempfile.mkstemp() |
- fileobj = os.fdopen(fd, 'w') |
- if self.compress == 'bz2': |
- mode='w|bz2' |
- elif self.compress == 'gz': |
- mode='w|gz' |
- else: |
- mode = 'w' |
- archive = tarfile.open(name=self.tarname, mode=mode, fileobj=fileobj) |
- archive.add(self.path, '') |
- archive.close() |
- fileobj.close() |
- |
- # Transfer it |
- self.fp = open(self.tarname, 'rb') |
- |
- self.sendStatus({'header': "sending %s" % self.path}) |
- |
- d = defer.Deferred() |
- reactor.callLater(0, self._loop, d) |
- def unpack(res): |
- # unpack the archive, but pass through any errors from _loop |
- d1 = self.writer.callRemote("unpack") |
- d1.addErrback(log.err) |
- d1.addCallback(lambda ignored: res) |
- return d1 |
- d.addCallback(unpack) |
- d.addBoth(self.finished) |
- return d |
- |
- def finished(self, res): |
- self.fp.close() |
- os.remove(self.tarname) |
- if self.debug: |
- log.msg('finished: stderr=%r, rc=%r' % (self.stderr, self.rc)) |
- if self.stderr is None: |
- self.sendStatus({'rc': self.rc}) |
- else: |
- self.sendStatus({'stderr': self.stderr, 'rc': self.rc}) |
- return res |
- |
-registerSlaveCommand("uploadDirectory", SlaveDirectoryUploadCommand, command_version) |
- |
- |
-class SlaveFileDownloadCommand(Command): |
- """ |
- Download a file from master to slave |
- Arguments: |
- |
- - ['workdir']: base directory to use |
- - ['slavedest']: name of the slave-side file to be created |
- - ['reader']: RemoteReference to a transfer._FileReader object |
- - ['maxsize']: max size (in bytes) of file to write |
- - ['blocksize']: max size for each data block |
- - ['mode']: access mode for the new file |
- """ |
- debug = False |
- |
- def setup(self, args): |
- self.workdir = args['workdir'] |
- self.filename = args['slavedest'] |
- self.reader = args['reader'] |
- self.bytes_remaining = args['maxsize'] |
- self.blocksize = args['blocksize'] |
- self.mode = args['mode'] |
- self.stderr = None |
- self.rc = 0 |
- |
- def start(self): |
- if self.debug: |
- log.msg('SlaveFileDownloadCommand starting') |
- |
- # Open file |
- self.path = os.path.join(self.builder.basedir, |
- self.workdir, |
- os.path.expanduser(self.filename)) |
- |
- dirname = os.path.dirname(self.path) |
- if not os.path.exists(dirname): |
- os.makedirs(dirname) |
- |
- try: |
- self.fp = open(self.path, 'wb') |
- if self.debug: |
- log.msg('Opened %r for download' % self.path) |
- if self.mode is not None: |
- # note: there is a brief window during which the new file |
- # will have the buildslave's default (umask) mode before we |
- # set the new one. Don't use this mode= feature to keep files |
- # private: use the buildslave's umask for that instead. (it |
- # is possible to call os.umask() before and after the open() |
- # call, but cleaning up from exceptions properly is more of a |
- # nuisance that way). |
- os.chmod(self.path, self.mode) |
- except IOError: |
- # TODO: this still needs cleanup |
- self.fp = None |
- self.stderr = 'Cannot open file %r for download' % self.path |
- self.rc = 1 |
- if self.debug: |
- log.msg('Cannot open file %r for download' % self.path) |
- |
- d = defer.Deferred() |
- reactor.callLater(0, self._loop, d) |
- def _close(res): |
- # close the file, but pass through any errors from _loop |
- d1 = self.reader.callRemote('close') |
- d1.addErrback(log.err) |
- d1.addCallback(lambda ignored: res) |
- return d1 |
- d.addBoth(_close) |
- d.addBoth(self.finished) |
- return d |
- |
- def _loop(self, fire_when_done): |
- d = defer.maybeDeferred(self._readBlock) |
- def _done(finished): |
- if finished: |
- fire_when_done.callback(None) |
- else: |
- self._loop(fire_when_done) |
- def _err(why): |
- fire_when_done.errback(why) |
- d.addCallbacks(_done, _err) |
- return None |
- |
- def _readBlock(self): |
- """Read a block of data from the remote reader.""" |
- |
- if self.interrupted or self.fp is None: |
- if self.debug: |
- log.msg('SlaveFileDownloadCommand._readBlock(): end') |
- return True |
- |
- length = self.blocksize |
- if self.bytes_remaining is not None and length > self.bytes_remaining: |
- length = self.bytes_remaining |
- |
- if length <= 0: |
- if self.stderr is None: |
- self.stderr = 'Maximum filesize reached, truncating file %r' \ |
- % self.path |
- self.rc = 1 |
- return True |
- else: |
- d = self.reader.callRemote('read', length) |
- d.addCallback(self._writeData) |
- return d |
- |
- def _writeData(self, data): |
- if self.debug: |
- log.msg('SlaveFileDownloadCommand._readBlock(): readlen=%d' % |
- len(data)) |
- if len(data) == 0: |
- return True |
- |
- if self.bytes_remaining is not None: |
- self.bytes_remaining = self.bytes_remaining - len(data) |
- assert self.bytes_remaining >= 0 |
- self.fp.write(data) |
- return False |
- |
- def interrupt(self): |
- if self.debug: |
- log.msg('interrupted') |
- if self.interrupted: |
- return |
- if self.stderr is None: |
- self.stderr = 'Download of %r interrupted' % self.path |
- self.rc = 1 |
- self.interrupted = True |
- # now we wait for the next read request to return. _readBlock will |
- # abandon the file when it sees self.interrupted set. |
- |
- def finished(self, res): |
- if self.fp is not None: |
- self.fp.close() |
- |
- if self.debug: |
- log.msg('finished: stderr=%r, rc=%r' % (self.stderr, self.rc)) |
- if self.stderr is None: |
- self.sendStatus({'rc': self.rc}) |
- else: |
- self.sendStatus({'stderr': self.stderr, 'rc': self.rc}) |
- return res |
- |
-registerSlaveCommand("downloadFile", SlaveFileDownloadCommand, command_version) |
- |
- |
- |
-class SlaveShellCommand(Command): |
- """This is a Command which runs a shell command. The args dict contains |
- the following keys: |
- |
- - ['command'] (required): a shell command to run. If this is a string, |
- it will be run with /bin/sh (['/bin/sh', |
- '-c', command]). If it is a list |
- (preferred), it will be used directly. |
- - ['workdir'] (required): subdirectory in which the command will be |
- run, relative to the builder dir |
- - ['env']: a dict of environment variables to augment/replace |
- os.environ . PYTHONPATH is treated specially, and |
- should be a list of path components to be prepended to |
- any existing PYTHONPATH environment variable. |
- - ['initial_stdin']: a string which will be written to the command's |
- stdin as soon as it starts |
- - ['keep_stdin_open']: unless True, the command's stdin will be |
- closed as soon as initial_stdin has been |
- written. Set this to True if you plan to write |
- to stdin after the command has been started. |
- - ['want_stdout']: 0 if stdout should be thrown away |
- - ['want_stderr']: 0 if stderr should be thrown away |
- - ['usePTY']: True or False if the command should use a PTY (defaults to |
- configuration of the slave) |
- - ['not_really']: 1 to skip execution and return rc=0 |
- - ['timeout']: seconds of silence to tolerate before killing command |
- - ['maxTime']: seconds before killing command |
- - ['logfiles']: dict mapping LogFile name to the workdir-relative |
- filename of a local log file. This local file will be |
- watched just like 'tail -f', and all changes will be |
- written to 'log' status updates. |
- - ['logEnviron']: False to not log the environment variables on the slave |
- |
- ShellCommand creates the following status messages: |
- - {'stdout': data} : when stdout data is available |
- - {'stderr': data} : when stderr data is available |
- - {'header': data} : when headers (command start/stop) are available |
- - {'log': (logfile_name, data)} : when log files have new contents |
- - {'rc': rc} : when the process has terminated |
- """ |
- |
- def start(self): |
- args = self.args |
- # args['workdir'] is relative to Builder directory, and is required. |
- assert args['workdir'] is not None |
- workdir = os.path.join(self.builder.basedir, args['workdir']) |
- |
- c = ShellCommand(self.builder, args['command'], |
- workdir, environ=args.get('env'), |
- timeout=args.get('timeout', None), |
- maxTime=args.get('maxTime', None), |
- sendStdout=args.get('want_stdout', True), |
- sendStderr=args.get('want_stderr', True), |
- sendRC=True, |
- initialStdin=args.get('initial_stdin'), |
- keepStdinOpen=args.get('keep_stdin_open'), |
- logfiles=args.get('logfiles', {}), |
- usePTY=args.get('usePTY', "slave-config"), |
- logEnviron=args.get('logEnviron', True), |
- ) |
- self.command = c |
- d = self.command.start() |
- return d |
- |
- def interrupt(self): |
- self.interrupted = True |
- self.command.kill("command interrupted") |
- |
- def writeStdin(self, data): |
- self.command.writeStdin(data) |
- |
- def closeStdin(self): |
- self.command.closeStdin() |
- |
-registerSlaveCommand("shell", SlaveShellCommand, command_version) |
- |
- |
-class DummyCommand(Command): |
- """ |
- I am a dummy no-op command that by default takes 5 seconds to complete. |
- See L{buildbot.steps.dummy.RemoteDummy} |
- """ |
- |
- def start(self): |
- self.d = defer.Deferred() |
- log.msg(" starting dummy command [%s]" % self.stepId) |
- self.timer = reactor.callLater(1, self.doStatus) |
- return self.d |
- |
- def interrupt(self): |
- if self.interrupted: |
- return |
- self.timer.cancel() |
- self.timer = None |
- self.interrupted = True |
- self.finished() |
- |
- def doStatus(self): |
- log.msg(" sending intermediate status") |
- self.sendStatus({'stdout': 'data'}) |
- timeout = self.args.get('timeout', 5) + 1 |
- self.timer = reactor.callLater(timeout - 1, self.finished) |
- |
- def finished(self): |
- log.msg(" dummy command finished [%s]" % self.stepId) |
- if self.interrupted: |
- self.sendStatus({'rc': 1}) |
- else: |
- self.sendStatus({'rc': 0}) |
- self.d.callback(0) |
- |
-registerSlaveCommand("dummy", DummyCommand, command_version) |
- |
- |
-# this maps handle names to a callable. When the WaitCommand starts, this |
-# callable is invoked with no arguments. It should return a Deferred. When |
-# that Deferred fires, our WaitCommand will finish. |
-waitCommandRegistry = {} |
- |
-class WaitCommand(Command): |
- """ |
- I am a dummy command used by the buildbot unit test suite. I want for the |
- unit test to tell us to finish. See L{buildbot.steps.dummy.Wait} |
- """ |
- |
- def start(self): |
- self.d = defer.Deferred() |
- log.msg(" starting wait command [%s]" % self.stepId) |
- handle = self.args['handle'] |
- cb = waitCommandRegistry[handle] |
- del waitCommandRegistry[handle] |
- def _called(): |
- log.msg(" wait-%s starting" % (handle,)) |
- d = cb() |
- def _done(res): |
- log.msg(" wait-%s finishing: %s" % (handle, res)) |
- return res |
- d.addBoth(_done) |
- d.addCallbacks(self.finished, self.failed) |
- reactor.callLater(0, _called) |
- return self.d |
- |
- def interrupt(self): |
- log.msg(" wait command interrupted") |
- if self.interrupted: |
- return |
- self.interrupted = True |
- self.finished("interrupted") |
- |
- def finished(self, res): |
- log.msg(" wait command finished [%s]" % self.stepId) |
- if self.interrupted: |
- self.sendStatus({'rc': 2}) |
- else: |
- self.sendStatus({'rc': 0}) |
- self.d.callback(0) |
- def failed(self, why): |
- log.msg(" wait command failed [%s]" % self.stepId) |
- self.sendStatus({'rc': 1}) |
- self.d.callback(0) |
- |
-registerSlaveCommand("dummy.wait", WaitCommand, command_version) |
- |
- |
-class SourceBase(Command): |
- """Abstract base class for Version Control System operations (checkout |
- and update). This class extracts the following arguments from the |
- dictionary received from the master: |
- |
- - ['workdir']: (required) the subdirectory where the buildable sources |
- should be placed |
- |
- - ['mode']: one of update/copy/clobber/export, defaults to 'update' |
- |
- - ['revision']: If not None, this is an int or string which indicates |
- which sources (along a time-like axis) should be used. |
- It is the thing you provide as the CVS -r or -D |
- argument. |
- |
- - ['patch']: If not None, this is a tuple of (striplevel, patch) |
- which contains a patch that should be applied after the |
- checkout has occurred. Once applied, the tree is no |
- longer eligible for use with mode='update', and it only |
- makes sense to use this in conjunction with a |
- ['revision'] argument. striplevel is an int, and patch |
- is a string in standard unified diff format. The patch |
- will be applied with 'patch -p%d <PATCH', with |
- STRIPLEVEL substituted as %d. The command will fail if |
- the patch process fails (rejected hunks). |
- |
- - ['timeout']: seconds of silence tolerated before we kill off the |
- command |
- |
- - ['maxTime']: seconds before we kill off the command |
- |
- - ['retry']: If not None, this is a tuple of (delay, repeats) |
- which means that any failed VC updates should be |
- reattempted, up to REPEATS times, after a delay of |
- DELAY seconds. This is intended to deal with slaves |
- that experience transient network failures. |
- """ |
- |
- sourcedata = "" |
- |
- def setup(self, args): |
- # if we need to parse the output, use this environment. Otherwise |
- # command output will be in whatever the buildslave's native language |
- # has been set to. |
- self.env = os.environ.copy() |
- self.env['LC_MESSAGES'] = "C" |
- |
- self.workdir = args['workdir'] |
- self.mode = args.get('mode', "update") |
- self.revision = args.get('revision') |
- self.patch = args.get('patch') |
- self.timeout = args.get('timeout', 120) |
- self.maxTime = args.get('maxTime', None) |
- self.retry = args.get('retry') |
- # VC-specific subclasses should override this to extract more args. |
- # Make sure to upcall! |
- |
- def start(self): |
- self.sendStatus({'header': "starting " + self.header + "\n"}) |
- self.command = None |
- |
- # self.srcdir is where the VC system should put the sources |
- if self.mode == "copy": |
- self.srcdir = "source" # hardwired directory name, sorry |
- else: |
- self.srcdir = self.workdir |
- self.sourcedatafile = os.path.join(self.builder.basedir, |
- self.srcdir, |
- ".buildbot-sourcedata") |
- |
- d = defer.succeed(None) |
- self.maybeClobber(d) |
- if not (self.sourcedirIsUpdateable() and self.sourcedataMatches()): |
- # the directory cannot be updated, so we have to clobber it. |
- # Perhaps the master just changed modes from 'export' to |
- # 'update'. |
- d.addCallback(self.doClobber, self.srcdir) |
- |
- d.addCallback(self.doVC) |
- |
- if self.mode == "copy": |
- d.addCallback(self.doCopy) |
- if self.patch: |
- d.addCallback(self.doPatch) |
- d.addCallbacks(self._sendRC, self._checkAbandoned) |
- return d |
- |
- def maybeClobber(self, d): |
- # do we need to clobber anything? |
- if self.mode in ("copy", "clobber", "export"): |
- d.addCallback(self.doClobber, self.workdir) |
- |
- def interrupt(self): |
- self.interrupted = True |
- if self.command: |
- self.command.kill("command interrupted") |
- |
- def doVC(self, res): |
- if self.interrupted: |
- raise AbandonChain(1) |
- if self.sourcedirIsUpdateable() and self.sourcedataMatches(): |
- d = self.doVCUpdate() |
- d.addCallback(self.maybeDoVCFallback) |
- else: |
- d = self.doVCFull() |
- d.addBoth(self.maybeDoVCRetry) |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(self._handleGotRevision) |
- d.addCallback(self.writeSourcedata) |
- return d |
- |
- def sourcedataMatches(self): |
- try: |
- olddata = self.readSourcedata() |
- if olddata != self.sourcedata: |
- return False |
- except IOError: |
- return False |
- return True |
- |
- def sourcedirIsPatched(self): |
- return os.path.exists(os.path.join(self.builder.basedir, |
- self.workdir, |
- ".buildbot-patched")) |
- |
- def _handleGotRevision(self, res): |
- d = defer.maybeDeferred(self.parseGotRevision) |
- d.addCallback(lambda got_revision: |
- self.sendStatus({'got_revision': got_revision})) |
- return d |
- |
- def parseGotRevision(self): |
- """Override this in a subclass. It should return a string that |
- represents which revision was actually checked out, or a Deferred |
- that will fire with such a string. If, in a future build, you were to |
- pass this 'got_revision' string in as the 'revision' component of a |
- SourceStamp, you should wind up with the same source code as this |
- checkout just obtained. |
- |
- It is probably most useful to scan self.command.stdout for a string |
- of some sort. Be sure to set keepStdout=True on the VC command that |
- you run, so that you'll have something available to look at. |
- |
- If this information is unavailable, just return None.""" |
- |
- return None |
- |
- def readSourcedata(self): |
- return open(self.sourcedatafile, "r").read() |
- |
- def writeSourcedata(self, res): |
- open(self.sourcedatafile, "w").write(self.sourcedata) |
- return res |
- |
- def sourcedirIsUpdateable(self): |
- """Returns True if the tree can be updated.""" |
- raise NotImplementedError("this must be implemented in a subclass") |
- |
- def doVCUpdate(self): |
- """Returns a deferred with the steps to update a checkout.""" |
- raise NotImplementedError("this must be implemented in a subclass") |
- |
- def doVCFull(self): |
- """Returns a deferred with the steps to do a fresh checkout.""" |
- raise NotImplementedError("this must be implemented in a subclass") |
- |
- def maybeDoVCFallback(self, rc): |
- if type(rc) is int and rc == 0: |
- return rc |
- if self.interrupted: |
- raise AbandonChain(1) |
- msg = "update failed, clobbering and trying again" |
- self.sendStatus({'header': msg + "\n"}) |
- log.msg(msg) |
- d = self.doClobber(None, self.srcdir) |
- d.addCallback(self.doVCFallback2) |
- return d |
- |
- def doVCFallback2(self, res): |
- msg = "now retrying VC operation" |
- self.sendStatus({'header': msg + "\n"}) |
- log.msg(msg) |
- d = self.doVCFull() |
- d.addBoth(self.maybeDoVCRetry) |
- d.addCallback(self._abandonOnFailure) |
- return d |
- |
- def maybeDoVCRetry(self, res): |
- """We get here somewhere after a VC chain has finished. res could |
- be:: |
- |
- - 0: the operation was successful |
- - nonzero: the operation failed. retry if possible |
- - AbandonChain: the operation failed, someone else noticed. retry. |
- - Failure: some other exception, re-raise |
- """ |
- |
- if isinstance(res, failure.Failure): |
- if self.interrupted: |
- return res # don't re-try interrupted builds |
- res.trap(AbandonChain) |
- else: |
- if type(res) is int and res == 0: |
- return res |
- if self.interrupted: |
- raise AbandonChain(1) |
- # if we get here, we should retry, if possible |
- if self.retry: |
- delay, repeats = self.retry |
- if repeats >= 0: |
- self.retry = (delay, repeats-1) |
- msg = ("update failed, trying %d more times after %d seconds" |
- % (repeats, delay)) |
- self.sendStatus({'header': msg + "\n"}) |
- log.msg(msg) |
- d = defer.Deferred() |
- self.maybeClobber(d) |
- d.addCallback(lambda res: self.doVCFull()) |
- d.addBoth(self.maybeDoVCRetry) |
- reactor.callLater(delay, d.callback, None) |
- return d |
- return res |
- |
- def doClobber(self, dummy, dirname, chmodDone=False): |
- # TODO: remove the old tree in the background |
-## workdir = os.path.join(self.builder.basedir, self.workdir) |
-## deaddir = self.workdir + ".deleting" |
-## if os.path.isdir(workdir): |
-## try: |
-## os.rename(workdir, deaddir) |
-## # might fail if deaddir already exists: previous deletion |
-## # hasn't finished yet |
-## # start the deletion in the background |
-## # TODO: there was a solaris/NetApp/NFS problem where a |
-## # process that was still running out of the directory we're |
-## # trying to delete could prevent the rm-rf from working. I |
-## # think it stalled the rm, but maybe it just died with |
-## # permission issues. Try to detect this. |
-## os.commands("rm -rf %s &" % deaddir) |
-## except: |
-## # fall back to sequential delete-then-checkout |
-## pass |
- d = os.path.join(self.builder.basedir, dirname) |
- if runtime.platformType != "posix": |
- # if we're running on w32, use rmtree instead. It will block, |
- # but hopefully it won't take too long. |
- rmdirRecursive(d) |
- return defer.succeed(0) |
- command = ["rm", "-rf", d] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=0, timeout=self.timeout, maxTime=self.maxTime, |
- usePTY=False) |
- |
- self.command = c |
- # sendRC=0 means the rm command will send stdout/stderr to the |
- # master, but not the rc=0 when it finishes. That job is left to |
- # _sendRC |
- d = c.start() |
- # The rm -rf may fail if there is a left-over subdir with chmod 000 |
- # permissions. So if we get a failure, we attempt to chmod suitable |
- # permissions and re-try the rm -rf. |
- if chmodDone: |
- d.addCallback(self._abandonOnFailure) |
- else: |
- d.addCallback(lambda rc: self.doClobberTryChmodIfFail(rc, dirname)) |
- return d |
- |
- def doClobberTryChmodIfFail(self, rc, dirname): |
- assert isinstance(rc, int) |
- if rc == 0: |
- return defer.succeed(0) |
- # Attempt a recursive chmod and re-try the rm -rf after. |
- command = ["chmod", "-R", "u+rwx", os.path.join(self.builder.basedir, dirname)] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=0, timeout=self.timeout, maxTime=self.maxTime, |
- usePTY=False) |
- |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(lambda dummy: self.doClobber(dummy, dirname, True)) |
- return d |
- |
- def doCopy(self, res): |
- # now copy tree to workdir |
- fromdir = os.path.join(self.builder.basedir, self.srcdir) |
- todir = os.path.join(self.builder.basedir, self.workdir) |
- if runtime.platformType != "posix": |
- self.sendStatus({'header': "Since we're on a non-POSIX platform, " |
- "we're not going to try to execute cp in a subprocess, but instead " |
- "use shutil.copytree(), which will block until it is complete. " |
- "fromdir: %s, todir: %s\n" % (fromdir, todir)}) |
- shutil.copytree(fromdir, todir) |
- return defer.succeed(0) |
- |
- if not os.path.exists(os.path.dirname(todir)): |
- os.makedirs(os.path.dirname(todir)) |
- if os.path.exists(todir): |
- # I don't think this happens, but just in case.. |
- log.msg("cp target '%s' already exists -- cp will not do what you think!" % todir) |
- |
- command = ['cp', '-R', '-P', '-p', fromdir, todir] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self.timeout, maxTime=self.maxTime, |
- usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- return d |
- |
- def doPatch(self, res): |
- patchlevel = self.patch[0] |
- diff = self.patch[1] |
- root = None |
- if len(self.patch) >= 3: |
- root = self.patch[2] |
- command = [ |
- getCommand("patch"), |
- '-p%d' % patchlevel, |
- '--remove-empty-files', |
- '--force', |
- '--forward', |
- ] |
- dir = os.path.join(self.builder.basedir, self.workdir) |
- # Mark the directory so we don't try to update it later, or at least try |
- # to revert first. |
- marker = open(os.path.join(dir, ".buildbot-patched"), "w") |
- marker.write("patched\n") |
- marker.close() |
- |
- # Update 'dir' with the 'root' option. Make sure it is a subdirectory |
- # of dir. |
- if (root and |
- os.path.abspath(os.path.join(dir, root) |
- ).startswith(os.path.abspath(dir))): |
- dir = os.path.join(dir, root) |
- |
- # now apply the patch |
- c = ShellCommand(self.builder, command, dir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, initialStdin=diff, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- return d |
- |
- |
-class CVS(SourceBase): |
- """CVS-specific VC operation. In addition to the arguments handled by |
- SourceBase, this command reads the following keys: |
- |
- ['cvsroot'] (required): the CVSROOT repository string |
- ['cvsmodule'] (required): the module to be retrieved |
- ['branch']: a '-r' tag or branch name to use for the checkout/update |
- ['login']: a string for use as a password to 'cvs login' |
- ['global_options']: a list of strings to use before the CVS verb |
- ['checkout_options']: a list of strings to use after checkout, |
- but before revision and branch specifiers |
- """ |
- |
- header = "cvs operation" |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.vcexe = getCommand("cvs") |
- self.cvsroot = args['cvsroot'] |
- self.cvsmodule = args['cvsmodule'] |
- self.global_options = args.get('global_options', []) |
- self.checkout_options = args.get('checkout_options', []) |
- self.branch = args.get('branch') |
- self.login = args.get('login') |
- self.sourcedata = "%s\n%s\n%s\n" % (self.cvsroot, self.cvsmodule, |
- self.branch) |
- |
- def sourcedirIsUpdateable(self): |
- return (not self.sourcedirIsPatched() and |
- os.path.isdir(os.path.join(self.builder.basedir, |
- self.srcdir, "CVS"))) |
- |
- def start(self): |
- if self.login is not None: |
- # need to do a 'cvs login' command first |
- d = self.builder.basedir |
- command = ([self.vcexe, '-d', self.cvsroot] + self.global_options |
- + ['login']) |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, |
- initialStdin=self.login+"\n", usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(self._didLogin) |
- return d |
- else: |
- return self._didLogin(None) |
- |
- def _didLogin(self, res): |
- # now we really start |
- return SourceBase.start(self) |
- |
- def doVCUpdate(self): |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, '-z3'] + self.global_options + ['update', '-dP'] |
- if self.branch: |
- command += ['-r', self.branch] |
- if self.revision: |
- command += ['-D', self.revision] |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def doVCFull(self): |
- d = self.builder.basedir |
- if self.mode == "export": |
- verb = "export" |
- else: |
- verb = "checkout" |
- command = ([self.vcexe, '-d', self.cvsroot, '-z3'] + |
- self.global_options + |
- [verb, '-d', self.srcdir]) |
- |
- if verb == "checkout": |
- command += self.checkout_options |
- if self.branch: |
- command += ['-r', self.branch] |
- if self.revision: |
- command += ['-D', self.revision] |
- command += [self.cvsmodule] |
- |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def parseGotRevision(self): |
- # CVS does not have any kind of revision stamp to speak of. We return |
- # the current timestamp as a best-effort guess, but this depends upon |
- # the local system having a clock that is |
- # reasonably-well-synchronized with the repository. |
- return time.strftime("%Y-%m-%d %H:%M:%S +0000", time.gmtime()) |
- |
-registerSlaveCommand("cvs", CVS, command_version) |
- |
-class SVN(SourceBase): |
- """Subversion-specific VC operation. In addition to the arguments |
- handled by SourceBase, this command reads the following keys: |
- |
- ['svnurl'] (required): the SVN repository string |
- ['username']: Username passed to the svn command |
- ['password']: Password passed to the svn command |
- ['keep_on_purge']: Files and directories to keep between updates |
- ['ignore_ignores']: Ignore ignores when purging changes |
- ['always_purge']: Always purge local changes after each build |
- ['depth']: Pass depth argument to subversion 1.5+ |
- """ |
- |
- header = "svn operation" |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.vcexe = getCommand("svn") |
- self.svnurl = args['svnurl'] |
- self.sourcedata = "%s\n" % self.svnurl |
- self.keep_on_purge = args.get('keep_on_purge', []) |
- self.keep_on_purge.append(".buildbot-sourcedata") |
- self.ignore_ignores = args.get('ignore_ignores', True) |
- self.always_purge = args.get('always_purge', False) |
- |
- self.svn_args = [] |
- if args.has_key('username'): |
- self.svn_args.extend(["--username", args['username']]) |
- if args.has_key('password'): |
- self.svn_args.extend(["--password", Obfuscated(args['password'], "XXXX")]) |
- if args.get('extra_args', None) is not None: |
- self.svn_args.extend(args['extra_args']) |
- |
- if args.has_key('depth'): |
- self.svn_args.extend(["--depth",args['depth']]) |
- |
- def _dovccmd(self, command, args, rootdir=None, cb=None, **kwargs): |
- if rootdir is None: |
- rootdir = os.path.join(self.builder.basedir, self.srcdir) |
- fullCmd = [self.vcexe, command, '--non-interactive', '--no-auth-cache'] |
- fullCmd.extend(self.svn_args) |
- fullCmd.extend(args) |
- c = ShellCommand(self.builder, fullCmd, rootdir, |
- environ=self.env, sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False, **kwargs) |
- self.command = c |
- d = c.start() |
- if cb: |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(cb) |
- return d |
- |
- def sourcedirIsUpdateable(self): |
- return os.path.isdir(os.path.join(self.builder.basedir, |
- self.srcdir, ".svn")) |
- |
- def doVCUpdate(self): |
- if self.sourcedirIsPatched() or self.always_purge: |
- return self._purgeAndUpdate() |
- revision = self.args['revision'] or 'HEAD' |
- # update: possible for mode in ('copy', 'update') |
- return self._dovccmd('update', ['--revision', str(revision)], |
- keepStdout=True) |
- |
- def doVCFull(self): |
- revision = self.args['revision'] or 'HEAD' |
- args = ['--revision', str(revision), self.svnurl, self.srcdir] |
- if self.mode == "export": |
- command = 'export' |
- else: |
- # mode=='clobber', or copy/update on a broken workspace |
- command = 'checkout' |
- return self._dovccmd(command, args, rootdir=self.builder.basedir, |
- keepStdout=True) |
- |
- def _purgeAndUpdate(self): |
- """svn revert has several corner cases that make it unpractical. |
- |
- Use the Force instead and delete everything that shows up in status.""" |
- args = ['--xml'] |
- if self.ignore_ignores: |
- args.append('--no-ignore') |
- return self._dovccmd('status', args, keepStdout=True, sendStdout=False, |
- cb=self._purgeAndUpdate2) |
- |
- def _purgeAndUpdate2(self, res): |
- """Delete everything that shown up on status.""" |
- result_xml = parseString(self.command.stdout) |
- for entry in result_xml.getElementsByTagName('entry'): |
- filename = entry.getAttribute('path') |
- if filename in self.keep_on_purge: |
- continue |
- filepath = os.path.join(self.builder.basedir, self.workdir, |
- filename) |
- self.sendStatus({'stdout': "%s\n" % filepath}) |
- if os.path.isfile(filepath): |
- os.chmod(filepath, 0700) |
- os.remove(filepath) |
- else: |
- rmdirRecursive(filepath) |
- # Now safe to update. |
- revision = self.args['revision'] or 'HEAD' |
- return self._dovccmd('update', ['--revision', str(revision)], |
- keepStdout=True) |
- |
- def getSvnVersionCommand(self): |
- """ |
- Get the (shell) command used to determine SVN revision number |
- of checked-out code |
- |
- return: list of strings, passable as the command argument to ShellCommand |
- """ |
- # svn checkout operations finish with 'Checked out revision 16657.' |
- # svn update operations finish the line 'At revision 16654.' |
- # But we don't use those. Instead, run 'svnversion'. |
- svnversion_command = getCommand("svnversion") |
- # older versions of 'svnversion' (1.1.4) require the WC_PATH |
- # argument, newer ones (1.3.1) do not. |
- return [svnversion_command, "."] |
- |
- def parseGotRevision(self): |
- c = ShellCommand(self.builder, |
- self.getSvnVersionCommand(), |
- os.path.join(self.builder.basedir, self.srcdir), |
- environ=self.env, |
- sendStdout=False, sendStderr=False, sendRC=False, |
- keepStdout=True, usePTY=False) |
- d = c.start() |
- def _parse(res): |
- r_raw = c.stdout.strip() |
- # Extract revision from the version "number" string |
- r = r_raw.rstrip('MS') |
- r = r.split(':')[-1] |
- got_version = None |
- try: |
- got_version = int(r) |
- except ValueError: |
- msg =("SVN.parseGotRevision unable to parse output " |
- "of svnversion: '%s'" % r_raw) |
- log.msg(msg) |
- self.sendStatus({'header': msg + "\n"}) |
- return got_version |
- d.addCallback(_parse) |
- return d |
- |
- |
-registerSlaveCommand("svn", SVN, command_version) |
- |
-class Darcs(SourceBase): |
- """Darcs-specific VC operation. In addition to the arguments |
- handled by SourceBase, this command reads the following keys: |
- |
- ['repourl'] (required): the Darcs repository string |
- """ |
- |
- header = "darcs operation" |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.vcexe = getCommand("darcs") |
- self.repourl = args['repourl'] |
- self.sourcedata = "%s\n" % self.repourl |
- self.revision = self.args.get('revision') |
- |
- def sourcedirIsUpdateable(self): |
- # checking out a specific revision requires a full 'darcs get' |
- return (not self.revision and |
- not self.sourcedirIsPatched() and |
- os.path.isdir(os.path.join(self.builder.basedir, |
- self.srcdir, "_darcs"))) |
- |
- def doVCUpdate(self): |
- assert not self.revision |
- # update: possible for mode in ('copy', 'update') |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, 'pull', '--all', '--verbose'] |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def doVCFull(self): |
- # checkout or export |
- d = self.builder.basedir |
- command = [self.vcexe, 'get', '--verbose', '--partial', |
- '--repo-name', self.srcdir] |
- if self.revision: |
- # write the context to a file |
- n = os.path.join(self.builder.basedir, ".darcs-context") |
- f = open(n, "wb") |
- f.write(self.revision) |
- f.close() |
- # tell Darcs to use that context |
- command.append('--context') |
- command.append(n) |
- command.append(self.repourl) |
- |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- if self.revision: |
- d.addCallback(self.removeContextFile, n) |
- return d |
- |
- def removeContextFile(self, res, n): |
- os.unlink(n) |
- return res |
- |
- def parseGotRevision(self): |
- # we use 'darcs context' to find out what we wound up with |
- command = [self.vcexe, "changes", "--context"] |
- c = ShellCommand(self.builder, command, |
- os.path.join(self.builder.basedir, self.srcdir), |
- environ=self.env, |
- sendStdout=False, sendStderr=False, sendRC=False, |
- keepStdout=True, usePTY=False) |
- d = c.start() |
- d.addCallback(lambda res: c.stdout) |
- return d |
- |
-registerSlaveCommand("darcs", Darcs, command_version) |
- |
-class Monotone(SourceBase): |
- """Monotone-specific VC operation. In addition to the arguments handled |
- by SourceBase, this command reads the following keys: |
- |
- ['server_addr'] (required): the address of the server to pull from |
- ['branch'] (required): the branch the revision is on |
- ['db_path'] (required): the local database path to use |
- ['revision'] (required): the revision to check out |
- ['monotone']: (required): path to monotone executable |
- """ |
- |
- header = "monotone operation" |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.server_addr = args["server_addr"] |
- self.branch = args["branch"] |
- self.db_path = args["db_path"] |
- self.revision = args["revision"] |
- self.monotone = args["monotone"] |
- self._made_fulls = False |
- self._pull_timeout = args["timeout"] |
- |
- def _makefulls(self): |
- if not self._made_fulls: |
- basedir = self.builder.basedir |
- self.full_db_path = os.path.join(basedir, self.db_path) |
- self.full_srcdir = os.path.join(basedir, self.srcdir) |
- self._made_fulls = True |
- |
- def sourcedirIsUpdateable(self): |
- self._makefulls() |
- return (not self.sourcedirIsPatched() and |
- os.path.isfile(self.full_db_path) and |
- os.path.isdir(os.path.join(self.full_srcdir, "MT"))) |
- |
- def doVCUpdate(self): |
- return self._withFreshDb(self._doUpdate) |
- |
- def _doUpdate(self): |
- # update: possible for mode in ('copy', 'update') |
- command = [self.monotone, "update", |
- "-r", self.revision, |
- "-b", self.branch] |
- c = ShellCommand(self.builder, command, self.full_srcdir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def doVCFull(self): |
- return self._withFreshDb(self._doFull) |
- |
- def _doFull(self): |
- command = [self.monotone, "--db=" + self.full_db_path, |
- "checkout", |
- "-r", self.revision, |
- "-b", self.branch, |
- self.full_srcdir] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def _withFreshDb(self, callback): |
- self._makefulls() |
- # first ensure the db exists and is usable |
- if os.path.isfile(self.full_db_path): |
- # already exists, so run 'db migrate' in case monotone has been |
- # upgraded under us |
- command = [self.monotone, "db", "migrate", |
- "--db=" + self.full_db_path] |
- else: |
- # We'll be doing an initial pull, so up the timeout to 3 hours to |
- # make sure it will have time to complete. |
- self._pull_timeout = max(self._pull_timeout, 3 * 60 * 60) |
- self.sendStatus({"header": "creating database %s\n" |
- % (self.full_db_path,)}) |
- command = [self.monotone, "db", "init", |
- "--db=" + self.full_db_path] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(self._didDbInit) |
- d.addCallback(self._didPull, callback) |
- return d |
- |
- def _didDbInit(self, res): |
- command = [self.monotone, "--db=" + self.full_db_path, |
- "pull", "--ticker=dot", self.server_addr, self.branch] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self._pull_timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.sendStatus({"header": "pulling %s from %s\n" |
- % (self.branch, self.server_addr)}) |
- self.command = c |
- return c.start() |
- |
- def _didPull(self, res, callback): |
- return callback() |
- |
-registerSlaveCommand("monotone", Monotone, command_version) |
- |
- |
-class Git(SourceBase): |
- """Git specific VC operation. In addition to the arguments |
- handled by SourceBase, this command reads the following keys: |
- |
- ['repourl'] (required): the upstream GIT repository string |
- ['branch'] (optional): which version (i.e. branch or tag) to |
- retrieve. Default: "master". |
- ['submodules'] (optional): whether to initialize and update |
- submodules. Default: False. |
- ['ignore_ignores']: ignore ignores when purging changes. |
- """ |
- |
- header = "git operation" |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.vcexe = getCommand("git") |
- self.repourl = args['repourl'] |
- self.branch = args.get('branch') |
- if not self.branch: |
- self.branch = "master" |
- self.sourcedata = "%s %s\n" % (self.repourl, self.branch) |
- self.submodules = args.get('submodules') |
- self.ignore_ignores = args.get('ignore_ignores', True) |
- |
- def _fullSrcdir(self): |
- return os.path.join(self.builder.basedir, self.srcdir) |
- |
- def _commitSpec(self): |
- if self.revision: |
- return self.revision |
- return self.branch |
- |
- def sourcedirIsUpdateable(self): |
- return os.path.isdir(os.path.join(self._fullSrcdir(), ".git")) |
- |
- def _dovccmd(self, command, cb=None, **kwargs): |
- c = ShellCommand(self.builder, [self.vcexe] + command, self._fullSrcdir(), |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False, **kwargs) |
- self.command = c |
- d = c.start() |
- if cb: |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(cb) |
- return d |
- |
- # If the repourl matches the sourcedata file, then |
- # we can say that the sourcedata matches. We can |
- # ignore branch changes, since Git can work with |
- # many branches fetched, and we deal with it properly |
- # in doVCUpdate. |
- def sourcedataMatches(self): |
- try: |
- olddata = self.readSourcedata() |
- if not olddata.startswith(self.repourl+' '): |
- return False |
- except IOError: |
- return False |
- return True |
- |
- def _cleanSubmodules(self, res): |
- command = ['submodule', 'foreach', 'git', 'clean', '-d', '-f'] |
- if self.ignore_ignores: |
- command.append('-x') |
- return self._dovccmd(command) |
- |
- def _updateSubmodules(self, res): |
- return self._dovccmd(['submodule', 'update'], self._cleanSubmodules) |
- |
- def _initSubmodules(self, res): |
- if self.submodules: |
- return self._dovccmd(['submodule', 'init'], self._updateSubmodules) |
- else: |
- return defer.succeed(0) |
- |
- def _didHeadCheckout(self, res): |
- # Rename branch, so that the repo will have the expected branch name |
- # For further information about this, see the commit message |
- command = ['branch', '-M', self.branch] |
- return self._dovccmd(command, self._initSubmodules) |
- |
- def _didFetch(self, res): |
- if self.revision: |
- head = self.revision |
- else: |
- head = 'FETCH_HEAD' |
- |
- # That is not sufficient. git will leave unversioned files and empty |
- # directories. Clean them up manually in _didReset. |
- command = ['reset', '--hard', head] |
- return self._dovccmd(command, self._didHeadCheckout) |
- |
- # Update first runs "git clean", removing local changes, |
- # if the branch to be checked out has changed. This, combined |
- # with the later "git reset" equates clobbering the repo, |
- # but it's much more efficient. |
- def doVCUpdate(self): |
- try: |
- # Check to see if our branch has changed |
- diffbranch = self.sourcedata != self.readSourcedata() |
- except IOError: |
- diffbranch = False |
- if diffbranch: |
- command = ['git', 'clean', '-f', '-d'] |
- if self.ignore_ignores: |
- command.append('-x') |
- c = ShellCommand(self.builder, command, self._fullSrcdir(), |
- sendRC=False, timeout=self.timeout, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(self._didClean) |
- return d |
- return self._didClean(None) |
- |
- def _doFetch(self, dummy): |
- # The plus will make sure the repo is moved to the branch's |
- # head even if it is not a simple "fast-forward" |
- command = ['fetch', '-t', self.repourl, '+%s' % self.branch] |
- self.sendStatus({"header": "fetching branch %s from %s\n" |
- % (self.branch, self.repourl)}) |
- return self._dovccmd(command, self._didFetch) |
- |
- def _didClean(self, dummy): |
- # After a clean, try to use the given revision if we have one. |
- if self.revision: |
- # We know what revision we want. See if we have it. |
- d = self._dovccmd(['reset', '--hard', self.revision], |
- self._initSubmodules) |
- # If we are unable to reset to the specified version, we |
- # must do a fetch first and retry. |
- d.addErrback(self._doFetch) |
- return d |
- else: |
- # No known revision, go grab the latest. |
- return self._doFetch(None) |
- |
- def _didInit(self, res): |
- return self.doVCUpdate() |
- |
- def doVCFull(self): |
- os.makedirs(self._fullSrcdir()) |
- return self._dovccmd(['init'], self._didInit) |
- |
- def parseGotRevision(self): |
- command = ['rev-parse', 'HEAD'] |
- def _parse(res): |
- hash = self.command.stdout.strip() |
- if len(hash) != 40: |
- return None |
- return hash |
- return self._dovccmd(command, _parse, keepStdout=True) |
- |
-registerSlaveCommand("git", Git, command_version) |
- |
-class Arch(SourceBase): |
- """Arch-specific (tla-specific) VC operation. In addition to the |
- arguments handled by SourceBase, this command reads the following keys: |
- |
- ['url'] (required): the repository string |
- ['version'] (required): which version (i.e. branch) to retrieve |
- ['revision'] (optional): the 'patch-NN' argument to check out |
- ['archive']: the archive name to use. If None, use the archive's default |
- ['build-config']: if present, give to 'tla build-config' after checkout |
- """ |
- |
- header = "arch operation" |
- buildconfig = None |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.vcexe = getCommand("tla") |
- self.archive = args.get('archive') |
- self.url = args['url'] |
- self.version = args['version'] |
- self.revision = args.get('revision') |
- self.buildconfig = args.get('build-config') |
- self.sourcedata = "%s\n%s\n%s\n" % (self.url, self.version, |
- self.buildconfig) |
- |
- def sourcedirIsUpdateable(self): |
- # Arch cannot roll a directory backwards, so if they ask for a |
- # specific revision, clobber the directory. Technically this |
- # could be limited to the cases where the requested revision is |
- # later than our current one, but it's too hard to extract the |
- # current revision from the tree. |
- return (not self.revision and |
- not self.sourcedirIsPatched() and |
- os.path.isdir(os.path.join(self.builder.basedir, |
- self.srcdir, "{arch}"))) |
- |
- def doVCUpdate(self): |
- # update: possible for mode in ('copy', 'update') |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, 'replay'] |
- if self.revision: |
- command.append(self.revision) |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def doVCFull(self): |
- # to do a checkout, we must first "register" the archive by giving |
- # the URL to tla, which will go to the repository at that URL and |
- # figure out the archive name. tla will tell you the archive name |
- # when it is done, and all further actions must refer to this name. |
- |
- command = [self.vcexe, 'register-archive', '--force', self.url] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, keepStdout=True, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(self._didRegister, c) |
- return d |
- |
- def _didRegister(self, res, c): |
- # find out what tla thinks the archive name is. If the user told us |
- # to use something specific, make sure it matches. |
- r = re.search(r'Registering archive: (\S+)\s*$', c.stdout) |
- if r: |
- msg = "tla reports archive name is '%s'" % r.group(1) |
- log.msg(msg) |
- self.builder.sendUpdate({'header': msg+"\n"}) |
- if self.archive and r.group(1) != self.archive: |
- msg = (" mismatch, we wanted an archive named '%s'" |
- % self.archive) |
- log.msg(msg) |
- self.builder.sendUpdate({'header': msg+"\n"}) |
- raise AbandonChain(-1) |
- self.archive = r.group(1) |
- assert self.archive, "need archive name to continue" |
- return self._doGet() |
- |
- def _doGet(self): |
- ver = self.version |
- if self.revision: |
- ver += "--%s" % self.revision |
- command = [self.vcexe, 'get', '--archive', self.archive, |
- '--no-pristine', |
- ver, self.srcdir] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- if self.buildconfig: |
- d.addCallback(self._didGet) |
- return d |
- |
- def _didGet(self, res): |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, 'build-config', self.buildconfig] |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- return d |
- |
- def parseGotRevision(self): |
- # using code from tryclient.TlaExtractor |
- # 'tla logs --full' gives us ARCHIVE/BRANCH--REVISION |
- # 'tla logs' gives us REVISION |
- command = [self.vcexe, "logs", "--full", "--reverse"] |
- c = ShellCommand(self.builder, command, |
- os.path.join(self.builder.basedir, self.srcdir), |
- environ=self.env, |
- sendStdout=False, sendStderr=False, sendRC=False, |
- keepStdout=True, usePTY=False) |
- d = c.start() |
- def _parse(res): |
- tid = c.stdout.split("\n")[0].strip() |
- slash = tid.index("/") |
- dd = tid.rindex("--") |
- #branch = tid[slash+1:dd] |
- baserev = tid[dd+2:] |
- return baserev |
- d.addCallback(_parse) |
- return d |
- |
-registerSlaveCommand("arch", Arch, command_version) |
- |
-class Bazaar(Arch): |
- """Bazaar (/usr/bin/baz) is an alternative client for Arch repositories. |
- It is mostly option-compatible, but archive registration is different |
- enough to warrant a separate Command. |
- |
- ['archive'] (required): the name of the archive being used |
- """ |
- |
- def setup(self, args): |
- Arch.setup(self, args) |
- self.vcexe = getCommand("baz") |
- # baz doesn't emit the repository name after registration (and |
- # grepping through the output of 'baz archives' is too hard), so we |
- # require that the buildmaster configuration to provide both the |
- # archive name and the URL. |
- self.archive = args['archive'] # required for Baz |
- self.sourcedata = "%s\n%s\n%s\n" % (self.url, self.version, |
- self.buildconfig) |
- |
- # in _didRegister, the regexp won't match, so we'll stick with the name |
- # in self.archive |
- |
- def _doGet(self): |
- # baz prefers ARCHIVE/VERSION. This will work even if |
- # my-default-archive is not set. |
- ver = self.archive + "/" + self.version |
- if self.revision: |
- ver += "--%s" % self.revision |
- command = [self.vcexe, 'get', '--no-pristine', |
- ver, self.srcdir] |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- if self.buildconfig: |
- d.addCallback(self._didGet) |
- return d |
- |
- def parseGotRevision(self): |
- # using code from tryclient.BazExtractor |
- command = [self.vcexe, "tree-id"] |
- c = ShellCommand(self.builder, command, |
- os.path.join(self.builder.basedir, self.srcdir), |
- environ=self.env, |
- sendStdout=False, sendStderr=False, sendRC=False, |
- keepStdout=True, usePTY=False) |
- d = c.start() |
- def _parse(res): |
- tid = c.stdout.strip() |
- slash = tid.index("/") |
- dd = tid.rindex("--") |
- #branch = tid[slash+1:dd] |
- baserev = tid[dd+2:] |
- return baserev |
- d.addCallback(_parse) |
- return d |
- |
-registerSlaveCommand("bazaar", Bazaar, command_version) |
- |
- |
-class Bzr(SourceBase): |
- """bzr-specific VC operation. In addition to the arguments |
- handled by SourceBase, this command reads the following keys: |
- |
- ['repourl'] (required): the Bzr repository string |
- """ |
- |
- header = "bzr operation" |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.vcexe = getCommand("bzr") |
- self.repourl = args['repourl'] |
- self.sourcedata = "%s\n" % self.repourl |
- self.revision = self.args.get('revision') |
- self.forceSharedRepo = args.get('forceSharedRepo') |
- |
- def sourcedirIsUpdateable(self): |
- # checking out a specific revision requires a full 'bzr checkout' |
- return (not self.revision and |
- not self.sourcedirIsPatched() and |
- os.path.isdir(os.path.join(self.builder.basedir, |
- self.srcdir, ".bzr"))) |
- |
- def start(self): |
- def cont(res): |
- # Continue with start() method in superclass. |
- return SourceBase.start(self) |
- |
- if self.forceSharedRepo: |
- d = self.doForceSharedRepo(); |
- d.addCallback(cont) |
- return d |
- else: |
- return cont(None) |
- |
- def doVCUpdate(self): |
- assert not self.revision |
- # update: possible for mode in ('copy', 'update') |
- srcdir = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, 'update'] |
- c = ShellCommand(self.builder, command, srcdir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def doVCFull(self): |
- # checkout or export |
- d = self.builder.basedir |
- if self.mode == "export": |
- # exporting in bzr requires a separate directory |
- return self.doVCExport() |
- # originally I added --lightweight here, but then 'bzr revno' is |
- # wrong. The revno reported in 'bzr version-info' is correct, |
- # however. Maybe this is a bzr bug? |
- # |
- # In addition, you cannot perform a 'bzr update' on a repo pulled |
- # from an HTTP repository that used 'bzr checkout --lightweight'. You |
- # get a "ERROR: Cannot lock: transport is read only" when you try. |
- # |
- # So I won't bother using --lightweight for now. |
- |
- command = [self.vcexe, 'checkout'] |
- if self.revision: |
- command.append('--revision') |
- command.append(str(self.revision)) |
- command.append(self.repourl) |
- command.append(self.srcdir) |
- |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- return d |
- |
- def doVCExport(self): |
- tmpdir = os.path.join(self.builder.basedir, "export-temp") |
- srcdir = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, 'checkout', '--lightweight'] |
- if self.revision: |
- command.append('--revision') |
- command.append(str(self.revision)) |
- command.append(self.repourl) |
- command.append(tmpdir) |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- d = c.start() |
- def _export(res): |
- command = [self.vcexe, 'export', srcdir] |
- c = ShellCommand(self.builder, command, tmpdir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- d.addCallback(_export) |
- return d |
- |
- def doForceSharedRepo(self): |
- # Don't send stderr. When there is no shared repo, this might confuse |
- # users, as they will see a bzr error message. But having no shared |
- # repo is not an error, just an indication that we need to make one. |
- c = ShellCommand(self.builder, [self.vcexe, 'info', '.'], |
- self.builder.basedir, |
- sendStderr=False, sendRC=False, usePTY=False) |
- d = c.start() |
- def afterCheckSharedRepo(res): |
- if type(res) is int and res != 0: |
- log.msg("No shared repo found, creating it") |
- # bzr info fails, try to create shared repo. |
- c = ShellCommand(self.builder, [self.vcexe, 'init-repo', '.'], |
- self.builder.basedir, |
- sendRC=False, usePTY=False) |
- self.command = c |
- return c.start() |
- else: |
- return defer.succeed(res) |
- d.addCallback(afterCheckSharedRepo) |
- return d |
- |
- def get_revision_number(self, out): |
- # it feels like 'bzr revno' sometimes gives different results than |
- # the 'revno:' line from 'bzr version-info', and the one from |
- # version-info is more likely to be correct. |
- for line in out.split("\n"): |
- colon = line.find(":") |
- if colon != -1: |
- key, value = line[:colon], line[colon+2:] |
- if key == "revno": |
- return int(value) |
- raise ValueError("unable to find revno: in bzr output: '%s'" % out) |
- |
- def parseGotRevision(self): |
- command = [self.vcexe, "version-info"] |
- c = ShellCommand(self.builder, command, |
- os.path.join(self.builder.basedir, self.srcdir), |
- environ=self.env, |
- sendStdout=False, sendStderr=False, sendRC=False, |
- keepStdout=True, usePTY=False) |
- d = c.start() |
- def _parse(res): |
- try: |
- return self.get_revision_number(c.stdout) |
- except ValueError: |
- msg =("Bzr.parseGotRevision unable to parse output " |
- "of bzr version-info: '%s'" % c.stdout.strip()) |
- log.msg(msg) |
- self.sendStatus({'header': msg + "\n"}) |
- return None |
- d.addCallback(_parse) |
- return d |
- |
-registerSlaveCommand("bzr", Bzr, command_version) |
- |
-class Mercurial(SourceBase): |
- """Mercurial specific VC operation. In addition to the arguments |
- handled by SourceBase, this command reads the following keys: |
- |
- ['repourl'] (required): the Mercurial repository string |
- ['clobberOnBranchChange']: Document me. See ticket #462. |
- """ |
- |
- header = "mercurial operation" |
- |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.vcexe = getCommand("hg") |
- self.repourl = args['repourl'] |
- self.clobberOnBranchChange = args.get('clobberOnBranchChange', True) |
- self.sourcedata = "%s\n" % self.repourl |
- self.branchType = args.get('branchType', 'dirname') |
- self.stdout = "" |
- self.stderr = "" |
- |
- def sourcedirIsUpdateable(self): |
- return os.path.isdir(os.path.join(self.builder.basedir, |
- self.srcdir, ".hg")) |
- |
- def doVCUpdate(self): |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, 'pull', '--verbose', self.repourl] |
- c = ShellCommand(self.builder, command, d, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, keepStdout=True, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._handleEmptyUpdate) |
- d.addCallback(self._update) |
- return d |
- |
- def _handleEmptyUpdate(self, res): |
- if type(res) is int and res == 1: |
- if self.command.stdout.find("no changes found") != -1: |
- # 'hg pull', when it doesn't have anything to do, exits with |
- # rc=1, and there appears to be no way to shut this off. It |
- # emits a distinctive message to stdout, though. So catch |
- # this and pretend that it completed successfully. |
- return 0 |
- return res |
- |
- def doVCFull(self): |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe, 'clone', '--verbose', '--noupdate'] |
- |
- # if got revision, clobbering and in dirname, only clone to specific revision |
- # (otherwise, do full clone to re-use .hg dir for subsequent builds) |
- if self.args.get('revision') and self.mode == 'clobber' and self.branchType == 'dirname': |
- command.extend(['--rev', self.args.get('revision')]) |
- command.extend([self.repourl, d]) |
- |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- cmd1 = c.start() |
- cmd1.addCallback(self._update) |
- return cmd1 |
- |
- def _clobber(self, dummy, dirname): |
- def _vcfull(res): |
- return self.doVCFull() |
- |
- c = self.doClobber(dummy, dirname) |
- c.addCallback(_vcfull) |
- |
- return c |
- |
- def _purge(self, dummy, dirname): |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- purge = [self.vcexe, 'purge', '--all'] |
- purgeCmd = ShellCommand(self.builder, purge, d, |
- sendStdout=False, sendStderr=False, |
- keepStdout=True, keepStderr=True, usePTY=False) |
- |
- def _clobber(res): |
- if res != 0: |
- # purge failed, we need to switch to a classic clobber |
- msg = "'hg purge' failed: %s\n%s. Clobbering." % (purgeCmd.stdout, purgeCmd.stderr) |
- self.sendStatus({'header': msg + "\n"}) |
- log.msg(msg) |
- |
- return self._clobber(dummy, dirname) |
- |
- # Purge was a success, then we need to update |
- return self._update2(res) |
- |
- p = purgeCmd.start() |
- p.addCallback(_clobber) |
- return p |
- |
- def _update(self, res): |
- if res != 0: |
- return res |
- |
- # compare current branch to update |
- self.update_branch = self.args.get('branch', 'default') |
- |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- parentscmd = [self.vcexe, 'identify', '--num', '--branch'] |
- cmd = ShellCommand(self.builder, parentscmd, d, |
- sendStdout=False, sendStderr=False, |
- keepStdout=True, keepStderr=True, usePTY=False) |
- |
- self.clobber = None |
- |
- def _parseIdentify(res): |
- if res != 0: |
- msg = "'hg identify' failed: %s\n%s" % (cmd.stdout, cmd.stderr) |
- self.sendStatus({'header': msg + "\n"}) |
- log.msg(msg) |
- return res |
- |
- log.msg('Output: %s' % cmd.stdout) |
- |
- match = re.search(r'^(.+) (.+)$', cmd.stdout) |
- assert match |
- |
- rev = match.group(1) |
- current_branch = match.group(2) |
- |
- if rev == '-1': |
- msg = "Fresh hg repo, don't worry about in-repo branch name" |
- log.msg(msg) |
- |
- elif self.sourcedirIsPatched(): |
- self.clobber = self._purge |
- |
- elif self.update_branch != current_branch: |
- msg = "Working dir is on in-repo branch '%s' and build needs '%s'." % (current_branch, self.update_branch) |
- if self.clobberOnBranchChange: |
- msg += ' Cloberring.' |
- else: |
- msg += ' Updating.' |
- |
- self.sendStatus({'header': msg + "\n"}) |
- log.msg(msg) |
- |
- # Clobbers only if clobberOnBranchChange is set |
- if self.clobberOnBranchChange: |
- self.clobber = self._purge |
- |
- else: |
- msg = "Working dir on same in-repo branch as build (%s)." % (current_branch) |
- log.msg(msg) |
- |
- return 0 |
- |
- def _checkRepoURL(res): |
- parentscmd = [self.vcexe, 'paths', 'default'] |
- cmd2 = ShellCommand(self.builder, parentscmd, d, |
- sendStdout=False, sendStderr=False, |
- keepStdout=True, keepStderr=True, usePTY=False) |
- |
- def _parseRepoURL(res): |
- if res == 1: |
- if "not found!" == cmd2.stderr.strip(): |
- msg = "hg default path not set. Not checking repo url for clobber test" |
- log.msg(msg) |
- return 0 |
- else: |
- msg = "'hg paths default' failed: %s\n%s" % (cmd2.stdout, cmd2.stderr) |
- log.msg(msg) |
- return 1 |
- |
- oldurl = cmd2.stdout.strip() |
- |
- log.msg("Repo cloned from: '%s'" % oldurl) |
- |
- if sys.platform == "win32": |
- oldurl = oldurl.lower().replace('\\', '/') |
- repourl = self.repourl.lower().replace('\\', '/') |
- if repourl.startswith('file://'): |
- repourl = repourl.split('file://')[1] |
- else: |
- repourl = self.repourl |
- |
- oldurl = remove_userpassword(oldurl) |
- repourl = remove_userpassword(repourl) |
- |
- if oldurl != repourl: |
- self.clobber = self._clobber |
- msg = "RepoURL changed from '%s' in wc to '%s' in update. Clobbering" % (oldurl, repourl) |
- log.msg(msg) |
- |
- return 0 |
- |
- c = cmd2.start() |
- c.addCallback(_parseRepoURL) |
- return c |
- |
- def _maybeClobber(res): |
- if self.clobber: |
- msg = "Clobber flag set. Doing clobbering" |
- log.msg(msg) |
- |
- def _vcfull(res): |
- return self.doVCFull() |
- |
- return self.clobber(None, self.srcdir) |
- |
- return 0 |
- |
- c = cmd.start() |
- c.addCallback(_parseIdentify) |
- c.addCallback(_checkRepoURL) |
- c.addCallback(_maybeClobber) |
- c.addCallback(self._update2) |
- return c |
- |
- def _update2(self, res): |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- |
- updatecmd=[self.vcexe, 'update', '--clean', '--repository', d] |
- if self.args.get('revision'): |
- updatecmd.extend(['--rev', self.args['revision']]) |
- else: |
- updatecmd.extend(['--rev', self.args.get('branch', 'default')]) |
- self.command = ShellCommand(self.builder, updatecmd, |
- self.builder.basedir, sendRC=False, |
- timeout=self.timeout, maxTime=self.maxTime, usePTY=False) |
- return self.command.start() |
- |
- def parseGotRevision(self): |
- # we use 'hg identify' to find out what we wound up with |
- command = [self.vcexe, "identify"] |
- c = ShellCommand(self.builder, command, |
- os.path.join(self.builder.basedir, self.srcdir), |
- environ=self.env, |
- sendStdout=False, sendStderr=False, sendRC=False, |
- keepStdout=True, usePTY=False) |
- d = c.start() |
- def _parse(res): |
- m = re.search(r'^(\w+)', c.stdout) |
- return m.group(1) |
- d.addCallback(_parse) |
- return d |
- |
-registerSlaveCommand("hg", Mercurial, command_version) |
- |
- |
-class P4Base(SourceBase): |
- """Base class for P4 source-updaters |
- |
- ['p4port'] (required): host:port for server to access |
- ['p4user'] (optional): user to use for access |
- ['p4passwd'] (optional): passwd to try for the user |
- ['p4client'] (optional): client spec to use |
- """ |
- def setup(self, args): |
- SourceBase.setup(self, args) |
- self.p4port = args['p4port'] |
- self.p4client = args['p4client'] |
- self.p4user = args['p4user'] |
- self.p4passwd = args['p4passwd'] |
- |
- def parseGotRevision(self): |
- # Executes a p4 command that will give us the latest changelist number |
- # of any file under the current (or default) client: |
- command = ['p4'] |
- if self.p4port: |
- command.extend(['-p', self.p4port]) |
- if self.p4user: |
- command.extend(['-u', self.p4user]) |
- if self.p4passwd: |
- command.extend(['-P', Obfuscated(self.p4passwd, 'XXXXXXXX')]) |
- if self.p4client: |
- command.extend(['-c', self.p4client]) |
- # add '-s submitted' for bug #626 |
- command.extend(['changes', '-s', 'submitted', '-m', '1', '#have']) |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- environ=self.env, timeout=self.timeout, |
- maxTime=self.maxTime, sendStdout=True, |
- sendStderr=False, sendRC=False, keepStdout=True, |
- usePTY=False) |
- self.command = c |
- d = c.start() |
- |
- def _parse(res): |
- # 'p4 -c clien-name change -m 1 "#have"' will produce an output like: |
- # "Change 28147 on 2008/04/07 by p4user@hostname..." |
- # The number after "Change" is the one we want. |
- m = re.match('Change\s+(\d+)\s+', c.stdout) |
- if m: |
- return m.group(1) |
- return None |
- d.addCallback(_parse) |
- return d |
- |
- |
-class P4(P4Base): |
- """A P4 source-updater. |
- |
- ['p4port'] (required): host:port for server to access |
- ['p4user'] (optional): user to use for access |
- ['p4passwd'] (optional): passwd to try for the user |
- ['p4client'] (optional): client spec to use |
- ['p4extra_views'] (optional): additional client views to use |
- """ |
- |
- header = "p4" |
- |
- def setup(self, args): |
- P4Base.setup(self, args) |
- self.p4base = args['p4base'] |
- self.p4extra_views = args['p4extra_views'] |
- self.p4mode = args['mode'] |
- self.p4branch = args['branch'] |
- |
- self.sourcedata = str([ |
- # Perforce server. |
- self.p4port, |
- |
- # Client spec. |
- self.p4client, |
- |
- # Depot side of view spec. |
- self.p4base, |
- self.p4branch, |
- self.p4extra_views, |
- |
- # Local side of view spec (srcdir is made from these). |
- self.builder.basedir, |
- self.mode, |
- self.workdir |
- ]) |
- |
- |
- def sourcedirIsUpdateable(self): |
- # We assume our client spec is still around. |
- # We just say we aren't updateable if the dir doesn't exist so we |
- # don't get ENOENT checking the sourcedata. |
- return (not self.sourcedirIsPatched() and |
- os.path.isdir(os.path.join(self.builder.basedir, |
- self.srcdir))) |
- |
- def doVCUpdate(self): |
- return self._doP4Sync(force=False) |
- |
- def _doP4Sync(self, force): |
- command = ['p4'] |
- |
- if self.p4port: |
- command.extend(['-p', self.p4port]) |
- if self.p4user: |
- command.extend(['-u', self.p4user]) |
- if self.p4passwd: |
- command.extend(['-P', Obfuscated(self.p4passwd, 'XXXXXXXX')]) |
- if self.p4client: |
- command.extend(['-c', self.p4client]) |
- command.extend(['sync']) |
- if force: |
- command.extend(['-f']) |
- if self.revision: |
- command.extend(['@' + str(self.revision)]) |
- env = {} |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- environ=env, sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, keepStdout=True, usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- return d |
- |
- |
- def doVCFull(self): |
- env = {} |
- command = ['p4'] |
- client_spec = '' |
- client_spec += "Client: %s\n\n" % self.p4client |
- client_spec += "Owner: %s\n\n" % self.p4user |
- client_spec += "Description:\n\tCreated by %s\n\n" % self.p4user |
- client_spec += "Root:\t%s\n\n" % self.builder.basedir |
- client_spec += "Options:\tallwrite rmdir\n\n" |
- client_spec += "LineEnd:\tlocal\n\n" |
- |
- # Setup a view |
- client_spec += "View:\n\t%s" % (self.p4base) |
- if self.p4branch: |
- client_spec += "%s/" % (self.p4branch) |
- client_spec += "... //%s/%s/...\n" % (self.p4client, self.srcdir) |
- if self.p4extra_views: |
- for k, v in self.p4extra_views: |
- client_spec += "\t%s/... //%s/%s%s/...\n" % (k, self.p4client, |
- self.srcdir, v) |
- if self.p4port: |
- command.extend(['-p', self.p4port]) |
- if self.p4user: |
- command.extend(['-u', self.p4user]) |
- if self.p4passwd: |
- command.extend(['-P', Obfuscated(self.p4passwd, 'XXXXXXXX')]) |
- command.extend(['client', '-i']) |
- log.msg(client_spec) |
- c = ShellCommand(self.builder, command, self.builder.basedir, |
- environ=env, sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, initialStdin=client_spec, |
- usePTY=False) |
- self.command = c |
- d = c.start() |
- d.addCallback(self._abandonOnFailure) |
- d.addCallback(lambda _: self._doP4Sync(force=True)) |
- return d |
- |
- def parseGotRevision(self): |
- rv = None |
- if self.revision: |
- rv = str(self.revision) |
- return rv |
- |
-registerSlaveCommand("p4", P4, command_version) |
- |
- |
-class P4Sync(P4Base): |
- """A partial P4 source-updater. Requires manual setup of a per-slave P4 |
- environment. The only thing which comes from the master is P4PORT. |
- 'mode' is required to be 'copy'. |
- |
- ['p4port'] (required): host:port for server to access |
- ['p4user'] (optional): user to use for access |
- ['p4passwd'] (optional): passwd to try for the user |
- ['p4client'] (optional): client spec to use |
- """ |
- |
- header = "p4 sync" |
- |
- def setup(self, args): |
- P4Base.setup(self, args) |
- self.vcexe = getCommand("p4") |
- |
- def sourcedirIsUpdateable(self): |
- return True |
- |
- def _doVC(self, force): |
- d = os.path.join(self.builder.basedir, self.srcdir) |
- command = [self.vcexe] |
- if self.p4port: |
- command.extend(['-p', self.p4port]) |
- if self.p4user: |
- command.extend(['-u', self.p4user]) |
- if self.p4passwd: |
- command.extend(['-P', Obfuscated(self.p4passwd, 'XXXXXXXX')]) |
- if self.p4client: |
- command.extend(['-c', self.p4client]) |
- command.extend(['sync']) |
- if force: |
- command.extend(['-f']) |
- if self.revision: |
- command.extend(['@' + self.revision]) |
- env = {} |
- c = ShellCommand(self.builder, command, d, environ=env, |
- sendRC=False, timeout=self.timeout, |
- maxTime=self.maxTime, usePTY=False) |
- self.command = c |
- return c.start() |
- |
- def doVCUpdate(self): |
- return self._doVC(force=False) |
- |
- def doVCFull(self): |
- return self._doVC(force=True) |
- |
- def parseGotRevision(self): |
- rv = None |
- if self.revision: |
- rv = str(self.revision) |
- return rv |
- |
-registerSlaveCommand("p4sync", P4Sync, command_version) |