|
| 1 | +import os |
| 2 | +import sys |
| 3 | +import json |
| 4 | +import tempfile |
| 5 | +import shutil |
| 6 | +import hashlib |
| 7 | +import glob |
| 8 | +import textwrap |
| 9 | +from subprocess import call, check_output, CalledProcessError |
| 10 | +from optparse import make_option |
| 11 | + |
| 12 | +import debug # pyflakes:ignore |
| 13 | + |
| 14 | +from django.core.management.base import BaseCommand |
| 15 | +from django.core.files.storage import FileSystemStorage |
| 16 | +from django.conf import settings |
| 17 | +from django.contrib.staticfiles.finders import BaseStorageFinder, AppDirectoriesFinder |
| 18 | + |
| 19 | +class BaseDirectoryFinder(BaseStorageFinder): |
| 20 | + storage = FileSystemStorage(location=settings.BASE_DIR) |
| 21 | + |
| 22 | +class Command(BaseCommand): |
| 23 | + """ |
| 24 | +
|
| 25 | + This command goes through any static/ directories of installed apps, |
| 26 | + and the directories listed in settings.STATICFILES_DIRS. If package |
| 27 | + description files for bower, npm, or grunt are found in any of these |
| 28 | + locations, it will use the appropriate package manager to install the |
| 29 | + listed packages in a temporary folder, using these commands: |
| 30 | +
|
| 31 | + - package.json: npm install |
| 32 | + - Gruntfile.js: grunt default |
| 33 | + - bower.json: bower install |
| 34 | +
|
| 35 | + It will then extract the distribution files to the location indicated in |
| 36 | + settings.COMPONENT_ROOT. |
| 37 | + """ |
| 38 | + help = textwrap.dedent(__doc__).lstrip() |
| 39 | + component_root = getattr(settings, 'COMPONENT_ROOT', os.path.join(settings.STATIC_ROOT, "components")) |
| 40 | + |
| 41 | + def add_arguments(self, parser): |
| 42 | + parser.add_argument('--with-version', dest='with_version', default=False, action='store_true', |
| 43 | + help='Create component directories with version numbers') |
| 44 | + parser.add_argument('--keep-packages', dest='keep_packages', default=False, action='store_true', |
| 45 | + help='Keep the downloaded bower packages, instead of removing them after moving ' |
| 46 | + 'distribution files to settings.COMPONENT_ROOT') |
| 47 | + |
| 48 | + bower_info = {} |
| 49 | + overrides = {} |
| 50 | + |
| 51 | + def npm_install(self, pkg_json_path): |
| 52 | + os.chdir(os.path.dirname(pkg_json_path)) |
| 53 | + call(['npm', 'install']) |
| 54 | + |
| 55 | + def grunt_default(self, grunt_js_path): |
| 56 | + os.chdir(os.path.dirname(grunt_js_path)) |
| 57 | + call(['grunt']) |
| 58 | + |
| 59 | + def bower_install(self, bower_json_path, dest_dir): |
| 60 | + """Runs bower commnand for the passed bower.json path. |
| 61 | +
|
| 62 | + :param bower_json_path: bower.json file to install |
| 63 | + :param dest_dir: where the compiled result will arrive |
| 64 | + """ |
| 65 | + |
| 66 | + # Verify that we are able to run bower, in order to give a good error message in the |
| 67 | + # case that it's not installed. Do this separately from the 'bower install' call, in |
| 68 | + # order not to warn about a missing bower in the case of installation-related errors. |
| 69 | + try: |
| 70 | + bower_version = check_output(['bower', '--version']).strip() |
| 71 | + except OSError as e: |
| 72 | + print("Trying to run bower failed -- is it installed? The error was: %s" % e) |
| 73 | + exit(1) |
| 74 | + except CalledProcessError as e: |
| 75 | + print("Checking the bower version failed: %s" % e) |
| 76 | + exit(2) |
| 77 | + |
| 78 | + print("\nBower %s" % bower_version) |
| 79 | + print("Installing from %s\n" % bower_json_path) |
| 80 | + |
| 81 | + # bower args |
| 82 | + args = ['bower', 'install', bower_json_path, |
| 83 | + '--verbose', '--config.cwd={}'.format(dest_dir), '-p'] |
| 84 | + |
| 85 | + # run bower command |
| 86 | + call(args) |
| 87 | + |
| 88 | + def get_bower_info(self, bower_json_path): |
| 89 | + if not bower_json_path in self.bower_info: |
| 90 | + self.bower_info[bower_json_path] = json.load(open(bower_json_path)) |
| 91 | + |
| 92 | + def get_bower_main_list(self, bower_json_path, override): |
| 93 | + """ |
| 94 | + Returns the bower.json main list or empty list. |
| 95 | + Applies overrides from the site-wide bower.json. |
| 96 | + """ |
| 97 | + self.get_bower_info(bower_json_path) |
| 98 | + |
| 99 | + main_list = self.bower_info[bower_json_path].get('main') |
| 100 | + component = self.bower_info[bower_json_path].get('name') |
| 101 | + |
| 102 | + if (override in self.bower_info |
| 103 | + and "overrides" in self.bower_info[override] |
| 104 | + and component in self.bower_info[override].get("overrides") |
| 105 | + and "main" in self.bower_info[override].get("overrides").get(component)): |
| 106 | + main_list = self.bower_info[override].get("overrides").get(component).get("main") |
| 107 | + |
| 108 | + if isinstance(main_list, list): |
| 109 | + return main_list |
| 110 | + |
| 111 | + if main_list: |
| 112 | + return [main_list] |
| 113 | + |
| 114 | + return [] |
| 115 | + |
| 116 | + def get_bower_version(self, bower_json_path): |
| 117 | + """Returns the bower.json main list or empty list. |
| 118 | + """ |
| 119 | + self.get_bower_info(bower_json_path) |
| 120 | + |
| 121 | + return self.bower_info[bower_json_path].get("version") |
| 122 | + |
| 123 | + def clean_components_to_static_dir(self, bower_dir, override): |
| 124 | + print("\nMoving component files to %s\n" % (self.component_root,)) |
| 125 | + |
| 126 | + for directory in os.listdir(bower_dir): |
| 127 | + print("Component: %s" % (directory, )) |
| 128 | + |
| 129 | + src_root = os.path.join(bower_dir, directory) |
| 130 | + |
| 131 | + for bower_json in ['bower.json', '.bower.json']: |
| 132 | + bower_json_path = os.path.join(src_root, bower_json) |
| 133 | + if os.path.exists(bower_json_path): |
| 134 | + main_list = self.get_bower_main_list(bower_json_path, override) + ['bower.json'] |
| 135 | + version = self.get_bower_version(bower_json_path) |
| 136 | + |
| 137 | + dst_root = os.path.join(self.component_root, directory) |
| 138 | + if self.with_version: |
| 139 | + assert not dst_root.endswith(os.sep) |
| 140 | + dst_root += "-"+version |
| 141 | + |
| 142 | + for pattern in filter(None, main_list): |
| 143 | + src_pattern = os.path.join(src_root, pattern) |
| 144 | + # main_list elements can be fileglob patterns |
| 145 | + for src_path in glob.glob(src_pattern): |
| 146 | + if not os.path.exists(src_path): |
| 147 | + print("Could not find source path: %s" % (src_path, )) |
| 148 | + |
| 149 | + # Build the destination path |
| 150 | + src_part = src_path[len(src_root+'/'):] |
| 151 | + if src_part.startswith('dist/'): |
| 152 | + src_part = src_part[len('dist/'):] |
| 153 | + dst_path = os.path.join(dst_root, src_part) |
| 154 | + |
| 155 | + # Normalize the paths, for good looks |
| 156 | + src_path = os.path.abspath(src_path) |
| 157 | + dst_path = os.path.abspath(dst_path) |
| 158 | + |
| 159 | + # Check if we need to copy the file at all. |
| 160 | + if os.path.exists(dst_path): |
| 161 | + with open(src_path) as src: |
| 162 | + src_hash = hashlib.sha1(src.read()).hexdigest() |
| 163 | + with open(dst_path) as dst: |
| 164 | + dst_hash = hashlib.sha1(dst.read()).hexdigest() |
| 165 | + if src_hash == dst_hash: |
| 166 | + #print('{0} = {1}'.format(src_path, dst_path)) |
| 167 | + continue |
| 168 | + |
| 169 | + # Make sure dest dir exists. |
| 170 | + dst_dir = os.path.dirname(dst_path) |
| 171 | + if not os.path.exists(dst_dir): |
| 172 | + os.makedirs(dst_dir) |
| 173 | + |
| 174 | + print(' {0} > {1}'.format(src_path, dst_path)) |
| 175 | + shutil.copy(src_path, dst_path) |
| 176 | + break |
| 177 | + |
| 178 | + def handle(self, *args, **options): |
| 179 | + |
| 180 | + self.with_version = options.get("with_version") |
| 181 | + self.keep_packages = options.get("keep_packages") |
| 182 | + |
| 183 | + temp_dir = getattr(settings, 'BWR_APP_TMP_FOLDER', 'tmp') |
| 184 | + temp_dir = os.path.abspath(temp_dir) |
| 185 | + |
| 186 | + # finders |
| 187 | + basefinder = BaseDirectoryFinder() |
| 188 | + appfinder = AppDirectoriesFinder() |
| 189 | + # Assume bower.json files are to be found in each app directory, |
| 190 | + # rather than in the app's static/ subdirectory: |
| 191 | + appfinder.source_dir = '.' |
| 192 | + |
| 193 | + finders = (basefinder, appfinder, ) |
| 194 | + |
| 195 | + if os.path.exists(temp_dir): |
| 196 | + if not self.keep_packages: |
| 197 | + sys.stderr.write( |
| 198 | + "\nWARNING:\n\n" |
| 199 | + " The temporary package installation directory exists, but the --keep-packages\n" |
| 200 | + " option has not been given. In order to not delete anything which should be\n" |
| 201 | + " kept, %s will not be removed.\n\n" |
| 202 | + " Please remove it manually, or use the --keep-packages option to avoid this\n" |
| 203 | + " message.\n\n" % (temp_dir,)) |
| 204 | + self.keep_packages = True |
| 205 | + else: |
| 206 | + os.makedirs(temp_dir) |
| 207 | + |
| 208 | + for finder in finders: |
| 209 | + for path in finder.find('package.json', all=True): |
| 210 | + self.npm_install(path) |
| 211 | + |
| 212 | + for finder in finders: |
| 213 | + for path in finder.find('Gruntfile.json', all=True): |
| 214 | + self.grunt_default(path) |
| 215 | + |
| 216 | + for finder in finders: |
| 217 | + for path in finder.find('bower.json', all=True): |
| 218 | + self.get_bower_info(path) |
| 219 | + self.bower_install(path, temp_dir) |
| 220 | + |
| 221 | + bower_dir = os.path.join(temp_dir, 'bower_components') |
| 222 | + |
| 223 | + # nothing to clean |
| 224 | + if not os.path.exists(bower_dir): |
| 225 | + print('No components seems to have been found by bower, exiting.') |
| 226 | + sys.exit(0) |
| 227 | + |
| 228 | + self.clean_components_to_static_dir(bower_dir, path) |
| 229 | + |
| 230 | + if not self.keep_packages: |
| 231 | + shutil.rmtree(temp_dir) |
| 232 | + |
0 commit comments