diff --git a/pavelib/assets.py b/pavelib/assets.py index ad933de19596..7569d97baff3 100644 --- a/pavelib/assets.py +++ b/pavelib/assets.py @@ -436,8 +436,6 @@ def compile_sass(options): '/edx/app/edxapp/edx-platform/themes' and '/edx/app/edxapp/edx-platform/common/test/'. """ - 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', []) @@ -449,155 +447,35 @@ def compile_sass(options): 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") + sh( + "scripts/assets/compile-scss.sh" + + ("".join(f" --theme {path(theme_dir)}" for theme in themes)) + + (" --skip-lms" if "lms" not in systems else "") + + (" --skip-cms" if "cms" not in systems else "") + + (" --dev" if options.get("debug") else "") + + (" --force" if options.get("force") else "") + + (" --dry" if tasks.environment.dry_run else "") + ) 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 system: system to compile sass for: 'lms' or 'cms'; any other value is a no-op. :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 + :param timing_info: no longer supported; no effect. """ - - # 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") - - 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=COMMON_LOOKUP_PATHS + 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. - """ - # 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 + sh( + "scripts/assets/compile-scss.sh" + + (f" --theme {theme} --skip-default-theme" if theme else "") + + (" --skip-lms" if system != "lms") + + (" --skip-cms" if system != "cms") + + (" --dev" if debug else "") + + (" --force" if force else "") + ) def process_npm_assets(): diff --git a/scripts/assets/compile-scss-dir.sh b/scripts/assets/compile-scss-dir.sh new file mode 100755 index 000000000000..6e029ee3d304 --- /dev/null +++ b/scripts/assets/compile-scss-dir.sh @@ -0,0 +1,133 @@ +#!/bin/sh +HELP="Recursively compile SCSS in one directory." + +# Enable stricter sh behavior. +set -eu + +USAGE="\ +USAGE:\n\ + $0 [OPTIONS] [] [OPTIONS]\n\ +\n\ +ARGUMENTS:\n\ + SCSS_SRC Source directory with SCSS\n\ + CSS_DEST Target directory for output CSS\n\ + INCLUDE_PATHS Colon-separated list of SCSS import roots\n\ +\n\ +OPTIONS:\n\ + -d, --dev Dev mode: don't compress output CSS\n\ + -r, --dry Dry run: don't execute commands (pairs well with -v)\n\ + -v, --verbose Print commands as they are executed.\n\ + -h, --help Display this.\n\ +" + +scss_src="" +css_dest="" +include_paths="" +output_options="--output-style=compressed" + +# Flags. +# Empty string (-z) => false +# Nonempty string (-n) => true +verbose="" +dry="" + +# Parse arguments and options. +while [ $# -gt 0 ]; do + case $1 in + -d|--dev) + output_options="--output-style=nested" + # TODO: When moving from `sass.compile(...)` to `sassc`, we had to stop using + # the " --source-comments" option because it is not available + # in the `sassc` CLI under libsass==0.10. After upgrading to libsass>=0.11, + # we should add back " --source-comments" when in --dev mode. + shift + ;; + -v|--verbose) + verbose="T" + shift + ;; + -r|--dry) + dry="T" + shift + ;; + -h|--help) + echo "$HELP" + echo + echo "$USAGE" + exit 0 + ;; + -*) + echo "Error: Unrecognized option: $1" + echo + echo "$USAGE" + exit 1 + ;; + *) + if [ -z "$scss_src" ] ; then + scss_src="$1" + elif [ -z "$css_dest" ] ; then + css_dest="$1" + elif [ -z "$include_paths" ] ; then + include_paths="$1" + else + echo "Error: unexpected argument: $1" + echo "$USAGE" + exit 1 + fi + shift + ;; + esac +done + +if [ -n "$verbose" ] ; then + set -x +fi + +if [ -z "$scss_src" ] || [ -z "$css_dest" ] ; then + echo "Error: SCSS source dir and CSS destination dir are required." + echo "$USAGE" + exit 1 +fi + +# Convert include paths into string of options. +# Include paths are colon-separated, so we can replace the colons with ' --include-path=' +# and then prepend one more '--include_path=' to the entire string, if nonempty. +include_path_options="$(echo "$include_paths" | sed -n 's/:/ --include-path=/pg')" +if [ -n "$include_path_options" ] ; then + include_path_options="--include-path=$include_path_options" +fi + +# Navigate into `scss_src` and recursively print out relative paths for all SCSS +# files, excluding underscore-prefixed ones, using `sed` to chop off the file extension. +# For each filepath, run `sassc` and, if appropriate, `rtlcss`. +# TODO: Unlike its Python API, libsass-python's CLI does not support compiling entire +# directories, so we must implement that logic ourselves. After we upgrade +# to node-sass or dart-sass, though, this logic might be able to be simplified. +for rel_path in $(cd "$scss_src" && find . \( -name \*.scss -and \! -name _\* \) | sed -n 's/.scss$//p') ; do + + # Make sure the destination directory exists. + mkdir -p "$(dirname "$css_dest/$rel_path")" + + # Compile one SCSS file into a CSS file. + # Note that scssc's $..._options arguments are not quoted, because they + # may contain multiple arguments, which we want to split apart rather than + # pass as one big argument. Hence the shellcheck disable directive. + # shellcheck disable=2086 + [ -n "$dry" ] || \ + sassc \ + $output_options \ + $include_path_options \ + "$scss_src/$rel_path.scss" \ + "$css_dest/$rel_path.css" + + # Generate converted RTL css too, if relevant. + case "$rel_path" in + *-rtl) + # SCSS is already RTL; no need to generate extra RTL file. + ;; + *) + # Generate RTL CSS from LTR CSS, appending -rtl to file name. + [ -n "$dry" ] || rtlcss "$css_dest/$rel_path.css" "$css_dest/$rel_path-rtl.css" + ;; + esac +done diff --git a/scripts/assets/compile-scss.sh b/scripts/assets/compile-scss.sh new file mode 100755 index 000000000000..2ea44d3d8a70 --- /dev/null +++ b/scripts/assets/compile-scss.sh @@ -0,0 +1,237 @@ +#!/bin/sh +HELP="Compile SCSS files for LMS and CMS, for default and/or custom themes." +# +# Run this from the root of edx-platform, after ... TOOD +# but before ... TODO + +# Enable stricter sh behavior. +set -eu + +USAGE="\ +USAGE:\n\ + $0 [OPTIONS]\n\ +\n\ +OPTIONS:\n\ + -t, --theme Path to a custom theme; can be provided multiple times\n\ + -L, --skip-lms Don't compile LMS-specific SCSS\n\ + -C, --skip-cms Don't compile CMS-specific SCSS\n\ + -D, --skip-default-theme Don't compile SCSS for the default theme\n\ + -n, --node-modules Path to installed node_modules directory\n\ + Defaults to ./node_modules\n\ + -f, --force Remove existing css before generating new css\n\ + -d, --dev Dev mode: Don't compress output CSS\n\ + -r, --dry Dry run: don't actually execute any changes\n\ + -v, --verbose Print commands as they are executed\n\ + -h, --help Display this\n\ +" + +# List of paths to custom themes, newline-separated. +theme_paths="" + +# By default, we look for node_modules in the current directory. +# Some Open edX distributions may want node_modules to be located somewhere +# else, so we let this be configured with -n|--node-modules. +node_modules_path="./node_modules" + +# Flags. +# Empty string (-z) => false +# Nonempty string (-n) => true +skip_lms="" +skip_cms="" +skip_default_theme="" +force="" +dev="" +dry="" +verbose="" + +# Parse arguments and options. +while [ $# -gt 0 ]; do + case $1 in + -t|--theme) + shift + if [ $# -eq 0 ] || [ ! -d "$1" ]; then + echo "Error: provided theme path is not a directory: ${1:-}" + echo + echo "$USAGE" + exit 1 + fi + theme_paths="$theme_paths\n$1" + shift + ;; + -L|--skip-lms) + skip_lms="T" + shift + ;; + -C|--skip-cms) + skip_cms="T" + shift + ;; + -D|--skip-default-theme) + skip_default_theme="T" + shift + ;; + -n|--node-modules) + shift + if [ $# -eq 0 ]; then + echo "Error: Missing value for -n/--node-modules" + echo + echo "$USAGE" + exit 1 + fi + node_modules_path="$1" + shift + ;; + -f|--force) + force="T" + shift + ;; + -d|--dev) + dev="T" + shift + ;; + -r|--dry) + dry="T" + shift + ;; + -v|--verbose) + verbose="T" + shift + ;; + -h|--help) + echo "$HELP" + echo + echo "$USAGE" + exit 0 + ;; + *) + echo "Error: Unrecognized option: $1" + echo + echo "$USAGE" + exit 1 + ;; + esac +done + +compile_dir_opts="" +if [ -n "$dev" ] ; then + compile_dir_opts="$compile_dir_opts --dev" +fi +if [ -n "$dry" ] ; then + compile_dir_opts="$compile_dir_opts --dry" +fi +if [ -n "$verbose" ] ; then + compile_dir_opts="$compile_dir_opts --verbose" +fi +compile_dir ( ) { + # Shellcheck will complain that $compile_dir_opts is unquoted. + # It's intentional: we want $compile_dir_opts to be split on spaces and + # passed as multiple arguments. Quoting it would pass it as a single argument. + # shellcheck disable=2086 + scripts/assets/compile-scss-dir.sh $compile_dir_opts "$1" "$2" "$3" +} + +echo "-------------------------------------------------------------------------" +echo " Compiling SCSS..." +echo +echo " Working directory : $(pwd)" +echo "-------------------------------------------------------------------------" + + +lms_scss="lms/static/sass" +lms_partials="lms/static/sass/partials" +cms_scss="cms/static/sass" +cms_partials="cms/static/sass/partials" +certs_scss="lms/static/certificates/sass" + +lms_css="lms/static/css" +cms_css="cms/static/css" +certs_css="lms/static/certificates/css" + +common_includes="common/static:common/static/sass:$node_modules_path:$node_modules_path/@edx" +lms_includes="$common_includes:$lms_scss:$lms_partials" +cms_includes="$common_includes:$cms_scss:$cms_partials:$lms_partials" +certs_includes="$lms_includes" + +if [ -n "$verbose" ] ; then + set -x +fi + +if [ -z "$skip_default_theme" ] ; then + echo "Compiling SCSS for default theme..." + if [ -n "$force" ] ; then + echo " Removing existing generated CSS first." + [ -n "$dry" ] || rm -rf "$lms_css" "$cms_css" "$certs_css" + fi + if [ -z "$skip_lms" ] ; then + echo " Compiling default LMS SCSS." + compile_dir "$lms_scss" "$lms_css" "$lms_includes" + echo " Compiling default certificates SCSS." + compile_dir "$certs_scss" "$certs_css" "$certs_includes" + fi + if [ -z "$skip_cms" ] ; then + echo " Compiling default CMS SCSS." + compile_dir "$cms_scss" "$cms_css" "$cms_includes" + fi + echo "Done compiling SCSS for default theme." +fi + +echo "$theme_paths" | while read -r theme_path ; do + + if [ -z "$theme_path" ] ; then + continue + fi + + theme_lms_scss="$theme_path/lms/static/sass" + theme_lms_partials="$theme_path/lms/static/sass/partials" + theme_cms_scss="$theme_path/cms/static/sass" + theme_cms_partials="$theme_path/cms/static/sass/partials" + theme_certs_scss="$theme_path/lms/static/certificates/sass" + + theme_lms_css="$theme_path/lms/static/css" + theme_cms_css="$theme_path/cms/static/css" + theme_certs_css="$theme_path/lms/static/certificates/css" + + theme_lms_includes="$lms_includes:$theme_lms_partials" + theme_cms_includes="$cms_includes:$theme_cms_partials" + theme_certs_includes="$common_includes:$theme_lms_scss:$theme_lms_partials" + + echo "Compiling SCSS for custom theme at $theme_path..." + if [ -n "$force" ] ; then + echo " Removing theme's existing generated CSS first." + [ -n "$dry" ] || rm -rf "$theme_lms_css" "$theme_cms_css" "$theme_certs_css" + fi + + if [ -z "$skip_lms" ] ; then + echo " Compiling default LMS SCSS into theme's CSS directory." + compile_dir "$lms_scss" "$theme_lms_css" "$theme_lms_includes" + if [ -d "$theme_lms_scss" ] ; then + echo " Compiling theme's LMS SCSS into theme's CSS directory." + compile_dir "$theme_lms_scss" "$theme_lms_css" "$theme_lms_includes" + else + echo " Theme has no LMS SCSS; skipping." + fi + if [ -d "$theme_certs_scss" ] ; then + echo " Compiling theme's certificate SCSS into theme's CSS directory." + compile_dir "$theme_certs_scss" "$theme_certs_css" "$theme_certs_includes" + else + echo " Theme has no certificate SCSS; skipping." + fi + fi + if [ -z "$skip_cms" ] ; then + echo " Compiling default CMS SCSS into theme's CSS directory." + compile_dir "$cms_scss" "$theme_cms_css" "$theme_cms_includes" + if [ -d "$theme_cms_scss" ] ; then + echo " Compiling theme's CMS SCSS into theme's CSS directory." + compile_dir "$theme_cms_scss" "$theme_cms_css" "$theme_cms_includes" + else + echo " Theme has no CMS SCSS; skipping." + fi + fi + + echo "Done compiling SCSS for custom theme at $theme_path." +done + +echo "-------------------------------------------------------------------------" +echo " Done compiling SCSS." +echo "-------------------------------------------------------------------------" +