about summary refs log tree commit diff stats
path: root/docs/gen/gen.py
blob: 0eb3d52e7f5c43a546ce2a3ebffbfa7d0e63c917 (plain) (blame)
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# Usage: python gen.py

import json
import os
import re
from collections import defaultdict

script_dir = os.path.dirname(os.path.abspath(__file__))
usage_file = os.path.join(script_dir, 'usage.json')

with open(usage_file, 'r') as file:
  data = json.load(file)
  
  with open(os.path.join(script_dir, '../USAGE.md'), 'w') as md_file:
    md_file.write("""<!--- This file is generated by gen.py, do not edit it directly. -->
Usage
----

There are many environment variables to control Box64's behaviour, which will be listed below by category.

There are 2 types of Box64 builds: the Wine WOW64 build (WowBox64) and the regular Linux build. Beware only some of the environment variables are available in WowBox64.

### Configuration files for Linux build

In addition to environment variables, if you're using the regular Linux build, Box64 also looks for 2 places for rcfile by default: the system-wide `/etc/box64.box64rc` and user-specific `~/.box64rc`.
Settings priority follows this order (from highest to lowest): `~/.box64rc` > `/etc/box64.box64rc` > environment variables.

Example configuration:

```
[factorio]
BOX64_DYNAREC_SAFEFLAGS=0
BOX64_DYNAREC_BIGBLOCK=2
BOX64_DYNAREC_FORWARD=1024
BOX64_DYNAREC_CALLRET=1
```

This configuration will apply the specified settings application-wide to any executable named `factorio`.

### Advanced usage for Linux build

1. **Wildcard Matching**

   Asterisks (`*`) can be used for basic pattern matching in application names. For instance, `[*setup*]` will match any program containing "setup" in its name. Note this implements simple wildcard matching rather than full regex support.
2. **Custom Configuration File**

   The `BOX64_RCFILE` environment variable can specify an alternative configuration file instead of the default `/etc/box64.box64rc`.
3. **Per-File Settings**

   Sections starting with `/` apply to specific files. For example:
   ```
   [/d3d9.dll]
   BOX64_DYNAREC_SAFEFLAGS=0
   ```
   These settings will only affect the `d3d9.dll` file. This syntax also works for **emulated** Linux libraries, e.g., `[/libstdc++.so.6]`.

----

""")

    categories = defaultdict(list)
    for entry in data:
      categories[entry["category"]].append(entry)

    for category, entries in categories.items():
      md_file.write(f"## {category}\n\n")
      for entry in entries:
        md_file.write(f"### {entry['name']}\n\n{entry['description']}{' Availble in WowBox64.' if entry['wine'] else ''}\n\n")
        for option in entry['options']:
          md_file.write(f" * {option['key']}: {option['description']} {'[Default]' if option['default'] else ''}\n")
        md_file.write("\n")


  with open(os.path.join(script_dir, '../box64.pod'), 'w') as pod_file:
    pod_file.write("""=head1 NAME

box64 - Linux Userspace x86_64 Emulator with a twist

=head1 SYNOPSIS

B<box64> [B<--help>] [B<--version>] I<executable>

=head1 DESCRIPTION

B<Box64> lets you run x86_64 Linux programs (such as games) on non-x86_64 Linux
systems, like ARM (host system needs to be 64-bit little-endian). Since B<Box64>
uses the native versions of some "system" libraries, like libc, libm, SDL, and
OpenGL, it's easy to integrate and use with most applications, and performance
can be surprisingly high in many cases. B<Box64> integrates with DynaRec (dynamic
recompiler) for the ARM64 platform, providing a speed boost between 5 to 10
times faster than using only the interpreter.

=head1 OPTIONS

=over 8

=item B<-h,--help>

Print box64 help and quit.

=item B<-v,--version>

Print box64 version and quit.

=back

=head1 BRIEF USAGE

There are many environment variables to control B<Box64>'s behaviour. In
addition to environment variables, B<Box64> also looks for 2 places for rcfile:
F</etc/box64.box64rc> and F<~/.box64rc>, in the format of .ini files.
Settings priority: F<~/.box64rc> > F</etc/box64.box64rc> > environment variables.
Example:

    [factorio]
    BOX64_DYNAREC_SAFEFLAGS=0
    BOX64_DYNAREC_BIGBLOCK=2
    BOX64_DYNAREC_FORWARD=1024
    BOX64_DYNAREC_CALLRET=1

=head1 ENVIRONMENT VARIABLES

=over 8

""")

    for entry in data:
      pod_file.write(f"\n=item B<{entry['name']}> =I<{ '|'.join(option['key'] for option in entry['options']) }>\n\n{entry['description']}{' Availble in WowBox64.' if entry['wine'] else ''}\n\n")
      for option in entry['options']:
        pod_file.write(f" * {option['key']} : {option['description']} {'[Default]' if option['default'] else ''}\n")
      pod_file.write("\n")

    pod_file.write("""
=back

=cut
""")

####################
# Validation
####################

PADDING = 30
INCLUDE_BLANK = False

def get_usage_entry(usage, define):
    entry = list(e for e in usage if e["name"] == define)
    if len(entry) == 0:
        print(f"{define:<{PADDING}}: missing usage.json entry")
    elif len(entry) > 1:
        print(f"{define:<{PADDING}}: multiple usage entries found", len(entry))
    else:
        return entry[0]
    return None

