pavement.py
changeset 2457 eebf0c4ace3d
child 2599 22e1ec7bd02a
equal deleted inserted replaced
2456:f9119180c294 2457:eebf0c4ace3d
       
     1 # -*- coding: utf-8 -*-
       
     2 
       
     3 """
       
     4 Example Usage
       
     5 =============
       
     6 
       
     7 The following commands can be run from the root directory of the Mercurial
       
     8 repo. To run ``paver``, however, you'll need to do ``easy_install Paver``.
       
     9 Most of the following commands accept other arguments; see ``command --help``
       
    10 for more information, or ``paver help`` for a list of all the valid commands.
       
    11 
       
    12     ``paver build``
       
    13         Builds the project. This essentially just runs a bunch of other tasks,
       
    14         like ``pylint``, ``django_zip`` and ``tinymce_zip``, etc.
       
    15     ``paver pylint``
       
    16         Runs PyLint on the project.
       
    17     ``paver django_zip``
       
    18         Builds the Django zip file.
       
    19     ``paver tinymce_zip``
       
    20         Builds the TinyMCE zip file.
       
    21 
       
    22 If you specify ``--dry-run`` before a task, then the action of that task will
       
    23 not actually be carried out, although logging output will be displayed as if
       
    24 it were. For example, you could run ``paver --dry-run django_zip`` to see what
       
    25 files would be added to the ``django.zip`` file, etc.
       
    26 """
       
    27 
       
    28 from cStringIO import StringIO
       
    29 import sys
       
    30 import zipfile
       
    31 
       
    32 import paver
       
    33 import paver.easy
       
    34 import paver.tasks
       
    35 from paver.easy import *
       
    36 from paver.path import path
       
    37 
       
    38 
       
    39 # Paver comes with Jason Orendorff's 'path' module; this makes path
       
    40 # manipulation easy and far more readable.
       
    41 PROJECT_DIR = path(__file__).dirname()
       
    42 
       
    43 
       
    44 # Set some default options. Having the options at the top of the file cleans
       
    45 # the whole thing up and makes the behaviour a lot more configurable.
       
    46 options(
       
    47     build = Bunch(
       
    48         project_dir = PROJECT_DIR,
       
    49         app_build = PROJECT_DIR / 'build',
       
    50         app_folder = PROJECT_DIR / 'app',
       
    51         app_files = ['app.yaml', 'cron.yaml', 'index.yaml', 'main.py',
       
    52                      'settings.py', 'shell.py', 'urls.py', 'gae_django.py'],
       
    53         app_dirs =  ['soc', 'ghop', 'gsoc', 'feedparser', 'python25src',
       
    54                      'reflistprop', 'jquery', 'ranklist', 'shell', 'json',
       
    55                      'htmlsanitizer', 'taggable-mixin', 'gviz'],
       
    56         zip_files = ['tiny_mce.zip'],
       
    57         skip_pylint = False,
       
    58     )
       
    59 )
       
    60 
       
    61 # The second call to options allows us to re-use some of the constants defined
       
    62 # in the first call.
       
    63 options(
       
    64     clean_build = options.build,
       
    65     tinymce_zip = options.build,
       
    66     
       
    67     django_zip = Bunch(
       
    68         prune_dirs = ['.svn', 'gis', 'admin', 'localflavor', 'mysql',
       
    69                       'mysql_old', 'oracle', 'postgresql',
       
    70                       'postgresql_psycopg2', 'sqlite3', 'test'],
       
    71         **options.build
       
    72     ),
       
    73     
       
    74     pylint = Bunch(
       
    75         check_modules = ['soc', 'reflistprop', 'settings.py', 'urls.py',
       
    76                          'main.py'],
       
    77         quiet = False,
       
    78         quiet_args = ['--disable-msg=W0511,R0401', '--reports=no',
       
    79                       '--disable-checker=similarities'],
       
    80         pylint_args = [],
       
    81         ignore = False,
       
    82         **options.build
       
    83     )
       
    84 )
       
    85 
       
    86 
       
    87 # Utility functions
       
    88 
       
    89 def django_zip_files(django_src_dir):
       
    90     """Yields each filename which should go into ``django.zip``."""
       
    91     for filename in django_src_dir.walkfiles():
       
    92         # The following seems unnecessarily unreadable, but unfortunately
       
    93         # it seems it cannot be unobfuscated any more (if someone finds a
       
    94         # nicer way of writing it, please tell me).
       
    95         if not (filename.ext in ['.pyc', '.pyo', '.po', '.mo'] or
       
    96                 any(name in filename.splitall()
       
    97                     for name in options.django_zip.prune_dirs)):
       
    98             # The filename is suitable to be added to the archive. In this
       
    99             # case, we yield the filename and the name it should be given in
       
   100             # the Zip archive.
       
   101             paver.tasks.environment.info(
       
   102                 '%-4sdjango.zip <- %s', '', filename)
       
   103             arcname = path('django') / django_src_dir.relpathto(filename)
       
   104             yield filename, arcname
       
   105 
       
   106 
       
   107 def tinymce_zip_files(tiny_mce_dir):
       
   108     """Yields each filename which should go into ``tiny_mce.zip``."""
       
   109     for filename in tiny_mce_dir.walkfiles():
       
   110         if '.svn' not in filename.splitall():
       
   111             # In this case, `tiny_mce/*` is in the root of the zip file, so
       
   112             # we do not need to prefix `arcname` with 'tinymce/' (like we did
       
   113             # with `django.zip`).
       
   114             paver.tasks.environment.info(
       
   115                 '%-4stiny_mce.zip <- %s', '', filename)
       
   116             arcname = tiny_mce_dir.relpathto(filename)
       
   117             yield filename, arcname
       
   118 
       
   119 
       
   120 def write_zip_file(zip_file_handle, files):
       
   121     if paver.tasks.environment.dry_run:
       
   122         for args in files:
       
   123             pass
       
   124         return
       
   125     zip_file = zipfile.ZipFile(zip_file_handle, mode='w')
       
   126     for args in files:
       
   127         zip_file.write(*args)
       
   128     zip_file.close()
       
   129 
       
   130 
       
   131 def symlink(target, link_name):
       
   132     if hasattr(target, 'symlink'):
       
   133         target.symlink(link_name)
       
   134     else:
       
   135         # If we are on a platform where symlinks are not supported (such as
       
   136         # Windows), simply copy the files across.
       
   137         target.copy(link_name)
       
   138 
       
   139 
       
   140 # Tasks
       
   141 
       
   142 
       
   143 @task
       
   144 @cmdopts([
       
   145     ('app-folder=', 'a', 'App folder directory (default /app)'),
       
   146     ('pylint-command=', 'c', 'Specify a custom pylint executable'),
       
   147     ('quiet', 'q', 'Disables a lot of the pylint output'),
       
   148     ('ignore', 'i', 'Ignore PyLint errors')
       
   149 ])
       
   150 def pylint(options):
       
   151     """Check the source code using PyLint."""
       
   152     from pylint import lint
       
   153     
       
   154     # Initial command.
       
   155     arguments = []
       
   156     
       
   157     if options.quiet:
       
   158         arguments.extend(options.quiet_args)
       
   159     if 'pylint_args' in options:
       
   160         arguments.extend(list(options.pylint_args))
       
   161     
       
   162     # Add the list of paths containing the modules to check using PyLint.
       
   163     arguments.extend(
       
   164         str(options.app_folder / module) for module in options.check_modules)
       
   165     
       
   166     # By placing run_pylint into its own function, it allows us to do dry runs
       
   167     # without actually running PyLint.
       
   168     def run_pylint():
       
   169         # Add app folder to path.
       
   170         sys.path.insert(0, options.app_folder.abspath())
       
   171         # Add google_appengine directory to path.
       
   172         sys.path.insert(0,
       
   173             options.project_dir.abspath() /
       
   174                 'thirdparty' / 'google_appengine')
       
   175         
       
   176         # Specify PyLint RC file.
       
   177         arguments.append('--rcfile=' +
       
   178             options.project_dir.abspath() /
       
   179                 'scripts' / 'pylint' / 'pylintrc')
       
   180         
       
   181         # `lint.Run.__init__` runs the PyLint command.
       
   182         try:
       
   183             lint.Run(arguments)
       
   184         # PyLint will `sys.exit()` when it has finished, so we need to catch
       
   185         # the exception and process it accordingly.
       
   186         except SystemExit, exc:
       
   187             return_code = exc.args[0]
       
   188             if return_code != 0 and (not options.pylint.ignore):
       
   189                 raise paver.tasks.BuildFailure(
       
   190                     'PyLint finished with a non-zero exit code')
       
   191     
       
   192     return dry('pylint ' + ' '.join(arguments), run_pylint)
       
   193 
       
   194 
       
   195 @task
       
   196 @cmdopts([
       
   197     ('app-build=', 'b', 'App build directory (default /build)'),
       
   198     ('app-folder=', 'a', 'App folder directory (default /app)'),
       
   199     ('skip-pylint', 's', 'Skip PyLint checker'),
       
   200     ('ignore-pylint', 'i', 'Ignore results of PyLint (but run it anyway)'),
       
   201     ('quiet-pylint', 'q', 'Make PyLint run quietly'),
       
   202 ])
       
   203 def build(options):
       
   204     """Build the project."""
       
   205     # If `--skip-pylint` is not provided, run PyLint.
       
   206     if not options.skip_pylint:
       
   207         # If `--ignore-pylint` is provided, act as if `paver pylint --ignore`
       
   208         # was run. Likewise for `--quiet-pylint`.
       
   209         if options.get('ignore_pylint', False):
       
   210             options.pylint.ignore = True
       
   211         if options.get('quiet_pylint', False):
       
   212             options.pylint.quiet = True
       
   213         pylint(options)
       
   214     
       
   215     # Clean old generated zip files from the app folder.
       
   216     clean_zip(options)
       
   217     
       
   218     # Clean the App build directory by removing and re-creating it.
       
   219     clean_build(options)
       
   220     
       
   221     # Build the django.zip file.
       
   222     django_zip(options)
       
   223     
       
   224     # Build the tiny_mce.zip file.
       
   225     tinymce_zip(options)
       
   226     
       
   227     # Make the necessary symlinks between the app and build directories.
       
   228     build_symlinks(options)
       
   229 
       
   230 
       
   231 @task
       
   232 @cmdopts([
       
   233     ('app-build=', 'b', 'App build directory (default /build)'),
       
   234     ('app-folder=', 'a', 'App folder directory (default /app)'),
       
   235 ])
       
   236 def build_symlinks(options):
       
   237     """Build symlinks between the app and build folders."""
       
   238     # Create the symbolic links from the app folder to the build folder.
       
   239     for filename in options.app_files + options.app_dirs + options.zip_files:
       
   240         # The `symlink()` function handles discrepancies between platforms.
       
   241         target = path(options.app_folder) / filename
       
   242         link = path(options.app_build) / filename
       
   243         dry(
       
   244             '%-4s%-20s <- %s' % ('', target, link),
       
   245             lambda: symlink(target, link))
       
   246 
       
   247 
       
   248 @task
       
   249 @cmdopts([
       
   250     ('app-build=', 'b', 'App build directory (default /build)'),
       
   251 ])
       
   252 def clean_build(options):
       
   253     """Clean the build folder."""
       
   254     # Not checking this could cause an error when trying to remove a
       
   255     # non-existent file.
       
   256     if path(options.app_build).exists():
       
   257         path(options.app_build).rmtree()
       
   258     path(options.app_build).makedirs()
       
   259 
       
   260 
       
   261 @task
       
   262 @cmdopts([
       
   263     ('app-folder=', 'a', 'App folder directory (default /app)'),
       
   264 ])
       
   265 def clean_zip(options):
       
   266     """Remove all the generated zip files from the app folder."""
       
   267     for zip_file in options.zip_files + ['django.zip']:
       
   268         zip_path = path(options.app_folder) / zip_file
       
   269         if zip_path.exists():
       
   270             zip_path.remove()
       
   271 
       
   272 
       
   273 @task
       
   274 @cmdopts([
       
   275     ('app-build=', 'b', 'App build directory (default /build)'),
       
   276     ('app-folder=', 'a', 'App folder directory (default /app)'),
       
   277 ])
       
   278 def django_zip(options):
       
   279     """Create the zip file containing Django (minus unwanted stuff)."""
       
   280     # Write the `django.zip` file. This requires finding all of the necessary
       
   281     # files and writing them to a `zipfile.ZipFile` instance. Python's
       
   282     # stdlib `zipfile` module is written in C, so it's fast and doesn't incur
       
   283     # much overhead.
       
   284     django_src_dir = path(options.app_folder) / 'django'
       
   285     django_zip_filename = path(options.app_build) / 'django.zip'
       
   286     if paver.tasks.environment.dry_run:
       
   287         django_zip_fp = StringIO()
       
   288     else:
       
   289         # Ensure the parent directories exist.
       
   290         django_zip_filename.dirname().makedirs()
       
   291         django_zip_fp = open(django_zip_filename, mode='w')
       
   292     
       
   293     # Write the zip file to disk; this uses the `write_zip_file()` function
       
   294     # defined above. The try/except/finally makes sure the `django.zip` file
       
   295     # handle is properly closed no matter what happens.
       
   296     try:
       
   297         write_zip_file(django_zip_fp, django_zip_files(django_src_dir))
       
   298     except Exception, exc:
       
   299         # Close and delete the (possibly corrupted) `django.zip` file.
       
   300         django_zip_fp.close()
       
   301         django_zip_filename.remove()
       
   302         # Raise the error, causing Paver to exit with a non-zero exit code.
       
   303         raise paver.tasks.BuildFailure(
       
   304             'Error occurred creating django.zip: %r' % (exc,))
       
   305     finally:
       
   306         # Close the file handle if it isn't already.
       
   307         if not django_zip_fp.closed:
       
   308             django_zip_fp.close()
       
   309 
       
   310 
       
   311 @task
       
   312 @cmdopts([
       
   313     ('app-folder=', 'a', 'App folder directory (default /app)'),
       
   314 ])
       
   315 def tinymce_zip(options):
       
   316     """Create the zip file containing TinyMCE."""
       
   317     # This is very similar to django_zip; see the comments there for
       
   318     # explanations.
       
   319     tinymce_dir = path(options.app_folder) / 'tiny_mce'
       
   320     tinymce_zip_filename = path(options.app_folder) / 'tiny_mce.zip'
       
   321     if paver.tasks.environment.dry_run:
       
   322         tinymce_zip_fp = StringIO()
       
   323     else:
       
   324         # Ensure the parent directories exist.
       
   325         tinymce_zip_filename.dirname().makedirs()
       
   326         tinymce_zip_fp = open(tinymce_zip_filename, mode='w')
       
   327     
       
   328     try:
       
   329         write_zip_file(tinymce_zip_fp, tinymce_zip_files(tinymce_dir))
       
   330     except Exception, exc:
       
   331         tinymce_zip_fp.close()
       
   332         tinymce_zip_filename.remove()
       
   333         raise paver.tasks.BuildFailure(
       
   334             'Error occurred creating tinymce.zip: %r' % (exc,))
       
   335     finally:
       
   336         if not tinymce_zip_fp.closed:
       
   337             tinymce_zip_fp.close()