Skip to content

Commit 7fb1431

Browse files
committed
web: Add simple C/C++ style preprocessor for module-available defines
Supported directives are #include, #define, #undef, #ifdef, #if, #else and #endif. In contrast to C/C++ a not defined symbol in an #if directive is an error and does not evaluate to false. For compatibilty with TypeScript all directives have to be prefixed with //. Code removal by #if/#endif blocks is realized by commeting out the TypeScript code using //. This preserves line numbers as no lines are added or removed. To make this work the base directory for the TypeScript import path got moved from web/ to web/src/. This breaks absolute imports starting with src/. They need to be changed to drop the src/.
1 parent f235737 commit 7fb1431

File tree

15 files changed

+397
-100
lines changed

15 files changed

+397
-100
lines changed

software/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ web/src/ts/branding.ts
3333
web/src/ts/translation.tsx
3434
web/src/ts/translation.json
3535
web/src/modules/*/*.enum.ts
36+
web/src/modules/*/module_available.inc
37+
web/src_tfpp/
3638
web/**/*.embedded.ts
3739
coredump_py_gdb_cmds
3840
api_info.json

software/pio_hooks.py

Lines changed: 116 additions & 61 deletions
Large diffs are not rendered by default.

software/tfpp.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import argparse
5+
import pathlib
6+
import re
7+
from dataclasses import dataclass
8+
9+
@dataclass
10+
class Define:
11+
value: int
12+
location: str
13+
14+
@dataclass
15+
class If:
16+
value: int
17+
location: str
18+
19+
@dataclass
20+
class Else:
21+
value: int
22+
location: str
23+
24+
def any_zero(ifs_elses):
25+
return any([if_else.value == 0 for if_else in ifs_elses])
26+
27+
def parse_file(input_path, defines, ifs_elses):
28+
output_lines = []
29+
30+
with input_path.open(encoding='utf-8') as input_file:
31+
for i, line in enumerate(input_file.readlines()):
32+
m = re.match(r'^[/\s]*//\s*#\s*(.*)$', line.strip())
33+
34+
if m != None:
35+
directive = m.group(1)
36+
m = re.match(r'^(include |define |undef |ifdef |if |else|endif)\s*(.*)$', directive)
37+
38+
if m == None:
39+
raise Exception(f'Malformed directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
40+
41+
verb = m.group(1).strip()
42+
arguments = m.group(2)
43+
44+
if verb == 'include':
45+
m = re.match(r'^(?:"([^"]+)"|\'([^\']+)\')$', arguments)
46+
47+
if m == None:
48+
raise Exception(f'Malformed path in #include directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
49+
50+
include_path = input_path.parent / pathlib.Path(m.group(1))
51+
52+
if not any_zero(ifs_elses):
53+
if not include_path.exists():
54+
raise Exception(f'File in #include directive at {input_path}:{i + 1} is missing: {include_path}')
55+
56+
parse_file(include_path, defines, ifs_elses)
57+
elif verb == 'define':
58+
m = re.match(r'^([A-Za-z_-][A-Za-z0-9_-]*)\s+(0|1)$', arguments)
59+
60+
if m == None:
61+
raise Exception(f'Malformed arguments in #define directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
62+
63+
if not any_zero(ifs_elses):
64+
symbol = m.group(1)
65+
value = int(m.group(2))
66+
define = defines.get(symbol)
67+
68+
if define != None:
69+
raise Exception(f'Symbol {symbol} in #define directive at {input_path}:{i + 1} is already defined as {define.value} at {define.location}: {line.rstrip('\r\n')}')
70+
else:
71+
defines[symbol] = Define(value, f'{input_path}:{i + 1}')
72+
elif verb == 'undef':
73+
m = re.match(r'^([A-Za-z_-][A-Za-z0-9_-]*)$', arguments)
74+
75+
if m == None:
76+
raise Exception(f'Malformed arguments in #undef directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
77+
78+
if not any_zero(ifs_elses):
79+
symbol = m.group(1)
80+
define = defines.get(symbol)
81+
82+
if define == None:
83+
raise Exception(f'Symbol {symbol} in #undef directive at {input_path}:{i + 1} is not defined: {line.rstrip('\r\n')}')
84+
85+
line = line.rstrip() + f' [defined at {define.location}]\n'
86+
defines.pop(symbol)
87+
elif verb == 'ifdef':
88+
m = re.match(r'^([A-Za-z_-][A-Za-z0-9_-]*)$', arguments)
89+
90+
if m == None:
91+
raise Exception(f'Malformed arguments in #ifdef directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
92+
93+
symbol = m.group(1)
94+
value = None
95+
96+
if not any_zero(ifs_elses):
97+
define = defines.get(symbol)
98+
99+
if define != None:
100+
value = 1
101+
line = line.rstrip() + f' [defined as {define.value} at {define.location}]\n'
102+
else:
103+
value = 0
104+
line = line.rstrip() + f' [not defined]\n'
105+
106+
ifs_elses.append(If(value, f'{input_path}:{i + 1}'))
107+
elif verb == 'if':
108+
m = re.match(r'^(1|0|[A-Za-z_-][A-Za-z0-9_-]*)$', arguments)
109+
110+
if m == None:
111+
raise Exception(f'Malformed arguments in #if directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
112+
113+
value_or_symbol = m.group(1)
114+
value = None
115+
116+
if not any_zero(ifs_elses):
117+
if value_or_symbol in ['1', '0']:
118+
value = int(value_or_symbol)
119+
else:
120+
symbol = value_or_symbol
121+
define = defines.get(symbol)
122+
123+
if define == None:
124+
raise Exception(f'Symbol {symbol} in #if directive at {input_path}:{i + 1} is not defined: {line.rstrip('\r\n')}')
125+
126+
value = define.value
127+
line = line.rstrip() + f' [defined as {value} at {define.location}]\n'
128+
129+
ifs_elses.append(If(value, f'{input_path}:{i + 1}'))
130+
elif verb == 'else':
131+
if len(arguments) > 0:
132+
raise Exception(f'Unexpected arguments in #else directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
133+
134+
if len(ifs_elses) == 0:
135+
raise Exception(f'Missing #if directive for #else directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
136+
137+
if_else = ifs_elses.pop()
138+
139+
if isinstance(if_else, Else):
140+
raise Exception(f'Duplicate #else directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
141+
142+
ifs_elses.append(Else(1 - if_else.value, f'{input_path}:{i + 1}'))
143+
elif verb == 'endif':
144+
if len(arguments) > 0:
145+
raise Exception(f'Unexpected arguments in #endif directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
146+
147+
if len(ifs_elses) == 0:
148+
raise Exception(f'Missing #if directive for #endif directive at {input_path}:{i + 1}: {line.rstrip('\r\n')}')
149+
150+
ifs_elses.pop()
151+
152+
if any_zero(ifs_elses) and not line.lstrip().startswith('//'):
153+
line = '//' + line
154+
155+
output_lines.append(line)
156+
157+
return output_lines
158+
159+
def tfpp(input_path, output_path, overwrite=False):
160+
input_path = pathlib.Path(input_path)
161+
output_path = pathlib.Path(output_path)
162+
163+
if not input_path.exists():
164+
raise Exception(f"Input file {input_path} is missing")
165+
166+
if output_path.exists() and not overwrite:
167+
raise Exception(f"Output file {output_path} already exists")
168+
169+
if input_path == output_path:
170+
raise Exception(f"Input file {input_path} and output file {output_path} are the same")
171+
172+
defines = {}
173+
ifs_elses = []
174+
output_lines = parse_file(input_path, defines, ifs_elses)
175+
176+
if len(ifs_elses) > 0:
177+
raise Exception(f'Missing #endif directive for #{"if" if isinstance(ifs_elses[0], If) else "else"} directive at {ifs_elses[0].location}')
178+
179+
output_path.parent.mkdir(parents=True, exist_ok=True)
180+
output_path_tmp = output_path.with_suffix('.tfpptmp')
181+
182+
with output_path_tmp.open(mode='w', encoding='utf-8') as output_file:
183+
output_file.writelines(output_lines)
184+
185+
output_path_tmp.replace(output_path)
186+
187+
def main():
188+
parser = argparse.ArgumentParser()
189+
parser.add_argument('input_path')
190+
parser.add_argument('output_path')
191+
parser.add_argument('--overwrite', action='store_true')
192+
193+
args = parser.parse_args()
194+
195+
tfpp(input_path=args.input_path, output_path=args.output_path, overwrite=args.overwrite)
196+
197+
if __name__ == '__main__':
198+
try:
199+
main()
200+
except Exception as e:
201+
print(f'Error: {e}')
202+
sys.exit(1)

software/web/build.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import shutil
44
import subprocess
55
from base64 import b64encode
6-
76
import argparse
7+
import tinkerforge_util as tfutil
8+
9+
tfutil.create_parent_module(__file__, 'software')
10+
11+
from software.tfpp import tfpp
812

913
JS_ANALYZE = False
1014

11-
BUILD_DIR = 'build'
15+
BUILD_DIR = '../build'
1216

1317
HTML_MINIFIER_TERSER_OPTIONS = [
1418
'--collapse-boolean-attributes',
@@ -40,6 +44,29 @@ def main():
4044
parser.add_argument('--no-minify', action='store_true')
4145
build_args = parser.parse_args()
4246

47+
try:
48+
shutil.rmtree('src_tfpp')
49+
except FileNotFoundError:
50+
pass
51+
52+
print('tfpp...')
53+
for root, dirs, files in os.walk('./src'):
54+
for name in files:
55+
src_path = os.path.join(root, name)
56+
src_tfpp_path = src_path.replace('./src/', './src_tfpp/')
57+
58+
if src_path.endswith('.ts') or src_path.endswith('.tsx'):
59+
try:
60+
tfpp(src_path, src_tfpp_path)
61+
except Exception as e:
62+
print(f'Error: {e}', file=sys.stderr)
63+
exit(42)
64+
else:
65+
os.makedirs(os.path.split(src_tfpp_path)[0], exist_ok=True)
66+
shutil.copy2(src_path, src_tfpp_path)
67+
68+
os.chdir('src_tfpp')
69+
4370
try:
4471
shutil.rmtree(BUILD_DIR)
4572
except FileNotFoundError:
@@ -59,12 +86,12 @@ def main():
5986
args = [
6087
'npx',
6188
'esbuild',
62-
'src/main.tsx',
89+
'main.tsx',
6390
'--metafile={}'.format(os.path.join(BUILD_DIR, 'meta.json')),
6491
'--bundle',
6592
'--target=es6',
66-
'--alias:argon2-browser=./node_modules/argon2-browser/dist/argon2-bundled.min.js',
67-
'--alias:jquery=./node_modules/jquery/dist/jquery.slim.min',
93+
'--alias:argon2-browser=../node_modules/argon2-browser/dist/argon2-bundled.min.js',
94+
'--alias:jquery=../node_modules/jquery/dist/jquery.slim.min',
6895
'--outfile={0}'.format(os.path.join(BUILD_DIR, 'bundle.min.js'))
6996
]
7097

@@ -89,7 +116,7 @@ def main():
89116
args += ['--no-source-map']
90117

91118
args += [
92-
'src/main.scss',
119+
'main.scss',
93120
os.path.join(BUILD_DIR, 'main.css')
94121
]
95122

@@ -125,7 +152,7 @@ def main():
125152
HTML_MINIFIER_TERSER_OPTIONS + [
126153
'-o',
127154
os.path.join(BUILD_DIR, 'index.min.html'),
128-
'src/index.html'
155+
'index.html'
129156
], shell=sys.platform == 'win32')
130157

131158
if __name__ == '__main__':

software/web/src/modules/automation/plugin_automation_trigger.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { InputSelect } from "../../ts/components/input_select";
2626
import { FormRow } from "../../ts/components/form_row";
2727
import * as util from "../../ts/util";
2828
import * as API from "../../ts/api";
29-
import { InputText } from "src/ts/components/input_text";
29+
import { InputText } from "../../ts/components/input_text";
3030

3131
export type CronAutomationTrigger = [
3232
AutomationTriggerID.Cron,

software/web/src/modules/charge_manager/chargers.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ import { Collapse, ListGroup, ListGroupItem } from "react-bootstrap";
2929
import { InputSelect } from "../../ts/components/input_select";
3030
import { SubPage } from "../../ts/components/sub_page";
3131
import { Table } from "../../ts/components/table";
32-
3332
import type { ChargeManagerStatus } from "./main"
34-
import { InputFloat } from "src/ts/components/input_float";
33+
import { InputFloat } from "../../ts/components/input_float";
3534

3635
type ChargeManagerConfig = API.getType["charge_manager/config"];
3736
type ChargerConfig = ChargeManagerConfig["chargers"][0];

software/web/src/modules/charge_manager/debug.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ import { Button, Card } from "react-bootstrap";
3232
import { SubPage } from "../../ts/components/sub_page";
3333
import { NavbarItem } from "../../ts/components/navbar_item";
3434
import { Download, Terminal } from "react-feather";
35-
import { InputNumber } from "src/ts/components/input_number";
36-
35+
import { InputNumber } from "../../ts/components/input_number";
3736

3837
const CMDOutFloat = (props: any) => <OutputFloat maxFractionalDigitsOnPage={3} maxUnitLengthOnPage={2} {...props}/>
3938
const CMDCardOutFloat = (props: any) => <OutputFloat maxUnitLengthOnPage={3.5} {...props}/>

software/web/src/modules/charge_manager/settings.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,16 @@ import { FormRow } from "../../ts/components/form_row";
2727
import { Button, Collapse } from "react-bootstrap";
2828
import { InputSelect } from "../../ts/components/input_select";
2929
import { InputFloat } from "../../ts/components/input_float";
30-
import { OutputFloat } from "src/ts/components/output_float";
30+
import { OutputFloat } from "../../ts/components/output_float";
3131
import { Switch } from "../../ts/components/switch";
3232
import { InputNumber } from "../../ts/components/input_number";
3333
import { SubPage } from "../../ts/components/sub_page";
34-
3534
import { MeterValueID } from "../meters/meter_value_id";
3635
import { get_noninternal_meter_slots, NoninternalMeterSelector } from "../power_manager/main";
3736
import type { ChargeManagerStatus } from "./main"
38-
import { FormSeparator } from "src/ts/components/form_separator";
39-
37+
import { FormSeparator } from "../../ts/components/form_separator";
4038
import { ChargeManagerDebug } from "./debug";
41-
import { CollapsedSection } from "src/ts/components/collapsed_section";
39+
import { CollapsedSection } from "../../ts/components/collapsed_section";
4240

4341
type ChargeManagerConfig = API.getType["charge_manager/config"];
4442

0 commit comments

Comments
 (0)