Finding dependencies for Make

How hard is it to get dependencies for your project to use in a Makefile?

Well, it depends.

Make uses “rules” to build object files and executable. A typical rule is of the form target: dependencies, or, for example:

plugins/filter.o: plugins/filter.cpp includes/wstream.hpp plugins/filter.hpp

includes/wstream.hpp:

plugins/filter.hpp:

These rules state that the file plugins/filter.o depends on plugins/filter.cpp and the (presumably) headers includes/wstream.hpp and plugins/filter.hpp. They also say that the headers do not depend on further files.

So, how do we get the rules out of the source code? There are many short answers.

  • If you’re using gcc/g++, you can use its capabilities to output the dependencies and create a number of make-compatible dependency files. The procedure is described here.
  • You can use a tool like makedepend that, however, doesn’t seem that standard. It will generate make-compatible files.

Or you can regex header files out of your source files. This, however, isn’t as straightforward as it sounds. To build the correct rules for make, you have to not only detect include headers but also to find where the corresponding files are. This requires you to know the include path of your program so that if there’s an include somewhere in your sourcecode, it expands to a path explicit enough to feed to make.

If we already have a list of the .cpp files (let’s say you get it from the Makefile), you just need to grep for lines that have includes. Since you can’t guaranty you’ll have the exact standard syntax (you’ll always have some …creative type that will write #   include       <thingie.hpp> // trololol), the grep regular expression must be flexible somewhat. Once you’ve got one of these lines, you extract what you find between < and >. We join everything in a single list by replacing newlines by spaces, and by removing trailing spaces. That would look something like:

# grep: finds lines that begin by #include
# sed: extracts between < >
# tr: replaces \n by space
# sed: removes trailing spaces
headers=$(
          grep -e '^\ *#\ *include' $f \
            | sed 's/.*<\(.*\)>.*/\1/' \
            | tr '\n' ' ' \
            | sed -e 's/[ ]*$//' \
         )

Once we have filenames, we must “qualify” them so that <filter.hpp> is expanded to plugins/filter.hpp in the rule. At first I thought of using find (another command I love to hate), but it doesn’t play well with filenames such as plugins/filter.hpp, because the -name argument must be a basename without dirname. Further, if you have an include path that has -I. -I/some/where -I./yet/somewhere/else, you must probe in these to see where is plugins/filter.hpp. This gives something like:

function exists {
    local where=$1
    local dir=$2
    local name=$3
    for w in ${where[@]}
    do
        local f=$w/$dir/$name
        if [ -f "$f" ]
        then
            echo -n $(joli $f)' '
        fi
    done
}

where where is the list of paths from the include path, dir is “plugins” for the file plugins/filter.cpp but is empty if the file isn’t specified with a path, and name the basename of the header file. joli is a function that replaces runs of multiple slashes by a single slash (something that happens when you concat dirs and filenames).

Lastly follows some pretty-printing that complies to make rule syntax.

*
* *

The changes in the makefile itself are not that important. Indeed:

OBJS=$(SOURCES:.cpp=.o)

-include .depend

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

$(NAME): $(OBJS)
	g++ $(OBJS) $(LDFLAGS) -o $(NAME) $(LIBS)

the $(OBJS) variable creates the default rules for the object files, which are generated from $(SOURCES), a variable that should contain a list of your source files (the .cpp). The variable $(INCLUDES)$ is the usual list of include directories, of the form -I. -I./includes/, etc.

Invoking make depend will call the script and create a .depend file containing the rules. The -include .depend imports the rules into the makefile.

*
* *

There’s a few things that still need work. Some are out of my control. For example, make thinks that an extensionless file (one that doesn’t end in .something) is a target, so that if you have a .cpp file including an extensionless file, it will try to figure out a rule to build it as an executable. Other are things that could be tweaked somehow, like computing the very minimal rules, that seem incomplete, but still build the executable correctly. Not sure it’s very useful, though.

*
* *

Click for the script’s full code

#!/usr/bin/env bash

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

function exists {
    local where=$1
    local dir=$2
    local name=$3
    for w in ${where[@]}
    do
        local f=$w/$dir/$name
        if [ -f "$f" ]
        then
            echo -n $(joli $f)' '
        fi
    done
}

################################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 < >
    # tr: replaces \n by space
    # sed: removes trailing spaces
    echo -n ${f//.*}.o": "$f" "
    headers=$(
        grep -e '^\ *#\ *include' $f \
            | sed 's/.*<\(.*\)>.*/\1/' \
            | tr '\n' ' ' \
            | sed -e 's/[ ]*$//' \
           )
    
    z=$(
    for h in ${headers[@]}
    do
        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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

%d bloggers like this: