Files
gex/scripts/lint-scripts.py
velzieandClaude Opus 5.5 a6e7c17f9e Restructure: hosts/, page/, ports/, site/, toolchain/, wine/, scripts/
The tree by what each part is, behind the top-level Makefile:
- shims/ -> hosts/ (the browser, kernel, native and Wayland hosts)
- web/ -> page/ (the page's source, build and serve), ports/mesa,
  ports/xwayland and ports/tint (the ports the page loads), site/ (static
  sites and the eject page)
- tools/gexcc -> toolchain/gexcc, with the sysroot and busybox builds from
  tools/wine-wasm in toolchain/sysroot and toolchain/busybox, and one
  toolchain/pins (musl, the LP64 musl, LLVM, busybox)
- tools/wine-wasm -> wine/: dist.sh (was wine64-dist.sh), wine.pins (64-bit
  Wine, and the 32-bit tree its wineserver builds from), libs.pins (the Unix
  libraries), the 32-bit-only pieces in wine/legacy32 (its own pins and
  legacy.mk, which wine/Makefile includes)
- tools/*.py and tools/*.sh -> scripts/ (the GL generators in scripts/gl)
- wine32/src.sh is scripts/fetch-pinned.sh, which takes the trees the old
  script marked as its own

References follow the moves: paths from the tree's root by the map, paths
from a script's own directory by where their target went.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_e0aa53aa95eb4b86910519b4d14812a5
2026-11-04 17:30:24 -05:00

106 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Every script in the tree is reached from a make target, or is a public tool.
Called by: make lint.
scripts/lint-scripts.py [-v]
A script is a tracked file that is executable, ends in .sh, .py or .mjs, or is
a Makefile or .mk, outside third_party, crates/*/src, the page's and runtime's module sources
(page/src, page/runtime/src, hosts/browser/js's modules) and test/ (run.mjs's own
tree). It passes when one of these holds:
- the Makefile names it;
- a file that is reached names it (its path from the tree's root, its path
from the naming file's directory, or its file name when no other tracked
file has that name), transitively from the Makefile;
- scripts/public-tools lists it (a tool other repos or users call by path);
- a cargo test names it (crates/*/tests/*.rs: fixture builders).
A Makefile counts as named when its directory is (make -C DIR), and a Python
module when a script beside it imports it. Executables with binary content
(test fixtures) are not scripts.
Exit 1, with the list, when any script passes none. -v prints each script's
first caller.
"""
import os, re, subprocess, sys
root = subprocess.run(['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True,
check=True).stdout.strip()
os.chdir(root)
files = subprocess.run(['git', 'ls-files', '-s'], capture_output=True, text=True,
check=True).stdout.splitlines()
mode = {}
for line in files:
m, _, _, path = line.split(None, 3)
mode[path] = m
SOURCE = re.compile(r'^(third_party/|crates/[^/]+/src/|page/src/|page/runtime/src/|test/)')
def is_script(p):
if SOURCE.match(p):
return False
if p.startswith('hosts/browser/js/') and not os.path.basename(p).startswith('check-'):
return False
if p.endswith(('.sh', '.py', '.mjs', '.mk')) or (os.path.basename(p) == 'Makefile' and p != 'Makefile'):
return True
if mode[p] != '100755':
return False
with open(p, 'rb') as f:
return b'\0' not in f.read(4096) # an executable fixture (.exe, .elf) is no script
scripts = sorted(p for p in mode if is_script(p))
names = {}
for p in mode:
names.setdefault(os.path.basename(p), []).append(p)
def read(p):
try:
with open(p, encoding='utf-8', errors='replace') as f:
return f.read()
except (IsADirectoryError, FileNotFoundError):
return ''
def named_in(text, by, s):
if s in text:
return True
if os.path.basename(s) == 'Makefile' and re.search(r'(^|[\s/])' + re.escape(os.path.dirname(s)) + r'\b', text):
return True # make -C DIR
stem = os.path.basename(s)[:-3]
if s.endswith('.py') and by.endswith('.py') and os.path.dirname(s) == os.path.dirname(by) \
and re.search(r'^\s*(import|from)\s+' + re.escape(stem) + r'\b', text, re.M):
return True # a module beside its importer
rel = os.path.relpath(s, os.path.dirname(by) or '.')
if not rel.startswith('..') and re.search(r'(^|[\s"\'/=(])' + re.escape(rel) + r'\b', text):
return True
b = os.path.basename(s)
return len(names[b]) == 1 and len(b) > 4 and re.search(r'\b' + re.escape(b) + r'\b', text) is not None
public = [l.split()[0] for l in read('scripts/public-tools').splitlines()
if l.strip() and not l.startswith('#')]
reached = {}
todo = ['Makefile']
for p in public:
reached[p] = 'scripts/public-tools'
todo.append(p)
for p in mode:
if re.match(r'crates/[^/]+/tests/[^/]+\.rs$', p):
todo.append(p)
seen = set()
while todo:
by = todo.pop()
if by in seen:
continue
seen.add(by)
text = read(by)
for s in scripts:
if s not in reached and s != by and named_in(text, by, s):
reached[s] = by
todo.append(s)
bad = [s for s in scripts if s not in reached]
if '-v' in sys.argv:
for s in scripts:
print(f'{s}: {reached.get(s, "UNREACHED")}')
for s in bad:
print(f'lint-scripts: {s}: no make target, no caller reached from one, not a public tool')
print(f'lint-scripts: {len(scripts) - len(bad)} of {len(scripts)} scripts reached'
+ ('' if not bad else f', {len(bad)} not'))
sys.exit(1 if bad else 0)