svn: ignore the trailing 'M' that indicates a modified file
[buildbot.git] / buildbot / slave / commands.py
blob4fd348e247f766ece8d7bb4951ec0357b761ae05
1 # -*- test-case-name: buildbot.test.test_slavecommand -*-
3 import os, re, signal, shutil, types, time
4 from stat import ST_CTIME, ST_MTIME, ST_SIZE
6 from zope.interface import implements
7 from twisted.internet.protocol import ProcessProtocol
8 from twisted.internet import reactor, defer, task
9 from twisted.python import log, failure, runtime
10 from twisted.python.procutils import which
12 from buildbot.slave.interfaces import ISlaveCommand
13 from buildbot.slave.registry import registerSlaveCommand
15 # this used to be a CVS $-style "Revision" auto-updated keyword, but since I
16 # moved to Darcs as the primary repository, this is updated manually each
17 # time this file is changed. The last cvs_ver that was here was 1.51 .
18 command_version = "2.3"
20 # version history:
21 # >=1.17: commands are interruptable
22 # >=1.28: Arch understands 'revision', added Bazaar
23 # >=1.33: Source classes understand 'retry'
24 # >=1.39: Source classes correctly handle changes in branch (except Git)
25 # Darcs accepts 'revision' (now all do but Git) (well, and P4Sync)
26 # Arch/Baz should accept 'build-config'
27 # >=1.51: (release 0.7.3)
28 # >= 2.1: SlaveShellCommand now accepts 'initial_stdin', 'keep_stdin_open',
29 # and 'logfiles'. It now sends 'log' messages in addition to
30 # stdout/stdin/header/rc. It acquired writeStdin/closeStdin methods,
31 # but these are not remotely callable yet.
32 # (not externally visible: ShellCommandPP has writeStdin/closeStdin.
33 # ShellCommand accepts new arguments (logfiles=, initialStdin=,
34 # keepStdinOpen=) and no longer accepts stdin=)
35 # (release 0.7.4)
36 # >= 2.2: added monotone, uploadFile, and downloadFile (release 0.7.5)
37 # >= 2.3: added bzr
39 class CommandInterrupted(Exception):
40 pass
41 class TimeoutError(Exception):
42 pass
44 class AbandonChain(Exception):
45 """A series of chained steps can raise this exception to indicate that
46 one of the intermediate ShellCommands has failed, such that there is no
47 point in running the remainder. 'rc' should be the non-zero exit code of
48 the failing ShellCommand."""
50 def __repr__(self):
51 return "<AbandonChain rc=%s>" % self.args[0]
53 def getCommand(name):
54 possibles = which(name)
55 if not possibles:
56 raise RuntimeError("Couldn't find executable for '%s'" % name)
57 return possibles[0]
59 def rmdirRecursive(dir):
60 """This is a replacement for shutil.rmtree that works better under
61 windows. Thanks to Bear at the OSAF for the code."""
62 if not os.path.exists(dir):
63 return
65 if os.path.islink(dir):
66 os.remove(dir)
67 return
69 # Verify the directory is read/write/execute for the current user
70 os.chmod(dir, 0700)
72 for name in os.listdir(dir):
73 full_name = os.path.join(dir, name)
74 # on Windows, if we don't have write permission we can't remove
75 # the file/directory either, so turn that on
76 if os.name == 'nt':
77 if not os.access(full_name, os.W_OK):
78 # I think this is now redundant, but I don't have an NT
79 # machine to test on, so I'm going to leave it in place
80 # -warner
81 os.chmod(full_name, 0600)
83 if os.path.isdir(full_name):
84 rmdirRecursive(full_name)
85 else:
86 os.chmod(full_name, 0700)
87 os.remove(full_name)
88 os.rmdir(dir)
90 class ShellCommandPP(ProcessProtocol):
91 debug = False
93 def __init__(self, command):
94 self.command = command
95 self.pending_stdin = ""
96 self.stdin_finished = False
98 def writeStdin(self, data):
99 assert not self.stdin_finished
100 if self.connected:
101 self.transport.write(data)
102 else:
103 self.pending_stdin += data
105 def closeStdin(self):
106 if self.connected:
107 if self.debug: log.msg(" closing stdin")
108 self.transport.closeStdin()
109 self.stdin_finished = True
111 def connectionMade(self):
112 if self.debug:
113 log.msg("ShellCommandPP.connectionMade")
114 if not self.command.process:
115 if self.debug:
116 log.msg(" assigning self.command.process: %s" %
117 (self.transport,))
118 self.command.process = self.transport
120 # TODO: maybe we shouldn't close stdin when using a PTY. I can't test
121 # this yet, recent debian glibc has a bug which causes thread-using
122 # test cases to SIGHUP trial, and the workaround is to either run
123 # the whole test with /bin/sh -c " ".join(argv) (way gross) or to
124 # not use a PTY. Once the bug is fixed, I'll be able to test what
125 # happens when you close stdin on a pty. My concern is that it will
126 # SIGHUP the child (since we are, in a sense, hanging up on them).
127 # But it may well be that keeping stdout open prevents the SIGHUP
128 # from being sent.
129 #if not self.command.usePTY:
131 if self.pending_stdin:
132 if self.debug: log.msg(" writing to stdin")
133 self.transport.write(self.pending_stdin)
134 if self.stdin_finished:
135 if self.debug: log.msg(" closing stdin")
136 self.transport.closeStdin()
138 def outReceived(self, data):
139 if self.debug:
140 log.msg("ShellCommandPP.outReceived")
141 self.command.addStdout(data)
143 def errReceived(self, data):
144 if self.debug:
145 log.msg("ShellCommandPP.errReceived")
146 self.command.addStderr(data)
148 def processEnded(self, status_object):
149 if self.debug:
150 log.msg("ShellCommandPP.processEnded", status_object)
151 # status_object is a Failure wrapped around an
152 # error.ProcessTerminated or and error.ProcessDone.
153 # requires twisted >= 1.0.4 to overcome a bug in process.py
154 sig = status_object.value.signal
155 rc = status_object.value.exitCode
156 self.command.finished(sig, rc)
158 class LogFileWatcher:
159 POLL_INTERVAL = 2
161 def __init__(self, command, name, logfile):
162 self.command = command
163 self.name = name
164 self.logfile = logfile
165 log.msg("LogFileWatcher created to watch %s" % logfile)
166 # we are created before the ShellCommand starts. If the logfile we're
167 # supposed to be watching already exists, record its size and
168 # ctime/mtime so we can tell when it starts to change.
169 self.old_logfile_stats = self.statFile()
170 self.started = False
172 # every 2 seconds we check on the file again
173 self.poller = task.LoopingCall(self.poll)
175 def start(self):
176 self.poller.start(self.POLL_INTERVAL).addErrback(self._cleanupPoll)
178 def _cleanupPoll(self, err):
179 log.err(err, msg="Polling error")
180 self.poller = None
182 def stop(self):
183 self.poll()
184 if self.poller is not None:
185 self.poller.stop()
186 if self.started:
187 self.f.close()
189 def statFile(self):
190 if os.path.exists(self.logfile):
191 s = os.stat(self.logfile)
192 return (s[ST_CTIME], s[ST_MTIME], s[ST_SIZE])
193 return None
195 def poll(self):
196 if not self.started:
197 s = self.statFile()
198 if s == self.old_logfile_stats:
199 return # not started yet
200 if not s:
201 # the file was there, but now it's deleted. Forget about the
202 # initial state, clearly the process has deleted the logfile
203 # in preparation for creating a new one.
204 self.old_logfile_stats = None
205 return # no file to work with
206 self.f = open(self.logfile, "rb")
207 self.started = True
208 self.f.seek(self.f.tell(), 0)
209 while True:
210 data = self.f.read(10000)
211 if not data:
212 return
213 self.command.addLogfile(self.name, data)
216 class ShellCommand:
217 # This is a helper class, used by SlaveCommands to run programs in a
218 # child shell.
220 notreally = False
221 BACKUP_TIMEOUT = 5
222 KILL = "KILL"
224 def __init__(self, builder, command,
225 workdir, environ=None,
226 sendStdout=True, sendStderr=True, sendRC=True,
227 timeout=None, initialStdin=None, keepStdinOpen=False,
228 keepStdout=False,
229 logfiles={}):
232 @param keepStdout: if True, we keep a copy of all the stdout text
233 that we've seen. This copy is available in
234 self.stdout, which can be read after the command
235 has finished.
239 self.builder = builder
240 self.command = command
241 self.sendStdout = sendStdout
242 self.sendStderr = sendStderr
243 self.sendRC = sendRC
244 self.logfiles = logfiles
245 self.workdir = workdir
246 self.environ = os.environ.copy()
247 if environ:
248 if environ.has_key('PYTHONPATH'):
249 ppath = environ['PYTHONPATH']
250 # Need to do os.pathsep translation. We could either do that
251 # by replacing all incoming ':'s with os.pathsep, or by
252 # accepting lists. I like lists better.
253 if not isinstance(ppath, str):
254 # If it's not a string, treat it as a sequence to be
255 # turned in to a string.
256 ppath = os.pathsep.join(ppath)
258 if self.environ.has_key('PYTHONPATH'):
259 # special case, prepend the builder's items to the
260 # existing ones. This will break if you send over empty
261 # strings, so don't do that.
262 ppath = ppath + os.pathsep + self.environ['PYTHONPATH']
264 environ['PYTHONPATH'] = ppath
266 self.environ.update(environ)
267 self.initialStdin = initialStdin
268 self.keepStdinOpen = keepStdinOpen
269 self.timeout = timeout
270 self.timer = None
271 self.keepStdout = keepStdout
273 # usePTY=True is a convenience for cleaning up all children and
274 # grandchildren of a hung command. Fall back to usePTY=False on
275 # systems where ptys cause problems.
277 self.usePTY = self.builder.usePTY
278 if runtime.platformType != "posix":
279 self.usePTY = False # PTYs are posix-only
280 if initialStdin is not None:
281 # for .closeStdin to matter, we must use a pipe, not a PTY
282 self.usePTY = False
284 self.logFileWatchers = []
285 for name,filename in self.logfiles.items():
286 w = LogFileWatcher(self, name,
287 os.path.join(self.workdir, filename))
288 self.logFileWatchers.append(w)
290 def __repr__(self):
291 return "<slavecommand.ShellCommand '%s'>" % self.command
293 def sendStatus(self, status):
294 self.builder.sendUpdate(status)
296 def start(self):
297 # return a Deferred which fires (with the exit code) when the command
298 # completes
299 if self.keepStdout:
300 self.stdout = ""
301 self.deferred = defer.Deferred()
302 try:
303 self._startCommand()
304 except:
305 log.msg("error in ShellCommand._startCommand")
306 log.err()
307 # pretend it was a shell error
308 self.deferred.errback(AbandonChain(-1))
309 return self.deferred
311 def _startCommand(self):
312 # ensure workdir exists
313 if not os.path.isdir(self.workdir):
314 os.makedirs(self.workdir)
315 log.msg("ShellCommand._startCommand")
316 if self.notreally:
317 self.sendStatus({'header': "command '%s' in dir %s" % \
318 (self.command, self.workdir)})
319 self.sendStatus({'header': "(not really)\n"})
320 self.finished(None, 0)
321 return
323 self.pp = ShellCommandPP(self)
325 if type(self.command) in types.StringTypes:
326 if runtime.platformType == 'win32':
327 argv = [os.environ['COMSPEC'], '/c', self.command]
328 else:
329 # for posix, use /bin/sh. for other non-posix, well, doesn't
330 # hurt to try
331 argv = ['/bin/sh', '-c', self.command]
332 else:
333 if runtime.platformType == 'win32':
334 argv = [os.environ['COMSPEC'], '/c'] + list(self.command)
335 else:
336 argv = self.command
338 # self.stdin is handled in ShellCommandPP.connectionMade
340 # first header line is the command in plain text, argv joined with
341 # spaces. You should be able to cut-and-paste this into a shell to
342 # obtain the same results. If there are spaces in the arguments, too
343 # bad.
344 msg = " ".join(argv)
345 log.msg(" " + msg)
346 self.sendStatus({'header': msg+"\n"})
348 # then comes the secondary information
349 msg = " in dir %s" % (self.workdir,)
350 if self.timeout:
351 msg += " (timeout %d secs)" % (self.timeout,)
352 log.msg(" " + msg)
353 self.sendStatus({'header': msg+"\n"})
355 msg = " watching logfiles %s" % (self.logfiles,)
356 log.msg(" " + msg)
357 self.sendStatus({'header': msg+"\n"})
359 # then the argv array for resolving unambiguity
360 msg = " argv: %s" % (argv,)
361 log.msg(" " + msg)
362 self.sendStatus({'header': msg+"\n"})
364 # then the environment, since it sometimes causes problems
365 msg = " environment:\n"
366 env_names = self.environ.keys()
367 env_names.sort()
368 for name in env_names:
369 msg += " %s=%s\n" % (name, self.environ[name])
370 log.msg(" environment: %s" % (self.environ,))
371 self.sendStatus({'header': msg})
373 # this will be buffered until connectionMade is called
374 if self.initialStdin:
375 self.pp.writeStdin(self.initialStdin)
376 if not self.keepStdinOpen:
377 self.pp.closeStdin()
379 # win32eventreactor's spawnProcess (under twisted <= 2.0.1) returns
380 # None, as opposed to all the posixbase-derived reactors (which
381 # return the new Process object). This is a nuisance. We can make up
382 # for it by having the ProcessProtocol give us their .transport
383 # attribute after they get one. I'd prefer to get it from
384 # spawnProcess because I'm concerned about returning from this method
385 # without having a valid self.process to work with. (if kill() were
386 # called right after we return, but somehow before connectionMade
387 # were called, then kill() would blow up).
388 self.process = None
389 p = reactor.spawnProcess(self.pp, argv[0], argv,
390 self.environ,
391 self.workdir,
392 usePTY=self.usePTY)
393 # connectionMade might have been called during spawnProcess
394 if not self.process:
395 self.process = p
397 # connectionMade also closes stdin as long as we're not using a PTY.
398 # This is intended to kill off inappropriately interactive commands
399 # better than the (long) hung-command timeout. ProcessPTY should be
400 # enhanced to allow the same childFDs argument that Process takes,
401 # which would let us connect stdin to /dev/null .
403 if self.timeout:
404 self.timer = reactor.callLater(self.timeout, self.doTimeout)
406 for w in self.logFileWatchers:
407 w.start()
410 def addStdout(self, data):
411 if self.sendStdout:
412 self.sendStatus({'stdout': data})
413 if self.keepStdout:
414 self.stdout += data
415 if self.timer:
416 self.timer.reset(self.timeout)
418 def addStderr(self, data):
419 if self.sendStderr:
420 self.sendStatus({'stderr': data})
421 if self.timer:
422 self.timer.reset(self.timeout)
424 def addLogfile(self, name, data):
425 self.sendStatus({'log': (name, data)})
426 if self.timer:
427 self.timer.reset(self.timeout)
429 def finished(self, sig, rc):
430 log.msg("command finished with signal %s, exit code %s" % (sig,rc))
431 for w in self.logFileWatchers:
432 # this will send the final updates
433 w.stop()
434 if sig is not None:
435 rc = -1
436 if self.sendRC:
437 if sig is not None:
438 self.sendStatus(
439 {'header': "process killed by signal %d\n" % sig})
440 self.sendStatus({'rc': rc})
441 if self.timer:
442 self.timer.cancel()
443 self.timer = None
444 d = self.deferred
445 self.deferred = None
446 if d:
447 d.callback(rc)
448 else:
449 log.msg("Hey, command %s finished twice" % self)
451 def failed(self, why):
452 log.msg("ShellCommand.failed: command failed: %s" % (why,))
453 if self.timer:
454 self.timer.cancel()
455 self.timer = None
456 d = self.deferred
457 self.deferred = None
458 if d:
459 d.errback(why)
460 else:
461 log.msg("Hey, command %s finished twice" % self)
463 def doTimeout(self):
464 self.timer = None
465 msg = "command timed out: %d seconds without output" % self.timeout
466 self.kill(msg)
468 def kill(self, msg):
469 # This may be called by the timeout, or when the user has decided to
470 # abort this build.
471 if self.timer:
472 self.timer.cancel()
473 self.timer = None
474 if hasattr(self.process, "pid"):
475 msg += ", killing pid %d" % self.process.pid
476 log.msg(msg)
477 self.sendStatus({'header': "\n" + msg + "\n"})
479 hit = 0
480 if runtime.platformType == "posix":
481 try:
482 # really want to kill off all child processes too. Process
483 # Groups are ideal for this, but that requires
484 # spawnProcess(usePTY=1). Try both ways in case process was
485 # not started that way.
487 # the test suite sets self.KILL=None to tell us we should
488 # only pretend to kill the child. This lets us test the
489 # backup timer.
491 sig = None
492 if self.KILL is not None:
493 sig = getattr(signal, "SIG"+ self.KILL, None)
495 if self.KILL == None:
496 log.msg("self.KILL==None, only pretending to kill child")
497 elif sig is None:
498 log.msg("signal module is missing SIG%s" % self.KILL)
499 elif not hasattr(os, "kill"):
500 log.msg("os module is missing the 'kill' function")
501 else:
502 log.msg("trying os.kill(-pid, %d)" % (sig,))
503 # TODO: maybe use os.killpg instead of a negative pid?
504 os.kill(-self.process.pid, sig)
505 log.msg(" signal %s sent successfully" % sig)
506 hit = 1
507 except OSError:
508 # probably no-such-process, maybe because there is no process
509 # group
510 pass
511 if not hit:
512 try:
513 if self.KILL is None:
514 log.msg("self.KILL==None, only pretending to kill child")
515 else:
516 log.msg("trying process.signalProcess('KILL')")
517 self.process.signalProcess(self.KILL)
518 log.msg(" signal %s sent successfully" % (self.KILL,))
519 hit = 1
520 except OSError:
521 # could be no-such-process, because they finished very recently
522 pass
523 if not hit:
524 log.msg("signalProcess/os.kill failed both times")
526 if runtime.platformType == "posix":
527 # we only do this under posix because the win32eventreactor
528 # blocks here until the process has terminated, while closing
529 # stderr. This is weird.
530 self.pp.transport.loseConnection()
532 # finished ought to be called momentarily. Just in case it doesn't,
533 # set a timer which will abandon the command.
534 self.timer = reactor.callLater(self.BACKUP_TIMEOUT,
535 self.doBackupTimeout)
537 def doBackupTimeout(self):
538 log.msg("we tried to kill the process, and it wouldn't die.."
539 " finish anyway")
540 self.timer = None
541 self.sendStatus({'header': "SIGKILL failed to kill process\n"})
542 if self.sendRC:
543 self.sendStatus({'header': "using fake rc=-1\n"})
544 self.sendStatus({'rc': -1})
545 self.failed(TimeoutError("SIGKILL failed to kill process"))
548 def writeStdin(self, data):
549 self.pp.writeStdin(data)
551 def closeStdin(self):
552 self.pp.closeStdin()
555 class Command:
556 implements(ISlaveCommand)
558 """This class defines one command that can be invoked by the build master.
559 The command is executed on the slave side, and always sends back a
560 completion message when it finishes. It may also send intermediate status
561 as it runs (by calling builder.sendStatus). Some commands can be
562 interrupted (either by the build master or a local timeout), in which
563 case the step is expected to complete normally with a status message that
564 indicates an error occurred.
566 These commands are used by BuildSteps on the master side. Each kind of
567 BuildStep uses a single Command. The slave must implement all the
568 Commands required by the set of BuildSteps used for any given build:
569 this is checked at startup time.
571 All Commands are constructed with the same signature:
572 c = CommandClass(builder, args)
573 where 'builder' is the parent SlaveBuilder object, and 'args' is a
574 dict that is interpreted per-command.
576 The setup(args) method is available for setup, and is run from __init__.
578 The Command is started with start(). This method must be implemented in a
579 subclass, and it should return a Deferred. When your step is done, you
580 should fire the Deferred (the results are not used). If the command is
581 interrupted, it should fire the Deferred anyway.
583 While the command runs. it may send status messages back to the
584 buildmaster by calling self.sendStatus(statusdict). The statusdict is
585 interpreted by the master-side BuildStep however it likes.
587 A separate completion message is sent when the deferred fires, which
588 indicates that the Command has finished, but does not carry any status
589 data. If the Command needs to return an exit code of some sort, that
590 should be sent as a regular status message before the deferred is fired .
591 Once builder.commandComplete has been run, no more status messages may be
592 sent.
594 If interrupt() is called, the Command should attempt to shut down as
595 quickly as possible. Child processes should be killed, new ones should
596 not be started. The Command should send some kind of error status update,
597 then complete as usual by firing the Deferred.
599 .interrupted should be set by interrupt(), and can be tested to avoid
600 sending multiple error status messages.
602 If .running is False, the bot is shutting down (or has otherwise lost the
603 connection to the master), and should not send any status messages. This
604 is checked in Command.sendStatus .
608 # builder methods:
609 # sendStatus(dict) (zero or more)
610 # commandComplete() or commandInterrupted() (one, at end)
612 debug = False
613 interrupted = False
614 running = False # set by Builder, cleared on shutdown or when the
615 # Deferred fires
617 def __init__(self, builder, stepId, args):
618 self.builder = builder
619 self.stepId = stepId # just for logging
620 self.args = args
621 self.setup(args)
623 def setup(self, args):
624 """Override this in a subclass to extract items from the args dict."""
625 pass
627 def doStart(self):
628 self.running = True
629 d = defer.maybeDeferred(self.start)
630 d.addBoth(self.commandComplete)
631 return d
633 def start(self):
634 """Start the command. This method should return a Deferred that will
635 fire when the command has completed. The Deferred's argument will be
636 ignored.
638 This method should be overridden by subclasses."""
639 raise NotImplementedError, "You must implement this in a subclass"
641 def sendStatus(self, status):
642 """Send a status update to the master."""
643 if self.debug:
644 log.msg("sendStatus", status)
645 if not self.running:
646 log.msg("would sendStatus but not .running")
647 return
648 self.builder.sendUpdate(status)
650 def doInterrupt(self):
651 self.running = False
652 self.interrupt()
654 def interrupt(self):
655 """Override this in a subclass to allow commands to be interrupted.
656 May be called multiple times, test and set self.interrupted=True if
657 this matters."""
658 pass
660 def commandComplete(self, res):
661 self.running = False
662 return res
664 # utility methods, mostly used by SlaveShellCommand and the like
666 def _abandonOnFailure(self, rc):
667 if type(rc) is not int:
668 log.msg("weird, _abandonOnFailure was given rc=%s (%s)" % \
669 (rc, type(rc)))
670 assert isinstance(rc, int)
671 if rc != 0:
672 raise AbandonChain(rc)
673 return rc
675 def _sendRC(self, res):
676 self.sendStatus({'rc': 0})
678 def _checkAbandoned(self, why):
679 log.msg("_checkAbandoned", why)
680 why.trap(AbandonChain)
681 log.msg(" abandoning chain", why.value)
682 self.sendStatus({'rc': why.value.args[0]})
683 return None
687 class SlaveFileUploadCommand(Command):
689 Upload a file from slave to build master
690 Arguments:
692 - ['workdir']: base directory to use
693 - ['slavesrc']: name of the slave-side file to read from
694 - ['writer']: RemoteReference to a transfer._FileWriter object
695 - ['maxsize']: max size (in bytes) of file to write
696 - ['blocksize']: max size for each data block
698 debug = False
700 def setup(self, args):
701 self.workdir = args['workdir']
702 self.filename = args['slavesrc']
703 self.writer = args['writer']
704 self.remaining = args['maxsize']
705 self.blocksize = args['blocksize']
706 self.stderr = None
707 self.rc = 0
709 def start(self):
710 if self.debug:
711 log.msg('SlaveFileUploadCommand started')
713 # Open file
714 self.path = os.path.join(self.builder.basedir,
715 self.workdir,
716 os.path.expanduser(self.filename))
717 try:
718 self.fp = open(self.path, 'rb')
719 if self.debug:
720 log.msg('Opened %r for upload' % self.path)
721 except:
722 # TODO: this needs cleanup
723 self.fp = None
724 self.stderr = 'Cannot open file %r for upload' % self.path
725 self.rc = 1
726 if self.debug:
727 log.msg('Cannot open file %r for upload' % self.path)
729 self.sendStatus({'header': "sending %s" % self.path})
731 d = defer.Deferred()
732 d.addCallback(self._writeBlock)
733 d.addBoth(self.finished)
734 reactor.callLater(0, d.callback, None)
735 return d
737 def _writeBlock(self, res):
739 Write a block of data to the remote writer
741 if self.interrupted or self.fp is None:
742 if self.debug:
743 log.msg('SlaveFileUploadCommand._writeBlock(): end')
744 d = self.writer.callRemote('close')
745 return d
747 length = self.blocksize
748 if self.remaining is not None and length > self.remaining:
749 length = self.remaining
751 if length <= 0:
752 if self.stderr is None:
753 self.stderr = 'Maximum filesize reached, truncating file %r' \
754 % self.path
755 self.rc = 1
756 data = ''
757 else:
758 data = self.fp.read(length)
760 if self.debug:
761 log.msg('SlaveFileUploadCommand._writeBlock(): '+
762 'allowed=%d readlen=%d' % (length, len(data)))
763 if len(data) == 0:
764 d = self.writer.callRemote('close')
765 return d
767 if self.remaining is not None:
768 self.remaining = self.remaining - len(data)
769 assert self.remaining >= 0
770 d = self.writer.callRemote('write', data)
771 d.addCallback(self._writeBlock)
772 return d
774 def interrupt(self):
775 if self.debug:
776 log.msg('interrupted')
777 if self.interrupted:
778 return
779 if self.stderr is None:
780 self.stderr = 'Upload of %r interrupted' % self.path
781 self.rc = 1
782 self.interrupted = True
783 # the next _writeBlock call will notice the .interrupted flag
785 def finished(self, res):
786 if self.debug:
787 log.msg('finished: stderr=%r, rc=%r' % (self.stderr, self.rc))
788 if self.stderr is None:
789 self.sendStatus({'rc': self.rc})
790 else:
791 self.sendStatus({'stderr': self.stderr, 'rc': self.rc})
792 return res
794 registerSlaveCommand("uploadFile", SlaveFileUploadCommand, command_version)
797 class SlaveFileDownloadCommand(Command):
799 Download a file from master to slave
800 Arguments:
802 - ['workdir']: base directory to use
803 - ['slavedest']: name of the slave-side file to be created
804 - ['reader']: RemoteReference to a transfer._FileReader object
805 - ['maxsize']: max size (in bytes) of file to write
806 - ['blocksize']: max size for each data block
807 - ['mode']: access mode for the new file
809 debug = False
811 def setup(self, args):
812 self.workdir = args['workdir']
813 self.filename = args['slavedest']
814 self.reader = args['reader']
815 self.bytes_remaining = args['maxsize']
816 self.blocksize = args['blocksize']
817 self.mode = args['mode']
818 self.stderr = None
819 self.rc = 0
821 def start(self):
822 if self.debug:
823 log.msg('SlaveFileDownloadCommand starting')
825 # Open file
826 self.path = os.path.join(self.builder.basedir,
827 self.workdir,
828 os.path.expanduser(self.filename))
829 try:
830 self.fp = open(self.path, 'wb')
831 if self.debug:
832 log.msg('Opened %r for download' % self.path)
833 if self.mode is not None:
834 # note: there is a brief window during which the new file
835 # will have the buildslave's default (umask) mode before we
836 # set the new one. Don't use this mode= feature to keep files
837 # private: use the buildslave's umask for that instead. (it
838 # is possible to call os.umask() before and after the open()
839 # call, but cleaning up from exceptions properly is more of a
840 # nuisance that way).
841 os.chmod(self.path, self.mode)
842 except IOError:
843 # TODO: this still needs cleanup
844 self.fp = None
845 self.stderr = 'Cannot open file %r for download' % self.path
846 self.rc = 1
847 if self.debug:
848 log.msg('Cannot open file %r for download' % self.path)
850 d = defer.Deferred()
851 d.addCallback(self._readBlock)
852 d.addBoth(self.finished)
853 reactor.callLater(0, d.callback, None)
854 return d
856 def _readBlock(self, res):
858 Read a block of data from the remote reader
860 if self.interrupted or self.fp is None:
861 if self.debug:
862 log.msg('SlaveFileDownloadCommand._readBlock(): end')
863 d = self.reader.callRemote('close')
864 return d
866 length = self.blocksize
867 if self.bytes_remaining is not None and length > self.bytes_remaining:
868 length = self.bytes_remaining
870 if length <= 0:
871 if self.stderr is None:
872 self.stderr = 'Maximum filesize reached, truncating file %r' \
873 % self.path
874 self.rc = 1
875 d = self.reader.callRemote('close')
876 else:
877 d = self.reader.callRemote('read', length)
878 d.addCallback(self._writeData)
879 return d
881 def _writeData(self, data):
882 if self.debug:
883 log.msg('SlaveFileDownloadCommand._readBlock(): readlen=%d' %
884 len(data))
885 if len(data) == 0:
886 d = self.reader.callRemote('close')
887 return d
889 if self.bytes_remaining is not None:
890 self.bytes_remaining = self.bytes_remaining - len(data)
891 assert self.bytes_remaining >= 0
892 self.fp.write(data)
893 d = self._readBlock(None) # setup call back for next block (or finish)
894 return d
896 def interrupt(self):
897 if self.debug:
898 log.msg('interrupted')
899 if self.interrupted:
900 return
901 if self.stderr is None:
902 self.stderr = 'Download of %r interrupted' % self.path
903 self.rc = 1
904 self.interrupted = True
905 # now we wait for the next read request to return. _readBlock will
906 # abandon the file when it sees self.interrupted set.
908 def finished(self, res):
909 if self.fp is not None:
910 self.fp.close()
912 if self.debug:
913 log.msg('finished: stderr=%r, rc=%r' % (self.stderr, self.rc))
914 if self.stderr is None:
915 self.sendStatus({'rc': self.rc})
916 else:
917 self.sendStatus({'stderr': self.stderr, 'rc': self.rc})
918 return res
920 registerSlaveCommand("downloadFile", SlaveFileDownloadCommand, command_version)
924 class SlaveShellCommand(Command):
925 """This is a Command which runs a shell command. The args dict contains
926 the following keys:
928 - ['command'] (required): a shell command to run. If this is a string,
929 it will be run with /bin/sh (['/bin/sh',
930 '-c', command]). If it is a list
931 (preferred), it will be used directly.
932 - ['workdir'] (required): subdirectory in which the command will be
933 run, relative to the builder dir
934 - ['env']: a dict of environment variables to augment/replace
935 os.environ . PYTHONPATH is treated specially, and
936 should be a list of path components to be prepended to
937 any existing PYTHONPATH environment variable.
938 - ['initial_stdin']: a string which will be written to the command's
939 stdin as soon as it starts
940 - ['keep_stdin_open']: unless True, the command's stdin will be
941 closed as soon as initial_stdin has been
942 written. Set this to True if you plan to write
943 to stdin after the command has been started.
944 - ['want_stdout']: 0 if stdout should be thrown away
945 - ['want_stderr']: 0 if stderr should be thrown away
946 - ['not_really']: 1 to skip execution and return rc=0
947 - ['timeout']: seconds of silence to tolerate before killing command
948 - ['logfiles']: dict mapping LogFile name to the workdir-relative
949 filename of a local log file. This local file will be
950 watched just like 'tail -f', and all changes will be
951 written to 'log' status updates.
953 ShellCommand creates the following status messages:
954 - {'stdout': data} : when stdout data is available
955 - {'stderr': data} : when stderr data is available
956 - {'header': data} : when headers (command start/stop) are available
957 - {'log': (logfile_name, data)} : when log files have new contents
958 - {'rc': rc} : when the process has terminated
961 def start(self):
962 args = self.args
963 # args['workdir'] is relative to Builder directory, and is required.
964 assert args['workdir'] is not None
965 workdir = os.path.join(self.builder.basedir, args['workdir'])
967 c = ShellCommand(self.builder, args['command'],
968 workdir, environ=args.get('env'),
969 timeout=args.get('timeout', None),
970 sendStdout=args.get('want_stdout', True),
971 sendStderr=args.get('want_stderr', True),
972 sendRC=True,
973 initialStdin=args.get('initial_stdin'),
974 keepStdinOpen=args.get('keep_stdin_open'),
975 logfiles=args.get('logfiles', {}),
977 self.command = c
978 d = self.command.start()
979 return d
981 def interrupt(self):
982 self.interrupted = True
983 self.command.kill("command interrupted")
985 def writeStdin(self, data):
986 self.command.writeStdin(data)
988 def closeStdin(self):
989 self.command.closeStdin()
991 registerSlaveCommand("shell", SlaveShellCommand, command_version)
994 class DummyCommand(Command):
996 I am a dummy no-op command that by default takes 5 seconds to complete.
997 See L{buildbot.steps.dummy.RemoteDummy}
1000 def start(self):
1001 self.d = defer.Deferred()
1002 log.msg(" starting dummy command [%s]" % self.stepId)
1003 self.timer = reactor.callLater(1, self.doStatus)
1004 return self.d
1006 def interrupt(self):
1007 if self.interrupted:
1008 return
1009 self.timer.cancel()
1010 self.timer = None
1011 self.interrupted = True
1012 self.finished()
1014 def doStatus(self):
1015 log.msg(" sending intermediate status")
1016 self.sendStatus({'stdout': 'data'})
1017 timeout = self.args.get('timeout', 5) + 1
1018 self.timer = reactor.callLater(timeout - 1, self.finished)
1020 def finished(self):
1021 log.msg(" dummy command finished [%s]" % self.stepId)
1022 if self.interrupted:
1023 self.sendStatus({'rc': 1})
1024 else:
1025 self.sendStatus({'rc': 0})
1026 self.d.callback(0)
1028 registerSlaveCommand("dummy", DummyCommand, command_version)
1031 # this maps handle names to a callable. When the WaitCommand starts, this
1032 # callable is invoked with no arguments. It should return a Deferred. When
1033 # that Deferred fires, our WaitCommand will finish.
1034 waitCommandRegistry = {}
1036 class WaitCommand(Command):
1038 I am a dummy command used by the buildbot unit test suite. I want for the
1039 unit test to tell us to finish. See L{buildbot.steps.dummy.Wait}
1042 def start(self):
1043 self.d = defer.Deferred()
1044 log.msg(" starting wait command [%s]" % self.stepId)
1045 handle = self.args['handle']
1046 cb = waitCommandRegistry[handle]
1047 del waitCommandRegistry[handle]
1048 def _called():
1049 log.msg(" wait-%s starting" % (handle,))
1050 d = cb()
1051 def _done(res):
1052 log.msg(" wait-%s finishing: %s" % (handle, res))
1053 return res
1054 d.addBoth(_done)
1055 d.addCallbacks(self.finished, self.failed)
1056 reactor.callLater(0, _called)
1057 return self.d
1059 def interrupt(self):
1060 log.msg(" wait command interrupted")
1061 if self.interrupted:
1062 return
1063 self.interrupted = True
1064 self.finished("interrupted")
1066 def finished(self, res):
1067 log.msg(" wait command finished [%s]" % self.stepId)
1068 if self.interrupted:
1069 self.sendStatus({'rc': 2})
1070 else:
1071 self.sendStatus({'rc': 0})
1072 self.d.callback(0)
1073 def failed(self, why):
1074 log.msg(" wait command failed [%s]" % self.stepId)
1075 self.sendStatus({'rc': 1})
1076 self.d.callback(0)
1078 registerSlaveCommand("dummy.wait", WaitCommand, command_version)
1081 class SourceBase(Command):
1082 """Abstract base class for Version Control System operations (checkout
1083 and update). This class extracts the following arguments from the
1084 dictionary received from the master:
1086 - ['workdir']: (required) the subdirectory where the buildable sources
1087 should be placed
1089 - ['mode']: one of update/copy/clobber/export, defaults to 'update'
1091 - ['revision']: If not None, this is an int or string which indicates
1092 which sources (along a time-like axis) should be used.
1093 It is the thing you provide as the CVS -r or -D
1094 argument.
1096 - ['patch']: If not None, this is a tuple of (striplevel, patch)
1097 which contains a patch that should be applied after the
1098 checkout has occurred. Once applied, the tree is no
1099 longer eligible for use with mode='update', and it only
1100 makes sense to use this in conjunction with a
1101 ['revision'] argument. striplevel is an int, and patch
1102 is a string in standard unified diff format. The patch
1103 will be applied with 'patch -p%d <PATCH', with
1104 STRIPLEVEL substituted as %d. The command will fail if
1105 the patch process fails (rejected hunks).
1107 - ['timeout']: seconds of silence tolerated before we kill off the
1108 command
1110 - ['retry']: If not None, this is a tuple of (delay, repeats)
1111 which means that any failed VC updates should be
1112 reattempted, up to REPEATS times, after a delay of
1113 DELAY seconds. This is intended to deal with slaves
1114 that experience transient network failures.
1117 sourcedata = ""
1119 def setup(self, args):
1120 # if we need to parse the output, use this environment. Otherwise
1121 # command output will be in whatever the buildslave's native language
1122 # has been set to.
1123 self.env = os.environ.copy()
1124 self.env['LC_ALL'] = "C"
1126 self.workdir = args['workdir']
1127 self.mode = args.get('mode', "update")
1128 self.revision = args.get('revision')
1129 self.patch = args.get('patch')
1130 self.timeout = args.get('timeout', 120)
1131 self.retry = args.get('retry')
1132 # VC-specific subclasses should override this to extract more args.
1133 # Make sure to upcall!
1135 def start(self):
1136 self.sendStatus({'header': "starting " + self.header + "\n"})
1137 self.command = None
1139 # self.srcdir is where the VC system should put the sources
1140 if self.mode == "copy":
1141 self.srcdir = "source" # hardwired directory name, sorry
1142 else:
1143 self.srcdir = self.workdir
1144 self.sourcedatafile = os.path.join(self.builder.basedir,
1145 self.srcdir,
1146 ".buildbot-sourcedata")
1148 d = defer.succeed(None)
1149 # do we need to clobber anything?
1150 if self.mode in ("copy", "clobber", "export"):
1151 d.addCallback(self.doClobber, self.workdir)
1152 if not (self.sourcedirIsUpdateable() and self.sourcedataMatches()):
1153 # the directory cannot be updated, so we have to clobber it.
1154 # Perhaps the master just changed modes from 'export' to
1155 # 'update'.
1156 d.addCallback(self.doClobber, self.srcdir)
1158 d.addCallback(self.doVC)
1160 if self.mode == "copy":
1161 d.addCallback(self.doCopy)
1162 if self.patch:
1163 d.addCallback(self.doPatch)
1164 d.addCallbacks(self._sendRC, self._checkAbandoned)
1165 return d
1167 def interrupt(self):
1168 self.interrupted = True
1169 if self.command:
1170 self.command.kill("command interrupted")
1172 def doVC(self, res):
1173 if self.interrupted:
1174 raise AbandonChain(1)
1175 if self.sourcedirIsUpdateable() and self.sourcedataMatches():
1176 d = self.doVCUpdate()
1177 d.addCallback(self.maybeDoVCFallback)
1178 else:
1179 d = self.doVCFull()
1180 d.addBoth(self.maybeDoVCRetry)
1181 d.addCallback(self._abandonOnFailure)
1182 d.addCallback(self._handleGotRevision)
1183 d.addCallback(self.writeSourcedata)
1184 return d
1186 def sourcedataMatches(self):
1187 try:
1188 olddata = open(self.sourcedatafile, "r").read()
1189 if olddata != self.sourcedata:
1190 return False
1191 except IOError:
1192 return False
1193 return True
1195 def _handleGotRevision(self, res):
1196 d = defer.maybeDeferred(self.parseGotRevision)
1197 d.addCallback(lambda got_revision:
1198 self.sendStatus({'got_revision': got_revision}))
1199 return d
1201 def parseGotRevision(self):
1202 """Override this in a subclass. It should return a string that
1203 represents which revision was actually checked out, or a Deferred
1204 that will fire with such a string. If, in a future build, you were to
1205 pass this 'got_revision' string in as the 'revision' component of a
1206 SourceStamp, you should wind up with the same source code as this
1207 checkout just obtained.
1209 It is probably most useful to scan self.command.stdout for a string
1210 of some sort. Be sure to set keepStdout=True on the VC command that
1211 you run, so that you'll have something available to look at.
1213 If this information is unavailable, just return None."""
1215 return None
1217 def writeSourcedata(self, res):
1218 open(self.sourcedatafile, "w").write(self.sourcedata)
1219 return res
1221 def sourcedirIsUpdateable(self):
1222 raise NotImplementedError("this must be implemented in a subclass")
1224 def doVCUpdate(self):
1225 raise NotImplementedError("this must be implemented in a subclass")
1227 def doVCFull(self):
1228 raise NotImplementedError("this must be implemented in a subclass")
1230 def maybeDoVCFallback(self, rc):
1231 if type(rc) is int and rc == 0:
1232 return rc
1233 if self.interrupted:
1234 raise AbandonChain(1)
1235 msg = "update failed, clobbering and trying again"
1236 self.sendStatus({'header': msg + "\n"})
1237 log.msg(msg)
1238 d = self.doClobber(None, self.srcdir)
1239 d.addCallback(self.doVCFallback2)
1240 return d
1242 def doVCFallback2(self, res):
1243 msg = "now retrying VC operation"
1244 self.sendStatus({'header': msg + "\n"})
1245 log.msg(msg)
1246 d = self.doVCFull()
1247 d.addBoth(self.maybeDoVCRetry)
1248 d.addCallback(self._abandonOnFailure)
1249 return d
1251 def maybeDoVCRetry(self, res):
1252 """We get here somewhere after a VC chain has finished. res could
1253 be::
1255 - 0: the operation was successful
1256 - nonzero: the operation failed. retry if possible
1257 - AbandonChain: the operation failed, someone else noticed. retry.
1258 - Failure: some other exception, re-raise
1261 if isinstance(res, failure.Failure):
1262 if self.interrupted:
1263 return res # don't re-try interrupted builds
1264 res.trap(AbandonChain)
1265 else:
1266 if type(res) is int and res == 0:
1267 return res
1268 if self.interrupted:
1269 raise AbandonChain(1)
1270 # if we get here, we should retry, if possible
1271 if self.retry:
1272 delay, repeats = self.retry
1273 if repeats >= 0:
1274 self.retry = (delay, repeats-1)
1275 msg = ("update failed, trying %d more times after %d seconds"
1276 % (repeats, delay))
1277 self.sendStatus({'header': msg + "\n"})
1278 log.msg(msg)
1279 d = defer.Deferred()
1280 d.addCallback(lambda res: self.doVCFull())
1281 d.addBoth(self.maybeDoVCRetry)
1282 reactor.callLater(delay, d.callback, None)
1283 return d
1284 return res
1286 def doClobber(self, dummy, dirname):
1287 # TODO: remove the old tree in the background
1288 ## workdir = os.path.join(self.builder.basedir, self.workdir)
1289 ## deaddir = self.workdir + ".deleting"
1290 ## if os.path.isdir(workdir):
1291 ## try:
1292 ## os.rename(workdir, deaddir)
1293 ## # might fail if deaddir already exists: previous deletion
1294 ## # hasn't finished yet
1295 ## # start the deletion in the background
1296 ## # TODO: there was a solaris/NetApp/NFS problem where a
1297 ## # process that was still running out of the directory we're
1298 ## # trying to delete could prevent the rm-rf from working. I
1299 ## # think it stalled the rm, but maybe it just died with
1300 ## # permission issues. Try to detect this.
1301 ## os.commands("rm -rf %s &" % deaddir)
1302 ## except:
1303 ## # fall back to sequential delete-then-checkout
1304 ## pass
1305 d = os.path.join(self.builder.basedir, dirname)
1306 if runtime.platformType != "posix":
1307 # if we're running on w32, use rmtree instead. It will block,
1308 # but hopefully it won't take too long.
1309 rmdirRecursive(d)
1310 return defer.succeed(0)
1311 command = ["rm", "-rf", d]
1312 c = ShellCommand(self.builder, command, self.builder.basedir,
1313 sendRC=0, timeout=self.timeout)
1314 self.command = c
1315 # sendRC=0 means the rm command will send stdout/stderr to the
1316 # master, but not the rc=0 when it finishes. That job is left to
1317 # _sendRC
1318 d = c.start()
1319 d.addCallback(self._abandonOnFailure)
1320 return d
1322 def doCopy(self, res):
1323 # now copy tree to workdir
1324 fromdir = os.path.join(self.builder.basedir, self.srcdir)
1325 todir = os.path.join(self.builder.basedir, self.workdir)
1326 if runtime.platformType != "posix":
1327 shutil.copytree(fromdir, todir)
1328 return defer.succeed(0)
1329 command = ['cp', '-r', '-p', fromdir, todir]
1330 c = ShellCommand(self.builder, command, self.builder.basedir,
1331 sendRC=False, timeout=self.timeout)
1332 self.command = c
1333 d = c.start()
1334 d.addCallback(self._abandonOnFailure)
1335 return d
1337 def doPatch(self, res):
1338 patchlevel, diff = self.patch
1339 command = [getCommand("patch"), '-p%d' % patchlevel]
1340 dir = os.path.join(self.builder.basedir, self.workdir)
1341 # mark the directory so we don't try to update it later
1342 open(os.path.join(dir, ".buildbot-patched"), "w").write("patched\n")
1343 # now apply the patch
1344 c = ShellCommand(self.builder, command, dir,
1345 sendRC=False, timeout=self.timeout,
1346 initialStdin=diff)
1347 self.command = c
1348 d = c.start()
1349 d.addCallback(self._abandonOnFailure)
1350 return d
1353 class CVS(SourceBase):
1354 """CVS-specific VC operation. In addition to the arguments handled by
1355 SourceBase, this command reads the following keys:
1357 ['cvsroot'] (required): the CVSROOT repository string
1358 ['cvsmodule'] (required): the module to be retrieved
1359 ['branch']: a '-r' tag or branch name to use for the checkout/update
1360 ['login']: a string for use as a password to 'cvs login'
1361 ['global_options']: a list of strings to use before the CVS verb
1364 header = "cvs operation"
1366 def setup(self, args):
1367 SourceBase.setup(self, args)
1368 self.vcexe = getCommand("cvs")
1369 self.cvsroot = args['cvsroot']
1370 self.cvsmodule = args['cvsmodule']
1371 self.global_options = args.get('global_options', [])
1372 self.branch = args.get('branch')
1373 self.login = args.get('login')
1374 self.sourcedata = "%s\n%s\n%s\n" % (self.cvsroot, self.cvsmodule,
1375 self.branch)
1377 def sourcedirIsUpdateable(self):
1378 if os.path.exists(os.path.join(self.builder.basedir,
1379 self.srcdir, ".buildbot-patched")):
1380 return False
1381 return os.path.isdir(os.path.join(self.builder.basedir,
1382 self.srcdir, "CVS"))
1384 def start(self):
1385 if self.login is not None:
1386 # need to do a 'cvs login' command first
1387 d = self.builder.basedir
1388 command = ([self.vcexe, '-d', self.cvsroot] + self.global_options
1389 + ['login'])
1390 c = ShellCommand(self.builder, command, d,
1391 sendRC=False, timeout=self.timeout,
1392 initialStdin=self.login+"\n")
1393 self.command = c
1394 d = c.start()
1395 d.addCallback(self._abandonOnFailure)
1396 d.addCallback(self._didLogin)
1397 return d
1398 else:
1399 return self._didLogin(None)
1401 def _didLogin(self, res):
1402 # now we really start
1403 return SourceBase.start(self)
1405 def doVCUpdate(self):
1406 d = os.path.join(self.builder.basedir, self.srcdir)
1407 command = [self.vcexe, '-z3'] + self.global_options + ['update', '-dP']
1408 if self.branch:
1409 command += ['-r', self.branch]
1410 if self.revision:
1411 command += ['-D', self.revision]
1412 c = ShellCommand(self.builder, command, d,
1413 sendRC=False, timeout=self.timeout)
1414 self.command = c
1415 return c.start()
1417 def doVCFull(self):
1418 d = self.builder.basedir
1419 if self.mode == "export":
1420 verb = "export"
1421 else:
1422 verb = "checkout"
1423 command = ([self.vcexe, '-d', self.cvsroot, '-z3'] +
1424 self.global_options +
1425 [verb, '-d', self.srcdir])
1426 if self.branch:
1427 command += ['-r', self.branch]
1428 if self.revision:
1429 command += ['-D', self.revision]
1430 command += [self.cvsmodule]
1431 c = ShellCommand(self.builder, command, d,
1432 sendRC=False, timeout=self.timeout)
1433 self.command = c
1434 return c.start()
1436 def parseGotRevision(self):
1437 # CVS does not have any kind of revision stamp to speak of. We return
1438 # the current timestamp as a best-effort guess, but this depends upon
1439 # the local system having a clock that is
1440 # reasonably-well-synchronized with the repository.
1441 return time.strftime("%Y-%m-%d %H:%M:%S +0000", time.gmtime())
1443 registerSlaveCommand("cvs", CVS, command_version)
1445 class SVN(SourceBase):
1446 """Subversion-specific VC operation. In addition to the arguments
1447 handled by SourceBase, this command reads the following keys:
1449 ['svnurl'] (required): the SVN repository string
1452 header = "svn operation"
1454 def setup(self, args):
1455 SourceBase.setup(self, args)
1456 self.vcexe = getCommand("svn")
1457 self.svnurl = args['svnurl']
1458 self.sourcedata = "%s\n" % self.svnurl
1460 def sourcedirIsUpdateable(self):
1461 if os.path.exists(os.path.join(self.builder.basedir,
1462 self.srcdir, ".buildbot-patched")):
1463 return False
1464 return os.path.isdir(os.path.join(self.builder.basedir,
1465 self.srcdir, ".svn"))
1467 def doVCUpdate(self):
1468 revision = self.args['revision'] or 'HEAD'
1469 # update: possible for mode in ('copy', 'update')
1470 d = os.path.join(self.builder.basedir, self.srcdir)
1471 command = [self.vcexe, 'update', '--revision', str(revision),
1472 '--non-interactive']
1473 c = ShellCommand(self.builder, command, d,
1474 sendRC=False, timeout=self.timeout,
1475 keepStdout=True)
1476 self.command = c
1477 return c.start()
1479 def doVCFull(self):
1480 revision = self.args['revision'] or 'HEAD'
1481 d = self.builder.basedir
1482 if self.mode == "export":
1483 command = [self.vcexe, 'export', '--revision', str(revision),
1484 '--non-interactive',
1485 self.svnurl, self.srcdir]
1486 else:
1487 # mode=='clobber', or copy/update on a broken workspace
1488 command = [self.vcexe, 'checkout', '--revision', str(revision),
1489 '--non-interactive',
1490 self.svnurl, self.srcdir]
1491 c = ShellCommand(self.builder, command, d,
1492 sendRC=False, timeout=self.timeout,
1493 keepStdout=True)
1494 self.command = c
1495 return c.start()
1497 def parseGotRevision(self):
1498 # svn checkout operations finish with 'Checked out revision 16657.'
1499 # svn update operations finish the line 'At revision 16654.'
1500 # But we don't use those. Instead, run 'svnversion'.
1501 svnversion_command = getCommand("svnversion")
1502 # older versions of 'svnversion' (1.1.4) require the WC_PATH
1503 # argument, newer ones (1.3.1) do not.
1504 command = [svnversion_command, "."]
1505 c = ShellCommand(self.builder, command,
1506 os.path.join(self.builder.basedir, self.srcdir),
1507 environ=self.env,
1508 sendStdout=False, sendStderr=False, sendRC=False,
1509 keepStdout=True)
1510 c.usePTY = False
1511 d = c.start()
1512 def _parse(res):
1513 r = c.stdout.strip()
1514 # Support for removing svnversion indicator for 'modified'
1515 if r[-1] == 'M':
1516 r = r[:-1]
1517 got_version = None
1518 try:
1519 got_version = int(r)
1520 except ValueError:
1521 msg =("SVN.parseGotRevision unable to parse output "
1522 "of svnversion: '%s'" % r)
1523 log.msg(msg)
1524 self.sendStatus({'header': msg + "\n"})
1525 return got_version
1526 d.addCallback(_parse)
1527 return d
1530 registerSlaveCommand("svn", SVN, command_version)
1532 class Darcs(SourceBase):
1533 """Darcs-specific VC operation. In addition to the arguments
1534 handled by SourceBase, this command reads the following keys:
1536 ['repourl'] (required): the Darcs repository string
1539 header = "darcs operation"
1541 def setup(self, args):
1542 SourceBase.setup(self, args)
1543 self.vcexe = getCommand("darcs")
1544 self.repourl = args['repourl']
1545 self.sourcedata = "%s\n" % self.repourl
1546 self.revision = self.args.get('revision')
1548 def sourcedirIsUpdateable(self):
1549 if os.path.exists(os.path.join(self.builder.basedir,
1550 self.srcdir, ".buildbot-patched")):
1551 return False
1552 if self.revision:
1553 # checking out a specific revision requires a full 'darcs get'
1554 return False
1555 return os.path.isdir(os.path.join(self.builder.basedir,
1556 self.srcdir, "_darcs"))
1558 def doVCUpdate(self):
1559 assert not self.revision
1560 # update: possible for mode in ('copy', 'update')
1561 d = os.path.join(self.builder.basedir, self.srcdir)
1562 command = [self.vcexe, 'pull', '--all', '--verbose']
1563 c = ShellCommand(self.builder, command, d,
1564 sendRC=False, timeout=self.timeout)
1565 self.command = c
1566 return c.start()
1568 def doVCFull(self):
1569 # checkout or export
1570 d = self.builder.basedir
1571 command = [self.vcexe, 'get', '--verbose', '--partial',
1572 '--repo-name', self.srcdir]
1573 if self.revision:
1574 # write the context to a file
1575 n = os.path.join(self.builder.basedir, ".darcs-context")
1576 f = open(n, "wb")
1577 f.write(self.revision)
1578 f.close()
1579 # tell Darcs to use that context
1580 command.append('--context')
1581 command.append(n)
1582 command.append(self.repourl)
1584 c = ShellCommand(self.builder, command, d,
1585 sendRC=False, timeout=self.timeout)
1586 self.command = c
1587 d = c.start()
1588 if self.revision:
1589 d.addCallback(self.removeContextFile, n)
1590 return d
1592 def removeContextFile(self, res, n):
1593 os.unlink(n)
1594 return res
1596 def parseGotRevision(self):
1597 # we use 'darcs context' to find out what we wound up with
1598 command = [self.vcexe, "changes", "--context"]
1599 c = ShellCommand(self.builder, command,
1600 os.path.join(self.builder.basedir, self.srcdir),
1601 environ=self.env,
1602 sendStdout=False, sendStderr=False, sendRC=False,
1603 keepStdout=True)
1604 c.usePTY = False
1605 d = c.start()
1606 d.addCallback(lambda res: c.stdout)
1607 return d
1609 registerSlaveCommand("darcs", Darcs, command_version)
1611 class Monotone(SourceBase):
1612 """Monotone-specific VC operation. In addition to the arguments handled
1613 by SourceBase, this command reads the following keys:
1615 ['server_addr'] (required): the address of the server to pull from
1616 ['branch'] (required): the branch the revision is on
1617 ['db_path'] (required): the local database path to use
1618 ['revision'] (required): the revision to check out
1619 ['monotone']: (required): path to monotone executable
1622 header = "monotone operation"
1624 def setup(self, args):
1625 SourceBase.setup(self, args)
1626 self.server_addr = args["server_addr"]
1627 self.branch = args["branch"]
1628 self.db_path = args["db_path"]
1629 self.revision = args["revision"]
1630 self.monotone = args["monotone"]
1631 self._made_fulls = False
1632 self._pull_timeout = args["timeout"]
1634 def _makefulls(self):
1635 if not self._made_fulls:
1636 basedir = self.builder.basedir
1637 self.full_db_path = os.path.join(basedir, self.db_path)
1638 self.full_srcdir = os.path.join(basedir, self.srcdir)
1639 self._made_fulls = True
1641 def sourcedirIsUpdateable(self):
1642 self._makefulls()
1643 if os.path.exists(os.path.join(self.full_srcdir,
1644 ".buildbot_patched")):
1645 return False
1646 return (os.path.isfile(self.full_db_path)
1647 and os.path.isdir(os.path.join(self.full_srcdir, "MT")))
1649 def doVCUpdate(self):
1650 return self._withFreshDb(self._doUpdate)
1652 def _doUpdate(self):
1653 # update: possible for mode in ('copy', 'update')
1654 command = [self.monotone, "update",
1655 "-r", self.revision,
1656 "-b", self.branch]
1657 c = ShellCommand(self.builder, command, self.full_srcdir,
1658 sendRC=False, timeout=self.timeout)
1659 self.command = c
1660 return c.start()
1662 def doVCFull(self):
1663 return self._withFreshDb(self._doFull)
1665 def _doFull(self):
1666 command = [self.monotone, "--db=" + self.full_db_path,
1667 "checkout",
1668 "-r", self.revision,
1669 "-b", self.branch,
1670 self.full_srcdir]
1671 c = ShellCommand(self.builder, command, self.builder.basedir,
1672 sendRC=False, timeout=self.timeout)
1673 self.command = c
1674 return c.start()
1676 def _withFreshDb(self, callback):
1677 self._makefulls()
1678 # first ensure the db exists and is usable
1679 if os.path.isfile(self.full_db_path):
1680 # already exists, so run 'db migrate' in case monotone has been
1681 # upgraded under us
1682 command = [self.monotone, "db", "migrate",
1683 "--db=" + self.full_db_path]
1684 else:
1685 # We'll be doing an initial pull, so up the timeout to 3 hours to
1686 # make sure it will have time to complete.
1687 self._pull_timeout = max(self._pull_timeout, 3 * 60 * 60)
1688 self.sendStatus({"header": "creating database %s\n"
1689 % (self.full_db_path,)})
1690 command = [self.monotone, "db", "init",
1691 "--db=" + self.full_db_path]
1692 c = ShellCommand(self.builder, command, self.builder.basedir,
1693 sendRC=False, timeout=self.timeout)
1694 self.command = c
1695 d = c.start()
1696 d.addCallback(self._abandonOnFailure)
1697 d.addCallback(self._didDbInit)
1698 d.addCallback(self._didPull, callback)
1699 return d
1701 def _didDbInit(self, res):
1702 command = [self.monotone, "--db=" + self.full_db_path,
1703 "pull", "--ticker=dot", self.server_addr, self.branch]
1704 c = ShellCommand(self.builder, command, self.builder.basedir,
1705 sendRC=False, timeout=self._pull_timeout)
1706 self.sendStatus({"header": "pulling %s from %s\n"
1707 % (self.branch, self.server_addr)})
1708 self.command = c
1709 return c.start()
1711 def _didPull(self, res, callback):
1712 return callback()
1714 registerSlaveCommand("monotone", Monotone, command_version)
1717 class Git(SourceBase):
1718 """Git specific VC operation. In addition to the arguments
1719 handled by SourceBase, this command reads the following keys:
1721 ['repourl'] (required): the Cogito repository string
1724 header = "git operation"
1726 def setup(self, args):
1727 SourceBase.setup(self, args)
1728 self.repourl = args['repourl']
1729 #self.sourcedata = "" # TODO
1731 def sourcedirIsUpdateable(self):
1732 if os.path.exists(os.path.join(self.builder.basedir,
1733 self.srcdir, ".buildbot-patched")):
1734 return False
1735 return os.path.isdir(os.path.join(self.builder.basedir,
1736 self.srcdir, ".git"))
1738 def doVCUpdate(self):
1739 d = os.path.join(self.builder.basedir, self.srcdir)
1740 command = ['cg-update']
1741 c = ShellCommand(self.builder, command, d,
1742 sendRC=False, timeout=self.timeout)
1743 self.command = c
1744 return c.start()
1746 def doVCFull(self):
1747 d = os.path.join(self.builder.basedir, self.srcdir)
1748 os.mkdir(d)
1749 command = ['cg-clone', '-s', self.repourl]
1750 c = ShellCommand(self.builder, command, d,
1751 sendRC=False, timeout=self.timeout)
1752 self.command = c
1753 return c.start()
1755 registerSlaveCommand("git", Git, command_version)
1757 class Arch(SourceBase):
1758 """Arch-specific (tla-specific) VC operation. In addition to the
1759 arguments handled by SourceBase, this command reads the following keys:
1761 ['url'] (required): the repository string
1762 ['version'] (required): which version (i.e. branch) to retrieve
1763 ['revision'] (optional): the 'patch-NN' argument to check out
1764 ['archive']: the archive name to use. If None, use the archive's default
1765 ['build-config']: if present, give to 'tla build-config' after checkout
1768 header = "arch operation"
1769 buildconfig = None
1771 def setup(self, args):
1772 SourceBase.setup(self, args)
1773 self.vcexe = getCommand("tla")
1774 self.archive = args.get('archive')
1775 self.url = args['url']
1776 self.version = args['version']
1777 self.revision = args.get('revision')
1778 self.buildconfig = args.get('build-config')
1779 self.sourcedata = "%s\n%s\n%s\n" % (self.url, self.version,
1780 self.buildconfig)
1782 def sourcedirIsUpdateable(self):
1783 if self.revision:
1784 # Arch cannot roll a directory backwards, so if they ask for a
1785 # specific revision, clobber the directory. Technically this
1786 # could be limited to the cases where the requested revision is
1787 # later than our current one, but it's too hard to extract the
1788 # current revision from the tree.
1789 return False
1790 if os.path.exists(os.path.join(self.builder.basedir,
1791 self.srcdir, ".buildbot-patched")):
1792 return False
1793 return os.path.isdir(os.path.join(self.builder.basedir,
1794 self.srcdir, "{arch}"))
1796 def doVCUpdate(self):
1797 # update: possible for mode in ('copy', 'update')
1798 d = os.path.join(self.builder.basedir, self.srcdir)
1799 command = [self.vcexe, 'replay']
1800 if self.revision:
1801 command.append(self.revision)
1802 c = ShellCommand(self.builder, command, d,
1803 sendRC=False, timeout=self.timeout)
1804 self.command = c
1805 return c.start()
1807 def doVCFull(self):
1808 # to do a checkout, we must first "register" the archive by giving
1809 # the URL to tla, which will go to the repository at that URL and
1810 # figure out the archive name. tla will tell you the archive name
1811 # when it is done, and all further actions must refer to this name.
1813 command = [self.vcexe, 'register-archive', '--force', self.url]
1814 c = ShellCommand(self.builder, command, self.builder.basedir,
1815 sendRC=False, keepStdout=True,
1816 timeout=self.timeout)
1817 self.command = c
1818 d = c.start()
1819 d.addCallback(self._abandonOnFailure)
1820 d.addCallback(self._didRegister, c)
1821 return d
1823 def _didRegister(self, res, c):
1824 # find out what tla thinks the archive name is. If the user told us
1825 # to use something specific, make sure it matches.
1826 r = re.search(r'Registering archive: (\S+)\s*$', c.stdout)
1827 if r:
1828 msg = "tla reports archive name is '%s'" % r.group(1)
1829 log.msg(msg)
1830 self.builder.sendUpdate({'header': msg+"\n"})
1831 if self.archive and r.group(1) != self.archive:
1832 msg = (" mismatch, we wanted an archive named '%s'"
1833 % self.archive)
1834 log.msg(msg)
1835 self.builder.sendUpdate({'header': msg+"\n"})
1836 raise AbandonChain(-1)
1837 self.archive = r.group(1)
1838 assert self.archive, "need archive name to continue"
1839 return self._doGet()
1841 def _doGet(self):
1842 ver = self.version
1843 if self.revision:
1844 ver += "--%s" % self.revision
1845 command = [self.vcexe, 'get', '--archive', self.archive,
1846 '--no-pristine',
1847 ver, self.srcdir]
1848 c = ShellCommand(self.builder, command, self.builder.basedir,
1849 sendRC=False, timeout=self.timeout)
1850 self.command = c
1851 d = c.start()
1852 d.addCallback(self._abandonOnFailure)
1853 if self.buildconfig:
1854 d.addCallback(self._didGet)
1855 return d
1857 def _didGet(self, res):
1858 d = os.path.join(self.builder.basedir, self.srcdir)
1859 command = [self.vcexe, 'build-config', self.buildconfig]
1860 c = ShellCommand(self.builder, command, d,
1861 sendRC=False, timeout=self.timeout)
1862 self.command = c
1863 d = c.start()
1864 d.addCallback(self._abandonOnFailure)
1865 return d
1867 def parseGotRevision(self):
1868 # using code from tryclient.TlaExtractor
1869 # 'tla logs --full' gives us ARCHIVE/BRANCH--REVISION
1870 # 'tla logs' gives us REVISION
1871 command = [self.vcexe, "logs", "--full", "--reverse"]
1872 c = ShellCommand(self.builder, command,
1873 os.path.join(self.builder.basedir, self.srcdir),
1874 environ=self.env,
1875 sendStdout=False, sendStderr=False, sendRC=False,
1876 keepStdout=True)
1877 c.usePTY = False
1878 d = c.start()
1879 def _parse(res):
1880 tid = c.stdout.split("\n")[0].strip()
1881 slash = tid.index("/")
1882 dd = tid.rindex("--")
1883 #branch = tid[slash+1:dd]
1884 baserev = tid[dd+2:]
1885 return baserev
1886 d.addCallback(_parse)
1887 return d
1889 registerSlaveCommand("arch", Arch, command_version)
1891 class Bazaar(Arch):
1892 """Bazaar (/usr/bin/baz) is an alternative client for Arch repositories.
1893 It is mostly option-compatible, but archive registration is different
1894 enough to warrant a separate Command.
1896 ['archive'] (required): the name of the archive being used
1899 def setup(self, args):
1900 Arch.setup(self, args)
1901 self.vcexe = getCommand("baz")
1902 # baz doesn't emit the repository name after registration (and
1903 # grepping through the output of 'baz archives' is too hard), so we
1904 # require that the buildmaster configuration to provide both the
1905 # archive name and the URL.
1906 self.archive = args['archive'] # required for Baz
1907 self.sourcedata = "%s\n%s\n%s\n" % (self.url, self.version,
1908 self.buildconfig)
1910 # in _didRegister, the regexp won't match, so we'll stick with the name
1911 # in self.archive
1913 def _doGet(self):
1914 # baz prefers ARCHIVE/VERSION. This will work even if
1915 # my-default-archive is not set.
1916 ver = self.archive + "/" + self.version
1917 if self.revision:
1918 ver += "--%s" % self.revision
1919 command = [self.vcexe, 'get', '--no-pristine',
1920 ver, self.srcdir]
1921 c = ShellCommand(self.builder, command, self.builder.basedir,
1922 sendRC=False, timeout=self.timeout)
1923 self.command = c
1924 d = c.start()
1925 d.addCallback(self._abandonOnFailure)
1926 if self.buildconfig:
1927 d.addCallback(self._didGet)
1928 return d
1930 def parseGotRevision(self):
1931 # using code from tryclient.BazExtractor
1932 command = [self.vcexe, "tree-id"]
1933 c = ShellCommand(self.builder, command,
1934 os.path.join(self.builder.basedir, self.srcdir),
1935 environ=self.env,
1936 sendStdout=False, sendStderr=False, sendRC=False,
1937 keepStdout=True)
1938 c.usePTY = False
1939 d = c.start()
1940 def _parse(res):
1941 tid = c.stdout.strip()
1942 slash = tid.index("/")
1943 dd = tid.rindex("--")
1944 #branch = tid[slash+1:dd]
1945 baserev = tid[dd+2:]
1946 return baserev
1947 d.addCallback(_parse)
1948 return d
1950 registerSlaveCommand("bazaar", Bazaar, command_version)
1953 class Bzr(SourceBase):
1954 """bzr-specific VC operation. In addition to the arguments
1955 handled by SourceBase, this command reads the following keys:
1957 ['repourl'] (required): the Bzr repository string
1960 header = "bzr operation"
1962 def setup(self, args):
1963 SourceBase.setup(self, args)
1964 self.vcexe = getCommand("bzr")
1965 self.repourl = args['repourl']
1966 self.sourcedata = "%s\n" % self.repourl
1967 self.revision = self.args.get('revision')
1969 def sourcedirIsUpdateable(self):
1970 if os.path.exists(os.path.join(self.builder.basedir,
1971 self.srcdir, ".buildbot-patched")):
1972 return False
1973 if self.revision:
1974 # checking out a specific revision requires a full 'bzr checkout'
1975 return False
1976 return os.path.isdir(os.path.join(self.builder.basedir,
1977 self.srcdir, ".bzr"))
1979 def doVCUpdate(self):
1980 assert not self.revision
1981 # update: possible for mode in ('copy', 'update')
1982 srcdir = os.path.join(self.builder.basedir, self.srcdir)
1983 command = [self.vcexe, 'update']
1984 c = ShellCommand(self.builder, command, srcdir,
1985 sendRC=False, timeout=self.timeout)
1986 self.command = c
1987 return c.start()
1989 def doVCFull(self):
1990 # checkout or export
1991 d = self.builder.basedir
1992 if self.mode == "export":
1993 # exporting in bzr requires a separate directory
1994 return self.doVCExport()
1995 # originally I added --lightweight here, but then 'bzr revno' is
1996 # wrong. The revno reported in 'bzr version-info' is correct,
1997 # however. Maybe this is a bzr bug?
1999 # In addition, you cannot perform a 'bzr update' on a repo pulled
2000 # from an HTTP repository that used 'bzr checkout --lightweight'. You
2001 # get a "ERROR: Cannot lock: transport is read only" when you try.
2003 # So I won't bother using --lightweight for now.
2005 command = [self.vcexe, 'checkout']
2006 if self.revision:
2007 command.append('--revision')
2008 command.append(str(self.revision))
2009 command.append(self.repourl)
2010 command.append(self.srcdir)
2012 c = ShellCommand(self.builder, command, d,
2013 sendRC=False, timeout=self.timeout)
2014 self.command = c
2015 d = c.start()
2016 return d
2018 def doVCExport(self):
2019 tmpdir = os.path.join(self.builder.basedir, "export-temp")
2020 srcdir = os.path.join(self.builder.basedir, self.srcdir)
2021 command = [self.vcexe, 'checkout', '--lightweight']
2022 if self.revision:
2023 command.append('--revision')
2024 command.append(str(self.revision))
2025 command.append(self.repourl)
2026 command.append(tmpdir)
2027 c = ShellCommand(self.builder, command, self.builder.basedir,
2028 sendRC=False, timeout=self.timeout)
2029 self.command = c
2030 d = c.start()
2031 def _export(res):
2032 command = [self.vcexe, 'export', srcdir]
2033 c = ShellCommand(self.builder, command, tmpdir,
2034 sendRC=False, timeout=self.timeout)
2035 self.command = c
2036 return c.start()
2037 d.addCallback(_export)
2038 return d
2040 def get_revision_number(self, out):
2041 # it feels like 'bzr revno' sometimes gives different results than
2042 # the 'revno:' line from 'bzr version-info', and the one from
2043 # version-info is more likely to be correct.
2044 for line in out.split("\n"):
2045 colon = line.find(":")
2046 if colon != -1:
2047 key, value = line[:colon], line[colon+2:]
2048 if key == "revno":
2049 return int(value)
2050 raise ValueError("unable to find revno: in bzr output: '%s'" % out)
2052 def parseGotRevision(self):
2053 command = [self.vcexe, "version-info"]
2054 c = ShellCommand(self.builder, command,
2055 os.path.join(self.builder.basedir, self.srcdir),
2056 environ=self.env,
2057 sendStdout=False, sendStderr=False, sendRC=False,
2058 keepStdout=True)
2059 c.usePTY = False
2060 d = c.start()
2061 def _parse(res):
2062 try:
2063 return self.get_revision_number(c.stdout)
2064 except ValueError:
2065 msg =("Bzr.parseGotRevision unable to parse output "
2066 "of bzr version-info: '%s'" % c.stdout.strip())
2067 log.msg(msg)
2068 self.sendStatus({'header': msg + "\n"})
2069 return None
2070 d.addCallback(_parse)
2071 return d
2073 registerSlaveCommand("bzr", Bzr, command_version)
2075 class Mercurial(SourceBase):
2076 """Mercurial specific VC operation. In addition to the arguments
2077 handled by SourceBase, this command reads the following keys:
2079 ['repourl'] (required): the Cogito repository string
2082 header = "mercurial operation"
2084 def setup(self, args):
2085 SourceBase.setup(self, args)
2086 self.vcexe = getCommand("hg")
2087 self.repourl = args['repourl']
2088 self.sourcedata = "%s\n" % self.repourl
2089 self.stdout = ""
2090 self.stderr = ""
2092 def sourcedirIsUpdateable(self):
2093 if os.path.exists(os.path.join(self.builder.basedir,
2094 self.srcdir, ".buildbot-patched")):
2095 return False
2096 # like Darcs, to check out a specific (old) revision, we have to do a
2097 # full checkout. TODO: I think 'hg pull' plus 'hg update' might work
2098 if self.revision:
2099 return False
2100 return os.path.isdir(os.path.join(self.builder.basedir,
2101 self.srcdir, ".hg"))
2103 def doVCUpdate(self):
2104 d = os.path.join(self.builder.basedir, self.srcdir)
2105 command = [self.vcexe, 'pull', '--update', '--verbose']
2106 if self.args['revision']:
2107 command.extend(['--rev', self.args['revision']])
2108 c = ShellCommand(self.builder, command, d,
2109 sendRC=False, timeout=self.timeout,
2110 keepStdout=True)
2111 self.command = c
2112 d = c.start()
2113 d.addCallback(self._handleEmptyUpdate)
2114 return d
2116 def _handleEmptyUpdate(self, res):
2117 if type(res) is int and res == 1:
2118 if self.command.stdout.find("no changes found") != -1:
2119 # 'hg pull', when it doesn't have anything to do, exits with
2120 # rc=1, and there appears to be no way to shut this off. It
2121 # emits a distinctive message to stdout, though. So catch
2122 # this and pretend that it completed successfully.
2123 return 0
2124 return res
2126 def doVCFull(self):
2127 d = os.path.join(self.builder.basedir, self.srcdir)
2128 command = [self.vcexe, 'clone']
2129 if self.args['revision']:
2130 command.extend(['--rev', self.args['revision']])
2131 command.extend([self.repourl, d])
2132 c = ShellCommand(self.builder, command, self.builder.basedir,
2133 sendRC=False, timeout=self.timeout)
2134 self.command = c
2135 return c.start()
2137 def parseGotRevision(self):
2138 # we use 'hg identify' to find out what we wound up with
2139 command = [self.vcexe, "identify"]
2140 c = ShellCommand(self.builder, command,
2141 os.path.join(self.builder.basedir, self.srcdir),
2142 environ=self.env,
2143 sendStdout=False, sendStderr=False, sendRC=False,
2144 keepStdout=True)
2145 d = c.start()
2146 def _parse(res):
2147 m = re.search(r'^(\w+)', c.stdout)
2148 return m.group(1)
2149 d.addCallback(_parse)
2150 return d
2152 registerSlaveCommand("hg", Mercurial, command_version)
2155 class P4(SourceBase):
2156 """A P4 source-updater.
2158 ['p4port'] (required): host:port for server to access
2159 ['p4user'] (optional): user to use for access
2160 ['p4passwd'] (optional): passwd to try for the user
2161 ['p4client'] (optional): client spec to use
2162 ['p4extra_views'] (optional): additional client views to use
2165 header = "p4"
2167 def setup(self, args):
2168 SourceBase.setup(self, args)
2169 self.p4port = args['p4port']
2170 self.p4client = args['p4client']
2171 self.p4user = args['p4user']
2172 self.p4passwd = args['p4passwd']
2173 self.p4base = args['p4base']
2174 self.p4extra_views = args['p4extra_views']
2175 self.p4mode = args['mode']
2176 self.p4branch = args['branch']
2178 self.sourcedata = str([
2179 # Perforce server.
2180 self.p4port,
2182 # Client spec.
2183 self.p4client,
2185 # Depot side of view spec.
2186 self.p4base,
2187 self.p4branch,
2188 self.p4extra_views,
2190 # Local side of view spec (srcdir is made from these).
2191 self.builder.basedir,
2192 self.mode,
2193 self.workdir
2197 def sourcedirIsUpdateable(self):
2198 if os.path.exists(os.path.join(self.builder.basedir,
2199 self.srcdir, ".buildbot-patched")):
2200 return False
2201 # We assume our client spec is still around.
2202 # We just say we aren't updateable if the dir doesn't exist so we
2203 # don't get ENOENT checking the sourcedata.
2204 return os.path.isdir(os.path.join(self.builder.basedir,
2205 self.srcdir))
2207 def doVCUpdate(self):
2208 return self._doP4Sync(force=False)
2210 def _doP4Sync(self, force):
2211 command = ['p4']
2213 if self.p4port:
2214 command.extend(['-p', self.p4port])
2215 if self.p4user:
2216 command.extend(['-u', self.p4user])
2217 if self.p4passwd:
2218 command.extend(['-P', self.p4passwd])
2219 if self.p4client:
2220 command.extend(['-c', self.p4client])
2221 command.extend(['sync'])
2222 if force:
2223 command.extend(['-f'])
2224 if self.revision:
2225 command.extend(['@' + str(self.revision)])
2226 env = {}
2227 c = ShellCommand(self.builder, command, self.builder.basedir,
2228 environ=env, sendRC=False, timeout=self.timeout,
2229 keepStdout=True)
2230 self.command = c
2231 d = c.start()
2232 d.addCallback(self._abandonOnFailure)
2233 return d
2236 def doVCFull(self):
2237 env = {}
2238 command = ['p4']
2239 client_spec = ''
2240 client_spec += "Client: %s\n\n" % self.p4client
2241 client_spec += "Owner: %s\n\n" % self.p4user
2242 client_spec += "Description:\n\tCreated by %s\n\n" % self.p4user
2243 client_spec += "Root:\t%s\n\n" % self.builder.basedir
2244 client_spec += "Options:\tallwrite rmdir\n\n"
2245 client_spec += "LineEnd:\tlocal\n\n"
2247 # Setup a view
2248 client_spec += "View:\n\t%s" % (self.p4base)
2249 if self.p4branch:
2250 client_spec += "%s/" % (self.p4branch)
2251 client_spec += "... //%s/%s/...\n" % (self.p4client, self.srcdir)
2252 if self.p4extra_views:
2253 for k, v in self.p4extra_views:
2254 client_spec += "\t%s/... //%s/%s%s/...\n" % (k, self.p4client,
2255 self.srcdir, v)
2256 if self.p4port:
2257 command.extend(['-p', self.p4port])
2258 if self.p4user:
2259 command.extend(['-u', self.p4user])
2260 if self.p4passwd:
2261 command.extend(['-P', self.p4passwd])
2262 command.extend(['client', '-i'])
2263 log.msg(client_spec)
2264 c = ShellCommand(self.builder, command, self.builder.basedir,
2265 environ=env, sendRC=False, timeout=self.timeout,
2266 initialStdin=client_spec)
2267 self.command = c
2268 d = c.start()
2269 d.addCallback(self._abandonOnFailure)
2270 d.addCallback(lambda _: self._doP4Sync(force=True))
2271 return d
2273 registerSlaveCommand("p4", P4, command_version)
2276 class P4Sync(SourceBase):
2277 """A partial P4 source-updater. Requires manual setup of a per-slave P4
2278 environment. The only thing which comes from the master is P4PORT.
2279 'mode' is required to be 'copy'.
2281 ['p4port'] (required): host:port for server to access
2282 ['p4user'] (optional): user to use for access
2283 ['p4passwd'] (optional): passwd to try for the user
2284 ['p4client'] (optional): client spec to use
2287 header = "p4 sync"
2289 def setup(self, args):
2290 SourceBase.setup(self, args)
2291 self.vcexe = getCommand("p4")
2292 self.p4port = args['p4port']
2293 self.p4user = args['p4user']
2294 self.p4passwd = args['p4passwd']
2295 self.p4client = args['p4client']
2297 def sourcedirIsUpdateable(self):
2298 return True
2300 def _doVC(self, force):
2301 d = os.path.join(self.builder.basedir, self.srcdir)
2302 command = [self.vcexe]
2303 if self.p4port:
2304 command.extend(['-p', self.p4port])
2305 if self.p4user:
2306 command.extend(['-u', self.p4user])
2307 if self.p4passwd:
2308 command.extend(['-P', self.p4passwd])
2309 if self.p4client:
2310 command.extend(['-c', self.p4client])
2311 command.extend(['sync'])
2312 if force:
2313 command.extend(['-f'])
2314 if self.revision:
2315 command.extend(['@' + self.revision])
2316 env = {}
2317 c = ShellCommand(self.builder, command, d, environ=env,
2318 sendRC=False, timeout=self.timeout)
2319 self.command = c
2320 return c.start()
2322 def doVCUpdate(self):
2323 return self._doVC(force=False)
2325 def doVCFull(self):
2326 return self._doVC(force=True)
2328 registerSlaveCommand("p4sync", P4Sync, command_version)