Skip to content

Instantly share code, notes, and snippets.

@emilbjorklund
Last active January 21, 2020 22:27
Show Gist options
  • Save emilbjorklund/77cb39aafd04d60ef1f5 to your computer and use it in GitHub Desktop.
Save emilbjorklund/77cb39aafd04d60ef1f5 to your computer and use it in GitHub Desktop.
Makefile for Sass?
# This is probably some pseudo-makefile syntax, but basically I want to do this:
# - The `assets/scss/` dir has a bunch of "top level" Sass files to
# be compiled - foo.scss, bar.scss etc.
# - Note: these files will each generate one resulting .css file of the
# same name as the source inside the build dir. foo.scss -> foo.css etc.
# - The build needs to be re-run any time any partial inside of a
# subdir in the scss folder changes: if `assets/scss/baz/_baz.scss` changes,
# I want to recompile all of the "root" .scss files.
# I.e. all of the partials in subdirs are prerequisites.
# So I want to run something like this for all the "root" .scss files, using the partials as a prerequisite.
build/css/foo.css: assets/scss/foo.scss assets/scss/*/*.scss
sassc -t compressed assets/scss/foo.scss > build/css/foo.css
# How do I generalize this in a sane way using variables in make?
@richardolsson
Copy link

I usually do a variation of you want to do (and the exact flavor varies from project to project). I adapted a Makefile from a recent project to do what you want to do:

CSS_C=sass
CSS_FLAGS=
CSS_SRC=assets/scss
CSS_OUT=build/css
CSS_TARGETS=$(patsubst $(CSS_SRC)/%.scss,%.css,$(wildcard $(CSS_SRC)/*.scss))

all: clean css

final: CSS_FLAGS = -t compact --sourcemap=none
final: all

css: $(CSS_TARGETS)

.SECONDEXPANSION:
%.css: $(CSS_SRC)/$$*.scss
    $(CSS_C) $(CSS_FLAGS) $? $(CSS_OUT)/$@

clean:
    rm -f $(CSS_OUT)/*

This will create a list of targets in the CSS_TARGET based on the files that exist in the assets/scss folder. For each file.scss, a file.css target is created. Sometimes I hard-code these, but this insane wildcard/pathsubst pattern will achieve automatic look-up.

It then contains a wildcard .css target, which will consume the SCSS file and produce the CSS file in the output folder. If you don't know how these work, RTFM (because I don't know how to explain it any shorter than that anyway). ;)

Note that this is just a Make target to compile your SCSS files. It doesn't run automatically by itself. To make that happen you need some sort of file system watcher. I use watchmedo from the Python watchdog package, which you can install with pip install watchdog. Here's a command that would probably work for your use case:

watchmedo shell-command -c make -p "*.scss* -W -R assets

It will look for all files matching the *.scss pattern in the assets folder recursively, and run the command "make" whenever one changes. The -W flag makes it wait until the command finishes before executing another one. This is useful because OSX dispatches several file system events for a single file change causing the command to be invoked redundantly if watchmedo is not told to wait for it to finish.

@richardolsson
Copy link

Oh, I left the final make target in there for reference, but it's of course not actually necessary to achieve this. I just keep it there and run make final manually, which adds some flags to the call to sass resulting in minified CSS files without source maps.

Same goes for the clean target. It's not required to make the compilation work, but I like to clean before I re-build, especially in the final case where you want the source maps to be removed. This particular clean target might not work for your use case though, depending on your output folder structure.

@emilbjorklund
Copy link
Author

@mattpr
Copy link

mattpr commented Sep 10, 2016

Came across this as I was looking to start using make to build my static sites (jekyll hasn't been flexible enough and since I am not a ruby guy...the whole compass ecosystem isn't my thing either).

I noticed that your approach above causes every css to be rebuilt every time even if the corresponding scss hasn't changed. This is because the "target" %.css never exists, so make runs the rule every time. The file that exists is $(CSS_OUT)/%.css so that needs to be the target if you want make to be clever and not rebuild everything.

Additionally if you want to trigger a rebuild of the top scss file(s) when any of the includes change...they also need to be in the dependency list.

Here's my tweaks (I'm no make expert so happy to get feedback/improvements).

BUILD=build

#
# css
#
CSS_C=sass
CSS_FLAGS=-I $(SCSS_INCLUDES)
CSS_SRC=css
SCSS_INCLUDES=$(CSS_SRC)/includes
CSS_OUT=$(BUILD)/css
CSS_TARGETS=$(patsubst $(CSS_SRC)/%.scss,$(CSS_OUT)/%.css,$(wildcard $(CSS_SRC)/*.scss))

.PHONY: all final css clean

all: clean css

final: CSS_FLAGS += -t compact --sourcemap=none
final: all

clean:
    rm -rf $(BUILD)/*

#
# CSS
#

css: $(CSS_TARGETS)

$(CSS_OUT):
    mkdir -p $(CSS_OUT)

$(CSS_TARGETS): $(CSS_OUT)/%.css : $(CSS_SRC)/%.scss $(SCSS_INCLUDES)/_*.scss | $(CSS_OUT)
    $(CSS_C) $(CSS_FLAGS) $< $@

I'm using static pattern rule instead of the second expansion because it feels cleaner to me.

Changes in detail...

  • CSS_TARGETS now are paths to the actual output files (e.g. build/css/foo.css)
    • This ensures that we only rebuild if the file is out of date (not there, older timestamp than dependencies)
  • We use the middle pattern ($(CSS_OUT)/%.css) to extract the name of the target (e.g. "foo")
  • This is then injected as the first dep ($(CSS_SRC)/%.scss)
    • Ensures that we only run target when the source scss file changed
  • Added all includes as deps ($(SCSS_INCLUDES)/_*.scss) ensures any changes to an include will cause all CSS_TARGETS to rebuild (whereas a change to top-level scss file will only cause a single css file to be rebuilt).
  • Added an ordered prereq to make sure our output directory exists (| $(CSS_OUT))

https://www.gnu.org/software/make/manual/html_node/Static-Usage.html#Static-Usage

Anyway, I learned some stuff from the comment here so thought I would share back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment