assets.py 6.64 KB
Newer Older
Will Daly committed
1 2 3
"""
Asset compilation and collection.
"""
4
from __future__ import print_function
Will Daly committed
5
import argparse
6
from paver.easy import sh, path, task, cmdopts, needs, consume_args, call_task, no_help
7 8 9 10
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
import glob
import traceback
11
import os
Will Daly committed
12 13 14
from .utils.envs import Env
from .utils.cmd import cmd, django_cmd

15 16
# setup baseline paths

Will Daly committed
17 18 19
COFFEE_DIRS = ['lms', 'cms', 'common']
SASS_LOAD_PATHS = ['./common/static/sass']
SASS_UPDATE_DIRS = ['*/static']
20
SASS_CACHE_PATH = '/tmp/sass-cache'
21

Will Daly committed
22

23 24 25 26 27 28 29 30 31 32 33
THEME_COFFEE_PATHS = []
THEME_SASS_PATHS = []

edxapp_env = Env()
if edxapp_env.feature_flags.get('USE_CUSTOM_THEME', False):
    theme_name = edxapp_env.env_tokens.get('THEME_NAME', '')
    parent_dir = path(edxapp_env.REPO_ROOT).abspath().parent
    theme_root = parent_dir / "themes" / theme_name
    THEME_COFFEE_PATHS = [theme_root]
    THEME_SASS_PATHS = [theme_root / "static" / "sass"]

Will Daly committed
34

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
class CoffeeScriptWatcher(PatternMatchingEventHandler):
    """
    Watches for coffeescript changes
    """
    ignore_directories = True
    patterns = ['*.coffee']

    def register(self, observer):
        """
        register files with observer
        """
        dirnames = set()
        for filename in sh(coffeescript_files(), capture=True).splitlines():
            dirnames.add(path(filename).dirname())
        for dirname in dirnames:
            observer.schedule(self, dirname)

    def on_modified(self, event):
        print('\tCHANGED:', event.src_path)
        try:
            compile_coffeescript(event.src_path)
56
        except Exception:  # pylint: disable=broad-except
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
            traceback.print_exc()


class SassWatcher(PatternMatchingEventHandler):
    """
    Watches for sass file changes
    """
    ignore_directories = True
    patterns = ['*.scss']
    ignore_patterns = ['common/static/xmodule/*']

    def register(self, observer):
        """
        register files with observer
        """
72
        for dirname in SASS_LOAD_PATHS + SASS_UPDATE_DIRS + THEME_SASS_PATHS:
73 74 75 76 77 78 79 80 81 82 83 84
            paths = []
            if '*' in dirname:
                paths.extend(glob.glob(dirname))
            else:
                paths.append(dirname)
            for dirname in paths:
                observer.schedule(self, dirname, recursive=True)

    def on_modified(self, event):
        print('\tCHANGED:', event.src_path)
        try:
            compile_sass()
85
        except Exception:  # pylint: disable=broad-except
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
            traceback.print_exc()


class XModuleSassWatcher(SassWatcher):
    """
    Watches for sass file changes
    """
    ignore_directories = True
    ignore_patterns = []

    def register(self, observer):
        """
        register files with observer
        """
        observer.schedule(self, 'common/lib/xmodule/', recursive=True)

    def on_modified(self, event):
        print('\tCHANGED:', event.src_path)
        try:
            process_xmodule_assets()
106
        except Exception:  # pylint: disable=broad-except
107 108 109 110
            traceback.print_exc()


def coffeescript_files():
Will Daly committed
111
    """
112
    return find command for paths containing coffee files
Will Daly committed
113
    """
114
    dirs = " ".join(THEME_COFFEE_PATHS + [Env.REPO_ROOT / coffee_dir for coffee_dir in COFFEE_DIRS])
115 116 117
    return cmd('find', dirs, '-type f', '-name \"*.coffee\"')


