autobuild: Move defaulttasks to one-per-line
[nivanova/samba-autobuild/.git] / script / autobuild.py
1 #!/usr/bin/env python
2 # run tests on all Samba subprojects and push to a git tree on success
3 # Copyright Andrew Tridgell 2010
4 # released under GNU GPL v3 or later
5
6 from subprocess import call, check_call,Popen, PIPE
7 import os, tarfile, sys, time
8 from optparse import OptionParser
9 import smtplib
10 import email
11 from email.mime.text import MIMEText
12 from email.mime.base import MIMEBase
13 from email.mime.application import MIMEApplication
14 from email.mime.multipart import MIMEMultipart
15 from distutils.sysconfig import get_python_lib
16 import platform
17
18 os.environ["PYTHONUNBUFFERED"] = "1"
19
20 # This speeds up testing remarkably.
21 os.environ['TDB_NO_FSYNC'] = '1'
22
23 cleanup_list = []
24
25 builddirs = {
26     "ctdb"    : "ctdb",
27     "samba"  : ".",
28     "samba-xc" : ".",
29     "samba-o3" : ".",
30     "samba-ctdb" : ".",
31     "samba-libs"  : ".",
32     "samba-static"  : ".",
33     "samba-test-only"  : ".",
34     "samba-systemkrb5"  : ".",
35     "samba-nopython"  : ".",
36     "ldb"     : "lib/ldb",
37     "tdb"     : "lib/tdb",
38     "talloc"  : "lib/talloc",
39     "replace" : "lib/replace",
40     "tevent"  : "lib/tevent",
41     "pidl"    : "pidl",
42     "pass"    : ".",
43     "fail"    : ".",
44     "retry"   : "."
45     }
46
47 defaulttasks = [ "ctdb",
48                  "samba",
49                  "samba-xc",
50                  "samba-o3",
51                  "samba-ctdb",
52                  "samba-libs",
53                  "samba-static",
54                  "samba-systemkrb5",
55                  "samba-nopython",
56                  "ldb",
57                  "tdb",
58                  "talloc",
59                  "replace",
60                  "tevent",
61                  "pidl" ]
62
63 if os.environ.get("AUTOBUILD_SKIP_SAMBA_O3", "0") == "1":
64     defaulttasks.remove("samba-o3")
65
66 ctdb_configure_params = " --enable-developer --picky-developer ${PREFIX}"
67 samba_configure_params = " --picky-developer ${PREFIX} ${EXTRA_PYTHON} --with-profiling-data"
68
69 samba_libs_envvars =  "PYTHONPATH=${PYTHON_PREFIX}/site-packages:$PYTHONPATH"
70 samba_libs_envvars += " PKG_CONFIG_PATH=$PKG_CONFIG_PATH:${PREFIX_DIR}/lib/pkgconfig"
71 samba_libs_envvars += " ADDITIONAL_CFLAGS='-Wmissing-prototypes'"
72 samba_libs_configure_base = samba_libs_envvars + " ./configure --abi-check --enable-debug --picky-developer -C ${PREFIX} ${EXTRA_PYTHON}"
73 samba_libs_configure_libs = samba_libs_configure_base + " --bundled-libraries=cmocka,NONE"
74 samba_libs_configure_samba = samba_libs_configure_base + " --bundled-libraries=!talloc,!pytalloc-util,!tdb,!pytdb,!ldb,!pyldb,!pyldb-util,!tevent,!pytevent"
75
76 if os.environ.get("AUTOBUILD_NO_EXTRA_PYTHON", "0") == "1":
77     extra_python = ""
78 else:
79     extra_python = "--extra-python=/usr/bin/python3"
80
81 tasks = {
82     "ctdb" : [ ("random-sleep", "../script/random-sleep.sh 60 600", "text/plain"),
83                ("configure", "./configure " + ctdb_configure_params, "text/plain"),
84                ("make", "make all", "text/plain"),
85                ("install", "make install", "text/plain"),
86                ("test", "make autotest", "text/plain"),
87                ("check-clean-tree", "../script/clean-source-tree.sh", "text/plain"),
88                ("clean", "make clean", "text/plain") ],
89
90     # We have 'test' before 'install' because, 'test' should work without 'install'
91     "samba" : [ ("configure", "./configure.developer --with-selftest-prefix=./bin/ab" + samba_configure_params, "text/plain"),
92                 ("make", "make -j", "text/plain"),
93                 ("test", "make test FAIL_IMMEDIATELY=1", "text/plain"),
94                 ("install", "make install", "text/plain"),
95                 ("check-clean-tree", "script/clean-source-tree.sh", "text/plain"),
96                 ("clean", "make clean", "text/plain") ],
97
98     "samba-test-only" : [ ("configure", "./configure.developer --with-selftest-prefix=./bin/ab  --abi-check-disable" + samba_configure_params, "text/plain"),
99                           ("make", "make -j", "text/plain"),
100                           ("test", 'make test FAIL_IMMEDIATELY=1 TESTS="${TESTS}"',"text/plain") ],
101
102     # Test cross-compile infrastructure
103     "samba-xc" : [ ("configure-native", "./configure.developer --with-selftest-prefix=./bin/ab" + samba_configure_params, "text/plain"),
104                    ("configure-cross-execute", "./configure.developer -b ./bin-xe --cross-compile --cross-execute=script/identity_cc.sh" \
105                     " --cross-answers=./bin-xe/cross-answers.txt --with-selftest-prefix=./bin-xe/ab" + samba_configure_params, "text/plain"),
106                    ("configure-cross-answers", "./configure.developer -b ./bin-xa --cross-compile" \
107                     " --cross-answers=./bin-xe/cross-answers.txt --with-selftest-prefix=./bin-xa/ab" + samba_configure_params, "text/plain"),
108                    ("compare-results", "script/compare_cc_results.py ./bin/c4che/default.cache.py ./bin-xe/c4che/default.cache.py ./bin-xa/c4che/default.cache.py", "text/plain")],
109
110     # test build with -O3 -- catches extra warnings and bugs, tests the ad_dc environments
111     "samba-o3" : [ ("random-sleep", "../script/random-sleep.sh 60 600", "text/plain"),
112                    ("configure", "ADDITIONAL_CFLAGS='-O3' ./configure.developer --with-selftest-prefix=./bin/ab --abi-check-disable" + samba_configure_params, "text/plain"),
113                    ("make", "make -j", "text/plain"),
114                    ("test", "make quicktest FAIL_IMMEDIATELY=1 TESTS='--include-env=ad_dc'", "text/plain"),
115                    ("install", "make install", "text/plain"),
116                    ("check-clean-tree", "script/clean-source-tree.sh", "text/plain"),
117                    ("clean", "make clean", "text/plain") ],
118
119     "samba-ctdb" : [ ("random-sleep", "script/random-sleep.sh 60 600", "text/plain"),
120
121                      # make sure we have tdb around:
122                      ("tdb-configure", "cd lib/tdb && PYTHONPATH=${PYTHON_PREFIX}/site-packages:$PYTHONPATH PKG_CONFIG_PATH=$PKG_CONFIG_PATH:${PREFIX_DIR}/lib/pkgconfig ./configure --bundled-libraries=NONE --abi-check --enable-debug -C ${PREFIX}", "text/plain"),
123                      ("tdb-make", "cd lib/tdb && make", "text/plain"),
124                      ("tdb-install", "cd lib/tdb && make install", "text/plain"),
125
126
127                      # build samba with cluster support (also building ctdb):
128                      ("samba-configure", "PYTHONPATH=${PYTHON_PREFIX}/site-packages:$PYTHONPATH PKG_CONFIG_PATH=${PREFIX_DIR}/lib/pkgconfig:${PKG_CONFIG_PATH} ./configure.developer --picky-developer ${PREFIX} --with-selftest-prefix=./bin/ab --with-cluster-support --bundled-libraries=!tdb", "text/plain"),
129                      ("samba-make", "make", "text/plain"),
130                      ("samba-check", "./bin/smbd -b | grep CLUSTER_SUPPORT", "text/plain"),
131                      ("samba-install", "make install", "text/plain"),
132                      ("ctdb-check", "test -e ${PREFIX_DIR}/sbin/ctdbd", "text/plain"),
133
134                      # clean up:
135                      ("check-clean-tree", "script/clean-source-tree.sh", "text/plain"),
136                      ("clean", "make clean", "text/plain"),
137                      ("ctdb-clean", "cd ./ctdb && make clean", "text/plain") ],
138
139     "samba-libs" : [
140                       ("random-sleep", "script/random-sleep.sh 60 600", "text/plain"),
141                       ("talloc-configure", "cd lib/talloc && " + samba_libs_configure_libs, "text/plain"),
142                       ("talloc-make", "cd lib/talloc && make", "text/plain"),
143                       ("talloc-install", "cd lib/talloc && make install", "text/plain"),
144
145                       ("tdb-configure", "cd lib/tdb && " + samba_libs_configure_libs, "text/plain"),
146                       ("tdb-make", "cd lib/tdb && make", "text/plain"),
147                       ("tdb-install", "cd lib/tdb && make install", "text/plain"),
148
149                       ("tevent-configure", "cd lib/tevent && " + samba_libs_configure_libs, "text/plain"),
150                       ("tevent-make", "cd lib/tevent && make", "text/plain"),
151                       ("tevent-install", "cd lib/tevent && make install", "text/plain"),
152
153                       ("ldb-configure", "cd lib/ldb && " + samba_libs_configure_libs, "text/plain"),
154                       ("ldb-make", "cd lib/ldb && make", "text/plain"),
155                       ("ldb-install", "cd lib/ldb && make install", "text/plain"),
156
157                       ("nondevel-configure", "./configure ${PREFIX}", "text/plain"),
158                       ("nondevel-make", "make -j", "text/plain"),
159                       ("nondevel-check", "./bin/smbd -b | grep WITH_NTVFS_FILESERVER && exit 1; exit 0", "text/plain"),
160                       ("nondevel-install", "make install", "text/plain"),
161                       ("nondevel-dist", "make dist", "text/plain"),
162
163                       # retry with all modules shared
164                       ("allshared-distclean", "make distclean", "text/plain"),
165                       ("allshared-configure", samba_libs_configure_samba + " --with-shared-modules=ALL", "text/plain"),
166                       ("allshared-make", "make -j", "text/plain")],
167
168     "samba-static" : [
169                       ("random-sleep", "script/random-sleep.sh 60 600", "text/plain"),
170                       # build with all modules static
171                       ("allstatic-configure", "./configure.developer " + samba_configure_params + " --with-static-modules=ALL", "text/plain"),
172                       ("allstatic-make", "make -j", "text/plain"),
173
174                       # retry without any required modules
175                       ("none-distclean", "make distclean", "text/plain"),
176                       ("none-configure", "./configure.developer " + samba_configure_params + " --with-static-modules=!FORCED,!DEFAULT --with-shared-modules=!FORCED,!DEFAULT", "text/plain"),
177                       ("none-make", "make -j", "text/plain"),
178
179                       # retry with nonshared smbd and smbtorture
180                       ("nonshared-distclean", "make distclean", "text/plain"),
181                       ("nonshared-configure", "./configure.developer " + samba_configure_params + " --bundled-libraries=talloc,tdb,pytdb,ldb,pyldb,tevent,pytevent --with-static-modules=ALL --nonshared-binary=smbtorture,smbd/smbd", "text/plain"),
182                       ("nonshared-make", "make -j", "text/plain")],
183
184     "samba-systemkrb5" : [
185                       ("random-sleep", "script/random-sleep.sh 60 600", "text/plain"),
186                       ("configure", "./configure.developer " + samba_configure_params + " --with-system-mitkrb5 --without-ad-dc", "text/plain"),
187                       ("make", "make -j", "text/plain"),
188                       # we currently cannot run a full make test, a limited list of tests could be run
189                       # via "make test TESTS=sometests"
190                       ("test", "make test FAIL_IMMEDIATELY=1 TESTS='--include-env=ktest'", "text/plain"),
191                       ("install", "make install", "text/plain"),
192                       ("check-clean-tree", "script/clean-source-tree.sh", "text/plain"),
193                       ("clean", "make clean", "text/plain")
194                       ],
195
196     # Test Samba without python still builds.  When this test fails
197     # due to more use of Python, the expectations is that the newly
198     # failing part of the code should be disabled when
199     # --disable-python is set (rather than major work being done to
200     # support this environment).  The target here is for vendors
201     # shipping a minimal smbd.
202     "samba-nopython" : [
203                       ("random-sleep", "script/random-sleep.sh 60 600", "text/plain"),
204                       ("configure", "./configure.developer --picky-developer ${PREFIX} --with-profiling-data --disable-python --without-ad-dc", "text/plain"),
205                       ("make", "make -j", "text/plain"),
206                       ("install", "make install", "text/plain"),
207                       ("check-clean-tree", "script/clean-source-tree.sh", "text/plain"),
208                       ("clean", "make clean", "text/plain")
209                       ],
210
211
212
213     "ldb" : [
214               ("random-sleep", "../../script/random-sleep.sh 60 600", "text/plain"),
215               ("configure", "./configure --enable-developer -C ${PREFIX} ${EXTRA_PYTHON}", "text/plain"),
216               ("make", "make", "text/plain"),
217               ("install", "make install", "text/plain"),
218               ("test", "make test", "text/plain"),
219               ("check-clean-tree", "../../script/clean-source-tree.sh", "text/plain"),
220               ("distcheck", "make distcheck", "text/plain"),
221               ("clean", "make clean", "text/plain") ],
222
223     "tdb" : [
224               ("random-sleep", "../../script/random-sleep.sh 60 600", "text/plain"),
225               ("configure", "./configure --enable-developer -C ${PREFIX} ${EXTRA_PYTHON}", "text/plain"),
226               ("make", "make", "text/plain"),
227               ("install", "make install", "text/plain"),
228               ("test", "make test", "text/plain"),
229               ("check-clean-tree", "../../script/clean-source-tree.sh", "text/plain"),
230               ("distcheck", "make distcheck", "text/plain"),
231               ("clean", "make clean", "text/plain") ],
232
233     "talloc" : [
234                  ("random-sleep", "../../script/random-sleep.sh 60 600", "text/plain"),
235                  ("configure", "./configure --enable-developer -C ${PREFIX} ${EXTRA_PYTHON}", "text/plain"),
236                  ("make", "make", "text/plain"),
237                  ("install", "make install", "text/plain"),
238                  ("test", "make test", "text/plain"),
239                  ("check-clean-tree", "../../script/clean-source-tree.sh", "text/plain"),
240                  ("distcheck", "make distcheck", "text/plain"),
241                  ("clean", "make clean", "text/plain") ],
242
243     "replace" : [
244                   ("random-sleep", "../../script/random-sleep.sh 60 600", "text/plain"),
245                   ("configure", "./configure --enable-developer -C ${PREFIX}", "text/plain"),
246                   ("make", "make", "text/plain"),
247                   ("install", "make install", "text/plain"),
248                   ("test", "make test", "text/plain"),
249                   ("check-clean-tree", "../../script/clean-source-tree.sh", "text/plain"),
250                   ("distcheck", "make distcheck", "text/plain"),
251                   ("clean", "make clean", "text/plain") ],
252
253     "tevent" : [
254                  ("random-sleep", "../../script/random-sleep.sh 60 600", "text/plain"),
255                  ("configure", "./configure --enable-developer -C ${PREFIX} ${EXTRA_PYTHON}", "text/plain"),
256                  ("make", "make", "text/plain"),
257                  ("install", "make install", "text/plain"),
258                  ("test", "make test", "text/plain"),
259                  ("check-clean-tree", "../../script/clean-source-tree.sh", "text/plain"),
260                  ("distcheck", "make distcheck", "text/plain"),
261                  ("clean", "make clean", "text/plain") ],
262
263     "pidl" : [
264                ("random-sleep", "../script/random-sleep.sh 60 600", "text/plain"),
265                ("configure", "perl Makefile.PL PREFIX=${PREFIX_DIR}", "text/plain"),
266                ("touch", "touch *.yp", "text/plain"),
267                ("make", "make", "text/plain"),
268                ("test", "make test", "text/plain"),
269                ("install", "make install", "text/plain"),
270                ("checkout-yapp-generated", "git checkout lib/Parse/Pidl/IDL.pm lib/Parse/Pidl/Expr.pm", "text/plain"),
271                ("check-clean-tree", "../script/clean-source-tree.sh", "text/plain"),
272                ("clean", "make clean", "text/plain") ],
273
274     # these are useful for debugging autobuild
275     'pass' : [ ("pass", 'echo passing && /bin/true', "text/plain") ],
276     'fail' : [ ("fail", 'echo failing && /bin/false', "text/plain") ]
277 }
278
279 def do_print(msg):
280     print "%s" % msg
281     sys.stdout.flush()
282     sys.stderr.flush()
283
284 def run_cmd(cmd, dir=".", show=None, output=False, checkfail=True):
285     if show is None:
286         show = options.verbose
287     if show:
288         do_print("Running: '%s' in '%s'" % (cmd, dir))
289     if output:
290         return Popen([cmd], shell=True, stdout=PIPE, cwd=dir).communicate()[0]
291     elif checkfail:
292         return check_call(cmd, shell=True, cwd=dir)
293     else:
294         return call(cmd, shell=True, cwd=dir)
295
296
297 class builder(object):
298     '''handle build of one directory'''
299
300     def __init__(self, name, sequence, cp=True):
301         self.name = name
302         self.dir = builddirs[name]
303
304         self.tag = self.name.replace('/', '_')
305         self.sequence = sequence
306         self.next = 0
307         self.stdout_path = "%s/%s.stdout" % (gitroot, self.tag)
308         self.stderr_path = "%s/%s.stderr" % (gitroot, self.tag)
309         if options.verbose:
310             do_print("stdout for %s in %s" % (self.name, self.stdout_path))
311             do_print("stderr for %s in %s" % (self.name, self.stderr_path))
312         run_cmd("rm -f %s %s" % (self.stdout_path, self.stderr_path))
313         self.stdout = open(self.stdout_path, 'w')
314         self.stderr = open(self.stderr_path, 'w')
315         self.stdin  = open("/dev/null", 'r')
316         self.sdir = "%s/%s" % (testbase, self.tag)
317         self.prefix = "%s/%s" % (test_prefix, self.tag)
318         run_cmd("rm -rf %s" % self.sdir)
319         run_cmd("rm -rf %s" % self.prefix)
320         if cp:
321             run_cmd("cp --recursive --link --archive %s %s" % (test_master, self.sdir), dir=test_master, show=True)
322         else:
323             run_cmd("git clone --recursive --shared %s %s" % (test_master, self.sdir), dir=test_master, show=True)
324         self.start_next()
325
326     def start_next(self):
327         if self.next == len(self.sequence):
328             if not options.nocleanup:
329                 run_cmd("rm -rf %s" % self.sdir)
330                 run_cmd("rm -rf %s" % self.prefix)
331             do_print('%s: Completed OK' % self.name)
332             self.done = True
333             return
334         (self.stage, self.cmd, self.output_mime_type) = self.sequence[self.next]
335         self.cmd = self.cmd.replace("${PYTHON_PREFIX}", get_python_lib(standard_lib=1, prefix=self.prefix))
336         self.cmd = self.cmd.replace("${PREFIX}", "--prefix=%s" % self.prefix)
337         self.cmd = self.cmd.replace("${EXTRA_PYTHON}", "%s" % extra_python)
338         self.cmd = self.cmd.replace("${PREFIX_DIR}", "%s" % self.prefix)
339         self.cmd = self.cmd.replace("${TESTS}", options.restrict_tests)
340 #        if self.output_mime_type == "text/x-subunit":
341 #            self.cmd += " | %s --immediate" % (os.path.join(os.path.dirname(__file__), "selftest/format-subunit"))
342         do_print('%s: [%s] Running %s' % (self.name, self.stage, self.cmd))
343         cwd = os.getcwd()
344         os.chdir("%s/%s" % (self.sdir, self.dir))
345         self.proc = Popen(self.cmd, shell=True,
346                           stdout=self.stdout, stderr=self.stderr, stdin=self.stdin)
347         os.chdir(cwd)
348         self.next += 1
349
350
351 class buildlist(object):
352     '''handle build of multiple directories'''
353
354     def __init__(self, tasknames, rebase_url, rebase_branch="master"):
355         global tasks
356         self.tlist = []
357         self.tail_proc = None
358         self.retry = None
359         if tasknames == []:
360             if options.restrict_tests:
361                 tasknames = ["samba-test-only"]
362             else:
363                 tasknames = defaulttasks
364         else:
365             # If we are only running one test,
366             # do not sleep randomly to wait for it to start
367             os.environ['AUTOBUILD_RANDOM_SLEEP_OVERRIDE'] = '1'
368
369         for n in tasknames:
370             b = builder(n, tasks[n], cp=n is not "pidl")
371             self.tlist.append(b)
372         if options.retry:
373             rebase_remote = "rebaseon"
374             retry_task = [ ("retry",
375                             '''set -e
376                             git remote add -t %s %s %s
377                             git fetch %s
378                             while :; do
379                               sleep 60
380                               git describe %s/%s > old_remote_branch.desc
381                               git fetch %s
382                               git describe %s/%s > remote_branch.desc
383                               diff old_remote_branch.desc remote_branch.desc
384                             done
385                            ''' % (
386                                rebase_branch, rebase_remote, rebase_url,
387                                rebase_remote,
388                                rebase_remote, rebase_branch,
389                                rebase_remote,
390                                rebase_remote, rebase_branch
391                            ),
392                            "test/plain" ) ]
393
394             self.retry = builder('retry', retry_task, cp=False)
395             self.need_retry = False
396
397     def kill_kids(self):
398         if self.tail_proc is not None:
399             self.tail_proc.terminate()
400             self.tail_proc.wait()
401             self.tail_proc = None
402         if self.retry is not None:
403             self.retry.proc.terminate()
404             self.retry.proc.wait()
405             self.retry = None
406         for b in self.tlist:
407             if b.proc is not None:
408                 run_cmd("killbysubdir %s > /dev/null 2>&1" % b.sdir, checkfail=False)
409                 b.proc.terminate()
410                 b.proc.wait()
411                 b.proc = None
412
413     def wait_one(self):
414         while True:
415             none_running = True
416             for b in self.tlist:
417                 if b.proc is None:
418                     continue
419                 none_running = False
420                 b.status = b.proc.poll()
421                 if b.status is None:
422                     continue
423                 b.proc = None
424                 return b
425             if options.retry:
426                 ret = self.retry.proc.poll()
427                 if ret is not None:
428                     self.need_retry = True
429                     self.retry = None
430                     return None
431             if none_running:
432                 return None
433             time.sleep(0.1)
434
435     def run(self):
436         while True:
437             b = self.wait_one()
438             if options.retry and self.need_retry:
439                 self.kill_kids()
440                 do_print("retry needed")
441                 return (0, None, None, None, "retry")
442             if b is None:
443                 break
444             if os.WIFSIGNALED(b.status) or os.WEXITSTATUS(b.status) != 0:
445                 self.kill_kids()
446                 return (b.status, b.name, b.stage, b.tag, "%s: [%s] failed '%s' with status %d" % (b.name, b.stage, b.cmd, b.status))
447             b.start_next()
448         self.kill_kids()
449         return (0, None, None, None, "All OK")
450
451     def write_system_info(self):
452         filename = 'system-info.txt'
453         f = open(filename, 'w')
454         for cmd in ['uname -a', 'free', 'cat /proc/cpuinfo']:
455             print >>f, '### %s' % cmd
456             print >>f, run_cmd(cmd, output=True, checkfail=False)
457             print >>f
458         f.close()
459         return filename
460
461     def tarlogs(self, fname):
462         tar = tarfile.open(fname, "w:gz")
463         for b in self.tlist:
464             tar.add(b.stdout_path, arcname="%s.stdout" % b.tag)
465             tar.add(b.stderr_path, arcname="%s.stderr" % b.tag)
466         if os.path.exists("autobuild.log"):
467             tar.add("autobuild.log")
468         sys_info = self.write_system_info()
469         tar.add(sys_info)
470         tar.close()
471
472     def remove_logs(self):
473         for b in self.tlist:
474             os.unlink(b.stdout_path)
475             os.unlink(b.stderr_path)
476
477     def start_tail(self):
478         cwd = os.getcwd()
479         cmd = "tail -f *.stdout *.stderr"
480         os.chdir(gitroot)
481         self.tail_proc = Popen(cmd, shell=True)
482         os.chdir(cwd)
483
484
485 def cleanup():
486     if options.nocleanup:
487         return
488     run_cmd("stat %s || true" % test_tmpdir, show=True)
489     run_cmd("stat %s" % testbase, show=True)
490     do_print("Cleaning up ....")
491     for d in cleanup_list:
492         run_cmd("rm -rf %s" % d)
493
494
495 def find_git_root():
496     '''get to the top of the git repo'''
497     p=os.getcwd()
498     while p != '/':
499         if os.path.isdir(os.path.join(p, ".git")):
500             return p
501         p = os.path.abspath(os.path.join(p, '..'))
502     return None
503
504
505 def daemonize(logfile):
506     pid = os.fork()
507     if pid == 0: # Parent
508         os.setsid()
509         pid = os.fork()
510         if pid != 0: # Actual daemon
511             os._exit(0)
512     else: # Grandparent
513         os._exit(0)
514
515     import resource      # Resource usage information.
516     maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
517     if maxfd == resource.RLIM_INFINITY:
518         maxfd = 1024 # Rough guess at maximum number of open file descriptors.
519     for fd in range(0, maxfd):
520         try:
521             os.close(fd)
522         except OSError:
523             pass
524     os.open(logfile, os.O_RDWR | os.O_CREAT)
525     os.dup2(0, 1)
526     os.dup2(0, 2)
527
528 def write_pidfile(fname):
529     '''write a pid file, cleanup on exit'''
530     f = open(fname, mode='w')
531     f.write("%u\n" % os.getpid())
532     f.close()
533
534
535 def rebase_tree(rebase_url, rebase_branch = "master"):
536     rebase_remote = "rebaseon"
537     do_print("Rebasing on %s" % rebase_url)
538     run_cmd("git describe HEAD", show=True, dir=test_master)
539     run_cmd("git remote add -t %s %s %s" %
540             (rebase_branch, rebase_remote, rebase_url),
541             show=True, dir=test_master)
542     run_cmd("git fetch %s" % rebase_remote, show=True, dir=test_master)
543     if options.fix_whitespace:
544         run_cmd("git rebase --force-rebase --whitespace=fix %s/%s" %
545                 (rebase_remote, rebase_branch),
546                 show=True, dir=test_master)
547     else:
548         run_cmd("git rebase --force-rebase %s/%s" %
549                 (rebase_remote, rebase_branch),
550                 show=True, dir=test_master)
551     diff = run_cmd("git --no-pager diff HEAD %s/%s" %
552                    (rebase_remote, rebase_branch),
553                    dir=test_master, output=True)
554     if diff == '':
555         do_print("No differences between HEAD and %s/%s - exiting" %
556               (rebase_remote, rebase_branch))
557         sys.exit(0)
558     run_cmd("git describe %s/%s" %
559             (rebase_remote, rebase_branch),
560             show=True, dir=test_master)
561     run_cmd("git describe HEAD", show=True, dir=test_master)
562     run_cmd("git --no-pager diff --stat HEAD %s/%s" %
563             (rebase_remote, rebase_branch),
564             show=True, dir=test_master)
565
566 def push_to(push_url, push_branch = "master"):
567     push_remote = "pushto"
568     do_print("Pushing to %s" % push_url)
569     if options.mark:
570         run_cmd("git config --replace-all core.editor script/commit_mark.sh", dir=test_master)
571         run_cmd("git commit --amend -c HEAD", dir=test_master)
572         # the notes method doesn't work yet, as metze hasn't allowed refs/notes/* in master
573         # run_cmd("EDITOR=script/commit_mark.sh git notes edit HEAD", dir=test_master)
574     run_cmd("git remote add -t %s %s %s" %
575             (push_branch, push_remote, push_url),
576             show=True, dir=test_master)
577     run_cmd("git push %s +HEAD:%s" %
578             (push_remote, push_branch),
579             show=True, dir=test_master)
580
581 def_testbase = os.getenv("AUTOBUILD_TESTBASE", "/memdisk/%s" % os.getenv('USER'))
582
583 gitroot = find_git_root()
584 if gitroot is None:
585     raise Exception("Failed to find git root")
586
587 parser = OptionParser()
588 parser.add_option("", "--tail", help="show output while running", default=False, action="store_true")
589 parser.add_option("", "--keeplogs", help="keep logs", default=False, action="store_true")
590 parser.add_option("", "--nocleanup", help="don't remove test tree", default=False, action="store_true")
591 parser.add_option("", "--testbase", help="base directory to run tests in (default %s)" % def_testbase,
592                   default=def_testbase)
593 parser.add_option("", "--passcmd", help="command to run on success", default=None)
594 parser.add_option("", "--verbose", help="show all commands as they are run",
595                   default=False, action="store_true")
596 parser.add_option("", "--rebase", help="rebase on the given tree before testing",
597                   default=None, type='str')
598 parser.add_option("", "--pushto", help="push to a git url on success",
599                   default=None, type='str')
600 parser.add_option("", "--mark", help="add a Tested-By signoff before pushing",
601                   default=False, action="store_true")
602 parser.add_option("", "--fix-whitespace", help="fix whitespace on rebase",
603                   default=False, action="store_true")
604 parser.add_option("", "--retry", help="automatically retry if master changes",
605                   default=False, action="store_true")
606 parser.add_option("", "--email", help="send email to the given address on failure",
607                   type='str', default=None)
608 parser.add_option("", "--email-from", help="send email from the given address",
609                   type='str', default="autobuild@samba.org")
610 parser.add_option("", "--email-server", help="send email via the given server",
611                   type='str', default='localhost')
612 parser.add_option("", "--always-email", help="always send email, even on success",
613                   action="store_true")
614 parser.add_option("", "--daemon", help="daemonize after initial setup",
615                   action="store_true")
616 parser.add_option("", "--branch", help="the branch to work on (default=master)",
617                   default="master", type='str')
618 parser.add_option("", "--log-base", help="location where the logs can be found (default=cwd)",
619                   default=gitroot, type='str')
620 parser.add_option("", "--attach-logs", help="Attach logs to mails sent on success/failure?",
621                   default=False, action="store_true")
622 parser.add_option("", "--restrict-tests", help="run as make test with this TESTS= regex",
623                   default='')
624
625 def send_email(subject, text, log_tar):
626     outer = MIMEMultipart()
627     outer['Subject'] = subject
628     outer['To'] = options.email
629     outer['From'] = options.email_from
630     outer['Date'] = email.utils.formatdate(localtime = True)
631     outer.preamble = 'Autobuild mails are now in MIME because we optionally attach the logs.\n'
632     outer.attach(MIMEText(text, 'plain'))
633     if options.attach_logs:
634         fp = open(log_tar, 'rb')
635         msg = MIMEApplication(fp.read(), 'gzip', email.encoders.encode_base64)
636         fp.close()
637         # Set the filename parameter
638         msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(log_tar))
639         outer.attach(msg)
640     content = outer.as_string()
641     s = smtplib.SMTP(options.email_server)
642     s.sendmail(options.email_from, [options.email], content)
643     s.set_debuglevel(1)
644     s.quit()
645
646 def email_failure(status, failed_task, failed_stage, failed_tag, errstr,
647                   elapsed_time, log_base=None, add_log_tail=True):
648     '''send an email to options.email about the failure'''
649     elapsed_minutes = elapsed_time / 60.0
650     user = os.getenv("USER")
651     if log_base is None:
652         log_base = gitroot
653     text = '''
654 Dear Developer,
655
656 Your autobuild on %s failed after %.1f minutes
657 when trying to test %s with the following error:
658
659    %s
660
661 the autobuild has been abandoned. Please fix the error and resubmit.
662
663 A summary of the autobuild process is here:
664
665   %s/autobuild.log
666 ''' % (platform.node(), elapsed_minutes, failed_task, errstr, log_base)
667
668     if options.restrict_tests:
669         text += """
670 The build was restricted to tests matching %s\n""" % options.restrict_tests
671
672     if failed_task != 'rebase':
673         text += '''
674 You can see logs of the failed task here:
675
676   %s/%s.stdout
677   %s/%s.stderr
678
679 or you can get full logs of all tasks in this job here:
680
681   %s/logs.tar.gz
682
683 The top commit for the tree that was built was:
684
685 %s
686
687 ''' % (log_base, failed_tag, log_base, failed_tag, log_base, top_commit_msg)
688
689     if add_log_tail:
690         f = open("%s/%s.stdout" % (gitroot, failed_tag), 'r')
691         lines = f.readlines()
692         log_tail = "".join(lines[-50:])
693         num_lines = len(lines)
694         if num_lines < 50:
695             # Also include stderr (compile failures) if < 50 lines of stdout
696             f = open("%s/%s.stderr" % (gitroot, failed_tag), 'r')
697             log_tail += "".join(f.readlines()[-(50-num_lines):])
698
699         text += '''
700 The last 50 lines of log messages:
701
702 %s
703     ''' % log_tail
704         f.close()
705
706     logs = os.path.join(gitroot, 'logs.tar.gz')
707     send_email('autobuild[%s] failure on %s for task %s during %s'
708                % (options.branch, platform.node(), failed_task, failed_stage),
709                text, logs)
710
711 def email_success(elapsed_time, log_base=None):
712     '''send an email to options.email about a successful build'''
713     user = os.getenv("USER")
714     if log_base is None:
715         log_base = gitroot
716     text = '''
717 Dear Developer,
718
719 Your autobuild on %s has succeeded after %.1f minutes.
720
721 ''' % (platform.node(), elapsed_time / 60.)
722
723     if options.restrict_tests:
724         text += """
725 The build was restricted to tests matching %s\n""" % options.restrict_tests
726
727     if options.keeplogs:
728         text += '''
729
730 you can get full logs of all tasks in this job here:
731
732   %s/logs.tar.gz
733
734 ''' % log_base
735
736     text += '''
737 The top commit for the tree that was built was:
738
739 %s
740 ''' % top_commit_msg
741
742     logs = os.path.join(gitroot, 'logs.tar.gz')
743     send_email('autobuild[%s] success on %s' % (options.branch, platform.node()),
744                text, logs)
745
746
747 (options, args) = parser.parse_args()
748
749 if options.retry:
750     if options.rebase is None:
751         raise Exception('You can only use --retry if you also rebase')
752
753 testbase = "%s/b%u" % (options.testbase, os.getpid())
754 test_master = "%s/master" % testbase
755 test_prefix = "%s/prefix" % testbase
756 test_tmpdir = "%s/tmp" % testbase
757 os.environ['TMPDIR'] = test_tmpdir
758
759 # get the top commit message, for emails
760 top_commit_msg = run_cmd("git log -1", dir=gitroot, output=True)
761
762 try:
763     os.makedirs(testbase)
764 except Exception as reason:
765     raise Exception("Unable to create %s : %s" % (testbase, reason))
766 cleanup_list.append(testbase)
767
768 if options.daemon:
769     logfile = os.path.join(testbase, "log")
770     do_print("Forking into the background, writing progress to %s" % logfile)
771     daemonize(logfile)
772
773 write_pidfile(gitroot + "/autobuild.pid")
774
775 start_time = time.time()
776
777 while True:
778     try:
779         run_cmd("rm -rf %s" % test_tmpdir, show=True)
780         os.makedirs(test_tmpdir)
781         # The waf uninstall code removes empty directories all the way
782         # up the tree.  Creating a file in test_tmpdir stops it from
783         # being removed.
784         run_cmd("touch %s" % os.path.join(test_tmpdir,
785                                           ".directory-is-not-empty"), show=True)
786         run_cmd("stat %s" % test_tmpdir, show=True)
787         run_cmd("stat %s" % testbase, show=True)
788         run_cmd("git clone --recursive --shared %s %s" % (gitroot, test_master), show=True, dir=gitroot)
789     except Exception:
790         cleanup()
791         raise
792
793     try:
794         try:
795             if options.rebase is not None:
796                 rebase_tree(options.rebase, rebase_branch=options.branch)
797         except Exception:
798             cleanup_list.append(gitroot + "/autobuild.pid")
799             cleanup()
800             elapsed_time = time.time() - start_time
801             email_failure(-1, 'rebase', 'rebase', 'rebase',
802                           'rebase on %s failed' % options.branch,
803                           elapsed_time, log_base=options.log_base)
804             sys.exit(1)
805         blist = buildlist(args, options.rebase, rebase_branch=options.branch)
806         if options.tail:
807             blist.start_tail()
808         (status, failed_task, failed_stage, failed_tag, errstr) = blist.run()
809         if status != 0 or errstr != "retry":
810             break
811         cleanup()
812     except Exception:
813         cleanup()
814         raise
815
816 cleanup_list.append(gitroot + "/autobuild.pid")
817
818 do_print(errstr)
819
820 blist.kill_kids()
821 if options.tail:
822     do_print("waiting for tail to flush")
823     time.sleep(1)
824
825 elapsed_time = time.time() - start_time
826 if status == 0:
827     if options.passcmd is not None:
828         do_print("Running passcmd: %s" % options.passcmd)
829         run_cmd(options.passcmd, dir=test_master)
830     if options.pushto is not None:
831         push_to(options.pushto, push_branch=options.branch)
832     if options.keeplogs or options.attach_logs:
833         blist.tarlogs("logs.tar.gz")
834         do_print("Logs in logs.tar.gz")
835     if options.always_email:
836         email_success(elapsed_time, log_base=options.log_base)
837     blist.remove_logs()
838     cleanup()
839     do_print(errstr)
840     sys.exit(0)
841
842 # something failed, gather a tar of the logs
843 blist.tarlogs("logs.tar.gz")
844
845 if options.email is not None:
846     email_failure(status, failed_task, failed_stage, failed_tag, errstr,
847                   elapsed_time, log_base=options.log_base)
848 else:
849     elapsed_minutes = elapsed_time / 60.0
850     print '''
851
852 ####################################################################
853
854 AUTOBUILD FAILURE
855
856 Your autobuild[%s] on %s failed after %.1f minutes
857 when trying to test %s with the following error:
858
859    %s
860
861 the autobuild has been abandoned. Please fix the error and resubmit.
862
863 ####################################################################
864
865 ''' % (options.branch, platform.node(), elapsed_minutes, failed_task, errstr)
866
867 cleanup()
868 do_print(errstr)
869 do_print("Logs in logs.tar.gz")
870 sys.exit(status)