summaryrefslogtreecommitdiffstats
path: root/src/zstd/contrib/single_file_libs/combine.sh
blob: 8eac4f9eb147a041aa0b8bfe1338f9ef982407da (plain)
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
#!/bin/sh -e

# Tool to bundle multiple C/C++ source files, inlining any includes.
# 
# Note: this POSIX-compliant script is many times slower than the original bash
# implementation (due to the grep calls) but it runs and works everywhere.
# 
# TODO: ROOTS, FOUND, etc., as arrays (since they fail on paths with spaces)
# TODO: revert to Bash-only regex (the grep ones being too slow)
# 
# Author: Carl Woffenden, Numfum GmbH (this script is released under a CC0 license/Public Domain)

# Common file roots
ROOTS="."

# -x option excluded includes
XINCS=""

# -k option includes to keep as include directives
KINCS=""

# Files previously visited
FOUND=""

# Optional destination file (empty string to write to stdout)
DESTN=""

# Whether the "#pragma once" directives should be written to the output
PONCE=0

# Prints the script usage then exits
usage() {
  echo "Usage: $0 [-r <path>] [-x <header>] [-k <header>] [-o <outfile>] infile"
  echo "  -r file root search path"
  echo "  -x file to completely exclude from inlining"
  echo "  -k file to exclude from inlining but keep the include directive"
  echo "  -p keep any '#pragma once' directives (removed by default)"
  echo "  -o output file (otherwise stdout)"
  echo "Example: $0 -r ../my/path - r ../other/path -o out.c in.c"
  exit 1
}

# Tests that the grep implementation works as expected (older OSX grep fails)
test_deps() {
  if ! echo '#include "foo"' | grep -Eq '^\s*#\s*include\s*".+"'; then
    echo "Aborting: the grep implementation fails to parse include lines"
    exit 1
  fi
  if ! echo '"foo.h"' | sed -E 's/"([^"]+)"/\1/' | grep -Eq '^foo\.h$'; then
    echo "Aborting: sed is unavailable or non-functional"
    exit 1
  fi
}

# Tests if list $1 has item $2 (returning zero on a match)
list_has_item() {
  if echo "$1" | grep -Eq "(^|\s*)$2(\$|\s*)"; then
    return 0
  else
    return 1
  fi
}

# Adds a new line with the supplied arguments to $DESTN (or stdout)
write_line() {
  if [ -n "$DESTN" ]; then
    printf '%s\n' "$@" >> "$DESTN"
  else
    printf '%s\n' "$@"
  fi
}

log_line() {
  echo $@ >&2
}

# Find this file!
resolve_include() {
  local srcdir=$1
  local inc=$2
  for root in $srcdir $ROOTS; do
    if [ -f "$root/$inc" ]; then
      # Try to reduce the file path into a canonical form (so that multiple)
      # includes of the same file are successfully deduplicated, even if they
      # are expressed differently.
      local relpath="$(realpath --relative-to . "$root/$inc" 2>/dev/null)"
      if [ "$relpath" != "" ]; then # not all realpaths support --relative-to
        echo "$relpath"
        return 0
      fi
      local relpath="$(realpath "$root/$inc" 2>/dev/null)"
      if [ "$relpath" != "" ]; then # not all distros have realpath...
        echo "$relpath"
        return 0
      fi
      # Fallback on Python to reduce the path if the above fails.
      local relpath=$(python -c "import os,sys; print os.path.relpath(sys.argv[1])" "$root/$inc" 2>/dev/null)
      if [ "$relpath" != "" ]; then # not all distros have realpath...
        echo "$relpath"
        return 0
      fi
      # Worst case, fall back to just the root + relative include path. The
      # problem with this is that it is possible to emit multiple different
      # resolved paths to the same file, depending on exactly how its included.
      # Since the main loop below keeps a list of the resolved paths it's
      # already included, in order to avoid repeated includes, this failure to
      # produce a canonical/reduced path can lead to multiple inclusions of the
      # same file. But it seems like the resulting single file library still
      # works (hurray include guards!), so I guess it's ok.
      echo "$root/$inc"
      return 0
    fi
  done
  return 1
}

# Adds the contents of $1 with any of its includes inlined
add_file() {
  local file=$1
  if [ -n "$file" ]; then
    log_line "Processing: $file"
    # Get directory of the current so we can resolve relative includes
    local srcdir="$(dirname "$file")"
    # Read the file
    local line=
    while IFS= read -r line; do
      if echo "$line" | grep -Eq '^\s*#\s*include\s*".+"'; then
        # We have an include directive so strip the (first) file
        local inc=$(echo "$line" | grep -Eo '".*"' | sed -E 's/"([^"]+)"/\1/' | head -1)
        local res_inc="$(resolve_include "$srcdir" "$inc")"
        if list_has_item "$XINCS" "$inc"; then
          # The file was excluded so error if the source attempts to use it
          write_line "#error Using excluded file: $inc"
          log_line "Excluding: $inc"
        else
          if ! list_has_item "$FOUND" "$res_inc"; then
            # The file was not previously encountered
            FOUND="$FOUND $res_inc"
            if list_has_item "$KINCS" "$inc"; then
              # But the include was flagged to keep as included
              write_line "/**** *NOT* inlining $inc ****/"
              write_line "$line"
              log_line "Not Inlining: $inc"
            else
              # The file was neither excluded nor seen before so inline it
              write_line "/**** start inlining $inc ****/"
              add_file "$res_inc"
              write_line "/**** ended inlining $inc ****/"
            fi
          else
            write_line "/**** skipping file: $inc ****/"
          fi
        fi
      else
        # Skip any 'pragma once' directives, otherwise write the source line
        local write=$PONCE
        if [ $write -eq 0 ]; then
          if echo "$line" | grep -Eqv '^\s*#\s*pragma\s*once\s*'; then
            write=1
          fi
        fi
        if [ $write -ne 0 ]; then
          write_line "$line"
        fi
      fi
    done < "$file"
  else
    write_line "#error Unable to find \"$1\""
    log_line "Error: Unable to find: \"$1\""
  fi
}

while getopts ":r:x:k:po:" opts; do
  case $opts in
  r)
    ROOTS="$ROOTS $OPTARG"
    ;;
  x)
    XINCS="$XINCS $OPTARG"
    ;;
  k)
    KINCS="$KINCS $OPTARG"
    ;;
  p)
    PONCE=1
    ;;
  o)
    DESTN="$OPTARG"
    ;;
  *)
    usage
    ;;
  esac
done
shift $((OPTIND-1))

if [ -n "$1" ]; then
  if [ -f "$1" ]; then
    if [ -n "$DESTN" ]; then
      printf "" > "$DESTN"
    fi
    test_deps
    add_file "$1"
  else
    echo "Input file not found: \"$1\""
    exit 1
  fi
else
  usage
fi
exit 0