118 119
@task
@no_help
120 121 122 123 124 125
def compile_coffeescript(*files):
    """
    Compile CoffeeScript to JavaScript.
    """
    if not files:
        files = ["`{}`".format(coffeescript_files())]
Will Daly committed
126
    sh(cmd(
127
        "node_modules/.bin/coffee", "--compile", *files
Will Daly committed
128 129 130
    ))


131
def compile_sass(debug=False):
Will Daly committed
132 133 134 135 136
    """
    Compile Sass to CSS.
    """
    sh(cmd(
        'sass', '' if debug else '--style compressed',
137
        "--sourcemap" if debug else '',
Will Daly committed
138
        "--cache-location {cache}".format(cache=SASS_CACHE_PATH),
139
        "--load-path", " ".join(SASS_LOAD_PATHS + THEME_SASS_PATHS),
140
        "--update", "-E", "utf-8", " ".join(SASS_UPDATE_DIRS + THEME_SASS_PATHS),
Will Daly committed
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    ))


def compile_templated_sass(systems, settings):
    """
    Render Mako templates for Sass files.
    `systems` is a list of systems (e.g. 'lms' or 'studio' or both)
    `settings` is the Django settings module to use.
    """
    for sys in systems:
        sh(django_cmd(sys, settings, 'preprocess_assets'))


def process_xmodule_assets():
    """
    Process XModule static assets.
    """
    sh('xmodule_assets common/static/xmodule')


def collect_assets(systems, settings):
    """
    Collect static assets, including Django pipeline processing.
    `systems` is a list of systems (e.g. 'lms' or 'studio' or both)
    `settings` is the Django settings module to use.
    """
    for sys in systems:
        sh(django_cmd(sys, settings, "collectstatic --noinput > /dev/null"))


@task
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
@cmdopts([('background', 'b', 'Background mode')])
def watch_assets(options):
    """
    Watch for changes to asset files, and regenerate js/css
    """
    observer = Observer()

    CoffeeScriptWatcher().register(observer)
    SassWatcher().register(observer)
    XModuleSassWatcher().register(observer)

    print("Starting asset watcher...")
    observer.start()
    if not getattr(options, 'background', False):
        # when running as a separate process, the main thread needs to loop
        # in order to allow for shutdown by contrl-c
        try:
            while True:
                observer.join(2)
        except KeyboardInterrupt:
            observer.stop()
        print("\nStopped asset watcher.")


@task
197 198 199 200
@needs(
    'pavelib.prereqs.install_ruby_prereqs',
    'pavelib.prereqs.install_node_prereqs',
)
Will Daly committed
201 202 203 204 205 206
@consume_args
def update_assets(args):
    """
    Compile CoffeeScript and Sass, then collect static assets.
    """
    parser = argparse.ArgumentParser(prog='paver update_assets')
207 208 209 210 211
    parser.add_argument(
        'system', type=str, nargs='*', default=['lms', 'studio'],
        help="lms or studio",
    )
    parser.add_argument(
212
        '--settings', type=str, default="devstack",
213 214 215 216 217 218 219 220 221 222
        help="Django settings module",
    )
    parser.add_argument(
        '--debug', action='store_true', default=False,
        help="Disable Sass compression",
    )
    parser.add_argument(
        '--skip-collect', dest='collect', action='store_false', default=True,
        help="Skip collection of static assets",
    )
223 224 225 226
    parser.add_argument(
        '--watch', action='store_true', default=False,
        help="Watch files for changes",
    )
Will Daly committed
227 228 229 230 231 232 233
    args = parser.parse_args(args)

    compile_templated_sass(args.system, args.settings)
    process_xmodule_assets()
    compile_coffeescript()
    compile_sass(args.debug)

234
    if args.collect:
Will Daly committed
235
        collect_assets(args.system, args.settings)
236 237 238

    if args.watch:
        call_task('watch_assets', options={'background': not args.debug})