Add paver script that will replace our current build.sh from scripts folder.
authorZachary Voase <disturbyte@googlemail.com>
Tue, 30 Jun 2009 10:59:05 +0200
changeset 2457 eebf0c4ace3d
parent 2456 f9119180c294
child 2458 467ec0cf3ece
Add paver script that will replace our current build.sh from scripts folder. Until I update the wiki to describe how this new paver build script works I'm leaving the old one in the repository. Reviewed by: Pawel Solyga
pavement.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pavement.py	Tue Jun 30 10:59:05 2009 +0200
@@ -0,0 +1,337 @@
+# -*- coding: utf-8 -*-
+
+"""
+Example Usage
+=============
+
+The following commands can be run from the root directory of the Mercurial
+repo. To run ``paver``, however, you'll need to do ``easy_install Paver``.
+Most of the following commands accept other arguments; see ``command --help``
+for more information, or ``paver help`` for a list of all the valid commands.
+
+    ``paver build``
+        Builds the project. This essentially just runs a bunch of other tasks,
+        like ``pylint``, ``django_zip`` and ``tinymce_zip``, etc.
+    ``paver pylint``
+        Runs PyLint on the project.
+    ``paver django_zip``
+        Builds the Django zip file.
+    ``paver tinymce_zip``
+        Builds the TinyMCE zip file.
+
+If you specify ``--dry-run`` before a task, then the action of that task will
+not actually be carried out, although logging output will be displayed as if
+it were. For example, you could run ``paver --dry-run django_zip`` to see what
+files would be added to the ``django.zip`` file, etc.
+"""
+
+from cStringIO import StringIO
+import sys
+import zipfile
+
+import paver
+import paver.easy
+import paver.tasks
+from paver.easy import *
+from paver.path import path
+
+
+# Paver comes with Jason Orendorff's 'path' module; this makes path
+# manipulation easy and far more readable.
+PROJECT_DIR = path(__file__).dirname()
+
+
+# Set some default options. Having the options at the top of the file cleans
+# the whole thing up and makes the behaviour a lot more configurable.
+options(
+    build = Bunch(
+        project_dir = PROJECT_DIR,
+        app_build = PROJECT_DIR / 'build',
+        app_folder = PROJECT_DIR / 'app',
+        app_files = ['app.yaml', 'cron.yaml', 'index.yaml', 'main.py',
+                     'settings.py', 'shell.py', 'urls.py', 'gae_django.py'],
+        app_dirs =  ['soc', 'ghop', 'gsoc', 'feedparser', 'python25src',
+                     'reflistprop', 'jquery', 'ranklist', 'shell', 'json',
+                     'htmlsanitizer', 'taggable-mixin', 'gviz'],
+        zip_files = ['tiny_mce.zip'],
+        skip_pylint = False,
+    )
+)
+
+# The second call to options allows us to re-use some of the constants defined
+# in the first call.
+options(
+    clean_build = options.build,
+    tinymce_zip = options.build,
+    
+    django_zip = Bunch(
+        prune_dirs = ['.svn', 'gis', 'admin', 'localflavor', 'mysql',
+                      'mysql_old', 'oracle', 'postgresql',
+                      'postgresql_psycopg2', 'sqlite3', 'test'],
+        **options.build
+    ),
+    
+    pylint = Bunch(
+        check_modules = ['soc', 'reflistprop', 'settings.py', 'urls.py',
+                         'main.py'],
+        quiet = False,
+        quiet_args = ['--disable-msg=W0511,R0401', '--reports=no',
+                      '--disable-checker=similarities'],
+        pylint_args = [],
+        ignore = False,
+        **options.build
+    )
+)
+
+
+# Utility functions
+
+def django_zip_files(django_src_dir):
+    """Yields each filename which should go into ``django.zip``."""
+    for filename in django_src_dir.walkfiles():
+        # The following seems unnecessarily unreadable, but unfortunately
+        # it seems it cannot be unobfuscated any more (if someone finds a
+        # nicer way of writing it, please tell me).
+        if not (filename.ext in ['.pyc', '.pyo', '.po', '.mo'] or
+                any(name in filename.splitall()
+                    for name in options.django_zip.prune_dirs)):
+            # The filename is suitable to be added to the archive. In this
+            # case, we yield the filename and the name it should be given in
+            # the Zip archive.
+            paver.tasks.environment.info(
+                '%-4sdjango.zip <- %s', '', filename)
+            arcname = path('django') / django_src_dir.relpathto(filename)
+            yield filename, arcname
+
+
+def tinymce_zip_files(tiny_mce_dir):
+    """Yields each filename which should go into ``tiny_mce.zip``."""
+    for filename in tiny_mce_dir.walkfiles():
+        if '.svn' not in filename.splitall():
+            # In this case, `tiny_mce/*` is in the root of the zip file, so
+            # we do not need to prefix `arcname` with 'tinymce/' (like we did
+            # with `django.zip`).
+            paver.tasks.environment.info(
+                '%-4stiny_mce.zip <- %s', '', filename)
+            arcname = tiny_mce_dir.relpathto(filename)
+            yield filename, arcname
+
+
+def write_zip_file(zip_file_handle, files):
+    if paver.tasks.environment.dry_run:
+        for args in files:
+            pass
+        return
+    zip_file = zipfile.ZipFile(zip_file_handle, mode='w')
+    for args in files:
+        zip_file.write(*args)
+    zip_file.close()
+
+
+def symlink(target, link_name):
+    if hasattr(target, 'symlink'):
+        target.symlink(link_name)
+    else:
+        # If we are on a platform where symlinks are not supported (such as
+        # Windows), simply copy the files across.
+        target.copy(link_name)
+
+
+# Tasks
+
+
+@task
+@cmdopts([
+    ('app-folder=', 'a', 'App folder directory (default /app)'),
+    ('pylint-command=', 'c', 'Specify a custom pylint executable'),
+    ('quiet', 'q', 'Disables a lot of the pylint output'),
+    ('ignore', 'i', 'Ignore PyLint errors')
+])
+def pylint(options):
+    """Check the source code using PyLint."""
+    from pylint import lint
+    
+    # Initial command.
+    arguments = []
+    
+    if options.quiet:
+        arguments.extend(options.quiet_args)
+    if 'pylint_args' in options:
+        arguments.extend(list(options.pylint_args))
+    
+    # Add the list of paths containing the modules to check using PyLint.
+    arguments.extend(
+        str(options.app_folder / module) for module in options.check_modules)
+    
+    # By placing run_pylint into its own function, it allows us to do dry runs
+    # without actually running PyLint.
+    def run_pylint():
+        # Add app folder to path.
+        sys.path.insert(0, options.app_folder.abspath())
+        # Add google_appengine directory to path.
+        sys.path.insert(0,
+            options.project_dir.abspath() /
+                'thirdparty' / 'google_appengine')
+        
+        # Specify PyLint RC file.
+        arguments.append('--rcfile=' +
+            options.project_dir.abspath() /
+                'scripts' / 'pylint' / 'pylintrc')
+        
+        # `lint.Run.__init__` runs the PyLint command.
+        try:
+            lint.Run(arguments)
+        # PyLint will `sys.exit()` when it has finished, so we need to catch
+        # the exception and process it accordingly.
+        except SystemExit, exc:
+            return_code = exc.args[0]
+            if return_code != 0 and (not options.pylint.ignore):
+                raise paver.tasks.BuildFailure(
+                    'PyLint finished with a non-zero exit code')
+    
+    return dry('pylint ' + ' '.join(arguments), run_pylint)
+
+
+@task
+@cmdopts([
+    ('app-build=', 'b', 'App build directory (default /build)'),
+    ('app-folder=', 'a', 'App folder directory (default /app)'),
+    ('skip-pylint', 's', 'Skip PyLint checker'),
+    ('ignore-pylint', 'i', 'Ignore results of PyLint (but run it anyway)'),
+    ('quiet-pylint', 'q', 'Make PyLint run quietly'),
+])
+def build(options):
+    """Build the project."""
+    # If `--skip-pylint` is not provided, run PyLint.
+    if not options.skip_pylint:
+        # If `--ignore-pylint` is provided, act as if `paver pylint --ignore`
+        # was run. Likewise for `--quiet-pylint`.
+        if options.get('ignore_pylint', False):
+            options.pylint.ignore = True
+        if options.get('quiet_pylint', False):
+            options.pylint.quiet = True
+        pylint(options)
+    
+    # Clean old generated zip files from the app folder.
+    clean_zip(options)
+    
+    # Clean the App build directory by removing and re-creating it.
+    clean_build(options)
+    
+    # Build the django.zip file.
+    django_zip(options)
+    
+    # Build the tiny_mce.zip file.
+    tinymce_zip(options)
+    
+    # Make the necessary symlinks between the app and build directories.
+    build_symlinks(options)
+
+
+@task
+@cmdopts([
+    ('app-build=', 'b', 'App build directory (default /build)'),
+    ('app-folder=', 'a', 'App folder directory (default /app)'),
+])
+def build_symlinks(options):
+    """Build symlinks between the app and build folders."""
+    # Create the symbolic links from the app folder to the build folder.
+    for filename in options.app_files + options.app_dirs + options.zip_files:
+        # The `symlink()` function handles discrepancies between platforms.
+        target = path(options.app_folder) / filename
+        link = path(options.app_build) / filename
+        dry(
+            '%-4s%-20s <- %s' % ('', target, link),
+            lambda: symlink(target, link))
+
+
+@task
+@cmdopts([
+    ('app-build=', 'b', 'App build directory (default /build)'),
+])
+def clean_build(options):
+    """Clean the build folder."""
+    # Not checking this could cause an error when trying to remove a
+    # non-existent file.
+    if path(options.app_build).exists():
+        path(options.app_build).rmtree()
+    path(options.app_build).makedirs()
+
+
+@task
+@cmdopts([
+    ('app-folder=', 'a', 'App folder directory (default /app)'),
+])
+def clean_zip(options):
+    """Remove all the generated zip files from the app folder."""
+    for zip_file in options.zip_files + ['django.zip']:
+        zip_path = path(options.app_folder) / zip_file
+        if zip_path.exists():
+            zip_path.remove()
+
+
+@task
+@cmdopts([
+    ('app-build=', 'b', 'App build directory (default /build)'),
+    ('app-folder=', 'a', 'App folder directory (default /app)'),
+])
+def django_zip(options):
+    """Create the zip file containing Django (minus unwanted stuff)."""
+    # Write the `django.zip` file. This requires finding all of the necessary
+    # files and writing them to a `zipfile.ZipFile` instance. Python's
+    # stdlib `zipfile` module is written in C, so it's fast and doesn't incur
+    # much overhead.
+    django_src_dir = path(options.app_folder) / 'django'
+    django_zip_filename = path(options.app_build) / 'django.zip'
+    if paver.tasks.environment.dry_run:
+        django_zip_fp = StringIO()
+    else:
+        # Ensure the parent directories exist.
+        django_zip_filename.dirname().makedirs()
+        django_zip_fp = open(django_zip_filename, mode='w')
+    
+    # Write the zip file to disk; this uses the `write_zip_file()` function
+    # defined above. The try/except/finally makes sure the `django.zip` file
+    # handle is properly closed no matter what happens.
+    try:
+        write_zip_file(django_zip_fp, django_zip_files(django_src_dir))
+    except Exception, exc:
+        # Close and delete the (possibly corrupted) `django.zip` file.
+        django_zip_fp.close()
+        django_zip_filename.remove()
+        # Raise the error, causing Paver to exit with a non-zero exit code.
+        raise paver.tasks.BuildFailure(
+            'Error occurred creating django.zip: %r' % (exc,))
+    finally:
+        # Close the file handle if it isn't already.
+        if not django_zip_fp.closed:
+            django_zip_fp.close()
+
+
+@task
+@cmdopts([
+    ('app-folder=', 'a', 'App folder directory (default /app)'),
+])
+def tinymce_zip(options):
+    """Create the zip file containing TinyMCE."""
+    # This is very similar to django_zip; see the comments there for
+    # explanations.
+    tinymce_dir = path(options.app_folder) / 'tiny_mce'
+    tinymce_zip_filename = path(options.app_folder) / 'tiny_mce.zip'
+    if paver.tasks.environment.dry_run:
+        tinymce_zip_fp = StringIO()
+    else:
+        # Ensure the parent directories exist.
+        tinymce_zip_filename.dirname().makedirs()
+        tinymce_zip_fp = open(tinymce_zip_filename, mode='w')
+    
+    try:
+        write_zip_file(tinymce_zip_fp, tinymce_zip_files(tinymce_dir))
+    except Exception, exc:
+        tinymce_zip_fp.close()
+        tinymce_zip_filename.remove()
+        raise paver.tasks.BuildFailure(
+            'Error occurred creating tinymce.zip: %r' % (exc,))
+    finally:
+        if not tinymce_zip_fp.closed:
+            tinymce_zip_fp.close()
\ No newline at end of file