]> git.sur5r.net Git - i3/i3.github.io/blob - _docs/buildbot
add docs/buildbot describing the i3 buildbot setup
[i3/i3.github.io] / _docs / buildbot
1 The i3 buildbot setup
2 =====================
3 Michael Stapelberg <michael@i3wm.org>
4 August 2012
5
6 This document explains the http://www.buildbot.net/[buildbot] setup we use to
7 provide up-to-date documentation and debian packages at http://build.i3wm.org/.
8 We publish these information so that our setup is well-documented (thus
9 decreasing future maintenance effort) and because it might be interesting for
10 other projects.
11
12 == Introduction
13
14 What we are doing in i3 is called Continuous Integration (see
15 http://en.wikipedia.org/wiki/Continuous_integration): we publish the changes we
16 make on our local machines as often as possible. In order to maintain a
17 continuously high quality, each time any developer pushes changes to the
18 official git repository, a number of quality assurance tools start running
19 automatically:
20
21 1. Latest documentation is generated and provided at
22    http://build.i3wm.org/docs/. This makes it easy to link to documentation for
23    features which are only in the current git version, not in the released
24    version.
25 2. The source code is compiled and it is automatically posted to the IRC
26    channel whether there were any compiler warnings. While developers should
27    notice compiler warnings, this mechanism creates a bit of public pressure
28    ("Oh, Michael introduced warnings with this commit!"). More importantly,
29    since this mechanism builds a dist tarball and then compiles that tarball,
30    any changes to the source which would result in an uncompilable dist tarball
31    are immediately obvious. Therefore, we could cut a release from the current
32    git version at any point in time.
33 3. The clang static analyzer runs and the resulting report is provided at
34    http://build.i3wm.org/clang-analyze/. While every developer needs to compile
35    his code before committing, he doesn’t necessarily use clang (so we catch
36    build failures when using clang) and he also probably doesn’t run a static
37    analyzer as part of his normal workflow. By just being available without any
38    friction, this mechanism encourages developers to look at the report and fix
39    problems.
40 4. Debian (and Ubuntu) packages are built. This not only ensures that we don’t
41    change anything in the source code which would lead to an FTBFS (Fails To
42    Build From Source) when building a Debian package, it also goes a long way
43    to encourage end users to test i3. To remove the need and resource
44    requirements for them to compile their own version of i3 regularly, we
45    provide packages that integrate conveniently with a normal Debian system
46    (e.g. that are automatically upgraded).
47
48 == Why buildbot?
49
50 Previously, I was unsatisfied with the current state of FOSS CI tools like
51 Jenkins, Tinderbox and others. They either seemed bloated, hard to use,
52 outdated or unappealing for some other reason.
53
54 Then I discovered buildbot and was impressed by its flexibility. It let me
55 implement everything I wanted from a CI tool and (in my opinion) it is
56 light-weight, easy to deploy and well maintained.
57
58 The only downside of buildbot is its configuration and documentation: You need
59 to spend quite a bit of time (I needed multiple days) until it works the way
60 you want it to and oftentimes, the documentation is far too sparse. This is one
61 of the reasons why I’m publishing the i3 setup.
62
63 == Configuration
64
65 See the next section for a complete, copy & pasteable configuration file. This
66 section covers the most important aspects without covering every line.
67
68 This document assumes you are running buildbot 0.8.6p1.
69
70 === Change sources
71
72 Since i3 uses a central git repository, we use the official buildbot
73 https://github.com/buildbot/buildbot/blob/master/master/contrib/git_buildbot.py[git
74 post-receive hook] that sends the change information to the buildbot master.
75
76 === Schedulers
77
78 There are two things (called "builders" in buildbot-language) which happen
79 whenever a new change in the +next+ branch of i3 occurs:
80
81 1. The "docs" builder builds and uploads the latest documentation. This happens
82    directly from the git repository with a custom asciidoc configuration which
83    indicates that these docs refer to the git version. Therefore, this builder
84    does not benefit from having a dist tarball available (contrary to the other
85    builders).
86
87 2. The "dist" builder prepares a dist tarball and then triggers the remaining
88    builders. This ensures that building the dist tarball (an operation which
89    takes about one minute due to documentation generation) only happens once.
90
91 Here is the relevant configuration part:
92
93 *Schedulers*:
94 ---------------------------------------------
95 c['schedulers'] = []
96
97 c['schedulers'].append(SingleBranchScheduler(
98     name = 'dist',
99     branch = 'next',
100     treeStableTimer = 10,
101     builderNames = [ 'dist', 'docs' ],
102 ))
103
104 c['schedulers'].append(Triggerable(
105     name = 'dist-tarball-done',
106     builderNames = [ 'compile', 'clang-analyze', 'debian-packages', 'ubuntu-packages' ],
107 ))
108 ---------------------------------------------
109
110 === Building the dist tarball
111
112 This builder clones the i3 git repository and runs "make dist", which creates a
113 tarball that could be named "i3-4.2.tar.bz2" for example. This tarball is then
114 renamed to dist-%(gitversion).tar.bz2 (so that we can work with a predictable
115 name in the next steps) and uploaded to the buildbot master (since we can have
116 multiple buildslaves, we cannot just let it rest on the buildslave that built
117 it). Afterwards, old dist tarballs are cleaned up and the remaining builders
118 are triggered:
119
120 *Building a dist tarball*:
121 ---------------------------------------------
122 factories = {}
123
124 f = factories['dist'] = BuildFactory()
125
126 # Check out the git repository.
127 f.addStep(s_git)
128
129 # Fill the 'gitversion' property with the output of git describe --tags.
130 f.addStep(shell.SetProperty(command = 'git describe --tags', property = 'gitversion'))
131
132 # Build the dist tarball.
133 cmd(f, name = 'make dist', command = [ 'make', 'dist' ])
134
135 # Rename the created tarball to a well-known name.
136 cmd(f,
137     name = 'rename tarball',
138     command = WithProperties('mv *.tar.bz2 dist-%(gitversion)s.tar.bz2'),
139 )
140
141 # Upload the dist tarball to the master (other factories download it later).
142 f.addStep(transfer.FileUpload(
143     slavesrc = WithProperties('dist-%(gitversion)s.tar.bz2'),
144     masterdest = WithProperties('distballs/dist-%(gitversion)s.tar.bz2'),
145 ))
146
147 # Cleanup old dist tarballs (everything older than tree days).
148 f.addStep(master.MasterShellCommand(
149     command = "find distballs -mtime +3 -exec rm '{}' \;",
150     name = 'cleanup old dist tarballs',
151 ))
152
153 # Everything worked fine, now trigger compilation.
154 f.addStep(Trigger(
155     schedulerNames = [ 'dist-tarball-done' ],
156     copy_properties = [ 'gitversion' ],
157 ))
158 ---------------------------------------------
159
160 Three things are noteworthy about this part of the configuration:
161
162 1. For convenience, we call each factory +f+ (just like the global buildbot
163    config uses +c+ for the top-level configuration) and add it to a dictionary.
164    Factories in that dictionary are later automatically configured for each
165    buildslave.
166
167 2. We have a shared step called +s_git+ so that we only have one location in
168    the configuration file where we specify the git repository URL and branch.
169
170 3. We have a custom function called +cmd+ which is a shortcut for defining a
171    +ShellCommand+ with +haltOnFailure=True+ (since each step is critical) and
172    +logEnviron=False+ (for brevity).
173
174 Here are their definitions:
175
176 *cmd*:
177 ---------------------------------------------
178 def cmd(factory, **kwargs):
179     factory.addStep(ShellCommand(
180         haltOnFailure = True,
181         logEnviron = False,
182         **kwargs
183     ))
184 ---------------------------------------------
185
186 *s_git*:
187 ---------------------------------------------
188 s_git = Git(
189     repourl = 'git://code.i3wm.org/i3',
190     branch = 'next',
191
192     # Check out the latest revision, not the one which caused this build.
193     alwaysUseLatest = True,
194
195     # We cannot use shallow because it breaks git describe --tags.
196     shallow = False,
197
198     # Delete remnants of previous builds.
199     mode = 'full',
200
201     # Store checkouts in source/ and copy them over to build/ to save
202     # bandwidth.
203     method = 'copy',
204 )
205 ---------------------------------------------
206
207 === Compiling the dist tarball
208
209 For this builder to work, you obviously need to install all the
210 build-dependencies for your software on each buildslave. In the case of i3,
211 this can be done with +apt-get build-dep i3-wm+.
212
213 The compilation is pretty straight-forward since it uses the builtin +Compile+
214 step. We call +make+ with +-j4+ (we don’t have enough buildslaves to make
215 figuring out the amount of cores at build-time worthwhile) and +DEBUG=0+ to
216 simulate release build conditions. Also, we pass the preprocessor flag
217 +-D_FORTIFY_SOURCE=2+ and the compiler flags +-Wformat+ and +-Wformat-security+
218 to enable additional warnings.
219
220 *Compiling the dist tarball*:
221 ---------------------------------------------
222 f = factories['compile'] = BuildFactory()
223 unpack_dist_tarball(f)
224 f.addStep(Compile(
225     command = [ 'make', 'DEBUG=0', '-j4' ],
226     warningPattern = '.*warning: ',
227     warnOnWarnings = True,
228     workdir = 'build/DIST',
229     env = {
230       'CPPFLAGS': '-D_FORTIFY_SOURCE=2',
231       'CFLAGS': '-Wformat -Wformat-security'
232     },
233 ))
234
235 f.addStep(WarningsToIRC())
236 ---------------------------------------------
237
238 Again, we use custom functions (and a custom buildstep) to make our lives
239 easier. Here is the definition of unpack_dist_tarball which adds three steps to
240 the factory that download and unpack the dist tarball to the +DIST/+ directory:
241
242 *unpack_dist_tarball*:
243 ---------------------------------------------
244 def unpack_dist_tarball(factory):
245     factory.addStep(transfer.FileDownload(
246         mastersrc = WithProperties('distballs/dist-%(gitversion)s.tar.bz2'),
247         slavedest = 'dist.tar.bz2',
248     ))
249
250     factory.addStep(slave.MakeDirectory(dir = 'build/DIST'))
251
252     cmd(factory,
253         name = 'unpack dist tarball',
254         command = [ 'tar', 'xf', 'dist.tar.bz2', '-C', 'DIST', '--strip-components=1' ],
255     )
256 ---------------------------------------------
257
258 The +WarningsToIRC+ build step is a custom build step which sets a property
259 called "ircsuffix" that is used by our custom IRC bot. This is covered later in
260 more detail. This property gets set to a green or red message, depending on
261 whether there were any warnings:
262
263 *WarningsToIRC*:
264 ---------------------------------------------
265 class WarningsToIRC(buildstep.BuildStep):
266     def start(self):
267         warnings = self.getProperty("warnings-count")
268         if warnings is not None and int(warnings) > 0:
269             warnings = int(warnings)  # just to be sure
270             self.setProperty("ircsuffix", ("\0037 with %d warning%s!" %
271                 (warnings, "s" if warnings != 1 else "")))
272         else:
273             self.setProperty("ircsuffix", "\0033 without warnings")
274         self.finished(SUCCESS)
275 ---------------------------------------------
276
277 === Static code analysis
278
279 For this builder to work, you additionally need the +clang+ compiler on each
280 buildslave: +apt-get install clang+.
281
282 This builder uses only custom functions which you already know by now. It runs
283 scan-build, then moves scan-build’s output from a date-based directory directly
284 into the +CLANG/+ directory and uploads that to the buildmaster.
285
286 On the buildmaster, a webserver is configured which has a symlink to
287 +/home/build/i3-master/htdocs/clang-analyze+ in its document root.
288
289 *static code analysis*:
290 ---------------------------------------------
291 f = factories['clang-analyze'] = BuildFactory()
292 unpack_dist_tarball(f)
293 cmd(f,
294     name='analyze',
295     command = [
296         'scan-build',
297         '-o', '../CLANG',
298         '--html-title', WithProperties('Analysis of i3 v%(gitversion)s'),
299         'make', '-j8',
300     ],
301     workdir = 'build/DIST',
302 )
303
304 # remove the subdirectory -- we always want to overwrite
305 cmd(f, command = 'mv CLANG/*/* CLANG/')
306
307 f.addStep(transfer.DirectoryUpload(
308     slavesrc = 'CLANG',
309     masterdest = 'htdocs/clang-analyze',
310     compress = 'bz2',
311     name = 'upload output',
312 ))
313
314 f.addStep(ClangToIRC())
315 ---------------------------------------------
316
317 The +ClangToIRC+ custom step is even simpler than +WarningsToIRC+. It simply
318 sets the ircsuffix property to a static message:
319
320 *ClangToIRC*:
321 ---------------------------------------------
322 class ClangToIRC(buildstep.BuildStep):
323     def start(self):
324         self.setProperty("ircsuffix", ", see http://build.i3wm.org/clang-analyze/")
325         self.finished(SUCCESS)
326 ---------------------------------------------
327
328 === Generating documentation
329
330 This builder is the one which is the least clean of all. It uses the Debian
331 packaging information to decide which docs to publish and which manpages to
332 generate. Additionally, it uses a for loop instead of calling a script. I
333 recommend including a script to do this in your repository instead.
334
335 Apart from these concerns, the builder is straight-forward: It clones the git
336 repository, generates the documentation and then uploads the documentation to
337 the buildmaster:
338
339 *Generating documentation*:
340 ---------------------------------------------
341 f = factories['docs'] = BuildFactory()
342 f.addStep(s_git)
343 # Fill the 'gitversion' property with the output of git describe --tags.
344 f.addStep(shell.SetProperty(command = 'git describe --tags', property = 'gitversion'))
345 cmd(f, name = 'build docs', command = [ 'make', '-C', 'docs', "ASCIIDOC=asciidoc -a linkcss -a stylesdir=http://i3wm.org/css -a scriptsdir=http://i3wm.org/js --backend=xhtml11 -f docs/asciidoc-git.conf" ])
346 cmd(f, name = 'build manpages', command = "for file in $(sed 's/\.1$/.man/g' debian/i3-wm.manpages); do asciidoc -a linkcss -a stylesdir=http://i3wm.org/css -a scriptsdir=http://i3wm.org/js --backend=xhtml11 -f docs/asciidoc-git.conf \"$file\"; done")
347 f.addStep(slave.MakeDirectory(dir='build/COPY-DOCS'))
348 cmd(f, name = 'copy docs', command = "cp $(tr '\\n' ' ' < debian/i3-wm.docs) COPY-DOCS")
349 cmd(f, name = 'copy manpages', command = "cp $(sed 's/\.1$/.html/g' debian/i3-wm.manpages | tr '\\n' ' ') COPY-DOCS")
350
351 f.addStep(transfer.DirectoryUpload(
352     slavesrc = 'COPY-DOCS',
353     masterdest = 'htdocs/docs-git',
354     compress = 'bz2',
355     name = 'upload docs'))
356
357 f.addStep(DocsToIRC())
358 ---------------------------------------------
359
360 Just as +ClangToIRC+, +DocsToIRC+ appends a static message:
361
362 *DocsToIRC*:
363 ---------------------------------------------
364 class DocsToIRC(buildstep.BuildStep):
365     def start(self):
366         self.setProperty("ircsuffix", ", see http://build.i3wm.org/docs/")
367         self.finished(SUCCESS)
368 ---------------------------------------------
369
370 === Building Debian/Ubuntu packages
371
372 This is the most complex builder of all. It uses +pbuilder-dist+, +debchange+,
373 +dpkg-buildpackage+ and +reprepro+ to generate a Debian repository with a
374 cleanly compiled package for amd64 and i386. In order for it to work, you need
375 to install the following packages: +apt-get install devscripts dpkg-dev
376 reprepro ubuntu-dev-tools+. Afterwards, you need to allow the user as which the
377 buildslave runs to execute pbuilder via sudo without needing a password, so run
378 +visudo+ and add a line like this one:
379
380 *sudoers line*:
381 ---------------------------------------------
382 build    ALL= NOPASSWD: SETENV: /usr/sbin/pbuilder
383 ---------------------------------------------
384
385 Then, as the user as which your buildslave runs, setup the pbuilder
386 environments (you only need to do this once):
387
388 *pbuilder preparation*:
389 ---------------------------------------------
390 sudo ln -s pbuilder-dist /usr/bin/pbuilder-sid-amd64
391 sudo ln -s pbuilder-dist /usr/bin/pbuilder-sid-i386
392 pbuilder-sid-amd64 create
393 pbuilder-sid-i386 create
394 ---------------------------------------------
395
396 Also, you will need a GPG key to sign these packages.
397
398 The debian builder starts by unpacking the dist tarball, copying the Debian
399 packaging from git, creating an empty Debian repository with the
400 +i3-autobuild-keyring+ contents in it. It then adds a new changelog entry to
401 reflect the git version and the fact that this package was built automatically,
402 builds a source package with +dpkg-buildpackage+ and adds it to the repository.
403 Afterwards, it updates each pbuilder and builds binary packages for each
404 architecture (amd64 and i386). After adding the resulting packages to the
405 repository, it uploads the repository to the buildmaster:
406
407 *Debian builder*:
408 ---------------------------------------------
409 distributions = [ 'sid-amd64', 'sid-i386' ]
410 gpg_key = 'BE1DB1F1'
411
412 f = factories['debian-packages'] = BuildFactory()
413 # We need the git repository for the Debian packaging.
414 f.addStep(s_git)
415 unpack_dist_tarball(f)
416 cmd(f, name = 'copy packaging', command = "cp -r debian DIST/")
417
418 # Add a new changelog entry to have the git version in the package version.
419 cmd(f,
420     name = 'update changelog',
421     workdir = 'build/DIST',
422     command = [ 'debchange', '-m', '-l', WithProperties('+g%(gitversion)s'), 'Automatically built' ],
423 )
424
425 cmd(f,
426     name = 'source pkg',
427     command = [ 'dpkg-buildpackage', '-S', '-us', '-uc' ],
428     workdir = 'build/DIST',
429 )
430
431 for dist in distributions:
432     f.addStep(slave.MakeDirectory(dir = 'build/RESULT-' + dist))
433
434 # Create debian sid repository
435 f.addStep(slave.MakeDirectory(dir = 'build/REPO-sid/conf'))
436 f.addStep(transfer.StringDownload(
437     """Codename: sid
438 Suite: unstable
439 Architectures: i386 amd64 source
440 Components: main
441 DebIndices: Packages Release . .gz .bz2
442 DscIndices: Sources Release . .gz .bz2
443 SignWith: %(gpg_key)s
444 """ % { "gpg_key": gpg_key },
445     slavedest = 'REPO-sid/conf/distributions',
446 ))
447
448 # add source package to repository
449 reprepro_include(f, 'i3-wm*_source.changes', 'dsc')
450
451 # Add keyring to the repository. We need to run git clone on our own because
452 # the Git() step assumes there’s precisely one repository we want to deal with.
453 # No big deal since the i3-autobuild-keyring repository is not big.
454 cmd(f,
455     name = 'clone keyring repo',
456     command = 'git clone git://code.i3wm.org/i3-autobuild-keyring',
457 )
458 reprepro_include(f, 'i3-autobuild-keyring/prebuilt/*.changes')
459
460 for dist in distributions:
461     # update the pbuilder
462     cmd(f, name = 'update builder', command = 'pbuilder-' + dist + ' update')
463
464     # build the package for each dist
465     f.addStep(ShellCommand(
466         logEnviron = False,
467         name = 'pkg ' + dist,
468         command = 'pbuilder-' + dist + ' build --binary-arch \
469 --buildresult RESULT-' + dist + ' --debbuildopts -j8 i3-wm*dsc',
470         warnOnFailure = True
471     ))
472
473     reprepro_include(f, 'RESULT-' + dist + '/*.changes')
474
475 # upload the sid repo
476 # Since the next step is cleaning up old files, we set haltOnFailure=True -- we
477 # prefer providing old packages over providing no packages at all :).
478 for directory in [ 'pool', 'dists' ]:
479     f.addStep(transfer.DirectoryUpload(
480         slavesrc = 'REPO-sid/' + directory,
481         masterdest = 'htdocs/debian/sid/' + directory,
482         compress = 'bz2',
483         name = 'upload sid ' + directory,
484         haltOnFailure = True,
485     ))
486
487 f.addStep(master.MasterShellCommand(
488     command = "find htdocs/debian/sid/pool -mtime +3 -exec rm '{}' \;",
489     name = 'cleanup old packages',
490 ))
491
492 # We ensure there is an empty i18n/Index to speed up apt (so that it does not
493 # try to download Translation-*)
494 f.addStep(master.MasterShellCommand(
495     command = [ 'mkdir', '-p', 'htdocs/debian/sid/dists/sid/main/i18n' ],
496     name = 'create i18n folder',
497 ))
498 f.addStep(master.MasterShellCommand(
499     command = [ 'touch', 'htdocs/debian/sid/dists/sid/main/i18n/Index' ],
500     name = 'touch i18n/Index',
501 ))
502 ---------------------------------------------
503
504 The +reprepro_include+ command is defined as follows:
505
506 *reprepro_include*:
507 ---------------------------------------------
508 def reprepro_include(factory, path, debtype='deb', **kwargs):
509     cmd(factory,
510         name = 'reprepro include',
511         command = 'reprepro --ignore=wrongdistribution -T ' + debtype + ' -b REPO-sid include sid ' + path,
512         **kwargs
513     )
514 ---------------------------------------------
515
516 Running such a builder for Ubuntu works exactly the same way, but you need to
517 replace "sid" with "precise" in all places (see the full configuration file for
518 an example).
519
520 === Status targets
521
522 We don’t advertise the HTTP status target. Instead, status is posted to IRC via
523 a custom bot. This bot provides an HTTP end point and buildbot is configured to
524 push status changes to that endpoint:
525
526 *http status target*:
527 ---------------------------------------------
528 c['status'].append(buildbot.status.status_push.HttpStatusPush(
529     serverUrl = 'http://localhost:8080/push_buildbot',
530 ))
531 ---------------------------------------------
532
533 You can find the source code of that bot at
534 http://code.stapelberg.de/git/go-buildbot-announce/. As the name suggests, it
535 is written in Go. Also, it is quite specific to i3, so you might be better off
536 implementing such a bot (or plugin) on your own. It might make for a nice
537 example, though, especially back when its only feature was announcing the build
538 status:
539
540 http://code.stapelberg.de/git/go-buildbot-announce/tree/src/i3build.go?id=eeebf1a546454c8a0d82ca623886bb835cd32ba0
541
542 == Full configuration file
543
544 This is the full configuration file, as tested and currently in use (except for
545 the passwords, though):
546
547 *master.cfg*:
548 ---------------------------------------------
549 # -*- python -*-
550 # -*- coding: utf-8
551 # vim:ts=4:sw=4:expandtab:syntax=python
552 #
553 # i3 buildbot configuration
554 # © 2012 Michael Stapelberg, Public Domain
555 # see http://i3wm.org/docs/buildbot.html for more information.
556
557 from buildbot.buildslave import BuildSlave
558 from buildbot.changes import pb
559 from buildbot.schedulers.basic import SingleBranchScheduler
560 from buildbot.schedulers.triggerable import Triggerable
561 from buildbot.process.properties import WithProperties
562 from buildbot.process.factory import BuildFactory
563 from buildbot.steps.source.git import Git
564 from buildbot.steps.shell import ShellCommand
565 from buildbot.steps.shell import Compile
566 from buildbot.steps.trigger import Trigger
567 from buildbot.steps import shell, transfer, master, slave
568 from buildbot.config import BuilderConfig
569 from buildbot.process import buildstep
570 from buildbot.status import html
571 from buildbot.status import words
572 import buildbot.status.status_push
573 from buildbot.status.web import auth, authz
574 from buildbot.status.builder import SUCCESS, FAILURE
575
576 c = BuildmasterConfig = {}
577
578 c['slaves'] = [BuildSlave('docsteel-vm', 'secret')]
579 c['slavePortnum'] = 9989
580 # Changes are pushed to buildbot using a git hook.
581 c['change_source'] = [pb.PBChangeSource(
582     user = 'i3-source',
583     passwd = 'secret',
584 )]
585
586 ################################################################################
587 # schedulers
588 ################################################################################
589
590 c['schedulers'] = []
591
592 # The first scheduler kicks off multiple builders:
593 # • 'dist' builds a dist tarball and starts the triggerable schedulers
594 #   'compile'
595 # • 'docs' builds the documentation with a special asciidoc configuration
596 #   (therefore, it does not profit from a dist tarball and can be run in
597 #    parallel).
598 c['schedulers'].append(SingleBranchScheduler(
599     name = 'dist',
600     branch = 'next',
601     treeStableTimer = 10,
602     builderNames = [ 'dist', 'docs' ],
603 ))
604
605 c['schedulers'].append(Triggerable(
606     name = 'dist-tarball-done',
607     builderNames = [ 'compile', 'clang-analyze', 'debian-packages', 'ubuntu-packages' ],
608 ))
609
610 ################################################################################
611 # Shortcuts for builders
612 ################################################################################
613
614 # shortcut for a ShellCommand with haltOnFailure=True, logEnviron=False
615 def cmd(factory, **kwargs):
616     factory.addStep(ShellCommand(
617         haltOnFailure=True,
618         logEnviron=False,
619         **kwargs
620     ))
621
622 # Shortcut to add steps necessary to download and unpack the dist tarball.
623 def unpack_dist_tarball(factory):
624     factory.addStep(transfer.FileDownload(
625         mastersrc=WithProperties('distballs/dist-%(gitversion)s.tar.bz2'),
626         slavedest='dist.tar.bz2',
627     ))
628     factory.addStep(slave.MakeDirectory(dir='build/DIST'))
629     cmd(factory,
630         name = 'unpack dist tarball',
631         command = [ 'tar', 'xf', 'dist.tar.bz2', '-C', 'DIST', '--strip-components=1' ],
632     )
633
634 # Includes the given path in REPO-sid using reprepro.
635 def reprepro_include(factory, path, debtype='deb', **kwargs):
636     cmd(factory,
637         name = 'reprepro include',
638         command = 'reprepro --ignore=wrongdistribution -T ' + debtype + ' -b REPO-sid include sid ' + path,
639         **kwargs
640     )
641
642 def reprepro_include_ubuntu(factory, path, debtype='deb', **kwargs):
643     cmd(factory,
644         name = 'reprepro include',
645         command = 'reprepro --ignore=wrongdistribution -T ' + debtype + ' -b REPO-sid include precise ' + path,
646         **kwargs
647     )
648
649 ################################################################################
650 # Custom steps
651 ################################################################################
652
653 # Adds the ircsuffix property to reflect whether there were warnings.
654 class WarningsToIRC(buildstep.BuildStep):
655   def start(self):
656     warnings = self.getProperty("warnings-count")
657     if warnings is not None and int(warnings) > 0:
658       warnings = int(warnings)  # just to be sure
659       self.setProperty("ircsuffix", "\0037 with %d warning%s!" % (warnings, "s" if warnings != 1 else ""))
660     else:
661       self.setProperty("ircsuffix", "\0033 without warnings")
662     self.finished(SUCCESS)
663
664 # Adds a link to the automatically generated documentation.
665 class DocsToIRC(buildstep.BuildStep):
666   def start(self):
667     self.setProperty("ircsuffix", ", see http://build.i3wm.org/docs/")
668     self.finished(SUCCESS)
669
670 # Adds a link to the clang report.
671 class ClangToIRC(buildstep.BuildStep):
672   def start(self):
673     self.setProperty("ircsuffix", ", see http://build.i3wm.org/clang-analyze/")
674     self.finished(SUCCESS)
675
676 ################################################################################
677 # Shared steps, used in different factories.
678 ################################################################################
679
680 s_git = Git(
681     repourl='git://code.i3wm.org/i3',
682     branch='next',
683
684     # Check out the latest revision, not the one which caused this build.
685     alwaysUseLatest=True,
686
687     # We cannot use shallow because it breaks git describe --tags.
688     shallow=False,
689
690     # Delete remnants of previous builds.
691     mode='full',
692
693     # Store checkouts in source/ and copy them over to build/ to save
694     # bandwidth.
695     method='copy',
696
697     # XXX: In newer versions of buildbot (> 0.8.6), we want to use
698     # getDescription={ 'tags': True } here and get rid of the extra git
699     # describe --tags step.
700 )
701
702 ################################################################################
703 # factory: "dist" — builds the dist tarball once (used by all other factories)
704 ################################################################################
705
706 factories = {}
707
708 f = factories['dist'] = BuildFactory()
709 # Check out the git repository.
710 f.addStep(s_git)
711 # Fill the 'gitversion' property with the output of git describe --tags.
712 f.addStep(shell.SetProperty(command = 'git describe --tags', property = 'gitversion'))
713 # Build the dist tarball.
714 cmd(f, name = 'make dist', command = [ 'make', 'dist' ])
715 # Rename the created tarball to a well-known name.
716 cmd(f, name = 'rename tarball', command = WithProperties('mv *.tar.bz2 dist-%(gitversion)s.tar.bz2'))
717 # Upload the dist tarball to the master (other factories download it later).
718 f.addStep(transfer.FileUpload(
719     slavesrc = WithProperties('dist-%(gitversion)s.tar.bz2'),
720     masterdest = WithProperties('distballs/dist-%(gitversion)s.tar.bz2'),
721 ))
722 # Cleanup old dist tarballs (everything older than tree days).
723 f.addStep(master.MasterShellCommand(
724     command = "find distballs -mtime +3 -exec rm '{}' \;",
725     name = 'cleanup old dist tarballs',
726 ))
727 # Everything worked fine, now trigger compilation.
728 f.addStep(Trigger(
729     schedulerNames = [ 'dist-tarball-done' ],
730     copy_properties = [ 'gitversion' ],
731 ))
732
733 ################################################################################
734 # factory: "compile" — compiles the dist tarball and reports warnings
735 ################################################################################
736
737 f = factories['compile'] = BuildFactory()
738 unpack_dist_tarball(f)
739 f.addStep(Compile(
740     command = [ 'make', 'DEBUG=0', '-j4' ],
741     warningPattern = '.*warning: ',
742     warnOnWarnings = True,
743     workdir = 'build/DIST',
744     env = {
745       'CPPFLAGS': '-D_FORTIFY_SOURCE=2',
746       'CFLAGS': '-Wformat -Wformat-security'
747     },
748 ))
749
750 f.addStep(WarningsToIRC())
751
752 ################################################################################
753 # factory: "clang-analyze" — runs a static code analysis
754 ################################################################################
755 # $ sudo apt-get install clang
756
757 f = factories['clang-analyze'] = BuildFactory()
758 unpack_dist_tarball(f)
759 cmd(f,
760     name='analyze',
761     command = [
762         'scan-build',
763         '-o', '../CLANG',
764         '--html-title', WithProperties('Analysis of i3 v%(gitversion)s'),
765         'make', '-j8',
766     ],
767     workdir = 'build/DIST',
768 )
769
770 # remove the subdirectory -- we always want to overwrite
771 cmd(f, command = 'mv CLANG/*/* CLANG/')
772
773 f.addStep(transfer.DirectoryUpload(
774     slavesrc = 'CLANG',
775     masterdest = 'htdocs/clang-analyze',
776     compress = 'bz2',
777     name = 'upload output',
778 ))
779
780 f.addStep(ClangToIRC())
781
782 ################################################################################
783 # factory: "docs" — builds documentation with a special asciidoc conf
784 ################################################################################
785
786 f = factories['docs'] = BuildFactory()
787 f.addStep(s_git)
788 # Fill the 'gitversion' property with the output of git describe --tags.
789 f.addStep(shell.SetProperty(command = 'git describe --tags', property = 'gitversion'))
790 cmd(f, name = 'build docs', command = [ 'make', '-C', 'docs', "ASCIIDOC=asciidoc -a linkcss -a stylesdir=http://i3wm.org/css -a scriptsdir=http://i3wm.org/js --backend=xhtml11 -f docs/asciidoc-git.conf" ])
791 cmd(f, name = 'build manpages', command = "for file in $(sed 's/\.1$/.man/g' debian/i3-wm.manpages); do asciidoc -a linkcss -a stylesdir=http://i3wm.org/css -a scriptsdir=http://i3wm.org/js --backend=xhtml11 -f docs/asciidoc-git.conf \"$file\"; done")
792 f.addStep(slave.MakeDirectory(dir='build/COPY-DOCS'))
793 cmd(f, name = 'copy docs', command = "cp $(tr '\\n' ' ' < debian/i3-wm.docs) COPY-DOCS")
794 cmd(f, name = 'copy manpages', command = "cp $(sed 's/\.1$/.html/g' debian/i3-wm.manpages | tr '\\n' ' ') COPY-DOCS")
795
796 f.addStep(transfer.DirectoryUpload(
797     slavesrc = 'COPY-DOCS',
798     masterdest = 'htdocs/docs-git',
799     compress = 'bz2',
800     name = 'upload docs'))
801
802 f.addStep(DocsToIRC())
803
804 ################################################################################
805 # factory: "debian-packages" — builds Debian (sid) packages for amd64 and i386
806 ################################################################################
807
808 distributions = [ 'sid-amd64', 'sid-i386' ]
809 gpg_key = 'BE1DB1F1'
810
811 f = factories['debian-packages'] = BuildFactory()
812 # We need the git repository for the Debian packaging.
813 f.addStep(s_git)
814 unpack_dist_tarball(f)
815 cmd(f, name='copy packaging', command = "cp -r debian DIST/")
816
817 # Add a new changelog entry to have the git version in the package version.
818 cmd(f,
819     name = 'update changelog',
820     workdir = 'build/DIST',
821     command = [ 'debchange', '-m', '-l', WithProperties('+g%(gitversion)s'), 'Automatically built' ],
822 )
823
824 cmd(f,
825     name = 'source pkg',
826     command = [ 'dpkg-buildpackage', '-S', '-us', '-uc' ],
827     workdir = 'build/DIST',
828 )
829
830 for dist in distributions:
831     f.addStep(slave.MakeDirectory(dir='build/RESULT-' + dist))
832
833 # Create debian sid repository
834 f.addStep(slave.MakeDirectory(dir='build/REPO-sid/conf'))
835 f.addStep(transfer.StringDownload(
836     """Codename: sid
837 Suite: unstable
838 Architectures: i386 amd64 source
839 Components: main
840 DebIndices: Packages Release . .gz .bz2
841 DscIndices: Sources Release . .gz .bz2
842 SignWith: %(gpg_key)s
843 """ % { "gpg_key": gpg_key },
844     slavedest = 'REPO-sid/conf/distributions',
845 ))
846
847 # add source package to repository
848 reprepro_include(f, 'i3-wm*_source.changes', 'dsc')
849
850 # Add keyring to the repository. We need to run git clone on our own because
851 # the Git() step assumes there’s precisely one repository we want to deal with.
852 # No big deal since the i3-autobuild-keyring repository is not big.
853 cmd(f, name='clone keyring repo', command = 'git clone git://code.i3wm.org/i3-autobuild-keyring')
854 reprepro_include(f, 'i3-autobuild-keyring/prebuilt/*.changes')
855
856 for dist in distributions:
857     # update the pbuilder
858     cmd(f, name = 'update builder', command = 'pbuilder-' + dist + ' update')
859
860     # build the package for each dist
861     f.addStep(ShellCommand(
862         logEnviron = False,
863         name = 'pkg ' + dist,
864         command = 'pbuilder-' + dist + ' build --binary-arch \
865 --buildresult RESULT-' + dist + ' --debbuildopts -j8 i3-wm*dsc',
866         warnOnFailure = True
867     ))
868
869     reprepro_include(f, 'RESULT-' + dist + '/*.changes')
870
871 # upload the sid repo
872 # Since the next step is cleaning up old files, we set haltOnFailure=True -- we
873 # prefer providing old packages over providing no packages at all :).
874 for directory in [ 'pool', 'dists' ]:
875     f.addStep(transfer.DirectoryUpload(
876         slavesrc = 'REPO-sid/' + directory,
877         masterdest = 'htdocs/debian/sid/' + directory,
878         compress = 'bz2',
879         name = 'upload sid ' + directory,
880         haltOnFailure = True,
881     ))
882
883 f.addStep(master.MasterShellCommand(
884     command = "find htdocs/debian/sid/pool -mtime +3 -exec rm '{}' \;",
885     name = 'cleanup old packages',
886 ))
887
888 # We ensure there is an empty i18n/Index to speed up apt (so that it does not
889 # try to download Translation-*)
890 f.addStep(master.MasterShellCommand(
891     command = [ 'mkdir', '-p', 'htdocs/debian/sid/dists/sid/main/i18n' ],
892     name = 'create i18n folder',
893 ))
894 f.addStep(master.MasterShellCommand(
895     command = [ 'touch', 'htdocs/debian/sid/dists/sid/main/i18n/Index' ],
896     name = 'touch i18n/Index',
897 ))
898
899 ################################################################################
900 # factory: "ubuntu-packages" — builds Ubuntu (precise) packages for amd64 and i386
901 ################################################################################
902
903 distributions = [ 'precise-amd64', 'precise-i386' ]
904 gpg_key = 'BE1DB1F1'
905
906 f = factories['ubuntu-packages'] = BuildFactory()
907 # We need the git repository for the Debian packaging.
908 f.addStep(s_git)
909 unpack_dist_tarball(f)
910 cmd(f, name='copy packaging', command = "cp -r debian DIST/")
911
912 # Add a new changelog entry to have the git version in the package version.
913 cmd(f,
914     name = 'update changelog',
915     workdir = 'build/DIST',
916     command = [ 'debchange', '-m', '-l', WithProperties('+g%(gitversion)s'), 'Automatically built' ],
917 )
918
919 cmd(f,
920     name = 'source pkg',
921     command = [ 'dpkg-buildpackage', '-S', '-us', '-uc' ],
922     workdir = 'build/DIST',
923 )
924
925 for dist in distributions:
926     f.addStep(slave.MakeDirectory(dir='build/RESULT-' + dist))
927
928 # Create debian sid repository
929 f.addStep(slave.MakeDirectory(dir='build/REPO-sid/conf'))
930 f.addStep(transfer.StringDownload(
931     """Codename: precise
932 Suite: unstable
933 Architectures: i386 amd64 source
934 Components: main
935 DebIndices: Packages Release . .gz .bz2
936 DscIndices: Sources Release . .gz .bz2
937 SignWith: %(gpg_key)s
938 """ % { "gpg_key": gpg_key },
939     slavedest = 'REPO-sid/conf/distributions',
940 ))
941
942 # add source package to repository
943 reprepro_include_ubuntu(f, 'i3-wm*_source.changes', 'dsc')
944
945 # Add keyring to the repository. We need to run git clone on our own because
946 # the Git() step assumes there’s precisely one repository we want to deal with.
947 # No big deal since the i3-autobuild-keyring repository is not big.
948 cmd(f, name='clone keyring repo', command = 'git clone git://code.i3wm.org/i3-autobuild-keyring')
949 reprepro_include_ubuntu(f, 'i3-autobuild-keyring/prebuilt/*.changes')
950
951 for dist in distributions:
952     # update the pbuilder
953     cmd(f, name = 'update builder', command = 'pbuilder-' + dist + ' update')
954
955     # build the package for each dist
956     f.addStep(ShellCommand(
957         logEnviron = False,
958         name = 'pkg ' + dist,
959         command = 'pbuilder-' + dist + ' build --binary-arch \
960 --buildresult RESULT-' + dist + ' --debbuildopts -j8 i3-wm*dsc',
961         warnOnFailure = True
962     ))
963
964     reprepro_include_ubuntu(f, 'RESULT-' + dist + '/*.changes')
965
966 # upload the sid repo
967 # Since the next step is cleaning up old files, we set haltOnFailure=True -- we
968 # prefer providing old packages over providing no packages at all :).
969 for directory in [ 'pool', 'dists' ]:
970     f.addStep(transfer.DirectoryUpload(
971         slavesrc = 'REPO-sid/' + directory,
972         masterdest = 'htdocs/ubuntu/precise/' + directory,
973         compress = 'bz2',
974         name = 'upload precise ' + directory,
975         haltOnFailure = True,
976     ))
977
978 f.addStep(master.MasterShellCommand(
979     command = "find htdocs/ubuntu/precise/pool -mtime +3 -exec rm '{}' \;",
980     name = 'cleanup old packages',
981 ))
982
983 # We ensure there is an empty i18n/Index to speed up apt (so that it does not
984 # try to download Translation-*)
985 f.addStep(master.MasterShellCommand(
986     command = [ 'mkdir', '-p', 'htdocs/ubuntu/precise/dists/sid/main/i18n' ],
987     name = 'create i18n folder',
988 ))
989 f.addStep(master.MasterShellCommand(
990     command = [ 'touch', 'htdocs/ubuntu/precise/dists/sid/main/i18n/Index' ],
991     name = 'touch i18n/Index',
992 ))
993
994
995 c['builders'] = []
996
997 # Add all builders to all buildslaves.
998 for factoryname in factories.keys():
999     c['builders'].append(BuilderConfig(
1000         name = factoryname,
1001         slavenames=['docsteel-vm'],
1002         factory=factories[factoryname],
1003     ))
1004
1005
1006 ####### STATUS TARGETS
1007
1008 c['status'] = []
1009
1010 authz_cfg=authz.Authz(
1011     gracefulShutdown = False,
1012     forceBuild = False,
1013     forceAllBuilds = False,
1014     pingBuilder = False,
1015     stopBuild = False,
1016     stopAllBuilds = False,
1017     cancelPendingBuild = False,
1018 )
1019
1020 c['status'].append(html.WebStatus(http_port=8010, authz=authz_cfg))
1021
1022 c['status'].append(buildbot.status.status_push.HttpStatusPush(
1023     serverUrl = 'http://localhost:8080/push_buildbot',
1024 ))
1025
1026 ####### PROJECT IDENTITY
1027
1028 c['title'] = 'i3'
1029 c['titleURL'] = 'http://i3wm.org/'
1030 # Removed so that search engines don’t crawl it
1031 c['buildbotURL'] = 'http://localhost/'
1032
1033 ####### DB URL
1034
1035 c['db'] = {
1036     # This specifies what database buildbot uses to store its state.  You can leave
1037     # this at its default for all but the largest installations.
1038     'db_url' : "sqlite:///state.sqlite",
1039 }
1040 ---------------------------------------------