~richardw/jarmon/prerelease-fixes

« back to all changes in this revision

Viewing changes to jarmonbuild/commands.py

  • Committer: Richard Wall
  • Date: 2010-08-22 21:38:05 UTC
  • mfrom: (68.1.18 documentation)
  • Revision ID: richard@aziz-20100822213805-7fgctcum5b9l5x51
Merge ~richardw/jarmon/auto-apidocumentation
 * Cleanup and add further doc strings for compatibility with yuidoc
 * Add a tool to automatically download and run yuidoc on the source tree
 * Add a tool to automatically create a releasable source archive containing apidocs
 * Add documentation of the apidoc and release tools.
 * Rearrange the source tree - moving all examples into the docs/examplesfolder

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2010 Richard Wall <richard (at) the-moon.net>
 
2
"""
 
3
Functions and Classes for automating the release of Jarmon
 
4
"""
 
5
 
 
6
import hashlib
 
7
import logging
 
8
import os
 
9
import shutil
 
10
import sys
 
11
 
 
12
from optparse import OptionParser
 
13
from subprocess import check_call, PIPE
 
14
from tempfile import gettempdir
 
15
from urllib2 import urlopen
 
16
from zipfile import ZipFile, ZIP_DEFLATED
 
17
 
 
18
import pkg_resources
 
19
 
 
20
 
 
21
JARMON_PROJECT_TITLE='Jarmon'
 
22
JARMON_PROJECT_URL='http://www.launchpad.net/jarmon'
 
23
 
 
24
YUIDOC_URL = 'http://yuilibrary.com/downloads/yuidoc/yuidoc_1.0.0b1.zip'
 
25
YUIDOC_MD5 = 'cd5545d2dec8f7afe3d18e793538162c'
 
26
YUIDOC_DEPENDENCIES = ['setuptools', 'pygments', 'cheetah']
 
27
 
 
28
 
 
29
class BuildError(Exception):
 
30
    """
 
31
    A base Exception for errors in the build system
 
32
    """
 
33
    pass
 
34
 
 
35
 
 
36
class BuildCommand(object):
 
37
    def __init__(self, buildversion, log=None):
 
38
        self.buildversion = buildversion
 
39
        if log is not None:
 
40
            self.log = log
 
41
        else:
 
42
            self.log = logging.getLogger(
 
43
                '%s.%s' % (__name__, self.__class__.__name__))
 
44
 
 
45
        self.workingbranch_dir = os.path.abspath(
 
46
            os.path.join(os.path.dirname(__file__), '..'))
 
47
 
 
48
        # setup working dir
 
49
        self.build_dir = os.path.join(self.workingbranch_dir, 'build')
 
50
 
 
51
        if not os.path.isdir(self.build_dir):
 
52
            self.log.debug('Creating build dir: %s' % (self.build_dir,))
 
53
            os.mkdir(self.build_dir)
 
54
        else:
 
55
            self.log.debug('Using build dir: %s' % (self.build_dir,))
 
56
 
 
57
 
 
58
class BuildApidocsCommand(BuildCommand):
 
59
    """
 
60
    Download YUI Doc and use it to generate apidocs for jarmon
 
61
    """
 
62
 
 
63
    def main(self, argv):
 
64
        """
 
65
        The main entry point for the build-apidocs command
 
66
 
 
67
        @param argv: The list of arguments passed to the build-apidocs command
 
68
        """
 
69
        tmpdir = gettempdir()
 
70
        workingbranch_dir = self.workingbranch_dir
 
71
        build_dir = self.build_dir
 
72
 
 
73
        # Check for yuidoc dependencies
 
74
        for r in pkg_resources.parse_requirements(YUIDOC_DEPENDENCIES):
 
75
            if not pkg_resources.working_set.find(r):
 
76
                raise BuildError('Unsatisfied yuidoc dependency: %r' % (r,))
 
77
 
 
78
        # download and cache yuidoc
 
79
        yuizip_path = os.path.join(tmpdir, os.path.basename(YUIDOC_URL))
 
80
        if os.path.exists(yuizip_path):
 
81
            self.log.debug('Using cached YUI doc')
 
82
            def producer():
 
83
                yield open(yuizip_path).read()
 
84
        else:
 
85
            self.log.debug('Downloading YUI Doc')
 
86
            def producer():
 
87
                with open(yuizip_path, 'w') as yuizip:
 
88
                    download = urlopen(YUIDOC_URL)
 
89
                    while True:
 
90
                        bytes = download.read(1024*10)
 
91
                        if not bytes:
 
92
                            break
 
93
                        else:
 
94
                            yuizip.write(bytes)
 
95
                            yield bytes
 
96
 
 
97
        checksum = hashlib.md5()
 
98
        for bytes in producer():
 
99
            checksum.update(bytes)
 
100
 
 
101
        actual_md5 = checksum.hexdigest()
 
102
        if actual_md5 != YUIDOC_MD5:
 
103
            raise BuildError(
 
104
                'YUI Doc checksum error. File: %s, '
 
105
                'Expected: %s, Got: %s' % (yuizip_path, YUIDOC_MD5, actual_md5))
 
106
        else:
 
107
            self.log.debug('YUI Doc checksum verified')
 
108
 
 
109
        # Remove any existing apidocs so that we can track removed files
 
110
        shutil.rmtree(os.path.join(build_dir, 'docs', 'apidocs'), True)
 
111
 
 
112
        yuidoc_dir = os.path.join(build_dir, 'yuidoc')
 
113
 
 
114
        # extract yuidoc folder from the downloaded zip file
 
