Is it possible to depend one rule on two files at the same time and execute the rule only once when either of those files have changed?
I'm adding compilation of SASS source files to my Makefile. I compile them with Compass but it has it's own configuration in which the source and destination folders are specified. Compass is executed without any parameters and compiles all changed SASS files into corresponding CSS files.
Say I have two SASS files:
folder1/file1.scss
folder2/file2.scss
If both files have changed, executing compass only once compiles both files, no need to execute Compass twice. Can I create a Makefile in which I can execute the compilation only once if either or both file are changed? Say something like this:
folder_out1/file1.css folder_out2/file2.css: folder1/file1.scss folder2/file2.scss
compass compile
EDIT - Amended script that works on FreeBSD (with a few small changes):
#!/usr/bin/env bash
# Start fresh: delete all files and folders
rm -rf folder folder_out Makefile.mock mock_compass
# Create the input folder and files
mkdir folder
echo 1 > folder/file1.scss
echo 2 > folder/file2.scss
# Create mock compass command that creates output directories and copies
# input files to output files without transforming them
echo '
#!/usr/bin/env bash
if ! [ -d folder_out ]; then
mkdir folder_out
fi
cp folder/file1.scss folder_out/file1.css
cp folder/file2.scss folder_out/file2.css' > ./mock_compass
chmod +x mock_compass
# Create the Makefile
echo -e '
# Disable implicit rules to simplify debugging output
.SUFFIXES:
%: %,v
%: RCS/%
%: RCS/%,v
%: s.%
%: SCCS/s.%
folder_out/file1.css folder_out/file2.css: folder/file1.scss folder/file2.scss
\t@echo Running compass
\t./mock_compass
' > Makefile.mock
# Test it!
echo 'Initial build:'
gmake -f Makefile.mock
echo
echo 'Changing only file1'
touch folder/file1.scss
gmake -f Makefile.mock
echo
echo 'Changing only file2'
touch folder/file2.scss
gmake -f Makefile.mock
echo
echo 'Changing both'
touch folder/file1.scss folder/file2.scss
gmake -f Makefile.mock
echo
Yes, those two lines by themselves are a proper Makefile to do exactly what you want.
However, is there any chance that you’re doing this on a Mac? Unfortunately the Mac filesystem has only 1-second file timestamp resolution, which can play havoc with Make in circumstances like this.
Here’s a test script:
#!/bin/bash
# Start fresh: delete all input and output folders
rm -rf folder{1,2,_out{1,2}}
# Create input folders and files
mkdir folder1
mkdir folder2
echo 1 > folder1/file1.scss
echo 2 > folder2/file2.scss
# Create mock compass command that creates output directories and copies
# input files to output files without transforming them
echo '
if ! [ -d folder_out1 ]; then
mkdir folder_out{1,2}
fi
cp folder1/file1.scss folder_out1/file1.css
cp folder2/file2.scss folder_out2/file2.css' > ./mock_compass
chmod +x mock_compass
# Create the Makefile
echo '
# Disable implicit rules to simplify debugging output
.SUFFIXES:
%: %,v
%: RCS/%
%: RCS/%,v
%: s.%
%: SCCS/s.%
folder_out1/file1.css folder_out2/file2.css: folder1/file1.scss folder2/file2.scss
@echo Running compass
./mock_compass
' > Makefile.mock
# Test it!
echo 'Initial build:'
make -f Makefile.mock
echo
echo 'Changing only file1'
touch folder1/file1.scss
make -f Makefile.mock
echo
echo 'Changing only file2'
touch folder2/file2.scss
make -f Makefile.mock
echo
echo 'Changing both'
touch folder1/file1.scss folder2/file2.scss
make -f Makefile.mock
echo
Everything is technically correct, but when you run it on a Mac, you get the incorrect output:
Initial build:
Running compass
./mock_compass
Changing only file1
make[1]: `folder_out1/file1.css' is up to date.
Changing only file2
make[1]: `folder_out1/file1.css' is up to date.
Changing both
make[1]: `folder_out1/file1.css' is up to date.
The input files are changed after the output files are created, but in the same second, so their order of creation is not saved on disk. When Make is re-run it sees the exact same timestamp with both, and assumes they are up-to-date. You can verify this by adding ls -lT
calls in the script, and adding the -d
flag to Make runs.
One hacky workaround is to change the mock_compass
call to this:
./mock_compass && sleep 1 # mac timestamp resolution is 1 second :(
which then causes the test script to produce the correct output:
Initial build:
Running compass
./mock_compass && sleep 1 # mac timestamp resolution is 1 second :(
Changing only file1
Running compass
./mock_compass && sleep 1 # mac timestamp resolution is 1 second :(
Changing only file2
Running compass
./mock_compass && sleep 1 # mac timestamp resolution is 1 second :(
Changing both
Running compass
./mock_compass && sleep 1 # mac timestamp resolution is 1 second :(
but also increases the script’s runtime from 0 to 4 seconds :'(
If you of a better workaound for this, please let me know!