Finding dependencies for Make (part II)

Quite a while ago, I presented my own simple, sed, grep and bash-based make-depend script. It was simple, and somewhat effective, except for the fact that it did not see include-only dependencies nor quote-includes. If you changed a header affecting a lot of things, it would not rebuild, because the make rules were incomplete. Let’s fix this.

The two main things I had to fix were header dependencies and missing files. The first is easily fixed, you just need to parse headers as well. The second isn’t too hard to fix either, depending on how you intend on finding files.

Finding headers relies on two parameters passed to the script. The first one is a series of include paths (that you’d have anyway in a Makefile) and the second the source files (that you’d also have in a Makefile). Typically, your Makefile will have lines such as:

SOURCES= \
	$(wildcard sources/*.cpp)

INCLUDES=-I. -I./includes/ -I./plugins -I/usr/include/boost/

and you invoke the script:

depend:
	@./make-depend.sh "$(INCLUDES)" "$(SOURCES)" > .depend

Then, you can grep your way into the files:

headers=$(
        grep -e '^\ *#\ *include' $f \
            | sed 's/.*\(<\|"\)\(.*\)\(>\|"\).*/\1\2\3/' \
            | tr '\n' ' ' \
            | sed -e 's/[ ]*$//' \
           )

This new version finds both <includes> and “includes”, and keeps the delimiter. I will use that later on to check whether or not the file is missing.

To check if an include exists in your project, I check all the directories pointed by the includes list, and if the script doesn’t find the files, it will check if it’s included within quotes or within <>. If it is quoted, then it must be local, and the script should have found it: it outputs an error about that file not being found. If it is enclosed in <> the script considers that if it includes a point (as in <thingie.hpp>) it’s part of the project and must be found, otherwise it assume it’s some part of the standard library and doesn’t complain.

function exists {
    local where=$1
    local dir=$2
    local name=$3
    local found=false
    
    dequoted_name=$(echo $name | sed 's/\(<\|>\|"\)//g')
    for w in ${where[@]}
    do
        local f=$w/$dir/$dequoted_name
        if [ -f "$f" ]
        then
            echo -n $(joli $f)' '
            found=true
        fi
    done

    # file not found?
    if [ $found = false ]
    then
        # check if it might be in the STL
        # by assuming that if there's a dot in
        # the name, it's user-defined (and if
        # it's in quotation, check anyway)
        #
        if [[ "$name" =~ .*'.'.* || \
              "$name" =~ \".*\" ]]
        then
            echo not found: $name 1>&2
        else
           :; # should do better checking
        fi
    fi

}

*
* 

There are still a number of things to fix. First, the script doesn’t deal with random spaces, for example < spaces >, but that shouldn’t be a problem.

*
* 

Click to deconflapulate:

#!/usr/bin/env bash

function joli {
    # sed: the first symbol after s is the separator
    # replaces multiple consecutives ///// by /
    # then removes initial ./ at the begining
    echo $1 | sed -e 's,/[/]*,/,g' | sed -e 's,^\./,,'
    }

function exists {
    local where=$1
    local dir=$2
    local name=$3
    local found=false
    
    dequoted_name=$(echo $name | sed 's/\(<\|>\|"\)//g')
    for w in ${where[@]}
    do
        local f=$w/$dir/$dequoted_name
        if [ -f "$f" ]
        then
            echo -n $(joli $f)' '
            found=true
        fi
    done

    # file not found?
    if [ $found = false ]
    then
        # check if it might be in the STL
        # by assuming that if there's a dot in
        # the name, it's user-defined (and if
        # it's in quotation, check anyway)
        #
        if [[ "$name" =~ .*'.'.* || \
              "$name" =~ \".*\" ]]
        then
            echo not found: $name 1>&2
        else
           :; # should do better checking
        fi
    fi

}


################################3
includes=$(echo $1 | sed s/-I//g)
files=$2

all_headers=( )

for f in ${files[@]}
do
    # grep: finds lines that begin by #include
    # sed: extracts between < > or " " but keeps delimiters
    # tr: replaces \n by space
    # sed: removes trailing spaces
    echo -n ${f%.*}.o": "$f" "
    headers=$(
        grep -e '^\ *#\ *include' $f \
            | sed 's/.*\(<\|"\)\(.*\)\(>\|"\).*/\1\2\3/' \
            | tr '\n' ' ' \
            | sed -e 's/[ ]*$//' \
           )

    z=$(
    for h in ${headers[@]}
    do
        #echo "$h" 1>&2
        d=$(dirname $h | sed -e 's/^\.//' )
        b=$(basename $h)
        exists "${includes[@]}" "$d" "$b"
    done )

    echo $z # | sed -e 's/\ /\ \\\n/g'
    echo

    all_headers+=( $z )
done


headers=$(echo ${all_headers[@]} | tr ' ' '\n' | sort -u)
for h in ${headers[@]}
do
    echo $h":"
    echo
done


exit 0

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: