1 """
2 CSB build related tools and programs.
3
4 When executed as a program, this module will run the CSB Build Console and
5 build the source tree it belongs to. The source tree is added at the
6 B{beginning} of sys.path to make sure that all subsequent imports from the
7 Test and Doc consoles will import the right thing (think of multiple CSB
8 packages installed on the same server).
9
10 Here is how to build, test and package the whole project::
11
12 $ hg clone https://hg.codeplex.com/csb CSB
13 $ CSB/csb/build.py -o <output directory>
14
15 The Console can also be imported and instantiated as a regular Python class.
16 In this case the Console again builds the source tree it is part of, but
17 sys.path will remain intact. Therefore, the Console will assume that all
18 modules currently in memory, as well as those that can be subsequently imported
19 by the Console itself, belong to the same CSB package.
20
21 @note: The CSB build services no longer support the option to build external
22 source trees.
23 @see: [CSB 0000038]
24 """
25 from __future__ import print_function
26
27 import os
28 import sys
29 import getopt
30 import traceback
31 import compileall
32
33 if os.path.basename(__file__) == '__init__.py':
34 PARENT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
35 else:
36 PARENT = os.path.abspath(os.path.dirname(__file__))
37
38 ROOT = 'csb'
39 SOURCETREE = os.path.abspath(os.path.join(PARENT, ".."))
40
41 if __name__ == '__main__':
42
43
44
45 for path in sys.path:
46 if path.startswith(SOURCETREE):
47 sys.path.remove(path)
48
49 import io
50 assert hasattr(io, 'BufferedIOBase')
51
52 sys.path = [SOURCETREE] + sys.path
53
54
55 """
56 It is now safe to import any modules
57 """
58 import imp
59 import shutil
60 import tarfile
61
62 import csb
63
64 from abc import ABCMeta, abstractmethod
65 from csb.io import Shell
69 """
70 Enumeration of build types.
71 """
72
73 SOURCE = 'source'
74 BINARY = 'binary'
75
76 _du = { SOURCE: 'sdist', BINARY: 'bdist' }
77
78 @staticmethod
80 try:
81 return BuildTypes._du[key]
82 except KeyError:
83 raise ValueError('Unhandled build type: {0}'.format(key))
84
87 """
88 CSB Build Bot. Run with -h for usage.
89
90 @param output: build output directory
91 @type output: str
92 @param verbosity: verbosity level
93 @type verbosity: int
94
95 @note: The build console automatically detects and builds the csb package
96 it belongs to. You cannot build a different source tree with it.
97 See the module documentation for more info.
98 """
99
100 PROGRAM = __file__
101
102 USAGE = r"""
103 CSB Build Console: build, test and package the entire csb project.
104
105 Usage:
106 python {program} -o output [-v verbosity] [-t type] [-h]
107
108 Options:
109 -o output Build output directory
110 -v verbosity Verbosity level, default is 1
111 -t type Build type:
112 source - build source code distribution (default)
113 binary - build executable
114 -h, --help Display this help
115 """
116
118
119 self._input = None
120 self._output = None
121 self._temp = None
122 self._docs = None
123 self._apidocs = None
124 self._root = None
125 self._verbosity = None
126 self._type = buildtype
127 self._dist = BuildTypes.get(buildtype)
128
129 if os.path.join(SOURCETREE, ROOT) != PARENT:
130 raise IOError('{0} must be a sub-package or sub-module of {1}'.format(__file__, ROOT))
131 self._input = SOURCETREE
132
133 self._success = True
134
135 self.output = output
136 self.verbosity = verbosity
137
138 @property
141
142 @property
145 @output.setter
147
148 self._output = os.path.abspath(value)
149 self._temp = os.path.join(self._output, 'build')
150 self._docs = os.path.join(self._temp, 'docs')
151 self._apidocs = os.path.join(self._docs, 'api')
152 self._root = os.path.join(self._temp, ROOT)
153
154 @property
156 return self._verbosity
157 @verbosity.setter
159 self._verbosity = int(value)
160
162 """
163 Run the console.
164 """
165 self.log('\n# Building package {0} from {1}\n'.format(ROOT, SOURCETREE))
166
167 self._init()
168 v = self._revision()
169 self._doc(v)
170 self._test()
171
172 self._compile()
173 vn = self._package()
174
175 if self._success:
176 self.log('\n# Done ({0}).\n'.format(vn.full))
177 return True
178
179 self.log('\n# Build failed.\n')
180 return False
181
182 - def log(self, message, level=1, ending='\n'):
183
184 if self._verbosity >= level:
185 sys.stdout.write(message)
186 sys.stdout.write(ending)
187 sys.stdout.flush()
188
190 """
191 Collect all required stuff in the output folder.
192 """
193 self.log('# Preparing the file system...')
194
195 if not os.path.exists(self._output):
196 self.log('Creating output directory {0}'.format(self._output), level=2)
197 os.mkdir(self._output)
198
199 if os.path.exists(self._temp):
200 self.log('Deleting existing temp directory {0}'.format(self._temp), level=2)
201 shutil.rmtree(self._temp)
202
203 self.log('Copying the source tree to temp directory {0}'.format(self._temp), level=2)
204 shutil.copytree(self._input, self._temp)
205
206 if os.path.exists(self._apidocs):
207 self.log('Deleting existing API docs directory {0}'.format(self._apidocs), level=2)
208 shutil.rmtree(self._apidocs)
209 if not os.path.isdir(self._docs):
210 self.log('Creating docs directory {0}'.format(self._docs), level=2)
211 os.mkdir(self._docs)
212 self.log('Creating API docs directory {0}'.format(self._apidocs), level=2)
213 os.mkdir(self._apidocs)
214
233
263
264 - def _doc(self, version):
265 """
266 Build documentation in the output folder.
267 """
268 self.log('\n# Generating API documentation...')
269 try:
270 import epydoc.cli
271 except ImportError:
272 self.log('\n Skipped: epydoc is missing')
273 return
274
275 self.log('\n# Emulating ARGV for the Doc Builder...', level=2)
276 argv = sys.argv
277 sys.argv = ['epydoc', '--html', '-o', self._apidocs,
278 '--name', '{0} v{1}'.format(ROOT.upper(), version),
279 '--no-private', '--introspect-only', '--exclude', 'csb.test.cases',
280 '--css', os.path.join(self._temp, 'epydoc.css'),
281 '--fail-on-error', '--fail-on-warning', '--fail-on-docstring-warning',
282 self._root]
283
284 if self._verbosity <= 1:
285 sys.argv.append('-q')
286
287 try:
288 epydoc.cli.cli()
289 sys.exit(0)
290 except SystemExit as ex:
291 if ex.code is 0:
292 self.log('\n Passed all doc tests')
293 else:
294 if ex.code == 2:
295 self.log('\n DID NOT PASS: Generated docs might be broken')
296 self._success = False
297 else:
298 self.log('\n FAIL: Epydoc returned "#{0.code}: {0}"'.format(ex))
299 self._success = False
300
301 self.log('\n# Restoring the previous ARGV...', level=2)
302 sys.argv = argv
303
305 """
306 Byte-compile all modules and packages.
307 """
308 self.log('\n# Byte-compiling all *.py files...')
309
310 quiet = self.verbosity <= 1
311 valid = compileall.compile_dir(self._root, quiet=quiet, force=True)
312
313 if not valid:
314 self.log('\n FAIL: Compilation error(s)\n')
315 self._success = False
316
318 """
319 Make package.
320 """
321 self.log('\n# Configuring CWD and ARGV for the Setup...', level=2)
322 cwd = os.curdir
323 os.chdir(self._temp)
324
325 if self._verbosity > 1:
326 verbosity = '-v'
327 else:
328 verbosity = '-q'
329 argv = sys.argv
330 sys.argv = ['setup.py', verbosity, self._dist, '-d', self._output]
331
332 self.log('\n# Building {0} distribution...'.format(self._type))
333 version = package = None
334
335 try:
336 setup = imp.load_source('setupcsb', 'setup.py')
337 d = setup.build()
338 version = setup.VERSION
339 package = d.dist_files[0][2]
340
341 if self._type == BuildTypes.BINARY:
342 self._strip_source(package)
343
344 except SystemExit as ex:
345 if ex.code is not 0:
346 self.log('\n FAIL: Setup returned: \n\n{0}\n'.format(ex))
347 self._success = False
348 package = 'FAIL'
349
350 self.log('\n# Restoring the previous CWD and ARGV...', level=2)
351 os.chdir(cwd)
352 sys.argv = argv
353
354 self.log(' Packaged ' + package)
355 return version
356
358 """
359 Delete plain text source code files from the package.
360 """
361 cwd = os.getcwd()
362
363 try:
364 tmp = os.path.join(self.output, 'tmp')
365 os.mkdir(tmp)
366
367 self.log('\n# Entering {1} in order to delete .py files from {0}...'.format(package, tmp), level=2)
368 os.chdir(tmp)
369
370 oldtar = tarfile.open(package, mode='r:gz')
371 oldtar.extractall(tmp)
372 oldtar.close()
373
374 newtar = tarfile.open(package, mode='w:gz')
375
376 try:
377 for i in os.walk('.'):
378 for fn in i[2]:
379 if fn.endswith('.py'):
380 module = os.path.join(i[0], fn);
381 if not os.path.isfile(module.replace('.py', '.pyc')):
382 raise ValueError('Missing bytecode for module {0}'.format(module))
383 else:
384 os.remove(os.path.join(i[0], fn))
385
386 for i in os.listdir('.'):
387 newtar.add(i)
388 finally:
389 newtar.close()
390
391 finally:
392 self.log('\n# Restoring the previous CWD...', level=2)
393 os.chdir(cwd)
394 if os.path.exists(tmp):
395 shutil.rmtree(tmp)
396
397 @staticmethod
398 - def exit(message=None, code=0, usage=True):
406
407 @staticmethod
408 - def run(argv=None):
409
410 if argv is None:
411 argv = sys.argv[1:]
412
413 output = None
414 verb = 1
415 buildtype = BuildTypes.SOURCE
416
417 try:
418 options, dummy = getopt.getopt(argv, 'o:v:t:h', ['output=', 'verbosity=', 'type=', 'help'])
419
420 for option, value in options:
421 if option in('-h', '--help'):
422 Console.exit(message=None, code=0)
423 if option in('-o', '--output'):
424 if not os.path.isdir(value):
425 Console.exit(message='E: Output directory not found "{0}".'.format(value), code=3)
426 output = value
427 if option in('-v', '--verbosity'):
428 try:
429 verb = int(value)
430 except ValueError:
431 Console.exit(message='E: Verbosity must be an integer.', code=4)
432 if option in('-t', '--type'):
433 if value not in [BuildTypes.SOURCE, BuildTypes.BINARY]:
434 Console.exit(message='E: Invalid build type "{0}".'.format(value), code=5)
435 buildtype = value
436 except getopt.GetoptError as oe:
437 Console.exit(message='E: ' + str(oe), code=1)
438
439 if not output:
440 Console.exit(code=1, usage=True)
441 else:
442 try:
443 ok = Console(output, verbosity=verb, buildtype=buildtype).build()
444 Console.exit(code=0 if ok else 66, usage=False)
445 except Exception as ex:
446 msg = 'Unexpected Error: {0}\n\n{1}'.format(ex, traceback.format_exc())
447 Console.exit(message=msg, code=99, usage=False)
448
457
459 """
460 Determines the current repository revision number of a working copy.
461
462 @param path: a local checkout path to be examined
463 @type path: str
464 @param sc: name of the source control program
465 @type sc: str
466 """
467
469
470 self._path = None
471 self._sc = None
472
473 if os.path.exists(path):
474 self._path = path
475 else:
476 raise IOError('Path not found: {0}'.format(path))
477 if Shell.run([sc, 'help']).code is 0:
478 self._sc = sc
479 else:
480 raise RevisionError('Source control binary probe failed', None, None)
481
482 @property
485
486 @property
489
490 @abstractmethod
492 """
493 Return the current revision information.
494 @rtype: L{RevisionInfo}
495 """
496 pass
497
498 - def write(self, revision, sourcefile):
499 """
500 Finalize the __version__ = major.minor.micro.{revision} tag.
501 Overwrite C{sourcefile} in place by substituting the {revision} macro.
502
503 @param revision: revision number to write to the source file.
504 @type revision: int
505 @param sourcefile: python source file with a __version__ tag, typically
506 "csb/__init__.py"
507 @type sourcefile: str
508
509 @return: sourcefile.__version__
510 """
511 content = open(sourcefile).readlines()
512
513 with open(sourcefile, 'w') as src:
514 for line in content:
515 if line.startswith('__version__'):
516 src.write(line.format(revision=revision))
517 else:
518 src.write(line)
519
520 self._delcache(sourcefile)
521 return imp.load_source('____source', sourcefile).__version__
522
523 - def _run(self, cmd):
524
525 si = Shell.run(cmd)
526 if si.code > 0:
527 raise RevisionError('SC failed ({0.code}): {0.stderr}'.format(si), si.code, si.cmd)
528
529 return si.stdout.splitlines()
530
532
533 compiled = os.path.splitext(sourcefile)[0] + '.pyc'
534 if os.path.isfile(compiled):
535 os.remove(compiled)
536
537 pycache = os.path.join(os.path.dirname(compiled), '__pycache__')
538 if os.path.isdir(pycache):
539 shutil.rmtree(pycache)
540
545
547
548 cmd = '{0.sc} info {0.path} -R'.format(self)
549 maxrevision = None
550
551 for line in self._run(cmd):
552 if line.startswith('Revision:'):
553 rev = int(line[9:] .strip())
554 if rev > maxrevision:
555 maxrevision = rev
556
557 if maxrevision is None:
558 raise RevisionError('No revision number found', code=0, cmd=cmd)
559
560 return RevisionInfo(self.path, maxrevision)
561
563
570
572
573 wd = os.getcwd()
574 os.chdir(self.path)
575
576 try:
577 cmd = '{0.sc} log -r tip'.format(self)
578
579 revision = None
580 changeset = ''
581
582 for line in self._run(cmd):
583 if line.startswith('changeset:'):
584 items = line[10:].split(':')
585 revision = int(items[0])
586 changeset = items[1].strip()
587 break
588
589 if revision is None:
590 raise RevisionError('No revision number found', code=0, cmd=cmd)
591
592 return RevisionInfo(self.path, revision, changeset)
593
594 finally:
595 os.chdir(wd)
596
599
606
628
630
631 - def __init__(self, item, revision, id=None):
636
640
641
642 if __name__ == '__main__':
643
644 main()
645