115
        self.log.debug(
 
116
            'Extracting YUI Doc from %s to %s' % (yuizip_path, yuidoc_dir))
 
117
        zip = ZipFile(yuizip_path)
 
118
        zip.extractall(
 
119
            build_dir, (m for m in zip.namelist() if m.startswith('yuidoc')))
 
120
 
 
121
        # Use the yuidoc script that we just extracted to generate new docs
 
122
        self.log.debug('Running YUI Doc')
 
123
        check_call((
 
124
            sys.executable,
 
125
            os.path.join(yuidoc_dir, 'bin', 'yuidoc.py'),
 
126
            os.path.join(workingbranch_dir, 'jarmon'),
 
127
            '--parseroutdir=%s' % (
 
128
                os.path.join(build_dir, 'docs', 'apidocs'),),
 
129
            '--outputdir=%s' % (
 
130
                os.path.join(build_dir, 'docs', 'apidocs'),),
 
131
            '--template=%s' % (
 
132
                os.path.join(
 
133
                    workingbranch_dir, 'jarmonbuild', 'yuidoc_template'),),
 
134
            '--version=%s' % (self.buildversion,),
 
135
            '--project=%s' % (JARMON_PROJECT_TITLE,),
 
136
            '--projecturl=%s' % (JARMON_PROJECT_URL,)
 
137
        ), stdout=PIPE, stderr=PIPE,)
 
138
 
 
139
        shutil.rmtree(yuidoc_dir)
 
140
 
 
141
 
 
142
class BuildReleaseCommand(BuildCommand):
 
143
    """
 
144
    Export all source files, generate apidocs and create a zip archive for
 
145
    upload to Launchpad.
 
146
    """
 
147
 
 
148
    def main(self, argv):
 
149
        workingbranch_dir = self.workingbranch_dir
 
150
        build_dir = self.build_dir
 
151
 
 
152
        self.log.debug('Export versioned files to a build folder')
 
153
        from bzrlib.commands import main as bzr_main
 
154
        status = bzr_main(['bzr', 'export', build_dir, workingbranch_dir])
 
155
        if status != 0:
 
156
            raise BuildError('bzr export failure. Status: %r' % (status,))
 
157
 
 
158
 
 
159
        self.log.debug('Record the branch version')
 
160
        from bzrlib.branch import Branch
 
161
        from bzrlib.version_info_formats import format_python
 
162
        v = format_python.PythonVersionInfoBuilder(
 
163
            Branch.open(workingbranch_dir))
 
164
        versionfile_path = os.path.join(build_dir, 'jarmonbuild', '_version.py')
 
165
        with open(versionfile_path, 'w') as f:
 
166
            v.generate(f)
 
167
 
 
168
 
 
169
        self.log.debug('Generate apidocs')
 
170
        BuildApidocsCommand(buildversion=self.buildversion).main(argv)
 
171
 
 
172
 
 
173
        self.log.debug('Generate archive')
 
174
        archive_root = 'jarmon-%s' % (self.buildversion,)
 
175
        prefix_len = len(build_dir) + 1
 
176
        z = ZipFile('%s.zip' % (archive_root,), 'w', ZIP_DEFLATED)
 
177
        try:
 
178
            for root, dirs, files in os.walk(build_dir):
 
179
                for file in files:
 
180
                    z.write(
 
181
                        os.path.join(root, file),
 
182
                        os.path.join(archive_root, root[prefix_len:], file)
 
183
                    )
 
184
        finally:
 
185
            z.close()
 
186
 
 
187
 
 
188
# The available sub commands
 
189
build_commands = {
 
190
    'apidocs': BuildApidocsCommand,
 
191
    'release': BuildReleaseCommand,
 
192
}
 
193
 
 
194
 
 
195
def main(argv=sys.argv[1:]):
 
196
    """
 
197
    The root build command which dispatches to various subcommands for eg
 
198
    building apidocs and release zip files.
 
199
    """
 
200
    parser = OptionParser(usage='%prog [options] SUBCOMMAND [options]')
 
201
    parser.add_option(
 
202
        '-V', '--build-version', dest='buildversion', default='0',
 
203
        metavar='BUILDVERSION', help='Specify the build version')
 
204
    parser.add_option(
 
205
        '-d', '--debug', action='store_true', default=False, dest='debug',
 
206
        help='Print verbose debug log to stderr')
 
207
 
 
208
    parser.disable_interspersed_args()
 
209
 
 
210
    options, args = parser.parse_args(argv)
 
211
 
 
212
    if len(args) < 1:
 
213
        parser.error('Please specify a sub command. '
 
214
                     'Available commands: %r' % (build_commands.keys()))
 
215
 
 
216
    # First argument is the name of a subcommand
 
217
    command_name = args.pop(0)
 
218
    command_factory = build_commands.get(command_name)
 
219
    if not command_factory:
 
220
        parser.error('Unrecognised subcommand: %r' % (command_name,))
 
221
 
 
222
    # Setup logging
 
223
    log = logging.getLogger(__name__)
 
224
    log.setLevel(logging.INFO)
 
225
    # create console handler and set level to debug
 
226
    ch = logging.StreamHandler()
 
227
    ch.setLevel(logging.INFO)
 
228
    # create formatter
 
229
    formatter = logging.Formatter(
 
230
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
231
    # add formatter to ch
 
232
    ch.setFormatter(formatter)
 
233
    log.addHandler(ch)
 
234
 
 
235
    if options.debug:
 
236
        log.setLevel(logging.DEBUG)
 
237
        ch.setLevel(logging.DEBUG)
 
238
 
 
239
    command_factory(buildversion=options.buildversion).main(argv=args)