3
\$\begingroup\$

The following is a (working) excerpt from the Makefile I want to use in web projects. I wanted this Makefile to contain all the necessary code and not rely on other self-written files. Partly, because I'm stubborn, partly because I just want to copy it from project to project.

The code uses a simple regexp match to find @import statements in *.scss files recursively and constructs dependency statements for my CSS targets.

I know that I probably should use Wellington for this. But I've been using sassc from before I knew about Wellington. Back then I was using a shell script for finding @import statements and had a Makefile that generated and used -include to generate and include a lot of *.dep files. And again, I felt somewhat stubborn: I wanted to make this work.

In my first attempt, I tried to write the recursion with pure GNU Makefile syntax. Besides the headache this was causing for me, this quickly started to look unreadable. So I switched to generating the dependency rules with Guile.

#    - Compiles scss files in $(sass-dir) to $(css-dir). $(sass-lib-dir)
#      may contain extra libraries not immediately part of the project for
#      inclusion. This tracks `@import` directives.  CAUTION: With the
#      current implementation this may break if any path involves spaces.

# [...]

sass-extension := scss
sass-dir := scss
sass-lib-dir := ~/share/bak.mdl/
css-dir := css

sass-cmd := sassc
sass-extra-args :=

# [...]

###########################################################################
.SUFFIXES: # Delete the default suffixes
.DEFAULT_GOAL := development

.PHONY: development
development:  css #optimize-img

# [...]

###########################################################################
# SASS

sass-srcs := $(wildcard $(sass-dir)/[^_]*.$(sass-extension))
sass-tgts := $(patsubst $(sass-dir)/%.scss,$(css-dir)/%.css,$(sass-srcs))

sass-src-subs := $(shell find $(sass-dir) -type f -name '_*.$(sass-extension)')

sass-libs := $(shell find $(sass-lib-dir) -type f -name '_*.$(sass-extension)')


# Getting dirnames and sorting them, making sure that local ones come
# before library ones.
sass-inc-dirs-local := $(sort $(foreach f,$(sass-src-subs),$(dir $f)))
sass-inc-dirs-lib := $(sort $(foreach f,$(sass-libs),$(dir $f)))
sass-inc-dirs := $(sass-inc-dirs-local) $(sass-inc-dirs-lib)

# Replace space separators with `:`
nullstring :=
space := $(nullstring) # end of line
sass-inc-path := $(subst $(space),:,$(sass-inc-dirs))


# Scheme for parsing sass files (simple regex match) for @import
# statements. Returns list of file that are dependencies. Below this is
# used to construct an `$(eval ...)`
define SASS-GUILE
(use-modules (ice-9 format))
(use-modules (ice-9 rdelim))
(use-modules (ice-9 regex))


(define extension (gmk-expand "$(sass-extension)"))


(define files
  (string-split (gmk-expand "$(sass-src-subs) $(sass-libs)") #\ ))


(define (find-inc-file ref)
  (let ((r (filter (lambda (x)
                     (string-match (format #f "_~a.~a$$" ref extension)
                                   x))
                   files)))
    (if (null? r)
        #f
        (car r))))


(define (get-imports file)
  (define regex
    (make-regexp "^[[:blank:]]*@import[[:blank:]]+[\"']([-_[:alnum:]]+)[\"']"))

  (if (access? file R_OK)
      (call-with-input-file file
        (lambda (port)
          (let loop ((res '()))
            (let ((l (read-line port)))
              (if (eof-object? l)
                  (reverse res)
                  (let ((match (regexp-exec regex l)))
                    (if match
                        (loop (cons (match:substring match 1)
                                    res))
                        (loop res))))))))
      (throw 'file-error "Couldn't open file:" file)))


(define (gather-dependencies file)
  (let loop ((open (list file))
             (done '())
             (deps '()))
    (cond ((null? open)
           (format #f "~{~a~^ ~}" (reverse! deps)))
          ((member (car open) done)
           (loop (cdr open) done deps))
          (else (let ((imports (get-imports (car open))))
                  (if (null? imports)
                      (loop (cdr open)
                            (cons (car open)
                                  done)
                            (cons (car open) deps))
                      (loop (append (map find-inc-file
                                         imports)
                                    (cdr open))
                            (cons (car open)
                                  done)
                            (cons (car open) deps))))))))
endef

# Load the guile code and make it available:
$(guile $(SASS-GUILE))

.PHONY: css
css: $(sass-tgts)

css/%.css: scss/%.scss
        @echo Compiling $@
        @$(sass-cmd) -I $(sass-inc-path) $(sass-extra-args) $< > $@

# Create and eval dependency rule for $(1)
define make-sass-dep
$(eval $(patsubst $(sass-dir)/%.$(sass-extension),$(css-dir)/%.css,$(1)):\
$(guile (gather-dependencies "$(1)")))
endef

$(foreach f,$(sass-srcs),$(call make-sass-dep,$f))

Although I like GNU Make, writing Makefiles is still a mystery to me. I'm relying on trial-and-error way too much. I'd like to know, if I could have been doing this in a more maintainable/readable way. Or if this could actually be easily achieved in standard Makefile syntax. Or if there is a way that executes faster and/or is more robust.

\$\endgroup\$
2
\$\begingroup\$

I don't know SASS, but this inline generation of dependencies has one big weakness: you read every source file, even if it hasn't changed, and even if it's irrelevant to the target being built.

The usual approach to dependencies is to write them into files (usually named for the target filename stem with .d or .dep suffix). Dependency files usually indicate when they themselves need to be rebuilt - i.e. they contain rules specifying which files were used to generate them.


One other point I'd make is about "pretty" commands like this:

        @echo Compiling $@
        @$(sass-cmd) -I $(sass-inc-path) $(sass-extra-args) $< > $@

The hiding of the actual command that is run ought to be optional, as it can be vital to see exactly what's run when you're debugging the build. I'd change the @echo and the other prefix @ to be variables:

ifdef verbose
describe := @true
quiet :=
else
describe := @echo
quiet := @
endif

css/%.css: scss/%.scss
        $(describe) Compiling $@
        $(quiet)$(sass-cmd) -I $(sass-inc-path) $(sass-extra-args) $< > $@
\$\endgroup\$
2
  • \$\begingroup\$ Thank you! I'm trying to implement both. -- Well, I let it read every source file, because all the (C-related) articles on make that I read were relying on gcc producing the dependencies and I figured that would be the equivalent of that. After a lot of trial-and-error I found a way using .d files where only files upward in a dependency graph are read. There are other problems, too. I don't know how to proceed: Should I append the revision to my questions? I don't want to abuse this forum for my personal quality control. OTOH, if I were googling for this, I'd want to see the second version. \$\endgroup\$ Apr 25 '18 at 7:54
  • \$\begingroup\$ The best advice is in I improved my code based on the reviews. What next?. In short, if you want a review of your modified code, you're invited to post it with the improvements, and encouraged to add links between the two questions. If you look through the questions, you'll find a few examples of re-review with improvements. You might want to wait a few days to see if you get any more answers first (though makefile is a fairly quiet corner of the site, so don't be too hopeful). \$\endgroup\$ Apr 25 '18 at 8:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.