# check `default` uniqueness
for entry in data:
    i = list(d["default"] for d in entry["options"]).count(True)
    if i > 1:
        print(f"{entry['name']:<{PADDING}}: multiple default values usage.json")


# regex to match env.h C code
regex = {
    "INTEGER": re.compile(
        r"^\s*INTEGER\((?P<define>\w+), (?P<name>\w+), (?P<default>\w+), (?P<min>\w+), (?P<max>\w+), (?P<wine>\w+)\)"
    ),
    "INTEGER64": re.compile(
        r"^\s*INTEGER64\((?P<define>\w+), (?P<name>\w+), (?P<default>\w+), (?P<wine>\w+)\)"
    ),
    "BOOLEAN": re.compile(
        r"^\s*BOOLEAN\((?P<define>\w+), (?P<name>\w+), (?P<default>\w+), (?P<wine>\w+)\)"
    ),
    "ADDRESS": re.compile(r"^\s*ADDRESS\((?P<define>\w+), (?P<name>\w+), (?P<wine>\w+)\)"),
    "STRING": re.compile(r"^\s*STRING\((?P<define>\w+), (?P<name>\w+), (?P<wine>\w+)\)"),
}

env_file = os.path.join(script_dir, "../../src/include/env.h")
with open(env_file, "r") as file:
    matches = {}
    for line in file.readlines():
        for t, r in regex.items():
            m = r.search(line)
            if m:
                # filter out comments and other non useful code
                if m.group("define") == "NAME":
                    continue

                if t not in matches:
                    matches[t] = {}
                if (m.group("define") in matches[t]):
                    # multiple definitions, no default
                    matches[t][m.group("define")]['no_default'] = True
                    break
                matches[t][m.group("define")] = m.groupdict()
                matches[t][m.group("define")]['no_default'] = False

for define, m in matches["INTEGER"].items():
    name = m["name"]
    default = None if m["no_default"] or not m["default"].isdigit() else int(m["default"])
    min = int(m["min"])
    max = int(m["max"])

    # Check default in valid range
    if default and (default < min or default > max):
        print(f"{define:<{PADDING}}: default lays outside of min/max range")

    # Check consistency with usage.json
    if e := get_usage_entry(data, define):
        # guess min/max values if possible
        min2 = max2 = None
        # blank means that the entry has an 'XXXX' entry which usually indicated that arbitrary values are valid
        blank = False
        default2 = None

        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

        for o in e["options"]:
            if o["key"] == "XXXX":
                blank = True
                continue

            val = int(o["key"])  # not supposed to fail

            min2 = min2 if min2 is not None and min2 < val else val
            max2 = max2 if max2 is not None and max2 > val else val

            if o["default"]:
                default2 = val

        if min2 and min2 != min:
            if not blank or (blank and INCLUDE_BLANK):
                print(
                    f"{define:<{PADDING}}: min value mismatch: env.h={min}, usage.json={min2}{(' (possible false positive)' if blank else '')}"
                )
        if max2 and max2 != max:
            if not blank or (blank and INCLUDE_BLANK):
                print(
                    f"{define:<{PADDING}}: max value mismatch: env.h={max}, usage.json={max2}{(' (possible false positive)' if blank else '')}"
                )
        if default2 != default:
            print(
                f"{define:<{PADDING}}: default value mismatch: env.h={default}, usage.json={default2}"
            )

for define, m in matches['INTEGER64'].items():
    # similar to INTEGER but without min/max
    name = m["name"]
    default = None if m["no_default"] or not m["default"].isdigit() else int(m["default"])

    # Check consistency with usage.json
    if e := get_usage_entry(data, define):
        default2 = None

        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

        for o in e["options"]:
            if o["key"] == "XXXX":
                continue

            val = int(o["key"])

            if o["default"]:
                default2 = val

        if default2 != default:
            print(
                f"{define:<{PADDING}}: default value mismatch: env.h={default}, usage.json={default2}"
            )

for define, m in matches["BOOLEAN"].items():
    name = m["name"]
    default = None if m["no_default"] or m["default"] not in ["0", "1"] else bool(m["default"])

    # Check consistency with usage.json
    if e := get_usage_entry(data, define):
        default2 = None

        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

        for o in e["options"]:
            try:
                val = bool(o["key"])
            except ValueError:
                print(f"{define:<{PADDING}}: failed to parse boolean {o['key']}")

            if o["default"]:
                default2 = val

        if default2 != default:
            print(
                f"{define:<{PADDING}}: default value mismatch: env.h={default}, usage.json={default2}"
            )

# ADDRESS and STRING are not that interesting
for define, m in matches["ADDRESS"].items():
    if e := get_usage_entry(data, define):
        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

for define, m in matches["STRING"].items():
    # skip BOX64_ENV[1-5] entries, they mismatch but this is fine
    if define.startswith("BOX64_ENV"):
        continue
    if e := get_usage_entry(data, define):
        if int(m["wine"]) != int(e["wine"]):
            print(f"{define:<{PADDING}}: wine mismatch: env.h={int(m['wine'])}, usage.json={int(e['wine'])}")

# check that everything from usage.json is in env.h
for e in data:
    define = e["name"]

    # skip BOX64_ENV[1-5] entries, they mismatch but this is fine
    if define.startswith("BOX64_ENV"):
        continue

    found = False
    for t in matches:
        for d in matches[t]:
            if d == define:
                found = True

    if not found:
        print(f"{define:<{PADDING}}: missing env.h entry")