1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
|
#!/bin/sh
set -eu
usage() {
cat <<-'EOF'
Usage:
ldev deps FILE...
ldev -h
EOF
}
help() {
cat <<-'EOF'
Options:
-h, --help show this message
FILE toplevel entrypoint file
Given a list of C FILEs, generate the Makefile dependencies
between them.
We have 2 types of object files:
- .o: plain object files;
- .to: compiled with -DTEST so it can expose its embedded unit
tests;
We also have 1 aggregate file:
- .xa: an e**x**ecutable **a**rchive, made with ar(1), that
includes all the .o dependencies required for linking, so that
one can have only the archive as the linker input, i.e.
`cc -o example.bin example.xa`. This way we don't need
separate targets for each executable, and we can instead deal
separately with dependencies and linking, but without specific
file listing for each case. We use an ar-chive to exploit the
fact that a) it can replace existing files with the same name
and b) the $? macro in make(1) gives us all the out-of-date
dependencies, so our rule in the Makefile is usually a simple
`$(AR) $(ARFLAGS) $@ $?`. This way each .xa file lists its
dependency separately, and the building of the .xa file is
taken care of, in the same way that the linkage into an
executable is also taken care of. For running unit tests, we
include as a dependency of "$NAME.xa" the "$NAME.to" file,
which is the object code of "$NAME.c" compiled with the -DTEST
flag. This exposes a main() function for running unit tests.
Also in order to run the unit tests without having to relink
them on each run, we have:
- .bin-check: a dedicated virtual target that does nothing but
execute the tests. In order to assert the binaries exist,
each "$NAME.bin-check" virtual target depends on the
equivalent "$NAME.check" physical target.
There are 2 types of dependencies that are generated:
1. self dependencies;
2. inter dependencies.
The self dependencies are the ones across different
manifestations of the same file so all derived assets are
correctly kept up-to-date:
- $NAME.o $NAME.to: $NAME.h
As the .SUFFIXES rule already covers the dependency to the
orinal $NAME.c file, all we do is say that whenever the public
interface of these binaries change, they need to be
recompiled;
- $NAME.xa: $NAME.to
We make sure to include in each executable archive (.xa) file
its own binary with unit tests. We include the "depN.o"
dependencies later;
- $NAME.bin-check: $NAME.bin
Enforce that the binary exists before we run them.
After we establish the self dependencies, we scrub each file's
content looking for `#include "..."` lines that denote
dependency to other C file. Once we do that we'll have:
- $NAME.o $NAME.to: dep1.h dep2.h ... depN.h
We'll recompile our file when its public header changes. When
only the body of the code changes we don't recompile, only
later relink;
- $NAME.xa: dep1.o dep2.o ... depN.o
Make sure to include all required dependencies in the
$NAME.bin binary so that the later linking works properly.
So if we have file1.c, file2.c and file3.c with their respective
headers, where file2.c and file3.c depend of file1.c, i.e. they
have `#include "file.h"` in their code, and file3.c depend of
file2.c, the expected output is:
file1.o file1.to: file1.h
file2.o file2.to: file2.h
file3.o file3.to: file3.h
file1.xa: file1.to
file2.xa: file2.to
file3.xa: file3.to
file1.bin-check: file1.bin
file2.bin-check: file2.bin
file3.bin-check: file3.bin
file1.o file1.to:
file2.o file2.to: file1.h
file3.o file3.to: file1.h file2.h
file1.xa:
file2.xa: file1.o
file3.xa: file1.o file2.o
This ensures that only the minimal amount of files need to get
recompiled, but no less.
Examples:
Get deps for all files in 'src/' but 'src/main.c':
$ sh tools/cdeps.sh `find src/*.c -not -name 'main.c'`
Emit dependencies for all C files in a Git repository:
$ sh tools/cdeps.sh `git ls-files | grep '\.c$'`
EOF
}
for flag in "$@"; do
case "$flag" in
(--)
break
;;
(--help)
usage
help
exit
;;
(*)
;;
esac
done
while getopts 'h' flag; do
case "$flag" in
(h)
usage
help
exit
;;
(*)
usage >&2
exit 2
;;
esac
done
shift $((OPTIND - 1))
shift
FILE="${1:-}"
eval "$(assert-arg "$FILE" 'FILE')"
each_f() {
fn="$1"
shift
for file in "$@"; do
f="${file%.c}"
"$fn" "$f"
done
printf '\n'
}
self_header_deps() {
printf '%s.o\t%s.to:\t%s.h\n' "$1" "$1" "$1"
}
self_xa_deps() {
printf '%s.xa:\t%s.to\n' "$1" "$1"
}
self_bincheck_deps() {
printf '%s.bin-check:\t%s.bin\n' "$1" "$1"
}
deps_for() {
ext="$2"
for file in $(awk -F'"' '/^#include "/ { print $2 }' "$1.c"); do
if [ "$file" = 'config.h' ]; then
continue
fi
if [ "$(basename "$file")" = 'tests-lib.h' ]; then
continue
fi
f="$(dirname "$1")/$file"
if [ "$f" = "$1.h" ]; then
continue
fi
printf '%s\n' "${f%.h}$2"
done
}
rebuild_deps() {
printf '\n'
printf '%s.o\t%s.to:' "$1" "$1"
printf ' %s' $(deps_for "$1" .h) | sed 's| *$||'
}
archive_deps() {
printf '\n'
printf '%s.xa:' "$1"
printf ' %s' $(deps_for "$1" .o) | sed 's| *$||'
}
each_f self_header_deps "$@"
each_f self_xa_deps "$@"
each_f self_bincheck_deps "$@"
each_f rebuild_deps "$@"
each_f archive_deps "$@"
|