-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlinkrule.bash
executable file
·215 lines (199 loc) · 7.06 KB
/
linkrule.bash
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
#!/usr/bin/env bash
# Makefile link rule helper script.
# To print instead of writing to [outfile], set [outfile] below to "-".
#
# Usage: ./linkrule.bash [outfile] [targetname] [rootdep] [headerprefix]
#
# Example: ./linkrule.bash build/deps/main.link.d build/hello_world build/deps/main.d src/
#
# This script can be used by a Makefile of a C project to write a
# program link rule that automatically "knows" the *.o files needed for
# linking, and thus never has to be updated by hand.
#
# This script writes two Make-compatible rules to [outfile]: one for
# [targetname] with a dependency list of all object files needed to
# link it, and one for [outfile] itself with a dependency list of the
# *.d files visited to create the first rule.
#
# This script starts by reading [rootdep]: the *.d file matching the
# *.c file containing the main() function entry point or whatever that
# eventually needs to be linked into a complete program.
# All *.d files in the same directory as [rootdep] refer to *.o files
# that should be linked, and *.h files that remap to *.d files after
# stripping [headerprefix] that should be read recursively.
# The end result is a full list of *.o files to be linked to make a
# program out of the original *.c file.
# *.h files without a matching *.d file are quietly ignored.
#
# The reason that this all works is that *.c, *.h, *.o and *.d files
# virtually always share the same base filename in typical C projects,
# and the *.d files generated by the compiler hold the full dependency
# graph of a project, where *.h files are really just links to other
# *.d files.
#
# As an example, a C Makefile with standard auto-generated dependencies
# might look like this:
#
# src_dir = src/
# build_dir = build/
# objs_dir = $(build_dir)objs/
# deps_dir = $(build_dir)deps/
# c_srcs = $(wildcard $(src_dir)*.c)
# c_objs = $(c_srcs:$(src_dir)%.c=$(objs_dir)%.o)
# c_deps = $(c_srcs:$(src_dir)%.c=$(deps_dir)%.d)
# main_target = $(build_dir)hello_world
#
# $(main_target): $(c_objs)
# $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
#
# $(c_objs): $(objs_dir)%.o: $(src_dir)%.c
# $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
#
# $(c_deps): $(deps_dir)%.d: $(src_dir)%.c
# $(CPP) $(CPPFLAGS) -MM \
# -MT $(@:$(deps_dir)%.d=$(objs_dir)%.o) -MT $@ -MF $@ $<
#
# $(main_target): | $(build_dir)
# $(c_objs): | $(objs_dir)
# $(c_deps): | $(deps_dir)
# #(build_dir) $(objs_dir) $(deps_dir):
# mkdir -p $@
#
# -include $(c_deps)
#
# The $(main_target) may not need every *.o file in $(c_objs).
# Traditionally, this would be fixed by listing the *.o files by hand.
# However, with this script, we can add this to the Makefile:
#
# header_dir = $(src_dir)
# main_link_dep = $(main_src:$(src_dir)%.c=$(deps_dir)%.link.d)
#
# $(main_link_dep): | $(c_deps)
# ./linkrule.bash $@ $(main_target) $(@:.link.d=.d) $(header_dir)
#
# $(main_link_dep): | $(deps_dir)
#
# -include $(main_link_dep)
#
# The $(main_target) link rule now reduces to this (note the lack of
# listed dependencies):
#
# $(main_target):
# $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
#
# The rule with linkrule.bash auto-generates a *.link.d file with the
# list of object files that the $^ variable above expands into.
# The *.link.d file also has a rule to keep itself up-to-date, making
# this whole setup self-maintaining!
#
# In the above example, this file is named build/deps/main.link.d, and
# would contain the following two rules if src/{main,foo,bar}.c existed
# and src/main.c included src/foo.h and src/bar.h, directly or indirectly:
#
# build/hello_world: build/obj/main.o build/obj/foo.o build/obj/bar.o
#
# build/deps/main.link.d: build/deps/main.d build/deps/foo.d build/deps/bar.d
#
# build/deps/main.d:
#
# build/deps/foo.d:
#
# build/deps/bar.d:
#
# Empty fake targets are added to let main.link.d update itself even if
# its dependent *.d files are deleted.
set -Eeuo pipefail
# Check that the script arguments are in order before proceeding.
if [[ $# -lt 3 ]]; then exit 1; fi
if [[ ! ${3} =~ \.d$ ]]; then exit 1; fi
# Extract deps_prefix from the root dependency path, e.g. build/foo.d -> build/
deps_prefix=$(dirname "${3}")/
if [[ ${deps_prefix} == ./ ]]; then
deps_prefix=
fi
# Optional header prefix directory, defaults to empty.
if [[ $# -ge 3 ]]; then
header_prefix=${4}
else
header_prefix=
fi
# Ensure non-empty header_prefix has a trailing slash.
if [[ -n ${header_prefix} && ! ${header_prefix} =~ /$ ]]; then
header_prefix=${header_prefix}/
fi
# Strip sequences of leading "./" from header_prefix.
while [[ ${header_prefix} =~ ^\./(.*)$ ]]; do
header_prefix=${BASH_REMATCH[1]}
done
deps=("${3}")
existing_deps=()
objs=()
d=0
# Loop through each *.d file.
while [[ ${d} -lt ${#deps[*]} ]]; do
#echo DEPEND ${d} "${deps[d]}"
# Check if the *.d file exists; silently skip it if it doesn't.
if [[ -f ${deps[d]} ]]; then
# This dependency file exists; add it to the existing_deps array.
# Ensure that spaces are escaped for the final out file update rule.
existing_deps+=("${deps[d]/ /\\ }")
# Loop through each line in the *.d file.
# Allow backslashes to escape spaces and newlines.
# shellcheck disable=SC2162
while read -a words; do
#echo ..LINE "${words[@]}"
# Loop through whitespace-separated words in the line.
for word in "${words[@]}"; do
#echo ..WORD "${word}"
if [[ ${word} =~ ^(.*\.o)\:?$ ]]; then
#echo NEWOBJ "${BASH_REMATCH[1]}"
# Word matched "*.o"; add it to the object list
# if it's not already there.
newobj=${BASH_REMATCH[1]}
if [[ ! ${objs[*]} =~ (^| )"${newobj}"($| ) ]]; then
#echo ADDOBJ "${newobj}"
objs+=("${newobj}")
fi
elif [[ ${word} =~ ^${header_prefix}(.*)\.h$ ]]; then
#echo NEWDEP "${BASH_REMATCH[1]}"
# Word matched "${header_prefix}*.h"; turn it into
# "${deps_prefix}*.d" and add that to the dependencies
# to search if it's not already there.
newdep=${deps_prefix}${BASH_REMATCH[1]}.d
if [[ ! ${deps[*]} =~ (^| )"${newdep}"($| ) ]]; then
#echo ADDDEP "${newdep}"
deps+=("${newdep}")
fi
fi
done
done <"${deps[d]}"
fi
# Advance to next *.d file.
((++d))
done
# Emit the Makefile rules for the link target and dependency file, e.g.
#
# foo: bar.o baz.o
#
# foo.link.d: foo.d bar.d baz.d
if [[ ${1} == - ]]; then
# If the output file is "-", print to standard output.
# Just guess an output filename for display purposes.
guessed_out_file="${deps[0]}"
if [[ ${guessed_out_file} =~ ^(.*)\.d$ ]]; then
guessed_out_file=${BASH_REMATCH[1]}.link.d
fi
printf "%s: %s\n\n%s: %s\n" "${2}" "${objs[*]}" \
"${guessed_out_file}" "${existing_deps[*]}"
for dep in "${existing_deps[@]}"; do
printf "\n%s:\n" "${dep}"
done
else
{
printf "%s: %s\n\n%s: %s\n" "${2}" "${objs[*]}" \
"${1}" "${existing_deps[*]}"
for dep in "${existing_deps[@]}"; do
printf "\n%s:\n" "${dep}"
done
} > "${1}"
fi