]>
Commit | Line | Data |
---|---|---|
f67539c2 TL |
1 | #!/bin/sh -e |
2 | ||
3 | # Tool to bundle multiple C/C++ source files, inlining any includes. | |
4 | # | |
5 | # Note: this POSIX-compliant script is many times slower than the original bash | |
6 | # implementation (due to the grep calls) but it runs and works everywhere. | |
7 | # | |
8 | # TODO: ROOTS, FOUND, etc., as arrays (since they fail on paths with spaces) | |
9 | # TODO: revert to Bash-only regex (the grep ones being too slow) | |
10 | # | |
11 | # Author: Carl Woffenden, Numfum GmbH (this script is released under a CC0 license/Public Domain) | |
12 | ||
13 | # Common file roots | |
14 | ROOTS="." | |
15 | ||
16 | # -x option excluded includes | |
17 | XINCS="" | |
18 | ||
19 | # -k option includes to keep as include directives | |
20 | KINCS="" | |
21 | ||
22 | # Files previously visited | |
23 | FOUND="" | |
24 | ||
25 | # Optional destination file (empty string to write to stdout) | |
26 | DESTN="" | |
27 | ||
28 | # Whether the "#pragma once" directives should be written to the output | |
29 | PONCE=0 | |
30 | ||
31 | # Prints the script usage then exits | |
32 | usage() { | |
33 | echo "Usage: $0 [-r <path>] [-x <header>] [-k <header>] [-o <outfile>] infile" | |
34 | echo " -r file root search path" | |
35 | echo " -x file to completely exclude from inlining" | |
36 | echo " -k file to exclude from inlining but keep the include directive" | |
37 | echo " -p keep any '#pragma once' directives (removed by default)" | |
38 | echo " -o output file (otherwise stdout)" | |
39 | echo "Example: $0 -r ../my/path - r ../other/path -o out.c in.c" | |
40 | exit 1 | |
41 | } | |
42 | ||
43 | # Tests that the grep implementation works as expected (older OSX grep fails) | |
44 | test_deps() { | |
45 | if ! echo '#include "foo"' | grep -Eq '^\s*#\s*include\s*".+"'; then | |
46 | echo "Aborting: the grep implementation fails to parse include lines" | |
47 | exit 1 | |
48 | fi | |
49 | if ! echo '"foo.h"' | sed -E 's/"([^"]+)"/\1/' | grep -Eq '^foo\.h$'; then | |
50 | echo "Aborting: sed is unavailable or non-functional" | |
51 | exit 1 | |
52 | fi | |
53 | } | |
54 | ||
55 | # Tests if list $1 has item $2 (returning zero on a match) | |
56 | list_has_item() { | |
57 | if echo "$1" | grep -Eq "(^|\s*)$2(\$|\s*)"; then | |
58 | return 0 | |
59 | else | |
60 | return 1 | |
61 | fi | |
62 | } | |
63 | ||
64 | # Adds a new line with the supplied arguments to $DESTN (or stdout) | |
65 | write_line() { | |
66 | if [ -n "$DESTN" ]; then | |
67 | printf '%s\n' "$@" >> "$DESTN" | |
68 | else | |
69 | printf '%s\n' "$@" | |
70 | fi | |
71 | } | |
72 | ||
73 | log_line() { | |
74 | echo $@ >&2 | |
75 | } | |
76 | ||
77 | # Find this file! | |
78 | resolve_include() { | |
79 | local srcdir=$1 | |
80 | local inc=$2 | |
81 | for root in $srcdir $ROOTS; do | |
82 | if [ -f "$root/$inc" ]; then | |
83 | # Try to reduce the file path into a canonical form (so that multiple) | |
84 | # includes of the same file are successfully deduplicated, even if they | |
85 | # are expressed differently. | |
86 | local relpath="$(realpath --relative-to . "$root/$inc" 2>/dev/null)" | |
87 | if [ "$relpath" != "" ]; then # not all realpaths support --relative-to | |
88 | echo "$relpath" | |
89 | return 0 | |
90 | fi | |
91 | local relpath="$(realpath "$root/$inc" 2>/dev/null)" | |
92 | if [ "$relpath" != "" ]; then # not all distros have realpath... | |
93 | echo "$relpath" | |
94 | return 0 | |
95 | fi | |
96 | # Fallback on Python to reduce the path if the above fails. | |
97 | local relpath=$(python -c "import os,sys; print os.path.relpath(sys.argv[1])" "$root/$inc" 2>/dev/null) | |
98 | if [ "$relpath" != "" ]; then # not all distros have realpath... | |
99 | echo "$relpath" | |
100 | return 0 | |
101 | fi | |
102 | # Worst case, fall back to just the root + relative include path. The | |
103 | # problem with this is that it is possible to emit multiple different | |
104 | # resolved paths to the same file, depending on exactly how its included. | |
105 | # Since the main loop below keeps a list of the resolved paths it's | |
106 | # already included, in order to avoid repeated includes, this failure to | |
107 | # produce a canonical/reduced path can lead to multiple inclusions of the | |
108 | # same file. But it seems like the resulting single file library still | |
109 | # works (hurray include guards!), so I guess it's ok. | |
110 | echo "$root/$inc" | |
111 | return 0 | |
112 | fi | |
113 | done | |
114 | return 1 | |
115 | } | |
116 | ||
117 | # Adds the contents of $1 with any of its includes inlined | |
118 | add_file() { | |
119 | local file=$1 | |
120 | if [ -n "$file" ]; then | |
121 | log_line "Processing: $file" | |
122 | # Get directory of the current so we can resolve relative includes | |
123 | local srcdir="$(dirname "$file")" | |
124 | # Read the file | |
125 | local line= | |
126 | while IFS= read -r line; do | |
127 | if echo "$line" | grep -Eq '^\s*#\s*include\s*".+"'; then | |
128 | # We have an include directive so strip the (first) file | |
129 | local inc=$(echo "$line" | grep -Eo '".*"' | sed -E 's/"([^"]+)"/\1/' | head -1) | |
130 | local res_inc="$(resolve_include "$srcdir" "$inc")" | |
131 | if list_has_item "$XINCS" "$inc"; then | |
132 | # The file was excluded so error if the source attempts to use it | |
133 | write_line "#error Using excluded file: $inc" | |
134 | log_line "Excluding: $inc" | |
135 | else | |
136 | if ! list_has_item "$FOUND" "$res_inc"; then | |
137 | # The file was not previously encountered | |
138 | FOUND="$FOUND $res_inc" | |
139 | if list_has_item "$KINCS" "$inc"; then | |
140 | # But the include was flagged to keep as included | |
141 | write_line "/**** *NOT* inlining $inc ****/" | |
142 | write_line "$line" | |
143 | log_line "Not Inlining: $inc" | |
144 | else | |
145 | # The file was neither excluded nor seen before so inline it | |
146 | write_line "/**** start inlining $inc ****/" | |
147 | add_file "$res_inc" | |
148 | write_line "/**** ended inlining $inc ****/" | |
149 | fi | |
150 | else | |
151 | write_line "/**** skipping file: $inc ****/" | |
152 | fi | |
153 | fi | |
154 | else | |
155 | # Skip any 'pragma once' directives, otherwise write the source line | |
156 | local write=$PONCE | |
157 | if [ $write -eq 0 ]; then | |
158 | if echo "$line" | grep -Eqv '^\s*#\s*pragma\s*once\s*'; then | |
159 | write=1 | |
160 | fi | |
161 | fi | |
162 | if [ $write -ne 0 ]; then | |
163 | write_line "$line" | |
164 | fi | |
165 | fi | |
166 | done < "$file" | |
167 | else | |
168 | write_line "#error Unable to find \"$1\"" | |
169 | log_line "Error: Unable to find: \"$1\"" | |
170 | fi | |
171 | } | |
172 | ||
173 | while getopts ":r:x:k:po:" opts; do | |
174 | case $opts in | |
175 | r) | |
176 | ROOTS="$ROOTS $OPTARG" | |
177 | ;; | |
178 | x) | |
179 | XINCS="$XINCS $OPTARG" | |
180 | ;; | |
181 | k) | |
182 | KINCS="$KINCS $OPTARG" | |
183 | ;; | |
184 | p) | |
185 | PONCE=1 | |
186 | ;; | |
187 | o) | |
188 | DESTN="$OPTARG" | |
189 | ;; | |
190 | *) | |
191 | usage | |
192 | ;; | |
193 | esac | |
194 | done | |
195 | shift $((OPTIND-1)) | |
196 | ||
197 | if [ -n "$1" ]; then | |
198 | if [ -f "$1" ]; then | |
199 | if [ -n "$DESTN" ]; then | |
200 | printf "" > "$DESTN" | |
201 | fi | |
202 | test_deps | |
203 | add_file "$1" | |
204 | else | |
205 | echo "Input file not found: \"$1\"" | |
206 | exit 1 | |
207 | fi | |
208 | else | |
209 | usage | |
210 | fi | |
211 | exit 0 |