1 # -*- test-case-name: buildbot.test.test_dependencies -*-
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
):
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
30 implements(interfaces
.IScheduler
)
32 def __init__(self
, name
, properties
={}):
34 @param name: name for this scheduler
36 @param properties: properties to be propagated from this scheduler
37 @type properties: dict
39 service
.MultiService
.__init
__(self
)
41 self
.properties
= Properties()
42 self
.properties
.update(properties
, "Scheduler")
43 self
.properties
.setProperty("scheduler", name
, "Scheduler")
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
):
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
):
75 if bss
.getResults() == builder
.SUCCESS
:
76 ss
= bss
.getSourceStamp()
77 for w
in self
.successWatchers
:
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.
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
={}):
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
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
132 assert callable(fileIsImportant
)
133 self
.fileIsImportant
= fileIsImportant
135 self
.importantChanges
= []
136 self
.unimportantChanges
= []
137 self
.nextBuildTime
= None
140 def listBuilderNames(self
):
141 return self
.builderNames
143 def getPendingBuildTimes(self
):
144 if self
.nextBuildTime
is not None:
145 return [self
.nextBuildTime
]
148 def addChange(self
, change
):
149 if change
.branch
!= self
.branch
:
150 log
.msg("%s ignoring off-branch %s" % (self
, change
))
152 if not self
.fileIsImportant
:
153 self
.addImportantChange(change
)
154 elif self
.fileIsImportant(change
):
155 self
.addImportantChange(change
)
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
))))
178 self
.timer
= reactor
.callLater(when
- now
, self
.fireTimer
)
186 # clear out our state
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
):
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
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
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.
261 assert callable(fileIsImportant
)
262 self
.fileIsImportant
= fileIsImportant
263 self
.schedulers
= {} # one per branch
266 return "<AnyBranchScheduler '%s'>" % self
.name
268 def listBuilderNames(self
):
269 return self
.builderNames
271 def getPendingBuildTimes(self
):
273 for s
in self
.schedulers
.values():
274 if s
.nextBuildTime
is not None:
275 bts
.append(s
.nextBuildTime
)
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
))
283 s
= self
.schedulers
.get(branch
)
286 name
= self
.name
+ "." + branch
288 name
= self
.name
+ ".<default>"
289 s
= self
.schedulerFactory(name
, branch
,
290 self
.treeStableTimer
,
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
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
)
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
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
353 self
.reason
= ("The Periodic scheduler named '%s' triggered this build"
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
367 def doPeriodicBuild(self
):
368 bs
= buildset
.BuildSet(self
.builderNames
,
369 SourceStamp(branch
=self
.branch
),
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
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
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
428 self
.dayOfMonth
= dayOfMonth
430 self
.dayOfWeek
= dayOfWeek
432 self
.delayedRun
= None
433 self
.nextRunTime
= None
434 self
.reason
= ("The Nightly scheduler named '%s' triggered this build"
437 def addTime(self
, timetuple
, secs
):
438 return time
.localtime(time
.mktime(timetuple
)+secs
)
439 def findFirstValueAtLeast(self
, values
, value
, default
=None):
441 if v
>= value
: return v
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
)
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
467 if not check(self
.hour
, timetuple
[3]):
468 #print 'bad hour', timetuple[3], self.hour
471 if not check(self
.month
, timetuple
[1]):
472 #print 'bad month', timetuple[1], self.month
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])):
484 if not check(self
.dayOfMonth
, timetuple
[2]):
485 #print 'bad day of month'
488 if not check(self
.dayOfWeek
, timetuple
[6]):
489 #print 'bad day of week'
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
519 if self
.nextRunTime
is None: return []
520 return [self
.nextRunTime
]
522 def doPeriodicBuild(self
):
523 # Schedule the next run
526 # And trigger a build
527 bs
= buildset
.BuildSet(self
.builderNames
,
528 SourceStamp(branch
=self
.branch
),
530 properties
=self
.properties
)
531 self
.submitBuildSet(bs
)
533 def addChange(self
, change
):
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
550 def addChange(self
, change
):
551 # Try schedulers ignore Changes
555 class BadJobfile(Exception):
558 class JobFileScanner(basic
.NetstringReceiver
):
561 self
.transport
= self
# so transport.loseConnection works
564 def stringReceived(self
, s
):
565 self
.strings
.append(s
)
567 def loseConnection(self
):
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
)
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"
594 p
.dataReceived(f
.read())
596 raise BadJobfile("unable to parse netstrings")
600 raise BadJobfile("unknown version '%s'" % ver
)
601 buildsetID
, branch
, baserev
, patchlevel
, diff
= s
[:5]
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
)
619 os
.rename(os
.path
.join(md
, "new", filename
),
620 os
.path
.join(md
, "cur", filename
))
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
)
631 builderNames
, ss
, bsid
= self
.parseJob(f
)
633 log
.msg("%s reports a bad jobfile in %s" % (self
, filename
))
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
,
644 log
.msg(" but that wasn't in our list: %s"
645 % (self
.builderNames
,))
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
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
)
669 f
= pb
.PBServerFactory(p
)
670 s
= strports
.service(port
, f
)
671 s
.setServiceParent(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
):
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
,
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
,))
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
,
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
):
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
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
)