compress old changelog
[buildbot.git] / buildbot / scheduler.py
blobb556512a57e29097e00c9a07ae6eab88d8ae4ca1
1 # -*- test-case-name: buildbot.test.test_dependencies -*-
3 import time, os.path
5 from zope.interface import implements
6 from twisted.internet import reactor
7 from twisted.application import service, internet, strports
8 from twisted.python import log, runtime
9 from twisted.protocols import basic
10 from twisted.cred import portal, checkers
11 from twisted.spread import pb
13 from buildbot import interfaces, buildset, util, pbutil
14 from buildbot.status import builder
15 from buildbot.sourcestamp import SourceStamp
16 from buildbot.changes.maildir import MaildirService
17 from buildbot.process.properties import Properties
20 class BaseScheduler(service.MultiService, util.ComparableMixin):
21 """
22 A Schduler creates BuildSets and submits them to the BuildMaster.
24 @ivar name: name of the scheduler
26 @ivar properties: additional properties specified in this
27 scheduler's configuration
28 @type properties: Properties object
29 """
30 implements(interfaces.IScheduler)
32 def __init__(self, name, properties={}):
33 """
34 @param name: name for this scheduler
36 @param properties: properties to be propagated from this scheduler
37 @type properties: dict
38 """
39 service.MultiService.__init__(self)
40 self.name = name
41 self.properties = Properties()
42 self.properties.update(properties, "Scheduler")
43 self.properties.setProperty("scheduler", name, "Scheduler")
45 def __repr__(self):
46 # TODO: why can't id() return a positive number? %d is ugly.
47 return "<Scheduler '%s' at %d>" % (self.name, id(self))
49 def submitBuildSet(self, bs):
50 self.parent.submitBuildSet(bs)
52 def addChange(self, change):
53 pass
55 class BaseUpstreamScheduler(BaseScheduler):
56 implements(interfaces.IUpstreamScheduler)
58 def __init__(self, name, properties={}):
59 BaseScheduler.__init__(self, name, properties)
60 self.successWatchers = []
62 def subscribeToSuccessfulBuilds(self, watcher):
63 self.successWatchers.append(watcher)
64 def unsubscribeToSuccessfulBuilds(self, watcher):
65 self.successWatchers.remove(watcher)
67 def submitBuildSet(self, bs):
68 d = bs.waitUntilFinished()
69 d.addCallback(self.buildSetFinished)
70 BaseScheduler.submitBuildSet(self, bs)
72 def buildSetFinished(self, bss):
73 if not self.running:
74 return
75 if bss.getResults() == builder.SUCCESS:
76 ss = bss.getSourceStamp()
77 for w in self.successWatchers:
78 w(ss)
81 class Scheduler(BaseUpstreamScheduler):
82 """The default Scheduler class will run a build after some period of time
83 called the C{treeStableTimer}, on a given set of Builders. It only pays
84 attention to a single branch. You you can provide a C{fileIsImportant}
85 function which will evaluate each Change to decide whether or not it
86 should trigger a new build.
87 """
89 fileIsImportant = None
90 compare_attrs = ('name', 'treeStableTimer', 'builderNames', 'branch',
91 'fileIsImportant', 'properties')
93 def __init__(self, name, branch, treeStableTimer, builderNames,
94 fileIsImportant=None, properties={}):
95 """
96 @param name: the name of this Scheduler
97 @param branch: The branch name that the Scheduler should pay
98 attention to. Any Change that is not on this branch
99 will be ignored. It can be set to None to only pay
100 attention to the default branch.
101 @param treeStableTimer: the duration, in seconds, for which the tree
102 must remain unchanged before a build will be
103 triggered. This is intended to avoid builds
104 of partially-committed fixes.
105 @param builderNames: a list of Builder names. When this Scheduler
106 decides to start a set of builds, they will be
107 run on the Builders named by this list.
109 @param fileIsImportant: A callable which takes one argument (a Change
110 instance) and returns True if the change is
111 worth building, and False if it is not.
112 Unimportant Changes are accumulated until the
113 build is triggered by an important change.
114 The default value of None means that all
115 Changes are important.
117 @param properties: properties to apply to all builds started from this
118 scheduler
121 BaseUpstreamScheduler.__init__(self, name, properties)
122 self.treeStableTimer = treeStableTimer
123 errmsg = ("The builderNames= argument to Scheduler must be a list "
124 "of Builder description names (i.e. the 'name' key of the "
125 "Builder specification dictionary)")
126 assert isinstance(builderNames, (list, tuple)), errmsg
127 for b in builderNames:
128 assert isinstance(b, str), errmsg
129 self.builderNames = builderNames
130 self.branch = branch
131 if fileIsImportant:
132 assert callable(fileIsImportant)
133 self.fileIsImportant = fileIsImportant
135 self.importantChanges = []
136 self.unimportantChanges = []
137 self.nextBuildTime = None
138 self.timer = None
140 def listBuilderNames(self):
141 return self.builderNames
143 def getPendingBuildTimes(self):
144 if self.nextBuildTime is not None:
145 return [self.nextBuildTime]
146 return []
148 def addChange(self, change):
149 if change.branch != self.branch:
150 log.msg("%s ignoring off-branch %s" % (self, change))
151 return
152 if not self.fileIsImportant:
153 self.addImportantChange(change)
154 elif self.fileIsImportant(change):
155 self.addImportantChange(change)
156 else:
157 self.addUnimportantChange(change)
159 def addImportantChange(self, change):
160 log.msg("%s: change is important, adding %s" % (self, change))
161 self.importantChanges.append(change)
162 self.nextBuildTime = max(self.nextBuildTime,
163 change.when + self.treeStableTimer)
164 self.setTimer(self.nextBuildTime)
166 def addUnimportantChange(self, change):
167 log.msg("%s: change is not important, adding %s" % (self, change))
168 self.unimportantChanges.append(change)
170 def setTimer(self, when):
171 log.msg("%s: setting timer to %s" %
172 (self, time.strftime("%H:%M:%S", time.localtime(when))))
173 now = util.now()
174 if when < now:
175 when = now + 1
176 if self.timer:
177 self.timer.cancel()
178 self.timer = reactor.callLater(when - now, self.fireTimer)
180 def stopTimer(self):
181 if self.timer:
182 self.timer.cancel()
183 self.timer = None
185 def fireTimer(self):
186 # clear out our state
187 self.timer = None
188 self.nextBuildTime = None
189 changes = self.importantChanges + self.unimportantChanges
190 self.importantChanges = []
191 self.unimportantChanges = []
193 # create a BuildSet, submit it to the BuildMaster
194 bs = buildset.BuildSet(self.builderNames,
195 SourceStamp(changes=changes),
196 properties=self.properties)
197 self.submitBuildSet(bs)
199 def stopService(self):
200 self.stopTimer()
201 return service.MultiService.stopService(self)
204 class AnyBranchScheduler(BaseUpstreamScheduler):
205 """This Scheduler will handle changes on a variety of branches. It will
206 accumulate Changes for each branch separately. It works by creating a
207 separate Scheduler for each new branch it sees."""
209 schedulerFactory = Scheduler
210 fileIsImportant = None
212 compare_attrs = ('name', 'branches', 'treeStableTimer', 'builderNames',
213 'fileIsImportant', 'properties')
215 def __init__(self, name, branches, treeStableTimer, builderNames,
216 fileIsImportant=None, properties={}):
218 @param name: the name of this Scheduler
219 @param branches: The branch names that the Scheduler should pay
220 attention to. Any Change that is not on one of these
221 branches will be ignored. It can be set to None to
222 accept changes from any branch. Don't use [] (an
223 empty list), because that means we don't pay
224 attention to *any* branches, so we'll never build
225 anything.
226 @param treeStableTimer: the duration, in seconds, for which the tree
227 must remain unchanged before a build will be
228 triggered. This is intended to avoid builds
229 of partially-committed fixes.
230 @param builderNames: a list of Builder names. When this Scheduler
231 decides to start a set of builds, they will be
232 run on the Builders named by this list.
234 @param fileIsImportant: A callable which takes one argument (a Change
235 instance) and returns True if the change is
236 worth building, and False if it is not.
237 Unimportant Changes are accumulated until the
238 build is triggered by an important change.
239 The default value of None means that all
240 Changes are important.
242 @param properties: properties to apply to all builds started from this
243 scheduler
246 BaseUpstreamScheduler.__init__(self, name, properties)
247 self.treeStableTimer = treeStableTimer
248 for b in builderNames:
249 assert isinstance(b, str)
250 self.builderNames = builderNames
251 self.branches = branches
252 if self.branches == []:
253 log.msg("AnyBranchScheduler %s: branches=[], so we will ignore "
254 "all branches, and never trigger any builds. Please set "
255 "branches=None to mean 'all branches'" % self)
256 # consider raising an exception here, to make this warning more
257 # prominent, but I can vaguely imagine situations where you might
258 # want to comment out branches temporarily and wouldn't
259 # appreciate it being treated as an error.
260 if fileIsImportant:
261 assert callable(fileIsImportant)
262 self.fileIsImportant = fileIsImportant
263 self.schedulers = {} # one per branch
265 def __repr__(self):
266 return "<AnyBranchScheduler '%s'>" % self.name
268 def listBuilderNames(self):
269 return self.builderNames
271 def getPendingBuildTimes(self):
272 bts = []
273 for s in self.schedulers.values():
274 if s.nextBuildTime is not None:
275 bts.append(s.nextBuildTime)
276 return bts
278 def addChange(self, change):
279 branch = change.branch
280 if self.branches is not None and branch not in self.branches:
281 log.msg("%s ignoring off-branch %s" % (self, change))
282 return
283 s = self.schedulers.get(branch)
284 if not s:
285 if branch:
286 name = self.name + "." + branch
287 else:
288 name = self.name + ".<default>"
289 s = self.schedulerFactory(name, branch,
290 self.treeStableTimer,
291 self.builderNames,
292 self.fileIsImportant)
293 s.successWatchers = self.successWatchers
294 s.setServiceParent(self)
295 # TODO: does this result in schedulers that stack up forever?
296 # When I make the persistify-pass, think about this some more.
297 self.schedulers[branch] = s
298 s.addChange(change)
301 class Dependent(BaseUpstreamScheduler):
302 """This scheduler runs some set of 'downstream' builds when the
303 'upstream' scheduler has completed successfully."""
305 compare_attrs = ('name', 'upstream', 'builders', 'properties')
307 def __init__(self, name, upstream, builderNames, properties={}):
308 assert interfaces.IUpstreamScheduler.providedBy(upstream)
309 BaseUpstreamScheduler.__init__(self, name, properties)
310 self.upstream = upstream
311 self.builderNames = builderNames
313 def listBuilderNames(self):
314 return self.builderNames
316 def getPendingBuildTimes(self):
317 # report the upstream's value
318 return self.upstream.getPendingBuildTimes()
320 def startService(self):
321 service.MultiService.startService(self)
322 self.upstream.subscribeToSuccessfulBuilds(self.upstreamBuilt)
324 def stopService(self):
325 d = service.MultiService.stopService(self)
326 self.upstream.unsubscribeToSuccessfulBuilds(self.upstreamBuilt)
327 return d
329 def upstreamBuilt(self, ss):
330 bs = buildset.BuildSet(self.builderNames, ss,
331 properties=self.properties)
332 self.submitBuildSet(bs)
336 class Periodic(BaseUpstreamScheduler):
337 """Instead of watching for Changes, this Scheduler can just start a build
338 at fixed intervals. The C{periodicBuildTimer} parameter sets the number
339 of seconds to wait between such periodic builds. The first build will be
340 run immediately."""
342 # TODO: consider having this watch another (changed-based) scheduler and
343 # merely enforce a minimum time between builds.
345 compare_attrs = ('name', 'builderNames', 'periodicBuildTimer', 'branch', 'properties')
347 def __init__(self, name, builderNames, periodicBuildTimer,
348 branch=None, properties={}):
349 BaseUpstreamScheduler.__init__(self, name, properties)
350 self.builderNames = builderNames
351 self.periodicBuildTimer = periodicBuildTimer
352 self.branch = branch
353 self.reason = ("The Periodic scheduler named '%s' triggered this build"
354 % name)
355 self.timer = internet.TimerService(self.periodicBuildTimer,
356 self.doPeriodicBuild)
357 self.timer.setServiceParent(self)
359 def listBuilderNames(self):
360 return self.builderNames
362 def getPendingBuildTimes(self):
363 # TODO: figure out when self.timer is going to fire next and report
364 # that
365 return []
367 def doPeriodicBuild(self):
368 bs = buildset.BuildSet(self.builderNames,
369 SourceStamp(branch=self.branch),
370 self.reason,
371 properties=self.properties)
372 self.submitBuildSet(bs)
376 class Nightly(BaseUpstreamScheduler):
377 """Imitate 'cron' scheduling. This can be used to schedule a nightly
378 build, or one which runs are certain times of the day, week, or month.
380 Pass some subset of minute, hour, dayOfMonth, month, and dayOfWeek; each
381 may be a single number or a list of valid values. The builds will be
382 triggered whenever the current time matches these values. Wildcards are
383 represented by a '*' string. All fields default to a wildcard except
384 'minute', so with no fields this defaults to a build every hour, on the
385 hour.
387 For example, the following master.cfg clause will cause a build to be
388 started every night at 3:00am::
390 s = Nightly('nightly', ['builder1', 'builder2'], hour=3, minute=0)
391 c['schedules'].append(s)
393 This scheduler will perform a build each monday morning at 6:23am and
394 again at 8:23am::
396 s = Nightly('BeforeWork', ['builder1'],
397 dayOfWeek=0, hour=[6,8], minute=23)
399 The following runs a build every two hours::
401 s = Nightly('every2hours', ['builder1'], hour=range(0, 24, 2))
403 And this one will run only on December 24th::
405 s = Nightly('SleighPreflightCheck', ['flying_circuits', 'radar'],
406 month=12, dayOfMonth=24, hour=12, minute=0)
408 For dayOfWeek and dayOfMonth, builds are triggered if the date matches
409 either of them. All time values are compared against the tuple returned
410 by time.localtime(), so month and dayOfMonth numbers start at 1, not
411 zero. dayOfWeek=0 is Monday, dayOfWeek=6 is Sunday.
414 compare_attrs = ('name', 'builderNames',
415 'minute', 'hour', 'dayOfMonth', 'month',
416 'dayOfWeek', 'branch', 'properties')
418 def __init__(self, name, builderNames, minute=0, hour='*',
419 dayOfMonth='*', month='*', dayOfWeek='*',
420 branch=None, properties={}):
421 # Setting minute=0 really makes this an 'Hourly' scheduler. This
422 # seemed like a better default than minute='*', which would result in
423 # a build every 60 seconds.
424 BaseUpstreamScheduler.__init__(self, name, properties)
425 self.builderNames = builderNames
426 self.minute = minute
427 self.hour = hour
428 self.dayOfMonth = dayOfMonth
429 self.month = month
430 self.dayOfWeek = dayOfWeek
431 self.branch = branch
432 self.delayedRun = None
433 self.nextRunTime = None
434 self.reason = ("The Nightly scheduler named '%s' triggered this build"
435 % name)
437 def addTime(self, timetuple, secs):
438 return time.localtime(time.mktime(timetuple)+secs)
439 def findFirstValueAtLeast(self, values, value, default=None):
440 for v in values:
441 if v >= value: return v
442 return default
444 def setTimer(self):
445 self.nextRunTime = self.calculateNextRunTime()
446 self.delayedRun = reactor.callLater(self.nextRunTime - time.time(),
447 self.doPeriodicBuild)
449 def startService(self):
450 BaseUpstreamScheduler.startService(self)
451 self.setTimer()
453 def stopService(self):
454 BaseUpstreamScheduler.stopService(self)
455 self.delayedRun.cancel()
457 def isRunTime(self, timetuple):
458 def check(ourvalue, value):
459 if ourvalue == '*': return True
460 if isinstance(ourvalue, int): return value == ourvalue
461 return (value in ourvalue)
463 if not check(self.minute, timetuple[4]):
464 #print 'bad minute', timetuple[4], self.minute
465 return False
467 if not check(self.hour, timetuple[3]):
468 #print 'bad hour', timetuple[3], self.hour
469 return False
471 if not check(self.month, timetuple[1]):
472 #print 'bad month', timetuple[1], self.month
473 return False
475 if self.dayOfMonth != '*' and self.dayOfWeek != '*':
476 # They specified both day(s) of month AND day(s) of week.
477 # This means that we only have to match one of the two. If
478 # neither one matches, this time is not the right time.
479 if not (check(self.dayOfMonth, timetuple[2]) or
480 check(self.dayOfWeek, timetuple[6])):
481 #print 'bad day'
482 return False
483 else:
484 if not check(self.dayOfMonth, timetuple[2]):
485 #print 'bad day of month'
486 return False
488 if not check(self.dayOfWeek, timetuple[6]):
489 #print 'bad day of week'
490 return False
492 return True
494 def calculateNextRunTime(self):
495 return self.calculateNextRunTimeFrom(time.time())
497 def calculateNextRunTimeFrom(self, now):
498 dateTime = time.localtime(now)
500 # Remove seconds by advancing to at least the next minue
501 dateTime = self.addTime(dateTime, 60-dateTime[5])
503 # Now we just keep adding minutes until we find something that matches
505 # It not an efficient algorithm, but it'll *work* for now
506 yearLimit = dateTime[0]+2
507 while not self.isRunTime(dateTime):
508 dateTime = self.addTime(dateTime, 60)
509 #print 'Trying', time.asctime(dateTime)
510 assert dateTime[0] < yearLimit, 'Something is wrong with this code'
511 return time.mktime(dateTime)
513 def listBuilderNames(self):
514 return self.builderNames
516 def getPendingBuildTimes(self):
517 # TODO: figure out when self.timer is going to fire next and report
518 # that
519 if self.nextRunTime is None: return []
520 return [self.nextRunTime]
522 def doPeriodicBuild(self):
523 # Schedule the next run
524 self.setTimer()
526 # And trigger a build
527 bs = buildset.BuildSet(self.builderNames,
528 SourceStamp(branch=self.branch),
529 self.reason,
530 properties=self.properties)
531 self.submitBuildSet(bs)
533 def addChange(self, change):
534 pass
538 class TryBase(BaseScheduler):
539 def __init__(self, name, builderNames, properties={}):
540 BaseScheduler.__init__(self, name, properties)
541 self.builderNames = builderNames
543 def listBuilderNames(self):
544 return self.builderNames
546 def getPendingBuildTimes(self):
547 # we can't predict what the developers are going to do in the future
548 return []
550 def addChange(self, change):
551 # Try schedulers ignore Changes
552 pass
555 class BadJobfile(Exception):
556 pass
558 class JobFileScanner(basic.NetstringReceiver):
559 def __init__(self):
560 self.strings = []
561 self.transport = self # so transport.loseConnection works
562 self.error = False
564 def stringReceived(self, s):
565 self.strings.append(s)
567 def loseConnection(self):
568 self.error = True
570 class Try_Jobdir(TryBase):
571 compare_attrs = ( 'name', 'builderNames', 'jobdir', 'properties' )
573 def __init__(self, name, builderNames, jobdir, properties={}):
574 TryBase.__init__(self, name, builderNames, properties)
575 self.jobdir = jobdir
576 self.watcher = MaildirService()
577 self.watcher.setServiceParent(self)
579 def setServiceParent(self, parent):
580 self.watcher.setBasedir(os.path.join(parent.basedir, self.jobdir))
581 TryBase.setServiceParent(self, parent)
583 def parseJob(self, f):
584 # jobfiles are serialized build requests. Each is a list of
585 # serialized netstrings, in the following order:
586 # "1", the version number of this format
587 # buildsetID, arbitrary string, used to find the buildSet later
588 # branch name, "" for default-branch
589 # base revision, "" for HEAD
590 # patchlevel, usually "1"
591 # patch
592 # builderNames...
593 p = JobFileScanner()
594 p.dataReceived(f.read())
595 if p.error:
596 raise BadJobfile("unable to parse netstrings")
597 s = p.strings
598 ver = s.pop(0)
599 if ver != "1":
600 raise BadJobfile("unknown version '%s'" % ver)
601 buildsetID, branch, baserev, patchlevel, diff = s[:5]
602 builderNames = s[5:]
603 if branch == "":
604 branch = None
605 if baserev == "":
606 baserev = None
607 patchlevel = int(patchlevel)
608 patch = (patchlevel, diff)
609 ss = SourceStamp(branch, baserev, patch)
610 return builderNames, ss, buildsetID
612 def messageReceived(self, filename):
613 md = os.path.join(self.parent.basedir, self.jobdir)
614 if runtime.platformType == "posix":
615 # open the file before moving it, because I'm afraid that once
616 # it's in cur/, someone might delete it at any moment
617 path = os.path.join(md, "new", filename)
618 f = open(path, "r")
619 os.rename(os.path.join(md, "new", filename),
620 os.path.join(md, "cur", filename))
621 else:
622 # do this backwards under windows, because you can't move a file
623 # that somebody is holding open. This was causing a Permission
624 # Denied error on bear's win32-twisted1.3 buildslave.
625 os.rename(os.path.join(md, "new", filename),
626 os.path.join(md, "cur", filename))
627 path = os.path.join(md, "cur", filename)
628 f = open(path, "r")
630 try:
631 builderNames, ss, bsid = self.parseJob(f)
632 except BadJobfile:
633 log.msg("%s reports a bad jobfile in %s" % (self, filename))
634 log.err()
635 return
636 # compare builderNames against self.builderNames
637 # TODO: think about this some more.. why bother restricting it?
638 # perhaps self.builderNames should be used as the default list
639 # instead of being used as a restriction?
640 for b in builderNames:
641 if not b in self.builderNames:
642 log.msg("%s got jobfile %s with builder %s" % (self,
643 filename, b))
644 log.msg(" but that wasn't in our list: %s"
645 % (self.builderNames,))
646 return
648 reason = "'try' job"
649 bs = buildset.BuildSet(builderNames, ss, reason=reason,
650 bsid=bsid, properties=self.properties)
651 self.submitBuildSet(bs)
653 class Try_Userpass(TryBase):
654 compare_attrs = ( 'name', 'builderNames', 'port', 'userpass', 'properties' )
655 implements(portal.IRealm)
657 def __init__(self, name, builderNames, port, userpass, properties={}):
658 TryBase.__init__(self, name, builderNames, properties)
659 if type(port) is int:
660 port = "tcp:%d" % port
661 self.port = port
662 self.userpass = userpass
663 c = checkers.InMemoryUsernamePasswordDatabaseDontUse()
664 for user,passwd in self.userpass:
665 c.addUser(user, passwd)
667 p = portal.Portal(self)
668 p.registerChecker(c)
669 f = pb.PBServerFactory(p)
670 s = strports.service(port, f)
671 s.setServiceParent(self)
673 def getPort(self):
674 # utility method for tests: figure out which TCP port we just opened.
675 return self.services[0]._port.getHost().port
677 def requestAvatar(self, avatarID, mind, interface):
678 log.msg("%s got connection from user %s" % (self, avatarID))
679 assert interface == pb.IPerspective
680 p = Try_Userpass_Perspective(self, avatarID)
681 return (pb.IPerspective, p, lambda: None)
683 class Try_Userpass_Perspective(pbutil.NewCredPerspective):
684 def __init__(self, parent, username):
685 self.parent = parent
686 self.username = username
688 def perspective_try(self, branch, revision, patch, builderNames, properties={}):
689 log.msg("user %s requesting build on builders %s" % (self.username,
690 builderNames))
691 for b in builderNames:
692 if not b in self.parent.builderNames:
693 log.msg("%s got job with builder %s" % (self, b))
694 log.msg(" but that wasn't in our list: %s"
695 % (self.parent.builderNames,))
696 return
697 ss = SourceStamp(branch, revision, patch)
698 reason = "'try' job from user %s" % self.username
700 # roll the specified props in with our inherited props
701 combined_props = Properties()
702 combined_props.updateFromProperties(self.parent.properties)
703 combined_props.update(properties, "try build")
705 bs = buildset.BuildSet(builderNames,
707 reason=reason,
708 properties=combined_props)
710 self.parent.submitBuildSet(bs)
712 # return a remotely-usable BuildSetStatus object
713 from buildbot.status.client import makeRemote
714 return makeRemote(bs.status)
716 class Triggerable(BaseUpstreamScheduler):
717 """This scheduler doesn't do anything until it is triggered by a Trigger
718 step in a factory. In general, that step will not complete until all of
719 the builds that I fire have finished.
722 compare_attrs = ('name', 'builderNames', 'properties')
724 def __init__(self, name, builderNames, properties={}):
725 BaseUpstreamScheduler.__init__(self, name, properties)
726 self.builderNames = builderNames
728 def listBuilderNames(self):
729 return self.builderNames
731 def getPendingBuildTimes(self):
732 return []
734 def trigger(self, ss, set_props=None):
735 """Trigger this scheduler. Returns a deferred that will fire when the
736 buildset is finished.
739 # properties for this buildset are composed of our own properties,
740 # potentially overridden by anything from the triggering build
741 props = Properties()
742 props.updateFromProperties(self.properties)
743 if set_props: props.updateFromProperties(set_props)
745 bs = buildset.BuildSet(self.builderNames, ss, properties=props)
746 d = bs.waitUntilFinished()
747 self.submitBuildSet(bs)
748 return d