aboutsummaryrefslogtreecommitdiffstats
path: root/net/fill_config.py
blob: b84c92ae64fffd4f25e4db1409c06ef9450b6ecb (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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python3
'''Take values from a config file and fill them into a set of templates.
Write the result to the current directory.'''

import os, sys, re, shutil
import argparse

def file_newer(path_a, than_path_b):
  return os.path.getmtime(path_a) > os.path.getmtime(than_path_b)

LAST_LOCAL_CONFIG_FILE = '.last_config'
LAST_TMPL_DIR = '.last_templates'

parser = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('sources', metavar='SRC', nargs='*',
                    help='Pass both a template directory and a config file.')
parser.add_argument('-s', '--check-stale', dest='check_stale', action='store_true',
                    help='only verify age of generated files vs. config and templates.'
                    ' Exit nonzero when any source file is newer. Do not write anything.')

args = parser.parse_args()

local_config_file = None
tmpl_dir = None

for src in args.sources:
  if os.path.isdir(src):
    if tmpl_dir is not None:
      print('Error: only one template dir permitted. (%r vs. %r)' % (tmpl_dir, src))
    tmpl_dir = src
  elif os.path.isfile(src):
    if local_config_file is not None:
      print('Error: only one config file permitted. (%r vs. %r)' % (local_config_file, src))
    local_config_file = src

if local_config_file is None and os.path.isfile(LAST_LOCAL_CONFIG_FILE):
  local_config_file = open(LAST_LOCAL_CONFIG_FILE).read().strip()

if tmpl_dir is None and os.path.isfile(LAST_TMPL_DIR):
  tmpl_dir = open(LAST_TMPL_DIR).read().strip()

if not tmpl_dir or not os.path.isdir(tmpl_dir):
  print("Template dir does not exist: %r" % tmpl_dir)
  exit(1)

if not local_config_file or not os.path.isfile(local_config_file):
  print("No such config file: %r" % local_config_file)
  exit(1)

local_config_file = os.path.realpath(local_config_file)
tmpl_dir = os.path.realpath(tmpl_dir)
net_dir = os.path.realpath(".")

print('using config file %r\non templates %r\nwith NET_DIR %r' % (local_config_file, tmpl_dir, net_dir))

with open(LAST_LOCAL_CONFIG_FILE, 'w') as last_file:
  last_file.write(local_config_file)
with open(LAST_TMPL_DIR, 'w') as last_file:
  last_file.write(tmpl_dir)

# read in variable values from config file
# NET_DIR is the folder where fill_config.py was started
local_config = {"NET_DIR": net_dir}

line_nr = 0
for line in open(local_config_file):
  line_nr += 1
  line = line.strip('\n')

  if line.startswith('#'):
    continue

  if not '=' in line:
    if line:
      print("Error: %r line %d: %r" % (local_config_file, line_nr, line))
      exit(1)
    continue

  split_pos = line.find('=')
  name = line[:split_pos]
  val = line[split_pos + 1:]

  if val.startswith('"') and val.endswith('"'):
    val = val[1:-1]

  if name in local_config:
    print("Error: duplicate identifier in %r line %d: %r" % (local_config_file, line_nr, line))
  local_config[name] = val

# replace variable names with above values recursively
replace_re = re.compile('\$\{([A-Za-z0-9_]*)\}')
command_re = re.compile('\$\{([A-Za-z0-9_]*)\(([^)]*)\)\}')

idx = 0

def check_stale(src_path, target_path):
  if file_newer(src_path, target_path):
    print('Stale: %r is newer than %r' % (src_path, target_path))
    exit(1)

def replace_vars(tmpl, tmpl_dir, tmpl_src, local_config, strict=True):
    used_vars = set()
    for m in replace_re.finditer(tmpl):
      name = m.group(1)
      if not name in local_config:
        if strict:
          print('Error: undefined var %r in %r' % (name, tmpl_src))
          exit(1)
        else:
          continue
      used_vars.add(name)

    for var in used_vars:
      tmpl = tmpl.replace('${%s}' % var, local_config.get(var))

    return tmpl

def insert_includes(tmpl, tmpl_dir, tmpl_src, local_config, arg):
    include_path = os.path.join(tmpl_dir, arg)
    if not os.path.isfile(include_path):
      print('Error: included file does not exist: %r in %r' % (include_path, tmpl_src))
      exit(1)
    try:
      incl = open(include_path).read()
    except:
      print('Cannot read %r for %r' % (include_path, tmpl_src))
      raise
    if args.check_stale:
      check_stale(include_path, dst)

    # recurse, to follow the paths that the included bits come from
    incl = handle_commands(incl, os.path.dirname(include_path), include_path, local_config)

    return tmpl.replace('${include(%s)}' % arg, incl)

def insert_foreach(tmpl, tmpl_dir, tmpl_src, match, local_config, arg):

    # figure out section to handle
    start_span = match.span()

    if tmpl[start_span[1]] == '\n':
      start_span = (start_span[0], start_span[1] + 1)

    end_str = '${foreach_end}\n'

    end_at = tmpl.find(end_str, start_span[1])
    if end_at < 0:
      end_str = end_str[:-1]
      end_at = tmpl.find(end_str, start_span[1])

    if end_at < 0:
      raise Exception('%r: unmatched %r' % (tmpl_src, match.string))

    end_span = (end_at, end_at + len(end_str))

    before_block = tmpl[:start_span[0]]
    foreach_block = tmpl[start_span[1]:end_span[0]]
    after_block = tmpl[end_span[1]:]

    # figure out what items matching the foreach(FOO<number>) there are
    item_re = re.compile('(^%s([0-9]+))_.*' % arg)
    items = set()
    for item in local_config.keys():
      item_m = item_re.match(item)
      if not item_m:
        continue
      items.add((item_m.group(1), item_m.group(2)))

    items = sorted(list(items))

    expanded = [before_block]
    for item, nr in items:
      expanded_block = foreach_block

      while True:
        expanded_block_was = expanded_block

        expanded_block = expanded_block.replace('${%sn_' % arg, '${%s_' % item)
        expanded_block = expanded_block.replace('${%sn}' % arg, nr)
        expanded_block = replace_vars(expanded_block, tmpl_dir, tmpl_src, local_config)

        if expanded_block_was == expanded_block:
          break

      expanded.append(expanded_block)

    expanded.extend(after_block)
    return ''.join(expanded)

def handle_commands(tmpl, tmpl_dir, tmpl_src, local_config):
    handled = 0
    for m in command_re.finditer(tmpl):
      handled += 1
      cmd = m.group(1)
      arg = m.group(2)
      if cmd == 'include':
        tmpl = insert_includes(tmpl, tmpl_dir, tmpl_src, local_config, arg)
      elif cmd == 'foreach':
        tmpl = insert_foreach(tmpl, tmpl_dir, tmpl_src, m, local_config, arg)
      else:
        print('Error: unknown command: %r in %r' % (cmd, tmpl_src))
        exit(1)

    return tmpl

for tmpl_name in sorted(os.listdir(tmpl_dir)):

  # omit "hidden" files
  if tmpl_name.startswith('.'):
    continue

  # omit files to be included by other files
  if tmpl_name.startswith('common_'):
    continue

  tmpl_src = os.path.join(tmpl_dir, tmpl_name)
  dst = tmpl_name

  if args.check_stale:
    check_stale(local_config_file, dst)
    check_stale(tmpl_src, dst)

  local_config['_fname'] = tmpl_name
  local_config['_name'] = os.path.splitext(tmpl_name)[0]
  local_config['_idx0'] = str(idx)
  idx += 1
  local_config['_idx1'] = str(idx)

  try:
    result = open(tmpl_src).read()
  except:
    print('Error in %r' % tmpl_src)
    raise

  while True:
    result_was = result
    result = handle_commands(result, tmpl_dir, tmpl_src, local_config)
    result = replace_vars(result, tmpl_dir, tmpl_src, local_config)
    if result_was == result:
      break

  if not args.check_stale:
    with open(dst, 'w') as dst_file:
      dst_file.write(result)
    shutil.copymode(tmpl_src, dst)

# vim: ts=2 sw=2 expandtab