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