From ac6c929c654875f68fc172c5d64b4a763e9d5996 Mon Sep 17 00:00:00 2001 From: "Kyle D. McCormick" Date: Tue, 26 Mar 2024 17:51:52 -0400 Subject: [PATCH] build: compile/watch sass with new npm scripts `paver` commands are deprecated for managing static assets. Starting in Sumac, only `npm run` commands will be supported for managing static assets. To ease the transition, both `paver` and `npm run` commands will work in Redwood. However, we want to stop using the *implementations* of the `paver` asset commands right now, as they are blocking the Python 3.11 upgrade. This will also make the removal of `paver` commands more straightforward come Sumac. So, this commit turns these commands/functions: * paver compile_sass (used by configuration) * paver watch_sass (used by configuration and devstack) * pavelib/assets.py:_compile_sass (used by Tutor) into very thin wrappers around the new `npm run` commands. Each of these paver routines now raise a loud deprecation warning, including a message of the `npm run` command that the operator can switch to. We expect no impact to site operators or end users. https://github.com/openedx/edx-platform/issues/31895 --- .../core/djangoapps/theming/paver_helpers.py | 75 --- pavelib/assets.py | 593 ++++-------------- pavelib/paver_tests/test_assets.py | 81 --- 3 files changed, 122 insertions(+), 627 deletions(-) delete mode 100644 openedx/core/djangoapps/theming/paver_helpers.py diff --git a/openedx/core/djangoapps/theming/paver_helpers.py b/openedx/core/djangoapps/theming/paver_helpers.py deleted file mode 100644 index b481d0f1b1fd..000000000000 --- a/openedx/core/djangoapps/theming/paver_helpers.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -This file contains helpers for paver commands, Django is not initialized in paver commands. -So, django settings, models etc. can not be used here. -""" - - -import os - -from path import Path - - -def get_theme_paths(themes, theme_dirs): - """ - get absolute path for all the given themes, if a theme is no found - at multiple places than all paths for the theme will be included. - If a theme is not found anywhere then theme will be skipped with - an error message printed on the console. - - If themes is 'None' then all themes in given dirs are returned. - - Args: - themes (list): list of all theme names - theme_dirs (list): list of base dirs that contain themes - Returns: - list of absolute paths to themes. - """ - theme_paths = [] - - for theme in themes: - theme_base_dirs = get_theme_base_dirs(theme, theme_dirs) - if not theme_base_dirs: - print(( - "\033[91m\nSkipping '{theme}': \n" - "Theme ({theme}) not found in any of the theme dirs ({theme_dirs}). \033[00m".format( - theme=theme, - theme_dirs=", ".join(theme_dirs) - ), - )) - theme_paths.extend(theme_base_dirs) - - return theme_paths - - -def get_theme_base_dirs(theme, theme_dirs): - """ - Get all base dirs where the given theme can be found. - - Args: - theme (str): name of the theme to find - theme_dirs (list): list of all base dirs where the given theme could be found - - Returns: - list of all the dirs for the goven theme - """ - theme_paths = [] - for _dir in theme_dirs: - for dir_name in {theme}.intersection(os.listdir(_dir)): - if is_theme_dir(Path(_dir) / dir_name): - theme_paths.append(Path(_dir) / dir_name) - return theme_paths - - -def is_theme_dir(_dir): - """ - Returns true if given dir contains theme overrides. - A theme dir must have subdirectory 'lms' or 'cms' or both. - - Args: - _dir: directory path to check for a theme - - Returns: - Returns true if given dir is a theme directory. - """ - theme_sub_directories = {'lms', 'cms'} - return bool(os.path.isdir(_dir) and theme_sub_directories.intersection(os.listdir(_dir))) diff --git a/pavelib/assets.py b/pavelib/assets.py index 8f950fe3f647..fd420e42a8c7 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -2,23 +2,18 @@ Asset compilation and collection. """ - import argparse import glob import json -import os +import shlex import traceback -from datetime import datetime from functools import wraps from threading import Timer from paver import tasks -from paver.easy import call_task, cmdopts, consume_args, needs, no_help, path, sh, task +from paver.easy import call_task, cmdopts, consume_args, needs, no_help, sh, task from watchdog.events import PatternMatchingEventHandler -from watchdog.observers import Observer -from watchdog.observers.api import DEFAULT_OBSERVER_TIMEOUT - -from openedx.core.djangoapps.theming.paver_helpers import get_theme_paths +from watchdog.observers import Observer # pylint disable=unused-import # Used by Tutor. Remove after Sumac cut. from .utils.cmd import cmd, django_cmd from .utils.envs import Env @@ -38,19 +33,6 @@ 'studio': CMS } -# Common lookup paths that are added to the lookup paths for all sass compilations -COMMON_LOOKUP_PATHS = [ - path("common/static"), - path("common/static/sass"), - path('node_modules/@edx'), - path('node_modules'), -] - -# system specific lookup path additions, add sass dirs if one system depends on the sass files for other systems -SASS_LOOKUP_DEPENDENCIES = { - 'cms': [path('lms') / 'static' / 'sass' / 'partials', ], -} - # Collectstatic log directory setting COLLECTSTATIC_LOG_DIR_ARG = 'collect_log_dir' @@ -58,242 +40,6 @@ WEBPACK_COMMAND = 'STATIC_ROOT_LMS={static_root_lms} STATIC_ROOT_CMS={static_root_cms} $(npm bin)/webpack {options}' -def get_sass_directories(system, theme_dir=None): - """ - Determine the set of SASS directories to be compiled for the specified list of system and theme - and return a list of those directories. - - Each item in the list is dict object containing the following key-value pairs. - { - "sass_source_dir": "", # directory where source sass files are present - "css_destination_dir": "", # destination where css files would be placed - "lookup_paths": [], # list of directories to be passed as lookup paths for @import resolution. - } - - if theme_dir is empty or None then return sass directories for the given system only. (i.e. lms or cms) - - :param system: name if the system for which to compile sass e.g. 'lms', 'cms' - :param theme_dir: absolute path of theme for which to compile sass files. - """ - if system not in SYSTEMS: - raise ValueError("'system' must be one of ({allowed_values})".format( - allowed_values=', '.join(list(SYSTEMS.keys()))) - ) - system = SYSTEMS[system] - - applicable_directories = [] - - if theme_dir: - # Add theme sass directories - applicable_directories.extend( - get_theme_sass_dirs(system, theme_dir) - ) - else: - # add system sass directories - applicable_directories.extend( - get_system_sass_dirs(system) - ) - - return applicable_directories - - -def get_common_sass_directories(): - """ - Determine the set of common SASS directories to be compiled for all the systems and themes. - - Each item in the returned list is dict object containing the following key-value pairs. - { - "sass_source_dir": "", # directory where source sass files are present - "css_destination_dir": "", # destination where css files would be placed - "lookup_paths": [], # list of directories to be passed as lookup paths for @import resolution. - } - """ - applicable_directories = [] - - # add common sass directories - applicable_directories.append({ - "sass_source_dir": path("common/static/sass"), - "css_destination_dir": path("common/static/css"), - "lookup_paths": COMMON_LOOKUP_PATHS, - }) - - return applicable_directories - - -def get_theme_sass_dirs(system, theme_dir): - """ - Return list of sass dirs that need to be compiled for the given theme. - - :param system: name if the system for which to compile sass e.g. 'lms', 'cms' - :param theme_dir: absolute path of theme for which to compile sass files. - """ - if system not in ('lms', 'cms'): - raise ValueError('"system" must either be "lms" or "cms"') - - dirs = [] - - system_sass_dir = path(system) / "static" / "sass" - sass_dir = theme_dir / system / "static" / "sass" - css_dir = theme_dir / system / "static" / "css" - certs_sass_dir = theme_dir / system / "static" / "certificates" / "sass" - certs_css_dir = theme_dir / system / "static" / "certificates" / "css" - builtin_xblock_sass = path("xmodule") / "assets" - - dependencies = SASS_LOOKUP_DEPENDENCIES.get(system, []) - if sass_dir.isdir(): - css_dir.mkdir_p() - - # first compile lms sass files and place css in theme dir - dirs.append({ - "sass_source_dir": system_sass_dir, - "css_destination_dir": css_dir, - "lookup_paths": dependencies + [ - sass_dir / "partials", - system_sass_dir / "partials", - system_sass_dir, - ], - }) - - # now compile theme sass files and override css files generated from lms - dirs.append({ - "sass_source_dir": sass_dir, - "css_destination_dir": css_dir, - "lookup_paths": dependencies + [ - sass_dir / "partials", - system_sass_dir / "partials", - system_sass_dir, - ], - }) - - # now compile theme sass files for certificate - if system == 'lms': - dirs.append({ - "sass_source_dir": certs_sass_dir, - "css_destination_dir": certs_css_dir, - "lookup_paths": [ - sass_dir / "partials", - sass_dir - ], - }) - - # Now, finally, compile builtin XBlocks' Sass. Themes cannot override these - # Sass files directly, but they *can* modify Sass variables which will affect - # the output here. We compile all builtin XBlocks' Sass both for LMS and CMS, - # not because we expect the output to be different between LMS and CMS, but - # because only LMS/CMS-compiled Sass can be themed; common sass is not themed. - dirs.append({ - "sass_source_dir": builtin_xblock_sass, - "css_destination_dir": css_dir, - "lookup_paths": [ - # XBlock editor views may need both LMS and CMS partials. - # XBlock display views should only need LMS patials. - # In order to keep this build script simpler, though, we just - # include everything and compile everything at once. - theme_dir / "lms" / "static" / "sass" / "partials", - theme_dir / "cms" / "static" / "sass" / "partials", - path("lms") / "static" / "sass" / "partials", - path("cms") / "static" / "sass" / "partials", - path("lms") / "static" / "sass", - path("cms") / "static" / "sass", - ], - }) - - return dirs - - -def get_system_sass_dirs(system): - """ - Return list of sass dirs that need to be compiled for the given system. - - :param system: name if the system for which to compile sass e.g. 'lms', 'cms' - """ - if system not in ('lms', 'cms'): - raise ValueError('"system" must either be "lms" or "cms"') - - dirs = [] - sass_dir = path(system) / "static" / "sass" - css_dir = path(system) / "static" / "css" - builtin_xblock_sass = path("xmodule") / "assets" - - dependencies = SASS_LOOKUP_DEPENDENCIES.get(system, []) - dirs.append({ - "sass_source_dir": sass_dir, - "css_destination_dir": css_dir, - "lookup_paths": dependencies + [ - sass_dir / "partials", - sass_dir, - ], - }) - - if system == 'lms': - dirs.append({ - "sass_source_dir": path(system) / "static" / "certificates" / "sass", - "css_destination_dir": path(system) / "static" / "certificates" / "css", - "lookup_paths": [ - sass_dir / "partials", - sass_dir - ], - }) - - # See builtin_xblock_sass compilation in get_theme_sass_dirs for details. - dirs.append({ - "sass_source_dir": builtin_xblock_sass, - "css_destination_dir": css_dir, - "lookup_paths": dependencies + [ - path("lms") / "static" / "sass" / "partials", - path("cms") / "static" / "sass" / "partials", - path("lms") / "static" / "sass", - path("cms") / "static" / "sass", - ], - }) - - return dirs - - -def get_watcher_dirs(theme_dirs=None, themes=None): - """ - Return sass directories that need to be added to sass watcher. - - Example: - >> get_watcher_dirs('/edx/app/edx-platform/themes', ['red-theme']) - [ - 'common/static', - 'common/static/sass', - 'lms/static/sass', - 'lms/static/sass/partials', - '/edx/app/edxapp/edx-platform/themes/red-theme/lms/static/sass', - '/edx/app/edxapp/edx-platform/themes/red-theme/lms/static/sass/partials', - 'cms/static/sass', - 'cms/static/sass/partials', - '/edx/app/edxapp/edx-platform/themes/red-theme/cms/static/sass/partials', - ] - - Parameters: - theme_dirs (list): list of theme base directories. - themes (list): list containing names of themes - Returns: - (list): dirs that need to be added to sass watchers. - """ - dirs = [] - dirs.extend(COMMON_LOOKUP_PATHS) - if theme_dirs and themes: - # Register sass watchers for all the given themes - themes = get_theme_paths(themes=themes, theme_dirs=theme_dirs) - for theme in themes: - for _dir in get_sass_directories('lms', theme) + get_sass_directories('cms', theme): - dirs.append(_dir['sass_source_dir']) - dirs.extend(_dir['lookup_paths']) - - # Register sass watchers for lms and cms - for _dir in get_sass_directories('lms') + get_sass_directories('cms') + get_common_sass_directories(): - dirs.append(_dir['sass_source_dir']) - dirs.extend(_dir['lookup_paths']) - - # remove duplicates - dirs = list(set(dirs)) - return dirs - - def debounce(seconds=1): """ Prevents the decorated function from being called more than every `seconds` @@ -327,7 +73,6 @@ class SassWatcher(PatternMatchingEventHandler): def register(self, observer, directories): """ register files with observer - Arguments: observer (watchdog.observers.Observer): sass file observer directories (list): list of directories to be register for sass watcher. @@ -357,8 +102,8 @@ def on_any_event(self, event): ('system=', 's', 'The system to compile sass for (defaults to all)'), ('theme-dirs=', '-td', 'Theme dirs containing all themes (defaults to None)'), ('themes=', '-t', 'The theme to compile sass for (defaults to None)'), - ('debug', 'd', 'Debug mode'), - ('force', '', 'Force full compilation'), + ('debug', 'd', 'DEPRECATED. Debug mode is now determined by NODE_ENV.'), + ('force', '', 'DEPRECATED. Full recompilation is now always forced.'), ]) @timed def compile_sass(options): @@ -396,173 +141,89 @@ def compile_sass(options): compile sass files for cms only for 'red-theme', 'stanford-style' and 'test-theme' present in '/edx/app/edxapp/edx-platform/themes' and '/edx/app/edxapp/edx-platform/common/test/'. + This is a DEPRECATED COMPATIBILITY WRAPPER. Use `npm run compile-sass` instead. """ - debug = options.get('debug') - force = options.get('force') - systems = get_parsed_option(options, 'system', ALL_SYSTEMS) - themes = get_parsed_option(options, 'themes', []) - theme_dirs = get_parsed_option(options, 'theme_dirs', []) - - if not theme_dirs and themes: - # We can not compile a theme sass without knowing the directory that contains the theme. - raise ValueError('theme-dirs must be provided for compiling theme sass.') - - if themes and theme_dirs: - themes = get_theme_paths(themes=themes, theme_dirs=theme_dirs) - - # Compile sass for OpenEdx theme after comprehensive themes - if None not in themes: - themes.append(None) - - timing_info = [] - dry_run = tasks.environment.dry_run - compilation_results = {'success': [], 'failure': []} - - print("\t\tStarted compiling Sass:") - - # compile common sass files - is_successful = _compile_sass('common', None, debug, force, timing_info) - if is_successful: - print("Finished compiling 'common' sass.") - compilation_results['success' if is_successful else 'failure'].append('"common" sass files.') - - for system in systems: - for theme in themes: - print("Started compiling '{system}' Sass for '{theme}'.".format(system=system, theme=theme or 'system')) - - # Compile sass files - is_successful = _compile_sass( - system=system, - theme=path(theme) if theme else None, - debug=debug, - force=force, - timing_info=timing_info - ) - - if is_successful: - print("Finished compiling '{system}' Sass for '{theme}'.".format( - system=system, theme=theme or 'system' - )) - - compilation_results['success' if is_successful else 'failure'].append('{system} sass for {theme}.'.format( - system=system, theme=theme or 'system', - )) - - print("\t\tFinished compiling Sass:") - if not dry_run: - for sass_dir, css_dir, duration in timing_info: - print(f">> {sass_dir} -> {css_dir} in {duration}s") - - if compilation_results['success']: - print("\033[92m\nSuccessful compilations:\n--- " + "\n--- ".join(compilation_results['success']) + "\n\033[00m") - if compilation_results['failure']: - print("\033[91m\nFailed compilations:\n--- " + "\n--- ".join(compilation_results['failure']) + "\n\033[00m") - + systems = set(get_parsed_option(options, 'system', ALL_SYSTEMS)) + command = shlex.join( + [ + "npm", + "run", + "compile-sass", + "--", + *(["--dry"] if tasks.environment.dry_run else []), + *(["--skip-lms"] if not systems & {"lms"} else []), + *(["--skip-cms"] if not systems & {"cms", "studio"} else []), + *( + arg + for theme_dir in get_parsed_option(options, 'theme_dirs', []) + for arg in ["--theme-dir", str(theme_dir)] + ), + *( + arg + for theme in get_parsed_option(options, "theme", []) + for arg in ["--theme", theme] + ), + ] + ) + depr_warning = ( + "\n" + + "⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ \n" + + "\n" + + "WARNING: 'paver compile_sass' is DEPRECATED! It will be removed before Sumac.\n" + + "The command you ran is now just a temporary wrapper around a new,\n" + + "supported command, which you should use instead:\n" + + "\n" + + f"\t{command}\n" + + "\n" + + "Details: https://github.com/openedx/edx-platform/issues/31895\n" + + "\n" + + ("WARNING: ignoring deprecated flag '--debug'\n" if options.get("debug") else "") + + ("WARNING: ignoring deprecated flag '--force'\n" if options.get("force") else "") + + "⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ \n" + + "\n" + ) + # Print deprecation warning twice so that it's more likely to be seen in the logs. + print(depr_warning) + sh(command) + print(depr_warning) -def _compile_sass(system, theme, debug, force, timing_info): - """ - Compile sass files for the given system and theme. - :param system: system to compile sass for e.g. 'lms', 'cms', 'common' - :param theme: absolute path of the theme to compile sass for. - :param debug: boolean showing whether to display source comments in resulted css - :param force: boolean showing whether to remove existing css files before generating new files - :param timing_info: list variable to keep track of timing for sass compilation +def _compile_sass(system, theme, _debug, _force, _timing_info): """ + This is a DEPRECATED COMPATIBILITY WRAPPER - # Note: import sass only when it is needed and not at the top of the file. - # This allows other paver commands to operate even without libsass being - # installed. In particular, this allows the install_prereqs command to be - # used to install the dependency. - import sass - if system == "common": - sass_dirs = get_common_sass_directories() - else: - sass_dirs = get_sass_directories(system, theme) - - dry_run = tasks.environment.dry_run - - # determine css out put style and source comments enabling - if debug: - source_comments = True - output_style = 'nested' - else: - source_comments = False - output_style = 'compressed' - - for dirs in sass_dirs: - start = datetime.now() - css_dir = dirs['css_destination_dir'] - sass_source_dir = dirs['sass_source_dir'] - lookup_paths = dirs['lookup_paths'] - - if not sass_source_dir.isdir(): - print("\033[91m Sass dir '{dir}' does not exists, skipping sass compilation for '{theme}' \033[00m".format( - dir=sass_source_dir, theme=theme or system, - )) - # theme doesn't override sass directory, so skip it - continue - - if force: - if dry_run: - tasks.environment.info("rm -rf {css_dir}/*.css".format( - css_dir=css_dir, - )) - else: - sh(f"rm -rf {css_dir}/*.css") - - all_lookup_paths = COMMON_LOOKUP_PATHS + lookup_paths - print(f"Compiling Sass: {sass_source_dir} -> {css_dir}") - for lookup_path in all_lookup_paths: - print(f" with Sass lookup path: {lookup_path}") - if dry_run: - tasks.environment.info("libsass {sass_dir}".format( - sass_dir=sass_source_dir, - )) - else: - sass.compile( - dirname=(sass_source_dir, css_dir), - include_paths=all_lookup_paths, - source_comments=source_comments, - output_style=output_style, - ) - - # For Sass files without explicit RTL versions, generate - # an RTL version of the CSS using the rtlcss library. - for sass_file in glob.glob(sass_source_dir + '/**/*.scss'): - if should_generate_rtl_css_file(sass_file): - source_css_file = sass_file.replace(sass_source_dir, css_dir).replace('.scss', '.css') - target_css_file = source_css_file.replace('.css', '-rtl.css') - sh("rtlcss {source_file} {target_file}".format( - source_file=source_css_file, - target_file=target_css_file, - )) - - # Capture the time taken - if not dry_run: - duration = datetime.now() - start - timing_info.append((sass_source_dir, css_dir, duration)) - return True - - -def should_generate_rtl_css_file(sass_file): - """ - Returns true if a Sass file should have an RTL version generated. + It exists to ease the transition for Tutor in Redwood, which directly imported and used this function. """ - # Don't generate RTL CSS for partials - if path(sass_file).name.startswith('_'): - return False - - # Don't generate RTL CSS if the file is itself an RTL version - if sass_file.endswith('-rtl.scss'): - return False - - # Don't generate RTL CSS if there is an explicit Sass version for RTL - rtl_sass_file = path(sass_file.replace('.scss', '-rtl.scss')) - if rtl_sass_file.exists(): - return False - - return True + command = shlex.join( + [ + "npm", + "run", + "compile-sass", + "--", + *(["--dry"] if tasks.environment.dry_run else []), + *(["--skip-default", "--theme-dir", str(theme.parent), "--theme", str(theme.name)] if theme else []), + ("--skip-cms" if system == "lms" else "--skip-lms"), + ] + ) + depr_warning = ( + "\n" + + "⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ \n" + + "\n" + + "WARNING: 'pavelib/assets.py' is DEPRECATED! It will be removed before Sumac.\n" + + "The function you called is just a temporary wrapper around a new, supported command,\n" + + "which you should use instead:\n" + + "\n" + + f"\t{command}\n" + + "\n" + + "Details: https://github.com/openedx/edx-platform/issues/31895\n" + + "\n" + + "⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ \n" + + "\n" + ) + # Print deprecation warning twice so that it's more likely to be seen in the logs. + print(depr_warning) + sh(command) + print(depr_warning) def process_npm_assets(): @@ -582,18 +243,6 @@ def process_xmodule_assets(): print("\t\tWhen paver is removed from edx-platform, this step will not replaced.") -def restart_django_servers(): - """ - Restart the django server. - - `$ touch` makes the Django file watcher thinks that something has changed, therefore - it restarts the server. - """ - sh(cmd( - "touch", 'lms/urls.py', 'cms/urls.py', - )) - - def collect_assets(systems, settings, **kwargs): """ Collect static assets, including Django pipeline processing. @@ -744,55 +393,57 @@ def listfy(data): @task @cmdopts([ - ('background', 'b', 'Background mode'), - ('settings=', 's', "Django settings (defaults to devstack)"), + ('background', 'b', 'DEPRECATED. Use shell tools like & to run in background if needed.'), + ('settings=', 's', "DEPRECATED. Django is not longer invoked to compile JS/Sass."), ('theme-dirs=', '-td', 'The themes dir containing all themes (defaults to None)'), - ('themes=', '-t', 'The themes to add sass watchers for (defaults to None)'), - ('wait=', '-w', 'How long to pause between filesystem scans.') + ('themes=', '-t', 'DEPRECATED. All themes in --theme-dirs are now watched.'), + ('wait=', '-w', 'DEPRECATED. Watchdog\'s default wait time is now used.'), ]) @timed def watch_assets(options): """ Watch for changes to asset files, and regenerate js/css + + This is a DEPRECATED COMPATIBILITY WRAPPER. Use `npm run watch` instead. """ # Don't watch assets when performing a dry run if tasks.environment.dry_run: return - settings = getattr(options, 'settings', Env.DEVSTACK_SETTINGS) - - themes = get_parsed_option(options, 'themes') - theme_dirs = get_parsed_option(options, 'theme_dirs', []) - - default_wait = [str(DEFAULT_OBSERVER_TIMEOUT)] - wait = float(get_parsed_option(options, 'wait', default_wait)[0]) - - if not theme_dirs and themes: # lint-amnesty, pylint: disable=no-else-raise - # We can not add theme sass watchers without knowing the directory that contains the themes. - raise ValueError('theme-dirs must be provided for watching theme sass.') - else: - theme_dirs = [path(_dir) for _dir in theme_dirs] - - sass_directories = get_watcher_dirs(theme_dirs, themes) - observer = Observer(timeout=wait) - - SassWatcher().register(observer, sass_directories) - - print("Starting asset watcher...") - observer.start() - - # Run the Webpack file system watcher too - execute_webpack_watch(settings=settings) - - 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 control-c - try: - while True: - observer.join(2) - except KeyboardInterrupt: - observer.stop() - print("\nStopped asset watcher.") + theme_dirs = ':'.join(get_parsed_option(options, 'theme_dirs', [])) + command = shlex.join( + [ + *( + ["env", f"EDX_PLATFORM_THEME_DIRS={theme_dirs}"] if theme_dirs else [] + ), + "npm", + "run", + "watch", + ] + ) + depr_warning = ( + "\n" + + "⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ \n" + + "\n" + + "WARNING: 'paver watch_assets' is DEPRECATED! It will be removed before Sumac.\n" + + "The command you ran is now just a temporary wrapper around a new,\n" + + "supported command, which you should use instead:\n" + + "\n" + + f"\t{command}\n" + + "\n" + + "Details: https://github.com/openedx/edx-platform/issues/31895\n" + + "\n" + + ("WARNING: ignoring deprecated flag '--debug'\n" if options.get("debug") else "") + + ("WARNING: ignoring deprecated flag '--themes'\n" if options.get("themes") else "") + + ("WARNING: ignoring deprecated flag '--settings'\n" if options.get("settings") else "") + + ("WARNING: ignoring deprecated flag '--background'\n" if options.get("background") else "") + + "⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ \n" + + "\n" + ) + # Print deprecation warning twice so that it's more likely to be seen in the logs. + print(depr_warning) + sh(command) + print(depr_warning) @task diff --git a/pavelib/paver_tests/test_assets.py b/pavelib/paver_tests/test_assets.py index 5722b2062478..029a8db67c4a 100644 --- a/pavelib/paver_tests/test_assets.py +++ b/pavelib/paver_tests/test_assets.py @@ -8,7 +8,6 @@ import ddt import paver.tasks from paver.easy import call_task, path -from watchdog.observers import Observer from pavelib.assets import COLLECTSTATIC_LOG_DIR_ARG, collect_assets @@ -197,86 +196,6 @@ def test_compile_theme_sass(self, options): assert len(self.task_messages) == len(expected_messages) -class TestPaverWatchAssetTasks(TestCase): - """ - Test the Paver watch asset tasks. - """ - - def setUp(self): - self.expected_sass_directories = [ - path('common/static/sass'), - path('common/static'), - path('node_modules/@edx'), - path('node_modules'), - path('lms/static/sass/partials'), - path('lms/static/sass'), - path('lms/static/certificates/sass'), - path('cms/static/sass'), - path('cms/static/sass/partials'), - ] - - # Reset the options that paver stores in a global variable (thus polluting tests) - if 'pavelib.assets.watch_assets' in paver.tasks.environment.options: - del paver.tasks.environment.options['pavelib.assets.watch_assets'] - - super().setUp() - - def tearDown(self): - self.expected_sass_directories = [] - super().tearDown() - - def test_watch_assets(self): - """ - Test the "compile_sass" task. - """ - with patch('pavelib.assets.SassWatcher.register') as mock_register: - with patch('pavelib.assets.Observer.start'): - with patch('pavelib.assets.execute_webpack_watch') as mock_webpack: - call_task( - 'pavelib.assets.watch_assets', - options={"background": True}, - ) - assert mock_register.call_count == 2 - assert mock_webpack.call_count == 1 - - sass_watcher_args = mock_register.call_args_list[0][0] - - assert isinstance(sass_watcher_args[0], Observer) - assert isinstance(sass_watcher_args[1], list) - assert len(sass_watcher_args[1]) == len(self.expected_sass_directories) - - def test_watch_theme_assets(self): - """ - Test the Paver watch asset tasks with theming enabled. - """ - self.expected_sass_directories.extend([ - path(TEST_THEME_DIR) / 'lms/static/sass', - path(TEST_THEME_DIR) / 'lms/static/sass/partials', - path(TEST_THEME_DIR) / 'lms/static/certificates/sass', - path(TEST_THEME_DIR) / 'cms/static/sass', - path(TEST_THEME_DIR) / 'cms/static/sass/partials', - ]) - - with patch('pavelib.assets.SassWatcher.register') as mock_register: - with patch('pavelib.assets.Observer.start'): - with patch('pavelib.assets.execute_webpack_watch') as mock_webpack: - call_task( - 'pavelib.assets.watch_assets', - options={ - "background": True, - "theme_dirs": [TEST_THEME_DIR.dirname()], - "themes": [TEST_THEME_DIR.basename()] - }, - ) - assert mock_register.call_count == 2 - assert mock_webpack.call_count == 1 - - sass_watcher_args = mock_register.call_args_list[0][0] - assert isinstance(sass_watcher_args[0], Observer) - assert isinstance(sass_watcher_args[1], list) - assert len(sass_watcher_args[1]) == len(self.expected_sass_directories) - - @ddt.ddt class TestCollectAssets(PaverTestCase): """