Trees | Indices | Help |
|
---|
|
1 # This file is part of Buildbot. Buildbot is free software: you can 2 # redistribute it and/or modify it under the terms of the GNU General Public 3 # License as published by the Free Software Foundation, version 2. 4 # 5 # This program is distributed in the hope that it will be useful, but WITHOUT 6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8 # details. 9 # 10 # You should have received a copy of the GNU General Public License along with 11 # this program; if not, write to the Free Software Foundation, Inc., 51 12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13 # 14 # Copyright Buildbot Team Members 15 16 17 import types 18 19 from zope.interface import implements 20 from twisted.python import log, components 21 from twisted.python.failure import Failure 22 from twisted.internet import reactor, defer, error 23 24 from buildbot import interfaces, locks 25 from buildbot.status.results import SUCCESS, WARNINGS, FAILURE, EXCEPTION, \ 26 RETRY, SKIPPED, worst_status 27 from buildbot.status.builder import Results 28 from buildbot.status.progress import BuildProgress 29 from buildbot.process import metrics, properties 30 3133 """I represent a single build by a single slave. Specialized Builders can 34 use subclasses of Build to hold status information unique to those build 35 processes. 36 37 I control B{how} the build proceeds. The actual build is broken up into a 38 series of steps, saved in the .buildSteps[] array as a list of 39 L{buildbot.process.step.BuildStep} objects. Each step is a single remote 40 command, possibly a shell command. 41 42 During the build, I put status information into my C{BuildStatus} 43 gatherer. 44 45 After the build, I go away. 46 47 I can be used by a factory by setting buildClass on 48 L{buildbot.process.factory.BuildFactory} 49 50 @ivar requests: the list of L{BuildRequest}s that triggered me 51 @ivar build_status: the L{buildbot.status.build.BuildStatus} that 52 collects our status 53 """ 54 55 implements(interfaces.IBuildControl) 56 57 workdir = "build" 58 build_status = None 59 reason = "changes" 60 finished = False 61 results = None 62 stopped = False 63 set_runtime_properties = True 64237 d.addCallback(_release_slave, self.slavebuilder.slave, build_status) 238 239 try: 240 self.setupBuild(expectations) # create .steps 241 except: 242 # the build hasn't started yet, so log the exception as a point 243 # event instead of flunking the build. 244 # TODO: associate this failure with the build instead. 245 # this involves doing 246 # self.build_status.buildStarted() from within the exception 247 # handler 248 log.msg("Build.setupBuild failed") 249 log.err(Failure()) 250 self.builder.builder_status.addPointEvent(["setupBuild", 251 "exception"]) 252 self.finished = True 253 self.results = FAILURE 254 self.deferred = None 255 d.callback(self) 256 return d 257 258 self.build_status.buildStarted(self) 259 self.acquireLocks().addCallback(self._startBuild_2) 260 return d 26166 self.requests = requests 67 self.locks = [] 68 69 # build a source stamp 70 self.source = requests[0].mergeWith(requests[1:]) 71 self.reason = requests[0].mergeReasons(requests[1:]) 72 73 self.progress = None 74 self.currentStep = None 75 self.slaveEnvironment = {} 76 77 self.terminate = False 78 79 self._acquiringLock = None8082 """ 83 Set the given builder as our builder. 84 85 @type builder: L{buildbot.process.builder.Builder} 86 """ 87 self.builder = builder88 91 9496 if repository is None: 97 return self.source 98 assert (repository in self.sources) 99 return self.sources[repository]100 103105 # return a list of all source files that were changed 106 files = [] 107 for c in self.allChanges(): 108 for f in c.files: 109 files.append(f) 110 return files111 114116 blamelist = [] 117 for c in self.allChanges(): 118 if c.who not in blamelist: 119 blamelist.append(c.who) 120 if self.source.patch_info: #Add patch author to blamelist 121 blamelist.append(self.source.patch_info[0]) 122 blamelist.sort() 123 return blamelist124126 changetext = "" 127 for c in self.allChanges(): 128 changetext += "-" * 60 + "\n\n" + c.asText() + "\n" 129 # consider sorting these by number 130 return changetext131133 """Set a list of 'step factories', which are tuples of (class, 134 kwargs), where 'class' is generally a subclass of step.BuildStep . 135 These are used to create the Steps themselves when the Build starts 136 (as opposed to when it is first created). By creating the steps 137 later, their __init__ method will have access to things like 138 build.allFiles() .""" 139 self.stepFactories = list(step_factories)140 141 useProgress = True 142 147149 props = interfaces.IProperties(self) 150 151 # give the properties a reference back to this build 152 props.build = self 153 154 # start with global properties from the configuration 155 buildmaster = self.builder.botmaster.parent 156 props.updateFromProperties(buildmaster.config.properties) 157 158 # from the SourceStamp, which has properties via Change 159 for change in self.source.changes: 160 props.updateFromProperties(change.properties) 161 162 # and finally, get any properties from requests (this is the path 163 # through which schedulers will send us properties) 164 for rq in self.requests: 165 props.updateFromProperties(rq.properties) 166 167 # now set some properties of our own, corresponding to the 168 # build itself 169 props.setProperty("buildnumber", self.build_status.number, "Build") 170 props.setProperty("branch", self.source.branch, "Build") 171 props.setProperty("revision", self.source.revision, "Build") 172 props.setProperty("repository", self.source.repository, "Build") 173 props.setProperty("project", self.source.project, "Build") 174 self.builder.setupProperties(props)175177 self.slavebuilder = slavebuilder 178 179 # navigate our way back to the L{buildbot.buildslave.BuildSlave} 180 # object that came from the config, and get its properties 181 buildslave_properties = slavebuilder.slave.properties 182 self.getProperties().updateFromProperties(buildslave_properties) 183 if slavebuilder.slave.slave_basedir: 184 self.setProperty("workdir", 185 slavebuilder.slave.path_module.join( 186 slavebuilder.slave.slave_basedir, 187 self.builder.config.slavebuilddir), 188 "slave") 189 190 self.slavename = slavebuilder.slave.slavename 191 self.build_status.setSlavename(self.slavename)192194 """This method sets up the build, then starts it by invoking the 195 first Step. It returns a Deferred which will fire when the build 196 finishes. This Deferred is guaranteed to never errback.""" 197 198 # we are taking responsibility for watching the connection to the 199 # remote. This responsibility was held by the Builder until our 200 # startBuild was called, and will not return to them until we fire 201 # the Deferred returned by this method. 202 203 log.msg("%s.startBuild" % self) 204 self.build_status = build_status 205 # now that we have a build_status, we can set properties 206 self.setupProperties() 207 self.setupSlaveBuilder(slavebuilder) 208 slavebuilder.slave.updateSlaveStatus(buildStarted=build_status) 209 210 # convert all locks into their real forms 211 lock_list = [] 212 for access in self.locks: 213 if not isinstance(access, locks.LockAccess): 214 # Buildbot 0.7.7 compability: user did not specify access 215 access = access.defaultAccess() 216 lock = self.builder.botmaster.getLockByID(access.lockid) 217 lock_list.append((lock, access)) 218 self.locks = lock_list 219 # then narrow SlaveLocks down to the right slave 220 self.locks = [(l.getLock(self.slavebuilder), la) 221 for l, la in self.locks] 222 self.remote = slavebuilder.remote 223 self.remote.notifyOnDisconnect(self.lostRemote) 224 225 metrics.MetricCountEvent.log('active_builds', 1) 226 227 d = self.deferred = defer.Deferred() 228 def _uncount_build(res): 229 metrics.MetricCountEvent.log('active_builds', -1) 230 return res231 d.addBoth(_uncount_build) 232 233 def _release_slave(res, slave, bs): 234 self.slavebuilder.buildFinished() 235 slave.updateSlaveStatus(buildFinished=bs) 236 return res263 self._acquiringLock = None 264 if not self.locks: 265 return defer.succeed(None) 266 if self.stopped: 267 return defer.succeed(None) 268 log.msg("acquireLocks(build %s, locks %s)" % (self, self.locks)) 269 for lock, access in self.locks: 270 if not lock.isAvailable(access): 271 log.msg("Build %s waiting for lock %s" % (self, lock)) 272 d = lock.waitUntilMaybeAvailable(self, access) 273 d.addCallback(self.acquireLocks) 274 self._acquiringLock = (lock, access, d) 275 return d 276 # all locks are available, claim them all 277 for lock, access in self.locks: 278 lock.claim(self, access) 279 return defer.succeed(None)280282 self.startNextStep()283285 # create the actual BuildSteps. If there are any name collisions, we 286 # add a count to the loser until it is unique. 287 self.steps = [] 288 self.stepStatuses = {} 289 stepnames = {} 290 sps = [] 291 292 for factory, args in self.stepFactories: 293 args = args.copy() 294 try: 295 step = factory(**args) 296 except: 297 log.msg("error while creating step, factory=%s, args=%s" 298 % (factory, args)) 299 raise 300 step.setBuild(self) 301 step.setBuildSlave(self.slavebuilder.slave) 302 if callable (self.workdir): 303 step.setDefaultWorkdir (self.workdir (self.source)) 304 else: 305 step.setDefaultWorkdir (self.workdir) 306 name = step.name 307 if stepnames.has_key(name): 308 count = stepnames[name] 309 count += 1 310 stepnames[name] = count 311 name = step.name + "_%d" % count 312 else: 313 stepnames[name] = 0 314 step.name = name 315 self.steps.append(step) 316 317 # tell the BuildStatus about the step. This will create a 318 # BuildStepStatus and bind it to the Step. 319 step_status = self.build_status.addStepWithName(name) 320 step.setStepStatus(step_status) 321 322 sp = None 323 if self.useProgress: 324 # XXX: maybe bail if step.progressMetrics is empty? or skip 325 # progress for that one step (i.e. "it is fast"), or have a 326 # separate "variable" flag that makes us bail on progress 327 # tracking 328 sp = step.setupProgress() 329 if sp: 330 sps.append(sp) 331 332 # Create a buildbot.status.progress.BuildProgress object. This is 333 # called once at startup to figure out how to build the long-term 334 # Expectations object, and again at the start of each build to get a 335 # fresh BuildProgress object to track progress for that individual 336 # build. TODO: revisit at-startup call 337 338 if self.useProgress: 339 self.progress = BuildProgress(sps) 340 if self.progress and expectations: 341 self.progress.setExpectationsFrom(expectations) 342 343 # we are now ready to set up our BuildStatus. 344 self.build_status.setSourceStamp(self.source) 345 self.build_status.setReason(self.reason) 346 self.build_status.setBlamelist(self.blamelist()) 347 self.build_status.setProgress(self.progress) 348 349 # gather owners from build requests 350 owners = [r.properties['owner'] for r in self.requests 351 if r.properties.has_key('owner')] 352 if owners: self.setProperty('owners', owners, self.reason) 353 354 self.results = [] # list of FAILURE, SUCCESS, WARNINGS, SKIPPED 355 self.result = SUCCESS # overall result, may downgrade after each step 356 self.text = [] # list of text string lists (text2)357359 """This method is called to obtain the next BuildStep for this build. 360 When it returns None (or raises a StopIteration exception), the build 361 is complete.""" 362 if not self.steps: 363 return None 364 if not self.remote: 365 return None 366 if self.terminate or self.stopped: 367 # Run any remaining alwaysRun steps, and skip over the others 368 while True: 369 s = self.steps.pop(0) 370 if s.alwaysRun: 371 return s 372 if not self.steps: 373 return None 374 else: 375 return self.steps.pop(0)376378 try: 379 s = self.getNextStep() 380 except StopIteration: 381 s = None 382 if not s: 383 return self.allStepsDone() 384 self.currentStep = s 385 d = defer.maybeDeferred(s.startStep, self.remote) 386 d.addCallback(self._stepDone, s) 387 d.addErrback(self.buildException)388390 self.currentStep = None 391 if self.finished: 392 return # build was interrupted, don't keep building 393 terminate = self.stepDone(results, step) # interpret/merge results 394 if terminate: 395 self.terminate = True 396 return self.startNextStep()397399 """This method is called when the BuildStep completes. It is passed a 400 status object from the BuildStep and is responsible for merging the 401 Step's results into those of the overall Build.""" 402 403 terminate = False 404 text = None 405 if type(result) == types.TupleType: 406 result, text = result 407 assert type(result) == type(SUCCESS) 408 log.msg(" step '%s' complete: %s" % (step.name, Results[result])) 409 self.results.append(result) 410 if text: 411 self.text.extend(text) 412 if not self.remote: 413 terminate = True 414 415 possible_overall_result = result 416 if result == FAILURE: 417 if not step.flunkOnFailure: 418 possible_overall_result = SUCCESS 419 if step.warnOnFailure: 420 possible_overall_result = WARNINGS 421 if step.flunkOnFailure: 422 possible_overall_result = FAILURE 423 if step.haltOnFailure: 424 terminate = True 425 elif result == WARNINGS: 426 if not step.warnOnWarnings: 427 possible_overall_result = SUCCESS 428 else: 429 possible_overall_result = WARNINGS 430 if step.flunkOnWarnings: 431 possible_overall_result = FAILURE 432 elif result in (EXCEPTION, RETRY): 433 terminate = True 434 435 # if we skipped this step, then don't adjust the build status 436 if result != SKIPPED: 437 self.result = worst_status(self.result, possible_overall_result) 438 439 return terminate440442 # the slave went away. There are several possible reasons for this, 443 # and they aren't necessarily fatal. For now, kill the build, but 444 # TODO: see if we can resume the build when it reconnects. 445 log.msg("%s.lostRemote" % self) 446 self.remote = None 447 if self.currentStep: 448 # this should cause the step to finish. 449 log.msg(" stopping currentStep", self.currentStep) 450 self.currentStep.interrupt(Failure(error.ConnectionLost())) 451 else: 452 self.result = RETRY 453 self.text = ["lost", "remote"] 454 self.stopped = True 455 if self._acquiringLock: 456 lock, access, d = self._acquiringLock 457 lock.stopWaitingUntilAvailable(self, access, d) 458 d.callback(None)459461 # the idea here is to let the user cancel a build because, e.g., 462 # they realized they committed a bug and they don't want to waste 463 # the time building something that they know will fail. Another 464 # reason might be to abandon a stuck build. We want to mark the 465 # build as failed quickly rather than waiting for the slave's 466 # timeout to kill it on its own. 467 468 log.msg(" %s: stopping build: %s" % (self, reason)) 469 if self.finished: 470 return 471 # TODO: include 'reason' in this point event 472 self.builder.builder_status.addPointEvent(['interrupt']) 473 self.stopped = True 474 if self.currentStep: 475 self.currentStep.interrupt(reason) 476 477 self.result = EXCEPTION 478 479 if self._acquiringLock: 480 lock, access, d = self._acquiringLock 481 lock.stopWaitingUntilAvailable(self, access, d) 482 d.callback(None)483485 if self.result == FAILURE: 486 text = ["failed"] 487 elif self.result == WARNINGS: 488 text = ["warnings"] 489 elif self.result == EXCEPTION: 490 text = ["exception"] 491 elif self.result == RETRY: 492 text = ["retry"] 493 else: 494 text = ["build", "successful"] 495 text.extend(self.text) 496 return self.buildFinished(text, self.result)497499 log.msg("%s.buildException" % self) 500 log.err(why) 501 # try to finish the build, but since we've already faced an exception, 502 # this may not work well. 503 try: 504 self.buildFinished(["build", "exception"], EXCEPTION) 505 except: 506 log.err(Failure(), 'while finishing a build with an exception')507509 """This method must be called when the last Step has completed. It 510 marks the Build as complete and returns the Builder to the 'idle' 511 state. 512 513 It takes two arguments which describe the overall build status: 514 text, results. 'results' is one of SUCCESS, WARNINGS, or FAILURE. 515 516 If 'results' is SUCCESS or WARNINGS, we will permit any dependant 517 builds to start. If it is 'FAILURE', those builds will be 518 abandoned.""" 519 520 self.finished = True 521 if self.remote: 522 self.remote.dontNotifyOnDisconnect(self.lostRemote) 523 self.remote = None 524 self.results = results 525 526 log.msg(" %s: build finished" % self) 527 self.build_status.setText(text) 528 self.build_status.setResults(results) 529 self.build_status.buildFinished() 530 if self.progress and results == SUCCESS: 531 # XXX: also test a 'timing consistent' flag? 532 log.msg(" setting expectations for next time") 533 self.builder.setExpectations(self.progress) 534 reactor.callLater(0, self.releaseLocks) 535 self.deferred.callback(self) 536 self.deferred = None537539 if self.locks: 540 log.msg("releaseLocks(%s): %s" % (self, self.locks)) 541 for lock, access in self.locks: 542 if lock.isOwner(self, access): 543 lock.release(self, access) 544 else: 545 # This should only happen if we've been interrupted 546 assert self.stopped547 548 # IBuildControl 549551 return self.build_status552 553 # stopBuild is defined earlier 554 555 components.registerAdapter( 556 lambda build : interfaces.IProperties(build.build_status), 557 Build, interfaces.IProperties) 558
Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Sun Mar 25 19:40:37 2012 | http://epydoc.sourceforge.net |