1 # jhbuild - a build script for GNOME 1.x and 2.x
2 # Copyright (C) 2001-2006 James Henstridge
3 # Copyright (C) 2008 Frederic Peters
5 # bot.py: buildbot control commands
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 # Some methods are derived from Buildbot own methods (when it was not possible
23 # to override just some parts of them). Buildbot is also licensed under the
24 # GNU General Public License.
30 from optparse
import make_option
37 import elementtree
.ElementTree
as ET
39 import xml
.etree
.ElementTree
as ET
41 import jhbuild
.moduleset
42 import jhbuild
.frontends
43 from jhbuild
.commands
import Command
, register_command
44 from jhbuild
.commands
.base
import cmd_build
45 from jhbuild
.config
import addpath
46 from jhbuild
.errors
import UsageError
, FatalError
, CommandError
53 class cmd_bot(Command
):
54 doc
= N_('Control buildbot')
57 usage_args
= N_('[ options ... ]')
60 Command
.__init
__(self
, [
61 make_option('--setup',
62 action
='store_true', dest
='setup', default
=False,
63 help=_('setup a buildbot environment')),
64 make_option('--start',
65 action
='store_true', dest
='start', default
=False,
66 help=_('start a buildbot slave server')),
68 action
='store_true', dest
='stop', default
=False,
69 help=_('stop a buildbot slave server')),
70 make_option('--start-server',
71 action
='store_true', dest
='start_server', default
=False,
72 help=_('start a buildbot master server')),
73 make_option('--reload-server-config',
74 action
='store_true', dest
='reload_server_config', default
=False,
75 help=_('reload a buildbot master server configuration')),
76 make_option('--stop-server',
77 action
='store_true', dest
='stop_server', default
=False,
78 help=_('stop a buildbot master server')),
79 make_option('--daemon',
80 action
='store_true', dest
='daemon', default
=False,
81 help=_('start as daemon')),
82 make_option('--pidfile', metavar
='PIDFILE',
83 action
='store', dest
='pidfile', default
=None,
84 help=_('pid file location')),
85 make_option('--logfile', metavar
='LOGFILE',
86 action
='store', dest
='logfile', default
=None,
87 help=_('log file location')),
88 make_option('--slaves-dir', metavar
='SLAVESDIR',
89 action
='store', dest
='slaves_dir', default
=None,
90 help=_('directory with slave files (only with --start-server)')),
91 make_option('--buildbot-dir', metavar
='BUILDBOTDIR',
92 action
='store', dest
='buildbot_dir', default
=None,
93 help=_('directory with buildbot work files (only with --start-server)')),
94 make_option('--mastercfg', metavar
='CFGFILE',
95 action
='store', dest
='mastercfgfile', default
=None,
96 help=_('master cfg file location (only with --start-server)')),
98 action
='store_true', dest
='step', default
=False,
99 help=_('exec a buildbot step (internal use only)')),
102 def run(self
, config
, options
, args
, help=None):
104 return self
.setup(config
)
109 pythonversion
= 'python' + str(sys
.version_info
[0]) + '.' + str(sys
.version_info
[1])
110 pythonpath
= os
.path
.join(config
.prefix
, 'lib', pythonversion
, 'site-packages')
111 site
.addsitedir(pythonpath
)
113 pythonpath
= os
.path
.join(config
.prefix
, 'lib64', pythonversion
, 'site-packages')
114 site
.addsitedir(pythonpath
)
118 raise FatalError(_('buildbot and twisted not found, run jhbuild bot --setup'))
120 # make jhbuild config file accessible to buildbot files
121 # (master.cfg , steps.py, etc.)
122 __builtin__
.__dict
__['jhbuild_config'] = config
127 slaves_dir
= config
.jhbuildbot_slaves_dir
128 mastercfgfile
= config
.jhbuildbot_mastercfg
129 buildbot_dir
= config
.jhbuildbot_dir
134 pidfile
= options
.pidfile
136 logfile
= options
.logfile
137 if options
.slaves_dir
:
138 slaves_dir
= options
.slaves_dir
139 if options
.mastercfgfile
:
140 mastercfgfile
= options
.mastercfgfile
141 if options
.buildbot_dir
:
142 buildbot_dir
= os
.path
.abspath(options
.buildbot_dir
)
145 return self
.start(config
, daemonize
, pidfile
, logfile
)
148 os
.environ
['JHBUILDRC'] = config
.filename
149 os
.environ
['LC_ALL'] = 'C'
150 os
.environ
['LANGUAGE'] = 'C'
151 os
.environ
['LANG'] = 'C'
152 __builtin__
.__dict
__['_'] = lambda x
: x
153 config
.interact
= False
154 config
.nonetwork
= True
155 os
.environ
['TERM'] = 'dumb'
156 if args
[0] in ('update', 'build', 'check', 'clean'):
157 module_set
= jhbuild
.moduleset
.load(config
)
158 buildscript
= jhbuild
.frontends
.get_buildscript(config
,
159 [module_set
.get_module(x
, ignore_case
=True) for x
in args
[1:]])
161 if args
[0] == 'update':
162 config
.nonetwork
= False
163 phases
= ['checkout']
164 elif args
[0] == 'build':
165 config
.alwaysautogen
= True
166 # make check will be run in another step
167 config
.makecheck
= False
168 config
.build_targets
= ['install']
169 elif args
[0] == 'check':
170 config
.makecheck
= True
171 config
.build_targets
= ['check']
173 elif args
[0] == 'clean':
175 rc
= buildscript
.build(phases
=phases
)
178 rc
= jhbuild
.commands
.run(command
, config
, args
[1:], help=None)
181 if options
.start_server
:
182 return self
.start_server(config
, daemonize
, pidfile
, logfile
,
183 slaves_dir
, mastercfgfile
, buildbot_dir
)
185 if options
.stop
or options
.stop_server
:
186 return self
.stop(config
, pidfile
)
188 if options
.reload_server_config
:
189 return self
.reload_server_config(config
, pidfile
)
191 def setup(self
, config
):
192 module_set
= jhbuild
.moduleset
.load(config
, 'buildbot')
193 module_list
= module_set
.get_module_list('all', config
.skip
)
194 build
= jhbuild
.frontends
.get_buildscript(config
, module_list
)
197 def start(self
, config
, daemonize
, pidfile
, logfile
):
198 from twisted
.application
import service
199 application
= service
.Application('buildslave')
200 if ':' in config
.jhbuildbot_master
:
201 master_host
, master_port
= config
.jhbuildbot_master
.split(':')
202 master_port
= int(master_port
)
204 master_host
, master_port
= config
.jhbuildbot_master
, 9070
206 slave_name
= config
.jhbuildbot_slavename
or socket
.gethostname()
211 basedir
= os
.path
.join(config
.checkoutroot
, 'jhbuildbot')
212 if not os
.path
.exists(os
.path
.join(basedir
, 'builddir')):
213 os
.makedirs(os
.path
.join(basedir
, 'builddir'))
216 from buildbot
.slave
.bot
import BuildSlave
217 s
= BuildSlave(master_host
, master_port
,
218 slave_name
, config
.jhbuildbot_password
, basedir
,
219 keepalive
, usepty
, umask
=umask
)
220 s
.setServiceParent(application
)
223 from twisted
.scripts
._twistd
_unix
import UnixApplicationRunner
, ServerOptions
227 opts
.append('--nodaemon')
229 opts
.extend(['--pidfile', pidfile
])
231 opts
.extend(['--logfile', logfile
])
232 options
= ServerOptions()
233 options
.parseOptions(opts
)
235 class JhBuildbotApplicationRunner(UnixApplicationRunner
):
238 def createOrGetApplication(self
):
239 return self
.application
241 JhBuildbotApplicationRunner
.application
= application
242 JhBuildbotApplicationRunner(options
).run()
244 def start_server(self
, config
, daemonize
, pidfile
, logfile
, slaves_dir
,
245 mastercfgfile
, buildbot_dir
):
247 from twisted
.scripts
._twistd
_unix
import UnixApplicationRunner
, ServerOptions
251 opts
.append('--nodaemon')
253 opts
.extend(['--pidfile', pidfile
])
255 opts
.extend(['--logfile', logfile
])
256 options
= ServerOptions()
257 options
.parseOptions(opts
)
259 class JhBuildbotApplicationRunner(UnixApplicationRunner
):
262 def createOrGetApplication(self
):
263 return self
.application
265 from twisted
.application
import service
, strports
266 from buildbot
.master
import BuildMaster
267 application
= service
.Application('buildmaster')
268 from buildbot
.buildslave
import BuildSlave
270 from twisted
.python
import log
271 from twisted
.internet
import defer
272 from buildbot
import interfaces
273 from buildbot
.process
.properties
import Properties
275 class JhBuildSlave(BuildSlave
):
287 run_coverage_report
= False
288 run_clean_afterwards
= False
290 def load_extra_configuration(self
, slaves_dir
):
291 from twisted
.python
import log
292 slave_xml_file
= os
.path
.join(slaves_dir
, self
.slavename
+ '.xml')
293 if not os
.path
.exists(slave_xml_file
):
294 log
.msg(_('No description for slave %s.') % self
.slavename
)
297 cfg
= ET
.parse(slave_xml_file
)
298 except: # parse error
299 log
.msg(_('Failed to parse slave config for %s.') % self
.slavename
)
302 for attribute
in ('config/max_builds', 'config/missing_timeout',
303 'config/run_checks', 'config/run_coverage_report',
304 'config/run_clean_afterwards',
306 'nightly_scheduler/minute',
307 'nightly_scheduler/hour',
308 'nightly_scheduler/dayOfMonth',
309 'nightly_scheduler/month',
310 'nightly_scheduler/dayOfWeek',
311 'info/contact_name', 'info/contact_email',
312 'info/url', 'info/distribution', 'info/architecture',
314 attr_name
= attribute
.split('/')[-1]
316 value
= cfg
.find(attribute
).text
317 except AttributeError:
320 if attr_name
in ('max_builds', 'missing_timeout'): # int value
326 if attr_name
in ('run_checks', 'run_coverage_report', 'run_clean_afterwards'):
327 value
= (value
== 'yes')
329 if attr_name
in ('minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'):
335 setattr(self
, attr_name
, value
)
337 if self
.scheduler
== 'nightly':
338 self
.nightly_kwargs
= {}
339 for attr_name
in ('minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'):
340 if hasattr(self
, attr_name
):
341 self
.nightly_kwargs
[attr_name
] = getattr(self
, attr_name
)
343 class JhBuildMaster(BuildMaster
):
344 jhbuild_config
= config
345 def loadConfig(self
, f
):
346 # modified from parent method to get slaves, projects, change
347 # sources, schedulers, builders and web status ouf of
348 # master.cfg [it would have been cleaner if jhbuild didn't
349 # have to copy all that code.]
350 localDict
= {'basedir': os
.path
.expanduser(self
.basedir
)}
354 log
.msg("error while parsing config file")
357 jhbuild_config
.load()
360 config
= localDict
['BuildmasterConfig']
362 log
.err("missing config dictionary")
363 log
.err("config file must define BuildmasterConfig")
366 known_keys
= ("bots", "slaves",
367 "sources", "change_source",
368 "schedulers", "builders",
369 "slavePortnum", "debugPassword", "manhole",
370 "status", "projectName", "projectURL", "buildbotURL",
373 for k
in config
.keys():
374 if k
not in known_keys
:
375 log
.msg("unknown key '%s' defined in config dictionary" % k
)
377 # the 'slaves' list is read from the 'slaves.csv' file in the
378 # current directory (unless instructed different from command line)
379 # it is a CSV file structured like this:
381 config
['slaves'] = []
382 slaves_csv_file
= os
.path
.join(slaves_dir
, 'slaves.csv')
383 if os
.path
.exists(slaves_csv_file
):
384 for x
in csv
.reader(file(slaves_csv_file
)):
385 if not x
or x
[0].startswith('#'):
388 build_slave
= JhBuildSlave(x
[0], x
[1])
389 build_slave
.load_extra_configuration(slaves_dir
)
390 config
['slaves'].append(build_slave
)
392 if len(config
['slaves']) == 0:
393 log
.msg('you must fill slaves.csv with slaves')
395 module_set
= jhbuild
.moduleset
.load(self
.jhbuild_config
)
396 module_list
= module_set
.get_module_list(
397 self
.jhbuild_config
.modules
,
398 self
.jhbuild_config
.skip
,
399 include_optional_modules
=True)
400 config
['projects'] = [x
.name
for x
in module_list \
401 if not x
.name
.startswith('meta-')]
403 if self
.jhbuild_config
.jhbuildbot_svn_commits_box
:
404 # trigger builds from mails to svn-commit-list
405 # (note Maildir must be correct, or everything will fail)
406 from jhbuild
.buildbot
.changes
import GnomeMaildirSource
407 config
['change_source'] = GnomeMaildirSource(
408 self
.jhbuild_config
.jhbuildbot_svn_commits_box
,
411 # support injection (use 'buildbot sendchange')
412 from buildbot
.changes
.pb
import PBChangeSource
413 config
['change_source'] = PBChangeSource()
416 from jhbuild
.buildbot
.scheduler
import SerialScheduler
, NightlySerialScheduler
, OnCommitScheduler
417 config
['schedulers'] = []
418 for slave
in config
['slaves']:
420 for project
in config
['projects']:
421 buildername
= str('%s-%s' % (project
, slave
.slavename
))
422 scheduler_kwargs
= {}
423 if slave
.scheduler
== 'nightly':
424 scheduler_class
= NightlySerialScheduler
425 scheduler_kwargs
= slave
.nightly_kwargs
427 scheduler_class
= SerialScheduler
428 s
= scheduler_class(buildername
, project
, upstream
=s
,
429 builderNames
=[buildername
],
431 config
['schedulers'].append(s
)
432 if self
.jhbuild_config
.jhbuildbot_svn_commits_box
:
433 # schedulers that will launch job when receiving
434 # change notifications
435 s2
= OnCommitScheduler('oc-' + buildername
,
436 project
, builderNames
=[buildername
])
437 config
['schedulers'].append(s2
)
440 from jhbuild
.buildbot
.factory
import JHBuildFactory
441 config
['builders'] = []
442 for project
in config
['projects']:
443 for slave
in config
['slaves']:
444 f
= JHBuildFactory(project
, slave
)
445 config
['builders'].append({
446 'name' : "%s-%s" % (project
, slave
.slavename
),
447 'slavename' : slave
.slavename
,
448 'builddir' : 'builddir/%s.%s' % (project
, slave
.slavename
),
454 if not config
.has_key('status'):
455 # let it be possible to define additional status in
457 config
['status'] = []
459 from jhbuild
.buildbot
.status
.web
import JHBuildWebStatus
460 config
['status'].append(
462 self
.jhbuild_config
.moduleset
,
464 [x
.slavename
for x
in config
['slaves']],
465 http_port
=8080, allowForce
=True)
468 # remaining of the method is a straight copy from buildbot
472 schedulers
= config
['schedulers']
473 builders
= config
['builders']
475 if k
['name'].startswith("_"):
476 errmsg
= ("builder names must not start with an "
477 "underscore: " + k
['name'])
479 raise ValueError(errmsg
)
481 slavePortnum
= config
['slavePortnum']
482 #slaves = config['slaves']
483 #change_source = config['change_source']
486 debugPassword
= config
.get('debugPassword')
487 manhole
= config
.get('manhole')
488 status
= config
.get('status', [])
489 projectName
= config
.get('projectName')
490 projectURL
= config
.get('projectURL')
491 buildbotURL
= config
.get('buildbotURL')
492 properties
= config
.get('properties', {})
495 log
.msg("config dictionary is missing a required parameter")
496 log
.msg("leaving old configuration in place")
499 #if "bots" in config:
500 # raise KeyError("c['bots'] is no longer accepted")
502 slaves
= config
.get('slaves', [])
504 m
= ("c['bots'] is deprecated as of 0.7.6 and will be "
505 "removed by 0.8.0 . Please use c['slaves'] instead.")
507 warnings
.warn(m
, DeprecationWarning)
508 for name
, passwd
in config
['bots']:
509 slaves
.append(JhBuildSlave(name
, passwd
))
511 if "bots" not in config
and "slaves" not in config
:
512 log
.msg("config dictionary must have either 'bots' or 'slaves'")
513 log
.msg("leaving old configuration in place")
514 raise KeyError("must have either 'bots' or 'slaves'")
516 #if "sources" in config:
517 # raise KeyError("c['sources'] is no longer accepted")
519 change_source
= config
.get('change_source', [])
520 if isinstance(change_source
, (list, tuple)):
521 change_sources
= change_source
523 change_sources
= [change_source
]
524 if "sources" in config
:
525 m
= ("c['sources'] is deprecated as of 0.7.6 and will be "
526 "removed by 0.8.0 . Please use c['change_source'] instead.")
528 warnings
.warn(m
, DeprecationWarning)
529 for s
in config
['sources']:
530 change_sources
.append(s
)
532 # do some validation first
534 assert isinstance(s
, JhBuildSlave
)
535 if s
.slavename
in ("debug", "change", "status"):
536 raise KeyError, "reserved name '%s' used for a bot" % s
.slavename
537 if config
.has_key('interlocks'):
538 raise KeyError("c['interlocks'] is no longer accepted")
540 assert isinstance(change_sources
, (list, tuple))
541 for s
in change_sources
:
542 assert interfaces
.IChangeSource(s
, None)
543 # this assertion catches c['schedulers'] = Scheduler(), since
544 # Schedulers are service.MultiServices and thus iterable.
545 errmsg
= "c['schedulers'] must be a list of Scheduler instances"
546 assert isinstance(schedulers
, (list, tuple)), errmsg
548 assert interfaces
.IScheduler(s
, None), errmsg
549 assert isinstance(status
, (list, tuple))
551 assert interfaces
.IStatusReceiver(s
, None)
553 slavenames
= [s
.slavename
for s
in slaves
]
558 raise ValueError("builder %s must be defined with a dict, "
559 "not a tuple" % b
[0])
560 if b
.has_key('slavename') and b
['slavename'] not in slavenames
:
561 raise ValueError("builder %s uses undefined slave %s" \
562 % (b
['name'], b
['slavename']))
563 for n
in b
.get('slavenames', []):
564 if n
not in slavenames
:
565 raise ValueError("builder %s uses undefined slave %s" \
567 if b
['name'] in buildernames
:
568 raise ValueError("duplicate builder name %s"
570 buildernames
.append(b
['name'])
571 if b
['builddir'] in dirnames
:
572 raise ValueError("builder %s reuses builddir %s"
573 % (b
['name'], b
['builddir']))
574 dirnames
.append(b
['builddir'])
576 unscheduled_buildernames
= buildernames
[:]
579 for b
in s
.listBuilderNames():
580 assert b
in buildernames
, \
581 "%s uses unknown builder %s" % (s
, b
)
582 if b
in unscheduled_buildernames
:
583 unscheduled_buildernames
.remove(b
)
585 if s
.name
in schedulernames
:
586 # TODO: schedulers share a namespace with other Service
587 # children of the BuildMaster node, like status plugins, the
588 # Manhole, the ChangeMaster, and the BotMaster (although most
589 # of these don't have names)
590 msg
= ("Schedulers must have unique names, but "
591 "'%s' was a duplicate" % (s
.name
,))
592 raise ValueError(msg
)
593 schedulernames
.append(s
.name
)
595 if unscheduled_buildernames
:
596 log
.msg("Warning: some Builders have no Schedulers to drive them:"
597 " %s" % (unscheduled_buildernames
,))
599 # assert that all locks used by the Builds and their Steps are
603 for l
in b
.get('locks', []):
604 if locks
.has_key(l
.name
):
605 if locks
[l
.name
] is not l
:
606 raise ValueError("Two different locks (%s and %s) "
608 % (l
, locks
[l
.name
], l
.name
))
611 # TODO: this will break with any BuildFactory that doesn't use a
612 # .steps list, but I think the verification step is more
614 for s
in b
['factory'].steps
:
615 for l
in s
[1].get('locks', []):
616 if locks
.has_key(l
.name
):
617 if locks
[l
.name
] is not l
:
618 raise ValueError("Two different locks (%s and %s)"
620 % (l
, locks
[l
.name
], l
.name
))
624 if not isinstance(properties
, dict):
625 raise ValueError("c['properties'] must be a dictionary")
627 # slavePortnum supposed to be a strports specification
628 if type(slavePortnum
) is int:
629 slavePortnum
= "tcp:%d" % slavePortnum
631 # now we're committed to implementing the new configuration, so do
633 # TODO: actually, this is spread across a couple of Deferreds, so it
634 # really isn't atomic.
636 d
= defer
.succeed(None)
638 self
.projectName
= projectName
639 self
.projectURL
= projectURL
640 self
.buildbotURL
= buildbotURL
642 self
.properties
= Properties()
643 self
.properties
.update(properties
, self
.configFileName
)
645 # self.slaves: Disconnect any that were attached and removed from the
646 # list. Update self.checker with the new list of passwords, including
647 # debug/change/status.
648 d
.addCallback(lambda res
: self
.loadConfig_Slaves(slaves
))
652 self
.checker
.addUser("debug", debugPassword
)
653 self
.debugPassword
= debugPassword
656 if manhole
!= self
.manhole
:
659 # disownServiceParent may return a Deferred
660 d
.addCallback(lambda res
: self
.manhole
.disownServiceParent())
664 d
.addCallback(_remove
)
667 self
.manhole
= manhole
668 manhole
.setServiceParent(self
)
671 # add/remove self.botmaster.builders to match builders. The
672 # botmaster will handle startup/shutdown issues.
673 d
.addCallback(lambda res
: self
.loadConfig_Builders(builders
))
675 d
.addCallback(lambda res
: self
.loadConfig_status(status
))
677 # Schedulers are added after Builders in case they start right away
678 d
.addCallback(lambda res
: self
.loadConfig_Schedulers(schedulers
))
679 # and Sources go after Schedulers for the same reason
680 d
.addCallback(lambda res
: self
.loadConfig_Sources(change_sources
))
683 if self
.slavePortnum
!= slavePortnum
:
685 def closeSlavePort(res
):
686 d1
= self
.slavePort
.disownServiceParent()
687 self
.slavePort
= None
689 d
.addCallback(closeSlavePort
)
690 if slavePortnum
is not None:
691 def openSlavePort(res
):
692 self
.slavePort
= strports
.service(slavePortnum
,
694 self
.slavePort
.setServiceParent(self
)
695 d
.addCallback(openSlavePort
)
696 log
.msg("BuildMaster listening on port %s" % slavePortnum
)
697 self
.slavePortnum
= slavePortnum
699 log
.msg("configuration update started")
701 self
.readConfig
= True
702 log
.msg("configuration update complete")
704 d
.addCallback(lambda res
: self
.botmaster
.maybeStartAllBuilds())
708 basedir
= buildbot_dir
711 basedir
= os
.path
.join(PKGDATADIR
, 'buildbot')
713 basedir
= os
.path
.join(SRCDIR
, 'buildbot')
715 if not os
.path
.exists(os
.path
.join(basedir
, 'builddir')):
716 os
.makedirs(os
.path
.join(basedir
, 'builddir'))
717 master_cfg_path
= mastercfgfile
719 JhBuildMaster(basedir
, master_cfg_path
).setServiceParent(application
)
721 JhBuildbotApplicationRunner
.application
= application
722 JhBuildbotApplicationRunner(options
).run()
724 def stop(self
, config
, pidfile
):
726 pid
= int(file(pidfile
).read())
728 raise FatalError(_('failed to get buildbot PID'))
730 os
.kill(pid
, signal
.SIGTERM
)
732 def reload_server_config(self
, config
, pidfile
):
734 pid
= int(file(pidfile
).read())
736 raise FatalError(_('failed to get buildbot PID'))
738 os
.kill(pid
, signal
.SIGHUP
)
741 register_command(cmd_bot
)