| 1 | #!/usr/bin/env python |
|---|
| 2 | """ |
|---|
| 3 | Create Package |
|---|
| 4 | ~~~~~~~~~~~~~~ |
|---|
| 5 | |
|---|
| 6 | This script dumps Zine with all files required files into a .tar.gz |
|---|
| 7 | and .zip archive for distribution. |
|---|
| 8 | |
|---|
| 9 | :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details. |
|---|
| 10 | :license: BSD, see LICENSE for more details. |
|---|
| 11 | """ |
|---|
| 12 | import re |
|---|
| 13 | import os |
|---|
| 14 | import sys |
|---|
| 15 | import shutil |
|---|
| 16 | import tempfile |
|---|
| 17 | import subprocess |
|---|
| 18 | from optparse import OptionParser |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | IGNORED = map(re.compile, r''' |
|---|
| 22 | ^artwork/ |
|---|
| 23 | ^\.hgignore$ |
|---|
| 24 | '''.strip().splitlines()) |
|---|
| 25 | |
|---|
| 26 | EXTRA_RST = 'README INSTALL'.split() |
|---|
| 27 | |
|---|
| 28 | _documented_plugin_re = re.compile(r'^zine/plugins/([^/]+)/docs/') |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | def is_ignored(filename): |
|---|
| 32 | for rule in IGNORED: |
|---|
| 33 | if rule.search(filename): |
|---|
| 34 | return True |
|---|
| 35 | return False |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | class BuildError(Exception): |
|---|
| 39 | pass |
|---|
| 40 | |
|---|
| 41 | |
|---|
| 42 | def find_files(force): |
|---|
| 43 | allowed_states = frozenset('CI?') |
|---|
| 44 | tracked_states = frozenset('CM') |
|---|
| 45 | client = subprocess.Popen(['hg', 'status', '-A'], stdout=subprocess.PIPE) |
|---|
| 46 | for line in client.communicate()[0].splitlines(): |
|---|
| 47 | line = line.split(None, 1) |
|---|
| 48 | if len(line) != 2: |
|---|
| 49 | continue |
|---|
| 50 | status, filename = line |
|---|
| 51 | if not force and status not in allowed_states: |
|---|
| 52 | raise BuildError('hg unclean. Uncommited modifications?') |
|---|
| 53 | if status in tracked_states and not is_ignored(filename): |
|---|
| 54 | yield filename |
|---|
| 55 | |
|---|
| 56 | |
|---|
| 57 | def get_zine_version(): |
|---|
| 58 | from zine import __version__ as rv |
|---|
| 59 | return rv |
|---|
| 60 | |
|---|
| 61 | |
|---|
| 62 | def run_script(dir, script, args=None): |
|---|
| 63 | subprocess.call([os.path.join(dir, 'scripts', script)] + list(args or ())) |
|---|
| 64 | |
|---|
| 65 | |
|---|
| 66 | def delete_pycs(folder): |
|---|
| 67 | for dirname, folders, files in os.walk(folder): |
|---|
| 68 | for filename in files: |
|---|
| 69 | if filename.endswith('.pyc'): |
|---|
| 70 | filename = os.path.join(folder, dirname, filename) |
|---|
| 71 | os.remove(filename) |
|---|
| 72 | |
|---|
| 73 | |
|---|
| 74 | def build(dst_dir, build_dir, options): |
|---|
| 75 | zine_version = get_zine_version() |
|---|
| 76 | release_name = 'Zine-%s' % '-'.join(zine_version.split()) |
|---|
| 77 | archive_base = os.path.join(dst_dir, release_name) |
|---|
| 78 | |
|---|
| 79 | # create a subfolder for this release |
|---|
| 80 | release_dir = os.path.join(build_dir, release_name) |
|---|
| 81 | os.mkdir(release_dir) |
|---|
| 82 | |
|---|
| 83 | documented_plugins = set() |
|---|
| 84 | |
|---|
| 85 | # copy all tracked files into the build dir |
|---|
| 86 | print 'Bootstrapping build directory [%s]' % build_dir |
|---|
| 87 | for filename in find_files(options.force): |
|---|
| 88 | target_folder = os.path.join(release_dir, os.path.dirname(filename)) |
|---|
| 89 | if not os.path.exists(target_folder): |
|---|
| 90 | os.makedirs(target_folder) |
|---|
| 91 | shutil.copy(filename, target_folder) |
|---|
| 92 | print filename |
|---|
| 93 | match = _documented_plugin_re.match(filename) |
|---|
| 94 | if match is not None: |
|---|
| 95 | documented_plugins.add(match.group(1)) |
|---|
| 96 | |
|---|
| 97 | print 'The following plugins are documented:' |
|---|
| 98 | for plugin in sorted(documented_plugins): |
|---|
| 99 | print ' *', plugin |
|---|
| 100 | |
|---|
| 101 | # build translations |
|---|
| 102 | run_script(release_dir, 'compile-translations') |
|---|
| 103 | |
|---|
| 104 | # and the documentation for everything |
|---|
| 105 | run_script(release_dir, 'build-documentation') |
|---|
| 106 | for plugin in documented_plugins: |
|---|
| 107 | run_script(release_dir, 'build-documentation', |
|---|
| 108 | [os.path.join(release_dir, 'zine', 'plugins', plugin)]) |
|---|
| 109 | |
|---|
| 110 | # build extra rst files into .html files |
|---|
| 111 | old_dir = os.getcwd() |
|---|
| 112 | try: |
|---|
| 113 | os.chdir(release_dir) |
|---|
| 114 | for filename in EXTRA_RST: |
|---|
| 115 | subprocess.call(['rst2html.py', filename, filename + '.html']) |
|---|
| 116 | finally: |
|---|
| 117 | os.chdir(old_dir) |
|---|
| 118 | |
|---|
| 119 | # delete .pyc files the documentation builder might have created |
|---|
| 120 | delete_pycs(release_dir) |
|---|
| 121 | |
|---|
| 122 | # now create the archives |
|---|
| 123 | messages = [] |
|---|
| 124 | old_dir = os.getcwd() |
|---|
| 125 | try: |
|---|
| 126 | os.chdir(build_dir) |
|---|
| 127 | if options.tar: |
|---|
| 128 | tarball_filename = archive_base + '.tar.gz' |
|---|
| 129 | subprocess.call(['tar', 'vczf', tarball_filename, release_name]) |
|---|
| 130 | messages.append('Created tarball in %s' % tarball_filename) |
|---|
| 131 | if options.zip: |
|---|
| 132 | zip_filename = archive_base + '.zip' |
|---|
| 133 | subprocess.call(['zip', '-r', zip_filename, release_name]) |
|---|
| 134 | messages.append('Created zip file in %s' % zip_filename) |
|---|
| 135 | finally: |
|---|
| 136 | os.chdir(old_dir) |
|---|
| 137 | |
|---|
| 138 | print '-' * 60 |
|---|
| 139 | print 'Summary' |
|---|
| 140 | for message in messages: |
|---|
| 141 | print message |
|---|
| 142 | |
|---|
| 143 | |
|---|
| 144 | def main(): |
|---|
| 145 | parser = OptionParser(usage='%prog [options] destination') |
|---|
| 146 | parser.add_option('--no-zip', dest='zip', action='store_false', |
|---|
| 147 | default=True, help='Do not generate a .zip file') |
|---|
| 148 | parser.add_option('--no-tar', dest='tar', action='store_false', |
|---|
| 149 | default=True, help='Do not generate a tarball') |
|---|
| 150 | parser.add_option('-f', '--force', dest='force', action='store_true', |
|---|
| 151 | default=False, help='Force build, even if local ' |
|---|
| 152 | 'modifications exist.') |
|---|
| 153 | options, args = parser.parse_args() |
|---|
| 154 | if len(args) != 1: |
|---|
| 155 | parser.error('incorrect number of arguments') |
|---|
| 156 | dst = args[0] |
|---|
| 157 | |
|---|
| 158 | if not os.path.exists(dst): |
|---|
| 159 | os.makedirs(dst) |
|---|
| 160 | |
|---|
| 161 | try: |
|---|
| 162 | build_dir = tempfile.mkdtemp(prefix='zine') |
|---|
| 163 | try: |
|---|
| 164 | build(os.path.abspath(dst), build_dir, options) |
|---|
| 165 | finally: |
|---|
| 166 | shutil.rmtree(build_dir, ignore_errors=True) |
|---|
| 167 | except BuildError, e: |
|---|
| 168 | print >> sys.stderr, e |
|---|
| 169 | |
|---|
| 170 | |
|---|
| 171 | if __name__ == '__main__': |
|---|
| 172 | base_dir = os.path.join(os.path.dirname(__file__), '..') |
|---|
| 173 | os.chdir(base_dir) |
|---|
| 174 | sys.path.insert(0, base_dir) |
|---|
| 175 | main() |
|---|