assets.py 7.42 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 import tasks
7
from paver.easy import sh, path, task, cmdopts, needs, consume_args, call_task, no_help
8 9 10 11
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
import glob
import traceback
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
COFFEE_DIRS = ['lms', 'cms', 'common']
18 19 20 21
SASS_DIRS = {
    "lms/static/sass": "lms/static/css",
    "cms/static/sass": "cms/static/css",
    "common/static/sass": "common/static/css",
22
    "lms/static/certificates/sass": "lms/static/certificates/css",
23 24
}
SASS_LOAD_PATHS = ['common/static', 'common/static/sass']
25
SASS_CACHE_PATH = '/tmp/sass-cache'
26

Will Daly committed
27

28 29 30 31 32
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
33 34
    COFFEE_DIRS.append(theme_root)
    SASS_DIRS[theme_root / "static" / "sass"] = None
35

Will Daly committed
36

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


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


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


133 134
@task
@no_help
135 136 137 138
@cmdopts([
    ('debug', 'd', 'Debug mode'),
    ('force', '', 'Force full compilation'),
])
139
def compile_sass(options):
Will Daly committed
140 141 142
    """
    Compile Sass to CSS.
    """
143
    debug = options.get('debug')
144 145 146 147 148 149 150
    parts = ["sass"]
    parts.append("--update")
    parts.append("--cache-location {cache}".format(cache=SASS_CACHE_PATH))
    parts.append("--default-encoding utf-8")
    if debug:
        parts.append("--sourcemap")
    else:
151
        parts.append("--style compressed --quiet")
152 153 154
    if options.get('force'):
        parts.append("--force")
    parts.append("--load-path .")
155 156 157 158 159 160 161 162 163 164
    for load_path in SASS_LOAD_PATHS + SASS_DIRS.keys():
        parts.append("--load-path {path}".format(path=load_path))

    for sass_dir, css_dir in SASS_DIRS.items():
        if css_dir:
            parts.append("{sass}:{css}".format(sass=sass_dir, css=css_dir))
        else:
            parts.append(sass_dir)

    sh(cmd(*parts))
Will Daly committed
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190


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:
191
        sh(django_cmd(sys, settings, "collectstatic --noinput > /dev/null"))
Will Daly committed
192 193 194


@task
195 196 197 198 199
@cmdopts([('background', 'b', 'Background mode')])
def watch_assets(options):
    """
    Watch for changes to asset files, and regenerate js/css
    """
200 201 202 203
    # Don't watch assets when performing a dry run
    if tasks.environment.dry_run:
        return

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    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
224 225 226 227
@needs(
    'pavelib.prereqs.install_ruby_prereqs',
    'pavelib.prereqs.install_node_prereqs',
)
Will Daly committed
228 229 230 231 232 233
@consume_args
def update_assets(args):
    """
    Compile CoffeeScript and Sass, then collect static assets.
    """
    parser = argparse.ArgumentParser(prog='paver update_assets')
234 235 236 237 238
    parser.add_argument(
        'system', type=str, nargs='*', default=['lms', 'studio'],
        help="lms or studio",
    )
    parser.add_argument(
239
        '--settings', type=str, default="devstack",
240 241 242 243 244 245 246 247 248 249
        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",
    )
250 251 252 253
    parser.add_argument(
        '--watch', action='store_true', default=False,
        help="Watch files for changes",
    )
Will Daly committed
254 255 256 257 258
    args = parser.parse_args(args)

    compile_templated_sass(args.system, args.settings)
    process_xmodule_assets()
    compile_coffeescript()
259
    call_task('pavelib.assets.compile_sass', options={'debug': args.debug})
Will Daly committed
260

261
    if args.collect:
Will Daly committed
262
        collect_assets(args.system, args.settings)
263 264

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