Search code examples
makefilesassgnu-makecompass-sass

Make execute a target when only one of the files have changed


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

Solution

  • 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!