diff --git a/WORKSPACE b/WORKSPACE index ed39724..94470f9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -41,7 +41,7 @@ git_repository( name = "rules_python", remote = "https://github.com/bazelbuild/rules_python.git", # tag = "0.4.0", - commit = "f0efec5cf8c0ae16483ee677a09ec70737a01bf5", + commit = "693a1587baf055979493565933f8f40225c00c6d", ) register_toolchains("//tools/python:python3_toolchain") diff --git a/tools/build_rules/prelude_bazel b/tools/build_rules/prelude_bazel index 164e07e..97565b3 100644 --- a/tools/build_rules/prelude_bazel +++ b/tools/build_rules/prelude_bazel @@ -10,6 +10,12 @@ load("//tools/python:defs.bzl", "py_project", ) +load("//tools/sass:defs.bzl", + "multi_sass_binary", + "sass_binary", + "sass_library", +) + load("@arrdem_source_pypi//:requirements.bzl", py_requirement="requirement" ) diff --git a/tools/python/defs.bzl b/tools/python/defs.bzl index dade4f0..b631eea 100644 --- a/tools/python/defs.bzl +++ b/tools/python/defs.bzl @@ -158,6 +158,7 @@ py_resources = rule( def py_project(name=None, main=None, main_deps=None, + main_data=None, shebang=None, lib_srcs=None, lib_deps=None, @@ -218,6 +219,7 @@ def py_project(name=None, name=name, main=main, deps=(main_deps or []) + [lib_name], + data=(main_data or []), imports=[ "src/python", "src/resources", diff --git a/tools/python/requirements.in b/tools/python/requirements.in index 45ce9b1..3692298 100644 --- a/tools/python/requirements.in +++ b/tools/python/requirements.in @@ -17,6 +17,7 @@ icmplib isort jinja2 lark +libsass livereload lxml markdown diff --git a/tools/python/requirements_lock.txt b/tools/python/requirements_lock.txt index 8a4eb52..e63e7f5 100644 --- a/tools/python/requirements_lock.txt +++ b/tools/python/requirements_lock.txt @@ -39,6 +39,7 @@ jsonschema==4.17.3 jsonschema-spec==0.1.4 lark==1.1.5 lazy-object-proxy==1.9.0 +libsass==0.22.0 livereload==2.6.3 lxml==4.9.2 Markdown==3.4.3 diff --git a/tools/sass/BUILD b/tools/sass/BUILD new file mode 100644 index 0000000..fd69945 --- /dev/null +++ b/tools/sass/BUILD @@ -0,0 +1,7 @@ +load("@arrdem_source_pypi//:requirements.bzl", "entry_point") + +alias( + name = "sassc", + actual = entry_point("libsass", "pysassc"), + visibility = ["//visibility:public"], +) diff --git a/tools/sass/defs.bzl b/tools/sass/defs.bzl new file mode 100644 index 0000000..e180c8c --- /dev/null +++ b/tools/sass/defs.bzl @@ -0,0 +1,317 @@ +# Copyright 2018 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"Compile Sass files to CSS" + +_ALLOWED_SRC_FILE_EXTENSIONS = [".sass", ".scss", ".css", ".svg", ".png", ".gif", ".cur", ".jpg", ".webp"] + +# Documentation for switching which compiler is used +_COMPILER_ATTR_DOC = """Choose which Sass compiler binary to use. + +By default, we use the JavaScript-transpiled version of the +dart-sass library, based on https://github.com/sass/dart-sass. +This is the canonical compiler under active development by the Sass team. +This compiler is convenient for frontend developers since it's released +as JavaScript and can run natively in NodeJS without being locally built. +While the compiler can be configured, there are no other implementations +explicitly supported at this time. In the future, there will be an option +to run Dart Sass natively in the Dart VM. This option depends on the Bazel +rules for Dart, which are currently not actively maintained (see +https://github.com/dart-lang/rules_dart). +""" + +SassInfo = provider( + doc = "Collects files from sass_library for use in downstream sass_binary", + fields = { + "transitive_sources": "Sass sources for this target and its dependencies", + }, +) + +def _collect_transitive_sources(srcs, deps): + "Sass compilation requires all transitive .sass source files" + return depset( + srcs, + transitive = [dep[SassInfo].transitive_sources for dep in deps], + # Provide .sass sources from dependencies first + order = "postorder", + ) + +def _sass_library_impl(ctx): + """sass_library collects all transitive sources for given srcs and deps. + + It doesn't execute any actions. + + Args: + ctx: The Bazel build context + + Returns: + The sass_library rule. + """ + transitive_sources = _collect_transitive_sources( + ctx.files.srcs, + ctx.attr.deps, + ) + return [ + SassInfo(transitive_sources = transitive_sources), + DefaultInfo( + files = transitive_sources, + runfiles = ctx.runfiles(transitive_files = transitive_sources), + ), + ] + +def _run_sass(ctx, input, css_output, map_output = None): + """run_sass performs an action to compile a single Sass file into CSS.""" + + # The Sass CLI expects inputs like + # sass + args = ctx.actions.args() + + # Flags (see https://github.com/sass/dart-sass/blob/master/lib/src/executable/options.dart) + args.add_joined(["--style", ctx.attr.output_style], join_with = "=") + + if ctx.attr.sourcemap: + args.add("--sourcemap") + if ctx.attr.sourcemap_embed_sources: + args.add("--sourcemap-contents") + + # Sources for compilation may exist in the source tree, in bazel-bin, or bazel-genfiles. + for prefix in [".", ctx.var["BINDIR"], ctx.var["GENDIR"]]: + args.add("--include-path=%s/" % prefix) + for include_path in ctx.attr.include_paths: + args.add("--include-path=%s/%s" % (prefix, include_path)) + + # Last arguments are input and output paths + # Note that the sourcemap is implicitly written to a path the same as the + # css with the added .map extension. + args.add_all([input.path, css_output.path]) + + ctx.actions.run( + mnemonic = "SassCompiler", + executable = ctx.executable.compiler, + inputs = _collect_transitive_sources([input], ctx.attr.deps), + tools = [ctx.executable.compiler], + arguments = [args], + outputs = [css_output, map_output] if map_output else [css_output], + use_default_shell_env = True, + ) + +def _sass_binary_impl(ctx): + # Make sure the output CSS is available in runfiles if used as a data dep. + if ctx.attr.sourcemap: + map_file = ctx.outputs.map_file + outputs = [ctx.outputs.css_file, map_file] + else: + map_file = None + outputs = [ctx.outputs.css_file] + + _run_sass(ctx, ctx.file.src, ctx.outputs.css_file, map_file) + return DefaultInfo(runfiles = ctx.runfiles(files = outputs)) + +def _sass_binary_outputs(src, output_name, output_dir, sourcemap): + """Get map of sass_binary outputs, including generated css and sourcemaps. + + Note that the arguments to this function are named after attributes on the rule. + + Args: + src: The rule's `src` attribute + output_name: The rule's `output_name` attribute + output_dir: The rule's `output_dir` attribute + sourcemap: The rule's `sourcemap` attribute + + Returns: + Outputs for the sass_binary + """ + + output_name = output_name or _strip_extension(src.name) + ".css" + css_file = "/".join([p for p in [output_dir, output_name] if p]) + + outputs = { + "css_file": css_file, + } + + if sourcemap: + outputs["map_file"] = "%s.map" % css_file + + return outputs + +def _strip_extension(path): + """Removes the final extension from a path.""" + components = path.split(".") + components.pop() + return ".".join(components) + +sass_deps_attr = attr.label_list( + doc = "sass_library targets to include in the compilation", + providers = [SassInfo], + allow_files = False, +) + +_sass_library_attrs = { + "srcs": attr.label_list( + doc = "Sass source files", + allow_files = _ALLOWED_SRC_FILE_EXTENSIONS, + allow_empty = False, + mandatory = True, + ), + "deps": sass_deps_attr, +} + +sass_library = rule( + implementation = _sass_library_impl, + attrs = _sass_library_attrs, +) +"""Defines a group of Sass include files. +""" + +_sass_binary_attrs = { + "src": attr.label( + doc = "Sass entrypoint file", + mandatory = True, + allow_single_file = _ALLOWED_SRC_FILE_EXTENSIONS, + ), + "sourcemap": attr.bool( + default = True, + doc = "Whether source maps should be emitted.", + ), + "sourcemap_embed_sources": attr.bool( + default = False, + doc = "Whether to embed source file contents in source maps.", + ), + "include_paths": attr.string_list( + doc = "Additional directories to search when resolving imports", + ), + "output_dir": attr.string( + doc = "Output directory, relative to this package.", + default = "", + ), + "output_name": attr.string( + doc = """Name of the output file, including the .css extension. + +By default, this is based on the `src` attribute: if `styles.scss` is +the `src` then the output file is `styles.css.`. +You can override this to be any other name. +Note that some tooling may assume that the output name is derived from +the input name, so use this attribute with caution.""", + default = "", + ), + "output_style": attr.string( + doc = "How to style the compiled CSS", + default = "compressed", + values = [ + "expanded", + "compressed", + ], + ), + "deps": sass_deps_attr, + "compiler": attr.label( + doc = _COMPILER_ATTR_DOC, + default = Label(":sassc"), + executable = True, + allow_files = True, + cfg = "host", + ), +} + +sass_binary = rule( + implementation = _sass_binary_impl, + attrs = _sass_binary_attrs, + outputs = _sass_binary_outputs, +) + +def _multi_sass_binary_impl(ctx): + """multi_sass_binary accepts a list of sources and compile all in one pass. + + Args: + ctx: The Bazel build context + + Returns: + The multi_sass_binary rule. + """ + + inputs = ctx.files.srcs + outputs = [] + # Every non-partial Sass file will produce one CSS output file and, + # optionally, one sourcemap file. + for f in inputs: + # Sass partial files (prefixed with an underscore) do not produce any + # outputs. + if f.basename.startswith("_"): + continue + name = _strip_extension(f.basename) + outputs.append(ctx.actions.declare_file( + name + ".css", + sibling = f, + )) + if ctx.attr.sourcemap: + outputs.append(ctx.actions.declare_file( + name + ".css.map", + sibling = f, + )) + + # Use the package directory as the compilation root given to the Sass compiler + root_dir = (ctx.label.workspace_root + "/" if ctx.label.workspace_root else "") + ctx.label.package + + # Declare arguments passed through to the Sass compiler. + # Start with flags and then expected program arguments. + args = ctx.actions.args() + args.add("--style", ctx.attr.output_style) + args.add("--load-path", root_dir) + + if not ctx.attr.sourcemap: + args.add("--no-source-map") + + args.add(root_dir + ":" + ctx.bin_dir.path + '/' + root_dir) + args.use_param_file("@%s", use_always = True) + args.set_param_file_format("multiline") + + if inputs: + ctx.actions.run( + inputs = inputs, + outputs = outputs, + executable = ctx.executable.compiler, + arguments = [args], + mnemonic = "SassCompiler", + progress_message = "Compiling Sass", + ) + + return [DefaultInfo(files = depset(outputs))] + +multi_sass_binary = rule( + implementation = _multi_sass_binary_impl, + attrs = { + "srcs": attr.label_list( + doc = "A list of Sass files and associated assets to compile", + allow_files = _ALLOWED_SRC_FILE_EXTENSIONS, + allow_empty = True, + mandatory = True, + ), + "sourcemap": attr.bool( + doc = "Whether sourcemaps should be emitted", + default = True, + ), + "output_style": attr.string( + doc = "How to style the compiled CSS", + default = "compressed", + values = [ + "expanded", + "compressed", + ], + ), + "compiler": attr.label( + doc = _COMPILER_ATTR_DOC, + default = Label(":sassc"), + executable = True, + cfg = "host", + ), + } +)