Import LinuxCNC source tree

This commit is contained in:
Wang Dequan
2026-08-20 06:17:39 +08:00
commit 2c814634c3
9429 changed files with 10556013 additions and 0 deletions

156
scripts/manpage-name-check.py Executable file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
# Report man pages whose rendered NAME drifted from their filename.
# asciidoctor takes the .TH name from the page's NAME section, the build pins the filename with -o, so a translated NAME just leaves the two disagreeing.
# Run after building the docs. Argument: the built man tree (default docs/build/man).
# Quiet when nothing drifted. When something has, it prints each page with the
# string to search in Weblate and its current value, so a translator can fix it.
# Never fails the build unless --enforce or MANPAGE_NAME_CHECK_ENFORCE is set:
# under GitHub Actions it writes the same list to the job
# summary and emits one warning pointing at it. The NAME lines are managed in
# Weblate, so fixes go there.
import os
import re
import sys
import glob
# The script lives in scripts/, the docs are one level up at ../docs, so it runs from anywhere.
HERE = os.path.dirname(os.path.realpath(__file__, strict=True))
DOCS = os.path.normpath(os.path.join(HERE, '..', 'docs'))
MAN = os.path.join(DOCS, 'build/man')
PO = os.path.join(DOCS, 'po')
SRC = os.path.join(DOCS, 'src/man')
def th_name(troff):
"""The .TH page name, lowercased with the roff backslash stripped, or None for an .so alias stub."""
with open(troff, encoding='utf-8', errors='replace') as f:
if f.readline().startswith('.so '):
return None
f.seek(0)
for line in f:
m = re.match(r'\.TH "([^"]*)"', line)
if m:
return m.group(1).replace('\\', '').lower()
return None
def name_line(page):
"""The first line of the NAME section of a page's AsciiDoc source, or None for a comp page rendered straight to troff."""
for adoc in sorted(glob.glob(f'{SRC}/man*/{page}.*.adoc')):
lines = open(adoc, encoding='utf-8', errors='replace').read().splitlines()
for i, line in enumerate(lines):
if line.strip() == '== NAME':
for rest in lines[i + 1:]:
if rest.strip():
return rest.strip()
return None
_po = {}
def po(lang):
"""Parse docs/po/<lang>.po into {msgid: msgstr}, joining the continuation lines that wrap one string."""
if lang not in _po:
d = {}
mid = mstr = mode = None
for line in open(f'{PO}/{lang}.po', encoding='utf-8', errors='replace'):
if line.startswith('msgid '):
if mid is not None:
d[mid] = mstr or ''
mid, mstr, mode = line[6:].strip().strip('"'), None, 'id'
elif line.startswith('msgstr '):
mstr, mode = line[7:].strip().strip('"'), 'str'
elif line.startswith('"'):
s = line.strip().strip('"')
if mode == 'id':
mid += s
elif mode == 'str':
mstr += s
elif not line.strip() and mid is not None:
d[mid] = mstr or ''
mid = mstr = mode = None
if mid is not None:
d[mid] = mstr or ''
_po[lang] = d
return _po[lang]
def weblate_string(lang, page):
"""Return (search, current): the text to search for in Weblate and its current translation."""
d = po(lang)
th = page.upper()
# A comp page's name is a troff .TH string, searched by the uppercase name.
if d.get(th, '').strip() and d[th].strip().lower() != page.lower():
return th, d[th]
# An AsciiDoc page's name lives in the NAME line, searched by its text.
nl = name_line(page)
if nl:
cur = d.get(nl)
if cur is None: # the source line and the msgid can differ in trailing text
for mid, mstr in d.items():
if mid.startswith(page) and ' - ' in mid:
nl, cur = mid, mstr
break
return nl, cur if cur is not None else '(search the text)'
return page, '(unknown)'
def drifted(man):
"""Yield (lang, page) for every built man page whose .TH name does not match its filename."""
for dirpath, _dirs, files in os.walk(man):
for fn in files:
if not re.search(r'\.\d$', fn):
continue
th = th_name(os.path.join(dirpath, fn))
page = re.sub(r'\.\d+$', '', fn)
if th is None or th == page.lower():
continue
lang = os.path.relpath(dirpath, man).split(os.sep)[0]
yield ('en' if re.fullmatch(r'man\d+', lang) else lang), page
def collect(man):
"""All drifted rows as (lang, search, current), sorted, English dropped."""
rows = []
for lang, page in drifted(man):
if lang == 'en': # English is the reference, it never drifts
continue
search, current = weblate_string(lang, page)
rows.append((lang, search, current))
rows.sort()
return rows
def print_local(rows):
print(f'# {len(rows)} man-page NAME mismatches. Search the "search" text in the Weblate docs component, language in brackets.')
for lang, search, current in rows:
print(f'\n[{lang}]')
print(f' search: {search}')
print(f' current: {current}')
def write_summary(rows, fh):
fh.write(f'## Manpage NAME mismatches: {len(rows)}\n\n')
fh.write('Translated NAME lines that drifted from the command name.\n')
fh.write('Search the "search" text in the Weblate docs component for the language and fix it there.\n\n')
fh.write('| lang | search | current |\n')
fh.write('| ---- | ------ | ------- |\n')
for lang, search, current in rows:
cells = (c.replace('|', '\\|') for c in (lang, search, current))
fh.write('| %s | %s | %s |\n' % tuple(cells))
def main(argv):
enforce = '--enforce' in argv or os.environ.get('MANPAGE_NAME_CHECK_ENFORCE')
args = [a for a in argv if a != '--enforce']
man = args[1] if len(args) > 1 else MAN
# A built tree has troff pages; without them the docs are not built and a
# silent "all clean" would read as success when nothing was actually scanned.
if not any(re.search(r'\.\d$', f) for _root, _dirs, files in os.walk(man) for f in files):
print(f'{man}: no built man pages found, build the docs first (make manpages docs).', file=sys.stderr)
return 1
rows = collect(man)
if not rows:
return 0 # quiet when nothing drifted
print_local(rows)
summary = os.environ.get('GITHUB_STEP_SUMMARY')
if summary:
with open(summary, 'a', encoding='utf-8') as fh:
write_summary(rows, fh)
if os.environ.get('GITHUB_ACTIONS'):
print(f'::warning title=manpage NAME drift::{len(rows)} manpage NAME line(s) disagree with their filename, see the job summary for the list to fix in Weblate')
return 1 if enforce else 0
if __name__ == '__main__':
sys.exit(main(sys.argv))