assets.py 6.59 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
Will Daly committed
11 12 13
from .utils.envs import Env
from .utils.cmd import cmd, django_cmd

14 15
# setup baseline paths

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

21 22 23 24 25 26 27 28 29 30 31
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
32

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
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)
        except Exception:  # pylint: disable=W0703
            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
        """
70
        for dirname in SASS_LOAD_PATHS + SASS_UPDATE_DIRS + THEME_SASS_PATHS:
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
            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()
        except Exception:  # pylint: disable=W0703
            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()
        except Exception:  # pylint: disable=W0703
            traceback.print_exc()


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


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


129
def compile_sass(debug=False):
Will Daly committed
130 131 132 133 134
    """
    Compile Sass to CSS.
    """
    sh(cmd(
        'sass', '' if debug else '--style compressed',
135
        "--sourcemap",
Will Daly committed
136
        "--cache-location {cache}".format(cache=SASS_CACHE_PATH),
137
        "--load-path", " ".join(SASS_LOAD_PATHS + THEME_SASS_PATHS),
138
        "--update", "-E", "utf-8", " ".join(SASS_UPDATE_DIRS + THEME_SASS_PATHS),
Will Daly committed
139 140 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
    ))


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
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
@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
195 196 197 198
@needs(
    'pavelib.prereqs.install_ruby_prereqs',
    'pavelib.prereqs.install_node_prereqs',
)
Will Daly committed
199 200 201 202 203 204
@consume_args
def update_assets(args):
    """
    Compile CoffeeScript and Sass, then collect static assets.
    """
    parser = argparse.ArgumentParser(prog='paver update_assets')
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    parser.add_argument(
        'system', type=str, nargs='*', default=['lms', 'studio'],
        help="lms or studio",
    )
    parser.add_argument(
        '--settings', type=str, default="dev",
        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",
    )
221 222 223 224
    parser.add_argument(
        '--watch', action='store_true', default=False,
        help="Watch files for changes",
    )
Will Daly committed
225 226 227 228 229 230 231
    args = parser.parse_args(args)

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

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

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