$ rm -ir depSandbox $ mkdir -p depSandbox/foo $ cd depSandbox/foo
include /etc/depsyntax cc -c -o foo.o foo.c cc -o foo foo.o
(dep: Can't read cache file .DepFileCache) Targetting foo foo.c (needed for foo.o) does not exist and has no write rule
#include <stdio.h> int main() { printf("Hello from foo\n"); return 0; }
(dep: Can't read cache file .DepFileCache) Targetting foo cc -c -o foo.o foo.c cc -o foo foo.o
This is in one sense unsurprising: those are the commands given in the DepFile. But, being a build tool, dep should be following file dependencies. Where are the dependencies?
This is also unsurprising to anyone who is familiar with C: The input is foo.c, the intermediate is foo.o, and the output is foo. This is all very clear from the commands.
dep determines the inputs and outputs by analysing the build command. To do so, it uses a hint in the file /etc/depsyntax, which is the first thing it included and read. One line of that file is:
syntax -mixed cc -g -f: -I:%includedir -W: -o:%out \ -c%findincludes -l:%library -L:%libdir -.* %in
Among other things, this tells dep that cc's -o option indicates an output file (%out), and that all non-option parameters are input files (%in).
So you don't actually have to specify the inputs and outputs of a command. dep can analyse the command and figure them out for itself.
(Some commands aren't quite so clear as to what their inputs or (especially) outputs are; more on those later.)
Hello from foo
#include <stdio.h> #include "bar.h" int main() { printf("Hello from foo\\n"); printf("bar() says %d\\n", bar()); return 0; }
#ifndef BAR_H #define BAR_H int bar(); #endif
#include "bar.h" int bar() { return 17; }
include /etc/depsyntax cc -c -o foo.o foo.c cc -c -o bar.o bar.c cc -o foo foo.o bar.o
Targetting foo cc -c -o bar.o bar.c cc -c -o foo.o foo.c cc -o foo foo.o bar.o
Targetting foo dep: all targets are up to date
$ touch bar.h
Targetting foo cc -c -o bar.o bar.c cc -c -o foo.o foo.c cc -o foo foo.o bar.o
Note that dep recompiled and relinked those programs due to the touched file bar.h, which is not mentioned in the DepFile. That is, it automatically included bar.h in its file dependency graph.
(make can do this, with some extra include directives, and the help of a compiler option that can output make's dependency syntax. (The tool ninja can also read make's dependency syntax. (Which I find somewhat strange. (Please excuse all these brackets.))))
dep takes a simpler approach: it reads the #include directives directly from the source files. Note that dep doesn't have a C++ parser hard-coded in; instead, it is in the configuration file /etc/depsyntax, in this line:
findincludes '#include[ \t]+"(.*)"[ \t]*\r?' *.c *.cpp *.h
It also does not read the entire file, just the first thousand lines (also configurable), and it is not reading the includes every time, it only reads them when the files have changed; when they haven't changed, it instead uses a copy of the includes in its cache file, in lines that read:
fileinfo bar.c OK 2024-05-30 17:46:04.8950837+10 47 failtime 1970-01-01 10:00:00+10 buildseconds 0 includes bar.h fileinfo foo.c OK 2024-05-30 17:46:05.6403056+10 188 failtime 1970-01-01 10:00:00+10 buildseconds 0 includes bar.h ../quux/quux.h
Along with other information, such as: old modification time, old file size, and a few other things.
Also, whereas make internally models include files as extra source files, dep internally models them as actual include files. This more realistic modelling saves a bit of time and space on big projects. Consider a project with ten .c files which all include one .h file that in turn includes ten other .h files: In dep, that requires 30 file relationships; in make, it is 110.
Hello from foo bar() says 17
include /etc/depsyntax cc -c -o %.o %.c cc -o foo foo.o bar.o
Targetting foo cc -c -o foo.o foo.c cc -c -o bar.o bar.c cc -o foo foo.o bar.o
#include <stdio.h> #include "bar.h" #include "quux.h" int main() { printf("Hello from foo\\n"); printf("bar() says %d\\n", bar()); printf("quux() says %d\\n", quux()); return 0; }
$ mkdir ../quux $ cd ../quux
#ifndef QUUX_H #define QUUX_H int quux(); #endif
#include "quux.h" int quux() { return 123; }
include /etc/depsyntax cc -c -o quux.o quux.c mkdir -p ../bin cc -shared -o ../bin/libquux.so quux.o
$ cd ../foo
and edit the
include ../quux/DepFile cc -c -o foo.o -I . -I ../quux foo.c cc -c -o bar.o bar.c mkdir ../bin cc -o ../bin/foo foo.o bar.o -L ../bin -Wl,-rpath,../bin -l quux
Targetting ../bin/foo (in ../quux) cc -c -o quux.o quux.c (in ../quux) mkdir -p ../bin (in ../quux) cc -shared -o ../bin/libquux.so quux.o cc -c -o foo.o -I . -I ../quux foo.c cc -o ../bin/foo foo.o bar.o -L ../bin -Wl,-rpath,../bin -l quux
The --mkdir option causes dep to automatically add existence dependencies between files mkdir commands. Without the option, dep does not add such dependencies, as there are a lot of them, and they are rarely needed.
An existence dependency is a dependency where a target is considered up to date if its source exists. Timestamps and file sizes are not compared any further. Make calls these order-only dependencies.
Hello from foo bar() says 17 quux() says 123
#include "quux.h" int quux() { return 4321; }
Targetting ../bin/foo (in ../quux) cc -c -o quux.o quux.c (in ../quux) cc -shared -o ../bin/libquux.so quux.o
Hello from foo bar() says 17 quux() says 4321
These are existence dependencies again. /etc/depsyntax has the line:
exists_file_ext .so .dll
This means that any source file with the extension .so or .dll gets an existence dependency instead of of an ordinary source dependency.
Targetting ../bin/foo cc -o ../bin/foo foo.o bar.o -L ../bin -Wl,-rpath,../bin -l quux
$ touch -t2312311655 foo.c
Targetting ../bin/foo dep: all targets are up to date
Targetting ../bin/foo cc -c -o foo.o -I . -I ../quux foo.c cc -o ../bin/foo foo.o bar.o -L ../bin -Wl,-rpath,../bin -l quux
$ echo >> foo.c $ touch -t2312311655 foo.c
Targetting ../bin/foo dep: all targets are up to date
Targetting ../bin/foo cc -c -o foo.o -I . -I ../quux foo.c cc -o ../bin/foo foo.o bar.o -L ../bin -Wl,-rpath,../bin -l quux
(dep --sense=all is a shortcut for --sense=newer,rule,modtime,size)
Bison is such a program. It can produce two output files: (1) a generated parser, and (2) a header file that contains token values for interfacing with such things as a lexical scanner, and parse tree- or AST-related code. The header file only changes when the list of tokens change, which compared to the rest of the parser, is not that often. So it makes sense to check that the file really changed, before recompiling its dependents.
In dep, this type of output file is known as a %cmpout file and /etc/depsyntax has one in its syntax for bison:
syntax bison -W: -t -v -o:%out --defines=%cmpout %in
To see this in action, create a minimal bison file,
%define api.prefix {intercal} %{ #include <stdio.h> int intercallex(); void intercalerror(const char *error) { fprintf(stderr, "%s\n", error); } %} %token KEYWORD_INTERLEAVE %% filecontents: KEYWORD_INTERLEAVE { }
Add intercallex() and a #include statement for intercalparse.h to
#include <stdio.h> #include "bar.h" #include "quux.h" #include "intercalparse.h" int main() { printf("Hello from foo\\n"); printf("bar() says %d\\n", bar()); printf("quux() says %d\\n", quux()); return 0; } int intercallex() { return KEYWORD_INTERLEAVE; }
Add
include ../quux/DepFile bison -o intercalparse.c --defines=intercalparse.h intercalparse.y cc -c -o intercalparse.o intercalparse.c cc -c -o foo.o -I . -I ../quux foo.c cc -c -o bar.o bar.c mkdir ../bin cc -o ../bin/foo foo.o bar.o intercalparse.o -L ../bin -l quux
Targetting ../bin/foo bison -o intercalparse.c --defines=intercalparse.h intercalparse.y intercalparse.h changed cc -c -o foo.o -I . -I ../quux foo.c cc -c -o intercalparse.o intercalparse.c cc -o ../bin/foo foo.o bar.o intercalparse.o -L ../bin -l quux
$ touch intercalparse.y
Targetting ../bin/foo bison -o intercalparse.c --defines=intercalparse.h intercalparse.y intercalparse.h did not change cc -c -o intercalparse.o intercalparse.c cc -o ../bin/foo foo.o bar.o intercalparse.o -L ../bin -l quux
Targetting ../bin/foo dep: all targets are up to date
$ touch intercalparse.y
bison -o intercalparse.c --defines=intercalparse.h intercalparse.y intercalparse.h did not change cc -c -o intercalparse.o intercalparse.c
Targetting ../bin/foo cc -o ../bin/foo foo.o bar.o intercalparse.o -L ../bin -l quux
Targetting ../bin/foo dep: all targets are up to date