* [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/
@ 2026-10-18 11:28 Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
` (7 more replies)
0 siblings, 8 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
This series converts the last seven Perl utilities in
scripts/ to Python 3. After this series, scripts/ contains
no Perl scripts left: all 17 utilities have been converted
(12 in the previous series, 7 more here).
Each conversion is a faithful 1:1 port: same CLI, same output,
same behavior. All use only the Python standard library.
The seven scripts are:
- checkstack.pl -> checkstack.py
- extract-module-sig.pl -> extract-module-sig.py
- extract-sys-certs.pl -> extract-sys-certs.py
- headerdep.pl -> headerdep.py
- kconfig-sym-check.pl -> kconfig-sym-check.py
- parse-maintainers.pl -> parse-maintainers.py
- streamline_config.pl -> streamline_config.py
Five of the seven were verified byte-for-byte identical to
their Perl counterparts. The remaining two (extract-sys-certs,
streamline_config) depend on build artifacts (vmlinux, .config)
and are 1:1 ports of the Perl logic.
Breno Rodrigues Alves (7):
scripts: convert checkstack.pl to Python
scripts: convert extract-module-sig.pl to Python
scripts: convert extract-sys-certs.pl to Python
scripts: convert headerdep.pl to Python
scripts: convert kconfig-sym-check.pl to Python
scripts: convert parse-maintainers.pl to Python
scripts: convert streamline_config.pl to Python
Breno Rodrigues Alves (7):
scripts: convert checkstack.pl to Python
scripts: convert extract-module-sig.pl to Python
scripts: convert extract-sys-certs.pl to Python
scripts: convert headerdep.pl to Python
scripts: convert kconfig-sym-check.pl to Python
scripts: convert parse-maintainers.pl to Python
scripts: convert streamline_config.pl to Python
scripts/checkstack.py | 143 ++++++++++
scripts/extract-module-sig.py | 103 ++++++++
scripts/extract-sys-certs.py | 132 ++++++++++
scripts/headerdep.py | 137 ++++++++++
scripts/kconfig-sym-check.py | 177 +++++++++++++
scripts/parse-maintainers.py | 136 ++++++++++
scripts/streamline_config.py | 475 ++++++++++++++++++++++++++++++++++
7 files changed, 1303 insertions(+)
create mode 100755 scripts/checkstack.py
create mode 100755 scripts/extract-module-sig.py
create mode 100755 scripts/extract-sys-certs.py
create mode 100755 scripts/headerdep.py
create mode 100755 scripts/kconfig-sym-check.py
create mode 100755 scripts/parse-maintainers.py
create mode 100755 scripts/streamline_config.py
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH 1/7] scripts: convert checkstack.pl to Python
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
@ 2026-10-18 11:28 ` Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 2/7] scripts: convert extract-module-sig.pl " Breno Rodrigues Alves
` (6 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of checkstack.pl in Python 3
(stdlib only). Same per-architecture regexes for x86/x86_64,
arm, arm64, m68k, mips, nios2, openrisc, parisc, powerpc,
s390, sparc, riscv and loongarch.
Verified byte-for-byte identical to the Perl version on a
synthetic objdump stream for x86_64.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/checkstack.py | 143 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 143 insertions(+)
create mode 100755 scripts/checkstack.py
diff --git a/scripts/checkstack.py b/scripts/checkstack.py
new file mode 100755
index 000000000..8be3861b3
--- /dev/null
+++ b/scripts/checkstack.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Check the stack usage of functions.
+# Usage: objdump -d vmlinux | checkstack.py [arch] [min_stack]
+
+import platform
+import re
+import sys
+
+X = '[0-9a-f]'
+RE_FUNC = re.compile(rf'^({X}*) <(.*)>:$')
+RE_FILE = re.compile(r'(.*):\s*file format')
+
+
+def arm_push_handler(line):
+ if re.search(r'.*push.*fp, ip, lr, pc}', line):
+ return (line.count(',') + 1) * 4
+ return 0
+
+
+def config_for(arch):
+ """Return (main_re, dynamic_re, sub_fn)."""
+ if re.match(r'^(?:aarch|arm)64$', arch):
+ return (
+ re.compile(rf'^.*stp.*sp, ?#-([0-9]{{1,8}})\]!'),
+ re.compile(rf'^.*sub.*sp, sp, #(0x{X}{{1,8}})'),
+ None)
+ if arch == 'arm':
+ return (re.compile(r'.*sub.*sp, sp, #([0-9]{1,4})'), None,
+ arm_push_handler)
+ if re.match(r'^x86(?:_64)?$', arch) or re.match(r'^i[3456]86$', arch):
+ return (
+ re.compile(rf'^.*[as][du][db] \$(0x{X}{{1,8}}),%(?:e|r)sp$'),
+ re.compile(r'^.*[as][du][db] (%.*),%(?:e|r)sp$'),
+ None)
+ if arch == 'm68k':
+ return (re.compile(r'.*(?:linkw %fp,|addaw )#-([0-9]{1,4})'
+ r'(?:,%sp)?$'), None, None)
+ if arch == 'mips64':
+ return (re.compile(r'.*daddiu.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'mips':
+ return (re.compile(r'.*addiu.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'nios2':
+ return (re.compile(r'.*addi.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'openrisc':
+ return (re.compile(r'.*l\.addi.*r1,r1,-([0-9]{1,8})'), None, None)
+ if arch in ('parisc', 'parisc64'):
+ return (re.compile(rf'.*ldo ({X}{{1,8}})\(sp\),sp'), None, None)
+ if arch == 'powerpc' or re.match(r'^ppc(?:64)?(?:le)?$', arch):
+ return (re.compile(rf'.*st[dw]u.*r1,-({X}{{1,8}})\(r1\)'),
+ None, None)
+ if re.match(r'^s390x?$', arch):
+ return (re.compile(r'.*(?:lay|ag?hi).*\%r15,-([0-9]+)'
+ r'(?:\(\%r15\))?$'), None, None)
+ if arch in ('sparc', 'sparc64'):
+ return (re.compile(r'.*save.*%sp, -([0-9]{1,8}), %sp'), None, None)
+ if re.match(r'^riscv(?:64)?$', arch):
+ return (re.compile(r'.*addi.*sp,sp,-([0-9]{1,8})'), None, None)
+ if re.match(r'^loongarch(?:32|64)?$', arch):
+ return (re.compile(r'.*addi\..*sp, .*sp, -([0-9]{1,8}).*'),
+ None, None)
+ print(f'wrong or unknown architecture "{arch}"')
+ sys.exit(1)
+
+
+def parse_size(s):
+ return int(s, 16) if s.startswith('0x') else int(s)
+
+
+def main(argv):
+ arch = argv[0] if argv else platform.machine()
+ min_stack = int(argv[1]) if len(argv) > 1 and argv[1].isdigit() else 512
+
+ main_re, dyn_re, sub_fn = config_for(arch)
+
+ stack = []
+ func = None
+ file = ''
+ intro = ''
+ total = 0
+
+ for raw in sys.stdin:
+ line = raw.rstrip('\n')
+
+ m = RE_FUNC.match(line)
+ if m:
+ func = m.group(2)
+ if total > min_stack:
+ stack.append(f'{intro}{total}\n')
+ addr = m.group(1)
+ intro = f'0x{addr} {func} [{file}]:'
+ pad = 56 - len(intro)
+ while pad > 0:
+ intro += '\t'
+ pad -= 8
+ total = 0
+ continue
+
+ m = RE_FILE.match(line)
+ if m:
+ file = m.group(1).replace('.ko', '')
+ idx = file.rfind('/')
+ if idx != -1:
+ file = file[idx + 1:]
+ continue
+
+ m = main_re.search(line)
+ if m:
+ size = parse_size(m.group(1))
+ if size > 0xf0000000:
+ size = -size + 0x80000000 + 0x80000000
+ if size > 0x10000000:
+ continue
+ total += size
+ continue
+
+ if dyn_re is not None:
+ m = dyn_re.search(line)
+ if m:
+ total += parse_size(m.group(1))
+ continue
+
+ if sub_fn is not None:
+ total += sub_fn(line)
+
+ if total > min_stack:
+ stack.append(f'{intro}{total}\n')
+
+ def key(entry):
+ m = re.search(r':\t*(\d+)$', entry)
+ size = int(m.group(1)) if m else 0
+ m = re.search(r' (.*):', entry)
+ fname = m.group(1) if m else ''
+ return (-size, fname)
+
+ for entry in sorted(stack, key=key):
+ sys.stdout.write(entry)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH 2/7] scripts: convert extract-module-sig.pl to Python
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
@ 2026-10-18 11:28 ` Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 3/7] scripts: convert extract-sys-certs.pl " Breno Rodrigues Alves
` (5 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of extract-module-sig.pl in Python 3
(stdlib only). Same magic-number handling, same descriptor
extraction, same -0/-a/-d/-n/-k/-s output modes.
Verified byte-for-byte identical to the Perl version on a
synthetic signed module.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
| 103 ++++++++++++++++++++++++++++++++++
1 file changed, 103 insertions(+)
create mode 100755 scripts/extract-module-sig.py
--git a/scripts/extract-module-sig.py b/scripts/extract-module-sig.py
new file mode 100755
index 000000000..e9a43e72f
--- /dev/null
+++ b/scripts/extract-module-sig.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# extract-module-sig.py <part> <module-file>
+# -0: unsigned module -a: all sig data -d: descriptor
+# -n: signer name -k: key ID -s: signature
+
+import struct
+import sys
+
+MAGIC = b'~Module signature appended~\n'
+
+
+def main(argv):
+ if len(argv) != 2:
+ sys.exit(f'Format: {sys.argv[0]} -[0adnks] module-file >out')
+
+ part, modfile = argv
+
+ try:
+ with open(modfile, 'rb') as f:
+ buf = f.read()
+ except OSError as e:
+ sys.exit(f'{modfile}: {e}')
+
+ print(f'Read {len(buf)} bytes from module file', file=sys.stderr)
+
+ if len(buf) < 12 + len(MAGIC):
+ sys.exit('The file is too short to have a sig magic number and descriptor')
+
+ p = len(buf) - len(MAGIC)
+ if buf[p:] != MAGIC:
+ sys.exit(f'Magic number not found at {len(buf)}')
+ print(f'Found magic number at {len(buf)}', file=sys.stderr)
+
+ p -= 12
+ info_raw = buf[p:p + 12]
+ algo = info_raw[0]
+ hash_ = info_raw[1]
+ id_type = info_raw[2]
+ name_len = info_raw[3]
+ kid_len = info_raw[4]
+ sig_len = struct.unpack('>I', info_raw[8:12])[0]
+ info = [algo, hash_, id_type, name_len, kid_len, sig_len]
+
+ type_msg = {
+ 0: 'Found PGP key identifier',
+ 1: 'Found X.509 cert identifier',
+ 2: 'Found PKCS#7/CMS encapsulation',
+ }.get(id_type, f'Found unsupported identifier type {id_type}')
+ print(type_msg, file=sys.stderr)
+
+ if p < name_len + kid_len + sig_len:
+ sys.exit('Insufficient name+kid+sig data in file')
+
+ p -= sig_len
+ raw_sig = buf[p:p + sig_len]
+ p -= kid_len
+ raw_kid = buf[p:p + kid_len]
+ p -= name_len
+ raw_name = buf[p:p + name_len]
+ module_len = p
+
+ if sig_len > 0:
+ n = min(16, sig_len)
+ hs = ''.join(f'{b:02x}' for b in raw_sig[:n])
+ print(f'Found {sig_len} bytes of signature [{hs}]', file=sys.stderr)
+
+ if kid_len > 0:
+ n = min(16, kid_len)
+ hs = ''.join(f'{b:02x}' for b in raw_kid[:n])
+ print(f'Found {kid_len} bytes of key identifier [{hs}]',
+ file=sys.stderr)
+
+ if name_len > 0:
+ sys.stderr.buffer.write(
+ f"Found {name_len} bytes of signer's name [".encode() +
+ raw_name + b']\n')
+
+ if part == '-0':
+ sys.stdout.buffer.write(buf[:module_len])
+ elif part == '-a':
+ sys.stdout.buffer.write(buf[module_len:])
+ elif part == '-d':
+ print(' '.join(str(x) for x in info))
+ elif part == '-n':
+ if id_type == 2:
+ print("No signer's name for PKCS#7 message type sig",
+ file=sys.stderr)
+ sys.stdout.buffer.write(raw_name)
+ elif part == '-k':
+ if id_type == 2:
+ print('No key ID for PKCS#7 message type sig', file=sys.stderr)
+ sys.stdout.buffer.write(raw_kid)
+ elif part == '-s':
+ sys.stdout.buffer.write(raw_sig)
+ else:
+ sys.exit(f'Unknown part: {part}')
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH 3/7] scripts: convert extract-sys-certs.pl to Python
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 2/7] scripts: convert extract-module-sig.pl " Breno Rodrigues Alves
@ 2026-10-18 11:28 ` Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 4/7] scripts: convert headerdep.pl " Breno Rodrigues Alves
` (4 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of extract-sys-certs.pl in Python 3
(stdlib only). Same vmlinux section-table parsing, same symbol
lookup (nm with System.map fallback), same VMA-to-file-offset
translation.
Requires a vmlinux with the system certificate list to fully
test; logic is 1:1 with the Perl version.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
| 132 +++++++++++++++++++++++++++++++++++
1 file changed, 132 insertions(+)
create mode 100755 scripts/extract-sys-certs.py
--git a/scripts/extract-sys-certs.py b/scripts/extract-sys-certs.py
new file mode 100755
index 000000000..92a414eb3
--- /dev/null
+++ b/scripts/extract-sys-certs.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# extract-sys-certs.py [-s <systemmap-file>] <vmlinux-file> <keyring-file>
+
+import re
+import subprocess
+import sys
+
+RE_SECTION = re.compile(
+ r'\s*([0-9]+)\s+(\S+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+'
+ r'([0-9a-f]+)\s+([0-9a-f]+)\s+2\*\*([0-9]+)')
+RE_SYMBOL = re.compile(r'([0-9a-f]+)\s([a-zA-Z])\s(\S+)')
+
+
+def parse_symbols(lines):
+ syms = {}
+ n = 0
+ for line in lines:
+ m = RE_SYMBOL.search(line)
+ if m:
+ syms[m.group(3)] = int(m.group(1), 16)
+ n += 1
+ return syms, n
+
+
+def main(argv):
+ args = list(argv)
+ sysmap = ''
+ if len(args) == 4 and args[0] == '-s':
+ sysmap = args[1]
+ args = args[2:]
+ elif len(args) != 2:
+ sys.exit(f'Format: {sys.argv[0]} [-s <systemmap-file>] '
+ f'<vmlinux-file> <keyring-file>')
+
+ vmlinux, keyring = args
+
+ try:
+ objdump_out = subprocess.check_output(
+ ['objdump', '-h', vmlinux], text=True)
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
+ sys.exit(f'{vmlinux}: {e}')
+
+ sections = []
+ for line in objdump_out.splitlines():
+ m = RE_SECTION.search(line)
+ if not m:
+ continue
+ sections.append({
+ 'name': m.group(2),
+ 'len': int(m.group(3), 16),
+ 'vma': int(m.group(4), 16),
+ 'foff': int(m.group(6), 16),
+ })
+
+ print(f'Have {len(sections)} sections')
+
+ try:
+ nm_out = subprocess.check_output(
+ ['nm', vmlinux], text=True, stderr=subprocess.DEVNULL)
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ nm_out = ''
+
+ symbols, nr = parse_symbols(nm_out.splitlines())
+
+ if nr == 0 and sysmap:
+ print(f'No symbols in vmlinux, trying {sysmap}')
+ with open(sysmap) as f:
+ symbols, nr = parse_symbols(f.readlines())
+
+ if nr == 0:
+ sys.exit('No symbols available')
+ print(f'Have {nr} symbols')
+
+ if '__cert_list_start' not in symbols or \
+ 'system_certificate_list_size' not in symbols:
+ sys.exit("Can't find system certificate list")
+
+ start = symbols['__cert_list_start']
+ size_sym = symbols['system_certificate_list_size']
+
+ found = None
+ size = 0
+ buf = b''
+ with open(vmlinux, 'rb') as f:
+ for sec in sections:
+ s_vma = sec['vma']
+ s_len = sec['len']
+ s_vend = s_vma + s_len
+
+ if start < s_vma or start >= s_vend:
+ continue
+
+ if size_sym < s_vma or size_sym > s_vend:
+ sys.exit('Certificate list size was not found on the same section')
+
+ if found is not None:
+ sys.exit(f"Cert object in multiple sections: "
+ f"{sec['name']} and {found['name']}")
+
+ size_off = size_sym - s_vma + sec['foff']
+ f.seek(size_off)
+ packed = f.read(8)
+ if len(packed) != 8:
+ sys.exit(vmlinux)
+ size = int.from_bytes(packed, sys.byteorder)
+ end = start + size
+
+ print(f'Have {size} bytes of certs at VMA 0x{start:x}')
+ if end > s_vend:
+ sys.exit(f"Cert object partially overflows section {sec['name']}")
+ found = sec
+
+ if found is None:
+ sys.exit('Cert object not inside a section')
+
+ print(f"Certificate list in section {found['name']}")
+ foff = start - found['vma'] + found['foff']
+ print(f'Certificate list at file offset 0x{foff:x}')
+ f.seek(foff)
+ buf = f.read(size)
+ if len(buf) != size:
+ sys.exit(f'Short read on {vmlinux}')
+
+ with open(keyring, 'wb') as f:
+ f.write(buf)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH 4/7] scripts: convert headerdep.pl to Python
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (2 preceding siblings ...)
2026-10-18 11:28 ` [RFC PATCH 3/7] scripts: convert extract-sys-certs.pl " Breno Rodrigues Alves
@ 2026-10-18 11:28 ` Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 5/7] scripts: convert kconfig-sym-check.pl " Breno Rodrigues Alves
` (3 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of headerdep.pl in Python 3
(stdlib only). Same include graph construction, same cycle
detection, same GraphViz output mode (--graph).
Verified byte-for-byte identical to the Perl version on a
synthetic cyclic include chain.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
| 137 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 137 insertions(+)
create mode 100755 scripts/headerdep.py
--git a/scripts/headerdep.py b/scripts/headerdep.py
new file mode 100755
index 000000000..4b9a57ab1
--- /dev/null
+++ b/scripts/headerdep.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Detect cycles in the header file dependency graph.
+# Vegard Nossum <vegardno@ifi.uio.no>
+
+import argparse
+import os
+import re
+import sys
+
+deps = {}
+opt_include = []
+
+
+def strip(filename):
+ for inc in opt_include:
+ prefix = inc + '/'
+ if filename.startswith(prefix):
+ return filename[len(prefix):]
+ return filename
+
+
+def search(filename):
+ if os.path.isfile(filename):
+ return filename
+ for inc in opt_include:
+ path = f'{inc}/{filename}'
+ if os.path.isfile(path):
+ return path
+ return None
+
+
+def parse_all(headers):
+ queue = list(headers)
+ while queue:
+ h = queue.pop()
+ if h in deps:
+ continue
+ deps[h] = []
+ path = search(h)
+ if path is None:
+ continue
+ with open(path) as f:
+ lines = f.readlines()
+ for i, line in enumerate(lines):
+ m = re.match(r'^#\s*include\s*<(.*?)>', line)
+ if m:
+ dep = m.group(1)
+ queue.append(dep)
+ deps[h].append((i + 1, dep))
+
+
+def print_cycle(cycle):
+ # Shift line numbers to align with the "In file included" message
+ cycle = list(cycle)
+ for i in range(len(cycle) - 1):
+ cycle[i] = (cycle[i + 1][0], cycle[i][1])
+ cycle[-1] = (0, cycle[-1][1])
+
+ first = cycle.pop(0)
+ last = cycle.pop() if cycle else None
+
+ msg = 'In file included'
+ if last is not None:
+ print(f'{msg} from {last[1]},')
+ for hdr in reversed(cycle):
+ marker = ' <-- here' if last is not None and hdr[1] == last[1] else ''
+ print(f'{" " * len(msg)} from {hdr[1]}:{hdr[0]}{marker}')
+ print(f'{first[1]}:{first[0]}: warning: recursive header inclusion')
+
+
+def detect_cycles(headers, opt_all):
+ queue = [[(0, h)] for h in headers]
+ while queue:
+ top = queue.pop()
+ name = top[-1][1]
+ for dep in deps.get(name, []):
+ chain = top + [(dep[0], dep[1])]
+ if any(c[1] == dep[1] for c in top):
+ print_cycle(chain)
+ if not opt_all:
+ return
+ continue
+ queue.append(chain)
+
+
+def mangle(name):
+ return name.replace('/', '__').replace('.', '_').replace('-', '_')
+
+
+def graph():
+ print('digraph {')
+ print()
+ print('\t/* vertices */')
+ for h in deps:
+ print(f'\t{mangle(h)} [label="{h}"];')
+ print()
+ print('\t/* edges */')
+ for h in deps:
+ for dep in deps[h]:
+ print(f'\t{mangle(h)} -> {mangle(dep[1])};')
+ print('}')
+
+
+def main(argv):
+ global opt_include
+
+ p = argparse.ArgumentParser(add_help=False)
+ p.add_argument('--all', action='store_true')
+ p.add_argument('--graph', action='store_true')
+ p.add_argument('-I', dest='includes', action='append', default=[])
+ p.add_argument('files', nargs='*')
+ p.add_argument('-h', '--help', '--version', action='store_true')
+ args = p.parse_args(argv)
+
+ if args.help:
+ print(f'Usage: {sys.argv[0]} [options] file...')
+ return 0
+
+ opt_include = list(args.includes) + ['include']
+ # Match Perl: grep { strip($_) } keeps the ORIGINAL path, using
+ # strip() only as a truthiness filter. This makes deps keys
+ # consistent with the Perl version (e.g. 'include/x.h', not 'x.h').
+ headers = [h for h in args.files if strip(h)]
+
+ parse_all(headers)
+
+ if args.graph:
+ graph()
+ else:
+ detect_cycles(headers, args.all)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH 5/7] scripts: convert kconfig-sym-check.pl to Python
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (3 preceding siblings ...)
2026-10-18 11:28 ` [RFC PATCH 4/7] scripts: convert headerdep.pl " Breno Rodrigues Alves
@ 2026-10-18 11:28 ` Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 6/7] scripts: convert parse-maintainers.pl " Breno Rodrigues Alves
` (2 subsequent siblings)
7 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of kconfig-sym-check.pl in Python 3
(stdlib only). Same Kconfig parsing, same symbol-reference
detection, same excludes-file handling, same exit code.
Verified byte-for-byte identical to the Perl version on the
full kernel tree (~/linux).
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/kconfig-sym-check.py | 177 +++++++++++++++++++++++++++++++++++
1 file changed, 177 insertions(+)
create mode 100755 scripts/kconfig-sym-check.py
diff --git a/scripts/kconfig-sym-check.py b/scripts/kconfig-sym-check.py
new file mode 100755
index 000000000..f15b25535
--- /dev/null
+++ b/scripts/kconfig-sym-check.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Detect unknown symbols referenced in Kconfig files.
+# Usage: kconfig-sym-check.py <srctree> [excludes file]
+
+import os
+import re
+import subprocess
+import sys
+
+VALID = re.compile(r'^[a-zA-Z0-9_]+$')
+RE_DEF = re.compile(r'^\s*(?:config|menuconfig)\s+([a-zA-Z0-9_]+)\s*(?:#.*)?$')
+RE_HELP = re.compile(r'^\s*(?:help|---help---)$')
+RE_REF = re.compile(
+ r'^\s*(?:default|def_bool|def_tristate|select|depends\s+on|imply|'
+ r'visible\s+if|range|if|bool|tristate|int|hex|string|prompt)\s+(.+)\s*$')
+RE_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
+RE_SKIP = re.compile(r'^\s*(?:#.*)?$')
+
+
+def indent_depth(ws):
+ col = 0
+ for c in ws:
+ if c == '\t':
+ col = (col // 8) * 8 + 8
+ else:
+ col += 1
+ return col
+
+
+def find_kconfig_files(srctree):
+ try:
+ out = subprocess.check_output(
+ ['git', '-C', srctree, 'ls-files', '*Kconfig*'],
+ text=True, stderr=subprocess.DEVNULL)
+ files = [ln.strip() for ln in out.splitlines() if ln.strip()]
+ if files:
+ return [os.path.join(srctree, f) for f in files]
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ pass
+ try:
+ out = subprocess.check_output(
+ ['find', srctree, '-name', '*Kconfig*'], text=True)
+ return [ln.strip() for ln in out.splitlines() if ln.strip()]
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return []
+
+
+def parse_kconfig(path, configs, refs):
+ with open(path) as f:
+ raw = f.readlines()
+
+ merged = []
+ i = 0
+ while i < len(raw):
+ line = raw[i].rstrip('\n')
+ while line.endswith('\\') and i + 1 < len(raw):
+ line = line[:-1] + ' ' + raw[i + 1].rstrip('\n')
+ i += 1
+ merged.append(line)
+ i += 1
+
+ help_mode = False
+ help_level = 0
+ i = 0
+ while i < len(merged):
+ line = merged[i]
+
+ if not line.strip():
+ i += 1
+ continue
+ if line.lstrip().startswith('#'):
+ i += 1
+ continue
+
+ level = indent_depth(re.match(r'^(\s*)', line).group(1))
+
+ if help_mode and level < help_level:
+ help_mode = False
+ if help_mode:
+ i += 1
+ continue
+
+ if RE_HELP.match(line):
+ help_mode = True
+ j = i + 1
+ while j < len(merged) and RE_SKIP.match(merged[j]):
+ j += 1
+ if j >= len(merged):
+ break
+ next_level = indent_depth(
+ re.match(r'^(\s*)', merged[j]).group(1))
+ if next_level >= level:
+ help_level = next_level
+ else:
+ help_mode = False
+ i = j
+ continue
+
+ m = RE_DEF.match(line)
+ if m:
+ configs[m.group(1)] = configs.get(m.group(1), 0) + 1
+ i += 1
+ continue
+
+ m = RE_REF.match(line)
+ if m:
+ s = m.group(1)
+ s = RE_STRING.sub('', s)
+ s = re.sub(r'#.*', '', s)
+ s = re.sub(r'\$\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\)', '', s)
+ s = re.sub(r'%%[^%]*%%', '', s)
+ for sym in re.split(r'[^a-zA-Z0-9_]+', s):
+ if not sym or not sym[0].isalpha():
+ continue
+ if sym in ('if', 'y', 'n', 'm'):
+ continue
+ if re.match(r'^0[xX][0-9a-fA-F]+$', sym):
+ continue
+ refs[sym] = refs.get(sym, 0) + 1
+ i += 1
+
+
+def main(argv):
+ if not argv:
+ s = ''
+ try:
+ s = subprocess.check_output(
+ ['git', 'rev-parse', '--show-toplevel'],
+ text=True, stderr=subprocess.DEVNULL).strip()
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ pass
+ msg = f'Usage: {sys.argv[0]} <srctree> [excludes file]\n'
+ msg += 'Please provide <srctree>.'
+ if s:
+ msg += f" Is it '{s}'?"
+ sys.exit(msg + '\n')
+
+ srctree = argv[0]
+ excludes = argv[1] if len(argv) > 1 else None
+
+ files = [f for f in find_kconfig_files(srctree)
+ if '/scripts/kconfig/tests/' not in f]
+
+ configs = {}
+ refs = {}
+ for f in files:
+ parse_kconfig(f, configs, refs)
+
+ known = {}
+ if excludes:
+ with open(excludes) as f:
+ for line in f:
+ line = line.rstrip('\n')
+ if not line.strip() or line.lstrip().startswith('#'):
+ continue
+ m = re.match(r'^\s*([a-zA-Z0-9_]+)\s*(?:#.*)?$', line)
+ if m:
+ known[m.group(1)] = 1
+
+ ret = 0
+ for k in sorted(refs):
+ if k in configs or k in known:
+ continue
+ print(k, end='')
+ if k in ('N', 'Y', 'M'):
+ print(f" - warning: '{k}' is probably not what you want; "
+ f"Kconfig tristate literals are always lowercase "
+ f"('n', 'y', 'm')", end='')
+ print()
+ ret = 1
+ return ret
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH 6/7] scripts: convert parse-maintainers.pl to Python
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (4 preceding siblings ...)
2026-10-18 11:28 ` [RFC PATCH 5/7] scripts: convert kconfig-sym-check.pl " Breno Rodrigues Alves
@ 2026-10-18 11:28 ` Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 7/7] scripts: convert streamline_config.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
7 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of parse-maintainers.pl in Python 3
(stdlib only). Same section parsing, same --order preferred
ordering, same --input/--output/--section behavior.
Verified byte-for-byte identical to the Perl version on the
kernel MAINTAINERS file (30,222 lines).
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/parse-maintainers.py | 136 +++++++++++++++++++++++++++++++++++
1 file changed, 136 insertions(+)
create mode 100755 scripts/parse-maintainers.py
diff --git a/scripts/parse-maintainers.py b/scripts/parse-maintainers.py
new file mode 100755
index 000000000..520f66ab8
--- /dev/null
+++ b/scripts/parse-maintainers.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Sort MAINTAINERS file sections alphabetically.
+
+import argparse
+import re
+import sys
+from functools import cmp_to_key
+
+PREFERRED_ORDER = 'MRLSWQBCPTFXNK'
+
+
+def by_category_cmp(a, b):
+ aa = a.upper().replace('THE REST', 'ZZZZZZ')
+ bb = b.upper().replace('THE REST', 'ZZZZZZ')
+ return (aa > bb) - (aa < bb)
+
+
+def by_pattern_cmp(a, b):
+ a1 = a[0].upper() if a else ''
+ b1 = b[0].upper() if b else ''
+ ai = PREFERRED_ORDER.find(a1)
+ bi = PREFERRED_ORDER.find(b1)
+ if ai < 0:
+ ai = 1000
+ if bi < 0:
+ bi = 1000
+ if (a1 == 'F' and b1 == 'F') or (a1 == 'X' and b1 == 'X'):
+ return (a > b) - (a < b)
+ if ai < bi:
+ return -1
+ if ai == bi:
+ return 0
+ return 1
+
+
+def trim(s):
+ return s.strip()
+
+
+def alpha_output(ref, filename, order):
+ if not ref:
+ return
+ with open(filename, 'w') as f:
+ sep = None
+ for key in sorted(ref.keys(), key=cmp_to_key(by_category_cmp)):
+ if key == ' ':
+ f.write(ref[key])
+ else:
+ if sep is None:
+ sep = '\n'
+ else:
+ f.write(sep)
+ f.write(key + '\n')
+ patterns = ref[key].split('\n')
+ if order:
+ patterns = sorted(patterns, key=cmp_to_key(by_pattern_cmp))
+ for p in patterns:
+ if p:
+ f.write(p + '\n')
+
+
+def file_input(ref, filename):
+ lastline = ''
+ case = ' '
+ ref[case] = ''
+
+ with open(filename) as f:
+ lines = f.readlines()
+
+ for line in lines:
+ m = re.match(r'^([A-Z]):\s*(.*)', line)
+ if m:
+ line = m.group(1) + ':\t' + trim(m.group(2)) + '\n'
+ if lastline == '':
+ ref[case] += line
+ continue
+ case = trim(lastline)
+ if case in ref:
+ sys.exit(f"Header '{case}' already exists")
+ ref[case] = line
+ lastline = ''
+ continue
+
+ if case == ' ':
+ ref[case] += lastline
+ lastline = line
+ continue
+ if trim(lastline) != '':
+ sys.exit(f"Odd non-pattern line '{lastline}' for '{case}'")
+ lastline = line
+
+ ref[case] += lastline
+
+
+def main(argv):
+ p = argparse.ArgumentParser(add_help=False)
+ p.add_argument('--input', default='MAINTAINERS')
+ p.add_argument('--output', default='MAINTAINERS.new')
+ p.add_argument('--section', default='SECTION.new')
+ p.add_argument('--order', action='store_true', default=False)
+ p.add_argument('-h', '--help', '--usage', action='store_true')
+ p.add_argument('patterns', nargs='*')
+ args = p.parse_args(argv)
+
+ if args.help:
+ print_usage()
+ return 0
+
+ hash_ = {}
+ file_input(hash_, args.input)
+
+ new_hash = {}
+ for pattern in args.patterns:
+ for key in list(hash_.keys()):
+ if re.search(pattern, key) or re.search(pattern, hash_[key]):
+ new_hash[key] = hash_[key]
+ del hash_[key]
+
+ alpha_output(hash_, args.output, args.order)
+ alpha_output(new_hash, args.section, args.order)
+ return 0
+
+
+def print_usage():
+ print(f'''usage: {sys.argv[0]} [options] <pattern matching regexes>
+
+ --input => MAINTAINERS file to read (default: MAINTAINERS)
+ --output => sorted MAINTAINERS file to write (default: MAINTAINERS.new)
+ --section => new sorted MAINTAINERS file to write (default: SECTION.new)
+ --order => Use the preferred section content output ordering (default: 0)''')
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH 7/7] scripts: convert streamline_config.pl to Python
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (5 preceding siblings ...)
2026-10-18 11:28 ` [RFC PATCH 6/7] scripts: convert parse-maintainers.pl " Breno Rodrigues Alves
@ 2026-10-18 11:28 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
7 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:28 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of streamline_config.pl in Python 3
(stdlib only). Same Kconfig recursion, same Makefile parsing,
same dependency/select resolution, same LMC_KEEP preservation.
Requires a .config and loaded modules to fully test; logic is
1:1 with the Perl version.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/streamline_config.py | 475 +++++++++++++++++++++++++++++++++++
1 file changed, 475 insertions(+)
create mode 100755 scripts/streamline_config.py
diff --git a/scripts/streamline_config.py b/scripts/streamline_config.py
new file mode 100755
index 000000000..f2e53975f
--- /dev/null
+++ b/scripts/streamline_config.py
@@ -0,0 +1,475 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright 2005-2009 - Steven Rostedt
+#
+# Simplify the kernel config to only the modules loaded on this system.
+
+import os
+import re
+import subprocess
+import sys
+
+DEBUG = 'LOCALMODCONFIG_DEBUG' in os.environ
+
+# Module state populated by read_kconfig()
+depends = {}
+selects = {}
+prompts = {}
+objects = {}
+config2kfile = {}
+defaults = {}
+read_kconfigs = {}
+ksource = '.'
+
+
+def dprint(*args):
+ if DEBUG:
+ print(*args, file=sys.stderr)
+
+
+def read_config():
+ uname = subprocess.check_output(['uname', '-r'], text=True).strip()
+
+ searchconfigs = [
+ ('.config', 'cat', None),
+ ('/proc/config.gz', 'zcat', None),
+ (f'/boot/config-{uname}', 'cat', None),
+ (f'/boot/vmlinuz-{uname}', 'scripts/extract-ikconfig',
+ 'scripts/extract-ikconfig'),
+ ('vmlinux', 'scripts/extract-ikconfig', 'scripts/extract-ikconfig'),
+ (f'/lib/modules/{uname}/kernel/kernel/configs.ko',
+ 'scripts/extract-ikconfig', 'scripts/extract-ikconfig'),
+ ('kernel/configs.ko', 'scripts/extract-ikconfig',
+ 'scripts/extract-ikconfig'),
+ ('kernel/configs.o', 'scripts/extract-ikconfig',
+ 'scripts/extract-ikconfig'),
+ ]
+
+ for file, exec_cmd, test_cmd in searchconfigs:
+ if not os.path.isfile(file):
+ continue
+ if test_cmd is not None:
+ r = subprocess.run([test_cmd, file], capture_output=True)
+ if r.returncode:
+ continue
+ print(f"using config: '{file}'", file=sys.stderr)
+ try:
+ out = subprocess.check_output(
+ f'{exec_cmd} {file}', shell=True, text=True,
+ stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ continue
+ return out.splitlines(keepends=True)
+
+ sys.exit('No config file found')
+
+
+def read_kconfig(kconfig, if_state):
+ """Recursively read a Kconfig file, populate globals.
+
+ if_state is a [level, [deps...]] list for mutation.
+ """
+ state = 'NONE'
+ config = ''
+ cont = False
+ line = ''
+
+ source = f'{ksource}/{kconfig}'
+ last_source = ''
+ while re.search(r'\$\((\w+)\)', source) and source != last_source:
+ last_source = source
+ m = re.search(r'\$\((\w+)\)', source)
+ source = source.replace(m.group(0), os.environ.get(m.group(1), ''))
+
+ try:
+ with open(source) as f:
+ raw_lines = f.readlines()
+ except OSError:
+ sys.exit(f"Can't open {source}")
+
+ iflevel = if_state[0]
+ ifdeps = if_state[1]
+
+ for raw in raw_lines:
+ line_text = raw.rstrip('\n')
+
+ if cont:
+ line_text = line + ' ' + line_text
+
+ if line_text.endswith('\\'):
+ cont = True
+ line = line_text[:-1]
+ continue
+ cont = False
+
+ m = re.match(r'^source\s+"?([^"]+)', line_text)
+ if m:
+ sub = m.group(1)
+ if sub not in read_kconfigs:
+ read_kconfigs[sub] = 1
+ read_kconfig(sub, if_state)
+ continue
+
+ m = re.match(r'^\s*(?:menu)?config\s+(\S+)\s*$', line_text)
+ if m:
+ state = 'NEW'
+ config = m.group(1)
+ config2kfile[f'CONFIG_{config}'] = kconfig
+ for i in range(if_state[0]):
+ if i:
+ depends[config] = depends.get(config, '') + ' ' + ifdeps[i]
+ else:
+ depends[config] = ifdeps[i]
+ state = 'DEP'
+ continue
+
+ m = re.match(r'^\s*depends\s+on\s+(.*)$', line_text)
+ if m and state in ('NEW', 'DEP'):
+ if state == 'NEW':
+ depends[config] = m.group(1)
+ state = 'DEP'
+ else:
+ depends[config] = depends.get(config, '') + ' ' + m.group(1)
+ continue
+
+ m = re.match(r'^\s*def(?:_(?:bool|tristate)|ault)\s+(\S.*)$',
+ line_text)
+ if m and state != 'NONE':
+ dep = m.group(1)
+ defaults[config] = 1
+ if not re.match(r'^\s*(?:y|m|n)\s*$', dep):
+ dep = re.sub(r'.*\sif\s+', '', dep)
+ depends[config] = depends.get(config, '') + ' ' + dep
+ dprint(f'Added default depends {dep} to {config}')
+ continue
+
+ m = re.match(r'^\s*select\s+(\S+)', line_text)
+ if m and state != 'NONE':
+ conf = m.group(1)
+ if conf in selects:
+ selects[conf] += ' ' + config
+ else:
+ selects[conf] = config
+ continue
+
+ if state != 'NONE' and \
+ re.match(r'^\s*(?:tristate\s+\S|prompt\b)', line_text):
+ prompts[config] = 1
+ continue
+
+ m = re.match(r'^if\s+(.*\S)\s*$', line_text)
+ if m:
+ deps_str = re.sub(r'^[^a-zA-Z0-9_]*', '', m.group(1))
+ deps_str = re.sub(r'[^a-zA-Z0-9_]*$', '', deps_str)
+ parts = re.split(r'[^a-zA-Z0-9_]+', deps_str)
+ if if_state[0] < len(ifdeps):
+ ifdeps[if_state[0]] = ':'.join(parts)
+ else:
+ ifdeps.append(':'.join(parts))
+ if_state[0] += 1
+ continue
+
+ if re.match(r'^endif', line_text):
+ if if_state[0]:
+ if_state[0] -= 1
+ continue
+
+ if re.match(r'^\s*(?:---)?help(?:---)?\s*$', line_text) or \
+ re.match(r'^(?:comment|choice|menu)\b', line_text):
+ state = 'NONE'
+
+
+def convert_vars(line, make_vars):
+ """Expand $(VAR) references in a Makefile line."""
+ out = ''
+ while True:
+ m = re.match(r'^(.*?)(\$\(([^()]+)\))(.*)$', line)
+ if not m:
+ out += line
+ break
+ out += m.group(1)
+ var = m.group(3)
+ if var in make_vars:
+ out += make_vars[var]
+ else:
+ out += m.group(2)
+ line = m.group(4)
+ return out
+
+
+def main(argv):
+ global ksource
+
+ config_file = read_config()
+
+ localmodconfig = False
+ localyesconfig = False
+ args = []
+ for a in argv:
+ if a == '--localmodconfig':
+ localmodconfig = True
+ elif a == '--localyesconfig':
+ localyesconfig = True
+ else:
+ args.append(a)
+
+ ksource = args[0] if args else '.'
+ kconfig = args[1] if len(args) > 1 else None
+ lsmod_file = os.environ.get('LSMOD')
+
+ try:
+ makefiles = subprocess.check_output(
+ ['find', ksource, '-name', 'Makefile', '-or', '-name', 'Kbuild'],
+ text=True, stderr=subprocess.DEVNULL).splitlines()
+ except subprocess.CalledProcessError:
+ makefiles = []
+
+ if kconfig:
+ read_kconfig(kconfig, [0, []])
+
+ # Parse Makefiles for obj-$(CONFIG_FOO) += obj.o
+ for makefile in makefiles:
+ line = ''
+ make_vars = {}
+ try:
+ with open(makefile) as f:
+ for raw in f:
+ line_text = raw.rstrip('\n')
+ if line_text.endswith('\\'):
+ line += line_text[:-1]
+ continue
+ line += line_text
+ expanded = convert_vars(line, make_vars)
+ line = ''
+
+ m = re.match(
+ r'obj-\$[({](CONFIG_[^})]*)[)}]\s*[+:]?=\s*(.*)',
+ expanded)
+ objs = None
+ var = None
+ if m:
+ var = m.group(1)
+ objs = m.group(2)
+ else:
+ m = re.match(r'^\s*(\S+)\s*[:]?=\s*(.*\S)',
+ expanded)
+ if m:
+ make_vars[m.group(1)] = m.group(2)
+ if objs is not None:
+ for o in re.split(r'\s+', objs):
+ o = o.replace('-', '_')
+ m = re.match(r'(.*)\.o$', o)
+ if m:
+ objects.setdefault(m.group(1), []).append(var)
+ except OSError:
+ continue
+
+ # Get loaded modules
+ modules = {}
+ if lsmod_file:
+ if not os.path.isfile(lsmod_file):
+ alt = os.path.join(os.environ.get('objtree', ''), lsmod_file)
+ if os.path.isfile(alt):
+ lsmod_file = alt
+ else:
+ sys.exit(f'{lsmod_file} not found')
+ with open(lsmod_file) as f:
+ lines = f.readlines()
+ else:
+ lsmod = 'lsmod'
+ for d in ('/sbin', '/bin', '/usr/sbin', '/usr/bin'):
+ if os.path.isfile(f'{d}/lsmod') and os.access(
+ f'{d}/lsmod', os.X_OK):
+ lsmod = f'{d}/lsmod'
+ break
+ try:
+ out = subprocess.check_output([lsmod], text=True,
+ stderr=subprocess.DEVNULL)
+ lines = out.splitlines(keepends=True)
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ sys.exit(f'Can not call lsmod with {lsmod}')
+
+ for line in lines:
+ if line.startswith('Module'):
+ continue
+ m = re.match(r'^(\S+)', line)
+ if m:
+ modules[m.group(1)] = 1
+
+ # Build config set from objects
+ configs = {}
+ for mod in modules:
+ if mod in objects:
+ for conf in objects[mod]:
+ configs[conf] = mod
+ dprint(f'{conf} added by direct ({mod})')
+ else:
+ print(f'{mod} config not found!', file=sys.stderr)
+
+ # Read current configs
+ orig_configs = {}
+ for line in config_file:
+ m = re.search(r'(CONFIG_[A-Za-z_0-9]*)=(m|y)', line)
+ if m:
+ orig_configs[m.group(1)] = m.group(2)
+
+ valid = 'A-Za-z_0-9'
+
+ def parse_config_depends(p, depconfig):
+ nonlocal repeat
+ while re.search(rf'[{valid}]', p):
+ m = re.match(rf'^[^{valid}]*([{valid}]+)', p)
+ if not m:
+ sys.exit('parse_config_depends: unexpected')
+ conf = 'CONFIG_' + m.group(1)
+ p = re.sub(rf'^[^{valid}]*[{valid}]+', '', p, count=1)
+
+ if conf not in orig_configs or orig_configs[conf] == 'y':
+ continue
+ if conf not in configs:
+ repeat = 1
+ dprint(f'{conf} selected by depend {depconfig}')
+ configs[conf] = 1
+
+ def parse_config_selects(config, p):
+ nonlocal repeat
+ next_config = None
+ while re.search(rf'[{valid}]', p):
+ m = re.match(rf'^[^{valid}]*([{valid}]+)', p)
+ if not m:
+ sys.exit('parse_config_selects: unexpected')
+ conf = 'CONFIG_' + m.group(1)
+ p = re.sub(rf'^[^{valid}]*[{valid}]+', '', p, count=1)
+
+ if conf not in orig_configs:
+ dprint(f'{conf} not set for {config} select')
+ continue
+ if orig_configs[conf] != 'm':
+ dprint(f'{conf} (non module) selects {config}, we are good')
+ return
+ if conf in configs:
+ dprint(f'{conf} selects {config} so we are good')
+ return
+ if next_config is None:
+ next_config = conf
+
+ if next_config is None:
+ if config not in defaults:
+ print(f'WARNING: {config} is required, but nothing in the',
+ file=sys.stderr)
+ print(f' current config selects it.', file=sys.stderr)
+ return
+
+ repeat = 1
+ configs[next_config] = 1
+ dprint(f'{next_config} selected by select {config}')
+
+ repeat = 1
+ while repeat:
+ repeat = 0
+ process_selects = {}
+ for config in list(configs.keys()):
+ if config in orig_configs and orig_configs[config] != 'm':
+ continue
+ short = config.replace('CONFIG_', '', 1)
+ if short in depends:
+ parse_config_depends(depends[short], short)
+ if short not in prompts and short in selects:
+ process_selects[short] = 1
+
+ for config in process_selects:
+ dprint(f'Process select {config}')
+ parse_config_selects(config, selects[config])
+
+ # Preserve list
+ preserved = []
+ if 'LMC_KEEP' in os.environ:
+ preserved = os.environ['LMC_KEEP'].split(':')
+
+ def in_preserved(conf):
+ kconf = config2kfile.get(conf)
+ if kconf is None:
+ return False
+ for excl in preserved:
+ if re.match(f'^{excl}', kconf):
+ return True
+ return False
+
+ setconfigs = {}
+
+ for line in config_file:
+ # IKCONFIG special case
+ m = re.search(r'CONFIG_IKCONFIG', line)
+ if m:
+ if '# CONFIG_IKCONFIG is not set' in line:
+ print('CONFIG_IKCONFIG=m')
+ print('# CONFIG_IKCONFIG_PROC is not set')
+ else:
+ sys.stdout.write(line)
+ continue
+
+ m = re.match(r'CONFIG_MODULE_SIG_KEY="(.+)"', line)
+ if m:
+ orig_cert = m.group(1)
+ default_cert = 'certs/signing_key.pem'
+ if 'MODULE_SIG_KEY' not in depends or \
+ f'"{default_cert}"' not in depends['MODULE_SIG_KEY']:
+ print(f'WARNING: MODULE_SIG_KEY assertion failure, '
+ f'update needed to {sys.argv[0]}', file=sys.stderr)
+ sys.stdout.write(line)
+ elif orig_cert != default_cert and not os.path.isfile(orig_cert):
+ print(f'Module signature verification enabled but module '
+ f'signing key "{orig_cert}" not found. Resetting '
+ f'signing key to default value.', file=sys.stderr)
+ print(f'CONFIG_MODULE_SIG_KEY="{default_cert}"')
+ else:
+ sys.stdout.write(line)
+ continue
+
+ m = re.match(r'CONFIG_SYSTEM_TRUSTED_KEYS="(.+)"', line)
+ if m:
+ orig_keys = m.group(1)
+ if not os.path.isfile(orig_keys):
+ print(f'System keyring enabled but keys "{orig_keys}" not '
+ f'found. Resetting keys to default value.',
+ file=sys.stderr)
+ print('CONFIG_SYSTEM_TRUSTED_KEYS=""')
+ else:
+ sys.stdout.write(line)
+ continue
+
+ m = re.match(r'^(CONFIG.*)=(m|y)', line)
+ if m:
+ conf, val = m.group(1), m.group(2)
+ if in_preserved(conf):
+ dprint(f'Preserve config {conf}')
+ sys.stdout.write(line)
+ continue
+ if conf in configs:
+ if localyesconfig:
+ setconfigs[conf] = 'y'
+ print(f'{conf}=y')
+ continue
+ setconfigs[conf] = val
+ elif val == 'm':
+ print(f'# {conf} is not set')
+ continue
+ sys.stdout.write(line)
+
+ # Integrity check
+ for mod in modules:
+ if mod not in objects:
+ continue
+ if not any(c in setconfigs for c in objects[mod]):
+ print(f'module {mod} did not have configs', end='',
+ file=sys.stderr)
+ for c in objects[mod]:
+ print(f' {c}', end='', file=sys.stderr)
+ print(file=sys.stderr)
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (6 preceding siblings ...)
2026-10-18 11:28 ` [RFC PATCH 7/7] scripts: convert streamline_config.pl " Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
` (6 more replies)
7 siblings, 7 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
v2 changes since v1:
- Fix the location of two converted scripts: kconfig-sym-check.py
and streamline_config.py belong in scripts/kconfig/, not in
scripts/ root, to match the path of their original .pl files.
Both are now added at the correct path from the start.
This series converts the last seven Perl utilities in scripts/ to
Python 3. After this series, scripts/ contains no remaining Perl
utilities pending conversion (the original .pl files are kept for
coexistence, matching the pattern of the earlier conversions).
Each conversion is a faithful 1:1 port: same CLI, same output,
same behavior. All use only the Python standard library.
The seven scripts are:
- checkstack.pl -> checkstack.py
- extract-module-sig.pl -> extract-module-sig.py
- extract-sys-certs.pl -> extract-sys-certs.py
- headerdep.pl -> headerdep.py
- kconfig-sym-check.pl -> scripts/kconfig/kconfig-sym-check.py
- parse-maintainers.pl -> parse-maintainers.py
- streamline_config.pl -> scripts/kconfig/streamline_config.py
Five of the seven were verified byte-for-byte identical to their
Perl counterparts. The remaining two (extract-sys-certs,
streamline_config) depend on build artifacts (vmlinux, .config)
and are 1:1 ports of the Perl logic.
Breno Rodrigues Alves (7):
scripts: convert checkstack.pl to Python
scripts: convert extract-module-sig.pl to Python
scripts: convert extract-sys-certs.pl to Python
scripts: convert headerdep.pl to Python
scripts: convert kconfig-sym-check.pl to Python
scripts: convert parse-maintainers.pl to Python
scripts: convert streamline_config.pl to Python
Breno Rodrigues Alves (7):
scripts: convert checkstack.pl to Python
scripts: convert extract-module-sig.pl to Python
scripts: convert extract-sys-certs.pl to Python
scripts: convert headerdep.pl to Python
scripts: convert kconfig-sym-check.pl to Python
scripts: convert parse-maintainers.pl to Python
scripts: convert streamline_config.pl to Python
scripts/checkstack.py | 143 ++++++++
scripts/extract-module-sig.py | 103 ++++++
scripts/extract-sys-certs.py | 132 ++++++++
scripts/headerdep.py | 137 ++++++++
scripts/kconfig/kconfig-sym-check.py | 177 ++++++++++
scripts/kconfig/streamline_config.py | 475 +++++++++++++++++++++++++++
scripts/parse-maintainers.py | 136 ++++++++
7 files changed, 1303 insertions(+)
create mode 100755 scripts/checkstack.py
create mode 100755 scripts/extract-module-sig.py
create mode 100755 scripts/extract-sys-certs.py
create mode 100755 scripts/headerdep.py
create mode 100755 scripts/kconfig/kconfig-sym-check.py
create mode 100755 scripts/kconfig/streamline_config.py
create mode 100755 scripts/parse-maintainers.py
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 1/7] scripts: convert checkstack.pl to Python
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 2/7] scripts: convert extract-module-sig.pl " Breno Rodrigues Alves
` (5 subsequent siblings)
6 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of checkstack.pl in Python 3
(stdlib only). Same per-architecture regexes for x86/x86_64,
arm, arm64, m68k, mips, nios2, openrisc, parisc, powerpc,
s390, sparc, riscv and loongarch.
Verified byte-for-byte identical to the Perl version on a
synthetic objdump stream for x86_64.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/checkstack.py | 143 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 143 insertions(+)
create mode 100755 scripts/checkstack.py
diff --git a/scripts/checkstack.py b/scripts/checkstack.py
new file mode 100755
index 000000000..8be3861b3
--- /dev/null
+++ b/scripts/checkstack.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Check the stack usage of functions.
+# Usage: objdump -d vmlinux | checkstack.py [arch] [min_stack]
+
+import platform
+import re
+import sys
+
+X = '[0-9a-f]'
+RE_FUNC = re.compile(rf'^({X}*) <(.*)>:$')
+RE_FILE = re.compile(r'(.*):\s*file format')
+
+
+def arm_push_handler(line):
+ if re.search(r'.*push.*fp, ip, lr, pc}', line):
+ return (line.count(',') + 1) * 4
+ return 0
+
+
+def config_for(arch):
+ """Return (main_re, dynamic_re, sub_fn)."""
+ if re.match(r'^(?:aarch|arm)64$', arch):
+ return (
+ re.compile(rf'^.*stp.*sp, ?#-([0-9]{{1,8}})\]!'),
+ re.compile(rf'^.*sub.*sp, sp, #(0x{X}{{1,8}})'),
+ None)
+ if arch == 'arm':
+ return (re.compile(r'.*sub.*sp, sp, #([0-9]{1,4})'), None,
+ arm_push_handler)
+ if re.match(r'^x86(?:_64)?$', arch) or re.match(r'^i[3456]86$', arch):
+ return (
+ re.compile(rf'^.*[as][du][db] \$(0x{X}{{1,8}}),%(?:e|r)sp$'),
+ re.compile(r'^.*[as][du][db] (%.*),%(?:e|r)sp$'),
+ None)
+ if arch == 'm68k':
+ return (re.compile(r'.*(?:linkw %fp,|addaw )#-([0-9]{1,4})'
+ r'(?:,%sp)?$'), None, None)
+ if arch == 'mips64':
+ return (re.compile(r'.*daddiu.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'mips':
+ return (re.compile(r'.*addiu.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'nios2':
+ return (re.compile(r'.*addi.*sp,sp,-([0-9]{1,8})'), None, None)
+ if arch == 'openrisc':
+ return (re.compile(r'.*l\.addi.*r1,r1,-([0-9]{1,8})'), None, None)
+ if arch in ('parisc', 'parisc64'):
+ return (re.compile(rf'.*ldo ({X}{{1,8}})\(sp\),sp'), None, None)
+ if arch == 'powerpc' or re.match(r'^ppc(?:64)?(?:le)?$', arch):
+ return (re.compile(rf'.*st[dw]u.*r1,-({X}{{1,8}})\(r1\)'),
+ None, None)
+ if re.match(r'^s390x?$', arch):
+ return (re.compile(r'.*(?:lay|ag?hi).*\%r15,-([0-9]+)'
+ r'(?:\(\%r15\))?$'), None, None)
+ if arch in ('sparc', 'sparc64'):
+ return (re.compile(r'.*save.*%sp, -([0-9]{1,8}), %sp'), None, None)
+ if re.match(r'^riscv(?:64)?$', arch):
+ return (re.compile(r'.*addi.*sp,sp,-([0-9]{1,8})'), None, None)
+ if re.match(r'^loongarch(?:32|64)?$', arch):
+ return (re.compile(r'.*addi\..*sp, .*sp, -([0-9]{1,8}).*'),
+ None, None)
+ print(f'wrong or unknown architecture "{arch}"')
+ sys.exit(1)
+
+
+def parse_size(s):
+ return int(s, 16) if s.startswith('0x') else int(s)
+
+
+def main(argv):
+ arch = argv[0] if argv else platform.machine()
+ min_stack = int(argv[1]) if len(argv) > 1 and argv[1].isdigit() else 512
+
+ main_re, dyn_re, sub_fn = config_for(arch)
+
+ stack = []
+ func = None
+ file = ''
+ intro = ''
+ total = 0
+
+ for raw in sys.stdin:
+ line = raw.rstrip('\n')
+
+ m = RE_FUNC.match(line)
+ if m:
+ func = m.group(2)
+ if total > min_stack:
+ stack.append(f'{intro}{total}\n')
+ addr = m.group(1)
+ intro = f'0x{addr} {func} [{file}]:'
+ pad = 56 - len(intro)
+ while pad > 0:
+ intro += '\t'
+ pad -= 8
+ total = 0
+ continue
+
+ m = RE_FILE.match(line)
+ if m:
+ file = m.group(1).replace('.ko', '')
+ idx = file.rfind('/')
+ if idx != -1:
+ file = file[idx + 1:]
+ continue
+
+ m = main_re.search(line)
+ if m:
+ size = parse_size(m.group(1))
+ if size > 0xf0000000:
+ size = -size + 0x80000000 + 0x80000000
+ if size > 0x10000000:
+ continue
+ total += size
+ continue
+
+ if dyn_re is not None:
+ m = dyn_re.search(line)
+ if m:
+ total += parse_size(m.group(1))
+ continue
+
+ if sub_fn is not None:
+ total += sub_fn(line)
+
+ if total > min_stack:
+ stack.append(f'{intro}{total}\n')
+
+ def key(entry):
+ m = re.search(r':\t*(\d+)$', entry)
+ size = int(m.group(1)) if m else 0
+ m = re.search(r' (.*):', entry)
+ fname = m.group(1) if m else ''
+ return (-size, fname)
+
+ for entry in sorted(stack, key=key):
+ sys.stdout.write(entry)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 2/7] scripts: convert extract-module-sig.pl to Python
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 3/7] scripts: convert extract-sys-certs.pl " Breno Rodrigues Alves
` (4 subsequent siblings)
6 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of extract-module-sig.pl in Python 3
(stdlib only). Same magic-number handling, same descriptor
extraction, same -0/-a/-d/-n/-k/-s output modes.
Verified byte-for-byte identical to the Perl version on a
synthetic signed module.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
| 103 ++++++++++++++++++++++++++++++++++
1 file changed, 103 insertions(+)
create mode 100755 scripts/extract-module-sig.py
--git a/scripts/extract-module-sig.py b/scripts/extract-module-sig.py
new file mode 100755
index 000000000..e9a43e72f
--- /dev/null
+++ b/scripts/extract-module-sig.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# extract-module-sig.py <part> <module-file>
+# -0: unsigned module -a: all sig data -d: descriptor
+# -n: signer name -k: key ID -s: signature
+
+import struct
+import sys
+
+MAGIC = b'~Module signature appended~\n'
+
+
+def main(argv):
+ if len(argv) != 2:
+ sys.exit(f'Format: {sys.argv[0]} -[0adnks] module-file >out')
+
+ part, modfile = argv
+
+ try:
+ with open(modfile, 'rb') as f:
+ buf = f.read()
+ except OSError as e:
+ sys.exit(f'{modfile}: {e}')
+
+ print(f'Read {len(buf)} bytes from module file', file=sys.stderr)
+
+ if len(buf) < 12 + len(MAGIC):
+ sys.exit('The file is too short to have a sig magic number and descriptor')
+
+ p = len(buf) - len(MAGIC)
+ if buf[p:] != MAGIC:
+ sys.exit(f'Magic number not found at {len(buf)}')
+ print(f'Found magic number at {len(buf)}', file=sys.stderr)
+
+ p -= 12
+ info_raw = buf[p:p + 12]
+ algo = info_raw[0]
+ hash_ = info_raw[1]
+ id_type = info_raw[2]
+ name_len = info_raw[3]
+ kid_len = info_raw[4]
+ sig_len = struct.unpack('>I', info_raw[8:12])[0]
+ info = [algo, hash_, id_type, name_len, kid_len, sig_len]
+
+ type_msg = {
+ 0: 'Found PGP key identifier',
+ 1: 'Found X.509 cert identifier',
+ 2: 'Found PKCS#7/CMS encapsulation',
+ }.get(id_type, f'Found unsupported identifier type {id_type}')
+ print(type_msg, file=sys.stderr)
+
+ if p < name_len + kid_len + sig_len:
+ sys.exit('Insufficient name+kid+sig data in file')
+
+ p -= sig_len
+ raw_sig = buf[p:p + sig_len]
+ p -= kid_len
+ raw_kid = buf[p:p + kid_len]
+ p -= name_len
+ raw_name = buf[p:p + name_len]
+ module_len = p
+
+ if sig_len > 0:
+ n = min(16, sig_len)
+ hs = ''.join(f'{b:02x}' for b in raw_sig[:n])
+ print(f'Found {sig_len} bytes of signature [{hs}]', file=sys.stderr)
+
+ if kid_len > 0:
+ n = min(16, kid_len)
+ hs = ''.join(f'{b:02x}' for b in raw_kid[:n])
+ print(f'Found {kid_len} bytes of key identifier [{hs}]',
+ file=sys.stderr)
+
+ if name_len > 0:
+ sys.stderr.buffer.write(
+ f"Found {name_len} bytes of signer's name [".encode() +
+ raw_name + b']\n')
+
+ if part == '-0':
+ sys.stdout.buffer.write(buf[:module_len])
+ elif part == '-a':
+ sys.stdout.buffer.write(buf[module_len:])
+ elif part == '-d':
+ print(' '.join(str(x) for x in info))
+ elif part == '-n':
+ if id_type == 2:
+ print("No signer's name for PKCS#7 message type sig",
+ file=sys.stderr)
+ sys.stdout.buffer.write(raw_name)
+ elif part == '-k':
+ if id_type == 2:
+ print('No key ID for PKCS#7 message type sig', file=sys.stderr)
+ sys.stdout.buffer.write(raw_kid)
+ elif part == '-s':
+ sys.stdout.buffer.write(raw_sig)
+ else:
+ sys.exit(f'Unknown part: {part}')
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 3/7] scripts: convert extract-sys-certs.pl to Python
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 2/7] scripts: convert extract-module-sig.pl " Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 4/7] scripts: convert headerdep.pl " Breno Rodrigues Alves
` (3 subsequent siblings)
6 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of extract-sys-certs.pl in Python 3
(stdlib only). Same vmlinux section-table parsing, same symbol
lookup (nm with System.map fallback), same VMA-to-file-offset
translation.
Requires a vmlinux with the system certificate list to fully
test; logic is 1:1 with the Perl version.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
| 132 +++++++++++++++++++++++++++++++++++
1 file changed, 132 insertions(+)
create mode 100755 scripts/extract-sys-certs.py
--git a/scripts/extract-sys-certs.py b/scripts/extract-sys-certs.py
new file mode 100755
index 000000000..92a414eb3
--- /dev/null
+++ b/scripts/extract-sys-certs.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# extract-sys-certs.py [-s <systemmap-file>] <vmlinux-file> <keyring-file>
+
+import re
+import subprocess
+import sys
+
+RE_SECTION = re.compile(
+ r'\s*([0-9]+)\s+(\S+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+'
+ r'([0-9a-f]+)\s+([0-9a-f]+)\s+2\*\*([0-9]+)')
+RE_SYMBOL = re.compile(r'([0-9a-f]+)\s([a-zA-Z])\s(\S+)')
+
+
+def parse_symbols(lines):
+ syms = {}
+ n = 0
+ for line in lines:
+ m = RE_SYMBOL.search(line)
+ if m:
+ syms[m.group(3)] = int(m.group(1), 16)
+ n += 1
+ return syms, n
+
+
+def main(argv):
+ args = list(argv)
+ sysmap = ''
+ if len(args) == 4 and args[0] == '-s':
+ sysmap = args[1]
+ args = args[2:]
+ elif len(args) != 2:
+ sys.exit(f'Format: {sys.argv[0]} [-s <systemmap-file>] '
+ f'<vmlinux-file> <keyring-file>')
+
+ vmlinux, keyring = args
+
+ try:
+ objdump_out = subprocess.check_output(
+ ['objdump', '-h', vmlinux], text=True)
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
+ sys.exit(f'{vmlinux}: {e}')
+
+ sections = []
+ for line in objdump_out.splitlines():
+ m = RE_SECTION.search(line)
+ if not m:
+ continue
+ sections.append({
+ 'name': m.group(2),
+ 'len': int(m.group(3), 16),
+ 'vma': int(m.group(4), 16),
+ 'foff': int(m.group(6), 16),
+ })
+
+ print(f'Have {len(sections)} sections')
+
+ try:
+ nm_out = subprocess.check_output(
+ ['nm', vmlinux], text=True, stderr=subprocess.DEVNULL)
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ nm_out = ''
+
+ symbols, nr = parse_symbols(nm_out.splitlines())
+
+ if nr == 0 and sysmap:
+ print(f'No symbols in vmlinux, trying {sysmap}')
+ with open(sysmap) as f:
+ symbols, nr = parse_symbols(f.readlines())
+
+ if nr == 0:
+ sys.exit('No symbols available')
+ print(f'Have {nr} symbols')
+
+ if '__cert_list_start' not in symbols or \
+ 'system_certificate_list_size' not in symbols:
+ sys.exit("Can't find system certificate list")
+
+ start = symbols['__cert_list_start']
+ size_sym = symbols['system_certificate_list_size']
+
+ found = None
+ size = 0
+ buf = b''
+ with open(vmlinux, 'rb') as f:
+ for sec in sections:
+ s_vma = sec['vma']
+ s_len = sec['len']
+ s_vend = s_vma + s_len
+
+ if start < s_vma or start >= s_vend:
+ continue
+
+ if size_sym < s_vma or size_sym > s_vend:
+ sys.exit('Certificate list size was not found on the same section')
+
+ if found is not None:
+ sys.exit(f"Cert object in multiple sections: "
+ f"{sec['name']} and {found['name']}")
+
+ size_off = size_sym - s_vma + sec['foff']
+ f.seek(size_off)
+ packed = f.read(8)
+ if len(packed) != 8:
+ sys.exit(vmlinux)
+ size = int.from_bytes(packed, sys.byteorder)
+ end = start + size
+
+ print(f'Have {size} bytes of certs at VMA 0x{start:x}')
+ if end > s_vend:
+ sys.exit(f"Cert object partially overflows section {sec['name']}")
+ found = sec
+
+ if found is None:
+ sys.exit('Cert object not inside a section')
+
+ print(f"Certificate list in section {found['name']}")
+ foff = start - found['vma'] + found['foff']
+ print(f'Certificate list at file offset 0x{foff:x}')
+ f.seek(foff)
+ buf = f.read(size)
+ if len(buf) != size:
+ sys.exit(f'Short read on {vmlinux}')
+
+ with open(keyring, 'wb') as f:
+ f.write(buf)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 4/7] scripts: convert headerdep.pl to Python
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (2 preceding siblings ...)
2026-10-18 13:57 ` [RFC PATCH v2 3/7] scripts: convert extract-sys-certs.pl " Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 5/7] scripts: convert kconfig-sym-check.pl " Breno Rodrigues Alves
` (2 subsequent siblings)
6 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of headerdep.pl in Python 3
(stdlib only). Same include graph construction, same cycle
detection, same GraphViz output mode (--graph).
Verified byte-for-byte identical to the Perl version on a
synthetic cyclic include chain.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
| 137 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 137 insertions(+)
create mode 100755 scripts/headerdep.py
--git a/scripts/headerdep.py b/scripts/headerdep.py
new file mode 100755
index 000000000..4b9a57ab1
--- /dev/null
+++ b/scripts/headerdep.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Detect cycles in the header file dependency graph.
+# Vegard Nossum <vegardno@ifi.uio.no>
+
+import argparse
+import os
+import re
+import sys
+
+deps = {}
+opt_include = []
+
+
+def strip(filename):
+ for inc in opt_include:
+ prefix = inc + '/'
+ if filename.startswith(prefix):
+ return filename[len(prefix):]
+ return filename
+
+
+def search(filename):
+ if os.path.isfile(filename):
+ return filename
+ for inc in opt_include:
+ path = f'{inc}/{filename}'
+ if os.path.isfile(path):
+ return path
+ return None
+
+
+def parse_all(headers):
+ queue = list(headers)
+ while queue:
+ h = queue.pop()
+ if h in deps:
+ continue
+ deps[h] = []
+ path = search(h)
+ if path is None:
+ continue
+ with open(path) as f:
+ lines = f.readlines()
+ for i, line in enumerate(lines):
+ m = re.match(r'^#\s*include\s*<(.*?)>', line)
+ if m:
+ dep = m.group(1)
+ queue.append(dep)
+ deps[h].append((i + 1, dep))
+
+
+def print_cycle(cycle):
+ # Shift line numbers to align with the "In file included" message
+ cycle = list(cycle)
+ for i in range(len(cycle) - 1):
+ cycle[i] = (cycle[i + 1][0], cycle[i][1])
+ cycle[-1] = (0, cycle[-1][1])
+
+ first = cycle.pop(0)
+ last = cycle.pop() if cycle else None
+
+ msg = 'In file included'
+ if last is not None:
+ print(f'{msg} from {last[1]},')
+ for hdr in reversed(cycle):
+ marker = ' <-- here' if last is not None and hdr[1] == last[1] else ''
+ print(f'{" " * len(msg)} from {hdr[1]}:{hdr[0]}{marker}')
+ print(f'{first[1]}:{first[0]}: warning: recursive header inclusion')
+
+
+def detect_cycles(headers, opt_all):
+ queue = [[(0, h)] for h in headers]
+ while queue:
+ top = queue.pop()
+ name = top[-1][1]
+ for dep in deps.get(name, []):
+ chain = top + [(dep[0], dep[1])]
+ if any(c[1] == dep[1] for c in top):
+ print_cycle(chain)
+ if not opt_all:
+ return
+ continue
+ queue.append(chain)
+
+
+def mangle(name):
+ return name.replace('/', '__').replace('.', '_').replace('-', '_')
+
+
+def graph():
+ print('digraph {')
+ print()
+ print('\t/* vertices */')
+ for h in deps:
+ print(f'\t{mangle(h)} [label="{h}"];')
+ print()
+ print('\t/* edges */')
+ for h in deps:
+ for dep in deps[h]:
+ print(f'\t{mangle(h)} -> {mangle(dep[1])};')
+ print('}')
+
+
+def main(argv):
+ global opt_include
+
+ p = argparse.ArgumentParser(add_help=False)
+ p.add_argument('--all', action='store_true')
+ p.add_argument('--graph', action='store_true')
+ p.add_argument('-I', dest='includes', action='append', default=[])
+ p.add_argument('files', nargs='*')
+ p.add_argument('-h', '--help', '--version', action='store_true')
+ args = p.parse_args(argv)
+
+ if args.help:
+ print(f'Usage: {sys.argv[0]} [options] file...')
+ return 0
+
+ opt_include = list(args.includes) + ['include']
+ # Match Perl: grep { strip($_) } keeps the ORIGINAL path, using
+ # strip() only as a truthiness filter. This makes deps keys
+ # consistent with the Perl version (e.g. 'include/x.h', not 'x.h').
+ headers = [h for h in args.files if strip(h)]
+
+ parse_all(headers)
+
+ if args.graph:
+ graph()
+ else:
+ detect_cycles(headers, args.all)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 5/7] scripts: convert kconfig-sym-check.pl to Python
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (3 preceding siblings ...)
2026-10-18 13:57 ` [RFC PATCH v2 4/7] scripts: convert headerdep.pl " Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 6/7] scripts: convert parse-maintainers.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 7/7] scripts: convert streamline_config.pl " Breno Rodrigues Alves
6 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of kconfig-sym-check.pl in Python 3
(stdlib only). Same Kconfig parsing, same symbol-reference
detection, same excludes-file handling, same exit code.
Verified byte-for-byte identical to the Perl version on the
full kernel tree (~/linux).
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/kconfig/kconfig-sym-check.py | 177 +++++++++++++++++++++++++++
1 file changed, 177 insertions(+)
create mode 100755 scripts/kconfig/kconfig-sym-check.py
diff --git a/scripts/kconfig/kconfig-sym-check.py b/scripts/kconfig/kconfig-sym-check.py
new file mode 100755
index 000000000..f15b25535
--- /dev/null
+++ b/scripts/kconfig/kconfig-sym-check.py
@@ -0,0 +1,177 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Detect unknown symbols referenced in Kconfig files.
+# Usage: kconfig-sym-check.py <srctree> [excludes file]
+
+import os
+import re
+import subprocess
+import sys
+
+VALID = re.compile(r'^[a-zA-Z0-9_]+$')
+RE_DEF = re.compile(r'^\s*(?:config|menuconfig)\s+([a-zA-Z0-9_]+)\s*(?:#.*)?$')
+RE_HELP = re.compile(r'^\s*(?:help|---help---)$')
+RE_REF = re.compile(
+ r'^\s*(?:default|def_bool|def_tristate|select|depends\s+on|imply|'
+ r'visible\s+if|range|if|bool|tristate|int|hex|string|prompt)\s+(.+)\s*$')
+RE_STRING = re.compile(r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'')
+RE_SKIP = re.compile(r'^\s*(?:#.*)?$')
+
+
+def indent_depth(ws):
+ col = 0
+ for c in ws:
+ if c == '\t':
+ col = (col // 8) * 8 + 8
+ else:
+ col += 1
+ return col
+
+
+def find_kconfig_files(srctree):
+ try:
+ out = subprocess.check_output(
+ ['git', '-C', srctree, 'ls-files', '*Kconfig*'],
+ text=True, stderr=subprocess.DEVNULL)
+ files = [ln.strip() for ln in out.splitlines() if ln.strip()]
+ if files:
+ return [os.path.join(srctree, f) for f in files]
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ pass
+ try:
+ out = subprocess.check_output(
+ ['find', srctree, '-name', '*Kconfig*'], text=True)
+ return [ln.strip() for ln in out.splitlines() if ln.strip()]
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ return []
+
+
+def parse_kconfig(path, configs, refs):
+ with open(path) as f:
+ raw = f.readlines()
+
+ merged = []
+ i = 0
+ while i < len(raw):
+ line = raw[i].rstrip('\n')
+ while line.endswith('\\') and i + 1 < len(raw):
+ line = line[:-1] + ' ' + raw[i + 1].rstrip('\n')
+ i += 1
+ merged.append(line)
+ i += 1
+
+ help_mode = False
+ help_level = 0
+ i = 0
+ while i < len(merged):
+ line = merged[i]
+
+ if not line.strip():
+ i += 1
+ continue
+ if line.lstrip().startswith('#'):
+ i += 1
+ continue
+
+ level = indent_depth(re.match(r'^(\s*)', line).group(1))
+
+ if help_mode and level < help_level:
+ help_mode = False
+ if help_mode:
+ i += 1
+ continue
+
+ if RE_HELP.match(line):
+ help_mode = True
+ j = i + 1
+ while j < len(merged) and RE_SKIP.match(merged[j]):
+ j += 1
+ if j >= len(merged):
+ break
+ next_level = indent_depth(
+ re.match(r'^(\s*)', merged[j]).group(1))
+ if next_level >= level:
+ help_level = next_level
+ else:
+ help_mode = False
+ i = j
+ continue
+
+ m = RE_DEF.match(line)
+ if m:
+ configs[m.group(1)] = configs.get(m.group(1), 0) + 1
+ i += 1
+ continue
+
+ m = RE_REF.match(line)
+ if m:
+ s = m.group(1)
+ s = RE_STRING.sub('', s)
+ s = re.sub(r'#.*', '', s)
+ s = re.sub(r'\$\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\)', '', s)
+ s = re.sub(r'%%[^%]*%%', '', s)
+ for sym in re.split(r'[^a-zA-Z0-9_]+', s):
+ if not sym or not sym[0].isalpha():
+ continue
+ if sym in ('if', 'y', 'n', 'm'):
+ continue
+ if re.match(r'^0[xX][0-9a-fA-F]+$', sym):
+ continue
+ refs[sym] = refs.get(sym, 0) + 1
+ i += 1
+
+
+def main(argv):
+ if not argv:
+ s = ''
+ try:
+ s = subprocess.check_output(
+ ['git', 'rev-parse', '--show-toplevel'],
+ text=True, stderr=subprocess.DEVNULL).strip()
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ pass
+ msg = f'Usage: {sys.argv[0]} <srctree> [excludes file]\n'
+ msg += 'Please provide <srctree>.'
+ if s:
+ msg += f" Is it '{s}'?"
+ sys.exit(msg + '\n')
+
+ srctree = argv[0]
+ excludes = argv[1] if len(argv) > 1 else None
+
+ files = [f for f in find_kconfig_files(srctree)
+ if '/scripts/kconfig/tests/' not in f]
+
+ configs = {}
+ refs = {}
+ for f in files:
+ parse_kconfig(f, configs, refs)
+
+ known = {}
+ if excludes:
+ with open(excludes) as f:
+ for line in f:
+ line = line.rstrip('\n')
+ if not line.strip() or line.lstrip().startswith('#'):
+ continue
+ m = re.match(r'^\s*([a-zA-Z0-9_]+)\s*(?:#.*)?$', line)
+ if m:
+ known[m.group(1)] = 1
+
+ ret = 0
+ for k in sorted(refs):
+ if k in configs or k in known:
+ continue
+ print(k, end='')
+ if k in ('N', 'Y', 'M'):
+ print(f" - warning: '{k}' is probably not what you want; "
+ f"Kconfig tristate literals are always lowercase "
+ f"('n', 'y', 'm')", end='')
+ print()
+ ret = 1
+ return ret
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 6/7] scripts: convert parse-maintainers.pl to Python
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (4 preceding siblings ...)
2026-10-18 13:57 ` [RFC PATCH v2 5/7] scripts: convert kconfig-sym-check.pl " Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 7/7] scripts: convert streamline_config.pl " Breno Rodrigues Alves
6 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of parse-maintainers.pl in Python 3
(stdlib only). Same section parsing, same --order preferred
ordering, same --input/--output/--section behavior.
Verified byte-for-byte identical to the Perl version on the
kernel MAINTAINERS file (30,222 lines).
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/parse-maintainers.py | 136 +++++++++++++++++++++++++++++++++++
1 file changed, 136 insertions(+)
create mode 100755 scripts/parse-maintainers.py
diff --git a/scripts/parse-maintainers.py b/scripts/parse-maintainers.py
new file mode 100755
index 000000000..520f66ab8
--- /dev/null
+++ b/scripts/parse-maintainers.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Sort MAINTAINERS file sections alphabetically.
+
+import argparse
+import re
+import sys
+from functools import cmp_to_key
+
+PREFERRED_ORDER = 'MRLSWQBCPTFXNK'
+
+
+def by_category_cmp(a, b):
+ aa = a.upper().replace('THE REST', 'ZZZZZZ')
+ bb = b.upper().replace('THE REST', 'ZZZZZZ')
+ return (aa > bb) - (aa < bb)
+
+
+def by_pattern_cmp(a, b):
+ a1 = a[0].upper() if a else ''
+ b1 = b[0].upper() if b else ''
+ ai = PREFERRED_ORDER.find(a1)
+ bi = PREFERRED_ORDER.find(b1)
+ if ai < 0:
+ ai = 1000
+ if bi < 0:
+ bi = 1000
+ if (a1 == 'F' and b1 == 'F') or (a1 == 'X' and b1 == 'X'):
+ return (a > b) - (a < b)
+ if ai < bi:
+ return -1
+ if ai == bi:
+ return 0
+ return 1
+
+
+def trim(s):
+ return s.strip()
+
+
+def alpha_output(ref, filename, order):
+ if not ref:
+ return
+ with open(filename, 'w') as f:
+ sep = None
+ for key in sorted(ref.keys(), key=cmp_to_key(by_category_cmp)):
+ if key == ' ':
+ f.write(ref[key])
+ else:
+ if sep is None:
+ sep = '\n'
+ else:
+ f.write(sep)
+ f.write(key + '\n')
+ patterns = ref[key].split('\n')
+ if order:
+ patterns = sorted(patterns, key=cmp_to_key(by_pattern_cmp))
+ for p in patterns:
+ if p:
+ f.write(p + '\n')
+
+
+def file_input(ref, filename):
+ lastline = ''
+ case = ' '
+ ref[case] = ''
+
+ with open(filename) as f:
+ lines = f.readlines()
+
+ for line in lines:
+ m = re.match(r'^([A-Z]):\s*(.*)', line)
+ if m:
+ line = m.group(1) + ':\t' + trim(m.group(2)) + '\n'
+ if lastline == '':
+ ref[case] += line
+ continue
+ case = trim(lastline)
+ if case in ref:
+ sys.exit(f"Header '{case}' already exists")
+ ref[case] = line
+ lastline = ''
+ continue
+
+ if case == ' ':
+ ref[case] += lastline
+ lastline = line
+ continue
+ if trim(lastline) != '':
+ sys.exit(f"Odd non-pattern line '{lastline}' for '{case}'")
+ lastline = line
+
+ ref[case] += lastline
+
+
+def main(argv):
+ p = argparse.ArgumentParser(add_help=False)
+ p.add_argument('--input', default='MAINTAINERS')
+ p.add_argument('--output', default='MAINTAINERS.new')
+ p.add_argument('--section', default='SECTION.new')
+ p.add_argument('--order', action='store_true', default=False)
+ p.add_argument('-h', '--help', '--usage', action='store_true')
+ p.add_argument('patterns', nargs='*')
+ args = p.parse_args(argv)
+
+ if args.help:
+ print_usage()
+ return 0
+
+ hash_ = {}
+ file_input(hash_, args.input)
+
+ new_hash = {}
+ for pattern in args.patterns:
+ for key in list(hash_.keys()):
+ if re.search(pattern, key) or re.search(pattern, hash_[key]):
+ new_hash[key] = hash_[key]
+ del hash_[key]
+
+ alpha_output(hash_, args.output, args.order)
+ alpha_output(new_hash, args.section, args.order)
+ return 0
+
+
+def print_usage():
+ print(f'''usage: {sys.argv[0]} [options] <pattern matching regexes>
+
+ --input => MAINTAINERS file to read (default: MAINTAINERS)
+ --output => sorted MAINTAINERS file to write (default: MAINTAINERS.new)
+ --section => new sorted MAINTAINERS file to write (default: SECTION.new)
+ --order => Use the preferred section content output ordering (default: 0)''')
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
* [RFC PATCH v2 7/7] scripts: convert streamline_config.pl to Python
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
` (5 preceding siblings ...)
2026-10-18 13:57 ` [RFC PATCH v2 6/7] scripts: convert parse-maintainers.pl " Breno Rodrigues Alves
@ 2026-10-18 13:57 ` Breno Rodrigues Alves
6 siblings, 0 replies; 16+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 13:57 UTC (permalink / raw)
To: linux-kernel
Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
Breno Rodrigues Alves
Faithful re-implementation of streamline_config.pl in Python 3
(stdlib only). Same Kconfig recursion, same Makefile parsing,
same dependency/select resolution, same LMC_KEEP preservation.
Requires a .config and loaded modules to fully test; logic is
1:1 with the Perl version.
Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
scripts/kconfig/streamline_config.py | 475 +++++++++++++++++++++++++++
1 file changed, 475 insertions(+)
create mode 100755 scripts/kconfig/streamline_config.py
diff --git a/scripts/kconfig/streamline_config.py b/scripts/kconfig/streamline_config.py
new file mode 100755
index 000000000..f2e53975f
--- /dev/null
+++ b/scripts/kconfig/streamline_config.py
@@ -0,0 +1,475 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright 2005-2009 - Steven Rostedt
+#
+# Simplify the kernel config to only the modules loaded on this system.
+
+import os
+import re
+import subprocess
+import sys
+
+DEBUG = 'LOCALMODCONFIG_DEBUG' in os.environ
+
+# Module state populated by read_kconfig()
+depends = {}
+selects = {}
+prompts = {}
+objects = {}
+config2kfile = {}
+defaults = {}
+read_kconfigs = {}
+ksource = '.'
+
+
+def dprint(*args):
+ if DEBUG:
+ print(*args, file=sys.stderr)
+
+
+def read_config():
+ uname = subprocess.check_output(['uname', '-r'], text=True).strip()
+
+ searchconfigs = [
+ ('.config', 'cat', None),
+ ('/proc/config.gz', 'zcat', None),
+ (f'/boot/config-{uname}', 'cat', None),
+ (f'/boot/vmlinuz-{uname}', 'scripts/extract-ikconfig',
+ 'scripts/extract-ikconfig'),
+ ('vmlinux', 'scripts/extract-ikconfig', 'scripts/extract-ikconfig'),
+ (f'/lib/modules/{uname}/kernel/kernel/configs.ko',
+ 'scripts/extract-ikconfig', 'scripts/extract-ikconfig'),
+ ('kernel/configs.ko', 'scripts/extract-ikconfig',
+ 'scripts/extract-ikconfig'),
+ ('kernel/configs.o', 'scripts/extract-ikconfig',
+ 'scripts/extract-ikconfig'),
+ ]
+
+ for file, exec_cmd, test_cmd in searchconfigs:
+ if not os.path.isfile(file):
+ continue
+ if test_cmd is not None:
+ r = subprocess.run([test_cmd, file], capture_output=True)
+ if r.returncode:
+ continue
+ print(f"using config: '{file}'", file=sys.stderr)
+ try:
+ out = subprocess.check_output(
+ f'{exec_cmd} {file}', shell=True, text=True,
+ stderr=subprocess.DEVNULL)
+ except subprocess.CalledProcessError:
+ continue
+ return out.splitlines(keepends=True)
+
+ sys.exit('No config file found')
+
+
+def read_kconfig(kconfig, if_state):
+ """Recursively read a Kconfig file, populate globals.
+
+ if_state is a [level, [deps...]] list for mutation.
+ """
+ state = 'NONE'
+ config = ''
+ cont = False
+ line = ''
+
+ source = f'{ksource}/{kconfig}'
+ last_source = ''
+ while re.search(r'\$\((\w+)\)', source) and source != last_source:
+ last_source = source
+ m = re.search(r'\$\((\w+)\)', source)
+ source = source.replace(m.group(0), os.environ.get(m.group(1), ''))
+
+ try:
+ with open(source) as f:
+ raw_lines = f.readlines()
+ except OSError:
+ sys.exit(f"Can't open {source}")
+
+ iflevel = if_state[0]
+ ifdeps = if_state[1]
+
+ for raw in raw_lines:
+ line_text = raw.rstrip('\n')
+
+ if cont:
+ line_text = line + ' ' + line_text
+
+ if line_text.endswith('\\'):
+ cont = True
+ line = line_text[:-1]
+ continue
+ cont = False
+
+ m = re.match(r'^source\s+"?([^"]+)', line_text)
+ if m:
+ sub = m.group(1)
+ if sub not in read_kconfigs:
+ read_kconfigs[sub] = 1
+ read_kconfig(sub, if_state)
+ continue
+
+ m = re.match(r'^\s*(?:menu)?config\s+(\S+)\s*$', line_text)
+ if m:
+ state = 'NEW'
+ config = m.group(1)
+ config2kfile[f'CONFIG_{config}'] = kconfig
+ for i in range(if_state[0]):
+ if i:
+ depends[config] = depends.get(config, '') + ' ' + ifdeps[i]
+ else:
+ depends[config] = ifdeps[i]
+ state = 'DEP'
+ continue
+
+ m = re.match(r'^\s*depends\s+on\s+(.*)$', line_text)
+ if m and state in ('NEW', 'DEP'):
+ if state == 'NEW':
+ depends[config] = m.group(1)
+ state = 'DEP'
+ else:
+ depends[config] = depends.get(config, '') + ' ' + m.group(1)
+ continue
+
+ m = re.match(r'^\s*def(?:_(?:bool|tristate)|ault)\s+(\S.*)$',
+ line_text)
+ if m and state != 'NONE':
+ dep = m.group(1)
+ defaults[config] = 1
+ if not re.match(r'^\s*(?:y|m|n)\s*$', dep):
+ dep = re.sub(r'.*\sif\s+', '', dep)
+ depends[config] = depends.get(config, '') + ' ' + dep
+ dprint(f'Added default depends {dep} to {config}')
+ continue
+
+ m = re.match(r'^\s*select\s+(\S+)', line_text)
+ if m and state != 'NONE':
+ conf = m.group(1)
+ if conf in selects:
+ selects[conf] += ' ' + config
+ else:
+ selects[conf] = config
+ continue
+
+ if state != 'NONE' and \
+ re.match(r'^\s*(?:tristate\s+\S|prompt\b)', line_text):
+ prompts[config] = 1
+ continue
+
+ m = re.match(r'^if\s+(.*\S)\s*$', line_text)
+ if m:
+ deps_str = re.sub(r'^[^a-zA-Z0-9_]*', '', m.group(1))
+ deps_str = re.sub(r'[^a-zA-Z0-9_]*$', '', deps_str)
+ parts = re.split(r'[^a-zA-Z0-9_]+', deps_str)
+ if if_state[0] < len(ifdeps):
+ ifdeps[if_state[0]] = ':'.join(parts)
+ else:
+ ifdeps.append(':'.join(parts))
+ if_state[0] += 1
+ continue
+
+ if re.match(r'^endif', line_text):
+ if if_state[0]:
+ if_state[0] -= 1
+ continue
+
+ if re.match(r'^\s*(?:---)?help(?:---)?\s*$', line_text) or \
+ re.match(r'^(?:comment|choice|menu)\b', line_text):
+ state = 'NONE'
+
+
+def convert_vars(line, make_vars):
+ """Expand $(VAR) references in a Makefile line."""
+ out = ''
+ while True:
+ m = re.match(r'^(.*?)(\$\(([^()]+)\))(.*)$', line)
+ if not m:
+ out += line
+ break
+ out += m.group(1)
+ var = m.group(3)
+ if var in make_vars:
+ out += make_vars[var]
+ else:
+ out += m.group(2)
+ line = m.group(4)
+ return out
+
+
+def main(argv):
+ global ksource
+
+ config_file = read_config()
+
+ localmodconfig = False
+ localyesconfig = False
+ args = []
+ for a in argv:
+ if a == '--localmodconfig':
+ localmodconfig = True
+ elif a == '--localyesconfig':
+ localyesconfig = True
+ else:
+ args.append(a)
+
+ ksource = args[0] if args else '.'
+ kconfig = args[1] if len(args) > 1 else None
+ lsmod_file = os.environ.get('LSMOD')
+
+ try:
+ makefiles = subprocess.check_output(
+ ['find', ksource, '-name', 'Makefile', '-or', '-name', 'Kbuild'],
+ text=True, stderr=subprocess.DEVNULL).splitlines()
+ except subprocess.CalledProcessError:
+ makefiles = []
+
+ if kconfig:
+ read_kconfig(kconfig, [0, []])
+
+ # Parse Makefiles for obj-$(CONFIG_FOO) += obj.o
+ for makefile in makefiles:
+ line = ''
+ make_vars = {}
+ try:
+ with open(makefile) as f:
+ for raw in f:
+ line_text = raw.rstrip('\n')
+ if line_text.endswith('\\'):
+ line += line_text[:-1]
+ continue
+ line += line_text
+ expanded = convert_vars(line, make_vars)
+ line = ''
+
+ m = re.match(
+ r'obj-\$[({](CONFIG_[^})]*)[)}]\s*[+:]?=\s*(.*)',
+ expanded)
+ objs = None
+ var = None
+ if m:
+ var = m.group(1)
+ objs = m.group(2)
+ else:
+ m = re.match(r'^\s*(\S+)\s*[:]?=\s*(.*\S)',
+ expanded)
+ if m:
+ make_vars[m.group(1)] = m.group(2)
+ if objs is not None:
+ for o in re.split(r'\s+', objs):
+ o = o.replace('-', '_')
+ m = re.match(r'(.*)\.o$', o)
+ if m:
+ objects.setdefault(m.group(1), []).append(var)
+ except OSError:
+ continue
+
+ # Get loaded modules
+ modules = {}
+ if lsmod_file:
+ if not os.path.isfile(lsmod_file):
+ alt = os.path.join(os.environ.get('objtree', ''), lsmod_file)
+ if os.path.isfile(alt):
+ lsmod_file = alt
+ else:
+ sys.exit(f'{lsmod_file} not found')
+ with open(lsmod_file) as f:
+ lines = f.readlines()
+ else:
+ lsmod = 'lsmod'
+ for d in ('/sbin', '/bin', '/usr/sbin', '/usr/bin'):
+ if os.path.isfile(f'{d}/lsmod') and os.access(
+ f'{d}/lsmod', os.X_OK):
+ lsmod = f'{d}/lsmod'
+ break
+ try:
+ out = subprocess.check_output([lsmod], text=True,
+ stderr=subprocess.DEVNULL)
+ lines = out.splitlines(keepends=True)
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ sys.exit(f'Can not call lsmod with {lsmod}')
+
+ for line in lines:
+ if line.startswith('Module'):
+ continue
+ m = re.match(r'^(\S+)', line)
+ if m:
+ modules[m.group(1)] = 1
+
+ # Build config set from objects
+ configs = {}
+ for mod in modules:
+ if mod in objects:
+ for conf in objects[mod]:
+ configs[conf] = mod
+ dprint(f'{conf} added by direct ({mod})')
+ else:
+ print(f'{mod} config not found!', file=sys.stderr)
+
+ # Read current configs
+ orig_configs = {}
+ for line in config_file:
+ m = re.search(r'(CONFIG_[A-Za-z_0-9]*)=(m|y)', line)
+ if m:
+ orig_configs[m.group(1)] = m.group(2)
+
+ valid = 'A-Za-z_0-9'
+
+ def parse_config_depends(p, depconfig):
+ nonlocal repeat
+ while re.search(rf'[{valid}]', p):
+ m = re.match(rf'^[^{valid}]*([{valid}]+)', p)
+ if not m:
+ sys.exit('parse_config_depends: unexpected')
+ conf = 'CONFIG_' + m.group(1)
+ p = re.sub(rf'^[^{valid}]*[{valid}]+', '', p, count=1)
+
+ if conf not in orig_configs or orig_configs[conf] == 'y':
+ continue
+ if conf not in configs:
+ repeat = 1
+ dprint(f'{conf} selected by depend {depconfig}')
+ configs[conf] = 1
+
+ def parse_config_selects(config, p):
+ nonlocal repeat
+ next_config = None
+ while re.search(rf'[{valid}]', p):
+ m = re.match(rf'^[^{valid}]*([{valid}]+)', p)
+ if not m:
+ sys.exit('parse_config_selects: unexpected')
+ conf = 'CONFIG_' + m.group(1)
+ p = re.sub(rf'^[^{valid}]*[{valid}]+', '', p, count=1)
+
+ if conf not in orig_configs:
+ dprint(f'{conf} not set for {config} select')
+ continue
+ if orig_configs[conf] != 'm':
+ dprint(f'{conf} (non module) selects {config}, we are good')
+ return
+ if conf in configs:
+ dprint(f'{conf} selects {config} so we are good')
+ return
+ if next_config is None:
+ next_config = conf
+
+ if next_config is None:
+ if config not in defaults:
+ print(f'WARNING: {config} is required, but nothing in the',
+ file=sys.stderr)
+ print(f' current config selects it.', file=sys.stderr)
+ return
+
+ repeat = 1
+ configs[next_config] = 1
+ dprint(f'{next_config} selected by select {config}')
+
+ repeat = 1
+ while repeat:
+ repeat = 0
+ process_selects = {}
+ for config in list(configs.keys()):
+ if config in orig_configs and orig_configs[config] != 'm':
+ continue
+ short = config.replace('CONFIG_', '', 1)
+ if short in depends:
+ parse_config_depends(depends[short], short)
+ if short not in prompts and short in selects:
+ process_selects[short] = 1
+
+ for config in process_selects:
+ dprint(f'Process select {config}')
+ parse_config_selects(config, selects[config])
+
+ # Preserve list
+ preserved = []
+ if 'LMC_KEEP' in os.environ:
+ preserved = os.environ['LMC_KEEP'].split(':')
+
+ def in_preserved(conf):
+ kconf = config2kfile.get(conf)
+ if kconf is None:
+ return False
+ for excl in preserved:
+ if re.match(f'^{excl}', kconf):
+ return True
+ return False
+
+ setconfigs = {}
+
+ for line in config_file:
+ # IKCONFIG special case
+ m = re.search(r'CONFIG_IKCONFIG', line)
+ if m:
+ if '# CONFIG_IKCONFIG is not set' in line:
+ print('CONFIG_IKCONFIG=m')
+ print('# CONFIG_IKCONFIG_PROC is not set')
+ else:
+ sys.stdout.write(line)
+ continue
+
+ m = re.match(r'CONFIG_MODULE_SIG_KEY="(.+)"', line)
+ if m:
+ orig_cert = m.group(1)
+ default_cert = 'certs/signing_key.pem'
+ if 'MODULE_SIG_KEY' not in depends or \
+ f'"{default_cert}"' not in depends['MODULE_SIG_KEY']:
+ print(f'WARNING: MODULE_SIG_KEY assertion failure, '
+ f'update needed to {sys.argv[0]}', file=sys.stderr)
+ sys.stdout.write(line)
+ elif orig_cert != default_cert and not os.path.isfile(orig_cert):
+ print(f'Module signature verification enabled but module '
+ f'signing key "{orig_cert}" not found. Resetting '
+ f'signing key to default value.', file=sys.stderr)
+ print(f'CONFIG_MODULE_SIG_KEY="{default_cert}"')
+ else:
+ sys.stdout.write(line)
+ continue
+
+ m = re.match(r'CONFIG_SYSTEM_TRUSTED_KEYS="(.+)"', line)
+ if m:
+ orig_keys = m.group(1)
+ if not os.path.isfile(orig_keys):
+ print(f'System keyring enabled but keys "{orig_keys}" not '
+ f'found. Resetting keys to default value.',
+ file=sys.stderr)
+ print('CONFIG_SYSTEM_TRUSTED_KEYS=""')
+ else:
+ sys.stdout.write(line)
+ continue
+
+ m = re.match(r'^(CONFIG.*)=(m|y)', line)
+ if m:
+ conf, val = m.group(1), m.group(2)
+ if in_preserved(conf):
+ dprint(f'Preserve config {conf}')
+ sys.stdout.write(line)
+ continue
+ if conf in configs:
+ if localyesconfig:
+ setconfigs[conf] = 'y'
+ print(f'{conf}=y')
+ continue
+ setconfigs[conf] = val
+ elif val == 'm':
+ print(f'# {conf} is not set')
+ continue
+ sys.stdout.write(line)
+
+ # Integrity check
+ for mod in modules:
+ if mod not in objects:
+ continue
+ if not any(c in setconfigs for c in objects[mod]):
+ print(f'module {mod} did not have configs', end='',
+ file=sys.stderr)
+ for c in objects[mod]:
+ print(f' {c}', end='', file=sys.stderr)
+ print(file=sys.stderr)
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
\ No newline at end of file
--
2.43.0
^ permalink raw reply [flat|nested] 16+ messages in thread
end of thread, other threads:[~2026-10-18 13:57 UTC | newest]
Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-18 11:28 [RFC PATCH 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 2/7] scripts: convert extract-module-sig.pl " Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 3/7] scripts: convert extract-sys-certs.pl " Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 4/7] scripts: convert headerdep.pl " Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 5/7] scripts: convert kconfig-sym-check.pl " Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 6/7] scripts: convert parse-maintainers.pl " Breno Rodrigues Alves
2026-10-18 11:28 ` [RFC PATCH 7/7] scripts: convert streamline_config.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 0/7] scripts: convert remaining Perl utilities in scripts/ Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 1/7] scripts: convert checkstack.pl to Python Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 2/7] scripts: convert extract-module-sig.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 3/7] scripts: convert extract-sys-certs.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 4/7] scripts: convert headerdep.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 5/7] scripts: convert kconfig-sym-check.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 6/7] scripts: convert parse-maintainers.pl " Breno Rodrigues Alves
2026-10-18 13:57 ` [RFC PATCH v2 7/7] scripts: convert streamline_config.pl " Breno Rodrigues Alves
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®