Package csb :: Module build |
[frames] | no frames]

# Source Code for Module csb.build

      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      # make sure "import io" imports the built in module, not csb.io
     44      # io is required by tarfile
     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
     66
     67
     68 -class BuildTypes(object):
     69      """
     70      Enumeration of build types.
     71      """
     72
     73      SOURCE = 'source'
     74      BINARY = 'binary'
     75
     76      _du = { SOURCE: 'sdist', BINARY: 'bdist' }
     77
     78      @staticmethod
     79 -    def get(key):
     80          try:
     81              return BuildTypes._du[key]
     82          except KeyError:
     83              raise ValueError('Unhandled build type: {0}'.format(key))
     84
     85
     86 -class Console(object):
     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
    117 -    def __init__(self, output='.', verbosity=1, buildtype=BuildTypes.SOURCE):
    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
    139 -    def input(self):
    140          return self._input
    141
    142      @property
    143 -    def output(self):
    144          return self._output
    145      @output.setter
    146 -    def output(self, value):
    147          #value = os.path.dirname(value)
    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
    155 -    def verbosity(self):
    156          return self._verbosity
    157      @verbosity.setter
    158 -    def verbosity(self, value):
    159          self._verbosity = int(value)
    160
    161 -    def build(self):
    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
    189 -    def _init(self):
    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
    215 -    def _revision(self):
    216          """
    217          Write the actual revision number to L{ROOT}.__version__
    218          """
    219          self.log('\n# Setting the most recent Revision Number...')
    220          root = os.path.join(self._root, '__init__.py')
    221
    222          self.log('Retrieving revision number from {0}'.format(root), level=2)
    223          rh = GitHandler(root)
    224          revision = rh.read().revision
    225
    226          self.log('Writing back revision number {0}'.format(revision), level=2)
    227          version = rh.write(revision, root)
    228
    229          self.log('  This is {0}.__version__ {1}'.format(ROOT, version), level=1)
    230          csb.__version__ = version
    231
    232          return version
    233
    234 -    def _test(self):
    235          """
    236          Run tests. Also make sure the current environment loads all modules from
    237          the input folder.
    238          """
    239          import csb.test
    240          assert csb.test.__file__.startswith(self._input), 'csb.test not loaded from the input!'     #@UndefinedVariable
    241
    242          from csb.test import unittest
    243
    244          newdata = os.path.join(self._temp, ROOT, 'test', 'data')
    245          csb.test.Config.setDefaultDataRoot(newdata)
    246          csb.test.Config.setDefaultGeneratedDataRoot(newdata)
    247
    248          self.log('\n# Updating all test pickles in {0} if necessary...'.format(newdata), level=2)
    249          csb.test.Config().ensureDataConsistency()
    250
    251          self.log('\n# Running the Test Console...')
    252
    253          builder = csb.test.AnyTestBuilder()
    254          suite = builder.loadTests(ROOT + '.test.cases.*')
    255
    256          runner = unittest.TextTestRunner(stream=sys.stderr, verbosity=self.verbosity)
    257          result = runner.run(suite)
    258          if result.wasSuccessful():
    259              self.log('\n  Passed all unit tests')
    260          else:
    261              self.log('\n  DID NOT PASS: This build might be broken')
    262              self._success = False
    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
    304 -    def _compile(self):
    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
    317 -    def _package(self):
    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
    357 -    def _strip_source(self, package, source='*.py'):
    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):
    399
    400          if message:
    401              print(message)
    402          if usage:
    403              print(Console.USAGE.format(program=Console.PROGRAM))
    404
    405          sys.exit(code)
    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
    449
    450 -class RevisionError(RuntimeError):
    451
    452 -    def __init__(self, msg, code, cmd):
    453
    454          super(RevisionError, self).__init__(msg)
    455          self.code = code
    456          self.cmd = cmd
    457
    458 -class RevisionHandler(object):
    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
    468 -    def __init__(self, path, sc):
    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
    483 -    def path(self):
    484          return self._path
    485
    486      @property
    487 -    def sc(self):
    488          return self._sc
    489
    490      @abstractmethod
    491 -    def read(self):
    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
    531 -    def _delcache(self, sourcefile):
    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
    541 -class SubversionHandler(RevisionHandler):
    542
    543 -    def __init__(self, path, sc='svn'):
    544          super(SubversionHandler, self).__init__(path, sc)
    545
    546 -    def read(self):
    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
    562 -class MercurialHandler(RevisionHandler):
    563
    564 -    def __init__(self, path, sc='hg'):
    565
    566          if os.path.isfile(path):
    567              path = os.path.dirname(path)
    568
    569          super(MercurialHandler, self).__init__(path, sc)
    570
    571 -    def read(self):
    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
    597
    598 -class GitHandler(RevisionHandler):
    599
    600 -    def __init__(self, path, sc='git'):
    601
    602          if os.path.isfile(path):
    603              path = os.path.dirname(path)
    604
    605          super(GitHandler, self).__init__(path, sc)
    606
    607 -    def read(self):
    608
    609          wd = os.getcwd()
    610          os.chdir(self.path)
    611
    612          try:
    613              cmd = '{0.sc} rev-parse --short HEAD'.format(self)
    614              revision = None
    615
    616              for line in self._run(cmd):
    617                  if line.strip():
    618                      revision = line.strip()
    619                      break
    620
    621              if revision is None:
    622                  raise RevisionError('No revision number found', code=0, cmd=cmd)
    623
    624              return RevisionInfo(self.path, revision, revision)
    625
    626          finally:
    627              os.chdir(wd)
    628
    629 -class RevisionInfo(object):
    630
    631 -    def __init__(self, item, revision, id=None):
    632
    633          self.item = item
    634          self.revision = revision
    635          self.id = id
    636
    637
    638 -def main():
    639      Console.run()
    640
    641
    642  if __name__ == '__main__':
    643
    644      main()
    645