Files
RetroArch/tools/settings_migrate_group.py
T
libretroadmin 91351e6dfc menu_setting: define the region emitters once for the whole unit
The per-region begin and end includes were still four hundred and
eighty-two include lines for one repeated purpose; nothing between
regions uses the emitter names, so the set now defines once before
the first def include and undefines once after the last, and a
generated region is exactly its def include. The compiled object is
byte-identical either way - this class of change never reaches the
binary - and the migration tool emits the bare include from here
on.
2026-07-07 10:08:44 +02:00

866 lines
49 KiB
Python
Executable File

#!/usr/bin/env python3
"""Migrate one menu descriptor table to a single-source settings def file.
Usage, from the repository root:
python3 tools/settings_migrate_group.py <table_name> <def_basename> "<group title>"
e.g.
python3 tools/settings_migrate_group.py bias_desc settings_def_video_bias.h "viewport bias group"
Extracts every scattered piece of each setting in the table - the us.h
VALUE and SUBLABEL rows, the _STR define, the label-table row, the
configuration.c row and the descriptor row - verifies per-setting
completeness up front, generates the def file, rewires all consumers by
in-place substitution, and runs the full gate battery. Nothing is kept
unless every gate passes; run from a clean worktree and reset on failure.
Handles: BOOL/UINT/INT/FLOAT kinds, rows without sublabels (_NS),
config keys that differ from the label string (kept literal behind
SETTINGS_DEF_CONFIG_PASS), and per-row preprocessor guards (carried
into the def file; all consumers gate identically).
Gates: six strict-C89 syntax lanes including D3DKMT and guard-heavy;
preprocessor token-stream identity of the table in both guard
polarities against the parent revision; lone-comma and empty-table
structural audits; msg_hash_us.json exact equality against a
scratch-tree regeneration of the parent revision; the full-language
lookup harness byte-identical; settings dump byte-identical against a
same-tree stash baseline (non-vacuous, dep-hygienic, try/finally).
The harness binaries under /tmp (stubs, lookup driver, out_pre baseline
and run configuration) are session-local; see the settings campaign
notes for how to rebuild them.
"""
import re, subprocess, os, json, sys
def run(cmd): return subprocess.run(cmd, shell=True, capture_output=True, text=True)
CSTR = r'"(?:[^"\\]|\\.)*"(?:\s*"(?:[^"\\]|\\.)*")*'
TABLE, DEF, TITLE = sys.argv[1], sys.argv[2], sys.argv[3]
assert not os.path.exists(os.path.join('settings', os.path.basename(DEF))), \
'def file %s already exists upstream - pick a different name' % DEF
SIGS = [('S_BOOL','f, T, n, d, sd, df, c'),
('S_UINT','f, T, n, d, sd, df, c, mn, mx, st, ob, ok, rp'),
('S_INT', 'f, T, n, d, sd, df, c, mn, mx, st, ob, ok, rp'),
('S_FLOAT','f, T, n, d, rnd, sd, df, c, mn, mx, st, ok, rp'),
('S_STRING','f, T, n, d, sd, c, ok, rp, sta, sel, lf, rt, ui'),
('S_DIR','f, T, n, d, el, sd, c, sta'),
('S_STRING_P','f, T, n, d, sd, c, ok, rp, sta, sel, lf, rt, ui'),
('S_PATH','f, T, n, d, sd, c, vals, rp, ui'),
('S_PATH_DS','f, T, n, df2, sd, c, vals, rp, ui'),
('S_ACTION','T, n'),
('S_BOOL_EX','f, T, n, d, sd, df, c, ok, rp, sta, sel, lf, rt, ui'),
('S_UINT_EX','f, T, n, d, sd, df, c, mn, mx, st, ob, ok, rp, sta, sel, lf, rt, ui'),
('S_INT_EX','f, T, n, d, sd, df, c, mn, mx, st, ob, ok, rp, sta, sel, lf, rt, ui'),
('S_FLOAT_EX','f, T, n, d, rnd, sd, df, c, mn, mx, st, ok, rp, sta, sel, lf, rt, ui'),
('S_ACTION_EX','T, n, sd, ok, rp, c'),
('S_BOOL_LV','f, T, TV, n, d, sd, df, c'),
('S_FLOAT_LV','f, T, TV, n, d, rnd, sd, df, c'),
('S_STRING_LV','f, T, TV, n, d, sd, c, ok, rp, sta, sel, lf, rt, ui'),
('S_ACTION_LV','T, TV, n, sd, ok, rp, c'),
('S_INT_AT','offs, T, n, d, sd, df, c, mn, mx, st, ob, ok, rp'),
('S_UINT_AT_EX','offs, T, n, d, sd, df, c, mn, mx, st, ob, ok, rp, sta, sel, lf, rt, ui')]
# Coupled platform defines: the polarity lanes derive tokens from row
# guards, but some tokens only ever appear alongside a partner in real
# builds (the SDK conditional vs the build system's own define); a lane
# carrying one without the other is a build no target produces.
DEFINE_COUPLES = {
'TARGET_OS_IOS': ('IOS',),
'TARGET_OS_TV': ('IOS',),
'HAVE_COCOATOUCH': ('HAVE_COCOA_METAL',),
'HAVE_CLOUDSYNC': ('HAVE_NETWORKING',),
}
def _couple(flag):
toks = [f[2:] for f in flag.split() if f.startswith('-D')]
extra = []
for tk in toks:
for p in DEFINE_COUPLES.get(tk, ()):
if p not in toks and ('-D%s' % p) not in extra:
extra.append('-D%s' % p)
return (flag + ' ' + ' '.join(extra)).strip()
UNDEFS = '\n'.join('#undef %s%s' % (b, s) for b, _ in SIGS for s in ('', '_NS', '_H', '_NS_H'))
def defs(mk):
L = []
for base, sig in SIGS:
L.append('#define %s(%s, us, sub)%s' % (base, sig, mk(base, True)))
L.append('#define %s_NS(%s, us)%s' % (base, sig, mk(base, False)))
return '\n'.join(L)
ms = open('menu/menu_setting.c').read()
tm = re.search(r'static const setting_desc_t %s\[\] = \{\n(.*?)\n( *)\};' % TABLE, ms, re.S)
assert tm, TABLE
def _negate_guard(g):
body = g.strip()
if body.startswith('#ifdef'):
return '#if !defined(%s)' % body.split()[1]
if body.startswith('#ifndef'):
return '#if defined(%s)' % body.split()[1]
return '#if !(%s)' % body[len('#if'):].strip()
def guard_at(text, pos):
"""Enclosing guard stack, else-aware: a position inside the #else
branch carries the negation of the branch condition. Blindness to
#else prepended '(HAVE_LAKKA || HAVE_ODROIDGO2)' to the
restart-visibility row that actually lives in that conditional's
else branch."""
stack = []
for gl in re.finditer(r'^[ \t]*#(if\w*[^\n]*|else|endif)', text[:pos], re.M):
s = gl.group(1)
if s.startswith('if'):
_full, _e = s, gl.end()
while _full.rstrip().endswith('\\'):
_nl = text.find('\n', _e)
if _nl < 0:
break
_nl2 = text.find('\n', _nl + 1)
_cont = text[_nl + 1:(_nl2 if _nl2 >= 0 else len(text))]
_full = _full.rstrip().rstrip('\\').rstrip() + ' ' + _cont.strip()
if _nl2 < 0:
break
_e = _nl2
stack.append('#' + _full.strip())
elif s.startswith('else') and stack:
stack[-1] = _negate_guard(stack[-1])
elif s.startswith('endif') and stack:
stack.pop()
return tuple(stack)
def guard_of(text, pat):
m = re.search(pat, text)
return guard_at(text, m.start()) if m else None
table_guard = tuple(guard_at(ms, tm.start()))
guards = {}
_cont_prev = None
body = tm.group(1)
# Depth-aware: record the FULL stack of enclosing #if guards for each row.
# A flat "#if ... #endif" regex (non-greedy) stops at the first #endif and
# so drops inner guards for nested blocks - e.g. a row under
# #ifdef HAVE_CDROM
# #ifdef HAVE_LAKKA
# would be emitted under HAVE_CDROM only, breaking any build where the
# outer flag is set but the inner is not. Carry every enclosing guard.
gstack = []
for ln in body.split('\n'):
s = ln.strip()
if _cont_prev is not None:
_joined = _cont_prev.rstrip('\\').rstrip() + ' ' + s
_cont_prev = _joined if s.endswith('\\') else None
if _cont_prev is None:
gstack.append(_joined)
continue
if s.startswith('#if'):
if s.endswith('\\'):
_cont_prev = s
continue
gstack.append(s)
elif s.startswith('#endif'):
if gstack:
gstack.pop()
else:
rm = re.match(r'\s*SDESC_\w+_ROW(?:_P|_DS|_EX|_LV|_AT|_AT_EX)?\(\s*(?:offsetof\s*\([^)]*\)\s*,\s*)?(\w+)\s*[,)]', ln)
if rm and gstack:
guards[rm.group(1)] = tuple(gstack)
_ordered = [(m.start(), (m.group(1) + (m.group(2) or ''), m.group(3), m.group(4), re.sub(r'\s+',' ',m.group(5)).strip()))
for m in re.finditer(r'SDESC_(BOOL|UINT|INT|FLOAT|STRING|DIR|PATH)_ROW(_P|_DS|_EX)?\(\s*(\w+),\s*(\w+),((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)', body)]
_ordered += [(m.start(), ('ACTION', '', m.group(1), '')) for m in re.finditer(r'SDESC_ACTION_ROW\(\s*(\w+)\s*\)', body)]
_ordered += [(m.start(), ('ACTION_EX', '', m.group(1), re.sub(r'\s+',' ',m.group(2)).strip())) for m in re.finditer(r'SDESC_ACTION_ROW_EX\(\s*(\w+),((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)', body)]
_ordered += [(m.start(), (m.group(1) + '_LV', m.group(2), m.group(3), re.sub(r'\s+',' ',m.group(5)).strip()))
for m in re.finditer(r'SDESC_(BOOL|FLOAT|STRING)_ROW_LV\(\s*(\w+),\s*(\w+),\s*(\w+),((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)', body)]
_ordered += [(m.start(), ('ACTION_LV', '', m.group(1), re.sub(r'\s+',' ',m.group(3)).strip()))
for m in re.finditer(r'SDESC_ACTION_ROW_LV\(\s*(\w+),\s*(\w+),((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)', body)]
lv_tv = {}
for m in re.finditer(r'SDESC_(?:BOOL|FLOAT|STRING)_ROW_LV\(\s*\w+,\s*(\w+),\s*(\w+),', body):
lv_tv[m.group(1)] = m.group(2)
for m in re.finditer(r'SDESC_ACTION_ROW_LV\(\s*(\w+),\s*(\w+),', body):
lv_tv[m.group(1)] = m.group(2)
at_offs = {}
for m in re.finditer(r'SDESC_(INT|UINT)_ROW_AT(_EX)?\(\s*(offsetof\s*\((?:[^()]|\([^()]*\))*\)),\s*(\w+),((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)', body):
_ordered.append((m.start(), (m.group(1) + '_AT' + (m.group(2) or ''), '', m.group(4), re.sub(r'\s+',' ',m.group(5)).strip())))
at_offs[m.group(4)] = re.sub(r'\s+', ' ', m.group(3)).strip()
rows = [r for _, r in sorted(_ordered)]
all_invocations = re.findall(r'SDESC_\w+?_ROW(?:_\w+)?\(', body)
assert len(rows) == len(all_invocations), (
'table contains %d rows but only %d are plain-grammar '
'SDESC_{BOOL,UINT,INT,FLOAT}_ROW; variant rows (_EX/_AT/_LV/...) are '
'outside the def grammar - migrate this table manually or extend the '
'grammar deliberately' % (len(all_invocations), len(rows)))
assert rows and len(rows) == len(re.findall(r'SDESC_\w+_ROW(?:_P|_DS|_EX|_LV|_AT|_AT_EX)?\(', tm.group(1))), (len(rows), tm.group(1)[:200])
us = open('intl/msg_hash_us.h').read()
ref_tokens = set()
cfg_absent = set()
cfg_full_guards = {}
usval, ussub, usspan, uscmt = {}, {}, [], {}
for k, f, T, a in rows:
if k.endswith('_LV'):
_town = run("grep -lE 'S_\\w*\\((\\w+, *)?%s,' settings/*.h" % T).stdout.strip()
if _town:
# both tokens owned elsewhere: the level-variant descriptor
# row stays a menu-only literal reference
ref_tokens.add(T)
continue
_tvm = re.search(r'MSG_HASH\(\s*(/\*.*?\*/)?\s*\n?\s*MENU_ENUM_LABEL_VALUE_%s,\s*\n?\s*(%s)\s*\n?\s*\)\n?' % (lv_tv[T], CSTR), us)
assert _tvm or run(
r"grep -lE 'S_[A-Z_]+\(\s*(\w+,\s*)?%s,' settings/*.h" % lv_tv[T]).stdout.strip() or run(
"grep -l 'MENU_ENUM_LABEL_VALUE_%s' settings/*.h" % lv_tv[T]).stdout.strip(), (
'LV value token %s has no value string anywhere' % lv_tv[T])
usval[T] = re.sub(r'\s*\n\s*', ' ', _tvm.group(2)) if _tvm else '""'
m2 = re.search(r'MSG_HASH\(\s*\n?\s*MENU_ENUM_SUBLABEL_%s,\s*\n?\s*(%s)\s*\n?\s*\)\n?' % (T, CSTR), us)
if m2:
ussub[T] = re.sub(r'\s*\n\s*', ' ', m2.group(1)); usspan.append((m2.start(), m2.end(), T))
continue
m = re.search(r'MSG_HASH\(\s*(/\*.*?\*/)?\s*\n?\s*MENU_ENUM_LABEL_VALUE_%s,\s*\n?\s*(%s)\s*\n?\s*\)\n?' % (T, CSTR), us)
if not m:
_own = run("grep -lE 'S_\\w*\\((\\w+, *)?%s,' settings/*.h" % T).stdout.strip()
if _own:
ref_tokens.add(T)
continue
assert m, ('VALUE', T)
usval[T] = re.sub(r'\s*\n\s*', ' ', m.group(2)); usspan.append((m.start(), m.end(), T))
if m.group(1):
uscmt[T] = m.group(1)
m = re.search(r'MSG_HASH\(\s*(/\*.*?\*/)?\s*\n?\s*MENU_ENUM_SUBLABEL_%s,\s*\n?\s*(%s)\s*\n?\s*\)\n?' % (T, CSTR), us)
if m:
ussub[T] = re.sub(r'\s*\n\s*', ' ', m.group(2)); usspan.append((m.start(), m.end(), T))
if m.group(1):
uscmt[T] = (uscmt.get(T, '') + ' ' + m.group(1)).strip()
lblstr = open('msg_hash_lbl_str.h').read()
names, lblstr_span = {}, []
for k, f, T, a in rows:
if T in ref_tokens:
continue
m = re.search(r'#define MENU_ENUM_LABEL_%s_STR (%s)\n' % (T, CSTR), lblstr)
if not m and run("grep -lE 'S_\\w*\\((\\w+, *)?%s,' settings/*.h" % T).stdout.strip():
# the label already lives in a migrated def (a level-variant twin
# took it); this row becomes a reference
ref_tokens.add(T)
usval.pop(T, None); ussub.pop(T, None)
usspan = [sp for sp in usspan if sp[2] != T]
continue
assert m, T
names[T] = m.group(1); lblstr_span.append((m.start(), m.end()))
ext = run('grep -rl "MENU_ENUM_LABEL_%s_STR\\b" --include=*.h --include=*.c . | grep -v msg_hash_lbl' % T).stdout.strip()
assert ext == '', (T, ext)
lbl = open('intl/msg_hash_lbl.h').read()
lbl_span = []
for k, f, T, a in rows:
if T in ref_tokens:
continue
m = re.search(r'MSG_HASH\(\s*\n?\s*MENU_ENUM_LABEL_%s,\s*\n?\s*MENU_ENUM_LABEL_%s_STR\s*\n?\s*\)\n?' % (T, T), lbl)
assert m, T
lbl_span.append((m.start(), m.end()))
# Entanglement guard: a field that also appears in a variant row
# (_LV level overrides, _EX, _AT and friends) elsewhere shares runtime
# state with that row; migrating only the plain row breaks the
# variant's resolution - found empirically when the widget scale
# migration zeroed the fullscreen override's default. Such tables
# wait for the variant grammar.
_ms_all = open('menu/menu_setting.c').read()
# the migrating table's own body is not an entanglement with itself
_ms_scan = _ms_all[:tm.start()] + _ms_all[tm.end():]
for _k, _f, _T, _a in rows:
if _k.endswith('_LV') or not _f:
continue
_vm = re.search(r'SDESC_\w+_ROW_(?:AT|AT_EX)\(\s*%s,' % _f, _ms_scan)
assert not _vm, ("field %s is entangled with a variant row" % _f,
_ms_scan[_vm.start():_vm.start()+60] if _vm else '')
if re.search(r'SDESC_\w+_ROW_LV\(\s*%s,' % _f, _ms_scan):
print(' note: %s has a level-variant twin - it will reference this def' % _f)
print("extraction ok: %d settings (%d without sublabel)" % (len(rows), sum(1 for _,_,T,_ in rows if T not in ussub)))
cfg = open('configuration.c').read()
cfg_spans = []
cfg_keeps = []
cfg_guards = {}
for k, f, T, a in rows:
if T in ref_tokens:
continue
_mac = {'STRING': 'ARRAY', 'DIR': 'PATH'}.get(k, k.replace('_EX', '').replace('_LV', ''))
m = re.search(r' *SETTING_%s\(\s*%s, *&?settings->\w+\.%s,[^;]*;\n' % (_mac, re.escape(names[T]), f), cfg)
if k.endswith('_LV') and f and re.search(r'SDESC_\w+_ROW(?:_P|_DS|_EX)?\(\s*%s,' % f, _ms_scan):
# the field has a plain twin in another table sharing one config
# row whose default belongs to neither variant exclusively; the
# configuration row stays literal
print(' note: %s shares its config row with a twin table - kept literal' % T)
m = None
cfg_guards[T] = ()
_keep_lv_twin = True
else:
_keep_lv_twin = False
if _keep_lv_twin:
pass
elif k in ('DIR', 'PATH', 'PATH_DS', 'STRING', 'STRING_LV', 'STRING_P', 'ACTION', 'ACTION_EX', 'ACTION_LV', 'INT_AT', 'UINT_AT_EX'):
m = None # dirs keep literal config rows: default-enable varies per row
if m:
cfg_full_guards[T] = tuple(guard_at(cfg, m.start()))
cfg_guards[T] = tuple(g for g in cfg_full_guards[T] if g not in table_guard)
cfg_spans.append((m.start(), m.end())); continue
m = re.search(r' *SETTING_%s\(\s*("(?:[^"\\]|\\.)*"), *&?settings->\w+\.%s,[^;]*;\n' % (r'\w+' if k in ('DIR', 'PATH', 'PATH_DS', 'STRING_P', 'ACTION', 'ACTION_EX', 'ACTION_LV', 'STRING_LV', 'INT_AT', 'UINT_AT_EX') else _mac, f), cfg)
if not m and _keep_lv_twin:
print(' note: %s config row untouched' % T)
continue
if not m and k in ('DIR', 'PATH', 'PATH_DS', 'STRING_P', 'ACTION', 'ACTION_EX', 'ACTION_LV', 'STRING_LV', 'INT_AT', 'UINT_AT_EX'):
# unpersisted setting: keep-literal kinds may lack a
# config row entirely; nothing to delete or emit
print(' note: %s has no config row - unpersisted, kept absent' % T)
continue
if not m and f and not re.search(r'settings->\w+\.%s\b' % re.escape(f), cfg):
# genuinely unpersisted: the field never appears in the
# configuration unit at all, so upstream saved nothing and the
# def emits no config row - behavior preserved exactly
print(' note: %s is unpersisted upstream - config row kept absent' % T)
cfg_absent.add(T)
continue
if not m and f and re.search(r'settings->\w+\.%s\s*=' % re.escape(f), cfg):
# custom persistence: the field is assigned outside the SETTING_
# rows (packed or computed defaults); config semantics stay with
# that code and the def emits no config row
print(' note: %s persists through custom code - config row kept absent' % T)
cfg_absent.add(T)
continue
assert m, (T, f, "no config row at all")
cfg_keeps.append((T, m.group(1)))
print(" note: %s config key %s differs from label %s - config row stays literal" % (T, m.group(1), names[T]))
KEEP = set(T for T, _ in cfg_keeps)
# Common enclosing guard of the removed config rows. These rows may sit
# inside a group-level guard in configuration.c (e.g. #ifdef HAVE_MENU
# around the whole menu-visibility block) that is NOT a per-row guard in
# the descriptor table, so it is never carried into the def file. Capture
# it now (before the rows are deleted) and replicate it on the config-pass
# include, or a HAVE_MENU-less build expands the rows against undeclared
# DEFAULT_* macros. Flags already carried per-row in the def file are
# dropped so the include is not double-guarded.
_row_stacks = [guard_at(cfg, s) for s, e in cfg_spans]
_common = _row_stacks[0] if _row_stacks else ()
for _st in _row_stacks[1:]:
_i = 0
while _i < len(_common) and _i < len(_st) and _common[_i] == _st[_i]:
_i += 1
_common = _common[:_i]
_defrow_flags = {g.split()[-1] for gs in guards.values() for g in gs}
CFG_GROUP_GUARD = tuple(g for g in _common if g.split()[-1] not in _defrow_flags)
if CFG_GROUP_GUARD:
print(" config-pass include will be guarded by: %s" % ' '.join(CFG_GROUP_GUARD))
for s, e in sorted(cfg_spans, reverse=True): cfg = cfg[:s] + cfg[e:]
cfg = re.sub(r'#if[^\n]*\n#endif\n', '', cfg)
out = ['''/* Single-source definitions: %s.
* Grammar identical to settings_def_video_sync.h plus S_FLOAT and
* the _NS no-sublabel variants; the descriptor argument span
* matches SDESC_<kind>_ROW; row order is menu display order;
* h2json.py parses these rows for the Crowdin source upload. */
''' % TITLE]
_mh_text = open('msg_hash.h').read()
_h_used = set()
enum_rows = []
if ref_tokens:
_tail = [r for r in rows if r[2] in ref_tokens]
_ref_literals = {}
for _k3, _f3, _t3, _a3 in _tail:
_head3 = (r'\s*%s,\s*%s,' % (_f3, _t3)) if _f3 else (r'\s*%s\s*(?=[,)])' % _t3)
_rm3 = re.search(r'[ \t]*SDESC_\w+_ROW(?:_P|_DS|_EX|_LV|_AT|_AT_EX)?\(%s(?:[^()]|\((?:[^()]|\([^()]*\))*\))*\),?' % _head3, body)
assert _rm3, _t3
_rl3 = _rm3.group(0).strip()
for _g3 in reversed(guards.get(_f3 or _t3, ())):
_rl3 = _g3 + '\n ' + _rl3 + '\n#endif'
_ref_literals[_t3] = _rl3
print(' note: %d reference row(s) carried literal inside the def' % len(_ref_literals))
else:
_ref_literals = {}
for k, f, T, a in rows:
if T in ref_tokens:
out.append('/* Row referencing %s; strings owned by another def file. */\n'
'#if !defined(SETTINGS_DEF_STRINGS_PASS) && !defined(SETTINGS_DEF_CONFIG_PASS) && !defined(SETTINGS_DEF_ENUM_PASS)\n'
'%s\n#endif' % (T, _ref_literals[T]))
continue
_hs = '_H' if ('MENU_LBL_H(%s),' % T) in _mh_text else ''
if _hs:
assert ('MENU_LABEL(%s),' % T) not in _mh_text, (T, 'both enum forms present')
if T in ussub:
if '_AT' in k:
row = 'S_%s%s(%s, %s,\n %s,\n %s,\n %s,\n %s)' % (k, _hs, at_offs[T], T, names[T], a, usval[T], ussub[T])
elif k.endswith('_LV'):
if k.startswith('ACTION'):
row = 'S_%s%s(%s, %s,\n %s,%s\n %s,\n %s)' % (k, _hs, T, lv_tv[T], names[T], (' ' + a + ',') if a else '', usval[T], ussub[T])
else:
row = 'S_%s%s(%s, %s, %s,\n %s,\n %s,\n %s,\n %s)' % (k, _hs, f, T, lv_tv[T], names[T], a, usval[T], ussub[T])
elif k.startswith('ACTION'):
row = 'S_%s%s(%s,\n %s,%s\n %s,\n %s)' % (k, _hs, T, names[T], (' ' + a + ',') if a else '', usval[T], ussub[T])
elif True:
row = 'S_%s%s(%s, %s,\n %s,\n %s,\n %s,\n %s)' % (
k, _hs, f, T, names[T], a, usval[T], ussub[T])
if _hs: _h_used.add('S_%s_H' % k)
else:
if '_AT' in k:
row = 'S_%s_NS%s(%s, %s,\n %s,\n %s,\n %s)' % (k, _hs, at_offs[T], T, names[T], a, usval[T])
elif k.endswith('_LV'):
if k.startswith('ACTION'):
row = 'S_%s_NS%s(%s, %s,\n %s,%s\n %s)' % (k, _hs, T, lv_tv[T], names[T], (' ' + a + ',') if a else '', usval[T])
else:
row = 'S_%s_NS%s(%s, %s, %s,\n %s,\n %s,\n %s)' % (k, _hs, f, T, lv_tv[T], names[T], a, usval[T])
elif k.startswith('ACTION'):
row = 'S_%s_NS%s(%s,\n %s,%s\n %s)' % (k, _hs, T, names[T], (' ' + a + ',') if a else '', usval[T])
else:
row = 'S_%s_NS%s(%s, %s,\n %s,\n %s,\n %s)' % (
k, _hs, f, T, names[T], a, usval[T])
if _hs: _h_used.add('S_%s_NS_H' % k)
if T in uscmt:
row = '/* %s */\n' % uscmt[T].strip('/* ').rstrip(' */') + row
if T in cfg_absent:
row = ('/* Persistence lives in custom configuration code; no config\n'
' * row is emitted. */\n'
'#ifndef SETTINGS_DEF_CONFIG_PASS\n') + row + '\n#endif'
_row_gs_all = list(table_guard) + (list(guards[f or T]) if (f or T) in guards else [])
_cfg_missing_row_guard = (T in cfg_full_guards
and any(g not in cfg_full_guards[T] for g in _row_gs_all))
_cc_src = (cfg_full_guards.get(T) if _cfg_missing_row_guard else cfg_guards.get(T)) or ()
if _cc_src:
_cc = ' && '.join(('defined(%s)' % g[len('#ifdef '):].strip()) if g.startswith('#ifdef ')
else ('!defined(%s)' % g[len('#ifndef '):].strip()) if g.startswith('#ifndef ')
else '(' + g[len('#if '):].strip() + ')' for g in _cc_src)
row = ('/* The configuration row lives under %s; other passes are\n'
' * unaffected. */\n'
'#if !defined(SETTINGS_DEF_CONFIG_PASS) || (%s)\n' % (_cc.replace('defined(', '').replace(')', '') if False else _cc, _cc)) + row + '\n#endif'
if T in KEEP:
ck = dict(cfg_keeps)[T]
row = ('/* config key %s differs from the label string; the\n'
' * configuration.c row stays literal for this setting. */\n'
'#ifndef SETTINGS_DEF_CONFIG_PASS\n' % ck) + row + '\n#endif'
row_gs = list(table_guard) + (list(guards[f or T]) if (f or T) in guards else [])
if row_gs:
gs = row_gs
def _cond(g):
if g.startswith('#ifdef '):
return 'defined(%s)' % g[len('#ifdef '):].strip()
if g.startswith('#ifndef '):
return '!defined(%s)' % g[len('#ifndef '):].strip()
return g[len('#if '):].strip()
# Parenthesize each guard level: an OR inside one level must
# not associate with the && joining the levels - unbracketed,
# 'A || B' && 'C' parses as A || (B && C), which eliminated
# the restart-visibility row in every non-Lakka menu pass.
cond = ' && '.join(
('(%s)' % _cond(g)) if ('||' in _cond(g) or '&&' in _cond(g))
else _cond(g) for g in gs)
gopen = '\n'.join(gs)
gclose = '\n'.join('#endif' for _ in gs)
us_g = guard_of(open('intl/msg_hash_us.h').read(), r'MENU_ENUM_LABEL_VALUE_%s,' % T)
lbl_g = guard_of(open('intl/msg_hash_lbl.h').read(), r'MENU_ENUM_LABEL_%s,' % T)
strings_guarded = bool(us_g) and bool(lbl_g)
strings_free = not us_g and not lbl_g
if us_g and not lbl_g:
# Guarded base-language strings under an unguarded label row:
# the campaign's model is strings-always, so the guarded rows
# are consumed and the def emits them unconditionally. Costs
# a few string bytes on the excluded platform; buys one
# source of truth. The context scaling pair, alternated on
# the OdroidGo2 define, surfaced the class.
print(' note: %s strings were guarded %s - emitted unconditionally' % (T, us_g))
strings_free = True
assert strings_guarded or strings_free, (T, us_g, lbl_g, 'mixed string-guard topology')
if strings_free:
_cfg_exempt = ' || defined(SETTINGS_DEF_CONFIG_PASS)' if _cfg_missing_row_guard else ''
row = ('/* Descriptor%s rows are %s; the string\n'
' * tables always carry this row via the strings pass. */\n'
'#if %s || defined(SETTINGS_DEF_STRINGS_PASS)%s\n' % (
'' if _cfg_missing_row_guard else ' and configuration',
gopen.replace('\n', ' '), cond, _cfg_exempt)) + row + '\n#endif'
enum_rows.append((T, 'S_%s%s_H' % (k, m2 or '') in _h_used if False else ('S_%s_H' % k in _h_used or 'S_%s_NS_H' % k in _h_used), []))
else:
row = gopen + '\n' + row + '\n' + gclose
enum_rows.append((T, 'S_%s_H' % k in _h_used or 'S_%s_NS_H' % k in _h_used, list(gs)))
else:
enum_rows.append((T, 'S_%s_H' % k in _h_used or 'S_%s_NS_H' % k in _h_used, []))
out.append(row)
DEF = os.path.basename(DEF) # includes are emitted relative to settings/
if _h_used:
_pre = ['/* Rows marked _H reserve a MENU_ENUM_LABEL_HELP_ enum member;',
' * outside the enum pass they behave exactly like the base row. */',
'#ifndef SETTINGS_DEF_ENUM_PASS']
for _al in sorted(_h_used):
_pre += ['#ifndef %s' % _al, '#define %s %s' % (_al, _al[:-2]), '#endif']
_pre.append('#endif')
out.insert(1, '\n'.join(_pre))
open(os.path.join('settings', DEF), 'w').write('\n'.join(out) + '\n')
if usspan:
first = min(s for s, e, _sT in usspan)
for s, e, _sT in sorted(usspan, reverse=True): us = us[:s] + us[e:]
else:
# nothing consumed from the base language (a pure-reference
# level-variant row without a sublabel): anchor the region at the
# end of the table, just before the file's closing newline run
first = len(us.rstrip()) + 1
mk_us = lambda b, has_sub: ((' \\\nMSG_HASH(MENU_ENUM_SUBLABEL_##T, sub)' if has_sub else '')
if b.endswith('_LV') else ' \\\nMSG_HASH(MENU_ENUM_LABEL_VALUE_##T, us) \\\nMSG_HASH(MENU_ENUM_SUBLABEL_##T, sub)'
if has_sub else ' \\\nMSG_HASH(MENU_ENUM_LABEL_VALUE_##T, us)')
region = ('/* GENERATED REGION: %s (see %s). */\n' % (TITLE, DEF)
+ '#define SETTINGS_DEF_STRINGS_PASS\n'
+ defs(mk_us) + '\n#include "../settings/%s"\n' % DEF + UNDEFS + '\n#undef SETTINGS_DEF_STRINGS_PASS\n')
us = us[:first] + region + us[first:]
us = re.sub(r'#if[^\n]*\n#endif\n', '', us)
open('intl/msg_hash_us.h','w').write(us)
for s, e in sorted(lblstr_span, reverse=True): lblstr = lblstr[:s] + lblstr[e:]
open('msg_hash_lbl_str.h','w').write(lblstr)
for s, e in sorted(lbl_span, reverse=True): lbl = lbl[:s] + lbl[e:]
lbl = re.sub(r'#if[^\n]*\n#endif\n', '', lbl)
open('intl/msg_hash_lbl.h','w').write(lbl)
mc = open('intl/msg_hash_us.c').read()
assert mc.count('#include "../settings/settings_def_video_sync.h"') == 2
mc = mc.replace('#include "../settings/settings_def_video_sync.h"',
'#include "../settings/settings_def_video_sync.h"\n#include "../settings/%s"' % DEF)
open('intl/msg_hash_us.c','w').write(mc)
assert cfg.count('#include "settings/settings_def_video_sync.h"') == 5
if '#define SETTINGS_DEF_CONFIG_PASS' not in cfg:
cfg = cfg.replace('#include "settings/settings_def_video_sync.h"',
'#define SETTINGS_DEF_CONFIG_PASS\n#include "settings/settings_def_video_sync.h"')
cfg = cfg.replace('#undef S_BOOL\n', '#undef SETTINGS_DEF_CONFIG_PASS\n#undef S_BOOL\n')
if CFG_GROUP_GUARD:
_cfg_inc = ('\n'.join(CFG_GROUP_GUARD) + '\n#include "settings/%s"\n' % DEF
+ '\n'.join('#endif' for _ in CFG_GROUP_GUARD))
else:
_cfg_inc = '#include "settings/%s"' % DEF
cfg = cfg.replace('#include "settings/settings_def_video_sync.h"',
'#include "settings/settings_def_video_sync.h"\n' + _cfg_inc)
open('configuration.c','w').write(cfg)
ms = open('menu/menu_setting.c').read()
tm = re.search(r'static const setting_desc_t %s\[\] = \{\n(.*?)\n( *)\};' % TABLE, ms, re.S)
MENU_EMIT = {'S_BOOL_EX':'SDESC_BOOL_ROW_EX(f, T, d, sd, df, c, ok, rp, sta, sel, lf, rt, ui),',
'S_UINT_EX':'SDESC_UINT_ROW_EX(f, T, d, sd, df, c, mn, mx, st, ob, ok, rp, sta, sel, lf, rt, ui),',
'S_INT_EX':'SDESC_INT_ROW_EX(f, T, d, sd, df, c, mn, mx, st, ob, ok, rp, sta, sel, lf, rt, ui),',
'S_FLOAT_EX':'SDESC_FLOAT_ROW_EX(f, T, d, rnd, sd, df, c, mn, mx, st, ok, rp, sta, sel, lf, rt, ui),',
'S_ACTION_EX':'SDESC_ACTION_ROW_EX(T, sd, ok, rp, c),',
'S_ACTION':'SDESC_ACTION_ROW(T),',
'S_STRING_P':'SDESC_STRING_ROW_P(f, T, d, sd, c, ok, rp, sta, sel, lf, rt, ui),',
'S_PATH':'SDESC_PATH_ROW(f, T, d, sd, c, vals, rp, ui),',
'S_PATH_DS':'SDESC_PATH_ROW_DS(f, T, df2, sd, c, vals, rp, ui),',
'S_DIR':'SDESC_DIR_ROW(f, T, d, el, sd, c, sta),',
'S_STRING':'SDESC_STRING_ROW(f, T, d, sd, c, ok, rp, sta, sel, lf, rt, ui),',
'S_BOOL':'SDESC_BOOL_ROW(f, T, d, sd, df, c),',
'S_UINT':'SDESC_UINT_ROW(f, T, d, sd, df, c, mn, mx, st, ob, ok, rp),',
'S_INT': 'SDESC_INT_ROW(f, T, d, sd, df, c, mn, mx, st, ob, ok, rp),',
'S_FLOAT':'SDESC_FLOAT_ROW(f, T, d, rnd, sd, df, c, mn, mx, st, ok, rp),',
'S_BOOL_LV':'SDESC_BOOL_ROW_LV(f, T, TV, d, sd, df, c),',
'S_FLOAT_LV':'SDESC_FLOAT_ROW_LV(f, T, TV, d, rnd, sd, df, c),',
'S_STRING_LV':'SDESC_STRING_ROW_LV(f, T, TV, d, sd, c, ok, rp, sta, sel, lf, rt, ui),',
'S_ACTION_LV':'SDESC_ACTION_ROW_LV(T, TV, sd, ok, rp, c),',
'S_INT_AT':'SDESC_INT_ROW_AT(offs, T, d, sd, df, c, mn, mx, st, ob, ok, rp),',
'S_UINT_AT_EX':'SDESC_UINT_ROW_AT_EX(offs, T, d, sd, df, c, mn, mx, st, ob, ok, rp, sta, sel, lf, rt, ui),'}
mk_menu = lambda b, _s: ' \\\n ' + MENU_EMIT[b]
new_body = ('/* GENERATED: rows come from %s in order. */\n' % DEF
+ '\n#include "../settings/%s"' % DEF)
ms = ms[:tm.start(1)] + new_body + ms[tm.end(1):]
open('menu/menu_setting.c','w').write(ms)
print("surgeries ok")
CFB = "-std=gnu89 -pedantic-errors -Wno-long-long -Wno-overlength-strings -O2 -fsyntax-only -I. -Imenu -Ilibretro-common/include -Ideps/7zip -Ideps/rcheevos/include -I/tmp/settings_mig_stubs -DRARCH_INTERNAL -DHAVE_MENU"
CF = CFB + " -DHAVE_CONFIGFILE -DHAVE_PATCH -DHAVE_REWIND -DHAVE_SCREENSHOTS -DHAVE_CHEATS -DHAVE_OVERLAY -DHAVE_MICROPHONE"
# Guard-isolation lanes: exercise every flag that gates a row *in
# isolation*. A dropped inner guard nested under an outer flag only
# breaks when the outer flag is set and the inner is not, so each gating
# flag must be compiled on its own - the eject-disc break (HAVE_CDROM
# set, HAVE_LAKKA unset) slipped through only because no base lane
# defined HAVE_CDROM by itself.
def _lakka(flags):
return (flags + ' -DHAVE_LAKKA_SERVER=\\"x\\" -DHAVE_LAKKA_PROJECT=\\"x\\"'
if 'HAVE_LAKKA ' in flags + ' ' else flags)
_gflags = sorted({tok for gs in guards.values() for g in gs
for tok in __import__('re').findall(r'\b([A-Z_][A-Z0-9_]{2,})\b', g)
if tok != 'defined'})
_iso = []
for _fl in _gflags:
_d = ' ' + _couple('-D' + _fl)
# menu_setting.c carries pre-existing -pedantic noise under HAVE_LAKKA
# (systemd_service_toggle static initializer); the base LAKKA lane
# already tolerates it with ('0','4'), so match that here rather than
# abort a legitimate migration that happens to gate a row on LAKKA.
_mok = ('0', '4') if 'HAVE_LAKKA' in _fl else (('0', '2') if '_3DS' in _fl else ('0',))
_iso += [('menu/menu_setting.c', CF + _d, _mok),
('intl/msg_hash_us.c', CF + ' -DHAVE_LANGEXTRA' + _d, ('0',)),
('configuration.c', CF + _lakka(_d), ('0',))]
_LANES = [('menu/menu_setting.c', CFB, ('0',)), ('menu/menu_setting.c', CF, ('0',)),
('menu/menu_setting.c', CF + ' -DHAVE_D3DKMT', ('0',)),
('menu/menu_setting.c', CF + ' -DRARCH_MOBILE -DHAVE_LIBRETRODB -DHAVE_GLSL -DHAVE_NETWORKING -DHAVE_LAKKA -DHAVE_AUDIOMIXER -DHAVE_QT -DHAVE_THREADS', ('0','4')),
('configuration.c', CF, ('0',)), ('intl/msg_hash_us.c', CF + ' -DHAVE_LANGEXTRA', ('0',))]
# Headless lanes: configuration.c and the string tables are compiled in
# HAVE_MENU-less builds too (menu_setting.c is not - it is gated on
# HAVE_MENU_COMMON in Makefile.common). Menu-visibility rows and their
# DEFAULT_* live under #ifdef HAVE_MENU, so a migration that lands a menu
# group's config-pass include OUTSIDE that guard breaks the headless
# build. CFB/CF always define HAVE_MENU, so without this no lane sees it
# - which is exactly how the menu_show_* groups broke the ASan headless
# CI lane.
CF_HL = CF.replace(' -DHAVE_MENU', '')
_headless = [('configuration.c', CF_HL, ('0',)),
('intl/msg_hash_us.c', CF_HL + ' -DHAVE_LANGEXTRA', ('0',))]
for tu, cfgf, okset in _LANES + _iso + _headless:
r = run("gcc %s %s 2>&1 | grep -c 'error:'" % (cfgf, tu))
if r.stdout.strip() not in okset:
# parent-baseline tolerance: platform lanes carry pre-existing
# pedantic noise the battery did not mint; a migration is clean
# when the count matches the parent tree exactly
run('git stash -q')
_p = run("gcc %s %s 2>&1 | grep -c 'error:'" % (cfgf, tu)).stdout.strip()
run('git stash pop -q')
if r.stdout.strip() == _p:
print(' note: lane %s carries %s pre-existing errors - parent-matched' % (cfgf.split()[-1], _p))
continue
assert r.stdout.strip() in okset, (tu, cfgf, run("gcc %s %s 2>&1 | grep -B1 error: | head -5" % (cfgf, tu)).stdout)
print("gate: lanes clean (%d base + %d guard-isolation + %d headless)" % (
len(_LANES), len(_iso), len(_headless)))
# ---- TOKEN-STREAM GATE: preprocessor-level emission identity ----
CPP = "gcc -E -P -I. -Imenu -Ilibretro-common/include -Ideps/7zip -Ideps/rcheevos/include -DRARCH_INTERNAL -DHAVE_MENU -DHAVE_CONFIGFILE -DHAVE_PATCH -DHAVE_REWIND -DHAVE_SCREENSHOTS -DHAVE_CHEATS -DHAVE_OVERLAY -DHAVE_MICROPHONE"
def table_tokens(src_path, extra=""):
"""Preprocessed initializer token stream, or None when the table's
enclosing guards eliminate it in this polarity."""
# Self-healing lane environment: external dependencies (mist.h for
# Steam builds) are not in-tree, and -E only needs the header to
# exist. Missing headers are stubbed empty, bounded, with a notice;
# an empty preprocessor output must be a table fact, never a lane
# environment defect.
os.makedirs('/tmp/settings_mig_stubs', exist_ok=True)
for _ in range(6):
r = run("%s -I/tmp/settings_mig_stubs %s %s 2>&1" % (CPP, extra, src_path))
fe = re.search(r"fatal error: ([\w./-]+\.h): No such file", r.stdout + r.stderr)
if not fe:
break
stub = os.path.join('/tmp/settings_mig_stubs', fe.group(1))
os.makedirs(os.path.dirname(stub) or '/tmp/settings_mig_stubs', exist_ok=True)
assert not os.path.exists(stub), ("stub loop", fe.group(1))
open(stub, 'w').write('/* empty stub for the token gate */\n')
print(" note: stubbed external header %s for the token gate" % fe.group(1))
m2 = re.search(r'static const setting_desc_t %s\[\] = \{(.*?)\};' % TABLE, r.stdout, re.S)
if not m2:
return None
return re.sub(r'\s+', ' ', m2.group(1)).strip()
run("git show HEAD:menu/menu_setting.c > /tmp/ms_head.c")
# Polarities are derived from the guards in and around the table: the
# enclosing stack (a whole table can live under a platform guard) plus
# every per-row guard. Each flag gets a lane; the table may legitimately
# not exist in some polarity, but pre and post must agree about that,
# and at least one polarity must see it.
_head = open('/tmp/ms_head.c').read()
_tm = re.search(r'static const setting_desc_t %s\[\] = \{' % TABLE, _head)
_gtoks = set()
for _g in guard_at(_head, _tm.start()):
_gtoks |= set(re.findall(r'\b([A-Z_][A-Z0-9_]{2,})\b', _g)) - {'defined'}
for _gs in guards.values():
for _g in (_gs if isinstance(_gs, (list, tuple)) else [_gs]):
_gtoks |= set(re.findall(r'\b([A-Z_][A-Z0-9_]{2,})\b', str(_g))) - {'defined'}
pols = [""] + sorted(_couple("-D%s" % g) for g in _gtoks)
if len(_gtoks) > 1:
pols.append(_couple(' '.join(sorted("-D%s" % g for g in _gtoks))))
seen_present = False
for pol in pols:
pre_t = table_tokens('/tmp/ms_head.c', pol)
post_t = table_tokens('menu/menu_setting.c', pol)
assert (pre_t is None) == (post_t is None), \
"TABLE EXISTENCE DRIFT (%s): pre=%s post=%s" % (pol, pre_t is not None, post_t is not None)
if pre_t is None:
continue
seen_present = True
if post_t.endswith(',') and not pre_t.endswith(','):
post_t = post_t[:-1].rstrip()
assert pre_t == post_t, "TOKEN DRIFT (%s):\npre: %s\npost: %s" % (pol, pre_t[:300], post_t[:300])
assert seen_present, "table not visible in any derived polarity"
print("gate: preprocessed table token-identical across %d derived polarities" % len(pols))
msn = open('menu/menu_setting.c').read()
for m in re.finditer(r'static const setting_desc_t \w+\[\] = \{\n(.*?)\n *\};', msn, re.S):
depth = 0; has_top = False
for ln in m.group(1).split('\n'):
s = ln.strip()
assert s != ',', "lone comma"
if s.startswith('#if') and not s.startswith('#include'): depth += 1
elif s.startswith('#endif'): depth -= 1
elif s.startswith('SDESC_') and depth == 0: has_top = True
assert has_top or '#include' in m.group(1), "empty-risk table"
print("gate: structural audits clean")
r = run("cd intl && python3 h2json.py msg_hash_us.h")
assert r.returncode == 0, r.stderr[-200:]
new = json.load(open('intl/msg_hash_us.json'))
run("rm -rf /tmp/base && mkdir -p /tmp/base/intl /tmp/base/settings")
run("git show HEAD:intl/msg_hash_us.h > /tmp/base/intl/msg_hash_us.h; git show HEAD:intl/h2json.py > /tmp/base/intl/h2json.py")
for df in run("git ls-tree -r HEAD --name-only | grep '^settings/settings_def_'").stdout.split():
run("git show HEAD:%s > /tmp/base/%s" % (df, df))
run("cd /tmp/base/intl && python3 h2json.py msg_hash_us.h")
basej = json.load(open('/tmp/base/intl/msg_hash_us.json'))
_miss = sorted(set(basej) - set(new)); _extra = sorted(set(new) - set(basej))
_diff = sorted(x for x in set(basej) & set(new) if basej[x] != new[x])
assert not _miss and not _extra and not _diff, (
'us.json drift: missing=%s extra=%s changed=%s' % (_miss[:4], _extra[:4],
[(k, basej[k][:50], new[k][:50]) for k in _diff[:2]]))
os.remove('intl/msg_hash_us.json')
print("gate: us.json exactly equal (%d keys)" % len(new))
MH = "msg_hash.c intl/msg_hash_us.c libretro-common/string/stdstring.c libretro-common/compat/compat_strl.c libretro-common/encodings/encoding_utf.c"
B = "-O2 -I. -Ilibretro-common/include -DRARCH_INTERNAL -DHAVE_MENU -DHAVE_LANGEXTRA"
r = run("gcc %s %s /tmp/stubs.c /tmp/test_msg_hash.c -o /tmp/tmh_g && /tmp/tmh_g > /tmp/out_g1.txt" % (B, MH))
assert os.path.getsize('/tmp/out_g1.txt') > 10**6, r.stderr[-200:]
def clean_objs():
for f in ('intl/msg_hash_us','menu/menu_setting','configuration'):
for ext in ('.o','.d'):
p = 'obj-unix/release/%s%s' % (f, ext)
if os.path.exists(p): os.remove(p)
def build_dump(out):
clean_objs(); run("rm -f ./retroarch"); run("CFLAGS='-O0 -g0' CXXFLAGS='-O0 -g0' nice make -j%d" % os.cpu_count())
if not os.path.exists('./retroarch'):
run("nice make -j%d" % os.cpu_count())
assert os.path.exists('./retroarch'), "BUILD FAILED: " + run("make 2>&1 | tail -2").stdout
run("HOME=/tmp/ra_home RETROARCH_SETTINGS_DUMP=%s timeout 8 ./retroarch --config /tmp/ra.cfg" % out)
assert os.path.getsize(out) > 10000
build_dump('/tmp/dump_g.txt')
assert run('git diff --quiet').returncode != 0, "VACUOUS"
r = run('git stash -u'); assert 'Saved' in r.stdout
try:
build_dump('/tmp/dump_gpre.txt')
# Per-run 244K baseline from the parent revision, inside the stash
# window: the integrated enum stage renumbers on every migration,
# so a session baseline goes stale after each commit.
r = run("gcc %s %s /tmp/stubs.c /tmp/test_msg_hash.c -o /tmp/tmh_p && /tmp/tmh_p > /tmp/out_gpre.txt" % (B, MH))
assert os.path.getsize('/tmp/out_gpre.txt') > 10**6, r.stderr[-200:]
finally:
assert run('git stash pop').returncode == 0
if run('cmp -s /tmp/out_gpre.txt /tmp/out_g1.txt').returncode != 0:
# strings-always emission legitimately resolves lookups that were
# guarded null on this build's define set; tolerate null-to-value
# transitions, and only those, for this migration's own tokens
_migstr = set(v.strip('"') for d in (usval, ussub, names) for v in d.values()
if isinstance(v, str))
_pre_l = open('/tmp/out_gpre.txt').read().split('\n')
_post_l = open('/tmp/out_g1.txt').read().split('\n')
assert len(_pre_l) == len(_post_l), 'lookup row count changed'
for _a, _b in zip(_pre_l, _post_l):
if _a == _b:
continue
_pa, _pb = _a.split('|'), _b.split('|')
assert (_pa[:2] == _pb[:2] and _pa[2] == 'null' and _pb[2] != 'null'
and _pb[2] in _migstr), \
run('diff /tmp/out_gpre.txt /tmp/out_g1.txt | head -4').stdout
print(' note: strings-always resolved lookups that were guarded null')
print("gate: 244K lookup byte-identical (per-run parent baseline)")
assert os.path.exists(os.path.join('settings', DEF)), "def file lost"
assert run('cmp -s /tmp/dump_gpre.txt /tmp/dump_g.txt').returncode == 0, run('diff /tmp/dump_gpre.txt /tmp/dump_g.txt | head -4').stdout
print("gate: settings dump byte-identical")
# ---- ENUM STAGE: consolidate this group's enum rows in msg_hash.h ----
# Marker-based insertion: the first removed row is replaced by a unique
# marker BEFORE the husk cleanup, so no offset can go stale - the class
# of defect that spliced the ANDROID overlay member.
r = run("python3 tools/msg_hash_name_harness.py /tmp/enum_pre.txt")
assert r.returncode == 0, r.stderr[-300:]
mh = open('msg_hash.h').read()
def _plains(text):
b = re.search(r'enum msg_hash_enums\n\{\n(.*?)\n\};', text, re.S).group(1)
return set(mm.group(1) for ln in b.split('\n')
for mm in [re.match(r'([A-Za-z_]\w*)\s*(?:=[^,]*)?,', ln.strip())] if mm)
_census_pre = _plains(mh)
_enum_ok = True
for _T, _h, _eg in enum_rows:
_form = 'MENU_LBL_H' if _h else 'MENU_LABEL'
_line = ' %s(%s),\n' % (_form, _T)
if mh.count(_line) != 1:
print(" enum stage skipped: %s row count %d" % (_T, mh.count(_line)))
_enum_ok = False; break
_stack = []
for _gl in re.finditer(r'^[ \t]*#(if\w*[^\n]*|else|endif)', mh[:mh.index(_line)], re.M):
_s = _gl.group(1)
if _s.startswith('if'): _stack.append('#' + _s.strip())
elif _s.startswith('endif') and _stack: _stack.pop()
_et = set(x for g in _stack if '__MSG_HASH_H' not in g
for x in re.findall(r'\b([A-Z_][A-Z0-9_]{2,})\b', g) if x != 'defined')
_dt = set(x for g in _eg for x in re.findall(r'\b([A-Z_][A-Z0-9_]{2,})\b', g) if x != 'defined')
if _et != _dt:
print(" enum stage skipped: %s guard mismatch enum=%s def=%s" % (_T, sorted(_et), sorted(_dt)))
_enum_ok = False; break
if not enum_rows:
_enum_ok = False
print('gate: enum stage - nothing to consolidate (all rows are references)')
if _enum_ok:
MARK = '/*__SETTINGS_DEF_ENUM_MARK__*/'
_first = True
for _T, _h, _eg in enum_rows:
_line = ' %s(%s),\n' % ('MENU_LBL_H' if _h else 'MENU_LABEL', _T)
mh = mh.replace(_line, MARK + '\n' if _first else '', 1)
_first = False
mh = re.sub(r'#if[^\n]*\n(?:%s\n)?#endif\n' % re.escape(MARK),
lambda m: MARK + '\n' if MARK in m.group(0) else '', mh)
_ar = dict(re.findall(r'#define (S_\w+)\(([^)]*)\)', open('intl/msg_hash_us.c').read()))
_d = [' /* GENERATED REGION: %s enum rows (see settings/%s). */' % (TITLE, DEF),
'#define SETTINGS_DEF_ENUM_PASS',
'#define SETTINGS_DEF_STRINGS_PASS']
for _n in ('S_BOOL','S_BOOL_NS','S_UINT','S_UINT_NS','S_INT','S_INT_NS','S_FLOAT','S_FLOAT_NS','S_STRING','S_STRING_NS','S_DIR','S_DIR_NS','S_STRING_P','S_STRING_P_NS','S_PATH','S_PATH_NS','S_PATH_DS','S_PATH_DS_NS','S_ACTION','S_ACTION_NS','S_BOOL_EX','S_BOOL_EX_NS','S_UINT_EX','S_UINT_EX_NS','S_INT_EX','S_INT_EX_NS','S_FLOAT_EX','S_FLOAT_EX_NS','S_ACTION_EX','S_ACTION_EX_NS','S_BOOL_LV','S_BOOL_LV_NS','S_FLOAT_LV','S_FLOAT_LV_NS','S_STRING_LV','S_STRING_LV_NS','S_ACTION_LV','S_ACTION_LV_NS','S_INT_AT','S_INT_AT_NS','S_UINT_AT_EX','S_UINT_AT_EX_NS'):
_d.append('#define %s(%s) MENU_LABEL(T),' % (_n, _ar[_n]))
for _hn, _b in (('S_BOOL_H','S_BOOL'), ('S_UINT_H','S_UINT'), ('S_BOOL_NS_H','S_BOOL_NS'),
('S_INT_H','S_INT'), ('S_FLOAT_H','S_FLOAT'),
('S_UINT_NS_H','S_UINT_NS'), ('S_INT_NS_H','S_INT_NS'), ('S_FLOAT_NS_H','S_FLOAT_NS'),
('S_STRING_H','S_STRING'), ('S_STRING_NS_H','S_STRING_NS'),
('S_DIR_H','S_DIR'), ('S_DIR_NS_H','S_DIR_NS'),
('S_STRING_P_H','S_STRING_P'), ('S_STRING_P_NS_H','S_STRING_P_NS'),
('S_PATH_H','S_PATH'), ('S_PATH_NS_H','S_PATH_NS'),
('S_PATH_DS_H','S_PATH_DS'), ('S_PATH_DS_NS_H','S_PATH_DS_NS'),
('S_ACTION_H','S_ACTION'), ('S_ACTION_NS_H','S_ACTION_NS'),
('S_BOOL_EX_H','S_BOOL_EX'), ('S_BOOL_EX_NS_H','S_BOOL_EX_NS'),
('S_UINT_EX_H','S_UINT_EX'), ('S_UINT_EX_NS_H','S_UINT_EX_NS'),
('S_INT_EX_H','S_INT_EX'), ('S_INT_EX_NS_H','S_INT_EX_NS'),
('S_FLOAT_EX_H','S_FLOAT_EX'), ('S_FLOAT_EX_NS_H','S_FLOAT_EX_NS'),
('S_ACTION_EX_H','S_ACTION_EX'), ('S_ACTION_EX_NS_H','S_ACTION_EX_NS')):
_d.append('#define %s(%s) MENU_LBL_H(T),' % (_hn, _ar[_b]))
_d.append('#include "settings/%s"' % DEF)
for _n in ('S_BOOL','S_BOOL_NS','S_UINT','S_UINT_NS','S_INT','S_INT_NS','S_FLOAT','S_FLOAT_NS',
'S_STRING','S_STRING_NS','S_STRING_H','S_STRING_NS_H',
'S_DIR','S_DIR_NS','S_DIR_H','S_DIR_NS_H',
'S_STRING_P','S_STRING_P_NS','S_STRING_P_H','S_STRING_P_NS_H',
'S_PATH','S_PATH_NS','S_PATH_H','S_PATH_NS_H',
'S_PATH_DS','S_PATH_DS_NS','S_PATH_DS_H','S_PATH_DS_NS_H',
'S_ACTION','S_ACTION_NS','S_ACTION_H','S_ACTION_NS_H',
'S_BOOL_EX','S_BOOL_EX_NS','S_BOOL_EX_H','S_BOOL_EX_NS_H',
'S_UINT_EX','S_UINT_EX_NS','S_UINT_EX_H','S_UINT_EX_NS_H',
'S_INT_EX','S_INT_EX_NS','S_INT_EX_H','S_INT_EX_NS_H',
'S_FLOAT_EX','S_FLOAT_EX_NS','S_FLOAT_EX_H','S_FLOAT_EX_NS_H',
'S_ACTION_EX','S_ACTION_EX_NS','S_ACTION_EX_H','S_ACTION_EX_NS_H',
'S_BOOL_H','S_UINT_H','S_BOOL_NS_H','S_INT_H','S_FLOAT_H','S_UINT_NS_H','S_INT_NS_H','S_FLOAT_NS_H',
'SETTINGS_DEF_STRINGS_PASS','SETTINGS_DEF_ENUM_PASS'):
_d.append('#undef %s' % _n)
assert mh.count(MARK) == 1
mh = mh.replace(MARK + '\n', '\n'.join(_d) + '\n')
open('msg_hash.h','w').write(mh)
assert _plains(open('msg_hash.h').read()) == _census_pre, "enum census drift"
r = run("python3 tools/msg_hash_name_harness.py /tmp/enum_post.txt")
assert r.returncode == 0, r.stderr[-300:]
if run('cmp -s /tmp/enum_pre.txt /tmp/enum_post.txt').returncode != 0:
# A token whose strings were first introduced by this migration's
# census rows resolves to null in the mid-run pre dump (the def is
# not yet included anywhere in msg_hash.h) and to its own name
# after the enum stage. That exact transition, for this
# migration's tokens only, is the definition of correct.
_mine = set(names[_t2] for _k2, _f2, _t2, _a2 in rows)
_pre = open('/tmp/enum_pre.txt').read().split('\n')
_post = open('/tmp/enum_post.txt').read().split('\n')
assert len(_pre) == len(_post), 'name dump row count changed'
for _lp, _lq in zip(_pre, _post):
if _lp == _lq:
continue
_pp, _qq = _lp.split('|'), _lq.split('|')
assert (_pp[:2] == _qq[:2] and _pp[2] == 'null'
and _qq[2] in _mine), (_lp, _lq)
_body = re.search(r'enum msg_hash_enums\n\{\n(.*?)\n\};', mh, re.S).group(1)
_toks = set(x for g in re.findall(r'^#if\w*([^\n]*)', _body, re.M)
for x in re.findall(r'\b([A-Z_][A-Z0-9_]{2,})\b', g) if x != 'defined')
import glob as _glob
for _df in _glob.glob('settings/settings_def_*.h'):
for g in re.findall(r'^#if\w*([^\n]*)', open(_df).read(), re.M):
_toks |= set(x for x in re.findall(r'\b([A-Z_][A-Z0-9_]{2,})\b', g)
if x != 'defined' and 'SETTINGS_DEF' not in x)
open('/tmp/mh_tu.c','w').write('#include <stdint.h>\n#include "msg_hash.h"\nint main(void){return (int)MSG_LAST;}\n')
_b = "gcc -std=gnu89 -fsyntax-only -I. -Ilibretro-common/include -DRARCH_INTERNAL"
_lanes = [''] + [_couple('-D%s' % x) for x in sorted(_toks)] + [_couple(' '.join('-D%s' % x for x in sorted(_toks)))]
for _lane in _lanes:
r = run("%s %s /tmp/mh_tu.c 2>&1 | grep -c error:" % (_b, _lane))
assert r.stdout.strip() == '0', (_lane[:60],)
print("gate: enum stage - name dump byte-identical, census exact, %d polarity lanes clean" % len(_lanes))
print("PIPELINE_OK")